7

将基于 Scratch Link 的插件迁移到 Snap!

 9 months ago
source link: http://wwj718.github.io/post/%E7%BC%96%E7%A8%8B/scratch-link-extensions-to-snap/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

将基于 Scratch Link 的插件迁移到 Snap!

2023-11-21

近期需要把 Scratch 的一些蓝牙(BLE)插件迁移到 Snap! 里。 – Snap! 蓝牙驱动库

虽然新的 BLE 设备喜欢采用 Web Bluetooth API(诸如 microbit more), 但依然有许多 BLE 设备是通过 Scratch Link 接入 Scratch 的。

我们在之前的文章里,已经将 Web Bluetooth API 包装为 Snap! BLE primitives。为了将 Scratch Link 兼容的设备接入 Snap! , 需要弄清楚 Scratch Link 采用的通信机制(包括编解码)。弄清这些 Scratch 插件在 write/read/notify 操作在字节层面的细节, 我们就能够使用 Snap! BLE primitives 制作相同功能。

让我们试着将 Scratch 官方的 micro:bit 插件 迁移到 Snap!

Scratch-Link-extensions-to-Snap-01.png

一旦完成这个实验, 迁移其他的 Scratch Link 插件就轻而易举了。

The challenge is not building it but understanding it – Bret 《Seeing Spaces》

Scratch Link 是一个蓝牙消息中继器, 在系统蓝牙接口与 Scratch 之间中转蓝牙消息。

我们之前在 分析 scratch3.0 与 micro:bit 的通信一文中分析了消息中转过程的细节。其中的有一处需要注意: Scratch 与 Scratch Link 通信的数据采用 Base64 格式, 而 Scratch Link 与 蓝牙设备的通信数据采用 uint8array。

micro:bit more 的作者通过 ble-web.js 实现了基于 Web Bluetooth API 的 Scratch Link。 通过阅读 ble-web.js , 我们能够更加清楚地知道 Scratch Link 做了什么。

Snap! BLE primitives 能够直接操作 uint8array(映射为 list), 所以在 Snap! 编写 BLE 插件, 要比使用 Scratch Link, ble-web.js 简易清晰得多!

BLE API 主要围绕对 Characteristic 进行 read/write/notify 来操作而构建。 – Snap! 蓝牙驱动库

本文的重点是展示原理, 希望突出重点,并且希望篇幅简短。所以我们只迁移这三个典型的积木。

Scratch-Link-extensions-to-Snap-02.png

Scratch 中使用的大多数 BLE 插件都是推(push)模式(NOTIFY) – Snap! 蓝牙驱动库

前 2 个与按下按钮相关的积木都基于 BLE notify 实现; 显示文本积木, 基于 BLE write 实现。

首先, 我们需要建立与 BLE 设备的连接, 并且弄清楚 read/notify/write 对应的 Characteristic

以下代码 给了我们这些信息:

// https://github.com/scratchfoundation/scratch-vm/blob/v2.1.16/src/extensions/scratch3_microbit/index.js#L53

/**
 * Enum for micro:bit protocol.
 * https://github.com/scratchfoundation/scratch-microbit-firmware/blob/master/protocol.md
 * @readonly
 * @enum {string}
 */
const BLEUUID = {
    service: 0xf005,
    rxChar: '5261da01-fa7e-42ab-850b-7c80220097cc',
    txChar: '5261da02-fa7e-42ab-850b-7c80220097cc'
};

显示文本积木

显示文本积木相关的 javascript 代码

/**
 * Enum for micro:bit BLE command protocol.
 * https://github.com/scratchfoundation/scratch-microbit-firmware/blob/master/protocol.md
 * @readonly
 * @enum {number}
 */
const BLECommand = {
    CMD_PIN_CONFIG: 0x80,
    CMD_DISPLAY_TEXT: 0x81,
    CMD_DISPLAY_LED: 0x82
};

...

/**
 * @param {string} text - the text to display.
 * @return {Promise} - a Promise that resolves when writing to peripheral.
 */
displayText(text) {
    const output = new Uint8Array(text.length);
    for (let i = 0; i < text.length; i++) {
        output[i] = text.charCodeAt(i);
    }
    return this.send(BLECommand.CMD_DISPLAY_TEXT, output);
}

...


/**
     * Send a message to the peripheral BLE socket.
     * @param {number} command - the BLE command hex.
     * @param {Uint8Array} message - the message to write
     */
send(command, message) {
    if (!this.isConnected()) return;
    if (this._busy) return;

    // Set a busy flag so that while we are sending a message and waiting for
    // the response, additional messages are ignored.
    this._busy = true;

    // Set a timeout after which to reset the busy flag. This is used in case
    // a BLE message was sent for which we never received a response, because
    // e.g. the peripheral was turned off after the message was sent. We reset
    // the busy flag after a while so that it is possible to try again later.
    this._busyTimeoutID = window.setTimeout(() => {
        this._busy = false;
    }, 5000);

    const output = new Uint8Array(message.length + 1);
    output[0] = command; // attach command to beginning of message
    for (let i = 0; i < message.length; i++) {
        output[i + 1] = message[i];
    }
    const data = Base64Util.uint8ArrayToBase64(output);

    this._ble.write(BLEUUID.service, BLEUUID.txChar, data, 'base64', true).then(
        () => {
            this._busy = false;
            window.clearTimeout(this._busyTimeoutID);
        }
    );
}

有了这些信息, 我们可以在 Snap! 中尝试制作相关积木:

Scratch-Link-extensions-to-Snap-03.png

以上便完成了显示文本积木的功能, 所有工作都构建在我们之前的 BLE primitives 里, 没有写一行 js 代码。让我们将其包装到一个自定义积木里:

Scratch-Link-extensions-to-Snap-04.png

按下按钮积木

按下按钮这一类的的事件, 通常在 Scratch 中会呈现为 2 中积木: hat 和 reporter。

这是一种冗余, 仅使用 reporter 积木便足够。 通过一个通用的 when 积木, reporter 可以转化为 hat 。 Snap! 采用这种更简洁和通用的设计。

所以在 Snap! 中只需要实现 reporter 版本的按下按钮积木即可.

/**
 * Process the sensor data from the incoming BLE characteristic.
 * @param {object} base64 - the incoming BLE data.
 * @private
 */
_onMessage(base64) {
    // parse data
    const data = Base64Util.base64ToUint8Array(base64);

    this._sensors.tiltX = data[1] | (data[0] << 8);
    if (this._sensors.tiltX > (1 << 15)) this._sensors.tiltX -= (1 << 16);
    this._sensors.tiltY = data[3] | (data[2] << 8);
    if (this._sensors.tiltY > (1 << 15)) this._sensors.tiltY -= (1 << 16);

    this._sensors.buttonA = data[4];
    this._sensors.buttonB = data[5];

    this._sensors.touchPins[0] = data[6];
    this._sensors.touchPins[1] = data[7];
    this._sensors.touchPins[2] = data[8];

    this._sensors.gestureState = data[9];

    // cancel disconnect timeout and start a new one
    window.clearTimeout(this._timeoutID);
    this._timeoutID = window.setTimeout(
        () => this._ble.handleDisconnectError(BLEDataStoppedError),
        BLETimeout
    );
}

...

/**
 * Test whether the A or B button is pressed
 * @param {object} args - the block's arguments.
 * @return {boolean} - true if the button is pressed.
 */
isButtonPressed(args) {
    if (args.BTN === 'any') {
        return (this._peripheral.buttonA | this._peripheral.buttonB) !== 0;
    } else if (args.BTN === 'A') {
        return this._peripheral.buttonA !== 0;
    } else if (args.BTN === 'B') {
        return this._peripheral.buttonB !== 0;
    }
    return false;
}

有了这些信息, 我们可以在 Snap! 中尝试制作相关积木。以下是 A 按键按下时, 收到的数据(第 5 位代表 A 按键状态, Snap! 列表计数从 1 开始):

Scratch-Link-extensions-to-Snap-05.png

将其包装到一个自定义积木里:

Scratch-Link-extensions-to-Snap-06.png

代码的清晰性和简易性, 都比 Scratch 提升一个数量级!

hat 积木

一旦完成 reporter 积木,也就完成了 hat 积木, 只需将其拉到通用的 when 积木里即可:

Scratch-Link-extensions-to-Snap-07.png

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK