DIY 口袋无线 KVM:基于 ESP32-C3 + CH9329 的免驱控制线

基于两块 ESP32-C3 SuperMini 与 CH9329 串口键鼠协议芯片,打造一根免驱的口袋无线 KVM 控制线。

项目地址:github.com/is-so-cool/esp32-wireless-kvm

核心特性

  • PiKVM 级全局无感锁鼠拦截 — Pointer Lock API + 相对/绝对坐标双模
  • ESP-NOW 动态变长无线物理隔离 — 键盘 14B / 相对鼠标 11B / 绝对鼠标 13B 智能分帧
  • BIOS 级秒控 — CH9329 硬件 HID 翻译,免驱直通
  • 极低成本 — 总物料约 ¥30

架构概览

1
2
3
4
┌──────────────┐     Web Serial      ┌──────────────────┐   ESP-NOW    ┌──────────────────┐     USB HID     ┌───────────────┐
│ 主控 PC/Mac │ ──────────────────► │ 发射端 ESP32-C3 │ ──────────► │ 接收端 ESP32-C3 │ ──────────────► │ 目标 Windows │
│ (浏览器) │ Type-C 数据线 │ │ 射频 │ + CH9329 │ USB 插入主板 │ 被控电脑 │
└──────────────┘ └──────────────────┘ └──────────────────┘ └───────────────┘

数据流从左到右单向流动:浏览器拦截键鼠事件 → 串口写入 → ESP-NOW 射频 → CH9329 翻译为 USB HID → 目标机。中间链路完全无线物理隔离,两台电脑之间没有任何有线数据连接。

硬件清单

物料 数量 参考价格
ESP32-C3 SuperMini 2 块 ~¥11/块
CH9329 串口转 USB HID 模块 1 块 ~¥5
Type-C 数据线(需支持数据) 1 根 —
细导线 / 杜邦线 若干 —

可选焊接或杜邦线连接。焊接体积小更稳定,杜邦线方便调试但体积较大。

选购注意

  • ESP32-C3 SuperMini 确认板载 USB Type-C 接口且支持数据通信,引脚包含 GPIO 20/21/GND/5V
  • CH9329 模块确认带有 CFG1、CFG0、MODE1、MODE0 四个跳线帽引脚(部分精简版缺失)
  • CH9329 收到后跳线帽保持拔除/悬空状态,不要插入

接线

A 面:发射端

不接任何外部引脚,仅用 Type-C 数据线连接主控 PC/Mac,通过 Web Serial 接收浏览器指令。

B 面:接收端

将 CH9329 模块与接收端 ESP32-C3 连接(焊接或杜邦线):

ESP32-C3 引脚 → CH9329 引脚 说明
GPIO 21 (TX) → RX 单片机发送,CH9329 接收键鼠数据流
GPIO 20 (RX) → TX 单片机接收(留作状态返回与自检)
GND → GND 必须共地,确立串口高低电平基准
5V / VCC → 5V / VCC 由被控端 Windows 主板供电

⚠️ CH9329 模块上的 CFG1, CFG0, MODE1, MODE0 四个引脚必须全部保持拔下、悬空状态,芯片才会默认开启串口协议传输模式。

固件烧录

编译前提:Arduino IDE 中 工具 (Tools) → USB CDC On Boot 必须切换为 Enabled。

第一步:获取接收端 MAC 地址

烧录发射端固件之前,需要先获取接收端 ESP32-C3 的物理 MAC 地址,用于建立 ESP-NOW 射频绑定。

将以下代码烧录到接收端 ESP32-C3(暂时不接 CH9329):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <WiFi.h>

void setup() {
Serial.begin(115200);
delay(2000);

WiFi.mode(WIFI_STA);

Serial.println("");
Serial.println("=========================================");
Serial.print("💡 这块 ESP32-C3 的 MAC 地址为: ");
Serial.println(WiFi.macAddress());
Serial.println("=========================================");
}

void loop() {
pinMode(8, OUTPUT);
digitalWrite(8, LOW);
delay(500);
digitalWrite(8, HIGH);
delay(500);
}

烧录完成后打开串口监视器(波特率 115200),记录输出的 MAC 地址,后续填入发射端固件的 broadcastAddress 数组中。

第二步:烧录发射端固件

烧录到插在 Mac 上的发射端 ESP32-C3。采用异步非阻塞轮询 + 变长编码智能分切架构,杜绝看门狗死锁。

⚠️ 将第 4 行 broadcastAddress 替换为你刚才查到的接收端 MAC 地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <esp_now.h>
#include <WiFi.h>

// 🔒 核心绑定:在此处填入你的【接收端】的 6 位物理 MAC 地址
uint8_t broadcastAddress[] = {0xF0, 0x08, 0xD1, 0x2E, 0x7A, 0xEC};

uint8_t serialRxBuf[32];
int rxCount = 0;
int expectedLen = 14;

void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.disconnect();

if (esp_now_init() != ESP_OK) return;

esp_now_peer_info_t peerInfo;
memcpy(peerInfo.peer_addr, broadcastAddress, 6);
peerInfo.channel = 0;
peerInfo.encrypt = false;

if (esp_now_add_peer(&peerInfo) != ESP_OK) return;
}

void loop() {
while (Serial.available() > 0) {
uint8_t c = Serial.read();

// 严格对齐 CH9329 协议帧头 0x57 0xAB
if (rxCount == 0 && c != 0x57) continue;
if (rxCount == 1 && c != 0xAB) { rxCount = 0; continue; }

// 动态变长分帧:第四字节(CMD)判定包边界
if (rxCount == 3) {
if (c == 0x05) expectedLen = 11; // 相对鼠标包
else if (c == 0x04) expectedLen = 13; // 绝对鼠标包
else expectedLen = 14; // 键盘包
}

serialRxBuf[rxCount++] = c;

if (rxCount == expectedLen) {
esp_now_send(broadcastAddress, serialRxBuf, expectedLen);
rxCount = 0;
}
}

if (rxCount >= 32) rxCount = 0; // 异常溢出防护
}

第三步:烧录接收端固件

烧录到连着 CH9329 的接收端 ESP32-C3。开机时通过 0x09 命令强制将 CH9329 写为工作模式 0x00(键盘 + 全功能鼠标复合模式),并在 onDataRecv 中实现纯净变长无损放行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <esp_now.h>
#include <WiFi.h>

void onDataRecv(const esp_now_recv_info *recvInfo, const uint8_t *incomingData, int len) {
// 纯净变长放行:一字不差直接倒给 CH9329
Serial0.write(incomingData, len);
}

void setup() {
Serial0.begin(9600, SERIAL_8N1, 20, 21);
delay(1200); // 等待 CH9329 通电完毕

// 强制工作模式 0x00(键盘 + 全功能鼠标复合模式)
uint8_t force_mode_0[] = {
0x57, 0xAB, 0x00, 0x09, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x25, 0x80, 0x08, 0x00, 0x00, 0x03,
0x86, 0x1A, 0x29, 0xE1, 0x00, 0x00, 0x00, 0x01, 0x00, 0x0D, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAF
};
Serial0.write(force_mode_0, sizeof(force_mode_0));
delay(500); // EEPROM 写入冷却

pinMode(8, OUTPUT);
digitalWrite(8, HIGH);

WiFi.mode(WIFI_STA);
WiFi.disconnect();

if (esp_now_init() == ESP_OK) {
esp_now_register_recv_cb(esp_now_recv_cb_t(onDataRecv));
}
}

void loop() {
// 空转,数据转发依靠射频底层硬件中断
}

⚠️ 开机关键步骤:烧录完成后,必须将接收端从 Windows 上彻底拔掉,等待 3 秒后再重新插回。设备管理器应出现 1 个键盘 + 1 个符合 HID 标准的鼠标,无黄色感叹号。

网页控制台

html/index.html 是部署在云服务器上的控制台页面,融合 Pointer Lock API 全屏锁鼠 与 33.3ms 前端限流消噪状态机,实现 VNC 级无感跨屏控制。

支持:原生大写字母、Command→Ctrl 映射、Ctrl+Alt+Del 等宏按键、相对鼠标丝滑跟手。

3D 打印外壳(可选)

scad/ 目录包含 OpenSCAD 参数化外壳模型,提供 STL 和 3MF 格式可直接切片打印。

打印建议:PLA/PETG,层高 0.2mm,壁厚 2mm,无支撑。可根据实际模块尺寸修改 .scad 参数后重新导出。

联调测试

  1. 设备自检:发射端插入 PC/Mac USB 口,接收端插入目标 Windows。检查设备管理器确认出现 1 个键盘 + 1 个符合 HID 标准的鼠标,无黄色感叹号。
  2. 连接握手:打开部署好的控制台网页,点击「连接发射端硬件」,在浏览器弹窗中勾选 ESP32-C3 串口并授权。
  3. 锁鼠托管:点击页面中央黑色控制区域,鼠标隐形锁定。测试键盘连击、组合键、鼠标移动是否丝滑跟手。
  4. 一键回城:按 ESC 键解除锁鼠,键鼠秒回本地。

故障排查

故障现象 排查动作
键盘正常,鼠标不动 检查网页打包函数:鼠标命令字 0x05 / 长度 11B;限流 33.3ms
鼠标蓝灯闪但光标不动 拔下接收端等 5 秒,换到主板原生 USB 3.0 接口重新冷启动
黄色感叹号 ⚠️ 设备管理器 → 卸载设备(勾选删除驱动)→ 重新插拔
高频打字幽灵连发 确认 USB CDC On Boot = Enabled;确认固件使用异步非阻塞 Serial.read()
杜邦线连接功能断续 更换优质导线;确保 GND 共地连接可靠