What this node publishes, and where it does not line up with the AgroSmart dashboard.
Scope note: this repository is hardware/firmware only. The observations below
about ../app/agroapp are recorded here for whoever builds the backend — they
are deliberately not fixed by editing the web app from this repo.
Built by lib/Telemetry. Versioned, so the schema
can move without bricking deployed nodes.
{
"v": 1,
"sensorId": "AGS-001",
"ts": 1753900000,
"readings": {
"temperature": 24.8,
"moisture": 42.5,
"salinity": 0.380,
"phSoil": 6.80,
"npk": { "nitrogen": 95, "phosphorus": 52, "potassium": 210 },
"phWater": 7.10,
"waterTemperature": 23.4,
"sunlight": 780,
"waterLevel": 63
},
"quality": { "npkEstimated": true, "stabilising": false, "soilDry": false },
"diag": { "rssi": -62, "uptime": 38211, "fw": "0.1.0" }
}| Field | Source | Unit | Confidence |
|---|---|---|---|
temperature |
soil probe | °C | measured, ±0.5 °C |
moisture |
soil probe | % | measured, ±2–3% |
salinity |
soil probe EC ÷ 1000 | dS/m | measured, ±3% FS |
phSoil |
soil probe | pH | rough — device range only 3–9, ±0.3 |
npk.* |
soil probe | mg/kg | estimated from EC, not measured |
phWater |
4-in-1 | pH | calibrated + temp-compensated |
waterTemperature |
4-in-1 | °C | mainly used to compensate phWater |
sunlight |
4-in-1 LDR | relative, NOT lux | uncalibrated |
waterLevel |
4-in-1 | relative, non-linear | coarse / presence |
1. Absent fields are omitted, not zeroed. A node whose 4-in-1 board is
unplugged emits no phWater key at all. It must never report "phWater": 0.0,
because that is a perfectly plausible-looking acid reading and nothing downstream
could tell it from a real one. Distinguish "no data" from "zero" by key absence.
2. The quality block is load-bearing, and always present.
| Flag | Meaning |
|---|---|
npkEstimated |
N/P/K are back-calculated from conductivity, not measured. Stays true until per-soil coefficients are written to the probe. Do not build fertiliser recommendations on these values while it is true. |
stabilising |
Within the probe's 5-minute post-insertion settling window. Readings are real but converging. |
soilDry |
Conductivity was zero, which zeroes derived NPK. Expected in dry soil, not a fault. Do not raise a sensor-failure alert. |
These are never omitted, so their absence can never be misread as false.
3. ts is omitted until NTP syncs. When absent, the server should stamp
arrival time rather than trust a device clock known to be wrong. When present it is
Unix epoch seconds. Buffered readings replayed after an outage carry their original
ts, which is the whole point of sending it.
Observed in ../app/agroapp. Each needs a decision from whoever wires the
backend.
| # | Observation | Location |
|---|---|---|
| 1 | Sensor.ph is single-valued, but the hardware produces two pH readings — water (0–14, calibrated) and soil (3–9, ±0.3). They are different quantities and cannot be merged. The payload uses phWater and phSoil. |
lib/types.ts |
| 2 | No waterLevel field exists in the dashboard type at all, though the 4-in-1 measures it. |
lib/types.ts |
| 3 | sensorId length conflict: sensorSchema enforces z.string().length(7) while sampleSensors mocks use 14-char IDs like "SENSOR-LKO-001". The firmware bakes one in — currently "AGS-001" (7 chars, matching what the schema actually validates). Must also stay URL-safe, since it routes /dashboard/sensors/[sensorID]. |
lib/schema.ts vs lib/table-data.ts |
| 4 | No timestamp field anywhere in the app's types. The per-reading table type has no time column, and the sensor detail page fabricates its "last 5 data points" by duplicating one reading five times. That is exactly the slot real time-series data fills. | lib/types.ts, app/dashboard/sensors/[sensorID]/page.tsx |
| 5 | npk is a nested object in TypeScript but naturally three Postgres columns. Decide where flattening happens — the API or the UI. The table reads row.original.npk.nitrogen directly. |
lib/types.ts, lib/table-columns.tsx |
| 6 | No ownership column (user_id / farm_id) exists, yet one is required for any workable row-level-security policy. |
— |
| 7 | status is commented // these are just test labels. It should be server-derived from staleness and thresholds, not device-reported. The firmware does not send it. |
lib/types.ts |
| 8 | sunlight is labelled "lux" in the table header, but the LDR is uncalibrated and relative. Either relabel it or calibrate it — see ../hardware/calibration.md. |
lib/table-columns.tsx |
| 9 | temperature is formatted with temp.toFixed(1), which throws on null. Since fields can legitimately be absent, the UI needs a null guard. |
lib/table-columns.tsx |
| 10 | No battery or RSSI fields exist, although mockAlerts references a battery-level alert. The payload sends rssi and uptime under diag. |
lib/types.ts |
At the time of writing:
../app/agro-app-backend/is an empty directory.- The Next.js app has no
app/api/routes and no Server Actions. - Supabase is wired for email/password auth only. A repo-wide search found no
.from(),.insert()or.select()calls outsidenode_modules. - There are no migrations and no generated database types, so the sensor table schema does not exist in version control.
lib/supabase/proxy.tsexportsupdateSessionbut there is nomiddleware.ts, so route protection is not active.
It would be technically possible to POST to PostgREST with the app's
publishable/anon key. Don't. No RLS policies exist on that project, so an
otherwise-open table would be world-writable by anyone holding a key that is
already shipped to every browser.
The correct shape:
device --(HTTPS + per-device token)--> app/api/ingest --(service-role key)--> Postgres
The ingest route validates a per-device secret server-side and writes with a
service-role key that never leaves the server. AGRO_DEVICE_TOKEN in
include/secrets.h is the device half of that.
Phase F develops against a local mock endpoint over plain HTTP — any trivial
listener that logs the body and returns 200. AGRO_INGEST_URL in
include/config.h points at it. Plain HTTP first is also the right order for a
second reason: BearSSL costs ~16–22 KB of heap per TLS handshake on top of WiFi,
so prove the payload before adding that pressure.