0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Raspberry Pi Zero 2 W でキーボード入力 ~gRPC+protobuf によるネットワーク経由~

Last updated at Posted at 2025-02-08

概要

Raspberry Pi Zero 2 W は、USB OTG (On-The-Go) 機能を通じて、他のPC(以下「ターゲットPC」)から見たときに「USBキーボード」として振る舞うことができます。
本記事では、gRPC + protobuf を使ってネットワーク経由でテキストを送信し、そのテキストを Pi 側の仮想キーボード経由でターゲットPCに入力させる仕組みを紹介します。

「リモートから文字列を送る → ターゲットPCにキーボード入力させる」ための実装例として、Python + gRPCサーバを Pi Zero 2 W に配置し、Windowsクライアントからテキストを送信する流れを解説します。


ゴールイメージ

  1. Raspberry Pi Zero 2 W(以下、Piと呼ぶ)

    • USB OTG 機能を利用して「キーボード (HID)」として設定。
    • 同時に gRPC サーバを起動して、ネットワーク経由で文字列を受け付ける。
  2. Windows クライアント

    • gRPC のクライアントプログラム(Python / C# / Go / C++ などでOK)を実行し、Pi に対してテキストを送信する。
  3. ターゲットPC

    • USBケーブルで Pi を接続しているPC。
    • Pi は「USBキーボード」として見えており、送信されたテキストがキーボード入力として注入される。

前提

  • Raspberry Pi Zero 2 W が手元にある(Zero W でも原理は同じですが、Zero 2 W のほうがCPU性能が上なので便利)。
  • Pi の microSD に Raspberry Pi OS (32bit or 64bit) を導入済み。SSHやWi-Fi接続ができる状態。
  • Windows クライアント側にも Python を導入し、pip などが使える状態(本記事のサンプルでは Python クライアントを使います)。
  • Linux系コマンド(sudo, modprobe, lsusb 等)にある程度なじみがあることを想定しています。

注意
JISキーボードには対応していますが、日本語文字列には未対応です。詳しくは Q8 を参照。


前準備: システムのアップデートとVimやxxdのインストール

まずはシステムを最新の状態にします。

sudo apt update
sudo apt upgrade -y

vimをインストールしていない場合はインストールします。
vim-tiny がインストールされている場合は削除してから再度 Vim をインストールする。
必要に応じて .vimrc を /root にコピーする。

sudo apt remove vim-tiny        # vim-tiny がインストールされている場合は削除する
sudo apt install -y vim         # vimをインストールしていない場合はインストール
sudo cp ~/.vimrc /root/.vimrc   # 必要に応じて .vimrc を /root にコピーする

xxdをインストールしていない場合はインストールします。

sudo apt install -y xxd

ステップ1: Raspberry Pi Zero 2 W を「USBキーボード」にする

1-1. OTG対応の設定

config.txt の修正

sudo vi /boot/firmware/config.txt

末尾あたりに以下の様に書いてある行があった場合は

dtoverlay=dwc2
または
dtoverlay=dwc2,dr_mode=host

以下に書き換えます。なかった場合は追加します。

dtoverlay=dwc2,dr_mode=peripheral

この設定は、Raspberry Pi の USB コントローラを「dwc2」ドライバで制御し、USB ポートをペリフェラル(デバイス)モードに設定する、という意味になります。
これにより、Raspberry Pi Zero 2 W を USB ガジェットとして使用し、PC に接続したときに USB デバイスとして認識させることが可能になります。

cmdline.txt の修正

sudo vi /boot/firmware/cmdline.txt

一行で長い設定が書かれていますが、rootwait の後ろにスペースを挟んで以下を追加してください。

modules-load=dwc2,libcomposite
  • 例:
    ... rootwait modules-load=dwc2,libcomposite quiet splash ...
    
  • 改行は入れず、同じ行に続けて書きます。
  • 保存後、再起動します。

これにより、ブート直後に dwc2libcomposite がロードされ、configfs を使ったUSBガジェット設定が可能になります。

補足1:
厳密には cmdline.txt の同じ行であれば、どこに追記しても modules-load=dwc2,libcomposite は有効です。ただし rootwait の直後に追加することで、ファイルシステムが読み込まれる前にモジュールがロードされやすくなり、より安定動作が期待できるため、本記事では「rootwait の直後」に挿入する方法を例示しています。

補足2:
起動時に /boot/firmware/cmdline.txtlibcomposite がロードされるため、後述のスクリプト内で modprobe libcomposite を呼び出すと重複ロードになる場合があります。ただし、重複ロードしてもエラーになることはほぼなく、無害にスキップされるため、サンプルコードのように modprobe しても動作には問題ありません。

補足3:
環境によっては、Raspberry Pi OS のブートパーティションが /boot/firmware ではなく /boot にマウントされる場合があります。その場合、以下のファイルのパスが変わります。
config.txt → /boot/config.txt
cmdline.txt → /boot/cmdline.txt

1-2. HIDガジェットの設定スクリプトを用意

次に、Pi がブートしたら自動で USB HID キーボード 設定を行えるようにします。
本記事では systemd サービスを使った方法をおすすめし、以下の手順を示します。

基本的に、configfs を使って USB HID ガジェットの設定を行うと、その設定はメモリ上に作成されるため、再起動すると消えてしまいます。つまり、起動の度に設定を再実行する必要があるため、systemd を使って自動実行するようにしておきます。

まずは libcompositeconfigfs を使った設定の中身をスクリプトとしてまとめます。
例えば /usr/local/bin/setup_hid_keyboard.sh のようなファイルを作成してください。

sudo vi /usr/local/bin/setup_hid_keyboard.sh

以下は内容の例です。

#!/bin/bash

# モジュールロード
# 起動時に、/boot/firmware/cmdline.txt で libcomposite がロードされるはずなので、必ずしも必要ではないが念のため
modprobe libcomposite

# configfs が自動マウントされていない場合に備え、ディレクトリの存在チェック
# if [ ! -e /sys/kernel/config/usb_gadget ]; then
#     mount -t configfs none /sys/kernel/config
# fi

# 実際にマウントされているかどうかをチェック
if ! mountpoint -q /sys/kernel/config; then
    mount -t configfs none /sys/kernel/config
fi

cd /sys/kernel/config/usb_gadget/
mkdir -p mykeyboard
cd mykeyboard

# ベンダーID / プロダクトID 設定 (例)
echo 0x1d6b > idVendor
echo 0x0104 > idProduct

# USBバージョン設定 (例: USB2.0 = 0x0200)
echo 0x0200 > bcdUSB
echo 0x0100 > bcdDevice      # デバイスバージョン

# 文字列記述子の設定 (英語:0x409)
mkdir -p strings/0x409
# 文字列 (シリアル/メーカー/製品名)
echo "1234567890" > strings/0x409/serialnumber
echo "MyPiVendor" > strings/0x409/manufacturer
echo "MyPiKeyboard" > strings/0x409/product

# Config ディレクトリの作成
mkdir -p configs/c.1
mkdir -p configs/c.1/strings/0x409
echo "Config 1: Keyboard" > configs/c.1/strings/0x409/configuration
echo 120 > configs/c.1/MaxPower

# HID 機能の追加 (ブートプロトコル対応キーボード)
mkdir -p functions/hid.usb0
echo 1 > functions/hid.usb0/protocol    # 1=キーボード
echo 1 > functions/hid.usb0/subclass    # 1=ブートインターフェース
echo 8 > functions/hid.usb0/report_length

# レポートディスクリプタ
# keyboard_report.desc は /home/pi に用意しておく
# なお、本記事では例として /home/pi に用意していますが、ご自身の環境に合わせて変更してください。
cat /home/pi/keyboard_report.desc > functions/hid.usb0/report_desc

# 上で作ったHID機能をConfigに紐づけ
ln -s functions/hid.usb0 configs/c.1/

# UDC(USB Device Controller)のバインド
UDCNAME=$(ls /sys/class/udc | head -n 1)
echo $UDCNAME > UDC
# 特定の UDC 名がわかっているのであれば、明示的に指定するほうがより安全

保存したら実行権限を付与します。

sudo chmod +x /usr/local/bin/setup_hid_keyboard.sh

UDCNAME=$(ls /sys/class/udc | head -n 1) の部分は、実際に存在するUDCの最初のエントリを取得する方法の一例です。
Zero / Zero 2W / OSバージョンに応じて 20980000.usb20200000.usb など異なる場合があるため、こうして自動取得しています。
特定の UDC 名がわかっているのであれば、明示的に指定するほうがより安全です。


1-3 /home/pi/keyboard_report.desc の作り方

USB HID キーボードがホストに提示するレポートディスクリプタは、送信するキー入力データの形式(フォーマット)を定義します。このレポートディスクリプタは、ホスト側に対して「自分がどのようなデータを送るのか」を説明する重要な情報です。

たとえば、標準的な8バイトのキーボードレポートでは、最初の1バイトに修飾キー(Shift, Ctrl など)、2バイト目に予約領域、残りの6バイトに同時押しされているキーのスキャンコードを入れる、という構成になっています。
ここでは、標準的な8バイトキーボードレポートディスクリプタの作成方法を紹介します。

1-3.1. レポートディスクリプタの基本例

以下は、標準的な8バイトHIDキーボード用レポートディスクリプタの例です。
16進数のデータとして定義されています。

05 01  # Usage Page (Generic Desktop)
09 06  # Usage (Keyboard)
A1 01  # Collection (Application)
05 07  # Usage Page (Key Codes)
19 E0  # Usage Minimum (224) - Modifier Keys
29 E7  # Usage Maximum (231)
15 00  # Logical Minimum (0)
25 01  # Logical Maximum (1)
75 01  # Report Size (1)
95 08  # Report Count (8)
81 02  # Input (Data, Var, Abs) - Modifier keys
95 01  # Report Count (1)
75 08  # Report Size (8)
81 03  # Input (Constant) - Reserved byte
95 06  # Report Count (6)
75 08  # Report Size (8)
15 00  # Logical Minimum (0)
25 65  # Logical Maximum (101) - Key Codes
05 07  # Usage Page (Key Codes)
19 00  # Usage Minimum (0)
29 65  # Usage Maximum (101)
81 00  # Input (Data, Array) - Keys
95 05  # Report Count (5)
75 01  # Report Size (1)
05 08  # Usage Page (LEDs)
19 01  # Usage Minimum (1)
29 05  # Usage Maximum (5)
91 02  # Output (Data, Var, Abs) - LED report
95 01  # Report Count (1)
75 03  # Report Size (3)
91 03  # Output (Constant) - LED padding
C0      # End Collection

このディスクリプタは、以下の情報をホストに伝えます。

  • 1バイト目: 修飾キー (Ctrl, Shift, Alt, GUI) の状態 (各1ビット)
  • 2バイト目: 予約領域 (常に0)
  • 3〜8バイト目: 同時に押される最大6キーのスキャンコード
  • 出力部: LED 状態(NumLock, CapsLock など)の報告

1-3.2. 作成方法

方法①: コマンドを使ってバイナリファイルを直接作成

以下のコマンドを Pi 上で実行すると、バイナリ形式の keyboard_report.desc が作成されます。

echo -ne "\x05\x01\x09\x06\xa1\x01\x05\x07\x19\xe0\x29\xe7\x15\x00\x25\x01\x75\x01\x95\x08\x81\x02\x95\x01\x75\x08\x81\x03\x95\x06\x75\x08\x15\x00\x25\x65\x05\x07\x19\x00\x29\x65\x81\x00\x95\x05\x75\x01\x05\x08\x19\x01\x29\x05\x91\x02\x95\x01\x75\x03\x91\x03\xc0" > keyboard_report.desc

このコマンドは、\xNN 形式の16進数データをバイナリ出力し、keyboard_report.desc に保存します。
念のため、ヘクサダンプを確認します。

hexdump -C keyboard_report.desc
# または
xxd keyboard_report.desc

出力例 :

00000000: 0501 0906 a101 0507 19e0 29e7 1500 2501  ..........)...%.
00000010: 7501 9508 8102 9501 7508 8103 9506 7508  u.......u.....u.
00000020: 1500 2565 0507 1900 2965 8100 9505 7501  ..%e....)e....u.
00000030: 0508 1901 2905 9102 9501 7503 9103 c0    ....).....u....
方法②: テキストエディタを利用して作成(※変換が必要)

テキストエディタで16進数の値を記述し、後で xxd や類似ツールでバイナリに変換する方法もありますが、上記コマンドで直接作成する方法が簡単です。

1-3.3. カスタマイズ

  • 標準の8バイトレポートでは、最大6キーまでの同時押しに制限されます。

  • Nキーロールオーバー (NKRO) 対応 や、マウス・ゲームパッド用に変更する場合は、HID Usage Tables などを参考にレポートディスクリプタの内容を変更してください。

  • 保存した keyboard_report.desc は、設定スクリプト内で読み込まれ、HIDガジェットのレポートディスクリプタとして適用されます。

  • USB OTG での動作では、ケーブルの品質や電源供給にも注意してください。


1-4. systemd サービスを作成して自動実行

起動時に上記スクリプトを呼び出す systemd サービスファイルを作り、
再起動しても毎回自動で HID キーボードガジェットが有効になるようにします。

  1. サービスファイルを作成
    sudo vi /etc/systemd/system/hidgadget.service
    
  2. 内容の例:
    [Unit]
    Description=Setup HID Gadget for USB OTG
    # After=systemd-modules-load.service network.target sysinit.target local-fs.target
    # 環境によっては configfs の自動マウント設定を確認する必要がある
    After=systemd-modules-load.service network.target sys-kernel-config.mount
    ConditionPathExists=/sys/kernel/config
    
    [Service]
    # 今回は一度設定を行うだけなので Type=oneshot とし、RemainAfterExit=yes にしている
    Type=oneshot
    ExecStart=/usr/local/bin/setup_hid_keyboard.sh
    RemainAfterExit=yes
    
    [Install]
    WantedBy=multi-user.target
    
  3. 有効化・起動
    sudo chmod +x /usr/local/bin/setup_hid_keyboard.sh
    sudo systemctl daemon-reload
    sudo systemctl enable hidgadget.service
    sudo systemctl start hidgadget.service
    
  4. 再起動テスト
    sudo reboot
    
    再起動後、USBケーブルをターゲットPCに挿せば Pi がキーボードとして認識されるか確認してください。なお、環境によっては configfs が自動でマウントされない場合があります。もし動作に問題が出た場合は、
    sudo mount -t configfs none /sys/kernel/config
    
    などで、手動でマウントする必要があるかもしれません。
    なお、再起動前に
    sudo systemctl enable hidgadget.service
    
    を実行しておけば、サービスの自動起動設定は永続化されます。
    そのため、再起動後に再度
    sudo systemctl enable hidgadget.service
    sudo systemctl start hidgadget.service
    
    と手動で実行する必要はありません。
    つまり、再起動後は systemd によって自動的に $\texttt{hidgadget.service}$ が起動され、
    USB HID の設定が自動的に再構築されるはずです。
    もし、再起動後にサービスが正しく起動していない場合は、
    sudo systemctl status hidgadget.service
    
    などで状態を確認し、ログなどを調査してください。

補足:

  • After=network.target などを指定していますが、本質的には**configfsが使える状態(sysinit.targetの後)**であれば十分な場合もあります。
  • モジュールロードを systemd-modules-load.service に任せるなら、modprobe が不要になるケースもあります。
  • いずれにせよ、記事中の例のままでも動作に問題なければOKです。

なお、/etc/moduleslibcomposite を記載したり、rc.local を使う方法もありますが、この様に systemd で管理するほうがモジュール依存関係や起動タイミングを整理しやすく、推奨される手法です。


/dev/hidg0 へのアクセス権限について

上記設定で作成された HID デバイスは /dev/hidg0 というファイルとして提供されます。
root ユーザであれば直接書き込みできますが、一般ユーザで操作する場合はパーミッションの問題でエラーになることがあります。
例えば以下のような udev ルールを作り、作成されるデバイスの権限を変更させることが可能です。

  1. udev ルールファイルの作成例:
    sudo vi /etc/udev/rules.d/99-hidg.rules
    
  2. 内容の例:
    KERNEL=="hidg[0-9]*", MODE="0660", GROUP="plugdev"
    
    • hidg0, hidg1, hidg2, ... の全デバイスに対して MODE="0660" (所有者とグループのみ読み書き可) とし、グループを plugdev に設定する例です。
    • 一般ユーザがこれを操作する場合は、そのユーザが plugdev グループに所属していれば書き込みが可能になります。
  3. ルール更新:
    sudo udevadm control --reload-rules
    sudo udevadm trigger
    
  4. ユーザをグループに追加:
    sudo usermod -aG plugdev <ユーザ名>
    
    ログアウト/再ログインまたは再起動後に反映されます。

ステップ2: gRPC サーバを構築する

ここではPython + gRPC を例に手順を示します。
「ターゲットPC から見れば、Pi はキーボード」ですが、同時に Pi に Wi-Fi などネットワーク接続されている想定です。(SSHやLANが使えないと開発が難しいため)

2-1. gRPC のインストール (Python)

sudo apt-get update
sudo apt-get install python3-pip
pip3 install grpcio grpcio-tools

2-2. protobuf 定義ファイル作成

keyboard.proto という名前で以下を用意。

syntax = "proto3";

service KeyboardService {
  // 1) テキストをまるごと送り、サーバー側で1文字ずつ打鍵する
  rpc SendText (TextMessage) returns (Empty);

  // 2) 複数キーをまとめて送り、同時押しもできる
  rpc SendKeys (KeysMessage) returns (Empty);
}

// 普通のテキスト用
message TextMessage {
  string text = 1;
}

// 複数キーをまとめて送る用
message KeysMessage {
  repeated string keys = 1;  // 例: ["SHIFT","A"] など
}

// 応答が不要なので空メッセージ
message Empty {}
  • SendText というRPCで文字列を受け取り、何も返さないイメージです。

2-3. .proto のコンパイル

同じディレクトリで以下を実行します。

python3 -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. keyboard.proto

すると keyboard_pb2.pykeyboard_pb2_grpc.py が生成されます。

2-4. Python サーバスクリプト

server.py(一例)

python3 server.py
import time
import threading
import grpc
from concurrent import futures

# protobuf から自動生成されたファイルをインポート (同じディレクトリにある想定)
import keyboard_pb2
import keyboard_pb2_grpc


###############################################################################
# 1. 定数定義
#    - HID の修飾キー (modifier) 用ビットフラグ (左Ctrl, 左Shift 等)
###############################################################################
MOD_LCTRL  = 0x01
MOD_LSHIFT = 0x02
MOD_LALT   = 0x04
MOD_LGUI   = 0x08
MOD_RCTRL  = 0x10
MOD_RSHIFT = 0x20
MOD_RALT   = 0x40
MOD_RGUI   = 0x80

# 修飾キー名 → ビットフラグ
MODIFIER_MAP = {
    "LCTRL":  MOD_LCTRL,
    "CTRL":   MOD_LCTRL,  # 便宜上 CTRL と書いたら左Ctrl扱いに
    "RCTRL":  MOD_RCTRL,
    "LSHIFT": MOD_LSHIFT,
    "SHIFT":  MOD_LSHIFT, # 便宜上 SHIFT と書いたら左Shift扱いに
    "RSHIFT": MOD_RSHIFT,
    "LALT":   MOD_LALT,
    "ALT":    MOD_LALT,   # ALT と書いたら左Alt扱い
    "RALT":   MOD_RALT,
    "LGUI":   MOD_LGUI,   # Windowsキー or Commandキー
    "GUI":    MOD_LGUI,
    "RGUI":   MOD_RGUI,
}


###############################################################################
# 2. JIS配列での ASCII文字 → (modifier, scancode) マッピング例
#    
#    - 「標準的な日本語キーボード (JIS)」で、PC 側が日本語配列として認識している場合に
#      ASCII文字を入力したとき、画面上に対応する文字が出るようなスキャンコードを定義。
#    - 下記の定義はあくまで一例です。実機環境や OS ドライバによっては違いがある場合があります。
###############################################################################
#  - 文字列: ASCII文字
#  - タプル: (modifierビット, スキャンコード)
#    例) 'A' → (MOD_LSHIFT, 0x04)  # shift押しながら "a" のキーコード
#  - 多くの記号が US配列と異なり、"@" や ":" などの入力に必要なキーが変わります。
#  - トップ行の「数字キー」「シフト時に出る記号」なども異なります。
###############################################################################

ASCII_JIS_KEY_MAP = {
    # -- 英字 (小文字) --
    'a': (0x00, 0x04), 'b': (0x00, 0x05), 'c': (0x00, 0x06), 'd': (0x00, 0x07),
    'e': (0x00, 0x08), 'f': (0x00, 0x09), 'g': (0x00, 0x0a), 'h': (0x00, 0x0b),
    'i': (0x00, 0x0c), 'j': (0x00, 0x0d), 'k': (0x00, 0x0e), 'l': (0x00, 0x0f),
    'm': (0x00, 0x10), 'n': (0x00, 0x11), 'o': (0x00, 0x12), 'p': (0x00, 0x13),
    'q': (0x00, 0x14), 'r': (0x00, 0x15), 's': (0x00, 0x16), 't': (0x00, 0x17),
    'u': (0x00, 0x18), 'v': (0x00, 0x19), 'w': (0x00, 0x1a), 'x': (0x00, 0x1b),
    'y': (0x00, 0x1c), 'z': (0x00, 0x1d),

    # -- 英字 (大文字) -- (Shift付き)
    'A': (MOD_LSHIFT, 0x04), 'B': (MOD_LSHIFT, 0x05), 'C': (MOD_LSHIFT, 0x06),
    'D': (MOD_LSHIFT, 0x07), 'E': (MOD_LSHIFT, 0x08), 'F': (MOD_LSHIFT, 0x09),
    'G': (MOD_LSHIFT, 0x0a), 'H': (MOD_LSHIFT, 0x0b), 'I': (MOD_LSHIFT, 0x0c),
    'J': (MOD_LSHIFT, 0x0d), 'K': (MOD_LSHIFT, 0x0e), 'L': (MOD_LSHIFT, 0x0f),
    'M': (MOD_LSHIFT, 0x10), 'N': (MOD_LSHIFT, 0x11), 'O': (MOD_LSHIFT, 0x12),
    'P': (MOD_LSHIFT, 0x13), 'Q': (MOD_LSHIFT, 0x14), 'R': (MOD_LSHIFT, 0x15),
    'S': (MOD_LSHIFT, 0x16), 'T': (MOD_LSHIFT, 0x17), 'U': (MOD_LSHIFT, 0x18),
    'V': (MOD_LSHIFT, 0x19), 'W': (MOD_LSHIFT, 0x1a), 'X': (MOD_LSHIFT, 0x1b),
    'Y': (MOD_LSHIFT, 0x1c), 'Z': (MOD_LSHIFT, 0x1d),

    # -- 数字キー (上段) --
    '1': (0x00, 0x1e),  # → キーラベル「1」  シフト無し = '1'
    '2': (0x00, 0x1f),  # → '2'
    '3': (0x00, 0x20),  # → '3'
    '4': (0x00, 0x21),  # → '4'
    '5': (0x00, 0x22),  # → '5'
    '6': (0x00, 0x23),  # → '6'
    '7': (0x00, 0x24),  # → '7'
    '8': (0x00, 0x25),  # → '8'
    '9': (0x00, 0x26),  # → '9'
    '0': (0x00, 0x27),  # → '0'

    # -- 上段シフト付き記号 (JIS) --
    '!': (MOD_LSHIFT, 0x1e),  # SHIFT + '1'
    '"': (MOD_LSHIFT, 0x1f),  # SHIFT + '2'
    '#': (MOD_LSHIFT, 0x20),  # SHIFT + '3'
    '$': (MOD_LSHIFT, 0x21),  # SHIFT + '4'
    '%': (MOD_LSHIFT, 0x22),  # SHIFT + '5'
    '&': (MOD_LSHIFT, 0x23),  # SHIFT + '6'
    "'": (MOD_LSHIFT, 0x24),  # SHIFT + '7' → アポストロフィ (※注意)
    '(': (MOD_LSHIFT, 0x25),  # SHIFT + '8'
    ')': (MOD_LSHIFT, 0x26),  # SHIFT + '9'
    # '0' の SHIFT は 環境によって ) or なし など若干ズレる場合もあり

    # -- 記号キー (JIS) --
    '-': (0x00, 0x2d),  # → '-'(ハイフン)
    '^': (0x00, 0x2e),  # → '^'
    '\\':(0x00, 0x31),  # → '¥' (実機では円マークに見える/入力される場合が多い)
    '@': (0x00, 0x2f),  # → '@' (JISでは「P」の右隣キー)
    '[': (0x00, 0x30),  # → '[' (JISでは「@」の右隣キー)
    ';': (0x00, 0x33),  # → ';'
    ':': (MOD_LSHIFT, 0x33), # → SHIFT + ';'
    ']': (MOD_LSHIFT, 0x30), # → SHIFT + '['
    # ')': (MOD_LSHIFT, 0x27), # 参考: SHIFT+'0' → ')' 「Shift + 9 → )」の方が一般的
    '=': (MOD_LSHIFT, 0x2d), # → SHIFT + '-'
    '~': (MOD_LSHIFT, 0x2e), # → SHIFT + '^'
    '|': (MOD_LSHIFT, 0x31), # → SHIFT + '\\'

    # -- スペースなど --
    ' ': (0x00, 0x2c),  # → スペース

    # -- カンマ, ピリオド, スラッシュ等 (日本語配列でも実質 US と同じ箇所が多い) --
    ',': (0x00, 0x36),  # → ','
    '.': (0x00, 0x37),  # → '.'
    '/': (0x00, 0x38),  # → '/'
    '<': (MOD_LSHIFT, 0x36),
    '>': (MOD_LSHIFT, 0x37),
    '?': (MOD_LSHIFT, 0x38),
    '`': (0x00, 0x35),   # キー左上(実機では ^/~ と別のキーになることも)
    # '~': (MOD_LSHIFT, 0x35),  # SHIFT + `   # JISでは(MOD_LSHIFT, 0x2e)の方が一般的
    '\n': (0x00, 0x28),      # Usage ID 0x28 は Enter/Return キー
}


###############################################################################
# 3. 日本語配列ならではの特殊キー (HANKAKU_ZENKAKU, HENKAN, MUHENKAN, KATAKANA)
#    を SPECIAL_KEY_MAP や別途で管理する例
###############################################################################
SPECIAL_KEY_MAP = {
    # 基本的な制御キー (US/JIS 共通)
    "ENTER":       0x28,
    "RETURN":      0x28,
    "ESC":         0x29,
    "ESCAPE":      0x29,
    "BACKSPACE":   0x2A,
    "TAB":         0x2B,
    "SPACE":       0x2C,
    "CAPS_LOCK":   0x39,
    "F1":  0x3A,  "F2":  0x3B,  "F3":  0x3C,  "F4":  0x3D,
    "F5":  0x3E,  "F6":  0x3F,  "F7":  0x40,  "F8":  0x41,
    "F9":  0x42,  "F10": 0x43,  "F11": 0x44,  "F12": 0x45,

    "PRINT_SCREEN": 0x46,
    "SCROLL_LOCK":  0x47,
    "PAUSE":        0x48,
    "INSERT":       0x49,
    "HOME":         0x4A,
    "PAGE_UP":      0x4B,
    "DELETE":       0x4C,
    "END":          0x4D,
    "PAGE_DOWN":    0x4E,
    "RIGHT_ARROW":  0x4F,
    "LEFT_ARROW":   0x50,
    "DOWN_ARROW":   0x51,
    "UP_ARROW":     0x52,

    # 日本語配列特有キー (例)
    "HANKAKU_ZENKAKU": 0x35,  # 半角/全角キー
    "MUHENKAN":       0x88,
    "HENKAN":         0x8A,
    "KATAKANA_HIRAGANA": 0x90,
}


###############################################################################
# 4. convert_char_for_text(ch): ASCII文字を (modifier, scancode) に変換
#    - テキスト入力 (SendText) 用
###############################################################################
def convert_char_for_text(ch: str):
    if ch in ASCII_JIS_KEY_MAP:
        mod, sc = ASCII_JIS_KEY_MAP[ch]
        return (mod, sc)
    # 未定義文字
    return (0, 0)


###############################################################################
# 5. convert_key_to_scancode(key_name): "SHIFT", "CTRL", "ENTER", "A", "F1", "HENKAN"等
#    汎用キー指定を (modifier_byte, scancode) に変換
#    - 同時押し (SendKeys) 用
###############################################################################
def convert_key_to_scancode(key_name: str):
    # 大文字小文字を区別せずに検索するため
    k_upper = key_name.upper()

    # 1) 修飾キーなら modifier ビットを返す
    if k_upper in MODIFIER_MAP:
        return (MODIFIER_MAP[k_upper], 0)

    # 2) SPECIAL_KEY_MAP (ENTER, ESC, HENKANなど)
    if k_upper in SPECIAL_KEY_MAP:
        return (0, SPECIAL_KEY_MAP[k_upper])

    # 3) ASCII_JIS_KEY_MAP (アルファベット、数字、記号)
    #    - 大文字小文字そのまま検索
    if key_name in ASCII_JIS_KEY_MAP:
        return ASCII_JIS_KEY_MAP[key_name]

    #    - 大文字/小文字を変換して再試行
    if key_name.lower() in ASCII_JIS_KEY_MAP:
        return ASCII_JIS_KEY_MAP[key_name.lower()]
    if key_name.upper() in ASCII_JIS_KEY_MAP:
        return ASCII_JIS_KEY_MAP[key_name.upper()]

    # 4) 該当なし
    return (0, 0)


###############################################################################
# 6. /dev/hidg0 へのアクセスを排他するロック (マルチスレッド対策)
###############################################################################
lock = threading.Lock()


###############################################################################
# 7. テキスト送信 (SendText) → 1文字ずつ押下→離す
###############################################################################
def send_text_char_by_char(text: str):
    # 改行コードをLFに統一
    text = text.replace("\r\n", "\n").replace("\r", "\n")
    
    with open("/dev/hidg0", "wb") as f:
        for ch in text:
            mod, sc = convert_char_for_text(ch)

            # 押下 (modifier, 0, sc, 0, 0, 0, 0, 0)
            press_report = bytes([mod, 0x00, sc, 0x00, 0x00, 0x00, 0x00, 0x00])
            f.write(press_report)
            time.sleep(0.02)

            # 離す (全0)
            release_report = b"\x00\x00\x00\x00\x00\x00\x00\x00"
            f.write(release_report)
            time.sleep(0.02)

            # 0.02 秒(20ミリ秒) のスリープでも多くの場合は大丈夫ですが、環境によっては取りこぼしが出るケースもあります。
            # もし「何文字か抜ける」「数連打すると間欠的に入力されない」等があれば、0.03〜0.05 秒ぐらいまで少し増やすと安定するかもしれません。

###############################################################################
# 8. 同時押し送信 (SendKeys)
#    - ["SHIFT","A","HENKAN"] などまとめて押して → 離す
###############################################################################
def send_keys_simultaneously(keys_list):
    # HID で同時押しできるキー数は最大6個
    keys_list = keys_list[:6]

    modifier_byte = 0x00
    scancodes = []

    for key_name in keys_list:
        mod_bit, sc = convert_key_to_scancode(key_name)
        modifier_byte |= mod_bit  # 修飾キーはビットORで合成
        if sc != 0:
            scancodes.append(sc)

    # スキャンコードは6つまで
    while len(scancodes) < 6:
        scancodes.append(0x00)

    # HID レポート: [modifier, 0, sc1, sc2, sc3, sc4, sc5, sc6]
    press_report = bytes([modifier_byte, 0x00] + scancodes[:6])

    with open("/dev/hidg0", "wb") as f:
        # 押し込み
        f.write(press_report)
        time.sleep(0.05)  # "押されている" 状態を確実に伝えるため少し待つ

        # 離す (全0)
        release_report = b"\x00\x00\x00\x00\x00\x00\x00\x00"
        f.write(release_report)
        time.sleep(0.02)

###############################################################################
# 9. gRPC サービス実装
###############################################################################
class KeyboardServiceServicer(keyboard_pb2_grpc.KeyboardServiceServicer):
    def SendText(self, request, context):
        """
        - テキスト (ASCII) を受け取り、1文字ずつ JIS配列に合わせたキーコードで入力
        """
        text = request.text
        print("[Server] Received text:", text)
        with lock:
            send_text_char_by_char(text)
        return keyboard_pb2.Empty()

    def SendKeys(self, request, context):
        """
        - キーリスト (例: ["SHIFT","A","ENTER","HENKAN"]) を受け取り、同時押しを1回送信
        """
        keys_list = list(request.keys)
        print("[Server] Received keys:", keys_list)
        with lock:
            send_keys_simultaneously(keys_list)
        return keyboard_pb2.Empty()


###############################################################################
# 10. メイン: gRPC サーバの起動
###############################################################################
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("gRPC server started on port 50051 (JIS layout).")
    server.wait_for_termination()


if __name__ == "__main__":
    serve()

以下で実行します。

python3 server.py

上記を起動すると、ポート 50051 で gRPC サーバが立ち上がり、SendText RPC の呼び出しを待機します。
受け取った文字列は send_text_char_by_char 関数によって /dev/hidg0 へ書き込み、ターゲットPC 上でキー入力として再生されます。

あくまで一例です。実機環境や OS ドライバによっては違いがある場合があります。

使い方・注意点

  • SendText: ASCII文字列を渡すと、上記の ASCII_JIS_KEY_MAP に従って1文字ずつキー入力します。

    • 例: "Hello, world!" → Shift を適切に使いながらキーを押下→離す処理。
    • 記号の多い文字列では、想定と異なる入力になることもあるため、必要に応じてマッピングを調整してください。
  • SendKeys: 引数で受け取ったリスト(例: ["SHIFT","A","HENKAN"])を同時押しとして1回送信します。

    • 6キーを超えると標準的な HID キーボードでは処理できないので、先頭6個までに切り詰めています。
  • time.sleep(0.02) の値は入力の取りこぼしを減らすための簡易ウェイトです。環境によっては 0.05 秒以上にしないと文字抜けが起こることがあります。速度と信頼性のバランスを見ながら調整してください。

  • /dev/hidg0 への書き込み時は lock を使い、同時アクセスを避けています。複数クライアントが同時に文字列を送ってきた場合、入力が混ざらないように排他制御しています。

  • 日本語IMEの切り替えを完全に制御したい場合は、「半角/全角」や「変換」「無変換」を送るだけではなく、OS側IMEの状態も考慮する必要があります。システムによってはキーイベントだけで制御できないこともあります。

  • 上記マッピングには含めていない記号や特殊キーがあるかもしれません。独自に追加する場合は

    1. ASCII文字で入力したいもの → ASCII_JIS_KEY_MAP(modifier, scancode) を定義
    2. 特殊キー (F13〜F24や他のキー)SPECIAL_KEY_MAP に追記
    3. もしくは 修飾キーMODIFIER_MAP に追加
      のいずれかを行ってください。

ステップ3: Windows クライアントでテキスト送信

3-1. Windows側のセットアップ

  • Python をインストールして pip install grpcio grpcio-tools などを行い、同様に keyboard.proto を用意しコンパイルしてください。
  • あるいは他言語(C#/Go/Node.js など)でも同じように gRPC クライアントを実装できます。

3-2. Python クライアント例

client.py(一例)

python client.py
import grpc
import keyboard_pb2
import keyboard_pb2_grpc

def run():
    # Raspberry Pi Zero 2 W の IPアドレスまたはホスト名:ポート
    channel = grpc.insecure_channel("192.168.1.50:50051")
    stub = keyboard_pb2_grpc.KeyboardServiceStub(channel)

    # (1) テキストを送信 -> サーバ側で 1文字ずつ打鍵
    text_msg = keyboard_pb2.TextMessage(text="Hello World")
    stub.SendText(text_msg)

    # (2) リストを送信 -> 同時押しや特殊キーを一括処理
    # 例: SHIFT + a + F1 を同時押し (実用性はさておき、デモ用)
    keys_msg = keyboard_pb2.KeysMessage(keys=["SHIFT", "a", "F1"])
    stub.SendKeys(keys_msg)

    # 例: 半角/全角キー を送る
    keys_msg = keyboard_pb2.KeysMessage(keys=["HANKAKU_ZENKAKU"])
    stub.SendKeys(keys_msg)

    print("All RPC calls completed.")

if __name__ == "__main__":
    run()
  • stub.SendText(...) では文字列を一括送信し、Pi 側で1文字ずつ押す・離す。
  • stub.SendKeys(...) では複数キーをリストで送り、Pi 側で「同時押し」や特殊キーの押下を実行。

Pi Zero 2 W が Wi-Fi でルーターに接続され、Windows PC も同じルーター経由で IP を取得している場合、192.168.x.x:50051 などで通信できます。
python client.py を実行すると、Pi が受信し、ターゲットPC 上で「Hello World」の文字が入力される…という仕組みになります。


よくある疑問と注意点

Q1. キーマッピングはどうやって実装する?

convert_key_to_scancode 関数で、ASCII文字や記号をHIDスキャンコードに変換する必要があります。
大文字には Shift を押す、数字や記号はシフトやAltGrを押す、など複雑なので、実際は下記のような方法がおすすめです。

  • 既存ライブラリやスクリプトを参考にする (例: hid-gadget-test にあるテーブル)
  • 「英字キーボードの簡易実装」にとどめ、頻出する文字だけマッピング。
  • Shiftキーの押下・リリースを含めて送ることで大文字やシンボルに対応する。
  • 日本語/マルチレイアウト対応をする場合はさらに大変。

簡単な例として、「A(大文字)」を入力するときは
$$
\text{Shift(左)を押す} \to \text{0x04(a)を送る} \to \text{Shiftを離す} \to \text{aを離す}
$$
のように複数ステップになるため、コード中で都度 $ \x02 $(左Shift) を立てるバイト列と、キーリリースのバイト列を挟む必要があります。

Q2. 文字が全部正しく入力されない・欠ける

キーを押してすぐ離す処理を連続で行うと、稀にターゲットPC側で文字の取りこぼしが起きる場合があります。
time.sleep(0.01)time.sleep(0.02) のように短い遅延を適宜入れることで解消するかもしれません。
必要に応じて間隔を広げる(0.05秒など)と、より確実に入力できる場合があります。

Q3. セキュリティは大丈夫?

  • Pi がネットワークから無制限にアクセス可能だと、第三者が gRPC を叩いて勝手にキーボード入力できてしまうリスクがあります。
  • VPN や ファイアウォール、ローカルネットワークのみで使うなどの対策を行いましょう。
  • gRPC の SSL/TLS 設定で安全に通信する方法もあります。

Q4. gRPC ではなく他のAPIでもOK?

もちろん HTTP / MQTT / WebSocket 等、他の手段でテキストを送信しても構いません。
gRPC は高速&型安全なRPCが書きやすいメリットがあるため、本記事では例示として利用しました。

Q5. gRPCサーバを同時に複数クライアントから呼ばれたら?

上記サンプルでは、1リクエストにつき send_text_as_keys() を呼び出して /dev/hidg0 を開閉して書き込んでいます。
もし同時アクセスが起きると、

  • 両方のリクエストが同時に /dev/hidg0 に書き込む競合
  • 文字入力が混じってしまう
    などのリスクがあります。

対策例:

  • 記事中でも示したように、スレッドロック (threading.Lock) を用いた排他制御send_text_as_keys を直列化する
  • リクエストをキューに入れて順番に処理するワーカーを作る
  • そもそも複数同時リクエストが来ない運用を想定する

Q6. Vendor ID (VID)/ Product ID (PID) の扱い

記事内では

echo 0x1d6b > idVendor       # Linux Foundation の例
echo 0x0104 > idProduct

という設定例を挙げていますが、Linux FoundationのVIDを無断で商用利用することは本来推奨されません。学習・趣味程度であれば問題になりにくいですが、もし将来的に量産や公共向け製品にする際は独自のVID/PIDを取得またはライセンス契約などを検討してください。

Q7. 既存のキーボードのUSB属性値を使いたい

動作や互換性の検証目的で、実際のキーボードと同じVID/PID/文字列を真似したい場合は、以下の "既存のキーボードのUSB属性値を調べる方法" の様に、lsusb -v などで取得した情報を echo 0x1234 > idVendor のように設定すれば、ホスト側でほぼ同一のデバイス名として認識されます。
ただし、他社製品のVID/PIDを流用することは商標・ライセンス上の問題をはらむ場合があるので注意しましょう。


Q8. 日本語の文字列を入力したいが難しい?

結論としては、本記事の方法で「ASCII英数記号の入力」はそこそこ制御できますが、「日本語文字列(あいうえお等)を入力する」のは難易度がかなり高いです。

理由:

  • IME(入力メソッド)の制御が必要になる:
    • キーボードからローマ字を送って OS 側で変換・確定する流れになるため、変換候補の操作や半角/全角キー、変換キー、無変換キーなどを押す必要がある。
    • さらに OS のIME状態を把握できないため、「いま IME がONなのかOFFなのか」を Pi 側だけで完全に管理するのは困難。
  • 複数ステップのキーシーケンスを定義する必要:
    • 例: 「か」→ 'k' 押下→離す → 'a' 押下→離す → 変換タイミング → 確定 …
    • 一文字入力するのに複数キーを自動送信し、OS上のIMEが期待どおり動いてくれないと文字化けになることも。
  • 本記事のサンプルコードは、ASCII (英字・数字・記号) や一部の特殊キー(F1,F2,etc) のみ扱っており、日本語は想定していない。
    • 日本語IME用のキー (無変換/変換/カタカナ/ひらがな等) を追加している例も存在しますが、IME側の設定しだいでは動かない場合も多々ある。

したがって、日本語(全角文字)をスクリプトで完全制御したい場合は、

  1. OS 側IMEをあらかじめ OFF にしておき、ローマ字のみ送る
    • 人間側で手動変換したりする
  2. もしくはIME を自動操作できる仕組みを OS 側で用意する (D-Bus経由など)
  3. あるいはそもそも HID キーボードによる入力でなく
    • (例)ターゲットPC上のソケット通信や専用クライアントアプリに直接文字列を送信して貼り付ける

といったアプローチが現実的です。

まとめ:

  • 本記事の方法では日本語は実用レベルで扱うのがかなり大変です。
  • IMEの挙動に強く依存するため、完全な自動化を目指すなら追加の工夫が必要となります。

既存のキーボードのUSB属性値を調べる方法(参考)

動作や互換性の検証目的で、あるキーボード挿したときと同じ情報で認識させたいという場合は、以下のような手順で該当キーボードの Vendor ID / Product ID / シリアル番号 / Manufacturer / Product などを取得します。
ここでは Linux環境を想定し、lsusb コマンドを用いた例を紹介します。(Windows では「USBDeview」「USB Device Viewer」等のツールを利用しても可)

  1. 対象のキーボードをPCに接続
    例として、LinuxマシンにUSBで挿します。

  2. lsusb コマンドで一覧を確認

    lsusb
    

    実行すると、例えば以下のような表示があるかもしれません:

    Bus 001 Device 005: ID 1234:5678 ExampleCorp ExampleKeyboard
    
    • ID 1234:5678VendorID:ProductID (16進数)
    • ExampleCorp ExampleKeyboardlsusb が持っている簡易情報
  3. 詳細情報を確認

    sudo lsusb -d 1234:5678 -v
    

    すると、iManufacturer, iProduct, iSerial などが表示され、さらに Report Descriptor: の項目も確認できます。

    例:

    iManufacturer           1 ExampleCorp
    iProduct                2 ExampleKeyboard
    iSerial                 3 ABCD12345
    ...
    

    ここに表示されている文字列 (Manufacturer, Product, SerialNumber) や、idVendor (0x1234), idProduct (0x5678) などがわかります。
    なお、デバイスがドライバにバインドされていると、lsusb がレポートディスクリプタを読み出すことができません。そこで、以下の様に一時的にHIDドライバからアンバインドすると読み出すことができます。

    # デバイスのバス番号とデバイス番号を特定 (例: Bus 001 Device 005)
    BUS=1
    DEVICE=5
    
    # ドライバをアンバインド
    echo -n "$BUS-$DEVICE" | sudo tee /sys/bus/usb/drivers/usbhid/unbind
    
    # lsusb -v でレポートディスクリプタを表示
    sudo lsusb -d 1234:5678 -v
    
    # ドライバを再バインド
    echo -n "$BUS-$DEVICE" | sudo tee /sys/bus/usb/drivers/usbhid/bind
    
  4. 上記情報を Pi の USB Gadget設定でそっくり設定
    この記事の「ステップ1」で echo 0x1d6b > idVendor としている箇所を、取得した Vendor ID (0x1234 等) に、
    文字列設定部分( echo "MyPiVendor" > strings/0x409/manufacturer など ) を「ExampleCorp」「ExampleKeyboard」などに書き換えればOKです。

  5. シリアル番号も同様に変更
    echo "1234567890" > strings/0x409/serialnumber としている箇所を、実際のキーボードの iSerial と同じ値にすれば、ホストPCから見てほぼ同一デバイス名として認識されます。

  6. レポートディスクリプタの取得と保存
    表示された「Report Descriptor:」の16進数データをコピーし、テキストエディタで report_descriptor.txt として保存します。
    次に、以下のコマンドでバイナリに変換し、keyboard_report.desc として保存します。

    xxd -r -p report_descriptor.txt > /home/pi/keyboard_report.desc
    

    これにより、既存のキーボードと全く同じレポートディスクリプタを得ることもできます。

注意:

  • 他社製キーボードの完全クローンを作ることは、動作や互換性の検証以外の目的では推奨されません。
  • 同じVendor IDを無断で使うと衝突や混乱が起きる可能性もあります。
  • あくまで趣味・研究目的で自己責任で行ってください。

まとめ

  • Raspberry Pi Zero 2 WUSB OTG 機能を利用し、ターゲットPC に対して「USBキーボード」として振る舞う設定を行いました。
  • システム起動時に自動でこの設定を有効化する方法として、systemd サービスを使う手順を紹介しました。
  • gRPC + Protobuf を使ってネットワーク越し(Wi-Fi / USB Ethernet)でテキストをやり取りし、Pi 側で受信したテキストを /dev/hidg0 へ書き込むことで物理キーボード入力相当を実現しました。
  • キーマップ対応(Shiftや記号など)複数リクエストの排他制御, 複合ガジェット構成 (RNDIS/ECM), セキュリティなど、運用用途に応じて検討すべき要素があります。

ネット経由で文字列を受け取り、それをターゲットPC 上に“打鍵”として送る装置としては、非常に柔軟な仕組みとなるはずです。ぜひ応用してみてください。


参考リンク


参考 US配列の場合のPythonサーバスクリプト例

python server.py
import time
import threading
import grpc
from concurrent import futures

# protobuf から自動生成されたファイルをインポート (同じディレクトリにある想定)
import keyboard_pb2
import keyboard_pb2_grpc


###############################################################################
# 1. 定数定義
#    - HID の修飾キー (modifier) 用ビットフラグ
#    - 1バイト目に格納される Left/Right CTRL/SHIFT/ALT/GUI など
###############################################################################
MOD_LCTRL  = 0x01
MOD_LSHIFT = 0x02
MOD_LALT   = 0x04
MOD_LGUI   = 0x08
MOD_RCTRL  = 0x10
MOD_RSHIFT = 0x20
MOD_RALT   = 0x40
MOD_RGUI   = 0x80

# 修飾キー名 → ビット
MODIFIER_MAP = {
    "LCTRL":  MOD_LCTRL,
    "LCtrl":  MOD_LCTRL,  # 大文字小文字混在の別名例
    "CTRL":   MOD_LCTRL,
    "RCTRL":  MOD_RCTRL,
    "LSHIFT": MOD_LSHIFT,
    "SHIFT":  MOD_LSHIFT,  # 便宜上 SHIFT と書くと左SHIFT扱い
    "RSHIFT": MOD_RSHIFT,
    "LALT":   MOD_LALT,
    "ALT":    MOD_LALT,    # ALT と書くと左ALT扱い
    "RALT":   MOD_RALT,
    "LGUI":   MOD_LGUI,    # WindowsキーやCommandキー相当
    "GUI":    MOD_LGUI,
    "RGUI":   MOD_RGUI,
}


###############################################################################
# 2. 文字 (ASCII) → (modifier, scancode) のマッピング (US配列想定)
#
#    - 大文字や記号など、Shiftが必要なものは (MOD_LSHIFT, ...) というタプルを定義
#    - 一般に HID Usage Tables では:
#       'a' → 0x04, 'b' → 0x05 ... 'z' → 0x1D
#       '1' → 0x1E, '2' → 0x1F, ... '0' → 0x27
#       Enter → 0x28, Esc → 0x29, Backspace → 0x2A, Tab → 0x2B, Space → 0x2C
#       - → 0x2D, = → 0x2E, [ → 0x2F, ] → 0x30, \ → 0x31, ; → 0x33, ' → 0x34
#       ` → 0x35, , → 0x36, . → 0x37, / → 0x38
#    - Shift付き記号: '!' → (SHIFT+1), '#' → (SHIFT+3), etc.
###############################################################################
ASCII_KEY_MAP = {
    # 小文字 (modifier=0)
    'a': (0x00, 0x04), 'b': (0x00, 0x05), 'c': (0x00, 0x06), 'd': (0x00, 0x07),
    'e': (0x00, 0x08), 'f': (0x00, 0x09), 'g': (0x00, 0x0a), 'h': (0x00, 0x0b),
    'i': (0x00, 0x0c), 'j': (0x00, 0x0d), 'k': (0x00, 0x0e), 'l': (0x00, 0x0f),
    'm': (0x00, 0x10), 'n': (0x00, 0x11), 'o': (0x00, 0x12), 'p': (0x00, 0x13),
    'q': (0x00, 0x14), 'r': (0x00, 0x15), 's': (0x00, 0x16), 't': (0x00, 0x17),
    'u': (0x00, 0x18), 'v': (0x00, 0x19), 'w': (0x00, 0x1a), 'x': (0x00, 0x1b),
    'y': (0x00, 0x1c), 'z': (0x00, 0x1d),

    # 大文字 (Shiftが必要 → modifier=0x02 (左Shift))
    'A': (MOD_LSHIFT, 0x04), 'B': (MOD_LSHIFT, 0x05), 'C': (MOD_LSHIFT, 0x06),
    'D': (MOD_LSHIFT, 0x07), 'E': (MOD_LSHIFT, 0x08), 'F': (MOD_LSHIFT, 0x09),
    'G': (MOD_LSHIFT, 0x0a), 'H': (MOD_LSHIFT, 0x0b), 'I': (MOD_LSHIFT, 0x0c),
    'J': (MOD_LSHIFT, 0x0d), 'K': (MOD_LSHIFT, 0x0e), 'L': (MOD_LSHIFT, 0x0f),
    'M': (MOD_LSHIFT, 0x10), 'N': (MOD_LSHIFT, 0x11), 'O': (MOD_LSHIFT, 0x12),
    'P': (MOD_LSHIFT, 0x13), 'Q': (MOD_LSHIFT, 0x14), 'R': (MOD_LSHIFT, 0x15),
    'S': (MOD_LSHIFT, 0x16), 'T': (MOD_LSHIFT, 0x17), 'U': (MOD_LSHIFT, 0x18),
    'V': (MOD_LSHIFT, 0x19), 'W': (MOD_LSHIFT, 0x1a), 'X': (MOD_LSHIFT, 0x1b),
    'Y': (MOD_LSHIFT, 0x1c), 'Z': (MOD_LSHIFT, 0x1d),

    # 数字 (上段キー)
    '1': (0x00, 0x1E), '2': (0x00, 0x1F), '3': (0x00, 0x20), '4': (0x00, 0x21),
    '5': (0x00, 0x22), '6': (0x00, 0x23), '7': (0x00, 0x24), '8': (0x00, 0x25),
    '9': (0x00, 0x26), '0': (0x00, 0x27),

    # Shift付き数字記号
    '!': (MOD_LSHIFT, 0x1E), '@': (MOD_LSHIFT, 0x1F), '#': (MOD_LSHIFT, 0x20),
    '$': (MOD_LSHIFT, 0x21), '%': (MOD_LSHIFT, 0x22), '^': (MOD_LSHIFT, 0x23),
    '&': (MOD_LSHIFT, 0x24), '*': (MOD_LSHIFT, 0x25), '(': (MOD_LSHIFT, 0x26),
    ')': (MOD_LSHIFT, 0x27),

    # 一般記号
    ' ': (0x00, 0x2C),  # スペース
    '-': (0x00, 0x2D),  '=': (0x00, 0x2E),
    '[': (0x00, 0x2F),  ']': (0x00, 0x30), '\\': (0x00, 0x31),
    ';': (0x00, 0x33),  "'":(0x00, 0x34),  # '単体
    '`': (0x00, 0x35),  ',': (0x00, 0x36),  '.': (0x00, 0x37),  '/': (0x00, 0x38),

    # Shift付き記号
    '_': (MOD_LSHIFT, 0x2D), '+': (MOD_LSHIFT, 0x2E), '{': (MOD_LSHIFT, 0x2F),
    '}': (MOD_LSHIFT, 0x30), '|': (MOD_LSHIFT, 0x31),
    ':': (MOD_LSHIFT, 0x33), '"': (MOD_LSHIFT, 0x34), '~': (MOD_LSHIFT, 0x35),
    '<': (MOD_LSHIFT, 0x36), '>': (MOD_LSHIFT, 0x37), '?': (MOD_LSHIFT, 0x38),
}


###############################################################################
# 3. 特殊キー・機能キー → スキャンコード (modifier=0) のマッピング
#
#    - Enter, Backspace, Escape, Tab, CapsLock, F1...F12, Insert, Delete, Arrow など
###############################################################################
SPECIAL_KEY_MAP = {
    "ENTER":       0x28,
    "RETURN":      0x28,  # 同じ
    "ESC":         0x29,
    "ESCAPE":      0x29,
    "BACKSPACE":   0x2A,
    "TAB":         0x2B,
    "SPACE":       0x2C,  # (ASCII_KEY_MAP の ' ' と同じ)

    "CAPS_LOCK":   0x39,

    "F1":  0x3A,  "F2":  0x3B,  "F3":  0x3C,  "F4":  0x3D,
    "F5":  0x3E,  "F6":  0x3F,  "F7":  0x40,  "F8":  0x41,
    "F9":  0x42,  "F10": 0x43,  "F11": 0x44,  "F12": 0x45,

    "PRINT_SCREEN": 0x46,
    "SCROLL_LOCK":  0x47,
    "PAUSE":        0x48,
    "INSERT":       0x49,
    "HOME":         0x4A,
    "PAGE_UP":      0x4B,
    "DELETE":       0x4C,
    "END":          0x4D,
    "PAGE_DOWN":    0x4E,
    "RIGHT_ARROW":  0x4F,
    "LEFT_ARROW":   0x50,
    "DOWN_ARROW":   0x51,
    "UP_ARROW":     0x52,
}


###############################################################################
# 4. convert_char_for_text(ch): 1文字のASCIIキャラをスキャンコードに変換
#    - テキスト入力用。ASCII_KEY_MAPを参照して (modifier, scancode) を返す
#    - もしマッピングに無い文字の場合は (0,0) を返す (何も入力されない)
###############################################################################
def convert_char_for_text(ch: str):
    if ch in ASCII_KEY_MAP:
        mod, sc = ASCII_KEY_MAP[ch]
        return (mod, sc)
    else:
        return (0, 0)  # 未対応文字


###############################################################################
# 5. convert_key_to_scancode(key_name): "SHIFT", "CTRL", "ENTER", "A", "F1" など、
#    文字列で指定されたキーを (modifier_byte, scancode) に変換する。
#    - 同時押し (SendKeys) 用に利用
#    - SHIFT や ALT, CTRL は scancodeではなく modifier ビットで扱う
#    - F1, ENTER, ESC などは SPECIAL_KEY_MAP
#    - アルファベットや数字・記号は ASCII_KEY_MAP
###############################################################################
def convert_key_to_scancode(key_name: str):
    # 大文字小文字を区別しない検索用
    k_upper = key_name.upper()

    # 1) 修飾キー(MODIFIER_MAP)かどうか?
    if k_upper in MODIFIER_MAP:
        mod_bit = MODIFIER_MAP[k_upper]
        return (mod_bit, 0)  # scancode=0, modifierビットだけ立てる

    # 2) SPECIAL_KEY_MAP に定義されているか?
    if k_upper in SPECIAL_KEY_MAP:
        sc = SPECIAL_KEY_MAP[k_upper]
        return (0, sc)

    # 3) ASCII_KEY_MAP に含まれているか?(例: "A", "!", "a", "1"など)
    #    大文字小文字をそのまま使う場合、ASCII_KEY_MAP は大文字区別するので
    #    引数のまま検索してみる
    if key_name in ASCII_KEY_MAP:
        return ASCII_KEY_MAP[key_name]

    # 大文字小文字を変換して再チェック
    if key_name.lower() in ASCII_KEY_MAP:
        return ASCII_KEY_MAP[key_name.lower()]
    if key_name.upper() in ASCII_KEY_MAP:
        return ASCII_KEY_MAP[key_name.upper()]

    # 4) 該当なし → (0,0)
    return (0, 0)


###############################################################################
# 6. /dev/hidg0 への書き込みを排他するためのロック
###############################################################################
lock = threading.Lock()


###############################################################################
# 7. send_text_char_by_char(text):
#    - 渡された文字列を「1文字ずつ」押下→リリースする
#    - 文字間に短いスリープを入れて取りこぼしを防止
###############################################################################
def send_text_char_by_char(text: str):
    # 改行コードをLFに統一
    text = text.replace("\r\n", "\n").replace("\r", "\n")

    with open("/dev/hidg0", "wb") as f:
        for ch in text:
            mod, sc = convert_char_for_text(ch)

            # 押す (modifier, 0, sc, 0,0,0,0,0)
            press_report = bytes([mod, 0x00, sc, 0x00, 0x00, 0x00, 0x00, 0x00])
            f.write(press_report)
            time.sleep(0.02)

            # 離す (全0)
            release_report = b"\x00\x00\x00\x00\x00\x00\x00\x00"
            f.write(release_report)
            time.sleep(0.02)

            # 0.02 秒(20ミリ秒) のスリープでも多くの場合は大丈夫ですが、環境によっては取りこぼしが出るケースもあります。
            # もし「何文字か抜ける」「数連打すると間欠的に入力されない」等があれば、0.03〜0.05 秒ぐらいまで少し増やすと安定するかもしれません。


###############################################################################
# 8. send_keys_simultaneously(keys_list):
#    - ["SHIFT","A","F1"] のようなリストを受け取り、同時押しに相当する
#      HID レポートを1回送信 → 少し待つ → 離す(全0) の2回送信
#    - 6キーまで同時押し可能 (HIDレポートの標準仕様)
###############################################################################
def send_keys_simultaneously(keys_list):
    # HID では6キー分までしか送れないので、万一多ければ先頭6個に制限
    keys_list = keys_list[:6]

    # 最終的に (modifier_byte, [scan1, scan2, ..., scan6]) を作る
    modifier_byte = 0x00
    scancodes = []

    for key_name in keys_list:
        mod_bit, sc = convert_key_to_scancode(key_name)
        # modifierビットは OR で蓄積
        modifier_byte |= mod_bit
        if sc != 0:
            scancodes.append(sc)

    # スキャンコードは最大6つ
    while len(scancodes) < 6:
        scancodes.append(0x00)

    # HID レポート: [modifier, reserved=0, sc1, sc2, sc3, sc4, sc5, sc6]
    press_report = bytes([modifier_byte, 0x00] + scancodes[:6])

    # 実際に書き込み (押す)
    with open("/dev/hidg0", "wb") as f:
        f.write(press_report)
        time.sleep(0.05)  # "押されている" 状態を確実に伝えるため少し待つ

        # 離す (全0)
        release_report = b"\x00\x00\x00\x00\x00\x00\x00\x00"
        f.write(release_report)
        time.sleep(0.02)


###############################################################################
# 9. gRPC サービス実装
#    - KeyboardService の SendText / SendKeys を実装
#    - /dev/hidg0 書き込み部分は排他ロックをとって衝突を防ぐ
###############################################################################
class KeyboardServiceServicer(keyboard_pb2_grpc.KeyboardServiceServicer):
    def SendText(self, request, context):
        """
        例: テキスト "Hello World" を受け取り、
            1文字ずつ送信する (Shiftや記号もASCII_KEY_MAPから判定)
        """
        text = request.text
        print("[Server] Received text:", text)
        with lock:
            send_text_char_by_char(text)
        return keyboard_pb2.Empty()

    def SendKeys(self, request, context):
        """
        例: ["SHIFT","A","F1"] のようなキーリストを受け取り、
            同時押しとして1回のレポートで送る
        """
        keys_list = list(request.keys)
        print("[Server] Received keys:", keys_list)
        with lock:
            send_keys_simultaneously(keys_list)
        return keyboard_pb2.Empty()


###############################################################################
# 10. サーバ起動コード (main)
###############################################################################
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("gRPC server started on port 50051.")
    server.wait_for_termination()


if __name__ == "__main__":
    serve()

以上

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?