From d4a6e8a335c11de304d271c0c5e761c3318f7f1c Mon Sep 17 00:00:00 2001 From: Florent Date: Mon, 26 Oct 2026 15:04:08 -0400 Subject: [PATCH 1/3] ui: add some parameters to the cli (mostly regarding eink behaviour) --- docs/companion_cli.md | 456 +++++++++++++++++++++ examples/companion_radio/MyMesh.cpp | 92 +++++ examples/companion_radio/NodePrefs.h | 55 +++ examples/companion_radio/ui-new/UITask.cpp | 160 ++++---- examples/companion_radio/ui-new/UITask.h | 2 +- src/helpers/ui/DisplayDriver.h | 2 +- src/helpers/ui/GxEPDDisplay.cpp | 39 +- src/helpers/ui/GxEPDDisplay.h | 3 +- 8 files changed, 717 insertions(+), 92 deletions(-) create mode 100644 docs/companion_cli.md diff --git a/docs/companion_cli.md b/docs/companion_cli.md new file mode 100644 index 00000000000..8eec6445940 --- /dev/null +++ b/docs/companion_cli.md @@ -0,0 +1,456 @@ +# Companion CLI + +Since version 1.18 meshcore companion radio firmware has a CLI interface, which can be accessed locally (through companion radio protocol) or remotely via the network. + +## Navigation +- [Operational](#operational) +- [Configuration](#configuration) + - [System](#system) + - [Radio](#radio) + - [BLE](#ble) + - [Wifi](#wifi) + - [UI](#ui) + +## Operational + +### Reboot the node +**Usage:** +- `reboot` + +**Note:** No reply is sent. + +--- + +### Power-off the node +**Usage:** +- `poweroff`, or +- `shutdown` + +**Note:** No reply is sent. + +--- + +## Configuration + +### System + +#### View or change this node's name +**Usage:** +- `get name` +- `set name ` + +**Parameters:** +- `name`: Node name + +**Set by build flag:** `ADVERT_NAME` + +**Default:** Varies by board + +**Note:** Advertised names can use up to 23 bytes when location is included and 31 bytes otherwise. Emoji and Unicode characters may take more than one byte. Names that exceed the available advert space are truncated at a valid UTF-8 code point boundary. + +--- + +#### View this node's firmware version +**Usage:** `ver` + +--- + +#### Configure this node's timezone +**Usage:** +- `set tz.offset ` +- `get tz.offset` + +**Parameters:** +- `offset`: float value of the offset in hours + +--- + +### Radio + +Radio parameters are common to all roles and handled by `CommonRadioPrefs`. + +#### View or change this node's radio parameters +**Usage:** +- `get radio` +- `set radio ,,,` + +**Parameters:** +- `freq`: Frequency in MHz +- `bw`: Bandwidth in kHz +- `sf`: Spreading factor (5-12) +- `cr`: Coding rate (5-8) + +**Set by build flag:** `LORA_FREQ`, `LORA_BW`, `LORA_SF`, `LORA_CR` + +**Default:** `869.618,62.5,8,5` + +**Note:** Requires reboot to apply + +--- + +#### View or change this node's transmit power +**Usage:** +- `get tx` +- `set tx ` + +**Parameters:** +- `dbm`: Power level in dBm (1-22) + +**Set by build flag:** `LORA_TX_POWER` + +**Default:** Varies by board + +**Notes:** This setting only controls the power level of the LoRa chip. Some nodes have an additional power amplifier stage which increases the total output. Refer to the node's manual for the correct setting to use. **Setting a value too high may violate the laws in your country.** + +--- + +#### View or change this node's frequency +**Usage:** +- `get freq` +- `set freq ` + +**Parameters:** +- `frequency`: Frequency in MHz + +**Default:** `869.618` + +**Note:** Requires reboot to apply +**Serial Only:** `set freq ` + +--- + +#### View or change this node's rx boosted gain mode (SX12xx and LR1110) +**Usage:** +- `get radio.rxgain` +- `set radio.rxgain ` + +**Parameters:** + - `state`: `on`|`off` + +**Default:** `on` + +--- + +#### View or change the LoRa FEM receive-path gain state on supported boards +**Usage:** +- `get radio.fem.rxgain` +- `set radio.fem.rxgain ` + +**Parameters:** +- `state`: `on`|`off` + +**Notes:** +- This controls the external LoRa FEM receive-path LNA where the board supports it. +- This is separate from `radio.rxgain`, which controls the radio chip receive gain mode. + +--- + +#### View or change the LoRa FEM transmit-path gain state on supported boards +**Usage:** +- `get radio.fem.txgain` +- `set radio.fem.txgain ` + +**Parameters:** +- `state`: `on`|`off` + +**Notes:** +- This controls a software-selectable external LoRa FEM transmit gain where the board supports it. +- On Station G3, remove the PA PL1 jumper to allow software control. `on` selects PA PL1 high/short and `off` selects PA PL1 low/open. The PA PL2 hardware jumper determines whether this switches between power levels 1/3 or 2/4. +- Select an operating level and SX1262 transmit power that comply with local RF limits and the Station G3 power-supply requirements. +- The setting is saved immediately, but on Station G3 the level is applied to the hardware at the start of the next transmit, so that the PA supply rail is never re-targeted while the PA is being driven. `get` reports the configured state, which may lead the hardware until the node next transmits. + +--- + +#### View or change the duty cycle limit +**Usage:** +- `get dutycycle` +- `set dutycycle ` + +**Parameters:** +- `value`: Duty cycle percentage (1-100) + +**Default:** `50%` (equivalent to airtime factor 1.0) + +**Examples:** +- `set dutycycle 100` — no duty cycle limit +- `set dutycycle 50` — 50% duty cycle (default) +- `set dutycycle 10` — 10% duty cycle +- `set dutycycle 1` — 1% duty cycle (strictest EU requirement) + +> **Note:** Added in firmware v1.15.0 + +--- + +#### View or change the airtime factor (duty cycle limit) +> **Deprecated** as of firmware v1.15.0. Use [`get/set dutycycle`](#view-or-change-the-duty-cycle-limit) instead. + +**Usage:** +- `get af` +- `set af ` + +**Parameters:** +- `value`: Airtime factor (0-9). After each transmission, the repeater enforces a silent period of approximately the on-air transmission time multiplied by the value. This results in a long-term duty cycle of roughly 1 divided by (1 plus the value). For example: + - `af = 1` → ~50% duty + - `af = 2` → ~33% duty + - `af = 3` → ~25% duty + - `af = 9` → ~10% duty + You are responsible for choosing a value that is appropriate for your jurisdiction and channel plan (for example EU 868 Mhz 10% duty cycle regulation). + +**Default:** `1.0` + +--- + +#### View or change the local interference threshold +**Usage:** +- `get int.thresh` +- `set int.thresh ` + +**Parameters:** +- `value`: Interference threshold value + +**Default:** `0.0` + +--- + +#### Enable or disable hardware Channel Activity Detection (CAD) +**Usage:** +- `get cad` +- `set cad ` + +**Description:** When enabled, the radio performs a hardware Channel Activity Detection scan before transmitting and defers if the channel is busy. Runs independently of `int.thresh` — either, both, or none may be active. + +**Parameters:** +- `on|off`: Enable or disable hardware CAD + +**Default:** `off` + +--- + +#### View or change the AGC Reset Interval +**Usage:** +- `get agc.reset.interval` +- `set agc.reset.interval ` + +**Parameters:** +- `value`: Interval in seconds, 0-1020, rounded down to a multiple of 4 (17 becomes 16). 0 to disable. + +**Default:** `0.0` + +--- + +#### Enable or disable Multi-Acks support +**Usage:** +- `get multi.acks` +- `set multi.acks ` + +**Parameters:** +- `state`: `0` (disable) or `1` (enable) + +**Default:** `0` + +--- + +#### View or change this node's advert path hash size +**Usage:** +- `get path.hash.mode` +- `set path.hash.mode ` + +**Parameters:** +- `value`: Path hash size (0-2) + - `0`: 1 Byte hash size (256 unique ids)[64 max flood] + - `1`: 2 Byte hash size (65,536 unique ids)[32 max flood] + - `2`: 3 Byte hash size (16,777,216 unique ids)[21 max flood] + - `3`: DO NOT USE (Reserved) + +**Default:** `0` + +**Note:** the 'path.hash.mode' sets the low-level ID/hash encoding size used when the repeater adverts. This setting has no impact on what packet ID/hash size this repeater forwards, all sizes should be forwarded on firmware >= 1.14. This feature was added in firmware 1.14 +#### View or change the retransmit delay factor for flood traffic +**Usage:** +- `get txdelay` +- `set txdelay ` + +**Parameters:** +- `value`: Transmit delay factor (0-2) + +**Default:** `0.5` + +**Note:** When multiple nearby repeaters all hear the same flood packet, each waits a random amount of time before retransmitting to avoid simultaneous collisions. This factor scales the size of that random window. Higher values reduce collision risk at the cost of added latency. `0` disables the window entirely. + +--- + +#### View or change the retransmit delay factor for direct traffic +**Usage:** +- `get direct.txdelay` +- `set direct.txdelay ` + +**Parameters:** +- `value`: Direct transmit delay factor (0-2) + +**Default:** `0.3` (Repeater) - `0.2` (Room Server, Sensor) + +**Note:** Same collision-avoidance random window as `txdelay`, but applied to direct (non-flood, routed) traffic. The default is lower because direct packets are addressed to a specific next hop, so far fewer nodes compete to retransmit them. + +--- + +#### [Experimental] View or change the processing delay for received traffic +**Usage:** +- `get rxdelay` +- `set rxdelay ` + +**Parameters:** +- `value`: Receive delay base (0-20) + +**Default:** `0.0` + +**Note:** When enabled, repeaters that received a flood packet with a weak signal are held in a delay queue before processing, while those that received it with a strong signal process it immediately. This gives strong-signal paths forwarding priority. By the time weak-signal nodes process their copy, the packet may have already propagated and will be suppressed as a duplicate, reducing redundant retransmissions. + +--- + +### BLE + +#### Change BLE PIN +**Usage:** +- `set pin ` + +**Parameters:** +- `pin`: New BLE PIN + +**Set by build flag:** `BLE_PIN_CODE` + +--- + +### Wifi + +#### Get status +**Usage:** +- `get wifi.status` +- `get wifi.ip` + +--- + +#### Toggle +**Usage:** +- `set wifi.enabled ` +- `get wifi.enabled` + +**Parameters:** +- `value`: `0` (disable) or `1` (enable) + +--- + +#### Set SSID +**Usage:** +- `set wifi.ssid ` +- `get wifi.ssid` + +**Parameters:** +- `value`: ssid as a string + +--- + +#### Set password +**Usage:** +- `set wifi.pwd ` +- `get wifi.pwd` + +**Parameters:** +- `value`: password string + +--- + +### UI + +#### Show clock +**Usage:** +- `set ui.show_clock ` +- `get ui.show_clock` + +**Parameters:** +- `state`: `0` (disable) or `1` (enable) + +**Default:** `0` (or `UI_SHOW_CLOCK` build flag) + +--- + +#### Node discovery screen shows nodes names +**Usage:** +- `set ui.nd_name ` +- `get ui.nd_name` + +**Parameters:** +- `state`: `1` (show name) or `0` (show pubkey_prefix) + +**Default:** `1` + +**Note:** When showing name, there is not always enough space to draw both incoming and outgoing snr values. + +--- + +#### Auto-off screen +**Usage:** +- `set ui.auto_off_delay ` +- `get ui.auto_off_delay` + +**Parameters:** +- `delay`: in seconds + +**Default:** `15` (or set by `AUTO_OFF_MILLIS` build flag (in ms)) + +**Note:** +On e-ink displays, when turned off, the screen is not necessarily blanked, see `ui.auto_off_blank` and `ui.backlight_delay`. + +--- + +#### Display backlight timeout +**Usage:** +- `set ui.backlight_delay ` +- `get ui.backlight_delay` + +**Parameters:** +- `delay`: in seconds (`-1` means always on, `0` means always off) + +--- + +#### When to turn backlight on +**Usage:** +- `set ui.backlight_on_keys ` +- `get ui.backlight_on_keys` +- `set ui.backlight_on_msgs ` +- `get ui.backlight_on_msgs` + +**Parameters:** +- `state`: `0` (disable) or `1` (enable) + +**Default:** `0` + +**Note:** +By default backlight is triggered by backlight button (as long as the button is pressed backlight is on, and is turned off after a delay). +These toggles permits to toggle backlight when keys are pressed or when a message arrives (it acts as a notification signal). + +--- + +#### Control if eink screen should be blanked +**Usage:** +- `set ui.auto_off_blank ` +- `get ui.auto_off_blank` + +**Parameters:** +- `state`: `0` (disable) or `1` (enable) + +**Default:** `0` (or set by `UI_SCREENOFF_BLANK` build flag) + +**Note:** +If disabled, when screen is shut the displays stays on screen and is not updated. + +--- + +#### Maximum number of partial refreshes +**Usage:** +- `set ui.max_partial ` +- `get ui.max_partial` + +**Parmeters:** +- `value`: max partial refreshes before doing a full refresh + +**Default:** 60 (or set by `EINK_MAX_PARTIAL_REFRESH` build flag) \ No newline at end of file diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 6112b2dbb17..9c15ae72cf5 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2209,6 +2209,98 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* } return true; } +#ifdef DISPLAY_CLASS + if (memcmp(command, "set ui.show_clock", 17) == 0) { + _prefs.ui_show_clock = atoi(&command[17]) ? 1 : 0; + savePrefs(); + sprintf(reply, "> ui.show_clock is now %d", _prefs.ui_show_clock); + return true; + } + if (memcmp(command, "set ui.auto_off_delay", 21) == 0) { + _prefs.ui_auto_off_delay = atoi(&command[21]); + savePrefs(); + sprintf(reply, "> ui.auto_off_delay is now %d seconds", _prefs.ui_auto_off_delay); + return true; + } + if (memcmp(command, "set ui.backlight_delay", 22) == 0) { + _prefs.ui_backlight_delay = atoi(&command[22]); + savePrefs(); + sprintf(reply, "> ui.backlight_delay is now %d seconds", _prefs.ui_backlight_delay); + return true; + } + if (memcmp(command, "set ui.backlight_on_keys", 24) == 0) { + _prefs.ui_backlight_on_keys = atoi(&command[24]); + savePrefs(); + sprintf(reply, "> ui.backlight_on_keys is now %d", _prefs.ui_backlight_on_keys); + return true; + } + if (memcmp(command, "set ui.backlight_on_msgs", 24) == 0) { + _prefs.ui_backlight_on_msgs = atoi(&command[24]); + savePrefs(); + sprintf(reply, "> ui.backlight_on_msgs is now %d", _prefs.ui_backlight_on_msgs); + return true; + } + if (memcmp(command, "set ui.auto_off_blank", 21) == 0) { + _prefs.ui_auto_off_blank = atoi(&command[21]); + savePrefs(); + sprintf(reply, "> ui.auto_off_blank is now %d", _prefs.ui_auto_off_blank); + return true; + } + if (memcmp(command, "set ui.auto_off_keep_msgs", 25) == 0) { + _prefs.ui_auto_off_keep_msgs = atoi(&command[25]); + savePrefs(); + sprintf(reply, "> ui.auto_off_keep_msgs is now %d", _prefs.ui_auto_off_keep_msgs); + return true; + } + if (memcmp(command, "set ui.max_partial", 18) == 0) { + _prefs.ui_max_partial = atoi(&command[18]); + savePrefs(); + sprintf(reply, "> ui.max_partial is now %d", _prefs.ui_max_partial); + return true; + } + if (memcmp(command, "set ui.nd_name", 14) == 0) { + _prefs.ui_nd_name = atoi(&command[14]); + savePrefs(); + sprintf(reply, "> ui.nd_name is now %d", _prefs.ui_nd_name); + return true; + } + if (memcmp(command, "get ui.show_clock", 17) == 0) { + sprintf(reply, "> %d", _prefs.ui_show_clock); + return true; + } + if (memcmp(command, "get ui.auto_off_delay", 21) == 0) { + sprintf(reply, "> %d seconds", _prefs.ui_auto_off_delay); + return true; + } + if (memcmp(command, "get ui.backlight_delay", 22) == 0) { + sprintf(reply, "> %d seconds", _prefs.ui_backlight_delay); + return true; + } + if (memcmp(command, "get ui.backlight_on_keys", 24) == 0) { + sprintf(reply, "> %d", _prefs.ui_backlight_on_keys); + return true; + } + if (memcmp(command, "get ui.backlight_on_msgs", 24) == 0) { + sprintf(reply, "> %d", _prefs.ui_backlight_on_msgs); + return true; + } + if (memcmp(command, "get ui.auto_off_blank", 21) == 0) { + sprintf(reply, "> %d", _prefs.ui_auto_off_blank); + return true; + } + if (memcmp(command, "get ui.auto_off_keep_msgs", 25) == 0) { + sprintf(reply, "> %d", _prefs.ui_auto_off_keep_msgs); + return true; + } + if (memcmp(command, "get ui.max_partial", 18) == 0) { + sprintf(reply, "> %d", _prefs.ui_max_partial); + return true; + } + if (memcmp(command, "get ui.nd_name", 14) == 0) { + sprintf(reply, "> %d", _prefs.ui_nd_name); + return true; + } +#endif return false; // not handled } diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 1cc5b501826..9ceb40a3947 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -58,6 +58,34 @@ class NodePrefs : public ConfigSerializer { // persisted to file // use ssid from prefs, or fallback to ssid from build flags const char* getWifiSSID() const { return wifi_ssid[0] ? wifi_ssid : WIFI_SSID; } #endif +#ifdef DISPLAY_CLASS + #ifndef UI_SHOW_CLOCK + #define UI_SHOW_CLOCK 0 + #endif + #ifndef AUTO_OFF_MILLIS + #define AUTO_OFF_MILLIS 15000 + #endif + #ifndef EINK_MAX_PARTIAL_REFRESH + #define EINK_MAX_PARTIAL_REFRESH 60 + #endif + uint8_t ui_show_clock = UI_SHOW_CLOCK; + uint32_t ui_auto_off_delay = AUTO_OFF_MILLIS / 1000; + int32_t ui_backlight_delay = 5; + #if defined(DISP_BACKLIGHT) || defined(EXP_PIN_BACKLIGHT) + uint8_t ui_backlight_on_keys = 0; + #else + uint8_t ui_backlight_on_keys = 1; + #endif + uint8_t ui_backlight_on_msgs = 0; +#ifdef UI_SCREENOFF_BLANK + uint8_t ui_auto_off_blank = 1; +#else + uint8_t ui_auto_off_blank = 0; +#endif + uint8_t ui_auto_off_keep_msgs = 1; + uint8_t ui_max_partial = EINK_MAX_PARTIAL_REFRESH; + uint8_t ui_nd_name = 1; +#endif private: class RadioPrefs : public CommonRadioPrefs { @@ -189,6 +217,27 @@ class NodePrefs : public ConfigSerializer { // persisted to file WiFiPrefs wifi; #endif +#ifdef DISPLAY_CLASS + class UI_Prefs : public ConfigSerializer { + NodePrefs* _parent; + protected: + void structure() override { + def("show_clock", _parent->ui_show_clock); + def("auto_off", _parent->ui_auto_off_delay); + def("bl_dly", _parent->ui_backlight_delay); + def("bl_keys", _parent->ui_backlight_on_keys); + def("bl_msgs", _parent->ui_backlight_on_msgs); + def("max_partial", _parent->ui_max_partial); + def("off_blank", _parent->ui_auto_off_blank); + def("off_keep_msgs", _parent->ui_auto_off_keep_msgs); + def("nd_name", _parent->ui_nd_name); + } + public: + UI_Prefs(NodePrefs* parent) : _parent(parent) {} + }; + UI_Prefs ui; +#endif + protected: void structure() override { def("name", node_name, sizeof(node_name)); @@ -203,12 +252,18 @@ class NodePrefs : public ConfigSerializer { // persisted to file def("custom", custom); #ifdef ENABLE_WIFI_INTERFACE def("wifi", wifi); +#endif +#ifdef DISPLAY_CLASS + def("ui", ui); #endif } public: NodePrefs() : radio(this), gps(this), companion(this), custom(&radio) #ifdef ENABLE_WIFI_INTERFACE , wifi(this) +#endif +#ifdef DISPLAY_CLASS + , ui(this) #endif { node_name[0] = 0; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index aced739dc45..4d467027c9e 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -122,7 +122,6 @@ class HomeScreen : public UIScreen { uint32_t disc_node_req_tag = 0; uint32_t disc_nodes_count = 0; uint32_t discovery_req_time = 0; - bool discovery_disp_names = true; // by default desplay names if available (removes SNR_O) #endif void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) { @@ -293,17 +292,17 @@ class HomeScreen : public UIScreen { sprintf(tmp, "MSG: %d", _task->getMsgCount()); display.drawTextCentered(display.width() / 2, 22, tmp); #endif - #ifdef UI_SHOW_CLOCK - display.setTextSize(3); - uint32_t now = _rtc->getCurrentTime(); - now += (int32_t)(_node_prefs->tz_offset * 3600); - DateTime dt (now); - sprintf(tmp, "%02d:%02d", dt.hour(), dt.minute()); - display.drawTextCentered(display.width() / 2, 60, tmp); - display.setTextSize(1); - sprintf(tmp, "%02d/%02d/%d", dt.day(), dt.month(), dt.year()); - display.drawTextCentered(display.width() / 2, 80, tmp); - #endif + if (_node_prefs->ui_show_clock) { + display.setTextSize(3); + uint32_t now = _rtc->getCurrentTime(); + now += (int32_t)(_node_prefs->tz_offset * 3600); + DateTime dt (now); + sprintf(tmp, "%02d:%02d", dt.hour(), dt.minute()); + display.drawTextCentered(display.width() / 2, 60, tmp); + display.setTextSize(1); + sprintf(tmp, "%02d/%02d/%d", dt.day(), dt.month(), dt.year()); + display.drawTextCentered(display.width() / 2, 80, tmp); + } #ifdef ENABLE_WIFI_INTERFACE IPAddress ip = WiFi.localIP(); snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); @@ -313,21 +312,20 @@ class HomeScreen : public UIScreen { if (_task->hasConnection()) { display.setColor(UIColor::warning_txt); display.setTextSize(1); - #ifdef UI_SHOW_CLOCK - display.drawTextCentered(display.width() / 2, 110, "< Connected >"); - #else - display.drawTextCentered(display.width() / 2, 43, "< Connected >"); - #endif + if (_node_prefs->ui_show_clock) + display.drawTextCentered(display.width() / 2, 110, "< Connected >"); + else + display.drawTextCentered(display.width() / 2, 43, "< Connected >"); } else if (the_mesh.getBLEPin() != 0) { // BT pin display.setColor(UIColor::warning_txt); sprintf(tmp, "Pin:%d", the_mesh.getBLEPin()); - #ifdef UI_SHOW_CLOCK - display.setTextSize(1); - display.drawTextCentered(display.width() / 2, 110, tmp); - #else - display.setTextSize(2); - display.drawTextCentered(display.width() / 2, 43, tmp); - #endif + if (_node_prefs->ui_show_clock) { + display.setTextSize(1); + display.drawTextCentered(display.width() / 2, 110, tmp); + } else { + display.setTextSize(2); + display.drawTextCentered(display.width() / 2, 43, tmp); + } } } else if (_page == HomePage::RECENT) { the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE); @@ -518,7 +516,7 @@ class HomeScreen : public UIScreen { for (int i = 0; i < count; i++, y += 11) { char name[32]; auto a = &discovered[i]; - if ((a->name[0] == 0) || !discovery_disp_names) { + if ((a->name[0] == 0) || !_node_prefs->ui_nd_name) { mesh::Utils::toHex(name, a->pubkey_prefix, 4); } else { strncpy(name, a->name, 32); @@ -618,7 +616,7 @@ class HomeScreen : public UIScreen { return true; } if (c == KEY_SELECT && _page == HomePage::DISCOVERY) { - discovery_disp_names = !discovery_disp_names; + _node_prefs->ui_nd_name = !_node_prefs->ui_nd_name; return true; } #endif @@ -726,7 +724,6 @@ class MsgPreviewScreen : public UIScreen { void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs) { _display = display; _sensors = sensors; - _auto_off = millis() + AUTO_OFF_MILLIS; #if defined(PIN_USER_BTN) user_btn.begin(); @@ -739,9 +736,12 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no #endif _node_prefs = node_prefs; + _auto_off = millis() + _node_prefs->ui_auto_off_delay * 1000; + _backlight_off = millis() + _node_prefs->ui_backlight_delay * 1000; if (_display != NULL) { _display->turnOn(); + _display->setMaxPartialRefresh(_node_prefs->ui_max_partial); } #ifdef PIN_BUZZER @@ -808,10 +808,14 @@ void UITask::onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t t if (_display != NULL) { if (!_display->isOn() && !hasConnection()) { _display->turnOn(); + _display->setMaxPartialRefresh(_node_prefs->ui_max_partial); } if (_display->isOn()) { - _auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer - _next_refresh = 100; // trigger refresh + _auto_off = millis() + _node_prefs->ui_auto_off_delay * 1000; // extend the auto-off timer + _next_refresh = 100; // trigger refresh + if (_node_prefs->ui_backlight_on_msgs) { + _backlight_off = millis() + _node_prefs->ui_backlight_delay * 1000; + } } } @@ -828,9 +832,10 @@ void UITask::onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_det if (_display != NULL) { if (!_display->isOn() && !hasConnection()) { _display->turnOn(); + _display->setMaxPartialRefresh(_node_prefs->ui_max_partial); } if (_display->isOn()) { - _auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer + _auto_off = millis() + _node_prefs->ui_auto_off_delay * 1000; // extend the auto-off timer _next_refresh = 100; // trigger refresh } } @@ -909,9 +914,8 @@ void UITask::shutdown(bool restart){ _board->reboot(); } else { if (_display->isEink()) { - _display->blankScreen(); _display->turnOff(); - _display->powerOff(); + _display->blankScreen(); } // Power off board including radio, display, GPS and components _board->powerOff(); @@ -957,9 +961,8 @@ void UITask::loop() { if (ev == BUTTON_EVENT_CLICK) { c = checkDisplayOn(KEY_ENTER); } else if (ev == BUTTON_EVENT_LONG_PRESS) { - display.blankScreen(); display.turnOff(); - display.powerOff(); + display.blankScreen(); } else if (ev == BUTTON_EVENT_DOUBLE_CLICK) { c = handleDoubleClick(KEY_SELECT); } else if (ev == BUTTON_EVENT_TRIPLE_CLICK) { @@ -979,11 +982,11 @@ void UITask::loop() { #endif #if defined(UI_HAS_ROTARY_INPUT) RotaryInputEvent rotaryEv = rotary_input.poll(); - if (c == 0 && _display != NULL && _display->isOn()) { + if (c == 0 && _display != NULL) { if (rotaryEv == RotaryInputEvent::Next) { - c = KEY_NEXT; + c = checkDisplayOn(KEY_NEXT); } else if (rotaryEv == RotaryInputEvent::Prev) { - c = KEY_PREV; + c = checkDisplayOn(KEY_PREV); } } #endif @@ -1002,39 +1005,48 @@ void UITask::loop() { _analogue_pin_read_millis = millis(); } #endif +#if defined(DISP_BACKLIGHT) || defined(EXP_PIN_BACKLIGHT) #if defined(BACKLIGHT_BTN) if (millis() > next_backlight_btn_check) { if (backlight_btn.isPressed()) { if (!_display->isOn()) { _display->turnOn(); + _display->setMaxPartialRefresh(_node_prefs->ui_max_partial); _next_refresh = 0; } - _auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer - backlight_btn_pressed_at = millis(); // still pressed - } - bool touch_state = (millis() - backlight_btn_pressed_at > 5000); - // Backlight button events - int bev = backlight_btn.check(); - if (bev == BUTTON_EVENT_DOUBLE_CLICK) { - _display->forceFullRefresh(); - } else if (bev == BUTTON_EVENT_TRIPLE_CLICK) { - _display->blankScreen(); - _display->turnOff(); - _display->powerOff(); - backlight_btn_pressed_at = 0; + _auto_off = millis() + _node_prefs->ui_auto_off_delay * 1000; // extend auto-off timer + _backlight_off = millis() + _node_prefs->ui_backlight_delay * 1000; // extend auto_off for backlight } + next_backlight_btn_check = millis() + 100; + } + int bev = backlight_btn.check(); + if (bev == BUTTON_EVENT_DOUBLE_CLICK) { + _display->forceFullRefresh(); + } else if (bev == BUTTON_EVENT_TRIPLE_CLICK) { + _display->turnOff(); + _display->blankScreen(); + _backlight_off = 0; + } +#endif + + bool backlight_state; + if (_node_prefs->ui_backlight_delay < 0) + backlight_state = true; + else if (_node_prefs->ui_backlight_delay == 0) + backlight_state = false; + else + backlight_state = (millis() < _backlight_off); + #if defined(DISP_BACKLIGHT) - digitalWrite(DISP_BACKLIGHT, !touch_state); + digitalWrite(DISP_BACKLIGHT, backlight_state); #elif defined(EXP_PIN_BACKLIGHT) - expander.digitalWrite(EXP_PIN_BACKLIGHT, !touch_state); + expander.digitalWrite(EXP_PIN_BACKLIGHT, backlight_state); #endif - next_backlight_btn_check = millis() + 100; - } #endif if (c != 0 && curr) { curr->handleInput(c); - _auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer + _auto_off = millis() + _node_prefs->ui_auto_off_delay * 1000; // extend auto-off timer _next_refresh = 100; // trigger refresh } @@ -1065,26 +1077,26 @@ void UITask::loop() { } _display->endFrame(); } -#if AUTO_OFF_MILLIS > 0 + if (_node_prefs->ui_auto_off_delay > 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; - } + // 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() + _node_prefs->ui_auto_off_delay * 1000; + } #endif - if (millis() > _auto_off) { - #ifdef EINK_SCREENOFF_BLANK - if (curr != msg_preview) { // don't blank if messages are waiting - _display->blankScreen(); - _display->powerOff(); + if (millis() > _auto_off) { + _display->turnOff(); + if (_node_prefs->ui_auto_off_blank && + !(_node_prefs->ui_auto_off_keep_msgs + && curr == msg_preview)) { + // don't blank if messages are waiting + _display->blankScreen(); } - #endif - _display->turnOff(); + } } -#endif } #ifdef PIN_VIBRATION @@ -1117,9 +1129,13 @@ char UITask::checkDisplayOn(char c) { if (_display != NULL) { if (!_display->isOn()) { _display->turnOn(); // turn display on and consume event + _display->setMaxPartialRefresh(_node_prefs->ui_max_partial); c = 0; } - _auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer + _auto_off = millis() + _node_prefs->ui_auto_off_delay * 1000; // extend auto-off timer + if (_node_prefs->ui_backlight_on_keys) { + _backlight_off = millis() + _node_prefs->ui_backlight_delay * 1000; + } _next_refresh = 0; // trigger refresh } return c; diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 4d44bfe1a6e..618314885d4 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -62,7 +62,7 @@ class UITask : public AbstractUITask { int _msgcount; unsigned long ui_started_at, next_batt_chck; unsigned long next_backlight_btn_check = 0; - unsigned long backlight_btn_pressed_at = 0; + unsigned long _backlight_off = 0; #ifdef PIN_STATUS_LED int led_state = 0; int next_led_change = 0; diff --git a/src/helpers/ui/DisplayDriver.h b/src/helpers/ui/DisplayDriver.h index 58147aa0e75..16b315c29d0 100644 --- a/src/helpers/ui/DisplayDriver.h +++ b/src/helpers/ui/DisplayDriver.h @@ -27,7 +27,7 @@ class DisplayDriver { virtual void turnOn() = 0; virtual void turnOff() = 0; virtual void blankScreen() {} // to cleanly erase screen before turning off - virtual void powerOff() {} + virtual void setMaxPartialRefresh(int count) {} virtual void clear() = 0; virtual void startFrame(ColorVal bkg = UIColor::window_bkg) = 0; virtual void setTextSize(int sz) = 0; diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index ce09423e11a..f3735f5f722 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -1,11 +1,6 @@ #include "GxEPDDisplay.h" -#ifdef EXP_PIN_BACKLIGHT - #include - extern PCA9557 expander; -#endif - #ifndef DISPLAY_ROTATION #define DISPLAY_ROTATION 3 #endif @@ -65,26 +60,34 @@ bool GxEPDDisplay::begin() { void GxEPDDisplay::turnOn() { if (!_init) begin(); -#if defined(DISP_BACKLIGHT) && !defined(BACKLIGHT_BTN) - digitalWrite(DISP_BACKLIGHT, HIGH); -#elif defined(EXP_PIN_BACKLIGHT) && !defined(BACKLIGHT_BTN) - expander.digitalWrite(EXP_PIN_BACKLIGHT, HIGH); -#endif if (!_isOn) { _isOn = true; } } -void GxEPDDisplay::powerOff() { - display.hibernate(); +// 24x24 sleep icon, MSB-first (matches GxEPDDisplay::drawXbm) +static const uint8_t sleep_icon_24x24[] = { + 0x00, 0x00, 0xff, 0x00, 0x00, 0xff, 0x00, 0xe0, 0x0e, 0x03, 0xc0, 0x18, + 0x0f, 0x80, 0x70, 0x1f, 0x80, 0xff, 0x3f, 0x80, 0xff, 0x3f, 0x00, 0x00, + 0x7f, 0x3f, 0x00, 0x7f, 0x3f, 0x00, 0xff, 0x0e, 0x00, 0xff, 0x1c, 0x7c, + 0xff, 0xbf, 0x7c, 0xff, 0xbf, 0x38, 0xff, 0xc0, 0x7c, 0xff, 0xe0, 0x7c, + 0xff, 0xf0, 0x00, 0x7f, 0xfc, 0x00, 0x7f, 0xff, 0xf8, 0x3f, 0xff, 0xf0, + 0x1f, 0xff, 0xf0, 0x0f, 0xff, 0xe0, 0x07, 0xff, 0x80, 0x01, 0xff, 0x00 +}; + +void GxEPDDisplay::drawSleepIcon() { + const int W = 24, H = 24; + int x = (int)(display.width() / scale_x) - W; // top-right, logical coordinates + int y = 0; + + display.fillRect((x - 5) * scale_x - 1, y * scale_y, (W+5) * scale_x + 2, (H-2) * scale_y + 1, GxEPD_WHITE); + _curr_color = GxEPD_BLACK; + drawXbm(x, y, sleep_icon_24x24, W, H); + display.display(true); } void GxEPDDisplay::turnOff() { // Only stops refresh -#if defined(DISP_BACKLIGHT) && !defined(BACKLIGHT_BTN) - digitalWrite(DISP_BACKLIGHT, LOW); -#elif defined(EXP_PIN_BACKLIGHT) && !defined(BACKLIGHT_BTN) - expander.digitalWrite(EXP_PIN_BACKLIGHT, LOW); -#endif + drawSleepIcon(); _isOn = false; last_display_crc_value=0; } @@ -96,6 +99,8 @@ void GxEPDDisplay::blankScreen() { display.writeScreenBuffer(0xFF); // Forces 0xFF (White) into the display controller's history registers resetPartialRefreshCounter(); last_display_crc_value=0; + // we can hibernate as screen has been cleaned + display.hibernate(); } void GxEPDDisplay::clear() { diff --git a/src/helpers/ui/GxEPDDisplay.h b/src/helpers/ui/GxEPDDisplay.h index 462744026bd..308a02a1976 100644 --- a/src/helpers/ui/GxEPDDisplay.h +++ b/src/helpers/ui/GxEPDDisplay.h @@ -61,7 +61,7 @@ class GxEPDDisplay : public DisplayDriver { void turnOn() override; void turnOff() override; void blankScreen() override; - void powerOff() override; + void setMaxPartialRefresh(int count) override {max_partial_refresh = count; resetPartialRefreshCounter();} void clear() override; void startFrame(ColorVal bkg = UIColor::window_bkg) override; void setTextSize(int sz) override; @@ -71,6 +71,7 @@ class GxEPDDisplay : public DisplayDriver { void fillRect(int x, int y, int w, int h) override; void drawRect(int x, int y, int w, int h) override; void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override; + void drawSleepIcon(); uint16_t getTextWidth(const char* str) override; void endFrame() override; }; From d41f20a473236e9db1d482acc0ae6bae496f3415 Mon Sep 17 00:00:00 2001 From: Florent Date: Wed, 28 Oct 2026 10:16:52 -0400 Subject: [PATCH 2/3] ui: better behaviour of max_partial and backlight_delay, improvements on cmdline --- docs/companion_cli.md | 28 +++++++++++++++++----- examples/companion_radio/MyMesh.cpp | 28 +++++++++++++++------- examples/companion_radio/NodePrefs.h | 4 ++-- examples/companion_radio/ui-new/UITask.cpp | 12 +++++++--- src/helpers/ui/GxEPDDisplay.cpp | 21 ++++++++-------- src/helpers/ui/GxEPDDisplay.h | 16 ++++--------- variants/lilygo_techo/platformio.ini | 3 +-- variants/thinknode_m1/platformio.ini | 3 +-- variants/thinknode_m5/platformio.ini | 3 +-- variants/thinknode_m8/platformio.ini | 4 +--- 10 files changed, 72 insertions(+), 50 deletions(-) diff --git a/docs/companion_cli.md b/docs/companion_cli.md index 8eec6445940..31a055847b1 100644 --- a/docs/companion_cli.md +++ b/docs/companion_cli.md @@ -393,13 +393,15 @@ Radio parameters are common to all roles and handled by `CommonRadioPrefs`. - `get ui.auto_off_delay` **Parameters:** -- `delay`: in seconds +- `delay`: in seconds (`0` means always on) **Default:** `15` (or set by `AUTO_OFF_MILLIS` build flag (in ms)) **Note:** On e-ink displays, when turned off, the screen is not necessarily blanked, see `ui.auto_off_blank` and `ui.backlight_delay`. +When screen is not blanked, it is not refreshed any more. To let the user aware of that, a sleep icon is shown. + --- #### Display backlight timeout @@ -408,7 +410,13 @@ On e-ink displays, when turned off, the screen is not necessarily blanked, see ` - `get ui.backlight_delay` **Parameters:** -- `delay`: in seconds (`-1` means always on, `0` means always off) +- `delay`: in seconds + - `0` means no timeout + - `-2` means always off + - `-1` means always on + +**Note:** +When set to `0`, backlight can only be triggered using backlight btn. --- @@ -422,11 +430,14 @@ On e-ink displays, when turned off, the screen is not necessarily blanked, see ` **Parameters:** - `state`: `0` (disable) or `1` (enable) -**Default:** `0` +**Default:** +- `ui.backlight_on_msgs = 0` +- `ui.backlight_on_keys = 0` if there is a backlight button on the board, else `1` **Note:** -By default backlight is triggered by backlight button (as long as the button is pressed backlight is on, and is turned off after a delay). +By default backlight is triggered by backlight button if present (as long as the button is pressed backlight is on, and is turned off after a delay). These toggles permits to toggle backlight when keys are pressed or when a message arrives (it acts as a notification signal). +If `backlight_delay` is set to `0` then this setting has no effect, backlight won't be turned on. --- @@ -441,7 +452,9 @@ These toggles permits to toggle backlight when keys are pressed or when a messag **Default:** `0` (or set by `UI_SCREENOFF_BLANK` build flag) **Note:** -If disabled, when screen is shut the displays stays on screen and is not updated. +If disabled, when screen is shut the displays stays on screen and is not updated (an sleep icons covers battery to show the screen is sleeping). + +When screen is blanked, the controller is also put into hibernation, which should preserve the screen a little and some battery ... --- @@ -453,4 +466,7 @@ If disabled, when screen is shut the displays stays on screen and is not updated **Parmeters:** - `value`: max partial refreshes before doing a full refresh -**Default:** 60 (or set by `EINK_MAX_PARTIAL_REFRESH` build flag) \ No newline at end of file +**Default:** 0 (no partial refresh or set by `EINK_MAX_PARTIAL_REFRESH` build flag) + +**Note:** +Forcing full refresh helps revives the screen when some tearing appears (under certain environmental conditions). If you see some tearing appearing, you can force full refresh on your screen manually by double clicking the backlight button. If tearing reappears often, use this setting to force full refresh every `value` partial refreshes. \ No newline at end of file diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 9c15ae72cf5..3424daf7061 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2213,49 +2213,54 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* if (memcmp(command, "set ui.show_clock", 17) == 0) { _prefs.ui_show_clock = atoi(&command[17]) ? 1 : 0; savePrefs(); - sprintf(reply, "> ui.show_clock is now %d", _prefs.ui_show_clock); + sprintf(reply, "> now %d", _prefs.ui_show_clock); return true; } if (memcmp(command, "set ui.auto_off_delay", 21) == 0) { _prefs.ui_auto_off_delay = atoi(&command[21]); savePrefs(); - sprintf(reply, "> ui.auto_off_delay is now %d seconds", _prefs.ui_auto_off_delay); + sprintf(reply, "> now %d seconds", _prefs.ui_auto_off_delay); return true; } if (memcmp(command, "set ui.backlight_delay", 22) == 0) { _prefs.ui_backlight_delay = atoi(&command[22]); savePrefs(); - sprintf(reply, "> ui.backlight_delay is now %d seconds", _prefs.ui_backlight_delay); + if (_prefs.ui_backlight_delay >= 0) + sprintf(reply, "> now %d seconds", _prefs.ui_backlight_delay); + else if (_prefs.ui_backlight_delay == -1) + sprintf(reply, "> now always on (-1)"); + else + sprintf(reply, "> now disabled (<-1)"); return true; } if (memcmp(command, "set ui.backlight_on_keys", 24) == 0) { _prefs.ui_backlight_on_keys = atoi(&command[24]); savePrefs(); - sprintf(reply, "> ui.backlight_on_keys is now %d", _prefs.ui_backlight_on_keys); + sprintf(reply, "> now %d", _prefs.ui_backlight_on_keys); return true; } if (memcmp(command, "set ui.backlight_on_msgs", 24) == 0) { _prefs.ui_backlight_on_msgs = atoi(&command[24]); savePrefs(); - sprintf(reply, "> ui.backlight_on_msgs is now %d", _prefs.ui_backlight_on_msgs); + sprintf(reply, "> now %d", _prefs.ui_backlight_on_msgs); return true; } if (memcmp(command, "set ui.auto_off_blank", 21) == 0) { _prefs.ui_auto_off_blank = atoi(&command[21]); savePrefs(); - sprintf(reply, "> ui.auto_off_blank is now %d", _prefs.ui_auto_off_blank); + sprintf(reply, "> now %d", _prefs.ui_auto_off_blank); return true; } if (memcmp(command, "set ui.auto_off_keep_msgs", 25) == 0) { _prefs.ui_auto_off_keep_msgs = atoi(&command[25]); savePrefs(); - sprintf(reply, "> ui.auto_off_keep_msgs is now %d", _prefs.ui_auto_off_keep_msgs); + sprintf(reply, "> now %d", _prefs.ui_auto_off_keep_msgs); return true; } if (memcmp(command, "set ui.max_partial", 18) == 0) { _prefs.ui_max_partial = atoi(&command[18]); savePrefs(); - sprintf(reply, "> ui.max_partial is now %d", _prefs.ui_max_partial); + sprintf(reply, "> now %d", _prefs.ui_max_partial); return true; } if (memcmp(command, "set ui.nd_name", 14) == 0) { @@ -2273,7 +2278,12 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* return true; } if (memcmp(command, "get ui.backlight_delay", 22) == 0) { - sprintf(reply, "> %d seconds", _prefs.ui_backlight_delay); + if (_prefs.ui_backlight_delay >= 0) + sprintf(reply, "> %d seconds", _prefs.ui_backlight_delay); + else if (_prefs.ui_backlight_delay == -1) + sprintf(reply, "> always on (-1)"); + else + sprintf(reply, "> disabled (<-1)"); return true; } if (memcmp(command, "get ui.backlight_on_keys", 24) == 0) { diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 9ceb40a3947..c5d1df04a42 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -66,12 +66,12 @@ class NodePrefs : public ConfigSerializer { // persisted to file #define AUTO_OFF_MILLIS 15000 #endif #ifndef EINK_MAX_PARTIAL_REFRESH - #define EINK_MAX_PARTIAL_REFRESH 60 + #define EINK_MAX_PARTIAL_REFRESH 0 #endif uint8_t ui_show_clock = UI_SHOW_CLOCK; uint32_t ui_auto_off_delay = AUTO_OFF_MILLIS / 1000; int32_t ui_backlight_delay = 5; - #if defined(DISP_BACKLIGHT) || defined(EXP_PIN_BACKLIGHT) + #if defined(BACKLIGHT_BTN) uint8_t ui_backlight_on_keys = 0; #else uint8_t ui_backlight_on_keys = 1; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 4d467027c9e..5416a475383 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -1030,12 +1030,18 @@ void UITask::loop() { #endif bool backlight_state; - if (_node_prefs->ui_backlight_delay < 0) + if (_node_prefs->ui_backlight_delay == -1) { backlight_state = true; - else if (_node_prefs->ui_backlight_delay == 0) + } else if (_node_prefs->ui_backlight_delay < -1) { backlight_state = false; - else + } else if (_node_prefs->ui_backlight_delay == 0) { + backlight_state = false; + #ifdef BACKLIGHT_BTN + backlight_state = backlight_btn.isPressed(); + #endif + } else { backlight_state = (millis() < _backlight_off); + } #if defined(DISP_BACKLIGHT) digitalWrite(DISP_BACKLIGHT, backlight_state); diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index f3735f5f722..97759461648 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -47,7 +47,7 @@ bool GxEPDDisplay::begin() { } display.setPartialWindow(0, 0, display.width(), display.height()); - resetPartialRefreshCounter(); + _partial_refresh_count = 0; #if DISP_BACKLIGHT digitalWrite(DISP_BACKLIGHT, LOW); @@ -97,8 +97,8 @@ void GxEPDDisplay::blankScreen() { // no full refresh needed at wakeup display.clearScreen(0xFF); // Clears microcontroller side RAM display.writeScreenBuffer(0xFF); // Forces 0xFF (White) into the display controller's history registers - resetPartialRefreshCounter(); - last_display_crc_value=0; + _partial_refresh_count = 0; + last_display_crc_value = 0; // we can hibernate as screen has been cleaned display.hibernate(); } @@ -113,15 +113,18 @@ void GxEPDDisplay::startFrame(ColorVal bkg) { display.fillScreen(bkg); display.setTextColor(_curr_color = UIColor::primary_txt); display_crc.reset(); - if (_cycles_before_full_refresh != 0) { - display.setPartialWindow(0, 0, display.width(), display.height()); - } else { + if (_force_full_refresh + || (_max_partial_refresh > 0 + && _partial_refresh_count >= _max_partial_refresh)) { // forces a full wipe of the screen ... display.clearScreen(0xFF); // Clears microcontroller side RAM display.writeScreenBuffer(0xFF); // Forces 0xFF (White) into the display controller's history registers // we'll need a partial refresh after that (whatever crc value is) last_display_crc_value = 0; - resetPartialRefreshCounter(); + _partial_refresh_count = 0; + _force_full_refresh = false; + } else { + display.setPartialWindow(0, 0, display.width(), display.height()); } } @@ -228,9 +231,7 @@ void GxEPDDisplay::endFrame() { uint32_t crc = display_crc.finalize(); if (crc != last_display_crc_value) { display.display(true); - if (_cycles_before_full_refresh > 0) { - _cycles_before_full_refresh--; - } + _partial_refresh_count++; } last_display_crc_value = crc; } diff --git a/src/helpers/ui/GxEPDDisplay.h b/src/helpers/ui/GxEPDDisplay.h index 308a02a1976..5c510de0a0e 100644 --- a/src/helpers/ui/GxEPDDisplay.h +++ b/src/helpers/ui/GxEPDDisplay.h @@ -16,12 +16,6 @@ #include "DisplayDriver.h" -#ifndef EINK_MAX_PARTIAL_REFRESH - // 60 to prevent ghosting when refreshing every minute ... - // set to -1 to disable the counter - #define EINK_MAX_PARTIAL_REFRESH -1 -#endif - class GxEPDDisplay : public DisplayDriver { #if defined(EINK_DISPLAY_MODEL) @@ -42,8 +36,9 @@ class GxEPDDisplay : public DisplayDriver { uint16_t _curr_color; CRC32 display_crc; int last_display_crc_value = 0; - int _cycles_before_full_refresh; - int max_partial_refresh = EINK_MAX_PARTIAL_REFRESH; // so this can be changed by an option after + bool _force_full_refresh = false; + int _partial_refresh_count = 0; + int _max_partial_refresh = 0; public: #if defined(EINK_DISPLAY_MODEL) @@ -56,12 +51,11 @@ class GxEPDDisplay : public DisplayDriver { bool isOn() override { return _isOn; } bool isEink() override { return true; } - void forceFullRefresh() override { _cycles_before_full_refresh = 0; }; - void resetPartialRefreshCounter() { _cycles_before_full_refresh = max_partial_refresh; }; + void forceFullRefresh() override { _force_full_refresh = true; } void turnOn() override; void turnOff() override; void blankScreen() override; - void setMaxPartialRefresh(int count) override {max_partial_refresh = count; resetPartialRefreshCounter();} + void setMaxPartialRefresh(int count) override {_max_partial_refresh = count; } void clear() override; void startFrame(ColorVal bkg = UIColor::window_bkg) override; void setTextSize(int sz) override; diff --git a/variants/lilygo_techo/platformio.ini b/variants/lilygo_techo/platformio.ini index ba39549e1f0..896e42a656f 100644 --- a/variants/lilygo_techo/platformio.ini +++ b/variants/lilygo_techo/platformio.ini @@ -40,8 +40,7 @@ build_flags = ${nrf52_base.build_flags} -D EINK_SCREENOFF_BLANK -D UI_RECENT_LIST_SIZE=9 -D UI_MSG_PREVIEW_SIZE=165 - -D EINK_MAX_PARTIAL_REFRESH=60 - -D AUTO_OFF_MILLIS=900000 + -D AUTO_OFF_MILLIS=300000 -D BACKLIGHT_BTN=PIN_BUTTON2 build_src_filter = ${nrf52_base.build_src_filter} + diff --git a/variants/thinknode_m1/platformio.ini b/variants/thinknode_m1/platformio.ini index 7c620e2ed6d..03b0eb4efff 100644 --- a/variants/thinknode_m1/platformio.ini +++ b/variants/thinknode_m1/platformio.ini @@ -26,14 +26,13 @@ build_flags = ${nrf52_base.build_flags} -D P_LORA_TX_LED=13 -D UI_RECENT_LIST_SIZE=9 -D UI_MSG_PREVIEW_SIZE=165 - -D EINK_MAX_PARTIAL_REFRESH=60 -D EINK_DISPLAY_MODEL=GxEPD2_154_D67 -D EINK_SCALE_X=1.5625f -D EINK_SCALE_Y=1.5625f -D EINK_X_OFFSET=0 -D EINK_Y_OFFSET=10 -D BACKLIGHT_BTN=PIN_BUTTON2 - -D AUTO_OFF_MILLIS=900000 + -D AUTO_OFF_MILLIS=300000 ; 5 minutes -D UI_NO_HIBERNATE -D DISABLE_DIAGNOSTIC_OUTPUT build_src_filter = ${nrf52_base.build_src_filter} diff --git a/variants/thinknode_m5/platformio.ini b/variants/thinknode_m5/platformio.ini index a9761597d8e..d2b791a327e 100644 --- a/variants/thinknode_m5/platformio.ini +++ b/variants/thinknode_m5/platformio.ini @@ -28,9 +28,8 @@ build_flags = ${esp32_base.build_flags} -D EINK_Y_OFFSET=10 -D UI_RECENT_LIST_SIZE=9 -D UI_MSG_PREVIEW_SIZE=165 - -D EINK_MAX_PARTIAL_REFRESH=60 -D BACKLIGHT_BTN=PIN_BUTTON2 - -D AUTO_OFF_MILLIS=900000 + -D AUTO_OFF_MILLIS=300000 -D UI_NO_HIBERNATE -D DISABLE_DIAGNOSTIC_OUTPUT -D SX126X_DIO2_AS_RF_SWITCH=true diff --git a/variants/thinknode_m8/platformio.ini b/variants/thinknode_m8/platformio.ini index 5282f280f6a..fdbaea1f587 100644 --- a/variants/thinknode_m8/platformio.ini +++ b/variants/thinknode_m8/platformio.ini @@ -23,11 +23,9 @@ build_flags = ${nrf52_base.build_flags} -D UI_HAS_NAV_INPUT=1 -D UI_RECENT_LIST_SIZE=9 -D UI_MSG_PREVIEW_SIZE=165 - -D EINK_MAX_PARTIAL_REFRESH=60 - -D AUTO_OFF_MILLIS=900000 + -D AUTO_OFF_MILLIS=300000 -D BACKLIGHT_BTN=PIN_BUTTON2 -D UI_SHOW_CLOCK=1 - -D EINK_SCREENOFF_BLANK -D EINK_DISPLAY_MODEL=GxEPD2_154_D67 ; -D EINK_DISPLAY_MODEL=GxEPD2_154_GDEY0154D67 ; -D EINK_DISPLAY_MODEL=GxEPD2_150_BN From 61fed4e6872a86696098b43c8eb7d67e066c4fa4 Mon Sep 17 00:00:00 2001 From: Florent Date: Wed, 28 Oct 2026 11:45:26 -0400 Subject: [PATCH 3/3] DisplayDriver: adding optional parameters for turnOn/turnOff (max_partial / blanking) --- examples/companion_radio/MyMesh.cpp | 2 +- examples/companion_radio/ui-new/UITask.cpp | 29 ++++++++-------------- src/helpers/ui/DisplayDriver.h | 4 +-- src/helpers/ui/GxEPDDisplay.cpp | 13 ++++++++-- src/helpers/ui/GxEPDDisplay.h | 8 +++--- 5 files changed, 30 insertions(+), 26 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 3424daf7061..8a8f7f1d3eb 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2260,7 +2260,7 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* if (memcmp(command, "set ui.max_partial", 18) == 0) { _prefs.ui_max_partial = atoi(&command[18]); savePrefs(); - sprintf(reply, "> now %d", _prefs.ui_max_partial); + sprintf(reply, "> now %d (applied at next screen wakeup)", _prefs.ui_max_partial); return true; } if (memcmp(command, "set ui.nd_name", 14) == 0) { diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 5416a475383..89008d7a4fd 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -740,8 +740,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no _backlight_off = millis() + _node_prefs->ui_backlight_delay * 1000; if (_display != NULL) { - _display->turnOn(); - _display->setMaxPartialRefresh(_node_prefs->ui_max_partial); + _display->turnOn(_node_prefs->ui_max_partial); } #ifdef PIN_BUZZER @@ -807,8 +806,7 @@ void UITask::onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t t if (_display != NULL) { if (!_display->isOn() && !hasConnection()) { - _display->turnOn(); - _display->setMaxPartialRefresh(_node_prefs->ui_max_partial); + _display->turnOn(_node_prefs->ui_max_partial); } if (_display->isOn()) { _auto_off = millis() + _node_prefs->ui_auto_off_delay * 1000; // extend the auto-off timer @@ -831,8 +829,7 @@ void UITask::onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_det if (_display != NULL) { if (!_display->isOn() && !hasConnection()) { - _display->turnOn(); - _display->setMaxPartialRefresh(_node_prefs->ui_max_partial); + _display->turnOn(_node_prefs->ui_max_partial); } if (_display->isOn()) { _auto_off = millis() + _node_prefs->ui_auto_off_delay * 1000; // extend the auto-off timer @@ -914,8 +911,7 @@ void UITask::shutdown(bool restart){ _board->reboot(); } else { if (_display->isEink()) { - _display->turnOff(); - _display->blankScreen(); + _display->turnOff(true); } // Power off board including radio, display, GPS and components _board->powerOff(); @@ -961,8 +957,7 @@ void UITask::loop() { if (ev == BUTTON_EVENT_CLICK) { c = checkDisplayOn(KEY_ENTER); } else if (ev == BUTTON_EVENT_LONG_PRESS) { - display.turnOff(); - display.blankScreen(); + display.turnOff(true); } else if (ev == BUTTON_EVENT_DOUBLE_CLICK) { c = handleDoubleClick(KEY_SELECT); } else if (ev == BUTTON_EVENT_TRIPLE_CLICK) { @@ -1010,8 +1005,7 @@ void UITask::loop() { if (millis() > next_backlight_btn_check) { if (backlight_btn.isPressed()) { if (!_display->isOn()) { - _display->turnOn(); - _display->setMaxPartialRefresh(_node_prefs->ui_max_partial); + _display->turnOn(_node_prefs->ui_max_partial); _next_refresh = 0; } _auto_off = millis() + _node_prefs->ui_auto_off_delay * 1000; // extend auto-off timer @@ -1023,8 +1017,7 @@ void UITask::loop() { if (bev == BUTTON_EVENT_DOUBLE_CLICK) { _display->forceFullRefresh(); } else if (bev == BUTTON_EVENT_TRIPLE_CLICK) { - _display->turnOff(); - _display->blankScreen(); + _display->turnOff(true); _backlight_off = 0; } #endif @@ -1094,12 +1087,13 @@ void UITask::loop() { } #endif if (millis() > _auto_off) { - _display->turnOff(); if (_node_prefs->ui_auto_off_blank && !(_node_prefs->ui_auto_off_keep_msgs && curr == msg_preview)) { // don't blank if messages are waiting - _display->blankScreen(); + _display->turnOff(true); + } else { + _display->turnOff(); } } } @@ -1134,8 +1128,7 @@ void UITask::loop() { char UITask::checkDisplayOn(char c) { if (_display != NULL) { if (!_display->isOn()) { - _display->turnOn(); // turn display on and consume event - _display->setMaxPartialRefresh(_node_prefs->ui_max_partial); + _display->turnOn(_node_prefs->ui_max_partial); // turn display on and consume event c = 0; } _auto_off = millis() + _node_prefs->ui_auto_off_delay * 1000; // extend auto-off timer diff --git a/src/helpers/ui/DisplayDriver.h b/src/helpers/ui/DisplayDriver.h index 16b315c29d0..0e41dab5e4d 100644 --- a/src/helpers/ui/DisplayDriver.h +++ b/src/helpers/ui/DisplayDriver.h @@ -25,9 +25,9 @@ class DisplayDriver { virtual bool isEink() { return false; } // default to non-eink, override in eink drivers virtual void forceFullRefresh() {} // next refresh will be full for eink virtual void turnOn() = 0; + virtual void turnOn(int max_partial) { turnOn(); } virtual void turnOff() = 0; - virtual void blankScreen() {} // to cleanly erase screen before turning off - virtual void setMaxPartialRefresh(int count) {} + virtual void turnOff(bool blank) { turnOff(); } virtual void clear() = 0; virtual void startFrame(ColorVal bkg = UIColor::window_bkg) = 0; virtual void setTextSize(int sz) = 0; diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index 97759461648..d7f9562bb3a 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -65,6 +65,11 @@ void GxEPDDisplay::turnOn() { } } +void GxEPDDisplay::turnOn(int max_partial) { + setMaxPartialRefresh(max_partial); + turnOn(); +} + // 24x24 sleep icon, MSB-first (matches GxEPDDisplay::drawXbm) static const uint8_t sleep_icon_24x24[] = { 0x00, 0x00, 0xff, 0x00, 0x00, 0xff, 0x00, 0xe0, 0x0e, 0x03, 0xc0, 0x18, @@ -86,8 +91,12 @@ void GxEPDDisplay::drawSleepIcon() { display.display(true); } -void GxEPDDisplay::turnOff() { // Only stops refresh - drawSleepIcon(); +void GxEPDDisplay::turnOff(bool blank) { + if (blank) { + blankScreen(); + } else { + drawSleepIcon(); + } _isOn = false; last_display_crc_value=0; } diff --git a/src/helpers/ui/GxEPDDisplay.h b/src/helpers/ui/GxEPDDisplay.h index 5c510de0a0e..9d91844303a 100644 --- a/src/helpers/ui/GxEPDDisplay.h +++ b/src/helpers/ui/GxEPDDisplay.h @@ -52,10 +52,12 @@ class GxEPDDisplay : public DisplayDriver { bool isOn() override { return _isOn; } bool isEink() override { return true; } void forceFullRefresh() override { _force_full_refresh = true; } + void turnOn(int max_partial) override; void turnOn() override; - void turnOff() override; - void blankScreen() override; - void setMaxPartialRefresh(int count) override {_max_partial_refresh = count; } + void turnOff(bool blank) override; + void turnOff() override { turnOff(false); } + virtual void blankScreen(); + virtual void setMaxPartialRefresh(int count) {_max_partial_refresh = count; } void clear() override; void startFrame(ColorVal bkg = UIColor::window_bkg) override; void setTextSize(int sz) override;