以前、Mac上での「@abandonware/noble」を使った BLEスキャンで、特定のデバイスがスキャンできないことがあり、最近それが解決できたという流れがありました(以下の 2つが、その関連記事)。
●【IoTLT 2019】新しい obniz で obniz-noble を試す(2019/12/22) - Qiita
https://qiita.com/youtoy/items/1a2e92ae7a83df7e9a82
●Mac で noble を使って BLE対応のデバイスをスキャンする(2021年4月版) - Qiita
https://qiita.com/youtoy/items/d9c8ff3a33985359f39b
そこで、以前やろうとしていた「Mac上での noble を使った BLE経由でのガジェット制御」にあらためて手をだしてみようと思い、この記事に書いた内容を進めていきました。
複数台の toio と micro:bit を混在させてスキャンしてみる
ゆくゆくは、異なるデバイスがそれぞれ複数台ある時に、それらをまとめて制御するようなことが試せればと思っています。そこで、まずは準備段階として toio・micro:bit を複数台準備して、単純なスキャンを行ってみることから試しました。
micro:bit に関しては、以下のツイートの画像にあるような BLE UART が実行される部分を含むプログラムを書き込んであります。
micro:bit V2 + Micro: Maqueen の組み合わせで、さらに BLE を一緒に使うものを試そうと思って手をつけられていなかったもの、
— you (@youtoy) April 25, 2021
試してみようかと思って、まずは MakeCode側のプログラムを試しに作ってみているところ。 pic.twitter.com/sTTLWUUzf4
まずは @abandonware/noble の公式サンプルを元にしたシンプルなスキャンと、スキャン結果の表示をするプログラムを書き、その結果の中の toio と micro:bit 関連の情報を見てみました。得られたスキャン結果は以下のとおりで、その中の両デバイスの localName の ? という部分は、固有の文字列が表示されたことを示しています。
-
toio
- id:【文字列】
- address:【文字列】
- addressType: "random"
- connectable: true
- advertisement:
- localName: "toio Core Cube-???"
- serviceUuids":["10b201005b3b45719508cf3efcd7bbae"]
- rssi:【数値】
- mtu: null
- state: "disconnected"
-
micro:bit
- id:【文字列】
- address:【文字列】
- addressType: "random"
- connectable: true
- advertisement:
- localName: BBC micro:bit [???]"
- (serviceUuids自体がない)
- rssi:【数値】
- mtu: null
- state: "disconnected"
今回のスキャンを行った際の前提となる話があるのですが、これらのデバイスのファームウェア等のバージョンが違うと、スキャンできる情報が少し異なるようでした。これ以降の記事の内容は、toio v02.0005(2021/3/17版) と micro:bit のファームウェア 0255(V2用)となったデバイスを前提として進めています。
以上の 2つのデバイスのスキャン結果を比べてみたところ、得られた情報で 1つ大きな違いがあります。具体的には micro:bit は advertisement の中に localName しか含んでおらず、serviceUuids の部分がありません。
この serviceUuids の有無という話がスキャン周りの話で影響する部分があり、少し注意が必要そうです。具体的には、以下のスキャン時の対象をフィルタする仕様に関する部分です。
これらを総合して考えると、micro:bit のスキャンに関しては、この service UUID を使ったフィルタを行うことはできないようです。
上記の話をググってみると、この話に触れている以下のような記事も見つかりました。この記事では、スキャンした結果に対して「localName」を使った前方一致の処理を行うことでフィルタをしていました。
●Raspberry PiのBLEをNode.jsのnobleから叩いてmicro:bitを見つけてみる - Androidのメモとか
https://relativelayout.hatenablog.com/entry/2019/10/07/232655
この処理と、 @abandonware/noble の公式サンプルを元に、toio と micro:bit のみをスキャンする処理を書いてみました。
const noble = require("@abandonware/noble");
noble.on("stateChange", async (state) => {
if (state === "poweredOn") {
await noble.startScanningAsync();
} else {
await noble.stopScanningAsync();
}
});
noble.on("discover", async (peripheral) => {
const localName = peripheral.advertisement.localName;
if (localName && (localName.startsWith('BBC micro:bit')||localName.startsWith('toio Core Cube'))) {
console.log(`${peripheral.address} (${localName})`);
}
});
これで、周りに他の BLE を使っているデバイスがあっても、スキャン後に toio と micro:bit のみを対象として続きの処理を行う準備ができました。
Async を使った処理
個人的に @abandonware/noble のサンプルを見ていて気になった部分があったので、それについて触れておきます。具体的には、 startScanningAsync や stopScanningAsync など、Async が名前についた部分の処理です。
GitHub の説明を見ていると、以下のような記載がありました。どうやら「全ての処理は 2種類の API があり、1つのコールバックを返すもとの 1つの Promise を返すもの(※ こちらが Async が suffix として付いたもの)」がそれぞれ利用可能なようです。
複数の toio への接続を試す(+書き込みを行う)
ここから複数のデバイスへの接続に進んでいこうと思うのですが、簡単のために、いったん toio のみを対象にして、複数のデバイスへの接続を試していきます。また、接続した後に複数台への書き込みも試してみようと思います。
ここで試す内容は「6台の toio に接続し、それら全てに書き込みを行う」というもので、書き込みはモーターを動かす処理を使います。以下に、ソースコードや動作している様子の動画を掲載します。
モーターの制御を行うもの
試行錯誤して作ってみたソースコードは以下のとおりです。
const noble = require("@abandonware/noble");
const TOIO_SERVICE_UUID = "10b20100-5b3b-4571-9508-cf3efcd7bbae";
const MOTOR_CHARACTERISTIC_UUID = "10b20102-5b3b-4571-9508-cf3efcd7bbae";
const motorBuf = Buffer.from("0201013202023278", "hex");
let toio = [];
noble.on("stateChange", async (state) => {
if (state === "poweredOn") {
await noble.startScanningAsync();
} else {
await noble.stopScanningAsync();
}
});
noble.on("discover", async (peripheral) => {
const localName = peripheral.advertisement.localName;
if (localName && localName.startsWith("toio Core Cube")) {
await noble.stopScanningAsync();
toio.push(peripheral);
await peripheral.connectAsync();
peripheral.once("connect", async () => {
console.log("connected");
console.log(toio.length);
if (toio.length < 6) {
await noble.startScanningAsync();
} else {
for (const element of toio) {
const {
characteristics,
} = await element.discoverSomeServicesAndCharacteristicsAsync(
[TOIO_SERVICE_UUID],
[MOTOR_CHARACTERISTIC_UUID]
);
await characteristics[0].write(motorBuf);
}
for (const element of toio) {
await element.disconnectAsync();
}
}
});
}
});
先ほどと動作が異なる部分は以下の部分となります。
・toio を発見したらスキャンを止める ⇒ peripheral を別途保持してから接続
・接続処理が行われたら、toio との同時接続数が 6個分になるまでは再度スキャン等の処理を行う
・同時接続数が 6個になったタイミングで、全ての toio に対して discoverSomeServicesAndCharacteristicsAsync を順番に実行してモーターを動かす処理を実行
またモーター制御用のバイナリデータは、toio の通信仕様のモーターの部分にある「時間指定付きモーター制御」を使っています。また、toio関連の UUID 2つも、以下の仕様に掲載されているものです。
●モーター · toio™コア キューブ 技術仕様
https://toio.github.io/toio-spec/docs/assets/motor_cube_direction.svg
上記のプログラムを動作させた時の様子は、以下のツイートの動画のとおりです。
自宅にある #toio が 6台に増えた状況なので
— you (@youtoy) April 25, 2021
「6台全てに接続をして、その後 1台ずつモーターの制御をする」
というプログラムを書いてみました!
開発は Node.js で、ライブラリとして toio.js があるものの、それは使わずに実装してみています(通信仕様に従ったバイナリのやりとりにて)。 pic.twitter.com/381VLMRTGT
まとめ
とりあえず動くものはできましたが、ソースコードはまだ改善すべきところがありそうな予感がします(特に 6台との接続 ⇒ モーターの制御の部分など)。
また、複数の toio と複数の micro:bit に接続した状態で、それらを連携させた処理というのも試せてないので、手をつけられればと思います。
追記: Web Bluetooth API での実装
今回の後半の内容である 6台同時制御を、ブラウザ上で動く JavaScript のプログラム(Web Bluetooth API で実装)でも作ってみました。
●Web Bluetooth API で toio を 6台同時に制御する - Qiita
https://qiita.com/youtoy/items/2fae3f4365788810215d