Files
mattandClaude ad9c457ea7 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>
2026-08-14 13:38:41 -07:00

3.3 KiB
Raw Permalink Blame History

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.