Canonical one-page contract

Publish one measurement.

Every device follows the same topic grammar and five-field JSON payload. We mandate what gets published. You own how.

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/status

Body

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.

The line you writespine.publish("temperature", reading, "celsius");
FieldTypeRule
device_idstringMust equal the device ID segment in the topic.
tsstring | nullISO 8601 UTC ending in Z, or null. The broker stamps arrival time when it is null.
valuenumber | stringUse a number for measurements and a string for status-like values.
unitstringMust exactly match the registered unit.
fwstringFree-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

MeasurementUnitTypeAccepted range or values
temperaturecelsiusnumber-40–80
humiditypercent-rhnumber0–100
light-levelpercentnumber0–100
light-directionenumstringleft, balanced, right
soil-moisturepercentnumber0–100
water-levelpercentnumber0–100
counteventsnumber0–1000000
occupancyenumstringfree, occupied, uncertain
durationsecondsnumber0–86400
distancecentimetresnumber0–500
motionenumstringstill, moving
tiltenumstringlevel, tilted
day-lengthminutesnumber0–1440
batterypercentnumber0–100
nodes-alivedevicesnumber0–100
selectionenumstringplant-1, plant-2, plant-3, plant-4
moodenumstringcalm, alert, warm, cold, busy
beatcountnumber0–1000000000
noisepercentnumber0–100
statusenumstringok, 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/count

Creativity 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.

ReasonHint shown to the student
payload_not_jsonThe message body isn't valid JSON. Common cause: single quotes instead of double quotes, or a trailing comma.
missing_fieldYour JSON is missing unit. All five fields are required — see the spec.
device_id_mismatchThe topic says gm-02 but the payload says gm-01. They must match — you probably copied the config from another node.
unit_mismatchYou sent unit: "C" but temperature expects celsius. Units are spelled out.
value_type_wrongvalue should be a number for temperature, but arrived as text. Drop the quotes.
value_out_of_range1284 °C is outside the plausible range. Raw sensor value instead of a converted one?
timestamp_unparseablets must be ISO 8601 UTC like 2026-08-14T09:32:00Z, or null if your device has no clock. null is fine.
topic_malformedTopic needs five segments: garden/zone/device-type/device-id/measurement.
zone_not_registeredZone greenhous-1 isn't in the registry — check the spelling, or open a PR to add it.
device_type_not_registeredThis device type is not registered. Copy it from your charter configuration.
device_not_registeredThis device ID is not registered. Copy it from the credential slip.
device_registration_mismatchThe topic's zone or device type does not match this device's registry entry.
unexpected_fieldThe payload has a field outside the five-field contract. Remove the extra field.
rate_limitedThis device published more than 60 messages in a minute and is being throttled. Slow your loop down.