图形化编程中的"帽子"积木
文章目录
前言
提醒: Snap! 最新版本支持自定义"帽子"积木! 案例演示
标题中的图形化编程是指:
- Scratch
- Snap!
- MicroBlocks
- GP
“帽子"积木
Scratch
在 Scratch 中, 有许多 “帽子”(Hat) 积木, 它们看起来像一顶帽子。
图中的黄色积木都来自 “事件” 分类, “帽子” 积木通常用于响应事件: 当某个事件发生时, 就运行帽子底下的积木。
图中的绿色积木来自插件, 开发者可以创建自定义的"帽子” 积木。
Snap! / MicroBlocks / GP
Snap! / MicroBlocks / GP 内置了一些"帽子"积木:
但它们仨都不允许用户自定义"帽子"积木。 而是提供了一个通用的"帽子"积木: 当(when)
当(when)
积木的功能是: 接受某个有返回值(true/false)的积木(椭圆(reporter)或者六边形(predicate))作为参数, 当返回值为 true 时, 则运行其下的积木。
Snap! / MicroBlocks / GP 的洞察是: hat = when + reporter
。
移植 Scratch 的"帽子"积木
当我们打算将 Scratch 插件迁移为 Snap! 插件时, 会遇到这样的问题: 怎么迁移"帽子"积木?
让我们来考察两个 Scratch 插件:
micro:bit 插件
先考察下 micro:bit 插件的这个"帽子"积木: 当按下 A 按钮
这个积木的功能是: 当 micro:bit 上的 A 按钮被按下时, 触发这个积木。
我们首先将碰到这个问题: 如何知道 A 按钮被按下
?
micro:bit 插件作者的想法是, 在 micro:bit 里持续运行一段程序(固件), 这段程序不停地观察 micro:bit 的状态(如 A 按键是否被按下), 并把这些状态以每秒几十次的频率传递给 Scratch。至于传递的方式, 可能通过 usb 串口也可能通过 BLE(蓝牙) 。 以一定的频率不断发送 micro:bit 自身状态
的想法受到 S4A 的影响。
在 Scratch 一边, 不断地收到 micro:bit 发来的状态数据, 不停地观察(在一个持续运行的循环中)这些数据中表示按钮的位置, 如果这个数据表明按下, 则运行当按下 A 按钮
帽子下的积木(执行它的语义功能)。以下是它的具体实现:
接收来自 micro:bit 的状态数据(_onMessage):
|
|
当按下 A 按钮
具体的实现代码(whenButtonPressed):
|
|
值得注意的是, _onMessage
和 whenButtonPressed
是两个独立的线程, 前者会不断写入(更新) buttonA 的状态, 后者则不断读取 buttonA 的状态。
_onMessage
中的状态消息是源源不断的, buttonA 变量相当于一个暂存它的槽( 只存一个最近的状态 )。而 whenButtonPressed 每次只能读到 最近一次存到槽里的数据 。
移植到 Snap! 中
一旦弄懂这些, 将 当按下 A 按钮
移植到 Snap! 中就不难了。
我们可以复用 micro:bit 的固件, 因为从前头的分析得知 micro:bit 的固件负责源源不断往外发送 micro:bit 的状态, 至于发给谁它并不关心, 既然可以发到 Scratch, 当然也可以发到 Snap! 也行。
以下是 Snap! 中实现 当按下 A 按钮
的相关代码:
有以下几点值得注意:
- Snap! 通过 BLE(蓝牙) 连接 micro:bit, 通过订阅 BLE 广播来接收 micro:bit 持续发出的状态信息。
- 将收到的状态信息存放到变量
mb_notify_datas
里, 相当于 Scratch 中存储最新消息的变量, 只存下 最近的一条状态信息 。 - 查看 A 按钮状态 被实现为 predicate 积木(六边形, 我将它视为一种返回布尔值的 reporter), 通过与
当when
积木的组合,而得到"帽子"积木。 - 一切都在 Snap! 里实现!
micro:bit more 插件
Scratch 扩展插件中的"帽子"积木, 采用的都是"状态流"的风格, 外部设备源源不断地传递"状态流"到 Scratch 里(受到 S4A 的影响)。
有一类信息天生适合使用"状态流"来传递, 诸如陀螺仪的数据。
但许多情况下,要传递外部设备或系统的信息,采用事件是更好的选择。诸如将 MQTT 引入图形化编程系统。
让我们分别采用"状态流"和事件两种视角来看待 当按下 A 按钮
。
- 状态流: 这种视角下, micro:bit 源源不断地报告 A 按钮的状态:
true false false false false true false
- 事件: 这种视角下, micro:bit 只在上述情况中(
true false false false false true false
)两次状态为 true 的时候, 各发布一个事件, 其余情况保持沉默(沉默表示 false)。这种方式显然更加高效。
micro:bit more 插件 正是采用了事件的风格。
让我们看看基于事件的 当按下 A 按钮
的实现(whenButtonEvent):
|
|
这里有 2 点值得注意:
- lastTimestamp 的使用。前边说过帽子积木(
whenButtonEvent
)在一个循环里持续不断地运行, 进来的事件会存放在一个变量(槽)里, whenButtonEvent 需要知道槽里的事件是否之前被访问过。有很多方式可以做到这点, 方案 1 是为事件添加一个时间戳(这种方式通常是最佳选择), 并且给 whenButtonEvent 分配一个变量记住最近一个事件的时间。 方案 2 是在每次访问事件(槽)之后, 清空它(EIM 插件 使用了这个技巧) 。 - 由于帽子积木(
whenButtonEvent
)在一个循环里持续不断地运行, 这导致任何有状态的变量需要存储在this
里。
移植到 Snap! 中
实际上, 我并没有将 micro:bit more 插件移植到 Snap!(因为我们有更强大和通用的 MicroBlocks 插件), 但要这么做是很简单的。
在 Snap! 中对事件以及 lastTimestamp 的处理非常容易, 我们通过以下的示例来说明:
值得注意的是这里的 lastTimestamp 是 块变量 , 块变量 在积木多次运行过程中保持自身的状态, 类似于前边 Scratch whenButtonEvent 函数中与 this
有关的变量。
设置 buttonAEvent
模拟了事件流入 Snap! 的过程。在真实的情况中, 事件可能是通过 usb 串口、BLE 广播(如前边 BLE 广播的例子) 或者经过网络(如 MQTT, WebSocket)进入 Snap! 的。 值得注意的是, buttonAEvent 使用一个列表存储消息, 列表的第 2 个元素是我们收到消息时的时间戳。这个时间戳究竟是事件本身携带的, 还是 Snap! 收到消息时添加的,并不重要。
点击尝试 whenButtonAEvent。
更多
当我将 EIM 插件从 Scratch 迁移到 Snap! 时, 我将基于清空事件(槽)的机制改为基于事件时间戳的机制。
|
|
这是目前 Snap! 的机制:
思考
我们在前边遇到的问题, 可以采用 缓冲区(buffer) 视角看待它(流(Stream)>)也是看待这个问题的一个有趣视角)。 我们将来自 micro:bit 的事件或状态流存储在 Scratch 或 Snap!的一个变量(buffer)里, 然后在 帽子/predicate 积木中根据 buffer 里的数据给出判断(A 按钮是否按下)。
在计算机科学中,数据缓冲区(或简称缓冲区(buffer))是用于临时存储数据的内存区域,当数据从一个地方移动到另一个地方时会使用 –wiki: Data buffer
由于我们识别到了问题, 得到了关键词: buffer, 把这把钥匙塞到 Google 的钥匙孔里, 就可以打开许多扇门, 里头尽是计算机领域长期积累的经验。
在典型的 Scratch/Snap! 插件中, 要处理来自外部系统的输入数据, 使用单一变量作为缓冲区就足够了, 因为 Scratch/Snap! 处理数据的速度比外部系统传入数据的速度更快。
缓冲区更典型的用途是, 在某些时间里, 进入系统的数据快过消耗它的速度。 此时需要构建更为复杂的缓冲区, 诸如环形缓冲区, John Maloney 在 MicroBlocks 使用了 环形缓冲区 来处理消息广播。
MicroBlocks 中的情形
这篇文章讨论 Scratch 和 Snap! 居多, 讨论 MicroBlocks 的部分很少。 原因之一是我最近的工作涉及将插件从 Scratch 迁移到 Snap! ; 原因之二是 MicroBlocks 在这方面的设计与 Snap! 很相似。
前边提到:
whenButtonEvent 需要知道槽里的事件是否之前被访问过。有很多方式可以做到这点, 方案 1 是为事件添加一个时间戳(这种方式通常是最佳选择), 并且给 whenButtonEvent 分配一个变量记住最近一个事件的时间。 方案 2 是在每次访问事件(槽)之后, 清空它
MicroBlocks 在涉及 buffer 的情况下(诸如处理网络消息),也需要处理这个问题, 但 MicroBlocks 选择了不同的方案。
在 MicroBlocks 存在两种风格处理这个问题:
其一以 wifi 广播库(以及 micro:bit radio 库)为代表:
这种风格里, 使用一个变量(wifi广播消息已收到?
)标记 buffer 的消息是否已经读取过, 使用另一个变量(wifi最后的数字
)存储最后收到的wifi广播消息, 即使最近只收到一条消息,多次点击这个积木,每次都会返回最后一条消息。
我们可以使用 当(when)
来重写上图的代码:
其二以 UDP 库为代表:
这种风格里, 只使用一个变量: UDP 接受数据包
, 如果最近只收到一条消息, 第一次运行这个积木, 将返回这条消息, 再次运行返回 false
。 这种风格类似我们之前提到的方案 2:
方案 2 是在每次访问事件(槽)之后, 清空它
这种风格的积木,无法与 当(when)
配合使用。
从上边的例子可以看到,无论哪种处理方式,都只是摆弄不同的变量而已,buffer 也只是变量。 变量(memory)在计算(computing)中如此重要有其深刻原因。变量(memory) 几乎是计算(computing)中最重要的两个要素之一,另一个要素是移动读写头,它们共同构成图灵机,摆弄变量(memory) 相当于读写图灵机的纸带。
参考
文章作者 种瓜
上次更新 2024-01-03