2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

はじめに

「toio」ではじめよう、おうちでロボット開発キャンペーン に応募しました。

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

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プログラミング ではtoio.jsをブラウザ用にwebpackでビルドしたスクリプトを公開してくれていますので、非常に簡単に使えます。

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

参考ドキュメント

チートシート

キューブの発見

toio技術仕様

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

JavaScript Snippet

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

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

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

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

toio技術仕様

先頭データ Descriptorの種類 印刷されているモノの例 取得できる情報
0x01 Position ID トイオ・コレクションに付属のプレイマット
toio コア キューブ(単体)に付属の簡易プレイマット
キューブの位置と角度
0x02 Standard ID トイオ・コレクションに付属の各種カード・シート
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 独自に定義されているもの。
座標値一覧 参照
3 UInt16 キューブの中心の Y 座標値 0x017fd(383) toio ID 独自に定義されているもの。
座標値一覧 参照
5 UInt16 キューブの角度 0x0132(306 度) X 軸方向が 0 度で時計回りが正となる値。 値の範囲は 0 度から 360 度。
7 UInt16 読み取りセンサーの X 座標値 0x02bc(700) toio ID 独自に定義されているもの。
座標値一覧 参照
9 UInt16 読み取りセンサーの Y 座標値 0x0182(386) toio ID 独自に定義されているもの。
座標値一覧 参照
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
(3670016: タイフーン)
キューブの位置に寄らないユニークな値。
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変換関数

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

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

Notify

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

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(正位置) 水平面に対する姿勢が変化したときに値が変わる。
姿勢検出の値一覧 参照

姿勢検出の値一覧

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

JavaScript Snippet

Descriptor変換関数

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

Read

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

Notify

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

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変換関数

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

Read

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

Notify

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

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

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

toio技術仕様

Descriptor

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

JavaScript Snippet

Descriptor変換関数

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

Read

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

Notify

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

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

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

toio技術仕様

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

Descriptor(モーター制御)

データ位置 タイプ 内容 備考
0 UInt8 制御の種類 0x01(モーター制御) 固定
1 UInt8 制御するモーターの ID 0x01(左) 左のモーターの ID が1で右のモーターの ID が2
データ位置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
データ位置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
データ位置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
データ位置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)

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

await motorCharacteristic.writeValue(motorWriteDescriptorConvert(array));

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

toio技術仕様

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

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

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

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

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

JavaScript Snippet

Descriptor変換関数

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

Read

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

Notify

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>
  <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>

その他

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

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?