Skip to content
This repository was archived by the owner on Aug 3, 2026. It is now read-only.

Commit 02b2714

Browse files
committed
DST-safe sun-event fix
1 parent f455388 commit 02b2714

7 files changed

Lines changed: 461 additions & 38 deletions

File tree

=7.0.0

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
The following files/directories have been created in /tmp/tmpifu8myj4
2+
include - Put project header files here
3+
lib - Put project specific (private) libraries here
4+
src - Put project source files here
5+
platformio.ini - Project Configuration File
6+
Resolving esp32dev dependencies...
7+
Library Manager: Installing git+https://github.com/ESPToolKit/esp-buffer-manager.git
8+
git version 2.43.0
9+
Library Manager: ESPBufferManager@0.1.0+sha.a72e21b has been installed!
10+
Already up-to-date.
11+
Project has been successfully initialized!
12+
Processing esp32dev (platform: https://github.com/pioarduino/platform-espressif32.git; board: esp32dev; framework: arduino)
13+
--------------------------------------------------------------------------------
14+
Verbose mode can be enabled via `-v, --verbose` option
15+
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32dev.html
16+
PLATFORM: Espressif 32 (55.3.37+sha.a9fac07) > Espressif ESP32 Dev Module
17+
HARDWARE: ESP32 240MHz, 320KB RAM, 4MB Flash
18+
DEBUG: Current (cmsis-dap) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
19+
PACKAGES:
20+
- contrib-piohome @ 3.4.4
21+
- framework-arduinoespressif32 @ 3.3.7
22+
- framework-arduinoespressif32-libs @ 5.5.0+sha.87912cd291
23+
- tool-esptoolpy @ 5.1.2
24+
- toolchain-xtensa-esp-elf @ 14.2.0+20251107
25+
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
26+
LDF Modes: Finder ~ chain, Compatibility ~ soft
27+
Library Manager: Installing ArduinoJson
28+
Unpacking 0% 10% 20% 30% 40% 50% 60% 70% 80% 90% 100%
29+
Library Manager: ArduinoJson@7.4.3 has been installed!
30+
Found 46 compatible libraries
31+
Scanning dependencies...
32+
Dependency Graph
33+
|-- ArduinoJson @ 7.4.3
34+
|-- ESPDate @ 1.0.2
35+
Building in release mode
36+
Compiling .pio/build/esp32dev/libaff/ESPBufferManager/esp_buffer_manager/buffer_manager.cpp.o
37+
Compiling .pio/build/esp32dev/libe64/esp-date/esp_date/date.cpp.o
38+
Compiling .pio/build/esp32dev/libe64/esp-date/esp_date/moon.cpp.o
39+
Compiling .pio/build/esp32dev/libe64/esp-date/esp_date/sun.cpp.o
40+
Compiling .pio/build/esp32dev/src/test_esp_date.cpp.o
41+
Building .pio/build/esp32dev/bootloader.bin
42+
Generating partitions .pio/build/esp32dev/partitions.bin
43+
Compiling .pio/build/esp32dev/FrameworkArduino/ColorFormat.c.o
44+
esptool v5.1.2
45+
Creating ESP32 image...
46+
Merged 2 ELF sections.
47+
Successfully created ESP32 image.
48+
Compiling .pio/build/esp32dev/FrameworkArduino/Esp.cpp.o
49+
Compiling .pio/build/esp32dev/FrameworkArduino/FirmwareMSC.cpp.o
50+
Compiling .pio/build/esp32dev/FrameworkArduino/FunctionalInterrupt.cpp.o
51+
Compiling .pio/build/esp32dev/FrameworkArduino/HEXBuilder.cpp.o
52+
Compiling .pio/build/esp32dev/FrameworkArduino/HWCDC.cpp.o
53+
Compiling .pio/build/esp32dev/FrameworkArduino/HardwareSerial.cpp.o
54+
Compiling .pio/build/esp32dev/FrameworkArduino/HashBuilder.cpp.o
55+
Compiling .pio/build/esp32dev/FrameworkArduino/IPAddress.cpp.o
56+
Compiling .pio/build/esp32dev/FrameworkArduino/MD5Builder.cpp.o
57+
Compiling .pio/build/esp32dev/FrameworkArduino/MacAddress.cpp.o
58+
Compiling .pio/build/esp32dev/FrameworkArduino/Print.cpp.o
59+
Compiling .pio/build/esp32dev/FrameworkArduino/Stream.cpp.o
60+
Compiling .pio/build/esp32dev/FrameworkArduino/StreamString.cpp.o
61+
Compiling .pio/build/esp32dev/FrameworkArduino/Tone.cpp.o
62+
Compiling .pio/build/esp32dev/FrameworkArduino/USB.cpp.o
63+
Compiling .pio/build/esp32dev/FrameworkArduino/USBCDC.cpp.o
64+
Compiling .pio/build/esp32dev/FrameworkArduino/USBMSC.cpp.o
65+
Compiling .pio/build/esp32dev/FrameworkArduino/WMath.cpp.o
66+
Compiling .pio/build/esp32dev/FrameworkArduino/WString.cpp.o
67+
Compiling .pio/build/esp32dev/FrameworkArduino/base64.cpp.o
68+
Compiling .pio/build/esp32dev/FrameworkArduino/cbuf.cpp.o
69+
Compiling .pio/build/esp32dev/FrameworkArduino/chip-debug-report.cpp.o
70+
Compiling .pio/build/esp32dev/FrameworkArduino/esp32-hal-adc.c.o
71+
Compiling .pio/build/esp32dev/FrameworkArduino/esp32-hal-bt.c.o
72+
Compiling .pio/build/esp32dev/FrameworkArduino/esp32-hal-cpu.c.o
73+
Compiling .pio/build/esp32dev/FrameworkArduino/esp32-hal-dac.c.o
74+
Compiling .pio/build/esp32dev/FrameworkArduino/esp32-hal-gpio.c.o
75+
Compiling .pio/build/esp32dev/FrameworkArduino/esp32-hal-hosted.c.o
76+
Compiling .pio/build/esp32dev/FrameworkArduino/esp32-hal-i2c-ng.c.o
77+
Compiling .pio/build/esp32dev/FrameworkArduino/esp32-hal-i2c-slave.c.o
78+
Compiling .pio/build/esp32dev/FrameworkArduino/esp32-hal-i2c.c.o
79+
Compiling .pio/build/esp32dev/FrameworkArduino/esp32-hal-ledc.c.o
80+
Compiling .pio/build/esp32dev/FrameworkArduino/esp32-hal-log-wrapper.c.o
81+
Compiling .pio/build/esp32dev/FrameworkArduino/esp32-hal-matrix.c.o
82+
Compiling .pio/build/esp32dev/FrameworkArduino/esp32-hal-misc.c.o
83+
Compiling .pio/build/esp32dev/FrameworkArduino/esp32-hal-periman.c.o
84+
Compiling .pio/build/esp32dev/FrameworkArduino/esp32-hal-psram.c.o
85+
Compiling .pio/build/esp32dev/FrameworkArduino/esp32-hal-rgb-led.c.o
86+
Compiling .pio/build/esp32dev/FrameworkArduino/esp32-hal-rmt.c.o
87+
Compiling .pio/build/esp32dev/FrameworkArduino/esp32-hal-sigmadelta.c.o
88+
Compiling .pio/build/esp32dev/FrameworkArduino/esp32-hal-spi.c.o
89+
Compiling .pio/build/esp32dev/FrameworkArduino/esp32-hal-time.c.o
90+
Compiling .pio/build/esp32dev/FrameworkArduino/esp32-hal-timer.c.o
91+
Compiling .pio/build/esp32dev/FrameworkArduino/esp32-hal-tinyusb.c.o
92+
Compiling .pio/build/esp32dev/FrameworkArduino/esp32-hal-touch-ng.c.o
93+
Compiling .pio/build/esp32dev/FrameworkArduino/esp32-hal-touch.c.o
94+
Compiling .pio/build/esp32dev/FrameworkArduino/esp32-hal-uart.c.o
95+
Compiling .pio/build/esp32dev/FrameworkArduino/firmware_msc_fat.c.o
96+
Compiling .pio/build/esp32dev/FrameworkArduino/freertos_stats.cpp.o
97+
Compiling .pio/build/esp32dev/FrameworkArduino/idf_openthread_mpool_wrapper.c.o
98+
Compiling .pio/build/esp32dev/FrameworkArduino/libb64/cdecode.c.o
99+
Compiling .pio/build/esp32dev/FrameworkArduino/libb64/cencode.c.o
100+
Compiling .pio/build/esp32dev/FrameworkArduino/main.cpp.o
101+
Compiling .pio/build/esp32dev/FrameworkArduino/stdlib_noniso.c.o
102+
Compiling .pio/build/esp32dev/FrameworkArduino/wiring_pulse.c.o
103+
Compiling .pio/build/esp32dev/FrameworkArduino/wiring_shift.c.o
104+
Archiving .pio/build/esp32dev/libFrameworkArduino.a
105+
Indexing .pio/build/esp32dev/libFrameworkArduino.a
106+
Linking .pio/build/esp32dev/firmware.elf
107+
Memory Type Usage Summary
108+
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
109+
┃ Memory Type/Section ┃ Used [bytes] ┃ Used [%] ┃ Remain [bytes] ┃ Total [bytes] ┃
110+
┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
111+
│ Flash Code │ 258196 │ │ │ │
112+
│ .text │ 258196 │ │ │ │
113+
│ Flash Data │ 94352 │ │ │ │
114+
│ .rodata │ 94096 │ │ │ │
115+
│ .appdesc │ 256 │ │ │ │
116+
│ IRAM │ 68559 │ 52.31 │ 62513 │ 131072 │
117+
│ .text │ 67531 │ 51.52 │ │ │
118+
│ .vectors │ 1028 │ 0.78 │ │ │
119+
│ DRAM │ 28310 │ 22.72 │ 96270 │ 124580 │
120+
│ .data │ 18230 │ 14.63 │ │ │
121+
│ .bss │ 10080 │ 8.09 │ │ │
122+
└─────────────────────┴──────────────┴──────────┴────────────────┴───────────────┘
123+
Retrieving maximum program size .pio/build/esp32dev/firmware.elf
124+
Checking size .pio/build/esp32dev/firmware.elf
125+
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
126+
RAM: [= ] 8.6% (used 28312 bytes from 327680 bytes)
127+
Flash: [=== ] 33.5% (used 439081 bytes from 1310720 bytes)
128+
Building .pio/build/esp32dev/firmware.bin
129+
esptool v5.1.2
130+
Creating ESP32 image...
131+
Merged 2 ELF sections.
132+
Successfully created ESP32 image.
133+
========================= [SUCCESS] Took 19.47 seconds =========================

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ The format follows Keep a Changelog and the project adheres to Semantic Versioni
2121
- Added `lastNtpSyncStringLocal/Utc` helpers (buffer + `std::string`) to serialize the last NTP sync timestamp directly.
2222
- Added direct `DateTime::utcString/localString` and `LocalDateTime::localString` methods so standalone values can be formatted without an `ESPDate` round-trip.
2323
- Added focused example sketches: `examples/string_helpers` and `examples/ntp_sync_tracking`.
24+
- Added additive NTP sync listeners via `addNtpSyncListener(...)` / `removeNtpSyncListener(...)` so multiple consumers can observe sync events without replacing the primary callback.
2425

2526
### Changed
2627
- Replaced the `ESPDateConfig` constructor with an explicit `init(const ESPDateConfig&)` so configuration happens after the Arduino runtime is alive, avoiding early SNTP watchdog resets on some boards.
@@ -30,6 +31,7 @@ The format follows Keep a Changelog and the project adheres to Semantic Versioni
3031
- Restored builds by adding the missing internal `utils.h` helpers referenced by the sun/scheduler code paths.
3132
- Resolved ambiguous `setNtpSyncCallback(...)` overload selection for non-capturing lambdas on ESP32 toolchains.
3233
- Added `ESPDate::deinit()` and destructor cleanup so a destroyed active instance releases SNTP callback ownership instead of leaving stale global callback state.
34+
- Sunrise/sunset now resolve UTC results from the event's local wall-clock time instead of the query timestamp offset, which keeps DST transition days stable before and after the clock change.
3335

3436
## [1.0.1] - 2025-12-09
3537
### Added

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ ESPDate is a tiny C++17 helper for ESP32 projects that makes working with dates
1616
- **Formatting / parsing**: ISO-8601 and `YYYY-MM-DD HH:MM:SS` helpers, plus `strftime`-style patterns for UTC or local time.
1717
- **String helpers**: embedded-safe buffer methods and `std::string` convenience wrappers for `DateTime`, `LocalDateTime`, `nowUtc`, and `nowLocal`.
1818
- **Direct value formatting**: `DateTime::localString/utcString` and `LocalDateTime::localString` let individual values format themselves.
19-
- **Sunrise / sunset**: compute daily sun times from lat/lon using numeric offsets or POSIX TZ strings (auto-DST aware).
19+
- **Sunrise / sunset**: compute daily sun times from lat/lon using numeric offsets or POSIX TZ strings (auto-DST aware, resolved at the event time on DST transition days).
2020
- **DST detection**: `isDstActive` reports whether daylight saving time applies using the stored TZ, an explicit POSIX TZ string, or the current system TZ.
2121
- **Moon phase**: `moonPhase` returns the current lunar phase angle and illumination fraction for any moment.
2222
- **Optional NTP bootstrap**: call `init` with `ESPDateConfig` containing `timeZone` and at least one NTP server (`ntpServer`, optional `ntpServer2`/`ntpServer3`) to set TZ and start SNTP after Arduino/WiFi is ready.
23-
- **NTP sync callback + manual re-sync**: register `setNtpSyncCallback(...)` with a function, lambda, or `std::bind`, call `syncNTP()` anytime to trigger an immediate refresh, and optionally override SNTP interval via `ntpSyncIntervalMs` / `setNtpSyncIntervalMs(...)`.
23+
- **NTP sync callback + listeners + manual re-sync**: register `setNtpSyncCallback(...)` plus additive `addNtpSyncListener(...)` observers, call `syncNTP()` anytime to trigger an immediate refresh, and optionally override SNTP interval via `ntpSyncIntervalMs` / `setNtpSyncIntervalMs(...)`.
2424
- **Optional PSRAM-backed config/state buffers**: `ESPDateConfig::usePSRAMBuffers` routes ESPDate-owned text state (timezone/NTP/scoped TZ restore buffers) through `ESPBufferManager` with automatic fallback.
2525
- **Explicit lifecycle cleanup**: `deinit()` unregisters ESPDate-owned SNTP callback hooks, clears runtime config buffers, and is safe to call repeatedly; the destructor calls it automatically.
2626
- **Init-state introspection**: `isInitialized()` reports whether `init(...)` has been called without a matching `deinit()`.
@@ -35,6 +35,7 @@ ESPDate does not configure SNTP by default. Call `init` with a POSIX TZ string p
3535
`syncNTP()` returns `true` only when one or more NTP servers are configured and the runtime supports `configTzTime`.
3636
SNTP exposes a system-level sync hook, so the last `setNtpSyncCallback(...)` registration is the active callback.
3737
For the same reason, `lastNtpSync()` is tracked on the currently active `ESPDate` instance.
38+
`addNtpSyncListener(...)` attaches extra observers on that active instance without replacing the primary callback; use the returned token with `removeNtpSyncListener(...)` to detach them.
3839
Example member-method binding style:
3940
`date.setNtpSyncCallback(std::bind(&App::handleNTPSync, this, std::placeholders::_1));`
4041
Set interval from config or at runtime:
@@ -160,7 +161,7 @@ date.formatLocal(when, ESPDateFormat::DateTime, buf, sizeof(buf));
160161
Serial.printf("Scheduled for local time: %s\n", buf);
161162
```
162163

163-
Sunrise/sunset use your configured TZ (or system TZ) to compute the correct local event, but they return a UTC-backed `DateTime`. Use `formatLocal`/`toLocal` to display those events in local time.
164+
Sunrise/sunset use your configured TZ (or system TZ) to compute the correct local event, but they return a UTC-backed `DateTime`. Use `formatLocal`/`toLocal` to display those events in local time. On DST transition days, ESPDate resolves the UTC result from the event's local wall-clock time, so sunrise/sunset remain stable for the whole local day even if you query before and after the clock change.
164165

165166
## Date & Time Model
166167
`DateTime` is a small value type representing a moment in time, backed by seconds since the Unix epoch:
@@ -212,6 +213,8 @@ public:
212213
bool setNtpSyncIntervalMs(uint32_t intervalMs);
213214
bool hasLastNtpSync() const;
214215
DateTime lastNtpSync() const;
216+
NtpSyncListenerId addNtpSyncListener(const NtpSyncCallable &listener);
217+
bool removeNtpSyncListener(NtpSyncListenerId id);
215218
bool syncNTP();
216219

217220
bool dateTimeToStringUtc(const DateTime &dt, char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const;

src/esp_date/date.cpp

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -71,16 +71,7 @@ void ESPDate::handleSntpSync(struct timeval *tv) {
7171
}
7272
const DateTime syncedAtUtc{syncedEpoch};
7373
if (activeNtpSyncOwner_) {
74-
activeNtpSyncOwner_->lastNtpSync_ = syncedAtUtc;
75-
activeNtpSyncOwner_->hasLastNtpSync_ = true;
76-
}
77-
78-
if (activeNtpSyncCallbackCallable_) {
79-
activeNtpSyncCallbackCallable_(syncedAtUtc);
80-
return;
81-
}
82-
if (activeNtpSyncCallback_) {
83-
activeNtpSyncCallback_(syncedAtUtc);
74+
activeNtpSyncOwner_->dispatchNtpSync(syncedAtUtc);
8475
}
8576
}
8677
#endif
@@ -202,6 +193,11 @@ void ESPDate::deinit() {
202193
ntpSyncCallbackCallable_ = NtpSyncCallable{};
203194
hasLastNtpSync_ = false;
204195
lastNtpSync_ = DateTime{};
196+
nextNtpSyncListenerId_ = 1;
197+
for (size_t i = 0; i < kMaxNtpSyncListeners; ++i) {
198+
ntpSyncListeners_[i].id = 0;
199+
ntpSyncListeners_[i].listener = NtpSyncCallable{};
200+
}
205201
hasLocation_ = false;
206202
latitude_ = 0.0f;
207203
longitude_ = 0.0f;
@@ -307,10 +303,62 @@ DateTime ESPDate::lastNtpSync() const {
307303
return lastNtpSync_;
308304
}
309305

306+
ESPDate::NtpSyncListenerId ESPDate::addNtpSyncListener(const NtpSyncCallable &listener) {
307+
if (!listener) {
308+
return 0;
309+
}
310+
for (size_t i = 0; i < kMaxNtpSyncListeners; ++i) {
311+
if (ntpSyncListeners_[i].id != 0) {
312+
continue;
313+
}
314+
NtpSyncListenerId id = nextNtpSyncListenerId_++;
315+
if (id == 0) {
316+
id = nextNtpSyncListenerId_++;
317+
}
318+
ntpSyncListeners_[i].id = id;
319+
ntpSyncListeners_[i].listener = listener;
320+
return id;
321+
}
322+
return 0;
323+
}
324+
325+
bool ESPDate::removeNtpSyncListener(NtpSyncListenerId id) {
326+
if (id == 0) {
327+
return false;
328+
}
329+
for (size_t i = 0; i < kMaxNtpSyncListeners; ++i) {
330+
if (ntpSyncListeners_[i].id != id) {
331+
continue;
332+
}
333+
ntpSyncListeners_[i].id = 0;
334+
ntpSyncListeners_[i].listener = NtpSyncCallable{};
335+
return true;
336+
}
337+
return false;
338+
}
339+
310340
bool ESPDate::syncNTP() {
311341
return applyNtpConfig();
312342
}
313343

344+
void ESPDate::dispatchNtpSync(const DateTime &syncedAtUtc) {
345+
lastNtpSync_ = syncedAtUtc;
346+
hasLastNtpSync_ = true;
347+
348+
if (activeNtpSyncCallbackCallable_) {
349+
activeNtpSyncCallbackCallable_(syncedAtUtc);
350+
} else if (activeNtpSyncCallback_) {
351+
activeNtpSyncCallback_(syncedAtUtc);
352+
}
353+
354+
for (size_t i = 0; i < kMaxNtpSyncListeners; ++i) {
355+
if (!ntpSyncListeners_[i].listener) {
356+
continue;
357+
}
358+
ntpSyncListeners_[i].listener(syncedAtUtc);
359+
}
360+
}
361+
314362
bool ESPDate::hasAnyNtpServerConfigured() const {
315363
for (size_t i = 0; i < kMaxNtpServers; ++i) {
316364
if (!ntpServers_[i].empty()) {

src/esp_date/date.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ class ESPDate {
7575
public:
7676
using NtpSyncCallback = void (*)(const DateTime &syncedAtUtc);
7777
using NtpSyncCallable = std::function<void(const DateTime &syncedAtUtc)>;
78+
using NtpSyncListenerId = uint32_t;
7879

7980
ESPDate();
8081
~ESPDate();
@@ -103,6 +104,8 @@ class ESPDate {
103104
// Returns the last SNTP sync timestamp (UTC epoch-backed DateTime).
104105
// When hasLastNtpSync() is false this returns DateTime{}.
105106
DateTime lastNtpSync() const;
107+
NtpSyncListenerId addNtpSyncListener(const NtpSyncCallable &listener);
108+
bool removeNtpSyncListener(NtpSyncListenerId id);
106109
// Triggers an immediate NTP sync with the configured server list.
107110
// Returns false when no NTP server is configured or SNTP runtime support is unavailable.
108111
bool syncNTP();
@@ -307,6 +310,7 @@ class ESPDate {
307310
static void handleSntpSync(struct timeval *tv);
308311
#endif
309312
#endif
313+
void dispatchNtpSync(const DateTime &syncedAtUtc);
310314
void setNtpSyncCallbackCallable(const NtpSyncCallable &callback);
311315
bool applyNtpConfig() const;
312316
bool hasAnyNtpServerConfigured() const;
@@ -326,9 +330,21 @@ class ESPDate {
326330
bool hasLastNtpSync_ = false;
327331
NtpSyncCallback ntpSyncCallback_ = nullptr;
328332
NtpSyncCallable ntpSyncCallbackCallable_;
333+
struct NtpSyncListenerSlot {
334+
NtpSyncListenerId id = 0;
335+
NtpSyncCallable listener{};
336+
};
337+
static constexpr size_t kMaxNtpSyncListeners = 4;
338+
NtpSyncListenerSlot ntpSyncListeners_[kMaxNtpSyncListeners]{};
339+
NtpSyncListenerId nextNtpSyncListenerId_ = 1;
329340
static NtpSyncCallback activeNtpSyncCallback_;
330341
static NtpSyncCallable activeNtpSyncCallbackCallable_;
331342
static ESPDate *activeNtpSyncOwner_;
332343
bool hasLocation_ = false;
333344
bool initialized_ = false;
345+
346+
public:
347+
void _testDispatchNtpSync(const DateTime &syncedAtUtc) {
348+
dispatchNtpSync(syncedAtUtc);
349+
}
334350
};

0 commit comments

Comments
 (0)