Pico WのBluetoothでキーボードを手軽に無線化

# 0. はじめに
[以前の記事](https://qiita.com/BParound30/items/648ec5f4ccdb6e48e2dd)で作ったオリジナルキーボード、ポータブル性にはにすっかり満足していたが、これをiPhoneやiPadで使いたい欲が出てきた。ただ、通常の有線キーボードを単にLightning-USB(Type-C)ケーブルでつないでも、iPad側ではそれをキーボードと認識してくれない。どうやら有線でつなごうとすると、[こちら記事](https://note.com/mym4t/n/nfe2cf405d2a4#Yqphc)のようなカメラアダプタが必要らしい…

本記事では、カメラアダプタではなくBluetooth接続を選択した経緯と、その実現方法や苦労した点、参考になったプロジェクトなどを備忘録として書き残す。

# 1. 成果物

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">インターフェース1月号でPico WのBluetoothが解禁になったと知って、有線キーボードを無線化できる外付けデバイスを作ってみた<br><br>せっかくなのでiPhoneに試し打ちした様子をキャプチャ<br><br>次はこれを収める箱でも設計しようかな<a href="https://twitter.com/hashtag/%E3%82%AD%E3%83%BC%E3%83%9C%E3%83%BC%E3%83%89?src=hash&amp;ref_src=twsrc%5Etfw">#キーボード</a><a href="https://twitter.com/hashtag/%E8%87%AA%E4%BD%9C%E3%82%AD%E3%83%BC%E3%83%9C%E3%83%BC%E3%83%89?src=hash&amp;ref_src=twsrc%5Etfw">#自作キーボード</a><a href="https://twitter.com/hashtag/%E7%84%A1%E7%B7%9A%E5%8C%96?src=hash&amp;ref_src=twsrc%5Etfw">#無線化</a><a href="https://twitter.com/hashtag/Bluetooth?src=hash&amp;ref_src=twsrc%5Etfw">#Bluetooth</a><a href="https://twitter.com/hashtag/BLE?src=hash&amp;ref_src=twsrc%5Etfw">#BLE</a> <a href="https://t.co/03egdImwkg">pic.twitter.com/03egdImwkg</a></p>&mdash; アラサーBP (@BParound30) <a href="https://twitter.com/BParound30/status/1747519435337081238?ref_src=twsrc%5Etfw">January 17, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

![IMG_3242.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3362399/c672c853-9483-03d7-27a1-b2e37d51bbc0.jpeg)

[ソースコード](https://github.com/taichan1113/picow_ble_hid_keyboard)

# 2. なぜBluetoothか（主観）

iPhone/iPadは通常手に持って使う。手に持った時にケーブルがついているとものすごく邪魔だ。特にiPadにApple Pencilという最強の組み合わせに、キーボードの有線ケーブルが割って入るスキなどなし。

要は **「iPadには手書きもタイピングもしたいが、iPad本体には何も有線接続したくない」** のである。Bluetooth接続だとこの課題が解決できそうだ。

# 3. 下調べ
キーボードのBluetooth化には、[BLE Micro Pro（BMP）](https://shop.yushakobo.jp/products/ble-micro-pro)をPro Microの代わりに使用することが主流だ。
しかし、Pro Micro用に設計したキーボード基板にしか使えないことから、筆者のキーボード（RP2400-Zeroを使用）は適用外。Pro Micro用に新たに基板設計するのも面倒だし、普段のPC利用では有線派なので、あくまで外付けデバイスという位置づけで、まずは類似した商品・プロジェクトがないか調べてみた。

::: note info
お気に入りのキーボード（Pro Micro使用）が複数ある場合、すべてBMPにするのはそれなりにコストがかかってしまう。外付けデバイスが1つあれば、少々不格好ではあるが、手軽に無線化が試せるメリットがある。また、デバイスを1から自作する楽しさもこのプロジェクトの醍醐味だ。
:::

::: note warn
もちろんBMPはそれ自体で素晴らしいプロダクトであり、今回は単に、筆者の利用シーンに合わなかったというだけのことである。いつか試してみたい。
:::

## 3-1. 既存品を探す
「キーボード 無線化」で調べると[外付けバッテリーで給電するタイプのデバイス](https://sole-game-creater.com/realforce-wireless/)が見つかった。これはこれで悪くないが、給電用のケーブルと合わせて2本ケーブルが必要なのが煩わしそうだ。もう少し調べる。

## 3-2. 自作する方法を探す
### 3-2-1. 既存プロジェクト1
[こちらの記事](https://www.thun-techblog.com/index.php/blog/esp32-and-fullscrached-keyboard/#google_vignette)はかなりやりたいことに近かった。最初はこの記事をまねることを考えていたが、記事後半の課題点（以下要約）が気になった。
- 入力遅延があって、ややもたつく
- 接続切断時に入力されっぱなし状態
- 消費電力が大きい
- USB接続した状態でペアリングすると2重入力になる

特にこの方法では、UARTによるシリアル通信でキーボードのマイコンから別のマイコンに信号を送っているが、筆者のキーボードマイコンではすでにUARTポートをキーマトリクスに割り当ててしまっており、別の手段で代替する必要があった。

### 3-2-2. 既存プロジェクト2
さらに下調べを続けていると、海外（中華圏？）の方で[こちらの動画](https://www.youtube.com/watch?v=RnpVAqOmc8E&t=2s)をみつけた。ほぼドンピシャだったので、まずはこの動画を再現することとした。概要欄に[ソースコード](https://rfwumcu.blogspot.com/2023/04/raspberry-pi-pico-w-btstack-ep-3-hid.html)も掲載されており、その後の開発にかなり役立った。実装してみて気になった点は以下。
- Bluetooth Classic での接続であり、消費電力がBLE（Bluetooth Low Energy）より劣る
- なぜか自分の環境では、最初のペアリング後の動作が不安定だった（いったん遮断後、再接続すると安定化）
- （確認してないが、Bluetooth Classicなので毎回ペアリングが必要？）

BMPという先行技術があったので、「キーボード接続はBLE」という感覚で、このプロジェクトをBLE化することを自身のプロジェクトとした。

::: note warn
上記の動画、よく見るとPico W を駆動しているのは3.7Vの電池のようだが、[Pico Wのデータシート](https://datasheets.raspberrypi.com/picow/pico-w-datasheet.pdf)を読むと、「USBホストとして利用する場合は、VBUSに5V供給してください」とある。~~4000年の歴史のなせる技術~~ キーボードぐらいなら3.7Vでも動くのかもしれないが、動作不良の原因になりかねないので、本プロジェクトでは5V確保したい。
:::

# 4. プロジェクトの要件と方策
## 4-1. 要件の整理
ここまでのことなども踏まえて、要件をひとまず整理してみた。
|要件|ねらう効果|
|---|---|
|キーボード側マイコンのファームウェアは書き換え不要|自作キーボードだけでなく、市販品（例えばリアルフォース）でも適用できる|
|外付けのデバイスでありながら、キーボードを駆動する電力も供給する|コンセント・モバイルバッテリーいらず|
|BLE対応|省電力かつ、最初だけペアリングすればOK|
|手のひらサイズ|持ち運びの心的負担が少ない|
|外付け方法は、有線用のUSBポートを使う|USB/BTの2重入力をハード的に不可能にする|

## 4-2 方策の検討
[下調べ](#3-下調べ)と並行してマイコンの情報収集をしていると、[Interfaceの1月号](https://interface.cqpub.co.jp/magazine/202401/)にて、Pico WでBluetoothができるようになったらしいことを知る。Pico/Pico W はUSBホストとしても動作するので、2つを合わせれば要件に沿って目的が実現できそうだ。

実現方法はこんなイメージ。
![Composition.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3362399/67d25c21-22cd-561a-1f12-7e9434adeaa7.png)

また、先述のとおり5Vの供給源が必要だが、これもいろいろ調べていると秋月で[5Vレギュレータキット](https://akizukidenshi.com/catalog/g/g112146/)をみつけ、これを採用することにした。

# 5. 苦労話

## 5-1. 環境構築
初歩的なところだが、ビルド環境につまづいた。Windowsで直接ビルドしようとしたがうまくいかず、あまり時間をかけたくなかったので、今回は公式推奨のラズパイ4Bでビルドすることにした。
参照：[Raspberry Pi Pico をセットアップしよう](https://datasheets.raspberrypi.com/pico/getting-started-with-pico-JP.pdf)

## 5-2. Bluetoothに関する知識と公式SDKの実装方法

この辺りは先述のInterface記事にて必要最低限の知識が得られたが、筆者はネットワーク関連（特にBluetoothは初見）に疎く、理解できるまで何度も読み直す必要があった。
特に理解に時間がかかった/実装時つまずいたことを箇条書きにメモしておく。
- Bluetooth Classic と Low Energy は通信スタックが違う（ので実装方法も違う）
    - [既存プロジェクト2](#3-2-2-既存プロジェクト2)がClassicだったので、変数設定ぐらいでBLEにできるやろ！と思っていた時期が筆者にもありました…
- BTStack(blue kitchen)のアーキテクチャとパケットハンドラの実装
    - この辺は正直まだよくわかってないので、ソースコードに不備・冗長箇所がある可能性大。
    - パケットハンドラに関しては、サンプルの理解やコードの実装・デバッグに最も時間をとられた箇所なので、[後に詳述](#6-ソースコードの実装)する。
- pico-sdkとpico-examplesのリポジトリ関係とCMakeによるつながり
    - はじめ、pico-examplesにソースコードがあると思ってたが、実際はCMakeListのみで、ソースコードはpico-sdk/lib/btstack/exampleにある。
- ヘッダーファイル（profile_data）の生成
    - pico-examplesのビルドでは不要だが、BLEで固有のプロジェクトをビルドするときにはGATTサービスを定義したヘッダファイルを作成する必要がある。<プロジェクト名>.gattファイルを定義し、pico-sdk/lib/BTstack/tool/comple_gatt.pyを実行してヘッダーファイルを生成しておかないと、変数（profile_data）未定義でエラーが起こる。

## 5-3. デバッグ方法
編集したコードが一発でうまく行くわけなく、何回か編集→ビルド→失敗を経てあきらめてデバッグ環境を用意した。
USBポートはキーボードとの接続に使うので、UARTでPico W ↔ ラズパイ4B を通信してデバッグすることとした。
1. プロジェクト直下の`CMakeList.txt`に以下のように設定
    ```CMakeLists.txt
    pico_enable_stdio_uart(picow_ble_hid_keyboard 1)
    pico_enable_stdio_usb(picow_ble_hid_keyboard 0)
    ```
1. 同じく直下の`btstack_config.h`に下記を設定（[デフォルト(?)](https://github.com/raspberrypi/pico-examples/blob/master/pico_w/bt/config/btstack_config.h)では既に設定済み）
    ```btstack_config.h
    #define ENABLE_LOG_INFO
    #define ENABLE_LOG_ERROR
    #define ENABLE_PRINTF_HEXDUMP // printf関数の出力がUART通信でラズパイ4B側に表示される
    ```

1. ラズパイ側のUARTを有効にして再起動（[公式ガイド](https://datasheets.raspberrypi.com/pico/getting-started-with-pico.pdf)の4.5章参照）
1. 下図のようにピンを接続して
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3362399/477b9bc2-1b99-c1c1-6ad2-abd2bf0143d5.png) 
1. ラズパイのターミナルで下記を実行
    ```
    $minicom -b 115200 -o -D /dev/serial0
    ```

# 6. ソースコードの実装
職業プログラマではないのでソースコードには自信がないが、[ここ](https://github.com/taichan1113/picow_ble_hid_keyboard)に公開しておく。公式SDKのexampleを切り貼りしてなんとか動作させた（いわゆるコピペプログラム）が、改良案があれば教えてほしいぐらいだ。

以下、筆者の理解の範疇でコードのさわり部分を説明するが、誤りがあれば記事へのコメントや[X](https://twitter.com/BParound30)で教えてほしい（切望）。

実装の詳細の前に、[公式example](https://github.com/bluekitchen/btstack/blob/272986f17af35a67815ab20897f6c91e710322a4/example/hog_keyboard_demo.c)との違いを整理すると以下。
- キーボードのマイコンからの入力をUSBで受け取っている
- 受け取った入力をHID(Human Interface Device)レポートとしてBluetoothで送信する

したがって、 [既存プロジェクト2](#3-2-2-既存プロジェクト2)と同様に、USB入力をキューに入れてそれをBluetoothの実行ループ内にて送信処理する、という方針で実装した。

SDK意外に用意したファイルはこんな感じ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3362399/856637e4-2029-1e39-962d-a7d771e1b9b0.png)

## 6-1. パケットハンドラの実装

下記のように、`packet_handler`は`packet`で受け取るイベントの種類によって処理を行う（状態マシン？）。デバッグによってペアリングが完了して接続が確立されたら`case HIDS_SUBEVENT_INPUT_REPORT_ENABLE:`に、それ以降のHIDレポート送信可能時は`case HIDS_SUBEVENT_CAN_SEND_NOW:`状態に入ることが分かった。

```btstack_hog_kb.c
static void packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size)
{
    UNUSED(channel);
    UNUSED(size);

    if (packet_type != HCI_EVENT_PACKET)
        return;

    switch (hci_event_packet_get_type(packet))
    {
//...（略）...
    case HCI_EVENT_HIDS_META:
        switch (hci_event_hids_meta_get_subevent_code(packet))
        {
        case HIDS_SUBEVENT_INPUT_REPORT_ENABLE: //接続確立
            con_handle = hids_subevent_input_report_enable_get_con_handle(packet);
            printf("Report Characteristic Subscribed %u\n", hids_subevent_input_report_enable_get_enable(packet));
            btstack_run_loop_remove_timer(&typing_timer); //再接続時にタイマーを（あれば）リセットする？
            btstack_run_loop_set_timer_handler(&typing_timer, typing_timer_handler);
            btstack_run_loop_set_timer(&typing_timer, TYPING_PERIOD_MS);
            btstack_run_loop_add_timer(&typing_timer);
            break;
        case HIDS_SUBEVENT_BOOT_KEYBOARD_INPUT_REPORT_ENABLE:
            con_handle = hids_subevent_boot_keyboard_input_report_enable_get_con_handle(packet);
            printf("Boot Keyboard Characteristic Subscribed %u\n", hids_subevent_boot_keyboard_input_report_enable_get_enable(packet));
            break;
        case HIDS_SUBEVENT_PROTOCOL_MODE:
            protocol_mode = hids_subevent_protocol_mode_get_protocol_mode(packet);
            printf("Protocol Mode: %s mode\n", hids_subevent_protocol_mode_get_protocol_mode(packet) ? "Report" : "Boot");
            break;
        case HIDS_SUBEVENT_CAN_SEND_NOW: //HIDレポート送信可能状態
            printf("can send now \n");
            btstack_run_loop_set_timer(&typing_timer, TYPING_PERIOD_MS);
            btstack_run_loop_add_timer(&typing_timer);
            break;
        default:
            break;
        }
        break;

    default:
        break;
    }
}
```

接続確立時に`typing_timer_handler`を設定して、実行ループにタイマーをset(`btstack_run_loop_set_timer`)・add(`btstack_run_loop_add_timer`)する。電力遮断でpico wをOFFした後再びONにするとうまくキー入力を送れず、一番つまずいたところであるが、`btstack_run_loop_remove_timer`を接続時処理に追加することで修正できた。
接続確立以降はHIDレポート送信可能状態になるたびにタイマーをset・addすることで、定期的に`typing_timer_handler`を呼び出す。

## 6-2. タイマーハンドラーの実装
`typing_timer_handler`は下記のように、USB入力でキューに送られたHIDレポートをBTstackのコールバック関数`hids_device_send_input_report`でBluetoothホスト（PCやiPad）に送信し、`hids_device_request_can_send_now_event`でHIDレポート送信可能状態に状態遷移させる。

```btstack_hog_kb.c
static void typing_timer_handler(btstack_timer_source_t *ts)
{
    // get keycode and send
    struct
    {
        uint8_t modifier;   /**< Keyboard modifier (KEYBOARD_MODIFIER_* masks). */
        uint8_t reserved;   /**< Reserved for OEM use, always set to 0. */
        uint8_t keycode[6]; /**< Key codes of the currently pressed keys. */
    } report_q;

    if (queue_try_remove(&hid_keyboard_report_queue, &report_q)){
        uint8_t report[] = { report_q.modifier, 0, 
                    report_q.keycode[0],report_q.keycode[1],report_q.keycode[2],report_q.keycode[3],report_q.keycode[4],report_q.keycode[5]};
        hids_device_send_input_report(con_handle, report, sizeof(report));
    }

    hids_device_request_can_send_now_event(con_handle);
}
```

## 6-3. USB入力キューの実装
pico wのUSBホストとしての振る舞いも、[別のexample](https://github.com/hathach/tinyusb/blob/86c416d4c0fb38432460b3e11b08b9de76941bf5/examples/host/cdc_msc_hid/src/hid_app.c)に手を加え、下記のように`process_kbd_report`関数内でHIDレポートのキューを加えている。

```tusb_hid.c
static void process_kbd_report(hid_keyboard_report_t const *report)
{
  queue_try_add(&hid_keyboard_report_queue, report); 
  //本実装ではキーボードのHIDレポートのみキューに入れているので、マウス操作やメディアキー等には対応していない。
  return;
  
  static hid_keyboard_report_t prev_report = { 0, 0, {0} }; // previous report to check key released

  //以下略
}
```

# 7. 留意点
実装後、特に大きな問題点もなく運用できており、現状この自作デバイスに大変満足しているが、気になる点がないわけではない。

## 7-1. 消費電力
無線化で多くの人が関心を持つ部分だと思うが、まぁ期待しないでほしい。BMPやXIAO BLEでも用いられている、Nordicのnrf52840というBTモジュールに比べると、pico Wのものは消費電力が大きいと聞いたこともある。[写真](#1-成果物)の構成で800mAhの9V型リチウムイオンバッテリーを使ってるが、1日は持つかな、程度でこまめな充電が必要。

ソースコード自体も特に電力マネジメントはしておらず、あくまでもちょっとした外出にお気に入りのキーボードを無線環境に持ち出したり、日替わり無線キーボードという遊び方ができるデバイスという付き合い方をしている。

## 7-2. 配線
コンパクトなケースにしたかったので、パーツを詰め詰めにした結果、配線周りのメンテナンスがしずらくなってしまった。シールド基板を設計すればもう少しマシになるかもしれないが、面倒なのでやる予定は今のところない。

# 8. あとがき
手前みそではあるが、遊びとしてはなかなか面白く便利なものが作れた。この記事を読まれた方も是非遊んでみてほしいし、ソースコードの改善案は大歓迎である（改善意見を義務に感じる必要はない）。

また、Pico WにはIOが30個もある！これをベースに機能拡張してみるのも面白そうだ（電力消費をあきらめててそっち方面に発展していく未来もあり得る？）。

個人的には見た目にもこだわってケースも設計したので、職場仲間に自慢したいが、在宅が多いので機会がまだ訪れていない。ケースはほぼ3Dプリンタ出力なので、要望があればデータ公開することも厭わない。ただし3mm厚のアクリル板の切削と、M2ネジ穴をあける作業をすべて手作業でする場合、握力が全部持っていかれ、晩御飯で箸をつかむ腕がプルプル震えること請け負いである。


# 参考資料
[ESP32と自作キーボードでBluetooth/QMK/VIA対応のキーボードを作る](https://www.thun-techblog.com/index.php/blog/esp32-and-fullscrached-keyboard/#google_vignette)
[HID device, Convert a USB Cabled Keyboard to a Bluetooth Wireless Keyboard](https://rfwumcu.blogspot.com/2023/04/raspberry-pi-pico-w-btstack-ep-3-hid.html)
[Interface 2024年1月号：PCやスマホからマイコンI/O［Bluetooth＆Wi-Fi］Pico W](https://interface.cqpub.co.jp/magazine/202401/)
[Interface : ラズベリー・パイPico/Pico W攻略本](https://interface.cqpub.co.jp/magazine/2023pico/)
[Raspberry Pi Picoをセットアップしよう](https://datasheets.raspberrypi.com/pico/getting-started-with-pico-JP.pdf)
[Raspberry Pi Pico W Datasheet](https://datasheets.raspberrypi.com/picow/pico-w-datasheet.pdf)
[BTstack Manual](https://bluekitchen-gmbh.com/btstack/#)