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

+79
View File
@@ -0,0 +1,79 @@
# Two-point calibration
Implements PRD §3.3. Do this **once per probe**, after sealing them (§2.3) and
with the probe connected through its final Cat5 run — the cable and the RC
filter both shift the numbers, so calibrating on the bench with short leads
produces values that are wrong in the field.
## Why per-probe
`CAL_AIR_MV` / `CAL_WATER_MV` in `include/config.h` are arrays, not scalars.
HW-390s vary: each has its own XC6206 LDO, its own coating thickness, and its
own cable run. Using one shared pair of endpoints for six probes typically
costs 5–15% of absolute accuracy, which is enough to trigger irrigation on a
bed that is already wet.
The defaults shipped in `config.h` are placeholders. They compile and produce
plausible-looking output, which makes them dangerous — plausible output is
exactly what stops you noticing you skipped this step.
## Procedure
1. Flash the mains build and let the node come up:
```
pio run -e mains -t upload -t monitor
```
2. In Home Assistant, find the **Raw mV 1–6** diagnostic entities on the node's
device page. (These exist for this procedure; set `PUBLISH_RAW_MV 0` in
`config.h` to hide them once you are done.) You can also read them straight
off the serial monitor in the `state:` line.
3. **Air point (0%).** Probe clean, dry, in open air. Wait ~30 s for the value
to settle. Record `raw_mv_N` → this is `CAL_AIR_MV[N-1]`.
4. **Water point (100%).** Submerge the probe in a glass of water **up to but
not past the marked line** on the PCB. Do not submerge the electronics.
Wait ~30 s. Record `raw_mv_N` → this is `CAL_WATER_MV[N-1]`.
5. Paste both arrays into `include/config.h` and reflash.
## Expected values
A healthy 3.3 V HW-390 v2.0 reads roughly:
| Condition | Typical |
|-----------|---------|
| Air (dry) | 2400–2800 mV |
| Water | 1000–1400 mV |
**Air must read higher than water** — the HW-390's output is inverted. The
firmware rejects `CAL_AIR_MV <= CAL_WATER_MV` at runtime and logs an error
rather than silently producing an inverted percentage, because a
moisture curve running backwards is very easy to miss on a dashboard and would
tell your irrigation to water dry soil less.
## Troubleshooting
**Air-to-water spread under ~800 mV.** Usually a probe that is degrading, or
one whose coating crept onto the prongs. A narrow span makes the percentage
noisy no matter how much you oversample.
**Reads ~0 mV.** No power reaching the probe: check the Cat5 5 V pair, the
MOSFET gate, and the splice. The firmware reports this as *unknown* rather than
0%, which is the giveaway.
**Reads ~3300 mV (rail).** Signal line shorted to the 5 V pair, or the probe's
output is disconnected and the ADC pin is floating up.
**Reading drifts down over weeks with no rain.** Moisture ingress into an
imperfectly sealed board. Re-seal per `WIRING.md` §6 and recalibrate.
## Fault margin
`SENSOR_FAULT_MARGIN_MV` (default 400 mV) sets how far outside the calibrated
window a reading may sit before it is reported as unavailable instead of being
clamped. Soil genuinely can read slightly outside the air/water endpoints —
saturated clay can go below the water point — so the margin exists to avoid
false faults. Widen it if healthy probes flap to *unknown*; narrow it if real
failures are being clamped into believable numbers.
+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.
+113
View File
@@ -0,0 +1,113 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 940 520" width="940" height="520" role="img" aria-labelledby="t4">
<title id="t4">Solar and battery power topology with boost converter and battery sense divider</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; }
.box { fill: #f3f4f6; stroke: #16181d; stroke-width: 1.6; }
.boxp { fill: #fff3ec; stroke: #c2410c; stroke-width: 1.8; }
.boxs { fill: #eef2ff; stroke: #1d4ed8; stroke-width: 1.8; }
.pw { fill: none; stroke: #c2410c; stroke-width: 2.2; }
.sigw { fill: none; stroke: #1d4ed8; stroke-width: 1.8; }
.gw { fill: none; stroke: #374151; stroke-width: 1.9; }
.comp { fill: #ffffff; stroke: #16181d; stroke-width: 1.5; }
</style>
<rect class="bg" x="0" y="0" width="940" height="520"/>
<text class="ttl" x="20" y="30">Solar / battery topology — remote node</text>
<text class="s" x="20" y="49">The cell cannot feed the probes directly: an 18650 runs 3.0–4.2V and the HW-390's XC6206 LDO needs ~3.4V of headroom.</text>
<!-- ============ chain ============ -->
<rect class="boxp" x="24" y="96" width="150" height="80" rx="6"/>
<text class="h" x="38" y="120">Solar panel</text>
<text class="m" x="38" y="139">6V / 1–2W</text>
<text class="s" x="38" y="158">sized for winter, not</text>
<text class="s" x="38" y="171">for a sunny afternoon</text>
<path class="pw" d="M174,136 L216,136" marker-end="url(#ar)"/>
<rect class="boxp" x="222" y="96" width="160" height="80" rx="6"/>
<text class="h" x="236" y="120">TP4056 + DW01</text>
<text class="m" x="236" y="139">CC/CV charger</text>
<text class="s" x="236" y="158">protection variant only</text>
<text class="s" x="236" y="171">— no MPPT</text>
<path class="pw" d="M382,136 L424,136" marker-end="url(#ar)"/>
<rect class="boxp" x="430" y="96" width="140" height="80" rx="6"/>
<text class="h" x="444" y="120">18650 Li-Ion</text>
<text class="m" x="444" y="139">3.0 – 4.2 V</text>
<text class="s" x="444" y="158">VBAT node</text>
<!-- split to MCU and boost -->
<circle cx="600" cy="136" r="4" fill="#c2410c"/>
<path class="pw" d="M570,136 L600,136"/>
<path class="pw" d="M600,136 L600,86 L716,86" marker-end="url(#ar)"/>
<path class="pw" d="M600,136 L600,206 L716,206" marker-end="url(#ar)"/>
<rect class="boxs" x="722" y="56" width="192" height="62" rx="6"/>
<text class="h" x="736" y="79">ESP32-S3 · 5V/VIN</text>
<text class="s" x="736" y="97">onboard reg accepts VBAT</text>
<text class="m" x="736" y="112">deep sleep ≈ 10 µA</text>
<rect class="boxp" x="722" y="176" width="192" height="62" rx="6"/>
<text class="h" x="736" y="199">Boost converter → 5V</text>
<text class="s" x="736" y="217">on the SWITCHED side of</text>
<text class="s" x="736" y="231">the P-FET, not before it</text>
<path class="pw" d="M818,238 L818,282" marker-end="url(#ar)"/>
<rect class="boxp" x="722" y="288" width="192" height="54" rx="6"/>
<text class="h" x="736" y="310">P-FET → Cat5 pin 2</text>
<text class="s" x="736" y="329">see power-switch.svg</text>
<!-- ============ battery sense divider ============ -->
<path class="sigw" d="M500,176 L500,222"/>
<circle cx="500" cy="176" r="4" fill="#c2410c"/>
<text class="h" x="300" y="228">Battery sense divider</text>
<rect class="comp" x="480" y="222" width="40" height="52" rx="2"/>
<text class="m" x="528" y="252">R1 1 MΩ</text>
<path class="sigw" d="M500,274 L500,302"/>
<circle cx="500" cy="302" r="4" fill="#1d4ed8"/>
<path class="sigw" d="M500,302 L640,302" marker-end="url(#ab)"/>
<text class="h" x="648" y="299">GPIO10</text>
<text class="s" x="648" y="315">ADC1_CH9</text>
<!-- shunt cap -->
<path class="sigw" d="M566,302 L566,336"/>
<circle cx="566" cy="302" r="3.4" fill="#1d4ed8"/>
<line x1="555" y1="336" x2="577" y2="336" stroke="#16181d" stroke-width="1.8"/>
<line x1="555" y1="342" x2="577" y2="342" stroke="#16181d" stroke-width="1.8"/>
<path class="gw" d="M566,342 L566,368"/>
<use xlink:href="#gnd" href="#gnd" x="566" y="368"/>
<text class="m" x="584" y="342">1 µF</text>
<rect class="comp" x="480" y="302" width="40" height="52" rx="2"/>
<text class="m" x="410" y="332">R2 1 MΩ</text>
<path class="gw" d="M500,354 L500,382"/>
<use xlink:href="#gnd" href="#gnd" x="500" y="382"/>
<!-- ============ notes ============ -->
<rect x="20" y="410" width="900" height="90" rx="5" fill="#fff3ec" stroke="#c2410c" stroke-width="1.4"/>
<text class="h" x="34" y="431">Why 1 MΩ and not the usual 100 kΩ.</text>
<text class="s" x="34" y="450">A 100 kΩ/100 kΩ divider draws ~21 µA continuously — more than the sleeping MCU itself (~10 µA), and enough to dominate</text>
<text class="s" x="34" y="468">the winter power budget. 1 MΩ/1 MΩ draws ~2 µA. The 1 µF cap is not optional at that impedance: it supplies the ADC's sampling charge.</text>
<text class="s" x="34" y="486">Trim BATTERY_DIVIDER_RATIO in config.h against a meter to cancel resistor tolerance.</text>
<defs>
<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>
<marker id="ab" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto">
<path d="M0,0 L9,4.5 L0,9 z" fill="#1d4ed8"/>
</marker>
<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: 5.9 KiB

+131
View File
@@ -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

+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

+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

+164
View File
@@ -0,0 +1,164 @@
<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>

After

Width:  |  Height:  |  Size: 8.9 KiB