ros 2 packages
Drop into your workspace. Always current.
The packages that ship with this site — hoodly_msgs and hoodly_ros. Download the zip, unzip into your ROS 2 workspace src/, install the Python SDK, and build.
download
hoodly-ros2.zip
16 files · 57.5 KB · rebuilt from ros2/ on every deploy. Full install guide in the ROS 2 docs.
contents
ros2/
16 files
README.md
255 lines · 9.7 KB
| 1 | # Hoodly for ROS 2 |
| 2 | |
| 3 | Give the work your robot does a receipt that holds up. Call an action when a task |
| 4 | finishes, and the evidence is hashed and written into a public transaction on |
| 5 | Robinhood Chain — verifiable by anyone, forever, without an account. |
| 6 | |
| 7 | ```bash |
| 8 | ros2 action send_goal --feedback /hoodly/prove_task hoodly_msgs/action/ProveTask \ |
| 9 | "{title: 'Inspect conveyor B4', evidence_json: '{\"defects_found\": 0}'}" |
| 10 | ``` |
| 11 | |
| 12 | The result hands you a link you can send to a customer, an insurer or an auditor. |
| 13 | They need nothing from you to check it. |
| 14 | |
| 15 | ## Before you start |
| 16 | |
| 17 | You need a Hoodly account and a robot API key. Sign in with a wallet at |
| 18 | [hoodly.fun](https://hoodly.fun), register a robot, and copy the key it shows you |
| 19 | — it is displayed exactly once. Anchoring also needs credit on the account; see |
| 20 | [hoodly.fun/docs/credits](https://hoodly.fun/docs/credits). |
| 21 | |
| 22 | Nothing on the robot needs a wallet, tokens or chain tooling. It speaks HTTPS. |
| 23 | |
| 24 | ## The two packages |
| 25 | |
| 26 | | Package | Build type | What it is | |
| 27 | | --- | --- | --- | |
| 28 | | `hoodly_msgs` | `ament_cmake` | The `ProveTask` action definition | |
| 29 | | `hoodly_ros` | `ament_python` | The node: action server, evidence collectors, offline queue | |
| 30 | |
| 31 | Tested against **ROS 2 Humble** and **Jazzy**. The node is plain `rclpy` with no |
| 32 | distro-specific code. |
| 33 | |
| 34 | ## Install |
| 35 | |
| 36 | The node needs the Hoodly Python SDK in the same interpreter that runs ROS 2. It |
| 37 | is pure standard library, so it pulls in nothing else — no resolver conflicts with |
| 38 | whatever is already on the robot: |
| 39 | |
| 40 | ```bash |
| 41 | pip install hoodly |
| 42 | ``` |
| 43 | |
| 44 | Then download the ROS packages from the site (always the tree that ships with |
| 45 | the current deploy — [hoodly.fun/ros2](https://www.hoodly.fun/ros2)): |
| 46 | |
| 47 | ```bash |
| 48 | cd ~/ros2_ws/src |
| 49 | curl -fsSL https://www.hoodly.fun/api/ros2/download -o hoodly-ros2.zip |
| 50 | unzip hoodly-ros2.zip |
| 51 | |
| 52 | cd ~/ros2_ws |
| 53 | rosdep install --from-paths src --ignore-src -r -y |
| 54 | colcon build --packages-select hoodly_msgs hoodly_ros |
| 55 | source install/setup.bash |
| 56 | ``` |
| 57 | |
| 58 | ## Run |
| 59 | |
| 60 | Put the API key in the environment rather than in a launch argument. Launch |
| 61 | arguments show up in process listings and in `ros2 param` output, and a key that |
| 62 | can spend your anchor quota does not belong there. |
| 63 | |
| 64 | ```bash |
| 65 | export HOODLY_API_KEY=hdly_your_api_key |
| 66 | ros2 launch hoodly_ros hoodly.launch.py |
| 67 | ``` |
| 68 | |
| 69 | Point `params_file` at your own YAML to change anything else: |
| 70 | |
| 71 | ```bash |
| 72 | ros2 launch hoodly_ros hoodly.launch.py params_file:=/etc/hoodly/params.yaml |
| 73 | ``` |
| 74 | |
| 75 | ## Calling it from your own node |
| 76 | |
| 77 | ```python |
| 78 | import json |
| 79 | |
| 80 | from rclpy.action import ActionClient |
| 81 | from rclpy.node import Node |
| 82 | |
| 83 | from hoodly_msgs.action import ProveTask |
| 84 | |
| 85 | |
| 86 | class InspectionRunner(Node): |
| 87 | def __init__(self): |
| 88 | super().__init__("inspection_runner") |
| 89 | self._prove = ActionClient(self, ProveTask, "hoodly/prove_task") |
| 90 | |
| 91 | async def finish_inspection(self, defects: int): |
| 92 | self._prove.wait_for_server() |
| 93 | |
| 94 | goal = ProveTask.Goal() |
| 95 | goal.title = "Inspect conveyor B4" |
| 96 | goal.evidence_json = json.dumps({"defects_found": defects}) |
| 97 | # Do not make the behaviour tree wait on a network round trip. |
| 98 | goal.queue_only = True |
| 99 | |
| 100 | handle = await self._prove.send_goal_async(goal) |
| 101 | result = (await handle.get_result_async()).result |
| 102 | self.get_logger().info(result.message) |
| 103 | ``` |
| 104 | |
| 105 | ### Why an action rather than a service |
| 106 | |
| 107 | Anchoring is a long-running goal with progress and a result. Claiming the task, |
| 108 | broadcasting a transaction and waiting for a block receipt takes seconds at best, |
| 109 | and on a robot that just came back into coverage it can take much longer. A |
| 110 | service call would block your caller with no feedback and no way to see which |
| 111 | stage it is in. |
| 112 | |
| 113 | Feedback reports the current `stage` and the `queue_depth`, so a fleet dashboard |
| 114 | can show a backlog building while a robot sits in a dead zone. |
| 115 | |
| 116 | ## What ends up in the proof |
| 117 | |
| 118 | Your `evidence_json` keys stay at the top level. Everything the node collects on |
| 119 | its own goes under a single `ros` key, so it can never collide with one of your |
| 120 | fields: |
| 121 | |
| 122 | ```json |
| 123 | { |
| 124 | "defects_found": 0, |
| 125 | "hoodly_occurred_at": "2026-08-06T12:41:07.812+00:00", |
| 126 | "ros": { |
| 127 | "pose": { |
| 128 | "frame_id": "map", |
| 129 | "child_frame_id": "base_link", |
| 130 | "position": { "x": 12.418, "y": -3.902, "z": 0.0 }, |
| 131 | "orientation": { "x": 0.0, "y": 0.0, "z": 0.707107, "w": 0.707107 }, |
| 132 | "yaw_deg": 90.0 |
| 133 | }, |
| 134 | "diagnostics": { "worst_level": "OK", "all_ok": true }, |
| 135 | "battery": { "percent": 61.5, "voltage_v": 24.812 }, |
| 136 | "recorded_by": "hoodly_proof" |
| 137 | } |
| 138 | } |
| 139 | ``` |
| 140 | |
| 141 | Pose is looked up from `tf` at the moment the goal is handled. Diagnostics and |
| 142 | battery state come from `/diagnostics` and `/battery_state`. Switch any of them |
| 143 | off per goal with `include_pose`, `include_diagnostics` and `include_battery`. |
| 144 | |
| 145 | Every source is optional at runtime, too: a missing frame, a missing topic or an |
| 146 | interface package that is not installed disables that piece of evidence rather |
| 147 | than failing your proof. |
| 148 | |
| 149 | Three details in that payload are deliberate and worth knowing, because they |
| 150 | affect what the receipt says. |
| 151 | |
| 152 | **Floats are rounded to six decimals.** Raw doubles carry noise far below any |
| 153 | sensor's real precision, and that noise goes into the hash. Without rounding, two |
| 154 | records of the same standstill would differ in the fifteenth decimal and produce |
| 155 | different proof hashes for identical facts. |
| 156 | |
| 157 | **`hoodly_occurred_at` is when the work happened**, not when the proof was |
| 158 | anchored. For a queued proof those are different moments, sometimes hours apart. |
| 159 | The chain timestamps the record; the evidence timestamps the event. |
| 160 | |
| 161 | **Diagnostics are condensed** to the worst level plus the names of anything not |
| 162 | OK. A full dump would add hundreds of lines saying "fine" to every proof, and the |
| 163 | interesting fact is which component was not. |
| 164 | |
| 165 | ## Being offline is the normal case |
| 166 | |
| 167 | Nothing here depends on having a connection when the goal arrives. The evidence is |
| 168 | written to a local SQLite file *before* any network call, and a timer uploads it |
| 169 | when connectivity returns. The file survives a reboot and a power cut. |
| 170 | |
| 171 | For a robot that must never block on the network, set `queue_only`. The goal |
| 172 | returns as soon as the write lands: |
| 173 | |
| 174 | ```bash |
| 175 | ros2 action send_goal /hoodly/prove_task hoodly_msgs/action/ProveTask \ |
| 176 | "{title: 'Waypoint 7 reached', queue_only: true}" |
| 177 | ``` |
| 178 | |
| 179 | The queue is careful about one case in particular: a crash *between* claiming a |
| 180 | task and anchoring it. The task id is saved the moment the claim succeeds, so |
| 181 | resuming finishes that same task instead of claiming a second one. One goal can |
| 182 | only ever produce one receipt. |
| 183 | |
| 184 | A flush also stops early rather than hammering a wall. No connection, no credit or |
| 185 | a rate limit ends the pass, because the next proof in line would hit exactly the |
| 186 | same condition. Order is preserved, so the anchored sequence matches the real one. |
| 187 | |
| 188 | ## When an anchor comes back unconfirmed |
| 189 | |
| 190 | If the transaction was broadcast but no receipt arrived in time, the goal |
| 191 | **succeeds** with `anchor_unconfirmed: true` and a `tx_hash`. |
| 192 | |
| 193 | That is an outcome, not an error, and specifically not a signal to send the goal |
| 194 | again. The proof may still land, and anchoring the same task twice costs real |
| 195 | quota and gas for a duplicate. The node keeps reconciling it on the flush timer |
| 196 | until it knows the answer. Open the `tx_hash` in the block explorer if you want |
| 197 | to watch. |
| 198 | |
| 199 | For the same reason, cancelling a goal is rejected: once a transaction is on the |
| 200 | wire it cannot be recalled, and the evidence is already safely queued. |
| 201 | |
| 202 | ## Parameters |
| 203 | |
| 204 | `config/params.yaml` has all of them with commentary. The ones that matter most: |
| 205 | |
| 206 | | Parameter | Default | Notes | |
| 207 | | --- | --- | --- | |
| 208 | | `api_key` | `""` | Empty reads `HOODLY_API_KEY`. Prefer that. | |
| 209 | | `base_url` | `https://www.hoodly.fun` | Point at staging if you have one. | |
| 210 | | `queue_path` | `~/.local/share/hoodly/queue.db` | Must be writable and persistent — surviving a reboot is the whole point. On a read-only rootfs, move this to the data partition. | |
| 211 | | `flush_interval_s` | `30.0` | One request per pass when the queue is empty. | |
| 212 | | `flush_batch_size` | `10` | Upper bound per pass, so a backlog of hundreds cannot occupy the executor in one go. | |
| 213 | | `request_timeout_s` | `90.0` | Above the server's anchoring budget, so a slow block is not read as a dead connection. | |
| 214 | | `purge_keep_last` | `500` | Anchored queue rows kept for inspection before trim. | |
| 215 | | `map_frame` / `robot_frame` | `map` / `base_link` | Match your stack. | |
| 216 | | `battery_topic` / `diagnostics_topic` | `/battery_state` / `/diagnostics` | | |
| 217 | |
| 218 | ## If something is not working |
| 219 | |
| 220 | **The node refuses to start.** It needs an API key and will not come up without |
| 221 | one, rather than starting and failing every goal. Check `HOODLY_API_KEY` is |
| 222 | exported in the shell that launches it. |
| 223 | |
| 224 | **Goals return "still queued".** The upload could not complete. The log line says |
| 225 | why — `offline`, `insufficient_credit` or `rate_limited`. Nothing is lost; the |
| 226 | next flush retries. Check your credit balance in the dashboard if it says |
| 227 | `insufficient_credit`. |
| 228 | |
| 229 | **No `pose` in the proof.** The transform from `map_frame` to `robot_frame` could |
| 230 | not be resolved, usually because the robot is not localised yet. This is logged at |
| 231 | debug level and left out of the evidence rather than failing the proof. Confirm |
| 232 | with `ros2 run tf2_ros tf2_echo map base_link`. |
| 233 | |
| 234 | **No `battery` or `diagnostics`.** Either the topic is not publishing or |
| 235 | `sensor_msgs` / `diagnostic_msgs` are not installed. The node logs which one at |
| 236 | startup. |
| 237 | |
| 238 | **A goal is rejected immediately.** Goals without a title are refused, since a |
| 239 | receipt with no description of the work is not worth anchoring. |
| 240 | |
| 241 | ## Running the tests |
| 242 | |
| 243 | Evidence assembly is deliberately free of `rclpy`, so the part that decides what a |
| 244 | proof claims can be checked without a ROS installation: |
| 245 | |
| 246 | ```bash |
| 247 | cd ros2/hoodly_ros |
| 248 | python -m pytest test -q |
| 249 | ``` |
| 250 | |
| 251 | ## More |
| 252 | |
| 253 | [Full ROS 2 documentation](https://hoodly.fun/docs/ros2) · |
| 254 | [Python SDK](https://hoodly.fun/docs/sdk) · |
| 255 | [How verification works](https://hoodly.fun/docs/verification) |
quick start
pip install hoodly cd ~/ros2_ws/src curl -fsSL https://www.hoodly.fun/api/ros2/download -o hoodly-ros2.zip unzip hoodly-ros2.zip cd ~/ros2_ws rosdep install --from-paths src --ignore-src -r -y colcon build --packages-select hoodly_msgs hoodly_ros source install/setup.bash