使用 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 是否同時擁有 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
代號偏移功能
十進制十六進制
bu130x0D按鈕功能長度
nn160x10當前選取設定檔編號

其他資料暫時未知道用途

選取
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
代號偏移功能
十進制十六進制
nn20x02選取設定檔的編號

選取設定檔時, 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
代號偏移功能
十進制十六進制
lo30x03讀取位置 低位元組
hi40x04讀取位置 高位元組

讀取位置數值必須是 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
代號偏移功能
十進制十六進制
ty8 + i * 40x08 + i * 4第i個 操作類型
p19 + i * 40x09 + i * 4第i個 參數1
p210 + i * 40x0A + i * 4第i個 參數2
p311 + i * 40x0B + 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位元組資料 省略,
並將所有 按鈕及旋鈕資料 連接,最後 將不必要的 按鈕及旋鈕資料 刪除,
便只剩餘需要使用的 按鈕及旋鈕資料。

操功類型
數值功能
十進制十六進制
160x10滑鼠按鈕
170x11滑鼠移動
320x20鍵盤按鈕
480x30媒體按鈕
640x40電源操作
960x60巨集操作
滑鼠按鈕參數
編號位元組順序功能
十進制十六進制
10x0000010x01, 0x00, 0x00左鍵
20x0000020x02, 0x00, 0x00右鍵
40x0000040x04, 0x00, 0x00中鍵
80x0000080x08, 0x00, 0x00後退
160x0000100x10, 0x00, 0x00前進
655360x0100000x00, 0x00, 0x01滾輪向上
167116800xFF00000x00, 0x00, 0xFF滾輪向下
滑鼠移動方向
編號功能
& 0x80 < 0向左移動
& 0x80 == 0向右移動
& 0x08 < 0向上移動
& 0x08 == 0向下移動
鍵盤按鈕參數
編號功能備註
十進制十六進制
00x00無效
10x01鍵盤 Error Roll Over通常無法使用
20x02鍵盤 POST Fail通常無法使用
30x03鍵盤 Error Undefined通常無法使用
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
400x28鍵盤 Enter
410x29鍵盤 Escape
420x2A鍵盤 Backspace
430x2B鍵盤 Tab
440x2C鍵盤 Space
450x2D鍵盤 -
460x2E鍵盤 =
470x2F鍵盤 [
480x30鍵盤 ]
490x31鍵盤 \
500x32鍵盤 \ (非US)
510x33鍵盤 ;
520x34鍵盤 '
530x35鍵盤 `
540x36鍵盤 ,
550x37鍵盤 .
560x38鍵盤 /
570x39鍵盤 Caps Lock
580x3A鍵盤 F1
590x3B鍵盤 F2
600x3C鍵盤 F3
610x3D鍵盤 F4
620x3E鍵盤 F5
630x3F鍵盤 F6
640x40鍵盤 F7
650x41鍵盤 F8
660x42鍵盤 F9
670x43鍵盤 F10
680x44鍵盤 F11
690x45鍵盤 F12
700x46鍵盤 Print Screen
710x47鍵盤 Scroll Lock
720x48鍵盤 Pause
730x49鍵盤 Insert
740x4A鍵盤 Home
750x4B鍵盤 Page Up
760x4C鍵盤 Delete
770x4D鍵盤 End
780x4E鍵盤 Page Down
790x4F鍵盤 Right Arrow
800x50鍵盤 Left Arrow
810x51鍵盤 Down Arrow
820x52鍵盤 Up Arrow
830x53鍵盤 Num Lock
840x54數字鍵盤 /
850x55數字鍵盤 *
860x56數字鍵盤 -
870x57數字鍵盤 +
880x58數字鍵盤 Enter
890x59數字鍵盤 1
900x5A數字鍵盤 2
910x5B數字鍵盤 3
920x5C數字鍵盤 4
930x5D數字鍵盤 5
940x5E數字鍵盤 6
950x5F數字鍵盤 7
960x60數字鍵盤 8
970x61數字鍵盤 9
980x62數字鍵盤 0
990x63數字鍵盤 .
1000x64數字鍵盤 <
1010x65鍵盤 Application
1020x66系統 關機
1030x67數字鍵盤 =
1040x68F13不同系統功能不同
1050x69F14不同系統功能不同
1060x6AF15不同系統功能不同
1070x6BF16不同系統功能不同
1080x6CF17不同系統功能不同
1090x6DF18不同系統功能不同
1100x6EF19不同系統功能不同
1110x6FF20不同系統功能不同
1120x70F21不同系統功能不同
1130x71F22不同系統功能不同
1140x72F23不同系統功能不同
1150x73F24不同系統功能不同
1160x74Open通常無法使用
1170x75Help通常無法使用
1180x76Props通常無法使用
1190x77Select通常無法使用
1200x78Stop通常無法使用
1210x79Redo通常無法使用
1220x7AUndo通常無法使用
1230x7BCut通常無法使用
1240x7CCopy通常無法使用
1250x7DPaste通常無法使用
1260x7EFind通常無法使用
1270x7F系統 靜音
1280x80系統 音量增加
1290x81系統 音量減少
1300x82系統 鎖定 Caps Lock通常無法使用
1310x83系統 鎖定 Num Lock通常無法使用
1320x84系統 鎖定 Scroll Lock通常無法使用
1330x85數字鍵盤 ,通常無法使用
1340x86數字鍵盤 =通常無法使用
1350x87International 1不同系統功能不同
1360x88International 2不同系統功能不同
1370x89International 3不同系統功能不同
1380x8AInternational 4不同系統功能不同
1390x8BInternational 5不同系統功能不同
1400x8CInternational 6不同系統功能不同
1410x8DInternational 7不同系統功能不同
1420x8EInternational 8不同系統功能不同
1430x8FInternational 9不同系統功能不同
1440x90Lang 1不同系統功能不同
1450x91Lang 2不同系統功能不同
1460x92Lang 3不同系統功能不同
1470x93Lang 4不同系統功能不同
1480x94Lang 5不同系統功能不同
1490x95Lang 6不同系統功能不同
1500x96Lang 7不同系統功能不同
1510x97Lang 8不同系統功能不同
1520x98Lang 9不同系統功能不同
1530x99Alternative Erase通常與 Delete 相同
1540x9ASysReq系統保留功能
1550x9BCancel通常無法使用
1560x9CClear通常無法使用
1570x9DPrior通常無法使用
1580x9EReturn通常無法使用
1590x9FSeparator通常無法使用
1600xA0Out通常無法使用
1610xA1Oper通常無法使用
1620xA2Clear/Again通常無法使用
1630xA3CrSel/Props通常無法使用
1640xA4ExSel通常無法使用
1650xA5保留
1660xA6保留
1670xA7保留
1680xA8保留
1690xA9保留
1700xAA保留
1710xAB保留
1720xAC保留
1730xAD保留
1740xAE保留
1750xAF保留
1760xB0數字鍵盤 00通常無法使用
1770xB1數字鍵盤 000通常無法使用
1780xB2數字鍵盤 Thousands Separator通常無法使用
1790xB3數字鍵盤 Decimal Separator通常無法使用
1800xB4數字鍵盤 Currency Unit通常無法使用
1810xB5數字鍵盤 Currency Sub-unit通常無法使用
1820xB6數字鍵盤 (通常無法使用
1830xB7數字鍵盤 )通常無法使用
1840xB8數字鍵盤 {通常無法使用
1850xB9數字鍵盤 }通常無法使用
1860xBA數字鍵盤 Tab通常無法使用
1870xBB數字鍵盤 Backspace通常無法使用
1880xBC數字鍵盤 A通常無法使用
1890xBD數字鍵盤 B通常無法使用
1900xBE數字鍵盤 C通常無法使用
1910xBF數字鍵盤 D通常無法使用
1920xC0數字鍵盤 E通常無法使用
1930xC1數字鍵盤 F通常無法使用
1940xC2數字鍵盤 ~通常無法使用
1950xC3數字鍵盤 ^通常無法使用
1960xC4數字鍵盤 %通常無法使用
1970xC5數字鍵盤 <通常無法使用
1980xC6數字鍵盤 >通常無法使用
1990xC7數字鍵盤 &通常無法使用
2000xC8數字鍵盤 &&通常無法使用
2010xC9數字鍵盤 |通常無法使用
2020xCA數字鍵盤 ||通常無法使用
2030xCB數字鍵盤 :通常無法使用
2040xCC數字鍵盤 #通常無法使用
2050xCD數字鍵盤 Space通常無法使用
2060xCE數字鍵盤 @通常無法使用
2070xCF數字鍵盤 !通常無法使用
2080xD0數字鍵盤 Memory Store通常無法使用
2090xD1數字鍵盤 Memory Recall通常無法使用
2100xD2數字鍵盤 Memory Clear通常無法使用
2110xD3數字鍵盤 Memory Add通常無法使用
2120xD4數字鍵盤 Memory Subtract通常無法使用
2130xD5數字鍵盤 Memory Multiply通常無法使用
2140xD6數字鍵盤 Memory Divide通常無法使用
2150xD7數字鍵盤 +/-通常無法使用
2160xD8數字鍵盤 Clear通常無法使用
2170xD9數字鍵盤 Clear Entry通常無法使用
2180xDA數字鍵盤 Binary通常無法使用
2190xDB數字鍵盤 Octal通常無法使用
2200xDC數字鍵盤 Decimal通常無法使用
2210xDD數字鍵盤 Hexadecimal通常無法使用
2220xDE保留
2230xDF保留
2240xE0鍵盤 左Ctrl
2250xE1鍵盤 左Shift
2260xE2鍵盤 左Alt
2270xE3鍵盤 左Meta
2280xE4鍵盤 右Ctrl
2290xE5鍵盤 右Shift
2300xE6鍵盤 右Alt
2310xE7鍵盤 右Meta
2320xE8保留
2330xE9保留
2340xEA保留
2350xEB保留
2360xEC保留
2370xED保留
2380xEE保留
2390xEF保留
2400xF0保留
2410xF1保留
2420xF2保留
2430xF3保留
2440xF4保留
2450xF5保留
2460xF6保留
2470xF7保留
2480xF8保留
2490xF9保留
2500xFA保留
2510xFB保留
2520xFC保留
2530xFD保留
2540xFE保留
2550xFF保留
媒體按鈕參數
編號位元組順序功能
十進制十六進制
1760x00B00xB0, 0x00媒體播放
1770x00B10xB1, 0x00媒體暫停
1780x00B20xB2, 0x00媒體攝錄
1790x00B30xB3, 0x00媒體快播
1800x00B40xB4, 0x00媒體回播
1810x00B50xB5, 0x00下一個媒體
1820x00B60xB6, 0x00上一個媒體
1830x00B70xB7, 0x00媒體停止
1840x00B80xB8, 0x00媒體退出
2040x00CC0xCC, 0x00媒體停止及退出
2050x00CD0xCD, 0x00媒體播放或暫停
2260x00E20xE2, 0x00靜音切換
2330x00E90xE9, 0x00音量提升
2340x00EA0xEA, 0x00音量降低
3870x01830x83, 0x01開啟控制中心
3940x018A0x8A, 0x01開啟郵件
4020x01920x92, 0x01開啟計算機
4040x01940x94, 0x01開啟檔案瀏覽器
4060x01960x96, 0x01開啟網頁瀏覽器
5450x02210x21, 0x02搜尋
5470x02230x23, 0x02網頁瀏覽器首頁
5480x02240x24, 0x02網頁瀏覽器後退
5490x02250x25, 0x02網頁瀏覽器前進
5500x02260x26, 0x02網頁瀏覽器停止
5510x02270x27, 0x02網頁瀏覽器重新載入
5540x022A0x2A, 0x02網頁瀏覽器書籤
電源操作參數
編號功能
十進制十六進制
10x01關機
20x02睡眠
40x04喚醒
巨集類型參數
數值功能
十進制十六進制
00x00循環次數
10x01循環直至觸發鍵開放
20x02循環直至其他鍵按下
30x03循環直至觸發鍵再按下
操作類型與參數配對
操作類型參數1參數2參數3
滑鼠按鈕滑鼠按鈕參數[2]滑鼠按鈕參數[1]滑鼠按鈕參數[0]
滑鼠移動移動方向水平偏移量垂直偏移量
鍵盤按鈕鍵盤修飾鍵鍵盤按鈕參數0x00
媒體按鈕媒體按鈕參數[1]媒體按鈕參數[0]0x00
電源操作電源操作參數0x000x00
巨集操作巨集編號巨集重覆次數巨集類型

設定
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
代號偏移功能
十進制十六進制
nn30x03按扭及旋鈕編號
ty80x08操作類型
p190x09參數1
p2100x0A參數2
p3110x0B參數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
代號偏移功能
十進制十六進制
mo70x07模式編號
br80x08光度數值
sp90x09速度數值
cu110x0B是否自訂
hh130x0DHSV/HSB色彩空間 的 色相(Hue) 數值
ss140x0EHSV/HSB色彩空間 的 飽和度(Saturation) 數值
vv150x0FHSV/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
代號偏移功能
十進制十六進制
mo70x07模式編號
br80x08光度數值
sp90x09速度數值
cu110x0B是否自訂
hh130x0DHSV/HSB色彩空間 的 色相(Hue) 數值
ss140x0EHSV/HSB色彩空間 的 飽和度(Saturation) 數值
vv150x0FHSV/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
代號偏移功能
十進制十六進制
lo30x03讀取位置 低位元組
hi40x04讀取位置 高位元組

讀取位置數值必須是 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
代號偏移功能
十進制十六進制
rr8 + i * 30x08 + i * 3第i個 RGB色彩空 的 紅值(Red)
bb9 + i * 30x09 + i * 3第i個 RGB色彩空 的 綠值(Green)
bb10 + i * 30x0A + 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
代號偏移功能
十進制十六進制
nn20x02選取LED的編號
rr80x08RGB色彩空 的 紅值(Red)
bb90x09RGB色彩空 的 綠值(Green)
bb100x0ARGB色彩空 的 藍值(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
代號偏移功能
十進制十六進制
lo30x03讀取位置 低位元組
hi40x04讀取位置 高位元組

讀取位置數值必須是 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
代號偏移功能
十進制十六進制
pl0 + i * 20x00 + i * 2第i參考位置 低位元組
ph1 + i * 20x01 + i * 2第i參考位置 高位元組
dl64 + i * 40x40 + i * 4第i個巨集動作 延遲時間(毫秒) 低位元組
dh65 + i * 40x41 + i * 4第i個巨集動作 延遲時間(毫秒) 高位元組
mm66 + i * 40x42 + i * 4第i個巨集動作 多種設定資料
kk67 + i * 40x43 + i * 4第i個巨集動作 按鍵編號

將不必要的資料省略後,巨集回應資料便會很清晰。

巨集回應資料 有2部分:

  • [0:63] 為 巨集動作的參考位置,共 64位元組 , 每個 參考位置 為 2位元組。
  • [64:4160] 為 巨集動作,共 4096位元組 , 每個 巨集動作 為 4位元組。

參考位置 的資料為指向 巨集動作的位置,然後執行連串巨集動作,直至訊號終止。

繼續或停止
偏移功能
十進制十六進制
00x00執行當前巨集操作後移向下個巨集操作
1280x80執行當前巨集操作後停止
按下或釋放
偏移功能
十進制十六進制
00x00釋放按鍵操作
640x40按下按鍵操作
按鍵類型
偏移功能
十進制十六進制
20x02鍵盤按鈕
30x03滑鼠按鈕
40x04滑鼠滾輪
按鍵操作

按鍵操作對應 滑鼠按鈕參數 、 滑鼠移動方向 、 鍵盤按鈕參數 ,因此不重覆說明。

例子

由於在下都覺得解讀後的內容非常複雜,因此在下覺得要提供例子說明。

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
代號偏移功能
十進制十六進制
pl0 + i * 20x00 + i * 2第i參考位置 低位元組
ph1 + i * 20x01 + i * 2第i參考位置 高位元組
dl64 + i * 40x40 + i * 4第i個巨集動作 延遲時間(毫秒) 低位元組
dh65 + i * 40x41 + i * 4第i個巨集動作 延遲時間(毫秒) 高位元組
mm66 + i * 40x42 + i * 4第i個巨集動作 多種設定資料
kk67 + i * 40x43 + 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 。

參考資料