toio™コア キューブをWeb Bluetoothを利用してブラウザで動かすときのチートシート

# はじめに

**[「toio」ではじめよう、おうちでロボット開発キャンペーン](https://note.toio.io/n/n15ace43c69e6)** に応募しました。

普段フロントエンドの開発でJavaScriptに慣れているため、 **[toioのJavaScriptライブラリ(toio.js)](https://github.com/toio/toio.js)** を使用した記事を書こうと思っていました。
しかし、普段VM(Linux)上のDockerでNode.jsを利用していることと、 **[toio.jsがBLE制御に使っているnobleが、Node.jsのv10で動かない等の記事](https://yaoyoroztech.com/saketamu-prototypedevelopment-ble/)**  も目に入ったので、二の足を踏みました。

Node.jsを利用したいわけではなく、JavaScriptでプログラミングすることが目的のため、toio.jsを使わずに、**BLEをブラウザからWeb Bluetoothで利用** することにしましたが、BLEについても、Web Bluetoothについても知識がなかったので、動作確認から始めることにしました。

本記事は動作確認を通して、 **toio™コア キューブ 技術仕様** と **Web Bluetoothを利用したサンプルコード** を紐づけた **チートシート** です。

※ なお、本記事ではBLEとWeb Bluetoothの勉強もかねて、直接toioのBLEを利用していますが、JavaScriptでtoioを操作したいだけであれば、@shinsuke-noguchiさんの **[1分で始めるtoioプログラミング](https://qiita.com/shinsuke-noguchi/items/219d3b36ed3dd99c2ff7)** ではtoio.jsをブラウザ用にwebpackでビルドしたスクリプトを公開してくれていますので、非常に簡単に使えます。

なお、toioのIFと、Web Bluetoothの理解も深めることを目的としているため、サンプルコードではArrayBufferの変換を、toio.jsのようなキー付きのオブジェクトではなく、配列や、引数のリストで扱うようにしています。

# 参考ドキュメント

 - **toio™コア キューブ 技術仕様**<br/>https://toio.github.io/toio-spec/
 - **Bluetooth - Web APIs | MDN**<br/>https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth
 - **Web Bluetooth Draft Community Group Report**<br/>https://webbluetoothcg.github.io/web-bluetooth/
 - **Web Bluetooth(Read/Write/Notifications)の使い方まとめ - Qiita**<br/>https://qiita.com/umi_kappa/items/45761c7454d5d4c10bf0

# チートシート

## キューブの発見

### toio技術仕様

|定義|値|
|:--|:--|
|Flags|General Discoverable Mode,<br/>BR/EDR Not Supported|
|Complete list of 128bit Service UUIDs|10B20100-5B3B-4571-9508-CF3EFCD7BBAE|
|Complete Local Name|toio Core Cube|

### JavaScript Snippet

``` javascript
const device= await navigator.bluetooth.requestDevice({
  filters: [{
    services: ['10b20100-5b3b-4571-9508-cf3efcd7bbae'],
  }]
});
const server = await device.gatt.connect();
```

## キューブの機能の利用

### toio技術仕様

|プロパティ|値|
|:--|:--|
|Type|Primary Service|
|Service UUID|10B20100-5B3B-4571-9508-CF3EFCD7BBAE|
|Characteristics|読み取りセンサー(ID Information)|
||モーションセンサー(Sensor Information)|
||ボタン(Button Information)|
||バッテリー(Battery Information)|
||モーター(Motor Control)|
||ランプ(Light Control)|
||サウンド(Sound Control)|
||設定(Configuration)|

### JavaScript Snippet

``` javascript
const service = await server.getPrimaryService('10b20100-5b3b-4571-9508-cf3efcd7bbae');
```

## 読み取りセンサー(ID Information)

### toio技術仕様

|プロパティ|値|
|:--|:--|
|Characteristic UUID|10B20101-5B3B-4571-9508-CF3EFCD7BBAE|
|Properties|Read, Notify|
|Descriptor|ID Information|

### JavaScript Snippet

``` javascript
const idCharacteristic = await service.getCharacteristic('10b20101-5b3b-4571-9508-cf3efcd7bbae');
```

## 読み取りセンサー(ID Information) # 読み出し操作 / 通知

### toio技術仕様

|先頭データ|Descriptorの種類|印刷されているモノの例|取得できる情報|
|:--|:--|:--|:--|
|`0x01`|Position ID|トイオ・コレクションに付属のプレイマット<br/>toio コア キューブ（単体）に付属の簡易プレイマット|キューブの位置と角度|
|`0x02`|Standard ID|トイオ・コレクションに付属の各種カード・シート<br/>toio コア キューブ（単体）に付属の簡易カード|ユニークな値とキューブの角度|
|`0x03`|Position ID missed|-|-|
|`0x04`|Standard ID missed|-|-|
|`0xff`|起動後IDなし|-|-|

#### Descriptor(Position ID)

|データ位置|タイプ|内容|例|備考|
|:-:|:--|:--|:--|:--|
|0|UInt8|情報の種類|`0x01`（Position ID）|固定値|
|1|UInt16|キューブの中心の X 座標値|`0x02c5`（709）|toio ID 独自に定義されているもの。<br/>**座標値一覧** 参照|
|3|UInt16|キューブの中心の Y 座標値|`0x017fd`（383）|toio ID 独自に定義されているもの。<br/>**座標値一覧** 参照|
|5|UInt16|キューブの角度|`0x0132`（306 度）| X 軸方向が 0 度で時計回りが正となる値。 値の範囲は 0 度から 360 度。|
|7|UInt16|読み取りセンサーの X 座標値|`0x02bc`（700）|toio ID 独自に定義されているもの。<br/>**座標値一覧** 参照|
|9|UInt16|読み取りセンサーの Y 座標値|`0x0182`（386）|toio ID 独自に定義されているもの。<br/>**座標値一覧** 参照|
|11|UInt16|読み取りセンサーの角度|`0x0132`（306 度）| X 軸方向が 0 度で時計回りが正となる値。 値の範囲は 0 度から 360 度。|

##### 座標値一覧

|商品|対象|左上 X 座標|左上 Y 座標|右下 X 座標|右下 Y 座標|
|:--|:--|:-:|:-:|:-:|:-:|
|トイオ・コレクション付属のプレイマット|土俵の面|45|45|455|455|
||色付きタイルの面|545|45|955|455|
|toio コア キューブ（単体）付属の簡易プレイマット|-|98|142|402|358|

#### Descriptor(Standard ID)

|データ位置|タイプ|内容|例|備考|
|:-:|:--|:--|:--|:--|
|0|UInt8|情報の種類|`0x02`（Standard ID）|固定値|
|1|UInt32|Standard ID の値|`0x00380000`<br/>（3670016: タイフーン）|キューブの位置に寄らないユニークな値。<br/>**Standard ID の値一覧** 参照|
|5|UInt16|キューブの角度|`0x0015`（21 度）|X 軸方向が 0 度で時計回りが正となる値。 値の範囲は 0 度から 360 度。|

##### Standard ID の値一覧

|商品|名前/マーク|値|値（16 進数）|
|:-:|:-:|:-:|:-:|
|トイオ・コレクション付属の各種カードやシート|タイフーン|3670016|0x00380000|
||ラッシュ|3670054|0x00380026|
||オートタックル|3670018|0x00380002|
||ランダム|3670056|0x00380028|
||ツキパワーアップ|3670020|0x00380004|
||ハリテパワーアップ|3670058|0x0038002a|
||サイドアタック|3670022|0x00380006|
||イージーモード|3670060|0x0038002c|
||ひだり|3670024|0x00380008|
||みぎ|3670062|0x0038002e|
||まえ|3670026|0x0038000a|
||うしろ|3670064|0x00380030|
||GO|3670028|0x0038000c|
||スカンク（青色）|3670078|0x0038003e|
||スカンク（緑色）|3670042|0x0038001a|
||スカンク（黄色）|3670080|0x00380040|
||スカンク（オレンジ色）|3670044|0x0038001c|
||スカンク（赤色）|3670082|0x00380042|
||スカンク（茶色）|3670046|0x0038001e|
||スピードアップ|3670066|0x00380032|
||スピードダウン|3670030|0x0038000e|
||ふらつき|3670068|0x00380034|
||パニック|3670032|0x00380010|
||スピン|3670070|0x00380036|
||ショック|3670034|0x00380012|
||クラフトファイター|3670048|0x00380020|
||リズム＆ゴー|3670052|0x00380024|
||スカンクチェイサー|3670086|0x00380046|
||フィンガーストライク|3670050|0x00380022|
||フィンガーストライク 1 人プレイ|3670088|0x00380048|
||フリームーブ|3670084|0x00380044|
|toio コア キューブ（単体）付属の簡易カード|0|3670320|0x00380130|
||1|3670321|0x00380131|
||2|3670322|0x00380132|
||3|3670323|0x00380133|
||4|3670324|0x00380134|
||5|3670325|0x00380135|
||6|3670326|0x00380136|
||7|3670327|0x00380137|
||8|3670328|0x00380138|
||9|3670329|0x00380139|
||A|3670337|0x00380141|
||B|3670338|0x00380142|
||C|3670339|0x00380143|
||D|3670340|0x00380144|
||E|3670341|0x00380145|
||F|3670342|0x00380146|
||G|3670343|0x00380147|
||H|3670344|0x00380148|
||I|3670345|0x00380149|
||J|3670346|0x0038014a|
||K|3670347|0x0038014b|
||L|3670348|0x0038014c|
||M|3670349|0x0038014d|
||N|3670350|0x0038014e|
||O|3670351|0x0038014f|
||P|3670352|0x00380150|
||Q|3670353|0x00380151|
||R|3670354|0x00380152|
||S|3670355|0x00380153|
||T|3670356|0x00380154|
||U|3670357|0x00380155|
||V|3670358|0x00380156|
||W|3670359|0x00380157|
||X|3670360|0x00380158|
||Y|3670361|0x00380159|
||Z|3670362|0x0038015a|
||!|3670305|0x00380121|
||↑|3670366|0x0038015e|
||?|3670335|0x0038013f|
||+|3670315|0x0038012b|
||−|3670317|0x0038012d|
||=|3670333|0x0038013d|
||←|3670332|0x0038013c|
||↓|3670367|0x0038015f|
||→|3670334|0x0038013e|
||×|3670314|0x0038012a|
||÷|3670319|0x0038012f|
||%|3670309|0x00380125|

#### Descriptor(Position ID missed)

|データ位置|タイプ|内容|例|備考|
|:-:|:--|:--|:--|:--|
|0|UInt8|情報の種類|`0x03`（Position ID missed）|固定値|

#### Descriptor(Standard ID missed)

|データ位置|タイプ|内容|例|備考|
|:-:|:--|:--|:--|:--|
|0|UInt8|情報の種類|`0x04`（Standard ID missed）|固定値|

#### Descriptor(起動後IDなし)

※ toio™コア キューブ 技術仕様には記載がないため、実機で確認

|データ位置|タイプ|内容|例|備考|
|:-:|:--|:--|:--|:--|
|0|UInt8|情報の種類|`0xff`|固定値|
|0 ～ 14|-|-|`0x00`|-|

### JavaScript Snippet

### Descriptor変換関数

``` javascript
const idDescriptorConvert = value => {
  const kind = value.getUint8(0);
  switch (kind) {
    case 1:
      return [kind, value.getUint16(1, true), value.getUint16(3, true), value.getUint16(5, true), value.getUint16(7, true), value.getUint16(9, true), value.getUint16(11, true)];
    case 2:
      return [kind, value.getUint32(1, true), value.getUint16(5, true)];
    default:
      return [kind];
  }
}
```

### Read

``` javascript
const value = await idCharacteristic.readValue();
console.log('ID Information#Read', idDescriptorConvert(value));
```

### Notify

``` javascript
const handler = event => {
  const value = event.target.value
  console.log('ID Information#Notify', idDescriptorConvert(value));
}

idCharacteristic.addEventListener('characteristicvaluechanged', handler)

await idCharacteristic.startNotifications().catch(error => {
  characteristic.removeEventListener('characteristicvaluechanged', handler)
  console.error(error)
})

```

## モーションセンサー(Sensor Information)

### toio技術仕様

|プロパティ|値|
|:--|:--|
|Characteristic UUID|10B20106-5B3B-4571-9508-CF3EFCD7BBAE|
|Properties|Read, Notify|
|Descriptor|Sensor Information|

### JavaScript Snippet

``` javascript
const sensorCharacteristic = await service.getCharacteristic('10b20106-5b3b-4571-9508-cf3efcd7bbae');
const value = await sensorCharacteristic.readValue();
console.log('Sensor Information#Read', sensorDescriptorConvert(value));
```

## モーションセンサー(Sensor Information) # 読み出し操作 / 通知

### toio技術仕様

#### Descriptor

|データ位置|タイプ|内容|例|備考|
|:-:|:--|:--|:--|:--|
|0|UInt8|情報の種類|`0x01`（検出）|固定値|
|1|UInt8|水平検出|`0x00`（水平ではない）|水平な時は`0x01`（水平）、一定の角度（下図の θ）より傾いた時に`0x00`（水平ではない）。角度のしきい値については **設定 - 水平検出のしきい値設定** で設定可能|
|2|UInt8|衝突検出|`0x00`（衝突なし）|衝突したときに0x00（衝突なし）から0x01（衝突あり）に変わる。 検出される衝突の大きさのしきい値については **設定 - 衝突検出のしきい値設定** で設定可能です。|
|3|UInt8|ダブルタップ検出|`0x00`（ダブルタップなし）|一度タップされてから一定時間内に再度タップされると、`0x00`（ダブルタップなし）から`0x01`（ダブルタップあり）に変わりる。タップの時間間隔は **設定 - ダブルタップ検出のしきい値設定** で設定可能です。また、検出するタップの強さは **設定 - 衝突検出のしきい値設定** の設定と同じです。|
|4|UInt8|姿勢検出|`0x01`（正位置）|水平面に対する姿勢が変化したときに値が変わる。<br/>**姿勢検出の値一覧** 参照|

#### 姿勢検出の値一覧

|値|キューブの姿勢|
|:-:|:--|
|1|天面が上|
|2|底面が上|
|3|背面が上|
|4|正面が上|
|5|右側面が上|
|6|左側面が上|

### JavaScript Snippet

### Descriptor変換関数

``` javascript
const sensorDescriptorConvert = value => {
  return [value.getUint8(0), value.getUint8(1), value.getUint8(2), value.getUint8(3), value.getUint8(4)];
}
```

### Read

``` javascript
const value = await sensorCharacteristic.readValue();
console.log('Sensor Information#Read', sensorDescriptorConvert(value));
```

### Notify

``` javascript
const handler = event => {
  const value = event.target.value
  console.log('Sensor Information#Notify', sensorDescriptorConvert(value));
}

sensorCharacteristic.addEventListener('characteristicvaluechanged', handler)

await sensorCharacteristic.startNotifications().catch(error => {
  characteristic.removeEventListener('characteristicvaluechanged', handler)
  console.error(error)
})

```

## ボタン(Button Information)

### toio技術仕様

|プロパティ|値|
|:--|:--|
|Characteristic UUID|10B20107-5B3B-4571-9508-CF3EFCD7BBAE|
|Properties|Read, Notify|
|Descriptor|Button Information|

### JavaScript Snippet

``` javascript
const buttonCharacteristic = await service.getCharacteristic('10b20107-5b3b-4571-9508-cf3efcd7bbae');
```

## ボタン(Button Information) # 読み出し操作 / 通知

### toio技術仕様

#### Descriptor

|データ位置|タイプ|内容|例|備考|
|:-:|:--|:--|:--|:--|
|0|UInt8|ボタンの ID|`0x01`（検出）|固定値|
|1|UInt8|ボタンの状態|`0x80`（押された）|押されたら`0x80`、離されたら`0x00`|


### JavaScript Snippet

### Descriptor変換関数

``` javascript
const buttonDescriptorConvert = value => {
  return [value.getUint8(0), value.getUint8(1)];
}
```

### Read

``` javascript
const value = await buttonCharacteristic.readValue();
console.log('Button Information#Read', buttonDescriptorConvert(value));
```

### Notify

``` javascript
const handler = event => {
  const value = event.target.value
  console.log('Button Information#Notify', buttonDescriptorConvert(value));
}

buttonCharacteristic.addEventListener('characteristicvaluechanged', handler)

await buttonCharacteristic.startNotifications().catch(error => {
  characteristic.removeEventListener('characteristicvaluechanged', handler)
  console.error(error)
})

```

## バッテリー(Battery Information)

### toio技術仕様

|プロパティ|値|
|:--|:--|
|Characteristic UUID|10B20108-5B3B-4571-9508-CF3EFCD7BBAE|
|Properties|Read, Notify|
|Descriptor|Battery Information|

### JavaScript Snippet

``` javascript
const batteryCharacteristic = await service.getCharacteristic('10b20108-5b3b-4571-9508-cf3efcd7bbae');
```

## バッテリー(Battery Information) # 読み出し操作 / 通知

### toio技術仕様

#### Descriptor

|データ位置|タイプ|内容|例|備考|
|:-:|:--|:--|:--|:--|
|0|UInt8|バッテリー残量|`0x50`（80 パーセント）| 0 から 100 までの範囲を 10 刻みで取得可能。単位はパーセント。|


### JavaScript Snippet

### Descriptor変換関数

``` javascript
const batteryDescriptorConvert = value => {
  return [value.getUint8(0)];
}
```

### Read

``` javascript
const value = await batteryCharacteristic.readValue();
console.log('Button Information#Read', batteryDescriptorConvert(value));
```

### Notify

``` javascript
const handler = event => {
  const value = event.target.value
  console.log('Button Information#Notify', batteryDescriptorConvert(value));
}

batteryCharacteristic.addEventListener('characteristicvaluechanged', handler)

await batteryCharacteristic.startNotifications().catch(error => {
  characteristic.removeEventListener('characteristicvaluechanged', handler)
  console.error(error)
})

```

## モーター(Motor Control)

### toio技術仕様

|プロパティ|値|
|:--|:--|
|Characteristic UUID|10B20102-5B3B-4571-9508-CF3EFCD7BBAE|
|Properties|Write without response, Read, Notify|
|Descriptor|Motor Control|

### JavaScript Snippet

``` javascript
const motorCharacteristic = await service.getCharacteristic('10b20102-5b3b-4571-9508-cf3efcd7bbae');
```

## モーター(Motor Control) # Write without response

### toio技術仕様

|先頭データ|Descriptorの種類|備考|
|:--|:--|:--|
|`0x01`|モーター制御|左右のモーターの回転方向(前後)と速度を指定してモータを制御する。<br/>次の書き込みが行われるまで、指定した速度で動き続ける。|
|`0x02`|時間指定付きモーター制御|動かす時間を指定してモーターを制御する。<br/>指定した時間を経過するとモーターは停止する。|
|`0x03`|目標指定付きモーター制御|目標を指定してモーターを制御する。<br/>目標に到達、もしくは、タイムアウト等のエラーが発生した場合は応答を返す。応答については **目標指定付きモーター制御の応答** 参照|
|`0x04`|複数目標指定付きモーター制御|複数の目標を指定してモーターを制御する。<br/>目標に到達、もしくは、タイムアウト等のエラーが発生した場合は応答を返す。応答については **複数目標指定付きモーター制御の応答** 参照|
|`0x05`|加速度指定付きモーター制御|加速度を指定してモーターを制御する。|

#### Descriptor(モーター制御)

|データ位置|タイプ|内容|例|備考|
|:-:|:--|:--|:--|:--|
|0|UInt8|制御の種類|`0x01`（モーター制御）|固定|
|1|UInt8|制御するモーターの ID|`0x01`（左）|左のモーターの ID が`1`で右のモーターの ID が`2`。<br/>データ位置4とは異なる値を指定すること。|
|2|UInt8|モーターの回転方向|`0x01`（前）|前進する方向が`1`。後退する方向が`2`。|
|3|UInt8|モーターの速度指示値|`0x64`（100）|0以上255以下の範囲で指定できる。0～7が0、8～115が34rpm～494rpmまで単調増加、116～255が494rpm|
|4|UInt8|制御するモーターの ID|`0x02`（右）|左のモーターの ID が`1`で右のモーターの ID が`2`。<br/>データ位置1とは異なる値を指定すること。|
|5|UInt8|モーターの回転方向|`0x02`（後ろ）|前進する方向が`1`。後退する方向が`2`。|
|6|UInt8|モーターの速度指示値|`0x14`（20）|0以上255以下の範囲で指定できる。0～7が0、8～115が34rpm～494rpmまで単調増加、116～255が494rpm|

#### Descriptor(時間指定付きモーター制御)

|データ位置|タイプ|内容|例|備考|
|:-:|:--|:--|:--|:--|
|0|UInt8|制御の種類|`0x02`（時間指定付きモーター制御）|固定|
|1|UInt8|制御するモーターの ID|`0x01`（左）|左のモーターの ID が`1`で右のモーターの ID が`2`。<br/>データ位置4とは異なる値を指定すること。|
|2|UInt8|モーターの回転方向|`0x01`（前）|`前進する方向が`1`。後退する方向が`2`。|
|3|UInt8|モーターの速度指示値|0x64（100）|0以上255以下の範囲で指定できる。0～7が0、8～115が34rpm～494rpmまで単調増加、116～255が494rpm|
|4|UInt8|制御するモーターの ID|0x02（右）|左のモーターの ID が`1`で右のモーターの ID が`2`。<br/>データ位置1とは異なる値を指定すること。|
|5|UInt8|モーターの回転方向|0x02（後）|前進する方向が`1`。後退する方向が`2`。|
|6|UInt8|モーターの速度指示値|0x14（20）|0以上255以下の範囲で指定できる。0～7が0、8～115が34rpm～494rpmまで単調増加、116～255が494rpm|
|7|UInt8|モーターの制御時間|0x0A（100 ミリ秒）|0は「時間制限無し」(次の書き込みが行われるまで、指定した速度で動きづづける)。1以上255以下の範囲では x10 ミリ秒動いたあと停止する。|

####  Descriptor(目標指定付きモーター制御)

|データ位置|タイプ|内容|例|備考|
|:-:|:--|:--|:--|:--|
|0|UInt8|制御の種類|0x03（目標指定付きモーター制御）|固定値|
|1|UInt8|制御識別値|0x00|**目標指定付きモーター制御の応答** を識別するための値。設定した値が対応する応答に含まれる。値の範囲は0以上255以下の範囲。|
|2|UInt8|タイムアウト時間|0x05（5 秒）|ここで設定した時間が経過してもキューブが目標地点に到達していない場合、キューブは動作を停止し`0x01`: タイムアウトの応答を返す。 値は0以上255以下の範囲。単位は秒。0のみ例外的に 10 秒。タイムアウト無しの設定はできない。|
|3|UInt8|移動タイプ|0x00（回転しながら移動）|**移動タイプ一覧** 参照|
|4|UInt8|モーターの最大速度指示値|0x50（80）|最大の速度指示値を指定する。8～115が34rpm～494rpmまで単調増加、116～255が494rpm。0～7を指定した場合、命令は破棄され`0x06`: 非サポートの応答を返す。|
|5|UInt8|モーターの速度変化タイプ|0x00（速度一定）|**モーターの速度変化タイプ一覧** 参照|
|6|UInt8|Reserved|0x00|固定値|
|7|UInt16|目標地点の X 座標値|0x02bc（700）|範囲は`0x0000`以上`0xffff以下`。`0xffff`は「書き込み操作時と同じ」という意味。|
|9|UInt16|目標地点の Y 座標値|0x0182（386）|範囲は`0x0000`以上`0xffff以下`。`0xffff`は「書き込み操作時と同じ」という意味。|
|11|UInt16|目標地点でのキューブの角度 Θ|0x005a（90 度）|16bit のうち下位 13bit は`0x0000`以上`0x1ffff`以下の範囲で角度を指定する。上位 3bit は **角度タイプ一覧** を参照。上位 3bit によって下位 13bit の表す角度の意味と動き方が変わる。|

##### 移動タイプ一覧

|値|移動方法の説明|
|:--|:--|
|0|回転しながら移動|
|1|回転しながら移動（後退なし）|
|2|回転してから移動|

##### モーターの速度変化タイプ一覧

|値|速度変化タイプ|
|:--|:--|
|0x00|速度一定|
|0x01|目標地点まで徐々に加速|
|0x02|目標地点まで徐々に減速|
|0x03|中間地点まで徐々に加速し、そこから目標地点まで減速|

##### 角度タイプ一覧

|上位 3bit の値|角度の意味|回転方向|
|:--|:--|:--|
|0x00|絶対角度|回転量が少ない方向|
|0x01|絶対角度|正方向|
|0x02|絶対角度|負方向|
|0x03|相対角度|正方向|
|0x04|相対角度|負方向|
|0x05|角度指定なし|回転しない|
|0x06|1 つ前の目標地点での設定、もしくは、書き込み操作時と同じ|回転量が少ない方向|

####  Descriptor(複数目標指定付きモーター制御)

|データ位置|タイプ|内容|例|備考|
|:-:|:--|:--|:--|:--|
|0|UInt8|制御の種類|0x04（複数目標指定付きモーター制御）|固定値|
|1|UInt8|制御識別値|0x00|**複数目標指定付きモーター制御の応答** を識別するための値。設定した値が対応する応答に含まれる。値の範囲は0以上255以下の範囲。|
|2|UInt8|タイムアウト時間|0x05（5 秒）|ここで設定した時間が経過してもキューブが目標地点に到達していない場合、キューブは動作を停止し`0x01`: タイムアウトの応答を返す。 値は0以上255以下の範囲。単位は秒。0のみ例外的に 10 秒。タイムアウト無しの設定はできない。1 つの目標に到達するとタイマーはリセットされる。|
|3|UInt8|移動タイプ|0x00（回転しながら移動）|**移動タイプ一覧** 参照|
|4|UInt8|モーターの最大速度指示値|0x50（80）|最大の速度指示値を指定する。8～115が34rpm～494rpmまで単調増加、116～255が494rpm。0～7を指定した場合、命令は破棄され`0x06`: 非サポートの応答を返す。|
|5|UInt8|モーターの速度変化タイプ|0x00（速度一定）|**モーターの速度変化タイプ一覧** 参照|
|6|UInt8|Reserved|0x00|固定値|
|7|UInt8|書き込み操作の追加設定|0x01（追加）|`0x00`: 上書き、もしくは`0x01`: 追加のどちらかを指定する。`0x01`: 追加を指定した場合は、「実行中のモーター制御が複数目標指定付きモーター制御である」と「既に追加した複数目標指定付きモーター制御がない」の 2 つの条件を満たす場合に1 回分の書き込み操作を保留して、実行中のモーター制御が完了した後に自動的に実行を開始する。`0x01`: 追加を指定しても、実行中のモーター制御が複数目標指定付きモーター制御でない場合は`0x00`: 上書きと同じ動作になる。また、既に追加した複数目標指定付きモーター制御が存在する場合は書き込み操作が破棄され、複数目標指定付きモーター制御の応答において`0x07`: 書き込み操作の追加不可の応答を返す。|
|8 + 6i|UInt16|目標地点 i の X 座標値|0x0064（100）|範囲は`0x0000`以上`0xffff以下`。`0xffff`は「1 つ前の目標地点での設定と同じ」という意味。|
|10 + 6i|UInt16|目標地点 i の Y 座標値|0x0064（100）|範囲は`0x0000`以上`0xffff以下`。`0xffff`は「1 つ前の目標地点での設定と同じ」という意味。|
|12 + 6i|UInt16|目標地点 i でのキューブの角度 Θ|0x0000（0 度）|16bit のうち下位 13bit は`0x0000`以上`0x1ffff`以下の範囲で角度を指定する。上位 3bit は **角度タイプ一覧** を参照。上位 3bit によって下位 13bit の表す角度の意味と動き方が変わる。|

i は28(29個の目標指定)まで。それを超える場合はエラー。

####  Descriptor(加速度指定付きモーター制御)

|データ位置|タイプ|内容|例|備考|
|:-:|:--|:--|:--|:--|
|0|UInt8|制御の種類|0x05（加速度指定付きモーター制御）|固定値|
|1|UInt8|キューブの並進速度|0x32（50）|0以上255以下の範囲で指定できる。0～7が0、8～115が34rpm～494rpmまで単調増加、116～255が494rpm|
|2|UInt8|キューブの加速度|0x05（5）|100 ミリ秒ごとの速度の増加分（または減少分）を指定する。キューブの速度が「キューブの並進速度」で指定した速度に到達すると加速を終了する。値は0以上255以下の範囲で指定でき、0の場合は書き込み操作をすると直ちに「キューブの並進速度」で指定した速度になる。この設定は加速度指定付きモーター制御を連続して書き込む場合のみ有効。キューブがその他のモーター制御で動いている場合は無視され、0を設定したときと同様に書き込み操作をすると直ちに「キューブの並進速度」で指定した速度になる。|
|3|UInt16|キューブの向きの回転速度|0x000F（15 度/秒）|キューブの向きの回転速度を指定する。値は0x00以上0xffff以下の範囲で指定でき、単位は[度/秒]です。|
|5|UInt8|キューブの向きの回転方向|0x00（正方向）|0が正方向で1が負方向。|
|6|UInt8|キューブの進行方向|0x00（前進）|0が前進する方向で1が後退する方向。|
|7|UInt8|優先指定|0x00（並進速度優先）|並進速度と回転速度の組み合わせによって、速度指示値の範囲を超えてしまった場合に、並進速度と回転速度のどちらを優先するかを指定する。0は並進速度を優先し、回転速度を調整する。1は回転速度を優先し、並進速度を調整する。|
|8|UInt8|制御時間|0x64（1 秒）|0以上255以下の範囲で指定する。0は「時間制限無し」を意味し、次の書き込み操作が行われるまでモーターは指定した速度で動きづづける。1以上255以下の範囲では x10 ミリ秒モーターは動いたあと停止する。|

### JavaScript Snippet

### Descriptor変換関数(Write without response)

``` javascript
const motorWriteDescriptorConvert = array => {
  let buffer, dataview;
  switch (array[0]) {
    case 1:
      buffer = new ArrayBuffer(7);
      dataview = new DataView(buffer);
      dataview.setUint8(0, array[0]);
      dataview.setUint8(1, array[1]);
      dataview.setUint8(2, array[2]);
      dataview.setUint8(3, array[3]);
      dataview.setUint8(4, array[4]);
      dataview.setUint8(5, array[5]);
      dataview.setUint8(6, array[6]);
      return buffer;
    case 2:
      buffer = new ArrayBuffer(8);
      dataview = new DataView(buffer);
      dataview.setUint8(0, array[0]);
      dataview.setUint8(1, array[1]);
      dataview.setUint8(2, array[2]);
      dataview.setUint8(3, array[3]);
      dataview.setUint8(4, array[4]);
      dataview.setUint8(5, array[5]);
      dataview.setUint8(6, array[6]);
      dataview.setUint8(7, array[7]);
      return buffer;
    case 3:
      buffer = new ArrayBuffer(13);
      dataview = new DataView(buffer);
      dataview.setUint8(0, array[0]);
      dataview.setUint8(1, array[1]);
      dataview.setUint8(2, array[2]);
      dataview.setUint8(3, array[3]);
      dataview.setUint8(4, array[4]);
      dataview.setUint8(5, array[5]);
      dataview.setUint8(6, array[6]);
      dataview.setUint16(7, array[7], true);
      dataview.setUint16(9, array[8], true);
      dataview.setUint16(11, array[9], true);
      return buffer;
    case 4:
      const pointNum = Math.floor((array.length -　8) / 3)
      buffer = new ArrayBuffer(13);
      dataview = new DataView(buffer);
      dataview.setUint8(0, array[0]);
      dataview.setUint8(1, array[1]);
      dataview.setUint8(2, array[2]);
      dataview.setUint8(3, array[3]);
      dataview.setUint8(4, array[4]);
      dataview.setUint8(5, array[5]);
      dataview.setUint8(6, array[6]);
      dataview.setUint8(7, array[7]);
      for (let i = 0; i < pointNum; i++) {
        dataview.setUint16(8 + 6 * i, array[8 + 3 * i], true);
        dataview.setUint16(10 + 6 * i, array[9 + 3 * i], true);
        dataview.setUint16(12 + 6 * i, array[10 + 3 * i], true);
      }
      return buffer;
    case 5:
      buffer = new ArrayBuffer(9);
      dataview = new DataView(buffer);
      dataview.setUint8(0, array[0]);
      dataview.setUint8(1, array[1]);
      dataview.setUint8(2, array[2]);
      dataview.setUint8(3, array[3]);
      dataview.setUint8(4, array[4]);
      dataview.setUint8(5, array[5]);
      dataview.setUint8(6, array[6]);
      dataview.setUint8(7, array[7]);
      dataview.setUint8(8, array[8]);
      return buffer;
    default:
      console.error(`${array[0]} is invalid code`)
      return null;
  }
}
```

### Write without response

``` javascript
await motorCharacteristic.writeValue(motorWriteDescriptorConvert(array));
```

## モーター(Motor Control) # 読み出し操作 / 通知

### toio技術仕様

|先頭データ|Descriptorの種類|備考|
|:--|:--|:--|
|`0x83`|目標指定付きモーター制御の応答|-|
|`0x84`|複数目標指定付きモーター制御の応答|-|

####  Descriptor(目標指定付きモーター制御の応答)

|データ位置|タイプ|内容|例|備考|
|:-:|:--|:--|:--|:--|
|0|UInt8|制御の種類|`0x83`（目標指定付きモーター制御の応答）|固定値|
|1|UInt8|制御識別値|`0x00`|対応する目標指定付きモーター制御で設定した値。|
|2|UInt8|応答内容|`0x00`（正常終了）|**目標指定付きモーター制御の応答内容一覧** 参照|

##### 目標指定付きモーター制御の応答内容一覧

|値|内容|状態|
|:-:|:--|:--|
|`0x00`|正常終了|目標に到達したとき。<br/>モーターは停止する。|
|`0x01`|タイムアウト|指定したタイムアウト時間を経過したとき。<br/>モーターは停止する。|
|`0x02`|toio ID missed|toio ID がない場所にキューブが置かれたとき。<br/>モーターは停止する。|
|`0x03`|不正なパラメーターの組み合わせ|座標 X, 座標 Y, 角度の全てが現在と同じだったとき。<br/>書き込み操作は破棄される。|
|`0x04`|不正な状態|電源を切られたとき。<br/>応答が通知された後、電源が切れる。|
|`0x05`|他の書き込み受付|目標指定付きモーター制御以外のモーター制御が書き込まれた時。<br/>実行中のモーター制御は終了し、新たに書き込まれたモーター制御が実行される。|
|`0x06`|非サポート|指定したモーターの最大速度指示値が8未満のとき。<br/>書き込み操作は破棄される。|

####  Descriptor(複数目標指定付きモーター制御の応答)

|データ位置|タイプ|内容|例|備考|
|:-:|:--|:--|:--|:--|
|0|UInt8|制御の種類|`0x84`（複数目標指定付きモーター制御の応答）|固定値|
|1|UInt8|制御識別値|`0x00`|対応する複数目標指定付きモーター制御で設定した値。|
|2|UInt8|応答内容|`0x00`（正常終了）|**複数目標指定付きモーター制御の応答内容一覧** 参照|

##### 複数目標指定付きモーター制御の応答内容一覧

|値|内容|状態|
|:-:|:--|:--|
|`0x00`|正常終了|目標に到達したとき。<br/>モーターは停止する。|
|`0x01`|タイムアウト|指定したタイムアウト時間を経過したとき。<br/>モーターは停止する。|
|`0x02`|toio ID missed|toio ID がない場所にキューブが置かれたとき。<br/>モーターは停止する。|
|`0x03`|不正なパラメーターの組み合わせ|座標 X, 座標 Y, 角度の全てが現在と同じだったとき。<br/>書き込み操作は破棄される。|
|`0x04`|不正な状態|電源を切られたとき。<br/>応答が通知された後、電源が切れる。|
|`0x05`|他の書き込み受付|目標指定付きモーター制御以外のモーター制御が書き込まれた時。<br/>実行中のモーター制御は終了し、新たに書き込まれたモーター制御が実行される。|
|`0x06`|非サポート|指定したモーターの最大速度指示値が8未満のとき。<br/>書き込み操作は破棄される。|
|`0x07`|書き込み操作の追加不可|書き込み操作の追加ができないとき<br/>書き込み操作は破棄される。|

### JavaScript Snippet

### Descriptor変換関数

``` javascript
const motorReadDescriptorConvert = value => {
  return [value.getUint8(0), value.getUint8(1), value.getUint8(2)];
}
```

### Read

``` javascript
const value = await motorCharacteristic.readValue();
console.log('Motor Control#Read', motorReadDescriptorConvert(value));
```

### Notify

``` javascript
const handler = event => {
  const value = event.target.value
  console.log('Motor Control#Notify', motorReadDescriptorConvert(value));
}

motorCharacteristic.addEventListener('characteristicvaluechanged', handler)

await motorCharacteristic.startNotifications().catch(error => {
  characteristic.removeEventListener('characteristicvaluechanged', handler)
  console.error(error)
})

```

## ランプ(Light Control)
追記予定

## サウンド(Sound Control)
追記予定

## 設定(Configuration)
追記予定

# 動作確認用html

上記コードスニペットを利用した単一HTMLファイル。
ローカルファイルとして保存して、chromeで開くだけで、コードを確認することができます。

``` html
<html>
  <head>
    <meta charset="utf-8">
    <style type="text/css">
      .service {
        float: left;
        padding: 12px;
      }
      .characteristic {
        padding: 12px;
      }
      .property {
        padding: 12px;
      }
      .log {
        margin: 4px;
        padding: 4px;
        height: 6em;
        overflow-y: scroll;
        border-style: solid;
        border-width: 1px;
        font-family:monospace;
        font-size: 8px;
      }
      button + button {
        margin-left: 4px;
      }
      
    </style>
  </head>
  <body>
    <div id="connect-area">
      <button id="connect">キューブの発見</button>
    </div>
    <div id="service-area">
    </div>
    <script>
const devices = [];

const connectEvent = async() => {
  console.log('キューブの発見(requestDevice, connect) START');
  const device= await navigator.bluetooth.requestDevice({
    filters: [{
      services: ['10b20100-5b3b-4571-9508-cf3efcd7bbae'],
    }]
  });
  if (devices.includes(device)) {
    alert('既に接続済みのキューブが選択されました。');
  } else {
    devices.push(device);
    const server = await device.gatt.connect();
    const div = createElement('div', null, 'service');
    const button = createElement('button', 'キューブの機能の利用');
    button.addEventListener('click', getPrimaryServiceEvent(server, div));
    div.appendChild(button);
    document.getElementById('service-area').appendChild(div);
  }
  console.log('キューブの発見(requestDevice, connect) END');
}

const getPrimaryServiceEvent = (server, div) => {
  let service = null;
  return async() => {
    console.log('キューブの機能の利用(getPrimaryService) START');
    if (service) {
      alert('既にキューブの機能の利用は実行済です。');
    } else {
      service = await server.getPrimaryService('10b20100-5b3b-4571-9508-cf3efcd7bbae');
      // ID Information用エリアの構築
      const idDiv = createElement('div', null, 'characteristic');
      const idButton = createElement('button', '読み取りセンサー(ID Information)');
      idButton.addEventListener('click', getIdCharacteristic(service, idDiv));
      idDiv.appendChild(idButton);
      div.appendChild(idDiv);
      // Sensor Information用エリアの構築
      const sensorDiv = createElement('div', null, 'characteristic');
      const sensorButton = createElement('button', 'モーションセンサー(Sensor Information)');
      sensorButton.addEventListener('click', getSensorCharacteristic(service, sensorDiv));
      sensorDiv.appendChild(sensorButton);
      div.appendChild(sensorDiv);
      // Button Information用エリアの構築
      const buttonDiv = createElement('div', null, 'characteristic');
      const buttonButton = createElement('button', 'ボタン(Button Information)');
      buttonButton.addEventListener('click', getButtonCharacteristic(service, buttonDiv));
      buttonDiv.appendChild(buttonButton);
      div.appendChild(buttonDiv);
      // Battery Information用エリアの構築
      const batteryDiv = createElement('div', null, 'characteristic');
      const batteryButton = createElement('button', 'バッテリー(Battery Information)');
      batteryButton.addEventListener('click', getBatteryCharacteristic(service, batteryDiv));
      batteryDiv.appendChild(batteryButton);
      div.appendChild(batteryDiv);
      // Motor Control用エリアの構築
      const motorDiv = createElement('div', null, 'characteristic');
      const motorButton = createElement('button', 'モーター(Motor Control)');
      motorButton.addEventListener('click', getMotorCharacteristic(service, motorDiv));
      motorDiv.appendChild(motorButton);
      div.appendChild(motorDiv);
    }
    console.log('キューブの機能の利用(getPrimaryService) END');
  }
}

const getIdCharacteristic = (service, div) => {
  let idCharacteristic  = null;
  return async() => {
    console.log('[Start] ID Information#getCharacteristic');
    if (idCharacteristic) {
      alert('既に読み取りセンサー(ID Information)は実行済です。');
    } else {
      idCharacteristic = await service.getCharacteristic('10b20101-5b3b-4571-9508-cf3efcd7bbae');
      // Readプロパティ用エリアの構築
      const readDiv = createElement('div', null, 'property');
      const readButton = createElement('button', 'Read');
      readButton.addEventListener('click', getIdRead(idCharacteristic, readDiv));
      readDiv.appendChild(readButton);
      div.appendChild(readDiv);
      // Notifyプロパティ用エリアの構築
      const notifyDiv = createElement('div', null, 'property');
      const notifyStartButton = createElement('button', 'Notify(Start)');
      const notifyEndButton = createElement('button', 'Notify(end)');
      const [notifyStartEvent, notifyEndEvent] = getIdNotify(idCharacteristic, notifyDiv)
      notifyStartButton.addEventListener('click', notifyStartEvent);
      notifyEndButton.addEventListener('click', notifyEndEvent);
      notifyDiv.appendChild(notifyStartButton);
      notifyDiv.appendChild(notifyEndButton);
      div.appendChild(notifyDiv);
    }
    console.log('[End] ID Information#getCharacteristic');
  }
}

const getIdRead = (idCharacteristic, div) => {
  let resultDiv = null;
  return async() => {
    console.log('[Start] ID Information#readValue');
    if (!resultDiv) {
      resultDiv = createElement('div', null, 'log');
      div.appendChild(resultDiv);
    }
    const value = await idCharacteristic.readValue();
    const result = idDescriptorConvert(value);
    resultDiv.appendChild(createElement('div', JSON.stringify(result)));
    resultDiv.scrollTop = resultDiv.scrollHeight;
    console.log('[End] ID Information#readValue');
  }
}

const getIdNotify = (idCharacteristic, div) => {
  let resultDiv = null;
  let notifyFlg = false;
  const handler = event => {
    const value = event.target.value
    const result = idDescriptorConvert(value);
    resultDiv.appendChild(createElement('div', JSON.stringify(result)));
    resultDiv.scrollTop = resultDiv.scrollHeight;
  };
  return [
    async() => {
      console.log('[Start] ID Information#startNotifications');
      if (!resultDiv) {
        resultDiv = createElement('div', null, 'log');
        div.appendChild(resultDiv);
      }
      if (notifyFlg) {
        alert('既に読み取りセンサー(ID Information)[Notify]は開始しています。');
      } else {
        idCharacteristic.addEventListener('characteristicvaluechanged', handler);
        await idCharacteristic.startNotifications().catch(error => {
          idCharacteristic.removeEventListener('characteristicvaluechanged', handler);
          console.error(error)
        });
        notifyFlg = true;
      }
      console.log('[End] ID Information#startNotifications');
    },
    async() => {
      console.log('[Start] ID Information#stopNotifications');
      if (notifyFlg) {
        await idCharacteristic.stopNotifications();
        idCharacteristic.removeEventListener('characteristicvaluechanged', handler)
        notifyFlg = false;
      } else {
        alert('読み取りセンサー(ID Information)[Notify]は開始していません。');
      }
      console.log('[End] ID Information#stopNotifications');
    },
  ]
}

const idDescriptorConvert = value => {
  const kind = value.getUint8(0);
  switch (kind) {
    case 1:
      return [kind, value.getUint16(1, true), value.getUint16(3, true), value.getUint16(5, true), value.getUint16(7, true), value.getUint16(9, true), value.getUint16(11, true)];
    case 2:
      return [kind, value.getUint32(1, true), value.getUint16(5, true)];
    default:
      return [kind];
  }
}

const getSensorCharacteristic = (service, div) => {
  let sensorCharacteristic = null;
  return async() => {
    console.log('[Start] Sensor Information#getCharacteristic');
    if (sensorCharacteristic) {
      alert('既にモーションセンサー(Sensor Information)は実行済です。');
    } else {
      sensorCharacteristic = await service.getCharacteristic('10b20106-5b3b-4571-9508-cf3efcd7bbae');
      // Readプロパティ用エリアの構築
      const readDiv = createElement('div', null, 'property');
      const readButton = createElement('button', 'Read');
      readButton.addEventListener('click', getSensorRead(sensorCharacteristic, readDiv));
      readDiv.appendChild(readButton);
      div.appendChild(readDiv);
      // Notifyプロパティ用エリアの構築
      const notifyDiv = createElement('div', null, 'property');
      const notifyStartButton = createElement('button', 'Notify(Start)');
      const notifyEndButton = createElement('button', 'Notify(end)');
      const [notifyStartEvent, notifyEndEvent] = getSensorNotify(sensorCharacteristic, notifyDiv)
      notifyStartButton.addEventListener('click', notifyStartEvent);
      notifyEndButton.addEventListener('click', notifyEndEvent);
      notifyDiv.appendChild(notifyStartButton);
      notifyDiv.appendChild(notifyEndButton);
      div.appendChild(notifyDiv);
    }
    console.log('[End] Sensor Information#getCharacteristic');
  }
}

const getSensorRead = (sensorCharacteristic, div) => {
  let resultDiv = null;
  return async() => {
    console.log('[Start] Sensor Information#readValue');
    if (!resultDiv) {
      resultDiv = createElement('div', null, 'log');
      div.appendChild(resultDiv);
    }
    const value = await sensorCharacteristic.readValue();
    const result = sensorDescriptorConvert(value);
    resultDiv.appendChild(createElement('div', JSON.stringify(result)));
    resultDiv.scrollTop = resultDiv.scrollHeight;
    console.log('[End] Sensor Information#readValue');
  }
}

const getSensorNotify = (sensorCharacteristic, div) => {
  let resultDiv = null;
  let notifyFlg = false;
  const handler = event => {
    const value = event.target.value
    const result = sensorDescriptorConvert(value);
    resultDiv.appendChild(createElement('div', JSON.stringify(result)));
    resultDiv.scrollTop = resultDiv.scrollHeight;
  };
  return [
    async() => {
      console.log('[Start] Sensor Information#startNotifications');
      if (!resultDiv) {
        resultDiv = createElement('div', null, 'log');
        div.appendChild(resultDiv);
      }
      if (notifyFlg) {
        alert('既にモーションセンサー(Sensor Information)[Notify]は開始しています。');
      } else {
        sensorCharacteristic.addEventListener('characteristicvaluechanged', handler);
        await sensorCharacteristic.startNotifications().catch(error => {
          sensorCharacteristic.removeEventListener('characteristicvaluechanged', handler);
          console.error(error)
        });
        notifyFlg = true;
      }
      console.log('[End] Sensor Information#startNotifications');
    },
    async() => {
      console.log('[Start] Sensor Information#stopNotifications');
      if (notifyFlg) {
        await sensorCharacteristic.stopNotifications();
        sensorCharacteristic.removeEventListener('characteristicvaluechanged', handler)
        notifyFlg = false;
      } else {
        alert('モーションセンサー(Sensor Information)[Notify]は開始していません。');
      }
      console.log('[End] Sensor Information#stopNotifications');
    },
  ]
}

const sensorDescriptorConvert = value => {
  return [value.getUint8(0), value.getUint8(1), value.getUint8(2), value.getUint8(3), value.getUint8(4)];
}

const getButtonCharacteristic = (service, div) => {
  let buttonCharacteristic = null;
  return async() => {
    console.log('[Start] Button Information#getCharacteristic');
    if (buttonCharacteristic) {
      alert('既にボタン(Button Information)は実行済です。');
    } else {
      buttonCharacteristic = await service.getCharacteristic('10b20107-5b3b-4571-9508-cf3efcd7bbae');
      // Readプロパティ用エリアの構築
      const readDiv = createElement('div', null, 'property');
      const readButton = createElement('button', 'Read');
      readButton.addEventListener('click', getButtonRead(buttonCharacteristic, readDiv));
      readDiv.appendChild(readButton);
      div.appendChild(readDiv);
      // Notifyプロパティ用エリアの構築
      const notifyDiv = createElement('div', null, 'property');
      const notifyStartButton = createElement('button', 'Notify(Start)');
      const notifyEndButton = createElement('button', 'Notify(end)');
      const [notifyStartEvent, notifyEndEvent] = getButtonNotify(buttonCharacteristic, notifyDiv)
      notifyStartButton.addEventListener('click', notifyStartEvent);
      notifyEndButton.addEventListener('click', notifyEndEvent);
      notifyDiv.appendChild(notifyStartButton);
      notifyDiv.appendChild(notifyEndButton);
      div.appendChild(notifyDiv);
    }
    console.log('[End] Button Information#getCharacteristic');
  }
}

const getButtonRead = (buttonCharacteristic, div) => {
  let resultDiv = null;
  return async() => {
    console.log('[Start] Button Information#readValue');
    if (!resultDiv) {
      resultDiv = createElement('div', null, 'log');
      div.appendChild(resultDiv);
    }
    const value = await buttonCharacteristic.readValue();
    const result = buttonDescriptorConvert(value);
    resultDiv.appendChild(createElement('div', JSON.stringify(result)));
    resultDiv.scrollTop = resultDiv.scrollHeight;
    console.log('[End] Button Information#readValue');
  }
}

const getButtonNotify = (buttonCharacteristic, div) => {
  let resultDiv = null;
  let notifyFlg = false;
  const handler = event => {
    const value = event.target.value
    const result = buttonDescriptorConvert(value);
    resultDiv.appendChild(createElement('div', JSON.stringify(result)));
    resultDiv.scrollTop = resultDiv.scrollHeight;
  };
  return [
    async() => {
      console.log('[Start] Button Information#startNotifications');
      if (!resultDiv) {
        resultDiv = createElement('div', null, 'log');
        div.appendChild(resultDiv);
      }
      if (notifyFlg) {
        alert('既にボタン(Button Information)[Notify]は開始しています。');
      } else {
        buttonCharacteristic.addEventListener('characteristicvaluechanged', handler);
        await buttonCharacteristic.startNotifications().catch(error => {
          buttonCharacteristic.removeEventListener('characteristicvaluechanged', handler);
          console.error(error)
        });
        notifyFlg = true;
      }
      console.log('[End] Button Information#startNotifications');
    },
    async() => {
      console.log('[Start] Button Information#stopNotifications');
      if (notifyFlg) {
        await buttonCharacteristic.stopNotifications();
        buttonCharacteristic.removeEventListener('characteristicvaluechanged', handler)
        notifyFlg = false;
      } else {
        alert('ボタン(Button Information)[Notify]は開始していません。');
      }
      console.log('[End] Button Information#stopNotifications');
    },
  ]
}

const buttonDescriptorConvert = value => {
  return [value.getUint8(0), value.getUint8(1)];
}

const getBatteryCharacteristic = (service, div) => {
  let batteryCharacteristic = null;
  return async() => {
    console.log('[Start] Battery Information#getCharacteristic');
    if (batteryCharacteristic) {
      alert('既にバッテリー(Battery Information)は実行済です。');
    } else {
      batteryCharacteristic = await service.getCharacteristic('10b20108-5b3b-4571-9508-cf3efcd7bbae');
      // Readプロパティ用エリアの構築
      const readDiv = createElement('div', null, 'property');
      const readButton = createElement('button', 'Read');
      readButton.addEventListener('click', getBatteryRead(batteryCharacteristic, readDiv));
      readDiv.appendChild(readButton);
      div.appendChild(readDiv);
      // Notifyプロパティ用エリアの構築
      const notifyDiv = createElement('div', null, 'property');
      const notifyStartButton = createElement('button', 'Notify(Start)');
      const notifyEndButton = createElement('button', 'Notify(end)');
      const [notifyStartEvent, notifyEndEvent] = getBatteryNotify(batteryCharacteristic, notifyDiv)
      notifyStartButton.addEventListener('click', notifyStartEvent);
      notifyEndButton.addEventListener('click', notifyEndEvent);
      notifyDiv.appendChild(notifyStartButton);
      notifyDiv.appendChild(notifyEndButton);
      div.appendChild(notifyDiv);
    }
    console.log('[End] Battery Information#getCharacteristic');
  }
}

const getBatteryRead = (batteryCharacteristic, div) => {
  let resultDiv = null;
  return async() => {
    console.log('[Start] Battery Information#readValue');
    if (!resultDiv) {
      resultDiv = createElement('div', null, 'log');
      div.appendChild(resultDiv);
    }
    const value = await batteryCharacteristic.readValue();
    const result = batteryDescriptorConvert(value);
    resultDiv.appendChild(createElement('div', JSON.stringify(result)));
    resultDiv.scrollTop = resultDiv.scrollHeight;
    console.log('[End] Battery Information#readValue');
  }
}

const getBatteryNotify = (batteryCharacteristic, div) => {
  let resultDiv = null;
  let notifyFlg = false;
  const handler = event => {
    const value = event.target.value
    const result = batteryDescriptorConvert(value);
    resultDiv.appendChild(createElement('div', JSON.stringify(result)));
    resultDiv.scrollTop = resultDiv.scrollHeight;
  };
  return [
    async() => {
      console.log('[Start] Battery Information#startNotifications');
      if (!resultDiv) {
        resultDiv = createElement('div', null, 'log');
        div.appendChild(resultDiv);
      }
      if (notifyFlg) {
        alert('既にバッテリー(Battery Information)[Notify]は開始しています。');
      } else {
        batteryCharacteristic.addEventListener('characteristicvaluechanged', handler);
        await batteryCharacteristic.startNotifications().catch(error => {
          batteryCharacteristic.removeEventListener('characteristicvaluechanged', handler);
          console.error(error)
        });
        notifyFlg = true;
      }
      console.log('[End] Battery Information#startNotifications');
    },
    async() => {
      console.log('[Start] Battery Information#stopNotifications');
      if (notifyFlg) {
        await batteryCharacteristic.stopNotifications();
        batteryCharacteristic.removeEventListener('characteristicvaluechanged', handler)
        notifyFlg = false;
      } else {
        alert('バッテリー(Battery Information)[Notify]は開始していません。');
      }
      console.log('[End] Battery Information#stopNotifications');
    },
  ]
}

const batteryDescriptorConvert = value => {
  return [value.getUint8(0)];
}

const getMotorCharacteristic = (service, div) => {
  let motorCharacteristic = null;
  return async() => {
    console.log('[Start] Motor Control#getCharacteristic');
    if (motorCharacteristic) {
      alert('既にモーター(Motor Control)は実行済です。');
    } else {
      motorCharacteristic = await service.getCharacteristic('10b20102-5b3b-4571-9508-cf3efcd7bbae');
      // Write without responseプロパティ用エリアの構築
      const writeDiv = createElement('div', null, 'property');
      const writeText = createTextElement('書き込みデータをカンマ区切りで入力してください');
      const writeButton = createElement('button', 'Write without response（モーター制御）');
      writeButton.addEventListener('click', getMotorWrite(motorCharacteristic, writeDiv, writeText));
      writeDiv.appendChild(writeText);
      writeDiv.appendChild(createElement('br'));
      writeDiv.appendChild(writeButton);
      div.appendChild(writeDiv);
      // Readプロパティ用エリアの構築
      const readDiv = createElement('div', null, 'property');
      const readButton = createElement('button', 'Read');
      readButton.addEventListener('click', getMotorRead(motorCharacteristic, readDiv));
      readDiv.appendChild(readButton);
      div.appendChild(readDiv);
      // Notifyプロパティ用エリアの構築
      const notifyDiv = createElement('div', null, 'property');
      const notifyStartButton = createElement('button', 'Notify(Start)');
      const notifyEndButton = createElement('button', 'Notify(end)');
      const [notifyStartEvent, notifyEndEvent] = getMotorNotify(motorCharacteristic, notifyDiv)
      notifyStartButton.addEventListener('click', notifyStartEvent);
      notifyEndButton.addEventListener('click', notifyEndEvent);
      notifyDiv.appendChild(notifyStartButton);
      notifyDiv.appendChild(notifyEndButton);
      div.appendChild(notifyDiv);
    }
    console.log('[End] Motor Control#getCharacteristic');
  }
}

const getMotorWrite = (motorCharacteristic, div, text) => {
  return async() => {
    console.log('[Start] Motor Control#writeValue');
    const array = text.value.split(',').map( v => parseInt(v) );
    await motorCharacteristic.writeValue(motorWriteDescriptorConvert(array));
    console.log('[End] Motor Control#writeValue');
  }
}

const motorWriteDescriptorConvert = array => {
  let buffer, dataview;
  switch (array[0]) {
    case 1:
      buffer = new ArrayBuffer(7);
      dataview = new DataView(buffer);
      dataview.setUint8(0, array[0]);
      dataview.setUint8(1, array[1]);
      dataview.setUint8(2, array[2]);
      dataview.setUint8(3, array[3]);
      dataview.setUint8(4, array[4]);
      dataview.setUint8(5, array[5]);
      dataview.setUint8(6, array[6]);
      return buffer;
    case 2:
      buffer = new ArrayBuffer(8);
      dataview = new DataView(buffer);
      dataview.setUint8(0, array[0]);
      dataview.setUint8(1, array[1]);
      dataview.setUint8(2, array[2]);
      dataview.setUint8(3, array[3]);
      dataview.setUint8(4, array[4]);
      dataview.setUint8(5, array[5]);
      dataview.setUint8(6, array[6]);
      dataview.setUint8(7, array[7]);
      return buffer;
    case 3:
      buffer = new ArrayBuffer(13);
      dataview = new DataView(buffer);
      dataview.setUint8(0, array[0]);
      dataview.setUint8(1, array[1]);
      dataview.setUint8(2, array[2]);
      dataview.setUint8(3, array[3]);
      dataview.setUint8(4, array[4]);
      dataview.setUint8(5, array[5]);
      dataview.setUint8(6, array[6]);
      dataview.setUint16(7, array[7], true);
      dataview.setUint16(9, array[8], true);
      dataview.setUint16(11, array[9], true);
      return buffer;
    case 4:
      const pointNum = Math.floor((array.length -　8) / 3)
      buffer = new ArrayBuffer(13);
      dataview = new DataView(buffer);
      dataview.setUint8(0, array[0]);
      dataview.setUint8(1, array[1]);
      dataview.setUint8(2, array[2]);
      dataview.setUint8(3, array[3]);
      dataview.setUint8(4, array[4]);
      dataview.setUint8(5, array[5]);
      dataview.setUint8(6, array[6]);
      dataview.setUint8(7, array[7]);
      for (let i = 0; i < pointNum; i++) {
        dataview.setUint16(8 + 6 * i, array[8 + 3 * i], true);
        dataview.setUint16(10 + 6 * i, array[9 + 3 * i], true);
        dataview.setUint16(12 + 6 * i, array[10 + 3 * i], true);
      }
      return buffer;
    case 5:
      buffer = new ArrayBuffer(9);
      dataview = new DataView(buffer);
      dataview.setUint8(0, array[0]);
      dataview.setUint8(1, array[1]);
      dataview.setUint8(2, array[2]);
      dataview.setUint8(3, array[3]);
      dataview.setUint8(4, array[4]);
      dataview.setUint8(5, array[5]);
      dataview.setUint8(6, array[6]);
      dataview.setUint8(7, array[7]);
      dataview.setUint8(8, array[8]);
      return buffer;
    default:
      console.error(`${array[0]} is invalid code`)
      return null;
  }
}

const getMotorRead = (motorCharacteristic, div) => {
  let resultDiv = null;
  return async() => {
    console.log('[Start] Motor Control#readValue');
    if (!resultDiv) {
      resultDiv = createElement('div', null, 'log');
      div.appendChild(resultDiv);
    }
    const value = await motorCharacteristic.readValue();
    const result = motorReadDescriptorConvert(value);
    resultDiv.appendChild(createElement('div', JSON.stringify(result)));
    resultDiv.scrollTop = resultDiv.scrollHeight;
    console.log('[End] Motor Control#readValue');
  }
}

const getMotorNotify = (motorCharacteristic, div) => {
  let resultDiv = null;
  let notifyFlg = false;
  const handler = event => {
    const value = event.target.value
    const result = motorReadDescriptorConvert(value);
    resultDiv.appendChild(createElement('div', JSON.stringify(result)));
    resultDiv.scrollTop = resultDiv.scrollHeight;
  };
  return [
    async() => {
      console.log('[Start] Motor Control#startNotifications');
      if (!resultDiv) {
        resultDiv = createElement('div', null, 'log');
        div.appendChild(resultDiv);
      }
      if (notifyFlg) {
        alert('既にモーター(Motor Control)[Notify]は開始しています。');
      } else {
        motorCharacteristic.addEventListener('characteristicvaluechanged', handler);
        await motorCharacteristic.startNotifications().catch(error => {
          motorCharacteristic.removeEventListener('characteristicvaluechanged', handler);
          console.error(error)
        });
        notifyFlg = true;
      }
      console.log('[End] Motor Control#startNotifications');
    },
    async() => {
      console.log('[Start] Motor Control#stopNotifications');
      if (notifyFlg) {
        await motorCharacteristic.stopNotifications();
        motorCharacteristic.removeEventListener('characteristicvaluechanged', handler)
        notifyFlg = false;
      } else {
        alert('モーター(Motor Control)[Notify]は開始していません。');
      }
      console.log('[End] Motor Control#stopNotifications');
    },
  ]
}

const motorReadDescriptorConvert = value => {
  return [value.getUint8(0), value.getUint8(1), value.getUint8(2)];
}

document.getElementById('connect').addEventListener('click', connectEvent);

const createElement = (tagName, text, className) => {
  const elem = document.createElement(tagName);
  if (text) {
    elem.textContent = text;
  }
  if (className) {
    elem.classList.add(className);
  }
  return elem;
}

const createTextElement = (placeholder, value) => {
  const elem = document.createElement('input');
  elem.setAttribute('type', 'text');
  elem.setAttribute('placeholder', placeholder);
  if (value != null) {
    elem.setAttribute('value', value);
  }
  return elem;
}

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

# <font color="#ddd">その他</font>

<font color="#ddd">すべての機能の確認ができていないし、一部検証不十分ではあるが、キャンペーンのため、まずは投稿する</font>