2026-10-31

MSI SteelSeries RGB鍵盤 逆向工程

在下過去雖然逆向過不少周邊裝置,但一直沒有研究過自己正在使用的電腦 RGB LED 操作原理。
這次決定一併把這個問題解決。

背景情況

在下使用的 MSI GS63 7RE Stealth Pro 電競電腦,由於 MSI SteelSeries RGB鍵盤 的控制軟體僅支援 Windows ,
這次依然需要透過 Wireshark 擷取封包並逆向訊號。不過,這次的擷取過程與過往有些許不同。

過去在下習慣在 Linux 環境下透過 Windows虛擬機(Virtual Machine) 安裝官方軟體,並藉由 USB Passthrough 配合 usbmon 與 Wireshark 擷取封包。

然而這次發現,即使在虛擬機中安裝了官方軟體,依然無法成功與 RGB LED 裝置配對。
因此,在下決定直接改用原生 Windows 環境來擷取資料。
(關於封包擷取的詳細方法,可參考在下過去的專案紀錄。)

由於本次目標是希望能在 Linux 上控制 MSI SteelSeries RGB鍵盤 ,
因此在下僅針對與 MSI SteelSeries RGB鍵盤 操作相關封包,並著手分析其封包結構。

訊號結構

MSI SteelSeries RGB 鍵盤每次傳送的封包長度固定為 8位元組 。

資料
目錄
位元組偏移
0 1 2 3 4 5 6 7
1 01 02 40 01 FF 00 00 00
2 01 02 40 02 FF 00 00 00
3 01 02 40 03 FF 00 00 00
4 01 02 40 03 00 FF 00 00
5 01 02 40 03 00 00 FF 00
6 01 02 50 05 00 00 00 00
7 01 02 50 06 00 00 00 00
8 01 02 50 07 00 00 00 00
9 01 02 50 0A 00 00 00 00
10 01 02 41 01 00 00 00 00
11 01 02 41 02 00 00 00 00
12 01 02 41 03 00 00 00 00
13 01 02 41 04 00 00 00 00
14 01 02 41 05 00 00 00 00
15 01 02 44 01 FF 00 00 00
16 01 02 44 02 00 00 00 00
17 01 02 44 03 01 00 00 00
18 01 02 44 04 FF 00 00 00
19 01 02 44 05 00 00 00 00
20 01 02 44 06 01 00 00 00
21 01 02 44 07 FF 00 00 00
22 01 02 44 08 00 00 00 00
23 01 02 44 09 01 00 00 00

在分析了 23筆封包後,結構整理如下:

偏移 功能
[0:1] 標頭資料:內容固定為 0x01, 0x02
[2] 指令資料:代表具體執行的操作指令
[3:7] 參數資料:附帶的相關參數或狀態設定

指令與參數

由於 標頭資料 固定為 0x01, 0x02, 無需額外解析,而 指令資料 皆為固定數值,
因此分析重點便集中在 參數資料 的格式變化上。

比對多組封包後,在下整理出以下主要的通訊指令與參數結構:

標頭資料 指令資料 參數資料 功能
[0:1] [2] [3] [4] [5] [6] [7]
"\x01\x02" 0x40 Index Red Green Blue 0x00 設定區域前景色。
Index (亮度等級):
  • 0x01: 左區
  • 0x02: 中區
  • 0x03: 右區
Red, Green, Blue (三原色): RGB顏色數值。
"\x01\x02" 0x50 Index 0x00 0x00 0x00 0x00 設定全域亮度。
Index (亮度等級):
  • 0x05: 0%
  • 0x06: 33%
  • 0x07: 66%
  • 0x0A: 100%
"\x01\x02" 0x41 Index 0x00 0x00 0x00 0x00 設定燈光模式。
Index (燈效模式):
  • 0x01: 固定
  • 0x02: 遊戲
  • 0x03: 呼吸
  • 0x04: 音樂
  • 0x05: 波浪
"\x01\x02" 0x44 Index Red Green Blue 0x00 設定區域背景色。
Index (區域範圍):
  • 0x01: 左區
  • 0x04: 中區
  • 0x07: 右區
Red, Green, Blue (三原色): RGB顏色數值。
"\x01\x02" 0x44 Index Dur 0x00 0x00 0x00 設定背景色到前景色漸變過渡時間。
Index (區域範圍):
  • 0x02: 左區
  • 0x05: 中區
  • 0x08: 右區
Dur (持續時間): 過渡秒數 (0 為自動最佳化)。
"\x01\x02" 0x44 Index Dur 0x00 0x00 0x00 設定前景色到背景色漸變過渡時間。
Index (區域範圍):
  • 0x03: 左區
  • 0x06: 中區
  • 0x09: 右區
Dur (持續時間): 過渡秒數 (0 為自動最佳化)。

與在下過去逆向過的巨集滑鼠、客製化巨集鍵盤等周邊相比,
MSI SteelSeries RGB鍵盤 的封包結構相對簡單,長度短、變化量也不大,因此逆向與分析過程相對輕鬆。

尋找裝置

列出系統中的所有 USB裝置 ,在 終端機 輸入:

lsusb

在輸出列表中確認 MSI SteelSeries RGB鍵盤 的 VID 與 PID 後,即可開始進行傳輸測試。

值得注意的是,根據先前逆向分析的結果,
這次擷取到的封包屬於 USB 的 控制傳輸 (Control Transfer) 的 Setup Data,
而非 HID 輸出報告 (Output Report) 的 Leftover Capture Data。
因此,無法直接將資料寫入到 /dev/hidraw* 來完成控制測試。

使用 Python 測試

import usb.core, usb.util
class MSISteelSeriesRGBKeyboard:
	def __init__(self):
		self.__BRIGHTNESS_MAP = {
			0x00: 0x05,
			0x01: 0x06,
			0x02: 0x07,
			0x03: 0x0A,
		}
		self.__CHUNK_SIZE = 8
		self.__DEVICE = None
	def open(self, vid, pid):
		self.__DEVICE = usb.core.find(idVendor = vid, idProduct = pid)
		if self.__DEVICE == None:
			raise ValueError("USB Device VID = {}, PID = {} not found".format(vid, pid))
		try:
			if self.__DEVICE.is_kernel_driver_active(0):
				self.__DEVICE.detach_kernel_driver(0)
		except NotImplementedError as error:
			raise error
	def close(self):
		try:
			if not self.__DEVICE.is_kernel_driver_active(0):
				usb.util.dispose_resources(self.__DEVICE)
				self.__DEVICE.attach_kernel_driver(0)
		except NotImplementedError as error:
			raise error
		self.__DEVICE = None
	def send(self, data_array):
		data_array += b"\x00" * (self.__CHUNK_SIZE - len(data_array))
		return self.__DEVICE.ctrl_transfer(
			bmRequestType = 0x21,
			bRequest = 0x09,
			wValue = 0x0301,
			wIndex = 0x0000,
			data_or_wLength = data_array
		)
	def sendCommand(self, command, parameters):
		self.send(b"\x01\x02" + command + parameters)
	def setMode(self, index):
		self.sendCommand(b"\x41", bytes([index]))
	def setColor(self, index, red, green, blue):
		self.sendCommand(b"\x40", bytes([index, red, green, blue]))
	def setBrightness(self, index):
		self.sendCommand(b"\x50", bytes([self.__BRIGHTNESS_MAP[index]]))
	def setBackgroundColor(self, index, red, green, blue):
		index = (index - 1) * 3 + 1;
		self.sendCommand(b"\x44", bytes([index, red, green, blue]))
	def setBackgroundDuration(self, index, seconds):
		index = (index - 1) * 3 + 2;
		self.sendCommand(b"\x44", bytes([index, seconds]))
	def setForegroundDuration(self, index, seconds):
		index = (index - 1) * 3 + 3;
		self.sendCommand(b"\x44", bytes([index, seconds]))
VENDOR_ID = 0x1770
PRODUCT_ID = 0xFF00
rgb = MSISteelSeriesRGBKeyboard()
rgb.open(VENDOR_ID, PRODUCT_ID)
# do something
rgb.close()

在下直接使用 Python 來測試。

在下將操作封裝成 類別 (Class) ,讓控制流程更加直覺。
由於測試不需要複雜的互動介面,因此程式碼邏輯相當簡單。

使用 WebHID API 測試

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
		<title>MSI SteelSeries RGB Keyboard</title>
		<style>
html, body {
	margin: 0px;
	padding: 0px;
}
		</style>
		<script>
class MSISteelSeriesRGBKeyboard {
	constructor() {
		this.brightnessMap = {
			0x00: 0x05,
			0x01: 0x06,
			0x02: 0x07,
			0x03: 0x0A,
		};
		this.chunkSize = 8;
		this.device = null;
	}
	async open(vendorId = 0x1770, productId = 0xFF00) {
		try {
			const devices = await navigator.hid.requestDevice({
				"filters": []
			});
			this.device = devices[0];
			await this.device.open();
		} catch (error) {
			throw error;
		}
	}
	async close() {
		try {
			await this.device.close();
			this.device = null;
		} catch (error) {
			throw error;
		}
	}
	async send(dataArray) {
		try {
			while (dataArray.length < this.chunkSize) {
				dataArray.push(0x00);
			}
			const reportId = dataArray.shift();
			await this.device.sendFeatureReport(reportId, new Uint8Array(dataArray));
		} catch (error) {
			throw error;
		}
	}
	async sendCommand(command, parameters) {
		const dataArray = [0x01, 0x02, command];
		for (const i in parameters) {
			dataArray.push(parameters[i]);
		}
		await this.send(dataArray);
	}
	async setMode(index) {
		await this.sendCommand([0x41], [index]);
	}
	async setColor(index, red, green, blue) {
		await this.sendCommand([0x40], [index, red, green, blue]);
	}
	async setBrightness(index) {
		const val = this.brightnessMap[index];
		if (val !== undefined) {
			await this.sendCommand([0x50], [val]);
		}
	}
}
window.addEventListener("load", function(loadEvent) {
	const rgb = new MSISteelSeriesRGBKeyboard();
	const spanBrightness = document.getElementById("spanBrightness");
	const spanRed = document.getElementById("spanRed");
	const spanGreen = document.getElementById("spanGreen");
	const spanBlue = document.getElementById("spanBlue");
	const spanColors = [
		spanRed,
		spanGreen,
		spanBlue,
	];
	const rangeColors = [
		document.rgbForm.rangeRed,
		document.rgbForm.rangeGreen,
		document.rgbForm.rangeBlue,
	];
	document.rgbForm.buttonOpen.addEventListener("click", async function(clickEvent) {
		try {
			await rgb.open(0x1770, 0xFF00);
		} catch (error) {
			console.error(error);
		}
	});
	document.rgbForm.buttonClose.addEventListener("click", async function(clickEvent) {
		try {
			await rgb.close();
		} catch (error) {
			console.error(error);
		}
	});
	for (const i in document.rgbForm.selectMode) {
		if (document.rgbForm.selectMode[i] instanceof Element) {
			document.rgbForm.selectMode[i].addEventListener("change", async function(clickEvent) {
				try {
					await rgb.setMode(this.value);
				} catch (error) {
					console.error(error);
				}
			});
		}
	}
	document.rgbForm.rangeBrightness.addEventListener("input", async function(clickEvent) {
		try {
			spanBrightness.textContent = `(${this.value})`;
			await rgb.setBrightness(this.value);
		} catch (error) {
			console.error(error);
		}
	});
	for (const i in rangeColors) {
		rangeColors[i].addEventListener("input", async function(clickEvent) {
			try {
				spanColors[i].textContent = `(${rangeColors[i].value})`;
				await rgb.setColor(
					document.rgbForm.selectIndex.value,
					document.rgbForm.rangeRed.value,
					document.rgbForm.rangeGreen.value,
					document.rgbForm.rangeBlue.value
				);
			} catch (error) {
				console.error(error);
			}
		});
	}
});
		</script>
	</head>
	<body>
		<form name="rgbForm" action="" method="get" onsubmit="return false;">
			<table style="border-collapse: collapse;" border="1" cellpadding="4" cellspacing="4">
				<tr>
					<td colspan="2">
						<button name="buttonOpen">Open</button>
						<button name="buttonClose">Close</button>
					</td>
				</tr>
				<tr>
					<td>模式</td>
					<td>
						<label><input name="selectMode" type="radio" value="1" checked="checked">一般</label>
						<label><input name="selectMode" type="radio" value="2">遊戲</label>
						<label><input name="selectMode" type="radio" value="3">呼吸</label>
						<label><input name="selectMode" type="radio" value="4">音樂</label>
						<label><input name="selectMode" type="radio" value="5">波浪</label>
					</td>
				</tr>
				<tr>
					<td>亮度</td>
					<td><label><input style="width: 255px;" name="rangeBrightness" type="range" min="0" max="3" step="1" value="0"/><span id="spanBrightness">(0)</span></label></td>
				</tr>
				<tr>
					<td>顏色</td>
					<td>
						<div>
							<label><input name="selectIndex" type="radio" value="1" checked="checked">左側區域</label>
							<label><input name="selectIndex" type="radio" value="2">中間區域</label>
							<label><input name="selectIndex" type="radio" value="3">右側區域</label>
						</div>
						<div><label>紅色<input style="width: 255px;" name="rangeRed" type="range" min="0" max="255" step="1" value="0"/><span id="spanRed">(0)</span></label></div>
						<div><label>綠色<input style="width: 255px;" name="rangeGreen" type="range" min="0" max="255" step="1" value="0"/><span id="spanGreen">(0)</span></label></div>
						<div><label>藍色<input style="width: 255px;" name="rangeBlue" type="range" min="0" max="255" step="1" value="0"/><span id="spanBlue">(0)</span></label></div>
					</td>
				</tr>
			</table>
		</form>
	</body>
</html>

測試完成後,在下打算改用 HTML 的 WebHID API 製作一個簡單的網頁版互動控制介面。

使用 WebHID API 時需要注意,基於 網頁瀏覽器 安全機制, WebHID API 無法自動載入或背景執行,
必須由 使用者主動觸發 才能顯出裝置選擇對話框,並掃描及連線至指定的 USB裝置。

實作後的控制效果與官方軟體相近,唯獨少了官方軟件的視覺動畫。
另外在下發現 音樂模式 會影響其他燈效設定,
估計官方軟件可能需要配合背景監控影音的服務或特定的 Windows API 才能正常運作。
由於不想花太多時間逆向這個功能,在下未將音樂模式納入這次的網頁介面中。

由於 WebHID API 目前主要僅支援 基於Chromium核心的網頁瀏覽器,
還有基於系統安全考量,極不建議使用 root 或 sudo 權限來啟動瀏覽器。
為了能以一般使用者權限存取 USB裝置,需要設定 udev規則。

在 /etc/udev/rules.d/ 目錄下建立 udev規則檔案,例如: 99-msi-steelseries-rgb-keyboard.rules :

SUBSYSTEMS=="usb|hidraw", ATTRS{idVendor}=="1770", ATTRS{idProduct}=="ff00", TAG+="uaccess"

udev 是依照檔名開頭的數字順序來依次載入規則的。
選用 99- 作為前綴,可以確保規則在系統預設規則全都載入完畢後才執行(數值越大、越後執行),避免被預設設定覆蓋。

檔案儲存後,套用新規則,在 終端機 載入:

sudo udevadm control --reload-rules
sudo udevadm trigger

完成後,無需提權,便能直接以一般使用者身分透過 WebHID API 或 Python 操作 MSI SteelSeries RGB鍵盤 。

補充資料

資源監視器

完成基礎控制後,在下打算進一步將這套 MSI SteelSeries RGB 鍵盤活用為系統狀態的硬體視覺儀表板,
分別透過鍵盤的三個區域來實時顯示 CPU使用率、記憶體使用率、儲存空間使用率。

為了讓顏色的變化更直觀且平滑,在下定義 5個顏色基準:

區域 數值 顏色 RGB色碼
1 0% 藍 (0, 0, 255)
2 25% 青 (0, 255, 255)
3 50% 綠 (0, 255, 0)
4 75% 黃 (255, 255, 0)
5 100% 紅 (255, 0, 0)

因為系統資源用量介於 0% 到 100% ,總共 101個等級 ,在下將上述 5個基準 分為 4段區間,
以 線性插值 (Linear Interpolation) 計算 每個等級 對應的 RGB色碼,建立 RGB色碼查表 (Lookup Table) 。

import usb.core, usb.util, time, psutil
mapping_rgb = [
	[0x00, 0x00, 0xFF],
	[0x00, 0x0A, 0xFF],
	[0x00, 0x14, 0xFF],
	[0x00, 0x1F, 0xFF],
	[0x00, 0x29, 0xFF],
	[0x00, 0x33, 0xFF],
	[0x00, 0x3D, 0xFF],
	[0x00, 0x47, 0xFF],
	[0x00, 0x52, 0xFF],
	[0x00, 0x5C, 0xFF],
	[0x00, 0x66, 0xFF],
	[0x00, 0x70, 0xFF],
	[0x00, 0x7A, 0xFF],
	[0x00, 0x85, 0xFF],
	[0x00, 0x8F, 0xFF],
	[0x00, 0x99, 0xFF],
	[0x00, 0xA3, 0xFF],
	[0x00, 0xAD, 0xFF],
	[0x00, 0xB8, 0xFF],
	[0x00, 0xC2, 0xFF],
	[0x00, 0xCC, 0xFF],
	[0x00, 0xD6, 0xFF],
	[0x00, 0xE0, 0xFF],
	[0x00, 0xEB, 0xFF],
	[0x00, 0xF5, 0xFF],
	[0x00, 0xFF, 0xFF],
	[0x00, 0xFF, 0xF5],
	[0x00, 0xFF, 0xEB],
	[0x00, 0xFF, 0xE0],
	[0x00, 0xFF, 0xD6],
	[0x00, 0xFF, 0xCC],
	[0x00, 0xFF, 0xC2],
	[0x00, 0xFF, 0xB8],
	[0x00, 0xFF, 0xAD],
	[0x00, 0xFF, 0xA3],
	[0x00, 0xFF, 0x99],
	[0x00, 0xFF, 0x8F],
	[0x00, 0xFF, 0x85],
	[0x00, 0xFF, 0x7A],
	[0x00, 0xFF, 0x70],
	[0x00, 0xFF, 0x66],
	[0x00, 0xFF, 0x5C],
	[0x00, 0xFF, 0x52],
	[0x00, 0xFF, 0x47],
	[0x00, 0xFF, 0x3D],
	[0x00, 0xFF, 0x33],
	[0x00, 0xFF, 0x29],
	[0x00, 0xFF, 0x1F],
	[0x00, 0xFF, 0x14],
	[0x00, 0xFF, 0x0A],
	[0x00, 0xFF, 0x00],
	[0x0A, 0xFF, 0x00],
	[0x14, 0xFF, 0x00],
	[0x1F, 0xFF, 0x00],
	[0x29, 0xFF, 0x00],
	[0x33, 0xFF, 0x00],
	[0x3D, 0xFF, 0x00],
	[0x47, 0xFF, 0x00],
	[0x52, 0xFF, 0x00],
	[0x5C, 0xFF, 0x00],
	[0x66, 0xFF, 0x00],
	[0x70, 0xFF, 0x00],
	[0x7A, 0xFF, 0x00],
	[0x85, 0xFF, 0x00],
	[0x8F, 0xFF, 0x00],
	[0x99, 0xFF, 0x00],
	[0xA3, 0xFF, 0x00],
	[0xAD, 0xFF, 0x00],
	[0xB8, 0xFF, 0x00],
	[0xC2, 0xFF, 0x00],
	[0xCC, 0xFF, 0x00],
	[0xD6, 0xFF, 0x00],
	[0xE0, 0xFF, 0x00],
	[0xEB, 0xFF, 0x00],
	[0xF5, 0xFF, 0x00],
	[0xFF, 0xFF, 0x00],
	[0xFF, 0xF5, 0x00],
	[0xFF, 0xEB, 0x00],
	[0xFF, 0xE0, 0x00],
	[0xFF, 0xD6, 0x00],
	[0xFF, 0xCC, 0x00],
	[0xFF, 0xC2, 0x00],
	[0xFF, 0xB8, 0x00],
	[0xFF, 0xAD, 0x00],
	[0xFF, 0xA3, 0x00],
	[0xFF, 0x99, 0x00],
	[0xFF, 0x8F, 0x00],
	[0xFF, 0x85, 0x00],
	[0xFF, 0x7A, 0x00],
	[0xFF, 0x70, 0x00],
	[0xFF, 0x66, 0x00],
	[0xFF, 0x5C, 0x00],
	[0xFF, 0x52, 0x00],
	[0xFF, 0x47, 0x00],
	[0xFF, 0x3D, 0x00],
	[0xFF, 0x33, 0x00],
	[0xFF, 0x29, 0x00],
	[0xFF, 0x1F, 0x00],
	[0xFF, 0x14, 0x00],
	[0xFF, 0x0A, 0x00],
	[0xFF, 0x00, 0x00],
]
VENDOR_ID = 0x1770
PRODUCT_ID = 0xFF00
class MSISteelSeriesRGBKeyboard:
	def __init__(self):
		self.__BRIGHTNESS_MAP = {
			0x00: 0x05,
			0x01: 0x06,
			0x02: 0x07,
			0x03: 0x0A,
		}
		self.__CHUNK_SIZE = 8
		self.__DEVICE = None
	def open(self, vid, pid):
		self.__DEVICE = usb.core.find(idVendor = vid, idProduct = pid)
		if self.__DEVICE == None:
			raise ValueError("USB Device VID = {}, PID = {} not found".format(vid, pid))
		try:
			if self.__DEVICE.is_kernel_driver_active(0):
				self.__DEVICE.detach_kernel_driver(0)
		except NotImplementedError as error:
			raise error
	def close(self):
		try:
			if not self.__DEVICE.is_kernel_driver_active(0):
				usb.util.dispose_resources(self.__DEVICE)
				self.__DEVICE.attach_kernel_driver(0)
		except NotImplementedError as error:
			raise error
		self.__DEVICE = None
	def send(self, data_array):
		data_array += b"\x00" * (self.__CHUNK_SIZE - len(data_array))
		return self.__DEVICE.ctrl_transfer(
			bmRequestType = 0x21,
			bRequest = 0x09,
			wValue = 0x0301,
			wIndex = 0x0000,
			data_or_wLength = data_array
		)
	def sendCommand(self, command, parameters):
		self.send(b"\x01\x02" + command + parameters)
	def setMode(self, index):
		self.sendCommand(b"\x41", bytes([index]))
	def setColor(self, index, red, green, blue):
		self.sendCommand(b"\x40", bytes([index, red, green, blue]))
	def setBrightness(self, index):
		self.sendCommand(b"\x50", bytes([self.__BRIGHTNESS_MAP[index]]))
	def setBackgroundColor(self, index, red, green, blue):
		index = (index - 1) * 3 + 1;
		self.sendCommand(b"\x44", bytes([index, red, green, blue]))
	def setBackgroundDuration(self, index, seconds):
		index = (index - 1) * 3 + 2;
		self.sendCommand(b"\x44", bytes([index, seconds]))
	def setForegroundDuration(self, index, seconds):
		index = (index - 1) * 3 + 3;
		self.sendCommand(b"\x44", bytes([index, seconds]))
rgb = MSISteelSeriesRGBKeyboard()
rgb.open(VENDOR_ID, PRODUCT_ID)
try:
	while True:
		cpu_percent = int(psutil.cpu_percent(interval = 1))
		ram_percent = int(psutil.virtual_memory().percent)
		dir_percent = int(psutil.disk_usage("/").percent)
		red, green, blue = mapping_rgb[cpu_percent]
		rgb.setColor(1, red, green, blue)
		red, green, blue = mapping_rgb[ram_percent]
		rgb.setColor(2, red, green, blue)
		red, green, blue = mapping_rgb[dir_percent]
		rgb.setColor(3, red, green, blue)
except KeyboardInterrupt as exception:
	print(exception)
rgb.close()

程式執行時,只需定期讀取系統當前的資源用量,從查表中檢索出對應的 RGB數值 ,
並傳送至 MSI SteelSeries RGB鍵盤 來呈現系統的運作狀態。

使用 OpenRGB

當然,若只是單純希望能在 Linux 上對鍵盤進行基礎的 RGB 燈效控制,
直接選用開源社群維護的 OpenRGB 專案無疑是最省時省力的方案。

總結

這個專案足足延遲了八年。
最初,在下只懂得抱怨 MSI SteelSeries RGB鍵盤 的官方控制軟件不支援 Linux。
雖然在下一直認為 RGB燈效 並非必須,但偏偏無法在 Linux 控制,總覺有些不甘心。

然而,在逐漸掌握了逆向工程的方法與邏輯後,在下才領悟到:
許多原本僅限 Windows 硬件周邊,只要願意投入時間去捕捉並分析訊號,就做出與官方軟件相同的核心功能。

透過這次從頭到尾的逆向工程、封包分析、WebHID API 以及系統狀態儀表板的開發,
不僅讓在下徹底釐清了這款鍵盤的通訊協定,也享受到了將硬體設備完全掌握在自己手中的樂趣。

參考資料

沒有留言 :

張貼留言