逆向工程巨集鍵盤修改軟件

最近公司有一個專案,其中一部分的內容需要將一款巨集鍵盤操作修改成特定內容
由於這些巨集鍵盤必須使用特定軟體才能修改內容,而且這個軟體只能透過滑鼠點擊來操作
如果只是十多個要修改還可以手動修改,但如果是數千個要手動修改會非常耗時
而且出錯率會很高,檢查時也非常困難
因此,在下想辦法將操作自動化,或至少能夠半自動化

鍵盤外觀

一款藍牙巨集鍵盤,共有 6個鍵盤按鈕 及 1個可按壓旋扭

可以使用 USB Type-C 差電 及 連接到電腦設定巨集內容

電源開關按鈕

階層切換按鈕,這款巨集鍵盤能保存多種按鍵模式

修改軟件

修改巨集鍵盤的按鈕、旋扭內容需要使用指定軟件

當連接 巨集鍵盤 後,軟件會自動偵測到鍵盤的類型
然後便可以設定指定按鈕、旋扭或操作

設定內容後,按 Download 便會修改巨集鍵盤的內容

監察資料

尋找裝置

雖然 巨集鍵盤 能在任意系統上使用,但修改軟件卻只提供 Windows 及 Mac OS 的版本,無法在 Linux 上使用
如果需要修改內容,仍然需要使用 Windows 或 Mac OS

在下估計,這類裝置都是透過 宿主系統 向 USB裝置 發送特定資料來修改操作內容
因此在下認為只要知道這些資料,理論上便能夠在 Linux 上,甚至在其他系統發送相同資料來修改 巨集鍵盤 的內容

由於在下不確定 巨集鍵盤 接收到甚麼資料來修改內容,因此將連接到電腦前後,都在 Terminal 輸入

ls "/dev"

比較連接到電腦前後的差異,發現 /dev/hidraw 的數量有分別,即是 巨集鍵盤 屬於 hidraw
因此在下再輸入

dmesg | grep -i "usb" | grep -i "hidraw"
lsusb

獲得 巨集鍵盤 的 hidraw, bus 及 device 的 編號

在下使用 Wireshark 來擷取 USB 的資料,輸入

sudo apt install wireshark

按此安裝 Wireshark

安裝後,輸入

sudo modprobe usbmon
sudo wireshark

啟動 usbmon模組 來監察 USB 資料
而由於 usbmon模組 需要 root 或 sudo user 才能監察 USB 資料,因此以 sudo 啟動 Wireshark

使用Wireshark

以 root 或 super user 啟動 Wireshark ,才可以顯 usbmon 的選項
usbmon0 是監察所有 USB 的資料,而大於 0 的 usbmon 則是監察對應 USB BUS 的資料
例如使用 lsusb 顯示 USB 裝置位於 bus 1 即是能夠使用 usbmon1 監察
因此使用指定 usbmon 比較節省系統資源

不過即使使用指定 usbmon 監察 USB 資料,仍然會監察到大量資料

過濾資料

Wireshark 能使用類似 C 的 判斷式 將資料過濾,只顯示需要的資料,在文字爛輸入

usb.device_address == ?

usb.device_address 可以過濾符合 lsusb 的 device 編號並只顯示該 USB 裝置的資料
既然能夠使用類似 C 的判斷式語法來過濾資料
除了 == (等於) 、 亦可以使用 != (不等於) 、 < (小於) 、 >= (大於或等於) 等比較方式

使用虛擬機

由於修改資料軟件不支援 Linux ,因此在下仍然需要在 Windows 測試
在下在 VirtualBox 運行 Windows虛擬機 ,並讓 虛擬機 存取 USB裝置

完成設定後,便可以讓 宿主系統 的 Wireshark 監察被 虛擬機 存取的 USB 裝置

然而當修改軟件開啟後,由於軟件會不斷偵測 巨集鍵盤 的狀態或類型等資料
因此不斷監察到有資料傳輸,同樣會影響監察修改時的資料

所以避免顯示到大量無關的資料,可以使用判斷式將不必要的資料過濾,輸入

usb.device_address == ? && usb.src == "host" && usb.data_len > 0

usb.src 的內容是字串,因此必須使用 " (雙引腳),可以過濾符合資料來源的名稱
usb.data_len 為統計 USB 資料的長度,因為 usb.data_len > 0 可以過濾並只顯示有資料傳輸的項目

擷取資料

當按下 Reading Device 按鈕後, Wireshark 便監察到資料傳輸

然後就是最重要的修改按鈕及旋扭操作的內容

保存資料

當擷取大量資料後,還是需要先保存資料才能分析

File > Export Packet Dissertions > As JSON...

設定檔案名後儲存

檔案中會找到 usbhid.data 的資料是一串很長的 十六進制資料 ,就是傳輸到 USB裝置 的資料

分析資料

既然確定能夠擷取資料,便可以使用類似操作的方式將資料作比較,來推斷出哪一組資料控制哪一個設定

例如 abc.json 測試使用相同按鈕,分別操作為 a鍵 、 b鍵 、 c鍵 ,會發現第12組資料為 04, 05, 06
便能夠推測試 第12組資料是設定按鈕的操作效果,及測試往後的數值代表哪個操作
而當資料為 04 即是操作 a鍵 , 05 即是操作 b鍵 , 06 即是操作 c鍵 ……是要不斷重要測試便可以配對所有 數值 及 操作

但 操作 還有些微區分,例如 modifiers.json 中測試 Ctrl鍵, Shift鍵, Alt鍵, Meta鍵 是 第11組資料
而且資料是 01, 02, 04, 08 的二進制遞增資料 ,這意味著將數值合併便能操作不同按鍵的組合
還可以測試 10, 20, 40, 80 是否還有額外功能

由於要測試的資料太多,因此在下沒有將全部資料都在文章中展示,只將部分資料記錄作例子及說明
詳細資料請查看資料表

修改操作
完整指令
偏移位元組長度類型功能
十進制十六進制
00x001無符號數值標頭,必定是 0x03
10x011無符號數值用途不明,通常修改資料時是 0xFD
20x021無符號數值操作類型
  • 0x01 ~ 0x0F 按鈕(Key)
  • 0x10 ~ 0x18 旋鈕(K)
  • 0xB0 LED
30x031無符號數值選擇層(Layer),由 1 開始的整數
40x041無符號數值按鈕觸發模擬類別
  • 0x01 標準(Standard)
  • 0x02 媒體(Media)
  • 0x03 滑與(Mouse)
  • 0x08 LED
50x052無符號數值延遲(毫秒)
例如要輸入 1000 ,即是 1000 / 256 = 3 餘 232
結果是 0xE8, 0x03
60x06
70x071用途不明,通常是 0x00
80x081用途不明,通常是 0x00
90x091用途不明,通常是 0x00
100x0A1無符號數值輸出內容,最多 18 (0x12)
110x0B1無符號數值當 模擬類別 為 0x01 ,第1輸出內容的,參考 修飾鍵
當 模擬類別 為 0x02 , 多媒體控制,參考 特殊鍵
120x0C1無符號數值當 模擬類別 為 0x01 ,第1輸出內容,參考 一般鍵
當 模擬類別 為 0x03 ,滑鼠按鈕,參考 滑鼠鍵
當 模擬類別 為 0x08 ,LED控制,參考 LED模式及顏色
130x0D1無符號數值第2輸出內容的修飾鍵,參考 修飾鍵
140x0E1無符號數值第2輸出內容,參考 一般鍵
150x0F1無符號數值當 模擬類別 為 0x01 ,第3輸出內容的修飾鍵,參考 修飾鍵
當 模擬類別 為 0x03 ,滑鼠滾輪,參考 滑鼠捲動
160x101無符號數值第3輸出內容,參考 一般鍵
170x111無符號數值第4輸出內容的修飾鍵,參考 修飾鍵
180x121無符號數值第4輸出內容,參考 一般鍵
190x131無符號數值第5輸出內容的修飾鍵,參考 修飾鍵
200x141無符號數值第5輸出內容,參考 一般鍵
210x151無符號數值第6輸出內容的修飾鍵,參考 修飾鍵
220x161無符號數值第6輸出內容,參考 一般鍵
230x171無符號數值第7輸出內容的修飾鍵,參考 修飾鍵
240x181無符號數值第7輸出內容,參考 一般鍵
250x191無符號數值第8輸出內容的修飾鍵,參考 修飾鍵
260x1A1無符號數值第8輸出內容,參考 一般鍵
270x1B1無符號數值第9輸出內容的修飾鍵,參考 修飾鍵
280x1C1無符號數值第9輸出內容,參考 一般鍵
290x1D1無符號數值第10輸出內容的修飾鍵,參考 修飾鍵
300x1E1無符號數值第10輸出內容,參考 一般鍵
310x1F1無符號數值第11輸出內容的修飾鍵,參考 修飾鍵
320x201無符號數值第11輸出內容,參考 一般鍵
330x211無符號數值第12輸出內容的修飾鍵,參考 修飾鍵
340x221無符號數值第12輸出內容,參考 一般鍵
350x231無符號數值第13輸出內容的修飾鍵,參考 修飾鍵
360x241無符號數值第13輸出內容,參考 一般鍵
370x251無符號數值第14輸出內容的修飾鍵,參考 修飾鍵
380x261無符號數值第14輸出內容,參考 一般鍵
390x271無符號數值第15輸出內容的修飾鍵,參考 修飾鍵
400x281無符號數值第15輸出內容,參考 一般鍵
410x291無符號數值第16輸出內容的修飾鍵,參考 修飾鍵
420x2A1無符號數值第16輸出內容,參考 一般鍵
430x2B1無符號數值第17輸出內容的修飾鍵,參考 修飾鍵
440x2C1無符號數值第17輸出內容,參考 一般鍵
450x2D1無符號數值第18輸出內容的修飾鍵,參考 修飾鍵
460x2E1無符號數值第18輸出內容,參考 一般鍵
470x2F1用途不明,通常是 0x00
480x301用途不明,通常是 0x00
490x311用途不明,通常是 0x00
500x321用途不明,通常是 0x00
510x331用途不明,通常是 0x00
520x341用途不明,通常是 0x00
530x351用途不明,通常是 0x00
540x361用途不明,通常是 0x00
550x371用途不明,通常是 0x00
560x381用途不明,通常是 0x00
570x391用途不明,通常是 0x00
580x3A1用途不明,通常是 0x00
590x3B1用途不明,通常是 0x00
600x3C1用途不明,通常是 0x00
610x3D1用途不明,通常是 0x00
620x3E1用途不明,通常是 0x00
630x3F1用途不明,通常是 0x00
640x401用途不明,通常是 0x00
修飾鍵
數值功能
十進制十六進制
10x01左Ctrl 或 左Control
20x02左Shift
40x04左Alt 或 左Option
80x08左Meta 或 左Command
160x10右Ctrl 或 右Control
320x20右Shift
640x40右Alt 或 右Option
1280x80右Meta 或 右Command
一般鍵
數值功能
十進制十六進制
00x00無效
10x01用途不明
20x02用途不明
30x03用途不明
40x04小寫字母a鍵
50x05小寫字母b鍵
60x06小寫字母c鍵
70x07小寫字母d鍵
80x08小寫字母e鍵
90x09小寫字母f鍵
100x0A小寫字母g鍵
110x0B小寫字母h鍵
120x0C小寫字母i鍵
130x0D小寫字母j鍵
140x0E小寫字母k鍵
150x0F小寫字母l鍵
160x10小寫字母m鍵
170x11小寫字母n鍵
180x12小寫字母o鍵
190x13小寫字母p鍵
200x14小寫字母q鍵
210x15小寫字母r鍵
220x16小寫字母s鍵
230x17小寫字母t鍵
240x18小寫字母u鍵
250x19小寫字母v鍵
260x1A小寫字母w鍵
270x1B小寫字母x鍵
280x1C小寫字母y鍵
290x1D小寫字母z鍵
300x1E數字1鍵
310x1F數字2鍵
320x20數字3鍵
330x21數字4鍵
340x22數字5鍵
350x23數字6鍵
360x24數字7鍵
370x25數字8鍵
380x26數字9鍵
390x27數字0鍵
400x28Enter鍵
410x29Escape鍵
420x2ABackspace鍵
430x2BTab鍵
440x2CSpace鍵
450x2D減號鍵
460x2E等於號鍵
470x2F開方括號鍵
480x30關方括號鍵
490x31反斜線鍵
500x32反斜線鍵(日)
510x33分號鍵
520x34單引號鍵
530x35反引號鍵
540x36逗號鍵
550x37點鍵
560x38斜線鍵
570x39Caps Lock鍵
580x3AF1鍵
590x3BF2鍵
600x3CF3鍵
610x3DF4鍵
620x3EF5鍵
630x3FF6鍵
640x40F7鍵
650x41F8鍵
660x42F9鍵
670x43F10鍵
680x44F11鍵
690x45F12鍵
700x46Print Screen鍵
710x47Scroll Lock鍵
720x48Pause鍵
730x49Insert鍵
740x4AHome鍵
750x4BPage Up鍵
760x4CDelete鍵
770x4DEnd鍵
780x4EPage Down鍵
790x4F右箭咀鍵
800x50左箭咀鍵
810x51下箭咀鍵
820x52上箭咀鍵
830x53Num Lock鍵
840x54NumPad 斜線鍵
850x55NumPad 星號鍵
860x56NumPad 減號鍵
870x57NumPad 加號鍵
880x58NumPad Enter鍵
890x59NumPad 數字1鍵
900x5ANumPad 數字2鍵
910x5BNumPad 數字3鍵
920x5CNumPad 數字4鍵
930x5DNumPad 數字5鍵
940x5ENumPad 數字6鍵
950x5FNumPad 數字7鍵
960x60NumPad 數字8鍵
970x61NumPad 數字9鍵
980x62NumPad 數字0鍵
990x63NumPad 點鍵
1000x64NumPad 小於號鍵
1010x65Context Menu鍵
1020x66Halt鍵
1030x67NumPad 等號鍵
1040x68F13鍵
1050x69F14鍵
1060x6AF15鍵
1070x6BF16鍵
1080x6CF17鍵
1090x6DF18鍵
1100x6EF19鍵
1110x6FF20鍵
1120x70F21鍵
1130x71F22鍵
1140x72F23鍵
1150x73F24鍵
1160x74Execute
1170x75Help
1180x76Menu
1190x77Select
1200x78Browser Stop
1210x79Redo
1220x7AUndo
1230x7BCut
1240x7CCopy
1250x7DPaste
1260x7EFind
1270x7FMute
1280x80Volume+
1290x81Volume-
1300x82用途不明
1310x83用途不明
1320x84用途不明
1330x85NumPad 逗號鍵
1340x86用途不明
1350x87Internation1
1360x88Internation2
1370x89Internation3
1380x8AInternation4
1390x8BInternation5
1400x8CInternation6
1410x8DInternation7
1420x8EInternation8
1430x8FInternation9
1440x90Lang1
1450x91Lang2
1460x92Lang3
1470x93Lang4
1480x94Lang5
1490x95Lang6
1500x96Lang7
1510x97Lang8
1520x98Lang9
1530x99未定義
1540x9A未定義
1550x9B未定義
1560x9CNumPad Delete鍵
1570x9D未定義
1580x9ESysReg鍵
1590x9FCancel
1600xA0Clear
1610xA1Prior
1620xA2Return
1630xA3Separater
1640xA4Out
1650xA5Oper
1660xA6ClearAgain
1670xA7CrSelProps
1680xA8ExSel
1690xA9未定義
1700xAA未定義
1710xAB未定義
1720xAC未定義
1730xAD未定義
1740xAE未定義
1750xAF未定義
1760xB0NumPad 00鍵
1770xB1NumPad 000鍵
1780xB2NumPad 千位分隔號鍵
1790xB3NumPad 小數點鍵
1800xB4NumPad 貨幣符號鍵
1810xB5NumPad 子貨幣符號鍵
1820xB6NumPad (鍵
1830xB7NumPad )鍵
1840xB8NumPad {鍵
1850xB9NumPad }鍵
1860xBANumPad Tab鍵
1870xBBNumPad Backspace鍵
1880xBCNumPad A鍵
1890xBDNumPad B鍵
1900xBENumPad C鍵
1910xBFNumPad D鍵
1920xC0NumPad E鍵
1930xC1NumPad F鍵
1940xC2NumPad XOR鍵
1950xC3NumPad ^鍵
1960xC4NumPad %鍵
1970xC5NumPad <鍵
1980xC6NumPad >鍵
1990xC7NumPad &鍵
2000xC8NumPad &&鍵
2010xC9NumPad |鍵
2020xCANumPad ||鍵
2030xCBNumPad :鍵
2040xCCNumPad #鍵
2050xCDNumPad Space鍵
2060xCENumPad @鍵
2070xCFNumPad !鍵
2080xD0NumPad Memory Store鍵
2090xD1NumPad Memory Recall鍵
2100xD2NumPad Memory Clear鍵
2110xD3NumPad Memory Add鍵
2120xD4NumPad Memory Subtract鍵
2130xD5NumPad Memory Mulitple鍵
2140xD6NumPad Memory Division鍵
2150xD7NumPad 正負號鍵
2160xD8NumPad Clear鍵
2170xD9NumPad Clear Entry鍵
2180xDANumPad 二進制鍵
2190xDBNumPad 八進制鍵
2200xDCNumPad 十進制鍵
2210xDDNumPad 十六進制鍵
2220xDE未定義
2230xDF未定義
2240xE0左Ctrl鍵 / 左Control鍵
2250xE1左Shift鍵
2260xE2左Alt鍵 / 左Option鍵
2270xE3左Meta鍵 / 左Command鍵
2280xE4右Ctrl鍵 / 右Control鍵
2290xE5右Shift鍵
2300xE6右Alt鍵 / 右Option鍵
2310xE7右Meta鍵 / 右Command鍵
2320xE8Play/Pause鍵
2330xE9按鍵有效,但不確定用途
2340xEA按鍵有效,但不確定用途
2350xEB按鍵有效,但不確定用途
2360xECEject鍵
2370xEDVolume+鍵
2380xEEVolume-鍵
2390xEFMute/Unmute鍵
2400xF0Browser Open鍵
2410xF1History Backward鍵
2420xF2History Forward鍵
2430xF3Browser Stop鍵
2440xF4Find鍵
2450xF5未定義
2460xF6未定義
2470xF7未定義
2480xF8Sleep鍵
2490xF9Lock鍵
2500xFABrowser Refresh鍵
2510xFBCalculator鍵
2520xFC未定義
2530xFD未定義
2540xFE未定義
2550xFF未定義
特殊鍵
數值功能
十進制十六進制
350x23瀏覽器首頁
360x24瀏覽器上一頁
370x25瀏覽器下一頁
390x27瀏覽器重新整理
820x52Bass+
830x53Bass-
840x54Treble+
850x55Treble-
1110x6F提高熒幕亮度
1120x70降低熒幕亮度
1310x83開啟預設多媒體播放器
1380x8A開啟預設電子郵件客戶端
1460x92開啟預設計算機
1480x94開啟本機目錄
1810xB5上一首媒體
1820xB6下一首媒體
2050xCD播放或暫停
2260xE2靜音切換
2330xE9提高播放音量
2340xEA降低播放音量

雖然 Bass 及 Treble 是與音樂有關的字詞,但在下仍然無法得知在電腦上的用途

滑鼠鍵
數值功能
十進制十六進制
10x01左鍵
20x02右鍵
40x04中鍵
LED 模式
數值功能
十進制十六進制
00x?0關閉所有LED
10x?1所有LED指定顏色長亮
20x?2操作按鈕時,所有LED順次序亮著指定顏色後關閉
30x?3操作按鈕時,所有LED逆次序亮著指定顏色後關閉
40x?4操作按鈕時,所有LED亮著指定顏色後關閉
50x?5所有LED白光長亮
LED 顏色
數值功能
十進制十六進制
16 ~ 310x1?紅色
32 ~ 470x2?橙色
48 ~ 630x3?黃色
64 ~ 790x4?綠色
80 ~ 950x5?青色
96 ~ 1110x6?藍色
112 ~ 1270x7?紫色
LED 模式及顏色
0x?00x?10x?20x?30x?40x?5
0x1?0x100x110x120x130x140x15
0x2?0x200x210x220x230x240x25
0x3?0x300x310x320x330x340x35
0x4?0x400x410x420x430x440x45
0x5?0x500x510x520x530x540x55
0x6?0x600x610x620x630x640x65
0x7?0x700x710x720x730x740x75

透過組合兩組數值,便可以設定不同顏色及模式,例如:
0x11 是 所有LED紅光長亮
0x23 是 操作按鈕時,所有LED順次序亮著黃光後關閉
0x64 是 操作按鈕時,所有LED亮著藍光後關閉
但 0x?0 及 0x?5 是無法設定顏色

滑鼠捲動
數值功能
十進制十六進制
10x01向上捲動
2550xFF向下捲動

根據在下判斷,255 即是二補數的 -1 ,因此變成向下捲動

測試效果

查找到一大堆資料,還是要將需要修改既資料寫入到 巨集鍵盤 才能達至目的
要將資料寫入,可以最簡單就是將資料經 標準輸出(Standard Out) 導向至 巨集鍵盤
不過,在下建議不要使用 echo 執行 標準輸出,而是 printf
因為 echo 會在將 標準輸出 自動添加 EOL ,而且預設不會執行跳脫字元的效果
需要以 echo -en 才能達到效果,但直接使用 printf 即可,例如

printf "\x03\xFD\x01\x01\x01\x00\x00\x00\x00\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" | sudo tee "/dev/hidraw?"

而且 printf 可以使用格式化文字,方便製作 範本 來修改 巨集鍵盤

device="/dev/hidraw?"
template="\x03\xFD%s\x01\x01\x00\x00\x00\x00\x00\x01%s%s\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
button="\x01"
modifier="\x00"
keystroke="\x04"
printf "${template}" "${button}" "${modifier}" "${keystroke}" | sudo tee "${device}"

不過,由於 /dev/hidraw? 的擁有者及群組都屬於 root , 因此一般帳戶無法直接使用 > (大於號) 將 標準輸出 導向至 /dev/hidraw?
需要 root 或使用 | sudo tee 將 標準輸出 導向至 /dev/hidraw?

測試效果

讀取操作

使用前可以向 巨集鍵盤 查詢,並回應按鈕資訊

在 Terminal 輸入:

sudo cat "/dev/hidraw?" | xxd -g 1 -c 65 | sed -r 's/.*(([0-9A-Fa-f]{2} ){65}).*/\1/g'

等待 巨集鍵盤 回應按鈕資訊

使用另一個 Terminal 輸入:

printf "\x03\xfa\x0f\x03\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" | sudo tee "/dev/hidraw?"

最關鍵的是 第4位元組 , 用來指定 巨集鍵盤 的 選擇層 的資訊

提交查詢後,巨集鍵盤回應資料

(不知為何提交1次查詢,巨集鍵無法回應所有資料,需要每層資料都要提交2次,才能回應每層所有資料)

尋找 USB裝置

在下發現使用在下所提供的方法來修改巨集鍵盤的設定後,當巨集鍵盤中斷連接並重新連接時,設定資料會回到原來的狀態
但使用原本的軟件進行設定修改後,斷開再重新連接卻能保持修改後的設定
因此,在下再次使用 Wireshark 向巨集鍵盤逆向工程,結果發現遺漏了

\x03\xfd\xfe\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00

需要向巨集鍵盤執行這組指令,才能讓設定永久儲存

保存資料

在之前的測試中,在下每次都需要手動尋找 /dev/hidraw 的編號

通常在尋找 USB裝置 時,可以根據 USB 的 Vendor ID、Product ID 來篩選 USB裝置
這種方法會更方便且能更準確地找到指定的 USB裝置

#VID=`printf "%04X" "4489"` # VID dec to hex
#PID=`printf "%04X" "34880"` # PID dec to hex
VID="1189"
PID="8840"
INT="0"
dmesg | grep "hidraw" | grep "${VID}:${PID}" | grep "input${INT}" | tail -1 | sed -r 's/.*(hidraw[0-9]+).*/\1/g
使用 Python

雖然在下過去一直偏好在 Linux 上使用 Bash 來測試程式
但自從多次使用 Python 編寫程式後,發現可以迅速製作原型,並且輕鬆建立臨時網頁伺服器
這樣一來,就能使用 HTML 製作具備圖形界面的操作方法
同時也可以透過 HTTP請求,以指令方式自行實現自動化或半自動化的運作

雖然 Python 有很多與 USB 相關的函式庫,但卻沒有藉 Vendor ID 、 Product ID 尋找的 /dev/hidraw 的函式庫
畢竟這些函式庫都是跨平台設計,而並非所有作業系統都有 /dev/hidraw 的系統結構,因為唯有自行製作

import subprocess, re
def get_hidraw_path(vid, pid, ino):
	path = None
	command = "dmesg | grep \"hidraw\" | grep \"{:04X}:{:04X}\" | grep \"input{}\" | tail -1".format(vid, pid, ino)
	result = subprocess.run(command, shell = True, stdout = subprocess.PIPE, stderr = subprocess.PIPE, text = True)
	if result.returncode == 0:
		matches = re.findall(".*hidraw([0-9]+):.*", result.stdout)
		if len(matches) > 0:
			path = "/dev/hidraw{}".format(matches[0])
	return path
hidraw_path = get_hidraw_path(0x1189, 0x8840, 0)
print(hidraw_path)
# returns /dev/hidraw2 from my G300s Macro Mouse

由於存取 USB裝置 還需要指定 Interface Number,因此在下借用系統指令
讓 Python 透過 Vendor ID 、 Product ID 及 Interface Number 獲取目標 USB 裝置的 /dev/hidraw 路徑

由於寫入資料到 /dev/hidraw 與一般寫入檔案相似,只是必須使用 wb模式(二進制寫入模式) 來開啟檔案

但從 /dev/hidraw 讀取資料則比較麻煩
必須先向 /dev/hidraw 執行讀取指令,並等待 /dev/hidraw 回應資料才能讀取資料
需要在執行讀取指令同時讀取資料,否則回應的資料就會遺失
由於程式是線性執行,無法同時讀取及寫入,因此需要將 讀取 及 寫入 分開2個檔案執行
或 在1個執行檔案中使用 執行緒(Thread) 同時執行 讀取 及 寫入

import threading, os
class ReturnThread(threading.Thread):
	def __init__(self, group = None, target = None, name = None, args = (), kwargs = {}):
		super().__init__(group, target, name, args, kwargs)
		self._return = None
	def run(self):
		if self._target is not None:
			self._return = self._target(*self._args, **self._kwargs)
	def join(self, *args):
		super().join(*args)
		return self._return
def hidraw_read(path, length):
	try:
		with open(path, "rb") as device:
			return list(device.read(length))
	except Exception as exception:
		raise exception
try:
	if os.path.exists(hidraw_path):
		write_data = [0x03, 0xfa, 0x0f, 0x03, 0x01] + [0x00] * 60
		length = 24 * 65
		read_thread = ReturnThread(target = hidraw_read, args = [hidraw_path, length])
		read_thread.start()
		with open(hidraw_path, "wb") as device:
			device.write(bytearray(write_data))
		read_data = read_thread.join()
		print(read_data)
#		[
#			3, 250, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 76, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
#			3, 250, 2, 1, 1, 0, 0, 0, 0, 0, 1, 16, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
#			3, 250, 3, 1, 1, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
#			3, 250, 4, 1, 1, 0, 0, 0, 0, 0, 1, 16, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
#			3, 250, 5, 1, 1, 0, 0, 0, 0, 0, 1, 48, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
#			3, 250, 6, 1, 1, 0, 0, 0, 0, 0, 1, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
#			3, 250, 7, 1, 1, 0, 0, 0, 0, 0, 1, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
#			3, 250, 8, 1, 1, 0, 0, 0, 0, 0, 1, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
#			3, 250, 9, 1, 1, 0, 0, 0, 0, 0, 1, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
#			3, 250, 10, 1, 1, 0, 0, 0, 0, 0, 1, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
#			3, 250, 11, 1, 1, 0, 0, 0, 0, 0, 1, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
#			3, 250, 12, 1, 1, 0, 0, 0, 0, 0, 1, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
#			3, 250, 13, 1, 1, 0, 0, 0, 0, 0, 1, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
#			3, 250, 14, 1, 1, 0, 0, 0, 0, 0, 1, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
#			3, 250, 15, 1, 1, 0, 0, 0, 0, 0, 1, 0, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
#			3, 250, 16, 1, 1, 0, 0, 0, 0, 0, 1, 0, 45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
#			3, 250, 17, 1, 1, 0, 0, 0, 0, 0, 1, 0, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
#			3, 250, 18, 1, 1, 0, 0, 0, 0, 0, 1, 2, 46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
#			3, 250, 19, 1, 1, 0, 0, 0, 0, 0, 1, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
#			3, 250, 20, 1, 1, 0, 0, 0, 0, 0, 1, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
#			3, 250, 21, 1, 1, 0, 0, 0, 0, 0, 1, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
#			3, 250, 22, 1, 1, 0, 0, 0, 0, 0, 1, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
#			3, 250, 23, 1, 1, 0, 0, 0, 0, 0, 1, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
#			3, 250, 24, 1, 1, 0, 0, 0, 0, 0, 1, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
#		]
		# returns the above list from my G300s Macro Mouse
	else:
		print("Target not found")
except Exception as exception:
	print(exception)

由於原本的 threading.Thread 無法將 目標(Target) 的資料傳回
因此在下製作了一個 類別(Class) 繼承自 threading.Thread,以便在使用 Thread.join 時能傳回運行後的資料

在下編寫一個網頁版本的操作界面來讓一般使用者可以不需要特殊知識及技術來更新巨集鍵盤
同樣,可以使用 HTTP Request 來自行製作自動化或半自動化操作

測試效果

在下將原始碼上載到 https://bitbucket.org/hkgoldenmra/linux-1189-8840-mini-keypad-updater
有興趣可以自行下載使用及修改

總結

最初接觸這種巨集鍵盤時,覺得與在下早前使用 Arduino 自製的鍵盤 或 RP2040 改裝的遊戲控制器非常相似
但分別在於在下需要每次修改內容,都需要修改原始碼、編譯、燒錄到微控制器
而巨集鍵盤則不需要編程知識便可以修改,但需要特定軟件才能修改
因此在下經常抱怨這些裝置只能使用只支援 Windows 的特定軟件

但由於在下已經使用 Arduino 之類微控制器開發板,而且在下過去亦曾經逆向工程一些裝置
因此在下才估計這類軟件是向裝置發送序列資料來修改內容

VirtualBox 亦支援 USB裝置 在 虛擬機 上運作,所以才想到在 虛擬機 運行 Windows 並執行該軟件
再讓 Wireshark 監察 USB資料 ,結果資料能正確地擷取

可能閣下會覺得能夠在 虛擬機Windows 設定內容,不需要大費周章地逆向工程
但如果考慮並非所有電腦都有足夠效能執行 虛擬機,能夠不依賴 Windows 便能夠修改內容是有必要性
而且這個逆向工程亦啟發了在下之前的鍵盤及遊戲控制器專案,可以使用序列資料修改內容
便不需要每次修改都要修改原始碼、編譯、燒錄到微控制器

最後,在下將 資料寫入 自動化,但 拔插巨集鍵盤 及 檢查偵錯 仍然需要人手操作
不過,在下已經將 操作時間最長、最容易出錯 的部分 自動化
這樣即使是不具備特定知識及技術的人也能輕鬆完成,而且簡化了大部分工序
因此,在下認為編寫一個能降低錯誤率的工具,比使用傳統工具更安全

參考資料