{"slug": "building-a-web-development-workbench-for-unitree-g1-with-c-17-sdk2-and", "title": "Building a Web Development Workbench for Unitree G1 with C++17, SDK2 and WebSockets", "summary": "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.", "body_md": "I’ve been building **UniRoboGui**, an open-source browser-based development and debugging workbench for the Unitree G1 EDU.\n\nGitHub:\n\n[https://github.com/ershui2500/UniRoboGui](https://github.com/ershui2500/UniRoboGui)\n\nThe 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.\n\nThat 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.\n\nA robotics SDK can expose every API you need and still leave a lot of integration work to the application developer.\n\nWhile working with the G1, I repeatedly needed small tools for things like:\n\nWriting a one-off tool for each problem works until those “one-off” tools become part of your daily workflow.\n\nSo I started consolidating them.\n\nThe architecture intentionally stays small:\n\n```\nBrowser\n  |\n  | HTTP / WebSocket\n  v\nC++17 web server\nBoost.Asio + Boost.Beast\n  |\n  | Unitree SDK2 DDS\n  v\nUnitree G1 EDU\n```\n\nBy default:\n\n``` php\neth0  -> SDK2 DDS\nwlan0 -> browser / Internet\n8080  -> UniRoboGui web service\n```\n\nI did not add a ROS bridge just to support the browser UI. The robot-facing communication path remains SDK2 DDS.\n\nThe executable composes a few focused services:\n\n```\nUnitreeDataSource\nSnapshotStore\nControlService\nPerceptionService\nCameraService\nVoiceService\nHttpServer\n```\n\nThe implementation lives mostly in:\n\n```\nsrc/unitree_data_source.cpp\nsrc/snapshot_store.cpp\nsrc/json_serializer.cpp\nsrc/http_server.cpp\nsrc/control_service.cpp\nsrc/perception_service.cpp\nsrc/camera_service.cpp\nsrc/voice_service.cpp\n```\n\nThe 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.\n\nThe backend subscribes directly to SDK2 DDS topics including:\n\n```\nrt/lf/lowstate\nrt/lf/bmsstate\nrt/lf/secondary_imu\nrt/lf/mainboardstate\nrt/odommodestate\nrt/sportmodestate\n```\n\nCallbacks update a shared snapshot instead of exposing SDK2 message types directly to the frontend.\n\nThe flow looks like this:\n\n``` php\nDDS subscribers\n      |\n      v\nSnapshotStore\n      |\n      +------> HTTP snapshot/status endpoints\n      |\n      +------> WebSocket telemetry\n```\n\nThis gives the browser a stable application-level representation of the current robot state.\n\nGeneral robot telemetry is pushed through:\n\n```\n/ws/telemetry\n```\n\nThe default rate is 10 Hz.\n\nThe WebSocket session is implemented with Boost.Beast. After each snapshot is written, a timer schedules the next write.\n\nThe main telemetry stream is used for lightweight state such as:\n\nLarge perception data is fetched separately instead of being forced into every telemetry message.\n\nThe frontend uses Three.js and URDF Loader to render the G1.\n\nLive joint data follows this path:\n\n```\nLowState\n   |\n   v\nSnapshot JSON\n   |\n   v\nWebSocket\n   |\n   v\n29 joint mapping\n   |\n   v\nThree.js URDF model\n```\n\nI keep both the 3D view and a numeric joint table.\n\nThe 3D model is great for spotting posture/mapping problems. The table is still necessary for exact values like temperature, torque and velocity.\n\nSending a full raw point cloud to a browser is not always a great idea.\n\nThe perception service can decode PointCloud2 and apply web-oriented filtering:\n\n```\nPointCloud2\n    |\n    v\nfield decoding\n    |\n    v\nrange crop\n    |\n    v\nheight crop\n    |\n    v\nvoxel filtering\n    |\n    v\noptional isolated-voxel removal\n    |\n    v\nmaximum point count\n```\n\nThe web representation is intentionally small:\n\n```\nstruct PointSample {\n  float x;\n  float y;\n  float z;\n  float intensity;\n};\n```\n\nThe 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.\n\nDuring mapping, the backend also maintains an accumulated global map.\n\nInstead of only rendering the latest LiDAR frame, the UI can show:\n\n```\ncurrent point cloud\n+\naccumulated map\n+\nrobot pose\n+\ntrajectory\n+\nnavigation target\n```\n\nA sequence number lets the frontend know when the global map changed and should be fetched again.\n\nFor obstacles, I currently prefer a semi-transparent 2.5D voxel representation over aggressive contour smoothing.\n\nThat 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.\n\nA navigation UI is not just a “send target” button.\n\nThe workflow needs to represent states such as:\n\n```\nidle\nmapping\nlocalizing\nnavigating\npaused\ncancelled\n```\n\nThe current interface supports:\n\nThe backend owns the robot-side transitions; the frontend reflects the current state and available actions.\n\nReal navigation is treated as an explicitly enabled physical capability.\n\nThe camera service supports both librealsense2 and V4L2.\n\nOne lesson from physical robot deployment is that this is fragile:\n\n```\nRGB   = /dev/video0\nDepth = /dev/video2\n```\n\nUSB device numbers can change after re-enumeration.\n\nSo the service can scan the current V4L2 devices and use capabilities/pixel formats to distinguish RGB from Z16 depth input.\n\nManual device paths still exist as overrides.\n\nThe service also detects stale frames instead of indefinitely serving the last successful frame as though the camera were still online.\n\nThe joint-debug page supports upper-body and full-body workflows.\n\nBefore a real command is accepted, the backend can verify conditions such as:\n\nA disabled button is useful UX, but it is not a safety boundary.\n\nThe backend still rejects invalid operations independently.\n\nThere is also an upper-body hand-guided teaching workflow.\n\nAt a high level:\n\n```\nstart recording\n      |\n      v\nmanually guide joints\n      |\n      v\nsample LowState at 20 Hz\n      |\n      v\nsave trajectory\n      |\n      v\nplay it back later\n```\n\nA saved action can either:\n\nLocal actions can also be mapped to reserved G1 controller button combinations.\n\nThis makes simple interactive/demo motions much faster to create than manually authoring every trajectory point.\n\nThe voice service currently handles:\n\n```\nASR\nUnitree native TTS\nlocal Kokoro TTS\nbuilt-in G1 conversation path\ncustomer OpenAI-compatible LLM\n```\n\nThe customer LLM mode supports configuration such as:\n\n```\nAPI URL\nmodel\nrole prompt\nfixed Q&A entries\nwake phrase\nTTS backend\n```\n\nThe API URL is normalized to a Chat Completions endpoint, and the API key is not sent back as normal plaintext telemetry.\n\nThe 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.\n\nThe customer LLM can optionally feed a local Kokoro TTS service:\n\n```\nLLM response\n    |\n    v\nlocal Kokoro HTTP TTS\n    |\n    v\n16 kHz PCM\n    |\n    v\nUnitree AudioClient\n    |\n    v\nG1 speaker\n```\n\nThe robot can therefore use a remote language model while keeping speech synthesis local.\n\nUnitree’s native TTS path remains available as well.\n\nThe built-in web UI is only one possible client.\n\nSome of the current routes are:\n\n```\nGET  /api/health\nGET  /api/snapshot\nWS   /ws/telemetry\n\nGET  /api/control/status\nPOST /api/control/command\nPOST /api/control/velocity\n\nGET  /api/perception/status\nGET  /api/perception/frame\nGET  /api/perception/global-map\nPOST /api/perception/command\n\nGET  /api/camera/status\nPOST /api/camera/command\n\nGET  /api/voice/status\nPOST /api/voice/tts\nPOST /api/voice/llm/chat\n```\n\nThat makes it possible to build another tablet, Electron or custom application UI on top of the same robot-side service.\n\nThe server supports:\n\n```\n--mock\n```\n\nIn mock mode it does not initialize real DDS.\n\nThe mock data source continuously updates simulated robot state, which is enough to exercise large parts of:\n\nThis is not meant to replace a simulator.\n\nIt exists so that a CSS change or frontend state-machine regression does not require a real humanoid robot to move.\n\nIf the G1 can reach GitHub:\n\n```\ngit clone https://github.com/ershui2500/UniRoboGui.git /home/unitree/UniRoboGui\ncd /home/unitree/UniRoboGui\nbash scripts/deploy_g1_online.sh\n```\n\nIf the robot itself cannot reliably reach GitHub/PyPI, an Internet-connected Linux PC can prepare the resources and deploy over SSH/rsync:\n\n```\ngit clone https://github.com/ershui2500/UniRoboGui.git\ncd UniRoboGui\nbash scripts/deploy_g1_from_pc.sh\n```\n\nRobots 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.\n\n```\nRobot:    Unitree G1 EDU\nDoF:      29DoF body preferred\nPC2:      Ubuntu 20.04 AArch64\nSDK:      Unitree SDK2\nLiDAR:    Livox Mid-360 / Mid360s\nCamera:   Intel RealSense D435i\nBrowser:  Chromium / Chrome / Edge\n```\n\nFirmware and hardware combinations vary, so compatibility should always be checked against the actual robot rather than assumed from an old test environment.\n\nGitHub:\n\n[https://github.com/ershui2500/UniRoboGui](https://github.com/ershui2500/UniRoboGui)\n\nIssues:\n\n[https://github.com/ershui2500/UniRoboGui/issues](https://github.com/ershui2500/UniRoboGui/issues)\n\nIf 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.\n\nUniRoboGui is an independent third-party project, not an official Unitree product.\n\nSafety 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.", "url": "https://wpnews.pro/news/building-a-web-development-workbench-for-unitree-g1-with-c-17-sdk2-and", "canonical_source": "https://dev.to/liershui2500/building-a-web-development-workbench-for-unitree-g1-with-c17-sdk2-and-websockets-3og1", "published_at": "2026-09-04 09:10:03+00:00", "updated_at": "2026-09-04 09:24:01.247172+00:00", "lang": "en", "topics": ["developer-tools", "robotics", "ai-infrastructure"], "entities": ["Unitree G1 EDU", "UniRoboGui", "Boost.Asio", "Boost.Beast", "Three.js", "URDF Loader", "SDK2 DDS"], "alternates": {"html": "https://wpnews.pro/news/building-a-web-development-workbench-for-unitree-g1-with-c-17-sdk2-and", "markdown": "https://wpnews.pro/news/building-a-web-development-workbench-for-unitree-g1-with-c-17-sdk2-and.md", "text": "https://wpnews.pro/news/building-a-web-development-workbench-for-unitree-g1-with-c-17-sdk2-and.txt", "jsonld": "https://wpnews.pro/news/building-a-web-development-workbench-for-unitree-g1-with-c-17-sdk2-and.jsonld"}}