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)
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)
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)
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:
- Enable Wi-Fi in station mode
- Connect to home network (credentials stored in flash)
- Query NTP server (pool.ntp.org)
- Set DS3231 via IΒ²C
- Mark RTC as valid
- Disable Wi-Fi
- Return to SERVER state
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.