2026-02-08

使用 WebHID API 與 HID 通訊

在下曾因為一些 巨集鍵盤或滑鼠 的 更新程式 沒有 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 是否同時擁有 inputReportsoutputReports 來判斷界面是否適合

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 ,必須使用 rootsudo 才能存取 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-VIDHID-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

解讀資料:

  1. 第0參數位置 使用 2位元組 ,因此資料為 40 00。
    1. 即是 0x0040 = 64。
    2. 表示 第0 巨集動作 在 第64位元組開始。
  2. 第64位元組 為 第0參數位置 的 巨集動作 , 使用 4位元組 ,因此資料為 01 00 42 04。
    1. 延遲 使用 2位元組 , 因此資料為 01 00。
      1. 即是 0x0001 = 1 ,表示 延遲 1毫秒。
    2. 類型 資料為 0x42。
      1. 第7位元 是 0 , 即是需要繼續執行下一組 巨集動作。
      2. 第6位元 是 1 , 即是 按下按鍵操作。
      3. 第[3:0]位元 是 2 , 即是 鍵盤按鈕參數。
    3. 按鍵操作 資料為 0x04。
      1. 根據 鍵盤按鈕參數 是 鍵盤a。
  3. 執行下一組 巨集動作,資料為 00 00 82 04。
    1. 延遲 使用 2位元組 , 因此資料為 00 00。
      1. 即是 0x0000 = 0 ,表示 延遲 0毫秒。
    2. 類型 資料為 0x82。
      1. 第7位元 是 1 , 即是完成後 停止 巨集動作。
      2. 第6位元 是 0 , 即是 釋放按鍵操作。
      3. 第[3:0]位元 是 2 , 即是 鍵盤按鈕參數。
    3. 按鍵操作 資料為 0x04。
      1. 根據 鍵盤按鈕參數 是 鍵盤a。

解讀意思:
第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個巨集動作 按鍵編號

省略後的設定資料 與 省略後的回應資料 完全相同;然而,要設定資料並不簡單。

其他功能設定資料,都是選擇編號及設定參數來更新資料,但巨集卻是整個設定一同更新,否則會遺失其他巨集設定。

  1. 讀取所有巨集回應資料。
  2. 結構化回應資料。
  3. 更新指定巨集編號的巨集動作。
  4. 重新計算巨集編號的巨集動作開始位置。
  5. 連接所有巨集編號的開始位置,如果 巨集編號 沒有保存 巨集動作 ,使用 FF FF 取代。
  6. 連接所有巨集動作,使用 00 將資料擴充至 4096位元組。
  7. 將巨集編號與巨集動作合併。
  8. 將合併後的資料 每59位元組 分割,並在分組資料前加上 06 0D 3B lo hi 後,如果資料不足 64位元組,使用 00 將資料擴充至 64位元組。
  9. 發送每組資料。

設定資料 比 回應資料 更複雜。

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 。

參考資料

沒有留言 :

張貼留言