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 3cceed56668..8b9b213d6dd 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; };