mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-20 20:42:03 +00:00
refactor(ui-core): move clock tools into ClockEngine; add Core event queue
Alarm / countdown / ring logic moves from UITask to ui-core/ClockEngine.h unchanged. New ui-core/UiEvents.h: fixed-size Core -> frontend event queue; UITask::tickCore() runs UiCore::loop() and reacts to ClockAlert / ClockRingEnded (wake, alert overlay, melody). ClockToolsScreen API unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -38,7 +38,7 @@ interleaved. Classified:
|
|||||||
- Locator — geofence state machine, proximity beeper (`evaluateLocator`, `fireLocator`, targets).
|
- Locator — geofence state machine, proximity beeper (`evaluateLocator`, `fireLocator`, targets).
|
||||||
- Trail — sampling, auto-pause, low-battery auto-save.
|
- Trail — sampling, auto-pause, low-battery auto-save.
|
||||||
- Course over ground — `pushCogFix`, `currentCourse`, `currentLocation`.
|
- Course over ground — `pushCogFix`, `currentCourse`, `currentLocation`.
|
||||||
- Clock tools — alarm / countdown / ring (`evaluateAlarm`, `tickClockTools`, `fireClockAlert`).
|
- Clock tools — alarm / countdown / ring ✅ `ui-core/ClockEngine.h`.
|
||||||
- Ping — `startPing`, `handlePingResult`.
|
- Ping — `startPing`, `handlePingResult`.
|
||||||
- Device controls — GPS on/off, GPIO (`setGpioMode`, bot GPIO), buzzer mode/volume, brightness, radio apply (`applyTxPower`, `applyApc`, `applyRadioParams`, …).
|
- Device controls — GPS on/off, GPIO (`setGpioMode`, bot GPIO), buzzer mode/volume, brightness, radio apply (`applyTxPower`, `applyApc`, `applyRadioParams`, …).
|
||||||
- Bot hooks — `botSetGPS`, `botBuzz`, `botSetGPIO`, …
|
- Bot hooks — `botSetGPS`, `botBuzz`, `botSetGPIO`, …
|
||||||
@@ -151,7 +151,7 @@ checked in the sim plus on L1 hardware before the next one.
|
|||||||
|
|
||||||
0. **Listener boundary** ✅ (upstream `MyMesh::Listener` ported; `MyMesh` has no UI calls left).
|
0. **Listener boundary** ✅ (upstream `MyMesh::Listener` ported; `MyMesh` has no UI calls left).
|
||||||
1. **Skeleton** ✅. `examples/companion_radio/ui-core/` with `UiCore.h` (facade), `MessageHistory.h`, `DmUnreadTable.h`. Header-only for now, reached from `ui-new/UITask.cpp` by relative include, so none of the 66 variant `platformio.ini` files that build `ui-new` change. `UITask` heap-allocates one `UiCore` in `begin()` (before the screens, as `MessagesScreen` used to own the history on the heap); `MessagesScreen` binds to `core().history` by reference. When the Core grows real `.cpp` files, compile them through a unity `.cpp` inside each frontend directory.
|
1. **Skeleton** ✅. `examples/companion_radio/ui-core/` with `UiCore.h` (facade), `MessageHistory.h`, `DmUnreadTable.h`. Header-only for now, reached from `ui-new/UITask.cpp` by relative include, so none of the 66 variant `platformio.ini` files that build `ui-new` change. `UITask` heap-allocates one `UiCore` in `begin()` (before the screens, as `MessagesScreen` used to own the history on the heap); `MessagesScreen` binds to `core().history` by reference. When the Core grows real `.cpp` files, compile them through a unity `.cpp` inside each frontend directory.
|
||||||
2. **Engines, one per commit.** Clock tools → ping → course-over-ground → live share → locator → trail → notifications. `UITask` shrinks to screen management + drawing.
|
2. **Engines, one per commit.** Clock tools ✅ (also introduced `ui-core/UiEvents.h`, the Core → frontend event queue; `UITask::tickCore()` runs `UiCore::loop()` and drains it) → ping → course-over-ground → live share → locator → trail → notifications. `UITask` shrinks to screen management + drawing.
|
||||||
3. **Flip the interface.** `UiCore` implements `AbstractUITask`; `ui-new`'s `UITask` becomes a frontend fed by events.
|
3. **Flip the interface.** `UiCore` implements `AbstractUITask`; `ui-new`'s `UITask` becomes a frontend fed by events.
|
||||||
4. **Settings schema.** Convert `SettingsScreen` section by section.
|
4. **Settings schema.** Convert `SettingsScreen` section by section.
|
||||||
5. **`ui-lvgl` skeleton** for L2 + sim target: boot, home, message list/conversation, keyboard. Then screens by priority.
|
5. **`ui-lvgl` skeleton** for L2 + sim target: boot, home, message list/conversation, keyboard. Then screens by priority.
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
#pragma once
|
||||||
|
// Clock tools engine: the alarm (wall-clock, persisted in NodePrefs), the
|
||||||
|
// countdown timer and the ring window that follows either firing. Runs from
|
||||||
|
// UiCore::loop() every frontend loop so it fires regardless of the current
|
||||||
|
// screen. The stopwatch is pure view state and stays in the frontend.
|
||||||
|
//
|
||||||
|
// The alarm is scheduled as an ABSOLUTE wall instant, recomputed from the stored
|
||||||
|
// time-of-day, so it survives RTC re-syncs (mesh/app/GPS/CLI all jump the
|
||||||
|
// clock) -- small corrections still fire on time, a jump over the target still
|
||||||
|
// fires (late). Timer + ring are millis-based.
|
||||||
|
//
|
||||||
|
// Output: UiEventType::ClockAlert when something fires, ClockRingEnded when the
|
||||||
|
// ring window lapses undismissed. While isRinging() the frontend keeps the
|
||||||
|
// melody going (it owns the buzzer).
|
||||||
|
|
||||||
|
#include "UiEvents.h"
|
||||||
|
|
||||||
|
class ClockEngine {
|
||||||
|
public:
|
||||||
|
static const uint32_t RING_MS = 60000;
|
||||||
|
static const uint32_t ALARM_CATCHUP_SECS = 6 * 3600; // fire late up to 6 h, else reschedule
|
||||||
|
|
||||||
|
void begin(NodePrefs* prefs, UiEventQueue* events) { _prefs = prefs; _events = events; }
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
uint32_t now_ms = millis();
|
||||||
|
// Ring window. Signed-difference compares (like the trail/loc-share timers)
|
||||||
|
// so deadlines landing past the millis() rollover don't read as elapsed.
|
||||||
|
if (_ringing && (int32_t)(now_ms - _ring_until_ms) >= 0) {
|
||||||
|
_ringing = false;
|
||||||
|
_events->push(UiEventType::ClockRingEnded);
|
||||||
|
}
|
||||||
|
// Countdown timer (millis -- sync-immune).
|
||||||
|
if (_timer_running && (int32_t)(now_ms - _timer_deadline_ms) >= 0) {
|
||||||
|
_timer_running = false;
|
||||||
|
fire("Timer done");
|
||||||
|
}
|
||||||
|
// Alarm (wall clock -- absolute schedule for sync robustness).
|
||||||
|
evaluateAlarm();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Alarm ──────────────────────────────────────────────────────────────────
|
||||||
|
void onAlarmChanged() { _alarm_next_fire = 0; } // re-schedule after an alarm edit
|
||||||
|
|
||||||
|
// ── Countdown ──────────────────────────────────────────────────────────────
|
||||||
|
void startTimer(uint32_t duration_ms) { _timer_running = true; _timer_deadline_ms = millis() + duration_ms; }
|
||||||
|
void stopTimer() { _timer_running = false; }
|
||||||
|
bool isTimerRunning() const { return _timer_running; }
|
||||||
|
uint32_t timerRemainingMs() const {
|
||||||
|
if (!_timer_running) return 0;
|
||||||
|
uint32_t now = millis();
|
||||||
|
if ((int32_t)(now - _timer_deadline_ms) >= 0) return 0;
|
||||||
|
return _timer_deadline_ms - now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Ring ───────────────────────────────────────────────────────────────────
|
||||||
|
bool isRinging() const { return _ringing; }
|
||||||
|
void dismissRing() { _ringing = false; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void fire(const char* label) {
|
||||||
|
_ringing = true;
|
||||||
|
_ring_until_ms = millis() + RING_MS;
|
||||||
|
_events->push(UiEventType::ClockAlert, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next absolute wall instant matching alarm_hour:alarm_min in local time,
|
||||||
|
// strictly after now_wall (an alarm set to the current minute waits a day).
|
||||||
|
// With alarm_repeat_mask == 0 that's just tomorrow's occurrence (one-shot).
|
||||||
|
// With a repeat mask set, scan today..+6 days for the next weekday whose bit
|
||||||
|
// is set (struct tm's tm_wday convention, same as the mask) -- today counts
|
||||||
|
// only if its time hasn't already passed.
|
||||||
|
uint32_t computeAlarmNextFire(uint32_t now_wall) const {
|
||||||
|
int tz = _prefs->tz_offset_hours;
|
||||||
|
int64_t now_local = (int64_t)now_wall + (int64_t)tz * 3600;
|
||||||
|
time_t t = (time_t)now_local;
|
||||||
|
struct tm* ti = gmtime(&t);
|
||||||
|
int64_t sod = ti->tm_hour * 3600 + ti->tm_min * 60 + ti->tm_sec; // secs since local midnight
|
||||||
|
int64_t midnight = now_local - sod;
|
||||||
|
int64_t time_of_day = (int64_t)_prefs->alarm_hour * 3600 + (int64_t)_prefs->alarm_min * 60;
|
||||||
|
uint8_t mask = _prefs->alarm_repeat_mask;
|
||||||
|
if (mask != 0) {
|
||||||
|
for (int d = 0; d < 7; d++) {
|
||||||
|
if (mask & (1 << ((ti->tm_wday + d) % 7))) {
|
||||||
|
int64_t target = midnight + (int64_t)d * 86400 + time_of_day;
|
||||||
|
if (target > now_local) return (uint32_t)(target - (int64_t)tz * 3600);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Mask had no bit set (shouldn't happen -- the UI only offers non-empty
|
||||||
|
// presets) -- fall through to the one-shot calculation so it still fires.
|
||||||
|
}
|
||||||
|
int64_t target = midnight + time_of_day;
|
||||||
|
if (target <= now_local) target += 86400;
|
||||||
|
return (uint32_t)(target - (int64_t)tz * 3600);
|
||||||
|
}
|
||||||
|
|
||||||
|
void evaluateAlarm() {
|
||||||
|
if (!_prefs || !_prefs->alarm_on) return;
|
||||||
|
uint32_t now_ms = millis();
|
||||||
|
if (now_ms - _alarm_check_ms < 500) return; // ~2 Hz is plenty for a minute alarm
|
||||||
|
_alarm_check_ms = now_ms;
|
||||||
|
uint32_t now_wall = rtc_clock.getCurrentTime();
|
||||||
|
if (now_wall < 1000000000UL) return; // need a real time sync first
|
||||||
|
if (_alarm_next_fire == 0) _alarm_next_fire = computeAlarmNextFire(now_wall);
|
||||||
|
if (now_wall < _alarm_next_fire) return;
|
||||||
|
if (now_wall - _alarm_next_fire < ALARM_CATCHUP_SECS) {
|
||||||
|
char lbl[20];
|
||||||
|
snprintf(lbl, sizeof(lbl), "Alarm %02d:%02d", _prefs->alarm_hour, _prefs->alarm_min);
|
||||||
|
if (_prefs->alarm_repeat_mask == 0) {
|
||||||
|
_prefs->alarm_on = 0; // one-shot
|
||||||
|
the_mesh.savePrefs();
|
||||||
|
}
|
||||||
|
// Repeating: alarm_on stays set: computeAlarmNextFire() re-arms it for the
|
||||||
|
// next matching weekday.
|
||||||
|
_alarm_next_fire = 0;
|
||||||
|
fire(lbl);
|
||||||
|
} else {
|
||||||
|
// Clock jumped implausibly far past the target -- reschedule rather than
|
||||||
|
// ringing absurdly late.
|
||||||
|
_alarm_next_fire = computeAlarmNextFire(now_wall);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NodePrefs* _prefs = nullptr;
|
||||||
|
UiEventQueue* _events = nullptr;
|
||||||
|
uint32_t _alarm_next_fire = 0; // unix; 0 = (re)compute lazily once time is valid
|
||||||
|
uint32_t _alarm_check_ms = 0; // throttle the wall-clock read to ~2 Hz
|
||||||
|
bool _timer_running = false;
|
||||||
|
uint32_t _timer_deadline_ms = 0;
|
||||||
|
bool _ringing = false;
|
||||||
|
uint32_t _ring_until_ms = 0;
|
||||||
|
};
|
||||||
@@ -9,9 +9,25 @@
|
|||||||
|
|
||||||
#include "MessageHistory.h"
|
#include "MessageHistory.h"
|
||||||
#include "DmUnreadTable.h"
|
#include "DmUnreadTable.h"
|
||||||
|
#include "UiEvents.h"
|
||||||
|
#include "ClockEngine.h"
|
||||||
|
|
||||||
class UiCore {
|
class UiCore {
|
||||||
public:
|
public:
|
||||||
|
void begin(NodePrefs* prefs) {
|
||||||
|
clock.begin(prefs, &events);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Driven from the frontend's loop(), before it drains `events`.
|
||||||
|
void loop() {
|
||||||
|
clock.loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
UiEventQueue events; // Core → frontend; drained by the frontend's loop()
|
||||||
|
|
||||||
|
// ── Engines ───────────────────────────────────────────────────────────────
|
||||||
|
ClockEngine clock; // alarm / countdown / ring
|
||||||
|
|
||||||
// ── Models ────────────────────────────────────────────────────────────────
|
// ── Models ────────────────────────────────────────────────────────────────
|
||||||
MessageHistory history; // channel + DM rings, delivery state, channel unread
|
MessageHistory history; // channel + DM rings, delivery state, channel unread
|
||||||
DmUnreadTable dm_unread; // per-contact DM unread counters
|
DmUnreadTable dm_unread; // per-contact DM unread counters
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#pragma once
|
||||||
|
// Core → frontend events. Engines never call into the frontend (no drawing, no
|
||||||
|
// sound, no display power); they push a tagged event here and the frontend
|
||||||
|
// drains the queue from its own loop() and reacts in its own way (ui-new:
|
||||||
|
// alert overlay + buzzer; ui-lvgl: a dialog). See docs/development/ui-core.md.
|
||||||
|
//
|
||||||
|
// Fixed-size ring, no heap. When full the oldest event is dropped -- events are
|
||||||
|
// hints for the view; the authoritative state stays queryable on the engines.
|
||||||
|
|
||||||
|
enum class UiEventType : uint8_t {
|
||||||
|
None = 0,
|
||||||
|
ClockAlert, // alarm / countdown fired: wake, show `text`, start the ring melody
|
||||||
|
ClockRingEnded, // ring window elapsed with no dismiss: stop melody, clear alert
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UiEvent {
|
||||||
|
UiEventType type;
|
||||||
|
char text[20];
|
||||||
|
};
|
||||||
|
|
||||||
|
class UiEventQueue {
|
||||||
|
public:
|
||||||
|
static const int SIZE = 8;
|
||||||
|
|
||||||
|
UiEventQueue() : _head(0), _count(0) {}
|
||||||
|
|
||||||
|
void push(UiEventType type, const char* text = nullptr) {
|
||||||
|
int pos;
|
||||||
|
if (_count < SIZE) { pos = (_head + _count) % SIZE; _count++; }
|
||||||
|
else { pos = _head; _head = (_head + 1) % SIZE; } // drop oldest
|
||||||
|
_q[pos].type = type;
|
||||||
|
if (text) {
|
||||||
|
strncpy(_q[pos].text, text, sizeof(_q[pos].text) - 1);
|
||||||
|
_q[pos].text[sizeof(_q[pos].text) - 1] = '\0';
|
||||||
|
} else {
|
||||||
|
_q[pos].text[0] = '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pop(UiEvent& out) {
|
||||||
|
if (_count == 0) return false;
|
||||||
|
out = _q[_head];
|
||||||
|
_head = (_head + 1) % SIZE;
|
||||||
|
_count--;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
UiEvent _q[SIZE];
|
||||||
|
int _head, _count;
|
||||||
|
};
|
||||||
@@ -3,12 +3,13 @@
|
|||||||
// Entered with Enter on the home CLOCK page. A small top menu picks one of the
|
// Entered with Enter on the home CLOCK page. A small top menu picks one of the
|
||||||
// three tools; Cancel backs out a level (tool → menu → home).
|
// three tools; Cancel backs out a level (tool → menu → home).
|
||||||
//
|
//
|
||||||
// This screen is pure UI. The time-critical machinery lives in UITask, which
|
// This screen is pure UI. The time-critical machinery lives in the UI Core's
|
||||||
// drives it every loop regardless of the current screen:
|
// ClockEngine (ui-core/ClockEngine.h), which UITask drives every loop
|
||||||
// • Alarm — UITask schedules an absolute fire instant from NodePrefs'
|
// regardless of the current screen:
|
||||||
|
// • Alarm — the engine schedules an absolute fire instant from NodePrefs'
|
||||||
// alarm_hour/min (robust to RTC re-syncs) and rings. Here we just edit the
|
// alarm_hour/min (robust to RTC re-syncs) and rings. Here we just edit the
|
||||||
// persisted fields and call onAlarmChanged() to re-schedule.
|
// persisted fields and call onAlarmChanged() to re-schedule.
|
||||||
// • Timer — UITask owns the running countdown (startTimer / stopTimer /
|
// • Timer — the engine owns the running countdown (startTimer / stopTimer /
|
||||||
// isTimerRunning / timerRemainingMs). It rings even when off-screen.
|
// isTimerRunning / timerRemainingMs). It rings even when off-screen.
|
||||||
// • Stopwatch — purely a display utility with no background action, so its
|
// • Stopwatch — purely a display utility with no background action, so its
|
||||||
// millis() state lives here; it keeps counting while you're elsewhere.
|
// millis() state lives here; it keeps counting while you're elsewhere.
|
||||||
|
|||||||
@@ -1655,6 +1655,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
|||||||
_ping_rtt_ms = 0;
|
_ping_rtt_ms = 0;
|
||||||
|
|
||||||
_core = new UiCore(); // before any screen -- MessagesScreen binds to its history
|
_core = new UiCore(); // before any screen -- MessagesScreen binds to its history
|
||||||
|
_core->begin(node_prefs);
|
||||||
splash = new SplashScreen(this);
|
splash = new SplashScreen(this);
|
||||||
home = new HomeScreen(this, &rtc_clock, sensors, node_prefs);
|
home = new HomeScreen(this, &rtc_clock, sensors, node_prefs);
|
||||||
syncLockToHome(); // booted locked (e.g. cover closed) → home starts on the LOCK page
|
syncLockToHome(); // booted locked (e.g. cover closed) → home starts on the LOCK page
|
||||||
@@ -1712,13 +1713,10 @@ void UITask::gotoGpioScreen() {
|
|||||||
}
|
}
|
||||||
void UITask::gotoLiveShareScreen() { setCurrScreen(live_share_screen); }
|
void UITask::gotoLiveShareScreen() { setCurrScreen(live_share_screen); }
|
||||||
|
|
||||||
// ── Clock tools engine (alarm / countdown / ring) ───────────────────────────
|
// ── Clock tools (engine in ui-core/ClockEngine.h) ───────────────────────────
|
||||||
// Lives here, not in ClockToolsScreen, so it fires regardless of the current
|
// The melody overrides mute (playMelody → buzzer.playForced); the ring
|
||||||
// screen. The melody overrides mute (playMelody → buzzer.playForced); the ring
|
// auto-stops after ClockEngine::RING_MS if no key dismisses it (see loop()).
|
||||||
// auto-stops after CLOCK_RING_MS if no key dismisses it (see UITask::loop).
|
static const char* CLOCK_ALARM_MELODY = "alarm:d=8,o=6,b=125:c,c,c,c,p,c,c,c,c,p";
|
||||||
static const char* CLOCK_ALARM_MELODY = "alarm:d=8,o=6,b=125:c,c,c,c,p,c,c,c,c,p";
|
|
||||||
static const uint32_t CLOCK_RING_MS = 60000;
|
|
||||||
static const uint32_t CLOCK_ALARM_CATCHUP_SECS = 6 * 3600; // fire late up to 6 h, else reschedule
|
|
||||||
|
|
||||||
void UITask::wakeForAlarm() {
|
void UITask::wakeForAlarm() {
|
||||||
if (_display != NULL) _display->turnOn();
|
if (_display != NULL) _display->turnOn();
|
||||||
@@ -1726,92 +1724,38 @@ void UITask::wakeForAlarm() {
|
|||||||
// back off once _lock_wake_until is in the past — which it always is by the
|
// back off once _lock_wake_until is in the past — which it always is by the
|
||||||
// time an alarm fires. Hold the wake window open for the whole ring so the
|
// time an alarm fires. Hold the wake window open for the whole ring so the
|
||||||
// lock screen (and its alert overlay) stays visible while ringing.
|
// lock screen (and its alert overlay) stays visible while ringing.
|
||||||
if (_locked) _lock_wake_until = millis() + CLOCK_RING_MS;
|
if (_locked) _lock_wake_until = millis() + ClockEngine::RING_MS;
|
||||||
_next_refresh = 0; // draw the alert overlay immediately
|
_next_refresh = 0; // draw the alert overlay immediately
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next absolute wall instant matching alarm_hour:alarm_min in local time,
|
void UITask::onAlarmChanged() { _core->clock.onAlarmChanged(); }
|
||||||
// strictly after now_wall (an alarm set to the current minute waits a day).
|
void UITask::startTimer(uint32_t duration_ms) { _core->clock.startTimer(duration_ms); }
|
||||||
// With alarm_repeat_mask == 0 that's just tomorrow's occurrence (one-shot).
|
void UITask::stopTimer() { _core->clock.stopTimer(); }
|
||||||
// With a repeat mask set, scan today..+6 days for the next weekday whose bit
|
bool UITask::isTimerRunning() const { return _core->clock.isTimerRunning(); }
|
||||||
// is set (struct tm's tm_wday convention, same as the mask) — today counts
|
uint32_t UITask::timerRemainingMs() const { return _core->clock.timerRemainingMs(); }
|
||||||
// only if its time hasn't already passed.
|
bool UITask::isRinging() const { return _core->clock.isRinging(); }
|
||||||
uint32_t UITask::computeAlarmNextFire(uint32_t now_wall) const {
|
void UITask::dismissRing() { stopMelody(); _core->clock.dismissRing(); clearAlert(); }
|
||||||
int tz = _node_prefs ? _node_prefs->tz_offset_hours : 0;
|
|
||||||
int64_t now_local = (int64_t)now_wall + (int64_t)tz * 3600;
|
void UITask::tickCore() {
|
||||||
time_t t = (time_t)now_local;
|
_core->loop();
|
||||||
struct tm* ti = gmtime(&t);
|
UiEvent ev;
|
||||||
int64_t sod = ti->tm_hour * 3600 + ti->tm_min * 60 + ti->tm_sec; // secs since local midnight
|
while (_core->events.pop(ev)) {
|
||||||
int64_t midnight = now_local - sod;
|
switch (ev.type) {
|
||||||
int64_t time_of_day = (int64_t)_node_prefs->alarm_hour * 3600 + (int64_t)_node_prefs->alarm_min * 60;
|
case UiEventType::ClockAlert:
|
||||||
uint8_t mask = _node_prefs->alarm_repeat_mask;
|
wakeForAlarm();
|
||||||
if (mask != 0) {
|
showAlert(ev.text, ClockEngine::RING_MS);
|
||||||
for (int d = 0; d < 7; d++) {
|
playMelody(CLOCK_ALARM_MELODY);
|
||||||
if (mask & (1 << ((ti->tm_wday + d) % 7))) {
|
break;
|
||||||
int64_t target = midnight + (int64_t)d * 86400 + time_of_day;
|
case UiEventType::ClockRingEnded:
|
||||||
if (target > now_local) return (uint32_t)(target - (int64_t)tz * 3600);
|
stopMelody();
|
||||||
}
|
clearAlert();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
// Mask had no bit set (shouldn't happen — the UI only offers non-empty
|
|
||||||
// presets) — fall through to the one-shot calculation so it still fires.
|
|
||||||
}
|
}
|
||||||
int64_t target = midnight + time_of_day;
|
// Repeat the ring melody until dismissed or the ring window elapses.
|
||||||
if (target <= now_local) target += 86400;
|
if (_core->clock.isRinging() && !isMelodyPlaying()) playMelody(CLOCK_ALARM_MELODY);
|
||||||
return (uint32_t)(target - (int64_t)tz * 3600);
|
|
||||||
}
|
|
||||||
|
|
||||||
void UITask::fireClockAlert(const char* label) {
|
|
||||||
snprintf(_ring_label, sizeof(_ring_label), "%s", label);
|
|
||||||
_ringing = true;
|
|
||||||
_ring_until_ms = millis() + CLOCK_RING_MS;
|
|
||||||
wakeForAlarm();
|
|
||||||
showAlert(label, CLOCK_RING_MS);
|
|
||||||
playMelody(CLOCK_ALARM_MELODY);
|
|
||||||
}
|
|
||||||
|
|
||||||
void UITask::evaluateAlarm() {
|
|
||||||
if (!_node_prefs || !_node_prefs->alarm_on) return;
|
|
||||||
uint32_t now_ms = millis();
|
|
||||||
if (now_ms - _alarm_check_ms < 500) return; // ~2 Hz is plenty for a minute alarm
|
|
||||||
_alarm_check_ms = now_ms;
|
|
||||||
uint32_t now_wall = rtc_clock.getCurrentTime();
|
|
||||||
if (now_wall < 1000000000UL) return; // need a real time sync first
|
|
||||||
if (_alarm_next_fire == 0) _alarm_next_fire = computeAlarmNextFire(now_wall);
|
|
||||||
if (now_wall < _alarm_next_fire) return;
|
|
||||||
if (now_wall - _alarm_next_fire < CLOCK_ALARM_CATCHUP_SECS) {
|
|
||||||
char lbl[20];
|
|
||||||
snprintf(lbl, sizeof(lbl), "Alarm %02d:%02d", _node_prefs->alarm_hour, _node_prefs->alarm_min);
|
|
||||||
if (_node_prefs->alarm_repeat_mask == 0) {
|
|
||||||
_node_prefs->alarm_on = 0; // one-shot
|
|
||||||
bool dirty = true; savePrefsIfDirty(dirty);
|
|
||||||
}
|
|
||||||
// Repeating: alarm_on stays set: computeAlarmNextFire() re-arms it for the
|
|
||||||
// next matching weekday below.
|
|
||||||
_alarm_next_fire = 0;
|
|
||||||
fireClockAlert(lbl);
|
|
||||||
} else {
|
|
||||||
// Clock jumped implausibly far past the target — reschedule rather than
|
|
||||||
// ringing absurdly late.
|
|
||||||
_alarm_next_fire = computeAlarmNextFire(now_wall);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void UITask::tickClockTools() {
|
|
||||||
uint32_t now_ms = millis();
|
|
||||||
// Ring maintenance: repeat the melody until dismissed or the window elapses.
|
|
||||||
// Signed-difference compares (like the trail/loc-share timers) so deadlines
|
|
||||||
// landing past the millis() rollover don't read as already elapsed.
|
|
||||||
if (_ringing) {
|
|
||||||
if ((int32_t)(now_ms - _ring_until_ms) >= 0) { stopMelody(); _ringing = false; clearAlert(); }
|
|
||||||
else if (!isMelodyPlaying()) playMelody(CLOCK_ALARM_MELODY);
|
|
||||||
}
|
|
||||||
// Countdown timer (millis — sync-immune).
|
|
||||||
if (_timer_running && (int32_t)(now_ms - _timer_deadline_ms) >= 0) {
|
|
||||||
_timer_running = false;
|
|
||||||
fireClockAlert("Timer done");
|
|
||||||
}
|
|
||||||
// Alarm (wall clock — absolute schedule for sync robustness).
|
|
||||||
evaluateAlarm();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ringtone takes a slot argument that onShow() can't carry — pass it after the
|
// Ringtone takes a slot argument that onShow() can't carry — pass it after the
|
||||||
@@ -2847,9 +2791,9 @@ void UITask::loop() {
|
|||||||
|
|
||||||
if (curr) curr->poll();
|
if (curr) curr->poll();
|
||||||
|
|
||||||
// Alarm + countdown run regardless of the current screen / display state, so
|
// UI Core engines (alarm + countdown, …) run regardless of the current screen
|
||||||
// they're driven here (not via the current screen's poll()).
|
// / display state, so they're driven here (not via the current screen's poll()).
|
||||||
tickClockTools();
|
tickCore();
|
||||||
|
|
||||||
if (_display != NULL && _display->isOn()) {
|
if (_display != NULL && _display->isOn()) {
|
||||||
if (_locked && (int32_t)(millis() - _lock_wake_until) >= 0) {
|
if (_locked && (int32_t)(millis() - _lock_wake_until) >= 0) {
|
||||||
|
|||||||
@@ -152,24 +152,9 @@ class UITask : public AbstractUITask {
|
|||||||
void fireLocator(bool arrived);
|
void fireLocator(bool arrived);
|
||||||
void locatorProximityBeeper();
|
void locatorProximityBeeper();
|
||||||
|
|
||||||
// Clock tools engine — owned here (not by ClockToolsScreen) so the one-shot
|
// Runs the UI Core engines and reacts to their events (alert overlay, buzzer,
|
||||||
// alarm and the countdown timer fire every loop regardless of the current
|
// display wake). Driven from loop() regardless of the current screen.
|
||||||
// screen / display state. ClockToolsScreen is pure UI over this state. The
|
void tickCore();
|
||||||
// alarm is scheduled as an ABSOLUTE wall instant, recomputed from the stored
|
|
||||||
// time-of-day, so it survives RTC re-syncs (mesh/app/GPS/CLI all jump the
|
|
||||||
// clock) — small corrections still fire on time, a jump over the target still
|
|
||||||
// fires (late). See evaluateAlarm(). Timer + ring are millis-based.
|
|
||||||
uint32_t _alarm_next_fire = 0; // unix; 0 = (re)compute lazily once time is valid
|
|
||||||
uint32_t _alarm_check_ms = 0; // throttle the wall-clock read to ~2 Hz
|
|
||||||
bool _timer_running = false;
|
|
||||||
uint32_t _timer_deadline_ms = 0;
|
|
||||||
bool _ringing = false;
|
|
||||||
uint32_t _ring_until_ms = 0;
|
|
||||||
char _ring_label[20] = {0};
|
|
||||||
uint32_t computeAlarmNextFire(uint32_t now_wall) const;
|
|
||||||
void evaluateAlarm(); // alarm scheduling + fire detection
|
|
||||||
void fireClockAlert(const char* label); // wake + alert + melody + start ring
|
|
||||||
void tickClockTools(); // driven from loop(): ring + timer + alarm
|
|
||||||
|
|
||||||
// Course-over-ground ring — a heading source independent of trail recording.
|
// Course-over-ground ring — a heading source independent of trail recording.
|
||||||
// Filled from the same periodic GPS poll regardless of _trail.isActive().
|
// Filled from the same periodic GPS poll regardless of _trail.isActive().
|
||||||
@@ -399,22 +384,15 @@ public:
|
|||||||
void wakeForAlarm();
|
void wakeForAlarm();
|
||||||
// Clear any active alert overlay early (alarm dismiss).
|
// Clear any active alert overlay early (alarm dismiss).
|
||||||
void clearAlert() { _alert_expiry = 0; }
|
void clearAlert() { _alert_expiry = 0; }
|
||||||
// Clock tools engine API — ClockToolsScreen drives these; the engine itself
|
// Clock tools engine API (ui-core/ClockEngine.h) — ClockToolsScreen drives
|
||||||
// runs in tickClockTools() from loop() so it fires regardless of the screen.
|
// these; the engine runs from tickCore() so it fires regardless of the screen.
|
||||||
void onAlarmChanged() { _alarm_next_fire = 0; } // re-schedule after an alarm edit
|
void onAlarmChanged();
|
||||||
void startTimer(uint32_t duration_ms) { _timer_running = true; _timer_deadline_ms = millis() + duration_ms; }
|
void startTimer(uint32_t duration_ms);
|
||||||
void stopTimer() { _timer_running = false; }
|
void stopTimer();
|
||||||
bool isTimerRunning() const { return _timer_running; }
|
bool isTimerRunning() const;
|
||||||
uint32_t timerRemainingMs() const {
|
uint32_t timerRemainingMs() const;
|
||||||
if (!_timer_running) return 0;
|
bool isRinging() const;
|
||||||
uint32_t now = millis();
|
void dismissRing();
|
||||||
// Signed-difference compare so a deadline that lands past the millis()
|
|
||||||
// rollover (~49.7 days) doesn't read as already elapsed.
|
|
||||||
if ((int32_t)(now - _timer_deadline_ms) >= 0) return 0;
|
|
||||||
return _timer_deadline_ms - now;
|
|
||||||
}
|
|
||||||
bool isRinging() const { return _ringing; }
|
|
||||||
void dismissRing() { stopMelody(); _ringing = false; clearAlert(); }
|
|
||||||
TrailStore& trail() { return _trail; }
|
TrailStore& trail() { return _trail; }
|
||||||
WaypointStore& waypoints() { return _waypoints; }
|
WaypointStore& waypoints() { return _waypoints; }
|
||||||
LiveTrackStore& liveTrack() { return _livetrack; }
|
LiveTrackStore& liveTrack() { return _livetrack; }
|
||||||
|
|||||||
Reference in New Issue
Block a user