【toio 2019】 toio.js で傾き検知と姿勢検出（2019/12/17）

この記事は 「[toio™(ロボットトイ | toio（トイオ）) Advent Calendar 2019](https://qiita.com/advent-calendar/2019/toiotomo)」の 17日目の記事です。

## はじめに

toio の 「toio バリューパック」・「トイオ・コレクション 拡張パック」と「工作生物 ゲズンロイド」を持っています [@youtoy](https://twitter.com/youtoy) です。

また、toio関連では、以下のユーザ会を 3人で共同主催しており、2度ほど LT会を開催したりしました。

▼ toio™で作ってみた！友の会（非公式）
　[⇒ 【Facebookグループ】 ](https://www.facebook.com/groups/toiotomo/)
　[⇒ 【connpassのグループ】 ](https://toiotomo.connpass.com/)

## この記事で試す内容について

以前、[toio.js](https://github.com/toio/toio.js/) を使って、toio と他デバイス（Amazon Echo や micro:bit）と連携させる記事を書いたことがあります。
　[● #toio を Alexa や micro:bit から操作する 〜概要編〜（JavaScriptライブラリを使ってみた） - Qiita]( https://qiita.com/youtoy/items/0ee56f5395e3f4c72c51 )
このときは、[キーボード入力で toio を動かすサンプル](https://github.com/toio/toio.js/tree/master/examples/keyboard-control)をベースに、作品を作っていました。

それから時間が経過し、[toio の仕様のページ]( https://toio.github.io/toio-spec/)や、[公式のニュース（タイトル： システムソフトウェアアップデート2019 実施）](https://toio.io/news/2019/20191112-3.html)にも書かれているように、アップデートが行われました。
今回の記事では、アップデートにより追加された内容を試すことにします。

それでは、何を試してみようかと、アップデートされた内容を見ていきたいと思います。
[仕様の「アップデートについて」というページ](https://toio.github.io/toio-spec/docs/how_to_update_cube)の下部を見てみると、以下のような記載があります（2019/12/16時点）。
> BLE プロトコルバージョン v2.1.0 では以下のような内容の変更・追加があります。
>
- ダブルタップ検出・姿勢検出の追加
- モーターの速度指示値の有効範囲の拡大
    - 速度指示値の最大値が100から115になり、より速い動作が可能になります
    - 速度指示値の最小値が10から8になり、よりゆっくりとした動作が可能になります
- モーター制御のバリエーションの追加
    - 目標指定付きモーター制御
    - 複数目標指定付きモーター制御
    - 加速度指定付きモーター制御

一番上の「ダブルタップ検出・姿勢検出の追加」あたりを試してみるのが、見た目にも分かりやすそうです。
上記の「ダブルタップ検出・姿勢検出の追加」の部分については、[仕様の「モーションセンサー」](https://toio.github.io/toio-spec/docs/ble_sensor#%E3%83%80%E3%83%96%E3%83%AB%E3%82%BF%E3%83%83%E3%83%97%E6%A4%9C%E5%87%BA)に仕様詳細が記載されていました。以下は、2019/12/16時点でページの一部をキャプチャしたものです。
<img width="654" alt="モーションセンサー_·_toio™コア_キューブ_技術仕様.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/1b954ae1-0902-f7f5-10f2-d67589b1f07f.jpeg">

今回は、ダブルタップ検出・姿勢検出の2つあるうちの「姿勢検出」を試していこうと思います。

## toio.js でモーションセンサーを使う

### 現状のモーションセンサー周りを見てみる

姿勢検出を試していく前に、まずは、現状の toio.js のモーションセンサー周りの処理で実装されている内容を見てみます。

API の情報をたどっていくと、「Globals @toio/cube Cube」の「Class Cube」の中に、[「getSlopeStatus」という部分](https://toio.github.io/toio.js/classes/_toio_cube.cube.html#getslopestatus)があります。どうやら、傾きをセンサーで検知するもののようです。

この部分には、以下のような記載があります。
<img width="653" alt="Cube___toio_js.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/930fbbf5-8229-2b3e-d45d-3d864ed93e4b.jpeg">
そして、上記の「Defined in cube/src/cube.ts:225」の部分をクリックすると、以下のようなソースコードがあります。
<img width="551" alt="toio_js_cube_ts_at_2c121885f89bfbe3d9269e634755eb223a3020ed_·_toio_toio_js.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/cc1c1218-0404-370d-7829-4d22b83cb48a.jpeg">

さらに、[上記の「isSloped」をキーワードにリポジトリ内で検索してみます](https://github.com/toio/toio.js/search?q=isSloped&unscoped_q=isSloped)。
すると、検索結果の中に、以下のような内容がありました。
<img width="552" alt="Search_·_isSloped.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/8ebc024d-e093-fffd-3c16-5aee1fd93348.jpeg">

そして、リンク先のソースに以下のような箇所がありました。
<img width="468" alt="toio_js_sensor-spec_ts_at_004a77ce6304fc90705c323571ebb4ac55a93924_·_toio_toio_js.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/7be3771e-658b-c6e9-5771-5c385319ce04.jpeg">

「const isSloped = buffer.readUInt8(1) === 0」という部分や「const isCollisionDetected = buffer.readUInt8(2) === 1」という部分がありますが、上記の[仕様の「モーションセンサー」](https://toio.github.io/toio-spec/docs/ble_sensor#%E3%83%80%E3%83%96%E3%83%AB%E3%82%BF%E3%83%83%E3%83%97%E6%A4%9C%E5%87%BA)の部分、もっと具体的に言うと以下の画像の赤枠部分が関連しそうです。
<img width="654" alt="モーションセンサー_·_toio™コア_キューブ_技術仕様2.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/8a7635dd-b56d-3a87-01d5-f42d595a10fe.jpeg">

### 姿勢の検出

ということは、データ位置が 2 に該当する「const isSloped = buffer.readUInt8(1) === 0」のところを、試しに「const isSloped = buffer.readUInt8(4) === 【数値】」としてみれば、特定の姿勢を検出する処理に変わるように思われます。

姿勢の違いを表す数値については、先ほど出てきたページの中で、以下のように書かれています。
<img width="663" alt="モーションセンサー_·_toio™コア_キューブ_技術仕様.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/8a8a6e92-a772-33ef-d31f-a9cfd04a94ca.jpeg">
この記事では、「底面が上（値 2）」と「正面が上（値 4）」の 2つを試してみることにします。

#### 姿勢検出を試す前のセットアップなど

最終的には姿勢検知を試していくのですが、その前に公式サンプルを動かしてみたり、上記の「getSlopeStatus」を使って傾きを検出するプログラムを試してみます。

##### 公式サンプルを動かしてみる

[GitHub の toio.js のページの「💻 Prerequisites」](https://github.com/toio/toio.js/#computer-prerequisites) を見て、事前のセットアップを行ってください。

そして、[GitHub の toio.js のページ](https://github.com/toio/toio.js/)に記載されている、以下の内容を試してみましょう。
> ▼ How to play sample application
>
  1. $ git clone https://github.com/toio/toio.js.git
  2. $ cd toio.js
  3. $ yarn install
  4. $ yarn build
  5. $ yarn example:keyboard-control

動かしてみたときの様子は以下のとおりです。
<blockquote class="twitter-tweet" data-conversation="none"><p lang="ja" dir="ltr">toio.js の公式サンプル、<a href="https://twitter.com/hashtag/toio?src=hash&amp;ref_src=twsrc%5Etfw">#toio</a> をキーボード操作するもの。 <a href="https://t.co/qVIs2VZm6h">pic.twitter.com/qVIs2VZm6h</a></p>&mdash; you (@youtoy) <a href="https://twitter.com/youtoy/status/1206349269080264704?ref_src=twsrc%5Etfw">December 15, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

今回の動画では、手順1 でクローンしてきたソースの中の「toio.js/examples/keyboard-control」の直下の「index.js」に少しだけ手を加えています。
具体的には、11行目〜17行目までの、スピードや移動距離に関わるパラメータを、以下のようにしました。

```javascript:index.js
const DURATION = 500 // ms
const SPEED = {
  forward: [50, 50],
  backward: [-50, -50],
  left: [15, 50],
  right: [50, 15],
}
```
また、上記の部分で少し書きかえた「toio.js/examples/keyboard-control」の直下の「index.js」を、以下のソースコードに置きかえて動作させてみたりもしました（※ [GitHub の toio.js のページ](https://github.com/toio/toio.js/)の冒頭のソースで、「cube.move(100, 100, 1000)」の部分を「cube.move(50, 50, 500)」にと、少しだけ変更したものです）。

```javascript:index.js
const { NearestScanner } = require('@toio/scanner')

async function main() {
  // start a scanner to find the nearest cube
  const cube = await new NearestScanner().start()

  // connect to the cube
  await cube.connect()

  // move cube
  cube.move(50, 50, 500)
  //         |    |     `--- duration [ms]
  //         |    `--------- right motor speed
  //          `------------- left motor speed
}

main()
```

書きかえた後のソースを動かすには``$ yarn example:keyboard-control``を実行する必要があります。これにより、変更後の内容が反映されました。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">久しぶりに toio.js を触った。<a href="https://twitter.com/hashtag/toio?src=hash&amp;ref_src=twsrc%5Etfw">#toio</a> <a href="https://t.co/np1DjhjbTS">pic.twitter.com/np1DjhjbTS</a></p>&mdash; you (@youtoy) <a href="https://twitter.com/youtoy/status/1206266010036133888?ref_src=twsrc%5Etfw">December 15, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

##### モーションセンサーの情報を使う

続いて「getSlopeStatus」を使って傾きを検出するプログラムを試してみます。
なお、普段あまりプログラムを書くことがないため、以下で出てくるプログラムで「もっと良い書き方があるよ」という場合や、「この書き方はまずいよ」という場合などは、コメント欄などで教えていただけると嬉しいです。

では、傾きを検出したか否か（true/false）の出力で、傾いていると検知された場合には「傾いてるぞ！」というテキストが表示されるプログラムを作ってみます。
上で出てきた2つ目のプログラムをベースに書きかえをしました。そして、判定処理は 500msec間隔で20回実行しています。判定処理のところは「getSlopeStatus」の「isSloped」が true か false を見る形です。

```javascript:index.js
const { NearestScanner } = require('@toio/scanner')

async function main() {
  let cubeStatus;
  const cube = await new NearestScanner().start()

  await cube.connect()
  for (let i = 0; i < 20; i++) {
    cubeStatus = await cube.getSlopeStatus();
    if(cubeStatus.isSloped) {
      console.log("傾いてるぞ！");
    } else {
      console.log("...");
    }
    await new Promise(r => setTimeout(r, 500));
  }
  console.log("状態チェック、終了。");
}

main()
```

実際に動作させてみると、以下の動画のようになりました。
toio が傾くと PCの画面に表示されるテキストが変わるのが、動画から分かるかと思います（ちなみに、傾いたかどうかの閾値はデフォルトのままで、その場合は 45度が閾値となるようです）。

<blockquote class="twitter-tweet" data-conversation="none"><p lang="ja" dir="ltr">toio.js を久しぶりに触っていた話の本命、<a href="https://twitter.com/hashtag/toio?src=hash&amp;ref_src=twsrc%5Etfw">#toio</a> のモーションセンサーで傾いたかどうかを検知。 <a href="https://t.co/SpM0LA4XUU">pic.twitter.com/SpM0LA4XUU</a></p>&mdash; you (@youtoy) <a href="https://twitter.com/youtoy/status/1206474276452413441?ref_src=twsrc%5Etfw">December 16, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

#### 姿勢検出を試してみる

それではいよいよ、姿勢検出へと進みます。
今回は短時間で試すために、（手抜きな感じの）楽な方法を採用します。本来はソースコードの追加や修正等をいろいろと行うべきところを、既存のコードの一部のみ（パラメータのみ）を書きかえることとします。
これだと、メソッド名と処理内容が一致しなくなってしまうのですが・・・（後で、公式でAPIが準備されるだろう、という見込んでの暫定対応ということで）。

具体的には「toio.js/packages/cube/src/characteristics/specs/sensor-spec.ts」の 31・32行目を以下のように書きかえました。

【書きかえ前】

```javascript
    const isSloped = buffer.readUInt8(1) === 0
    const isCollisionDetected = buffer.readUInt8(2) === 1
```

【上記の書きかえ後】

```javascript
    const isSloped = buffer.readUInt8(4) === 2
    const isCollisionDetected = buffer.readUInt8(4) === 4
```

これにより、以下のような動作をするようになるはずです。
　「isSloped はキューブの底面が上を向くと true」
　「isCollisionDetected はキューブの正面が上を向くと true」

上記の 2つの状態（底面が上/正面が上）が検出された場合に、それぞれに対応したテキストを PC画面上に表示するようにしてみました。

```javascript
const { NearestScanner } = require('@toio/scanner')

async function main() {
  let cubeStatus1, cubeStatus2;
  const cube = await new NearestScanner().start()

  await cube.connect()
  for (let i = 0; i < 20; i++) {
    cubeStatus1 = await cube.getSlopeStatus();
    cubeStatus2 = await cube.getCollisionStatus();
    if(cubeStatus1.isSloped) {
      console.log("逆さまになっています");
    } else if(cubeStatus2.isCollisionDetected) {
      console.log("上向き！");
    } else {
      console.log("...");
    }
    await new Promise(r => setTimeout(r, 500));
  }
  console.log("状態チェック、終了。");
}

main()
```

実際に動かしてみたのが以下の動画です。

<blockquote class="twitter-tweet" data-conversation="none"><p lang="ja" dir="ltr">toio.js を使った、<a href="https://twitter.com/hashtag/toio?src=hash&amp;ref_src=twsrc%5Etfw">#toio</a> のモーションセンサー利用の 2つ目、<br>この前のアップデートで対応されたばかりの姿勢検知。 <a href="https://t.co/84sNfs70uH">pic.twitter.com/84sNfs70uH</a></p>&mdash; you (@youtoy) <a href="https://twitter.com/youtoy/status/1206474623883153410?ref_src=twsrc%5Etfw">December 16, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

無事に動いていそうです。

## さいごに

今回、 toio.js で傾き検知と姿勢検出を試してみました。

本当は、動画でわかりやすいように「姿勢によって異なる音が鳴る・異なる光り方をする」とか、2台の toio を連携させるような内容をやりたかったのですが、時間の関係でここまでとなりました。
その辺りは、引き続き試していこうと思います！