Address
Topic grammar
garden/{zone}/{device-type}/{device-id}/{measurement}Use lowercase ASCII, hyphens, and no spaces. A team prefix is two letters; a device ID combines that prefix with two digits. GardenSpine.h reads zone, device type, and device ID from config.h, then adds the measurement passed to publish().
garden/greenhouse-1/climate-node/gm-01/temperature
garden/entrance/counter/gk-01/count
garden/greenhouse-2/ladybug/lb-07/statusBody
Payload
Every accepted message becomes UTF-8 JSON with one measurement and exactly five fields. This is what you see on the dashboard. You never have to write it by hand.
"device_id": "gm-01"Copied once from your credential slip into config.h.
"ts": nullThe helper sends null. The broker records arrival time.
"value": 23.4The changing observation your project produces.
"unit": "celsius"One registered word passed to publish().
"fw": "0.3.1"Read from config.h for debugging.
spine.publish("temperature", reading, "celsius");| Field | Type | Rule |
|---|---|---|
device_id | string | Must equal the device ID segment in the topic. |
ts | string | null | ISO 8601 UTC ending in Z, or null. The broker stamps arrival time when it is null. |
value | number | string | Use a number for measurements and a string for status-like values. |
unit | string | Must exactly match the registered unit. |
fw | string | Free-form firmware version. It is informational. |
Single source of truth
Registry
Zones
greenhouse-1greenhouse-2entrancepondcompostoutdoor-1visitor-centreworkshop
Device types
climate-nodelight-nodemoisture-nodefrost-nodedraft-noderain-nodecompost-nodemotion-nodecounterbench-nodepath-nodepollinator-nodeladybugchorusnight-nodekiosktowersonifierwhispererspine-doctorsleepy-nodesound-nodepond-nodedoor-nodetallyballot-nodeparasol-nodevent-node
Measurements
| Measurement | Unit | Type | Accepted range or values |
|---|---|---|---|
temperature | celsius | number | -40–80 |
humidity | percent-rh | number | 0–100 |
light-level | percent | number | 0–100 |
light-direction | enum | string | left, balanced, right |
soil-moisture | percent | number | 0–100 |
water-level | percent | number | 0–100 |
count | events | number | 0–1000000 |
occupancy | enum | string | free, occupied, uncertain |
duration | seconds | number | 0–86400 |
distance | centimetres | number | 0–500 |
motion | enum | string | still, moving |
tilt | enum | string | level, tilted |
day-length | minutes | number | 0–1440 |
battery | percent | number | 0–100 |
nodes-alive | devices | number | 0–100 |
selection | enum | string | plant-1, plant-2, plant-3, plant-4 |
mood | enum | string | calm, alert, warm, cold, busy |
beat | count | number | 0–1000000000 |
noise | percent | number | 0–100 |
status | enum | string | ok, hiding, low-battery, error, warning, active, sleeping, open, closed, fan-on, fan-off |
Example registration
prefix: gm
zone: greenhouse-1
device type: climate-node
device: gm-01
reads: garden/entrance/counter/gk-01/countCreativity stays open
Free measurements
A team may publish an unregistered measurement under its own device prefix. The backbone stores it and marks it not in registry. It is not rejected. The optional charter measurements happy-votes, open-minutes, comfort, and temperature-delta use this lane. Declare a topic in the registry only when it becomes part of the shared contract.
Never fail silently
Rejected messages
Every rejected message is stored with its topic, payload, reason, and a beginner-readable fix.
| Reason | Hint shown to the student |
|---|---|
payload_not_json | The message body isn't valid JSON. Common cause: single quotes instead of double quotes, or a trailing comma. |
missing_field | Your JSON is missing unit. All five fields are required — see the spec. |
device_id_mismatch | The topic says gm-02 but the payload says gm-01. They must match — you probably copied the config from another node. |
unit_mismatch | You sent unit: "C" but temperature expects celsius. Units are spelled out. |
value_type_wrong | value should be a number for temperature, but arrived as text. Drop the quotes. |
value_out_of_range | 1284 °C is outside the plausible range. Raw sensor value instead of a converted one? |
timestamp_unparseable | ts must be ISO 8601 UTC like 2026-08-14T09:32:00Z, or null if your device has no clock. null is fine. |
topic_malformed | Topic needs five segments: garden/zone/device-type/device-id/measurement. |
zone_not_registered | Zone greenhous-1 isn't in the registry — check the spelling, or open a PR to add it. |
device_type_not_registered | This device type is not registered. Copy it from your charter configuration. |
device_not_registered | This device ID is not registered. Copy it from the credential slip. |
device_registration_mismatch | The topic's zone or device type does not match this device's registry entry. |
unexpected_field | The payload has a field outside the five-field contract. Remove the extra field. |
rate_limited | This device published more than 60 messages in a minute and is being throttled. Slow your loop down. |