【obniz 2021】 #obniz で複数台の #toio を同時に扱うための試行錯誤と 2台同時制御

この記事は、[2021年の obniz のアドベントカレンダー](https://qiita.com/advent-calendar/2021/obniz) の 17日目の記事です。

## 今回の内容
この記事の内容は、obniz と複数台の toio を組み合わせるための準備をしていく過程での試行錯誤と、2台同時の制御を行ったという内容を記事にしています。

Web Bluetooth API を使った toio の制御は、以下の記事に書いた 6台同時というのをやったことがあったのですが、「obniz で複数台の制御をやろうとするとどうなるのかな？」と思って手をつけたのが今回の内容です。

●Web Bluetooth API で toio を 6台同時に制御する - Qiita
　https://qiita.com/youtoy/items/2fae3f4365788810215d

### 途中の試行錯誤の情報が不要な場合
途中に試行錯誤した過程を、後々の自分用のメモも兼ねて色々と書いています。

結果の部分だけさっと確認したいとう方は、記事の最後のほうの「同時制御も試す」の「うまくいったやり方」という部分のあたりからお読みください。

### obniz で toio を扱う
obniz と toio を BLE で接続して扱うためには、以下の SDK を使う方法があります。

●toio_corecube | JS Parts Library | obniz
　https://obniz.com/ja/sdk/parts/toio_corecube/README.md

また、上記の SDK で実装されていない toio の機能を使う場合は、以下と toio の技術仕様の情報を使う方法があります。

●BLE - obniz Docs
　https://obniz.com/ja/doc/reference/common/ble/

まずは、上記の 2つのやり方のそれぞれを使って、複数の toio をスキャンをするところからやってみようと思います。

## 実際に試していく 【その1】
### スキャンのみを行う
#### toio のみをスキャン（SDK を利用しないもの）
まずは、SDK を使わずに toio をスキャンする処理を実装してみて、複数台を発見できるか試します。

その際、周りの関係ない機器をスキャンしないようにもしておきます。
具体的には、[公式ページの「Central: スキャン」の「条件付き検索」](https://obniz.com/ja/doc/reference/common/ble/central-scan#%E6%9D%A1%E4%BB%B6%E4%BB%98%E3%81%8D%E6%A4%9C%E7%B4%A2)に以下の記載された中の `localNamePrefix` （前方一致でデバイス名を指定するもの）を使う形で、それについて `toio` を指定します。
![条件付検索.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/b02eef66-f56f-12e1-9995-409def4fec8a.jpeg)

1つの HTMLファイルで作ってしまうような形で、以下を準備しました。
操作用のボタンなども用意したのですが、UI の見た目の部分は、[CSSフレームワークの Bulma](https://bulma.io/) を使って少し整えています。

```html
<html>

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.css" />
    <script src="https://unpkg.com/obniz/obniz.js"></script>
</head>

<body>
    <section class="section">
        <div class="container">
            <h1 class="title">obniz と toio</h1>
            <div class="buttons" style="margin-top: 1.0rem">
                <button class="button is-success is-light" type="button" onclick="onStartButtonClick()">
                    接続
                </button>
            </div>
        </div>
        <div id="obniz-debug" style="margin-top: 2.5rem"></div>
    </section>
    <script>
        let obniz;

        function onStartButtonClick() {
            obniz = new Obniz("OBNIZ_ID_HERE");

            obniz.onconnect = async function () {
                await obniz.ble.initWait();
                const target = {
                    localNamePrefix: "toio"
                };
                obniz.ble.scan.onfind = async function (peripheral) {
                    console.log(peripheral.localName)
                };
                obniz.ble.scan.onfinish = async function (peripherals, error) {
                    console.log("scan timeout!")
                };
                await obniz.ble.scan.startWait(target);
            }
        }
    </script>
</body>

</html>
```

上記の使い方・処理の流れは、、接続ボタンを押下後に obniz との接続や BLE のスキャンが実行され、スキャン結果をログに出力する、というものになります。
2個の toio を電源ON にした状態で上記を実行したところ、2つの toio をスキャンできました。
![スキャン1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/dcaf12f4-e290-0e2a-3146-c15ea3aa7bc0.jpeg)

ちなみに、 `const target = { localNamePrefix: "toio" };` を指定せずにスキャンした場合は、以下のような出力になります。
![ターゲットの指定をしないスキャン](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/683b0ed5-f0ed-07a0-7a02-e80cf206b211.jpeg)

#### toio のスキャン結果のみを出力（SDK を利用）
次は、SDK を使って、スキャン結果の中で toio の結果のみをログ出力するようにしてみます。
具体的には、以下のような処理にしてみました。

```html
<html>

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.css" />
    <script src="https://unpkg.com/obniz/obniz.js"></script>
</head>

<body>
    <section class="section">
        <div class="container">
            <h1 class="title">obniz と toio</h1>
            <div class="buttons" style="margin-top: 1.0rem">
                <button class="button is-success is-light" type="button" onclick="onStartButtonClick()">
                    接続
                </button>
            </div>
        </div>
        <div id="obniz-debug" style="margin-top: 2.5rem"></div>
    </section>
    <script>
        let obniz;

        function onStartButtonClick() {
            obniz = new Obniz("OBNIZ_ID_HERE");

            obniz.onconnect = async function () {
                await obniz.ble.initWait();
                const Toio_CoreCube = Obniz.getPartsClass("toio_CoreCube");
                obniz.ble.scan.onfind = async (peripheral) => {
                    if (Toio_CoreCube.isDevice(peripheral)) {
                        console.log("find");
                        console.log(peripheral.localName);
                    }
                };
                await obniz.ble.scan.startWait();
            }
        }
    </script>
</body>

</html>
```

上記を実行してみた結果は以下となりました。
![スキャン2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/f296e647-3e8c-446e-6d05-675024f6973f.jpeg)

「スキャンした結果、toio だった場合にのみログ出力をする」という処理にしたつもりだったのですが、出力に toio 以外のものが出てきている感じがも...（自分の実装のやり方が違ってたりするのかな...）

### 2台の toio に接続する
ここからは、スキャンした toio に接続する部分を試していきます。
なお、この後は SDK を使わない方法のほうで試していきました。

#### 接続処理をまずは足してみる
まずは、接続する処理を単純に追加してみます。

処理を追加した後の、JavaScript のプログラムの部分は以下となります。

```javascript
        let obniz;

        function onStartButtonClick() {
            obniz = new Obniz("OBNIZ_ID_HERE");

            obniz.onconnect = async function () {
                await obniz.ble.initWait();
                var target = {
                    localNamePrefix: "toio"
                };
                obniz.ble.scan.onfind = async function (peripheral) {
                    console.log(peripheral.localName)
                    try {
                        await peripheral.connectWait();
                        console.log("connected");
                    } catch (e) {
                        console.error(e);
                    }
                };
                obniz.ble.scan.onfinish = async function (peripherals, error) {
                    console.log("scan timeout!")
                };
                await obniz.ble.scan.startWait(target);
            }
        }
```

動作させてみると、1台目が見つかったら接続をして、その際にスキャンは停止するという流れとなりました。
![1台のみ接続.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/4127009a-e16e-2d01-6e0d-4c0f42061890.jpeg)

このあたりは、公式ドキュメントに書いてあるとおりの動作です。

>接続とスキャンの同時実行はできないため、接続開始と同時にスキャンは自動停止されます。ただ、接続してしまったあとの両立はできますので再度スキャンの実行ができます。

#### 接続後の再スキャンを行う
スキャン終了後に、再スキャンをする処理を足して、2台目にも接続できないか試します。

以下は、処理の追加をした部分の抜粋です。

```javascript
                obniz.ble.scan.onfinish = async function (peripherals, error) {
                    console.log("scan timeout!")
                    await obniz.ble.scan.startWait(target);
                };
```

処理を実行してみると、2台とも接続した状態にはなっていそうですが、ログには何やら意図しない挙動だったような感じがする出力が...

![2台と接続.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/ca621ac7-097d-a849-bd01-ebcea5dab1e2.jpeg)

#### 接続に使う処理を変えてみる
再度、スキャンに関するドキュメントを見て、やり方を変えてみました。

具体的には、以下に書かれた[「1つのみ検索」](https://obniz.com/ja/doc/reference/common/ble/central-scan#%EF%BC%91%E3%81%A4%E3%81%AE%E3%81%BF%E6%A4%9C%E7%B4%A2)というのを使って、1台検索して接続してから 2台目を検索して接続する、というやり方にしてみます。
![1つのみ検索.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/65859120-1b17-f5df-c37c-861cdeb5be75.jpeg)

以下に、JavaScript の処理の部分だけ記載します。

```javascript
        function onStartButtonClick() {
            obniz = new Obniz("OBNIZ_ID_HERE");

            obniz.onconnect = async function () {
                await obniz.ble.initWait();
                const target = {
                    localNamePrefix: "toio"
                };

                for (let i = 0; i < 2; i++) {
                    const peripheral = await obniz.ble.scan.startOneWait(target);
                    console.log(peripheral.localName);
                    peripherals.push(peripheral);
                    try {
                        await peripherals[peripherals.length - 1].connectWait();
                        console.log("connected");
                    } catch (e) {
                        console.error(e);
                    }
                }
            }
        }
```

実行してみると、先ほど見た気がするエラーが...
![1つだけ検索するやり方.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/77a62c50-139b-bb75-c3ad-68b7969ce1ce.jpeg)

もっと、やり方を変える必要があるのかな...

## 実際に試していく 【その2】
### スキャン＆接続
#### 接続する toio を指定するやり方
上記で試したものとは別の方法を使ってみます。

スキャンしてアドレスを取得しておいて、その後にアドレス指定を使った接続、というのを試してみることにします。
![アドレス指定.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/22bae7ad-2ab9-c45c-9ad4-66ff791dc167.jpeg)

処理の流れは以下の通りです。

1. toio を検出したらアドレスを記録しておく
2. 2台の toio のアドレスを取得できた時点でスキャンを終了させる
3. 記録していた　2つのアドレス情報を使って、1台ずつ接続を試みる

JavaScript のプログラムは、以下のようにしてみました。

```javascript
        let obniz,
            addressList = [];

        function onStartButtonClick() {
            obniz = new Obniz("OBNIZ_ID_HERE");

            obniz.onconnect = async function () {
                await obniz.ble.initWait();
                const target = {
                    localNamePrefix: "toio"
                };
                obniz.ble.scan.onfind = async function (peripheral) {
                    addressList.push(peripheral.address);
                    if (addressList.length === 2) {
                        await obniz.ble.scan.endWait();
                        console.log(addressList);
                        for (const address of addressList) {
                            try {
                                await obniz.ble.directConnectWait(address, "random");
                                console.log("connected", address);
                            } catch (e) {
                                console.log("can't connect");
                            }
                        }

                    }
                };
                obniz.ble.scan.onfinish = async function (peripherals, error) {
                    console.log("scan timeout!");
                };
                await obniz.ble.scan.startWait(target);
            }
        }
```

上記を実行すると、こんどこそエラーが出ることなく、2つの toio に接続された状態になったようでした。
![2台と接続（アドレス指定）.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/e9a3bf55-c83b-4fb0-332f-a11e431f0030.jpeg)

### 同時制御も試す
上記の接続後に、toio のモーター制御も実行してみます。

以下の [obniz公式ドキュメントの書き込み処理の部分](https://obniz.com/ja/doc/reference/common/ble/central-read-write#%E6%9B%B8%E3%81%8D%E8%BE%BC%E3%81%BF)と、[toio公式の通信仕様](https://toio.github.io/toio-spec/docs/ble_communication_overview)の情報を使って、モーター制御の部分も足してみます。
（toio の周りの処理は、[「冒頭で掲載していた過去に書いた記事」](https://qiita.com/youtoy/items/2fae3f4365788810215d)で使った内容を流用しています）
![書き込みの処理.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/48467c72-442a-675b-a80e-3cd0ea9f9d39.jpeg)

#### 失敗したやり方
当初、以下のように 2台の toio に接続する処理を行ったタイミングで peripheral の情報を配列に保持するやり方を試してみました。
そうすると、toio の挙動的には接続されたような動きであったものの、「can't connect」というメッセージがログに出力される状況でした。

```javascript
try {
    const peripheral = await obniz.ble.directConnectWait(address, "random");
    peripheralList.pusu(peripheral);
    console.log("connected", address);
} catch (e) {
    console.log("can't connect");
}
```

#### うまくいったやり方
上記の失敗があったので、いったん 2台同時接続ができた際の処理と、以下の処理とを組み合わせることにしました。

●ObnizBLE ＞ getconnectedperipherals
　https://obniz.github.io/obniz/obnizjs/classes/obnizcore.components.ble.hci.obnizble.html#getconnectedperipherals

最終的に、以下のような内容となりました。

```html
<html>

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.css" />
    <script src="https://unpkg.com/obniz/obniz.js"></script>
</head>

<body>
    <section class="section">
        <div class="container">
            <h1 class="title">obniz と toio</h1>
            <div class="buttons" style="margin-top: 1.0rem">
                <button class="button is-success is-light" type="button" onclick="connect()">
                    接続
                </button>
            </div>
        </div>
        <div id="obniz-debug" style="margin-top: 2.5rem"></div>
    </section>
    <script>
        let obniz,
            addressList = [];
        const TOIO_SERVICE_UUID = "10b20100-5b3b-4571-9508-cf3efcd7bbae";
        const MOTOR_CHARACTERISTIC_UUID = "10b20102-5b3b-4571-9508-cf3efcd7bbae";
        const motorBuf = new Uint8Array([0x02, 0x01, 0x01, 0x32, 0x02, 0x02, 0x32, 0x78,]);

        function connect() {
            obniz = new Obniz("OBNIZ_ID_HERE");

            obniz.onconnect = async function () {
                await obniz.ble.initWait();
                const target = {
                    localNamePrefix: "toio"
                };
                obniz.ble.scan.onfind = async function (peripheral) {
                    addressList.push(peripheral.address);
                    if (addressList.length === 2) {
                        await obniz.ble.scan.endWait();
                        console.log(addressList);
                        for (const address of addressList) {
                            try {
                                await obniz.ble.directConnectWait(address, "random");
                                console.log("connected", address);
                            } catch (e) {
                                console.log("can't connect");
                            }
                        }
                    }
                };
                obniz.ble.scan.onfinish = async function (peripherals, error) {
                    console.log("scan timeout!");
                };
                await obniz.ble.scan.startWait(target);

                obniz.switch.onchange = async function (state) {
                    if (state === "push") {
                        console.log("pressed");
                        // console.log(obniz.ble.getConnectedPeripherals());
                        peripheralList = obniz.ble.getConnectedPeripherals();
                        if (peripheralList.length > 0) {
                            for (const peripheral of peripheralList) {
                                console.log(peripheral);
                                await peripheral.getService(TOIO_SERVICE_UUID).getCharacteristic(MOTOR_CHARACTERISTIC_UUID).writeWait(motorBuf);
                            }
                        }
                    }
                }
            }
        }
    </script>
</body>

</html>
```

上記のプログラムを動作させた際の様子がこちらです。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/obniz?src=hash&amp;ref_src=twsrc%5Etfw">#obniz</a> の BLE を使った、 <a href="https://twitter.com/hashtag/toio?src=hash&amp;ref_src=twsrc%5Etfw">#toio</a> 2台の同時制御ができた！ <a href="https://t.co/bu4lAASMzk">pic.twitter.com/bu4lAASMzk</a></p>&mdash; you (@youtoy) <a href="https://twitter.com/youtoy/status/1471517118818811905?ref_src=twsrc%5Etfw">December 16, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

無事に、toio 2台を同時に動かすことができました。
（少し、動作開始のタイミングがずれるのだけど...）