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-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 |
status | enum | string | ok, hiding, low-battery, error, warning, active, sleeping |
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. Declare the topic in the registry 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. |