Raspberry Pi Zero 2 W でキーボード入力 ～Bluetooth キーボード入力との統合～ 

---

# はじめに

本記事は、[前回の記事](https://qiita.com/sukimaengineer/items/7e49d60ab23962c97428) をベースに、**Raspberry Pi Zero 2 W** を **USB Gadget (g_hid)** として仮想キーボード化し、

1. **ネットワーク経由 (gRPC + Protobuf) で受信した文字列** をターゲットPCへ“キーボード入力”として送信  
2. **Bluetooth キーボードからのキー入力**（Modifierキーや複数キー同時押下を含む）もターゲットPCへ送信  

を**同時に**実現します。  
さらに、**Print Screen**・**Scroll Lock**・**Pause/Break** などの特殊キーにも対応し、**JIS配列向けとしてほぼ完成形の実装例**を紹介します。  
参考として、最後にUS配列向けのコード例も追加しています。  
また、**「Pi 側ではBluetooth キーボードからのキー入力を受け取りたくない（USB HIDへの入力のプロセス専用にする）」** 場合の `dev.grab()` の使い方やプロセス終了時の動作なども解説します。

> 注: 本記事のサンプルスクリプトでは、Python の evdev ライブラリを使用します。  
> Raspberry Pi OS には標準でインストールされていない場合が多いため、事前に  
> ```bash
> sudo apt update && sudo apt install python3-evdev
> ```  
> あるいは  
> ```bash
> pip3 install evdev
> ```  
> などで導入してください。  
> また、実際に発生するキーコード (ecodes.KEY_???) はキーボードやOSにより微妙に異なる場合があるため、`evtest` などで実機確認し、必要に応じてマップを調整してください。

---

## 1. 前回の設定が済んでいれば

前回の記事で、  
- **USB Gadget (g_hid) 設定**: Pi Zero 2 W がターゲットPCから見て「USBキーボード」と認識される状態  
- **gRPC サーバ**: ネットワークで受け取った文字列 → `/dev/hidg0` に書き込む仕組み  

が既に整っている場合、本記事の **Bluetooth キーボード入力スクリプト** を追加するだけで、以下の2つの入力ソースが同時に `/dev/hidg0` へ書き込めるようになります。

1. **ネットワーク入力** (gRPC)  
2. **Bluetooth キーボード入力** (evdev)

ターゲットPC側から見ると、**単一のUSBキーボード**が同時に2種類の入力を送信してくるイメージです。
なお、本記事は前提として「BluetoothキーボードがPiに接続済み」である必要があります。

---

## 2. システム全体の概要

### 2.1 構成

以下の図は、システム全体を示しています。

```mermaid
flowchart TD
    subgraph 送信元PC
            I[gRPCクライアント<br>テキスト送信]
    end
    subgraph Bluetooth キーボード
            J[キーを押す]
    end
    subgraph Raspberry Pi Zero 2 W
        A[gRPCサーバ<br>テキスト受信]
        B[Bluetooth<br>キーボード入力<br>evdev]
        subgraph プロセスA
            D[ネットワークから<br>受け付けるプロセス<br>で HID レポート生成]
        end
        subgraph プロセスB
            G[Bluetooth キーボードから<br>受け付けるプロセス<br>で HID レポート生成]
        end
        E["/dev/hidg0"<br>USB HID デバイス]
        subgraph  Pi OS への入力
            H[Raspberry Pi Zero 2 W<br>への通常のキーボード入力]
        end
        K(["**[注] /dev/hidg0 への<br>書き込みが競合します<br>7.1 競合対策を参照**"])
    end
    F[ターゲットPC<br>USBホスト<br>キーボードとして認識]

    A -- "文字列→キーコード変換" --> D
    B -- "キーイベント取得<br>grab() で占有可能" --> G
    D -- "書き込み" --> E
    G -- "書き込み" --> E
    E -- "USB OTG" --> F
    B -- "grab() で遮断可能" --> H
    I -- "Wi-Fi" --> A
    J -- "Bluetooth" --> B
```

- **A**: ネットワーク越しの文字列を受け取り、1文字ずつキーコードに変換  
- **B**: Bluetooth キーボードの押下情報を evdev で取得  
- **D**: 各入力ソースを取りまとめ、ターゲットPCへ送るHIDレポートを生成  
- **E**: `/dev/hidg0` に対して HID レポートを書き込み  
- **F**: ターゲットPCは「USBキーボード」として認識し、キー入力が反映される

### 2.2 メリット

1. **ネットワーク入力** と **物理キーボード入力** をまとめて扱える  
2. USB OTG 機能のおかげで、ターゲットPC側に専用ドライバは不要（標準的なUSBキーボードとして動作）  
3. Bluetooth キーボードを Pi 側でも使えるし、**必要に応じて Pi 側で受け取らず HID 専用にする**こともできる

---

## 3. システム構築手順の要点

| ステップ                           | 内容                                                                     | 補足                                                                                  |
|------------------------------------|-------------------------------------------------------------------------|---------------------------------------------------------------------------------------|
| **1. USB Gadget 設定 (前回記事)** | `/usr/local/bin/setup_hid_keyboard.sh` などで Pi を「USBキーボード」に設定 | ターゲットPCが Pi を USBキーボードとして認識する                                    |
| **2. gRPC サーバ (前回記事)**     | ネットワークで文字列受信 → 1文字ずつ `/dev/hidg0` に書き込む               | リモート入力をキーボード入力にエミュレート                                          |
| **3. Bluetooth 入力スクリプト**   | evdev でキーイベントを取得 → HID レポート生成 → `/dev/hidg0` に送る         | Modifierキーや複数同時押し、特殊キーにも対応 (本記事)                               |
| **4. systemd サービス**           | スクリプトを自動起動                                                     | Pi のブート時から常時キーボードを監視                                               |
| **5. (任意) dev.grab()**          | Pi 側の入力を「奪って」HID専用にする                                      | grab を呼ばなければ Pi OS でも通常のキーボード入力として使え、呼ぶ場合は Pi OS への入力が遮断される  |
| **6. 競合対策 (必要に応じて)**     | gRPC と Bluetooth が同時書き込みする場合、Lock やキューを検討              | **複数プロセスによる同時アクセスが想定される場合は、必ず排他ロック（例: Python の threading.Lock）を導入してください。** |

---

## 4. dev.grab() を使って Pi 側への入力を遮断する方法

### 4.1 デフォルト状態（grab なし）

通常は、Bluetooth キーボードを接続すると、**Pi OS としても使え**、同時に evdev スクリプトでも読み取れます。  
この状態だと、Pi のコンソールやGUI操作にも影響し、キーボード入力として反映されます。

### 4.2 dev.grab() の動作

evdev の `dev.grab()` を呼び出すと、  
- **そのプロセスが動作中の間**、入力デバイスが**排他ロック**状態になり、Pi OS の標準入力には送られず、USB HID へのみレポートが出力されます。

```python
dev = evdev.InputDevice("/dev/input/event3")
dev.grab()   # これを呼ぶと、Pi OS ではキー入力として認識されなくなります
```

**注意**: プロセスが終了するとファイルディスクリプタが閉じられ、  
**自動的に ungrab (アングラブ) されます。**  
そのため **プロセス終了後は Pi OS が再び入力を受け取れる状態に戻ります**。

#### 4.2.1 SSH への影響はない

SSH はあくまでネットワーク経由なので、**grab しても SSH の動作には影響しません**。  
Pi を操作する際は SSH を利用し、物理キーボードはUSB HID出力専用にする、といった使い分けが可能です。

---

## 5. Modifier＋複数キー＋特殊キー対応の実装例

以下は、Modifierキーや最大6キー同時押し、Print Screen/Scroll Lock/Pause なども含めた Python スクリプトの例です。  
**grab()** を呼ぶかどうかは、コメントアウトの有無で制御できます。

> **補足:**  
> - キーコード (ecodes.KEY_???) は実機で `evtest` や `/proc/bus/input/devices` を使用して確認してください。  
> - 環境によっては `ecodes.KEY_CARET` や `ecodes.KEY_AT` の定義が異なる場合があるため、必要に応じてマップを調整してください。  
> - また、**`time.sleep(0.005)` の値は環境によって 0.01～0.02 秒程度に増やすと取りこぼしが減る場合があります**。必要に応じて調整してください。

```python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
JIS配列の物理キーボード入力 (evdev) を取得し、
/dev/hidg0 に対してHIDレポートを書き込むサンプル。

- Raspberry Pi Zero 2 W (g_hid でUSBキーボード化) などで利用。
- JIS配列を意識したKEYCODE_MAPを定義（ホストOSがJISキーボードとして認識している前提）。
- Modifierキー(Shift,Ctrl,Alt,Meta) や最大6キー同時押しに対応。
- Print Screen / Scroll Lock / Pause 等の特殊キーにも対応。
- CapsLockやNumLock、矢印キー、には未対応。
- 半角/全角キー、変換、無変換、カナなど日本語特有のキーも一部定義例あり（※実機要確認）。
"""

import time
import struct
import evdev
from evdev import ecodes

#=========================================================================
# 1. Modifierキー として扱うキーコード → ビットマスク
#=========================================================================
MODIFIER_MAP = {
    ecodes.KEY_LEFTCTRL:   0x01,
    ecodes.KEY_LEFTSHIFT:  0x02,
    ecodes.KEY_LEFTALT:    0x04,
    ecodes.KEY_LEFTMETA:   0x08,  # Windowsキー or Commandキー
    ecodes.KEY_RIGHTCTRL:  0x10,
    ecodes.KEY_RIGHTSHIFT: 0x20,
    ecodes.KEY_RIGHTALT:   0x40,  # AltGr
    ecodes.KEY_RIGHTMETA:  0x80,
}

#=========================================================================
# 2. 通常キー (JIS配列を想定した例)
#=========================================================================
KEYCODE_MAP = {
    #----------------------------------------------------
    # アルファベット A-Z
    #----------------------------------------------------
    ecodes.KEY_A: 0x04, ecodes.KEY_B: 0x05, ecodes.KEY_C: 0x06,
    ecodes.KEY_D: 0x07, ecodes.KEY_E: 0x08, ecodes.KEY_F: 0x09,
    ecodes.KEY_G: 0x0A, ecodes.KEY_H: 0x0B, ecodes.KEY_I: 0x0C,
    ecodes.KEY_J: 0x0D, ecodes.KEY_K: 0x0E, ecodes.KEY_L: 0x0F,
    ecodes.KEY_M: 0x10, ecodes.KEY_N: 0x11, ecodes.KEY_O: 0x12,
    ecodes.KEY_P: 0x13, ecodes.KEY_Q: 0x14, ecodes.KEY_R: 0x15,
    ecodes.KEY_S: 0x16, ecodes.KEY_T: 0x17, ecodes.KEY_U: 0x18,
    ecodes.KEY_V: 0x19, ecodes.KEY_W: 0x1A, ecodes.KEY_X: 0x1B,
    ecodes.KEY_Y: 0x1C, ecodes.KEY_Z: 0x1D,

    #----------------------------------------------------
    # 数字キー (上段)
    #----------------------------------------------------
    ecodes.KEY_1: 0x1E, ecodes.KEY_2: 0x1F, ecodes.KEY_3: 0x20,
    ecodes.KEY_4: 0x21, ecodes.KEY_5: 0x22, ecodes.KEY_6: 0x23,
    ecodes.KEY_7: 0x24, ecodes.KEY_8: 0x25, ecodes.KEY_9: 0x26,
    ecodes.KEY_0: 0x27,

    #----------------------------------------------------
    # 改行, バックスペース, Esc, Tab, Space
    #----------------------------------------------------
    ecodes.KEY_ENTER:      0x28,
    ecodes.KEY_ESC:        0x29,
    ecodes.KEY_BACKSPACE:  0x2A,
    ecodes.KEY_TAB:        0x2B,
    ecodes.KEY_SPACE:      0x2C,

    #----------------------------------------------------
    # 記号キー (JIS配列版)
    #----------------------------------------------------
    ecodes.KEY_MINUS:      0x2D,  # '-' (SHIFT時に '=')
    ecodes.KEY_CARET:      0x2E,  # '^' (SHIFT時に '~')
    ecodes.KEY_AT:         0x2F,  # '@' (SHIFT時に '`')
    ecodes.KEY_LEFTBRACE:  0x30,  # '[' (SHIFT時に '{')
    ecodes.KEY_YEN:        0x31,  # '¥' (SHIFT時に '|')
    ecodes.KEY_SEMICOLON:  0x33,  # ';' (SHIFT時に ':')
    ecodes.KEY_COLON:      0x34,  # 環境により未定義の場合も
    ecodes.KEY_GRAVE:      0x35,  # '`' など
    ecodes.KEY_COMMA:      0x36,  # ',' (SHIFTで '<')
    ecodes.KEY_DOT:        0x37,  # '.' (SHIFTで '>')
    ecodes.KEY_SLASH:      0x38,  # '/' (SHIFTで '?')

    #----------------------------------------------------
    # ファンクションキー
    #----------------------------------------------------
    ecodes.KEY_F1:  0x3A, ecodes.KEY_F2:  0x3B, ecodes.KEY_F3:  0x3C,
    ecodes.KEY_F4:  0x3D, ecodes.KEY_F5:  0x3E, ecodes.KEY_F6:  0x3F,
    ecodes.KEY_F7:  0x40, ecodes.KEY_F8:  0x41, ecodes.KEY_F9:  0x42,
    ecodes.KEY_F10: 0x43, ecodes.KEY_F11: 0x44, ecodes.KEY_F12: 0x45,

    #----------------------------------------------------
    # Print Screen / Scroll Lock / Pause
    #----------------------------------------------------
    ecodes.KEY_SYSRQ:      0x46,  # PrintScreen
    ecodes.KEY_SCROLLLOCK: 0x47,
    ecodes.KEY_PAUSE:      0x48,

    #----------------------------------------------------
    # 日本語特有キー (変換, 無変換, 全角/半角, カナ 等)
    #----------------------------------------------------
    ecodes.KEY_HENKAN:         0x8A,
    ecodes.KEY_MUHENKAN:       0x8B,
    ecodes.KEY_ZENKAKUHANKAKU: 0x91,
    ecodes.KEY_KATAKANA:       0x92,
}

#==============================================================================
# build_hid_report(modifier_byte, pressed_keys):
#   - modifier_byte: 1バイト (各ModifierキーのビットをORした値)
#   - pressed_keys:  同時押しキーのUsage ID (最大6個まで)
#   → 8バイトのHIDレポート (固定フォーマット) をbytesで返す
#==============================================================================
def build_hid_report(modifier_byte, pressed_keys):
    report = [0]*8
    report[0] = modifier_byte
    report[1] = 0x00

    idx = 2
    for usage_id in list(pressed_keys)[:6]:
        report[idx] = usage_id
        idx += 1

    return struct.pack('8B', *report)

def main():
    # ※注意: 接続されている入力デバイスのパスは環境により異なります。
    # ls /dev/input/event* や evtest でご使用のデバイスを確認してください。
    device_path = "/dev/input/event3"
    dev = evdev.InputDevice(device_path)

    print(f"Using input device: {device_path} ({dev.name})")
    # HID Gadget (ターゲットPCへ送る先)
    hidg_path = "/dev/hidg0"
    print(f"Writing HID reports to: {hidg_path}")

    # dev.grab() を呼ぶと、Pi 側への入力を遮断しUSB HID専用にできます。
    # dev.grab()

    pressed_keys = set()
    modifier_state = 0x00

    with open(hidg_path, "wb") as hid:
        for event in dev.read_loop():
            if event.type == ecodes.EV_KEY:
                # event.value: 0=key up, 1=key down, 2=key repeat
                if event.value not in (0,1):
                    continue
                is_pressed = (event.value == 1)

                if event.code in MODIFIER_MAP:
                    bitmask = MODIFIER_MAP[event.code]
                    if is_pressed:
                        modifier_state |= bitmask
                    else:
                        modifier_state &= ~bitmask
                elif event.code in KEYCODE_MAP:
                    usage_id = KEYCODE_MAP[event.code]
                    if is_pressed:
                        pressed_keys.add(usage_id)
                    else:
                        pressed_keys.discard(usage_id)

                report = build_hid_report(modifier_state, pressed_keys)
                hid.write(report)

                # 環境によっては 0.01～0.02 秒程度に増やすと安定する場合があります
                time.sleep(0.005)

if __name__ == "__main__":
    main()
```

---

## 6. systemd サービスで自動起動

このスクリプトを `/usr/local/bin/bluetooth_input.py` として保存し、  
```bash
sudo chmod +x /usr/local/bin/bluetooth_input.py
```  
を付与後、以下のような systemd サービスを作成してください。

```bash
sudo vi /etc/systemd/system/bluetooth-input.service
```

```ini
[Unit]
Description=Bluetooth Keyboard Input Service
After=hidgadget.service
Requires=hidgadget.service

[Service]
Type=simple
ExecStart=/usr/local/bin/bluetooth_input.py
Restart=always

[Install]
WantedBy=multi-user.target
```

次に、以下のコマンドでサービスを有効化・起動します。

```bash
sudo systemctl daemon-reload
sudo systemctl enable bluetooth-input.service
sudo systemctl start bluetooth-input.service
```

再起動（`sudo reboot`）後、Pi は自動でスクリプトを起動し、Bluetooth キーボード入力が `/dev/hidg0` に書き込まれます。

---

## 7. gRPC サーバとの統合

**前回記事**で構築した **gRPC サーバ**（ネットワーク → `/dev/hidg0`）を同時に動かすと、以下2つが並行して `/dev/hidg0` へ書き込みます。

1. **Bluetooth 入力スクリプト**  
2. **ネットワーク入力 (gRPC)**

ターゲットPCに対しては単一のUSBキーボード入力として届くため、リモート文字列入力と物理キーボード入力が混在して送信されます。

### 7.1 競合対策

同時アクセスによりHIDレポートが上書きされ、キー入力が混ざる恐れがあります。必要に応じて以下のいずれかを導入する必要があります。（なお、今回は趣味レベルなので、gRPC クライアントから送るときは、必ず手を離してキーボードを押さない、という運用で対策しています。）
- 別プロセスの場合: `multiprocessing.Lock` やファイルロック (fcntl) などを利用し、両方のプロセスが共有できる手段でロックを実装する。
- 同一プロセスにした場合: `threading.Lock` などで `/dev/hidg0` への書き込み区間を保護する。
- 1プロセス統合: gRPC サーバと Bluetooth 入力を同一プロセス内で管理し、送信を直列化する  
- イベントキュー: 入力イベントをキューに貯め、順番に処理する

> **注意:** 複数プロセスが並行して `/dev/hidg0` に書き込む場合は、厳密なロックやキュー管理を行わないと誤動作の原因となります。

---

## 8. CPU 負荷は少ない

`for event in dev.read_loop():` はブロッキング読み込みのため、キーイベントがないときは待機状態となり、CPU 使用率はほぼ0% です。  
高頻度のキー操作があっても、`time.sleep(0.005)` により書き込みが制御され、CPU 負荷が急激に上がることはありません。

---

## 9. プロセス終了時に grab はどうなる？

`dev.grab()` は該当プロセスが開いている間のみ有効です。プロセス終了時にはファイルディスクリプタが閉じられ、

- **自動的に ungrab (アングラブ)** され、Pi OS が再び通常のキーボード入力として利用できるようになります。

SSH はネットワーク経由のため、この変更による影響はありません。

---

## 10. 「Pi 側での入力有無」を比較する表

以下の表は、本スクリプトにおいて `dev.grab()` を呼ぶ／呼ばないときの違いをまとめたものです。

| 項目                                          | grab() **呼ばない**                                 | grab() **呼ぶ**                                                          |
|-----------------------------------------------|------------------------------------------------------|--------------------------------------------------------------------------|
| **Pi 側でのキー操作**                         | できる（通常のキーボードとして認識）                | できない（OSが入力を受け取らない）                                       |
| **USB HID 出力**                              | 可能（/dev/hidg0 への書き込みは常に行われる）        | 可能（/dev/hidg0 への書き込みは常に行われる）                            |
| **Pi 側への影響**                             | Pi のコンソールや GUI でもキー操作が可能            | Pi は全くキーを受け取らない                                             |
| **プロセス終了後**                            | 変わらず Pi 側は通常利用できる                      | **ungrab** され、Pi OS が再びキー入力を受け取る (プロセス動作中のみ grab 有効) |
| **SSH への影響**                              | 特になし（SSH はネットワーク経由）                 | 特になし（SSH はネットワーク経由）                                       |
| **用途**                                      | Pi 自身も操作したい、かつ USB HID に入力を送りたい場合 | Pi を完全に操作対象外とし、USB HID 専用キーボードにしたい場合              |

---

## 11. 今後の拡張・注意点

1. **日本語配列 (JIS) と実機確認**  
   - CapsLockやNumLock、矢印キー、に現状では未対応です
   - 本記事はJIS配列版として記述していますが、実際のキーボードやOSのレイアウト設定により、円マークや変換キー、無変換キー、カナキーなどのUsage IDが異なる場合があります。**`evtest` 等でキーコードを確認し、必要に応じて調整してください。**
2. **追加の特殊キー・メディアキー**  
   - 音量、ミュートなどが必要な場合は、`KEYCODE_MAP` に対応するキーコードを追加してください。
3. **Nキーロールオーバー (NKRO)**  
   - 8バイトのブートプロトコルは最大6キー同時押しまでの仕様です。Nキー全面対応にはHIDレポート形式の拡張が必要です。
4. **エラーハンドリング**  
   - デバイス切断時の再接続など、長期間の運用時は例外処理を追加することを推奨します。
5. **US配列版との違い**  
   - JIS配列版とUS配列版では、特に記号キーのUsage IDが異なります。使用環境に合わせた調整が必要です（後述のUS配列版の例を参照）。

---

## 12. まとめ

1. **前回の記事の設定（USB Gadget + gRPCサーバ）が完了していれば、本記事の Bluetooth 入力スクリプトを追加するだけで動作**  
   - ネットワーク入力 (gRPC) と Bluetooth 入力 (evdev) が同時に `/dev/hidg0` へ書き込み可能  
2. **Pi 側で入力を使うかどうかは `dev.grab()` の呼び出し次第**  
   - grab() を呼ばなければ、Pi OS 側でも通常のキーボード入力として使えます（併用可能）  
   - grab() を呼ぶと、Pi OS への入力が遮断され、USB HID 出力専用になります（SSH は影響なし）  
   - プロセス終了時に自動的に ungrab され、再び通常の入力が可能になります  
3. **CPU 負荷は低い**  
   - `read_loop()` はキー入力がない間はブロッキング待機となり、`time.sleep(0.005)` により負荷が抑えられます  
   - 環境により `0.01～0.02秒` に増やすと文字の取りこぼしが減ることがあります
4. **競合時の排他制御**  
   - gRPC と Bluetooth 入力を同時運用する場合は、Lock や1プロセス統合などで衝突を防ぐ（趣味の範囲を超える場合は必須）
5. **JIS配列版のベースとしては、そこそこ完成しているが未対応のキーがある**  
   - Modifier＋複数キー＋特殊キー (PrintScreen/ScrollLock/Pause) に対応済み
   - CapsLockやNumLock、矢印キー、に未対応です
   - 日本語配列やメディアキー、NKRO対応などは用途に合わせて追加可能です

**これにより、Raspberry Pi Zero 2 W は“USB キーボード”として多彩な入力ソースを統合でき、Pi 側での使用可否（grab）も柔軟に切り替えられます。**

---

## 13. 参考リンク

- [Linux USB Gadget - ConfigFS Documentation (kernel.org)](https://www.kernel.org/doc/html/latest/usb/gadget_configfs.html)  
- [evdev (Python) ドキュメント](https://pypi.org/project/evdev/)  
- [USB HID Usage Tables (USB.org)](https://usb.org/document-library/hid-usage-tables-121)

---

## 参考 US配列の場合のPythonスクリプト例

```python
#!/usr/bin/env python3
import evdev
import struct
import time
from evdev import ecodes

"""
Modifier(Shift,Ctrl,Alt,Meta) + 複数キー同時押し(最大6キー)対応のサンプル。
/dev/hidg0 へ8バイトのHIDレポートを送信し、ターゲットPCがキーボード入力として認識。
こちらはUS配列のキーコードマッピング例です。
Print Screen, Scroll Lock, Pause/Break にも対応。
"""

# 1) Modifier キーの evdev code → HID modifier ビット対応表
MODIFIER_MAP = {
    ecodes.KEY_LEFTCTRL:   0x01,  # 左Ctrl
    ecodes.KEY_LEFTSHIFT:  0x02,  # 左Shift
    ecodes.KEY_LEFTALT:    0x04,  # 左Alt
    ecodes.KEY_LEFTMETA:   0x08,  # 左Meta(Win)
    ecodes.KEY_RIGHTCTRL:  0x10,  # 右Ctrl
    ecodes.KEY_RIGHTSHIFT: 0x20,  # 右Shift
    ecodes.KEY_RIGHTALT:   0x40,  # 右Alt(AltGr)
    ecodes.KEY_RIGHTMETA:  0x80,  # 右Meta
}

# 2) 通常キーの evdev code → HID Usage ID 対応表（US配列）
KEYCODE_MAP = {
    # 文字キー (アルファベット)
    ecodes.KEY_A: 0x04, ecodes.KEY_B: 0x05, ecodes.KEY_C: 0x06,
    ecodes.KEY_D: 0x07, ecodes.KEY_E: 0x08, ecodes.KEY_F: 0x09,
    ecodes.KEY_G: 0x0A, ecodes.KEY_H: 0x0B, ecodes.KEY_I: 0x0C,
    ecodes.KEY_J: 0x0D, ecodes.KEY_K: 0x0E, ecodes.KEY_L: 0x0F,
    ecodes.KEY_M: 0x10, ecodes.KEY_N: 0x11, ecodes.KEY_O: 0x12,
    ecodes.KEY_P: 0x13, ecodes.KEY_Q: 0x14, ecodes.KEY_R: 0x15,
    ecodes.KEY_S: 0x16, ecodes.KEY_T: 0x17, ecodes.KEY_U: 0x18,
    ecodes.KEY_V: 0x19, ecodes.KEY_W: 0x1A, ecodes.KEY_X: 0x1B,
    ecodes.KEY_Y: 0x1C, ecodes.KEY_Z: 0x1D,

    # 数字キー (上段)
    ecodes.KEY_1: 0x1E, ecodes.KEY_2: 0x1F, ecodes.KEY_3: 0x20,
    ecodes.KEY_4: 0x21, ecodes.KEY_5: 0x22, ecodes.KEY_6: 0x23,
    ecodes.KEY_7: 0x24, ecodes.KEY_8: 0x25, ecodes.KEY_9: 0x26,
    ecodes.KEY_0: 0x27,

    # 記号・特殊キー
    ecodes.KEY_ENTER:     0x28,
    ecodes.KEY_ESC:       0x29,
    ecodes.KEY_BACKSPACE: 0x2A,
    ecodes.KEY_TAB:       0x2B,
    ecodes.KEY_SPACE:     0x2C,
    ecodes.KEY_MINUS:     0x2D,  # '-' or '_'
    ecodes.KEY_EQUAL:     0x2E,  # '=' or '+'
    ecodes.KEY_LEFTBRACE: 0x2F,  # '['
    ecodes.KEY_RIGHTBRACE:0x30,  # ']'
    ecodes.KEY_BACKSLASH: 0x31,  # '\'
    ecodes.KEY_SEMICOLON: 0x33,  # ';'
    ecodes.KEY_APOSTROPHE:0x34,  # '\''
    ecodes.KEY_GRAVE:     0x35,  # '`'
    ecodes.KEY_COMMA:     0x36,  # ','
    ecodes.KEY_DOT:       0x37,  # '.'
    ecodes.KEY_SLASH:     0x38,  # '/'

    # ファンクションキー
    ecodes.KEY_F1:  0x3A, ecodes.KEY_F2:  0x3B, ecodes.KEY_F3:  0x3C,
    ecodes.KEY_F4:  0x3D, ecodes.KEY_F5:  0x3E, ecodes.KEY_F6:  0x3F,
    ecodes.KEY_F7:  0x40, ecodes.KEY_F8:  0x41, ecodes.KEY_F9:  0x42,
    ecodes.KEY_F10: 0x43, ecodes.KEY_F11: 0x44, ecodes.KEY_F12: 0x45,

    # PrintScreen, ScrollLock, Pause
    ecodes.KEY_SYSRQ:      0x46,  # PrintScreen
    ecodes.KEY_SCROLLLOCK: 0x47,
    ecodes.KEY_PAUSE:      0x48,

}

def build_hid_report(modifier_byte, pressed_keys):
    report = [0]*8
    report[0] = modifier_byte
    report[1] = 0x00
    idx = 2
    for usage_id in list(pressed_keys)[:6]:
        report[idx] = usage_id
        idx += 1
    return struct.pack('8B', *report)

def main():
    device_path = "/dev/input/event3"
    dev = evdev.InputDevice(device_path)
    # dev.grab()  # Pi側への入力を遮断したい場合は有効化

    print(f"Bluetooth keyboard device: {device_path}")
    pressed_keys = set()
    modifier_state = 0x00

    with open("/dev/hidg0", "wb") as hid:
        for event in dev.read_loop():
            if event.type == ecodes.EV_KEY:
                if event.value not in (0,1):
                    continue
                pressed = (event.value == 1)

                if event.code in MODIFIER_MAP:
                    bitmask = MODIFIER_MAP[event.code]
                    if pressed:
                        modifier_state |= bitmask
                    else:
                        modifier_state &= ~bitmask
                elif event.code in KEYCODE_MAP:
                    usage_id = KEYCODE_MAP[event.code]
                    if pressed:
                        pressed_keys.add(usage_id)
                    else:
                        pressed_keys.discard(usage_id)

                report = build_hid_report(modifier_state, pressed_keys)
                hid.write(report)
                # 同様に 0.005秒を 0.01～0.02 に調整可能
                time.sleep(0.005)

if __name__ == "__main__":
    main()
```

---

以上