Implements ESP32_Soil_Moisture_Node_PRD.md on the VCC-GND Studio
YD-ESP32-S3 (ESP32-S3-WROOM-1) using Arduino via PlatformIO, publishing
to Home Assistant over MQTT with auto-discovery.
Two power profiles behind a compile-time switch (PRD 4):
-e mains always on, publishes every 60s
-e battery RTC wake, sample, publish, deep sleep 15min
PRD mitigations:
- All six probes on ADC1 (GPIO4-9); ADC2 is unusable while Wi-Fi is up.
- analogReadMilliVolts() for eFuse ADC calibration rather than raw counts.
- 16 samples per probe, sorted, 10% trimmed from each end, averaged.
- Per-probe two-point calibration; HW-390 output is inverted (dry reads
high), and an inverted config is rejected rather than silently producing
a backwards curve.
- MAC-derived node ID so identical binaries never collide on topics.
Design decisions that diverge from a literal reading of the PRD:
- Out-of-range probes publish null (HA "unknown") instead of clamping to
0%/100%. A cut Cat5 pair reads ~0mV, which clamps to a believable "very
dry" and would drive irrigation on wrong data.
- The battery profile uses expire_after rather than an availability topic;
a sleeping node is offline most of the time and would sit greyed out.
- AWAKE_BUDGET_MS forces deep sleep if a wake cycle overruns, covering
failure modes the per-step network timeouts miss (DHCP never issued,
CONNACK never sent) that would otherwise flatten the cell.
- docs/WIRING.md notes the 18650 cannot feed the probes directly: the
XC6206 LDO needs ~3.4V headroom, so a boost converter is required on the
switched side of the MOSFET.
Both profiles compile clean under -Wall. Not yet run on hardware; pin map,
MOSFET circuit and calibration defaults are unverified against a board.
Co-Authored-By: Claude <noreply@anthropic.com>
3.3 KiB
Two-point calibration
Implements PRD §3.3. Do this once per probe, after sealing them (§2.3) and with the probe connected through its final Cat5 run — the cable and the RC filter both shift the numbers, so calibrating on the bench with short leads produces values that are wrong in the field.
Why per-probe
CAL_AIR_MV / CAL_WATER_MV in include/config.h are arrays, not scalars.
HW-390s vary: each has its own XC6206 LDO, its own coating thickness, and its
own cable run. Using one shared pair of endpoints for six probes typically
costs 5–15% of absolute accuracy, which is enough to trigger irrigation on a
bed that is already wet.
The defaults shipped in config.h are placeholders. They compile and produce
plausible-looking output, which makes them dangerous — plausible output is
exactly what stops you noticing you skipped this step.
Procedure
-
Flash the mains build and let the node come up:
pio run -e mains -t upload -t monitor -
In Home Assistant, find the Raw mV 1–6 diagnostic entities on the node's device page. (These exist for this procedure; set
PUBLISH_RAW_MV 0inconfig.hto hide them once you are done.) You can also read them straight off the serial monitor in thestate:line. -
Air point (0%). Probe clean, dry, in open air. Wait ~30 s for the value to settle. Record
raw_mv_N→ this isCAL_AIR_MV[N-1]. -
Water point (100%). Submerge the probe in a glass of water up to but not past the marked line on the PCB. Do not submerge the electronics. Wait ~30 s. Record
raw_mv_N→ this isCAL_WATER_MV[N-1]. -
Paste both arrays into
include/config.hand reflash.
Expected values
A healthy 3.3 V HW-390 v2.0 reads roughly:
| Condition | Typical |
|---|---|
| Air (dry) | 2400–2800 mV |
| Water | 1000–1400 mV |
Air must read higher than water — the HW-390's output is inverted. The
firmware rejects CAL_AIR_MV <= CAL_WATER_MV at runtime and logs an error
rather than silently producing an inverted percentage, because a
moisture curve running backwards is very easy to miss on a dashboard and would
tell your irrigation to water dry soil less.
Troubleshooting
Air-to-water spread under ~800 mV. Usually a probe that is degrading, or one whose coating crept onto the prongs. A narrow span makes the percentage noisy no matter how much you oversample.
Reads ~0 mV. No power reaching the probe: check the Cat5 5 V pair, the MOSFET gate, and the splice. The firmware reports this as unknown rather than 0%, which is the giveaway.
Reads ~3300 mV (rail). Signal line shorted to the 5 V pair, or the probe's output is disconnected and the ADC pin is floating up.
Reading drifts down over weeks with no rain. Moisture ingress into an
imperfectly sealed board. Re-seal per WIRING.md §6 and recalibrate.
Fault margin
SENSOR_FAULT_MARGIN_MV (default 400 mV) sets how far outside the calibrated
window a reading may sit before it is reported as unavailable instead of being
clamped. Soil genuinely can read slightly outside the air/water endpoints —
saturated clay can go below the water point — so the margin exists to avoid
false faults. Widen it if healthy probes flap to unknown; narrow it if real
failures are being clamped into believable numbers.