【後編(マウス追加)】Raspberry Pi Zero 2 W + gRPC + protobuf で「キーボード＆マウス操作」をターゲットPCに送る仕組み

---
## 概要

- **前編** では、**Raspberry Pi Zero 2 W** を **USB OTG ガジェットモード（HIDキーボード）** として認識させ、**gRPC + protobuf** によってリモート（ネット経由）から文字列を受け取り、**ターゲットPC** へキーボード入力を注入するシステムを構築しました。  
- **本記事** (前編＋続編の統合版) では、さらに「**マウス操作**」も同じUSBガジェットに追加し、**キーボード＋マウス** の複合HIDとして Target PC に見せる仕組みを紹介します。  
- これにより、**gRPC呼び出し**で「キー入力」だけでなく「マウス移動・クリック」もターゲットPCに送れる強力なリモート制御が可能になります。

---

## 1. 全体構成イメージ

1. **Raspberry Pi Zero 2 W**  
   - USB OTGガジェット：**キーボード＋マウス（HID）** を合成  
   - gRPCサーバ：`SendText()` / `SendMouse()` RPC でキー操作＆マウス操作リクエストを受け取り  
   - `/dev/hidg0` (キーボード用) と `/dev/hidg1` (マウス用) に書き込む

2. **ターゲットPC**  
   - Pi Zero 2 W が「キーボード＆マウスを備えたUSBデバイス」として認識される  
   - 受け取ったキー入力やマウス移動がOS上で反映される

3. **クライアント (Windows / Linux / Mac など)**  
   - ネットワーク経由で Pi に gRPC リクエストを送る  
   - たとえば Python 版 `stub.SendText(...)` / `stub.SendMouse(...)`

ファイル構成例（フォルダ`my_hid_project`）:

```
my_hid_project/
  ├─ keyboard.proto         // gRPC定義 (SendText & SendMouse)
  ├─ server.py              // Pi側サーバ (キーボード+マウス両対応)
  ├─ client.py              // クライアント (キーボード+マウス両対応)
  ├─ keyboard_report.desc   // HIDキーボードのレポートデスクリプタ (ブートプロトコル等)
  ├─ mouse_report.desc      // HIDマウスのレポートデスクリプタ
  └─ ... (その他設定ファイル等)
```

---

## 2. USBガジェット設定（キーボード＋マウス）

### 2-1. config.txt / cmdline.txt

- `/boot/config.txt` に:  
  ```
  dtoverlay=dwc2
  ```
- `/boot/cmdline.txt` の `rootwait` のすぐ後に:  
  ```
  modules-load=dwc2,libcomposite
  ```
- 再起動後、`dwc2` & `libcomposite` が有効になり、Piを**USBデバイスモード**で設定できる。

### 2-2. 複合HIDスクリプト例

```bash
sudo modprobe libcomposite
cd /sys/kernel/config/usb_gadget
sudo mkdir myhid
cd myhid

# ベンダーID/プロダクトIDなど共通情報
echo 0x1d6b > idVendor
echo 0x0104 > idProduct
echo 0x0200 > bcdUSB
echo 0x0100 > bcdDevice

# 文字列記述子
mkdir strings/0x409
echo "1234567890" > strings/0x409/serialnumber
echo "MyPiVendor" > strings/0x409/manufacturer
echo "PiKeyboardMouse" > strings/0x409/product

# config
mkdir configs/c.1
mkdir configs/c.1/strings/0x409
echo "Config 1: Keyboard+Mouse" > configs/c.1/strings/0x409/configuration
echo 120 > configs/c.1/MaxPower

# 1) キーボード (hid.usb0)
mkdir functions/hid.usb0
echo 1 > functions/hid.usb0/protocol    # 1=キーボード
echo 1 > functions/hid.usb0/subclass
echo 8 > functions/hid.usb0/report_length
cat /home/pi/keyboard_report.desc > functions/hid.usb0/report_desc
ln -s functions/hid.usb0 configs/c.1/

# 2) マウス (hid.usb1)
mkdir functions/hid.usb1
echo 2 > functions/hid.usb1/protocol    # 2=マウス
echo 1 > functions/hid.usb1/subclass
echo 4 > functions/hid.usb1/report_length
cat /home/pi/mouse_report.desc > functions/hid.usb1/report_desc
ln -s functions/hid.usb1 configs/c.1/

# バインド
ls /sys/class/udc
echo "20200000.usb" > UDC
```

- `keyboard_report.desc`：キーボード用レポートデスクリプタ（ブートプロトコル8バイト等）  
- `mouse_report.desc`：マウス用（4バイト程度、[buttons, x, y, wheel]）  
- 実行後、**USBケーブルでターゲットPCに繋ぐ**と「キーボード & マウス」の複合デバイスとして認識される。  

---

## 3. .proto ファイル (keyboard.proto)

### 3-1. 前編の「SendText」＋続編の「SendMouse」両方

```protobuf
syntax = "proto3";

package hid;

service KeyboardService {
  // 1) キーボード入力
  rpc SendText(TextMessage) returns (Empty);

  // 2) マウス操作
  rpc SendMouse(MouseMessage) returns (Empty);
}

message TextMessage {
  string text = 1;
}

message MouseMessage {
  // 移動量 (相対), 左ボタン押下/離し
  int32 dx = 1;
  int32 dy = 2;
  bool leftDown = 3; 
  bool leftUp = 4;   
  // 必要に応じて右クリック, 中クリック, ホイールなど拡張
}

message Empty {}
```

**ポイント**:  
- 前編で使った `SendText` / `TextMessage` はそのまま。  
- **`MouseMessage`** と **`SendMouse()`** を**追加**して、一つのサービス `KeyboardService` で両方対応。

---

## 4. サーバプログラム (server.py)

### 4-1. 前編でのキーボード部

- `/dev/hidg0` への書き込み関数（ASCIIをキーコードに変換 → ファイルにバイナリ出力）  
- `SendText(TextMessage)` RPC

### 4-2. 続編でマウスを追加

- `/dev/hidg1` を使う  
- `SendMouse(MouseMessage)` RPC

### 4-3. 統合した最終例

```python
import grpc
from concurrent import futures
import keyboard_pb2
import keyboard_pb2_grpc

# HID デバイスファイル
HID_KEYBOARD = "/dev/hidg0"
HID_MOUSE    = "/dev/hidg1"

def send_text_as_keys(text):
    # 前編: テキストをキーボード入力に変換
    with open(HID_KEYBOARD, "wb") as f:
        # 例: 8バイトレポート [modifier, ..., keycodes...]
        # ASCII→キーコード変換をここに書く
        pass

def send_mouse(dx, dy, leftDown, leftUp):
    # 4バイト [buttons, x, y, wheel=0]
    buttons = 0
    if leftDown:
        buttons |= 0x01  # 左ボタン(ビット0)

    # dx, dy は ±127 程度にクリップ
    def clamp(v):
        return max(-127, min(127, v))
    bx = clamp(dx) & 0xFF
    by = clamp(dy) & 0xFF

    report = bytes([buttons, bx, by, 0x00])
    with open(HID_MOUSE, "wb") as f:
        f.write(report)

class KeyboardServiceServicer(keyboard_pb2_grpc.KeyboardServiceServicer):

    def SendText(self, request, context):
        send_text_as_keys(request.text)
        return keyboard_pb2.Empty()

    def SendMouse(self, request, context):
        send_mouse(request.dx, request.dy, request.leftDown, request.leftUp)
        return keyboard_pb2.Empty()

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=1))
    keyboard_pb2_grpc.add_KeyboardServiceServicer_to_server(
        KeyboardServiceServicer(), server
    )
    server.add_insecure_port('[::]:50051')
    server.start()
    print("Server started on port 50051")
    server.wait_for_termination()

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

**挿入ポイント**:  
1. **`send_text_as_keys()`** (前編) → そのままコピーし、このファイルのトップレベルに追加。  
2. **`send_mouse()`** (新規) をその下に配置。  
3. **`KeyboardServiceServicer`** の中に `SendText()`(前編) と `SendMouse()`(今回追加) を並べて実装する。

---

## 5. クライアントプログラム (client.py)

### 5-1. 前編のキーボード送信

```python
import grpc
import keyboard_pb2
import keyboard_pb2_grpc

def main():
    channel = grpc.insecure_channel('192.168.x.y:50051')
    stub = keyboard_pb2_grpc.KeyboardServiceStub(channel)

    # キーボード入力
    stub.SendText(keyboard_pb2.TextMessage(text="Hello from client!"))

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

### 5-2. 続編のマウス送信を追加

```python
def main():
    channel = grpc.insecure_channel('192.168.x.y:50051')
    stub = keyboard_pb2_grpc.KeyboardServiceStub(channel)

    # 1) キーボード
    stub.SendText(keyboard_pb2.TextMessage(text="Hello from client!"))

    # 2) マウス例: 右方向へ50px移動 + 左ボタン押し下げ
    stub.SendMouse(keyboard_pb2.MouseMessage(dx=50, dy=0, leftDown=True, leftUp=False))
    # ボタン離し
    stub.SendMouse(keyboard_pb2.MouseMessage(dx=0, dy=0, leftDown=False, leftUp=True))

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

**挿入ポイント**:  
- 前編のまま `SendText(...)` 呼び出しを残しておき、  
- **続けて `SendMouse(...)` 呼び出し**を**追加**すればOK。

---

## 6. 注意点

1. **マウスのレポートデスクリプタ**  
   - `mouse_report.desc` の中身と `send_mouse()` のレポート構造を一致させる。  
   - 例えば 4バイト `[buttons, X, Y, wheel]` なら、デスクリプタも同じ順序で定義。

2. **クリックのタイミング**  
   - `leftDown=True, leftUp=False` → ボタン押しっぱなし  
   - すぐ後に `leftDown=False, leftUp=True` で離す  
   - ドラッグ操作をする場合は、押しっぱなし状態で複数の移動 RPC を発行など工夫が必要。

3. **連続移動**  
   - 大量の RPC 呼び出しはネットワーク負荷が高いかもしれないので、適度にインターバルを入れるなど調整が必要。

4. **ターゲットPC 側の認識**  
   - Pi Zero 2 W は 1つのUSBポートから「Keyboard + Mouse」HIDデバイスを出すため、正常に合成デバイスとして認識されるかチェック。  
   - Windows / Linux / MacOS など大抵は問題なく認識するが、特殊OSだと複合HIDが怪しいケースも。

---

## 7. まとめ

1. **前編の「SendText」(キーボード)** と、**続編の「SendMouse」(マウス)** を**同じprotoファイル**＆**同じサーバプログラム**にまとめることで、**キーボード+マウス複合HID**をターゲットPCに見せられる。  
2. **複合HIDガジェット**をlibcompositeで設定し、`hid.usb0`(keyboard) と `hid.usb1`(mouse) を同時に `configs/c.1` に紐付けるだけ。  
3. **gRPC** で受信した「キー入力」「マウス移動」要求を Pi が `/dev/hidg0` / `/dev/hidg1` に書き込む → Target PC にリアルタイムで操作が注入される。

これで、**ネット経由でPiに指示し、Piを介してターゲットPCをリモート操作（キー＆マウス）**する環境が完成します。ぜひ応用してみてください！

---

## 参考リンク

- [前編: Raspberry Pi Zero 2 W + gRPC + Protobuf でキーボード操作を送る](記事URL)  
- [Linux USB Gadget - 複合HID設定 (libcomposite)](https://www.kernel.org/doc/Documentation/usb/linux-usb.html)  
- [Protocol Buffers + gRPC (Python)](https://grpc.io/docs/languages/python/)  
- [USB HIDレポートの例 (マウス/キーボード)](https://eleccelerator.com/tutorial-about-usb-hid-report-descriptors/)  

---

以上