Files
soilMoistureSystem/docs/img/system-architecture.svg
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

165 lines
8.9 KiB
XML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 980 600" width="980" height="600" role="img" aria-labelledby="t1">
<title id="t1">System architecture: six HW-390 probes over Cat5 to an ESP32-S3, published to Home Assistant over MQTT</title>
<style>
.bg { fill: #fcfcfa; }
.box { fill: #f3f4f6; stroke: #16181d; stroke-width: 1.6; }
.boxhi { fill: #eef2ff; stroke: #1d4ed8; stroke-width: 1.8; }
.boxpw { fill: #fff3ec; stroke: #c2410c; stroke-width: 1.8; }
.boxha { fill: #ecfdf3; stroke: #15803d; stroke-width: 1.8; }
.ttl { font: 600 19px ui-sans-serif, system-ui, 'Segoe UI', Roboto, sans-serif; fill: #16181d; }
.h { font: 600 13.5px 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; }
.mb { font: 600 11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; fill: #1d4ed8; }
.sig { fill: none; stroke: #1d4ed8; stroke-width: 1.5; }
.cat5 { fill: none; stroke: #1d4ed8; stroke-width: 8; stroke-linecap: round; }
.pwr { fill: none; stroke: #c2410c; stroke-width: 2.2; }
.data { fill: none; stroke: #15803d; stroke-width: 2.2; }
.dash { stroke-dasharray: 5 4; }
</style>
<rect class="bg" x="0" y="0" width="980" height="600"/>
<text class="ttl" x="20" y="30">Soil Moisture Node — system architecture</text>
<text class="s" x="20" y="49">Up to 6 HW-390 v2.0 probes per node · one Cat5 run · multi-node safe via MAC-derived ID</text>
<!-- ============ probes ============ -->
<text class="h" x="20" y="80">Field sensors</text>
<g>
<rect class="box" x="20" y="90" width="152" height="44" rx="5"/>
<text class="h" x="32" y="110">HW-390 #1</text><text class="m" x="32" y="126">sealed · 5mA</text>
<rect class="box" x="20" y="146" width="152" height="44" rx="5"/>
<text class="h" x="32" y="166">HW-390 #2</text><text class="m" x="32" y="182">sealed · 5mA</text>
<rect class="box" x="20" y="202" width="152" height="44" rx="5"/>
<text class="h" x="32" y="222">HW-390 #3</text><text class="m" x="32" y="238">sealed · 5mA</text>
<rect class="box" x="20" y="258" width="152" height="44" rx="5"/>
<text class="h" x="32" y="278">HW-390 #4</text><text class="m" x="32" y="294">sealed · 5mA</text>
<rect class="box" x="20" y="314" width="152" height="44" rx="5"/>
<text class="h" x="32" y="334">HW-390 #5</text><text class="m" x="32" y="350">sealed · 5mA</text>
<rect class="box" x="20" y="370" width="152" height="44" rx="5"/>
<text class="h" x="32" y="390">HW-390 #6</text><text class="m" x="32" y="406">sealed · 5mA</text>
</g>
<text class="s" x="20" y="437">Each: XC6206 LDO 5V→3.3V on-board.</text>
<text class="s" x="20" y="452">Upper PCB epoxy-sealed; prongs bare.</text>
<!-- ============ cat5 funnel ============ -->
<text class="h" x="186" y="80">Cat5 24AWG</text>
<text class="s" x="186" y="96">8 conductors · T568B</text>
<path class="sig" d="M172,112 Q212,112 244,252"/>
<path class="sig" d="M172,168 Q212,168 244,252"/>
<path class="sig" d="M172,224 Q216,224 244,252"/>
<path class="sig" d="M172,280 Q216,280 244,252"/>
<path class="sig" d="M172,336 Q212,336 244,252"/>
<path class="sig" d="M172,392 Q212,392 244,252"/>
<path class="cat5" d="M244,252 L300,252"/>
<!-- ============ breakout ============ -->
<rect class="boxhi" x="300" y="112" width="184" height="302" rx="6"/>
<text class="h" x="314" y="136">RJ45 breakout</text>
<text class="s" x="314" y="153">at MCU enclosure</text>
<rect x="314" y="166" width="156" height="96" rx="4" fill="#ffffff" stroke="#1d4ed8" stroke-width="1.2"/>
<text class="h" x="326" y="187">6× RC low-pass</text>
<text class="m" x="326" y="205">10 kΩ series</text>
<text class="m" x="326" y="221">0.1 µF to GND</text>
<text class="m" x="326" y="237">at the ADC pin</text>
<text class="m" x="326" y="253" fill="#c2410c">MANDATORY §3.1</text>
<rect x="314" y="274" width="156" height="58" rx="4" fill="#ffffff" stroke="#374151" stroke-width="1.2"/>
<text class="h" x="326" y="294">Star ground</text>
<text class="m" x="326" y="312">single point, no</text>
<text class="m" x="326" y="326">daisy-chaining</text>
<text class="m" x="314" y="356" fill="#c2410c">pin 2 → +5V (switched)</text>
<text class="m" x="314" y="373" fill="#374151">pin 1 → GND</text>
<text class="m" x="314" y="390" fill="#1d4ed8">pins 3-8 → 6 signals</text>
<!-- breakout to mcu -->
<path class="sig" d="M484,252 L522,252" marker-end="url(#arrowb)"/>
<!-- ============ esp32 ============ -->
<rect class="boxhi" x="530" y="112" width="180" height="302" rx="6"/>
<text class="h" x="544" y="136">ESP32-S3</text>
<text class="s" x="544" y="153">YD-ESP32-S3 (WROOM-1)</text>
<rect x="544" y="166" width="152" height="122" rx="4" fill="#ffffff" stroke="#1d4ed8" stroke-width="1.2"/>
<text class="h" x="556" y="186">ADC1 only</text>
<text class="s" x="556" y="201">ADC2 is owned by Wi-Fi</text>
<text class="m" x="556" y="221">GPIO4-9 moisture 1-6</text>
<text class="m" x="556" y="238">GPIO10 battery÷2</text>
<text class="m" x="556" y="255">GPIO14 rail MOSFET</text>
<text class="m" x="556" y="272">GPIO48 status LED</text>
<rect x="544" y="300" width="152" height="100" rx="4" fill="#ffffff" stroke="#374151" stroke-width="1.2"/>
<text class="h" x="556" y="320">Firmware</text>
<text class="m" x="556" y="338">16× oversample</text>
<text class="m" x="556" y="354">trim 10% outliers</text>
<text class="m" x="556" y="370">eFuse ADC cal</text>
<text class="m" x="556" y="386">2-point curve → %</text>
<!-- ============ wifi ============ -->
<path class="data" d="M710,220 L770,180" marker-end="url(#arrowg)"/>
<text class="s" x="716" y="248">Wi-Fi</text>
<text class="s" x="716" y="262">MQTT</text>
<!-- ============ broker + HA ============ -->
<rect class="boxha" x="790" y="130" width="170" height="86" rx="6"/>
<text class="h" x="804" y="153">MQTT broker</text>
<text class="s" x="804" y="170">Mosquitto</text>
<text class="m" x="804" y="190">soil/&lt;node&gt;/state</text>
<text class="m" x="804" y="206">retained JSON</text>
<path class="data" d="M875,216 L875,266" marker-end="url(#arrowg)"/>
<rect class="boxha" x="790" y="270" width="170" height="120" rx="6"/>
<text class="h" x="804" y="293">Home Assistant</text>
<text class="s" x="804" y="310">auto-discovery, no YAML</text>
<text class="m" x="804" y="331">moisture 1-6 %</text>
<text class="m" x="804" y="348">battery level %</text>
<text class="m" x="804" y="365">wifi signal dBm</text>
<text class="m" x="804" y="382">uptime s</text>
<!-- ============ power path ============ -->
<!-- Flow runs right-to-left along the bottom: source -> MOSFET -> breakout -> Cat5 -->
<rect class="boxpw" x="530" y="440" width="180" height="76" rx="6"/>
<text class="h" x="544" y="462">Power source</text>
<text class="m" x="544" y="481">mains: 5V USB / buck</text>
<text class="m" x="544" y="497">remote: 18650 + solar</text>
<text class="m" x="544" y="512">→ boost to 5V</text>
<path class="pwr" d="M530,470 L492,470" marker-end="url(#arrowr)"/>
<rect class="boxpw" x="300" y="440" width="184" height="76" rx="6"/>
<text class="h" x="314" y="462">Switched 5V rail</text>
<text class="m" x="314" y="481">P-FET high-side</text>
<text class="m" x="314" y="497">HIGH = sensors on</text>
<text class="m" x="314" y="512">floating = off</text>
<path class="pwr" d="M392,440 L392,418" marker-end="url(#arrowr)"/>
<!-- GPIO14 gate control: out the right of the MCU, under both boxes, up into the FET -->
<path class="pwr dash" d="M690,414 L730,414 L730,548 L430,548 L430,522" marker-end="url(#arrowr)"/>
<text class="m" x="560" y="542" fill="#c2410c">GPIO14 gate</text>
<!-- ============ legend ============ -->
<line x1="20" y1="575" x2="46" y2="575" stroke="#1d4ed8" stroke-width="2.2"/>
<text class="s" x="54" y="579">analog signal</text>
<line x1="152" y1="575" x2="178" y2="575" stroke="#c2410c" stroke-width="2.2"/>
<text class="s" x="186" y="579">power</text>
<line x1="248" y1="575" x2="274" y2="575" stroke="#15803d" stroke-width="2.2"/>
<text class="s" x="282" y="579">network / data</text>
<line x1="378" y1="575" x2="404" y2="575" stroke="#c2410c" stroke-width="2.2" stroke-dasharray="5 4"/>
<text class="s" x="412" y="579">control</text>
<defs>
<marker id="arrowb" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto">
<path d="M0,0 L9,4.5 L0,9 z" fill="#1d4ed8"/>
</marker>
<marker id="arrowr" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto">
<path d="M0,0 L9,4.5 L0,9 z" fill="#c2410c"/>
</marker>
<marker id="arrowg" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto">
<path d="M0,0 L9,4.5 L0,9 z" fill="#15803d"/>
</marker>
</defs>
</svg>