Initial firmware for ESP32-S3 six-channel soil moisture node

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>
This commit is contained in:
mattandClaude committed 2026-08-14 13:38:41 -07:00
commit ad9c457ea7
24 files changed
+2178

No files matched your search

+79
View File
@@ -0,0 +1,79 @@
# 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
1. Flash the mains build and let the node come up:
```
pio run -e mains -t upload -t monitor
```
2. 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 0` in
`config.h` to hide them once you are done.) You can also read them straight
off the serial monitor in the `state:` line.
3. **Air point (0%).** Probe clean, dry, in open air. Wait ~30 s for the value
to settle. Record `raw_mv_N` → this is `CAL_AIR_MV[N-1]`.
4. **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 is `CAL_WATER_MV[N-1]`.
5. Paste both arrays into `include/config.h` and 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.