diff --git a/lib/ui/src/toast_queue.cpp b/lib/ui/src/toast_queue.cpp index a54faec..c4cb3c8 100644 --- a/lib/ui/src/toast_queue.cpp +++ b/lib/ui/src/toast_queue.cpp @@ -9,7 +9,7 @@ void ToastQueue::push(const std::string& text, NotificationLevel level, uint32_t queue_.erase(queue_.begin() + (showing_ && queue_.size() > 1 ? 1 : 0)); if (!showing_) changed_ = true; } - queue_.push_back({text, level}); + queue_.push_back({text, level, nextId_++}); if (!showing_) { showing_ = true; shownAtMs_ = nowMs; @@ -19,7 +19,9 @@ void ToastQueue::push(const std::string& text, NotificationLevel level, uint32_t const Toast* ToastQueue::current(uint32_t nowMs) { if (!showing_) return nullptr; - if (nowMs - shownAtMs_ >= durationMs_) dismiss(shownAtMs_ + durationMs_); + // Signed: a caller's clock reading may be a few ms older than the Toast's stamp. + int32_t elapsed = static_cast(nowMs - shownAtMs_); + if (elapsed >= static_cast(durationMs_)) dismiss(shownAtMs_ + durationMs_); return showing_ ? &queue_.front() : nullptr; } diff --git a/lib/ui/src/toast_queue.h b/lib/ui/src/toast_queue.h index ecef5ac..9b8001c 100644 --- a/lib/ui/src/toast_queue.h +++ b/lib/ui/src/toast_queue.h @@ -11,6 +11,7 @@ namespace roro { struct Toast { std::string text; NotificationLevel level; + uint32_t id; // unique per push, to tell two identical Toasts apart }; // Notifications waiting to be shown as Toasts, one at a time, each for the same duration. @@ -34,6 +35,7 @@ class ToastQueue { bool showing_ = false; uint32_t shownAtMs_ = 0; bool changed_ = false; + uint32_t nextId_ = 1; }; } // namespace roro diff --git a/src/ui/notifier.cpp b/src/ui/notifier.cpp index 13b69aa..ad6962a 100644 --- a/src/ui/notifier.cpp +++ b/src/ui/notifier.cpp @@ -9,14 +9,12 @@ constexpr int kLedPin = 21; // WS2812, powered with the backlight rail (G38) } Notifier::Notifier(EventBus& bus, const Settings& settings) : settings_(settings) { - bus.subscribe(EventType::Notification, [this](const Event& e) { present(e); }); + bus.subscribe(EventType::Notification, [this](const Event& e) { + toasts_.push(e.text, static_cast(e.a), millis()); + }); } -void Notifier::present(const Event& e) { - auto level = static_cast(e.a); - uint32_t now = millis(); - toasts_.push(e.text, level, now); - +void Notifier::alert(NotificationLevel level, uint32_t nowMs) { if (!settings_.getBool(Setting::Sound)) return; M5Cardputer.Speaker.tone(level == NotificationLevel::Warning ? 880 : 1760, 60); switch (level) { @@ -25,10 +23,15 @@ void Notifier::present(const Event& e) { default: rgbLedWrite(kLedPin, 0, 16, 40); break; } ledOn_ = true; - ledOffAtMs_ = now + 150; + ledOffAtMs_ = nowMs + 150; } void Notifier::update(uint32_t nowMs) { + const Toast* shown = toasts_.current(nowMs); + if (shown && shown->id != alertedId_) { + alertedId_ = shown->id; + alert(shown->level, nowMs); + } if (ledOn_ && static_cast(nowMs - ledOffAtMs_) >= 0) { rgbLedWrite(kLedPin, 0, 0, 0); ledOn_ = false; diff --git a/src/ui/notifier.h b/src/ui/notifier.h index aee57d9..cbf9ece 100644 --- a/src/ui/notifier.h +++ b/src/ui/notifier.h @@ -6,20 +6,22 @@ namespace roro { -// Presents Notifications: a Toast, plus a short beep and LED flash (when Sound is on). +// Presents Notifications as Toasts. Each Toast gets a short beep and LED flash (when Sound is on) +// as it appears, so a burst of Notifications alerts once per Toast, not all at once. class Notifier { public: Notifier(EventBus& bus, const Settings& settings); - void update(uint32_t nowMs); // turns the LED off after the flash + void update(uint32_t nowMs); // alerts for a newly shown Toast, ends the LED flash ToastQueue& toasts() { return toasts_; } private: - void present(const Event& e); + void alert(NotificationLevel level, uint32_t nowMs); const Settings& settings_; ToastQueue toasts_{3000}; uint32_t ledOffAtMs_ = 0; bool ledOn_ = false; + uint32_t alertedId_ = 0; }; } // namespace roro diff --git a/test/test_toast_queue/test_toast_queue.cpp b/test/test_toast_queue/test_toast_queue.cpp index b572750..22e3b17 100644 --- a/test/test_toast_queue/test_toast_queue.cpp +++ b/test/test_toast_queue/test_toast_queue.cpp @@ -1,5 +1,7 @@ #include +#include + #include "toast_queue.h" using namespace roro; @@ -64,6 +66,32 @@ void test_reports_when_a_new_toast_appears() { TEST_ASSERT_TRUE(q.takeChanged()); } +void test_asking_with_a_slightly_older_time_does_not_expire_the_toast() { + // The main loop reads the time once, then a Notification pushed later in the same pass is + // stamped a few ms after it. + ToastQueue q(3000); + q.push("late", NotificationLevel::Info, 1005); + TEST_ASSERT_NOT_NULL(q.current(1000)); + TEST_ASSERT_EQUAL_STRING("late", q.current(1000)->text.c_str()); +} + +void test_burst_shows_each_toast_in_turn() { + ToastQueue q(3000); + for (int i = 1; i <= 3; i++) q.push(std::to_string(i), NotificationLevel::Info, 1002); + TEST_ASSERT_EQUAL_STRING("1", q.current(1000)->text.c_str()); + TEST_ASSERT_EQUAL_STRING("2", q.current(4002)->text.c_str()); + TEST_ASSERT_EQUAL_STRING("3", q.current(7002)->text.c_str()); + TEST_ASSERT_NULL(q.current(10002)); +} + +void test_each_toast_has_its_own_id() { + ToastQueue q(3000); + q.push("a", NotificationLevel::Info, 0); + q.push("a", NotificationLevel::Info, 0); // same text, still a different Toast + uint32_t first = q.current(0)->id; + TEST_ASSERT_NOT_EQUAL(first, q.current(3000)->id); +} + int main() { UNITY_BEGIN(); RUN_TEST(test_nothing_shown_when_empty); @@ -73,5 +101,8 @@ int main() { RUN_TEST(test_flood_keeps_the_visible_toast_and_drops_the_oldest_waiting); RUN_TEST(test_dismiss_shows_the_next_one_immediately); RUN_TEST(test_reports_when_a_new_toast_appears); + RUN_TEST(test_asking_with_a_slightly_older_time_does_not_expire_the_toast); + RUN_TEST(test_burst_shows_each_toast_in_turn); + RUN_TEST(test_each_toast_has_its_own_id); return UNITY_END(); }