在下曾因為一些 巨集鍵盤或滑鼠 的 更新程式 沒有 Linux版本 而 逆向工程更新訊號,
根據訊號的規格自行編寫了 Python程式 來更新,讓 Linux 也能更新這些裝置。
不過,使用 Python 仍然需要額外安裝一些 函式庫,因此在下希望能找到更方便的操作方法。
WebHID API
WebHID API 是一種網頁技術,允許網頁存取 HID,
然而,與大多數能讀取硬件裝置的 Web API 技術相同,目前大多數 網頁瀏覽器 仍未支援或實作這些功能,
暫時只有 基於 Chromium 的 網頁瀏覽器 支援 WebHID API。
載入 HID
要使用 WebHID API 存取 HID ,需要使用:
navigator.hid.requestDevice({
"filters": [
]
});
WebHID API 會傳回 Promise物件 ,並保存 HIDDevice物件陣列 。
載入要求
要留意, WebHID API 禁止自動執行,不論是直接在網頁載入後 直接或間接呼叫 或 以定時器方式延遲呼叫 都不被允許,否則將會出現錯誤:
Uncaught (in promise) SecurityError: Failed to execute 'requestDevice' on 'HID': Must be handling a user gesture to show a permission request.
必須由使用者 互動操作 才能啟動。
function connectHID() {
let devices = navigator.hid.requestDevice({
"filters": [
]
});
console.log(devices);
}
window.addEventListener("load", function(loadEvent) {
document.getElementById("connect-hid").addEventListener("click", function(clickEvent) {
connectHID();
});
});
WebHID API 執行時,網頁會 請求存取 HID ,使用者選擇 需要存取的 HID
傳回內容
無論是點擊取消還是連線, WebHID API 都會傳回 Promise物件
如果點擊 取消, Promise物件 的 PromiseResult 為 空白陣列
傳回 Promise物件
如果點擊 連線, Promise物件 的 PromiseResult 會保存 HIDDevice物件陣列
function connectHID() {
let devices = navigator.hid.requestDevice({
"filters": [
]
});
devices.then(function(result) {
console.log(result);
});
}
window.addEventListener("load", function(loadEvent) {
document.getElementById("connect-hid").addEventListener("click", function(clickEvent) {
connectHID();
});
});
Promise物件 可以使用 then 方法來讀取每個 HIDDevice物件。
由於 Promise物件 不屬於 主執行緒 (Main Thread), Promise物件 可能會因為 非同步操作 (Asynchronous Operations) 而在不知情的情況下發生變化。
傳回 HIDDevice物件陣列
async function connectHID() {
let devices = await navigator.hid.requestDevice({
"filters": [
]
});
console.log(devices);
}
window.addEventListener("load", function(loadEvent) {
document.getElementById("connect-hid").addEventListener("click", async function(clickEvent) {
await connectHID();
});
});
為了避免 非同步操作 影響 Promise物件,在下改用 await 等待 WebHID API,
而使用 await 必須在 非同步函式 中宣告為 async,否則會導致錯誤。
Uncaught SyntaxError: await is only valid in async functions and the top level bodies of modules
雖然 async 使功能變為 非同步,但其內部運行依然是同步的,不過,傳回 Promise物件 的 則是非同步的,
當使用 await 執行 WebHID API 時,會中止 Promise物件,並等待操作完成,
並直接傳回 PromiseResult , 而傳回的資料則不會受到其他 非同步操作 的影響。
判斷 HID界面
不論是使用 Promise物件 或 await/async語法,最重要是 HIDDevice物件陣列,
一般情況下, HIDDevice物件陣列 通常只有一個 HIDDevice物件,但某些 巨集HID 會提供多於一個的界面,以區分不同的功能,
這些界面可能分別用於 輸入裝置 及 更新韌體設定。
async function connectHID() {
let devices = await navigator.hid.requestDevice({
"filters": [
]
});
for (let i in devices) {
for (let j in devices[i].collections) {
if (devices[i].collections[j].inputReports.length > 0 && devices[i].collections[j].outputReports.length > 0) {
console.log(devices[i]);
}
}
}
}
window.addEventListener("load", function(loadEvent) {
document.getElementById("connect-hid").addEventListener("click", async function(clickEvent) {
await connectHID();
});
});
在下並沒有確定的方法來尋找對應 更新韌體設定 的界面,但在下推測 更新韌體設定 的操作需要與 宿主裝置 通訊,
也就是需要 接收 及 回應 資料,因此 HIDDevice物件 需要同時具備 輸入 及 輸出 功能,
因此在下認為檢查 HIDDevice.collections 是否同時擁有 inputReports 及 outputReports 來判斷界面是否適合。
HID 權限
async function connectHID() {
let devices = await navigator.hid.requestDevice({
"filters": [
]
});
for (let i in devices) {
for (let j in devices[i].collections) {
if (devices[i].collections[j].inputReports.length > 0 && devices[i].collections[j].outputReports.length > 0) {
await devices[i].open();
}
}
}
}
window.addEventListener("load", function(loadEvent) {
document.getElementById("connect-hid").addEventListener("click", async function(clickEvent) {
await connectHID();
});
});
基於 Linux 的安全限制,一般使用者無法存取 HID ,必須使用 root 或 sudo 才能存取 HID,
如果以一般使用者身份嘗試開啟裝置,會出現錯誤:
Uncaught (in promise) NotAllowedError: Failed to open the device.
設定 udev規則
理論上可以使用 root 或 sudo 來啟動基於 Chromium 的網頁瀏覽器,但同樣因為 Linux 的安全限制無法以 root 或 sudo 啟動,
因此,只能設定 udev規則 ,讓一般使用者能夠存取 HID。
在 /etc/udev/rules.d 中建立目標 HID 的 .rules 檔案,並在檔案中輸入:
KERNEL=="hidraw*", ATTRS{busnum}=="*", ATTRS{idVendor}=="HID-VID", ATTRS{idProduct}=="HID-PID", MODE="0666"
將 HID-VID 及 HID-PID 替換為目標 HID 的 十六進制 VID 及 PID
完成後,可以選擇 重新啟動系統 或 輸入:
sudo udevadm control --reload-rules sudo udevadm trigger sudo service udev restart
發送資料
async function connectHID() {
let devices = await navigator.hid.requestDevice({
"filters": [
]
});
for (let i in devices) {
for (let j in devices[i].collections) {
if (devices[i].collections[j].inputReports.length > 0 && devices[i].collections[j].outputReports.length > 0) {
await devices[i].open();
let byteArray = []; // byte array send to HID
await devices[i].sendReport(devices[i].collections[j].outputReports[0].reportId, new Uint8Array(byteArray));
}
}
}
}
window.addEventListener("load", function(loadEvent) {
document.getElementById("connect-hid").addEventListener("click", async function(clickEvent) {
await connectHID();
});
});
確定 HIDDevice物件 能夠啟用後,就可以使用 HIDDevice.sendReport(reportId, uint8array) 向 HID 發送資料。
回應資料
async function connectHID() {
let devices = await navigator.hid.requestDevice({
"filters": [
]
});
for (let i in devices) {
for (let j in devices[i].collections) {
if (devices[i].collections[j].inputReports.length > 0 && devices[i].collections[j].outputReports.length > 0) {
devices[i].addEventListener("inputreport", function(inputreportEvent) {
let data = [...new Uint8Array(inputreportEvent.data.buffer)];
console.log(data);
});
await devices[i].open();
let byteArray = []; // byte array send to HID
await devices[i].sendReport(devices[i].collections[j].outputReports[0].reportId, new Uint8Array(byteArray));
}
}
}
}
window.addEventListener("load", function(loadEvent) {
document.getElementById("connect-hid").addEventListener("click", async function(clickEvent) {
await connectHID();
});
});
除了向 HID 發送資料,還需要獲取 HID 的回應資料,
可以在 HIDDevice物件 中加入 InputReport事件 來接收 HID 的回應資料。
發送及回應次序問題
async function connectHID() {
let devices = await navigator.hid.requestDevice({
"filters": [
]
});
for (let i in devices) {
for (let j in devices[i].collections) {
if (devices[i].collections[j].inputReports.length > 0 && devices[i].collections[j].outputReports.length > 0) {
devices[i].addEventListener("inputreport", function(inputreportEvent) {
console.log("input");
});
await devices[i].open();
let byteArray = []; // byte array send to HID
console.log("output");
await devices[i].sendReport(devices[i].collections[j].outputReports[0].reportId, new Uint8Array(byteArray));
console.log("output");
await devices[i].sendReport(devices[i].collections[j].outputReports[0].reportId, new Uint8Array(byteArray));
}
}
}
}
window.addEventListener("load", function(loadEvent) {
document.getElementById("connect-hid").addEventListener("click", async function(clickEvent) {
await connectHID();
});
});
然而,在下在測試時發現向 HID 發送兩次資料,回應的次序並不正確,預期的情況是:
第1次發送資料後應該接收到第1次回應,然後第2次發送資料後接收第2次回應,
但測試結果是,發送2次資料後接收兩2回應,
雖然不會影響操作,但如果 發送 與 接收 的資料不是同一次序,這可能在偵錯時造成難以找到正確的配對資料。
修正發送及回應次序
async function connectHID() {
let devices = await navigator.hid.requestDevice({
"filters": [
]
});
for (let i in devices) {
for (let j in devices[i].collections) {
if (devices[i].collections[j].inputReports.length > 0 && devices[i].collections[j].outputReports.length > 0) {
await devices[i].open();
let reportId = devices[i].collections[j].outputReports[0].reportId;
let byteArray = []; // byte array send to HID
await sendData(devices[i], reportId, byteArray);
await sendData(devices[i], reportId, byteArray);
}
}
}
}
async function sendData(device, reportId, byteArray) {
console.log("output");
await device.sendReport(reportId, new Uint8Array(byteArray));
let data = await retrieveData(device);
console.log("input");
return data;
}
function retrieveData(device) {
return new Promise(function(resolve) {
device.addEventListener("inputreport", function eventListener(inputreportEvent) {
let data = [...new Uint8Array(inputreportEvent.data.buffer)];
resolve(data);
device.removeEventListener("inputreport", eventListener);
});
});
}
window.addEventListener("load", function(loadEvent) {
document.getElementById("connect-hid").addEventListener("click", async function(clickEvent) {
await connectHID();
});
});
在下將 獲取回應資料 於 Promise物件中 執行,並使用 async/await 的方式強制等待 回應資料,
才能執行下次發送操作,以確保 發送 及 回應 的順序要一致。
自製 HIDConnector 類別
// HIDConnector.js
class HIDConnector {
static ORDER_BIG_ENDIAN = true;
static ORDER_SMALL_ENDIAN = false;
static valueToByteArray(value, length, order) {
let byteArray = [];
for (let i = 0; i < length || value > 0; i++) {
byteArray.push(value & 0xFF);
value >>= 8;
}
if (order) {
byteArray = byteArray.reverse();
}
return byteArray;
}
static byteArrayToValue(byteArray, order) {
let array = byteArray.slice();
if (order) {
array = array.reverse();
}
let value = 0;
for (let i in array) {
value |= array[i] << (i << 3);
}
return value;
}
static decToHexString(dec, length) {
return dec.toString(16).padStart(length, "0").toUpperCase();
}
static printDebugData(title, data, columns) {
if (columns > 1) {
let map = ["".padStart(4, " ")];
for (let i = 0; i < columns; i++) {
map.push(HIDConnector.decToHexString(i, 2));
}
map = [map.join(" ")];
for (let i = 0; i < data.length; i += columns) {
let bytes = data.slice(i, i + columns);
for (let j in bytes) {
bytes[j] = HIDConnector.decToHexString(bytes[j], 2);
}
bytes = [HIDConnector.decToHexString(i, 4)].concat(bytes);
map.push(bytes.join(" "));
}
console.log(`---------- ${title} ----------`);
console.log(map.join("\n"));
}
}
#device = null;
#reportId = null;
constructor() {
}
async connect(vendorId, productId) {
try {
if (this.#device == null) {
let devices = await navigator.hid.requestDevice({
"filters": [
{
"vendorId": vendorId,
"productId": productId,
},
]
});
for (let i in devices) {
for (let j in devices[i].collections) {
if (devices[i].collections[j].inputReports.length > 0 && devices[i].collections[j].outputReports.length > 0 && devices[i].vendorId == vendorId && devices[i].productId == productId) {
this.#device = devices[i];
this.#reportId = this.#device.collections[j].outputReports[0].reportId;
break;
}
}
if (this.#device != null) {
await this.#device.open();
break;
}
}
}
} catch (error) {
throw error;
}
}
async sendData(outputByteArray) {
try {
this.#device.sendReport(this.#reportId, new Uint8Array(outputByteArray));
let inputByteArray = await this.#retrieveData();
return inputByteArray;
} catch (error) {
throw error;
}
}
#retrieveData() {
return new Promise(function(resolve) {
this.#device.addEventListener("inputreport", function eventListener(inputreportEvent) {
let inputByteArray = [...new Uint8Array(inputreportEvent.data.buffer)];
resolve(inputByteArray);
this.#device.removeEventListener("inputreport", eventListener);
}.bind(this));
}.bind(this));
}
}
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title></title>
<script src="HIDConnector.js"></script>
<script>
// <!--
const VENDOR_ID_HEX = 0x0000;
const PRODUCT_ID_HEX = 0x0000;
let hidConnector = new HIDConnector(VENDOR_ID_HEX, PRODUCT_ID_HEX);
window.addEventListener("load", function(loadEvent) {
document.getElementById("hid-connector").addEventListener("click", async function(clickEvent) {
await hidConnector.connect();
let byteArray = [];
console.log(byteArray);
byteArray = await hidConnector.sendData(byteArray);
console.log(byteArray);
});
});
// -->
</script>
</head>
<body>
<button id="hid-connector">HID Connector</button>
</body>
</html>
由於 Javascript 已經支援 類別(Class) 語法,因此在下以 類別 的方式編寫,可以方便使用及延伸功能。
SDCX巨集鍵盤
之前逆向工程巨集鍵盤在下覺得好用,因此在下都買來使用,但卻無法使用相同的軟件修改設定,亦即是之前逆向工程的操作都無法應用在這款 SDCX巨集鍵盤 ,
幸好這款 SDCX巨集鍵盤 的官方網頁除了提供軟件修改設定,亦有提供能夠在網頁上修改設定的功能,
亦即是在下不需要將控制訊號逆向工程都能夠在 Linux 修改設定,不過在下仍然會將控制訊號逆向工程,
其中是學習,另外是如果網頁服務服止, Linux 同樣會無法修改,因此仍然有需要逆向工程,確保巨集鍵盤不會因為在外原因而無法使用。
需要發送 64個位元組資料 來控制 SDCX巨集鍵盤 然後回應 64個位元組資料。
外觀
在下購買的 SDCX巨集鍵盤 共有 16個按鈕 及 3個旋扭 , 每個旋扭都具備按下、右旋轉及左旋轉 3種操作功能,
不過旋扭的 3種操作功能,都會當作按鈕,因此可以視這款 SDCX巨集鍵盤 實際共有 25個按鈕。
官方修改方法
SDCX巨集鍵盤 的 官方網頁 提供能夠經網頁更新的工具,只要使用基於 Chromium 的網頁瀏覽器都能夠使用,
亦即是 Linux 都能夠簡單地修改 SDCX巨集鍵盤。
不過網頁有機會失效,而軟件版本又是不支援 Linux,所以在下仍然想將 修改方法 逆向工程。
設定檔
SDCX巨集鍵盤 提供多個 設定檔使用及設定
讀取
06 05 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
讀取當設定檔的資料
回應
AA 05 0E 00 00 01 00 75 24 6A 00 00 01 bu 00 06 nn 01 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
| 代號 | 偏移 | 功能 | |
|---|---|---|---|
| 十進制 | 十六進制 | ||
| bu | 13 | 0x0D | 按鈕功能長度 |
| nn | 16 | 0x10 | 當前選取設定檔編號 |
其他資料暫時未知道用途
選取
06 FB nn 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
| 代號 | 偏移 | 功能 | |
|---|---|---|---|
| 十進制 | 十六進制 | ||
| nn | 2 | 0x02 | 選取設定檔的編號 |
選取設定檔時, SDCX巨集鍵盤 的 第0 LED 會閃動,並根據選取的設定檔編號閃動對應次數
按鈕及旋鈕
讀取
06 08 3A lo hi 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
| 代號 | 偏移 | 功能 | |
|---|---|---|---|
| 十進制 | 十六進制 | ||
| lo | 3 | 0x03 | 讀取位置 低位元組 |
| hi | 4 | 0x04 | 讀取位置 高位元組 |
讀取位置數值必須是 56的正倍數,否則會向下取整數。
回應
AA 07 3A lo hi 00 00 00 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3
| 代號 | 偏移 | 功能 | |
|---|---|---|---|
| 十進制 | 十六進制 | ||
| ty | 8 + i * 4 | 0x08 + i * 4 | 第i個 操作類型 |
| p1 | 9 + i * 4 | 0x09 + i * 4 | 第i個 參數1 |
| p2 | 10 + i * 4 | 0x0A + i * 4 | 第i個 參數2 |
| p3 | 11 + i * 4 | 0x0B + i * 4 | 第i個 參數3 |
SDCX巨集鍵盤 回應 64個位元組資料,
回應的 lo 及 hi 與指令的 lo 及 hi 相同,都是回應位置。
然後由第8位元組開始為每個按鈕的設定資料,
ty 為 按鈕類型 , p1 、 p2 、 p3 分別為對應按鈕類型的 參數。
(因此,按鈕功能需要 4位元組。)
回應資料數量 是相對於 讀取資料數量 ,即是 56位元組。
由於按鈕及旋鈕資料並非存取1次便獲取所有資料,因此需要多次調整位置數值讀取對應位置的資料,
例如在下的 SDCX巨集鍵盤 共有 100位元組資料,如果每次讀取會回應 56位元組,即是需要讀取2次才能回應所有按鈕及旋鈕資料。
const VENDOR_ID_HEX = 0x0000;
const PRODUCT_ID_HEX = 0x0000;
let hidConnector = new HIDConnector(VENDOR_ID_HEX, PRODUCT_ID_HEX);
window.addEventListener("load", function(loadEvent) {
document.getElementById("hid-connector").addEventListener("click", async function(clickEvent) {
await hidConnector.connect();
for (let i = 0; i < 2; i++) {
let position = HIDConnector.valueToByteArray(i * 56, 2, HIDConnector.ORDER_SMALL_ENDIAN);
let byteArray = [
0x06, 0x08, 0x3A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
byteArray[3] = position[0];
byteArray[4] = position[1];
HIDConnector.printDebugData(`Input ${i}`, byteArray, 16);
byteArray = await hidConnector.sendData(byteArray);
HIDConnector.printDebugData(`Output ${i}`, byteArray, 16);
}
});
});
// 讀取 及 回應 結果 // 讀取 第0 至 第55 位元組 資料 06 08 3A 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 // 回應 第0 至 第55 位元組 資料 AA 07 3A 00 00 00 00 00 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 // 讀取 第56 至 第111 位元組 資料 06 08 3A 38 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 // 回應 第56 至 第111 位元組 資料 AA 07 3A 00 00 00 00 00 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 13 00 00 00 13 00 00 00 13 00 00 00
回應 第56 至 第111 按鈕及旋鈕 位元組 資料 時,由於在下的 按鈕及旋鈕資料 只有 100位元組,
因此 只有 第0 至 第99 位元組 為 按鈕及旋鈕資料;而第100 至 第111 位元組 為 不明資料,
或 有機會是適用於更多按鈕或旋鈕的 SDCX巨集鍵盤 的設定值,亦即是調整 位置數值 有機會讀取更多 按鈕及旋鈕資料。
const VENDOR_ID_HEX = 0x0000;
const PRODUCT_ID_HEX = 0x0000;
let hidConnector = new HIDConnector(VENDOR_ID_HEX, PRODUCT_ID_HEX);
window.addEventListener("load", function(loadEvent) {
document.getElementById("hid-connector").addEventListener("click", async function(clickEvent) {
await hidConnector.connect();
let output = [];
for (let i = 0; output.length < 100; i++) {
let position = HIDConnector.valueToByteArray(i * 56, 2, HIDConnector.ORDER_SMALL_ENDIAN);
let byteArray = [
0x06, 0x08, 0x3A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
byteArray[3] = position[0];
byteArray[4] = position[1];
byteArray = await hidConnector.sendData(byteArray);
output = output.concat(byteArray.slice(8));
}
output = output.slice(0, 100);
HIDConnector.printDebugData("Output", output, 16);
});
});
// 回應 第0 至 第99 位元組 資料 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3 ty p1 p2 p3
由於 首8位元組資料 並非 按鈕及旋鈕資料,因此將每次回應的 首8位元組資料 省略,
並將所有 按鈕及旋鈕資料 連接,最後 將不必要的 按鈕及旋鈕資料 刪除,
便只剩餘需要使用的 按鈕及旋鈕資料。
操功類型
| 數值 | 功能 | |
|---|---|---|
| 十進制 | 十六進制 | |
| 16 | 0x10 | 滑鼠按鈕 |
| 17 | 0x11 | 滑鼠移動 |
| 32 | 0x20 | 鍵盤按鈕 |
| 48 | 0x30 | 媒體按鈕 |
| 64 | 0x40 | 電源操作 |
| 96 | 0x60 | 巨集操作 |
滑鼠按鈕參數
| 編號 | 位元組順序 | 功能 | |
|---|---|---|---|
| 十進制 | 十六進制 | ||
| 1 | 0x000001 | 0x01, 0x00, 0x00 | 左鍵 |
| 2 | 0x000002 | 0x02, 0x00, 0x00 | 右鍵 |
| 4 | 0x000004 | 0x04, 0x00, 0x00 | 中鍵 |
| 8 | 0x000008 | 0x08, 0x00, 0x00 | 後退 |
| 16 | 0x000010 | 0x10, 0x00, 0x00 | 前進 |
| 65536 | 0x010000 | 0x00, 0x00, 0x01 | 滾輪向上 |
| 16711680 | 0xFF0000 | 0x00, 0x00, 0xFF | 滾輪向下 |
滑鼠移動方向
| 編號 | 功能 |
|---|---|
| & 0x80 < 0 | 向左移動 |
| & 0x80 == 0 | 向右移動 |
| & 0x08 < 0 | 向上移動 |
| & 0x08 == 0 | 向下移動 |
鍵盤按鈕參數
| 編號 | 功能 | 備註 | |
|---|---|---|---|
| 十進制 | 十六進制 | ||
| 0 | 0x00 | 無效 | |
| 1 | 0x01 | 鍵盤 Error Roll Over | 通常無法使用 |
| 2 | 0x02 | 鍵盤 POST Fail | 通常無法使用 |
| 3 | 0x03 | 鍵盤 Error Undefined | 通常無法使用 |
| 4 | 0x04 | 鍵盤小寫 a | |
| 5 | 0x05 | 鍵盤小寫 b | |
| 6 | 0x06 | 鍵盤小寫 c | |
| 7 | 0x07 | 鍵盤小寫 d | |
| 8 | 0x08 | 鍵盤小寫 e | |
| 9 | 0x09 | 鍵盤小寫 f | |
| 10 | 0x0A | 鍵盤小寫 g | |
| 11 | 0x0B | 鍵盤小寫 h | |
| 12 | 0x0C | 鍵盤小寫 i | |
| 13 | 0x0D | 鍵盤小寫 j | |
| 14 | 0x0E | 鍵盤小寫 k | |
| 15 | 0x0F | 鍵盤小寫 l | |
| 16 | 0x10 | 鍵盤小寫 m | |
| 17 | 0x11 | 鍵盤小寫 n | |
| 18 | 0x12 | 鍵盤小寫 o | |
| 19 | 0x13 | 鍵盤小寫 p | |
| 20 | 0x14 | 鍵盤小寫 q | |
| 21 | 0x15 | 鍵盤小寫 r | |
| 22 | 0x16 | 鍵盤小寫 s | |
| 23 | 0x17 | 鍵盤小寫 t | |
| 24 | 0x18 | 鍵盤小寫 u | |
| 25 | 0x19 | 鍵盤小寫 v | |
| 26 | 0x1A | 鍵盤小寫 w | |
| 27 | 0x1B | 鍵盤小寫 x | |
| 28 | 0x1C | 鍵盤小寫 y | |
| 29 | 0x1D | 鍵盤小寫 z | |
| 30 | 0x1E | 鍵盤數字 1 | |
| 31 | 0x1F | 鍵盤數字 2 | |
| 32 | 0x20 | 鍵盤數字 3 | |
| 33 | 0x21 | 鍵盤數字 4 | |
| 34 | 0x22 | 鍵盤數字 5 | |
| 35 | 0x23 | 鍵盤數字 6 | |
| 36 | 0x24 | 鍵盤數字 7 | |
| 37 | 0x25 | 鍵盤數字 8 | |
| 38 | 0x26 | 鍵盤數字 9 | |
| 39 | 0x27 | 鍵盤數字 0 | |
| 40 | 0x28 | 鍵盤 Enter | |
| 41 | 0x29 | 鍵盤 Escape | |
| 42 | 0x2A | 鍵盤 Backspace | |
| 43 | 0x2B | 鍵盤 Tab | |
| 44 | 0x2C | 鍵盤 Space | |
| 45 | 0x2D | 鍵盤 - | |
| 46 | 0x2E | 鍵盤 = | |
| 47 | 0x2F | 鍵盤 [ | |
| 48 | 0x30 | 鍵盤 ] | |
| 49 | 0x31 | 鍵盤 \ | |
| 50 | 0x32 | 鍵盤 \ (非US) | |
| 51 | 0x33 | 鍵盤 ; | |
| 52 | 0x34 | 鍵盤 ' | |
| 53 | 0x35 | 鍵盤 ` | |
| 54 | 0x36 | 鍵盤 , | |
| 55 | 0x37 | 鍵盤 . | |
| 56 | 0x38 | 鍵盤 / | |
| 57 | 0x39 | 鍵盤 Caps Lock | |
| 58 | 0x3A | 鍵盤 F1 | |
| 59 | 0x3B | 鍵盤 F2 | |
| 60 | 0x3C | 鍵盤 F3 | |
| 61 | 0x3D | 鍵盤 F4 | |
| 62 | 0x3E | 鍵盤 F5 | |
| 63 | 0x3F | 鍵盤 F6 | |
| 64 | 0x40 | 鍵盤 F7 | |
| 65 | 0x41 | 鍵盤 F8 | |
| 66 | 0x42 | 鍵盤 F9 | |
| 67 | 0x43 | 鍵盤 F10 | |
| 68 | 0x44 | 鍵盤 F11 | |
| 69 | 0x45 | 鍵盤 F12 | |
| 70 | 0x46 | 鍵盤 Print Screen | |
| 71 | 0x47 | 鍵盤 Scroll Lock | |
| 72 | 0x48 | 鍵盤 Pause | |
| 73 | 0x49 | 鍵盤 Insert | |
| 74 | 0x4A | 鍵盤 Home | |
| 75 | 0x4B | 鍵盤 Page Up | |
| 76 | 0x4C | 鍵盤 Delete | |
| 77 | 0x4D | 鍵盤 End | |
| 78 | 0x4E | 鍵盤 Page Down | |
| 79 | 0x4F | 鍵盤 Right Arrow | |
| 80 | 0x50 | 鍵盤 Left Arrow | |
| 81 | 0x51 | 鍵盤 Down Arrow | |
| 82 | 0x52 | 鍵盤 Up Arrow | |
| 83 | 0x53 | 鍵盤 Num Lock | |
| 84 | 0x54 | 數字鍵盤 / | |
| 85 | 0x55 | 數字鍵盤 * | |
| 86 | 0x56 | 數字鍵盤 - | |
| 87 | 0x57 | 數字鍵盤 + | |
| 88 | 0x58 | 數字鍵盤 Enter | |
| 89 | 0x59 | 數字鍵盤 1 | |
| 90 | 0x5A | 數字鍵盤 2 | |
| 91 | 0x5B | 數字鍵盤 3 | |
| 92 | 0x5C | 數字鍵盤 4 | |
| 93 | 0x5D | 數字鍵盤 5 | |
| 94 | 0x5E | 數字鍵盤 6 | |
| 95 | 0x5F | 數字鍵盤 7 | |
| 96 | 0x60 | 數字鍵盤 8 | |
| 97 | 0x61 | 數字鍵盤 9 | |
| 98 | 0x62 | 數字鍵盤 0 | |
| 99 | 0x63 | 數字鍵盤 . | |
| 100 | 0x64 | 數字鍵盤 < | |
| 101 | 0x65 | 鍵盤 Application | |
| 102 | 0x66 | 系統 關機 | |
| 103 | 0x67 | 數字鍵盤 = | |
| 104 | 0x68 | F13 | 不同系統功能不同 |
| 105 | 0x69 | F14 | 不同系統功能不同 |
| 106 | 0x6A | F15 | 不同系統功能不同 |
| 107 | 0x6B | F16 | 不同系統功能不同 |
| 108 | 0x6C | F17 | 不同系統功能不同 |
| 109 | 0x6D | F18 | 不同系統功能不同 |
| 110 | 0x6E | F19 | 不同系統功能不同 |
| 111 | 0x6F | F20 | 不同系統功能不同 |
| 112 | 0x70 | F21 | 不同系統功能不同 |
| 113 | 0x71 | F22 | 不同系統功能不同 |
| 114 | 0x72 | F23 | 不同系統功能不同 |
| 115 | 0x73 | F24 | 不同系統功能不同 |
| 116 | 0x74 | Open | 通常無法使用 |
| 117 | 0x75 | Help | 通常無法使用 |
| 118 | 0x76 | Props | 通常無法使用 |
| 119 | 0x77 | Select | 通常無法使用 |
| 120 | 0x78 | Stop | 通常無法使用 |
| 121 | 0x79 | Redo | 通常無法使用 |
| 122 | 0x7A | Undo | 通常無法使用 |
| 123 | 0x7B | Cut | 通常無法使用 |
| 124 | 0x7C | Copy | 通常無法使用 |
| 125 | 0x7D | Paste | 通常無法使用 |
| 126 | 0x7E | Find | 通常無法使用 |
| 127 | 0x7F | 系統 靜音 | |
| 128 | 0x80 | 系統 音量增加 | |
| 129 | 0x81 | 系統 音量減少 | |
| 130 | 0x82 | 系統 鎖定 Caps Lock | 通常無法使用 |
| 131 | 0x83 | 系統 鎖定 Num Lock | 通常無法使用 |
| 132 | 0x84 | 系統 鎖定 Scroll Lock | 通常無法使用 |
| 133 | 0x85 | 數字鍵盤 , | 通常無法使用 |
| 134 | 0x86 | 數字鍵盤 = | 通常無法使用 |
| 135 | 0x87 | International 1 | 不同系統功能不同 |
| 136 | 0x88 | International 2 | 不同系統功能不同 |
| 137 | 0x89 | International 3 | 不同系統功能不同 |
| 138 | 0x8A | International 4 | 不同系統功能不同 |
| 139 | 0x8B | International 5 | 不同系統功能不同 |
| 140 | 0x8C | International 6 | 不同系統功能不同 |
| 141 | 0x8D | International 7 | 不同系統功能不同 |
| 142 | 0x8E | International 8 | 不同系統功能不同 |
| 143 | 0x8F | International 9 | 不同系統功能不同 |
| 144 | 0x90 | Lang 1 | 不同系統功能不同 |
| 145 | 0x91 | Lang 2 | 不同系統功能不同 |
| 146 | 0x92 | Lang 3 | 不同系統功能不同 |
| 147 | 0x93 | Lang 4 | 不同系統功能不同 |
| 148 | 0x94 | Lang 5 | 不同系統功能不同 |
| 149 | 0x95 | Lang 6 | 不同系統功能不同 |
| 150 | 0x96 | Lang 7 | 不同系統功能不同 |
| 151 | 0x97 | Lang 8 | 不同系統功能不同 |
| 152 | 0x98 | Lang 9 | 不同系統功能不同 |
| 153 | 0x99 | Alternative Erase | 通常與 Delete 相同 |
| 154 | 0x9A | SysReq | 系統保留功能 |
| 155 | 0x9B | Cancel | 通常無法使用 |
| 156 | 0x9C | Clear | 通常無法使用 |
| 157 | 0x9D | Prior | 通常無法使用 |
| 158 | 0x9E | Return | 通常無法使用 |
| 159 | 0x9F | Separator | 通常無法使用 |
| 160 | 0xA0 | Out | 通常無法使用 |
| 161 | 0xA1 | Oper | 通常無法使用 |
| 162 | 0xA2 | Clear/Again | 通常無法使用 |
| 163 | 0xA3 | CrSel/Props | 通常無法使用 |
| 164 | 0xA4 | ExSel | 通常無法使用 |
| 165 | 0xA5 | 保留 | |
| 166 | 0xA6 | 保留 | |
| 167 | 0xA7 | 保留 | |
| 168 | 0xA8 | 保留 | |
| 169 | 0xA9 | 保留 | |
| 170 | 0xAA | 保留 | |
| 171 | 0xAB | 保留 | |
| 172 | 0xAC | 保留 | |
| 173 | 0xAD | 保留 | |
| 174 | 0xAE | 保留 | |
| 175 | 0xAF | 保留 | |
| 176 | 0xB0 | 數字鍵盤 00 | 通常無法使用 |
| 177 | 0xB1 | 數字鍵盤 000 | 通常無法使用 |
| 178 | 0xB2 | 數字鍵盤 Thousands Separator | 通常無法使用 |
| 179 | 0xB3 | 數字鍵盤 Decimal Separator | 通常無法使用 |
| 180 | 0xB4 | 數字鍵盤 Currency Unit | 通常無法使用 |
| 181 | 0xB5 | 數字鍵盤 Currency Sub-unit | 通常無法使用 |
| 182 | 0xB6 | 數字鍵盤 ( | 通常無法使用 |
| 183 | 0xB7 | 數字鍵盤 ) | 通常無法使用 |
| 184 | 0xB8 | 數字鍵盤 { | 通常無法使用 |
| 185 | 0xB9 | 數字鍵盤 } | 通常無法使用 |
| 186 | 0xBA | 數字鍵盤 Tab | 通常無法使用 |
| 187 | 0xBB | 數字鍵盤 Backspace | 通常無法使用 |
| 188 | 0xBC | 數字鍵盤 A | 通常無法使用 |
| 189 | 0xBD | 數字鍵盤 B | 通常無法使用 |
| 190 | 0xBE | 數字鍵盤 C | 通常無法使用 |
| 191 | 0xBF | 數字鍵盤 D | 通常無法使用 |
| 192 | 0xC0 | 數字鍵盤 E | 通常無法使用 |
| 193 | 0xC1 | 數字鍵盤 F | 通常無法使用 |
| 194 | 0xC2 | 數字鍵盤 ~ | 通常無法使用 |
| 195 | 0xC3 | 數字鍵盤 ^ | 通常無法使用 |
| 196 | 0xC4 | 數字鍵盤 % | 通常無法使用 |
| 197 | 0xC5 | 數字鍵盤 < | 通常無法使用 |
| 198 | 0xC6 | 數字鍵盤 > | 通常無法使用 |
| 199 | 0xC7 | 數字鍵盤 & | 通常無法使用 |
| 200 | 0xC8 | 數字鍵盤 && | 通常無法使用 |
| 201 | 0xC9 | 數字鍵盤 | | 通常無法使用 |
| 202 | 0xCA | 數字鍵盤 || | 通常無法使用 |
| 203 | 0xCB | 數字鍵盤 : | 通常無法使用 |
| 204 | 0xCC | 數字鍵盤 # | 通常無法使用 |
| 205 | 0xCD | 數字鍵盤 Space | 通常無法使用 |
| 206 | 0xCE | 數字鍵盤 @ | 通常無法使用 |
| 207 | 0xCF | 數字鍵盤 ! | 通常無法使用 |
| 208 | 0xD0 | 數字鍵盤 Memory Store | 通常無法使用 |
| 209 | 0xD1 | 數字鍵盤 Memory Recall | 通常無法使用 |
| 210 | 0xD2 | 數字鍵盤 Memory Clear | 通常無法使用 |
| 211 | 0xD3 | 數字鍵盤 Memory Add | 通常無法使用 |
| 212 | 0xD4 | 數字鍵盤 Memory Subtract | 通常無法使用 |
| 213 | 0xD5 | 數字鍵盤 Memory Multiply | 通常無法使用 |
| 214 | 0xD6 | 數字鍵盤 Memory Divide | 通常無法使用 |
| 215 | 0xD7 | 數字鍵盤 +/- | 通常無法使用 |
| 216 | 0xD8 | 數字鍵盤 Clear | 通常無法使用 |
| 217 | 0xD9 | 數字鍵盤 Clear Entry | 通常無法使用 |
| 218 | 0xDA | 數字鍵盤 Binary | 通常無法使用 |
| 219 | 0xDB | 數字鍵盤 Octal | 通常無法使用 |
| 220 | 0xDC | 數字鍵盤 Decimal | 通常無法使用 |
| 221 | 0xDD | 數字鍵盤 Hexadecimal | 通常無法使用 |
| 222 | 0xDE | 保留 | |
| 223 | 0xDF | 保留 | |
| 224 | 0xE0 | 鍵盤 左Ctrl | |
| 225 | 0xE1 | 鍵盤 左Shift | |
| 226 | 0xE2 | 鍵盤 左Alt | |
| 227 | 0xE3 | 鍵盤 左Meta | |
| 228 | 0xE4 | 鍵盤 右Ctrl | |
| 229 | 0xE5 | 鍵盤 右Shift | |
| 230 | 0xE6 | 鍵盤 右Alt | |
| 231 | 0xE7 | 鍵盤 右Meta | |
| 232 | 0xE8 | 保留 | |
| 233 | 0xE9 | 保留 | |
| 234 | 0xEA | 保留 | |
| 235 | 0xEB | 保留 | |
| 236 | 0xEC | 保留 | |
| 237 | 0xED | 保留 | |
| 238 | 0xEE | 保留 | |
| 239 | 0xEF | 保留 | |
| 240 | 0xF0 | 保留 | |
| 241 | 0xF1 | 保留 | |
| 242 | 0xF2 | 保留 | |
| 243 | 0xF3 | 保留 | |
| 244 | 0xF4 | 保留 | |
| 245 | 0xF5 | 保留 | |
| 246 | 0xF6 | 保留 | |
| 247 | 0xF7 | 保留 | |
| 248 | 0xF8 | 保留 | |
| 249 | 0xF9 | 保留 | |
| 250 | 0xFA | 保留 | |
| 251 | 0xFB | 保留 | |
| 252 | 0xFC | 保留 | |
| 253 | 0xFD | 保留 | |
| 254 | 0xFE | 保留 | |
| 255 | 0xFF | 保留 | |
媒體按鈕參數
| 編號 | 位元組順序 | 功能 | |
|---|---|---|---|
| 十進制 | 十六進制 | ||
| 176 | 0x00B0 | 0xB0, 0x00 | 媒體播放 |
| 177 | 0x00B1 | 0xB1, 0x00 | 媒體暫停 |
| 178 | 0x00B2 | 0xB2, 0x00 | 媒體攝錄 |
| 179 | 0x00B3 | 0xB3, 0x00 | 媒體快播 |
| 180 | 0x00B4 | 0xB4, 0x00 | 媒體回播 |
| 181 | 0x00B5 | 0xB5, 0x00 | 下一個媒體 |
| 182 | 0x00B6 | 0xB6, 0x00 | 上一個媒體 |
| 183 | 0x00B7 | 0xB7, 0x00 | 媒體停止 |
| 184 | 0x00B8 | 0xB8, 0x00 | 媒體退出 |
| 204 | 0x00CC | 0xCC, 0x00 | 媒體停止及退出 |
| 205 | 0x00CD | 0xCD, 0x00 | 媒體播放或暫停 |
| 226 | 0x00E2 | 0xE2, 0x00 | 靜音切換 |
| 233 | 0x00E9 | 0xE9, 0x00 | 音量提升 |
| 234 | 0x00EA | 0xEA, 0x00 | 音量降低 |
| 387 | 0x0183 | 0x83, 0x01 | 開啟控制中心 |
| 394 | 0x018A | 0x8A, 0x01 | 開啟郵件 |
| 402 | 0x0192 | 0x92, 0x01 | 開啟計算機 |
| 404 | 0x0194 | 0x94, 0x01 | 開啟檔案瀏覽器 |
| 406 | 0x0196 | 0x96, 0x01 | 開啟網頁瀏覽器 |
| 545 | 0x0221 | 0x21, 0x02 | 搜尋 |
| 547 | 0x0223 | 0x23, 0x02 | 網頁瀏覽器首頁 |
| 548 | 0x0224 | 0x24, 0x02 | 網頁瀏覽器後退 |
| 549 | 0x0225 | 0x25, 0x02 | 網頁瀏覽器前進 |
| 550 | 0x0226 | 0x26, 0x02 | 網頁瀏覽器停止 |
| 551 | 0x0227 | 0x27, 0x02 | 網頁瀏覽器重新載入 |
| 554 | 0x022A | 0x2A, 0x02 | 網頁瀏覽器書籤 |
電源操作參數
| 編號 | 功能 | |
|---|---|---|
| 十進制 | 十六進制 | |
| 1 | 0x01 | 關機 |
| 2 | 0x02 | 睡眠 |
| 4 | 0x04 | 喚醒 |
巨集類型參數
| 數值 | 功能 | |
|---|---|---|
| 十進制 | 十六進制 | |
| 0 | 0x00 | 循環次數 |
| 1 | 0x01 | 循環直至觸發鍵開放 |
| 2 | 0x02 | 循環直至其他鍵按下 |
| 3 | 0x03 | 循環直至觸發鍵再按下 |
操作類型與參數配對
| 操作類型 | 參數1 | 參數2 | 參數3 |
|---|---|---|---|
| 滑鼠按鈕 | 滑鼠按鈕參數[2] | 滑鼠按鈕參數[1] | 滑鼠按鈕參數[0] |
| 滑鼠移動 | 移動方向 | 水平偏移量 | 垂直偏移量 |
| 鍵盤按鈕 | 鍵盤修飾鍵 | 鍵盤按鈕參數 | 0x00 |
| 媒體按鈕 | 媒體按鈕參數[1] | 媒體按鈕參數[0] | 0x00 |
| 電源操作 | 電源操作參數 | 0x00 | 0x00 |
| 巨集操作 | 巨集編號 | 巨集重覆次數 | 巨集類型 |
設定
06 10 07 nn 00 00 00 00 ty p1 p2 p3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
| 代號 | 偏移 | 功能 | |
|---|---|---|---|
| 十進制 | 十六進制 | ||
| nn | 3 | 0x03 | 按扭及旋鈕編號 |
| ty | 8 | 0x08 | 操作類型 |
| p1 | 9 | 0x09 | 參數1 |
| p2 | 10 | 0x0A | 參數2 |
| p3 | 11 | 0x0B | 參數3 |
設定 按鈕及旋鈕資料 相對 讀取資料 簡單,
根據以上的 類型資料 及對應的 參數資料 ,便能將 功能 設定到指定的 按鈕或旋鈕編號,
第0 按鈕或旋鈕編號 為 0 , 第1 按鈕或旋鈕編號 為 4 …… 第i 按鈕或旋鈕編號 為 i * 4。
LED模式
讀取
06 0A 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
讀取當前LED模式的資料
回應
AA 0A 0B 00 00 01 00 mo br sp 00 cu 00 hh ss vv 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
| 代號 | 偏移 | 功能 | |
|---|---|---|---|
| 十進制 | 十六進制 | ||
| mo | 7 | 0x07 | 模式編號 |
| br | 8 | 0x08 | 光度數值 |
| sp | 9 | 0x09 | 速度數值 |
| cu | 11 | 0x0B | 是否自訂 |
| hh | 13 | 0x0D | HSV/HSB色彩空間 的 色相(Hue) 數值 |
| ss | 14 | 0x0E | HSV/HSB色彩空間 的 飽和度(Saturation) 數值 |
| vv | 15 | 0x0F | HSV/HSB色彩空間 的 明度(Value) 數值 |
通常 HSV/HSB色彩空間 的:
- 色相 範圍是 0 至 360
- 飽和度 範圍是 0 至 100
- 明度 範圍是 0 至 100
但 SDCX巨集鍵盤 強制將數值轉換成 0 至 255 以符合 1位元組 的限制,
因此如果能夠將數值轉換成對應 色相 、 飽和度 、 明度 的範圍會比較容易理解。
設定
06 0B 0B 00 00 01 00 mo br sp 00 cu 00 hh ss vv 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
| 代號 | 偏移 | 功能 | |
|---|---|---|---|
| 十進制 | 十六進制 | ||
| mo | 7 | 0x07 | 模式編號 |
| br | 8 | 0x08 | 光度數值 |
| sp | 9 | 0x09 | 速度數值 |
| cu | 11 | 0x0B | 是否自訂 |
| hh | 13 | 0x0D | HSV/HSB色彩空間 的 色相(Hue) 數值 |
| ss | 14 | 0x0E | HSV/HSB色彩空間 的 飽和度(Saturation) 數值 |
| vv | 15 | 0x0F | HSV/HSB色彩空間 的 明度(Value) 數值 |
回應LED模式的資料相同,
同樣使用 HSV/HSB色彩空間 , 色相 、 飽和度 、 明度 範圍都是 0 至 255,
但原本 色相 、 飽和度 、 明度 範圍都不是 0 至 255 ,因此需要設定值轉換為 0 至 255 數值。
測試 切換LED模式。
測試 改變 LED 光度 及 速度。
測試 自訂 LED。
LED顏色
讀取
06 13 3A lo hi 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
| 代號 | 偏移 | 功能 | |
|---|---|---|---|
| 十進制 | 十六進制 | ||
| lo | 3 | 0x03 | 讀取位置 低位元組 |
| hi | 4 | 0x04 | 讀取位置 高位元組 |
讀取位置數值必須是 56的正倍數,否則會向下取整數。
回應
AA 13 3A lo hi 00 00 00 rr gg bb rr gg bb rr gg bb rr gg bb rr gg bb rr gg bb rr gg bb rr gg bb rr gg bb rr gg bb rr gg bb rr gg bb rr gg bb rr gg bb rr gg bb rr gg bb rr gg bb rr gg bb rr gg
| 代號 | 偏移 | 功能 | |
|---|---|---|---|
| 十進制 | 十六進制 | ||
| rr | 8 + i * 3 | 0x08 + i * 3 | 第i個 RGB色彩空 的 紅值(Red) |
| bb | 9 + i * 3 | 0x09 + i * 3 | 第i個 RGB色彩空 的 綠值(Green) |
| bb | 10 + i * 3 | 0x0A + i * 3 | 第i個 RGB色彩空 的 藍值(Blue) |
SDCX巨集鍵盤 回應 64個位元組資料,
回應的 lo 及 hi 與指令的 lo 及 hi 相同,都是回應位置。
然後由第8位元組開始為每個按鈕的設定資料,
rr 、 gg 、 bb 分別是 紅色、綠色、藍色 ,範圍為 0 至 255。
(因此,LED顏色需要 3位元組。)
回應資料數量 是相對於 讀取資料數量 ,即是 56位元組。
由於LED顏色資料並非存取1次便獲取所有資料,因此需要多次調整位置數值讀取對應位置的資料,
例如在下的 SDCX巨集鍵盤 有 16粒LED,每粒LED用3位元組,因此總共用48位元組,
因此只需要讀取1次就足夠,但有可能有其他 SDCX巨集鍵盤 超過16粒LED ,要讀取所有資料需要執行超過1次。
設定
06 14 03 nn 00 00 00 00 rr gg bb 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
| 代號 | 偏移 | 功能 | |
|---|---|---|---|
| 十進制 | 十六進制 | ||
| nn | 2 | 0x02 | 選取LED的編號 |
| rr | 8 | 0x08 | RGB色彩空 的 紅值(Red) |
| bb | 9 | 0x09 | RGB色彩空 的 綠值(Green) |
| bb | 10 | 0x0A | RGB色彩空 的 藍值(Blue) |
設定 LED顏色 相對 讀取資料 簡單,
第0 LED編號 為 0 , 第1 LED編號 為 3 …… 第i LED編號 為 i * 3。
測試 LED自訂模 個別設定 LED顏色。
巨集
讀取
06 0C 38 lo hi 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
| 代號 | 偏移 | 功能 | |
|---|---|---|---|
| 十進制 | 十六進制 | ||
| lo | 3 | 0x03 | 讀取位置 低位元組 |
| hi | 4 | 0x04 | 讀取位置 高位元組 |
讀取位置數值必須是 56的正倍數,否則會向下取整數。
回應
// 回應 結果 // 讀取 第0 至 第55 位元組 資料 AA 0C 38 lo hi 00 00 00 pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph // 讀取 第56 至 第111 位元組 資料 AA 0C 38 lo hi 00 00 00 pl ph pl ph pl ph pl ph dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk
巨集回應資料 比較複雜,需要將不必要的資料省略,否則分析上會比較困難。
// 讀取 第0 至 第111 位元組 資料 pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk
| 代號 | 偏移 | 功能 | |
|---|---|---|---|
| 十進制 | 十六進制 | ||
| pl | 0 + i * 2 | 0x00 + i * 2 | 第i參考位置 低位元組 |
| ph | 1 + i * 2 | 0x01 + i * 2 | 第i參考位置 高位元組 |
| dl | 64 + i * 4 | 0x40 + i * 4 | 第i個巨集動作 延遲時間(毫秒) 低位元組 |
| dh | 65 + i * 4 | 0x41 + i * 4 | 第i個巨集動作 延遲時間(毫秒) 高位元組 |
| mm | 66 + i * 4 | 0x42 + i * 4 | 第i個巨集動作 多種設定資料 |
| kk | 67 + i * 4 | 0x43 + i * 4 | 第i個巨集動作 按鍵編號 |
將不必要的資料省略後,巨集回應資料便會很清晰。
巨集回應資料 有2部分:
- [0:63] 為 巨集動作的參考位置,共 64位元組 , 每個 參考位置 為 2位元組。
- [64:4160] 為 巨集動作,共 4096位元組 , 每個 巨集動作 為 4位元組。
參考位置 的資料為指向 巨集動作的位置,然後執行連串巨集動作,直至訊號終止。
繼續或停止
| 偏移 | 功能 | |
|---|---|---|
| 十進制 | 十六進制 | |
| 0 | 0x00 | 執行當前巨集操作後移向下個巨集操作 |
| 128 | 0x80 | 執行當前巨集操作後停止 |
按下或釋放
| 偏移 | 功能 | |
|---|---|---|
| 十進制 | 十六進制 | |
| 0 | 0x00 | 釋放按鍵操作 |
| 64 | 0x40 | 按下按鍵操作 |
按鍵類型
| 偏移 | 功能 | |
|---|---|---|
| 十進制 | 十六進制 | |
| 2 | 0x02 | 鍵盤按鈕 |
| 3 | 0x03 | 滑鼠按鈕 |
| 4 | 0x04 | 滑鼠滾輪 |
按鍵操作
按鍵操作對應 滑鼠按鈕參數 、 滑鼠移動方向 、 鍵盤按鈕參數 ,因此不重覆說明。
例子
由於在下都覺得解讀後的內容非常複雜,因此在下覺得要提供例子說明。
40 00 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF 01 00 42 04 00 00 82 04
解讀資料:
- 第0參數位置 使用 2位元組 ,因此資料為 40 00。
- 即是 0x0040 = 64。
- 表示 第0 巨集動作 在 第64位元組開始。
- 第64位元組 為 第0參數位置 的 巨集動作 , 使用 4位元組 ,因此資料為 01 00 42 04。
- 延遲 使用 2位元組 , 因此資料為 01 00。
- 即是 0x0001 = 1 ,表示 延遲 1毫秒。
- 類型 資料為 0x42。
- 第7位元 是 0 , 即是需要繼續執行下一組 巨集動作。
- 第6位元 是 1 , 即是 按下按鍵操作。
- 第[3:0]位元 是 2 , 即是 鍵盤按鈕參數。
- 按鍵操作 資料為 0x04。
- 根據 鍵盤按鈕參數 是 鍵盤a。
- 延遲 使用 2位元組 , 因此資料為 01 00。
- 執行下一組 巨集動作,資料為 00 00 82 04。
- 延遲 使用 2位元組 , 因此資料為 00 00。
- 即是 0x0000 = 0 ,表示 延遲 0毫秒。
- 類型 資料為 0x82。
- 第7位元 是 1 , 即是完成後 停止 巨集動作。
- 第6位元 是 0 , 即是 釋放按鍵操作。
- 第[3:0]位元 是 2 , 即是 鍵盤按鈕參數。
- 按鍵操作 資料為 0x04。
- 根據 鍵盤按鈕參數 是 鍵盤a。
- 延遲 使用 2位元組 , 因此資料為 00 00。
解讀意思:
第0巨集 功能為:
按下 鍵盤a , 延遲 1毫秒 ; 釋放 鍵盤a , 延遲 0毫秒。
明白原理後便可以修改程式。
const VENDOR_ID_HEX = 0x0000;
const PRODUCT_ID_HEX = 0x0000;
let hidConnector = new HIDConnector(VENDOR_ID_HEX, PRODUCT_ID_HEX);
window.addEventListener("load", function(loadEvent) {
document.getElementById("hid-connector").addEventListener("click", async function(clickEvent) {
await hidConnector.connect();
let count = 64 + 4096
let output = [];
for (let i = 0; output.length < count; i++) {
let position = HIDConnector.valueToByteArray(i * 56, 2, HIDConnector.ORDER_SMALL_ENDIAN);
let byteArray = [
0x06, 0x0C, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
byteArray[3] = position[0];
byteArray[4] = position[1];
byteArray = await hidConnector.sendData(byteArray);
output = output.concat(byteArray.slice(8));
}
output = output.slice(0, count);
for (let i = 0; i < 64; i += 2) {
let array = [];
let value = HIDConnector.byteArrayToValue(output.slice(i, i + 2), HIDConnector.ORDER_SMALL_ENDIAN);
if (value < 0xFFFF) {
for (let j = value; j < count; j += 4) {
if ((output[j + 2] & 0x80) > 0) {
array = output.slice(value, j + 4);
break;
}
}
}
console.log(array);
}
});
});
// 分析前的回應資料 [dl, dh, mm, kk, dl, dh, mm, kk, dl, dh, mm, kk, dl, dh, mm, kk, ...] . . .
每個巨集都能夠完整收集所有巨集動作。
let hidConnector = new HIDConnector(VENDOR_ID_HEX, PRODUCT_ID_HEX);
window.addEventListener("load", function(loadEvent) {
document.getElementById("hid-connector").addEventListener("click", async function(clickEvent) {
await hidConnector.connect();
let count = 64 + 4096;
let output = [];
for (let i = 0; output.length < count; i++) {
let position = HIDConnector.valueToByteArray(i * 56, 2, HIDConnector.ORDER_SMALL_ENDIAN);
let byteArray = [
0x06, 0x0C, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
byteArray[3] = position[0];
byteArray[4] = position[1];
byteArray = await hidConnector.sendData(byteArray);
output = output.concat(byteArray.slice(8));
}
output = output.slice(0, count);
for (let i = 0; i < 64; i += 2) {
let value = HIDConnector.byteArrayToValue(output.slice(i, i + 2), HIDConnector.ORDER_SMALL_ENDIAN);
let array = [];
if (value < 0xFFFF) {
for (let j = value; j < count; j += 4) {
let action = output.slice(j, j + 4);
array.push({
"delay": HIDConnector.byteArrayToValue(action.slice(0, 2), HIDConnector.ORDER_SMALL_ENDIAN),
"pressed": (action[2] & 0x40) > 0,
"type": action[2] & 0x0F, // type
"key": action[3], // key
});
if ((action[2] & 0x80) > 0) {
break;
}
}
}
console.log(array);
}
});
});
// 分析後的回應資料
[
{delay: 175, pressed: true, type: 2, key: 4},
{delay: 0, pressed: false, type: 2, key: 4},
]
.
.
.
將每個巨集動作分析成對應都操作效果。
設定
// 設定 指令 // 設定 第0 至 第57 位元組 資料 06 0D 3B 00 00 pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl // 設定 第58 至 第115 位元組 資料 06 0D 3B 3B 00 ph pl ph pl ph dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh
巨集設定資料 同樣 比較複雜,需要將不必要的資料省略,否則分析上會比較困難。
// 設定 指令 // 設定 第0 至 第115 位元組 資料 pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph pl ph dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh mm kk dl dh
| 代號 | 偏移 | 功能 | |
|---|---|---|---|
| 十進制 | 十六進制 | ||
| pl | 0 + i * 2 | 0x00 + i * 2 | 第i參考位置 低位元組 |
| ph | 1 + i * 2 | 0x01 + i * 2 | 第i參考位置 高位元組 |
| dl | 64 + i * 4 | 0x40 + i * 4 | 第i個巨集動作 延遲時間(毫秒) 低位元組 |
| dh | 65 + i * 4 | 0x41 + i * 4 | 第i個巨集動作 延遲時間(毫秒) 高位元組 |
| mm | 66 + i * 4 | 0x42 + i * 4 | 第i個巨集動作 多種設定資料 |
| kk | 67 + i * 4 | 0x43 + i * 4 | 第i個巨集動作 按鍵編號 |
省略後的設定資料 與 省略後的回應資料 完全相同;然而,要設定資料並不簡單。
其他功能設定資料,都是選擇編號及設定參數來更新資料,但巨集卻是整個設定一同更新,否則會遺失其他巨集設定。
- 讀取所有巨集回應資料。
- 結構化回應資料。
- 更新指定巨集編號的巨集動作。
- 重新計算巨集編號的巨集動作開始位置。
- 連接所有巨集編號的開始位置,如果 巨集編號 沒有保存 巨集動作 ,使用 FF FF 取代。
- 連接所有巨集動作,使用 00 將資料擴充至 4096位元組。
- 將巨集編號與巨集動作合併。
- 將合併後的資料 每59位元組 分割,並在分組資料前加上 06 0D 3B lo hi 後,如果資料不足 64位元組,使用 00 將資料擴充至 64位元組。
- 發送每組資料。
設定資料 比 回應資料 更複雜。
const VENDOR_ID_HEX = 0x0000;
const PRODUCT_ID_HEX = 0x0000;
let hidConnector = new HIDConnector(VENDOR_ID_HEX, PRODUCT_ID_HEX);
window.addEventListener("load", function(loadEvent) {
document.getElementById("hid-connector").addEventListener("click", async function(clickEvent) {
await hidConnector.connect();
let count = 64 + 4096;
let output = [];
for (let i = 0; output.length < count; i++) {
let position = HIDConnector.valueToByteArray(i * 56, 2, HIDConnector.ORDER_SMALL_ENDIAN);
let byteArray = [
0x06, 0x0C, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
byteArray[3] = position[0];
byteArray[4] = position[1];
byteArray = await hidConnector.sendData(byteArray);
output = output.concat(byteArray.slice(8));
}
output = output.slice(0, count);
let macros = [];
for (let i = 0; i < 64; i += 2) {
let value = HIDConnector.byteArrayToValue(output.slice(i, i + 2), HIDConnector.ORDER_SMALL_ENDIAN);
let array = [];
if (value < 0xFFFF) {
for (let j = value; j < count; j += 4) {
if ((action[2] & 0x80) > 0) {
array = output.slice(value, j + 4);
break;
}
}
}
macros.push(array);
}
// do something here
/*
macros[0] = [
0x01, 0x00, 0x42, 0x04, // press a and delay 1 ms
0x01, 0x00, 0x82, 0x04, // release a and delay 1 ms then finished
];
// ctrl a - select all
macros[1] = [
0x01, 0x00, 0x42, 0xE0, // press ctrl and delay 1 ms
0x01, 0x00, 0x42, 0x04, // press a and delay 1 ms
0x01, 0x00, 0x02, 0x04, // release a and delay 1 ms
0x01, 0x00, 0x82, 0xE0, // release ctrl and delay 1 ms then finished
];
// ctrl shift s - save as
macros[2] = [
0x01, 0x00, 0x42, 0xE0, // press ctrl and delay 1 ms
0x01, 0x00, 0x42, 0xE1, // press shift and delay 1 ms
0x01, 0x00, 0x42, 0x16, // press a and delay 1 ms
0x01, 0x00, 0x02, 0x16, // release a and delay 1 ms
0x01, 0x00, 0x02, 0xE1, // release shift and delay 1 ms
0x01, 0x00, 0x82, 0xE0, // release ctrl and delay 1 ms then finished
];
*/
let headers = [];
let data = [];
for (let i in macros) {
let position = HIDConnector.valueToByteArray(macros[i].length > 0 ? 0x40 + data.length : 0xFFFF, 2, HIDConnector.ORDER_SMALL_ENDIAN);
headers.push(position[0]);
headers.push(position[1]);
data = [...data, ...macros[i]];
}
data = [...headers, ...data, ...new Array(4096).fill(0x00)].slice(0, 4096);
for (let i = 0; i < data.length; i += 59) {
let position = HIDConnector.valueToByteArray(i, 2, HIDConnector.ORDER_SMALL_ENDIAN);
let byteArray = [0x06, 0x0D, 0x3B, position[0], position[1], ...data.slice(i, i + 59)];
byteArray = [...byteArray, ...new Array(64).fill(0x00)].slice(0, 64);
byteArray = await hidConnector.sendData(byteArray);
console.log(byteArray);
}
});
});
由於巨集保存非常多資料,導致 設定 與 回應 的資料都非常多。
補充資料
其實在下不能確定巨集能保存多少資料,使用 SDCX官方網頁的工具 時,
每次讀取巨集資料最後的讀取位置為 0x0FE7 = 4071 , 而設定巨集資料最後的位置為 0x0FF8 = 4088 ,
兩者的資料都 接近4096位元組 ,因此判斷最多能夠保存 4096位元組 資料。
總結
在使用 WebHID API 之前,在下曾經見過 Promise 及 async/await 的語法,
只知道它們能讓網頁程式在運作時進行非同步操作,例如使用 Fetch API。
然而,當時在下只是複製別人的程式碼,並未深入了解其實際用途。
由於這個專案涉及許多同步及非同步操作,雖然 Promise 確實方便於非同步操作,
但在這種情況下,反而更需要強調同步操作,因此,在下選擇使用 async/await,以確保程式以順序執行。
此外,async/await 的編程方式更接近傳統編程方式,只需確保每次使用 await 的函式都加上 async。
在開始逆向工程 SDCX巨集鍵盤 指令時,最讓在下感到奇怪的是,
自訂LED 的設定並不是使用符合 1位元組的 RGB色彩空間,而是使用不符合 1位元組的 HSB/HSV色彩空間。
因此,需要轉換 HSB/HSL色彩空間的數值以符合 1位元組,而在設定 自訂LED模式 時,卻使用 RGB色彩空間。
不知為何設計者會使用兩種不同色彩空間來設定 LED 。



















沒有留言 :
張貼留言