基于Blockly与ROSWeb的可视化编程基础开发

平台

简介

  基于BlocklyROSWeb实现可视化turtlesimWeb界面基础开发。

环境搭建

配置Vue环境

  安装以下依赖:

1
2
3
npm install blockly
npm install --save eval5
npm i element-ui -S

  修改src->main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'

import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'

import router from './router'

Vue.config.productionTip = false

Vue.use(ElementUI)

Vue.config.ignoredElements.push('xml')
Vue.config.ignoredElements.push('block')
Vue.config.ignoredElements.push('field')
Vue.config.ignoredElements.push('category')
Vue.config.ignoredElements.push('sep')
Vue.config.ignoredElements.push('value')
Vue.config.ignoredElements.push('statement')
Vue.config.ignoredElements.push('mutation')

/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})

  这里主要是导入ElementUI和注册Blockly相关组件。

配置Blockly环境

  将node_modules->blockly->media所有的文件复制到static文件夹下,这里主要是blockly所需的媒体资源文件。

配置ROSWeb环境

  在static中新建文件夹rosweb,用于存放所需要的第三方JavaScript文件。
rosweb文件夹
  其中roslib.jsros3d.jsros-web提供的库文件,剩余JavaScript文件为用于绘制3D可视化模型的库文件(主要为three.js框架及其依赖库)。
  然后在index.html入口文件中,将这些文件添加<head>标签中。

1
2
3
4
5
6
<script src="./static/rosweb/three.js"></script>
<script src="./static/rosweb/ColladaLoader.js"></script>
<script src="./static/rosweb/STLLoader.js"></script>
<script src="./static/rosweb/eventemitter2.min.js"></script>
<script src="./static/rosweb/roslib.js"></script>
<script src="./static/rosweb/ros3d.js"></script>

ESlint代码规范

  这里针对ESlint做一些代码规范处理,并添加ROSWeb全局变量。修改.eslintrc.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint',
sourceType: 'module'
},
env: {
browser: true,
node: true,
es6: true,
},
extends: ['plugin:vue/recommended', 'eslint:recommended'],

// add your custom rules here
//it is base on https://github.com/vuejs/eslint-config-vue
rules: {
"vue/max-attributes-per-line": [2, {
"singleline": 10,
"multiline": {
"max": 1,
"allowFirstLine": false
}
}],
"vue/singleline-html-element-content-newline": "off",
"vue/multiline-html-element-content-newline":"off",
"vue/name-property-casing": ["error", "PascalCase"],
"vue/no-v-html": "off",
'accessor-pairs': 2,
'arrow-spacing': [2, {
'before': true,
'after': true
}],
'block-spacing': [2, 'always'],
'brace-style': [2, '1tbs', {
'allowSingleLine': true
}],
'camelcase': [0, {
'properties': 'always'
}],
'comma-dangle': [2, 'never'],
'comma-spacing': [2, {
'before': false,
'after': true
}],
'comma-style': [2, 'last'],
'constructor-super': 2,
'curly': [2, 'multi-line'],
'dot-location': [2, 'property'],
'eol-last': 2,
'eqeqeq': ["error", "always", {"null": "ignore"}],
'generator-star-spacing': [2, {
'before': true,
'after': true
}],
'handle-callback-err': [2, '^(err|error)$'],
'indent': [2, 2, {
'SwitchCase': 1
}],
'jsx-quotes': [2, 'prefer-single'],
'key-spacing': [2, {
'beforeColon': false,
'afterColon': true
}],
'keyword-spacing': [2, {
'before': true,
'after': true
}],
'new-cap': [2, {
'newIsCap': true,
'capIsNew': false
}],
'new-parens': 2,
'no-array-constructor': 2,
'no-caller': 2,
'no-console': 'off',
'no-class-assign': 2,
'no-cond-assign': 2,
'no-const-assign': 2,
'no-control-regex': 0,
'no-delete-var': 2,
'no-dupe-args': 2,
'no-dupe-class-members': 2,
'no-dupe-keys': 2,
'no-duplicate-case': 2,
'no-empty-character-class': 2,
'no-empty-pattern': 2,
'no-eval': 2,
'no-ex-assign': 2,
'no-extend-native': 2,
'no-extra-bind': 2,
'no-extra-boolean-cast': 2,
'no-extra-parens': [2, 'functions'],
'no-fallthrough': 2,
'no-floating-decimal': 2,
'no-func-assign': 2,
'no-implied-eval': 2,
'no-inner-declarations': [2, 'functions'],
'no-invalid-regexp': 2,
'no-irregular-whitespace': 2,
'no-iterator': 2,
'no-label-var': 2,
'no-labels': [2, {
'allowLoop': false,
'allowSwitch': false
}],
'no-lone-blocks': 2,
'no-mixed-spaces-and-tabs': 2,
'no-multi-spaces': 2,
'no-multi-str': 2,
'no-multiple-empty-lines': [2, {
'max': 1
}],
'no-native-reassign': 2,
'no-negated-in-lhs': 2,
'no-new-object': 2,
'no-new-require': 2,
'no-new-symbol': 2,
'no-new-wrappers': 2,
'no-obj-calls': 2,
'no-octal': 2,
'no-octal-escape': 2,
'no-path-concat': 2,
'no-proto': 2,
'no-redeclare': 2,
'no-regex-spaces': 2,
'no-return-assign': [2, 'except-parens'],
'no-self-assign': 2,
'no-self-compare': 2,
'no-sequences': 2,
'no-shadow-restricted-names': 2,
'no-spaced-func': 2,
'no-sparse-arrays': 2,
'no-this-before-super': 2,
'no-throw-literal': 2,
'no-trailing-spaces': 2,
'no-undef': 2,
'no-undef-init': 2,
'no-unexpected-multiline': 2,
'no-unmodified-loop-condition': 2,
'no-unneeded-ternary': [2, {
'defaultAssignment': false
}],
'no-unreachable': 2,
'no-unsafe-finally': 2,
'no-unused-vars': 'off',
// 'no-unused-vars': [2, {
// 'vars': 'all',
// 'args': 'none'
// }],
'no-useless-call': 2,
'no-useless-computed-key': 2,
'no-useless-constructor': 2,
'no-useless-escape': 0,
'no-whitespace-before-property': 2,
'no-with': 2,
'one-var': [2, {
'initialized': 'never'
}],
'operator-linebreak': [2, 'after', {
'overrides': {
'?': 'before',
':': 'before'
}
}],
'padded-blocks': [2, 'never'],
'quotes': [2, 'single', {
'avoidEscape': true,
'allowTemplateLiterals': true
}],
'semi': [2, 'never'],
'semi-spacing': [2, {
'before': false,
'after': true
}],
'space-before-blocks': [2, 'always'],
'space-before-function-paren': [2, 'never'],
'space-in-parens': [2, 'never'],
'space-infix-ops': 2,
'space-unary-ops': [2, {
'words': true,
'nonwords': false
}],
'spaced-comment': [2, 'always', {
'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
}],
'template-curly-spacing': [2, 'never'],
'use-isnan': 2,
'valid-typeof': 2,
'wrap-iife': [2, 'any'],
'yield-star-spacing': [2, 'both'],
'yoda': [2, 'never'],
'prefer-const': 2,
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'object-curly-spacing': [2, 'always', {
objectsInObjects: false
}],
'array-bracket-spacing': [2, 'never'],
},
"globals":{
"ROSLIB": true,
"ROS3D": true
}
}

工具类

blocklytools

  新建src->utils->blocklytools->myBlockly.jssrc->utils->blocklytools->toolboxStyle.css.js:

myBlockly.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
// 引入Blockly
import Blockly from 'blockly'

// 1. 为了创建自定义类别,先创建自定义类别,继承自Blockly.ToolboxCategory
class CustomCategory extends Blockly.ToolboxCategory {
// 自定义类别创造函数
// categoryDef: 类别定义的信息
// toolbox: 表示类别的父级toolbox
// opt_parent: 可选参数,表示其父类别
constructor(categoryDef, toolbox, optParent) {
super(categoryDef, toolbox)
}

// ToolboxCategory类中默认的方法addColourBorder_会在类别的左侧添加一个颜色条,
// 在定制类CustomCategory中覆盖此方法,将其改为设置背景色
addColourBorder_(colour) {
this.rowDiv_.style.backgroundColor = colour
}

// 如果两次点击某个类别,会发现背景色消失了,
// 为了解决这个问题,重写setSelected这个方法
setSelected(isSelected) {
// 使用getElementsByClassName选中类别对应的span元素
var labelDom = this.rowDiv_.getElementsByClassName('blocklyTreeLabel')[0]
if (isSelected) {
// 选中的类别背景色设置为白色
this.rowDiv_.style.backgroundColor = 'white'
// 选中的类别文本设置为原背景色
labelDom.style.color = this.colour_
// 设置icon的颜色和文本颜色相同
this.iconDom_.style.color = this.colour_
} else {
// 未选中的类别背景色设置
this.rowDiv_.style.backgroundColor = this.colour_
// 未选中的类别文本设置为白色
labelDom.style.color = 'black'
// 设置icon的颜色和文本颜色相同
this.iconDom_.style.color = 'black'
}
// This is used for accessibility purposes.
Blockly.utils.aria.setState(/** @type {!Element} */ (this.htmlDiv_),
Blockly.utils.aria.State.SELECTED, isSelected)
}

// // 将icon图标改成image
// createIconDom_ () {
// const img = document.createElement('img')
// img.src = './logo_only.svg'
// img.alt = ''
// img.width = '35'
// img.height = '25'
// return img
// }
}

// 2. 自定义类别需要向Blockly注册,告知自定义类别的存在,不然会无法识别新建的类
Blockly.registry.register(
Blockly.registry.Type.TOOLBOX_ITEM,
Blockly.ToolboxCategory.registrationName,
CustomCategory, true)

export const initBlockly = (div, toolbox) => {
return Blockly.inject(div,
{
// 工具栏
toolbox: document.getElementById(toolbox),
// 网格效果
grid: { spacing: 20, length: 3, colour: '#ccc', snap: true },
// 媒体资源 (该框架下需要将资源文件放在public文件夹下)
media: './static/media/',
// 垃圾桶
trashcan: true,
zoom: {
controls: true,
wheel: true,
startScale: 1.0,
maxScale: 3,
minScale: 0.3,
scaleSpeed: 1.2
}
}
)
}

export const initMyBlockly = () => {
Blockly.Blocks['turtlesim_move'] = {
init: function() {
this.appendValueInput('X')
.setCheck('Number')
.appendField('移动方向:x')
this.appendValueInput('Y')
.setCheck('Number')
.appendField('移动方向:y')
this.setInputsInline(false)
this.setPreviousStatement(true, null)
this.setNextStatement(true, null)
this.setColour(230)
this.setTooltip('')
this.setHelpUrl('')
}
}

Blockly.JavaScript['turtlesim_move'] = (block) => {
// 后面的 || '\'\'' 部分表示当输入为空时 返回的值
var x = Blockly.JavaScript.valueToCode(block, 'X',
Blockly.JavaScript.ORDER_FUNCTION_CALL)
var y = Blockly.JavaScript.valueToCode(block, 'Y',
Blockly.JavaScript.ORDER_FUNCTION_CALL)
// 无输入
if (x === '' || y === '') {
return 'WAMERROR: 参数错误 请输入正确的参数!'
} else {
return 'turtlesimMove' + '(' + x + ',' + y + ')' + ';' + '\n'
// return 'test()' + ';' + '\n'
}
// 第二个参数为当前使用的操作符对应的优先级 https://www.wenjiangs.com/doc/wkeldh8c
// return [code, Blockly.JavaScript.ORDER_FUNCTION_CALL]
// return code
}
}

  这里主要重新设计了Blockly的样式,并自定义了一个turtlesim_move的代码块。

toolboxStyle.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/* 侧边栏样式 */
.blocklyToolboxDiv{
/* background-color: #008B93; */
}
/* 模块字体 */
.blocklyTreeLabel {
color: black;
font-size: 18px;
}
/* 激活模块的样式 */
.blocklyToolboxContents {
padding: .5em;
}
/* Adds space between the categories, rounds the corners and adds space around the label. */
.blocklyTreeRow {
padding: 1px;
margin-bottom: 5px;
border-radius: 3px;
height: initial;
}

/* Changes color of the icon to white. */
.customIcon {
/* color: white; */
width: 30px;
}
/* 模块样式 */
.blocklyTreeRowContentContainer {
width: 100px;
height: 40px;
display: flex;
flex-direction: row;
align-items: center;
}

  这里主要是Blockly侧边栏的样式修改。

roswebtools

turtlesim.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export const callTurtlesimMoveService = (ros, x, y, z) => {
var service = new ROSLIB.Service({
ros: ros,
name: '/turtlesim_move',
serviceType: 'turtlesim_move/turtlesim_move_srv'
})

var request = new ROSLIB.ServiceRequest({
x: x,
y: y,
z: z
})

return new Promise((resolve, reject) => {
service.callService(request, (result) => {
resolve(result)
// 异常处理
}, (falseResult) => {
// 抛出异常
reject(new Error('error'))
})
})
}

  由于ROS话题(topic)和服务(service)的设计机制,当运行多个话题/服务时,只会运行最后一个,因此需要在应用层做一些逻辑处理。
  由于服务具有返回值的特性,可以根据返回值信息来判断是否进行下一个服务的调用,因此本项目ROS端的接口采用服务(service)的机制。
  本项目采用ES7提出的asyncawait异步特性,并结合Promise处理正确/异常信息,从而让其每次调用服务(service)时,都会等待其返回状态,并根据返回状态做下一步的处理。
  首先需要返回一个Promise对象,其包含2个参数,即resolve(解析)和reject(拒绝)。然后通过ROSWeb提供的callService(request, callback, failedCallback)判断服务(service)的完成状态。如果返回值为true,则解析并返回结果,如果返回值为false,则抛出异常,并结束该回调函数。
  最后在Web端通过asyncawait接收并处理该Promise(见下文分析)。

Web主界面

  HelloWorld.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
<template>
<div id="visualProgram" class="visualProgram">
<el-row
:gutter="10"
type="flex">
<el-col :span="12">
<!-- blockly工作区 -->
<div id="blocklyDiv" class="div-blocklyDiv">
<!-- blockly工具栏 -->
<!-- xml不能在浏览器中正常渲染,因此需要设置为不可见 -->
<xml id="toolbox" style="display: none">
<category name="逻辑控制" colour="%{BKY_LOGIC_HUE}">
<category name="If" colour="#008B00">
<block type="controls_if" />
<block type="controls_if">
<mutation else="1" />
</block>
<block type="controls_if">
<mutation elseif="1" else="1" />
</block>
</category>

<category name="Boolean" colour="%{BKY_LOGIC_HUE}">
<block type="logic_compare" />
<block type="logic_operation" />
<block type="logic_negate" />
<block type="logic_ternary" />
</category>

<category name="Loop" colour="%{BKY_LOOPS_HUE}">
<block type="controls_repeat_ext">
<value name="TIMES">
<block type="math_number">
<field name="NUM">10</field>
</block>
</value>
</block>
<block type="controls_whileUntil" />
<block type="controls_for">
<field name="VAR">i</field>
<value name="FROM">
<block type="math_number">
<field name="NUM">1</field>
</block>
</value>
<value name="TO">
<block type="math_number">
<field name="NUM">10</field>
</block>
</value>
<value name="BY">
<block type="math_number">
<field name="NUM">1</field>
</block>
</value>
</block>
<block type="controls_forEach" />
<block type="controls_flow_statements" />
</category>
</category>

<category name="数学运算" colour="%{BKY_MATH_HUE}">
<block type="math_arithmetic" />
<block type="math_single" />
<block type="math_trig" />
<block type="math_number_property" />
<block type="math_round" />
<block type="math_on_list" />
<block type="math_modulo" />
<block type="math_constrain">
<value name="LOW">
<block type="math_number">
<field name="NUM">1</field>
</block>
</value>
<value name="HIGH">
<block type="math_number">
<field name="NUM">100</field>
</block>
</value>
</block>
<block type="math_random_int">
<value name="FROM">
<block type="math_number">
<field name="NUM">1</field>
</block>
</value>
<value name="TO">
<block type="math_number">
<field name="NUM">100</field>
</block>
</value>
</block>
<block type="math_random_float" />
<block type="math_atan2" />
</category>

<category name="列表运算" colour="%{BKY_LISTS_HUE}">
<block type="lists_create_empty" />
<block type="lists_create_with" />
<block type="lists_repeat">
<value name="NUM">
<block type="math_number">
<field name="NUM">5</field>
</block>
</value>
</block>
<block type="lists_length" />
<block type="lists_isEmpty" />
<block type="lists_indexOf" />
<block type="lists_getIndex" />
<block type="lists_setIndex" />
</category>

<category name="文本控制" colour="%{BKY_TEXTS_HUE}">
<block type="text_length" />
<block type="text_print" />
</category>

<category name="常用变量" colour="#556B2F">
<block type="math_number">
<field name="NUM">1</field>
</block>
<block type="math_number">
<field name="NUM">0</field>
</block>
<block type="math_number">
<field name="NUM">-1</field>
</block>
<block type="logic_boolean" />
<block type="logic_null" />
<block type="math_constant" />
<block type="text" />
</category>

<category name="ROS控制" colour="#FF7F00">
<block type="turtlesim_move" />
</category>
</xml>

<div class="div-run-code">
<el-button class="el-button-run-code" icon="el-icon-video-play" @click="runJavascriptCode" />
<el-button class="el-button-run-code" icon="el-icon-refresh" @click="refreshJavascriptCode" />
<el-button class="el-button-run-code" icon="el-icon-video-pause" @click="stopJavascriptCode" />
<el-button class="el-button-run-code" icon="el-icon-circle-close" @click="clearWorkspace" />
<!-- <el-button @click="test">测试</el-button> -->
</div>
</div>
</el-col>

<el-col :span="6">
<div class="div-blockly-code">
<!-- blockly代码区 -->
<el-input
v-model="blocklyCodeMessage"
:disabled="true"
:rows="39"
type="textarea"
class="el-input-blockly-code"
/>
</div>
</el-col>

<el-col :span="6">
<div class="div-blockly-console">
<!-- blockly代码控制台 -->
<el-input
v-model="blocklyConsoleMessage"
:disabled="true"
:rows="39"
type="textarea"
class="el-input-blockly-console"
/>
</div>

<div class="div-clear-console">
<el-button style="width:28px;height:28px;padding:0px;font-size:25px" icon="el-icon-circle-close" @click="clearConsoleMessage" />
</div>
</el-col>
</el-row>
</div>
</template>

<script>
// https://github.com/bplok20010/eval5
import { Interpreter } from 'eval5'

import * as myblock from '@/utils/blocklytools/myBlockly'

// 引入Blockly
import Blockly from 'blockly'
// 引入想要转换的语言,语言有php python dart lua javascript
import 'blockly/javascript'
import 'blockly/python'
// 引入语言包并使用
import * as hans from 'blockly/msg/zh-hans'
Blockly.setLocale(hans)

import * as turtlesim from '@/utils/roswebtools/turtlesim'

export default {
name: 'HelloWorld',
data() {
return {
workspace: null,
blocklyCodeMessage: '',
blocklyConsoleMessage: '',
jsCode: null,

ros: null,
isConnected: false,
isRunWamNode: false,
wamServerIp: '192.168.31.99',

isDone: true
}
},
mounted() {
this.initBlockly()
myblock.initMyBlockly()

// 将自定义函数添加至window中,否则解析时,无法识别函数
window.turtlesimMove = this.turtlesimMove
},
created() {
this.ros = new ROSLIB.Ros({
url: 'ws://' + this.wamServerIp + ':9090'
})

this.ros.on('connection', () => {
this.isConnected = true
this.$message.success('连接ROS成功!')
this.blocklyConsoleMessage += '连接ROS成功!' + '\n'
})

this.ros.on('error', (e) => {
this.isConnected = false
this.$message.error('连接ROS失败!')
this.blocklyConsoleMessage += '连接ROS失败!' + '\n'
})

this.ros.on('close', () => {
this.isConnected = false
this.$message.error('关闭ROS连接!')
this.blocklyConsoleMessage += '关闭ROS连接!' + '\n'
})
},
beforeDestroy() {
// 关闭ros连接
if (this.isConnected) {
this.ros.close()
}
},
methods: {
initBlockly() {
this.workspace = myblock.initBlockly('blocklyDiv', 'toolbox')
// 工作区监听代码生成器
this.workspace.addChangeListener(this.myUpdateFunction)
var toolbox = Blockly.getMainWorkspace().getToolbox()
},
// 代码生成器
myUpdateFunction(event) {
var codeJs = Blockly.JavaScript.workspaceToCode(this.workspace)
this.blocklyCodeMessage = codeJs
},
async runJavascriptCode() {
// const interpreter = new Interpreter(window, {
// timeout: 1000
// })
// 实例化JavaScript解释器eval5
const interpreter = new Interpreter(window)
// Blokly获取JavaScript代码
this.jsCode = Blockly.JavaScript.workspaceToCode(this.workspace)

try {
// 代码预检查
var isOk = this.checkCode()

if (isOk) {
// 按分号切分指令
var stringList = this.jsCode.split(';')
for (var i = 0; i < stringList.length - 1; i++) {
// await 执行evaluate()
var result = await interpreter.evaluate(stringList[i])
this.blocklyConsoleMessage += result + '\n'
}
// 异常处理
if (this.isDone === false) {
this.$message.error('运行错误!')
}
} else {
// 预检查错误
this.blocklyConsoleMessage += '运行错误' + '\n'
}
} catch (e) {
// 运行错误
this.blocklyConsoleMessage += e + '\n'
}
},
refreshJavascriptCode() {
this.isDone = true
this.$message.success('重置程序')
},
stopJavascriptCode() {
this.$message.error('停止运行')
},
clearWorkspace() {
this.workspace.clear()
this.$message.success('清除工作空间')
},
clearConsoleMessage() {
this.blocklyConsoleMessage = ''
},
async turtlesimMove(x, y) {
if (this.isDone) {
try {
this.isDone = false
await turtlesim.callTurtlesimMoveService(this.ros, x, y, 0)
this.isDone = true
return 'turtlesim' + '向右移动了' + x + ',向上移动了' + y + '.'
} catch (error) {
this.isDone = false
return '运行错误'
}
}
},
checkCode() {
// 没有错误
if (this.jsCode.length === 0) {
this.$message.error('请输入指令')
return false
} else if (this.jsCode.indexOf('WAMERROR') === -1) {
return true
} else {
return false
}
}
}
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
#visualProgram .el-input-blockly-code /deep/ .el-textarea__inner{
color: black;
background-color: white
}
#visualProgram .el-input-blockly-console /deep/ .el-textarea__inner{
color: black;
background-color: white
}
</style>

<!-- 这里需要是全局样式,即去掉scoped -->
<!-- 这里加上页面的id https://www.jianshu.com/p/4ff9a5397427 -->
<style>
@import "../utils/blocklytools/toolboxStyle.css";
.div-blocklyDiv{
width:800px;
height:850px;
margin-top: 20px;
margin-left: 20px;
position: relative;
border: 2px solid #008B93;
}
.div-run-code{
position: absolute;
right: 300px;
/* bottom: 800px; */
bottom: 30px;
z-index: 2;
}
.el-button-run-code{
width:40px;
height:40px;
padding:0px;
font-size:30px
}
.div-blockly-code{
margin-top: 20px;
height:850px;
color: #008B93;
border: 2px solid #008B93;
}
.div-blockly-console{
margin-top: 20px;
margin-right: 10px;
height:850px;
border: 2px solid #008B93;
}
.el-input-blockly-code{
width: 99%;
margin: 2px;
border: 1px solid #008B93;
}
.el-input-blockly-console{
width: 99%;
margin: 2px;
border: 1px solid #008B93;
}
.div-clear-console{
position: absolute;
right: 25px;
top: 30px;
z-index: 2;
}
</style>

  这里比较重要的代码部分为runJavascriptCode()函数和blockly生成的自定义函数,这里为turtlesimMove()
  首先需要将turtlesimMove()声明为async的异步函数,并通过await调用ROSWeb的接口,保证每次都会等待服务(service)的返回状态。
  为了统一管理服务(service)返回的状态,这里通过全局变量isDone去处理,即如果正常执行完服务(service),则将该变量置为true,否则一旦捕获到异常信息,则置为false
  由于eval5并不支持在内部解析await,因此需要将blockly生成的代码字符串jsCode按行解析成单句,通过将runJavascriptCode()声明为async的异步函数,并使用await等待evaluate()的完成状态。
  这里会根据全局变量isDone来处理异常情况,一旦ROSWeb接收到异常的状态,会给出用户的错误信息提示。

Ros Service

  最后需要在ROS中重写服务(service)的接口。

Service类型

  在ROS工程文件夹下,新建srv->turtlesim_move_srv.srv

1
2
3
4
5
float32 x
float32 y
float32 z
---
bool result

Service接口

  在ROS工程文件夹下,新建src->turtlesim_move.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <ros/ros.h>
#include <geometry_msgs/Twist.h>
#include <turtlesim_move/turtlesim_move_srv.h>

#include <iostream>
#include <unistd.h>

ros::Publisher turtle_vel_pub;

// service回调函数,输入参数req,输出参数res
bool moveCallback(turtlesim_move::turtlesim_move_srv::Request &req,
turtlesim_move::turtlesim_move_srv::Response &res)
{
geometry_msgs::Twist vel_msg;
vel_msg.linear.x = req.x;
vel_msg.linear.y = req.y;
vel_msg.linear.z = req.z;
turtle_vel_pub.publish(vel_msg);

// 设置反馈数据
sleep(2);

res.result = true;

return res.result;
}

int main(int argc, char **argv)
{
// ROS节点初始化
ros::init(argc, argv, "turtlesim_move_server");

// 创建节点句柄
ros::NodeHandle n;

// 创建一个名为/turtlesim_move的server,注册回调函数moveCallback
ros::ServiceServer move_service = n.advertiseService("/turtlesim_move", moveCallback);

// 创建一个Publisher,发布名为/turtle1/cmd_vel的topic,消息类型为geometry_msgs::Twist,队列长度10
turtle_vel_pub = n.advertise<geometry_msgs::Twist>("/turtle1/cmd_vel", 10);

ros::spin();

return 0;
}

  这里主要调用/turtle1/cmd_vel话题实现turtlesim的移动。
  这里的sleep(2);只是为了测试功能,实际的工程项目中,需要根据实际情况返回true/false状态。

编译运行

  CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
cmake_minimum_required(VERSION 3.0.2)
project(turtlesim_move)

## Compile as C++11, supported in ROS Kinetic and newer
# add_compile_options(-std=c++11)

## Find catkin macros and libraries
## if COMPONENTS list like find_package(catkin REQUIRED COMPONENTS xyz)
## is used, also find other catkin packages
find_package(catkin REQUIRED COMPONENTS
geometry_msgs
roscpp
rospy
std_msgs
turtlesim
message_generation
)


## Generate services in the 'srv' folder
add_service_files(FILES turtlesim_move_srv.srv)

## Generate added messages and services with any dependencies listed here
generate_messages(
DEPENDENCIES
geometry_msgs
std_msgs
)

###################################
## catkin specific configuration ##
###################################
## The catkin_package macro generates cmake config files for your package
## Declare things to be passed to dependent projects
## INCLUDE_DIRS: uncomment this if your package contains header files
## LIBRARIES: libraries you create in this project that dependent projects also need
## CATKIN_DEPENDS: catkin_packages dependent projects also need
## DEPENDS: system dependencies of this project that dependent projects also need
catkin_package(
# INCLUDE_DIRS include
# LIBRARIES turtlesim_move
# CATKIN_DEPENDS geometry_msgs roscpp rospy std_msgs turtlesim
# DEPENDS system_lib
)

###########
## Build ##
###########

## Specify additional locations of header files
## Your package locations should be listed before other locations
include_directories(
# include
${catkin_INCLUDE_DIRS}
)

## Specify libraries to link a library or executable target against
# target_link_libraries(${PROJECT_NAME}_node
# ${catkin_LIBRARIES}
# )
target_link_libraries(turtlesim_move ${catkin_LIBRARIES})

功能测试

  依次启动ROS接口:

1
2
3
roslaunch rosbridge_server rosbridge_websocket.launch
rosrun turtlesim turtlesim_node
rosrun turtlesim_move turtlesim_move

  拖拽blockly控件,输入正确的参数,运行:
turtlesim

谢谢老板!
-------------本文结束感谢您的阅读给个五星好评吧~~-------------