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:
1 parent
3d4432e848
commit
96ef0ae905
39 files changed
+3187
-2488
No files matched your search
@@ -108,6 +108,8 @@ jobs:
|
||||
|
||||
- name: Verify Companion Bluetooth settings and WebConfig
|
||||
run: |
|
||||
python3 -B test/test_display_power.py
|
||||
python3 -B test/test_heltec_r8_external_power.py
|
||||
python3 -B test/test_bluetooth_mac_contract.py
|
||||
python3 -B test/test_companion_settings_persistence_contract.py
|
||||
python3 -B test/test_webconfig_ui.py
|
||||
|
||||
@@ -268,7 +268,8 @@ should use command `0x42`. See [Companion radio binary protocol](companion_proto
|
||||
| Operational | [`advert`](cli_commands.md#send-a-flood-advert) | Advert-capable role | Yes | Yes | Yes | Yes | Yes |
|
||||
| Operational | [`advert.zerohop`](cli_commands.md#send-a-zero-hop-advert) | Advert-capable role | Yes | Yes | Yes | Yes | Yes |
|
||||
| Operational | [`erase`](cli_commands.md#erasefactory-reset) | Local connection | Local | Local | Local | Local | Local |
|
||||
| Display | [`get/set display.timeout`; `get/set display.flip`](cli_commands.md#set-mqtt-observer-display-timeout-and-flip) | MQTT observer with display; flip needs a supported driver | No | No | No | Feature | No |
|
||||
| Display | [`get/set display.mode`; `display.timeout`; `display.usb.mode`; `display.usb.timeout`](cli_commands.md#set-display-modes-and-timeouts) | Display-equipped roles; pairing modes require BLE Companion | Feature | Feature | Feature | Feature | Feature |
|
||||
| Display | [`get/set display.flip`](cli_commands.md#set-mqtt-observer-display-flip) | MQTT observer with supported display driver | No | No | No | Feature | No |
|
||||
| Information | [`ver`](cli_commands.md#get-the-version) | All text CLI roles | Yes | Yes | Yes | Yes | Yes |
|
||||
| Information | [`board`](cli_commands.md#show-the-hardware-name) | All text CLI roles | Yes | Yes | Yes | Yes | Yes |
|
||||
| Diagnostics | [`get storage.layout`](cli_commands.md#show-the-storage-layout) | Detected flash size and live partition table | Yes | Yes | Yes | Yes | Yes |
|
||||
|
||||
+52
-11
@@ -779,20 +779,61 @@ build flag is needed.
|
||||
|
||||
---
|
||||
|
||||
## Set MQTT observer display timeout and flip
|
||||
## Set display modes and timeouts
|
||||
|
||||
MQTT observer builds with a display support a persisted inactivity timeout:
|
||||
Display-equipped Companions, repeaters, room servers, and sensors have four
|
||||
saved settings. The WebConfig **Display** card exposes the same controls:
|
||||
|
||||
| Setting | CLI | Default |
|
||||
| --- | --- | --- |
|
||||
| Battery mode | `get/set display.mode` | `button-pairing` on BLE Companions; `button` elsewhere |
|
||||
| Battery timeout, seconds | `get/set display.timeout` | `15` |
|
||||
| USB mode | `get/set display.usb.mode` | Same as battery mode |
|
||||
| USB timeout, seconds | `get/set display.usb.timeout` | `15` |
|
||||
|
||||
Modes are `off`, `on`, `button`, `pairing`, `button-pairing`, and `automatic`.
|
||||
Pairing modes are available only on builds supporting BLE pairing.
|
||||
|
||||
- `off`: stays dark, including boot and pairing.
|
||||
- `on`: stays on while the device is awake.
|
||||
- `button`: a physical button, keyboard/navigation input, or supported touch
|
||||
wakes the display and restarts the timeout.
|
||||
- `pairing`: wakes only for an actual BLE PIN request, until pairing ends or
|
||||
expires. The inactivity timeout does not shorten the pairing prompt.
|
||||
- `button-pairing`: combines the button and pairing behaviors.
|
||||
- `automatic`: shows startup briefly (at most four seconds), and wakes for
|
||||
messages while the app is disconnected. An app connection blanks the screen;
|
||||
buttons and pairing requests can still wake it. Disconnecting alone does not
|
||||
wake it. Network traffic and screen redraws do not extend the timeout.
|
||||
|
||||
Timeouts are whole seconds from `1` to `3600`. Use mode `on` for a permanent
|
||||
display rather than timeout `0`. The timeout is retained but unused in `off`,
|
||||
`on`, and `pairing` modes. Settings apply immediately, including while blanked;
|
||||
changing power source applies the other profile without carrying over a wake
|
||||
from the previous profile. Settings survive reboot and filesystem-preserving
|
||||
updates in `/display_prefs`; erasing the filesystem restores the defaults.
|
||||
An existing observer timeout that differs from its old 60-second default is
|
||||
migrated once to both power profiles; an old zero timeout becomes mode `on`.
|
||||
|
||||
```text
|
||||
get display.timeout
|
||||
set display.timeout 0
|
||||
set display.timeout 60
|
||||
set display.mode button-pairing
|
||||
set display.timeout 15
|
||||
set display.usb.mode on
|
||||
set display.usb.timeout 60
|
||||
```
|
||||
|
||||
The value is seconds. `0` keeps the display on; `1` through `3600` blanks it
|
||||
after that much inactivity. The default is `60`. A change applies immediately
|
||||
and restarts the countdown. On the Heltec V4 R8 Expansion Kit V2 observer, a
|
||||
panel tap or USER-button click can also blank or wake the display.
|
||||
USB profile selection uses the board's external-power detector, with USB-host
|
||||
detection as a fallback. VBUS-capable boards detect chargers without a data
|
||||
connection. Heltec V4 R8 uses either a detected USB host or a calibrated battery
|
||||
voltage above 4.21 V. The voltage estimate is checked every five seconds and
|
||||
clears at 4.20 V or below; host detection takes effect immediately. This estimate
|
||||
can miss a charger while the battery is low, depends on ADC calibration, and
|
||||
cannot distinguish USB from solar charging. Other boards without a power
|
||||
detector may require a USB host to recognize USB power. Wi-Fi/MQTT connection or
|
||||
BLE pairing is not evidence of USB power. E-paper panels can retain their last
|
||||
image when powered off.
|
||||
|
||||
## Set MQTT observer display flip
|
||||
|
||||
Supported observer displays, including the R8 OLED and ST7789 panels, can also
|
||||
be turned 180 degrees relative to their compiled orientation:
|
||||
@@ -809,8 +850,8 @@ relative 180-degree mounting choice and cannot switch between portrait and
|
||||
landscape. The value is harmless on an observer display driver that does not
|
||||
support flipping.
|
||||
|
||||
Both settings survive reboot and firmware updates that preserve the filesystem;
|
||||
erasing flash restores the `60`/`off` defaults. The boot log reports the saved
|
||||
Flip survives reboot and firmware updates that preserve the filesystem;
|
||||
erasing flash restores the `off` default. The boot log reports the saved
|
||||
flip state on display-enabled observer builds.
|
||||
|
||||
---
|
||||
|
||||
@@ -13,9 +13,6 @@
|
||||
|
||||
#include "NodePrefs.h"
|
||||
|
||||
#ifndef UI_USB_AUTO_OFF_MULTIPLIER
|
||||
#define UI_USB_AUTO_OFF_MULTIPLIER 5UL
|
||||
#endif
|
||||
|
||||
enum class UIEventType {
|
||||
none,
|
||||
@@ -50,26 +47,9 @@ protected:
|
||||
|| _shutdownGuard->prepareForUiShutdown();
|
||||
}
|
||||
|
||||
bool isDisplayAutoOffDue(unsigned long configured_deadline,
|
||||
unsigned long configured_timeout_millis) const {
|
||||
unsigned long deadline = configured_deadline;
|
||||
#if UI_USB_AUTO_OFF_MULTIPLIER > 1
|
||||
if (_board->isUsbHostConnected()) {
|
||||
// The configured deadline already includes the first timeout period.
|
||||
// Add the remaining periods while attached to a computer.
|
||||
deadline += configured_timeout_millis
|
||||
* (UI_USB_AUTO_OFF_MULTIPLIER - 1UL);
|
||||
}
|
||||
#endif
|
||||
return static_cast<int32_t>(millis() - deadline) > 0;
|
||||
}
|
||||
|
||||
bool shouldWakeDisplayForMessage() const {
|
||||
// Keep the existing BLE-only behavior, where the connected companion is
|
||||
// expected to surface the notification. A computer attached over USB is
|
||||
// the exception: show the message on the device even if BLE is connected.
|
||||
return !hasConnection() || _board->isUsbHostConnected();
|
||||
}
|
||||
|
||||
bool shouldWakeDisplayForMessage() const { return !hasConnection(); }
|
||||
|
||||
public:
|
||||
void setShutdownGuard(UIShutdownGuard* guard) { _shutdownGuard = guard; }
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#include <helpers/ui/DisplayPowerSettings.h>
|
||||
#include "MyMesh.h"
|
||||
|
||||
#include <Arduino.h> // needed for PlatformIO
|
||||
@@ -1723,6 +1724,14 @@ void MyMesh::begin(bool has_display, bool radio_available) {
|
||||
}
|
||||
#endif
|
||||
|
||||
mesh::ui::loadDisplayPowerSettings(_store->getPrimaryFS(),
|
||||
#ifdef BLE_PIN_CODE
|
||||
true
|
||||
#else
|
||||
false
|
||||
#endif
|
||||
);
|
||||
|
||||
// load persisted prefs
|
||||
const bool prefs_ready =
|
||||
_store->loadPrefs(_prefs, sensors.node_lat, sensors.node_lon);
|
||||
@@ -2546,6 +2555,8 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mesh::ui::handleDisplayPowerCommand(command, reply, reply_size)) return true;
|
||||
|
||||
if (strcmp(command, "get display.touch") == 0) {
|
||||
if (_ui == NULL || !_ui->supportsTouchDebug()) {
|
||||
snprintf(reply, reply_size, "Error: touchscreen diagnostics unsupported");
|
||||
|
||||
@@ -1591,7 +1591,7 @@ void halt() {
|
||||
|| static_cast<int32_t>(millis() - companion_setup_display_refresh) < 0) return;
|
||||
companion_setup_display_refresh = millis() + 1000;
|
||||
|
||||
companion_setup_display->turnOn();
|
||||
if (!companion_setup_display->isOn()) return;
|
||||
companion_setup_display->startFrame();
|
||||
companion_setup_display->setCompactText(false);
|
||||
companion_setup_display->setTextSize(1);
|
||||
@@ -2294,12 +2294,7 @@ void setup() {
|
||||
&& UI_WIFI_SETUP_HOME_PAGE != 1
|
||||
companion_setup_display = disp;
|
||||
#endif
|
||||
disp->startFrame();
|
||||
#ifdef ST7789
|
||||
disp->setTextSize(2);
|
||||
#endif
|
||||
disp->drawTextCentered(disp->width() / 2, 28, "Loading...");
|
||||
disp->endFrame();
|
||||
disp->turnOff(); // No boot content before the saved policy is loaded.
|
||||
}
|
||||
#if defined(ESP32) && defined(WIFI_SSID)
|
||||
companion_display_available = disp != NULL;
|
||||
@@ -2737,6 +2732,7 @@ void loop() {
|
||||
#if defined(TBEAM_1W) && defined(PIN_WIFI_BTN)
|
||||
ui_task.serviceWiFiToggleButton();
|
||||
#endif
|
||||
ui_task.loop(); // Continue button/touch handling while the portal is active.
|
||||
renderCompanionSetupDisplay();
|
||||
} else {
|
||||
ui_task.loop();
|
||||
|
||||
@@ -427,7 +427,6 @@ class HomeScreen : public UIScreen {
|
||||
uint64_t _uptime_millis;
|
||||
AdvertPath recent[UI_RECENT_LIST_SIZE];
|
||||
|
||||
|
||||
int renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) {
|
||||
// Convert millivolts to percentage
|
||||
#ifndef BATT_MIN_MILLIVOLTS
|
||||
@@ -1662,7 +1661,6 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, CompanionNode
|
||||
if (_display != NULL) {
|
||||
_display->setRotationDegrees(node_prefs->display_rotation_degrees);
|
||||
}
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
|
||||
#if defined(PIN_USER_BTN)
|
||||
user_btn.begin();
|
||||
@@ -1682,7 +1680,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, CompanionNode
|
||||
_node_prefs = node_prefs;
|
||||
|
||||
if (_display != NULL) {
|
||||
_display->turnOn();
|
||||
_display->servicePower(_board->isExternalPowered() || _board->isUsbHostConnected(), hasConnection());
|
||||
}
|
||||
|
||||
#ifdef PIN_BUZZER
|
||||
@@ -1726,18 +1724,16 @@ void UITask::showMessages() {
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
void UITask::showJohnReader() {
|
||||
if (!_display || isPairingScreenActive()) return;
|
||||
if (!_display->isOn()) _display->turnOn();
|
||||
if (!_display->isOn()) _display->wake(mesh::ui::DisplayWake::Button);
|
||||
if (!john_reader) john_reader = new (std::nothrow) JohnReaderScreen(this, _display);
|
||||
if (!john_reader) { showAlert("Reader: no memory", 1500); return; }
|
||||
static_cast<JohnReaderScreen*>(john_reader)->open();
|
||||
setCurrScreen(john_reader);
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
}
|
||||
|
||||
void UITask::closeJohnReader() {
|
||||
// Home retains the selected radio page while the reader is open.
|
||||
setCurrScreen(home);
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1778,7 +1774,6 @@ switch(t){
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
void UITask::msgRead(int msgcount) {
|
||||
_msgcount = msgcount;
|
||||
if (msgcount == 0) {
|
||||
@@ -1826,13 +1821,10 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text,
|
||||
: 0;
|
||||
|
||||
if (_display != NULL) {
|
||||
if (!_display->isOn() && shouldWakeDisplayForMessage()) {
|
||||
_display->turnOn();
|
||||
}
|
||||
if (_display->isOn()) {
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer
|
||||
_next_refresh = 100; // trigger refresh
|
||||
if (shouldWakeDisplayForMessage()) {
|
||||
_display->wake(mesh::ui::DisplayWake::Message);
|
||||
}
|
||||
if (_display->isOn()) _next_refresh = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1900,10 +1892,8 @@ void UITask::showPairingPin() {
|
||||
}
|
||||
static_cast<HomeScreen*>(home)->showFirstPage();
|
||||
setCurrScreen(home);
|
||||
|
||||
if (_display != NULL) {
|
||||
if (!_display->isOn()) _display->turnOn();
|
||||
_auto_off = now + AUTO_OFF_MILLIS;
|
||||
_display->servicePower(_board->isExternalPowered() || _board->isUsbHostConnected(), hasConnection(), true);
|
||||
_next_refresh = 0;
|
||||
}
|
||||
}
|
||||
@@ -1914,14 +1904,12 @@ void UITask::finishPairingScreen(bool timed_out) {
|
||||
if (_deferred_msg_preview && _msgcount > 0) {
|
||||
_deferred_msg_preview = false;
|
||||
setCurrScreen(msg_preview);
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
} else {
|
||||
_deferred_msg_preview = false;
|
||||
gotoHomeScreen();
|
||||
if (timed_out && _display != NULL) {
|
||||
_display->turnOff();
|
||||
_display->dismiss();
|
||||
} else {
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
_next_refresh = 0;
|
||||
}
|
||||
}
|
||||
@@ -1939,6 +1927,15 @@ void UITask::servicePairingState() {
|
||||
finishPairingScreen(timed_out);
|
||||
}
|
||||
}
|
||||
if (_display != NULL && _display->servicePower(
|
||||
_board->isExternalPowered() || _board->isUsbHostConnected(),
|
||||
hasConnection(), isPairingScreenActive())) {
|
||||
_next_refresh = 0;
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
if (!_display->isOn() && isJohnReaderActive())
|
||||
static_cast<JohnReaderScreen*>(john_reader)->flush();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -2032,7 +2029,7 @@ void UITask::loop() {
|
||||
c = handleLongPress(KEY_ENTER);
|
||||
else
|
||||
#endif
|
||||
display.turnOff();
|
||||
display.dismiss();
|
||||
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
|
||||
c = handleDoubleClick(KEY_SELECT);
|
||||
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK || ev == BUTTON_EVENT_QUADRUPLE_CLICK) {
|
||||
@@ -2149,12 +2146,11 @@ void UITask::loop() {
|
||||
#endif
|
||||
#if defined(BACKLIGHT_BTN)
|
||||
if (millis() > next_backlight_btn_check) {
|
||||
bool touch_state = digitalRead(PIN_BUTTON2);
|
||||
#if defined(DISP_BACKLIGHT)
|
||||
digitalWrite(DISP_BACKLIGHT, !touch_state);
|
||||
#elif defined(EXP_PIN_BACKLIGHT)
|
||||
expander.digitalWrite(EXP_PIN_BACKLIGHT, !touch_state);
|
||||
#endif
|
||||
static bool was_pressed = false;
|
||||
const bool pressed = digitalRead(PIN_BUTTON2) == LOW;
|
||||
if (pressed && !was_pressed && _display != NULL)
|
||||
_display->wake(mesh::ui::DisplayWake::Button);
|
||||
was_pressed = pressed;
|
||||
next_backlight_btn_check = millis() + 300;
|
||||
}
|
||||
#endif
|
||||
@@ -2167,8 +2163,8 @@ void UITask::loop() {
|
||||
}
|
||||
|
||||
if (c != 0 && curr) {
|
||||
_display->wake(mesh::ui::DisplayWake::Button);
|
||||
curr->handleInput(c);
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
|
||||
_next_refresh = 100; // trigger refresh
|
||||
}
|
||||
|
||||
@@ -2225,24 +2221,9 @@ void UITask::loop() {
|
||||
_display->endFrame();
|
||||
}
|
||||
#if AUTO_OFF_MILLIS > 0
|
||||
#ifdef KEEP_DISPLAY_ON_USB
|
||||
// Opt-in: refresh the auto-off deadline while externally powered, so the
|
||||
// timer counts from the moment external power is removed. Off by default
|
||||
// because OLED panels burn in quickly; only enable for LCD targets or
|
||||
// where the display is replaceable.
|
||||
if (board.isExternalPowered()) {
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
}
|
||||
#endif
|
||||
#if MOMENTARY_BUTTON_WAKE_HOLD_MS > 0 && defined(PIN_USER_BTN)
|
||||
if (user_btn.isWakeHoldActive()) _auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
if (user_btn.isWakeHoldActive()) _display->wake(mesh::ui::DisplayWake::Button);
|
||||
#endif
|
||||
if (!isPairingScreenActive() && isDisplayAutoOffDue(_auto_off, AUTO_OFF_MILLIS)) {
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
if (isJohnReaderActive()) static_cast<JohnReaderScreen*>(john_reader)->flush();
|
||||
#endif
|
||||
_display->turnOff();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -2305,11 +2286,11 @@ void UITask::getTouchControls(mesh::ui::TouchSplitSelector& transport_touch_sele
|
||||
|
||||
char UITask::checkDisplayOn(char c) {
|
||||
if (_display != NULL) {
|
||||
if (!_display->isOn()) {
|
||||
_display->turnOn(); // turn display on and consume event
|
||||
const bool was_on = _display->isOn();
|
||||
_display->wake(mesh::ui::DisplayWake::Button);
|
||||
if (!was_on) {
|
||||
c = 0;
|
||||
}
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
|
||||
_next_refresh = 0; // trigger refresh
|
||||
}
|
||||
return c;
|
||||
|
||||
@@ -35,7 +35,7 @@ class UITask : public AbstractUITask {
|
||||
#ifdef PIN_VIBRATION
|
||||
GenericVibration vibration;
|
||||
#endif
|
||||
unsigned long _next_refresh, _auto_off;
|
||||
unsigned long _next_refresh;
|
||||
unsigned long _msg_preview_until;
|
||||
unsigned long _pairing_screen_until;
|
||||
bool _deferred_msg_preview;
|
||||
|
||||
@@ -53,11 +53,11 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, CompanionNode
|
||||
if (_display != NULL) {
|
||||
_display->setRotationDegrees(node_prefs->display_rotation_degrees);
|
||||
}
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
|
||||
clearMsgPreview();
|
||||
_node_prefs = node_prefs;
|
||||
if (_display != NULL) {
|
||||
_display->turnOn();
|
||||
_display->servicePower(_board->isExternalPowered() || _board->isUsbHostConnected(), hasConnection());
|
||||
}
|
||||
|
||||
// strip off dash and commit hash by changing dash to null terminator
|
||||
@@ -182,13 +182,10 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text,
|
||||
StrHelper::strncpy(_msg, text, sizeof(_msg));
|
||||
|
||||
if (_display != NULL) {
|
||||
if (!_display->isOn() && shouldWakeDisplayForMessage()) {
|
||||
_display->turnOn();
|
||||
}
|
||||
if (_display->isOn()) {
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer
|
||||
_need_refresh = true;
|
||||
if (shouldWakeDisplayForMessage()) {
|
||||
_display->wake(mesh::ui::DisplayWake::Message);
|
||||
}
|
||||
if (_display->isOn()) _need_refresh = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,7 +482,7 @@ void UITask::showPairingPin() {
|
||||
_need_refresh = true;
|
||||
_next_refresh = 0;
|
||||
if (_display != NULL && !_display->isOn()) {
|
||||
_display->turnOn();
|
||||
_display->servicePower(_board->isExternalPowered() || _board->isUsbHostConnected(), hasConnection(), true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -496,9 +493,7 @@ void UITask::finishPairingScreen(bool timed_out) {
|
||||
if (_display == NULL) return;
|
||||
|
||||
if (timed_out) {
|
||||
_display->turnOff();
|
||||
} else {
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
_display->dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -514,6 +509,12 @@ void UITask::servicePairingState() {
|
||||
finishPairingScreen(timed_out);
|
||||
}
|
||||
}
|
||||
if (_display != NULL && _display->servicePower(
|
||||
_board->isExternalPowered() || _board->isUsbHostConnected(),
|
||||
hasConnection(), isPairingScreenActive())) {
|
||||
_next_refresh = 0;
|
||||
_need_refresh = true;
|
||||
}
|
||||
}
|
||||
|
||||
void UITask::loop() {
|
||||
@@ -548,18 +549,6 @@ void UITask::loop() {
|
||||
|
||||
_next_refresh = millis() + 1000; // refresh every second
|
||||
}
|
||||
#ifdef KEEP_DISPLAY_ON_USB
|
||||
// Opt-in: refresh the auto-off deadline while externally powered, so the
|
||||
// timer counts from the moment external power is removed. Off by default
|
||||
// because OLED panels burn in quickly; only enable for LCD targets or
|
||||
// where the display is replaceable.
|
||||
if (board.isExternalPowered()) {
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
}
|
||||
#endif
|
||||
if (!isPairingScreenActive() && isDisplayAutoOffDue(_auto_off, AUTO_OFF_MILLIS)) {
|
||||
_display->turnOff();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -569,10 +558,7 @@ void UITask::handleButtonAnyPress() {
|
||||
// do not refresh the display here, as it may block the button handler
|
||||
if (_display != NULL) {
|
||||
_displayWasOn = _display->isOn(); // Track display state before any action
|
||||
if (!_displayWasOn) {
|
||||
_display->turnOn();
|
||||
}
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
|
||||
_display->wake(mesh::ui::DisplayWake::Button);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ class UITask : public AbstractUITask {
|
||||
#ifdef HAS_DRV2605
|
||||
DRV2605Vibration vibration;
|
||||
#endif
|
||||
unsigned long _next_refresh, _auto_off;
|
||||
unsigned long _next_refresh;
|
||||
CompanionNodePrefs* _node_prefs;
|
||||
char _version_info[32];
|
||||
char _origin[62];
|
||||
|
||||
@@ -171,7 +171,6 @@ public:
|
||||
// display.setCursor(0, 0);
|
||||
// display.print(filtered_name);
|
||||
|
||||
|
||||
display.setColor(UIColor::primary_txt);
|
||||
display.setTextSize(2);
|
||||
sprintf(tmp, "MSG: %d", _task->getMsgCount());
|
||||
@@ -441,7 +440,6 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
#if UI_SMALL_MESSAGE_FONT == 1
|
||||
class TinyMessageScreen : public UIScreen {
|
||||
UITask* _task;
|
||||
@@ -480,7 +478,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, CompanionNode
|
||||
if (_display != NULL) {
|
||||
_display->setRotationDegrees(node_prefs->display_rotation_degrees);
|
||||
}
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
|
||||
_cached_batt_mv = getBattMilliVolts();
|
||||
|
||||
#if defined(PIN_USER_BTN)
|
||||
@@ -493,11 +491,10 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, CompanionNode
|
||||
_node_prefs = node_prefs;
|
||||
|
||||
if (_display != NULL) {
|
||||
_display->turnOn();
|
||||
_display->servicePower(_board->isExternalPowered() || _board->isUsbHostConnected(), hasConnection());
|
||||
}
|
||||
_statusBar.begin(_display->width());
|
||||
|
||||
|
||||
#ifdef PIN_BUZZER
|
||||
buzzer.begin();
|
||||
buzzer.quiet(_node_prefs->buzzer_quiet);
|
||||
@@ -553,7 +550,6 @@ switch(t){
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
void UITask::msgRead(int msgcount) {
|
||||
_msgcount = msgcount;
|
||||
if (msgcount == 0) {
|
||||
@@ -580,13 +576,10 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text,
|
||||
#endif
|
||||
|
||||
if (_display != NULL) {
|
||||
if (!_display->isOn() && shouldWakeDisplayForMessage()) {
|
||||
_display->turnOn();
|
||||
}
|
||||
if (_display->isOn()) {
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer
|
||||
_next_refresh = 100; // trigger refresh
|
||||
if (shouldWakeDisplayForMessage()) {
|
||||
_display->wake(mesh::ui::DisplayWake::Message);
|
||||
}
|
||||
if (_display->isOn()) _next_refresh = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -630,9 +623,8 @@ void UITask::showPairingPin() {
|
||||
_pairing_screen_until = now + BLE_PAIRING_DISPLAY_MILLIS;
|
||||
static_cast<HomeScreen*>(home)->showFirstPage();
|
||||
setCurrScreen(home);
|
||||
|
||||
if (_display != NULL) {
|
||||
if (!_display->isOn()) _display->turnOn();
|
||||
_display->servicePower(_board->isExternalPowered() || _board->isUsbHostConnected(), hasConnection(), true);
|
||||
_next_refresh = 0;
|
||||
}
|
||||
}
|
||||
@@ -649,9 +641,7 @@ void UITask::finishPairingScreen(bool timed_out) {
|
||||
if (_display == NULL) return;
|
||||
|
||||
if (timed_out) {
|
||||
_display->turnOff();
|
||||
} else {
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
_display->dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -667,6 +657,9 @@ void UITask::servicePairingState() {
|
||||
finishPairingScreen(timed_out);
|
||||
}
|
||||
}
|
||||
if (_display != NULL && _display->servicePower(
|
||||
_board->isExternalPowered() || _board->isUsbHostConnected(),
|
||||
hasConnection(), isPairingScreenActive())) _next_refresh = 0;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -760,12 +753,11 @@ void UITask::loop() {
|
||||
#endif
|
||||
#if defined(BACKLIGHT_BTN)
|
||||
if (millis() > next_backlight_btn_check) {
|
||||
bool touch_state = digitalRead(PIN_BUTTON2);
|
||||
#if defined(DISP_BACKLIGHT)
|
||||
digitalWrite(DISP_BACKLIGHT, !touch_state);
|
||||
#elif defined(EXP_PIN_BACKLIGHT)
|
||||
expander.digitalWrite(EXP_PIN_BACKLIGHT, !touch_state);
|
||||
#endif
|
||||
static bool was_pressed = false;
|
||||
const bool pressed = digitalRead(PIN_BUTTON2) == LOW;
|
||||
if (pressed && !was_pressed && _display != NULL)
|
||||
_display->wake(mesh::ui::DisplayWake::Button);
|
||||
was_pressed = pressed;
|
||||
next_backlight_btn_check = millis() + 300;
|
||||
}
|
||||
#endif
|
||||
@@ -786,8 +778,8 @@ void UITask::loop() {
|
||||
}
|
||||
|
||||
if (c != 0 && curr) {
|
||||
_display->wake(mesh::ui::DisplayWake::Button);
|
||||
curr->handleInput(c);
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
|
||||
_next_refresh = 100; // trigger refresh
|
||||
}
|
||||
|
||||
@@ -836,18 +828,9 @@ void UITask::loop() {
|
||||
_display->endFrame();
|
||||
}
|
||||
#if AUTO_OFF_MILLIS > 0
|
||||
#ifdef KEEP_DISPLAY_ON_USB
|
||||
// Opt-in: refresh the auto-off deadline while externally powered, so the
|
||||
// timer counts from the moment external power is removed. Off by default
|
||||
// because OLED panels burn in quickly; only enable for LCD targets or
|
||||
// where the display is replaceable.
|
||||
if (board.isExternalPowered()) {
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
}
|
||||
#if MOMENTARY_BUTTON_WAKE_HOLD_MS > 0 && defined(PIN_USER_BTN)
|
||||
if (user_btn.isWakeHoldActive()) _display->wake(mesh::ui::DisplayWake::Button);
|
||||
#endif
|
||||
if (!isPairingScreenActive() && isDisplayAutoOffDue(_auto_off, AUTO_OFF_MILLIS)) {
|
||||
_display->turnOff();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -884,11 +867,11 @@ void UITask::loop() {
|
||||
|
||||
char UITask::checkDisplayOn(char c) {
|
||||
if (_display != NULL) {
|
||||
if (!_display->isOn()) {
|
||||
_display->turnOn(); // turn display on and consume event
|
||||
const bool was_on = _display->isOn();
|
||||
_display->wake(mesh::ui::DisplayWake::Button);
|
||||
if (!was_on) {
|
||||
c = 0;
|
||||
}
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
|
||||
_next_refresh = 0; // trigger refresh
|
||||
}
|
||||
return c;
|
||||
|
||||
@@ -35,7 +35,7 @@ class UITask : public AbstractUITask {
|
||||
#ifdef PIN_VIBRATION
|
||||
GenericVibration vibration;
|
||||
#endif
|
||||
unsigned long _next_refresh, _auto_off;
|
||||
unsigned long _next_refresh;
|
||||
CompanionNodePrefs* _node_prefs;
|
||||
char _alert[80];
|
||||
unsigned long _alert_expiry;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
class UITask {
|
||||
mesh::MainBoard* _board;
|
||||
DisplayDriver* _display;
|
||||
unsigned long _next_read, _next_refresh, _auto_off;
|
||||
unsigned long _next_read, _next_refresh;
|
||||
int _prevBtnState;
|
||||
NodePrefs* _node_prefs;
|
||||
char _version_info[32];
|
||||
@@ -54,11 +54,9 @@ class UITask {
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
MQTTPrefs* _observer_prefs = NULL;
|
||||
#endif
|
||||
unsigned long _timeout_seen = 0; // to notice a live `display.timeout` change
|
||||
uint8_t _flip_seen = 0xFF; // 0xFF forces the first apply
|
||||
void applyDisplayFlip();
|
||||
|
||||
unsigned long displayTimeoutMillis() const;
|
||||
|
||||
void renderCurrScreen();
|
||||
public:
|
||||
@@ -66,8 +64,7 @@ public:
|
||||
void begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version);
|
||||
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
// Supplies `display.timeout`, which is read live so a config change applies
|
||||
// without a reboot. Call before begin().
|
||||
// Supplies display.flip. Call before begin().
|
||||
void setObserverPrefs(MQTTPrefs* prefs) { _observer_prefs = prefs; }
|
||||
#endif
|
||||
|
||||
|
||||
@@ -96,10 +96,7 @@ void setup() {
|
||||
#ifdef DISPLAY_CLASS
|
||||
display_ready = display.begin();
|
||||
if (display_ready) {
|
||||
display.startFrame();
|
||||
display.setCursor(0, 0);
|
||||
display.print("Please wait...");
|
||||
display.endFrame();
|
||||
display.turnOff(); // Stay dark until the saved display policy is loaded.
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -18,9 +18,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
|
||||
@@ -54,15 +51,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
|
||||
|
||||
#ifdef DISPLAY_TOUCH_TOGGLE
|
||||
@@ -88,14 +76,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();
|
||||
@@ -342,11 +328,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
|
||||
@@ -359,17 +341,25 @@ 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_TOUCH_TOGGLE)
|
||||
int ev = user_btn.check();
|
||||
if (ev == BUTTON_EVENT_CLICK) {
|
||||
toggleDisplay("button");
|
||||
} 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
|
||||
@@ -390,7 +380,7 @@ void UITask::loop() {
|
||||
int ev = user_btn.check();
|
||||
if (ev != BUTTON_EVENT_NONE) {
|
||||
if (!_display->isOn()) {
|
||||
_display->turnOn();
|
||||
_display->wake(mesh::ui::DisplayWake::Button);
|
||||
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
||||
_frame_valid = false;
|
||||
#endif
|
||||
@@ -399,7 +389,7 @@ void UITask::loop() {
|
||||
#endif
|
||||
_next_refresh = 0;
|
||||
}
|
||||
_auto_off = millis() + displayTimeoutMillis();
|
||||
_display->wake(mesh::ui::DisplayWake::Button);
|
||||
}
|
||||
#elif defined(PIN_USER_BTN)
|
||||
if (millisReached(millis(), _next_read)) {
|
||||
@@ -412,7 +402,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
|
||||
@@ -420,7 +410,7 @@ void UITask::loop() {
|
||||
_rows_valid = false;
|
||||
#endif
|
||||
}
|
||||
_auto_off = millis() + displayTimeoutMillis(); // extend auto-off timer
|
||||
_display->wake(mesh::ui::DisplayWake::Button);
|
||||
#endif
|
||||
}
|
||||
_prevBtnState = btnState;
|
||||
@@ -429,23 +419,6 @@ void UITask::loop() {
|
||||
}
|
||||
#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();
|
||||
@@ -456,25 +429,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.
|
||||
@@ -503,25 +457,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
|
||||
#ifdef DISPLAY_TOUCH_TOGGLE
|
||||
if (_powering_off_at == 0 && timeout > 0 && millisReached(millis(), _auto_off)) {
|
||||
#else
|
||||
if (timeout > 0 && millisReached(millis(), _auto_off)) {
|
||||
#endif
|
||||
_display->turnOff();
|
||||
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
||||
_frame_valid = false;
|
||||
#endif
|
||||
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
||||
_rows_valid = false;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef DISPLAY_TOUCH_TOGGLE
|
||||
|
||||
@@ -21,7 +21,7 @@ class UITask {
|
||||
mesh::MainBoard* _board;
|
||||
#endif
|
||||
DisplayDriver* _display;
|
||||
unsigned long _next_read, _next_refresh, _auto_off;
|
||||
unsigned long _next_read, _next_refresh;
|
||||
int _prevBtnState;
|
||||
NodePrefs* _node_prefs;
|
||||
char _version_info[32];
|
||||
@@ -58,11 +58,9 @@ class UITask {
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
MQTTPrefs* _observer_prefs = NULL;
|
||||
#endif
|
||||
unsigned long _timeout_seen = 0; // to notice a live `display.timeout` change
|
||||
uint8_t _flip_seen = 0xFF; // 0xFF forces the first apply
|
||||
void applyDisplayFlip();
|
||||
|
||||
unsigned long displayTimeoutMillis() const;
|
||||
|
||||
void renderCurrScreen();
|
||||
public:
|
||||
@@ -76,8 +74,7 @@ public:
|
||||
void begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version);
|
||||
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
// Supplies `display.timeout`, which is read live so a config change applies
|
||||
// without a reboot. Call before begin().
|
||||
// Supplies display.flip. Call before begin().
|
||||
void setObserverPrefs(MQTTPrefs* prefs) { _observer_prefs = prefs; }
|
||||
#endif
|
||||
|
||||
|
||||
@@ -62,10 +62,7 @@ void setup() {
|
||||
#ifdef DISPLAY_CLASS
|
||||
display_ready = display.begin();
|
||||
if (display_ready) {
|
||||
display.startFrame();
|
||||
display.setCursor(0, 0);
|
||||
display.print("Please wait...");
|
||||
display.endFrame();
|
||||
display.turnOff(); // Stay dark until the saved display policy is loaded.
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#define USER_BTN_PRESSED LOW
|
||||
#endif
|
||||
|
||||
#define AUTO_OFF_MILLIS 20000 // 20 seconds
|
||||
#define BOOT_SCREEN_MILLIS 4000 // 4 seconds
|
||||
|
||||
// 'meshcore', 128x13px
|
||||
@@ -29,9 +28,8 @@ static const uint8_t meshcore_logo [] PROGMEM = {
|
||||
|
||||
void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version) {
|
||||
_prevBtnState = HIGH;
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
_node_prefs = node_prefs;
|
||||
_display->turnOn();
|
||||
_display->servicePower(board.isExternalPowered() || board.isUsbHostConnected());
|
||||
#if defined(PIN_USER_BTN) && defined(DISPLAY_CLASS) \
|
||||
&& defined(MOMENTARY_BUTTON_WAKE_FROM_SLEEP) \
|
||||
&& MOMENTARY_BUTTON_WAKE_FROM_SLEEP
|
||||
@@ -98,16 +96,17 @@ void UITask::renderCurrScreen() {
|
||||
}
|
||||
|
||||
void UITask::loop() {
|
||||
if (_display->servicePower(board.isExternalPowered() || board.isUsbHostConnected())) _next_refresh = 0;
|
||||
#if defined(PIN_USER_BTN) && defined(DISPLAY_CLASS) \
|
||||
&& defined(MOMENTARY_BUTTON_WAKE_FROM_SLEEP) \
|
||||
&& MOMENTARY_BUTTON_WAKE_FROM_SLEEP
|
||||
int ev = user_btn.check();
|
||||
if (ev != BUTTON_EVENT_NONE) {
|
||||
if (!_display->isOn()) {
|
||||
_display->turnOn();
|
||||
_display->wake(mesh::ui::DisplayWake::Button);
|
||||
_next_refresh = 0;
|
||||
}
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
_display->wake(mesh::ui::DisplayWake::Button);
|
||||
}
|
||||
#elif defined(PIN_USER_BTN)
|
||||
if (millis() >= _next_read) {
|
||||
@@ -117,9 +116,9 @@ void UITask::loop() {
|
||||
if (_display->isOn()) {
|
||||
// TODO: any action ?
|
||||
} else {
|
||||
_display->turnOn();
|
||||
_display->wake(mesh::ui::DisplayWake::Button);
|
||||
}
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
|
||||
_display->wake(mesh::ui::DisplayWake::Button); // extend auto-off timer
|
||||
}
|
||||
_prevBtnState = btnState;
|
||||
}
|
||||
@@ -136,10 +135,8 @@ void UITask::loop() {
|
||||
_next_refresh = millis() + 1000; // refresh every second
|
||||
}
|
||||
#if MOMENTARY_BUTTON_WAKE_HOLD_MS > 0 && defined(PIN_USER_BTN)
|
||||
if (user_btn.isWakeHoldActive()) _auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
if (user_btn.isWakeHoldActive()) _display->wake(mesh::ui::DisplayWake::Button);
|
||||
#endif
|
||||
if (millis() > _auto_off) {
|
||||
_display->turnOff();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
class UITask {
|
||||
DisplayDriver* _display;
|
||||
unsigned long _next_read, _next_refresh, _auto_off;
|
||||
unsigned long _next_read, _next_refresh;
|
||||
int _prevBtnState;
|
||||
NodePrefs* _node_prefs;
|
||||
char _version_info[32];
|
||||
|
||||
@@ -75,9 +75,8 @@ void setup() {
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
if (display.begin()) {
|
||||
display.startFrame();
|
||||
display.print("Please wait...");
|
||||
display.endFrame();
|
||||
display.turnOff();
|
||||
// Saved display policy is applied by UITask::begin().
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -58,6 +58,8 @@ build_flags = -w -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1
|
||||
build_src_filter =
|
||||
+<*.cpp>
|
||||
+<helpers/*.cpp>
|
||||
+<helpers/ui/DisplayDriver.cpp>
|
||||
+<helpers/ui/DisplayPowerSettings.cpp>
|
||||
+<helpers/ota/OtaTinf.c>
|
||||
; MQTT-only sources (need Timezone/NTPClient); observer envs add them back.
|
||||
-<helpers/MQTTMessageBuilder.cpp>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <Arduino.h>
|
||||
#include "CommonCLI.h"
|
||||
#include <helpers/ui/DisplayPowerSettings.h>
|
||||
#include "CLICommandUtils.h"
|
||||
#include "FloodAdvertCLI.h"
|
||||
#include "StorageLayout.h"
|
||||
@@ -731,6 +732,8 @@ static void formatSnrDbX4Short(char* dest, size_t dest_len, int16_t snr_x4) {
|
||||
}
|
||||
|
||||
void CommonCLI::loadPrefs(FILESYSTEM* fs) {
|
||||
const bool display_settings_loaded = mesh::ui::loadDisplayPowerSettings(fs, false);
|
||||
(void)display_settings_loaded;
|
||||
bool is_fresh_install = false;
|
||||
bool is_upgrade = false;
|
||||
bool loaded = false;
|
||||
@@ -785,6 +788,8 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) {
|
||||
MQTTPrefsAtomicStore::LegacyUpgradeGate legacy_upgrade(
|
||||
_com_prefs_needs_upgrade || node_prefs_needs_migration);
|
||||
loadMQTTPrefs(fs, &legacy_upgrade);
|
||||
if (!display_settings_loaded && _mqtt_prefs.display_timeout_secs != DISPLAY_TIMEOUT_DEFAULT_SECS)
|
||||
mesh::ui::migrateLegacyDisplayTimeout(_mqtt_prefs.display_timeout_secs);
|
||||
if (_mqtt_prefs_hold) legacy_upgrade.holdMqttSource();
|
||||
|
||||
// For MQTT bridge, migrate bridge.source to RX (logRx) only on fresh installs or upgrades
|
||||
@@ -2457,6 +2462,7 @@ uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) {
|
||||
|
||||
void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
|
||||
mesh::cli::normalizeCommandVerb(command);
|
||||
if (mesh::ui::handleDisplayPowerCommand(command, reply, 160)) return;
|
||||
|
||||
// Observer-only top-level commands (ota check/update, tls.bundletest, alert test)
|
||||
// live in CommonCLI_Observer.cpp.
|
||||
|
||||
@@ -22,6 +22,7 @@ static const char* const WC_ALLOWED_SET_KEYS[] = {
|
||||
"lat", "lon", "radio", "tx", "af", "rxdelay", "txdelay",
|
||||
"cad", "radio.rxgain", "radio.fem.rxgain", "radio.rxps", "powersaving",
|
||||
"repeat",
|
||||
"display.mode", "display.timeout", "display.usb.mode", "display.usb.timeout",
|
||||
"advert.interval", "flood.advert.interval",
|
||||
"flood.max", "flood.max.advert", "flood.max.unscoped", "loop.detect",
|
||||
// MQTTPrefs (WiFi / MQTT / misc observer)
|
||||
|
||||
+2171
-2137
File diff suppressed because it is too large.
Load diff
@@ -10,6 +10,8 @@ static_assert(sizeof(WEBCONFIG_AP_PREFIX) <= 28,
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <DNSServer.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <helpers/ui/DisplayPowerSettings.h>
|
||||
#include <helpers/ui/DisplayBuildFlags.h>
|
||||
#include <Preferences.h>
|
||||
#include <esp_wifi.h>
|
||||
#include <esp_system.h>
|
||||
@@ -1791,6 +1793,15 @@ void WebConfigServer::handleConfigGet(AsyncWebServerRequest* req) {
|
||||
radio["rxps_rx_us"] = node.rx_ps_rx_us;
|
||||
radio["rxps_sleep_us"] = node.rx_ps_sleep_us;
|
||||
radio["powersaving"] = (bool)node.power_saving;
|
||||
#ifdef MESHCORE_HAS_REAL_DISPLAY
|
||||
JsonObject screen = doc["display"].to<JsonObject>();
|
||||
const auto& display_prefs = mesh::ui::displayPowerPrefs();
|
||||
screen["mode"] = mesh::ui::displayModeName(display_prefs.battery.mode);
|
||||
screen["timeout"] = display_prefs.battery.seconds;
|
||||
screen["usb_mode"] = mesh::ui::displayModeName(display_prefs.usb.mode);
|
||||
screen["usb_timeout"] = display_prefs.usb.seconds;
|
||||
screen["pairing"] = mesh::ui::displayPairingSupported();
|
||||
#endif
|
||||
radio["repeat"] = (bool)node.repeat;
|
||||
radio["flood_max"] = node.flood_max;
|
||||
radio["flood_max_advert"] = node.flood_max_advert;
|
||||
|
||||
@@ -1,4 +1,23 @@
|
||||
#include "DisplayDriver.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
bool DisplayDriver::servicePower(bool usb_power, bool app_connected, bool pairing) {
|
||||
_power_policy.update(mesh::ui::displayPowerPrefs(), usb_power, app_connected, pairing, millis());
|
||||
const bool changed = isOn() != _power_policy.on();
|
||||
if (changed) { if (_power_policy.on()) turnOn(); else turnOff(); }
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool DisplayDriver::wake(mesh::ui::DisplayWake reason) {
|
||||
if (!_power_policy.wake(reason, millis())) return false;
|
||||
if (_power_policy.on() && !isOn()) turnOn();
|
||||
return true;
|
||||
}
|
||||
|
||||
void DisplayDriver::dismiss() {
|
||||
_power_policy.dismiss();
|
||||
if (!_power_policy.on() && isOn()) turnOff();
|
||||
}
|
||||
|
||||
#if defined(ESP32_PLATFORM) && defined(MESHCORE_HAS_REAL_DISPLAY)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "DisplayBuildFlags.h"
|
||||
#include "DisplayPowerPolicy.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
@@ -15,6 +16,7 @@ public:
|
||||
|
||||
class DisplayDriver {
|
||||
int _w, _h;
|
||||
mesh::ui::DisplayPowerPolicy _power_policy;
|
||||
protected:
|
||||
DisplayDriver(int w, int h) { _w = w; _h = h; }
|
||||
void setDimensions(int w, int h) { _w = w; _h = h; }
|
||||
@@ -52,6 +54,10 @@ public:
|
||||
virtual void forceFullRefresh() {} // next refresh will be full for eink
|
||||
virtual void turnOn() = 0;
|
||||
virtual void turnOff() = 0;
|
||||
// Returns whether the panel state changed, allowing cached UIs to redraw.
|
||||
bool servicePower(bool usb_power, bool app_connected = false, bool pairing = false);
|
||||
bool wake(mesh::ui::DisplayWake reason);
|
||||
void dismiss();
|
||||
virtual void clear() = 0;
|
||||
virtual void startFrame(ColorVal bkg = UIColor::window_bkg) = 0;
|
||||
virtual void setTextSize(int sz) = 0;
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh { namespace ui {
|
||||
|
||||
enum class DisplayMode : uint8_t { Off, On, Button, Pairing, ButtonPairing, Automatic };
|
||||
enum class DisplayWake : uint8_t { Boot, Button, Message };
|
||||
|
||||
struct DisplayPowerProfile {
|
||||
DisplayMode mode = DisplayMode::Button;
|
||||
uint16_t seconds = 15;
|
||||
};
|
||||
struct DisplayPowerPrefs {
|
||||
DisplayPowerProfile battery;
|
||||
DisplayPowerProfile usb;
|
||||
};
|
||||
|
||||
inline DisplayPowerPrefs& displayPowerPrefs() {
|
||||
static DisplayPowerPrefs prefs;
|
||||
return prefs;
|
||||
}
|
||||
inline const char* displayModeName(DisplayMode mode) {
|
||||
switch (mode) {
|
||||
case DisplayMode::Off: return "off";
|
||||
case DisplayMode::On: return "on";
|
||||
case DisplayMode::Button: return "button";
|
||||
case DisplayMode::Pairing: return "pairing";
|
||||
case DisplayMode::ButtonPairing: return "button-pairing";
|
||||
case DisplayMode::Automatic: return "automatic";
|
||||
}
|
||||
return "off";
|
||||
}
|
||||
inline bool parseDisplayMode(const char* text, DisplayMode& mode) {
|
||||
for (uint8_t i = 0; i <= uint8_t(DisplayMode::Automatic); ++i) {
|
||||
if (strcmp(text, displayModeName(DisplayMode(i))) == 0) {
|
||||
mode = DisplayMode(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
inline bool displayModeSupportsPairing(DisplayMode mode) {
|
||||
return mode == DisplayMode::Pairing || mode == DisplayMode::ButtonPairing;
|
||||
}
|
||||
inline bool validDisplayProfile(const DisplayPowerProfile& profile) {
|
||||
return uint8_t(profile.mode) <= uint8_t(DisplayMode::Automatic)
|
||||
&& profile.seconds >= 1 && profile.seconds <= 3600;
|
||||
}
|
||||
|
||||
// Pure, wrap-safe policy shared by every UI. Radio traffic, refreshes and USB
|
||||
// presence are not user activity. An active pairing request has its own expiry.
|
||||
class DisplayPowerPolicy {
|
||||
DisplayPowerProfile _profile;
|
||||
bool _initialized = false, _usb = false, _connected = false, _pairing = false;
|
||||
bool _timed = false;
|
||||
uint32_t _deadline = 0;
|
||||
public:
|
||||
void update(const DisplayPowerPrefs& prefs, bool usb, bool connected,
|
||||
bool pairing, uint32_t now) {
|
||||
const auto& selected = usb ? prefs.usb : prefs.battery;
|
||||
const bool reset = !_initialized || _usb != usb || _profile.mode != selected.mode;
|
||||
const bool timeout_changed = _profile.seconds != selected.seconds;
|
||||
const bool just_connected = connected && !_connected;
|
||||
_profile = selected;
|
||||
_usb = usb;
|
||||
_connected = connected;
|
||||
_pairing = pairing;
|
||||
if (reset) {
|
||||
_timed = false;
|
||||
_initialized = true;
|
||||
wake(DisplayWake::Boot, now);
|
||||
} else if (timeout_changed && _timed) {
|
||||
_deadline = now + uint32_t(_profile.seconds) * 1000;
|
||||
}
|
||||
if (just_connected && _profile.mode == DisplayMode::Automatic) _timed = false;
|
||||
if (_timed && int32_t(now - _deadline) >= 0) _timed = false;
|
||||
}
|
||||
bool wake(DisplayWake reason, uint32_t now) {
|
||||
if (!_initialized) return false;
|
||||
bool allowed = _profile.mode == DisplayMode::On;
|
||||
if (reason == DisplayWake::Button) {
|
||||
allowed |= _profile.mode == DisplayMode::Button
|
||||
|| _profile.mode == DisplayMode::ButtonPairing
|
||||
|| _profile.mode == DisplayMode::Automatic;
|
||||
} else {
|
||||
allowed |= _profile.mode == DisplayMode::Automatic && !_connected;
|
||||
}
|
||||
if (!allowed) return false;
|
||||
_timed = true;
|
||||
uint32_t duration = uint32_t(_profile.seconds) * 1000;
|
||||
if (reason == DisplayWake::Boot && duration > 4000) duration = 4000;
|
||||
_deadline = now + duration;
|
||||
return true;
|
||||
}
|
||||
void dismiss() { _timed = false; }
|
||||
bool on() const {
|
||||
if (!_initialized || _profile.mode == DisplayMode::Off) return false;
|
||||
if (_profile.mode == DisplayMode::On) return true;
|
||||
if (_pairing && (_profile.mode == DisplayMode::Automatic
|
||||
|| displayModeSupportsPairing(_profile.mode))) return true;
|
||||
return _profile.mode != DisplayMode::Pairing && _timed;
|
||||
}
|
||||
};
|
||||
|
||||
} }
|
||||
@@ -0,0 +1,159 @@
|
||||
#include "DisplayPowerSettings.h"
|
||||
#include "DisplayBuildFlags.h"
|
||||
#include <stdio.h>
|
||||
|
||||
namespace mesh { namespace ui {
|
||||
namespace {
|
||||
FILESYSTEM* settings_fs = nullptr;
|
||||
bool pairing_available = false;
|
||||
const char* const path = "/display_prefs";
|
||||
const char* const backup = "/display_prefs.bak";
|
||||
const char* const temporary = "/display_prefs.tmp";
|
||||
|
||||
// Explicit bytes avoid compiler padding and preserve a small fixed format.
|
||||
void encode(const DisplayPowerPrefs& prefs, uint8_t (&data)[12]) {
|
||||
data[0] = 'D'; data[1] = 'P'; data[2] = 1;
|
||||
data[3] = uint8_t(prefs.battery.mode);
|
||||
data[4] = prefs.battery.seconds & 255; data[5] = prefs.battery.seconds >> 8;
|
||||
data[6] = uint8_t(prefs.usb.mode);
|
||||
data[7] = prefs.usb.seconds & 255; data[8] = prefs.usb.seconds >> 8;
|
||||
data[9] = 0; data[10] = 0; data[11] = 0;
|
||||
for (unsigned i = 0; i < 11; ++i) data[11] ^= data[i];
|
||||
}
|
||||
bool readPrefs(const char* name, DisplayPowerPrefs& prefs) {
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
File file(*settings_fs);
|
||||
if (!file.open(name, FILE_O_READ)) return false;
|
||||
#else
|
||||
File file = settings_fs->open(name, "r");
|
||||
if (!file) return false;
|
||||
#endif
|
||||
uint8_t data[12], expected[12];
|
||||
const bool complete = file.size() == sizeof(data)
|
||||
&& file.read(data, sizeof(data)) == sizeof(data);
|
||||
file.close();
|
||||
if (!complete) return false;
|
||||
prefs.battery.mode = DisplayMode(data[3]);
|
||||
prefs.battery.seconds = uint16_t(data[4]) | (uint16_t(data[5]) << 8);
|
||||
prefs.usb.mode = DisplayMode(data[6]);
|
||||
prefs.usb.seconds = uint16_t(data[7]) | (uint16_t(data[8]) << 8);
|
||||
encode(prefs, expected);
|
||||
return memcmp(data, expected, sizeof(data)) == 0
|
||||
&& validDisplayProfile(prefs.battery) && validDisplayProfile(prefs.usb);
|
||||
}
|
||||
bool savePrefs(const DisplayPowerPrefs& prefs) {
|
||||
if (!settings_fs) return false;
|
||||
// Preserve the previous complete file until the replacement is verified.
|
||||
settings_fs->remove(temporary);
|
||||
if (settings_fs->exists(temporary)) return false;
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
File file(*settings_fs);
|
||||
if (!file.open(temporary, FILE_O_WRITE)) return false;
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
File file = settings_fs->open(temporary, "w");
|
||||
if (!file) return false;
|
||||
#else
|
||||
File file = settings_fs->open(temporary, "w", true);
|
||||
if (!file) return false;
|
||||
#endif
|
||||
uint8_t data[12];
|
||||
encode(prefs, data);
|
||||
const bool written = file.write(data, sizeof(data)) == sizeof(data);
|
||||
file.close();
|
||||
DisplayPowerPrefs checked;
|
||||
if (!written || !readPrefs(temporary, checked)) return false;
|
||||
if (settings_fs->exists(backup) && !settings_fs->remove(backup)) return false;
|
||||
if (settings_fs->exists(path) && !settings_fs->rename(path, backup)) return false;
|
||||
if (!settings_fs->rename(temporary, path)) {
|
||||
if (settings_fs->exists(backup)) settings_fs->rename(backup, path);
|
||||
return false;
|
||||
}
|
||||
settings_fs->remove(backup);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool loadDisplayPowerSettings(FILESYSTEM* fs, bool pairing_supported) {
|
||||
settings_fs = fs;
|
||||
pairing_available = pairing_supported;
|
||||
DisplayPowerPrefs prefs;
|
||||
if (pairing_supported) prefs.battery.mode = prefs.usb.mode = DisplayMode::ButtonPairing;
|
||||
displayPowerPrefs() = prefs;
|
||||
if (!fs) return false;
|
||||
bool loaded = readPrefs(path, prefs);
|
||||
if (!loaded && readPrefs(backup, prefs)) {
|
||||
loaded = true;
|
||||
fs->remove(path);
|
||||
fs->rename(backup, path);
|
||||
}
|
||||
if (!loaded) return false;
|
||||
if (!pairing_supported) {
|
||||
if (displayModeSupportsPairing(prefs.battery.mode)) prefs.battery.mode = DisplayMode::Button;
|
||||
if (displayModeSupportsPairing(prefs.usb.mode)) prefs.usb.mode = DisplayMode::Button;
|
||||
}
|
||||
displayPowerPrefs() = prefs;
|
||||
return true;
|
||||
}
|
||||
bool displayPairingSupported() { return pairing_available; }
|
||||
|
||||
void migrateLegacyDisplayTimeout(uint16_t seconds) {
|
||||
if (seconds > 3600) return;
|
||||
auto prefs = displayPowerPrefs();
|
||||
prefs.battery.mode = prefs.usb.mode = seconds == 0 ? DisplayMode::On : DisplayMode::Automatic;
|
||||
if (seconds) prefs.battery.seconds = prefs.usb.seconds = seconds;
|
||||
// Keep the old behavior in memory even if flash is temporarily unavailable.
|
||||
displayPowerPrefs() = prefs;
|
||||
savePrefs(prefs);
|
||||
}
|
||||
|
||||
bool handleDisplayPowerCommand(const char* command, char* reply, size_t reply_size) {
|
||||
const bool get = strncmp(command, "get ", 4) == 0;
|
||||
const bool set = strncmp(command, "set ", 4) == 0;
|
||||
if (!get && !set) return false;
|
||||
const char* key = command + 4;
|
||||
const char* keys[] = {"display.mode", "display.timeout", "display.usb.mode", "display.usb.timeout"};
|
||||
int index = -1;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
const size_t len = strlen(keys[i]);
|
||||
if (strncmp(key, keys[i], len) == 0 && (key[len] == 0 || key[len] == ' ')) {
|
||||
index = i; break;
|
||||
}
|
||||
}
|
||||
if (index < 0) return false;
|
||||
#ifndef MESHCORE_HAS_REAL_DISPLAY
|
||||
snprintf(reply, reply_size, "Error: display unsupported");
|
||||
return true;
|
||||
#else
|
||||
auto candidate = displayPowerPrefs();
|
||||
auto& profile = index < 2 ? candidate.battery : candidate.usb;
|
||||
const char* value = key + strlen(keys[index]);
|
||||
if (get) {
|
||||
if (*value) snprintf(reply, reply_size, "Error: unexpected argument");
|
||||
else if (index & 1) snprintf(reply, reply_size, "> %u", unsigned(profile.seconds));
|
||||
else snprintf(reply, reply_size, "> %s", displayModeName(profile.mode));
|
||||
return true;
|
||||
}
|
||||
while (*value == ' ') ++value;
|
||||
if (index & 1) {
|
||||
uint32_t seconds = 0;
|
||||
bool valid = *value != 0;
|
||||
for (const char* p = value; *p && valid; ++p) {
|
||||
valid = *p >= '0' && *p <= '9';
|
||||
if (valid) { seconds = seconds * 10 + (*p - '0'); valid = seconds <= 3600; }
|
||||
}
|
||||
if (!valid || seconds == 0) {
|
||||
snprintf(reply, reply_size, "Error: timeout must be 1-3600 seconds"); return true;
|
||||
}
|
||||
profile.seconds = uint16_t(seconds);
|
||||
} else if (!parseDisplayMode(value, profile.mode)) {
|
||||
snprintf(reply, reply_size, "Error: use off|on|button|pairing|button-pairing|automatic"); return true;
|
||||
} else if (displayModeSupportsPairing(profile.mode) && !pairing_available) {
|
||||
snprintf(reply, reply_size, "Error: BLE pairing display unsupported"); return true;
|
||||
}
|
||||
if (!savePrefs(candidate)) snprintf(reply, reply_size, "Error: display settings save failed");
|
||||
else { displayPowerPrefs() = candidate; snprintf(reply, reply_size, "OK"); }
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
} }
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <helpers/IdentityStore.h>
|
||||
#include "DisplayPowerPolicy.h"
|
||||
|
||||
namespace mesh { namespace ui {
|
||||
|
||||
// Separate, versioned file: existing node and MQTT preference layouts stay
|
||||
// readable by older firmware. Returns true when a saved profile was loaded.
|
||||
bool loadDisplayPowerSettings(FILESYSTEM* fs, bool pairing_supported);
|
||||
bool handleDisplayPowerCommand(const char* command, char* reply, size_t reply_size);
|
||||
bool displayPairingSupported();
|
||||
void migrateLegacyDisplayTimeout(uint16_t seconds);
|
||||
|
||||
} }
|
||||
@@ -0,0 +1,193 @@
|
||||
"""Exercise the shared display policy and real settings/CLI code on the host."""
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
FS_STUB = r'''
|
||||
#pragma once
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstring>
|
||||
#include <cstdint>
|
||||
struct FakeFS;
|
||||
struct File {
|
||||
FakeFS* fs = nullptr;
|
||||
std::string name;
|
||||
explicit operator bool() const { return fs != nullptr; }
|
||||
size_t size() const;
|
||||
size_t read(uint8_t*, size_t);
|
||||
size_t write(const uint8_t*, size_t);
|
||||
void close() {}
|
||||
};
|
||||
struct FakeFS {
|
||||
std::map<std::string, std::vector<uint8_t>> files;
|
||||
bool fail_write = false, fail_publish = false;
|
||||
bool exists(const char* name) { return files.count(name); }
|
||||
bool remove(const char* name) { files.erase(name); return true; }
|
||||
bool rename(const char* from, const char* to) {
|
||||
if (fail_publish && std::string(from) == "/display_prefs.tmp") return false;
|
||||
if (!exists(from) || exists(to)) return false;
|
||||
files[to] = files[from]; files.erase(from); return true;
|
||||
}
|
||||
File open(const char* name, const char* mode, bool = false) {
|
||||
if (*mode == 'w') files[name].clear();
|
||||
if (!exists(name)) return {};
|
||||
return File{this, name};
|
||||
}
|
||||
};
|
||||
inline size_t File::size() const { return fs->files[name].size(); }
|
||||
inline size_t File::read(uint8_t* out, size_t count) {
|
||||
const auto& bytes = fs->files[name];
|
||||
count = count < bytes.size() ? count : bytes.size();
|
||||
memcpy(out, bytes.data(), count); return count;
|
||||
}
|
||||
inline size_t File::write(const uint8_t* in, size_t count) {
|
||||
if (fs->fail_write) return 0;
|
||||
fs->files[name].assign(in, in + count); return count;
|
||||
}
|
||||
#define FILESYSTEM FakeFS
|
||||
'''
|
||||
|
||||
TEST = r'''
|
||||
#include <cassert>
|
||||
#include <helpers/ui/DisplayPowerSettings.h>
|
||||
using namespace mesh::ui;
|
||||
void policyTests() {
|
||||
for (unsigned mode = 0; mode <= 5; ++mode) {
|
||||
DisplayPowerPrefs prefs;
|
||||
prefs.battery.mode = DisplayMode(mode);
|
||||
DisplayPowerPolicy p;
|
||||
p.update(prefs, false, false, false, 0);
|
||||
assert(p.on() == (mode == 1 || mode == 5));
|
||||
p.update(prefs, false, false, false, 4000);
|
||||
assert(p.on() == (mode == 1));
|
||||
bool button = mode == 1 || mode == 2 || mode == 4 || mode == 5;
|
||||
assert(p.wake(DisplayWake::Button, 5000) == button);
|
||||
assert(p.on() == button);
|
||||
p.update(prefs, false, false, false, 20000);
|
||||
assert(p.on() == (mode == 1));
|
||||
assert(p.wake(DisplayWake::Message, 21000) == (mode == 1 || mode == 5));
|
||||
p.update(prefs, false, true, false, 22000);
|
||||
assert(p.on() == (mode == 1));
|
||||
assert(p.wake(DisplayWake::Message, 23000) == (mode == 1));
|
||||
assert(p.wake(DisplayWake::Button, 24000) == button);
|
||||
p.update(prefs, false, true, false, 40000);
|
||||
p.update(prefs, false, false, true, 41000);
|
||||
assert(p.on() == (mode == 1 || mode == 3 || mode == 4 || mode == 5));
|
||||
p.update(prefs, false, false, true, 100000);
|
||||
assert(p.on() == (mode == 1 || mode == 3 || mode == 4 || mode == 5));
|
||||
p.update(prefs, false, true, false, 100001);
|
||||
assert(p.on() == (mode == 1));
|
||||
}
|
||||
DisplayPowerPrefs prefs;
|
||||
prefs.usb.mode = DisplayMode::On;
|
||||
DisplayPowerPolicy p;
|
||||
p.update(prefs, false, false, false, 100);
|
||||
p.wake(DisplayWake::Button, 200);
|
||||
p.update(prefs, true, false, false, 300);
|
||||
assert(p.on());
|
||||
p.update(prefs, false, false, false, 400);
|
||||
assert(!p.on()); // USB wake does not leak into the battery profile.
|
||||
p.wake(DisplayWake::Button, 500);
|
||||
prefs.battery.seconds = 1;
|
||||
p.update(prefs, false, false, false, 600);
|
||||
assert(p.on());
|
||||
p.update(prefs, false, false, false, 1600);
|
||||
assert(!p.on());
|
||||
p.wake(DisplayWake::Button, UINT32_MAX - 500);
|
||||
p.update(prefs, false, false, false, 498);
|
||||
assert(p.on());
|
||||
p.update(prefs, false, false, false, 499);
|
||||
assert(!p.on());
|
||||
p.wake(DisplayWake::Button, 1000);
|
||||
prefs.battery.mode = DisplayMode::Off;
|
||||
p.update(prefs, false, false, true, 1001);
|
||||
assert(!p.on()); // Off wins over a pending button wake and pairing.
|
||||
}
|
||||
void commandTests() {
|
||||
FakeFS fs;
|
||||
assert(!loadDisplayPowerSettings(&fs, true));
|
||||
assert(displayPowerPrefs().battery.mode == DisplayMode::ButtonPairing);
|
||||
char reply[160];
|
||||
auto run = [&](const char* text, const char* expected) {
|
||||
assert(handleDisplayPowerCommand(text, reply, sizeof(reply)));
|
||||
assert(std::string(reply) == expected);
|
||||
};
|
||||
run("set display.mode automatic", "OK");
|
||||
run("set display.timeout 27", "OK");
|
||||
run("set display.usb.mode on", "OK");
|
||||
run("set display.usb.timeout 123", "OK");
|
||||
assert(loadDisplayPowerSettings(&fs, true));
|
||||
run("get display.mode", "> automatic");
|
||||
run("get display.timeout", "> 27");
|
||||
run("get display.usb.mode", "> on");
|
||||
run("get display.usb.timeout", "> 123");
|
||||
const auto saved = fs.files["/display_prefs"];
|
||||
for (const char* value : {"", "0", "-1", "+2", "1.5", "15seconds", "3601", "9999999999999999999999"}) {
|
||||
const std::string command = std::string("set display.timeout ") + value;
|
||||
run(command.c_str(), "Error: timeout must be 1-3600 seconds");
|
||||
assert(fs.files["/display_prefs"] == saved);
|
||||
}
|
||||
run("set display.mode typo", "Error: use off|on|button|pairing|button-pairing|automatic");
|
||||
assert(!handleDisplayPowerCommand("set display.model off", reply, sizeof(reply)));
|
||||
fs.fail_write = true;
|
||||
run("set display.mode off", "Error: display settings save failed");
|
||||
assert(displayPowerPrefs().battery.mode == DisplayMode::Automatic);
|
||||
assert(fs.files["/display_prefs"] == saved);
|
||||
fs.fail_write = false; fs.fail_publish = true;
|
||||
run("set display.mode off", "Error: display settings save failed");
|
||||
assert(loadDisplayPowerSettings(&fs, true));
|
||||
run("get display.mode", "> automatic");
|
||||
fs.fail_publish = false;
|
||||
fs.files["/display_prefs.bak"] = saved;
|
||||
fs.files["/display_prefs"][4] ^= 0x80;
|
||||
assert(loadDisplayPowerSettings(&fs, true));
|
||||
run("get display.timeout", "> 27");
|
||||
assert(loadDisplayPowerSettings(&fs, false));
|
||||
run("set display.mode pairing", "Error: BLE pairing display unsupported");
|
||||
run("set display.usb.mode button-pairing", "Error: BLE pairing display unsupported");
|
||||
FakeFS legacy;
|
||||
assert(!loadDisplayPowerSettings(&legacy, false));
|
||||
migrateLegacyDisplayTimeout(0);
|
||||
assert(loadDisplayPowerSettings(&legacy, false));
|
||||
run("get display.mode", "> on");
|
||||
run("get display.usb.mode", "> on");
|
||||
migrateLegacyDisplayTimeout(42);
|
||||
assert(loadDisplayPowerSettings(&legacy, false));
|
||||
run("get display.mode", "> automatic");
|
||||
run("get display.timeout", "> 42");
|
||||
run("get display.usb.timeout", "> 42");
|
||||
}
|
||||
int main() { policyTests(); commandTests(); }
|
||||
'''
|
||||
|
||||
|
||||
class DisplayPowerTest(unittest.TestCase):
|
||||
def test_policy_and_persistent_commands(self):
|
||||
compiler = shutil.which("g++") or shutil.which("clang++")
|
||||
self.assertIsNotNone(compiler, "host C++ compiler required")
|
||||
with tempfile.TemporaryDirectory(prefix="mesh-display-test-") as name:
|
||||
work = Path(name)
|
||||
(work / "helpers").mkdir()
|
||||
(work / "helpers/IdentityStore.h").write_text(FS_STUB)
|
||||
(work / "test.cpp").write_text(TEST)
|
||||
executable = work / "display-test.exe"
|
||||
result = subprocess.run([
|
||||
compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
||||
"-DDISPLAY_CLASS=SSD1306Display", "-I" + str(work),
|
||||
"-I" + str(ROOT / "src"), str(work / "test.cpp"),
|
||||
str(ROOT / "src/helpers/ui/DisplayPowerSettings.cpp"),
|
||||
"-o", str(executable),
|
||||
], capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
result = subprocess.run([str(executable)], capture_output=True, text=True, timeout=10)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,212 @@
|
||||
"""Run the real R8 board's power detector with simulated USB, ADC and time."""
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
ARDUINO = r'''
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#define PIN_VEXT_EN 40
|
||||
#define PIN_VEXT_EN_ACTIVE 0
|
||||
#define PIN_VBAT_READ 1
|
||||
#define P_LORA_DIO_1 14
|
||||
#define P_LORA_NSS 8
|
||||
#define P_LORA_TX_LED 46
|
||||
#define P_LORA_PA_POWER 7
|
||||
#define HIGH 1
|
||||
#define LOW 0
|
||||
#define BD_STARTUP_RX_PACKET 1
|
||||
inline uint32_t test_now = 0;
|
||||
inline uint32_t millis() { return test_now; }
|
||||
inline void digitalWrite(int, int) {}
|
||||
inline void analogReadResolution(int) {}
|
||||
inline uint32_t analogReadMilliVolts(int) { return 830; }
|
||||
'''
|
||||
|
||||
BASE_BOARD = r'''
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
class ESP32Board {
|
||||
protected:
|
||||
uint8_t startup_reason = 0;
|
||||
public:
|
||||
void begin() {}
|
||||
virtual ~ESP32Board() = default;
|
||||
virtual void onBeforeTransmit() {}
|
||||
virtual void onAfterTransmit() {}
|
||||
virtual void powerOff() {}
|
||||
virtual bool setLoRaFemLnaEnabled(bool) { return false; }
|
||||
virtual bool canControlLoRaFemLna() const { return false; }
|
||||
virtual bool isLoRaFemLnaEnabled() const { return false; }
|
||||
virtual uint16_t getBattMilliVolts() { return 0; }
|
||||
virtual bool isExternalPowered() { return false; }
|
||||
virtual bool isUsbHostConnected() { return false; }
|
||||
virtual bool setAdcMultiplier(float) { return false; }
|
||||
virtual float getAdcMultiplier() const { return 0; }
|
||||
virtual const char* getManufacturerName() const { return "test"; }
|
||||
};
|
||||
'''
|
||||
|
||||
TEST = r'''
|
||||
#include <cassert>
|
||||
#include "HeltecV4R8Board.h"
|
||||
#include <helpers/ui/DisplayPowerPolicy.h>
|
||||
void LoRaFEMControl::init() {}
|
||||
void LoRaFEMControl::setSleepModeEnable() {}
|
||||
void LoRaFEMControl::setTxModeEnable() {}
|
||||
void LoRaFEMControl::setRxModeEnable() {}
|
||||
void LoRaFEMControl::setLNAEnable(bool enabled) { lna_enabled = enabled; }
|
||||
|
||||
class TestBoard : public HeltecV4R8Board {
|
||||
public:
|
||||
bool host = false;
|
||||
uint16_t voltage = 3700;
|
||||
unsigned reads = 0;
|
||||
bool isUsbHostConnected() override { return host; }
|
||||
uint16_t getBattMilliVolts() override { ++reads; return voltage; }
|
||||
};
|
||||
|
||||
int main() {
|
||||
TestBoard board;
|
||||
assert(!board.isExternalPowered());
|
||||
assert(board.reads == 1); // Sample immediately, including at millis() == 0.
|
||||
board.voltage = 4230;
|
||||
test_now = 4999;
|
||||
assert(!board.isExternalPowered());
|
||||
assert(board.reads == 1); // Frequent UI polling must not repeatedly use ADC.
|
||||
test_now = 5000;
|
||||
assert(board.isExternalPowered());
|
||||
assert(board.reads == 2);
|
||||
board.voltage = 4205;
|
||||
test_now = 10000;
|
||||
assert(board.isExternalPowered()); // Noise inside the margin retains state.
|
||||
board.voltage = 4200;
|
||||
test_now = 15000;
|
||||
assert(!board.isExternalPowered());
|
||||
board.voltage = 4210;
|
||||
test_now = 20000;
|
||||
assert(!board.isExternalPowered()); // Exactly 4.21 V is not above threshold.
|
||||
board.voltage = 4211;
|
||||
test_now = 25000;
|
||||
assert(board.isExternalPowered());
|
||||
board.voltage = 0;
|
||||
test_now = 30000;
|
||||
assert(!board.isExternalPowered()); // An absent/invalid low reading clears it.
|
||||
|
||||
// Host detection overrides a cached negative voltage result immediately,
|
||||
// including an empty battery; removing the host must not latch external power.
|
||||
const unsigned reads = board.reads;
|
||||
board.host = true;
|
||||
assert(board.isExternalPowered());
|
||||
assert(board.reads == reads);
|
||||
board.host = false;
|
||||
assert(!board.isExternalPowered());
|
||||
|
||||
// A long host connection must not preserve stale high battery voltage.
|
||||
board.voltage = 4250;
|
||||
test_now = 35000;
|
||||
assert(board.isExternalPowered());
|
||||
board.host = true;
|
||||
board.voltage = 3700;
|
||||
test_now = 50000;
|
||||
assert(board.isExternalPowered());
|
||||
board.host = false;
|
||||
assert(!board.isExternalPowered());
|
||||
|
||||
TestBoard wrapped;
|
||||
test_now = UINT32_MAX - 1000;
|
||||
assert(!wrapped.isExternalPowered());
|
||||
wrapped.voltage = 4230;
|
||||
test_now = 3998;
|
||||
assert(!wrapped.isExternalPowered());
|
||||
test_now = 3999;
|
||||
assert(wrapped.isExternalPowered()); // Five-second sampling survives wrap.
|
||||
|
||||
// Both sources select the actual USB display profile, returning to battery
|
||||
// when neither source is detected. No additional display setting is needed.
|
||||
mesh::ui::DisplayPowerPrefs prefs;
|
||||
prefs.battery.mode = mesh::ui::DisplayMode::Off;
|
||||
prefs.usb.mode = mesh::ui::DisplayMode::On;
|
||||
mesh::ui::DisplayPowerPolicy display;
|
||||
TestBoard source;
|
||||
test_now = 0;
|
||||
const auto update = [&]() {
|
||||
display.update(prefs, source.isExternalPowered() || source.isUsbHostConnected(),
|
||||
false, false, test_now);
|
||||
};
|
||||
update(); assert(!display.on());
|
||||
source.host = true;
|
||||
update(); assert(display.on());
|
||||
source.host = false;
|
||||
update(); assert(!display.on());
|
||||
source.voltage = 4230;
|
||||
test_now = 5000;
|
||||
update(); assert(display.on());
|
||||
source.voltage = 4100;
|
||||
test_now = 10000;
|
||||
update(); assert(!display.on());
|
||||
|
||||
// The estimate uses the real calibrated ADC path, including user calibration.
|
||||
HeltecV4R8Board calibrated;
|
||||
calibrated.setAdcMultiplier(5.1f);
|
||||
assert(calibrated.isExternalPowered()); // 830 mV * 5.1 = 4233 mV.
|
||||
calibrated.setAdcMultiplier(5.0f);
|
||||
test_now += 5000;
|
||||
assert(!calibrated.isExternalPowered()); // Same raw sample becomes 4150 mV.
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class R8ExternalPowerTest(unittest.TestCase):
|
||||
def test_host_voltage_and_display_profile(self):
|
||||
compiler = shutil.which("g++") or shutil.which("clang++")
|
||||
self.assertIsNotNone(compiler, "host C++ compiler required")
|
||||
with tempfile.TemporaryDirectory(prefix="mesh-r8-power-") as directory:
|
||||
work = Path(directory)
|
||||
files = {
|
||||
"Arduino.h": ARDUINO,
|
||||
"helpers/ESP32Board.h": BASE_BOARD,
|
||||
"helpers/RefCountedDigitalPin.h": """#pragma once
|
||||
class RefCountedDigitalPin {
|
||||
public:
|
||||
RefCountedDigitalPin(int, int) {}
|
||||
void begin() {}
|
||||
void claim() {}
|
||||
void release() {}
|
||||
};
|
||||
""",
|
||||
"driver/rtc_io.h": """#pragma once
|
||||
using gpio_num_t = int;
|
||||
using esp_reset_reason_t = int;
|
||||
constexpr int ESP_RST_DEEPSLEEP = 1;
|
||||
inline int esp_reset_reason() { return 0; }
|
||||
inline long esp_sleep_get_ext1_wakeup_status() { return 0; }
|
||||
inline void rtc_gpio_hold_dis(int) {}
|
||||
inline void rtc_gpio_hold_en(int) {}
|
||||
inline void rtc_gpio_deinit(int) {}
|
||||
""",
|
||||
"test.cpp": TEST,
|
||||
}
|
||||
for path, content in files.items():
|
||||
target = work / path
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_text(content)
|
||||
executable = work / "r8-power.exe"
|
||||
variant = ROOT / "variants/heltec_v4_r8"
|
||||
result = subprocess.run([
|
||||
compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
||||
"-I" + str(work), "-I" + str(variant), "-I" + str(ROOT / "src"),
|
||||
str(work / "test.cpp"), str(variant / "HeltecV4R8Board.cpp"),
|
||||
"-o", str(executable),
|
||||
], capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
result = subprocess.run([str(executable)], capture_output=True, text=True, timeout=10)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -152,6 +152,8 @@ int main() {
|
||||
g_mock_pin_levels[7]=HIGH;
|
||||
user_btn.begin(); user_btn.enableQuadrupleClick();
|
||||
Display display;
|
||||
display.servicePower(false);
|
||||
display.wake(mesh::ui::DisplayWake::Button);
|
||||
UITask task(display);
|
||||
HomeScreen home(&task);
|
||||
Screen group;
|
||||
@@ -393,6 +395,7 @@ class MessageNavigationTest(unittest.TestCase):
|
||||
"-I" + str(ROOT / "src"), "-I" + str(ROOT / "test/mocks"),
|
||||
"-fsanitize=address,undefined", "-fno-pie", "-no-pie",
|
||||
"-x", "c++", "-", str(ROOT / "src/helpers/ui/MomentaryButton.cpp"),
|
||||
str(ROOT / "src/helpers/ui/DisplayDriver.cpp"),
|
||||
"-o", str(binary),
|
||||
], input=preamble + implementation + SCENARIOS, text=True, capture_output=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
@@ -47,6 +47,43 @@ def embedded_page():
|
||||
|
||||
|
||||
class WebConfigUiRuntimeTest(unittest.TestCase):
|
||||
def test_four_display_controls_and_pairing_capability(self):
|
||||
status, config = self.setup_values()
|
||||
config["display"] = {
|
||||
"mode": "button", "timeout": 23,
|
||||
"usb_mode": "on", "usb_timeout": 55, "pairing": False,
|
||||
}
|
||||
prelude = r'''<script>
|
||||
window.fetch=function(path){
|
||||
var value=path==="/api/status"?%s:%s;
|
||||
return Promise.resolve({ok:true,status:200,json:function(){return Promise.resolve(value)}});
|
||||
};
|
||||
window.addEventListener("load",function(){
|
||||
setTimeout(function(){enterApp()},50);
|
||||
setTimeout(function(){
|
||||
var card=document.getElementById("display-settings"), body=document.body;
|
||||
body.setAttribute("data-test-display-visible",!card.classList.contains("hide"));
|
||||
body.setAttribute("data-test-display-count",card.querySelectorAll("[data-k]").length);
|
||||
body.setAttribute("data-test-battery-timeout",card.querySelector('[data-k="display.timeout"]').value);
|
||||
body.setAttribute("data-test-usb-timeout",card.querySelector('[data-k="display.usb.timeout"]').value);
|
||||
body.setAttribute("data-test-usb-disabled",card.querySelector('[data-k="display.usb.timeout"]').disabled);
|
||||
body.setAttribute("data-test-pairing-disabled",card.querySelector('option[value="pairing"]').disabled);
|
||||
var mode=card.querySelector('[data-k="display.mode"]');
|
||||
mode.value="off";mode.dispatchEvent(new Event("input",{bubbles:true}));
|
||||
body.setAttribute("data-test-battery-disabled",card.querySelector('[data-k="display.timeout"]').disabled);
|
||||
body.setAttribute("data-test-dirty-display",st.dirty["display.mode"]);
|
||||
},650);
|
||||
});
|
||||
</script>''' % (json.dumps(status), json.dumps(config))
|
||||
dom = self.run_page(prelude)
|
||||
for attribute, value in (
|
||||
("display-visible", "true"), ("display-count", "4"),
|
||||
("battery-timeout", "23"), ("usb-timeout", "55"),
|
||||
("usb-disabled", "true"), ("pairing-disabled", "true"),
|
||||
("battery-disabled", "true"), ("dirty-display", "off"),
|
||||
):
|
||||
self.assertIn('data-test-%s="%s"' % (attribute, value), dom)
|
||||
|
||||
def setup_values(self):
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
try:
|
||||
|
||||
@@ -65,6 +65,24 @@ uint16_t HeltecV4R8Board::getBattMilliVolts() {
|
||||
return (adc_mult * raw);
|
||||
}
|
||||
|
||||
bool HeltecV4R8Board::isExternalPowered() {
|
||||
// R8 has no MCU-connected VBUS/charger-status pin. A USB host is definitive;
|
||||
// otherwise use the calibrated battery voltage as a best-effort estimate.
|
||||
if (isUsbHostConnected()) return true;
|
||||
|
||||
const uint32_t now = millis();
|
||||
if (!power_voltage_sampled || uint32_t(now - last_power_check_ms) >= 5000) {
|
||||
last_power_check_ms = now;
|
||||
power_voltage_sampled = true;
|
||||
// Like Meshtastic's analog power detection, enter above a full Li-ion cell
|
||||
// plus 10 mV. Release at 4.20 V to reduce display-profile chatter. This can
|
||||
// miss a charger while the battery is low; it is not a charging-status flag.
|
||||
const uint16_t millivolts = getBattMilliVolts();
|
||||
voltage_external_power = millivolts > (voltage_external_power ? 4200 : 4210);
|
||||
}
|
||||
return voltage_external_power;
|
||||
}
|
||||
|
||||
const char* HeltecV4R8Board::getManufacturerName() const {
|
||||
#ifdef HELTEC_V4_R8_TFT
|
||||
return "Heltec V4 R8 TFT";
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
class HeltecV4R8Board : public ESP32Board {
|
||||
protected:
|
||||
float adc_mult = ADC_MULTIPLIER;
|
||||
uint32_t last_power_check_ms = 0;
|
||||
bool power_voltage_sampled = false;
|
||||
bool voltage_external_power = false;
|
||||
|
||||
public:
|
||||
RefCountedDigitalPin periph_power;
|
||||
@@ -28,6 +31,7 @@ public:
|
||||
bool canControlLoRaFemLna() const override;
|
||||
bool isLoRaFemLnaEnabled() const override;
|
||||
uint16_t getBattMilliVolts() override;
|
||||
bool isExternalPowered() override;
|
||||
bool setAdcMultiplier(float multiplier) override {
|
||||
if (multiplier == 0.0f) {
|
||||
adc_mult = ADC_MULTIPLIER;
|
||||
|
||||
@@ -347,8 +347,8 @@ build_flags =
|
||||
-D DISPLAY_ACTIVITY_DASHBOARD=1
|
||||
-D USER_BTN_LONG_PRESS_MILLIS=3000
|
||||
-D USER_BTN_MULTICLICK=1
|
||||
; display.timeout controls runtime blanking; touch and the existing button
|
||||
; handling can toggle the panel manually. Multiclick remains enabled.
|
||||
; Battery/USB display modes and timeouts control runtime blanking.
|
||||
; Touch and button presses request a wake when allowed. Multiclick stays enabled.
|
||||
-D DISPLAY_TOUCH_TOGGLE=1
|
||||
build_src_filter = ${heltec_v4_r8_tft.build_src_filter}
|
||||
+<helpers/bridges/MQTTBridge.cpp>
|
||||
@@ -422,8 +422,8 @@ build_flags =
|
||||
-D DISPLAY_ACTIVITY_DASHBOARD=1
|
||||
-D USER_BTN_LONG_PRESS_MILLIS=3000
|
||||
-D USER_BTN_MULTICLICK=0
|
||||
; display.timeout controls runtime blanking; touch and the existing button
|
||||
; handling can toggle the panel manually. Room has no multiclick action, so a
|
||||
; Battery/USB display modes and timeouts control runtime blanking. Touch and
|
||||
; button presses request a wake when allowed. Room has no multiclick action, so a
|
||||
; click is delivered immediately instead of waiting for a second click.
|
||||
-D DISPLAY_TOUCH_TOGGLE=1
|
||||
build_src_filter = ${heltec_v4_r8_tft.build_src_filter}
|
||||
|
||||
@@ -393,6 +393,18 @@ body.tab-cli{padding-bottom:0}
|
||||
</div>
|
||||
<div class="f"><label>TX power (dBm)</label><input type="number" data-k="tx" min="-9" max="30" style="max-width:120px"></div>
|
||||
</div>
|
||||
<div class="card hide" id="display-settings">
|
||||
<h2>Display</h2>
|
||||
<div class="row">
|
||||
<div class="f"><label>On battery</label><select data-k="display.mode"><option value="off">Off</option><option value="on">On</option><option value="button">Button</option><option value="pairing">Pairing only</option><option value="button-pairing">Button + pairing</option><option value="automatic">Automatic</option></select></div>
|
||||
<div class="f"><label>Timeout (seconds)</label><input type="number" data-k="display.timeout" min="1" max="3600" step="1"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="f"><label>On USB power</label><select data-k="display.usb.mode"><option value="off">Off</option><option value="on">On</option><option value="button">Button</option><option value="pairing">Pairing only</option><option value="button-pairing">Button + pairing</option><option value="automatic">Automatic</option></select></div>
|
||||
<div class="f"><label>Timeout (seconds)</label><input type="number" data-k="display.usb.timeout" min="1" max="3600" step="1"></div>
|
||||
</div>
|
||||
<div class="hint">Automatic shows startup and messages while the app is disconnected. Connecting blanks the display; a button or pairing request can wake it. USB power detection depends on the board.</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Advanced</h2>
|
||||
<div class="row">
|
||||
@@ -800,6 +812,10 @@ function cfgVal(k){ // map a `set` key to its current value string, from st.cfg
|
||||
case"radio.fem.rxgain":return r.fem_rxgain?"on":"off";
|
||||
case"radio.rxps":return rxpsCfgValue(r);
|
||||
case"powersaving":return r.powersaving?"on":"off";
|
||||
case"display.mode":return (st.cfg.display||{}).mode||"button";
|
||||
case"display.timeout":return String((st.cfg.display||{}).timeout||15);
|
||||
case"display.usb.mode":return (st.cfg.display||{}).usb_mode||"button";
|
||||
case"display.usb.timeout":return String((st.cfg.display||{}).usb_timeout||15);
|
||||
case"repeat":return r.repeat?"on":"off";
|
||||
case"flood.max":return String(r.flood_max);
|
||||
case"flood.max.advert":return String(r.flood_max_advert);
|
||||
@@ -945,6 +961,9 @@ function loadConfig(loadCapture){
|
||||
});
|
||||
$$("[data-cfm]").forEach(function(el){el.value=""});
|
||||
$$("[data-rg]").forEach(function(el){el.value=String(c.radio[el.dataset.rg]);el.classList.remove("dirty")});
|
||||
$("#display-settings").classList.toggle("hide",!c.display);
|
||||
$$('#display-settings option[value="pairing"],#display-settings option[value="button-pairing"]').forEach(function(el){el.hidden=!(c.display&&c.display.pairing);el.disabled=el.hidden});
|
||||
syncDisplayTimeouts();
|
||||
loadRxps(c.radio);
|
||||
st.orig.radio=radioCombo();afHint();
|
||||
for(var i=1;i<=st.nslots;i++){refreshSlotFields($("#app-slots"),i);refreshSlotFields($("#wz-slots"),i)}
|
||||
@@ -957,10 +976,17 @@ function loadConfig(loadCapture){
|
||||
return true;
|
||||
});
|
||||
}
|
||||
function syncDisplayTimeouts(){
|
||||
["display","display.usb"].forEach(function(prefix){
|
||||
var mode=$('[data-k="'+prefix+'.mode"]').value;
|
||||
$('[data-k="'+prefix+'.timeout"]').disabled=(mode==="off"||mode==="on"||mode==="pairing");
|
||||
});
|
||||
}
|
||||
function markDirty(k,v,el){
|
||||
if(v===st.orig[k]){delete st.dirty[k]}else{st.dirty[k]=v}
|
||||
if(el)el.classList.toggle("dirty",k in st.dirty);
|
||||
if(k==="af")afHint();
|
||||
if(k==="display.mode"||k==="display.usb.mode")syncDisplayTimeouts();
|
||||
updateSaveBar();
|
||||
}
|
||||
// The CLI's `dutycycle` is not its own pref - it just writes airtime_factor as
|
||||
@@ -1860,6 +1886,10 @@ var CLI_VERBS=[
|
||||
mode: 0 = get and set, 1 = get only, 2 = set only
|
||||
values: enum offered as value completions after `set <key> ` */
|
||||
var CLI_KEYS=[
|
||||
["display.mode","Display on battery",0,"off|on|button|pairing|button-pairing|automatic"],
|
||||
["display.timeout","Battery display timeout in seconds (1-3600)",0],
|
||||
["display.usb.mode","Display on USB power",0,"off|on|button|pairing|button-pairing|automatic"],
|
||||
["display.usb.timeout","USB display timeout in seconds (1-3600)",0],
|
||||
["name","Node name",0],
|
||||
["lat","Advert latitude",0],
|
||||
["lon","Advert longitude",0],
|
||||
@@ -2000,6 +2030,10 @@ var CLI_COMPANION_VERBS=[
|
||||
["reboot","Restart the node"]
|
||||
];
|
||||
var CLI_COMPANION_KEYS=[
|
||||
["display.mode","Display on battery",0,"off|on|button|pairing|button-pairing|automatic"],
|
||||
["display.timeout","Battery display timeout in seconds (1-3600)",0],
|
||||
["display.usb.mode","Display on USB power",0,"off|on|button|pairing|button-pairing|automatic"],
|
||||
["display.usb.timeout","USB display timeout in seconds (1-3600)",0],
|
||||
["prv.key","Read this node's private identity key",1],
|
||||
["name","Node name",0], ["role","Node role",1],
|
||||
["lat","Advert latitude",0], ["lon","Advert longitude",0],
|
||||
|
||||
Reference in new issue
Block a user