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

+135
View File
@@ -0,0 +1,135 @@
# Wiring & Breakout
Implements PRD §2 (cabling) and §3.1–3.2 (signal integrity, ground bounce).
![System architecture](img/system-architecture.svg)
## 1. Cat5 pinout
Standard T568B colours. Power and ground share one twisted pair so the
supply loop area stays small (PRD §2.2).
| RJ45 pin | Colour | Function | MCU side |
|----------|--------------|---------------------|----------------|
| 1 | Orange/White | **GND** | GND |
| 2 | Orange | **+5V** | Switched 5V rail |
| 3 | Green/White | Analog signal 1 | GPIO4 (ADC1_CH3) |
| 6 | Green | Analog signal 2 | GPIO5 (ADC1_CH4) |
| 4 | Blue | Analog signal 3 | GPIO6 (ADC1_CH5) |
| 5 | Blue/White | Analog signal 4 | GPIO7 (ADC1_CH6) |
| 7 | Brown/White | Analog signal 5 | GPIO8 (ADC1_CH7) |
| 8 | Brown | Analog signal 6 | GPIO9 (ADC1_CH8) |
**Known tradeoff:** six signals over three pairs means each pair carries two
*signals* rather than a signal and its return, which is the worst case for
pair-to-pair crosstalk. It is acceptable here only because these are slow DC
levels and the RC filters below roll off everything above a few hertz. If you
ever repurpose this harness for anything with edges in it, re-plan the pairing.
## 2. RC low-pass filter — mandatory (PRD §3.1)
![RJ45 breakout schematic](img/rj45-breakout.svg)
One per analog line, on the RJ45 breakout board, **between the jack and the
ESP32 pin**:
```
RJ45 signal ──[ 10 kΩ ]──┬── GPIO (ADC1)
│
[ 0.1 µF ] ceramic, X7R
│
GND
```
Placement is the whole point: the capacitor must sit at the *ESP32 end* of the
resistor, physically close to the pin. Put it at the jack end and you have
filtered the cable but left the ADC pin exposed to everything picked up on the
last few centimetres of board trace.
- 10 kΩ + 0.1 µF → τ = 1 ms. Fine for soil, which moves over minutes.
- 10 kΩ + 1 µF → τ = 10 ms. Quieter; still far faster than the sample burst.
The series resistor also raises source impedance above what the SAR ADC would
like. The capacitor is what fixes that — it supplies the sampling charge — so
do not fit the resistor without it.
## 3. Ground integrity (PRD §3.2)
- Every splice at the sensor end: soldered + heat-shrink, or a high-retention
Wago. Twist-and-tape connections corrode and drift the ground reference.
- Budget ~5 mA per HW-390, so ~30 mA total. One 24 AWG conductor carries this
with negligible drop over a normal garden run.
- Run **one** GND back to a single point at the MCU board. Do not daisy-chain
sensor grounds through each other — that is exactly the shared-return path
that makes one probe's current show up as another probe's reading.
## 4. Sensor power switch (battery profile)
![High-side P-FET switch](img/power-switch.svg)
`SENSOR_POWER_GATE_PIN` (GPIO14) is `HIGH` for *sensors on*. A 3.3 V GPIO
cannot drive a P-channel MOSFET's gate on a 5 V rail hard enough to fully turn
it off, so an inverting stage is required:
```
+5V ──────────┬──────────────┬─── source
│ │
[100kΩ] [ P-ch MOSFET ] e.g. AO3401
│ │
GPIO14 ──[10kΩ]──┤ gate └─── drain ──→ Cat5 pin 2 (+5V to sensors)
│
[ 2N7002 N-ch ] drain ──┘
│
GND
```
- GPIO14 HIGH → 2N7002 conducts → P-FET gate pulled low → rail **on**.
- GPIO14 LOW **or floating** → 100 kΩ pulls the gate to 5 V → rail **off**.
The pull-up is what makes deep sleep safe: pins float during sleep, and this
guarantees a floating pin means *off* rather than *undefined*.
## 5. Battery & solar (PRD §4.2)
![Solar and battery topology](img/battery-solar.svg)
**The 18650 cannot feed the sensors directly.** A cell runs 3.0–4.2 V, and the
HW-390's XC6206 LDO needs roughly 3.4 V of headroom to hold a clean 3.3 V. Wire
the cell straight to the Cat5 5 V pair and the probes brown out and drift as
the cell discharges — readings that look like the soil drying out. Put a
**boost converter to 5 V on the switched side** of the MOSFET.
```
Solar panel (6V) ──→ TP4056 (with DW01 protection) ──→ 18650
│
├──→ ESP32-S3 5V/VIN
│
└──→ [P-FET] ──→ boost 5V ──→ Cat5 pin 2
```
Battery sense divider into GPIO10:
```
VBAT ──[ R1 1MΩ ]──┬── GPIO10 (ADC1_CH9)
│
[ R2 1MΩ ] ┬── [1µF]
│ │
GND GND
```
1 MΩ/1 MΩ draws ~2 µA, which is the right order next to the ESP32's ~10 µA
deep-sleep current. The obvious 100 kΩ/100 kΩ pair draws ~21 µA — more than the
sleeping MCU, and enough to dominate your winter power budget. The 1 µF cap is
not optional at this impedance: it supplies the ADC's sampling charge.
`BATTERY_DIVIDER_RATIO = 2.0` in `include/config.h` assumes R1 == R2. Measure
the actual cell voltage with a meter and adjust the ratio to trim out resistor
tolerance.
## 6. Sensor sealing (PRD §2.3)
Every HW-390's upper PCB — components, exposed board edges, and the splice —
sealed with marine epoxy or conformal coating. **Only the capacitive prongs
stay bare.** This is the single most common reason these probes die in a
season; an unsealed board wicks moisture up under the silkscreen and the
readings drift long before anything looks visibly corroded.