toio を Web Bluetooth API で制御（「通知・読み出し・書き込み」を行う）

## はじめに
この記事は、[Web Bluetooth API](https://webbluetoothcg.github.io/web-bluetooth/) を使ってロボットトイ「[toio](https://toio.io/)」を制御した際の過程・プログラムをメモしたものです。
具体的には以下の 4種類の処理・制御を試しました。

- 通知の ON/OFF（モーションセンサーの値を受け取る）
- デバイスの切断・再接続
- 値の書き込み（ランプの点灯の制御）
- 値の読み出し（読み取りセンサーの情報を受け取る）

制御に利用する情報は、以下の公開情報から得ています。
　●通信概要 · toio™コア キューブ 技術仕様
　　https://toio.github.io/toio-spec/docs/ble_communication_overview.html

## 以前試した内容
Web Bluetooth API による toio の制御は、以前も利用したことがありました。
その時に行った内容は、2台の toio に同時に接続してモーターの制御を行ったりするもの等で、試した内容は記事に書きました。

- [toio を音で制御してみた（Audio用の Teachable Machine でベルやタンバリンの音を機械学習） - Qiita](https://qiita.com/youtoy/items/37f70bb4ce630e6cbd92)
- [#toio の目標指定付きモーター制御の仕様を確認して使ってみる（ #GWアドベントカレンダー 5/3 ） #おうちでロボット開発 - Qiita](https://qiita.com/youtoy/items/6620385a0f77e22ac6fd)
- [toio を Web Bluetooth API で制御した際の応答を得る（目標指定付きモーター制御） - Qiita](https://qiita.com/youtoy/items/2c9c52517ee3b10c006d)

このとき、値の読み出しや通知の停止・再開といった内容を試してなかったため、今回はそれらを含めて通信周りを一通り試してみます。

## 参考にしたサイト
今回、下記にたくさんあるサンプルの中の 4つを参照して、参照したソースコードの中の不要な部分を削ったり、toio用の処理に合わない部分を書きかえたり、ということをやりました。
　●samples/web-bluetooth at gh-pages · GoogleChrome/samples
　　https://github.com/GoogleChrome/samples/tree/gh-pages/web-bluetooth

参照したソースは以下の4つです。

- [samples/notifications-async-await.js](https://github.com/GoogleChrome/samples/blob/gh-pages/web-bluetooth/notifications-async-await.js)
- [samples/device-disconnect-async-await.js](https://github.com/GoogleChrome/samples/blob/gh-pages/web-bluetooth/device-disconnect-async-await.js)
- [samples/write-descriptor-async-await.js](https://github.com/GoogleChrome/samples/blob/gh-pages/web-bluetooth/write-descriptor-async-await.js)
- [samples/read-characteristic-value-changed-async-await.js](https://github.com/GoogleChrome/samples/blob/gh-pages/web-bluetooth/read-characteristic-value-changed-async-await.js)

## 作成した 4つのソースコード
以下で、toio を制御するためのソースコードを掲載します。
この記事では、ソースコードに関する補足は割愛しています。また、全てのソースコードでCSSフレームワークの「[Bulma](https://bulma.io/)」を読み込んでいます。

### 通知の ON/OFF

```JavaScript
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Web Bluetooth API で通知</title>
<link rel="stylesheet" href="./bulma.min.css">
</head>
<body>
  <section class="section">
    <div class="container">
      <h1 class="title">
        操作用ボタン1
      </h1>
      <div class="buttons">
        <button class="button is-success is-light" type="button" onclick="onStartButtonClick()">接続＋通知ON</button>
        <button class="button is-danger is-light"  type="button" onclick="onStopButtonClick()">通知OFF</button>
        <button class="button is-info is-light" type="button" onclick="onStartNotificationsButtonClick()">通知ON</button>
      </div>
    </div>
</section>

<script>
const TOIO_SERVICE_UUID          = '10b20100-5b3b-4571-9508-cf3efcd7bbae';
const MOTION_CHARACTERISTIC_UUID = '10b20106-5b3b-4571-9508-cf3efcd7bbae';

let myCharacteristic;

async function onStartButtonClick() {
  let serviceUuid = TOIO_SERVICE_UUID;
  let characteristicUuid = MOTION_CHARACTERISTIC_UUID;

  try {
    console.log('Requesting Bluetooth Device...');
    const device = await navigator.bluetooth.requestDevice({
        filters: [{services: [serviceUuid]}]});
    console.log('Connecting to GATT Server...');
    const server = await device.gatt.connect();
    console.log('Getting Service...');
    const service = await server.getPrimaryService(serviceUuid);
    console.log('Getting Characteristic...');
    myCharacteristic = await service.getCharacteristic(characteristicUuid);
    await myCharacteristic.startNotifications();
    console.log('> Notifications started');
    myCharacteristic.addEventListener('characteristicvaluechanged',
        handleNotifications);
  } catch(error) {
    console.log('Argh! ' + error);
  }
}

async function onStopButtonClick() {
  if (myCharacteristic) {
    try {
      await myCharacteristic.stopNotifications();
      console.log('> Notifications stopped');
      myCharacteristic.removeEventListener('characteristicvaluechanged',
          handleNotifications);
    } catch(error) {
      console.log('Argh! ' + error);
    }
  }
}

async function onStartNotificationsButtonClick() {
  try {
    console.log('Starting Notifications...');
    await myCharacteristic.startNotifications();
    myCharacteristic.addEventListener('characteristicvaluechanged',
        handleNotifications);
    console.log('> Notifications started');
  } catch(error) {
    log('Argh! ' + error);
  }
}

function handleNotifications(event) {
  let value = event.target.value;
  let a = [];
  for (let i = 0; i < value.byteLength; i++) {
    a.push('0x' + ('00' + value.getUint8(i).toString(16)).slice(-2));
  }
  console.log('> ' + a.join(' '));
}

</script>
</body>
</html>
```

### デバイスの切断・再接続

```JavaScript
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Web Bluetooth API で接続・切断</title>
<link rel="stylesheet" href="./bulma.min.css">
</head>
<body>
  <section class="section">
    <div class="container">
      <h1 class="title">
        操作用ボタン1
      </h1>
      <div class="buttons">
        <button class="button is-success is-light" type="button" onclick="onStarButtonClick()">接続</button>
        <button class="button is-light"            type="button" onclick="onReadButtonClick()">読み込み</button>
      </div>
    </div>
</section>

<script>
const TOIO_SERVICE_UUID          = '10b20100-5b3b-4571-9508-cf3efcd7bbae';
const ID_SENSOR_CHARACTERISTICS_UUID = '10b20101-5b3b-4571-9508-cf3efcd7bbae';

let bluetoothDevice;
let idSensorCharacteristic;

async function onStarButtonClick() {
  try {
    if (!bluetoothDevice) {
      await requestDevice();
    }
  } catch(error) {
    console.log('Argh! ' + error);
  }
}

async function requestDevice() {
  let serviceUuid = TOIO_SERVICE_UUID;
  let characteristicUuid = ID_SENSOR_CHARACTERISTICS_UUID;

  try {
    console.log('Requesting Bluetooth Device...');
    bluetoothDevice = await navigator.bluetooth.requestDevice({
          filters: [{services: [serviceUuid]}]});
    console.log('Connecting to GATT Server...');
    const server = await bluetoothDevice.gatt.connect();
    console.log('Getting Service...');
    const service = await server.getPrimaryService(serviceUuid);
    console.log('Getting Characteristic...');
    idSensorCharacteristic = await service.getCharacteristic(characteristicUuid);
  } catch(error) {
    console.log('Argh! ' + error);
  }
}

async function onReadButtonClick() {
  if (idSensorCharacteristic) {
    try {
      console.log('Reading ...');
      await idSensorCharacteristic.readValue().then(value => {
        let a = [];
        for (let i = 0; i < value.byteLength; i++) {
          a.push('0x' + ('00' + value.getUint8(i).toString(16)).slice(-2));
        }
        console.log('> ' + a.join(' '));
      });
    } catch(error) {
      console.log('Argh! ' + error);
    }
  }
}

</script>
</body>
</html>
```

### 値の書き込み

```JavaScript
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Web Bluetooth API で書き込み</title>
<link rel="stylesheet" href="./bulma.min.css">
</head>
<body>
  <section class="section">
    <div class="container">
      <h1 class="title">
        操作用ボタン1
      </h1>
      <div class="buttons">
        <button class="button is-success is-light" type="button" onclick="onReadButtonClick()">接続</button>
        <button class="button is-info is-light" type="button" onclick="onWriteButtonClick()">書き込み</button>
      </div>
    </div>
</section>

<script>
const TOIO_SERVICE_UUID          = '10b20100-5b3b-4571-9508-cf3efcd7bbae';
const LIGHT_CHARACTERISTICS_UUID = '10b20103-5b3b-4571-9508-cf3efcd7bbae';
const light_buf = new Uint8Array([ 0x03, 0x00, 0x01, 0x01, 0x00, 0xFF, 0x00 ]);

let myDescriptor;
let characteristic;

async function onReadButtonClick() {
  let serviceUuid = TOIO_SERVICE_UUID;
  let characteristicUuid = LIGHT_CHARACTERISTICS_UUID;

  try {
    console.log('Requesting Bluetooth Device...');
    const device = await navigator.bluetooth.requestDevice({
        filters: [{services: [serviceUuid]}]});
    console.log('Connecting to GATT Server...');
    const server = await device.gatt.connect();
    console.log('Getting Service...');
    const service = await server.getPrimaryService(serviceUuid);
    console.log('Getting Characteristic...');
    characteristic = await service.getCharacteristic(characteristicUuid);

    console.log('Getting Descriptor...');
    myDescriptor = await characteristic.getDescriptor('gatt.characteristic_user_description');
    const value = await myDescriptor.readValue();
    let decoder = new TextDecoder('utf-8');
    console.log('> Characteristic User Description: ' + decoder.decode(value));
  } catch(error) {
    console.log('Argh! ' + error);
  }
}

async function onWriteButtonClick() {
  if (!characteristic) {
    return;
  }
  let value = light_buf;
  try {
    await characteristic.writeValue(value);
  } catch(error) {
    console.log('Argh! ' + error);
  }
}

</script>
</body>
</html>
```

### 値の読み出し

```JavaScript
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Web Bluetooth API で読み込み</title>
<link rel="stylesheet" href="./bulma.min.css">
</head>
<body>
  <section class="section">
    <div class="container">
      <h1 class="title">
        操作用ボタン1
      </h1>
      <div class="buttons">
        <button class="button is-success is-light" type="button" onclick="onStarButtonClick()">接続</button>
        <button class="button is-light"            type="button" onclick="onReadButtonClick()">読み込み</button>
      </div>
    </div>
</section>

<script>
const TOIO_SERVICE_UUID          = '10b20100-5b3b-4571-9508-cf3efcd7bbae';
const ID_SENSOR_CHARACTERISTICS_UUID = '10b20101-5b3b-4571-9508-cf3efcd7bbae';

let bluetoothDevice;
let idSensorCharacteristic;

async function onStarButtonClick() {
  try {
    if (!bluetoothDevice) {
      await requestDevice();
    }
  } catch(error) {
    console.log('Argh! ' + error);
  }
}

async function requestDevice() {
  let serviceUuid = TOIO_SERVICE_UUID;
  let characteristicUuid = ID_SENSOR_CHARACTERISTICS_UUID;

  try {
    console.log('Requesting Bluetooth Device...');
    bluetoothDevice = await navigator.bluetooth.requestDevice({
          filters: [{services: [serviceUuid]}]});
    console.log('Connecting to GATT Server...');
    const server = await bluetoothDevice.gatt.connect();
    console.log('Getting Service...');
    const service = await server.getPrimaryService(serviceUuid);
    console.log('Getting Characteristic...');
    idSensorCharacteristic = await service.getCharacteristic(characteristicUuid);
  } catch(error) {
    console.log('Argh! ' + error);
  }
}

async function onReadButtonClick() {
  if (idSensorCharacteristic) {
    try {
      console.log('Reading ...');
      await idSensorCharacteristic.readValue().then(value => {
        let a = [];
        for (let i = 0; i < value.byteLength; i++) {
          a.push('0x' + ('00' + value.getUint8(i).toString(16)).slice(-2));
        }
        console.log('> ' + a.join(' '));
      });
    } catch(error) {
      console.log('Argh! ' + error);
    }
  }
}

</script>
</body>
</html>
```

## まとめ
無事に、Web Bluetooth API を使っての値のやりとり（通知、書き込み、読み出し）を行うことができました。
この後は、センサー等の値の読み出しで取得した結果や、通知されたセンサー等の値の変化によって、toio の挙動を変えたりするようなプログラムを作ってみようと思います。