前言

Like many of us, you are a Smalltalker at heart! – John Maloney

Thanks to MicroBlocks and Snap!, we are not homeless –wenjie

在制作完 Snap! 中的 CoCube 库 后, 我发现这个库的大多数工作都是通用的, 我们可以将其用于任何 MicroBlocks 设备!

通过提取出 CoCube 库的通用部分, 得到了 MicroBlocks Client 库.

这篇文章的许多内容与Snap! 中的 CoCube 库重合, MicroBlocks Client 库实质上是通用版本的 CoCube 库.

动机

使用 Snap! 驱动 MicroBlocks 设备, 通常有以下两个动机:

  • 将 Snap! 已有的 AI 库用于 MicroBlocks 设备
  • 在算力更强的上位机中制作高级驱动库(尤其是机器人相关的项目)

思路

构建优秀且可扩展的系统的关键更多的是设计模块之间如何通信,而不仅仅是设计其内部属性和行为 – Alan Kay

由于 Snap! 和 MicroBlocks 都是个人计算精神的产物, 都拥有极为出色的动态性, 我确信可以通过消息传递来实现目标: 在 Snap! 中对 MicroBlocks 设备进行编程. 事实上, MicroBlocks IDE 正是通过消息传递机制与 vm 协作的

我喜欢采用 消息传递 来设计系统之间的互操作机制, 这样做能够最大限度地降低了系统之间的耦合度.

Snap! 中已经存在了用于在 Snap! 和 MicroBlocks 直接传递消息的库:

这个库提供了传递消息的基础功能, 我打算以这个库为基础, 制作 MicroBlocks Client 库, 力求做到:

  1. 用户可以在 Snap! 中获取 MicroBlocks 设备的状态信息(reporter 积木)
  2. 用户可以在 Snap! 中给 MicroBlocks 设备发送指令(command 积木)
  3. 用户只需要在 Snap! 中编程即可, 无需在 Snap! 和 MicroBlocks 中来回切换.
  4. 尽可能好的可扩展性

关于第 1 点: 获取另一个系统的状态信息, 通常有两种方式:

  • push(推) 风格: 一个系统源源不断地向另一个系统广播携带状态数据的消息
  • pull(拉) 风格: 按需查询需要的状态信息, 通常是采用 request-response 风格的通信.
    • RPC/HTTP 是这种风格
    • Dynatalk 是这种风格

MicroBlocks messaging(BLE) 库提供了 PubSub 风格的消息机制, 我打算基于它构建出 dynatalk-over-ble, 然后基于 dynatalk-over-ble(类似dynatalk-over-postmessage) 实现 pull 风格的 MicroBlocks Client 库.

在我们的用例中, 只需要实现单向的 C/S 架构(Snap! 为 client, MicroBlocks 为 server). 所以 dynatalk-over-ble 的复杂性只有常规 dynatalk 的一半, 令人开心的是, 正好计算能力偏弱的 MicroBlocks 那一半极为简单.

实施

MicroBlocks 一侧

前头提到, 我们力求做到:

用户只需要在 Snap! 中编程即可, 无需在 Snap! 和 MicroBlocks 中来回切换.

因此我希望 MicroBlocks 一侧中的代码是完全通用的, 用户只需要刷入这个通用程序, 就不再需要打开 MicroBlocks 平台, 完全在 Snap! 中对设备进行编程

为了做到这点, 我们在 MicroBlocks 中运行这个通用程序:

这段程序负责以下工作:

  • 解释收到的消息, 将消息的语义"接地"到硬件设备的具体功能上. “接地” 工作是使用 call 积木实现的, 它类似其他编程语言中的 eval , 可以动态运行自定义积木和 vm primitive.
    • handle_message 积木用于处理非阻塞式的消息
    • handle_blocking_message 积木用于处理阻塞式的消息

你可以将这个程序视为运行在硬件中的 server, Snap! 则是它的 client. 我们使用了 C/S 架构.

Snap! 一侧

前边提到:

  1. 用户可以在 Snap! 中获取 MicroBlocks 设备的状态信息(reporter 积木)
  2. 用户可以在 Snap! 中给 MicroBlocks 设备发送指令(command 积木)

我们分别展示它们是如何做到的:

对于第 1 点, 获取状态信息(reporter 积木):

对于第 2 点, 发送指令(command 积木):

消息采用纯字符串形式, 携带方法名(自定义积木名字或者 vm primitive)和 参数, 以 , 分隔 , 以下是一个例子: call, <msg-id>, [display:mbPlot], 3, 3

如何使用 MicroBlocks Client 库?

要使用 MicroBlocks Client 库, 只需打开 Snap! 平台即可.

加载 MicroBlocks Client 库,

点击 open MicroBlocks project 积木, 将打开 MicroBlocks 窗口, 连接设备将程序刷入后, 即可断开并关闭 MicroBlocks 窗口.

然后在 Snap! 中开始编程.

FAQ

如何添加更多 MicroBlocks Client?

下载 https://snap.codelab.club/libraries/MicroBlocks-Client.xml. (在浏览器打开, 另存为本地文件)

执行以下操作

  • MicroBlocks-Client.xml 改名为 MicroBlocks-Client2.xml
  • 编辑 MicroBlocks-Client2.xml 文件:
    • 🐰 全部为 🐰2 (vscode 有批量替换功能)
    • _ble_ 全部替换为 _ble2_
  • 将修改获得文件(这是一个示例: https://wwj718.github.io/post/img/MicroBlocks-Client2.xml), 拖拽到 Snap! 里.

以此类推, 你可以添加任意多个 MicroBlocks Client!

使用场景

当有多个 MicroBlocks 设备时, Snap! 非常适合作为 “演示控制台”, 我在这个视频里做了分享

如何同时调试 Snap! MicroBlocks 的代码?

是的, 你可以同时调试软件和硬件侧的代码! 一切都是活性(liveness)的!

你可以在 Snap! MicroBlocks 中同时连接同一个设备, 然后观察消息在它们之间的流动:

如何在 Snap! 的 MicroBlocks 窗口中保存和加载 MicroBlocks 项目?

保存项目

由于 iframe 的限制 , 无法使用菜单中的 Save 按钮打开本地文件.

替代方案: 点击 Copy project url to clipboard, 然后粘贴到浏览器地址栏里, 之后再保存

加载项目

由于 iframe 的限制, 无法使用菜单中的 Open 按钮打开本地文件.

替代方案: 将本地文件直接拖拽到 MicroBlocks 窗口

如何查看 MicroBlocks 支持的 vm primitive?

可以在 ubl 库文件中找到这些 primitive, 它的形式是[a:b], 如 [display:mbPlot]

Peter 在 Discord 上分享了一些例子:

push 风格的版本

MicroBlocks 部分:

这段程序负责以下工作:

  1. 源源不断地将设备的状态 push 出去(when started 积木), 这些状态可以自行扩展. 目前的更新速率是 20 次/秒(可自定义)
  2. 解释收到的消息(handle_message 积木), 将消息的语义"接地"到硬件设备的具体功能上. “接地” 工作是使用 call 积木实现的, 它类似其他编程语言中的 eval , 可以动态运行自定义积木和 vm primitive.

Snap! 示例(点击运行)

值得注意的是, push 版本相比于 pull 版本有以下优势:

  • 速度更快
  • 更简单

如有需要, 可以把它们二者结合.

传递 event

有时候我们想要更精确地传递 event, 可以像 push status 一样 push event, 类似这样:

是否可以制作一个类似的 Python client 库?

本文的设计很容易迁移到 Python 中, 使用我之前为 Python 制作的 microblocks_messaging_librarydynatalk-py 库, 可以很容易地实现跟 MicroBlocks Client 库相同的功能.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# pip install -U microblocks
from microblocks import MicroblocksClient

client = MicroblocksClient()
found_devices = client.discover(timeout=5)
print("found devices:", found_devices)
client.connect("MicroBlocks AZB", timeout=3)

client.display_character('f')

while True:
    tiltX = client.tiltX
    print(tiltX)

记得先往设备里刷入这个程序(和 Snap! 相同)

更多 API 例子参考: notebooks

microblocks 库的源代码

批量连接设备

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from microblocks import MicroblocksClient

client = MicroblocksClient()
found_devices = client.discover(timeout=3) # timeout=5 in windows
print("found devices:", found_devices)
# connect all discovered devices
devices = [MicroblocksClient(i) for i in found_devices]

for index, device in enumerate(devices):
    device.display_character(index)

并行控制多个设备

1
2
3
4
5
6
from concurrent.futures import ThreadPoolExecutor

# Launching parallel tasks: https://docs.python.org/3/library/concurrent.futures.html
with ThreadPoolExecutor() as executor:
    for client in devices:
        executor.submit(client.scroll_text, "hello")

如何为自定义 MicroBlocks 设备制作 Python 库

上述 MicroblocksClient 类是通用的, 要为自定义的 MicroBlocks 设备制作 Python 库, 只需要继承 MicroblocksClient 类, 然后添加自定义方法即可. 参考 CoCube

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from microblocks import MicroblocksClient

class CoCube(MicroblocksClient):

    @property
    def battery(self):
        return int(self.request("Battery Percentage", []))

    @property
    def position_x(self):
        return int(self.request("position_X", []))

    def move_forward(self, speed=50):
        self.request("Move Forward", [speed])

    def move_backward(self, speed=50):
        self.request("Move Backward", [speed])

CoCube Python 库 使用 Hatch 创建, 非常推荐使用 Hatch 来开发和发布 Python 项目.

参考