Skip to content

NETRA robot API (v1) ​

Pashupati 3 (on Dharma 0.2) keeps this protocol. Changes against 2.x, all additive except one:

  • Changed: the alert states TF_UNAVAILABLE and AVOIDANCE_STALE are 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 in nav.message (TF_STALE: ..., AVOIDANCE_STALE: ...). New alert state FAILED (the follower gave up).
  • New: nav.state FAILED; health.level (ok, warn, error) and health.speed_scale; the commands dock and undock; site in the route upload meta.
  • mission is lap <n> while a looping patrol from NETRA runs (was the follower's behaviour text), localization is none or the source of the last correction (tag:base_map, ...).
  • New: clock offset. The robot sends time_sync after hello and every 10 s; a server that answers it (see Clocks) gets telemetry.clock {offset_ms, rtt_ms, age_sec}. A server that ignores it keeps working (clock stays 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 ​

typeFields
hellorobot_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_eventevent (started, lap, finished), lap?, reason?, patrol_id, route_id, ts
stop_eventindex, event (arrived, inspected, departed, skipped), message, x, y, yaw_deg, patrol_id, route_id, ts
alertstate (BLOCKED, OFF_PATH, FAILED, ESTOP with the e-stop reasons in message), message, pose, patrol_id, route_id, ts
crowdts_start, ts_end, cell_size (m), cells [[ix, iy, peak_people], ...] (cell = floor(x / cell_size)), peak_people, frames, patrol_id, route_id, ts
ackid, ok, msg
time_synct0 (the robot's clock when sent)

Server → robot ​

{"type": "command", "id": <unique>, "name": <name>, "args": {...}}, answered by one ack.

nameargsRobot behaviour
start_patrolroute_id, patrol_iddownload 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_patrolstop navigation
pause, resumehold / continue navigation
localizelocalize from a visible AprilTag
estoplatch the software e-stop (robot holds still, navigation pauses)
estop_releaserelease it; navigation stays paused until resume
set_inspectionany 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_peopleenabled (bool)person detection on or off (kept across robot restarts)
dock, undockdock onto the charger in view, leave it (docking/dock, docking/undock)

{"type": "time_sync", "t0": &lt;the robot's t0&gt;, "t1": &lt;server time when received&gt;, "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
uididempotency key
categoryrobot's early classification, e.g. trash, spill, floor_damage, fallen_person
confidence0..1
descriptiondetector text
pose{x, y, yaw} | null
evidenceanalysed_frame (the image is the frame the robot classified) or latest_frame (fallback)
stop_indexstop point index when taken at a stop, else null
patrol_id, route_id, tscontext
detectorrobot

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.