Add configurable battery and USB display behavior

Persist separate display modes and timeouts in seconds for battery and USB power, expose four controls in WebConfig and CLI, and apply one shared policy across display-equipped roles.

Detect R8 external power from a USB host or calibrated battery voltage above 4.21 V, with cached sampling and hysteresis. Add policy, persistence, browser, and R8 power-detection regression coverage.
This commit is contained in:
mikecarper committed 2026-09-10 21:03:15 -07:00
1 parent d83153e935
commit a814a00ce7
39 files changed
+3187 -2488

No files matched your search

+15 -77
View File
@@ -19,9 +19,6 @@
#include <helpers/esp32/WebConfigServer.h> // defines WITH_WEBCONFIG on ESP32
#endif
#ifndef AUTO_OFF_MILLIS
#define AUTO_OFF_MILLIS 20000 // 20 seconds; 0 keeps the screen on
#endif
#ifdef DISPLAY_TOUCH_TOGGLE
#define TOUCH_POLL_MILLIS 50
@@ -55,15 +52,6 @@ void UITask::applyDisplayFlip() {
#endif
}
// `display.timeout` when the observer prefs are available, otherwise the
// compiled-in default. Read on every use so a `set display.timeout` takes
// effect immediately.
unsigned long UITask::displayTimeoutMillis() const {
#ifdef WITH_MQTT_BRIDGE
if (_observer_prefs) return (unsigned long)_observer_prefs->display_timeout_secs * 1000UL;
#endif
return AUTO_OFF_MILLIS;
}
#define BOOT_SCREEN_MILLIS 4000 // 4 seconds
#define POWEROFF_DELAY 3000
@@ -87,14 +75,12 @@ static const uint8_t meshcore_logo [] PROGMEM = {
void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version) {
_prevBtnState = HIGH;
_timeout_seen = displayTimeoutMillis();
_auto_off = millis() + displayTimeoutMillis();
_started_at = millis();
_node_prefs = node_prefs;
#ifdef DISPLAY_ACTIVITY_DASHBOARD
ObserverDashboard::applyDarkPalette(); // retunes UIColor for this target only
#endif
_display->turnOn();
_display->servicePower(board.isExternalPowered() || board.isUsbHostConnected());
applyDisplayFlip();
#ifdef DISPLAY_TOUCH_TOGGLE
_touch.begin();
@@ -360,11 +346,7 @@ void UITask::updateActivityRows() {
#ifdef DISPLAY_TOUCH_TOGGLE
void UITask::toggleDisplay(const char* source) {
if (_display->isOn()) {
_display->turnOff();
} else {
_display->turnOn();
}
_display->wake(mesh::ui::DisplayWake::Button);
#ifdef DISPLAY_TOUCH_DEBUG
mesh::usbConsolePort().printf("Display: %s -> %s\n", source, _display->isOn() ? "on" : "off");
#else
@@ -377,11 +359,19 @@ void UITask::toggleDisplay(const char* source) {
_rows_valid = false;
#endif
_next_refresh = 0; // redraw at once rather than showing the stale frame
_auto_off = millis() + displayTimeoutMillis();
}
#endif
void UITask::loop() {
if (_display->servicePower(board.isExternalPowered() || board.isUsbHostConnected())) {
_next_refresh = 0;
#ifdef DISPLAY_REDRAW_ON_CHANGE
_frame_valid = false;
#endif
#ifdef DISPLAY_ACTIVITY_DASHBOARD
_rows_valid = false;
#endif
}
#if defined(PIN_USER_BTN) && defined(DISPLAY_CLASS)
int ev = user_btn.check();
// Multiclick stays enabled on the R8 observer so triple-click can toggle
@@ -394,7 +384,7 @@ void UITask::loop() {
if (_display->isOn()) {
// TODO: any action ?
} else {
_display->turnOn();
_display->wake(mesh::ui::DisplayWake::Button);
#ifdef DISPLAY_REDRAW_ON_CHANGE
_frame_valid = false;
#endif
@@ -402,10 +392,10 @@ void UITask::loop() {
_rows_valid = false;
#endif
}
_auto_off = millis() + displayTimeoutMillis(); // extend auto-off timer
_display->wake(mesh::ui::DisplayWake::Button);
#endif
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
_display->turnOn();
_display->wake(mesh::ui::DisplayWake::Button);
mesh::usbConsolePort().printf("Powering Off\r\n");
_powering_off_at = millis() + POWEROFF_DELAY;
#ifdef DISPLAY_REDRAW_ON_CHANGE
@@ -420,29 +410,11 @@ void UITask::loop() {
// Preserve the fork's persistent WebUI toggle. R8 observer repeaters keep
// multiclick enabled specifically for this action.
WebConfigServer::requestToggleFromButton();
_display->turnOn();
_auto_off = millis() + displayTimeoutMillis();
_display->wake(mesh::ui::DisplayWake::Button);
#endif
}
#endif
#ifdef WITH_WEBCONFIG
// While the setup portal is up there's no user button to wake the screen
// reliably - keep it on so the join instructions stay visible.
if (WebConfigServer::getSetupInfo(NULL, 0, NULL, 0)) {
if (!_display->isOn()) {
_display->turnOn();
#ifdef DISPLAY_REDRAW_ON_CHANGE
_frame_valid = false;
#endif
#ifdef DISPLAY_ACTIVITY_DASHBOARD
_rows_valid = false;
#endif
}
_auto_off = millis() + displayTimeoutMillis();
}
#endif
#ifdef DISPLAY_TOUCH_TOGGLE
{
unsigned long now = millis();
@@ -453,25 +425,6 @@ void UITask::loop() {
}
#endif
// Observe timeout changes even while the panel is blanked. In particular,
// `set display.timeout 0` means the display must stay on, so wake it now
// rather than waiting for a button/touch event that may never arrive.
unsigned long timeout = displayTimeoutMillis();
if (timeout != _timeout_seen) {
_timeout_seen = timeout;
_auto_off = millis() + timeout;
if (timeout == 0 && !_display->isOn()) {
_display->turnOn();
#ifdef DISPLAY_REDRAW_ON_CHANGE
_frame_valid = false;
#endif
#ifdef DISPLAY_ACTIVITY_DASHBOARD
_rows_valid = false;
#endif
_next_refresh = 0;
}
}
if (_display->isOn()) {
// Apply a live orientation change before drawing, including the first frame
// after a preference was changed while the panel was blanked.
@@ -500,21 +453,6 @@ void UITask::loop() {
_next_refresh = millis() + 1000; // check for visible changes every second
}
// `_auto_off` is only armed on activity, so a timeout changed at runtime has
// to restart the countdown here - otherwise 0 -> 60 blanks instantly off a
// boot-time deadline, and 60 -> 3600 still blanks at the old 60 s mark.
#if MOMENTARY_BUTTON_WAKE_HOLD_MS > 0 && defined(PIN_USER_BTN)
if (user_btn.isWakeHoldActive()) _auto_off = millis() + timeout;
#endif
if (_powering_off_at == 0 && timeout > 0 && millisReached(millis(), _auto_off)) {
_display->turnOff();
#ifdef DISPLAY_REDRAW_ON_CHANGE
_frame_valid = false;
#endif
#ifdef DISPLAY_ACTIVITY_DASHBOARD
_rows_valid = false;
#endif
}
}
if (_powering_off_at > 0) { // power off timer armed