背景

在前一篇关于blockly的文章中,我参考了webduino对Web Speech的包装,并将其移植到blockly4pi中。

webduino对blockly的使用,有许多出彩的地方。这篇文章将简要对其核心部分做个分析,为了更深入地借鉴它的设计

入手

我买了webduino的smart模块,所以我们从最简单的案例入手:将smart的led灯设为红色

首先当然是将smart连上wifi,完成后它分配到网址:192.168.0.119(显示在wifi名称中)

接着我们拼搭出积木块

运行后,成功点亮红灯

分析

首先查看上边的积木生成的代码:

1
2
3
4
5
6
7
8
var rgbled;

boardReady({board: 'Smart', url: '192.168.0.119'}, function (board) {
  board.systemReset();
  board.samplingInterval = 50;
  rgbled = getRGBLedCathode(board, 15, 12, 13);
  rgbled.setColor('#ff0000');
});

可以看到webduino通过积木生成js代码, 然后运行js代码来控制start.(我的blockly4pi同时生成js和python)

容易看出,boardReady是浏览器与start通信的代理,接着我们进入源码,去跟踪boardReaety,它定义在这里

 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
  function boardReady(options, autoReconnect, callback) {
    var callback = (typeof autoReconnect === 'function' ? autoReconnect : callback),
        index = boards.length,
        board = createBoard(options),
        terminate = function () {
        board = null;
    delete boards[index];
    boards.splice(index, 1);

    if (autoReconnect === true) {
        setTimeout(function () {
        boardReady(options, autoReconnect, callback);
        }, 5000);
    }
    };

    board.once(webduino.BoardEvent.ERROR, function (err) {
      if (board.isConnected) {
        board.once(webduino.BoardEvent.DISCONNECT, terminate);
        board.disconnect();
      } else {
        terminate();
      }
    });

    board.once(webduino.BoardEvent.READY, callback);

    boards.push(board);
  }

其中board定义在Board

通过EventEmitter传递全局消息

休眠一秒

在js中休眠是个有趣的话题,我们知道js是非阻塞的,webduino使用await来实现(很新的特性,之后会通过babel来在浏览器里编译,后文再说)

对应的代码是

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
(async function () {

var rgbled;


boardReady({board: 'Smart', url: '192.168.0.119'}, async function (board) {
  board.systemReset();
  board.samplingInterval = 50;
  await delay(1);
  rgbled = getRGBLedCathode(board, 15, 12, 13);
  rgbled.setColor('#ff0000');
});

}());

这部分很有意思,我们仅仅添加一个等待一秒的模块,生成的代码结构整体变了,新添加的模块并不特殊,它生成的代码为

1
2
3
4
5
Blockly.JavaScript['delay'] = function (block) {
      var value_secs_ = Blockly.JavaScript.valueToCode(block, 'secs_', Blockly.JavaScript.ORDER_NONE);
        var code = 'await delay(' + value_secs_ + ');\n';
          return code;
};

可以推测,代码在generate之前有个预处理,大概是根据generate出来的代码里是够包含await关键字来处理,我们可以搜索源码来验证我们的想法:search await,果不其然

代码执行

早期

blockly-src/demos/code/code.js Code.runJS这个好理解,早期的做法,目前被废弃

早期的执行机制如注释中说的Just a quick and dirty eval,简单粗暴

1
2
3
4
5
6
7
var code = Blockly.JavaScript.workspaceToCode(Code.workspace);
Blockly.JavaScript.INFINITE_LOOP_TRAP = null;
try {
eval(code);
} catch (e) {
alert(MSG['badCode'].replace('%1', e));
}

后来的机制颇为费解

后来的机制

我们可以从bindClick runButton追踪起、经过Code.reloadSandbox(),在这个函数中将模块generate为代码:Code.getContext,generate的过程会判断是否需要使用babel编译,如果需要会做好编辑,在之后的程序中用babel编译它:Code.transform

如此一来我们就得到了可在普通浏览器里运行的js代码,如果是早期的做法,至此丢到eval里就结束了,目前的做法要复杂些,把代码丢到一个iframe中跑,用到了iframe-api (本质上是window.postMessage)

一些核心步骤如下:

通信过程

websocket

我们先关注采用websocket的通信过程(本地运行)

onmessage handle定义了收到smart数据的回调

值得注意的是,数据的传输使用了二进制数据

之后通过事件系统发布消息,emit的定义之处在EventEmitter emit

ps:使用mqtt与云端通信的部分在这 MqttTransport.js

控制流

有了上边这些探索,我们可以挑战最后一个问题了,控制指令是如何抵达smart的,拿最初的例子来说,把点亮红灯积木块跑起来的时候发生了什么?

首先是积木生成对应的代码

1
2
  rgbled = getRGBLedCathode(board, 15, 12, 13);
  rgbled.setColor('#ff0000');

board我们在前头说过,检索源码发现getRGBLedCathode不过是对webduino.module.RGBLed的包装

于是我们到webduino-js继续探索.我们沿着rgbled.setColor('#ff0000');一路追踪下去,看到DataTransfer.js , webduino采用二进制来通信,发送数据的地方定义在send -> sendOut 在此我们可以看到数据的发送也是用的二进制(收发都是)

其他感兴趣的地方

与页面元素交互

demo_light_click generator

参考