toio を Rust で制御するライブラリ


toio-rs

toio を Rust から動かすためのライブラリを作ったのでその紹介をします.

いきさつ

おばあちゃんから娘(当時2歳)に toio が届きましたが,遊び方がわからないで放置されていました.これではもったいないので代わりにお父さん(当時32歳)が遊ぶことにしました.Rust で制御してみようと思ったのですが,ライブラリが見つからなかったので作りました.

主な特徴

  • toio™コア キューブ 技術仕様 2.1.0 の機能に対応.
  • JavaScript 版 相当の機能を持ってるはず.
  • かんたんな API (進め!戻れ!光れ!)と,BLE メッセージレベルの細かい API を提供.
  • async/await に対応している.
  • cross platform (になろうとしている)
    • MacOS に対応
    • Linux 実装中
    • Windows はまだ始まっていない

設計と実装

BLE Abstraction

toio は Bluetooth 4.0 以降追加された Bluetooth Low Energy (BLE) とよばれる低消費電力の Bluetooth 通信規格で制御されます.そこで Rust の Bluetooth ライブラリを使います.

btleplug という cross platform なライブラリもありましたが,Service でデバイス検知できないなど現状少し機能が足りてなさそうでしたので個別にライブラリを使いました.

これらのライブラリを使った上で,BLE の基本的な操作(デバイス検索,接続,書き込み,読み込みなど)を抽象化します.

Toio Protocol

コアキューブ技術仕様 に従って,各種メッセージ構造体を定義しました. serde というシリアライザーをつかって toio が理解できるバイト列に変換できるようになっています.

構造体一覧はこちら

Low-level API

上記の構造体を toio に対して送信または受信するための API です.上記 Toio protocol で用意した構造体をシリアライズして, BLE Abstraction で用意した関数経由で送受信します.

High-level API

Low-level API を少しラップして,よりシンプルな API を提供します.例えば,キューブを移動させる API はこんな感じです.

pub async fn go<'_>(
    &'_ mut self,
    left: isize, // 左車輪回転速度
    right: isize, // 右車輪回転速度
    duration: Option<Duration> // 回転時間
) -> Result<()>;

用意した API をざっくり一覧にまとめるとこんな感じになります.

  • キューブの検索
  • キューブの移動
  • ライトの ON/OFF や点灯パターン指定
  • 音の再生
  • ID(Standard ID, Position ID)の取得
  • 各種ステートの取得(バッテリ残量,ボタンの ON/OFF,傾き等)
  • イベント(各種ステート変化)のサブスクライブ

API一覧はこちら

ライブラリの使い方

Cargo.toml の依存関係にこちらを追加します.API は基本的に async なので tokio という非同期ライブラリを一緒に入れます.

[dependencies]
toio = "0.1.4"
tokio = "0.2"

(tokio は Rust の async 関数を実行するための有名な非同期ライブラリです.名前が似すぎてて rust toio でググってもタイポ判定されて tokio でひっかかります)

とりあえず前進

こちらは 3 秒間キューブを直進させるプログラムです.PC の Bluetooth (BLE) を有効にして,近くに電源を入れたキューブを置いておきます.そこで,このプログラムを実行します.

use std::time::Duration;
use toio::Cube;
use tokio::time::delay_for;

#[tokio::main]
async fn main() {
    // 一番近い toio キューブを見つける.
    let mut cube = Cube::search().nearest().await.unwrap();

    // 見つかったキューブに接続する.
    cube.connect().await.unwrap();

    // 右の車輪を速度 20 で正転,左も 20.この API は toio に指示を出してすぐもどってくる.
    cube.go(20, 20, None).await.unwrap();

    // 3秒間待つ.
    delay_for(Duration::from_secs(3)).await;

    // プログラムが終了するとコネクションは自動的に切断される(`cube` が `Drop` すると切断)
}

Lチカ

LED を光らせます.

    // LED を RGB それぞれ 255 の強さで光らせる (範囲は 0 ~ 255)
    cube.light_on(255, 255, 255, None).await.unwrap();

    // 2 秒待つ.
    delay_for(Duration::from_secs(2)).await;

    // LED を消す.
    cube.light_off().await.unwrap();

音を鳴らす

キューブ内に用意されてる効果音を再生します.

    // Enter というプリセット効果音を再生
    cube.play_preset(SoundPresetId::Enter).await.unwrap();

また,Note を指定して音を鳴らします.

    // Note を指定して音を再生.
    cube.play(
        3,
        vec![
            SoundOp::new(Note::C5, Duration::from_millis(500)),
            SoundOp::new(Note::A6, Duration::from_millis(500)),
        ],
    )
    .await
    .unwrap();

バッテリー情報などを取得

    println!("version   : {}", cube.version().await.unwrap());
    println!("battery   : {}%", cube.battery().await.unwrap());
    println!("slope     : {}", cube.slope().await.unwrap());
    println!("collision : {}", cube.collision().await.unwrap());
    println!("button    : {}", cube.button().await.unwrap());

他の例はこちら

動作確認

動作確認ということで,このライブラリを使っていくつかかんたんな toio プログラムを作りました.

サンプル1: midi を演奏させる.

< 興味ないね

MIDI ファイル を読み込んで,toio キューブに再生させるコマンドラインプログラムです.どの楽譜をどの cube に演奏させるかをオプションで指定できます.

toio は Note の配列を受け取ってそのとおり演奏する API を持っています.ただ 1 度に最大 59 個しか Note を送れないため,59 個再生時間相当待機して,また次の 59 個を送信するという動きをします.

MIDI に関する知識が浅いので,ちょっとファイルによってはバグか誤解かで再生できないかもです.複数キューブの同期に苦労しました.まだずれてるかもです.

サンプル2: ゲームコントローラーで制御する.

Rust にはゲームコントローラーからの入力を受け付けるライブラリがあるので,それを利用しました.動作確認は手元にあった PS4 用の DualShock 4 コントローラでためしています.

サンプル3: 鬼ごっこ

鬼ごっこです.これは 2 つ以上の toio キューブを使います. 2つのうち1つは逃げる役,残りは全員鬼役です.逃げる役は ゲームコントローラーでプレイヤーが操作できます.他の鬼キューブは自動で勝手に追いかけてきます.できるだけ長く逃げましょう.

鬼と接触するたびにプレイヤーのライフが減り, LED が危ない色になります(バイオ的な).ライフがゼロになるとゲームオーバー.

鬼プログラムの種類は拡張可能で,trait を実装すると具体的な鬼が実装できます.

まとめ

娘(現在 3 歳)も最近はやっと toio で遊べるようになりました.お父さんと取り合いになることもあります.

Rust は js などとはまた違ったライブラリの ecosystem を持ってると思うので,別の楽しみ方も見えてくるかもしれません.Linux & Windows 対応がまだですがゆっくり作っていきたいです.