【scratch-vm応用講座】スクラッチの拡張機能でブラウザから直接シリアル通信を操作する


※ 当ページには【広告/PR】を含む場合があります。
2021/12/17
合同会社タコスキングダム|TacosKingdom,LLC.

以前のブログ記事でscratch-vmをちょこっと改造してasync/await構文が使えるように「非同期処理」対応させることが出来ました。

合同会社タコスキングダム|タコキンのPスクール
【scratch-vm応用講座】Scratchの拡張機能でもasync/await構文を使う方法

スクラッチでもasyc/await非同期処理を扱えるようにscratch-vmのコード修正していきます。

その記事でも述べたように、非同期処理が使えるようになると色々な応用の幅も広がります。

今回は
別ブログで説明した「Web Serial API」もスクラッチの拡張機能から問題なく使えるようになっているか試してみましょう。


合同会社タコスキングダム|タコキンのPスクール【Pschool厳選】お子様に通わせたいロボットプログラミング教室&教材・学習サービスまとめ

拡張機能でArduinoを動かす?

ウェブ検索するといくつかスクラッチからArduinoなどの小型シングルボード機器を操作できる拡張機能を公開されています。

OneGPIOもプロジェクトの一つで、pythonベースのサーバーアプリを経由してPCとArduinoをシリアル通信させる方式になっています。

合同会社タコスキングダム|タコキンのPスクール
【Linux OS対応】ScratchのOneGPIOエクステンションでArduino Unoを動かす

Linuxの知識が必要ですが、OneGPIOエクステンションを使ってScratchとArduinoの通信を確立してみます。

OneGPIOなど公開されているエクステンションは自分で機能を実装しなくていいので使う分には楽ですが、Pythonをインストールしたり、手動でサーバープログラムを立ち上げて、Arduinoとの接続を確認したりと、シリアル通信を使う準備段階までが手間でした。

スクラッチの拡張機能を自作できる人であれば、今回の記事のお題目の通り、Chromeブラウザなどに標準搭載されるようになった
「Web Serial API」で直接シリアル通信を使えばいいじゃない、と思うのは自然な発想です。

着想としては以下の図のようになります。

合同会社タコスキングダム|タコキンのPスクール

今回はArduinoとChromeブラウザとのシリアル通信を確認するだけの簡単な拡張機能の作り方を以降で説明していきましょう。


合同会社タコスキングダム|タコキンのPスクール【Pschool厳選】Scratchをしっかり学ぶためのオススメ書籍まとめ

Arduino側の準備

手始めにArduino単体でシリアル通信できているかをArduino IDEのシリアルモニターで確認しましょう。

合同会社タコスキングダム|タコキンのPスクール

Arduinoプログラムの実装する中身は以下の通りです。

            
            // スイッチが押されている = LOW
const int buttonON = LOW;
// スイッチが押されている = HIGH
const int buttonOFF = HIGH;
// スイッチのON/OFF状態
int buttonState = 0;
// 文字列の送信フラグ
boolean sendOnceFlag = false;

// デジタルピン#4
const int buttonPin = 4;
// ビルドインLED
const int ledPin = 13;

void setup() {
    pinMode(ledPin, OUTPUT);
    pinMode(buttonPin, INPUT_PULLUP);
    Serial.begin(9600);
    delay(200);
}

void loop(){
    buttonState = digitalRead(buttonPin);
    if (buttonState == buttonON) {
        digitalWrite(ledPin, HIGH);
        if (!sendOnceFlag) {
            Serial.println("ボタンが押されました");
            sendOnceFlag = true;
        }
    } else {
        digitalWrite(ledPin, LOW);
        if (sendOnceFlag) {
            sendOnceFlag = false;
        }
    }
}
        
ここではマイコンの内部抵抗を使ってプルアップピン入力するためにデジタル4番ピンをINPUT_PULLUP指定していることに注意してください。

スイッチを使うときでも内部でプルアップしなくないなら、外部で適当なプルアップ抵抗を付けるのをお忘れの無いようにしましよう。

Arduino IDEのシリアルモニタからボーレート9600bpsで接続し、タクトスイッチを押すたびにArduino側からメッセージが届けばOKです。

合同会社タコスキングダム|タコキンのPスクール


合同会社タコスキングダム|タコキンのPスクール【Pschool厳選】お子様に通わせたいロボットプログラミング教室&教材・学習サービスまとめ

スクラッチで拡張機能作成

今回はシリアル通信でメッセージを受け取るだけの拡張機能を作成します。

シリアル機能を作成する場合、最低限必要になるのは「接続」と「切断」の2つです。

まずはChromeブラウザのデバッグコンソールの表示を確認します。

なおスクラッチ3.0の拡張機能の基本的な作り方は以前の記事に特集していたので、詳しい手順はそちらをご覧ください。

合同会社タコスキングダム|タコキンのPスクール
【Scratch拡張機能・自作】テキストデータを読み出し・保存するエクステンションを作ろう

Scratchの拡張機能の自作手順の基礎を、テキストを読み出し・書き出しできるような機能の実装を例に解説します。

文字列の受信

とりあえず試しにArduinoに接続して文字列を受け取るだけの拡張機能のindex.jsを以下に示します。

            
            const ArgumentType = require('../../extension-support/argument-type');
const BlockType = require('../../extension-support/block-type');
const Cast = require('../../util/cast');
const log = require('../../util/log');

class Scratch3WebSerialMonitor {
    constructor (runtime) {
        this.runtime = runtime;
        this.stopFlag = false;
    }

    getInfo () {
        return {
            id: 'webserialmonitor',
            name: 'Simple Test for Web Serial API',
            blocks: [
                {
                    opcode: 'connectSerial',
                    blockType: BlockType.COMMAND
                },
                {
                    opcode: 'disconnectSerial',
                    blockType: BlockType.COMMAND
                }
            ],
            menus: {
            }
        };
    }

    async startSerial() {
        try {
            console.log("INFO: 接続が確立しました");
            this.stopFlag = false;
            const port = await navigator.serial.requestPort();
            await port.open({
                baudRate: 9600,
                dataBits: 8,
                stopBits: 1,
                parity: "none",
                bufferSize: 255,
                //👇設定ポイント①
                flowControl: "hardware"
            });
            while (port.readable) {
                const reader = port.readable.getReader();
                try {
                    while (!this.stopFlag) {
                        const { value, done } = await reader.read();
                        if (done) {
                            console.log("INFO: 読込モード終了");
                            break;
                        }
                        //👇生データはバイナリなので、ユニコード文字へデコード
                        const inputValue = new TextDecoder().decode(value);
                        console.log(inputValue);
                        //👇ついでに生のバイナリ(Uint8Arrayインスタンス)も表示
                        console.log(value);
                    }
                } catch (error) {
                    console.log("ERROR: 読み出し失敗");
                    console.log(error);
                } finally {
                    reader.releaseLock();
                    await port.close();
                    console.log("INFO: 接続を切断しました");
                }
            }
        } catch (error) {
            console.log("ERRORR: ポートが開けません");
            console.log(error);
        }
    }

    stopSerial() {
        this.stopFlag = true;
    }

    connectSerial() {
        console.log('Connected!');
        this.startSerial();
    }

    disconnectSerial() {
        console.log('Disconnected');
        this.stopSerial();
    }
}

module.exports = Scratch3WebSerialMonitor;
        
上のソースコードにコメントした設定ポイント①についてですが、Arduinoのシリアル通信はDTR有りの設定なっていますので、適切なコンデンサをリセットピンに物理的に差し込んでもらうか、もしくはWeb Serial APIでDTR信号を有効に設定する必要があります。

Web Serial APIの
port.openメソッドで、フローコントロールを設定するのですが、デフォルトではflowControl: "none"となってDTRが使えないため、flowControl: "hardware"に明示してやるとコンデンサ差込無しでシリアル通信が可能です。

拡張機能を読み込んで、接続のコマンドブロックを押すと、自動でArduinoにUSB接続しているシリアルポートが表示されます。

合同会社タコスキングダム|タコキンのPスクール

Arduinoに通信接続して、先程と同様にタクトスイッチを押してブラウザのデバッグコンソールをみると...

合同会社タコスキングダム|タコキンのPスクール

こんな感じだったり、

合同会社タコスキングダム|タコキンのPスクール

こんな感じだったり、

合同会社タコスキングダム|タコキンのPスクール

こんな感じに、バイナリデータの一つ一つはお漏しなく受け取っていそうですが、送り出されるUInt8Arrayのデータ数がバラバラです。

最初はボーレートの設定が悪さしているかと思って色々値を弄ってみましたが結果は同じようなものでした。

どうやらデータを非同期に受け取る
reader.readメソッドがチャンクを送り出すタイミングを任意に内部で最適化しているようで、reader.readメソッドが送ってくる信号をそのまま文字列に変換しただけでは思うような表示ができていないことが分かります。

ですので、改行コードの
13 10を信号の区切りとして、きちんとしたチャンクを自分で実装してあげる必要がありそうです。

ではこの点を踏まえて、先程の
index.jsstartSerial関数の一部を書き換えます。

            
            //...中略
    async startSerial() {
        try {
            //...中略
                try {
                    const buff_ = [];
                    let lastByte;
                    while (!this.stopFlag) {
                        const { value, done } = await reader.read();
                        if (done) {
                            console.log("INFO: 読込モード終了");
                            break;
                        }
                        if (value) {
                            console.log(value);
                            for (let i=0;i < value.length;i++) {
                                buff_.push(value[i]);
                                if (value[i] == 10 && lastByte == 13) {
                                    //👇生データはバイナリなので、ユニコード文字へデコード
                                    const inputValue = new TextDecoder("utf-8").decode(new Uint8Array(buff_));
                                    console.log(inputValue);
                                    buff_.splice(0);
                                    console.log("INFO: 読込完了");
                                    break;
                                }
                                lastByte = value[i];
                            }
                        }
                    }
//...以降省略
        
このコードでは、reader.readメソッドが任意の長さで読み取ったUint8Arrayの中身を配列buff_に一時的にチャンクしておいて、13 > 10のバイトデータのパターンが出現した際にユニコード文字に一括変換し、コンソールに出力させるように修正しています。

合同会社タコスキングダム|タコキンのPスクール

ということで、Web Serial APIを介してシリアル通信できるスクラッチ拡張機能の基礎の部分が完成しました。


合同会社タコスキングダム|タコキンのPスクール【Pschool厳選】お子様に通わせたいロボットプログラミング教室&教材・学習サービスまとめ

スクラッチプログラムに表示させる

スクラッチアプリに組み込むためには、もう少し工夫が必要です。

そこで前回の記事で解説していたように、
HATブロックでメッセージが更新されたときを検出するイベントを拡張機能に追加してみましょう。

            
            //...中略
    constructor (runtime) {
        //...中略
        //👇最新のメッセージをキャプチャ
        this.message = '';
        //👇メッセージ更新時のトリガーとして利用
        this.isRecieveMessage = false;
    }

    getInfo () {
        return {
            //...中略
            blocks: [
                //...中略
                //👇メッセージ受信時のイベント(HATブロック)
                {
                    opcode: 'updateMessage',
                    blockType: BlockType.HAT
                },
                //👇メッセージ取得(REPORTERブロック)
                {
                    opcode: 'getMessage',
                    blockType: BlockType.REPORTER
                }
            ],
            //...中略
        };
    }

    async startSerial() {
        try {
            //...中略
                try {
                    const buff_ = [];
                    let lastByte;
                    while (!this.stopFlag) {
                        const { value, done } = await reader.read();
                        if (done) {
                            console.log("INFO: 読込モード終了");
                            break;
                        }
                        if (value) {
                            for (let i=0;i < value.length;i++) {
                                buff_.push(value[i]);
                                if (value[i] == 10 && lastByte == 13) {
                                    const inputValue = new TextDecoder("utf-8").decode(new Uint8Array(buff_));
                                    buff_.splice(0);
                                    //👇メッセージの更新
                                    this.message = inputValue;
                                    //👇HATブロックにtrueを流す
                                    this.isRecieveMessage = true;
                                    console.log("INFO: 読込完了");
                                    break;
                                }
                                lastByte = value[i];
                            }
                        }
                    }
        //...中略
    }

    //...中略

    //👇HATブロックの中身
    updateMessage() {
        const triggered = this.isRecieveMessage;
        this.isRecieveMessage = false;
        return triggered;
    }

    //👇REPORTERブロックの中身
    getMessage() {
        return this.message;
    }

//...以降省略
        

この修正をエクステンションに反映させて、デフォルトのネコのスプライトにArduinoから送られてきたメッセージを喋らせてみましょう。

ボタンを押す度にちゃんとネコのスプライトへメッセージが送信されているようです。


合同会社タコスキングダム|タコキンのPスクール【Pschool厳選】Scratchをしっかり学ぶためのオススメ書籍まとめ

まとめ

今回は一部のブラウザで標準搭載されるようになった「Web Serial API」をスクラッチの拡張機能から使うための基本的な手順を考察してみました。

折角スクラッチアプリから直接シリアル通信が出来るようになったので、次回以降ではこのエクステンションを活用したプロジェクトを作ってみたいと思います。

参考サイト

意外と知られていない?INPUT_PULLUP