ブラウザの Web Bluetooth API を使って toio の時間指定付きモーター制御を扱う（環境は p5.js Web Editor を利用）

## はじめに
ブラウザの Web Bluetooth API と toio を組み合わせたお試しについての記事です。

toio の通信仕様は公開されていて、それに従った BLE での処理を行うと、toio を様々なプログラミング言語で制御することができます。

今回の記事では、ブラウザ上で実行する JavaScript の処理で、以下の toio の時間指定付きモーター制御を扱います（その際に、ブラウザの API である「Web Bluetooth API」を使っています）。

●モーター | toio™コア キューブ 技術仕様 ＞ 「時間指定付きモーター制御」
　https://toio.github.io/toio-spec/docs/ble_motor/#%E6%99%82%E9%96%93%E6%8C%87%E5%AE%9A%E4%BB%98%E3%81%8D%E3%83%A2%E3%83%BC%E3%82%BF%E3%83%BC%E5%88%B6%E5%BE%A1

その際に、時間指定付きモーター制御の時間指定の値などを、処理の途中で書きかえつつ toio を動かすということも試しています。

### 直近で書いた記事について
直近では、Web Bluetooth API で以下のサウンド機能を扱う話を記事にしていました。その記事の内容と今回の内容は、基本的な流れは同じです。

●ブラウザの Web Bluetooth API を使って toio で使える効果音のいくつかをセットで鳴らす（環境は p5.js Web Editor を利用） - Qiita
　https://qiita.com/youtoy/items/00abe2b06e23bf9e0f09

## 今回の内容を試した時の様子
実装内容の話に入る前に、先に今回の内容を試した時の様子（以下の動画）を紹介します。

<iframe width="560" height="315" src="https://www.youtube.com/embed/63rdkt9L6ps?si=GSACmHOPWtQFNtiJ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

こちらでは、「画面をクリックした際に toio のモーター制御を 3回実行する」ということをやっています。

また、モーター制御に関する少し変則的な処理や、p5.js のちょっとした描画処理を入れています。
それらについて、具体的には「3回目の動作を 1/2 の確率で異なる回転方向・回転時間となるようする」「一連のモーター制御を行っている間、動画の左の方にうつっている p5.js の描画用キャンバスの、背景色を青系の色に変化させる」というものです。

## 今回の実装
それでは、今回実装した内容の話に入っていきます。

まずは、プログラム全体を示します。ちょっと雑な部分があります・・・。
※ 開発・実行に [p5.js Web Editor](https://editor.p5js.org/) を用いており、その環境内の sketch.js のみ書きかえて使っています（HTML や CSS はそのまま）。

```javascript:sketch.js
const TOIO_SERVICE_UUID = "10b20100-5b3b-4571-9508-cf3efcd7bbae";
const TOIO_MOTOR_CHARACTERISTIC_UUID = "10b20102-5b3b-4571-9508-cf3efcd7bbae";

const sleep = (msec) => new Promise((resolve) => setTimeout(resolve, msec));

let characteristic;
let isConnected = false;

async function controlMotor() {
  try {
    if (!isConnected) {
      const device = await navigator.bluetooth.requestDevice({
        filters: [{ services: [TOIO_SERVICE_UUID] }],
        optionalServices: [TOIO_MOTOR_CHARACTERISTIC_UUID],
      });
      const server = await device.gatt.connect();
      const service = await server.getPrimaryService(TOIO_SERVICE_UUID);
      characteristic = await service.getCharacteristic(
        TOIO_MOTOR_CHARACTERISTIC_UUID
      );
      isConnected = true;
      console.log("接続しました");
    } else {
      background(100, 140, 190);
      console.log("モーター制御を開始");
      const data = new Uint8Array([
        0x02,
        0x01,
        1,
        0x64,
        0x02,
        2,
        0x14,
        10, // 0x0a を数値指定、10倍した値が時間（単位 ミリ秒）となる
      ]);

      await characteristic.writeValueWithoutResponse(data);

      await sleep(800);
      toggleValues(data, [2, 5]);
      data[7] = 20;
      await characteristic.writeValueWithoutResponse(data);
      await sleep(600);
      let t;
      if (random() < 0.5) {
        toggleValues(data, [2, 5]);
        t = 90;
      } else {
        t = 30;
      }
      data[7] = t;
      await characteristic.writeValueWithoutResponse(data);
      await sleep(t * 10);
      console.log("モーター制御完了");
      background(220);
    }
  } catch (error) {
    console.error(`Error: ${error}`);
  }
}

function toggleValues(data, indices) {
  indices.forEach((index) => {
    data[index] = data[index] === 1 ? 2 : 1;
  });
}

function setup() {
  createCanvas(550, 450);
  background(220);
}

function mouseClicked() {
  controlMotor();
}
```

### 実装内容の補足： 大まかな部分
上記で実装している内容は、おおまかには以下のとおりです。

- 画面をクリックすると controlMotor() を実行する
   - controlMotor() の処理は以下のとおり
      - toio との接続が行われていなかったら、toio との接続を行う： `if (!isConnected) {}` の中の処理
      - 既に toio と接続されていたら効果音を鳴らす： `background(100, 140, 190)` 〜 `background(220)` までの処理

また、複数回のモーター制御を実行する際に、モーター制御の上書きを防ぐため、冒頭に書いている以下の処理を用いています（※ モーター制御が完了する前に、次の別のモーター制御を実行すると、完了していなかったモーター制御は途中までしか実行されません）。

```javascript
const sleep = (msec) => new Promise((resolve) => setTimeout(resolve, msec));
```

なお、冒頭に書いている 2つの UUID は、toio の仕様のページに記載があるものを用いています。

### 実装内容の補足： モーター制御
モーター制御の処理は、writeValueWithoutResponse() を使っている部分です。その中で、Uint8Array() で toio に送るデータを作っています。

※ 前のサウンド機能を使った時は writeValueWithResponse() でしたが、今回は writeValueWithoutResponse() を使うことになります（違いは with か without か）

#### Uint8Array() の括弧の中の内容
Uint8Array() の括弧の中の内容は、以下となっています。

- [0x02, 0x01, 1, 0x64, 0x02, 2, 0x14, 10]
- [0x02, 0x01, 2, 0x64, 0x02, 1, 0x14, 20]
- 1/2 の確率で以下のどちらかになる
   - [0x02, 0x01, 2, 0x64, 0x02, 1, 0x14, 30]
   - [0x02, 0x01, 1, 0x64, 0x02, 2, 0x14, 30]

今回、`const data = new Uint8Array()` で定義した値を、途中で書きかえる実験をしてみたかったため、`background(100, 140, 190)` 〜 `background(220)` までの処理はごちゃごちゃした内容になっています。

なお、ここで指定している数値などの意味は、toio公式の仕様で書かれている以下の内容に従ったものです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/71fe9de8-7452-6abf-d9f2-d91ba27297c5.png)

#### 回転方向を反転させる処理
今回、3回の時間指定付きモーター制御を行う際に、回転方向が反転する部分があります。

その処理では、Uint8Array() の「3番目」「6番目」の両方の値を「1 ⇒ 2、または 2 ⇒ 1」に変化させています。それを実行するために以下の toggleValues() を用いています。

```javascript
function toggleValues(data, indices) {
  indices.forEach((index) => {
    data[index] = data[index] === 1 ? 2 : 1;
  });
}
```