Field-node firmware for the AgroSmart precision-agriculture system. A NodeMCU ESP-12E (ESP8266) samples a water sensor board and a soil probe, applies calibration and temperature compensation, and publishes readings as versioned JSON.
The ESP8266 is not 5 V tolerant — 3.6 V absolute maximum on an input. Both
sensor boards are 5 V parts, so every sensor-TX → ESP-RX line needs a resistor
divider (1 kΩ series + 2 kΩ to ground) or a level shifter, fitted before first
power-on. Details in docs/hardware/wiring.md.
Watch polarity on the soil probe. Its manual warns that reversed power or swapped A/B differential lines can destroy the device.
| Device | Interface | Provides |
|---|---|---|
| 4-in-1 water board | TTL UART, 9600 8N1, 5 V | water pH, light, temperature, water level |
| 7-in-1 soil probe | RS485 Modbus RTU, 4800 8N1, 4.5–30 V | moisture, soil temp, EC, soil pH, N, P, K |
The two boards run at different baud rates. Getting them the wrong way round is the single most common cause of a sensor that appears dead.
| Function | Pin | GPIO | Notes |
|---|---|---|---|
| 4-in-1 RX (← board TX) | D5 | 14 | 9600, via divider |
| 4-in-1 TX (→ board RX) | D6 | 12 | |
| Soil probe RX (← converter TX) | D7 | 13 | 4800, via divider |
| Soil probe TX (→ converter RX) | D1 | 5 | |
| Water-level power gate | D2 | 4 | duty-cycled to prevent trace corrosion |
| Debug + flashing | — | 1/3 | UART0 on USB, 115200 |
Both sensors run on SoftwareSerial, which is comfortable at these rates and
leaves UART0 on USB for logging throughout — no Serial.swap(), no external
USB-TTL adapter, no boot-strap-pin risk on D8. There is no DE/RE pin because the
supplied RS485 converter does automatic direction control.
# Host-side unit tests — no board required
pio test -e native
# Build and flash the firmware
pio run -e nodemcuv2 -t upload
pio device monitor
# Phase B: decode the 4-in-1's undocumented serial format
pio run -e sniffer -t upload
pio device monitor -e snifferOptional, and not needed until Phase F:
cp include/secrets.example.h include/secrets.h1. The soil probe does not measure N, P or K. It measures bulk electrical
conductivity and back-calculates all three with a linear fit. Out of the factory
they are one EC reading wearing three scaling factors, and dry soil legitimately
reports zero nitrogen. Every reading carries an npkEstimated flag that stays
true until per-soil coefficients are written to the device, and there is a unit
test asserting that a zero-NPK dry-soil frame decodes as valid rather than as a
fault. Treat these values as relative trends, not lab results.
See docs/hardware/npk-7in1-probe.md.
2. The 4-in-1's serial format is undocumented. Its framing is implemented and
tested; its field layout is provisional, and the CSV column order is an explicit
guess until you run the sniffer. Nothing trustworthy comes out of phWater,
sunlight or waterLevel before that.
See docs/hardware/ph-4in1-protocol.md.
src/main.cpp composition root — wiring and scheduling only
src/sniffer/ Phase B protocol sniffer (separate env)
include/config.h pins, timings, identity
lib/ firmware modules (see lib/README)
test/ host-side Unity tests (see test/README)
docs/ hardware, module and integration documentation
Design rule: parsers take a byte buffer, never a serial port. Everything with
logic in it is a pure function, and the hardware wrappers compile to nothing on the
host. That is what makes pio test -e native possible, and it matters because
protocol decoding is both the most likely thing to be wrong and the most painful
thing to debug on-target. docs/modules/README.md covers
the decomposition and how each module is broken down before coding.
| Phase | State |
|---|---|
| A — Toolchain | ✅ Arduino framework; both envs build clean, no warnings |
| B — 4-in-1 protocol | ⬜ Blocked on hardware. Sniffer ready |
| C — Modbus soil probe | ✅ Implemented + tested; needs on-hardware cross-check |
| D — Calibration | ✅ Maths done + tested; LittleFS persistence pending |
| E — Integration | ✅ Both sensors sampled, payload built and logged |
| F — Network delivery | ⬜ WiFi, NTP, store-and-forward, HTTP POST, OTA |
Firmware footprint: RAM 37.1%, Flash 26.9% — leaving headroom for the ~16–22 KB BearSSL needs per TLS handshake in Phase F.
The soil probe's vendor code has two defects, each with a named regression test:
- Negative temperatures decode as garbage. Their Arduino and Python samples
read the temperature register unsigned, so
0xFF9B(−10.1 °C) becomes ~6549 °C. - The Arduino sample never verifies CRC. RS485 in a wet field picks up noise, and an unchecked frame occasionally hands you a plausible-looking wrong number.
{
"v": 1, "sensorId": "AGS-001",
"readings": { "temperature": 24.8, "moisture": 42.5, "salinity": 0.380,
"phSoil": 6.80, "npk": { "nitrogen": 95, "phosphorus": 52, "potassium": 210 },
"phWater": 7.10, "sunlight": 780, "waterLevel": 63 },
"quality": { "npkEstimated": true, "stabilising": false, "soilDry": false },
"diag": { "rssi": -62, "uptime": 38211, "fw": "0.1.0" }
}Absent fields are omitted, not zeroed — an unplugged board must never report
"phWater": 0.0, which is a plausible acid reading nobody downstream could
distinguish from a real one.
There is currently nowhere to send this: the AgroSmart backend directory is
empty and the web app has no API routes. The contract, the field mapping, and ten
specific mismatches with the existing dashboard are documented in
docs/integration/dashboard-contract.md.
Note in particular: do not have the device write directly to Supabase with the
app's publishable key — no RLS policies exist, so the table would be
world-writable.
See CONTRIBUTING.md. Host tests must pass (pio test -e native)
and both environments must build warning-free.
Dual-licensed by content type:
Third-party dependency licences, including one that needs attention before a
release, are listed in THIRD-PARTY.md.