はじめに
「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™コア キューブ 技術仕様
https://toio.github.io/toio-spec/ -
Bluetooth - Web APIs | MDN
https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth -
Web Bluetooth Draft Community Group Report
https://webbluetoothcg.github.io/web-bluetooth/ -
Web Bluetooth(Read/Write/Notifications)の使い方まとめ - Qiita
https://qiita.com/umi_kappa/items/45761c7454d5d4c10bf0
チートシート
キューブの発見
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>
その他
すべての機能の確認ができていないし、一部検証不十分ではあるが、キャンペーンのため、まずは投稿する