DEV Community

liershui2500
liershui2500

Posted on

Building a Web Development Workbench for Unitree G1 with C++17, SDK2 and WebSockets

I’ve been building UniRoboGui, an open-source browser-based development and debugging workbench for the Unitree G1 EDU.

GitHub:

https://github.com/ershui2500/UniRoboGui

The main idea is simple: keep the robot-facing stack in C++17 on the G1 PC2, communicate directly through Unitree SDK2 DDS, and expose a browser interface through HTTP and WebSocket.

That gives me one place to inspect telemetry, 29DoF joints, URDF, point clouds, SLAM, navigation, RealSense, joint-debugging state and voice/LLM workflows without creating a separate desktop tool for each subsystem.

UniRoboGui Unitree G1 web development workbench

Why I built it

A robotics SDK can expose every API you need and still leave a lot of integration work to the application developer.

While working with the G1, I repeatedly needed small tools for things like:

  • checking fresh LowState data
  • inspecting BMS, IMU, FSM and odometry
  • visualizing all 29 joints
  • checking Livox Mid-360 PointCloud2 data
  • watching SLAM mapping progress
  • setting localization/navigation targets
  • checking RealSense RGB and depth
  • debugging joint control conditions
  • recording upper-body motions
  • testing ASR/TTS
  • connecting an LLM

Writing a one-off tool for each problem works until those “one-off” tools become part of your daily workflow.

So I started consolidating them.

High-level architecture

The architecture intentionally stays small:

Browser
  |
  | HTTP / WebSocket
  v
C++17 web server
Boost.Asio + Boost.Beast
  |
  | Unitree SDK2 DDS
  v
Unitree G1 EDU
Enter fullscreen mode Exit fullscreen mode

By default:

eth0  -> SDK2 DDS
wlan0 -> browser / Internet
8080  -> UniRoboGui web service
Enter fullscreen mode Exit fullscreen mode

I did not add a ROS bridge just to support the browser UI. The robot-facing communication path remains SDK2 DDS.

Backend services

The executable composes a few focused services:

UnitreeDataSource
SnapshotStore
ControlService
PerceptionService
CameraService
VoiceService
HttpServer
Enter fullscreen mode Exit fullscreen mode

The implementation lives mostly in:

src/unitree_data_source.cpp
src/snapshot_store.cpp
src/json_serializer.cpp
src/http_server.cpp
src/control_service.cpp
src/perception_service.cpp
src/camera_service.cpp
src/voice_service.cpp
Enter fullscreen mode Exit fullscreen mode

The goal is not a complicated framework. The separation mainly keeps DDS callbacks, robot state, HTTP handlers, perception and control from turning into one large file.

SDK2 telemetry

The backend subscribes directly to SDK2 DDS topics including:

rt/lf/lowstate
rt/lf/bmsstate
rt/lf/secondary_imu
rt/lf/mainboardstate
rt/odommodestate
rt/sportmodestate
Enter fullscreen mode Exit fullscreen mode

Callbacks update a shared snapshot instead of exposing SDK2 message types directly to the frontend.

The flow looks like this:

DDS subscribers
      |
      v
SnapshotStore
      |
      +------> HTTP snapshot/status endpoints
      |
      +------> WebSocket telemetry
Enter fullscreen mode Exit fullscreen mode

This gives the browser a stable application-level representation of the current robot state.

WebSocket telemetry at 10 Hz

General robot telemetry is pushed through:

/ws/telemetry
Enter fullscreen mode Exit fullscreen mode

The default rate is 10 Hz.

The WebSocket session is implemented with Boost.Beast. After each snapshot is written, a timer schedules the next write.

The main telemetry stream is used for lightweight state such as:

  • DDS status
  • FSM
  • battery state
  • IMUs
  • odometry
  • joint state
  • control status
  • voice status

Large perception data is fetched separately instead of being forced into every telemetry message.

29DoF URDF visualization

The frontend uses Three.js and URDF Loader to render the G1.

Live joint data follows this path:

LowState
   |
   v
Snapshot JSON
   |
   v
WebSocket
   |
   v
29 joint mapping
   |
   v
Three.js URDF model
Enter fullscreen mode Exit fullscreen mode

Unitree G1 live status and 29DoF URDF

I keep both the 3D view and a numeric joint table.

The 3D model is great for spotting posture/mapping problems. The table is still necessary for exact values like temperature, torque and velocity.

PointCloud2 processing for the browser

Sending a full raw point cloud to a browser is not always a great idea.

The perception service can decode PointCloud2 and apply web-oriented filtering:

PointCloud2
    |
    v
field decoding
    |
    v
range crop
    |
    v
height crop
    |
    v
voxel filtering
    |
    v
optional isolated-voxel removal
    |
    v
maximum point count
Enter fullscreen mode Exit fullscreen mode

The web representation is intentionally small:

struct PointSample {
  float x;
  float y;
  float z;
  float intensity;
};
Enter fullscreen mode Exit fullscreen mode

The purpose is not to reproduce every feature of RViz in a browser. It is to make the perception chain easy to inspect from the same development interface.

Accumulated SLAM map

During mapping, the backend also maintains an accumulated global map.

Instead of only rendering the latest LiDAR frame, the UI can show:

current point cloud
+
accumulated map
+
robot pose
+
trajectory
+
navigation target
Enter fullscreen mode Exit fullscreen mode

A sequence number lets the frontend know when the global map changed and should be fetched again.

For obstacles, I currently prefer a semi-transparent 2.5D voxel representation over aggressive contour smoothing.

That choice came from a debugging concern: a visualization should not hide a short wall or small obstacle just because removing it makes the map look cleaner.

Navigation as a state machine

A navigation UI is not just a “send target” button.

The workflow needs to represent states such as:

idle
mapping
localizing
navigating
paused
cancelled
Enter fullscreen mode Exit fullscreen mode

The current interface supports:

  • selecting an initial pose
  • selecting a goal and heading
  • single-goal navigation
  • multi-goal navigation
  • pause
  • resume
  • cancel
  • saved local navigation tasks
  • exiting the current map

The backend owns the robot-side transitions; the frontend reflects the current state and available actions.

Real navigation is treated as an explicitly enabled physical capability.

RealSense D435i without assuming device numbers

The camera service supports both librealsense2 and V4L2.

One lesson from physical robot deployment is that this is fragile:

RGB   = /dev/video0
Depth = /dev/video2
Enter fullscreen mode Exit fullscreen mode

USB device numbers can change after re-enumeration.

So the service can scan the current V4L2 devices and use capabilities/pixel formats to distinguish RGB from Z16 depth input.

Manual device paths still exist as overrides.

The service also detects stale frames instead of indefinitely serving the last successful frame as though the camera were still online.

Joint debugging: backend checks matter more than UI controls

The joint-debug page supports upper-body and full-body workflows.

Unitree G1 joint debugging and motion teaching

Before a real command is accepted, the backend can verify conditions such as:

  • fresh LowState
  • allowed FSM state
  • control/DDS readiness
  • URDF joint limits
  • browser control lease / heartbeat

A disabled button is useful UX, but it is not a safety boundary.

The backend still rejects invalid operations independently.

20 Hz kinesthetic motion recording

There is also an upper-body hand-guided teaching workflow.

At a high level:

start recording
      |
      v
manually guide joints
      |
      v
sample LowState at 20 Hz
      |
      v
save trajectory
      |
      v
play it back later
Enter fullscreen mode Exit fullscreen mode

A saved action can either:

  • release control after playback, or
  • hold the final pose after playback.

Local actions can also be mapped to reserved G1 controller button combinations.

This makes simple interactive/demo motions much faster to create than manually authoring every trajectory point.

Voice and LLM integration

The voice service currently handles:

ASR
Unitree native TTS
local Kokoro TTS
built-in G1 conversation path
customer OpenAI-compatible LLM
Enter fullscreen mode Exit fullscreen mode

The customer LLM mode supports configuration such as:

API URL
model
role prompt
fixed Q&A entries
wake phrase
TTS backend
Enter fullscreen mode Exit fullscreen mode

The API URL is normalized to a Chat Completions endpoint, and the API key is not sent back as normal plaintext telemetry.

The main reason I used an OpenAI-compatible interface is portability. I do not want the rest of the robot application to depend on a single model provider.

Local Kokoro TTS

The customer LLM can optionally feed a local Kokoro TTS service:

LLM response
    |
    v
local Kokoro HTTP TTS
    |
    v
16 kHz PCM
    |
    v
Unitree AudioClient
    |
    v
G1 speaker
Enter fullscreen mode Exit fullscreen mode

The robot can therefore use a remote language model while keeping speech synthesis local.

Unitree’s native TTS path remains available as well.

HTTP API

The built-in web UI is only one possible client.

Some of the current routes are:

GET  /api/health
GET  /api/snapshot
WS   /ws/telemetry

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
Enter fullscreen mode Exit fullscreen mode

That makes it possible to build another tablet, Electron or custom application UI on top of the same robot-side service.

Mock mode

The server supports:

--mock
Enter fullscreen mode Exit fullscreen mode

In mock mode it does not initialize real DDS.

The mock data source continuously updates simulated robot state, which is enough to exercise large parts of:

  • the web UI
  • HTTP endpoints
  • WebSocket telemetry
  • perception UI
  • camera UI
  • control-state behavior
  • voice UI

This is not meant to replace a simulator.

It exists so that a CSS change or frontend state-machine regression does not require a real humanoid robot to move.

Deployment: online and from another PC

If the G1 can reach GitHub:

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

If the robot itself cannot reliably reach GitHub/PyPI, an Internet-connected Linux PC can prepare the resources and deploy over SSH/rsync:

git clone https://github.com/ershui2500/UniRoboGui.git
cd UniRoboGui
bash scripts/deploy_g1_from_pc.sh
Enter fullscreen mode Exit fullscreen mode

Robots often live on much less convenient networks than developer laptops, so I wanted both workflows to be first-class rather than treating offline-ish deployment as an edge case.

Current target environment

Robot:    Unitree G1 EDU
DoF:      29DoF body preferred
PC2:      Ubuntu 20.04 AArch64
SDK:      Unitree SDK2
LiDAR:    Livox Mid-360 / Mid360s
Camera:   Intel RealSense D435i
Browser:  Chromium / Chrome / Edge
Enter fullscreen mode Exit fullscreen mode

Firmware and hardware combinations vary, so compatibility should always be checked against the actual robot rather than assumed from an old test environment.

Repository

GitHub:

https://github.com/ershui2500/UniRoboGui

Issues:

https://github.com/ershui2500/UniRoboGui/issues

If you work with Unitree G1 hardware, I’d be especially interested in feedback about different firmware/hardware combinations and the debugging workflows you still find yourself rebuilding.

UniRoboGui is an independent third-party project, not an official Unitree product.

Safety note: walking, navigation, joint control, kinesthetic teaching and motion playback can cause real physical movement. The current web UI also has no authentication layer, so its control port should not be exposed directly to an untrusted network or the public Internet.

Top comments (0)