STEP 1 · 🖥️ HOST
Server deployment
limit-docs :3000 · limit-dash :3002
→
STEP 2 · 🤖 ROBOT
Robot deployment
limit-robot on the Jetson · aarch64
Server deployment 🖥️ host x86-64
The host (Server2, 192.168.0.83) runs two containers — the docs site and the dashboard. Nothing else: no database, no optional extras.
Before you start
- Two machines on the same L2 network — the host and the robot (Jetson AGX Thor). No internet needed at runtime.
- Docker Engine installed and
sudo-free for the deploy user on both machines. - Images on the host:
limit-docs:1.0,limit-dash:1.0(both x86-64);limit-robot:1.0(aarch64) on the Jetson. - Ports free on the host: 3000 (docs), 3002 (dash), 9000/udp (telemetry in from the robot).
⚠Architecture rule: x86-64 images run only on the host,
limit-robot (aarch64) only on the Jetson. docker load of the wrong image on the wrong machine fails immediately — the only thing that crosses the wire is UDP JSON telemetry (robot → host, port 9000).Air-gapped image transfer
bash · save → transport → load
# 1. on the machine that BUILT the image docker save limit-dash:1.0 -o limit-dash.tar # 2. copy limit-dash.tar to the target (USB stick, sftp, whatever) # 3. on the TARGET machine docker load -i limit-dash.tar docker images | grep limit-dash # confirm tag + arch before you run
Docs site (port 3000)
bash · docs
docker run -d \ --name limit-docs \ -p 3000:80 \ limit-docs:1.0
- Static site — the nginx container is the whole thing. No database, no backend.
- Pages:
index,robot,robot_model,drive, anddeploy— this one.
Limit Dash (port 3002)
bash · dash
docker run -d \ --name limit-dash \ -p 3002:80 \ -p 9000:9000/udp \ limit-dash:1.0
- One container, two parts: nginx serves the UI on
:80(published as 3002) and the C++ relay ingests UDP JSON on:9000, feeding the UI over local SSE on:8000with buffering off. - Relay endpoints:
GET /api/snapshot,GET /api/stream(SSE),GET /api/health. - 27 telemetry panels (T01–T27), 4 status LEDs, and a SIM toggle for no-robot testing.
Verify
curl http://192.168.0.83:3002/— dashboard HTML.curl http://192.168.0.83:3002/api/health— relay reports alive.- Open
http://192.168.0.83:3002— panels are grey/stale until the robot starts; flip SIM and they light up with synthetic data, proving the pipeline.
Day-to-day
| Task | Command |
|---|---|
| follow the logs | docker logs -f limit-dash (or limit-docs) |
| restart a piece | docker restart limit-dash |
| change the dash port | rerun with a free published port (e.g. -p 8443:80) — the relay's internal 9000/8000 split doesn't change |
| stop the host stack | docker stop limit-dash limit-docs |
Robot deployment 🤖 robot aarch64
One container on the Jetson, all ROS 2 Jazzy. It sees the USB cameras, drives the joints, and streams the /limitx/* telemetry to the host over UDP 9000.
What's in the box
| Node | Role |
|---|---|
limitx_gateway | ingests the ROS topics, packs them into the UDP JSON envelope |
limitx_driver | talks to the joint bus, runs the gait state machine |
camera_bridge | grabs ZED 2i frames, encodes per the camera rule below |
telemetry_publisher | stamps, sequences, and publishes the /limitx/* topics |
The contract
- Publishes:
/limitx/imu,/limitx/gnss,/limitx/joints,/limitx/gait,/limitx/cam_left,/limitx/cam_right,/limitx/odometry,/limitx/status. - Subscribes:
/limitx/cmd— gait select, speed, turn, stop. - Camera rule: frame > 36,864 bytes → downscale to ≤ 320×240, JPEG quality 60,
codec=1; otherwise send RGB24 as-is,codec=2. The gateway base64-encodes binary payloads into the same UDP datagram — no second port.
Deploy
bash · robot
# on the Jetson, as the limit user docker run -d \ --name limit-robot \ --network host \ --device /dev/video0 \ --device /dev/video1 \ -v /home/limit/limit-robot/config:/app/config \ limit-robot:1.0
--network hostis required — the joint bus and the UDP egress both want the machine's own interfaces./dev/video*are the ZED 2i UVC streams; the container grabs them as-is.- Config lives at
/home/limit/limit-robot/config; the key file isgateway.yaml:
yaml · gateway.yaml
relay_host: 192.168.0.83 relay_port: 9000
⚠gateway.yaml changes are read at startup only. Edit the host file, then
docker restart limit-robot — editing inside the running container is a no-op.Verify
bash · verify
docker logs -f limit-robot ros2 topic hz /limitx/joints ros2 topic echo /limitx/status
Then on the dashboard: panels T01–T27 light up and the 4 status LEDs reflect the real sensor state — the whole loop working, no sim involved.
⚠IMU: fused from the ZED 2i — if the camera is cold-booted after the IMU, expect ~5 s of no data on
/limitx/imu before lock.⚠GNSS: a F9P in an indoor lab reports "no fix" forever. That's correct behaviour, not a fault — point the antenna at sky and it locks.
Day-to-day
| Task | Command |
|---|---|
| follow the logs | docker logs -f limit-robot |
| restart after a config edit | docker restart limit-robot |
| record a session | ros2 bag record /limitx/* on the Jetson |
| stop the robot | docker stop limit-robot — joints drop to the safe idle pose |
✦Stop order: robot first, then the host stack. The robot is the producer and holds the physical state; the dashboard can drop off any time.