Light the screen (dimmed) while a Notification's Toast shows

An Off screen turns on dimmed for the Toast's duration so the user can
see what beeped. It isn't user activity: the timeouts aren't restarted,
and a key pressed meanwhile reaches the App.

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:35:13 +02:00
co-authored by Claude Opus 5.5
parent 15ea53e17f
commit 9218a34c3e
7 changed files with 65 additions and 1 deletions
+6
View File
@@ -12,6 +12,12 @@ bool PowerPolicy::activity(uint32_t nowMs) {
ScreenState PowerPolicy::update(uint32_t nowMs) {
uint32_t idle = nowMs - lastActivityMs_;
state_ = idle >= offMs_ ? ScreenState::Off : idle >= dimMs_ ? ScreenState::Dimmed : ScreenState::On;
if (notifying_) {
if (static_cast<int32_t>(nowMs - notifyUntilMs_) >= 0)
notifying_ = false;
else if (state_ == ScreenState::Off)
state_ = ScreenState::Dimmed;
}
return state_;
}
+10
View File
@@ -19,6 +19,14 @@ class PowerPolicy {
// Records a key press. Returns true if the key only woke an Off screen and must not reach the App.
bool activity(uint32_t nowMs);
// A Notification is showing until untilMs: an Off screen lights up Dimmed meanwhile. It's not
// user activity, so it doesn't restart the timeouts.
void notify(uint32_t nowMs, uint32_t untilMs) {
(void)nowMs;
notifyUntilMs_ = untilMs;
notifying_ = true;
}
ScreenState update(uint32_t nowMs);
ScreenState state() const { return state_; }
@@ -26,6 +34,8 @@ class PowerPolicy {
uint32_t dimMs_;
uint32_t offMs_;
uint32_t lastActivityMs_ = 0;
uint32_t notifyUntilMs_ = 0;
bool notifying_ = false;
ScreenState state_ = ScreenState::On;
};
+1
View File
@@ -79,6 +79,7 @@ void setup() {
power = new PowerService(settings);
clockService = new ClockService(settings, bus);
notifier = new Notifier(bus, settings);
notifier->onShow = [](uint32_t now, uint32_t until) { power->onNotification(now, until); };
services.add(*power);
services.add(*clockService);
services.add(*battery);
+3
View File
@@ -19,6 +19,9 @@ class PowerService : public Service {
// Call for every key press before routing it. Returns true if the key must not reach the App.
bool onKey(uint32_t nowMs) { return policy_.activity(nowMs); }
// A Toast is showing until untilMs: light an Off screen (dimmed) so it can be read.
void onNotification(uint32_t nowMs, uint32_t untilMs) { policy_.notify(nowMs, untilMs); }
ScreenState screen() const { return applied_; }
private:
+1
View File
@@ -31,6 +31,7 @@ void Notifier::update(uint32_t nowMs) {
if (shown && shown->id != alertedId_) {
alertedId_ = shown->id;
alert(shown->level, nowMs);
if (onShow) onShow(nowMs, nowMs + kToastMs);
}
if (ledOn_ && static_cast<int32_t>(nowMs - ledOffAtMs_) >= 0) {
rgbLedWrite(kLedPin, 0, 0, 0);
+7 -1
View File
@@ -1,5 +1,7 @@
#pragma once
#include <functional>
#include "event_bus.h"
#include "settings.h"
#include "toast_queue.h"
@@ -14,11 +16,15 @@ class Notifier {
void update(uint32_t nowMs); // alerts for a newly shown Toast, ends the LED flash
ToastQueue& toasts() { return toasts_; }
// Called when a Toast appears, with the time it will disappear (e.g. to light the screen).
std::function<void(uint32_t nowMs, uint32_t untilMs)> onShow;
private:
void alert(NotificationLevel level, uint32_t nowMs);
const Settings& settings_;
ToastQueue toasts_{3000, 8};
static constexpr uint32_t kToastMs = 3000;
ToastQueue toasts_{kToastMs, 8};
uint32_t ledOffAtMs_ = 0;
bool ledOn_ = false;
uint32_t alertedId_ = 0;
+37
View File
@@ -38,6 +38,39 @@ void test_new_timeouts_apply_immediately() {
TEST_ASSERT_EQUAL(static_cast<int>(ScreenState::Off), static_cast<int>(p.update(20000)));
}
void test_notification_lights_an_off_screen_dimmed_until_it_ends() {
PowerPolicy p(30000, 60000);
p.activity(0);
p.update(70000); // off
p.notify(70000, 73000);
TEST_ASSERT_EQUAL(static_cast<int>(ScreenState::Dimmed), static_cast<int>(p.update(70001)));
TEST_ASSERT_EQUAL(static_cast<int>(ScreenState::Dimmed), static_cast<int>(p.update(72999)));
TEST_ASSERT_EQUAL(static_cast<int>(ScreenState::Off), static_cast<int>(p.update(73000)));
}
void test_notification_does_not_count_as_activity() {
PowerPolicy p(30000, 60000);
p.activity(0);
p.notify(25000, 28000);
TEST_ASSERT_EQUAL(static_cast<int>(ScreenState::Dimmed), static_cast<int>(p.update(30000)));
}
void test_notification_never_brightens_or_dims_an_on_screen() {
PowerPolicy p(30000, 60000);
p.activity(0);
p.notify(1000, 4000);
TEST_ASSERT_EQUAL(static_cast<int>(ScreenState::On), static_cast<int>(p.update(2000)));
}
void test_key_on_a_notification_lit_screen_acts() {
PowerPolicy p(30000, 60000);
p.activity(0);
p.update(70000);
p.notify(70000, 73000);
p.update(70001);
TEST_ASSERT_FALSE(p.activity(71000)); // the screen is visible: the key reaches the App
}
void test_short_press_reported_on_release() {
PowerButton b(2000);
TEST_ASSERT_EQUAL(static_cast<int>(ButtonAction::None), static_cast<int>(b.update(true, 0)));
@@ -59,6 +92,10 @@ int main() {
RUN_TEST(test_key_on_dimmed_screen_wakes_and_still_acts);
RUN_TEST(test_key_on_off_screen_only_wakes);
RUN_TEST(test_new_timeouts_apply_immediately);
RUN_TEST(test_notification_lights_an_off_screen_dimmed_until_it_ends);
RUN_TEST(test_notification_does_not_count_as_activity);
RUN_TEST(test_notification_never_brightens_or_dims_an_on_screen);
RUN_TEST(test_key_on_a_notification_lit_screen_acts);
RUN_TEST(test_short_press_reported_on_release);
RUN_TEST(test_long_press_fires_once_while_still_held);
return UNITY_END();