Skip to content

Commit bf90f66

Browse files
fhirschmannclaude
andcommitted
feat: battery-backed RTC (DS3231) support
Add an optional real-time-clock module so ESPuino keeps the correct time without WiFi/NTP. The DS3231 sits on the existing external I2C bus (i2cBusTwo) and is enabled via the new RTC_ENABLE switch. - new Rtc module: seeds the system clock from the RTC at boot (before WiFi is up); NTP stays the master and disciplines the RTC on every sync and once an hour. RTC stores UTC, timezone applied on top. - MQTT: publishes the current time on the `rtc` state-topic (every minute) plus a Home Assistant discovery sensor. - COMMAND 155 (CMD_RTC_RESYNC) re-disciplines the RTC from system time. - Web: /info exposes RTC availability, time, DS3231 temperature and lost-power flag; shown as a row in the system-information dialog (de/en/fr labels). - platformio: adafruit/RTClib dependency (only compiled with RTC_ENABLE). - README documents the feature. Verified: complete-env builds both with and without RTC_ENABLE; clang-format (v16) clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 85f2c76 commit bf90f66

15 files changed

Lines changed: 224 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ neon logo that doubles as the SVG favicon ([`7be5254`](../../commit/7be5254)):
130130
| Rolling build version (`rN` from the commit count, embedded via `gitVersion.py` and published in the release `version.json`) shown as a navbar badge — green when the device runs the latest rolling release, amber when an update is available (passive `/version` check); clicking the amber badge starts the GitHub OTA; also listed in the info dialog | [`b736abc`](../../commit/b736abc) |
131131
| Home Assistant **MQTT discovery**: on connect the device auto-registers all entities under one HA device — track/status/firmware/software/WiFi (+ battery) sensors, volume & LED-brightness numbers, lock & ambient-light switches, equalizer select, and transport/update/shutdown buttons | [`db73db1`](../../commit/db73db1) |
132132
| Consolidated **Settings** tab (UniFi-style): WiFi, MQTT, FTP and Bluetooth settings merged with the general settings into one top tab with centered horizontal sub-tabs (General / Buttons / WiFi / MQTT / FTP / Bluetooth), the button-assignment config moved into its own **Buttons** sub-tab, plus an FTP start/stop button in the control tab; also fixes the sleep-timer dropdown staying open on outside-click and the desktop control-tab volume slider rendering at zero width | [`8b01876`](../../commit/8b01876) |
133+
| Battery-backed **RTC** (DS3231): optional `RTC_ENABLE` adds a real-time clock on the external I²C bus so the time stays correct without WiFi/NTP. The RTC stores UTC and seeds the system clock at boot (before WiFi is up); NTP remains the master and disciplines the RTC on every sync and hourly. Current time (and DS3231 die-temperature) shown in the info dialog, published via the MQTT state-topic `rtc` (+ Home Assistant discovery), and the RTC can be re-disciplined from system time via the bindable command **155** (button/RFID modifier) | [`pending`](../../commit/pending) |
133134

134135
#### Feature highlights
135136

html/locales/de.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,7 @@
622622
"lblVoltage": "Akkuspannung",
623623
"lblCharge": "Ladezustand",
624624
"lblHall": "Hall-Sensor",
625+
"lblRtc": "RTC (Echtzeituhr)",
625626
"title": "Information",
626627
"softwareversion": "ESPuino {{softwareversion}}",
627628
"gitversion": "ESPuino {{gitversion}}",

html/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,7 @@
624624
"lblVoltage": "Battery voltage",
625625
"lblCharge": "Charge level",
626626
"lblHall": "Hall sensor",
627+
"lblRtc": "RTC (real-time clock)",
627628
"title": "Information",
628629
"softwareversion": "ESPuino {{softwareversion}}",
629630
"gitversion": "ESPuino {{gitversion}}",

html/locales/fr.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,7 @@
611611
"lblVoltage": "Tension batterie",
612612
"lblCharge": "Niveau de charge",
613613
"lblHall": "Capteur Hall",
614+
"lblRtc": "RTC (horloge temps réel)",
614615
"title": "Informations",
615616
"softwareversion": "ESPuino {{softwareversion}}",
616617
"gitversion": "ESPuino {{gitversion}}",

html/management.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5656,6 +5656,12 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
56565656
if (info.battery.chargeLevel) add('systeminfo.lblCharge', info.battery.chargeLevel.toFixed(1) + ' %');
56575657
}
56585658
if (info.hallsensor) add('systeminfo.lblHall', 'null ' + info.hallsensor.nullFieldValue + ', actual ' + info.hallsensor.actual + ', diff ' + info.hallsensor.diff);
5659+
if (info.rtc && info.rtc.available) {
5660+
let rtcText = info.rtc.time || '–';
5661+
if (info.rtc.temperature !== undefined) rtcText += ' (' + info.rtc.temperature.toFixed(1) + ' °C)';
5662+
if (info.rtc.lostPower) rtcText += ' ⚠';
5663+
add('systeminfo.lblRtc', rtcText);
5664+
}
56595665

56605666
const html = '<table class="table table-sm table-borderless cp-info-table mb-0"><tbody>' + rows.join('') + '</tbody></table>';
56615667
$('#modalInfoContent').html(html);

platformio.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ lib_deps =
4747
https://github.com/SZenglein/Arduino-MAX17055_Driver.git#60aa4657c42bd61a436831009f8188c1cbbf71dd
4848
https://github.com/tueddy/natsort.git#ebbf6604c573c5315daa8fa77da8f047f202bd63 ; avoid warnings, fork from https://github.com/sourcefrog/natsort.git#cdd8df9
4949
olikraus/U8g2@^2.35.9 ; OLED display (SH1106/SSD1306), only compiled when OLED_ENABLE is set
50+
adafruit/RTClib@^2.1.4 ; battery-backed RTC (DS3231), only compiled when RTC_ENABLE is set
5051

5152
board_build.embed_txtfiles =
5253
managed_components/espressif__esp_insights/server_certs/https_server.crt

src/Cmd.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include "Mqtt.h"
1313
#include "Queues.h"
1414
#include "Rfid.h"
15+
#include "Rtc.h"
1516
#include "System.h"
1617
#include "Web.h"
1718
#include "Wlan.h"
@@ -312,6 +313,16 @@ void Cmd_Action(const uint16_t mod) {
312313
break;
313314
}
314315

316+
case CMD_RTC_RESYNC: {
317+
if (Rtc_IsAvailable()) {
318+
Rtc_SetFromSystemTime();
319+
System_IndicateOk();
320+
} else {
321+
System_IndicateError();
322+
}
323+
break;
324+
}
325+
315326
case CMD_PLAYPAUSE: {
316327
if ((OPMODE_NORMAL == System_GetOperationMode()) || (OPMODE_BLUETOOTH_SOURCE == System_GetOperationMode())) {
317328
AudioPlayer_SetTrackControl(PAUSEPLAY);

src/Mqtt.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,10 @@ static void Mqtt_PublishHassDiscovery(void) {
372372
mqttHassPublish("sensor", "battery_voltage", "\"name\":\"Battery voltage\",\"device_class\":\"voltage\",\"unit_of_measurement\":\"V\",\"entity_category\":\"diagnostic\",\"state_topic\":\"" + String(Mqtt_GetStateTopic(topicBatteryVoltage)) + "\"");
373373
mqttHassPublish("sensor", "battery", "\"name\":\"Battery\",\"device_class\":\"battery\",\"unit_of_measurement\":\"%\",\"entity_category\":\"diagnostic\",\"state_topic\":\"" + String(Mqtt_GetStateTopic(topicBatterySOC)) + "\"");
374374
#endif
375+
376+
#ifdef RTC_ENABLE
377+
mqttHassPublish("sensor", "rtc", "\"name\":\"RTC time\",\"icon\":\"mdi:clock-outline\",\"entity_category\":\"diagnostic\",\"state_topic\":\"" + String(Mqtt_GetStateTopic(topicRtc)) + "\"");
378+
#endif
375379
}
376380
#endif
377381

src/Rtc.cpp

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
#include <Arduino.h>
2+
#include "settings.h"
3+
4+
#include "Rtc.h"
5+
6+
#include "Log.h"
7+
#include "Mqtt.h"
8+
#include "Wlan.h"
9+
10+
#ifdef RTC_ENABLE
11+
#include <RTClib.h>
12+
#include <Wire.h>
13+
14+
#include <time.h>
15+
16+
extern TwoWire i2cBusTwo;
17+
18+
static RTC_DS3231 rtc;
19+
static bool rtcAvailable = false;
20+
21+
// How often the (NTP-disciplined) system time is written back to the RTC
22+
static constexpr uint32_t rtcResyncInterval = 3600UL * 1000UL; // 1h
23+
// How often the current time is published via MQTT
24+
static constexpr uint32_t rtcPublishInterval = 60UL * 1000UL; // 1min
25+
26+
static uint32_t lastResyncTimestamp = 0;
27+
static uint32_t lastPublishTimestamp = 0;
28+
29+
// Returns true if the system clock holds a plausible (NTP-set) time
30+
static bool systemTimeIsValid(struct tm *timeinfo) {
31+
if (!getLocalTime(timeinfo, 5)) {
32+
return false;
33+
}
34+
return (timeinfo->tm_year + 1900) >= 2024;
35+
}
36+
37+
void Rtc_Init(void) {
38+
if (!rtc.begin(&i2cBusTwo)) {
39+
Log_Println("RTC> DS3231 not found on i2cBusTwo", LOGLEVEL_NOTICE);
40+
return;
41+
}
42+
rtcAvailable = true;
43+
44+
if (rtc.lostPower()) {
45+
Log_Println("RTC> DS3231 lost power - waiting for NTP to set the time", LOGLEVEL_NOTICE);
46+
return;
47+
}
48+
49+
// RTC stores UTC; seed the system clock from it so the time is correct
50+
// immediately, even before WiFi/NTP are up.
51+
const DateTime now = rtc.now();
52+
if (now.year() < 2024) {
53+
Log_Println("RTC> stored time implausible - waiting for NTP", LOGLEVEL_NOTICE);
54+
return;
55+
}
56+
struct timeval tv = {.tv_sec = (time_t) now.unixtime(), .tv_usec = 0};
57+
settimeofday(&tv, nullptr);
58+
Log_Printf(LOGLEVEL_NOTICE, "RTC> system time set from DS3231: %04d-%02d-%02d %02d:%02d:%02d UTC", now.year(), now.month(), now.day(), now.hour(), now.minute(), now.second());
59+
}
60+
61+
void Rtc_SetFromSystemTime(void) {
62+
if (!rtcAvailable) {
63+
return;
64+
}
65+
struct tm timeinfo;
66+
if (!systemTimeIsValid(&timeinfo)) {
67+
return;
68+
}
69+
// store UTC in the RTC (DateTime(uint32_t) interprets the value as unix-time)
70+
rtc.adjust(DateTime((uint32_t) time(nullptr)));
71+
Log_Println("RTC> DS3231 disciplined from system time", LOGLEVEL_NOTICE);
72+
}
73+
74+
bool Rtc_IsAvailable(void) {
75+
return rtcAvailable;
76+
}
77+
78+
bool Rtc_LostPower(void) {
79+
return rtcAvailable && rtc.lostPower();
80+
}
81+
82+
float Rtc_GetTemperature(void) {
83+
if (!rtcAvailable) {
84+
return NAN;
85+
}
86+
return rtc.getTemperature();
87+
}
88+
89+
void Rtc_Cyclic(void) {
90+
if (!rtcAvailable) {
91+
return;
92+
}
93+
94+
const uint32_t millisNow = millis();
95+
96+
// Periodically write the (NTP-disciplined) system time back to the RTC to
97+
// compensate for drift. Only when WiFi is up so we trust the system clock.
98+
if (millisNow - lastResyncTimestamp >= rtcResyncInterval) {
99+
lastResyncTimestamp = millisNow;
100+
if (Wlan_IsConnected()) {
101+
Rtc_SetFromSystemTime();
102+
}
103+
}
104+
105+
// Publish the current time via MQTT
106+
#ifdef MQTT_ENABLE
107+
if (millisNow - lastPublishTimestamp >= rtcPublishInterval) {
108+
lastPublishTimestamp = millisNow;
109+
struct tm timeinfo;
110+
if (systemTimeIsValid(&timeinfo)) {
111+
char timeStringBuff[32];
112+
strftime(timeStringBuff, sizeof(timeStringBuff), "%Y-%m-%d %H:%M:%S", &timeinfo);
113+
publishMqtt(topicRtc, timeStringBuff, false);
114+
}
115+
}
116+
#endif
117+
}
118+
119+
#else // RTC_ENABLE not set: provide empty stubs
120+
121+
void Rtc_Init(void) {
122+
}
123+
void Rtc_Cyclic(void) {
124+
}
125+
void Rtc_SetFromSystemTime(void) {
126+
}
127+
128+
bool Rtc_IsAvailable(void) {
129+
return false;
130+
}
131+
132+
bool Rtc_LostPower(void) {
133+
return false;
134+
}
135+
136+
float Rtc_GetTemperature(void) {
137+
return NAN;
138+
}
139+
140+
#endif

src/Rtc.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#pragma once
2+
3+
// Battery-backed hardware real-time-clock (DS3231 on i2cBusTwo).
4+
// Keeps ESPuino's system time correct even without WiFi/NTP. The RTC always
5+
// stores UTC; the system timezone (settings.h: timeZone) is applied on top.
6+
7+
void Rtc_Init(void);
8+
void Rtc_Cyclic(void);
9+
10+
// true if a DS3231 was detected on the bus during Rtc_Init()
11+
bool Rtc_IsAvailable(void);
12+
13+
// true if the RTC lost power since it was last set (time is not trustworthy)
14+
bool Rtc_LostPower(void);
15+
16+
// Write the current system time (UTC) into the RTC. Called after a successful
17+
// NTP-sync (NTP is the master clock) and via CMD_RTC_RESYNC.
18+
void Rtc_SetFromSystemTime(void);
19+
20+
// DS3231 die-temperature in degrees Celsius (NaN if no RTC present)
21+
float Rtc_GetTemperature(void);

0 commit comments

Comments
 (0)