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

+94
View File
@@ -0,0 +1,94 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 740 500" width="740" height="500" role="img" aria-labelledby="t3">
<title id="t3">High-side P-channel MOSFET switch for the 5V sensor rail, driven through a 2N7002 inverting stage</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; }
.pin { font: 600 11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; fill: #16181d; }
.pw { fill: none; stroke: #c2410c; stroke-width: 2; }
.sigw { fill: none; stroke: #1d4ed8; stroke-width: 1.8; }
.gw { fill: none; stroke: #374151; stroke-width: 2; }
.comp { fill: #ffffff; stroke: #16181d; stroke-width: 1.5; }
.fet { fill: #eef2ff; stroke: #1d4ed8; stroke-width: 1.8; }
</style>
<rect class="bg" x="0" y="0" width="740" height="500"/>
<text class="ttl" x="20" y="30">Switched 5V sensor rail — high-side P-FET</text>
<text class="s" x="20" y="49">A 3.3V GPIO cannot turn a P-FET on a 5V rail fully off. The 2N7002 inverting stage is what makes it work.</text>
<!-- ============ +5V rail ============ -->
<path class="pw" d="M60,110 L660,110"/>
<text class="h" x="60" y="100">+5V</text>
<text class="s" x="98" y="100">from buck (mains) or boost converter (battery)</text>
<!-- ============ 100k pull-up ============ -->
<path class="pw" d="M330,110 L330,150"/>
<circle cx="330" cy="110" r="3.6" fill="#c2410c"/>
<rect class="comp" x="310" y="150" width="40" height="56" rx="2"/>
<text class="m" x="212" y="174">100 kΩ</text>
<text class="s" x="212" y="190">holds gate off</text>
<path class="sigw" d="M330,206 L330,240"/>
<!-- ============ P-channel MOSFET ============ -->
<rect class="fet" x="400" y="180" width="130" height="96" rx="6"/>
<text class="h" x="416" y="206">P-channel</text>
<text class="m" x="416" y="224">AO3401</text>
<text class="s" x="416" y="242">or IRLML6402</text>
<text class="pin" x="416" y="262">S ▲ G ◀ D ▼</text>
<!-- P-FET source up to rail -->
<path class="pw" d="M465,180 L465,110"/>
<circle cx="465" cy="110" r="3.6" fill="#c2410c"/>
<text class="pin" x="472" y="172">S</text>
<!-- P-FET gate left to node -->
<path class="sigw" d="M400,240 L330,240"/>
<circle cx="330" cy="240" r="3.6" fill="#1d4ed8"/>
<text class="pin" x="380" y="234">G</text>
<!-- P-FET drain down to output -->
<path class="pw" d="M465,276 L465,330 L620,330"/>
<text class="pin" x="472" y="292">D</text>
<text class="h" x="500" y="322">+5V to Cat5 pin 2</text>
<text class="s" x="500" y="350">sensors + battery-side boost load</text>
<!-- ============ 2N7002 inverting stage ============ -->
<path class="sigw" d="M330,240 L235,240 L235,290"/>
<rect class="fet" x="170" y="290" width="130" height="90" rx="6"/>
<text class="h" x="186" y="315">N-channel</text>
<text class="m" x="186" y="333">2N7002</text>
<text class="s" x="186" y="351">logic-level</text>
<text class="pin" x="186" y="370">D ▲ G ◀ S ▼</text>
<text class="pin" x="242" y="284">D</text>
<!-- gate drive from GPIO14 -->
<path class="sigw" d="M170,335 L140,335"/>
<rect class="comp" x="80" y="325" width="60" height="20" rx="2"/>
<text class="m" x="92" y="339">10 kΩ</text>
<path class="sigw" d="M80,335 L36,335"/>
<text class="h" x="20" y="318">GPIO14</text>
<text class="pin" x="150" y="329">G</text>
<!-- source to ground -->
<path class="gw" d="M235,380 L235,420"/>
<text class="pin" x="242" y="396">S</text>
<path class="gw" d="M60,420 L660,420"/>
<circle cx="235" cy="420" r="3.6" fill="#374151"/>
<use xlink:href="#gnd" href="#gnd" x="360" y="420"/>
<text class="h" x="60" y="440">GND</text>
<text class="s" x="98" y="440">common star point with Cat5 pin 1</text>
<!-- ============ truth table ============ -->
<rect x="20" y="456" width="700" height="34" rx="5" fill="#fff3ec" stroke="#c2410c" stroke-width="1.4"/>
<text class="m" x="34" y="477">GPIO14 HIGH → 2N7002 conducts → gate pulled low → rail ON · GPIO14 LOW or FLOATING → 100 kΩ pulls gate to 5V → rail OFF</text>
<defs>
<g id="gnd">
<line x1="-10" y1="0" x2="10" y2="0" stroke="#374151" stroke-width="2"/>
<line x1="-6" y1="4.5" x2="6" y2="4.5" stroke="#374151" stroke-width="2"/>
<line x1="-2.5" y1="9" x2="2.5" y2="9" stroke="#374151" stroke-width="2"/>
</g>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB