NETRA robot API (v1)
Pashupati 3 (on Dharma 0.2) keeps this protocol. Changes against 2.x, all additive except one:
- Changed: the
alertstatesTF_UNAVAILABLEandAVOIDANCE_STALEare no longer sent. On Dharma 0.2 the follower holds the robot for a stale pose or a stale avoidance without leaving its state; the reason is innav.message(TF_STALE: ...,AVOIDANCE_STALE: ...). New alert stateFAILED(the follower gave up). - New:
nav.stateFAILED;health.level(ok,warn,error) andhealth.speed_scale; the commandsdockandundock;sitein the route upload meta. missionislap <n>while a looping patrol from NETRA runs (was the follower's behaviour text),localizationisnoneor the source of the last correction (tag:base_map, ...).- New: clock offset. The robot sends
time_syncafterhelloand every 10 s; a server that answers it (see Clocks) getstelemetry.clock{offset_ms, rtt_ms, age_sec}. A server that ignores it keeps working (clockstays null).
Contract between a robot (fleet_uplink_node) and the NETRA command center. Every request carries Authorization: Bearer <robot token> and X-Robot-Id: <robot_id>; the server maps the token to a robot and rejects mismatches. All times are Unix seconds (float), positions are metres in the robot's map frame, yaw in radians unless the field says _deg.
WebSocket GET /api/v1/robot/ws
One connection per robot, JSON text frames. The robot reconnects with backoff; events produced while offline are replayed after hello (telemetry is not).
Robot → server
type | Fields |
|---|---|
hello | robot_id, version |
telemetry (1 Hz) | ts, pose {x, y, yaw} | null, mode (auto/remote), estop (bool), field (protective field state), nav {state (NAV_INACTIVE, JOINING, FOLLOWING, APPROACHING, DWELLING, PAUSED, BLOCKED, OFF_PATH, GOAL_REACHED, FAILED), message}, mission, localization, battery {pct, v, a, temp} | null, driver {level, msg} | null, people (count in view), inspection / people_detection {enabled, state (loading, running, off, error), error, model?, interval_sec?, count?, backend?} | null (the detectors' status; null when not running), health {ok, level, issues, speed_scale, topics ({topic: Hz}), cpu_temp_c, memory_percent, memory_used_gb, memory_total_gb, cpu_percent, gpu_percent, disk_free_gb, load} | null, patrol {patrol_id, route_id} | null, clock {offset_ms (the server's clock minus the robot's), rtt_ms, age_sec} | null (no answered time_sync in the last 30 s) |
patrol_event | event (started, lap, finished), lap?, reason?, patrol_id, route_id, ts |
stop_event | index, event (arrived, inspected, departed, skipped), message, x, y, yaw_deg, patrol_id, route_id, ts |
alert | state (BLOCKED, OFF_PATH, FAILED, ESTOP with the e-stop reasons in message), message, pose, patrol_id, route_id, ts |
crowd | ts_start, ts_end, cell_size (m), cells [[ix, iy, peak_people], ...] (cell = floor(x / cell_size)), peak_people, frames, patrol_id, route_id, ts |
ack | id, ok, msg |
time_sync | t0 (the robot's clock when sent) |
Server → robot
{"type": "command", "id": <unique>, "name": <name>, "args": {...}}, answered by one ack.
name | args | Robot behaviour |
|---|---|---|
start_patrol | route_id, patrol_id | download the route file into the active site (<site>/netra/<route_id>.csv; refused without an active site), load it, start navigation, emit patrol_event started |
stop_patrol | stop navigation | |
pause, resume | hold / continue navigation | |
localize | localize from a visible AprilTag | |
estop | latch the software e-stop (robot holds still, navigation pauses) | |
estop_release | release it; navigation stays paused until resume | |
set_inspection | any of enabled (bool), model (Ollama model), interval (s: 0 continuous, > 0 every N s, < 0 stop points only) | VLM inspection live (kept across robot restarts) |
set_people | enabled (bool) | person detection on or off (kept across robot restarts) |
dock, undock | dock onto the charger in view, leave it (docking/dock, docking/undock) |
{"type": "time_sync", "t0": <the robot's t0>, "t1": <server time when received>, "t2": <server time when sent>} answers a time_sync (not an ack).
Clocks
Every ts is the robot's clock. Robots synchronise it with chrony (scripts/setup.sh install timesync, TIME_SERVERS = the server, or the same NTP pool as the server), so the offset stays in milliseconds. The robot also measures it, NTP style: it sends time_sync {t0}; the server answers at once with {t0, t1, t2} from its own clock; the robot notes t3 on arrival and computes offset = ((t1 - t0) + (t2 - t3)) / 2 and round trip = (t3 - t0) - (t2 - t1). The shortest round trip of the last 8 answers wins. A server can place a robot event on its own clock with ts + clock.offset_ms / 1000, and should flag a robot whose |offset| is large (the robot logs a warning above clock_warn_ms, 200 ms).
HTTP uploads (multipart/form-data)
Field meta holds a JSON object with a client-generated uid; uploads are retried until the server answers 2xx, so the server must treat a known uid as success (2xx or 409).
POST /api/v1/robot/anomalies
Files: image.jpg. meta:
| Field | |
|---|---|
uid | idempotency key |
category | robot's early classification, e.g. trash, spill, floor_damage, fallen_person |
confidence | 0..1 |
description | detector text |
pose | {x, y, yaw} | null |
evidence | analysed_frame (the image is the frame the robot classified) or latest_frame (fallback) |
stop_index | stop point index when taken at a stop, else null |
patrol_id, route_id, ts | context |
detector | robot |
POST /api/v1/robot/routes
Files: route.csv (x,y,yaw_deg,dwell_sec rows). meta: uid, name, recorded_at, source_path, site, length_m, points, stops, closed.
GET /api/v1/robot/routes/{route_id}/file
Returns the route CSV for start_patrol.
Anomaly detector input
fleet_uplink_node listens to anomaly_topic (std_msgs/String, JSON). Accepted shapes: {"is_anomaly"|"anomaly"|"detected": bool, "category"|"label"|"class"|"type": str, "confidence"|"score": float, "description"|"reason": str}. Results with no anomaly, category none/normal, or confidence below anomaly_min_confidence are ignored; the latest frame from anomaly_image_topic (≤ 3 s old) is attached.
