Hardware Sensor Event Contract¶
This is the pre-hardware contract for ESP32 and other sensor devices. It is local documentation only; no device connection or actuator control is enabled by this contract.
Transport¶
MQTT topic:
The MQTT payload must be one JSON object matching
schemas/sensor-event.schema.json. The same object can be submitted to
POST /v1/sensors/events for HTTP testing.
Canonical payload¶
{
"device_id": "esp32-greenhouse-01",
"farm_id": "demo-farm",
"zone_id": "greenhouse-a",
"crop": "tomato",
"growth_stage": "flowering",
"system_type": "greenhouse_substrate",
"air_temperature_c": 31.2,
"humidity_pct": 88.0,
"ec_ms_cm": 3.4,
"ph": 7.5,
"soil_moisture_pct": 42.0,
"timestamp": "2026-07-21T10:00:00Z",
"source": "esp32",
"schema_version": "1.0"
}
schema_version is the payload contract version, "MAJOR.MINOR" as a string. A packet
without it is treated as "1.0", so older senders keep working. Core accepts any 1.x
(minor versions only add optional fields) and rejects another major version, for example
"2.0", with a validation error instead of storing it as if it were 1.x. The stored payload
keeps the version, so old records stay readable after the contract changes. The same field
applies to modular observations (POST /v1/sensors/observations).
Boundary rules¶
| Field | Unit | Accepted range |
|---|---|---|
air_temperature_c |
Celsius | -40 to 80 |
humidity_pct |
percent | 0 to 100 |
ec_ms_cm |
mS/cm | 0 to 20 |
ph |
pH scale | 0 to 14 |
soil_moisture_pct |
percent | 0 to 100 |
Supported deployment profiles are represented by system_type, for example
soil, greenhouse_substrate, hydroponic, and aquaponic. The transport
contract is shared, but each profile needs its own expected fields and rules.
Core rejects malformed packets and values outside these transport ranges. A value inside the transport range can still be agronomically suspicious; the Sensor Quality reasoner remains responsible for stale, conflicting, drift, or crop-specific checks.
Hardware safety boundary¶
Modular observation API (monitoring only)¶
Temperature-only nodes can use POST /v1/sensors/observations, or MQTT topic
pomona/{farm_id}/{zone_id}/sensor/{device_id}/observation. The existing
full-packet /state topic and /v1/sensors/events requirements are unchanged.
{
"device_id": "temperature-node-01",
"farm_id": "demo-farm",
"zone_id": "greenhouse-a",
"sensor_id": "air-temperature-01",
"measurement": "air_temperature_c",
"unit": "C",
"value": 24.5,
"quality": "valid",
"timestamp": "2026-09-05T10:00:00Z",
"sequence": 1,
"boot_id": "boot-01",
"firmware": "prototype-0.1"
}
Supported measurements are air/water temperature (C), humidity and soil
moisture (%), pH (pH), EC (mS/cm), and low-level contact (boolean, 0/1).
Contact polarity remains a device-specific convention, not an irrigation rule.
Quality may be valid, missing, suspect, conflicting, or disconnected.
Missing values must not be marked valid. Optional calibration_timestamp and
sample timestamps require timezones. Quality/calibration are sender-reported,
not evidence of validation; sequence/boot metadata are stored, not deduplicated.
Core overwrites received_at with its own receipt time.
These records are not fused into reasoner inputs. No synthetic pH/EC values are filled in. A validated state-assembly policy is a later task.
Scoped history and device visibility¶
GET /v1/sensors/eventsand/events/latestacceptfarm_idandzone_id.GET /v1/sensors/observationsaccepts the same filters,limit, andoffset.GET /v1/sensors/devicesreports last receipt, sample age, andrecentorsilent(default threshold 120 seconds). This is inferred availability, not proof of connectivity. Old migrated records do not invent a last-seen time.GET /v1/sensors/export.csv?kind=observations&farm_id=demo-farm&zone_id=greenhouse-aexports a bounded CSV;kind=eventsexports full packets. Maximum page size is 10,000; offset pages newest records first, chronological within a page. Pause ingestion for a consistent multi-page export: offsets can shift during writes or retention cleanup. Spreadsheet formula-like strings are escaped.
Retention defaults to seven days by server receipt time and at most 100,000
records per farm/zone per record type, whichever limit is reached first.
Cleanup runs on ingest, not on a timer. Configure RETENTION_DAYS, MAX_EVENTS,
and DEVICE_TIMEOUT_SECONDS. Legacy rows without receipt times remain subject
to the count limit; device last-seen metadata is retained separately. This is
not a global disk quota: many zones/devices can increase storage usage.
Back up SQLite before upgrading; the schema migration adds scope columns and
observation/presence tables. No authentication change accompanies these APIs;
keep the MVP on a trusted local network.
The dashboard accepts a farm/zone selection and displays device health plus 100-record history pages, with CSV download for the same selection and offset. Full-packet trend sparklines remain separate from the modular observation table; modular observations are not assembled into recommendations.
Command boundary¶
Sensor events are observation data only. They cannot authorize a pump, valve, fertigation, climate, pesticide, or diagnostic command. Proposed actions must go through the guarded pipeline and deterministic Safety Checker. Hardware integration must begin in dry-run mode with commands logged but not executed.