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,134 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
// ===========================================================================
|
||||
// Compile-time power profile (PRD 4)
|
||||
// Exactly one of these is set by the PlatformIO environment. See platformio.ini.
|
||||
// ===========================================================================
|
||||
#if defined(POWER_PROFILE_MAINS) && defined(POWER_PROFILE_BATTERY)
|
||||
#error "Define only one of POWER_PROFILE_MAINS / POWER_PROFILE_BATTERY"
|
||||
#endif
|
||||
#if !defined(POWER_PROFILE_MAINS) && !defined(POWER_PROFILE_BATTERY)
|
||||
#error "No power profile selected. Build with -e mains or -e battery."
|
||||
#endif
|
||||
|
||||
// ===========================================================================
|
||||
// Pin map -- VCC-GND Studio YD-ESP32-S3 (ESP32-S3-WROOM-1)
|
||||
//
|
||||
// Every sensor input sits on ADC1. This is not a style choice: ADC2 is owned
|
||||
// by the Wi-Fi radio on the ESP32-S3, and analogRead() on an ADC2 pin returns
|
||||
// garbage (or blocks) whenever the radio is active. ADC1 is GPIO1..GPIO10,
|
||||
// which is exactly enough for 6 sensors + a battery divider.
|
||||
//
|
||||
// Also avoided on this board:
|
||||
// GPIO0 strapping / BOOT button
|
||||
// GPIO3 strapping (JTAG source select)
|
||||
// GPIO19/20 native USB D-/D+
|
||||
// GPIO33..37 octal flash/PSRAM on N*R8 variants
|
||||
// GPIO43/44 UART0 TX/RX (serial monitor)
|
||||
// GPIO45/46 strapping (VDD_SPI / boot log)
|
||||
// ===========================================================================
|
||||
constexpr uint8_t SENSOR_COUNT = 6;
|
||||
|
||||
// Sensor 1..6 -> ADC1_CH3..CH8
|
||||
constexpr uint8_t SENSOR_ADC_PINS[SENSOR_COUNT] = {4, 5, 6, 7, 8, 9};
|
||||
|
||||
// Battery sense via 2:1 divider -> ADC1_CH9. Battery profile only.
|
||||
constexpr uint8_t BATTERY_ADC_PIN = 10;
|
||||
|
||||
// Gate of the P-channel MOSFET (or high-side load switch) feeding the 5V
|
||||
// sensor rail. HIGH = sensors powered. Battery profile only; on mains the
|
||||
// rail is simply left on. Needs an external pulldown/pullup so the rail is
|
||||
// OFF while the MCU is in deep sleep and the pin is floating.
|
||||
constexpr uint8_t SENSOR_POWER_GATE_PIN = 14;
|
||||
|
||||
// Onboard addressable RGB LED. Used only for coarse boot/error feedback.
|
||||
constexpr uint8_t STATUS_LED_PIN = 48;
|
||||
|
||||
// ===========================================================================
|
||||
// ADC sampling & smoothing (PRD 3.3)
|
||||
// ===========================================================================
|
||||
|
||||
// Readings taken per sensor per cycle, before outlier rejection.
|
||||
constexpr uint8_t OVERSAMPLE_COUNT = 16;
|
||||
|
||||
// Fraction discarded from each end of the sorted sample set (10% per PRD).
|
||||
// At 16 samples this trims 1 from the top and 1 from the bottom.
|
||||
constexpr uint8_t OVERSAMPLE_TRIM_PERCENT = 10;
|
||||
|
||||
// Microseconds between individual reads within one sensor's burst. Spreads
|
||||
// the burst across mains-hum periods rather than sampling one point on it.
|
||||
constexpr uint32_t OVERSAMPLE_DELAY_US = 2000;
|
||||
|
||||
// Time for the 5V rail and the sensors' XC6206 LDOs to settle after the
|
||||
// MOSFET turns on (PRD 4.2 step 3).
|
||||
constexpr uint32_t SENSOR_STABILIZE_MS = 500;
|
||||
|
||||
// ===========================================================================
|
||||
// Two-point calibration (PRD 3.3)
|
||||
//
|
||||
// HW-390 output is INVERTED: dry/air reads HIGH, submerged reads LOW.
|
||||
// So AIR_MV > WATER_MV, and 0% is at AIR_MV.
|
||||
//
|
||||
// Per-sensor values because each probe's LDO and coating differ. Run
|
||||
// `docs/CALIBRATION.md` once per probe and paste the numbers here.
|
||||
// The defaults below are typical for a 3.3V HW-390 v2.0 and WILL be wrong
|
||||
// for your probes -- they exist so a fresh build compiles and reads
|
||||
// plausibly, not so you can skip calibration.
|
||||
// ===========================================================================
|
||||
constexpr uint16_t CAL_AIR_MV[SENSOR_COUNT] = {2600, 2600, 2600, 2600, 2600, 2600};
|
||||
constexpr uint16_t CAL_WATER_MV[SENSOR_COUNT] = {1200, 1200, 1200, 1200, 1200, 1200};
|
||||
|
||||
// A probe reading outside [WATER_MV - margin, AIR_MV + margin] is reported as
|
||||
// unavailable rather than clamped -- that pattern means a cut Cat5 pair, a
|
||||
// dead LDO, or a probe pulled out of the ground, and silently clamping it to
|
||||
// 0%/100% would hide a fault behind a plausible number.
|
||||
constexpr uint16_t SENSOR_FAULT_MARGIN_MV = 400;
|
||||
|
||||
// ===========================================================================
|
||||
// Battery monitoring (battery profile only)
|
||||
// ===========================================================================
|
||||
|
||||
// Divider ratio: Vbat --[R1]--+--[R2]-- GND, ADC on the junction.
|
||||
// With R1 = R2 = 100k the measured voltage is half the battery voltage.
|
||||
constexpr float BATTERY_DIVIDER_RATIO = 2.0f;
|
||||
|
||||
// Li-Ion 18650 discharge window used for the 0-100% mapping.
|
||||
constexpr uint16_t BATTERY_EMPTY_MV = 3300;
|
||||
constexpr uint16_t BATTERY_FULL_MV = 4200;
|
||||
|
||||
// ===========================================================================
|
||||
// Timing
|
||||
// ===========================================================================
|
||||
#if defined(POWER_PROFILE_MAINS)
|
||||
// Publish cadence while awake (PRD 4.1).
|
||||
constexpr uint32_t PUBLISH_INTERVAL_MS = 60UL * 1000UL;
|
||||
#else
|
||||
// RTC wake interval (PRD 4.2). 15 minutes.
|
||||
constexpr uint64_t DEEP_SLEEP_INTERVAL_S = 15ULL * 60ULL;
|
||||
|
||||
// Hard ceiling on one wake cycle. If Wi-Fi or the broker is unreachable we
|
||||
// sleep anyway instead of sitting at ~80mA until the cell is flat.
|
||||
constexpr uint32_t AWAKE_BUDGET_MS = 30UL * 1000UL;
|
||||
#endif
|
||||
|
||||
constexpr uint32_t WIFI_CONNECT_TIMEOUT_MS = 15UL * 1000UL;
|
||||
constexpr uint32_t MQTT_CONNECT_TIMEOUT_MS = 10UL * 1000UL;
|
||||
|
||||
// ===========================================================================
|
||||
// MQTT / Home Assistant
|
||||
// ===========================================================================
|
||||
|
||||
// Prefix HA listens on for auto-discovery. Matches the HA MQTT integration's
|
||||
// "discovery prefix" setting; only change it if you changed it there.
|
||||
#define HA_DISCOVERY_PREFIX "homeassistant"
|
||||
|
||||
// Root of this project's own state topics.
|
||||
#define MQTT_TOPIC_ROOT "soil"
|
||||
|
||||
// Publish raw millivolts alongside the percentages as diagnostic entities.
|
||||
// Leave on: these are what you read off the HA dashboard when calibrating.
|
||||
#define PUBLISH_RAW_MV 1
|
||||
|
||||
#define FIRMWARE_VERSION "1.0.0"
|
||||
Reference in new issue
Block a user