GitHub:github.com/ershui2500/…
最近我在做 Unitree G1 EDU 的二次开发时,把原本分散的 SDK2 状态、SLAM、点云、相机、关节调试和语音能力整理成了一个浏览器工作台:UniRoboGui。
它不是一个“网页遥控器”,更像是运行在 G1 PC2 上的开发基础设施:
Unitree SDK2 DDS
│
▼
C++17 Backend
Boost.Asio / Beast
│
┌────┴─────┐
HTTP WebSocket
│ │
└────┬──────┘
▼
Browser
项目地址:
这篇主要从开发者视角讲一下它的架构和几个比较关键的工程选择。
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 模型又不适合检查温度、力矩、电压等具体字段。
所以状态页面同时保留:
- 三维姿态;
- 关节表格;
- 搜索和状态过滤。
这是一个典型的“可视化不替代原始数据”的设计。
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
机器人调试页最显眼的东西可能是 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:
18. 最后
UniRoboGui 目前最想解决的问题不是“再做一个机器人 UI”,而是:
让 G1 开发者少重复搭调试工具,更快从“SDK 有没有通”进入真正的机器人应用开发。
如果你在做:
- Unitree G1;
- Unitree SDK2;
- G1 SLAM / Navigation;
- 机器人 Web GUI;
- Humanoid Robot;
- 29DoF 关节调试;
- Robot + LLM;
欢迎看看项目:
也欢迎 Issue / PR,尤其是不同 G1 固件和硬件环境下的兼容性反馈。
注意:行走、导航、关节控制、示教和动作播放都有真实物理副作用。真机测试前需要保证机器人可靠支撑、周围无人,并确认当前状态允许操作。当前 Web 页面没有身份认证,也不应该直接暴露到公网。