Firmware

Status: 🟒 Stable (architecture) | 🟑 Draft (implementation)


Design principle

The firmware follows one rule: execute exactly one role per wake-up, then sleep.

No persistent loops. No background tasks. No RTOS. The ESP32 wakes, evaluates state, does its job, and goes back to sleep. This is not a simplification β€” it is the correct model for a battery-constrained device.


Finite State Machine (FSM)

Note

The canonical FSM β€” including the full state diagram, wakeup-cause dispatch (CHECK_WAKEUP, READ_SWITCH), and LED table β€” is defined in System Architecture. This section covers the implementation perspective only: what each state does in code and how boot priority is resolved.

States

State Trigger What happens
DATA_LOGGER RTC alarm (EXT0 wakeup) Read sensors β†’ write SD β†’ set next alarm β†’ sleep
SERVER Mode switch in SERVER position (EXT1 wakeup or power-on) Start Wi-Fi AP β†’ serve web UI β†’ no measurements
INIT_RTC RTC not valid, or long button press in SERVER Connect to NTP β†’ set DS3231 β†’ return to SERVER

Boot priority (power-on only)

When wakeup cause is UNDEFINED (fresh power-on) and the mode switch state must be sampled:

1. Mode switch = SERVER?  β†’ SERVER state
2. RTC not valid?         β†’ INIT_RTC state
3. Otherwise             β†’ DATA_LOGGER state

Wake-up cycle (DATA_LOGGER)

sequenceDiagram
    participant RTC as DS3231
    participant FW as Firmware
    participant Sensors
    participant SD

    RTC->>FW: INT pin LOW (alarm)
    FW->>FW: Boot, evaluate FSM β†’ DATA_LOGGER
    FW->>RTC: Clear alarm flag (IΒ²C)
    FW->>RTC: Read current timestamp
    FW->>Sensors: Read all active sensors
    FW->>SD: Mount β†’ open log β†’ append row β†’ close β†’ unmount
    FW->>RTC: Set next alarm (now + interval)
    FW->>FW: Enter deep sleep (EXT0 on RTC INT pin)

Typical active window: 10–30 seconds (dominated by SD mount/write time). The rest of the cycle is deep sleep.


Time management β€” DS3231 RTC

Why an external RTC is mandatory

Issue ESP32 internal timer DS3231
Survives power loss ❌ βœ… (CR2032 backup)
Drift over deep sleep High < 2 ppm
Hardware wake alarm ❌ βœ…
Absolute timestamp ❌ βœ…

The DS3231 is the sole time authority in the system. Timestamps on the SD card always come from the RTC β€” never from the ESP32’s internal counter.

Alarm scheduling strategy

After every measurement cycle:

next_alarm = RTC.now() + measurement_interval
RTC.setAlarm(next_alarm)

This β€œrolling alarm” strategy is robust: if the ESP32 crashes mid-cycle or is reset, the next boot will simply evaluate the FSM state and continue. No state machine corruption possible.

RTC initialization (INIT_RTC)

Triggered when RTC.isValid() == false or user requests resync:

  1. Enable Wi-Fi in station mode
  2. Connect to home network (credentials stored in flash)
  3. Query NTP server (pool.ntp.org)
  4. Set DS3231 via IΒ²C
  5. Mark RTC as valid
  6. Disable Wi-Fi
  7. Return to SERVER state
Warning

Wi-Fi is used only during INIT_RTC. It is never enabled during normal DATA_LOGGER cycles. This is a deliberate power and reliability choice.


Data logging β€” SD card

File format

Single append-only file. One row per measurement cycle.

YYYY-MM-DD HH:MM:SS,TEMP,HUM,PRES,LIGHT,UV_IDX,SOIL,RAIN,WIND_SPD,WIND_DIR

Example row (Phase 2, all sensors active):

2026-05-13 10:25:00,18.4,62.1,1013.2,12400,2.1,54.3,0.0,0.0,0

Unused sensor columns are written as empty or -1 until the corresponding hardware is installed. This keeps the schema stable across all phases.

Write procedure

Mount SD
Open file (append mode)
Write row(s)
Flush
Close file
Unmount SD

SD card is mounted and unmounted every cycle. This minimizes corruption risk β€” a power loss between cycles loses at most one measurement.

Data retention

Data is never deleted automatically. In SERVER mode, the web UI allows the user to: - List log files - Download a file - Delete a file (user-initiated only)


SERVER mode

What it does

  • ESP32 creates a Wi-Fi access point (SSID: WeatherStation, password: configurable)
  • A minimal HTTP server runs on port 80
  • No measurements occur during SERVER mode

Web interface (minimal)

Endpoint Action
GET / Status page: firmware version, RTC time, SD free space
GET /files List log files on SD
GET /download?file=X Download a specific log file
DELETE /delete?file=X Delete a log file (optional)
POST /sync-rtc Trigger INIT_RTC sequence

The web UI is intentionally minimal. A phone browser is sufficient β€” no app required.


Status LED

System state LED behavior
DATA_LOGGER Off (deep sleep)
SERVER Steady on
INIT_RTC Slow blink (500 ms)
Critical error Fast blink (100 ms)

The LED reflects actual system state, never user intent.


Electrical notes

Pull-up strategy

Signal Pull-up
IΒ²C SDA/SCL External 4.7 kΞ© to 3.3 V
DS3231 INT External 10 kΞ© to 3.3 V
Mode switch Internal (ESP32)
Push button Internal (ESP32)
LED Series 330 Ξ© resistor

Debounce

All digital inputs are debounced in firmware: - Buttons: 20–50 ms software delay - Rain gauge / anemometer pulses: minimum inter-pulse filter (~100 ms)


Firmware structure (suggested)

src/
β”œβ”€β”€ main.cpp          # Boot: FSM evaluation and dispatch
β”œβ”€β”€ fsm/
β”‚   β”œβ”€β”€ data_logger.cpp   # Sensor read + SD write + alarm set
β”‚   β”œβ”€β”€ server.cpp        # Wi-Fi AP + web server
β”‚   └── init_rtc.cpp      # NTP sync + DS3231 set
β”œβ”€β”€ drivers/
β”‚   β”œβ”€β”€ rtc.cpp           # DS3231 abstraction
β”‚   β”œβ”€β”€ sensors.cpp       # All sensor reads (BME280, BH1750, etc.)
β”‚   β”œβ”€β”€ sd_logger.cpp     # SD mount/write/unmount
β”‚   └── pulse.cpp         # Rain + wind pulse counting
└── config.h              # Pins, intervals, credentials (gitignored)

This structure maps directly to FSM states. Adding a new sensor means touching only drivers/sensors.cpp and the CSV format in sd_logger.cpp.