Building a Web Development Workbench for Unitree G1 with C++17, SDK2 and WebSockets A developer has released UniRoboGui, an open-source browser-based workbench for the Unitree G1 EDU humanoid robot. The tool uses C++17 and the Unitree SDK2 DDS for robot communication, while exposing a web interface via HTTP and WebSocket for inspecting telemetry, joints, point clouds, and more. The architecture avoids a ROS bridge, keeping the robot-facing path direct. 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 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. 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: 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. The architecture intentionally stays small: Browser | | HTTP / WebSocket v C++17 web server Boost.Asio + Boost.Beast | | Unitree SDK2 DDS v Unitree G1 EDU By default: php eth0 - SDK2 DDS wlan0 - browser / Internet 8080 - UniRoboGui web service I did not add a ROS bridge just to support the browser UI. The robot-facing communication path remains SDK2 DDS. The executable composes a few focused services: UnitreeDataSource SnapshotStore ControlService PerceptionService CameraService VoiceService HttpServer 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 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. 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 Callbacks update a shared snapshot instead of exposing SDK2 message types directly to the frontend. The flow looks like this: php DDS subscribers | v SnapshotStore | +------ HTTP snapshot/status endpoints | +------ WebSocket telemetry This gives the browser a stable application-level representation of the current robot state. General robot telemetry is pushed through: /ws/telemetry 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: Large perception data is fetched separately instead of being forced into every telemetry message. 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 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. 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 The web representation is intentionally small: struct PointSample { float x; float y; float z; float intensity; }; 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. 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 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. A navigation UI is not just a “send target” button. The workflow needs to represent states such as: idle mapping localizing navigating paused cancelled The current interface supports: 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. 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 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. The joint-debug page supports upper-body and full-body workflows. Before a real command is accepted, the backend can verify conditions such as: A disabled button is useful UX, but it is not a safety boundary. The backend still rejects invalid operations independently. 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 A saved action can either: 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. The voice service currently handles: ASR Unitree native TTS local Kokoro TTS built-in G1 conversation path customer OpenAI-compatible LLM The customer LLM mode supports configuration such as: API URL model role prompt fixed Q&A entries wake phrase TTS backend 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. 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 The robot can therefore use a remote language model while keeping speech synthesis local. Unitree’s native TTS path remains available as well. 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 That makes it possible to build another tablet, Electron or custom application UI on top of the same robot-side service. The server supports: --mock 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: 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. 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 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 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. 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 Firmware and hardware combinations vary, so compatibility should always be checked against the actual robot rather than assumed from an old test environment. GitHub: https://github.com/ershui2500/UniRoboGui https://github.com/ershui2500/UniRoboGui Issues: 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.