ESC
科技 11 分钟阅读

AI 助力改造非智能升降桌:智能升降、语音控制、多端联动……

作者改造自家非智能升降桌,通过ESP32-S3破解原厂控制协议,实现智能升降、语音控制、多端联动,并加入童锁和ToF保护功能。文章分享了详细的改造过程和开源项目,但存在安全风险,需注意防护。

来源:少数派

利益相关声明:作者与文中产品有直接的利益相关(开发者、自家产品等)

注:

  • 本文在提纲整理、技术段落润色和代码摘录整理等环节使用了 AI 辅助
  • 拆板、逻辑分析仪抓包、I²C 时序、上拉电阻排查、软件 I²C 失败统计、ToF 读数和多端接入,则来自作者真机实验
  • 成稿时已核对文中键码、7-bit 地址、上拉阻值、传感器型号和开源仓库链接;笔者已核对所有文本表述,并修改不便于理解的描述
  • 文中的 Desk Gateway 是我自己维护的开源项目(MIT),没有商业赞助
  • 升降桌改造存在夹伤风险,文里的童锁和 ToF 保护不能替代原厂安全认证;紧急情况请用 STOP、原厂面板或直接断电

背景

早在 2019 年搬入新家之后,我就一直在迭代优化办公桌,现在应该是 V3 版本了:

这个桌子从搬入新家就一直陪伴着我,它承载了太多太多——是的,就是字面上的意思:

后来因为工作原因,我有点轻微腰椎间盘突出,所以决定给它换一个「躯干」,但桌板还是用的原来的(毕竟好一点的升降桌都要 3000+)。最终我选择了一款非智能的双电机版本,升降范围 64~129 cm。我确信凭借自己对硬件的热爱,可以很容易将其改装成智能控制,但可能也是因为对自己的技术过于自信,这个想法拖到最近才得以实现。一方面因为有充足的时间了,二则是因为有了 AI 的加持。

升降桌的基本情况

我的升降桌之前只能通过原厂控制面板操作,面板上有上升、下降和四个档位键:

有时候会出现 B12 错误码,不过同时按住上下键 5 秒就能重置。我最初想增加一个旋钮,顺时针旋转升高,逆时针旋转降低,按下旋钮停止。动手前,我看了少数派的《从零开始,改造一张可以用 Siri 控制的升降桌》,也研究了开源项目 Upsy Desky,它们使用的通信协议和我的升降桌不同,我需要重新分析原厂控制面板与控制盒之间的通信。

除了旋钮,我还准备接入键盘、手机、Web、手表和语音控制。ESP32-S3 负责接收这些入口的指令,再按照原厂协议控制升降桌。后来我又接入了小智 AI:一块 JC3636W518C,放在桌上作为语音终端。

如果你和我的控制面板一样,那大概率是可以复刻的。不过我还是建议你最好拆开面板看一下,看看控制面板的线序、网线的线序是否与我的一致。如果不一样可能就需要使用逻辑分析仪来抓包分析原始数据了。

只要能通过 ESP32-S3 发送正确的指令控制升降,那多端接入就非常简单了。

改造目标与方案选择

原厂控制面板的使用局限

如上面所说,原厂控制面板有上升、下降和四个档位键。单击档位键可以移动到保存的高度,长按会覆盖对应档位。每次操作都需要使用安装在桌边的控制面板。

目前最大的问题是没有童锁功能,家里的小孩儿觉得好玩儿,经常趁我不注意按着玩儿,有一次离家之前将升降桌设置到了最高位置,正好超过了椅子的高度,不知道是不是老人的安全意识不够,把孩子抱起来按了档位 1,椅子抵住了桌面差点把桌子顶翻(这也是一个局限的点,没有障碍检测)。

所以我老早就决定改造成智能升降桌,而且必须加上童锁功能。契机则是一个 zFrontier 的上的一个旋钮控制方案。

从旋钮控制扩展到多终端接入

玩儿客制化键盘的都知道 zFrontier,我没事就会在 ZF 上逛逛。正好看到 Serendipity Dial,我觉得它的可定制化功能非常多,正好我桌面上有 2 个旋钮设备,想着是不是可以通过旋钮来控制升降桌。然后就是上面提到的《从零开始,改造一张可以用 Siri 控制的升降桌》并了解到了 Upsy Desky 这个开源项目,脑海中逐渐有了可落地的方案:

这套架构的核心是 ESP32-S3,它当控制中枢,北向对接 App、手表、键盘、旋钮这类控制端,设备只发统一指令,不需要处理原厂协议;南向对接升降桌控制盒,由 ESP32-S3 把这些指令翻译成控制盒能识别的键码,桌子才会动。

借助 ESP32-S3 内置的 Wi-Fi 和蓝牙通信,我们可以集成多种控制入口。硬件架构如下:

整套方案里最关键的是如何拿到原厂控制盒认的那套指令。当前分层和硬件拓扑记在仓库的架构总览里。

现有开源方案的适配限制

少数派的 Siri 升降桌改造和 Upsy Desky,思路都是在控制盒与面板之间加一块控制模块,这点和我后来做的网关接近。但升降桌面板没有统一协议,品牌不同、批次不同,通信方式都可能不一样。

我把这两份方案对照自己的桌子看过之后,发现都不适配。Upsy Desky 支持的控制盒和我的不是一种,我的原厂面板用 RJ45 插头只引出四根线,线序、协议和按键数据都得自己确认,然后用 ESP32-S3 模拟原厂控制面板和控制盒通信。

原厂控制面板拆解与总线识别

RJ45 接口与四线定义

原厂面板通过 RJ45 插头与控制盒连接。拆开面板后,电路板背面有四个测试点,标记分别为 GND(接地)、CLK(时钟信号) 、DAT(传输数据) 和 3.3V(电源):

TM1650 芯片与总线类型判断

原厂面板使用 TM1650 驱动数码管,结合 CLK 和 DAT 两个测试点,初步判断面板与控制盒之间使用 I²C 通信。

芯片型号还不能确定完整协议。只能继续通过总线抓包确认主从关系、设备地址、按键编码和高度数据。

逻辑分析仪接线与采样参数调整

抓包时,原厂面板保持与控制盒连接,逻辑分析仪接入 GND、CLK 和 DAT 三个测试点。分析仪不连接面板的 3.3V,也不参与供电。

我最初把采样率设为 20 kHz,只能看到电平变化,无法解码完整数据。把采样率提高到 12 MHz 后,PulseView 可以稳定识别地址和数据。

我分别采集了静置、按住上升、按住下降、单击档位和长按保存的数据。采集高度数据时,我让桌子从 64 cm 升到 129 cm,同时记录面板显示的高度,方便后续对照。抓包步骤见逻辑分析仪抓包。

  • SDA 对应 DAT
  • SCL 对应 CLK
  • 然后将协议修改为 I²C 后就可以正常读取

I²C 通信机制与协议解析

物理上我们只有一对 CLK / DAT,TM1650 用不同从机地址拆成两条通道:控制盒读 0x24 问现在按了哪颗键,写 0x34 到 0x37 刷数码管;PulseView 显示的是 7-bit 地址;TM1650 手册常用 8-bit 写地址,0x24 就是手册里的 0x48,0x34 就是 0x68。

完整键码、时序和抓包索引在协议逆向笔记。

I²C 主从关系与周期轮询

通过抓包,我们可以确认:

  • 控制盒是 I²C Master,原厂面板是 Slave
  • 时钟由控制盒产生,SCL 大约 9.6 kHz
  • 面板不主动上报,只在被问到时把当前键态放上总线。

另外静置时总线不是空闲的。控制盒大约每 3.7 毫秒做一轮完整问答:先往 0x24 写一字节 DW=0x01,再读一字节 DR。DW 实测一直是 0x01,像显示控制 / 亮度一类的寄存器,键态不在这里。键码在 DR 里。静置时 DR 是 0x2E。

这张图是静置总览,写 0x01 和读 0x2E 一直在交替:

这一轮写和读是分开的两笔事务,中间各有一次 STOP,一次完整轮询是:

S Address write 0x24 ACK Data write 0x01 ACK P
约 29 us
S Address read 0x24 ACK Data read 0x2E ACK P
约 95 us 后再开下一轮

读完那一字节之后,控制盒用的是 ACK 再 STOP,不是常见的 NACK 再 STOP。后面用 ESP32 去复放面板侧时序,就是卡在这一点上。这张图是单次读写放大:

持续返回上升或下降键码时,桌子保持运动;改回 0x2E 后停止。控桌的本质是改 Slave 每次被读时交出的 DR,不是往总线上塞一条「升桌命令帧」。

升降、停止与档位指令解析

对比静置、按住上升、按住下降的抓包:DW 始终 0x01,变的只有 DR。

操作**DR**说明空闲、松手0x2E必须一直回这个,不要静默不应答按住上升0x47要升多久就回多久按住下降0x4F要降多久就回多久档位 1 单击0x17前往已保存高度档位 1 长按保存0x57``0x17 加上 bit6(0x40)档位 4 单击0x2F前往已保存高度档位 4 长按保存0x6F``0x2F 加上 bit6

上升和下降只差 bit3(0x47 ⊕ 0x4F = 0x08)。档位短码和长按差的是 bit6,事务骨架不变。

另外,档位 2 和档位 3 暂时没有抓包,因为我现在常用的只有档位 1 和档位 4,后续可以通过高度传感器来处理多档位问题。

同时按住上和下大约 5 秒,原厂可以清 B12。抓包里对应 DR=0x7F,有效区间大约 7.5 秒,结束后必须回到 0x2E。

数码管高度数据解析

高度不在 0x24 的 DR 里。按住上升时 DR 一直是 0x47,不会跟着厘米数往上爬。我最初在键通道里搜 64、102 这种字面量,找不到,还把写往 0x34 至 0x37 的帧当成电机干扰滤掉了。

拆板确认 U2 是 TM1650 之后,这些地址对上了手册里的 digit 寄存器(8-bit 0x68 / 0x6A / 0x6C / 0x6E)。控制盒一边读键,一边把七段段码写到数码管。面板自己没有高度 MCU,总线上的高度是显示镜像,整数厘米。

逻辑位7-bit显示DIG10x34百位,两位高度时常写 0x00 消隐DIG20x35十位DIG30x36个位DIG40x37第四位;静止时常等于 DIG3

总线写序多数是 DIG3 → DIG2 → DIG1 → DIG4,组装高度要按逻辑编号,不能按时间顺序当百、十、个。长按保存档位 1 / 4 的静置帧里,四元组精确重复,段码不变:

AW:36 DW:C5 / AW:35 DW:DB / AW:34 DW:00 / AW:37 DW:C5 → 64 cm
AW:36 DW:9E / AW:35 DW:5F / AW:34 DW:44 / AW:37 DW:9E → 102 cm

对照 TM1650 段码表,还能还原出 82、121。静态大约每 2 到 3 秒刷一整帧;运动中更密。

ESP32-S3 控制链路验证

拿到了指令数据,就可以开始使用 ESP32-S3 来模拟指令实现升降桌控制了:控制盒通过 RJ45 接口和控制面板连接,如果需要使用 ESP32-S3 来下发控制指令的话,需要一个 RJ45 模块。

这里我买的是独立的双口 RJ45 模块,左侧接口通过网线连接到升降桌的控制盒,进而使用 ESP32-S3 下发指令来控制升降桌,右口是连接原厂控制面板,意思是保留原厂控制面板的控制功能,不过这里需要 ESP32-S3 来透传指令。

ESP32-S3 模拟原厂控制面板

我把 ESP32-S3 配置成地址 0x24 的 I²C Slave,用它替代原厂面板响应控制盒。Web 页面按住上升时返回 0x47,松手时返回 0x2E;下降操作返回 0x4F。

控桌不是 ESP32 主动发一条「升桌帧」,而是控制盒每大约 3.7 ms 来读一次,Slave 把当前键码交出去。固件在 mxtark.c 里大致是这样:

#define ADDR_KEY_7BIT 0x24u
#define DR_IDLE 0x2Eu
#define DR_UP 0x47u
#define DR_DOWN 0x4Fu
i2c_slave_config_t cfg = {
 .i2c_port = I2C_NUM_0,
 .scl_io_num = GPIO_NUM_4, /* CLK */
 .sda_io_num = GPIO_NUM_5, /* DAT */
 .slave_addr = ADDR_KEY_7BIT,
 .addr_bit_len = I2C_ADDR_BIT_LEN_7,
 .send_buf_depth = 64,
 .receive_buf_depth = 64,
};
i2c_new_slave_device(&cfg, &handle);
i2c_slave_register_event_callbacks(handle, &cbs, ctx);
/* 控制盒发起读请求时,ISR 只往队列丢一个记号,不在中断里写总线 */
static bool IRAM_ATTR on_request_cb(i2c_slave_dev_handle_t i2c_slave,
 const i2c_slave_request_event_data_t *evt,
 void *arg)
{
 slave_ctx_t *ctx = arg;
 xQueueOverwriteFromISR(ctx->tx_q, &token, &hp);
 return hp == pdTRUE;
}
/* 真正写总线放在任务里,每次交出当前 DR */
static void slave_tx_task(void *arg)
{
 for (;;) {
 xQueueReceive(ctx->tx_q, &token, portMAX_DELAY);
 uint8_t dr = (uint8_t)atomic_load(&s_dr); /* 0x2E / 0x47 / 0x4F */
 i2c_slave_write(ctx->handle, &dr, 1, &written, 50);
 }
}

Web 按住升,只是把 s_dr 改成 0x47;松手改回 0x2E。I²C 外设负责 ACK 和移位,业务代码不用去翻 CLK/DAT。

我先用 Web 页面发送指令,分别检查网络请求、固件状态、串口日志和 I²C 通信,暂时不接入旋钮。

首次联调中的通信异常

首次上电时,串口日志可以正常记录键码变化。点击上升后当前键码变为 0x47,点击下降后变为 0x4F,但升降桌没有动作。启动后 Slave 是起来了的:

I (3124) mxtark: I2C slave @0x24 SCL=4 SDA=5

Web 按住升、松手、再按住降,日志大致是这样,桌子纹丝不动:

I (18420) desk_core: motion request source=rest mode=hold dir=up action=start status=idle
I (18421) mxtark: DR=0x47
I (19208) mxtark: DR=0x2E
I (22110) desk_core: motion request source=rest mode=hold dir=down action=start status=idle
I (22111) mxtark: DR=0x4F
I (22940) mxtark: DR=0x2E

mxtark:DR=0x47 只说明固件把待回传的字节改了。串口日志只能证明固件更新了待发送的键码,不能证明控制盒已经通过 I²C 读到该数据。

所以这里我卡了一天,查了很多资料,直到我再次查看原厂 PCB 才大概确认的问题:

TM1650 旁边有两只贴片电阻,一端接到 3.3V,另一端分别接到 CLK 和 DAT。断电量下来大约 1.99 kΩ。I²C 的 CLK、DAT 不能像普通 GPIO 那样由芯片把线推到高电平。总线上的设备只会把线拉到地,表示 0;要表示 1,得靠电阻把线拉回 3.3V,这就是上拉电阻。没有上拉电阻,线会悬在中间,控制盒读到的既不是稳定的高,也不是稳定的低。Slave 就算把 0x47 准备好了,对面也轮询不下来。

原厂面板插着的时候,上拉电阻焊在面板上。我用 ESP32 替换面板,等于把这两只电阻一起拔掉了。所以日志里 DR 在变,总线上却没有合格的高电平。

I²C 上拉电阻与通信恢复

确认之后,我用控制盒的 3.3V,分别通过一只 2 kΩ 电阻接到 CLK 和 DAT。电阻是并联到信号线上的上拉,不是串在 GPIO 前面的限流电阻;白线和黑线仍直接进 GPIO4、GPIO5。红线只给这两只电阻供电,不能接到 ESP32 的 3V3。

ESP32-S3 继续使用 USB-C 单独供电,并与控制盒共地。增加上拉电阻后,Web 页面可以控制桌子升高和降低。万事开头难,从零到一总算让桌子动了起来,当时的留下的照片,有必要纪念一下:

接线图如下:

加上电阻后连续升高、连续降低和松手停止都正常工作。不过还没有启用档位功能。

至此 ESP32-S3 已经能按原厂协议升降桌子,Web 控桌可以用了。接下来要把高度读出来。桌面右侧有键盘架,升太高会顶上去,所以必须限制最高位:

双口 RJ45 透传原厂面板

Web 能控制桌子升降之后,桌边那块原厂面板还空着。双口模块左口已经接到控制盒,右口用来接回原厂面板,让按键继续能用。

不能简单地把左右 RJ45 的 CLK/DAT 短接,这会绕过 ESP32,童锁和来源权限都拦不住。所以让面板按键从右口进 GPIO6 / GPIO7,和 Web、BLE 一样先交给 desk_core,再由左口 GPIO4 / GPIO5 的硬件 I²C Slave 回给控制盒。控制逻辑的数据流向图:

**信号****左口(控制盒)****右口(原厂面板)**pin 1 / 红 / 3.3V现有两只 2 kΩ 上拉的电源端与左口 pin 1 直接跳线pin 2 / 白 / CLKGPIO4GPIO6pin 3 / 绿 / GNDESP32 GND与左口 pin 3 直接跳线pin 4 / 黑 / DATGPIO5GPIO7

红线和地左右跳通,只是给原厂面板供电。CLK/DAT 不要左右对跳。右口不再补上拉,原厂面板板上那两只约 1.99 kΩ 已经在。红线仍然不接 ESP32 的 3.3V。这张图是右口接线:

左口还是对着控制盒:ESP32 当 I²C Slave,地址 0x24,控制盒来问键码,它负责回答。右口则相反,对面是原厂面板。面板自己不会主动上报,得有人装成控制盒去问它「现在按的是哪颗键」。

所以右口这边,ESP32 得当 Master。抓包看到的问答大概是 9.6 kHz,而且写和读是两笔独立事务:先写完一字节,中间有一次 STOP,再另开一笔去读键码。ESP-IDF 自带的硬件 I²C Master 习惯把「写 + 读」捏成一笔,结束时的 ACK/NACK 也跟控制盒不一样,对不上这套时序,所以复放不了。右口只能改用 GPIO 自己翻脚来模拟。

所以 GPIO6、GPIO7 不用芯片里的 I²C 外设,改成程序自己翻转这两根脚。I²C 平时靠上拉电阻把线拉高,芯片只负责把线拉低,这种接法叫「开漏」。软件按抓到的时序去读键码,再交给和 Web 同一套仲裁。没开童锁、Panel 也开着时,桌边按键优先于 Web。按住升降时拔掉右口网线,桌子会停。童锁和单独关掉 Panel 后面单独写。左口右口针脚见真机验收清单和固件 README。

高度数据解析与通信方案调整

ESP32-S3 用硬件 I²C Slave 顶替原厂控制面板,只应答控制盒对 0x24 的键轮询。业务代码更新当前键码,ACK、移位和总线时序都交给 I²C 外设。键码如下:

停止 0x2E
上升 0x47
下降 0x4F

目前通过 ESP32-S3 能正常控制升降,但是还不能正常显示高度。Web、REST、BLE 没有真实高度,档位闭环和最高安全高度也做不了。

原厂面板有三位七段数码管,我的第一反应是既然 ESP32 已经在同一组 CLK/DAT 上应答控制盒,能不能把高度一并读出来?于是继续分析抓包数据:

通道地址方向****内容键通道0x24控制盒读面板当前按键,DR 里是 0x2E / 0x47 / 0x4F 这类键码显示通道0x34–0x37控制盒写面板TM1650 digit 段码,面板拿它刷新数码管

DR 里找不到 64、102 这种厘米字面量。高度是控制盒写给数码管的段码。旁路嗅探 0x34–0x37 的写事务,再用本面板标定过的经验字库,可以把显示高度还原成整数厘米。

ESP32-S3 硬件 I²C 资源限制

要在 ESP32-S3 中同时处理按键和高度,需要响应同一对 CLK/DAT 上的五个 Slave 地址。0x24 用于按键,0x34 至 0x37 用于四个数码管位置。ESP32-S3 有两组硬件 I²C 控制器,但一个硬件 I²C Slave 只能配置一个精确地址。

两组控制器无法同时覆盖这五个地址。这里的限制是五个 Slave 地址,不是五组 I²C 线,物理连接仍然只有一对 CLK/DAT。

产品路径已经占用 @0x24 回键码。同一组 GPIO4/5 上再去 ACK 0x34–0x37,硬件外设做不到。于是出现两条实验路径:

**GPIO 边沿嗅探,**硬件 Slave 继续只应答 0x24,另用 GPIO ISR 听 CLK/DAT,试图在不 ACK 的情况下偷看 digit 写。真机结果是 NO-GO:SCL 每个上升沿和 SDA 边沿都会打高频中断,和硬件 Slave 抢同一组脚。控制盒在 0x34–0x37 被 NACK 后往往会停写,纯监听拿不到段码 data。

更糟的是升降本身开始不稳,高度仍然未知。

另一个路径是软件 I²C 多地址响应方案。

软件 I²C 多地址响应方案

为了同时响应五个地址,我将控制盒一侧改成软件 I²C。程序通过 GPIO 中断读取时钟和数据边沿,并在软件中处理 START、STOP、地址、数据和 ACK。状态机在 mxtark_soft_i2c_sm.c,GPIO 边沿在 mxtark_soft_i2c_esp.c:

#define ADDR_KEY_7BIT 0x24u
#define ADDR_DIG1_7BIT 0x34u
#define ADDR_DIG4_7BIT 0x37u
/* 0x24 读写都应答;0x34–0x37 只应答写,用来收段码 */
static bool address_is_supported(uint8_t addr7, bool read)
{
 if (addr7 == ADDR_KEY_7BIT) {
 return true;
 }
 return !read && addr7 >= ADDR_DIG1_7BIT && addr7 

我采购了 2 类距离传感器,分别是 TOF050C 和 TOF400C:

**安装位置****模块 / 芯片****当前用途****产品数据**桌面右侧,水平朝向右侧障碍物TOF050C / VL6180X测量桌面右侧间距,参与低位上升保护`right_gap_mm`桌板下方,垂直朝向地面TOF400C / VL53L1X直接作为桌面高度,参与档位闭环和最高高度保护`height_mm` / `tof_height_mm`

![](https://cdnfile.sspai.com/2026/08/18/95cc9ab3f74889e5f4c71d5ac599f2cb.jpg?imageView2/2/w/1120/q/90/interlace/1/ignore-error/1)

两块模块的接口丝印相同,均为 `VIN/GND/SDA/SCL/INT/SHUT`。TOF400C 模块正面标注 `TOF400C-VL53L1X`;固件还会读取芯片 ID,TOF050C 期望 `0xB4`,TOF400C 期望 `0xEACC`。

当前高度不做卷尺高度换算,也不叠加安装偏移。实测参考关系为:

**TOF400C 读数****卷尺参考高度****用途**约 `56 cm`约 `71 cm`坐姿档位约 `87 cm`约 `105 cm`站姿档位`94 cm`—允许配置的最高传感器高度

两颗传感器共用 GPIO10 和 GPIO11 的硬件 I²C 总线,并由 ESP32-S3 的 3.3V 供电。它们上电后的默认地址都是 `0x29`。固件先通过 GPIO12 释放 TOF050C,把它的运行地址改为 `0x30`,再通过 GPIO13 启动保持 `0x29` 的 TOF400C。

![](https://cdnfile.sspai.com/2026/08/18/1ba53bfdceb58358f843b497342e37aa.png?imageView2/2/w/1120/q/90/interlace/1/ignore-error/1)

固件会过滤无效量程状态,并在数据超过 1 秒没有更新时标记为离线。高度和右侧距离都增加了稳定滤波,Web 页面可以实时显示两路数据。

![](https://cdnfile.sspai.com/2026/08/18/b815b4beed023509b6d2f409abfa9081.png?imageView2/2/w/1120/q/90/interlace/1/ignore-error/1)

Web 和 OLED 现在都能显示两颗传感器的实时数据。固件已经使用 TOF400C 的原始距离完成档位闭环和最高位限制,并使用 TOF050C 处理低位向上运动时的右侧障碍。上升策略见[双 ToF 安全策略](https://github.com/dong4j/desk-gateway/blob/main/docs/architecture/tof-safety.md)。

## OLED 本地状态显示

为了避免只能在 WEB 端看到当前桌面高度,我在 ESP32-S3 上又接入了一块 0.91 英寸 SSD1306 OLED,分辨率为 128×32。OLED 与两颗 ToF 传感器共用 GPIO10 和 GPIO11,使用 3.3V 供电。固件启动时会自动检测 `0x3C` 和 `0x3D` 两个常用地址。

**OLED****ESP32-S3****说明**GNDGND必须与 ESP32、ToF 外部电源共地VCC3V3不接外部 5V,避免 SDA/SCL 被上拉到 5VSCLGPIO10共享 I2C1 时钟SDAGPIO11共享 I2C1 数据

![](https://cdnfile.sspai.com/2026/08/18/7b4a01e96fe5bd97e255a1bd6707f922.png?imageView2/2/w/1120/q/90/interlace/1/ignore-error/1)

屏幕每 4 秒轮播以下页面:

- 大字显示桌面高度和右侧墙面间距。
- 显示桌子运动状态、TOF050C/TOF400C 在线状态和童锁。
- 显示 Wi-Fi 模式、IP 地址、BLE 当前连接数和 Bond 数量。

桌子运动时,OLED 会优先显示 `UP`、`DOWN` 或 `PRESET` 以及实时高度。传感器掉线后,屏幕显示 `SENSOR OFFLINE`。OLED 只读取各模块的状态,不发送升降指令,也不会在传感器异常时阻止 STOP 或手动升降。页面和接线见[OLED 状态屏](https://github.com/dong4j/desk-gateway/blob/main/docs/architecture/oled-status-display.md)。

![](https://cdnfile.sspai.com/2026/08/18/720d70f3a72a964f7711f27644047854.jpg?imageView2/2/w/1120/q/90/interlace/1/ignore-error/1)

## 多终端控制、语音与本地状态显示

桌子能动、高度也能读之后,这项目算是完成了一大半了。剩下是多端控制,Web 其实已经能做完全部操作,不过都走到这一步了,多端就是锦上添花。

那就多添一点花。目前已实现的控制端有(用法汇总见[多种方式控制升降桌](https://github.com/dong4j/desk-gateway/blob/main/docs/guides/control-methods.md)):

**控制端****怎么用**局域网 Web浏览器打开网关页面,按住升降、切档位、开关童锁REST / 脚本`desk-preset.sh`、curl、快捷键都走这一组接口iPhone / Android AppBLE 优先,必要时回退局域网Apple WatchDigital Crown 连续升降键盘Karabiner 快捷键切坐姿、站姿、停止桌面旋钮顺时针升、逆时针降、按下停止GoatRemoteMac 上语音切坐姿 / 站姿小智 AI桌上 JC3636W518C,圆屏页面和语音Ulanzi D200H实体键坐 / 站 / 番茄原厂面板双 RJ45 透传后,桌边按键仍可用

还没做的入口:

**控制端****打算怎么接****现状**Home Assistant局域网 MQTT,Discovery 出 Cover 实体,坐 / 站 / 停方案写了,固件没接MatterESP32-S3 上跑 Matter,手机当控制器添加未实现Siri走 Matter 或快捷指令,不另做一套云未实现米家 / 华为智慧生活正品级要认证模组,不打算在现有板上硬搓私有协议未实现

这些都不走公网端口映射。HA 如果接上,也只挂在家里的 Broker 上。方案分别在[MQTT / Home Assistant](https://github.com/dong4j/desk-gateway/blob/main/docs/future/mqtt-home-assistant.md)和[米家 / 华为生态调研](https://github.com/dong4j/desk-gateway/blob/main/docs/future/ecosystem-xiaomi-huawei.md)(编注:发稿时 HA 已成功接入)。

### Web 端

ESP32 可以按原厂协议控制桌子升降之后,为了快速验证项目,我先做了局域网 Web。

![](https://cdnfile.sspai.com/2026/08/18/f5551920b7b57e06f8f9343439792e19.png?imageView2/2/w/1120/q/90/interlace/1/ignore-error/1)

浏览器打开网关页面就能按住升降、松手停止,也可以切坐姿、站姿和童锁。没有 Wi-Fi 时设备会开 SoftAP,SSID 是 `DeskGateway`,配网页在 `http://192.168.4.1/`。默认密码和 Web 登录密码都是 `desk-gateway`,可以到设置修改密码。

页面上的升/降是按住才会动,完美复刻原厂控制面板。高度来自 TOF400C,设置页可以改档位、来源权限和童锁。Web 只挂在局域网,不要做公网端口映射。本机要改的 IP、密码和路径见[本地多端部署](https://github.com/dong4j/desk-gateway/blob/main/docs/guides/local-multi-client-setup.md)。

![](https://cdnfile.sspai.com/2026/08/18/edc64aa1f0b8b51f841ceebe5e11a020.png?imageView2/2/w/1120/q/90/interlace/1/ignore-error/1)

### REST API

我把这组接口固定成 REST API,方便对接多种控制端。脚本和第三方用 `X-Desk-Key`,浏览器用 `Authorization: Bearer`,两个头等价,值等于当前 Web 密码。

键盘、旋钮、GoatRemote、D200H、小智桥接,以及手机和手表的 Wi-Fi 通道,发的都是同一套升、降、停、档位指令,不用各自去处理 I²C。后面能继续加入口,靠的就是这组 REST API。契约见 [REST API](https://github.com/dong4j/desk-gateway/blob/main/docs/guides/rest-api.md)。

curl -s -H “X-Desk-Key: $DESK_KEY” “http://$DESK_IP/api/v1/desk/status” curl -s -X POST -H “X-Desk-Key: $DESK_KEY”
“http://$DESK_IP/api/v1/desk/preset/4/goto” curl -s -X POST -H “X-Desk-Key: $DESK_KEY”
“http://$DESK_IP/api/v1/desk/stop”


仓库里的 [scripts/desk-preset.sh](https://github.com/dong4j/desk-gateway/blob/main/scripts/desk-preset.sh) 把常用动作包了一层:`1` 坐姿,`4` 站姿,`up` / `down` 是旋钮那种短租约 jog,不是 Web 长按。

### 键盘和旋钮

有了 REST 之后,键盘和旋钮就只需要发 HTTP 请求即可操作升降桌。我用 Karabiner 把旋钮映射成三个闲置功能键,分别对应顺时针、逆时针和按下。按键事件进 Shell 脚本,脚本再调 ESP32 的局域网接口。

键盘快捷键同样走 `desk-preset.sh`,切坐姿、站姿或停止。映射和 jog 时序见[键盘、旋钮与语音控制](https://github.com/dong4j/desk-gateway/blob/main/docs/guides/keyboard-voice-control.md),Karabiner 配置在 [integrations/karabiner](https://github.com/dong4j/desk-gateway/blob/main/integrations/karabiner/README.zh-CN.md)。

{ “description”: “ForcePad: F1 → 升降桌:1”, “manipulators”: [ { “type”: “basic”, “from”: { “key_code”: “f1”, “modifiers”: { “optional”: [“any”] } }, “to”: [ { “shell_command”: “/Users/dong4j/Developer/1.AI/ai-incubator/desk-gateway/scripts/desk-preset.sh 1” } ], “conditions”: [ { “type”: “device_if”, “identifiers”: [ { “vendor_id”: 6790, “product_id”: 65024 } ] } ] } ] }, { “description”: “ForcePad: F2 → 升降桌:stop”, “manipulators”: [ { “type”: “basic”, “from”: { “key_code”: “f2”, “modifiers”: { “optional”: [“any”] } }, “to”: [ { “shell_command”: “/Users/dong4j/Developer/1.AI/ai-incubator/desk-gateway/scripts/desk-preset.sh stop” } ], “conditions”: [ { “type”: “device_if”, “identifiers”: [ { “vendor_id”: 6790, “product_id”: 65024 } ] } ] } ] }, { “description”: “ForcePad: F3 → 升降桌:4”, “manipulators”: [ { “type”: “basic”, “from”: { “key_code”: “f3”, “modifiers”: { “optional”: [“any”] } }, “to”: [ { “shell_command”: “/Users/dong4j/Developer/1.AI/ai-incubator/desk-gateway/scripts/desk-preset.sh 4” } ], “conditions”: [ { “type”: “device_if”, “identifiers”: [ { “vendor_id”: 6790, “product_id”: 65024 } ] } ] } ] }


旋钮不能跟 Web 长按用同一套语义。第一个刻度只记录方向,700 毫秒内出现第二个同方向刻度后才启动升降。停止旋转后,最后一次指令的运动租约大约 500 毫秒后失效,ESP32-S3 自动停止。反向旋转时先停掉当前运动,再启动相反方向。

{ “description”: “ForcePad: ⌘ + PageUp → 升降桌:持续上升”, “manipulators”: [ { “type”: “basic”, “from”: { “key_code”: “page_up”, “modifiers”: { “mandatory”: [“command”], “optional”: [“any”] } }, “to”: [ { “shell_command”: “/Users/dong4j/Developer/1.AI/ai-incubator/desk-gateway/scripts/desk-preset.sh up” } ], “conditions”: [ { “type”: “device_if”, “identifiers”: [ { “vendor_id”: 6790, “product_id”: 65024 } ] } ] } ] }, { “description”: “ForcePad: ⌘ + PageDown → 升降桌:持续下降”, “manipulators”: [ { “type”: “basic”, “from”: { “key_code”: “page_down”, “modifiers”: { “mandatory”: [“command”], “optional”: [“any”] } }, “to”: [ { “shell_command”: “/Users/dong4j/Developer/1.AI/ai-incubator/desk-gateway/scripts/desk-preset.sh down” } ], “conditions”: [ { “type”: “device_if”, “identifiers”: [ { “vendor_id”: 6790, “product_id”: 65024 } ] } ] } ] }


现在可以全部使用键盘和旋钮来控制升降:

![](https://cdnfile.sspai.com/2026/08/18/a0bceb9723edfc30370f80b92681ff90.jpg?imageView2/2/w/1120/q/90/interlace/1/ignore-error/1)

### 手机和手表也可以走 REST

同一组 REST,iPhone、Android 和 Apple Watch 也能用。App 不需要懂原厂键码,发升、降、停、档位即可;Watch 的 Digital Crown 正向升高、反向降低,停转后发 STOP;Crown 走 jog,和旋钮一样靠固件短租约兜底,不用 Web 那种按住续期的接口。

手机见 [App README](https://github.com/dong4j/desk-gateway/blob/main/mobile/app/README.zh-CN.md),手表见 [Watch README](https://github.com/dong4j/desk-gateway/blob/main/mobile/watch/README.zh-CN.md),双通道见[移动端双通道](https://github.com/dong4j/desk-gateway/blob/main/docs/architecture/mobile-connection-transport.md)。

![](https://cdnfile.sspai.com/2026/08/18/9449c69e29bbe908fff521d1b0fef01f.jpeg?imageView2/2/w/1120/q/90/interlace/1/ignore-error/1)

### 再补一条蓝牙通道

局域网够用,但手机离路由器远、或者不想填 IP 的时候,REST 就不方便。固件后来也做了 BLE:广播名 `DeskGateway`,最多三个 Central,同一时间只有一个运动所有者,非所有者会收到 Desk Busy `0x80`,任意 STOP 始终有效。UUID 和字节协议见 [BLE Accessory Profile](https://github.com/dong4j/desk-gateway/blob/main/docs/architecture/ble-accessory-profile.md)。

手机和手表现在都是双通道:默认优先 BLE,连不上或断开后再走局域网 REST。通道切换不会重放刚才的运动命令。童锁、来源权限、运动租约和断连停止仍由 ESP32 上的同一套逻辑处理,客户端只是入口。

### 优篮子 D200H

桌面上还有一块优篮子 D200H,按键布局接近 Stream Deck。有了 REST 之后,我给它写了一个插件,把升降桌和番茄时钟做到三个实体键上。三个键的动作都是调用 REST API:

**按键****调用****键面显示**请坐`POST /api/v1/desk/preset/1/goto`坐姿目标高度和当前高度站立`POST /api/v1/desk/preset/4/goto`站姿目标高度和当前高度番茄时刻`POST /api/v1/reminder/action`,`{"action"`:`"start_focus"}`设备端剩余倒计时

三个键共用一次 `GET /api/v1/desk/status` 轮询。倒计时在 ESP32 上跑,插件不在电脑里另起一套计时器。网关离线时键面显示「离线」,不会继续刷陈旧高度。

番茄时钟默认专注 25 分钟、短休息 5 分钟,每完成 4 个专注再进 15 分钟长休息。提醒只出语音、音效和状态,不会到点自动升桌或降桌。

![](https://cdnfile.sspai.com/2026/08/18/36522d32dc416e1860b2ade87dfc5b8f.jpg?imageView2/2/w/1120/q/90/interlace/1/ignore-error/1)

源码在 [integrations/ulanzi-d200h](https://github.com/dong4j/desk-gateway/blob/main/integrations/ulanzi-d200h/README.zh-CN.md),不能直接复制进 UlanziStudio,要按插件 README 用官方 SDK 编译后再安装。任意一个键的属性面板里填网关地址和 `X-Desk-Key`,三个键共享这份配置,密钥不写进源码。电脑、UlanziStudio 和网关必须能互通,UlanziStudio 也要保持运行。`.local` 解析不了就改填 DHCP IP.

### GoatRemote

Mac 上我还用 [GoatRemote](https://goatremote.com/) 通过 Apple TV Remote 来控制:

![](https://cdnfile.sspai.com/2026/08/18/5f8700ae1f665da731fac082b8586315.jpg?imageView2/2/w/1120/q/90/interlace/1/ignore-error/1)

可以将听到的话直接映射成已经写好的 Shell 命令。命令和键盘、旋钮一样,调用 `scripts/desk-preset.sh`。提示词和命令表见 [GoatRemote 集成](https://github.com/dong4j/desk-gateway/blob/main/integrations/goatremote/README.zh-CN.md)。

![](https://cdnfile.sspai.com/2026/08/18/c925d1191b837cf347cbbc1db6a9f651.png?imageView2/2/w/1120/q/90/interlace/1/ignore-error/1)

我只加了两句:

**我说的话****脚本**升降桌降到最低`desk-preset.sh 1`升降桌升到最高`desk-preset.sh 4`

Action 选 `shell`。不在 Terminal 里时,GoatRemote 会包成 `osascript` / `do shell script`,所以脚本路径要用这台机器的绝对路径,并先改好脚本里的 `DESK_BASE_URL` 和 `DESK_KEY`。

### 小智 AI 硬件

桌上这块是晶彩的 JC3636W518C,1.8 英寸圆屏,小智固件里的板卡名是太极派 `taiji-pi-s3`。我没有一直用官方预编译包,而是在 `xiaozhi-esp32` 里给这块板加了一套空闲界面:左右滑动三个页面,小智进入对话后仍回到原来的聊天界面。点到没有按钮的区域,还是原来的对话开关。三页分别是:

**页面****做什么**主页时间、日期,以及桌面高度和番茄钟是否在跑升降桌显示当前高度;坐姿、站姿两个键,两秒内再点一次才发送,只走档位 1 和档位 4,没有持续升降番茄钟显示设备端倒计时,可以开始、暂停、继续和停止

![](https://cdnfile.sspai.com/2026/08/18/d19e79ea16674cb2aaeadbfba2ef0889.jpg?imageView2/2/w/1120/q/90/interlace/1/ignore-error/1)

后台自己轮询 Desk Gateway 的 `GET /api/v1/desk/status`,按键走同一组 REST。网关地址和 `X-Desk-Key` 写在本机 `sdkconfig` 里,不进 Git。URL 或密钥留空时,控制中心还能看时间,桌控和番茄钟会显示未配置。童锁打开或上升被拦住时,页面会直接提示,不会再发运动。硬件侧部署见[小智固件与本地 Server](https://github.com/dong4j/desk-gateway/blob/main/docs/guides/xiaozhi-firmware-and-local-server.md)。

语音仍先说「你好小智」,等进入聆听后再下指令。

### 从小智智能体到升降桌

触摸页面会直接调用局域网 REST API。语音这条没有改成设备直连,仍走小智云智能体上的 MCP,再进 Mac 上的桥接。这张图是语音链路:

![](https://cdnfile.sspai.com/2026/08/18/41d26bacd705ac9bc146ebd0f590e951.png?imageView2/2/w/1120/q/90/interlace/1/ignore-error/1)

小智智能体只看到五个固定 MCP 工具。Mac 上的桥接程序把工具映射到固定 REST 路由,Desk Gateway 的地址和访问密钥保存在本地配置文件中。桥接已经交给 `launchd` 常驻,登录后自动启动,异常退出后也会重新拉起。用法见[小智 AI 控桌](https://github.com/dong4j/desk-gateway/blob/main/docs/guides/xiaozhi-ai-desk-control.md),桥接代码在[integrations/xiaozhi-mcp](https://github.com/dong4j/desk-gateway/blob/main/integrations/xiaozhi-mcp/README.zh-CN.md)。

![](https://cdnfile.sspai.com/2026/08/18/d35bb8b38cc82bc27ced4bc734ac7c08.jpg?imageView2/2/w/1120/q/90/interlace/1/ignore-error/1)

小智官方在 [MCP IoT Control Usage](https://github.com/78/xiaozhi-esp32/blob/main/docs/mcp-usage.md) 中把 MCP 作为 IoT 控制协议,后端通过 `initialize`、`tools/list` 和 `tools/call` 发现并调用工具。这里复用的是同一套机制,区别只是工具最终调用了我自己的 Desk Gateway,而不是直接控制小智硬件上的外设。

WebSocket 和 `initialize` 仍用官方 `mcp_pipe.py`,我不重写那一层。自己写的是 stdio 上的 MCP Server:`tools/list` 只报五个名字,`tools/call` 落到固定函数,函数里再打局域网 REST。路径、Method、`X-Desk-Key` 都写死在代码里,模型传不进来。

[desk_mcp.py](https://github.com/dong4j/desk-gateway/blob/main/integrations/xiaozhi-mcp/desk_mcp.py) 核心就是注册这五个工具:

def create_mcp_server() -> FastMCP: client = DeskGatewayClient(DeskGatewayConfig.from_env()) server = FastMCP(“DeskGateway”) @server.tool(name=“desk.get_status”) def get_status() -> dict: return client.get_status() @server.tool(name=“desk.goto_sit”) def goto_sit() -> dict: return client.goto_sit() @server.tool(name=“desk.goto_stand”) def goto_stand() -> dict: return client.goto_stand() @server.tool(name=“desk.raise_to_max”) def raise_to_max() -> dict: return client.raise_to_max() @server.tool(name=“desk.stop”) def stop() -> dict: return client.stop() return server


真正发 HTTP 的是 `desk_gateway_client.py`。工具没有 URL 参数,路径在客户端里写死:

def _request(self, method: str, path: str) -> dict: request = Request( f"{self._config.base_url}{path}", data=b"" if method == “POST” else None, method=method, headers={ “Accept”: “application/json”, “X-Desk-Key”: self._config.api_key, }, ) with urlopen(request, timeout=self._config.timeout_seconds) as response: return json.loads(response.read().decode(“utf-8”)) def goto_sit(self) -> dict: self._status_preflight() self._request(“POST”, “/api/v1/desk/preset/1/goto”) return {“ok”: True, “state”: “started”, “target”: “sit”} def stop(self) -> dict: self._request(“POST”, “/api/v1/desk/stop”) return {“ok”: True, “state”: “stopped”}


坐姿就是档位 1,站姿就是档位 4。运动前会看童锁和 REST 来源开没开;STOP 不做这些检查。`ok` 或 `state=started` 只表示网关接了命令,不表示桌子已经到位。

#### 开放的语音操作

桥接只提供固定动作,不接受大模型生成的 URL、Header 或任意目标高度:

**我说的话****MCP 工具****Desk Gateway 执行的动作**「升降桌现在多高」`desk.get_status`查询当前高度、运动状态和安全状态「把升降桌调到坐姿」`desk.goto_sit`前往档位 1「把升降桌调到站姿」`desk.goto_stand`前往档位 4「把升降桌升到最高」`desk.raise_to_max`在设备侧持续检查 ToF,到达最高安全位置后停止「停止升降桌」`desk.stop`发送固定 STOP 指令

「站姿」和「最高」是两个动作。站姿使用已经配置的档位 4,最高则使用 Desk Gateway 的安全上限。`raise-to-max` 开始前会检查真实 ToF 高度、Driver 能力、童锁和向上阻挡状态;开始运动后,最终停止由 ESP32 本机负责,不依赖小智、MCP 或 Mac 桥接继续在线。

「升高一点」「降低一点」和「调到 75 厘米」暂时不会执行。当前桥接没有开放持续按住升降,也没有把任意高度作为参数交给大模型。语音识别有延迟,紧急情况下仍应使用原厂面板、实体 STOP 或直接断电。

## 番茄钟

坐久了要有人提醒,这块我放在 ESP32 上跑,不依赖浏览器。Web、移动端、D200H 和小智圆屏都只是入口,倒计时、到期和语音都以网关为准。

默认专注 25 分钟、短休息 5 分钟,每完成 4 个专注再进 15 分钟长休息。到期后等我手动开始下一阶段,不会在后台把休息时间偷偷扣掉。提醒只出语音、音效和界面状态,不会到点自动升桌或降桌。设备重启后倒计时回到空闲,时长和音量配置还在,但不会补播上一次没听完的提醒。

语音走 I2S 到 MAX98357A,再推扬声器。WAV 放在独立的 `audio` 分区里,更新语音必须完整烧录,不能只烧 `app`。

**ESP32-S3****MAX98357A****说明**`3V3` 或板背短接后的 `5V``VIN`YD-ESP32-S3 排针 `5V` 出厂不通。不要用桌子 RJ45 红线`GND``GND`与 ESP、USB 电源共地GPIO14`BCLK`I2S Bit ClockGPIO15`LRC` / `WS`I2S Word SelectGPIO16`DIN`ESP 到功放的数据—`SPK+` / `SPK-`扬声器接在两端之间,任意一端都不接 GND

![](https://cdnfile.sspai.com/2026/08/18/21752f6e3ce1971b5d11627b07580fbc.png?imageView2/2/w/1120/q/90/interlace/1/ignore-error/1)

REST 提供开始、暂停、继续、跳过、停止、延后 5 分钟、音量、静音和试听。状态机和接线见[番茄语音提醒](https://github.com/dong4j/desk-gateway/blob/main/docs/architecture/pomodoro-reminder.md)。

## 童锁、自动童锁和禁用原厂面板

升降桌必要的童锁功能也添加上了,裁决放在 ESP32 上,客户端只负责开关。

全局童锁打开后,除了 STOP 和解除童锁,REST、串口、蓝牙和原厂面板都不能启动或维持运动。Web 和手机 App 都能开关。童锁关掉之后,每个来源还要过自己的权限:REST、Bluetooth、Panel 可以分别关掉。关掉某个来源会先停掉当前运动,STOP 始终放行。

这里还添加了一个自动童锁的功能,目的是在我离家后自动打开童锁功能。这个功能需要一部授权手机。这部手机通过加密 BLE 或已认证 REST 大约每 30 秒发一次 Presence。两种通道都断了大约 3 分钟后,固件用 `auto_away` 来源锁上。其他已经配对的手机在线,不会延长倒计时。人回家后,只解除这次自动锁;我自己手动打开的童锁,仍要手动关。

原厂面板可以单独禁用。Web 设置里关掉 Panel,或者 `POST /api/v1/desk/access,{"source": "panel","enabled": false}`。关掉后面板升、降、档位都无效,STOP 还在。重新打开时,如果手指一直按着旧按键,桌子不会立刻动,必须先松开再按。裁决顺序见[架构总览](https://github.com/dong4j/desk-gateway/blob/main/docs/architecture/overview.md)里的童锁与仲裁。

## 升降桌网关的后续设计

### 当前原型的硬件形态

当前原型以 ESP32-S3 开发板为主控。控制盒侧使用 GPIO4 和 GPIO5,原厂面板侧使用 GPIO6 和 GPIO7。TOF050C、TOF400C 和 OLED 共用 GPIO10 和 GPIO11 的外设 I²C 总线。板型见[ESP32-S3 N16R8 选型](https://github.com/dong4j/desk-gateway/blob/main/docs/hardware/esp32-s3-n16r8.md)。

![](https://cdnfile.sspai.com/2026/08/18/6f8efffb13acf3625f5e666b087c85ce.jpg?imageView2/2/w/1120/q/90/interlace/1/ignore-error/1)

Web、手机和旋钮已经分别控制过真实升降桌。小智这块圆屏已经能轮询 Desk Gateway 的实时高度。Web 和 OLED 能显示两颗 ToF 的实时数据。当前硬件仍然使用开发板和临时接线,适合继续调试,还不适合长期安装。

### 多厂商升降桌协议适配

我把不同升降桌的协议实现放在 Desk Driver 中,自己的升降桌使用 `mxtark`。Web、手机、手表和 BLE 只调用统一控制接口,不直接依赖具体桌型协议。

Upsy Desky 的分层方式可以用于后续 Driver 设计。以后会根据真实设备和抓包结果增加 UART、Loctek 或 Jiecang 等协议支持。可插拔 Driver 的边界写在[平台架构设计](https://github.com/dong4j/desk-gateway/blob/main/docs/architecture/platform-design.md)里。

## 硬件清单

这是我这套原型实际用到的东西。当前还是开发板加飞线,不是成品套件。GPIO 和接法以[仓库 README](https://github.com/dong4j/desk-gateway/blob/main/README.zh-CN.md)、[真机验收清单](https://github.com/dong4j/desk-gateway/blob/main/docs/guides/bringup-checklist.md)为准。

以下大部分硬件都是之前玩儿树莓派剩下的,所以整体成本没有超过 100,但是前前后后折腾了 2 周左右,总算是完工了:ESP32 用 USB-C 单独供电,桌子 RJ45 红线 3.3V 只给 CLK/DAT 上拉,不要接到 ESP32 的 `3V3`。

### 网关本体

**数量****型号****用途**1ESP32-S3 N16R8(源地 / VCC-GND)主控。Flash 16MB、PSRAM 8MB1USB-C 线和 5V 电源给 ESP32 供电,日常走板载 USB 转 UART 口1双口 RJ45 模块左口接控制盒,右口接原厂面板2网线控制盒 ↔ 左口,原厂面板 ↔ 右口22 kΩ 电阻(可用 2.2 kΩ)拔掉原厂面板后,补 CLK/DAT 上拉到桌子 3.3V若干杜邦线 / 飞线临时接线

升降桌本身是非智能双电机、原厂 Mxtark 面板。面板协议和线序对不上的,这一套不能直接套。

### 传感器、屏幕和语音

**数量****型号****用途**1TOF400C / VL53L1X桌板朝地,产品高度1TOF050C / VL6180X桌面朝右,低位上升保护10.91 英寸 SSD1306 OLED,128×32本地状态,和两颗 ToF 共用 GPIO10 / GPIO111MAX98357AI2S 功放,GPIO14 / 15 / 161扬声器接模块 `SPK+` / `SPK-`,任意一端都不接 GND

两颗 ToF 的 `SHUT` 还要各占 GPIO12、GPIO13。功放和扬声器的真机音质还没验收。红黄蓝三颗状态灯固件已经接到 GPIO1 / GPIO2 / GPIO8,各串一只限流电阻(红黄 220 Ω~330 Ω,蓝 68 Ω~100 Ω)。

真机观感未验收,不做也可以,不影响控桌。

### 控制端(可选)

网关本身不依赖这些。有了局域网 REST 之后再加:

**数量****型号****用途**1晶彩 JC3636W518C桌上小智终端,固件板卡名 `taiji-pi-s3`1优篮子 D200H实体键:请坐 / 站立 / 番茄1客制化键盘Karabiner 快捷键切坐姿、站姿、停止1桌面旋钮顺时针升、逆时针降、按下停止—已授权手机App 控桌;自动童锁只绑其中一部

GoatRemote 跑在 Mac 上,不另买硬件。

### 调试

**数量****型号****用途**1逻辑分析仪抓原厂 CLK/DAT,只接 GND / CLK / DAT,不接 3.3V1万用表核对线序、上拉电阻和共地

### 完整接线图

这张图是当前原型的完整接线:

![](https://cdnfile.sspai.com/2026/08/18/b655af1713cd717b302bf3fbd912ca74.png?imageView2/2/w/1120/q/90/interlace/1/ignore-error/1)

## 小结

文中提到的固件、手机 App、Watch、脚本和文档都在[这个仓库](https://github.com/dong4j/desk-gateway),顶层目录大致是:

firmware/desk-gateway/ ESP32-S3 主固件(ESP-IDF) mobile/app/ iPhone / Android(React Native + Expo) mobile/watch/ 独立 Apple Watch App integrations/ 小智 MCP、D200H、Karabiner、GoatRemote scripts/ 固件检查、烧录、desk-preset.sh docs/ 接线、协议、REST、当前状态


第三方入口在 [integrations/](https://github.com/dong4j/desk-gateway/blob/main/integrations/README.zh-CN.md),脚本在 [scripts/](https://github.com/dong4j/desk-gateway/tree/main/scripts),都接到同一套局域网 REST,不直接碰厂商协议。接线、GPIO 和烧录以[仓库 README](https://github.com/dong4j/desk-gateway/blob/main/README.zh-CN.md)和[真机验收清单](https://github.com/dong4j/desk-gateway/blob/main/docs/guides/bringup-checklist.md)为准。文档总目录是[docs/README.zh-CN.md](https://github.com/dong4j/desk-gateway/blob/main/docs/README.zh-CN.md)。

桌上那块小智圆屏的空闲界面改在另一份 `xiaozhi-esp32` 工程里,不在这个仓库。

这套东西现在能日常用了,Web、键盘、旋钮、手机、手表、D200H、GoatRemote 和小智发的是同一组升、降、停、档位指令,ESP32-S3 再翻译成控制盒认的键码,而且桌边原厂面板也可控。童锁开着的时候,除了 STOP 和开锁,REST、蓝牙和原厂面板都不能让桌子动。

过程里有两处比较大的弯路:拔掉原厂面板等于拔掉 CLK/DAT 上拉,控制盒读不到键码,补 2 kΩ 到桌子 3.3V 之后升降才通,后来想用软件 I²C 同时应答五个地址,把高度从数码管段码里抠出来,桌子会在运动中自己停掉,最后退回硬件 I²C Slave 只回 `0x24`,高度改外接 ToF。

不过总体来说,目前这个项目已经达到我的预期了,后续有机会再来集成其他协议的升降桌。

![](https://cdnfile.sspai.com/2026/08/18/3895e6c806a9722a352d2d0dc5427501.jpg?imageView2/2/w/1120/q/90/interlace/1/ignore-error/1)

下一步可能就是改我的键盘了,比如接上 iPhone 7 Plus 的线性马达来实现打字震动的效果——目前遇到的问题是并联 3 个线性马达后振动就非常弱,怀疑是电流太小的问题,有哪位大佬可以指点一下呀,感谢 🙏🏻

> 下载[少数派 2.0 客户端](https://sspai.com/page/client)、关注[少数派公众号](https://sspai.com/s/J71e),解锁全新阅读体验 📰

> 实用、好用的[正版软件](https://sspai.com/mall),少数派为你呈现 🚀

[教程](/tag/教程)[智能硬件](/tag/智能硬件)

 本文责编:
 [
 @克莱德
 ](/u/clyde)

 © 本文著作权归作者所有,并授权少数派独家使用,未经少数派许可,不得转载使用。
 

13位派友已充电

文章目录

目录 0

 请绑定手机号码
 

 

 

 

 

 
 
 

 
- [ 发送邮件](mailto:contact@sspai.com)
- [ 商务合作](https://sspai.com/page/bussiness)
- [ 少数派 App](https://sspai.com/page/client)
 

 

 

 

 
 
 

 

请用微信手机端扫码咨询客服
 ![](https://cdn-static.sspai.com/ui/qrcode_service.png)