GoTo 仮想トラベル

2020年、世界はコロナ渦の真っ只中です。
家にいる時間が増えました。そして運動不足になりがちです。

というわけで、今回はtoioコアキューブを使った運動不足解消ネタです。

## 要約

toioコアキューブの[シェイク検出](https://toio.github.io/toio-spec/docs/ble_sensor#%E3%82%B7%E3%82%A7%E3%82%A4%E3%82%AF%E6%A4%9C%E5%87%BA)機能でgoogleマップ（ストリートビュー）を移動します。

## やりたいこと

toioコアキューブを2個使います。
一つずつ手に握って腕を振り、左右の振りの早さで進む速度や向きをコントロールしたいと思います。
腕の振りを鍛えて世界一周しましょう。

## 実現方法

具体的には、2個のキューブでシェイク検出を行い、

* 左右を振っていれば前進
* 片方だけなら、振っている腕のほうを向く

という動きにします。

ストリートビューは別途ブラウザで開いておいて、操作は上記のキューブの動きをキー入力に変換して伝えます。
キューブの動きをキー入力に変換するのは[去年のアドベントカレンダーでもやった](https://qiita.com/kaz399/items/8b41ae811c26a3c8bac8)ので、コードは使い回しです。
というわけで前回同様に実装言語はrust、動作環境はWindows10です。


## 今回のおはなしのながれ

というわけで、今回は成果物の新規性が低く若干（かなり？）ショボいので、実装の流れを書いていきたいと思います。

## 実装の記録

ここからは実装の記録です。
プログラムを実行する前準備として、toioコアキューブを二個[Windowsのbluetoothデバイスとして登録](https://support.microsoft.com/ja-jp/windows/windows-%E3%81%A7-bluetooth-%E3%83%87%E3%83%90%E3%82%A4%E3%82%B9%E3%82%92%E3%83%9A%E3%82%A2%E3%83%AA%E3%83%B3%E3%82%B0%E3%81%99%E3%82%8B-2be7b51f-6ae9-b757-a3b9-95ee40c3e242)しておいてください。
このプログラムはWindowsのbluetooth登録情報を参照して接続対象となるキューブを探します。

### スタート

[去年のパワポコントローラー](https://qiita.com/kaz399/items/8b41ae811c26a3c8bac8)を改造します。コードは[これ](https://github.com/kaz399/rs_toio_cube)です。

このパワポコントローラーは、発表後にbleのライブラリをマイクロソフト製のwinrt-rs(v0.7)に変更したのですが、そのへんの話は今回の記事の主旨ではないので軽く省略します。

コードを取得してきたらブランチを作って作業します。ブランチ名は安易に`goto_vtravel`です。
そして、`examples`ディレクトリを作って丸コピーします。
ファイル名は`goto_vtravel.rs`にします。


```bash
git clone https://github.com/kaz399/rs_toio_cube

cd rs_toio_cube
git switch -c goto_vtravel
mkdir exsamples
cp src/main.rs exsamples/goto_vtravel.rs
```

### とりあえず二個認識させてみよう

まずは二個のキューブを認識させるところから始めます。
実は下回りの実装をあまり覚えていないので、何も考えずに接続処理を追加してみます。

コードは[これ](https://github.com/kaz399/rs_toio_cube/commit/68bdc0f3c8e1eaf86688ead121b43c0a37de98c5)です。
うまくいけば、二個目のキューブのLEDが青色に点灯するはずです。
実行しましょう。


```
cargo run --example goto_vtravel
    Finished dev [unoptimized + debuginfo] target(s) in 0.07s
     Running `target\debug\examples\goto_vtravel.exe`
Page key mode
search registered cubes
success to connect
battery level 100%
search registered cubes
thread 'main' panicked at 'error: read: Error { code: 0x8000000B, message: "有効な範囲外のデータにアクセスしようとしました" }', core_cube\src\win10.rs:328:14
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\examples\goto_vtravel.exe` (exit code: 101)
```

ああ、だめですね。適当すぎたようです。

二つ目のキューブの接続チェックの際に、すでに接続済みの一つ目のキューブに接続してreadを試みて失敗しているようです。  
下回りにも修正を入れます。
接続済みだった場合は`OK(false)`という成功なのか失敗なのかよくわからない値を返してread処理まで進まないようにします。
接続状態を検出するコードはこんな感じです。全体としては[こう](https://github.com/kaz399/rs_toio_cube/commit/96c5d315b5d1906b3495fe9dcd9a0b57eb4f9e0a)なります。

```rust:core_cube/src/win10.rs
        let connection_status = ble_device.connection_status().unwrap();
        debug!("Connection Status: {:?}", connection_status);
        if connection_status == BluetoothConnectionStatus::Connected {
            return Ok(false);
        }
```

そしてふたたび実行してみます。

```
cargo run --example goto_vtravel
    Finished dev [unoptimized + debuginfo] target(s) in 0.07s
     Running `target\debug\examples\goto_vtravel.exe`
Page key mode
search registered cubes
success to connect
battery level 100%
search registered cubes
success to connect
battery level 100%
```

今度は両方のキューブに接続できました。
二つのキューブのLEDが無事に緑と青に点灯しました。
ちなみに右手用が青、左手用が緑になります。

### 二個のキューブのセンサーの値を取得してみよう

無事に二個接続できたので、次はセンサー値の取得です。
[こんな感じ](https://github.com/kaz399/rs_toio_cube/commit/01f439a51b6500ba61e303e13c67b5b8e0fa988c)で両方のキューブに同じNotifyハンドラを登録してみます。

デバッグ情報を有効にして実行すると。。

```
export RUST_LOG=debug
cargo run --example goto_vtravel

（中略）

[2020-12-14T13:10:54Z DEBUG goto_vtravel] sensor [SensorInfo { time: Instant { t: 9158.6269347s }, slope: Aslant, collision: NotDetect, double_tap: NotDetect, posture: Normal }, SensorInfo { time: Instant { t: 9158.6790044s }, slope: Aslant, collision: NotDetect, double_tap: NotDetect, posture: LeftSideUp }]
[2020-12-14T13:10:55Z DEBUG goto_vtravel] sensor information status changed [1, 1, 1, 0, 1, 0]
[2020-12-14T13:10:55Z DEBUG goto_vtravel] sensor [SensorInfo { time: Instant { t: 9159.7795435s }, slope: Horizontal, collision: Detect, double_tap: NotDetect, posture: Normal }]
```

うーん。これではどちらのキューブのセンサー情報なのか区別できません。

クロージャを使ってハンドラ関数を生成すればいい感じになるのかも？という気がしましたが、やり方がよくわからなかったのでハンドラ関数を二つ作るというダサい手法でしのぎます。
今回の目標はとにかく運動不足の解消です。知力不足の解消は別の機会になんとかします。

[二つのキューブに別々のハンドラを登録するよう変更](https://github.com/kaz399/rs_toio_cube/commit/78a7a40638bf1f400320f046e06a9d5bf76f4cbe)しました。シェイク検出の定義も追加してあります。
実行結果はこんな感じです。

```
[2020-12-16T01:54:09Z DEBUG goto_vtravel] sensor(cube1) information status changed [1, 0, 0, 0, 3, 1]
[2020-12-16T01:54:09Z DEBUG goto_vtravel] cube1:sensor [SensorInfo { time: Instant { t: 55169.0215776s }, slope: Aslant, collision: NotDetect, double_tap: NotDetect, posture: Downward, shaking: 1 }]
[2020-12-16T01:54:11Z DEBUG goto_vtravel] sensor(cube2) information status changed [1, 0, 0, 0, 1, 2]
[2020-12-16T01:54:11Z DEBUG goto_vtravel] cube2:sensor [SensorInfo { time: Instant { t: 55171.1816288s }, slope: Aslant, collision: NotDetect, double_tap: NotDetect, posture: Normal, shaking: 2 }]
```

無事に二個のセンサー情報を別々に取得できるようになりました。



### キーイベントを発生させよう

ブラウザのgoogleマップ（ストリートビュー）は下記のキーボード操作が行えます。


| キー | 動き |
|---|---|
| <kbd>↑</kbd> または <kbd>W</kbd> | 前進 |
| <kbd>←</kbd> または <kbd>A</kbd> | 左を向く |
| <kbd>→</kbd> または <kbd>D</kbd> | 右を向く |
| <kbd>↓</kbd> または <kbd>S</kbd> | バック |


二個のキューブのシェイク検出値をもとにキーイベントを生成します。
昨年の経験から、[enigo 0.0.13](https://crates.io/crates/enigo/0.0.13)ではNumLockを有効状態にしないとカーソルキーイベントが生成できないことがわかっているので今回は無難にWASD操作にします。
enigoには新しいバージョンの0.0.14がありますが、こちらは試していないのでカーソルキーが普通に生成できるのかどうかは不明です。

キューブのシェイク検出値とキーイベント発生の関係ですが、こういうのは実際に操作しながら調整してみないと、いわゆる「いい感じ」かどうかわからないので、まずは適当に実装して様子を見てみたいと思います。


腕の振りの早さに応じてマップの移動スピードを可変させたいです。
シェイク検出値を積算して一定値を超えたらキーイベントを発行させる方法を試してみます。

[おためし実装はこれ](https://github.com/kaz399/rs_toio_cube/commit/368e05f3b6fe0725eda7b1985c02a400e9d39267)です。キーイベント処理を追加して、パワポコントローラー用の不要な部分はバッサリ削除しました。


googleマップを操作するための手順は

1. コンソールでgoto_vtravelを実行させる（ログレベルはinfoにしておきます）  
1. ログを見てキューブが二個とも接続できたことを確認
1. ブラウザを起動してgoogleマップにアクセスし、ストリートビューモードにする（人形を道路にドラッグ＆ドロップ）
1. ウィンドウのフォーカスはブラウザで放置
1. キューブを握ってひたすら腕を振る

です。


実行コマンドは下記です。

```
export RUST_LOG=info
cargo run --example goto_vtravel
```


試したところ、前進はまあまあ普通だけど**左右移動が少なすぎて方向転換が超辛い**ということがわかりました。
これでは180度ターンするだけで酸欠で気絶しそうです。


### 操作感を調整しよう

ストリートビューは左右キーを押している間はずっと方向を変える作りなので、方向転換時のキーオフのタイミングを遅らせることにより変化量を増やします。
キーイベント発行処理は100ms周期でループしているので、方向転換時は3周期（300ms）の間キーをオンにするようにします。

あと試してみてわかったのですが、腕は左右交互に振るので左右「同時」ではなく「交互」にシェイク検出値が得られます。
両方の腕を交互に振り続けていることを判断するために、直近のシェイク検出イベントから600ms以内であれば有効値とみなしています。
そのうえで、(右のシェイク検出レベル)×(左のシェイク検出レベル)が0でない間は両腕が振られていると判断しています。

修正したものは[これ](https://github.com/kaz399/rs_toio_cube/commit/6f859c9658c67426d88320721be1fc971c558cca)です。

もし振っている腕のほうを向くという操作性に違和感があり、逆のほうがいいと思う場合には左右のキューブを持ち替えてください。


## 完成

プログラムができたので実際に使ってみたいと思います。

### 皇居一周にチャレンジ

手始めに皇居を一周してみたいと思います。
雰囲気を出すために腕だけではなく、その場駆け足もします。


大手町あたりから代官町まで走りましたが、かなり疲れます。
いまハアハア言いながら書いています。
運動不足の自分には皇居一周は結構厳しいです。
しかも、部屋でバタバタやっていたら愛犬に吠えられてしまいました。残念です。


## おわりに

やっぱり外で運動するのが一番です。
早くコロナが収束し、また心置きなく外で遊び回れる日が来ますように。