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

+174
View File
@@ -0,0 +1,174 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 800 690" width="800" height="690" role="img" aria-labelledby="t2">
<title id="t2">RJ45 breakout schematic with six RC low-pass filters feeding ESP32-S3 ADC1 pins</title>
<style>
.bg { fill: #fcfcfa; }
.ttl { font: 600 19px ui-sans-serif, system-ui, 'Segoe UI', Roboto, sans-serif; fill: #16181d; }
.h { font: 600 13px ui-sans-serif, system-ui, 'Segoe UI', Roboto, sans-serif; fill: #16181d; }
.s { font: 11.5px ui-sans-serif, system-ui, 'Segoe UI', Roboto, sans-serif; fill: #6b7280; }
.m { font: 11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; fill: #374151; }
.mp { font: 600 11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; fill: #16181d; }
.wire { fill: none; stroke: #1d4ed8; stroke-width: 1.7; }
.pw { fill: none; stroke: #c2410c; stroke-width: 1.9; }
.gw { fill: none; stroke: #374151; stroke-width: 1.9; }
.comp { fill: #ffffff; stroke: #16181d; stroke-width: 1.5; }
.jack { fill: #f3f4f6; stroke: #16181d; stroke-width: 1.8; }
</style>
<rect class="bg" x="0" y="0" width="800" height="690"/>
<text class="ttl" x="20" y="30">RJ45 breakout — RC low-pass per analog line</text>
<text class="s" x="20" y="49">PRD §3.1, mandatory. One filter per signal, sited at the ESP32 end of the series resistor.</text>
<!-- ================= jack ================= -->
<rect class="jack" x="34" y="86" width="104" height="478" rx="6"/>
<text class="h" x="46" y="110">RJ45</text>
<text class="s" x="46" y="126">T568B</text>
<!-- column headers -->
<text class="s" x="152" y="80">wire</text>
<text class="s" x="238" y="80">series R</text>
<text class="s" x="360" y="80">shunt C</text>
<text class="s" x="470" y="80">to MCU</text>
<!-- ================= pin 1: GND ================= -->
<text class="mp" x="46" y="156">1</text>
<rect x="62" y="146" width="26" height="12" fill="#ffffff" stroke="#f97316" stroke-width="1.2"/>
<rect x="62" y="150" width="26" height="4" fill="#f97316"/>
<path class="gw" d="M138,152 L470,152"/>
<text class="m" x="152" y="138">Orange/White</text>
<text class="h" x="480" y="157">GND</text>
<text class="s" x="524" y="157">star point</text>
<!-- ================= pin 2: +5V ================= -->
<text class="mp" x="46" y="214">2</text>
<rect x="62" y="204" width="26" height="12" fill="#f97316" stroke="#374151" stroke-width="1"/>
<path class="pw" d="M138,210 L470,210"/>
<text class="m" x="152" y="196">Orange</text>
<text class="h" x="480" y="215">+5V</text>
<text class="s" x="524" y="215">switched rail</text>
<!-- ================= signal rows ================= -->
<!-- row: pin 3 -->
<text class="mp" x="46" y="272">3</text>
<rect x="62" y="262" width="26" height="12" fill="#ffffff" stroke="#16a34a" stroke-width="1.2"/>
<rect x="62" y="266" width="26" height="4" fill="#16a34a"/>
<text class="m" x="152" y="254">Green/White</text>
<path class="wire" d="M138,268 L206,268"/>
<rect class="comp" x="206" y="258" width="64" height="20" rx="2"/>
<text class="m" x="222" y="272">10 kΩ</text>
<path class="wire" d="M270,268 L470,268"/>
<circle cx="352" cy="268" r="3.4" fill="#1d4ed8"/>
<path class="wire" d="M352,268 L352,286"/>
<line x1="341" y1="286" x2="363" y2="286" stroke="#16181d" stroke-width="1.8"/>
<line x1="341" y1="292" x2="363" y2="292" stroke="#16181d" stroke-width="1.8"/>
<path class="gw" d="M352,292 L352,300"/>
<use xlink:href="#gnd" href="#gnd" x="352" y="300"/>
<text class="m" x="370" y="292">0.1 µF</text>
<text class="h" x="480" y="273">GPIO4</text>
<text class="s" x="534" y="273">ADC1_CH3 · moisture 1</text>
<!-- row: pin 4 -->
<text class="mp" x="46" y="330">4</text>
<rect x="62" y="320" width="26" height="12" fill="#2563eb" stroke="#374151" stroke-width="1"/>
<text class="m" x="152" y="312">Blue</text>
<path class="wire" d="M138,326 L206,326"/>
<rect class="comp" x="206" y="316" width="64" height="20" rx="2"/>
<text class="m" x="222" y="330">10 kΩ</text>
<path class="wire" d="M270,326 L470,326"/>
<circle cx="352" cy="326" r="3.4" fill="#1d4ed8"/>
<path class="wire" d="M352,326 L352,344"/>
<line x1="341" y1="344" x2="363" y2="344" stroke="#16181d" stroke-width="1.8"/>
<line x1="341" y1="350" x2="363" y2="350" stroke="#16181d" stroke-width="1.8"/>
<path class="gw" d="M352,350 L352,358"/>
<use xlink:href="#gnd" href="#gnd" x="352" y="358"/>
<text class="m" x="370" y="350">0.1 µF</text>
<text class="h" x="480" y="331">GPIO6</text>
<text class="s" x="534" y="331">ADC1_CH5 · moisture 3</text>
<!-- row: pin 5 -->
<text class="mp" x="46" y="388">5</text>
<rect x="62" y="378" width="26" height="12" fill="#ffffff" stroke="#2563eb" stroke-width="1.2"/>
<rect x="62" y="382" width="26" height="4" fill="#2563eb"/>
<text class="m" x="152" y="370">Blue/White</text>
<path class="wire" d="M138,384 L206,384"/>
<rect class="comp" x="206" y="374" width="64" height="20" rx="2"/>
<text class="m" x="222" y="388">10 kΩ</text>
<path class="wire" d="M270,384 L470,384"/>
<circle cx="352" cy="384" r="3.4" fill="#1d4ed8"/>
<path class="wire" d="M352,384 L352,402"/>
<line x1="341" y1="402" x2="363" y2="402" stroke="#16181d" stroke-width="1.8"/>
<line x1="341" y1="408" x2="363" y2="408" stroke="#16181d" stroke-width="1.8"/>
<path class="gw" d="M352,408 L352,416"/>
<use xlink:href="#gnd" href="#gnd" x="352" y="416"/>
<text class="m" x="370" y="408">0.1 µF</text>
<text class="h" x="480" y="389">GPIO7</text>
<text class="s" x="534" y="389">ADC1_CH6 · moisture 4</text>
<!-- row: pin 6 -->
<text class="mp" x="46" y="446">6</text>
<rect x="62" y="436" width="26" height="12" fill="#16a34a" stroke="#374151" stroke-width="1"/>
<text class="m" x="152" y="428">Green</text>
<path class="wire" d="M138,442 L206,442"/>
<rect class="comp" x="206" y="432" width="64" height="20" rx="2"/>
<text class="m" x="222" y="446">10 kΩ</text>
<path class="wire" d="M270,442 L470,442"/>
<circle cx="352" cy="442" r="3.4" fill="#1d4ed8"/>
<path class="wire" d="M352,442 L352,460"/>
<line x1="341" y1="460" x2="363" y2="460" stroke="#16181d" stroke-width="1.8"/>
<line x1="341" y1="466" x2="363" y2="466" stroke="#16181d" stroke-width="1.8"/>
<path class="gw" d="M352,466 L352,474"/>
<use xlink:href="#gnd" href="#gnd" x="352" y="474"/>
<text class="m" x="370" y="466">0.1 µF</text>
<text class="h" x="480" y="447">GPIO5</text>
<text class="s" x="534" y="447">ADC1_CH4 · moisture 2</text>
<!-- row: pin 7 -->
<text class="mp" x="46" y="504">7</text>
<rect x="62" y="494" width="26" height="12" fill="#ffffff" stroke="#92400e" stroke-width="1.2"/>
<rect x="62" y="498" width="26" height="4" fill="#92400e"/>
<text class="m" x="152" y="486">Brown/White</text>
<path class="wire" d="M138,500 L206,500"/>
<rect class="comp" x="206" y="490" width="64" height="20" rx="2"/>
<text class="m" x="222" y="504">10 kΩ</text>
<path class="wire" d="M270,500 L470,500"/>
<circle cx="352" cy="500" r="3.4" fill="#1d4ed8"/>
<path class="wire" d="M352,500 L352,518"/>
<line x1="341" y1="518" x2="363" y2="518" stroke="#16181d" stroke-width="1.8"/>
<line x1="341" y1="524" x2="363" y2="524" stroke="#16181d" stroke-width="1.8"/>
<path class="gw" d="M352,524 L352,532"/>
<use xlink:href="#gnd" href="#gnd" x="352" y="532"/>
<text class="m" x="370" y="524">0.1 µF</text>
<text class="h" x="480" y="505">GPIO8</text>
<text class="s" x="534" y="505">ADC1_CH7 · moisture 5</text>
<!-- row: pin 8 -->
<text class="mp" x="46" y="562">8</text>
<rect x="62" y="552" width="26" height="12" fill="#92400e" stroke="#374151" stroke-width="1"/>
<text class="m" x="152" y="544">Brown</text>
<path class="wire" d="M138,558 L206,558"/>
<rect class="comp" x="206" y="548" width="64" height="20" rx="2"/>
<text class="m" x="222" y="562">10 kΩ</text>
<path class="wire" d="M270,558 L470,558"/>
<circle cx="352" cy="558" r="3.4" fill="#1d4ed8"/>
<path class="wire" d="M352,558 L352,576"/>
<line x1="341" y1="576" x2="363" y2="576" stroke="#16181d" stroke-width="1.8"/>
<line x1="341" y1="582" x2="363" y2="582" stroke="#16181d" stroke-width="1.8"/>
<path class="gw" d="M352,582 L352,590"/>
<use xlink:href="#gnd" href="#gnd" x="352" y="590"/>
<text class="m" x="370" y="582">0.1 µF</text>
<text class="h" x="480" y="563">GPIO9</text>
<text class="s" x="534" y="563">ADC1_CH8 · moisture 6</text>
<!-- ================= notes ================= -->
<rect x="20" y="610" width="760" height="64" rx="5" fill="#fff3ec" stroke="#c2410c" stroke-width="1.4"/>
<text class="h" x="34" y="631">Placement matters more than the values.</text>
<text class="s" x="34" y="650">The capacitor must sit at the ESP32 end of the resistor, close to the pin — it is what supplies the ADC's sampling charge.</text>
<text class="s" x="34" y="667">Fitting the 10 kΩ without the cap leaves source impedance worse than having no filter at all.</text>
<defs>
<g id="gnd">
<line x1="-10" y1="0" x2="10" y2="0" stroke="#374151" stroke-width="1.9"/>
<line x1="-6" y1="4.5" x2="6" y2="4.5" stroke="#374151" stroke-width="1.9"/>
<line x1="-2.5" y1="9" x2="2.5" y2="9" stroke="#374151" stroke-width="1.9"/>
</g>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 9.2 KiB