Wiiバランスボードを「アクセルとブレーキ」にする ― 巡航スクロールアプリ BALAN を作った話

みなさん、こんにちは。

先日、Wiiリモコンを左手デバイスにするアプリ「KUNAI」の記事を書きました。

我ながら出来が良いと思い、毎日使っています。左手デバイス、本当に便利ですね。

そうそう、Wiiの周辺機器でもう一つ、押し入れで眠っているものがありました。Wiiバランスボードです。

体重計のような、あの白い板です。フィットネスゲームで遊んだ後、そっと押し入れにしまった人も多いのではないでしょうか。あの板には、四隅に荷重センサーが付いています。しかも Bluetooth で PC につながります。これ、使わない手はないですよね。

KUNAI の出来栄えに満足した私は、早速Wiiバランスボードの新しい使い道を考えつきました。

というわけで今回は、バランスボードでスクロール速度を制御するアプリ BALAN(Balance Board Assisted Learning And Navigation)について書いてみたいと思います。


「足でスクロール」ではなく「足で速度を制御する」

最初に、作りたいもののコンセプトを決めました。

作りたかったのは、「足でスクロールするツール」というより情報を読む速さを、足で決めるツールです。

Webブラウザ、RSSリーダー、Cosense、PDF、技術資料……。デスクワークの多くは、長い文章をひたすら読み進める作業です。その間、手はキーボードやマウスにある。だったら、スクロールくらい足に任せたいですよね。

そこで、車の運転をお手本にしました。

  • 右足 = アクセル ⇒ 踏むほど、スクロールが速くなる
  • 左足 = ブレーキ ⇒ 踏むと減速して止まる
  • 何も踏まない = 停止 ⇒ 何も起きない

一般的な操作だと「左が上、右が下」のように位置を指定しがちですが、BALAN は速度を指定します。「右は進む、左は止まる」。説明が1文で終わるのも、大事なポイントです。

なお、これは縦スクロール用の「Vertical モード」の話です。公開後に、横スクロール用の「Horizontal モード」も追加しました(後ほど詳しく説明します)。

一番大切にしたのは、「思った場所でちゃんと止められること」です。スクロールが止まらないツールほど、使っていて不快なものはありません。そのため、迷ったら必ず「動かない」「止まる」側に倒す設計にしました。


作ったツール「BALAN」の概要

画面は1枚構成です。接続状態、4つのセンサーの荷重、巡航の速度、感度のスライダー、スクロールモードの切り替えがすっきりと並んでいます。

BALANの画面

スクロールモードは2つ

  • Vertical(既定):縦方向の巡航スクロール。右足がアクセル、左足がブレーキ
  • Horizontal:横方向のスクロール(チルトホイール)。右足で右へ、左足で左へ
  • 画面で切り替えるとすぐに反映され、次回起動時も前回の選択が復元されます

まずは Vertical モード を中心に概要をまとめます。

  • PEDALS(荷重メーター)
    • TR(右奥)の荷重 = 上へスクロール
    • BR(右手前)の荷重 = 下へスクロール
    • TR と BR は、荷重が大きい方が勝つ
    • TL か BL(左側)のどちらかが一定以上 = ブレーキ
  • 速度の段階(Vertical・Horizontal 共通)
    • 2kg 未満は無反応
    • 2〜3kg が Slow、3〜4kg が Medium、4kg 以上が Fast
    • ただし階段状に変わるのではなく、なめらかなカーブで速度が上がる
  • 感度の調整
    • Dead Zone(反応が始まる荷重)、Max Speed、Brake Threshold(ブレーキが効く荷重)、Brake Strength
    • スライダーを動かすとすぐ反映されて、設定は自動で保存される
  • Calibrate
    • 足を載せただけの状態の荷重を測って、ゼロにする
  • トレイ常駐
    • 閉じる・最小化でトレイに隠れる。ずっと起動しておく前提の設計
  • 技術スタック
    • .NET 8 LTS と C#
    • Avalonia 11 と ReactiveUI
    • Windows は SendInput、Linux は uinput でスクロールを送出(縦は通常のマウスホイール、横は水平ホイール)

AIと一緒に、まず「絵」から作る

今回も Claude Code と一緒に作りました。KUNAI のときと同じく、「コードを書く前に、画面のモックを作る」ところから始めました。

Python の画像ライブラリ(Pillow)で、画面のモックを PNG として生成します。

実装のフェーズは、次のように分けてもらいました。

  1. Python による UI モックの生成
  2. Wii Balance Board への接続
  3. 荷重の取得
  4. 重心(荷重)の表示
  5. 巡航速度の計算
  6. スクロールの送出
  7. 設定の保存(JSON)
  8. トレイ常駐

構成は KUNAI を参考にして、Core(モデル・計算・制御)を一番内側に置き、Device(ボードとの通信)、Input(OS へのスクロール送出)、UI が Core の抽象を実装する形にしました。巡航の計算は、ボードも OS も画面もなくてもテストできます。


実機テストで、設計が根本から変わった

ここが今回の大きなターニングポイントでした。

最初の設計は、立ってボードに乗り、体重移動で操作する前提でした。四隅の荷重から左右の重心を計算し、右に傾ければ進む、左に傾ければ止まる。デッドゾーンは重心 ±0.15、アクセル曲線は X2X^2、ノイズは指数移動平均で……と、仕様書を作り、実装しました。

ところが、実機で試すと、前提が根本から間違っていることに気づきました。

座って足で操作するので、荷重はせいぜい 5kg くらいまでしかかからなかったのです。

体重移動で操作する想定の設計では、ほぼ使い物になりません。そこで、設計からやり直しました。

  • 重心の計算をやめて、センサーごとの荷重(kg)をそのまま使う
  • TR の荷重 → 上スクロール、BR の荷重 → 下スクロール(大きい方を採用)
  • TL / BL の荷重が一定以上 → ブレーキ(アクセルより優先)
  • 2kg 未満は無反応、2・3・4kg で Slow・Medium・Fast
  • ただし階段にはせず、なめらかなカーブで速度を上げる

実機テストで確認した数値を、そのまま自動テストにしました。「2kg ちょうどは無反応」「1.99kg → 2.0kg の境目」「0.1kg 刻みで荷重を上げたとき、速度が単調に、飛ばずに増える」といった具合です。

その後、実機で使いながら、ブレーキのしきい値のデフォルトを 5.0kg に変え、画面からも調整できるようにしました。足の力加減は人によって違うので、ここは画面で調整できて大正解でした。

AIが生成した仕様書を読んだだけではおかしな点に気づけなかったのですが、実際に触ってみるとわかる問題点はやっぱり残るものですね。座ったら荷重が減るというのは、考えてみたら当たり前のことでした。


横スクロール用の「Horizontal モード」を追加した

使っているうちに、「横にもスクロールしたい」と思うようになりました。横に長い表や、コードエディタ、動画や音声のタイムライン、ホワイトボード系のツールなどで利用するマウスのチルトホイール(ホイールを左右に倒すと横スクロールするもの)の役割を、足に任せられないかと考えました。

BALANのHorizontalモード
BALANのHorizontalモード

Horizontal モードの動きは、次のとおりです。

  • 右足(TR または BR) = 右へスクロール
  • 左足(TL または BL) = 左へスクロール
  • 足を離せば停止。ブレーキという概念は使わない
  • 左右はそれぞれ独立した入力で、軽く踏めば遅く、強く踏めば速い

ここで一番気をつけたのは、「速度の計算を複製しないこと」です。荷重から速度を決める部分をもう一つ作ってしまうと、片方だけ直して挙動がずれる、という事故が起きます。そこで、構造は次のようにしました。

  • 荷重 → 速度の変換(Dead Zone、カーブ、加速・減速)は、Vertical とまったく同じものを使う
  • 違うのは、足の割り当て(右足を正方向、左足を負方向として渡すだけ)と、最後のスクロール送出の軸だけ
  • 送出は、縦なら垂直ホイール、横なら水平ホイールのイベントを送る

Windows では、SendInput の水平ホイール(MOUSEEVENTF_HWHEEL)を使います。Linux では、既存の uinput の仮想マウスに水平ホイール(REL_WHEEL と、細かい量の REL_WHEEL_HI_RES)を追加しました。

実装の途中で、判断が必要なところがいくつかありました。

  • 左右を同時に踏んだら、荷重が大きい方が勝つ(Vertical の TR と BR と同じ考え方にして、逆方向へは一度止まってから切り替わる)
  • 加速に約 0.5 秒、足を離してから止まるまで約 0.25 秒かかる
  • 速度の計算を共通にした結果、チルトホイールにしてはやや「巡航」寄りの動きになる。これは様子を見て調整したい
  • モードを切り替えた瞬間は、速度と積算の端数をリセットする(縦の勢いが、そのまま横に流れ込まないようにするため)
  • Horizontal ではブレーキを使わないので、画面側も合わせる(PEDALS の見出しが TR/BR ▶ RIGHT / TL/BL ◀ LEFT に変わり、ブレーキの行や関連スライダーは薄く表示して操作不可にする)

実機では、Windows で Horizontal の動作を確認しました。横スクロールを頻繁に使うAudacityで、右足で右へ、左足で左へ動くことを確認できています。


「止まる」ための工夫(Vertical モード)

「思った場所で止まる」ために、いくつか仕掛けを入れています。

  • 加速はゆるやか、減速は速い
    • 最大速度に達するまで約 0.5 秒。急発進しない
    • 荷重を抜くと、約 0.25 秒で止まる
    • ブレーキを踏むと、ほぼ一瞬で止まる
  • センサーが途絶えたら必ず止める
    • 電池切れや切断で、最後に受け取った荷重が残ってスクロールが走り続けたら大惨事です
    • 300ms データが来なければ「誰も踏んでいない」扱いにする
  • 方向の切り替えは、いったん止めてから
    • TR と BR を急に踏み替えても、逆走しない
  • 1フレームの跳ねは無視
    • 各センサーに指数移動平均(filtered = old × 0.8 + new × 0.2)をかける

ちなみに、スクロールの送出は、速度(ノッチ/秒)に時間を掛けて積算し、端数を持ち越しています。ゆっくりでも、カクカクしません。


Wiiバランスボードとの通信

ボードとの通信は、Wiiリモコンと同じ仕組みです。HID のレポートで、荷重は「拡張コントローラ」として届きます。

  • Product ID が Wiiリモコンと同じ(0x0306)
    • なので、見つけた HID デバイスが本当にバランスボードかどうかは、拡張 ID(00 00 A4 20 04 02)を読んで判定する
    • 違ったら閉じて、次の候補へ
  • ボードの中に、較正値が入っている
    • 0kg / 17kg / 34kg のときの各センサーの生の値が、本体に記録されている
    • これを読んで、生の値を kg に変換する。ユーザーが較正しなくても、ほぼ正しい重さが出る
  • 荷重は連続レポートで届く
    • ボタンと拡張8バイトのレポートを、約100Hzで受け取る

とはいえ、手元の開発環境(WSL2)には、ボードも Bluetooth もありません。そこで、バランスボードのふりをする偽の接続を作って、次のことをテストしてもらいました。

  • 初期化の手順(拡張の初期化 → ID の確認 → 較正値の読み出し → 連続レポートの開始)
  • Wiiリモコンを掴んでしまったときは、閉じて次の候補に進むこと
  • 切断されたときに、ちゃんと「切断」として通知されること

また、実機なしで画面と巡航を確認できるよう、--simulate という起動オプションで動く模擬ボードも用意しました。


ペアリングは KUNAI の知見を持ってきた

Windows では、ここでも PIN の壁があります。

Windows の標準の画面でペアリングしようとすると PIN を求められますが、その PIN は Bluetooth アドレスをバイナリで並べた値で、キーボードから入力できません。KUNAI のときに、アプリが自分でペアリングする仕組みを作っていたので、これをバランスボード用に移植しました。バランスボードは、電池ボックスの中の赤い SYNC ボタンを押すとペアリングできる状態になります。ただ、KUNAIと異なり、一度ペアリングしたとしても、切断後は次回もSYNCボタンを押してペアリングしないと自動接続してくれないようで、この挙動にちょっと悩んでいます。

Linux も KUNAI と同じ方針です。BlueZ のペアリングは使わず、bluetoothctl で探索して、L2CAP で直接つなぎます。スクロールの送出は、uinput の仮想マウスです。ホイールの細かい量(REL_WHEEL_HI_RES)と、1ノッチごとの通常のホイール(REL_WHEEL)の両要把を送っています。

トレイも、KUNAI の X11(XEmbed)方式を移植しました。環境に合わせて、XEmbed → StatusNotifierItem の順に、使えるものを自動で選びます。


実機で見つかったバグと対策

1. Calibrate でアプリが落ちる

実機で Calibrate ボタンを押したら、アプリが落ちました。 無荷重なのに TL が 1.4kg と、ボードの個体差のようなずれが出ていたので、それをゼロにしようと Calibrate を押したときのことです。 落ちる前に、あらかじめ crash.log に例外を書き出す仕組みを入れていたので、原因はすぐに分かりました。

System.InvalidOperationException: Call from invalid thread
   at Avalonia.Controls.Button.get_Command()
   at ReactiveUI.ReactiveCommandBase`2.OnCanExecuteChanged(...)

原因は、画面の ViewModel を作るタイミングでした。

ReactiveUI は、「UI のスレッドはこれ」というスケジューラを、Avalonia の初期化のときに設定します。ところが、ViewModel をそれより前に作っていたので、ReactiveUI が UI スレッドではないスレッドを覚えてしまっていたのです。Calibrate を押すとボタンの有効・無効が切り替わり、その通知が別のスレッドから届いて、ボタンを触った瞬間に落ちていました。

ViewModel の生成を、Avalonia の初期化が終わった後に移して無事に解決しました。

「手元で動いた」と「実機で動いた」は別物だと、改めて思い知らされました。他にも、初期バージョンはウィンドウ高さを固定していたせいでスライダーを追加した際にメーターの下部が切れる不具合もありましたが、実機で検証することの重要性を痛感しました。

2. アプリの二重起動を防ぐ

Horizontal モードを実機で試しているときに、もう一つ問題に気づきました。アプリが二重に起動してしまうのです。トレイに常駐していると、起動済みだと気づかずにもう一度起動してしまいがちです。

そこで、起動時に設定フォルダのロックファイルを排他で開くようにしました。開けなかったら既に起動していると判断して、何も表示せずに終了します。ロックは OS がプロセスの終了時に解放してくれるので、クラッシュしても次に起動できなくなることはありません。Windows の実機で、二重に起動しないことを確認しました。


アイコンも作った

KUNAI と同じく、アイコンも作りました。

balan-icon
BALANのアイコン

バランスボードを真上から見た図です。左半分に赤い停止ボタン(ブレーキ)、右半分に緑の下向きシェブロン(下へスクロール)。Vertical モードの「右で進む、左で止まる」を1枚で表しています。

アイコンも Python(Pillow)で生成していて、.ico と、トレイ用の接続状態(赤・黄・緑の丸つき)も同じスクリプトから作っています。


注意点と使い方

まだ公開したばかりのバージョン(v0.3.1)です。以下の点にご注意ください。

  • Windows 版は、実機で動作を確認しています(Vertical・Horizontal の両モードと二重起動防止を確認済み)
  • Linux 版はXubuntu 22.04 環境でテストしています
    • ボードが他の端末(Windows など)とペアリングされていると、接続できません。その場合は、先に相手側でペアリングを解除してください
  • バイナリはコード署名をしていないので、Windows で SmartScreen の警告が出ることがあります

導入方法

  1. リリースページ(v0.3.1)から、BALAN-win-x64.zip(Windows)か BALAN-linux-x64.tar.gz(Linux)をダウンロードする
  2. 展開して、BALAN.exe(Windows)または ./BALAN(Linux)を実行する
  3. Connect を押して、ボードの赤い SYNC ボタンを押す
  4. 足をボードに載せた状態(体重をかけずに)で Calibrate を押す
  5. Scroll Mode を選ぶ(既定は Vertical)
    • Vertical:右足で TR / BR を踏むと上 / 下へスクロール、左足で TL / BL を踏むとブレーキ
    • Horizontal:右足で TR / BR を踏むと右へ、左足で TL / BL を踏むと左へスクロール

実機がなくても、BALAN.exe --simulate で画面と巡航の動きを確認できます。

なお、BALAN は二重起動できません。既に起動している場合は、トレイに常駐しているものを使ってください。

.NET のランタイムは同梱してあるので、別途インストールは要りません。Linux でスクロールを送るには /dev/uinput の権限が必要です。設定は、同梱の udev ルール(99-balan.rules)と、リポジトリの README.md を見てください。


これから

今のところ、以下のようなことを考えています。

  • 自動再接続:ボードが自動で電源オフになって切れたあと、バックグラウンドで再接続したい
  • Horizontal モードの操作感の調整:チルトホイールらしく、もう少しキビキビ動かすか検討する
  • しきい値の調整のしやすさ:人によって足の力加減は違うので、もう少し簡単に調整できるようにしたい

足でスクロールするというのは、使ってみると意外と気持ちが良いです。慣れたらアリだと思います。ただ、習慣とは恐ろしいもので、意識しないと指でスクロールしちゃいますけどね。

気になったら、ぜひ押し入れのバランスボードを引っ張り出して試してみてください。

こんな使い方が欲しい、うまく動かない、といったフィードバックをいただけると嬉しいです。もちろん、不具合報告も大歓迎です。

本日も最後までお読みいただきありがとうございました。

それでは、よいバランスボードライフを!


「自分も何か作ってみたい」と思われた方、そのアイデア、頭の中で寝かせておくのはもったいないかもしれません。

ビューローみかみでは、構想段階の壁打ちからPoC・実装・現場導入まで、現場で「使い続けられる」ものづくりを支援しています。

アイデアを短期間で「動くもの」にし、PoCで終わらせず、実運用まで伴走します。まずは「これ、作る価値ありますか?」という壁打ちからでも大歓迎です。

▶ PoC・MVP開発サービスの詳細はこちら

気になることがあればお気軽にご相談ください。

カテゴリ: ハードウェア
上部へスクロール