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:
commit
ad9c457ea7
24 files changed
+2178
No files matched your search
@@ -0,0 +1,131 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 710" width="900" height="710" role="img" aria-labelledby="t5">
|
||||
<title id="t5">Firmware control flow for the mains and battery power profiles</title>
|
||||
<style>
|
||||
.bg { fill: #fcfcfa; }
|
||||
.ttl { font: 600 19px ui-sans-serif, system-ui, 'Segoe UI', Roboto, sans-serif; fill: #16181d; }
|
||||
.col { font: 600 14px ui-sans-serif, system-ui, 'Segoe UI', Roboto, sans-serif; fill: #16181d; }
|
||||
.h { font: 600 12.5px ui-sans-serif, system-ui, 'Segoe UI', Roboto, sans-serif; fill: #16181d; }
|
||||
.s { font: 11px ui-sans-serif, system-ui, 'Segoe UI', Roboto, sans-serif; fill: #6b7280; }
|
||||
.m { font: 10.5px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; fill: #374151; }
|
||||
.node { fill: #f3f4f6; stroke: #16181d; stroke-width: 1.5; }
|
||||
.nsig { fill: #eef2ff; stroke: #1d4ed8; stroke-width: 1.6; }
|
||||
.npw { fill: #fff3ec; stroke: #c2410c; stroke-width: 1.6; }
|
||||
.nnet { fill: #ecfdf3; stroke: #15803d; stroke-width: 1.6; }
|
||||
.flow { fill: none; stroke: #16181d; stroke-width: 1.6; }
|
||||
.alt { fill: none; stroke: #c2410c; stroke-width: 1.6; stroke-dasharray: 5 4; }
|
||||
</style>
|
||||
|
||||
<rect class="bg" x="0" y="0" width="900" height="710"/>
|
||||
<text class="ttl" x="20" y="30">Firmware flow — one compile-time switch, two shapes</text>
|
||||
<text class="s" x="20" y="49">POWER_PROFILE_MAINS keeps setup() short and does the work in loop(). POWER_PROFILE_BATTERY does everything in setup() and never reaches loop().</text>
|
||||
|
||||
<!-- divider -->
|
||||
<line x1="450" y1="72" x2="450" y2="676" stroke="#d1d5db" stroke-width="1.4" stroke-dasharray="4 4"/>
|
||||
|
||||
<!-- ================= MAINS ================= -->
|
||||
<text class="col" x="30" y="94">-e mains · continuous power</text>
|
||||
|
||||
<rect class="node" x="30" y="108" width="200" height="38" rx="5"/>
|
||||
<text class="h" x="44" y="132">boot → setup()</text>
|
||||
<path class="flow" d="M130,146 L130,168" marker-end="url(#a)"/>
|
||||
|
||||
<rect class="npw" x="30" y="168" width="200" height="52" rx="5"/>
|
||||
<text class="h" x="44" y="189">power::begin()</text>
|
||||
<text class="m" x="44" y="207">gate HIGH, rail stays on</text>
|
||||
<path class="flow" d="M130,220 L130,242" marker-end="url(#a)"/>
|
||||
|
||||
<rect class="node" x="30" y="242" width="200" height="52" rx="5"/>
|
||||
<text class="h" x="44" y="263">node_id::begin()</text>
|
||||
<text class="m" x="44" y="281">MAC → soil-node-a1b2c3</text>
|
||||
<path class="flow" d="M130,294 L130,316" marker-end="url(#a)"/>
|
||||
|
||||
<rect class="nnet" x="30" y="316" width="200" height="52" rx="5"/>
|
||||
<text class="h" x="44" y="337">Wi-Fi + MQTT connect</text>
|
||||
<text class="m" x="44" y="355">LWT → availability topic</text>
|
||||
<path class="flow" d="M130,368 L130,390" marker-end="url(#a)"/>
|
||||
|
||||
<rect class="nnet" x="30" y="390" width="200" height="52" rx="5"/>
|
||||
<text class="h" x="44" y="411">publish discovery</text>
|
||||
<text class="m" x="44" y="429">retained, homeassistant/</text>
|
||||
<path class="flow" d="M130,442 L130,464" marker-end="url(#a)"/>
|
||||
|
||||
<rect class="node" x="30" y="464" width="200" height="38" rx="5"/>
|
||||
<text class="h" x="44" y="488">loop()</text>
|
||||
<path class="flow" d="M130,502 L130,524" marker-end="url(#a)"/>
|
||||
|
||||
<rect class="nsig" x="30" y="524" width="200" height="52" rx="5"/>
|
||||
<text class="h" x="44" y="545">every 60 s: sample</text>
|
||||
<text class="m" x="44" y="563">+ publish state JSON</text>
|
||||
|
||||
<!-- loop back -->
|
||||
<path class="flow" d="M230,550 L256,550 L256,483 L230,483" marker-end="url(#a)"/>
|
||||
<text class="s" x="262" y="520">repeat</text>
|
||||
|
||||
<!-- reconnect branch, routed clear of the repeat loop -->
|
||||
<path class="alt" d="M230,566 L330,566 L330,342 L230,342" marker-end="url(#ar)"/>
|
||||
<text class="s" x="338" y="436">on drop:</text>
|
||||
<text class="s" x="338" y="452">reconnect +</text>
|
||||
<text class="s" x="338" y="468">re-announce</text>
|
||||
|
||||
<!-- ================= BATTERY ================= -->
|
||||
<text class="col" x="482" y="94">-e battery · solar / 18650</text>
|
||||
|
||||
<rect class="node" x="482" y="108" width="212" height="38" rx="5"/>
|
||||
<text class="h" x="496" y="132">RTC wake → setup()</text>
|
||||
<path class="flow" d="M588,146 L588,166" marker-end="url(#a)"/>
|
||||
|
||||
<rect class="npw" x="482" y="166" width="212" height="52" rx="5"/>
|
||||
<text class="h" x="496" y="187">armAwakeBudget()</text>
|
||||
<text class="m" x="496" y="205">30 s ceiling on the cycle</text>
|
||||
<path class="flow" d="M588,218 L588,238" marker-end="url(#a)"/>
|
||||
|
||||
<rect class="npw" x="482" y="238" width="212" height="52" rx="5"/>
|
||||
<text class="h" x="496" y="259">sensorsOn() + settle</text>
|
||||
<text class="m" x="496" y="277">MOSFET on, wait 500 ms</text>
|
||||
<path class="flow" d="M588,290 L588,310" marker-end="url(#a)"/>
|
||||
|
||||
<rect class="nnet" x="482" y="310" width="212" height="52" rx="5"/>
|
||||
<text class="h" x="496" y="331">Wi-Fi + MQTT connect</text>
|
||||
<text class="m" x="496" y="349">expire_after, no LWT</text>
|
||||
<path class="flow" d="M588,362 L588,382" marker-end="url(#a)"/>
|
||||
|
||||
<rect class="nnet" x="482" y="382" width="212" height="52" rx="5"/>
|
||||
<text class="h" x="496" y="403">cold boot? → discovery</text>
|
||||
<text class="m" x="496" y="421">skipped on RTC wake</text>
|
||||
<path class="flow" d="M588,434 L588,454" marker-end="url(#a)"/>
|
||||
|
||||
<rect class="nsig" x="482" y="454" width="212" height="66" rx="5"/>
|
||||
<text class="h" x="496" y="475">oversample + publish</text>
|
||||
<text class="m" x="496" y="493">16 reads, trim 10%,</text>
|
||||
<text class="m" x="496" y="509">map, one JSON publish</text>
|
||||
<path class="flow" d="M588,520 L588,540" marker-end="url(#a)"/>
|
||||
|
||||
<rect class="npw" x="482" y="540" width="212" height="66" rx="5"/>
|
||||
<text class="h" x="496" y="561">flush → sensorsOff()</text>
|
||||
<text class="m" x="496" y="579">→ deep sleep 15 min</text>
|
||||
<text class="m" x="496" y="595">loop() never runs</text>
|
||||
|
||||
<!-- wake cycle back -->
|
||||
<path class="flow" d="M694,573 L744,573 L744,127 L694,127" marker-end="url(#a)"/>
|
||||
<text class="s" x="752" y="346">RTC timer</text>
|
||||
<text class="s" x="752" y="361">wake</text>
|
||||
|
||||
<!-- early-sleep bailout -->
|
||||
<path class="alt" d="M482,336 L458,336 L458,573 L482,573" marker-end="url(#ar)"/>
|
||||
<text class="s" x="466" y="470" transform="rotate(-90 466 470)">network down → sleep early</text>
|
||||
|
||||
<!-- ================= note ================= -->
|
||||
<rect x="20" y="626" width="860" height="62" rx="5" fill="#fff3ec" stroke="#c2410c" stroke-width="1.4"/>
|
||||
<text class="h" x="34" y="646">The awake budget is armed before anything that can block.</text>
|
||||
<text class="s" x="34" y="664">Per-step timeouts don't cover an AP that associates but never issues a DHCP lease, or a broker that completes TCP but never</text>
|
||||
<text class="s" x="34" y="681">sends CONNACK. Without the ceiling, one bad night flattens the cell.</text>
|
||||
|
||||
<defs>
|
||||
<marker id="a" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto">
|
||||
<path d="M0,0 L9,4.5 L0,9 z" fill="#16181d"/>
|
||||
</marker>
|
||||
<marker id="ar" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto">
|
||||
<path d="M0,0 L9,4.5 L0,9 z" fill="#c2410c"/>
|
||||
</marker>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.9 KiB |
Reference in new issue
Block a user