Contributing
Branches
| Branch | Purpose |
|---|---|
dev | integration branch, always builds and passes CI; the robot runs a tagged commit of it |
fix/<topic> | bug fixes, small and reviewable, merged first |
feat/<topic> | one feature per branch, with tests and docs |
refactor/<topic>, ci/<topic>, docs/<topic> | no behaviour change (or a documented breaking change) |
Open a pull request into dev; CI (.github/workflows/ci.yml) must be green. Stacked branches (one built on another) say so in the PR description and are merged in that order. Releases are tags vX.Y.Z on dev with an entry in CHANGELOG.md.
Commits follow Conventional Commits: fix(navigation): ..., feat(bridge): ..., refactor(bringup)!: ... plus a BREAKING CHANGE: footer when users have to change something.
Build and test
scripts/dev.sh up && scripts/dev.sh build && scripts/dev.sh test # dev container (macOS/Linux)
colcon build && colcon test --executor sequential && colcon test-result --verbose # on the robot
pip install pre-commit && pre-commit install # same lint as CI, on commitThe lint is the Dharma hub's: ruff (ruff.toml), prettier for YAML and Markdown, clang-format (.pre-commit-config.yaml). Every package runs test/test_style.py (dharma_utils' check_package) and its own tests in colcon test. Tests are split into test/unit (pure, plain pytest on a laptop) and test/ros (in-process nodes with fake data, skipped without rclpy); src/pashupati/test/e2e runs the whole robot on the simulator.
Package layout
Like every Dharma package: core/ is pure Python (no rclpy, no I/O at import), utils/ holds the ROS-side helpers, and the nodes are thin DharmaNodes that read a typed config with declare_config() and run through dharma_utils.ros.run(). Modules stay under 300 lines (tests under 400). Status goes out as the typed dharma_interfaces messages, never parsed from strings.
Rules that come from real crashes
- Never redefine an rclpy
Nodemember (handle,context,executor,_clock, ...) in a node class (self.clientswas one).test/test_style.pyin every package enforces it. - One logging call site per severity. rclpy raises
Logger severity cannot be changed between callswhen the same line logs once asinfoand once aswarn; write two calls instead oflog = logger.warn if bad else logger.info. - Stop cleanly:
main()isdharma_utils.ros.run(NodeClass), which handles Ctrl-C and an external shutdown; a node that starts a thread stops it in a shutdown hook (add_shutdown_hook) and never publishes after it was destroyed. - TF through
self.tf(the DharmaNode's cache, on the node's own executor), never aTransformListenerwithspin_thread=True, which adds the whole node to a second executor. - One BLAS thread per process. pip numpy's OpenBLAS threads busy-wait between the small matrix operations nodes run at 10-50 Hz and burn whole cores;
run_robot.shandscripts/env.shsetOPENBLAS_NUM_THREADS=1. - No per-element Python objects at sensor rate. rclpy builds one object per element of a message array (a Livox CustomMsg has ~20k points): read big messages as numpy straight from the buffer (
PointCloud2.data, orraw=Trueand the CDR bytes, seelivox_cdr.pyin dharma_perception). - Subscribe to heavy topics only while someone uses them (the teleop camera and cloud).
- Validate everything from the network (teleop UDP, NETRA commands, spool files): clamp numbers, reject non-finite values, keep file paths inside their directory, never raise in a callback.
- Parameters are typed config fields (
param(default, min, max, unit, doc)) with the packageconfig/*.yamlas the defaults; the robot's tuned values live insrc/pashupati/config, and one robot is tuned in/etc/pashupati/site.yaml, never by editing the repository on the robot. - The env file (
/etc/pashupati/pashupati.env) is read by both systemd and bash: no comment after a value (systemd makes it part of the value) and quotes around a value with spaces (bash runs the second word as a command).scripts/setup.sh checkflags both. - Signal only
ros2 launch. Stop the stack with one SIGINT to the launch process (systemd does this withKillMode=mixed), not to its process group: launch forwards it to every node, and a second SIGINT interrupts the nodes in the middle of their shutdown.
Safety-relevant changes
Anything that changes how the robot moves (speeds, limits, e-stop, avoidance, the control mux, the teleop gateway) needs a test that drives the logic, a note in the PR on how it was tried on the robot (slow speed, open area, someone at the e-stop), and a CHANGELOG entry. Try it in the simulator first (ros2 launch pashupati sim.launch.py rviz:=true); its end-to-end test (a taught route patrolled with inspections, the handheld taking over through the control mux) runs in CI and must stay green.
