Windowsの隣のiPadも、いつものキーボードとマウスで操作したい。 そんな環境向けに、Windows 11の入力をBluetoothでiPadへ送るOSS「Universal Bridge」を開発しています。この記事では、実装に使ったBLE HID・Windows API・入力復帰の設計を解説します。
日本語の使い方 / v0.3.2の配布ページ / ソースコード
試す前に:できることと必要な環境
Windowsで作業しながら、横に置いたiPadで検索やメモをしたい方を想定しています。Mac・VM・iPad用の専用アプリは不要で、対応PCなら入力機器の追加購入も不要です。無料・MITライセンスで、日本語・英語・簡体字のGUIがあります。
必要なのは Windows 11 x64と、BLE Peripheralに対応したBluetoothアダプターおよびドライバー です。Bluetooth搭載だけで対応するとは限らず、アプリが対応状況を確認します。トラックパッドのジェスチャー取得にはWindows 11 24H2以降の対応する実験的APIも必要です。未対応なら取得をオフにしてキーボード・マウスだけを使います。
v0.3.2は署名なしのプレビュー版です。 基本入力の実機成功報告はありますが、機器ごとの互換性・長時間の安定性・ジェスチャーは検証が残ります。画面ミラーリング、クリップボード共有、ファイル転送、iPadネイティブのマルチタッチ転送は提供していません。Apple・Microsoftの公式製品ではありません。
最初の入力まで
- 配布ページの
UniversalBridge-0.3.2-win-x64.zipを展開し、通常権限でUniversalControl.exeを起動します。.NETは同梱しています。 - 「Bluetooth HIDを公開」を押し、iPadの「設定 → Bluetooth」で Windows PCのコンピューター名 を選びます。ペアリング確認は両端末で行います。
- アプリでiPadを選び、キーボードとマウスの両方の購読が揃うのを待ちます。
- Ctrl+Alt+Iを押して全キーを離し、メモなどで入力を試します。Ctrl+Alt+Q または Escを3回 でWindowsへ戻ります。画面端切り替えは入力確認後に有効にしてください。
以下では、この入力共有をどのように実装したかを説明します。
WindowsとiPadを併用していると、ちょっとした不便に遭遇する。
Windowsで作業をしながら、横に置いたiPadで資料を確認する。iPadを操作するたびに手を伸ばして、またWindowsのマウスに戻る。
MacとiPadならAppleの「ユニバーサルコントロール」があるのに、WindowsとiPadの組み合わせでは同じように操作できない。
それなら、自分で作ってしまえばいいのでは?
そんな発想から、Windowsに接続されたマウスとキーボードを、Bluetooth経由でそのままiPadに使えるようにするアプリを開発した。
名前は「Universal Bridge」。
GitHubでMITライセンスのOSSとして公開している。
GitHub: noppy3325-droid/universal-bridge
初期の動作するアプリは、Codexを使ったAI駆動開発によって約3時間で形になった。その後も改良を重ね、執筆時点のv0.3.2では、日本語・英語・中国語への対応や、トラックパッド入力の変換なども実装している。
この記事では、完成したアプリの紹介に加えて、その裏側にあるBluetooth HID、Windows API、入力制御の仕組みを解説していく。
1. iPad用アプリを作らずに操作するには?
最初にぶつかるのは、iPadOS側の制約だ。
一般的なiPadOSアプリには、システム全体のマウスカーソルを自由に操作したり、他のアプリにキーボード入力を注入したりする権限がない。
つまり、Windowsからネットワークで入力イベントを送信し、iPad側のアプリで再現するという単純な構成では、ホーム画面やSafariまで自由に操作することは難しい。
しかし、別の方法がある。
WindowsそのものをBluetoothキーボード・マウスとしてiPadに認識させればいい。
iPadOSは市販のBluetoothキーボードやマウスを標準でサポートしている。
であれば、WindowsがBluetooth HIDデバイスとして振る舞えば、iPadには普通の周辺機器として認識してもらえるはずだ。
これがUniversal Bridgeの基本的な発想だ。
Windows PC
├─ 物理キーボード
└─ 物理マウス
│
▼
Windows入力監視
│
▼
HID Reportへ変換
│
▼
Bluetooth LE Peripheral
│
│ HID over GATT
▼
iPadOS
│
▼
標準キーボード・マウスとして動作
画面を映像として送信するリモートデスクトップ方式ではない。
iPadはiPadOSのまま独立して動作し、入力だけがWindowsから届く。
この方式なら、iPadへの専用アプリも、追加のBluetoothドングルやマイコンも、操作中のインターネット接続も必要ない。
ただし、Windows PCのBluetoothアダプターとドライバーがBLE Peripheral Roleに対応していることが条件になる。
2. WindowsはBluetoothデバイスになれるのか?
ここが今回の開発で最も興味深かった部分だ。
Bluetooth機器には大きく分けて、CentralとPeripheralという役割がある。
普段のWindows PCは、Bluetoothキーボードやマウスに接続するCentral側として使われることが多い。
今回はそれを逆転させる。
Windows PC自身がPeripheral側となり、iPadから接続されるHIDデバイスとして振る舞う。
実装にはWindows RuntimeのGattServiceProviderを利用した。
実際の実装では、まずアダプターがPeripheral Roleに対応しているかを確認している。
if (!adapter.IsLowEnergySupported ||
!adapter.IsPeripheralRoleSupported)
{
throw new NotSupportedException(
"BLE Peripheralに対応していません。");
}
対応していれば、Bluetooth HIDサービスを作成する。
var result =
await GattServiceProvider.CreateAsync(
HidDescriptors.HidService
);
HidDescriptors.HidServiceが表すUUIDは0x1812。
Bluetooth SIGが定義しているHID Serviceだ。
Universal Bridgeでは、このサービスにキーボードとマウスのGATT Characteristicを追加し、iPadに公開している。
主要な属性は次のとおり。
| GATT属性 | UUID | 用途 |
|---|---|---|
| HID Service | 0x1812 | HID機器のサービス |
| HID Information | 0x2A4A | HIDの情報 |
| Report Map | 0x2A4B | 入力データの形式 |
| HID Control Point | 0x2A4C | Suspendなどの制御 |
| HID Report | 0x2A4D | キー・マウス入力 |
特に重要なのがReport Map。
これは、送信されるバイト列が何を意味するかを、受信側へ伝えるための定義だ。
Universal Bridgeはキーボードとマウスを一つの複合HIDデバイスとして構成し、それぞれの入力をGATT Notificationとして送信している。
さらに、GATT属性には暗号化を要求し、ペアリング済みの選択された端末にのみ入力を送る設計になっている。
一般的なBluetooth周辺機器の仕組みを利用しているので、iPadOS側に特殊な権限や改造は必要ない。
なお、この部分の開発ではwindows-ble-hidというMITライセンスのOSSを参考にし、一部のHID Descriptorやキーマップを出典表記付きで利用している。
3. マウスの動きはどうやって送信する?
Bluetoothの接続だけでは、当然マウスを操作できない。
Windowsで発生した入力を取得し、iPadが理解できる形式に変換する必要がある。
Windowsの入力を監視する
キーボードやクリックについては、WindowsのLow-Level Hookを利用している。
具体的には、SetWindowsHookExで次のフックを登録する。
- WH_KEYBOARD_LL:キーボード入力
- WH_MOUSE_LL:マウス入力
iPad操作モードでは、これらの入力をWindows側で通常どおり処理する代わりに、HIDレポートとしてiPadへ転送する。
一方、マウスの移動量には別の工夫を加えた。
Windowsのカーソル座標を使うと、動きがずれる
最初はWindows上のカーソル位置の変化から、iPadへの移動量を計算する方法もある。
しかし、この方式には問題がある。
Windowsのマウス加速や感度設定が反映された座標を利用すると、iPad側でもポインター処理が行われ、期待した動きとずれてしまう可能性がある。
そこでUniversal Bridgeでは、Raw Inputを採用した。
Raw Inputからマウスの相対移動量を取得し、それをHIDレポートへ変換している。
物理マウス
│
├─ Low-Level Hook
│ └─ 入力の抑制・クリック処理
│
└─ Raw Input
└─ 相対移動量 dx, dy
│
▼
HID Encoder
│
▼
Bluetooth送信
Raw Inputを利用できない場合に備えて、従来の座標ベースの互換処理も残している。
このように、正常に動く環境では精度の高い方法を使い、対応できない環境では別の経路へフォールバックする構成になっている。
HIDレポートの中身
実装ではキーボード入力を8バイト、マウス入力を7バイトのレポートとして構成している。
マウスレポートの内訳は次のようになっている。
| バイト | 内容 |
|---|---|
| 0 | マウスボタン状態 |
| 1〜2 | X方向の相対移動量 |
| 3〜4 | Y方向の相対移動量 |
| 5 | 縦スクロール |
| 6 | 横スクロール |
実際のソースコードでは、次のように移動量を書き込んでいる。
BinaryPrimitives.WriteInt16LittleEndian(
bytes.AsSpan(1),
(short)Math.Clamp(dx, -32767, 32767)
);
BinaryPrimitives.WriteInt16LittleEndian(
bytes.AsSpan(3),
(short)Math.Clamp(dy, -32767, 32767)
);
この処理自体はシンプルだが、入力イベントは非常に細かい間隔で発生する。
すべてのイベントをそのままBluetoothへ送ればよいわけではない。
Universal Bridgeでは、移動量を一定時間蓄積して送信する仕組みを採用している。感度調整によって生じる小数部分も捨てずに保持し、次のレポートへ繰り越す。
例えば感度を0.1倍に設定したとき、1カウントの移動を10回受け取ったなら、最終的には1カウントを送信したい。
このような細かい動きの精度も、実装上の重要なポイントになっている。
4. 実は一番怖いのはBluetoothの切断
入力共有アプリで最も避けたいのは、マウスやキーボードが突然使えなくなることだ。
例えば、Windowsの入力を抑制したままBluetoothが切断されたらどうなるだろう。
Windowsは入力を受け付けず、iPadにも入力が届かない。
これはかなり困る。
そこでUniversal Bridgeでは、入力を送信できることだけでなく、失敗を検出した際にWindowsへ制御を戻すことを重視している。物理切断の検出には遅れがあり、強制終了時のiPad側のキー解放はBluetoothの切断処理に依存する。
入力の所有権を管理する
内部のInputRouterでは、入力先がWindowsなのかiPadなのかを管理している。
iPadへ切り替える際には、いきなり入力抑制を有効にするのではなく、接続状態やキーの押下状態などを確認する。
Windowsモード
│
▼
切り替え要求
│
▼
接続・購読状態の確認
│
▼
入力監視の正常性を確認
│
▼
押下中のキー・ボタンがないか確認
│
▼
中立HIDレポートを送信
│
▼
再確認
│
▼
iPadモード
ここでいう中立レポートとは、すべてのキーやマウスボタンが解放されている状態を表すものだ。
接続確認や準備が失敗した場合は、入力をWindows側に残す。
Bluetooth送信は別の処理系にする
入力フックの中でBluetooth通信を直接待つことも避けている。
Bluetoothは無線通信なので、応答が遅れたり、通信そのものが失敗したりすることがある。
その間ずっとWindowsの入力処理が待たされるのは望ましくない。
そこで、入力を処理するInputRouterと、非同期で送信するReportDispatcherを分離した。
Win32 Input Hooks
│
▼
InputRouter
│
▼
Bounded Channel
(最大256件)
│
▼
ReportDispatcher
│
▼
BLE HID Transport
│
▼
iPad
送信キューには上限を設けている。
もし送信側が処理しきれなくなってキューがいっぱいになったら、無理に入力をため続けず、Windows側へ制御を戻す。
さらに、送信にはタイムアウトを設けている。
古い入力が後から送信されることを防ぐため、generationに相当する世代番号も管理している。入力先が切り替わった際には世代を更新し、以前の世代に属するキュー内のイベントを無効化する仕組みだ。
これにより、iPadからWindowsへ戻した後に、古いクリックやキー入力が遅れて送られる事態を防いでいる。
緊急復帰も用意
通常のショートカットは、初期設定で次のようになっている。
| 操作 | ショートカット |
|---|---|
| iPadに切り替え | Ctrl + Alt + I |
| Windowsに戻る | Ctrl + Alt + Q |
| 緊急復帰 | Escを3回 |
もちろん、Bluetoothの切断や入力監視の異常も復帰条件に含まれている。
入力を横取りするソフトウェアでは、便利さより先に「安全に入力を返せること」を設計する必要があると感じた。
5. 開発中に遭遇した、日本語IME特有の不具合
今回の開発で面白かったのが、日本語入力環境に起因する問題だ。
初期バージョンでは、Bluetoothのペアリングは成功しているのに、iPadへの入力切り替えができない現象が発生した。
調べてみると、原因は接続ではなく、キーの押下状態を判定する処理にあった。
WindowsのGetAsyncKeyStateを使ってすべての仮想キーを確認する実装では、実際にはキーを押していないにもかかわらず、一部の仮想キーが押下状態と判定されていた。
具体的には、0xF0や0xF4などのIME関連の状態が問題になっていた。
アプリから見ると、常に何かのキーが押されている扱いになってしまう。
その結果、安全対策として設けた「キーが押されている間は入力先を切り替えない」という処理によって、切り替えそのものが拒否されていた。
これは少し皮肉なバグだった。
安全のために追加した判定が、環境によっては正常な操作まで止めてしまっていた。
修正後は、実際の物理キーに相当する仮想キーを判定対象にし、IMEの状態フラグなどを区別する方式に変更した。
加えて、フックから観測したキー押下も独立して追跡することで、未対応の物理キーが押されている場合でも安全性を維持している。
この修正によって、実際のiPadへの入力切り替えと、マウス・キーボード操作に成功した。
また、後続バージョンでは日本語キーボードの「半角/全角」キーにも対応した。
WindowsとiPadOSでは入力言語の切り替え方法が異なるため、対象キーを検出した際にControl + SpaceのHID入力を送信する処理を追加している。
Windows専用のキー操作を、別のOSが理解できる入力へどう翻訳するか。こうした細かな互換性の問題も、OSをまたぐアプリならではの難しさだ。
6. トラックパッドのジェスチャーはさらに複雑
現在のUniversal Bridgeでは、対応環境でWindows Precision Touchpadの一部ジェスチャーもiPad向けに変換できる。
ただし、キーボード・マウスの入力監視とは別の仕組みが必要になる。
Windowsでは、3本指や4本指のスワイプなどがOS側で処理される場合があり、通常のLow-Level Hookだけで取得できるとは限らない。
そこで、対応するWindows 11環境ではTouchpadGesturesControllerとPhysicalGestureRecognizerを利用している。
複数指ジェスチャーを取得するには前面ウィンドウの扱いも重要になるため、iPad操作中だけ専用の小さな入力受け取りウィンドウを利用する設計になっている。
この部分は実験的なWindows APIに依存するため、実行環境での対応判定が必要だ。
さらに重要なのが、Universal BridgeはiPadのネイティブなマルチタッチ入力を再現しているわけではないという点。
2本指のスクロールはHIDのホイール入力へ、その他の対応ジェスチャーはキーボードショートカットなどへ変換している。
そのため、iPadOSの純正トラックパッドと完全に同じ操作ができるわけではない。
Windows側でジェスチャーを取得することと、iPadOS側で期待した動作になることは、別々に検証する必要がある。
7. なぜC# / WPFを採用したのか
最終的な技術スタックは次の構成になった。
| 分野 | 技術 |
|---|---|
| 言語 | C# |
| ランタイム | .NET 10 |
| GUI | WPF |
| Bluetooth | Windows Runtime GATT API |
| 入力監視 | Win32 API / Raw Input |
| 非同期送信 | System.Threading.Channels |
| テスト | xUnit |
| CI/CD | GitHub Actions |
もともとはTauriとReactを利用する構成も検討していた。
しかし、今回のアプリではWindows APIとの連携が中心になる。
Bluetooth通信も入力フックもWindows固有の処理が多く、WebViewベースのGUIとネイティブプロセスを分ける必要性はそれほど大きくない。
そこで、C#とWPFだけで完結する構成を採用した。
コードベースは大きく3つに分離している。
UniversalControl.Core
├─ HIDレポート生成
├─ 入力ルーティング
├─ キー変換
├─ 座標・感度処理
└─ 再接続ポリシー
UniversalControl.Windows
├─ BLE GATT通信
├─ Win32入力フック
├─ Raw Input
└─ タッチパッド入力取得
UniversalControl.App
├─ WPF GUI
├─ タスクトレイ
├─ 設定管理
└─ 診断機能
Windows依存の実装をCoreから分離したことで、入力ルーティングやHIDエンコードを、実際のBluetooth機器がなくてもテストできる。
これはAI駆動開発においても大きな利点だった。
8. CodexによるAI駆動開発とテスト
今回、実装の中心にはOpenAIのCodexを利用した。
自分が実現したい操作や制約を指定し、Codexに調査、設計、実装、テストまで進めてもらうという形だ。
もちろん、一度コードを生成しただけで完全に動いたわけではない。
初期段階ではiPadとのペアリングや入力切り替えを実機で確認し、正常に動かない箇所があればログと症状をもとに修正を進めた。
特に前述のIME関連の問題は、実際の利用環境がなければ見落としてしまう可能性があった。
一方で、実機がないと確認できない部分以外は、可能な限り自動テストできるように設計している。
v0.3.2の検証記録では、自動テスト135件が成功し、ビルドも警告0件・エラー0件となっている。
テスト対象には、HIDレポートの長さや符号付き値の変換だけでなく、次のような異常系も含まれる。
- キーを押したままBluetoothが切断された場合
- 入力送信キューがあふれた場合
- 送信タイムアウトが発生した場合
- 切り替え処理の途中で復帰が要求された場合
- スリープ後にGATTセッションが入れ替わった場合
- キーボードとマウスの解放処理が一部だけ成功した場合
特にv0.3.2では、iPadのスリープ復帰によって送信が停止した場合の再接続処理を改良している。
ただし、これらは主にソフトウェア上のテストであり、すべてのiPadやBluetoothアダプターで実機動作が確認されたことを意味しない。
基本的なマウス・キーボード入力はiPad実機で成功しているものの、長時間の安定性やジェスチャーの操作感など、まだ検証が必要な項目も残っている。
AIが大量のコードを書けるようになったからこそ、どこまでテストされたのか、どこからが未確認なのかを明確にすることは重要だと感じている。
9. 今後の課題
現在のUniversal Bridgeはプレビュー版として公開している。
基本的な入力共有は利用できるものの、改善したいところはまだ多い。
特に対応ハードウェアの問題は重要だ。
今回の方式はBLE Peripheral Roleを利用するため、すべてのWindows PCで動くわけではない。
また、Windows側からiPad上のマウスカーソルの正確な絶対座標を取得することもできない。
画面端での切り替え機能は実装しているが、iPadからWindowsへの復帰は推定座標を利用するため、実機での調整が必要になる。
個人的にはショートカットによる切り替えで十分便利に使えているので、まずはこちらを中心に安定性を高めたいと考えている。
さらに、USB接続のキーボードやマウスを含む異なる環境、Bluetoothアダプターの違い、日本語配列以外のキーボードなども、今後検証していきたい。
10. おわりに
今回の開発で面白かったのは、単にWindowsからiPadを操作できたことだけではない。
WindowsをBluetoothキーボード・マウスとして振る舞わせることで、iPadOS側を一切変更せずにOS間の入力共有を実現できたことだ。
Bluetooth HIDの仕組み、Windowsの低レベル入力処理、非同期通信、入力の所有権管理、異常時の復帰処理。
普段何気なく使っているマウスやキーボードの裏側にも、これだけの仕組みがある。
そして、それらを組み合わせたアプリの初期版を、Codexとの開発によってわずか数時間で形にできた。
もちろん、AIがコードを書いてくれるからといって、設計や実機での検証が不要になるわけではない。
むしろ、「何を実現したいのか」「どこまで安全性を確保するのか」「何をもって完成と判断するのか」という部分は、より重要になってくるのだと思う。
最初は単純に、自分のWindows PCとiPadをもっと便利に使いたいというところから始まった。
同じような環境で困っている人がいたら、ぜひ使ってみてほしい。
Universal BridgeはGitHubでソースコードを公開している。IssueやPull Requestも歓迎している。
