Written in Japanese(UTF-8)
2021.11. 2
INASOFT

/トップ/目次/管理人のひとこと/Raspberry Pi Zero WをPCに繋ぎ、ボタン(タクトスイッチ)を押したら「管」の文字を入力する仕組みを作りたい

3129492 (+0424)[+0447]

管理人のふたこと

Raspberry Pi Zero WをPCに繋ぎ、ボタン(タクトスイッチ)を押したら「管」の文字を入力する仕組みを作りたい



公開日:2021/11/02
更新日:2021/11/02 PC側のJoypad監視の仕組みにPowerShellスクリプト版を加えました
更新日:2021/11/04 Windows 11の、実際のWindows+Xメニューでも試してみた

【目次】

■Windows 11で「管」をアクセラレータキーとするメニューがあるという話から

少し前の話になりますが、Windows 11で、[Windows]+[X]メニューの「Windowsターミナル(管理者)」のアクセラレータキーが「管」に設定されていて、キーボードから押せないという問題が起きていました。そしてそれを、ゲームボーイ(カラー)を使った力業で解決したという記事がありました。

どうやら、GlovePieで「key.Unicode7BA1 = joystick.button2」を設定して「管」を入力する仕組みを構築したようです。
(7BA1は「管」のUTF-16(BE)表現。JoystickのButton2の入力をPCが受け取ると、GlovePieが「管」の文字を送出するようである)

これって、Raspberry Pi Zero WをUSB接続のHIDとして認識させる方法でもできるはずだよね?ということを考えまして、実現する方法をずっと考えておりました。

Raspberry Pi Zero Wのピン付き Raspberry Pi Zero Wのピン付き(カバーをかぶせた)

ボタンを付けるので、GPIOピンの付いたRaspberry Pi Zero Wを用意する必要があります。

多分「ブレッドボードとタクトスイッチを使って、ボタンを押したら日本語キーボードにはなかなか存在しなさそうなスキャンコードをWindows PCに送り込み、そのキーをRegisterHotkey()で捕まえて、SendInput() APIのKEYEVENTF_UNICODEモードで『管』の文字を送り込む」方法でどうにかなるだろうなと思っていたものの、まとまった時間が確保できず、なかなか試せずにいました。



■Raspberry Pi Zeroをキーボード扱いではなくJoypad(GamePad)扱いしたほうが確実かも

SendInput() APIのKEYEVENTF_UNICODEモードで「管」の文字を送り込むの部分については、最近公開した疑似シリーズで、上記の事件を参考に作ったジョークネタとして「管」をアクセラレータキーにするという状態を作った際、単体テストとしてキーボードシミュレータを使ってみるということをしたので、実現できることは確認できています。

管理者へ昇格するための機能を「ファイル」メニューに追加
「管理者権限で実行()」のアクセラレータキーが「管」になっている(ジョークネタ)。キーボードから「管」を入力できれば即座に選べるのだが…マウスやカーソルキーで選ぶのではつまらない


キーボードシミュレータで「5秒後に『管』の文字を送り込む」という設定をすることで単体テストをクリア

実際色々考えて見ると、「日本語キーボードにはなかなか存在しなさそうなスキャンコード」とWindowsのVirtual Key Codeの対応を調べるのは至難の業だし、「日本語キーボードにない」からといって、正しくそのコードを受け取りたい動きを邪魔してしまうのはよくない。

というわけで、キーボードではなく、Joypad(GamePad)のボタンを押すシミュレーションをした方が確実ではないかな?と思うようになりました。


Raspberry Pi ZeroをJoypad化してSendInputで「管」の文字を送り込む構想
Raspberry Pi ZeroをJoypad化してSendInputで「管」の文字を送り込む構想のイメージ図

■ラズパイZeroのHID(Joypad)化とPC側でのキーの読み替えの仕込み

Raspberry Pi Zero WをUSB接続のHIDとして認識させて「物理版マウスふるふる」を作ったときの状態を、少し変化させることによって実現してみます。

(1) まず、物理版マウスふるふるを作った時には、/etc/fstabを編集してSDカードをRead Only化してしまっていましたので、Read Onlyを解除後、fstabを編集して恒久的に元に戻します(赤字の部分)。

pi@raspberrypi5:~ $ mountfs rw
Filesystem mounted in READ-WRITE mode

pi@raspberrypi5:~ $ sudo vi /etc/fstab

proc /proc proc defaults 0 0 PARTUUID=○○○○-01 /boot vfat defaults 0 2 PARTUUID=○○○○-02 / ext4 defaults,noatime 0 1 # a swapfile is not a swap partition, no line here # use dphys-swapfile swap[on|off] for that tmpfs /tmp tmpfs defaults,size=64m,noatime,mode=1777 0 0 tmpfs /var/tmp tmpfs defaults,size=16m,noatime,mode=1777 0 0 tmpfs /var/log tmpfs defaults,size=32m,noatime,mode=0755 0 0 tmpfs /var/lib/systemd tmpfs defaults,size=64m,noatime,mode=0755 0 0

(2) 次の内容で hid.sh を修正します。接続したPCに対し、Joypadとして認識させるシェルスクリプトです。

 前回、PCに対してHID (Human Interface Device) なUSBデバイスだと認識させるための設定を行っていましたが、マウスではなくJoypad(GamePad)として認識させるように変更します。今回も、以下の内容は、TomoSoftさんの内容を参考に構成しています。

pi@raspberrypi5:~ $ vi hid.sh

#!/bin/bash /usr/sbin/modprobe libcomposite cd /sys/kernel/config/usb_gadget/ mkdir -p g1 cd g1 echo 0x1d6b > idVendor # Linux Foundation echo 0x0104 > idProduct # Multifunction Composite Gadget echo 0x0100 > bcdDevice # v1.0.0 echo 0x0200 > bcdUSB # USB2 mkdir -p strings/0x409 echo "S/N DUMMY 000-00" > strings/0x409/serialnumber echo "inasoft.org" > strings/0x409/manufacturer echo "Generic USB Joypad" > strings/0x409/product N="usb0" mkdir -p functions/hid.$N echo 1 > functions/hid.$N/protocol echo 1 > functions/hid.$N/subclass echo 8 > functions/hid.$N/report_length echo -ne \\x05\\x01\\x09\\x04\\xa1\\x01\\x09\\x01\\xa1\\x00\\x05\\x09\\x19\\x01\\x29\\x03\\x15\\x00\\x25\\x01\\x95\\x03\\x75\\x01\\x81\\x02\\x95\\x01\\x75\\x05\\x81\\x03\\x05\\x01\\x09\\x33\\x09\\x34\\x09\\x35\\x16\\x01\\x80\\x26\\xff\\x7f\\x75\\x10\\x95\\x03\\x81\\x06\\xc0\\xc0 > functions/hid.usb0/report_desc C=1 mkdir -p configs/c.$C/strings/0x409 echo "Config $C: ECM network" > configs/c.$C/strings/0x409/configuration echo 250 > configs/c.$C/MaxPower ln -s functions/hid.$N configs/c.$C/ # End functions ls /sys/class/udc > UDC
pi@raspberrypi5:~ $ chmod 744 hid.sh

(2) Raspberry Pi Zero WのGPIOとブレッドボードとタクトスイッチを使って、Joypadボタンを作ります。

Raspberry Pi Zero WのGPIOとブレッドボードとタクトスイッチを使って、Joypadボタンを作る

 上記の写真では分かりづらいので、模式図を示します。

Raspberry Pi Zero WのGPIOとブレッドボードとタクトスイッチを使って、Joypadボタンを作る(模式図)


(3) タクトスイッチを押したら「Joypadの1ボタンを押されたとPCに伝える」スクリプトを作成します。

 ボタンを押したらGPIO#24がHIGHになるので、それを監視して、Joypadのボタンを押下状態にします。

pi@raspberrypi5:~ $ vi watch_button_as_joypad.py

#!/usr/bin/env python3 import RPi.GPIO as GPIO import time GPIO.setmode(GPIO.BCM) GPIO.setup(24, GPIO.IN) def write_report(report): with open('/dev/hidg0', 'rb+') as fd: fd.write(report.encode()) while True: if GPIO.input(24) == GPIO.HIGH: write_report(chr(1)+chr(0x00)+chr(0)+chr(0x00)+chr(0)+chr(0x00)+chr(0)) else: write_report(chr(0)+chr(0x00)+chr(0)+chr(0x00)+chr(0)+chr(0x00)+chr(0)) time.sleep(0.1)
pi@raspberrypi5:~ $ chmod 744 watch_button_as_joypad.py

 hid.shで準備した/dev/hidg0に対して、Joypadの状態を書き込みます。最初の1バイトがボタン押下状態を表しているそうです。


(4) さきほど準備したスクリプトを、rootのcronでOS起動時に実行するようにします。末尾を下記の内容で修正します。

pi@raspberrypi5:~ $ sudo crontab -e

MAILTO="" @reboot /usr/bin/bash /home/pi/hid.sh;/usr/bin/python /home/pi/watch_button_as_joypad.py > /tmp/errlog.txt 2>&1

(5) Raspberry Pi Zero側の準備はひとまず完了です。いったん、Raspberry Pi Zeroを安全にシャットダウンします。

pi@raspberrypi5:~ $ sudo shutdown -h now

(6) PC側でJoypadのボタン1を受け取ったら、SendInput() APIで『管』の文字を送出するプログラムを作成し、実行しっぱなしにします。

C++版 / PowerShell版

【PowerShell版】 joyPadBtn2Kan.ps1

# C# class for calling win32api $source = @" using System; using System.Runtime.InteropServices; // for DllImport, Marshal public class Win32API { [DllImport("user32.dll")] extern static uint SendInput( uint nInputs, // Number of INPUT structure INPUT[] pInputs, // INPUT structure int cbSize // Size of INPUT structure ); [StructLayout(LayoutKind.Sequential)] struct INPUT { public int type; // 0 = INPUT_MOUSE // 1 = INPUT_KEYBOARD public KEYBDINPUT ki; } [StructLayout(LayoutKind.Sequential)] struct KEYBDINPUT { public short wVk; public short wScan; // Unicode character which is to be sent to the foreground application public int dwFlags; public int time; public UIntPtr dwExtraInfo; public int dummy1; public int dummy2; } public static uint sendInputKan() { INPUT[] inputs = new INPUT[2]; // [0]Pull down inputs[0].type = 1; // INPUT_KEYBOARD inputs[0].ki.dwFlags = 4; // KEYEVENTF_UNICODE inputs[0].ki.wScan = 0x7BA1; // "kan" (means administrator acronym in Japanese) inputs[0].ki.wVk = 0; inputs[0].ki.time = 0; inputs[0].ki.dwExtraInfo = UIntPtr.Zero; // [1]Release inputs[1].type = 1; // INPUT_KEYBOARD inputs[1].ki.dwFlags = 6; // KEYEVENTF_UNICODE | KEYEVENTF_KEYUP inputs[1].ki.wScan = 0x7BA1; // "kan" (means administrator acronym in Japanese) inputs[1].ki.wVk = 0; inputs[1].ki.time = 0; inputs[1].ki.dwExtraInfo = UIntPtr.Zero; // Key Input simulation(Unicode mode) return SendInput(2, inputs, Marshal.SizeOf(inputs[0])); } [DllImport("winmm.dll")] extern static int joyGetPos( int uJoyID, // Identifier of the joystick to be queried IntPtr pji // Pointer to a JOYINFO structure ); [StructLayout(LayoutKind.Sequential)] struct JOYINFO { public int dwXpos; public int dwYpos; public int dwZpos; public int dwButtons; // Current state of joystick buttons } // Get current state of joystick button #1 public static bool joyGetBtn1(int iJoy) { IntPtr ptr; JOYINFO ji = new JOYINFO(); ptr = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(JOYINFO))); Marshal.StructureToPtr(ji, ptr, false); if ( 0 == joyGetPos(iJoy, ptr)) { ji = (JOYINFO)Marshal.PtrToStructure(ptr, ji.GetType()); if ( (ji.dwButtons & 0x01) == 0x01 ) { Marshal.FreeCoTaskMem(ptr); return true; } } Marshal.FreeCoTaskMem(ptr); return false; } } "@ Add-Type -Language CSharp -TypeDefinition $source # Last button press state $bLastPushButton = $false while ($true) { $bPushButton = $false # Get joystick No.1 button1 status if ( [Win32API]::joyGetBtn1(0)) { $bPushButton = $true } # Get joystick No.2 button1 status if ( [Win32API]::joyGetBtn1(1)) { $bPushButton = $true } if ($bPushButton) { if ($bLastPushButton) { # Ignore if you hold down the button } else { $bLastPushButton = $true Write-Host -NoNewline "*" $dmy = [Win32API]::sendInputKan() } } else { $bLastPushButton = $false } # Sleep to prevent CPU load Start-Sleep -m 50 }

Joypadの状態を読み取るためにjoyGetPos() APIを利用し、Unicode文字の入力をシミュレーションするためにSendInput() APIを利用します。
Win32 APIを使用するには、C#の力を借りる必要があるため、このPowerShellスクリプト内にはC#のプログラムが文字列として含まれており、インラインでコンパイルして動作しています。これにより、PowerShellから間接的にWin32 APIを呼び出しています。
このスクリプトを起動させっぱなしにします。停止させたいときは [×]ボタンを押すか、Ctrl+C を押します。
(コピペして利用しやすくするため、コメントをすべて英字にしてあります)


(7) Raspberry Pi ZeroとPCをUSB接続します。Raspberry Pi Zero側のmicro USB端子は、「PWR」ではなく「USB」の方を使用します。PCとRaspberry Pi Zeroが接続されると、Raspberry Pi Zeroが起動します。

PCと接続するときはUSB側のmicro USB端子を使う

(8) 動作テストです。待っていると、PC側でJoypadとして認識されます。

Windows 10でJoypadとして認識されたことを確認

前回マウスとして認識させたRaspberry Pi Zeroを再利用しようとした場合、今回もマウスとして認識されてしまうことがあります。その場合は、PC側で「デバイスの削除」をしてから、再接続してみてください。

Raspberry Pi Zero側のタクトスイッチを押してみると、「管」の文字が入力されることが分かります。
(上記(6)のJoypadの監視プログラムを一般権限で動作させた場合は、キー入力の対象とするプログラムも一般権限で動作させてください。キー入力の対象とするプログラムの方だけが管理者権限で動作していると、キーの入力ができません)

管理者へ昇格するための機能を「ファイル」メニューに追加
「管理者権限で実行()」のアクセラレータキーが「管」になっている(ジョークネタ(2回目))


Raspberry Pi Zero Wを契機にして、諸々経由して『管』の文字を送り込むテストをクリア

(9) 終了するときは、Raspberry Pi Zeroを安全にシャットダウンします。

pi@raspberrypi5:~ $ sudo shutdown -h now

 /etc/fstabを編集してSDカードをRead Only化すればブチ切りもできます。

■Windows 11の、実際のWindows+Xメニューでも試してみた

Windows 11の正式公開から約1か月経過しました。公開当初は誤字(翻訳ミス?)があったり、動作が不安定なところがあったりと、あまり良い話は聞かなかったためWindows 10からのアップグレードを躊躇っておりましたが、さすがに1か月経てば安定するだろうというと思いまして、アップグレードを試してみることにしました。

Windows 11へアップグレード中1
Windows 11へアップグレード中2

当初、アップグレード中の進捗率の表示が「あなたはそこに○○%です」と表示されるということが笑われていましたが、さすがにその点は解消されたようですね。

で、実際にアップグレードをしてみますと、手元の環境では今のところ、少なくとも

という問題が発生しており、お世辞にも安定したOSとは言えません。このあたりは、などのサイトで継続的に情報収集し対処していくとか、Windows Updateでパッチが登場するのをひたすら待つ必要がありそうです。また、個人的に、

など、仕様と思われる点も、使いづらいと言わざるを得ません。Windows 10に戻そうかな……。

それはさておき、実際にRaspberry Pi ZeroをWindows 11で試してみたいと思います。

先ほども述べた通り、Windows 11では、マウス操作でポップアップメニューを呼び出した場合と、キーボード操作でポップアップメニューを呼び出した場合とで、メニューの見た目や操作感が変わってきます。

スタートボタン(Windowsマーク)の右クリックメニュー&Windows+Xメニュー

今回対象となるのは、右側の「Windows+X」キーでメニューを呼び出した場合となります。見ての通り、「Windowsターミナル(理者)」となっており、「」の部分にアンダーラインが引かれていて、管の字を直接キーボードから入力しなければなりません。

もちろん、Windows+Xでポップアップメニューが表示されているところで、IMEを呼び出し「管」を漢字変換して呼び出すこともできますが、今回はそれをやりたいわけではないので、さっそく、Raspberry Pi ZeroとJoypad監視&「管」文字送信のPowerShellスクリプトを試してみます。


Raspberry Pi Zero Wを契機にして、諸々経由して『管』の文字を送り込むテストをクリア

うまく行きました。

さて、うまく行ったし、Windows 11はまだまだ使いづらいし、Windows 10に戻そうかな……。

あ、ちなみに、Raspberry Pi Zeroを使わなくても、PC用に使えるUSB接続のゲームコントローラーがあれば、1番目と認識されるボタンを押下することで同じことが試せます。

管理者へ昇格するための機能を「ファイル」メニューに追加
例えばこういうUSB接続のゲームコントローラー
参考
関連(ラズピコの話、ガワの話)



本ページへは、自己責任の範囲内であれば自由にリンクしていただいて構いません。
本ページに掲載されている内容は、自由にお使いいただいて構いませんが、必ずしも筆者が内容を保証するものではありませんので、ご利用に際しては自己の責任においてお使いいただきますよう、お願いいたします。
このページのURLやアンカーは、サーバ運営・サイト運営・ページ運営・その他の都合により無告知で一時的あるいは永遠に消滅したり、変更したりする可能性がありますので、あらかじめご了承下さい。
本ページは、公開から1年半経過後の任意のタイミングで削除される予定です。本ページの内容は複製・公開していただいて構いません。


/トップ/目次/管理人のひとこと/Raspberry Pi Zero WをPCに繋ぎ、ボタン(タクトスイッチ)を押したら「管」の文字を入力する仕組みを作りたい