【scratch-vm応用講座】スクラッチの拡張機能でブラウザから直接シリアル通信を操作する
※ 当ページには【広告/PR】を含む場合があります。
2021/12/17

その記事でも述べたように、非同期処理が使えるようになると色々な応用の幅も広がります。
今回は
拡張機能でArduinoを動かす?
ウェブ検索するといくつかスクラッチからArduinoなどの小型シングルボード機器を操作できる拡張機能を公開されています。
OneGPIOもプロジェクトの一つで、pythonベースのサーバーアプリを経由してPCとArduinoをシリアル通信させる方式になっています。
OneGPIOなど公開されているエクステンションは自分で機能を実装しなくていいので使う分には楽ですが、Pythonをインストールしたり、手動でサーバープログラムを立ち上げて、Arduinoとの接続を確認したりと、シリアル通信を使う準備段階までが手間でした。
スクラッチの拡張機能を自作できる人であれば、今回の記事のお題目の通り、Chromeブラウザなどに標準搭載されるようになった
着想としては以下の図のようになります。

今回はArduinoとChromeブラウザとのシリアル通信を確認するだけの簡単な拡張機能の作り方を以降で説明していきましょう。
Arduino側の準備
手始めにArduino単体でシリアル通信できているかをArduino IDEのシリアルモニターで確認しましょう。

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;
}
}
}
ここではマイコンの内部抵抗を使って
INPUT_PULLUP
スイッチを使うときでも内部でプルアップしなくないなら、外部で適当なプルアップ抵抗を付けるのをお忘れの無いようにしましよう。
Arduino IDEのシリアルモニタからボーレート9600bpsで接続し、タクトスイッチを押すたびにArduino側からメッセージが届けばOKです。

スクラッチで拡張機能作成
今回はシリアル通信でメッセージを受け取るだけの拡張機能を作成します。
シリアル機能を作成する場合、最低限必要になるのは「接続」と「切断」の2つです。
まずはChromeブラウザのデバッグコンソールの表示を確認します。
なおスクラッチ3.0の拡張機能の基本的な作り方は以前の記事に特集していたので、詳しい手順はそちらをご覧ください。
文字列の受信
とりあえず試しに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;
上のソースコードにコメントした
設定ポイント①
Web Serial APIの
port.open
flowControl: "none"
flowControl: "hardware"
拡張機能を読み込んで、接続のコマンドブロックを押すと、自動でArduinoにUSB接続しているシリアルポートが表示されます。

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

こんな感じだったり、

こんな感じだったり、

こんな感じに、バイナリデータの一つ一つはお漏しなく受け取っていそうですが、送り出されるUInt8Arrayのデータ数がバラバラです。
最初はボーレートの設定が悪さしているかと思って色々値を弄ってみましたが結果は同じようなものでした。
どうやらデータを非同期に受け取る
reader.read
ですので、改行コードの
13 10
ではこの点を踏まえて、先程の
index.js
startSerial
//...中略
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
buff_
13 > 10

ということで、Web Serial APIを介してシリアル通信できるスクラッチ拡張機能の基礎の部分が完成しました。
スクラッチプログラムに表示させる
スクラッチアプリに組み込むためには、もう少し工夫が必要です。
そこで前回の記事で解説していたように、
//...中略
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から送られてきたメッセージを喋らせてみましょう。
ボタンを押す度にちゃんとネコのスプライトへメッセージが送信されているようです。
まとめ
今回は一部のブラウザで標準搭載されるようになった
折角スクラッチアプリから直接シリアル通信が出来るようになったので、次回以降ではこのエクステンションを活用したプロジェクトを作ってみたいと思います。