Raspberry Piでキーボードエミュレーションをしてみる

最近Windows不信が募ってきて、RTX 3050が載っているマウスDAIVをゲーム用に持っているのに、ついついPS5を買ってしまいました。

やるのは主に原神なわけなんですが、「マルチプレイ募集掲示板からUIDを転記して参加する」のがつらすぎる!

# ほなキーボードエミュレーションしますか

用意するもの
* どこのご家庭にも余っているRaspberry Pi Zero W (2があるならなお良い)
* 比較的まともそうなmicroSDカード
* まともなUSB A-microUSBケーブル (一昔まえのスマートフォンをパソコンにつなぐのに使うのに十分なグレードのもの)
* SDカードを読み書きできるパソコン
* Wi-Fi環境のある家庭
* やる気
* HDMI液晶ディスプレイと有線キーボードはあったほうがいいかもしれないけど別にいらない

# SDカードにRasberry Pi OSを焼く

普通にRaspberry Pi Imagerを使いましょう。

![スクリーンショット 2025-02-12 23.24.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4005442/389275c9-cb31-47a9-81bf-64fa9465c5b7.png)
今回はPi Zeroなので、その通りに選択。

![スクリーンショット 2025-02-12 23.24.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4005442/59e04c2a-14d7-41a9-b027-793d868125ae.png)

Pi ZeroにデスクトップつきのDebianなんか焼いたら◯んでしまうので、Otherを開く。

![スクリーンショット 2025-02-12 23.24.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4005442/53b9fe60-fc5e-4935-8ada-b324e0340c45.png)

`Lite` を選ぶ。
あとは焼くSDカードを選ぶ。

![スクリーンショット 2025-02-12 23.25.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4005442/86844887-157d-4dac-9f65-e747d552df04.png)

ここで「設定を編集する」

![hoge.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4005442/7dc72e4f-8081-47b4-9d35-18ff59556568.png)

こんな感じで適当に設定する。
Wi-Fiのところは注意が必要で、今どきの5GHzアクセスポイントにつなぐことができません。ルーターから`5G`みたいなのとそうじゃないのがあるときは、そうじゃない方にしましょう。

![スクリーンショット 2025-02-12 23.25.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4005442/4ae61605-d6d0-419a-966a-723eec6fa3fd.png)

SSH鍵も設定しておくと楽。何を言っているかわからない人は「パスワード認証」にチェックを入れておけばOK。
保存して、先程のダイアログを「はい」で書き込みに進む。

# Piを起動する

書き込みの終わったSDカードはPiに挿して電源を入れましょう。電源供給をする場合は右の端子に挿してくださいね。
初回起動の設定は割と時間かかるので、お茶でも飲んで待ちましょう。

# SSHログインする

最近はWindowsでも`ssh`コマンドが使えるはずなので、Win + Rして `wt` と入れてEnter押してください。Macの人はターミナル。Linuxの人はこの記事読む必要ある?

ホスト名を `nanapi.local` 、ユーザーを `nekono` で設定したので、コマンドラインに

```
ssh nekono@nanapi.local
```

と入れてEnterを押しましょう。初回だと「この接続先信頼していい?」みたいなことを訊かれるので`yes`と答える。SSH鍵を設定した人はすぐログイン完了。パスワードの人はパスワードを入れる (打ったパスワードは一切画面に表示されないので、打てたと思ったらEnterを押せば良い)。

入れたのを確認したら 

```
sudo shutdown now
```

# HIDガジェットの設定をする

[参考](https://qiita.com/sukimaengineer/items/7e49d60ab23962c97428)

vimで色々できるならそうすればいいですが、難しいと思うので、一旦パソコンにSDカードを挿し直してください。

`config.txt` というファイルが見えると思いますので、 先頭に1行書き足しちゃいましょう。

```
dtoverlay=dwc2,dr_mode=peripheral
```

それから `cmdline.txt`というのがありますので、 `rootwait `の後ろに書き足す。
```
... rootwait modules-load=dwc2,libcomposite ...
```

SDカードを挿して起動、SSH接続。以下は参考記事からほぼそのまま拝借。

```
sudo tee /usr/local/bin/setup_hid_keyboard.sh
```

下記をコピペで貼り付けたら Ctrl + D
```
#!/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 に用意していますが、ご自身の環境に合わせて変更してください。
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" > 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 名がわかっているのであれば、明示的に指定するほうがより安全


# ここだけオリジナルに追記
chown nekono:nekono /dev/hidg0

```

これは、「私はUSBキーボードですよ」とケーブルを通じて主張する部分です。最後の部分は、 `/dev/hidg0` というこのあと使うファイルの所有権を変える部分なので、ユーザー名は設定したものを使ってください。
権限設定は本当はudevっていうのを使うべきなんだけど、たかがRas Piにそこまでこだわるのもだるいのでこれでいいかと。

```
sudo chmod +x /usr/local/bin/setup_hid_keyboard.sh
```
実行権限を付与しておきます。

次に自動起動設定

```
sudo tee /etc/systemd/system/hidgadget.service
```

以下を貼り付けてCtrl + D
```

[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

```

そして有効化と再起動。

```
sudo systemctl enable hidgadget.service
sudo reboot
```

ここまでやると、再起動後にPS5側で「キーボードが接続されました」とか出るようになるはず。

# タイピングスクリプトをこしらえる

`/dev/hidg0`っていうファイルにキーボードの状態が変わった旨のメッセージを書き込むだけなんですが、まあそれを自動化しないと意味がないわけですね。

こういう面倒なのはChatGPTに丸投げしましょうかね。

```
cat > type.py
```

貼り付け内容は以下の通り。
```
#!/usr/bin/env python3

import sys
import time

NULL_CHAR = chr(0)
ENTER_KEY = 40  # HID usage ID for Enter key
SHIFT_KEY = 0x02  # HID usage ID for Shift key
DELAY_TIME = 0.05  # Constant delay time (seconds)

def get_key_code(char):
    """Return the HID usage ID for a given character and whether Shift is required."""
    # Handle digits 0-9
    if '0' <= char <= '9':
        return (ord(char) - ord('1') + 30 if char != '0' else 39, False)
    # Handle lowercase letters a-z
    elif 'a' <= char <= 'z':
        return (ord(char) - ord('a') + 4, False)
    # Handle uppercase letters A-Z (Shift is required)
    elif 'A' <= char <= 'Z':
        return (ord(char) - ord('A') + 4, True)
    return None  # Ignore unsupported characters

def write_report(report):
    with open('/dev/hidg0', 'rb+') as fd:
        fd.write(report.encode())

def send_key(char):
    key_code_data = get_key_code(char)
    if key_code_data:
        key_code, shift_required = key_code_data
        # If Shift is required, press it along with the key
        if shift_required:
            write_report(chr(SHIFT_KEY) + NULL_CHAR + chr(key_code) + NULL_CHAR*4)
            time.sleep(DELAY_TIME)
            write_report(NULL_CHAR*8)  # Release all keys
        else:
            write_report(NULL_CHAR*2 + chr(key_code) + NULL_CHAR*5)
            time.sleep(DELAY_TIME)
            write_report(NULL_CHAR*8)  # Release all keys
        time.sleep(DELAY_TIME)

def press_alt_backquote():
    # Press and hold Alt (modifier 0x04)
    write_report(chr(0x04) + NULL_CHAR + NULL_CHAR*6)
    time.sleep(DELAY_TIME)
    # Press backquote (usage ID 0x35)
    write_report(chr(0x04) + NULL_CHAR + chr(0x35) + NULL_CHAR*5)
    time.sleep(DELAY_TIME)
    # Release backquote, keep holding Alt
    write_report(chr(0x04) + NULL_CHAR + NULL_CHAR*6)
    time.sleep(DELAY_TIME)
    # Release Alt
    write_report(NULL_CHAR*8)
    time.sleep(DELAY_TIME)

def press_enter():
    # Press Enter
    write_report(NULL_CHAR*2 + chr(ENTER_KEY) + NULL_CHAR*5)
    time.sleep(DELAY_TIME)
    # Release Enter
    write_report(NULL_CHAR*8)
    time.sleep(DELAY_TIME)

def main():
    if len(sys.argv) != 2:
        print("Usage: python3 send_keys.py <string_of_text>")
        sys.exit(1)

    input_string = sys.argv[1]

    # Perform the Alt + backquote sequence
    press_alt_backquote()

    # Send the input string as keystrokes
    for char in input_string:
        send_key(char)

    # Press and release Enter
    press_enter()

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

```
chmod +x type.py
```

使い方ですが、まず原神のユーザー検索モードを `UID` にします。
![スクリーンショット 2025-02-13 1.04.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4005442/37920326-704b-477b-89fa-876a98a4da47.png)

□ボタンで入力画面が出ます。
![スクリーンショット 2025-02-13 1.05.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4005442/60efa7d6-c8b9-4d51-b85a-2f36468032dc.png)

ここでSSH接続したPi上で下のように入力

```
./type.py 869999999
```
「日本語入力を解除して」「UIDを入れて」「Enterキーを押す」ところまでやってくれます。

私のキーボードはUS配列なので日本語解除は `Alt + Backquote` ですが、日本語配列設定の場合はAltキーを押す必要はないかと思います (全角半角キーはBackquoteキーと同じキーです) 。