前言

上篇文章提到:

@梁博士 制作了一个桌面级模块化群控机器人系统, 取名"CoCube" (Collaborative-Cube), CoCube 将 MicroBlocks 用作其编程环境. CoCube 与 toio 有许多相似的特性, 也有一些 toio 不具备的能力, 如可扩展的硬件接口, 以及与 MicroBlocks 更紧密的结合.

最近我为 CoCube(由 MicroBlocks 驱动) 设计了一个 Snap! 驱动库:

动机

CoCube 已经可以在 MicroBlocks 中编程了(感谢 @梁博士 的出色工作!). 为 CoCube 制作 Snap! 驱动库有以下两个动机:

  • 将 Snap! 已有的库(尤其是需要更多算力的 AI 相关的库)用于 CoCube
  • 在算力更强的上位机中制作高级驱动库
    • toio 也是这样做的
    • 如 @梁博士 说的 “之前做 ros 也是的,上位机承担大部分计算。最后只发机器人角速度和线速度的指令;或者左轮右轮转速指令”

思路

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

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

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

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

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

  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 风格的 CoCube 库.

在我们的用例中, 只需要实现单向的 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>, Move Forward, 50

如何使用?

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

加载 CoCube 库,

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

然后在 Snap! 中开始编程.

你甚至调用任意的 vm primitive:

FAQ

如何添加更多 CoCube?

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

执行以下操作

  • CoCube2.xml 改名为 CoCube3.xml
  • 编辑 CoCube3.xml 文件:
    • 🚙 全部为新的 emoji, 如 🚕. (vscode有批量替换功能)
    • 🚕 CoCube2 全部替换为 🚕 CoCube3
    • _CoCube2_ 全部替换为 _CoCube3_
  • 将修改获得文件, 拖拽到 Snap! 里

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

使用场景

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

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

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

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

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

保存项目

无法使用菜单中的 Save 按钮打开本地文件, 点击 Copy project url to clipboard, 然后粘贴到浏览器地址栏里, 之后再保存

加载项目

无法使用菜单中的 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 快很多, 如有需要, 可以把它们二者结合.

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

是否可以制作一个类似的 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 cocube
from cocube import CoCube

client = CoCube()
found_devices = client.discover(timeout=3) # timeout=5 in windows
print("found devices:", found_devices)
client.connect("MicroBlocks QCQ", timeout=3)

client.display_character("c")

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

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

更多 API 例子参考: notebooks

CoCube 库的源代码

批量连接设备

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from cocube import CoCube

client = CoCube()
found_devices = client.discover(timeout=5)
print("found devices:", found_devices)
# connect all discovered devices
devices = [CoCube(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")

参考