Fix Toast timing and alert once per shown Toast

- ToastQueue compared times unsigned: a Toast stamped a few ms after the
  main loop read the clock looked expired, so Toasts were skipped or
  shown late and bursts chained wrongly
- Notifier now beeps and flashes when each Toast appears instead of when
  the Notification arrives, so a burst alerts once per Toast

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
2026-10-02 11:09:46 +02:00
co-authored by Claude Opus 5.5
parent 879cb0d0b6
commit a053b1a5de
5 changed files with 52 additions and 12 deletions
+4 -2
View File
@@ -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<int32_t>(nowMs - shownAtMs_);
if (elapsed >= static_cast<int32_t>(durationMs_)) dismiss(shownAtMs_ + durationMs_);
return showing_ ? &queue_.front() : nullptr;
}
+2
View File
@@ -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
+10 -7
View File
@@ -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<NotificationLevel>(e.a), millis());
});
}
void Notifier::present(const Event& e) {
auto level = static_cast<NotificationLevel>(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<int32_t>(nowMs - ledOffAtMs_) >= 0) {
rgbLedWrite(kLedPin, 0, 0, 0);
ledOn_ = false;
+5 -3
View File
@@ -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
@@ -1,5 +1,7 @@
#include <unity.h>
#include <string>
#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();
}