前言

本文讨论如何开发一个 CodeLab Adapter 插件(基于 Python)。

CodeLab Adapter 自带一个消息系统,理论上,任何语言都可以与之通信,任何有开放接口的事物都可以接入其中。

本文仅讨论如何在 Scratch 上构建客户端(Scratch Extension,基于 JavaScript),使其与 CodeLab Adapter 通信(收发消息)来扩展 Scratch 的能力。当然,你也可以在任何语言中做这件事。

思路

一个 Adapter 插件(plugin)被视为 Adapter 系统的一个节点(Node), 通过这些节点去适配不同的外部硬件/软件,进而将其接入到系统中。

在系统中,流动的一切都是消息,所以由这些插件连接的事物(软件/硬件)可以彼此沟通(talk),系统得以持续生长。

希望使用 Adapter 某个插件的能力时(如在 Scratch 中),只需要发送消息与 Adapter 对话即可。

开始

案例(Tello)

本文采用案例式教学。

近期我们重写了 Adapter Tello 插件,本文将以此为例,介绍 开发一个 CodeLab Adapter 插件 的典型流程。

该流程是完全通用的。

如何交互?

首先考虑第一个问题:你想接入什么东西?与之交互的方式是什么?(Adapter 是一个利用消息不停交互的系统)

如果你想接入的东西是硬件(如 Tello),那么与之通信的方式可能是调用它们的开放 SDK 。

如果你想接入的东西是软件(如 Teachable Machine),那么与之通信的方式可能是基于某些标准协议(如 http/websocket).

如果你想接入的东西是一门编程语言的内核(如 Python),那么与之通信的方式可能是 eval

寻找 SDK

在本文中,我们 想接入什么东西 是 Tello。 我们在 Github 上找到与之通信的 Python SDK: DJITelloPy

与 Tello 通信的方式是利用 socket, DJITelloPy封装了细节,使我们可以以面向对象的风格与之交互, 我们来一撇 SDK 的使用方式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from djitellopy import Tello

tello = Tello()

tello.connect()
tello.takeoff()

tello.move_left(100)
tello.rotate_counter_clockwise(90)
tello.move_forward(100)

tello.land()

语义清晰,非常简易。

构建 Adapter 插件

一个 Adapter 插件不是一个孤岛,它试图与其他事物交谈(talk), 对外部的请求做出回应。

实现这件事的方式很多,软件工程有大量工作围绕这块: 对请求作出回应,提供服务,我们会想到 RESTful API、RPC…

Adapter 如果完成以上目标? 我们采取的策略是: 收发消息。我们把一切看作消息, 并且倾向于晚绑定(late binding)

回到正题。我们先来快速浏览一下 Tello 插件的代码(不必弄懂它,稍后会讲解),连同注释和模版代码,一共才 64 行。

 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
# pip install https://github.com/wwj718/DJITelloPy/archive/master.zip
import time
from loguru import logger
from codelab_adapter_client import AdapterNode
from codelab_adapter_client.utils import get_or_create_node_logger_dir
from djitellopy import Tello

node_logger_dir = get_or_create_node_logger_dir()
debug_log = str(node_logger_dir / "debug.log")
logger.add(debug_log, rotation="1 MB", level="DEBUG")


class Tello2Node(AdapterNode):
    NODE_ID = "eim/node_tello2"
    HELP_URL = "https://adapter.codelab.club/extension_guide/tello2/"
    DESCRIPTION = "tello 2.0"

    def __init__(self):
        super().__init__(logger=logger)
        self.tello = None
    def run_python_code(self, code):
        try:
            output = eval(code, {"__builtins__": None}, {"tello": self.tello})
        except Exception as e:
            output = e
        return output

    def extension_message_handle(self, topic, payload):
        self.logger.info(f'code: {payload["content"]}')
        message_id = payload.get("message_id")
        python_code = payload["content"]
        if self.tello:
            output = self.run_python_code(python_code)
            payload["content"] = str(output)
            message = {"payload": payload}
            self.publish(message)

    def run(self):
        "避免插件结束退出"
        try:
            self.tello = Tello()
        except Exception as e:
            self.logger.error(e)
            self.pub_notification(str(e), type="ERROR")
            return
        while self._running:
            time.sleep(0.5)

    def terminate(self, **kwargs):
        try:
            self.tello.__del__()
            self.tello = None
        except:
            pass
        super().terminate(**kwargs)

if __name__ == "__main__":
    try:
        node = Tello2Node()
        node.receive_loop_as_thread()
        node.run()
    except:
        if node._running:
            node.terminate()

你可以使用 Adapter 内置的 JupyterLab 浏览/修改 这些插件源码, 保存并重启插件之后,即刻生效(不需要重启 Adapter)

我们来看看 tello 插件各部分代码的含义和功能是什么。

什么?这就结束了?

是的这就结束了。

可是,都没有见到跟 Tello 有关的业务逻辑啊?

是的,这正是我们想法的核心部分: 晚绑定(late binding), 将功能描述不断后推,交给 client(甚至是用户)。

Adapter Tello 插件看起来颇像一个 REPL,它解释(run_python_code)收到的消息(副作用是 tello 飞行器的行为), Tello 的行为将由输入的消息决定,消息携带语义。我们贪图便利,直接将 Python 代码视为消息(因其能很好携带语义)

客户端

接下来我们来构建一个客户端来使用 Adapter Tello 插件。

前头提到,我们计划在 Scratch 里构建一个客户端,它是一个 Scratch Extension。

Scratch Extension

如果你对构建 Scratch Extension 不熟悉,请参考: 创建你的第一个 Scratch3.0 Extension

我们已经将 Scratch Tello 插件开放在这儿: scratch3_tello2

如何交互(talk)?

前头提到:

一个 Adapter 插件不是一个孤岛,它试图与其他事物交谈(talk), 对外部的请求做出回应。

Scratch Tello 插件(JavaScript)是如何与 Adapter 插件(Python)交互的呢?

它们通过 websocket(socketio)沟通, 但你不需要在意和弄懂它们沟通的细节,我们已经构建了一个 Adapter js client,抽象掉了 talk 的细节,让你可以基于它轻松在 js 里与 Adapter 交互。 (注意:你的开发环境里,需要有scratch3_eim)

源码解读

接下来,一起深入到源码里看看。

我们通过阐述这两块积木,来看看引擎盖后发生的事情。

首先看看,当我们 起飞 积木运行的时候发生了什么:

实际上,当 Scratch 中, 起飞 积木运行时,消息 tello.takeoff() 将发送到 Adapter Tello 插件,插件将解释这则消息– eval(执行)这段 Python 代码。

接着我们来看看 设置速度 积木(带有参数)运行的时候发生了什么:

可以看出,我们试图将参数拼凑到 Python 代码里。

this.client.emit_with_messageid 是与 Adapter 通信的关键,这部分也很简单,只是发送消息,如果你兴趣不大,不需要弄懂它, 将其视为模版代码,跟着既有的插件(我们开放了插件)填空即可。

需要注意的是,消息并不一定是 Python 代码,它只要携带语义就行。

发布 Adapter 插件

如果你构建了新的 Adapter 插件,欢迎提交到插件市场

调试

为了方便开发 Adapter 插件,一些调试技巧可能对你有用

进阶 && 进一步阅读

FAQ

如何以 Scratch 的风格连接硬件?

这部分主要是通过与 Scratch 的 runtime 交互实现的,更多细节参考以下两个Scratch 插件的 scan 函数

参考:

如何刷入自定义固件

Adatper 内置了哪些第三方库

wiki

如何引入新的 Python 第三方库

Adapter 允许再分发, 把需要的第三方库放在相应目录下,再分发即可

放在目录下即可,再分发

更多 FAQ

参考