Changelog
All notable changes to the Pashupati robot stack. Format: Keep a Changelog, versions follow SemVer.
[Unreleased]
Added
- Internet check:
teleop_udp_nodepingsinternet_target(8.8.8.8, Google's DNS) through the robot's modem everyinternet_period_sec(5 s). The handheld getsstate.net(online, the round trip, the share of the last 12 pings lost; PROTOCOL.md), the console an Internet line (internet/status, its ownlevel). Offline after two unanswered pings in a row. - Backing up when stuck (Dharma navigation, on): no progress along the route for 30 s, the robot backs up 1 m along the way it came and tries again, 3 times at one place, then it waits. Tuning Guide 2.9, User Manual 1.6.
- The Dharma console (
dharma_console, pinned inpashupati.repos):pconfig/scripts/console.sh menuconfigedits/etc/pashupati/site.yamland the env file through a menu of every node's typed parameters, Dharma's and Pashupati's (console.pyofpashupati_bridge,pashupati_inspection,pashupati_fleet), with where each value comes from, range checks, a.bakand a restart;pmonitor/scripts/console.sh monitorshows the robot live (status, health checks with rates, the computer, warnings, inspection, people) and writes a report file (r,--once). Product filesrc/pashupati/config/console.yaml. Tuning Guide 2.8 (1.2, 8.7), User Manual 1.5. scripts/depth_floor.py: where the depth camera sees the floor, by RANSAC without a window around the nominal floor; prints the heights above the floor by distance, the floor's depth under base_link and the pitch and roll the camera's mount is off its TF (Tuning Guide 5.3).- Low obstacles from the RealSense (Dharma pin,
lidar_sector_nodedepth.*instack.yaml): the D435i's depth point cloud joins both sector scans, so a box, a bag or a low step below the Mid-360 (which sees nothing below ~40 cm within a metre) stops the robot or is passed around. The floor is fitted in every cloud (a few degrees of mount or body pitch make no ghost obstacles; the mount's offset from its TF is logged once a minute), an obstacle needs 4 points in a 5 cm cell and the cloud before it too (no speckles, no one-frame ghosts), and a box the camera lost beside the robot stays in the safety scan for 6 s until the camera sees its place again. A silent camera leaves the scans to the LiDAR, with a warning; health watches/camera/camera/depth/camera_info. Needs the robot package's depth cloud (l1w_bringuprealsense_params.yaml: depth 424 x 240 decimated to 212 x 120, no texture). Off in the simulator. - Detours (Dharma pin): stuck while following (slower than 0.1 m/s for
detour.after_sec, 1.5 s, not near a stop point), the robot turns in place toward the gap that takes it furthest along the route and drives a straight run through it, checked like every command; path shift then brings it back. It ends as soon as the way along the route opens again (a crowd). Walls too close to slide with a box ahead, or a gap it passes only facing it, held it BLOCKED. - Trip meter: Dharma's
odometry_monitor_nodecounts the distance, the moving time, the speed now, the average and the top speed since the last reset (odometry/trip, kept in~/.pashupati/trip.jsonacross restarts). The handheld shows it under the glance panel (state.trip) and a long press resets it (trip_reset,odometry/trip_reset). Needs PashupatiController 2.7. - Documentation site:
docs/site(the kit from the Dharma template, VitePress) builds the User Manual and the Tuning Guide (numbered like their PDFs, with the generated parameter reference), the READMEs, NETRA API and handheld protocol into a site with search and the PDFs, published to GitHub Pages by.github/workflows/docs.yml. The parameter reference moved fromdocs/pdf/build.pytodocs/pdf/params.py(PyYAML only), shared by the PDFs and the site. - One clock for every stamp:
scripts/setup.sh install timesync ptp(scripts/timesync.sh, from the Dharma template). chrony makes the clock followTIME_SERVERS(e.g. NETRA's host) and serves the robot's other computers (TIME_ALLOW);ptp4las PTP master on the LiDAR's interface (the route toLIVOX_IP, orPTP_IFACE) makes the Mid-360 stamp its points with the same clock. Both are in the default install; health watches the offset on the robot (clock.enabledinstack.yaml, off in the simulator). - The offset to NETRA's clock: the uplink sends
time_syncafterhelloand everyclock_sync_period_sec(10 s), NETRA answers with its times, and the offset NTP style goes totelemetry.clock{offset_ms, rtt_ms, age_sec}; a warning aboveclock_warn_ms(200 ms). A server that does not answer yet keeps working (clocknull). See API.md, "Clocks". FakeNetra answers it (--clock-offsetsimulates a server clock that differs). - Docs: the Tuning Guide 2.1 has a time synchronisation section (8.6), the env file rows and two symptoms; the User Manual 1.1 has it for the technician (12.4) and a troubleshooting row. Both PDFs rebuilt.
Changed
- The console takes taps: one line per part in
pmonitor, a tap for the details, buttons instead of key hints; menuconfig selects a line on the first tap and opens it on the second. pashupati.repospins every Dharma repository at its main of 2026-11-15, with the console and the tidy-up merged (dharma_interfaces at its main of 2026-11-14, the same TripStatus).- Tidy-up: one-line docstrings (the details as short comments, usage lines of the scripts and launch files as comments), the PDF builder and the diagrams in smaller functions. No change in behaviour.
- The L1W steps over low things and passes narrower gaps (
stack.yaml, field-tested on the robot):lidar_sector_nodefilter.height_min0.12 (Dharma 0.10), so a 10 cm step, a taped cable or a dip is walked over; 5 cm beside the body instead of 10 for path shift (path_shift.clearance), the motion guard (safety.lateral_margin) and the protective field (field.lateral_margin);obstacle_avoidance_nodeshared.emergency_cone_deg40 (in the 60 deg cone a gap's sides held the robot in front of gaps narrower than about 0.7 m); 144 safety sectors of 2.5 deg (Dharma 72). Gaps from about 0.7 m pass (0.76 m before). A site file copied from the oldsite.example.yamlstill setsheight_min: 0.10: set 0.12 or remove it. Tuning Guide 2.7 (5.1, 5.2, symptoms), User Manual 1.4.
Fixed
- Low obstacles from the RealSense on the L1W (robot package
l1w_ros2): on the Jetson the point cloud parameters arepointcloud__neon_.*(librealsense's ARM block), so the cloud was never published; depth now 640 x 480 decimated to 160 x 120. The D435i's mount, measured from its floor, is 18.3 deg down and 1.85 deg rolled, not the mesh's 10 deg: the floor fit gave up and the floor read as obstacles 0.6 m ahead. Tuning Guide 2.6: the measured mount, the Jetson parameters, USB 3, the symptoms. - Tuning Guide: the "Set it back" warning lost its indentation (the formatter must not touch
docs/manualanddocs/tuning) and printed as plain text. fleet_uplink_nodecould hang right after start and never reach NETRA: its WebSocket and upload threads importedwebsocketandrequestsat the same time, which can deadlock in the import system (six.moves on Ubuntu 22.04's Python 3.10). Both are imported before the threads start.- Dharma pinned to
main 2026-10-30(pashupati.repos): nodes stop cleanly on Ctrl-C (notake_messageRuntimeError, no publication afterdestroy_node),planner/go_towaits up topose_wait_sec(2 s) for the robot pose right after localization, dharma_rviz builds with GCC 15. Later on the same day:navigation/startwaits for the pose too, and a node still alive 8 s after a stop signal prints every thread's stack and exits with 1 instead of being SIGKILLed without a trace. - The end-to-end patrol test waits for the ARRIVED of its own goal: a late ARRIVED of the previous goal on a busy machine marked a stop point at the wrong place and the patrol then inspected only two of the three.
- Field fixes on the L1W (2026-11-14):
- Odometry, its TF and the clouds were ~1.4 s old (safety scan blind, protective field every sector blocked): FAST-LIO took the IMU reliable with a queue of 10. A patch in
src/xtras/patchestakes it best effort;scripts/setup.sh install buildapplies it. - CPU, GPU and RAM never reached the handheld or NETRA: the launches'
diagnosticsremap also moved/diagnostics. The driver topic is a parameter (driver_diagnostics_topic). - RTSP camera: frame rate and scaling inside the pipeline, frames to ROS without a per-byte copy; the person detector takes
camera_infowith sensor QoS. - Perception as in 2.0 (fixed height band, no obstacle memory, front scan 180°), no on-line extrinsic estimation in FAST-LIO, odometry tilt check at 9°: the robot stopped and weaved around ghost obstacles.
- Path shift (Dharma pin): a pole in the emergency cone held the robot for good with room beside it; the emergency now stops only the forward motion and it slides out (
path_shift.emergency_slide). In front of a person or a box it waitspath_shift.patience_sec(3 s), then searches a way up topath_shift.search_offset(1.5 m) instead of waiting until Blocked. - Path shift (Dharma pin): with something beside it (a person, a bench) and no offset with the full clearance, the robot waited for it to go away with nothing ahead; it now drives on parallel along its own line (points beside only count when they touch the body, as for the protective field) and returns to the route behind them. A narrow lane is driven in its middle (
path_shift.centre_room, 0.25 m): a corridor 0.80 m wide whose middle was 5 cm off the route stopped the robot at its mouth.
- Odometry, its TF and the clouds were ~1.4 s old (safety scan blind, protective field every sector blocked): FAST-LIO took the IMU reliable with a queue of 10. A patch in
[3.0.0] - 2026-10-29
The same patrol robot on Dharma 0.2 (main 2026-10-28, pinned in pashupati.repos).
Changed
- Breaking: the Dharma 0.2 interfaces:
control_mux_nodeis the only publisher of/<robot_ns>/cmd_vel; the handheld drives throughcontrol/remote/handheld/cmd_vel; services arecontrol/set_mode(mode and source),control/estop/engage|release,navigation/start|stop|pause|resume,navigation/load_path(pathinside the sites dir),route/start|stop|mark_stop_point,planner/go_to; status topics are typed messages (ControlStatus,NavigationStatus,LocalizationStatus,HealthStatus,SiteStatus,StopPointEvent,DockingStatus). The oldbridge/*services and the string status topics are gone. - Breaking: sites replace
map/: a site holds its occupancy map, tags and taught routes underSITES_DIR(~/.pashupati/sites).ROUTEandTAGSare gone frompashupati.env;SITEandSITES_DIRreplace them.mode:=teachmaps the site from the start. - Breaking:
ROBOT_BRINGUPisROBOT_DRIVER; the robot's footprint, speed limits and base height are the robot profile (robot.*,src/pashupati/config/robot.yaml,ROBOT_PARAMS) instead offield.*and per-node limits. - Breaking: the configs are
config/robot.yamlandconfig/stack.yaml(one file for the whole stack, the values tuned on the robot carried over) andbridge.yaml,inspection.yaml,fleet.yamlin their packages; the old*_params.yamlnames are gone. A site file keyed by node name still goes on top of everything. - Breaking:
script/isscripts/. - Every Pashupati node is a
DharmaNodewith a typed config (declare_config), a purecore/and its ROS helpers inutils/; lint is the hub's (ruff, prettier, clang-format, pre-commit). - The inspection after a stop point is Dharma's follower hook (
hooks.inspection); a disabled inspection answers at once and never holds the patrol. The back-up after an inspection isinspection.reverse_distance_m(0: off). - Handheld protocol v1 and the NETRA API v1 keep their wire format; the differences are listed at the top of PROTOCOL.md and API.md (
TF_UNAVAILABLEandAVOIDANCE_STALEalerts are now innav.message).
Added
pashupati_utils: typed status to protocol names, health figures, launch helpers.- Camera source:
CAMERA=rtspandCAMERA_URLstartrtsp_camera_nodefor an IP camera. - Docking:
dockandundockfrom the handheld and NETRA (Dharma'sdocking_node, the robot driver's dock services). - Patrol goals with the planner (
planner/go_to) on the site's map. - Dev container from the hub template (Humble, VNC):
compose.yaml,.devcontainer/,scripts/dev.sh,scripts/start_tmux.sh robot|sim. fake_netra(ros2 run pashupati_fleet fake_netra): a local NETRA for the simulator and tests.- End-to-end test on the simulator: localize, teach a route with three stop points, patrol it with the VLM inspection (stand-in Ollama), hand over to the handheld and back, report to NETRA.
- CI: pre-commit lint and the whole stack from
pashupati.reposon Humble.
Removed
docker-compose.yamlanddocker/README.md(nowcompose.yamland the README).map/example(routes and tags belong to a site now).
[2.0.0] - 2026-10-18
Same patrol as 1.0.0, now on the Dharma stack.
Changed
- Breaking: odometry, localization, navigation, LiDAR perception, the control mux, the health monitor, the RViz panels and the simulator come from the
dharma_*packages, pinned inpashupati.repos(script/setup.sh install dharma, orvcs import src < pashupati.repos). - Breaking: the remaining packages are
pashupati,pashupati_bridge,pashupati_fleetandpashupati_inspection; person detection is part ofpashupati_inspection. - The configs tuned on the robot live in
src/pashupati/config; the simulation isros2 launch pashupati sim.launch.pyand its end-to-end test lives inpashupati.
Added
script/setup.shcomponentdharma.
[1.0.0] - 2026-10-18
First stable release, field-tested at Lippo Mall Puri on a Genisom L1W.
Added
Handheld video over WebRTC:
robot.launch.pystarts MediaMTX (script/setup.sh install webrtc), which runs the newvideo_stream_nodewhile the handheld watches. H.264 from the Jetson hardware encoder (x264 or OpenH264 where there is none), read with the teleop token. Thewelcomeoffers it asvideo: {port, path}(WHEP); apps without WebRTC and robots without MediaMTX keep the JPEG frames.The handheld video shows the AprilTags the detector sees:
state.tagscarries their corners as fractions of the image (protocol v1, an extra field older apps ignore).Person boxes on the handheld video:
perception/people_statusandstate.people.boxescarry them as fractions of the image.rate_hz: 0(new default) detects 5 times a second on the GPU, 2 on the CPU.Own finding types from the handheld: add a type with a description of what to look for, choose whether it is reported, edit or remove it (
categories,alarm_categoriesand the newcategory_descriptionsare live and kept across restarts). Intervals below a second.Interval inspection queues its frames: every
interval_seca frame is taken even while the model is busy and waits in a first-in-first-out queue (kept across restarts,queue_max50) that the model works through when free. Findings keep the time and pose of the frame.Guides in
docs/, as A5 PDFs for reading on a phone: a User Manual for operators (Genisom L1W with the Pashupati Controller, real app screenshots) and a Tuning Guide for technicians (every parameter, the site file, measuring the braking, symptoms and fixes, and a parameter reference generated from the config files).docs/pdf/build.pyrebuilds them; CI builds them too.
Changed
- REMOTE drives without the protective field (
field_in_remote: false): the operator watches the robot; the software e-stop and the speed envelope still apply. The simulator keeps it on.
Fixed
- The robot ran out of RAM with inspection on (15.1 of 15.3 GB used, swapping): Ollama's llama-server kept up to 8 GiB of earlier prompts in RAM, useless when every prompt has a new image.
script/setup.sh install ollamaturns that cache off for the Ollama service. The detector also unloads its model when switched off or stopped instead of keeping it forever. - The AprilTag detector got no camera_info from
apriltag_throttle(best effort, the detector reads it reliable), so no tag was ever detected: the throttle publishes reliable. ros2 topic liststayed empty in a robot shell afterscript/env.sh: a ros2 daemon started from another environment kept its view;env.shrestarts it.- Teleop camera stayed black: driver JPEGs (quality 95) were too big to arrive whole as one burst of datagrams. Frames above
camera_max_bytesare re-encoded atcamera_jpeg_qualityandcamera_max_width, the send buffer is larger, and a frame stops at its first dropped chunk. - Lighter on the robot: the AprilTag detector gets the camera at 5 Hz (
apriltag_throttle) instead of every frame;lidar_sector_nodestops listening to TF once it knows the LiDAR mount,route_recorder_nodeonly listens while recording, andfleet_uplink_nodeonly runs whenCC_URLis set. The health monitor watches the camera oncamera_info: its subscription to the JPEG topic made the camera driver encode every frame all the time. lidar_sector_nodewithinput_type: livoxran at ~6 Hz on a full Jetson core, because rclpy built a Python object for each of the ~20k points of a scan. It now reads the serialized CustomMsg straight into numpy (~1 ms per scan) and drops points with no return.- Python nodes burned whole cores (
localization_node200 %): pip numpy's OpenBLAS threads busy-wait. One BLAS thread per process (OPENBLAS_NUM_THREADS=1inrun_robot.sh, the env file andscript/env.sh); TF listeners run on the node's own executor. - Teleop: a handheld with a wrong token got no answer at all, now
denied. Acks are per session: an app restarted with command ids from 1 got the previous session's ack and the command never ran. Camera, cloud and TF are subscribed only while a client wants them, and a reader thread replaces the 100 Hz polling timer (~1/3 of a core while nobody was connected). - Fleet uplink and teleop crashed on a driver
DiagnosticStatus(rclpy giveslevelas bytes). - Obstacle markers in RViz blinked with "No transform": they are drawn on the latest TF.
run_robot.shpassed empty launch arguments (route:=), whichros2 launchrejects;robot.launch.pyreads them from the environment.start_tmux.shpassed emptyserver_url:=/token:=and did not use the service's RMW and domain.- The service unit started Ollama even with
INSPECTION=false; re-runscript/setup.sh install service. - Inspection retried silently when the model was missing; it now logs
ollama pull <model>, or that Ollama is unreachable. - The control mux forwarded NaN / infinite velocities (the protective field computed a NaN scale and let them through); they are now dropped as zero, and every command is capped at the robot's speed envelope (
max_vx,max_vy,max_wz). path_follower_nodecrashed on its first FOLLOWING → BLOCKED / OFF_PATH / EMERGENCY_STOP change (rclpy refuses to change a log call site's severity); the respawned node came back with navigation inactive.teleop_udp_nodeandfleet_uplink_nodecrashed at start (handle/contextshadowed rclpy Node members); all nodes stop cleanly on Ctrl-C / SIGTERM.- Teleop: NaN / inf / non-numeric joystick values were clamped to full speed, now 0; malformed datagrams no longer raise; one driver at a time (
busy); token checked in constant time;load_routerestricted toroutes_dir. - Fleet uplink: a bad spool item no longer kills the uploader (quarantined to
.failed/); route ids from the server cannot escaperoutes_dir. localization_nodecrash-looped when the tag file did not exist yet.- Route CSV rows with NaN, infinity or text are skipped and logged with their line number instead of loading waypoints no controller can follow; a negative dwell is no stop.
apriltag_nodeaborted at start and respawned forever:detector.refine/detector.debugwere integers,apriltag_rosdeclares them as booleans.rosdep installfailed onrobot_localization(ament_pythonis not a rosdep key).- Low-battery flag never cleared after charging; inspection answers could be reported for the wrong stop.
- The anomaly photo sent to NETRA is now the frame the VLM analysed (
anomaly_detector/evidence). - No traceback from the TF listener thread when nodes stop.
map → odomis a dynamic transform on/tf(REP-105), republished atmap_odom_hzand after every relocalization. As a static transform it only worked on Humble: from Jazzy on, the static broadcaster keeps the first value, so relocalizing had no effect.- A Ctrl-C that lands while a node builds its wait set no longer ends it with an
RCLErrortraceback (exit 1); the teleop gateway frees its UDP port as soon as it stops. - A stop whose inspection services are all down now reports
inspectedwithnot availableright away; before, no inspection event was sent for it at all. systemctl stop/ restart interrupted every node twice (systemd andros2 launchboth sent SIGINT), so nodes died mid-shutdown with aKeyboardInterrupt. The service now signals onlyros2 launch(KillMode=mixed); re-runscript/setup.sh install serviceto update the unit.
Added
script/env.sh: the service's environment in a shell (same RMW, domain and robot settings), andpnodeto run one node with the package defaults and the site file.script/setup.shreads/etc/pashupati/pashupati.envitself and has aconfigcomponent that creates the env and site files, adds keys that are new in the example, and flags unquoted values with spaces.checkreports an outdated service unit and a build older than the checkout, skips Ollama and the person model when they are disabled, and (Jetson) blacklistsuvcvideoso the RealSense is not bound twice.BUILD_WORKERS/BUILD_JOBSlimit the build.- Teleop
welcomecarries the robotnameand itsrobot_commands(the handheld's action menu). - Teleop LAN discovery (
discover→beaconwith name, token required, who is driving, mode, battery), aseqnumber on everystatefor packet-loss estimates, and a camera rate the handheld can lower (camera_fps, capped by the parameter). robot_odometry: tuned FAST-LIO config, stillness gate before IMU initialisation (also on respawn), odometry monitor (odometry/status,odometry/restarted), optional Leg-KILO, route recorder (route/record,route/mark_stop_point,route/saved).- Site configuration
/etc/pashupati/site.yamlapplied on top of every node's defaults. - Latched software e-stop in the control mux (
bridge/estop, handheldestop/emergency_stop, NETRAestop),ESTOPalert to NETRA; the mux also zeroes the robot when navigation goes silent. - Protective field (speed and separation monitoring) in the control mux: every command, navigation or handheld, in any direction, is limited to a speed that stops before the nearest obstacle in the swept path; 360°
perception/safety_scanwith LiDAR-blind detection. - Health monitor: missing nodes (the launch passes the list it starts), lost odometry, stale safety scan and e-stop reported as critical (
level: error) next to sensor rates and the computer. - CI: build, unit + in-process ROS tests, lint (flake8, pep257, shellcheck, YAML) on every PR.
odometry:=external: no estimator is started,/Odometrycomes from elsewhere (the simulator); the odometry monitor and route recorder still run.robot_sim: the production stack on a simulated quadruped (2D world, ray-cast LiDAR, the driver and FAST-LIO interfaces, people dropped in at runtime, stand-in inspection), and an end-to-end CI test: patrol through a stop, stop for a person, drive on, the handheld cannot drive into a person, clean shutdown.
Changed
- Person detection and VLM inspection are off in the env example (
PEOPLE=false,INSPECTION=false), which also setsRMW_IMPLEMENTATION=rmw_fastrtps_cppandROS_LOCALHOST_ONLY=1(nothing on the robot's Wi-Fi can publish to the driver; 0 for RViz on a laptop or a driver on another computer). - The RealSense driver comes from apt (
ros-humble-realsense2-camera) instead of therealsense-rossubmodule, which is removed.script/setup.sh install realsenseinstalls it and the udev rules (from the package when it ships them, so it also works offline); the component replacesrealsense_udev. - Following uses x, y and yaw. The new default
holonomiccontroller does three things:- It walks toward a carrot on the route in any direction of the body: cross-track errors close sideways without weaving, and the robot backs up or strafes when the route is behind or beside it (after a stop, when starting off the route).
- It keeps its heading along the route.
- It caps sideways and reverse speed.
- Obstacles: path shift. Obstacles on the route are passed by sliding sideways, within
path_shift.max_offset(0.8 m) of the route and on the 360° safety scan. The robot returns once the obstacle is past its body, and stops short when there is no gap. - Reactive avoidance (
avoidance_mode: reactive, other controllers) now reacts within 1.0 m instead of 1.5 m. - Breaking:
pashupati_bringup→pashupati(ros2 launch pashupati robot.launch.py),robot_mappingmerged intorobot_odometry,mapping/*services →route/*, modemapping→teach(old name still accepted). - Breaking: robot-agnostic. Pashupati no longer ships a robot driver: any quadruped plugs in through a robot package (
ROBOT_BRINGUP/robot_bringup:=<package>[/<file>.launch.py], started withnamespace:=<robot_ns>), whose contract is insrc/xtras/README.md. Thegenisom_l1w_ros2submodule is removed,sensors:=is replaced byrobot_bringup:=, the defaultrobot_nsisrobot(setROBOT_NSto keep another one), the handheld's default robot commands arestand_up,sit_down,emergency_stop(extras viarobot_commands), the vendor-autostart stop becameROBOT_PRESTART, andscript/setup.sh install robotruns the robot package's own installer (ROBOT_SETUP). Health topics may use{robot_ns}.
