我用 C++17 + Unitree SDK2 做了一个 G1 Web 开发工作台:SLAM、WebSocket、D435i相机、机器人控制 与语音交互

0 阅读9分钟

GitHub:github.com/ershui2500/…

最近我在做 Unitree G1 EDU 的二次开发时,把原本分散的 SDK2 状态、SLAM、点云、相机、关节调试和语音能力整理成了一个浏览器工作台:UniRoboGui

它不是一个“网页遥控器”,更像是运行在 G1 PC2 上的开发基础设施:

Unitree SDK2 DDS
      │
      ▼
C++17 Backend
Boost.Asio / Beast
      │
 ┌────┴─────┐
HTTP     WebSocket
 │           │
 └────┬──────┘
      ▼
Browser

项目地址:

github.com/ershui2500/…

Unitree G1 UniRoboGui 综合 Web 开发工作台

这篇主要从开发者视角讲一下它的架构和几个比较关键的工程选择。


1. 问题不是 SDK2 没能力,而是能力太分散

Unitree SDK2 已经提供大量机器人接口。

真正做项目时的问题往往是:为了验证每一条链路,都要再做一层自己的调试工具。

比如:

  • LowState / BMS / IMU 的 DDS Subscriber;
  • FSM 和里程计状态;
  • 29DoF 可视化;
  • Mid-360 PointCloud2;
  • SLAM 建图状态;
  • 地图保存和加载;
  • 导航目标;
  • D435i RGB / Depth;
  • 关节目标控制;
  • 动作示教;
  • ASR / TTS;
  • LLM API。

如果这些东西都以单独 Demo 的形式存在,开发时很快会变成:

5 个终端 + 3 个脚本 + 1 个 RViz + 1 个浏览器

所以我给 UniRoboGui 定了一个比较克制的目标:

不重做 SDK2,只把已经存在的能力整理成一个统一 Web workbench。


2. 为什么后端直接用 C++17?

机器人核心通信本身就在 C++ SDK2 里。

如果为了 Web 再引入一套独立 runtime 和桥接层,会增加:

  • 数据复制;
  • 状态同步;
  • 部署依赖;
  • 现场问题定位复杂度。

所以后端直接使用:

C++17
Unitree SDK2
Boost.Asio
Boost.Beast
JsonCpp

根据模块再可选:

libcurl
OpenCV
librealsense2
ZeroMQ

入口进程里把几个核心服务组合起来:

UnitreeDataSource
VoiceService
ControlService
PerceptionService
CameraService
HttpServer

这样所有机器人侧状态都在同一个后端进程里汇总,浏览器只是客户端。


3. DDS 数据统一进入 SnapshotStore

机器人实时状态主要从 SDK2 DDS 来。

目前直接订阅:

rt/lf/lowstate
rt/lf/bmsstate
rt/lf/secondary_imu
rt/lf/mainboardstate
rt/odommodestate
rt/sportmodestate

Subscriber 收到数据后,不是让每个 HTTP Handler 自己去读 DDS,而是统一更新 SnapshotStore。

因此数据流变成:

DDS Subscriber
      ↓
SnapshotStore
      ↓
Serializer
      ├── GET /api/snapshot
      └── /ws/telemetry

这个结构很普通,但对于机器人 UI 很实用。

浏览器拿到的是统一时间点附近的状态快照,不需要了解底层每个 Topic 的 Subscriber 生命周期。


4. WebSocket 默认 10 Hz 推送遥测

HTTP 适合命令和按需数据,但机器人状态如果让前端不断轮询,会比较别扭。

因此项目提供:

/ws/telemetry

默认 10 Hz 推送 snapshot。

C++ 端直接用 Boost.Beast WebSocket session,写完一次 snapshot 后通过 timer 调度下一次发送。

浏览器因此可以持续更新:

  • DDS online;
  • FSM;
  • BMS;
  • IMU;
  • 里程计;
  • 29 个关节;
  • Control / Voice 相关状态。

而像全局点云这种体积更大的内容则通过独立 API 按序列变化拉取,不把所有重数据都硬塞进主遥测流。


5. 29DoF:数值表和 URDF Viewer 同时存在

只看关节数组,很难快速理解整机姿态。

所以 Web 端使用 Three.js + URDF Loader 显示 G1 模型。

LowState motor state
       ↓
29 joint mapping
       ↓
WebSocket JSON
       ↓
Three.js URDF

另一方面,纯 3D 模型又不适合检查温度、力矩、电压等具体字段。

所以状态页面同时保留:

  • 三维姿态;
  • 关节表格;
  • 搜索和状态过滤。

Unitree G1 SDK2 机器人状态与 29DoF URDF 实时姿态

这是一个典型的“可视化不替代原始数据”的设计。


6. 点云不能直接无脑塞给浏览器

SLAM / LiDAR 是这个项目里数据量比较大的模块。

PerceptionService 会处理 PointCloud2,并提供面向 Web 的过滤:

Decode PointCloud2
       ↓
Range crop
       ↓
Z crop
       ↓
Voxel grid
       ↓
Optional isolated voxel removal
       ↓
Maximum web point cap

点结构只保留浏览器渲染需要的:

x, y, z, intensity

这样可以降低 JSON 和 WebGL 的压力。

实时点云和累计地图也不是一个概念:

  • 实时点云强调当前传感器数据;
  • 累计地图强调建图过程中的全局环境。

建图期间后端持续合并全局点云,并通过 global_sequence 告诉前端什么时候需要刷新 /api/perception/global-map


7. 地图显示为什么做成 2.5D Voxel?

一开始很容易想到“把地图轮廓平滑一下,看起来更漂亮”。

但机器人调试有个问题:视觉上的“干净”不一定是信息上的“准确”。

短墙、桌腿、低矮障碍,如果被轮廓阈值直接过滤掉,页面会让开发者误判环境。

因此现在更倾向于:

  • 保留有效占据 voxel;
  • 轻量去孤点;
  • 蓝色半透明体素;
  • 用高度体现环境层次;
  • 不用“最小总周长”这种规则吞掉小障碍。

它不是 OctoMap,但显示思路更接近占据地图,而不是做一张好看的平面插画。


8. SLAM / 导航在工程上是一个状态机问题

导航 API 本身可能只有几个调用。

但 Web 工具需要管理更多状态:

idle
mapping
map saved
localizing
navigating
paused
cancelled

用户还会做这些操作:

  • 保存地图;
  • 加载地图;
  • 退出地图;
  • 选初始位姿;
  • 选目标;
  • 单点导航;
  • 多点导航;
  • 暂停;
  • 继续;
  • 取消。

所以项目没有把这些直接散落在前端按钮里,而是让后端 PerceptionService 管理机器人侧状态,再由 UI 根据状态决定哪些操作有效。

真实导航默认也不会因为程序启动就自动开放;它需要显式启用。


9. CameraService:不要相信 /dev/videoN 永远不变

RealSense 在机器人上另一个很典型的问题是设备枚举。

如果把设备永久写死为:

RGB   -> /dev/video0
Depth -> /dev/video2

USB 重新枚举后就可能直接失效。

因此 CameraService 支持:

  • librealsense2;
  • V4L2;
  • 自动扫描;
  • 根据像素格式判断 RGB / Z16;
  • 状态 API 返回实际 source;
  • frame stale 检测。

页面中的设备输入仍然保留,但更适合作为手动 override,而不是唯一识别方式。


10. 关节调试:最重要的是互锁,不是 Slider

Unitree G1 29DoF 关节调试与手掰示教 Web GUI

机器人调试页最显眼的东西可能是 Slider,但真正重要的是 Slider 后面的限制。

例如:

  • LowState 必须是新鲜的;
  • FSM 必须在允许范围;
  • DDS Publisher 必须正常;
  • 浏览器控制使用心跳租约;
  • URDF 限位前后端都要检查;
  • 状态不满足时后端拒绝,而不是只让按钮变灰。

换句话说:

UI 安全提示是体验,后端拒绝才是边界。


11. 20 Hz 手掰示教如何进入 Web 工作流

项目支持把实际关节轨迹录下来。

流程:

开始手掰录制
       ↓
20 Hz sample LowState
       ↓
保存 trajectory
       ↓
Web 列表管理
       ↓
play / delete / bind

动作还带有结束语义:

release_control = true

播放后释放;或者:

hold_after_playback = true

播放到最后一帧后保持姿态。

并且可以把本地动作绑定到 G1 遥控器预留组合键。

这使得动作从“开发时的数据”变成了“现场可以重复调用的能力”。


12. VoiceService:内置链路和客户 LLM 共存

VoiceService 里同时处理:

  • ASR;
  • Unitree TTS;
  • 本地 Kokoro;
  • G1 内置对话;
  • Customer LLM。

客户大模型采用 OpenAI-compatible Chat Completions 形式。

配置包含:

api_url
api_key
model
role_prompt
wake_word
qa_entries
tts_backend

Base URL 会规范化到 /chat/completions

API Key 不会通过 telemetry 原样返回,浏览器端只拿“是否已配置”这类状态。

这类细节对 Demo 看起来没区别,但对真正长期运行的机器人应用很重要。


13. 本地 Kokoro 为什么值得单独做?

云端 LLM 不代表 TTS 也一定要走云。

当前项目提供机器人本地 Kokoro TTS 服务:

Customer LLM response
        ↓
Local Kokoro HTTP TTS
        ↓
16 kHz s16le PCM
        ↓
Unitree AudioClient
        ↓
G1 speaker

本地 TTS 的优点主要是:

  • 不绑定外部 TTS 厂商;
  • 网络异常时更可控;
  • 角色和 LLM 服务可以更自由地组合。

同时仍保留 Unitree 原生 TTS 作为另一条链路。


14. Mock 是机器人项目非常值得保留的一层

程序启动参数里有:

--mock

Mock 模式不初始化真实 DDS。

数据源会持续生成模拟状态,因此可以测试:

  • 页面布局;
  • WebSocket;
  • HTTP API;
  • 控制状态机;
  • SLAM UI;
  • Camera UI;
  • Voice UI。

这让很多回归测试不需要让真实机器人承担物理副作用。

如果一个前端按钮的逻辑必须每次都靠真机走一步才能验证,开发成本和风险都会很高。


15. 当前 HTTP 接口

项目提供了一组直接可复用的接口,例如:

GET  /api/health
GET  /api/snapshot
GET  /api/control/status
POST /api/control/command
POST /api/control/velocity

GET  /api/perception/status
GET  /api/perception/frame
GET  /api/perception/global-map
POST /api/perception/command

GET  /api/camera/status
POST /api/camera/command

GET  /api/voice/status
POST /api/voice/tts
POST /api/voice/llm/chat

WS   /ws/telemetry

因此 UniRoboGui 自带页面只是一个客户端。

后续要做平板端、Electron 或自己的业务 UI,也可以继续复用后端。


16. 部署也分在线和离线

G1 自己能访问 GitHub

git clone https://github.com/ershui2500/UniRoboGui.git /home/unitree/UniRoboGui
cd /home/unitree/UniRoboGui
bash scripts/deploy_g1_online.sh

G1 不能访问 GitHub / PyPI

在联网 Linux PC:

git clone https://github.com/ershui2500/UniRoboGui.git
cd UniRoboGui
bash scripts/deploy_g1_from_pc.sh

PC 准备资源,再 SSH / rsync 到机器人。

我觉得机器人部署脚本如果不考虑这种现场网络差异,最后很容易只在作者自己的网络里可用。


17. 项目结构

UniRoboGui/
├── include/
├── src/
│   ├── unitree_data_source.cpp
│   ├── http_server.cpp
│   ├── control_service.cpp
│   ├── perception_service.cpp
│   ├── camera_service.cpp
│   └── voice_service.cpp
├── web/
├── tests/
├── scripts/
├── deploy/
├── docs/
└── CMakeLists.txt

项目 README:

github.com/ershui2500/…


18. 最后

UniRoboGui 目前最想解决的问题不是“再做一个机器人 UI”,而是:

让 G1 开发者少重复搭调试工具,更快从“SDK 有没有通”进入真正的机器人应用开发。

如果你在做:

  • Unitree G1;
  • Unitree SDK2;
  • G1 SLAM / Navigation;
  • 机器人 Web GUI;
  • Humanoid Robot;
  • 29DoF 关节调试;
  • Robot + LLM;

欢迎看看项目:

github.com/ershui2500/…

也欢迎 Issue / PR,尤其是不同 G1 固件和硬件环境下的兼容性反馈。

注意:行走、导航、关节控制、示教和动作播放都有真实物理副作用。真机测试前需要保证机器人可靠支撑、周围无人,并确认当前状态允许操作。当前 Web 页面没有身份认证,也不应该直接暴露到公网。