From 992cc083e6b414e1452f2ebcca9b56c6a1ab40b7 Mon Sep 17 00:00:00 2001 From: AI7NC <77077873+AI7NC@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:22:00 -0800 Subject: [PATCH 01/20] Update cli_commands.md to include path.hash.mode and loop.detect Adding the new repeater cli commands introduced in 1.14 Ref: https://buymeacoffee.com/ripplebiz/path-diagnostics-improvements --- docs/cli_commands.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 1d3430db2..8c48ced73 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -383,6 +383,43 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +### View or change this node's avert 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 count)[64 max flood] + - `1`: 2 Byte hash size (65,536 count)[32 max flood] + - `2`: 3 Byte hash size (16,777,216 count)[21 max flood] + +**Default:** `0` + +**Note:** the 'mode' is the low-level encoding (0..3) where 0 -> 1-byte (legacy), 1 -> 2-byte, 2 -> 3-byte for the ID/hash of the repeaters adverts. This feature was added in firmware 1.14 + +--- + +### View or change this node's loop detection +**Usage:** +- `get loop.detect` +- `set loop.detect ` + +**Parameters:** +- `state`: + - `off`: no loop detection is performed + - `minimal`: packets are dropped if repeaters ID/hash shows more times than 4(1-byte), 2(2-byte), 1(3-byte) + - `moderate`: packets are dropped if repeaters ID/hash shows more times than 2(1-byte), 1(2-byte), 1(3-byte) + - `strict`: packets are dropped if repeaters ID/hash shows more times than 1(1-byte), 1(2-byte), 1(3-byte) + +**Default:** `off` + +**Note:** When it is enabled, repeaters will now reject flood packets which look like they are in a loop. This has been happening recently in some meshes when there is just a single 'bad' repeater firmware out there (prob some forked or custom firmware). If the payload is messed with, then forwarded, the same packet ends up causing a packet storm, repeated up to the max 64 hops. This feature was added in firmware 1.14 + +**Example:** If preference is `loop.detect minimal`, and a 1-byte path size packet is received, the repeater will see if its own ID/hash is already in the path. If it's already encoded 4 times, it will reject the packet. If the packet uses 2-byte path size, and repeater's own ID/hash is already encoded 2 times, it rejects. If the packet uses 3-byte path size, and the repeater's own ID/hash is already encoded 1 time, it rejects. + +--- + #### View or change the retransmit delay factor for flood traffic **Usage:** - `get txdelay` From d2a6fda8d587a9bb7f8af09d74d1bc9bce5e3e64 Mon Sep 17 00:00:00 2001 From: AI7NC <77077873+AI7NC@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:30:45 -0800 Subject: [PATCH 02/20] Update cli_commands.md --- docs/cli_commands.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 8c48ced73..ff5f2366e 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -398,6 +398,8 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Note:** the 'mode' is the low-level encoding (0..3) where 0 -> 1-byte (legacy), 1 -> 2-byte, 2 -> 3-byte for the ID/hash of the repeaters adverts. This feature was added in firmware 1.14 +**Temporary Note:** adverts with hash sizes of 2 or 3 bytes may have limited flood propogation in your network while this feature is new. Consider your install base of firmware >=1.14 before implementing. + --- ### View or change this node's loop detection From 721c21f1e52867cc4ff39b70d1b523c7fac01128 Mon Sep 17 00:00:00 2001 From: AI7NC <77077873+AI7NC@users.noreply.github.com> Date: Sat, 7 Mar 2026 10:40:41 -0800 Subject: [PATCH 03/20] Apply suggestion from @weebl2000 Co-authored-by: Wessel --- docs/cli_commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index ff5f2366e..e7cb85e30 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -383,7 +383,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- -### View or change this node's avert path hash size +#### View or change this node's advert path hash size **Usage:** - `get path.hash.mode` - `set path.hash.mode ` From 0228d596e8aef95d32596b407f14874ac08df6fb Mon Sep 17 00:00:00 2001 From: AI7NC <77077873+AI7NC@users.noreply.github.com> Date: Sat, 7 Mar 2026 10:42:13 -0800 Subject: [PATCH 04/20] Apply suggestion from @weebl2000 Co-authored-by: Wessel --- docs/cli_commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index e7cb85e30..d4c97e844 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -402,7 +402,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- -### View or change this node's loop detection +#### View or change this node's loop detection **Usage:** - `get loop.detect` - `set loop.detect ` From 4aaa557dafe18d757c5ef9a7acb60b48b6be09be Mon Sep 17 00:00:00 2001 From: AI7NC <77077873+AI7NC@users.noreply.github.com> Date: Sat, 7 Mar 2026 10:43:23 -0800 Subject: [PATCH 05/20] Apply suggestion from @weebl2000 Co-authored-by: Wessel --- docs/cli_commands.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index d4c97e844..9f84808c3 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -410,9 +410,9 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `state`: - `off`: no loop detection is performed - - `minimal`: packets are dropped if repeaters ID/hash shows more times than 4(1-byte), 2(2-byte), 1(3-byte) - - `moderate`: packets are dropped if repeaters ID/hash shows more times than 2(1-byte), 1(2-byte), 1(3-byte) - - `strict`: packets are dropped if repeaters ID/hash shows more times than 1(1-byte), 1(2-byte), 1(3-byte) + - `minimal`: packets are dropped if repeater's ID/hash appears 4 or more times (1-byte), 2 or more (2-byte), 1 or more (3-byte) + - `moderate`: packets are dropped if repeater's ID/hash appears 2 or more times (1-byte), 1 or more (2-byte), 1 or more (3-byte) + - `strict`: packets are dropped if repeater's ID/hash appears 1 or more times (1-byte), 1 or more (2-byte), 1 or more (3-byte) **Default:** `off` From fe32f16aa40a00dfed9d7315261680d84b9bf174 Mon Sep 17 00:00:00 2001 From: AI7NC <77077873+AI7NC@users.noreply.github.com> Date: Sat, 7 Mar 2026 10:55:40 -0800 Subject: [PATCH 06/20] Update cli_commands.md R399 updates --- docs/cli_commands.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 9f84808c3..1bcb33ccb 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -390,13 +390,14 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `value`: Path hash size (0-2) - - `0`: 1 Byte hash size (256 count)[64 max flood] - - `1`: 2 Byte hash size (65,536 count)[32 max flood] - - `2`: 3 Byte hash size (16,777,216 count)[21 max flood] + - `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 'mode' is the low-level encoding (0..3) where 0 -> 1-byte (legacy), 1 -> 2-byte, 2 -> 3-byte for the ID/hash of the repeaters adverts. This feature was added in firmware 1.14 +**Note:** the 'path.hash.mode' sets the low-level ID/hash encoding size used the for 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 **Temporary Note:** adverts with hash sizes of 2 or 3 bytes may have limited flood propogation in your network while this feature is new. Consider your install base of firmware >=1.14 before implementing. From bb454861c7b893c76813c3e29f0f19a871f67c71 Mon Sep 17 00:00:00 2001 From: AI7NC <77077873+AI7NC@users.noreply.github.com> Date: Sat, 7 Mar 2026 11:01:20 -0800 Subject: [PATCH 07/20] Update cli_commands.md R402 --- docs/cli_commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 1bcb33ccb..78df127e9 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -399,7 +399,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Note:** the 'path.hash.mode' sets the low-level ID/hash encoding size used the for 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 -**Temporary Note:** adverts with hash sizes of 2 or 3 bytes may have limited flood propogation in your network while this feature is new. Consider your install base of firmware >=1.14 before implementing. +**Temporary Note:** adverts with ID/hash sizes of 2 or 3 bytes may have limited flood propogation in your network while this feature is new as v1.13.0 firmware and older will drop packets with multibyte path ID/hashes as only 1-byte hashes are suppored. Consider your install base of firmware >=1.14 has reached a criticality for effective network flooding before implementing higher ID/hash sizes. --- From 36db50a0d25af181de03fe2239cd9f8053c89930 Mon Sep 17 00:00:00 2001 From: AI7NC <77077873+AI7NC@users.noreply.github.com> Date: Sat, 7 Mar 2026 11:14:03 -0800 Subject: [PATCH 08/20] Update cli_commands.md R400 grammer Small grammar fix --- docs/cli_commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 78df127e9..98238f0d1 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -397,7 +397,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Default:** `0` -**Note:** the 'path.hash.mode' sets the low-level ID/hash encoding size used the for 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 +**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 **Temporary Note:** adverts with ID/hash sizes of 2 or 3 bytes may have limited flood propogation in your network while this feature is new as v1.13.0 firmware and older will drop packets with multibyte path ID/hashes as only 1-byte hashes are suppored. Consider your install base of firmware >=1.14 has reached a criticality for effective network flooding before implementing higher ID/hash sizes. From b872abda46c588b14a464a39f9d379b0d0aae9c5 Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Sun, 8 Mar 2026 03:50:37 +0000 Subject: [PATCH 09/20] feat(heltec_v3): unified BLE/USB/WiFi companion transport with runtime switching Implements the core ask from #1188 by enabling companion transport changes without re-flashing (BLE <-> serial/TCP path), and extends it per follow-up request to include WiFi/TCP network switching and CLI-configurable credentials. - add CompanionTransportInterface for ESP32 all-transports mode - support runtime transport switching: BLE <-> TCP/WiFi - add WiFi config vars: wifi.mode (ap/client), wifi.ssid/pwd, wifi.ap.ssid/pwd - persist new WiFi settings in NodePrefs/DataStore - wire settings into startup path in main.cpp - add MyMesh handlers for protocol + CLI Rescue commands - update Heltec v3 tcp_usb_ble variant flags/build wiring - improve UI: dedicated BLE/WiFi pages, hide inactive page, no overlap - refresh UI immediately on command-driven mode changes - show AP name/password/IP and client SSID/IP status on-device - add docs/wifi_transport_command_examples.md with raw/python/CLI examples --- docs/wifi_transport_command_examples.md | 139 ++++++++++++ examples/companion_radio/DataStore.cpp | 12 + examples/companion_radio/MyMesh.cpp | 208 +++++++++++++++++- examples/companion_radio/NodePrefs.h | 8 +- examples/companion_radio/main.cpp | 35 ++- examples/companion_radio/ui-new/UITask.cpp | 178 ++++++++++++--- examples/companion_radio/ui-new/UITask.h | 7 + .../esp32/CompanionTransportInterface.cpp | 204 +++++++++++++++++ .../esp32/CompanionTransportInterface.h | 80 +++++++ variants/heltec_v3/platformio.ini | 25 ++- 10 files changed, 865 insertions(+), 31 deletions(-) create mode 100644 docs/wifi_transport_command_examples.md create mode 100644 src/helpers/esp32/CompanionTransportInterface.cpp create mode 100644 src/helpers/esp32/CompanionTransportInterface.h diff --git a/docs/wifi_transport_command_examples.md b/docs/wifi_transport_command_examples.md new file mode 100644 index 000000000..4ecf43a80 --- /dev/null +++ b/docs/wifi_transport_command_examples.md @@ -0,0 +1,139 @@ +# WiFi/Transport Command Examples + +This file shows the same settings commands in three formats: + +1. Raw companion protocol frames (USB/TCP) +2. Python (build/send framed command) +3. Text CLI commands + +## Command Keys + +These are the custom-var keys used by firmware: + +- `transport:ble` +- `transport:wifi` +- `wifi.mode:ap` +- `wifi.mode:client` +- `wifi.ssid:` +- `wifi.pwd:` +- `wifi.ap.ssid:` +- `wifi.ap.pwd:` + +## 1) Raw Companion Protocol Form + +Frame format: + +- App -> radio: `0x3c` + `` + `` +- Payload for set custom var: `0x29` + ASCII string + +Example raw bytes (hex): + +- `transport:wifi` + - Payload: `29 74 72 61 6e 73 70 6f 72 74 3a 77 69 66 69` + - Full frame: `3c 0f 00 29 74 72 61 6e 73 70 6f 72 74 3a 77 69 66 69` + +- `wifi.mode:ap` + - Payload: `29 77 69 66 69 2e 6d 6f 64 65 3a 61 70` + - Full frame: `3c 0d 00 29 77 69 66 69 2e 6d 6f 64 65 3a 61 70` + +- `wifi.mode:client` + - Payload: `29 77 69 66 69 2e 6d 6f 64 65 3a 63 6c 69 65 6e 74` + - Full frame: `3c 11 00 29 77 69 66 69 2e 6d 6f 64 65 3a 63 6c 69 65 6e 74` + +## 2) Python Form + +### Helper function + +```python +import os + +def send_custom_var(port, kv): + fd = os.open(port, os.O_WRONLY) + payload = bytes([0x29]) + kv.encode("ascii") + frame = bytes([0x3c, len(payload) & 0xFF, (len(payload) >> 8) & 0xFF]) + payload + os.write(fd, frame) + os.close(fd) +``` + +### Example usage + +```python +send_custom_var("/dev/ttyUSB0", "transport:wifi") +send_custom_var("/dev/ttyUSB0", "wifi.mode:ap") +send_custom_var("/dev/ttyUSB0", "wifi.ap.ssid:MeshCore-TestAP") +send_custom_var("/dev/ttyUSB0", "wifi.ap.pwd:testpass123") + +send_custom_var("/dev/ttyUSB0", "wifi.mode:client") +send_custom_var("/dev/ttyUSB0", "wifi.ssid:YourSSID") +send_custom_var("/dev/ttyUSB0", "wifi.pwd:YourPassword") + +send_custom_var("/dev/ttyUSB0", "transport:ble") +``` + +### One-liner pattern + +```bash +python3 -c "import os;fd=os.open('/dev/ttyUSB0',os.O_WRONLY);s='wifi.mode:ap';p=bytes([0x29])+s.encode();os.write(fd,bytes([0x3c,len(p)&255,(len(p)>>8)&255])+p);os.close(fd)" +``` + +## 3) Text CLI Form + +There are two text-CLI contexts: + +1. `meshcore-cli` companion commands +2. CLI Rescue (device text console) + +### 3.1 meshcore-cli (companion mode) + +Use serial companion mode (no `-r`): + +```bash +meshcore-cli -s /dev/ttyUSB0 -b 115200 infos +``` + +Then set values: + +```bash +meshcore-cli -s /dev/ttyUSB0 -b 115200 set transport tcp +meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.mode ap +meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.ap.ssid MeshCore-TestAP +meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.ap.pwd testpass123 + +meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.mode client +meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.ssid YourSSID +meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.pwd YourPassword + +meshcore-cli -s /dev/ttyUSB0 -b 115200 set transport ble +``` + +### 3.2 CLI Rescue text commands + +Inside CLI Rescue terminal: + +```text +set transport tcp +set wifi.mode ap +set wifi.ap.ssid MeshCore-TestAP +set wifi.ap.pwd testpass123 + +set wifi.mode client +set wifi.ssid YourSSID +set wifi.pwd YourPassword + +set transport ble +``` + +## TCP Test Example + +If device client IP is known (example `192.168.40.55`): + +```bash +meshcore-cli -t 192.168.40.55 -p 5000 infos +``` + +## Notes + +- `transport:wifi` / `set transport tcp` enables WiFi/TCP transport and disables BLE. +- `transport:ble` / `set transport ble` enables BLE and disables WiFi/TCP. +- AP password should be empty (open AP) or at least 8 characters. +- Do not commit real SSIDs/passwords to git. diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index d9ebacb41..22d08f006 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -230,6 +230,12 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no file.read((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86 file.read((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87 file.read((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops)); // 88 + file.read((uint8_t *)&_prefs.transport_mode, sizeof(_prefs.transport_mode)); // 89 + file.read((uint8_t *)_prefs.wifi_ssid, sizeof(_prefs.wifi_ssid)); // 90 + file.read((uint8_t *)_prefs.wifi_pwd, sizeof(_prefs.wifi_pwd)); // 123 + file.read((uint8_t *)&_prefs.wifi_mode, sizeof(_prefs.wifi_mode)); // 188 + file.read((uint8_t *)_prefs.wifi_ap_ssid, sizeof(_prefs.wifi_ap_ssid)); // 189 + file.read((uint8_t *)_prefs.wifi_ap_pwd, sizeof(_prefs.wifi_ap_pwd)); // 222 file.close(); } @@ -267,6 +273,12 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ file.write((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86 file.write((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87 file.write((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops)); // 88 + file.write((uint8_t *)&_prefs.transport_mode, sizeof(_prefs.transport_mode)); // 89 + file.write((uint8_t *)_prefs.wifi_ssid, sizeof(_prefs.wifi_ssid)); // 90 + file.write((uint8_t *)_prefs.wifi_pwd, sizeof(_prefs.wifi_pwd)); // 123 + file.write((uint8_t *)&_prefs.wifi_mode, sizeof(_prefs.wifi_mode)); // 188 + file.write((uint8_t *)_prefs.wifi_ap_ssid, sizeof(_prefs.wifi_ap_ssid)); // 189 + file.write((uint8_t *)_prefs.wifi_ap_pwd, sizeof(_prefs.wifi_ap_pwd)); // 222 file.close(); } diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 2ca226781..747522cca 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2,6 +2,9 @@ #include // needed for PlatformIO #include +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) + #include +#endif #define CMD_APP_START 1 #define CMD_SEND_TXT_MSG 2 @@ -119,6 +122,30 @@ #define PUSH_CODE_CONTACT_DELETED 0x8F // used to notify client app of deleted contact when overwriting oldest #define PUSH_CODE_CONTACTS_FULL 0x90 // used to notify client app that contacts storage is full +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) +static bool applyTransportModePref(NodePrefs& prefs) { + auto* transport = CompanionTransportInterface::instance(); + if (!transport) return false; + + auto mode = (prefs.transport_mode == 1) + ? CompanionTransportInterface::WIRELESS_MODE_TCP + : CompanionTransportInterface::WIRELESS_MODE_BLE; + return transport->setWirelessMode(mode); +} + +static bool applyWifiPrefs(NodePrefs& prefs) { + auto* transport = CompanionTransportInterface::instance(); + if (!transport) return false; + + if (!transport->setWifiCredentials(prefs.wifi_ssid, prefs.wifi_pwd)) return false; + if (!transport->setWifiApCredentials(prefs.wifi_ap_ssid, prefs.wifi_ap_pwd)) return false; + auto wifi_mode = (prefs.wifi_mode == 1) + ? CompanionTransportInterface::WIFI_MODE_STA_CLIENT + : CompanionTransportInterface::WIFI_MODE_AP_ONLY; + return transport->setWifiMode(wifi_mode); +} +#endif + #define ERR_CODE_UNSUPPORTED_CMD 1 #define ERR_CODE_NOT_FOUND 2 #define ERR_CODE_TABLE_FULL 3 @@ -820,6 +847,12 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _prefs.tx_power_dbm = LORA_TX_POWER; _prefs.gps_enabled = 0; // GPS disabled by default _prefs.gps_interval = 0; // No automatic GPS updates by default + _prefs.transport_mode = 0; // BLE by default + _prefs.wifi_mode = 0; // WiFi AP mode by default + _prefs.wifi_ssid[0] = 0; + _prefs.wifi_pwd[0] = 0; + _prefs.wifi_ap_ssid[0] = 0; + _prefs.wifi_ap_pwd[0] = 0; //_prefs.rx_delay_base = 10.0f; enable once new algo fixed } @@ -859,6 +892,12 @@ void MyMesh::begin(bool has_display) { _prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, -9, MAX_LORA_TX_POWER); _prefs.gps_enabled = constrain(_prefs.gps_enabled, 0, 1); // Ensure boolean 0 or 1 _prefs.gps_interval = constrain(_prefs.gps_interval, 0, 86400); // Max 24 hours + _prefs.transport_mode = constrain(_prefs.transport_mode, 0, 1); + _prefs.wifi_mode = constrain(_prefs.wifi_mode, 0, 1); + _prefs.wifi_ssid[sizeof(_prefs.wifi_ssid) - 1] = 0; + _prefs.wifi_pwd[sizeof(_prefs.wifi_pwd) - 1] = 0; + _prefs.wifi_ap_ssid[sizeof(_prefs.wifi_ap_ssid) - 1] = 0; + _prefs.wifi_ap_pwd[sizeof(_prefs.wifi_ap_pwd) - 1] = 0; #ifdef BLE_PIN_CODE // 123456 by default if (_prefs.ble_pin == 0) { @@ -1667,7 +1706,60 @@ void MyMesh::handleCmdFrame(size_t len) { char *np = strchr(sp, ':'); // look for separator char if (np) { *np++ = 0; // modify 'cmd_frame', replace ':' with null - bool success = sensors.setSettingValue(sp, np); + bool success = false; + +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) + if (strcmp(sp, "transport") == 0) { + if (strcmp(np, "ble") == 0) { + _prefs.transport_mode = 0; + success = applyTransportModePref(_prefs); + } else if (strcmp(np, "tcp") == 0 || strcmp(np, "wifi") == 0) { + _prefs.transport_mode = 1; + success = applyTransportModePref(_prefs); + } + if (success) { + savePrefs(); + } + } else if (strcmp(sp, "wifi.ssid") == 0) { + if (strlen(np) < sizeof(_prefs.wifi_ssid)) { + StrHelper::strncpy(_prefs.wifi_ssid, np, sizeof(_prefs.wifi_ssid)); + success = applyWifiPrefs(_prefs); + if (success) savePrefs(); + } + } else if (strcmp(sp, "wifi.pwd") == 0) { + if (strlen(np) < sizeof(_prefs.wifi_pwd)) { + StrHelper::strncpy(_prefs.wifi_pwd, np, sizeof(_prefs.wifi_pwd)); + success = applyWifiPrefs(_prefs); + if (success) savePrefs(); + } + } else if (strcmp(sp, "wifi.mode") == 0) { + if (strcmp(np, "ap") == 0) { + _prefs.wifi_mode = 0; + success = applyWifiPrefs(_prefs); + if (success) savePrefs(); + } else if (strcmp(np, "client") == 0 || strcmp(np, "sta") == 0) { + _prefs.wifi_mode = 1; + success = applyWifiPrefs(_prefs); + if (success) savePrefs(); + } + } else if (strcmp(sp, "wifi.ap.ssid") == 0) { + if (strlen(np) < sizeof(_prefs.wifi_ap_ssid)) { + StrHelper::strncpy(_prefs.wifi_ap_ssid, np, sizeof(_prefs.wifi_ap_ssid)); + success = applyWifiPrefs(_prefs); + if (success) savePrefs(); + } + } else if (strcmp(sp, "wifi.ap.pwd") == 0) { + if (strlen(np) < sizeof(_prefs.wifi_ap_pwd)) { + StrHelper::strncpy(_prefs.wifi_ap_pwd, np, sizeof(_prefs.wifi_ap_pwd)); + success = applyWifiPrefs(_prefs); + if (success) savePrefs(); + } + } +#endif + + if (!success) { + success = sensors.setSettingValue(sp, np); + } if (success) { #if ENV_INCLUDE_GPS == 1 // Update node preferences for GPS settings @@ -1844,9 +1936,123 @@ void MyMesh::checkCLIRescueCmd() { _prefs.ble_pin = atoi(&config[4]); savePrefs(); Serial.printf(" > pin is now %06d\n", _prefs.ble_pin); +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) + } else if (memcmp(config, "transport ", 10) == 0) { + const char* mode = &config[10]; + if (strcmp(mode, "ble") == 0) { + _prefs.transport_mode = 0; + if (applyTransportModePref(_prefs)) { + savePrefs(); + Serial.println(" > wireless transport is now BLE"); + } else { + Serial.println(" Error: failed to switch transport"); + } + } else if (strcmp(mode, "tcp") == 0 || strcmp(mode, "wifi") == 0) { + _prefs.transport_mode = 1; + if (applyTransportModePref(_prefs)) { + savePrefs(); + Serial.println(" > wireless transport is now TCP/WiFi"); + } else { + Serial.println(" Error: failed to switch transport"); + } + } else { + Serial.println(" Error: usage set transport ble|tcp"); + } + } else if (memcmp(config, "wifi.ssid ", 10) == 0) { + const char* ssid = &config[10]; + if (strlen(ssid) < sizeof(_prefs.wifi_ssid)) { + StrHelper::strncpy(_prefs.wifi_ssid, ssid, sizeof(_prefs.wifi_ssid)); + if (applyWifiPrefs(_prefs)) { + savePrefs(); + Serial.println(" > wifi ssid updated"); + } else { + Serial.println(" Error: failed to set wifi config"); + } + } else { + Serial.println(" Error: ssid too long"); + } + } else if (memcmp(config, "wifi.pwd ", 9) == 0) { + const char* pwd = &config[9]; + if (strlen(pwd) < sizeof(_prefs.wifi_pwd)) { + StrHelper::strncpy(_prefs.wifi_pwd, pwd, sizeof(_prefs.wifi_pwd)); + if (applyWifiPrefs(_prefs)) { + savePrefs(); + Serial.println(" > wifi password updated"); + } else { + Serial.println(" Error: failed to set wifi config"); + } + } else { + Serial.println(" Error: password too long"); + } + } else if (memcmp(config, "wifi.mode ", 10) == 0) { + const char* mode = &config[10]; + if (strcmp(mode, "ap") == 0) { + _prefs.wifi_mode = 0; + if (applyWifiPrefs(_prefs)) { + savePrefs(); + Serial.println(" > wifi mode is now AP"); + } else { + Serial.println(" Error: failed to set wifi mode"); + } + } else if (strcmp(mode, "client") == 0 || strcmp(mode, "sta") == 0) { + _prefs.wifi_mode = 1; + if (applyWifiPrefs(_prefs)) { + savePrefs(); + Serial.println(" > wifi mode is now CLIENT"); + } else { + Serial.println(" Error: failed to set wifi mode"); + } + } else { + Serial.println(" Error: usage set wifi.mode ap|client"); + } + } else if (memcmp(config, "wifi.ap.ssid ", 13) == 0) { + const char* ssid = &config[13]; + if (strlen(ssid) < sizeof(_prefs.wifi_ap_ssid)) { + StrHelper::strncpy(_prefs.wifi_ap_ssid, ssid, sizeof(_prefs.wifi_ap_ssid)); + if (applyWifiPrefs(_prefs)) { + savePrefs(); + Serial.println(" > wifi ap ssid updated"); + } else { + Serial.println(" Error: failed to set wifi config"); + } + } else { + Serial.println(" Error: ssid too long"); + } + } else if (memcmp(config, "wifi.ap.pwd ", 12) == 0) { + const char* pwd = &config[12]; + if (strlen(pwd) < sizeof(_prefs.wifi_ap_pwd)) { + StrHelper::strncpy(_prefs.wifi_ap_pwd, pwd, sizeof(_prefs.wifi_ap_pwd)); + if (applyWifiPrefs(_prefs)) { + savePrefs(); + Serial.println(" > wifi ap password updated"); + } else { + Serial.println(" Error: failed to set wifi config (password must be empty or >= 8 chars)"); + } + } else { + Serial.println(" Error: password too long"); + } +#endif } else { Serial.printf(" Error: unknown config: %s\n", config); } +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) + } else if (strcmp(cli_command, "wifi connect") == 0) { + _prefs.transport_mode = 1; + if (applyTransportModePref(_prefs)) { + savePrefs(); + Serial.println(" > switched to TCP/WiFi transport"); + } else { + Serial.println(" Error: failed to enable TCP/WiFi"); + } + } else if (strcmp(cli_command, "ble connect") == 0) { + _prefs.transport_mode = 0; + if (applyTransportModePref(_prefs)) { + savePrefs(); + Serial.println(" > switched to BLE transport"); + } else { + Serial.println(" Error: failed to enable BLE"); + } +#endif } else if (strcmp(cli_command, "rebuild") == 0) { bool success = _store->formatFileSystem(); if (success) { diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 090209c1d..7ef3ef666 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -31,4 +31,10 @@ struct NodePrefs { // persisted to file uint8_t client_repeat; uint8_t path_hash_mode; // which path mode to use when sending uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) -}; \ No newline at end of file + uint8_t transport_mode; // 0 = BLE, 1 = TCP/WiFi + char wifi_ssid[33]; + char wifi_pwd[65]; + uint8_t wifi_mode; // 0 = AP, 1 = Client/STA + char wifi_ap_ssid[33]; + char wifi_ap_pwd[65]; +}; diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index eff9efca4..02e5c5c4f 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -35,7 +35,16 @@ static uint32_t _atoi(const char* sp) { #endif #ifdef ESP32 - #ifdef WIFI_SSID + #if defined(COMPANION_ALL_TRANSPORTS) + #include + CompanionTransportInterface serial_interface; + #ifdef SERIAL_RX + HardwareSerial companion_serial(1); + #endif + #ifndef TCP_PORT + #define TCP_PORT 5000 + #endif + #elif defined(WIFI_SSID) #include SerialWifiInterface serial_interface; #ifndef TCP_PORT @@ -193,7 +202,29 @@ void setup() { #endif ); -#ifdef WIFI_SSID +#if defined(COMPANION_ALL_TRANSPORTS) + board.setInhibitSleep(true); // transport manager will keep one wireless stack active + #if defined(SERIAL_RX) + companion_serial.setPins(SERIAL_RX, SERIAL_TX); + companion_serial.begin(115200); + serial_interface.begin(companion_serial, BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin(), TCP_PORT); + #else + serial_interface.begin(Serial, BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin(), TCP_PORT); + #endif + serial_interface.setWifiCredentials(the_mesh.getNodePrefs()->wifi_ssid, the_mesh.getNodePrefs()->wifi_pwd); + serial_interface.setWifiApCredentials(the_mesh.getNodePrefs()->wifi_ap_ssid, the_mesh.getNodePrefs()->wifi_ap_pwd); + serial_interface.setWifiMode( + the_mesh.getNodePrefs()->wifi_mode == 1 + ? CompanionTransportInterface::WIFI_MODE_STA_CLIENT + : CompanionTransportInterface::WIFI_MODE_AP_ONLY + ); + serial_interface.setWirelessMode( + the_mesh.getNodePrefs()->transport_mode == 1 + ? CompanionTransportInterface::WIRELESS_MODE_TCP + : CompanionTransportInterface::WIRELESS_MODE_BLE, + true + ); +#elif defined(WIFI_SSID) board.setInhibitSleep(true); // prevent sleep when WiFi is active WiFi.begin(WIFI_SSID, WIFI_PWD); serial_interface.begin(TCP_PORT); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 265532be0..976d7ff56 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -2,6 +2,12 @@ #include #include "../MyMesh.h" #include "target.h" +#if defined(ESP32) + #include +#endif +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) + #include +#endif #ifdef WIFI_SSID #include #endif @@ -77,18 +83,19 @@ class SplashScreen : public UIScreen { class HomeScreen : public UIScreen { enum HomePage { - FIRST, - RECENT, - RADIO, - BLUETOOTH, - ADVERT, + STATUS, + RECENT_ADVERTS, + RADIO_INFO, + BLE_TRANSPORT, + WIFI_TRANSPORT, + ADVERT_ACTION, #if ENV_INCLUDE_GPS == 1 - GPS, + GPS_STATUS, #endif #if UI_SENSORS_PAGE == 1 - SENSORS, + SENSOR_VALUES, #endif - SHUTDOWN, + POWER, Count // keep as last }; @@ -100,6 +107,35 @@ class HomeScreen : public UIScreen { bool _shutdown_init; AdvertPath recent[UI_RECENT_LIST_SIZE]; + bool isPageVisible(HomePage page) const { + if (page == HomePage::BLE_TRANSPORT) return _node_prefs->transport_mode == 0; + if (page == HomePage::WIFI_TRANSPORT) return _node_prefs->transport_mode == 1; + return true; + } + + void ensureVisiblePage() { + if (isPageVisible((HomePage)_page)) return; + for (int i = 0; i < HomePage::Count; i++) { + uint8_t candidate = (_page + i) % HomePage::Count; + if (isPageVisible((HomePage)candidate)) { + _page = candidate; + return; + } + } + } + + uint8_t nextVisiblePage(int dir) const { + for (int i = 1; i <= HomePage::Count; i++) { + int candidate = (int)_page + (dir * i); + while (candidate < 0) candidate += HomePage::Count; + candidate %= HomePage::Count; + if (isPageVisible((HomePage)candidate)) { + return (uint8_t)candidate; + } + } + return _page; + } + void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) { // Convert millivolts to percentage @@ -180,6 +216,7 @@ class HomeScreen : public UIScreen { } int render(DisplayDriver& display) override { + ensureVisiblePage(); char tmp[80]; // node name display.setTextSize(1); @@ -203,7 +240,7 @@ class HomeScreen : public UIScreen { } } - if (_page == HomePage::FIRST) { + if (_page == HomePage::STATUS) { display.setColor(DisplayDriver::YELLOW); display.setTextSize(2); sprintf(tmp, "MSG: %d", _task->getMsgCount()); @@ -226,7 +263,7 @@ class HomeScreen : public UIScreen { sprintf(tmp, "Pin:%d", the_mesh.getBLEPin()); display.drawTextCentered(display.width() / 2, 43, tmp); } - } else if (_page == HomePage::RECENT) { + } else if (_page == HomePage::RECENT_ADVERTS) { the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE); display.setColor(DisplayDriver::GREEN); int y = 20; @@ -251,7 +288,7 @@ class HomeScreen : public UIScreen { display.setCursor(display.width() - timestamp_width - 1, y); display.print(tmp); } - } else if (_page == HomePage::RADIO) { + } else if (_page == HomePage::RADIO_INFO) { display.setColor(DisplayDriver::YELLOW); display.setTextSize(1); // freq / sf @@ -270,19 +307,57 @@ class HomeScreen : public UIScreen { display.setCursor(0, 53); sprintf(tmp, "Noise floor: %d", radio_driver.getNoiseFloor()); display.print(tmp); - } else if (_page == HomePage::BLUETOOTH) { + } else if (_page == HomePage::BLE_TRANSPORT) { display.setColor(DisplayDriver::GREEN); - display.drawXbm((display.width() - 32) / 2, 18, + display.drawTextCentered(display.width() / 2, 20, "BLE transport"); + display.drawXbm((display.width() - 32) / 2, 28, _task->isSerialEnabled() ? bluetooth_on : bluetooth_off, 32, 32); display.setTextSize(1); - display.drawTextCentered(display.width() / 2, 64 - 11, "toggle: " PRESS_LABEL); - } else if (_page == HomePage::ADVERT) { + display.drawTextCentered(display.width() / 2, 64 - 11, "Long press: WiFi"); + } else if (_page == HomePage::WIFI_TRANSPORT) { + display.setColor(DisplayDriver::GREEN); + display.setTextSize(1); + display.drawTextCentered(display.width() / 2, 20, "WiFi transport"); + if (_node_prefs->wifi_mode == 0) { + if (_node_prefs->wifi_ap_ssid[0]) { + snprintf(tmp, sizeof(tmp), "AP:%s", _node_prefs->wifi_ap_ssid); + } else { + snprintf(tmp, sizeof(tmp), "AP:MeshCore-%s", _node_prefs->node_name); + } + display.drawTextCentered(display.width() / 2, 31, tmp); +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) + IPAddress ap_ip = WiFi.softAPIP(); + snprintf(tmp, sizeof(tmp), "IP:%d.%d.%d.%d", ap_ip[0], ap_ip[1], ap_ip[2], ap_ip[3]); +#else + snprintf(tmp, sizeof(tmp), "IP:n/a"); +#endif + display.drawTextCentered(display.width() / 2, 42, tmp); + const char* ap_pwd = _node_prefs->wifi_ap_pwd[0] ? _node_prefs->wifi_ap_pwd : ""; + snprintf(tmp, sizeof(tmp), "PW:%s", ap_pwd); + display.drawTextCentered(display.width() / 2, 53, tmp); + } else { + display.drawTextCentered(display.width() / 2, 31, "Mode: Client"); + snprintf(tmp, sizeof(tmp), "SSID:%s", _node_prefs->wifi_ssid[0] ? _node_prefs->wifi_ssid : ""); + display.drawTextCentered(display.width() / 2, 42, tmp); +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) + if (WiFi.status() == WL_CONNECTED) { + IPAddress ip = WiFi.localIP(); + snprintf(tmp, sizeof(tmp), "IP:%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); + } else { + snprintf(tmp, sizeof(tmp), "IP: "); + } +#else + snprintf(tmp, sizeof(tmp), "IP: n/a"); +#endif + display.drawTextCentered(display.width() / 2, 53, tmp); + } + } else if (_page == HomePage::ADVERT_ACTION) { display.setColor(DisplayDriver::GREEN); display.drawXbm((display.width() - 32) / 2, 18, advert_icon, 32, 32); display.drawTextCentered(display.width() / 2, 64 - 11, "advert: " PRESS_LABEL); #if ENV_INCLUDE_GPS == 1 - } else if (_page == HomePage::GPS) { + } else if (_page == HomePage::GPS_STATUS) { LocationProvider* nmea = sensors.getLocationProvider(); char buf[50]; int y = 18; @@ -321,7 +396,7 @@ class HomeScreen : public UIScreen { } #endif #if UI_SENSORS_PAGE == 1 - } else if (_page == HomePage::SENSORS) { + } else if (_page == HomePage::SENSOR_VALUES) { int y = 18; refresh_sensors(); char buf[30]; @@ -392,7 +467,7 @@ class HomeScreen : public UIScreen { if (sensors_scroll) sensors_scroll_offset = (sensors_scroll_offset+1)%sensors_nb; else sensors_scroll_offset = 0; #endif - } else if (_page == HomePage::SHUTDOWN) { + } else if (_page == HomePage::POWER) { display.setColor(DisplayDriver::GREEN); display.setTextSize(1); if (_shutdown_init) { @@ -406,18 +481,36 @@ class HomeScreen : public UIScreen { } bool handleInput(char c) override { + ensureVisiblePage(); if (c == KEY_LEFT || c == KEY_PREV) { - _page = (_page + HomePage::Count - 1) % HomePage::Count; + _page = nextVisiblePage(-1); return true; } if (c == KEY_NEXT || c == KEY_RIGHT) { - _page = (_page + 1) % HomePage::Count; - if (_page == HomePage::RECENT) { + _page = nextVisiblePage(1); + if (_page == HomePage::RECENT_ADVERTS) { _task->showAlert("Recent adverts", 800); } return true; } - if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) { + if (c == KEY_ENTER && _page == HomePage::BLE_TRANSPORT) { +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) + if (_task->consumeLongPress()) { + bool success = false; + if (auto* transport = CompanionTransportInterface::instance()) { + success = transport->setWirelessMode(CompanionTransportInterface::WIRELESS_MODE_TCP); + if (success) _node_prefs->transport_mode = 1; + } + if (success) { + the_mesh.savePrefs(); + _task->notify(UIEventType::ack); + _task->showAlert("WiFi enabled", 1200); + } else { + _task->showAlert("Transport switch failed", 1200); + } + return true; + } +#endif if (_task->isSerialEnabled()) { // toggle Bluetooth on/off _task->disableSerial(); } else { @@ -425,7 +518,26 @@ class HomeScreen : public UIScreen { } return true; } - if (c == KEY_ENTER && _page == HomePage::ADVERT) { + if (c == KEY_ENTER && _page == HomePage::WIFI_TRANSPORT) { +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) + if (_task->consumeLongPress()) { + bool success = false; + if (auto* transport = CompanionTransportInterface::instance()) { + success = transport->setWirelessMode(CompanionTransportInterface::WIRELESS_MODE_BLE); + if (success) _node_prefs->transport_mode = 0; + } + if (success) { + the_mesh.savePrefs(); + _task->notify(UIEventType::ack); + _task->showAlert("BLE enabled", 1200); + } else { + _task->showAlert("Transport switch failed", 1200); + } + } +#endif + return true; + } + if (c == KEY_ENTER && _page == HomePage::ADVERT_ACTION) { _task->notify(UIEventType::ack); if (the_mesh.advert()) { _task->showAlert("Advert sent!", 1000); @@ -435,19 +547,19 @@ class HomeScreen : public UIScreen { return true; } #if ENV_INCLUDE_GPS == 1 - if (c == KEY_ENTER && _page == HomePage::GPS) { + if (c == KEY_ENTER && _page == HomePage::GPS_STATUS) { _task->toggleGPS(); return true; } #endif #if UI_SENSORS_PAGE == 1 - if (c == KEY_ENTER && _page == HomePage::SENSORS) { + if (c == KEY_ENTER && _page == HomePage::SENSOR_VALUES) { _task->toggleGPS(); next_sensors_refresh=0; return true; } #endif - if (c == KEY_ENTER && _page == HomePage::SHUTDOWN) { + if (c == KEY_ENTER && _page == HomePage::POWER) { _shutdown_init = true; // need to wait for button to be released return true; } @@ -559,6 +671,8 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no #endif _node_prefs = node_prefs; + _last_transport_mode = _node_prefs ? _node_prefs->transport_mode : 255; + _last_wifi_mode = _node_prefs ? _node_prefs->wifi_mode : 255; #if ENV_INCLUDE_GPS == 1 // Apply GPS preferences from stored prefs @@ -716,6 +830,15 @@ bool UITask::isButtonPressed() const { void UITask::loop() { char c = 0; + + if (_node_prefs != NULL) { + if (_node_prefs->transport_mode != _last_transport_mode || _node_prefs->wifi_mode != _last_wifi_mode) { + _last_transport_mode = _node_prefs->transport_mode; + _last_wifi_mode = _node_prefs->wifi_mode; + _next_refresh = 0; // immediate redraw after remote command changes + } + } + #if UI_HAS_JOYSTICK int ev = user_btn.check(); if (ev == BUTTON_EVENT_CLICK) { @@ -780,6 +903,7 @@ void UITask::loop() { if (c != 0 && curr) { curr->handleInput(c); + _pending_long_press = false; // one-shot flag _auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer _next_refresh = 100; // trigger refresh } @@ -864,6 +988,8 @@ char UITask::handleLongPress(char c) { if (millis() - ui_started_at < 8000) { // long press in first 8 seconds since startup -> CLI/rescue the_mesh.enterCLIRescue(); c = 0; // consume event + } else { + _pending_long_press = true; } return c; } diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index a77ad6e7e..906c6ebb0 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -32,6 +32,9 @@ class UITask : public AbstractUITask { GenericVibration vibration; #endif unsigned long _next_refresh, _auto_off; + bool _pending_long_press; + uint8_t _last_transport_mode; + uint8_t _last_wifi_mode; NodePrefs* _node_prefs; char _alert[80]; unsigned long _alert_expiry; @@ -68,6 +71,9 @@ class UITask : public AbstractUITask { UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) { next_batt_chck = _next_refresh = 0; ui_started_at = 0; + _pending_long_press = false; + _last_transport_mode = 255; + _last_wifi_mode = 255; curr = NULL; } void begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs); @@ -77,6 +83,7 @@ class UITask : public AbstractUITask { int getMsgCount() const { return _msgcount; } bool hasDisplay() const { return _display != NULL; } bool isButtonPressed() const; + bool consumeLongPress() { bool v = _pending_long_press; _pending_long_press = false; return v; } bool isBuzzerQuiet() { #ifdef PIN_BUZZER diff --git a/src/helpers/esp32/CompanionTransportInterface.cpp b/src/helpers/esp32/CompanionTransportInterface.cpp new file mode 100644 index 000000000..3940b2590 --- /dev/null +++ b/src/helpers/esp32/CompanionTransportInterface.cpp @@ -0,0 +1,204 @@ +#include "CompanionTransportInterface.h" +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) +#include +#include + +CompanionTransportInterface* CompanionTransportInterface::_instance = nullptr; + +CompanionTransportInterface::CompanionTransportInterface() + : _isEnabled(false), _wifi_server_started(false), _last_source(0), _mode(WIRELESS_MODE_BLE), _wifi_started(false), + _wifi_mode(WIFI_MODE_AP_ONLY), _tcp_port(5000) { + _node_name[0] = 0; + _wifi_ssid[0] = 0; + _wifi_pwd[0] = 0; + _wifi_ap_ssid[0] = 0; + _wifi_ap_pwd[0] = 0; + _instance = this; +} + +void CompanionTransportInterface::begin(Stream& usb_serial, const char* ble_prefix, char* node_name, uint32_t ble_pin, uint16_t tcp_port) { + _tcp_port = tcp_port; + snprintf(_node_name, sizeof(_node_name), "%s", node_name); + + _usb.begin(usb_serial); + _ble.begin(ble_prefix, node_name, ble_pin); +} + +void CompanionTransportInterface::begin(HardwareSerial& usb_serial, const char* ble_prefix, char* node_name, uint32_t ble_pin, uint16_t tcp_port) { + begin((Stream&)usb_serial, ble_prefix, node_name, ble_pin, tcp_port); +} + +void CompanionTransportInterface::connectWifi() { + WiFi.mode(WIFI_STA); + WiFi.begin(_wifi_ssid, _wifi_pwd); +} + +void CompanionTransportInterface::startWifiAP() { + WiFi.mode(WIFI_AP); + char ap_name[33] = {0}; + if (_wifi_ap_ssid[0] != 0) { + snprintf(ap_name, sizeof(ap_name), "%s", _wifi_ap_ssid); + } else { + snprintf(ap_name, sizeof(ap_name), "MeshCore-%s", _node_name); + } + + if (_wifi_ap_pwd[0] != 0 && strlen(_wifi_ap_pwd) >= 8) { + WiFi.softAP(ap_name, _wifi_ap_pwd); + } else { + WiFi.softAP(ap_name); + } +} + +void CompanionTransportInterface::startWifi() { + _wifi_started = true; + if (_wifi_mode == WIFI_MODE_STA_CLIENT) { + connectWifi(); + } else { + startWifiAP(); + } +} + +void CompanionTransportInterface::stopWifi() { + if (!_wifi_started) return; + WiFi.disconnect(true, true); + WiFi.mode(WIFI_OFF); + _wifi_started = false; +} + +void CompanionTransportInterface::startWireless() { + if (_mode == WIRELESS_MODE_TCP) { + startWifi(); + if (!_wifi_server_started) { + _wifi.begin(_tcp_port); + _wifi_server_started = true; + } + _wifi.enable(); + _ble.disable(); + } else { + _wifi.disable(); + stopWifi(); + _ble.enable(); + } +} + +void CompanionTransportInterface::stopWireless() { + _ble.disable(); + _wifi.disable(); + stopWifi(); +} + +bool CompanionTransportInterface::setWirelessMode(WirelessMode mode, bool persist_only) { + if (mode != WIRELESS_MODE_BLE && mode != WIRELESS_MODE_TCP) { + return false; + } + _mode = mode; + if (_isEnabled && !persist_only) { + startWireless(); + } + return true; +} + +bool CompanionTransportInterface::setWifiCredentials(const char* ssid, const char* pwd) { + if (!ssid || !pwd) return false; + if (strlen(ssid) > 32 || strlen(pwd) > 64) return false; + snprintf(_wifi_ssid, sizeof(_wifi_ssid), "%s", ssid); + snprintf(_wifi_pwd, sizeof(_wifi_pwd), "%s", pwd); + + if (_isEnabled && _mode == WIRELESS_MODE_TCP && _wifi_mode == WIFI_MODE_STA_CLIENT) { + startWifi(); + } + return true; +} + +bool CompanionTransportInterface::setWifiMode(WifiMode mode) { + if (mode != WIFI_MODE_AP_ONLY && mode != WIFI_MODE_STA_CLIENT) { + return false; + } + _wifi_mode = mode; + if (_isEnabled && _mode == WIRELESS_MODE_TCP) { + startWifi(); + } + return true; +} + +bool CompanionTransportInterface::setWifiApCredentials(const char* ssid, const char* pwd) { + if (!ssid || !pwd) return false; + if (strlen(ssid) > 32 || strlen(pwd) > 64) return false; + if (pwd[0] != 0 && strlen(pwd) < 8) return false; // WPA2 requirement + snprintf(_wifi_ap_ssid, sizeof(_wifi_ap_ssid), "%s", ssid); + snprintf(_wifi_ap_pwd, sizeof(_wifi_ap_pwd), "%s", pwd); + + if (_isEnabled && _mode == WIRELESS_MODE_TCP && _wifi_mode == WIFI_MODE_AP_ONLY) { + startWifi(); + } + return true; +} + +void CompanionTransportInterface::enable() { + if (_isEnabled) return; + _isEnabled = true; + _last_source = 0; + _usb.enable(); + startWireless(); +} + +void CompanionTransportInterface::disable() { + _isEnabled = false; + stopWireless(); + _usb.disable(); +} + +bool CompanionTransportInterface::isConnected() const { + if (_last_source == 2) return _wifi.isConnected(); + if (_last_source == 1) return _ble.isConnected(); + if (_last_source == 0) return _usb.isConnected(); + return _usb.isConnected() || _ble.isConnected() || _wifi.isConnected(); +} + +bool CompanionTransportInterface::isWriteBusy() const { + if (_last_source == 2) return _wifi.isWriteBusy(); + if (_last_source == 1) return _ble.isWriteBusy(); + return _usb.isWriteBusy(); +} + +size_t CompanionTransportInterface::writeFrame(const uint8_t src[], size_t len) { + if (_last_source == 2 && _wifi.isConnected()) return _wifi.writeFrame(src, len); + if (_last_source == 1 && _ble.isConnected()) return _ble.writeFrame(src, len); + return _usb.writeFrame(src, len); +} + +size_t CompanionTransportInterface::checkRecvFrame(uint8_t dest[]) { + if (_mode == WIRELESS_MODE_TCP) { + static unsigned long last_wifi_retry_at = 0; + if (_wifi_mode == WIFI_MODE_STA_CLIENT && _wifi_ssid[0] != 0 && WiFi.status() != WL_CONNECTED) { + unsigned long now = millis(); + if (last_wifi_retry_at == 0 || (unsigned long)(now - last_wifi_retry_at) >= 10000) { + // Keep trying to reconnect in STA mode, but avoid tight reconnect loops. + WiFi.disconnect(); + connectWifi(); + last_wifi_retry_at = now; + } + } + + size_t wifi_len = _wifi.checkRecvFrame(dest); + if (wifi_len > 0) { + _last_source = 2; + return wifi_len; + } + } else { + size_t ble_len = _ble.checkRecvFrame(dest); + if (ble_len > 0) { + _last_source = 1; + return ble_len; + } + } + + size_t usb_len = _usb.checkRecvFrame(dest); + if (usb_len > 0) { + _last_source = 0; + return usb_len; + } + return 0; +} + +#endif diff --git a/src/helpers/esp32/CompanionTransportInterface.h b/src/helpers/esp32/CompanionTransportInterface.h new file mode 100644 index 000000000..6daf193c3 --- /dev/null +++ b/src/helpers/esp32/CompanionTransportInterface.h @@ -0,0 +1,80 @@ +#pragma once + +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) + +#include +#include "../BaseSerialInterface.h" +#include "../ArduinoSerialInterface.h" +#include "SerialBLEInterface.h" +#include "SerialWifiInterface.h" + +class CompanionTransportInterface : public BaseSerialInterface { +public: + enum WirelessMode : uint8_t { + WIRELESS_MODE_BLE = 0, + WIRELESS_MODE_TCP = 1 + }; + enum WifiMode : uint8_t { + WIFI_MODE_AP_ONLY = 0, + WIFI_MODE_STA_CLIENT = 1 + }; + +private: + static CompanionTransportInterface* _instance; + + ArduinoSerialInterface _usb; + SerialBLEInterface _ble; + SerialWifiInterface _wifi; + + bool _isEnabled; + bool _wifi_server_started; + uint8_t _last_source; // 0=usb,1=ble,2=wifi + WirelessMode _mode; + bool _wifi_started; + + char _node_name[33]; + char _wifi_ssid[33]; + char _wifi_pwd[65]; + WifiMode _wifi_mode; + char _wifi_ap_ssid[33]; + char _wifi_ap_pwd[65]; + uint16_t _tcp_port; + +public: + CompanionTransportInterface(); + + static CompanionTransportInterface* instance() { return _instance; } + + void begin(Stream& usb_serial, const char* ble_prefix, char* node_name, uint32_t ble_pin, uint16_t tcp_port); + void begin(HardwareSerial& usb_serial, const char* ble_prefix, char* node_name, uint32_t ble_pin, uint16_t tcp_port); + + bool setWirelessMode(WirelessMode mode, bool persist_only = false); + WirelessMode getWirelessMode() const { return _mode; } + + bool setWifiCredentials(const char* ssid, const char* pwd); + bool setWifiMode(WifiMode mode); + WifiMode getWifiMode() const { return _wifi_mode; } + bool setWifiApCredentials(const char* ssid, const char* pwd); + const char* getWifiSSID() const { return _wifi_ssid; } + const char* getWifiApSSID() const { return _wifi_ap_ssid; } + const char* getWifiApPassword() const { return _wifi_ap_pwd; } + bool hasWifiCredentials() const { return _wifi_ssid[0] != 0; } + + void enable() override; + void disable() override; + bool isEnabled() const override { return _isEnabled; } + bool isConnected() const override; + bool isWriteBusy() const override; + size_t writeFrame(const uint8_t src[], size_t len) override; + size_t checkRecvFrame(uint8_t dest[]) override; + +private: + void startWireless(); + void stopWireless(); + void startWifi(); + void stopWifi(); + void connectWifi(); + void startWifiAP(); +}; + +#endif diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index 4d299104e..5cc8e02a0 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -202,6 +202,29 @@ lib_deps = ${Heltec_lora32_v3.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:Heltec_v3_companion_radio_tcp_usb_ble] +extends = Heltec_lora32_v3 +build_flags = + ${Heltec_lora32_v3.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=SSD1306Display + -D BLE_PIN_CODE=123456 + -D COMPANION_ALL_TRANSPORTS + -D OFFLINE_QUEUE_SIZE=256 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_lora32_v3.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_lora32_v3.lib_deps} + densaugeo/base64 @ ~1.4.0 + [env:Heltec_v3_sensor] extends = Heltec_lora32_v3 build_flags = @@ -375,4 +398,4 @@ build_flags = build_src_filter = ${Heltec_lora32_v3.build_src_filter} +<../examples/kiss_modem/> lib_deps = - ${Heltec_lora32_v3.lib_deps} \ No newline at end of file + ${Heltec_lora32_v3.lib_deps} From 43712e74687936929297894945febd110151eafd Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Sun, 8 Mar 2026 14:47:34 -0400 Subject: [PATCH 10/20] update commands --- build.sh | 1 + docs/wifi_transport_command_examples.md | 109 +++++++++++++++++++++--- examples/companion_radio/MyMesh.cpp | 45 +++++++++- 3 files changed, 138 insertions(+), 17 deletions(-) diff --git a/build.sh b/build.sh index 313c4c47a..ff0ff3970 100755 --- a/build.sh +++ b/build.sh @@ -225,6 +225,7 @@ build_companion_firmwares() { # build all companion firmwares build_all_firmwares_by_suffix "_companion_radio_usb" build_all_firmwares_by_suffix "_companion_radio_ble" + build_all_firmwares_by_suffix "_companion_radio_tcp_usb_ble" } diff --git a/docs/wifi_transport_command_examples.md b/docs/wifi_transport_command_examples.md index 4ecf43a80..f7aa268c6 100644 --- a/docs/wifi_transport_command_examples.md +++ b/docs/wifi_transport_command_examples.md @@ -60,12 +60,12 @@ def send_custom_var(port, kv): ```python send_custom_var("/dev/ttyUSB0", "transport:wifi") send_custom_var("/dev/ttyUSB0", "wifi.mode:ap") -send_custom_var("/dev/ttyUSB0", "wifi.ap.ssid:MeshCore-TestAP") -send_custom_var("/dev/ttyUSB0", "wifi.ap.pwd:testpass123") +send_custom_var("/dev/ttyUSB0", "wifi.ap.ssid:MeshCore-SampleAP") +send_custom_var("/dev/ttyUSB0", "wifi.ap.pwd:SampleAPPwd123") send_custom_var("/dev/ttyUSB0", "wifi.mode:client") -send_custom_var("/dev/ttyUSB0", "wifi.ssid:YourSSID") -send_custom_var("/dev/ttyUSB0", "wifi.pwd:YourPassword") +send_custom_var("/dev/ttyUSB0", "wifi.ssid:SampleSSID") +send_custom_var("/dev/ttyUSB0", "wifi.pwd:SamplePwd123") send_custom_var("/dev/ttyUSB0", "transport:ble") ``` @@ -96,31 +96,114 @@ Then set values: ```bash meshcore-cli -s /dev/ttyUSB0 -b 115200 set transport tcp meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.mode ap -meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.ap.ssid MeshCore-TestAP -meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.ap.pwd testpass123 +meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.ap.ssid MeshCore-SampleAP +meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.ap.pwd SampleAPPwd123 meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.mode client -meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.ssid YourSSID -meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.pwd YourPassword +meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.ssid SampleSSID +meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.pwd SamplePwd123 meshcore-cli -s /dev/ttyUSB0 -b 115200 set transport ble + +Read back current transport/WiFi values: + +```bash +meshcore-cli -s /dev/ttyUSB0 -b 115200 get transport +meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.mode +meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.ssid +meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.pwd +meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.ap.ssid +meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.ap.pwd +``` + +### 3.2 meshcli interactive mode (recommended for manual testing) + +Start interactive mode: + +```bash +meshcore-cli -s /dev/ttyUSB0 -b 115200 +``` + +Example interactive session: + +```text +KD3CGK|* set wifi.mode client +Var wifi.mode set to client +KD3CGK|* get wifi.mode +client +KD3CGK|* set transport ble +Var transport set to ble +KD3CGK|* get transport +ble +KD3CGK|* get wifi.ssid +SampleSSID +KD3CGK|* get wifi.ap.ssid +MeshCore-SampleAP +KD3CGK|* get wifi.pwd +SamplePwd123 +KD3CGK|* get wifi.ap.pwd +SampleAPPwd123 +``` + +### 3.2.1 Full sample test sequence + +Run this exact command sequence to validate WiFi/transport set+get behavior with sample values: + +```bash +meshcore-cli -s /dev/ttyUSB0 -b 115200 infos +meshcore-cli -s /dev/ttyUSB0 -b 115200 set transport ble +meshcore-cli -s /dev/ttyUSB0 -b 115200 get transport + +meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.mode ap +meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.mode +meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.ap.ssid MeshCore-SampleAP +meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.ap.ssid +meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.ap.pwd SampleAPPwd123 +meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.ap.pwd + +meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.mode client +meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.mode +meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.ssid SampleSSID +meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.ssid +meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.pwd SamplePwd123 +meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.pwd + +meshcore-cli -s /dev/ttyUSB0 -b 115200 set transport tcp +meshcore-cli -s /dev/ttyUSB0 -b 115200 get transport +meshcore-cli -s /dev/ttyUSB0 -b 115200 set transport wifi +meshcore-cli -s /dev/ttyUSB0 -b 115200 get transport +meshcore-cli -s /dev/ttyUSB0 -b 115200 set transport ble +meshcore-cli -s /dev/ttyUSB0 -b 115200 get transport + +meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.mode +meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.ssid +meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.pwd +meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.ap.ssid +meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.ap.pwd ``` -### 3.2 CLI Rescue text commands +### 3.3 CLI Rescue text commands Inside CLI Rescue terminal: ```text set transport tcp set wifi.mode ap -set wifi.ap.ssid MeshCore-TestAP -set wifi.ap.pwd testpass123 +set wifi.ap.ssid MeshCore-SampleAP +set wifi.ap.pwd SampleAPPwd123 set wifi.mode client -set wifi.ssid YourSSID -set wifi.pwd YourPassword +set wifi.ssid SampleSSID +set wifi.pwd SamplePwd123 set transport ble + +get transport +get wifi.mode +get wifi.ssid +get wifi.pwd +get wifi.ap.ssid +get wifi.ap.pwd ``` ## TCP Test Example diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 747522cca..ec6d294d0 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1689,15 +1689,33 @@ void MyMesh::handleCmdFrame(size_t len) { } else if (cmd_frame[0] == CMD_GET_CUSTOM_VARS) { out_frame[0] = RESP_CODE_CUSTOM_VARS; char *dp = (char *)&out_frame[1]; - for (int i = 0; i < sensors.getNumSettings() && dp - (char *)&out_frame[1] < 140; i++) { - if (i > 0) { + auto appendKV = [&](const char* key, const char* value) -> bool { + if (!key || !value) return false; + int used = dp - (char *)&out_frame[1]; + int required = strlen(key) + 1 + strlen(value) + (used > 0 ? 1 : 0); + if (used + required >= 140) return false; + if (used > 0) { *dp++ = ','; } - strcpy(dp, sensors.getSettingName(i)); + strcpy(dp, key); dp = strchr(dp, 0); *dp++ = ':'; - strcpy(dp, sensors.getSettingValue(i)); + strcpy(dp, value); dp = strchr(dp, 0); + return true; + }; + +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) + appendKV("transport", _prefs.transport_mode == 1 ? "wifi" : "ble"); + appendKV("wifi.mode", _prefs.wifi_mode == 1 ? "client" : "ap"); + appendKV("wifi.ssid", _prefs.wifi_ssid); + appendKV("wifi.ap.ssid", _prefs.wifi_ap_ssid); + appendKV("wifi.pwd", _prefs.wifi_pwd); + appendKV("wifi.ap.pwd", _prefs.wifi_ap_pwd); +#endif + + for (int i = 0; i < sensors.getNumSettings() && dp - (char *)&out_frame[1] < 140; i++) { + if (!appendKV(sensors.getSettingName(i), sensors.getSettingValue(i))) break; } _serial->writeFrame(out_frame, dp - (char *)out_frame); } else if (cmd_frame[0] == CMD_SET_CUSTOM_VAR && len >= 4) { @@ -2035,6 +2053,25 @@ void MyMesh::checkCLIRescueCmd() { } else { Serial.printf(" Error: unknown config: %s\n", config); } +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) + } else if (memcmp(cli_command, "get ", 4) == 0) { + const char* key = &cli_command[4]; + if (strcmp(key, "transport") == 0) { + Serial.printf(" > %s\n", _prefs.transport_mode == 1 ? "wifi" : "ble"); + } else if (strcmp(key, "wifi.mode") == 0) { + Serial.printf(" > %s\n", _prefs.wifi_mode == 1 ? "client" : "ap"); + } else if (strcmp(key, "wifi.ssid") == 0) { + Serial.printf(" > %s\n", _prefs.wifi_ssid); + } else if (strcmp(key, "wifi.pwd") == 0) { + Serial.printf(" > %s\n", _prefs.wifi_pwd); + } else if (strcmp(key, "wifi.ap.ssid") == 0) { + Serial.printf(" > %s\n", _prefs.wifi_ap_ssid); + } else if (strcmp(key, "wifi.ap.pwd") == 0) { + Serial.printf(" > %s\n", _prefs.wifi_ap_pwd); + } else { + Serial.printf(" Error: unknown key: %s\n", key); + } +#endif #if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) } else if (strcmp(cli_command, "wifi connect") == 0) { _prefs.transport_mode = 1; From b61b00ae0d41f035f3915969fa7cf5885e4c893d Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Sun, 8 Mar 2026 16:15:09 -0400 Subject: [PATCH 11/20] companion: harden runtime transport/wifi custom var controls --- docs/wifi_transport_command_examples.md | 124 +++++++++------- examples/companion_radio/MyMesh.cpp | 136 ++++++++++++++++-- examples/companion_radio/MyMesh.h | 2 + .../esp32/CompanionTransportInterface.cpp | 35 ++++- .../esp32/CompanionTransportInterface.h | 2 + 5 files changed, 228 insertions(+), 71 deletions(-) diff --git a/docs/wifi_transport_command_examples.md b/docs/wifi_transport_command_examples.md index f7aa268c6..d869dc4e9 100644 --- a/docs/wifi_transport_command_examples.md +++ b/docs/wifi_transport_command_examples.md @@ -18,6 +18,9 @@ These are the custom-var keys used by firmware: - `wifi.pwd:` - `wifi.ap.ssid:` - `wifi.ap.pwd:` +- `tcp.port:` +- `ip` (runtime IP in companion mode) +- `wifi.ip` / `wifi.status` / `wifi.rssi` / `wifi.gateway` / `wifi.mask` / `wifi.mac` (runtime keys in CLI Rescue) ## 1) Raw Companion Protocol Form @@ -80,40 +83,43 @@ python3 -c "import os;fd=os.open('/dev/ttyUSB0',os.O_WRONLY);s='wifi.mode:ap';p= There are two text-CLI contexts: -1. `meshcore-cli` companion commands +1. `meshcli` companion commands 2. CLI Rescue (device text console) -### 3.1 meshcore-cli (companion mode) +### 3.1 meshcli (companion mode) Use serial companion mode (no `-r`): ```bash -meshcore-cli -s /dev/ttyUSB0 -b 115200 infos +meshcli -s /dev/ttyUSB0 -b 115200 infos ``` Then set values: ```bash -meshcore-cli -s /dev/ttyUSB0 -b 115200 set transport tcp -meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.mode ap -meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.ap.ssid MeshCore-SampleAP -meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.ap.pwd SampleAPPwd123 +meshcli -s /dev/ttyUSB0 -b 115200 set transport tcp +meshcli -s /dev/ttyUSB0 -b 115200 set wifi.mode ap +meshcli -s /dev/ttyUSB0 -b 115200 set tcp.port 5000 +meshcli -s /dev/ttyUSB0 -b 115200 set wifi.ap.ssid MeshCore-SampleAP +meshcli -s /dev/ttyUSB0 -b 115200 set wifi.ap.pwd SampleAPPwd123 -meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.mode client -meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.ssid SampleSSID -meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.pwd SamplePwd123 +meshcli -s /dev/ttyUSB0 -b 115200 set wifi.mode client +meshcli -s /dev/ttyUSB0 -b 115200 set wifi.ssid SampleSSID +meshcli -s /dev/ttyUSB0 -b 115200 set wifi.pwd SamplePwd123 -meshcore-cli -s /dev/ttyUSB0 -b 115200 set transport ble +meshcli -s /dev/ttyUSB0 -b 115200 set transport ble +``` Read back current transport/WiFi values: ```bash -meshcore-cli -s /dev/ttyUSB0 -b 115200 get transport -meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.mode -meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.ssid -meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.pwd -meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.ap.ssid -meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.ap.pwd +meshcli -s /dev/ttyUSB0 -b 115200 get transport +meshcli -s /dev/ttyUSB0 -b 115200 get tcp.port +meshcli -s /dev/ttyUSB0 -b 115200 get ip +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.mode +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.ssid +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.pwd +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.ap.pwd ``` ### 3.2 meshcli interactive mode (recommended for manual testing) @@ -121,7 +127,7 @@ meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.ap.pwd Start interactive mode: ```bash -meshcore-cli -s /dev/ttyUSB0 -b 115200 +meshcli -s /dev/ttyUSB0 -b 115200 ``` Example interactive session: @@ -131,14 +137,18 @@ KD3CGK|* set wifi.mode client Var wifi.mode set to client KD3CGK|* get wifi.mode client +KD3CGK|* set tcp.port 6001 +Var tcp.port set to 6001 +KD3CGK|* get tcp.port +6001 KD3CGK|* set transport ble Var transport set to ble KD3CGK|* get transport ble +KD3CGK|* get ip +0.0.0.0 KD3CGK|* get wifi.ssid SampleSSID -KD3CGK|* get wifi.ap.ssid -MeshCore-SampleAP KD3CGK|* get wifi.pwd SamplePwd123 KD3CGK|* get wifi.ap.pwd @@ -150,36 +160,37 @@ SampleAPPwd123 Run this exact command sequence to validate WiFi/transport set+get behavior with sample values: ```bash -meshcore-cli -s /dev/ttyUSB0 -b 115200 infos -meshcore-cli -s /dev/ttyUSB0 -b 115200 set transport ble -meshcore-cli -s /dev/ttyUSB0 -b 115200 get transport - -meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.mode ap -meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.mode -meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.ap.ssid MeshCore-SampleAP -meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.ap.ssid -meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.ap.pwd SampleAPPwd123 -meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.ap.pwd - -meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.mode client -meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.mode -meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.ssid SampleSSID -meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.ssid -meshcore-cli -s /dev/ttyUSB0 -b 115200 set wifi.pwd SamplePwd123 -meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.pwd - -meshcore-cli -s /dev/ttyUSB0 -b 115200 set transport tcp -meshcore-cli -s /dev/ttyUSB0 -b 115200 get transport -meshcore-cli -s /dev/ttyUSB0 -b 115200 set transport wifi -meshcore-cli -s /dev/ttyUSB0 -b 115200 get transport -meshcore-cli -s /dev/ttyUSB0 -b 115200 set transport ble -meshcore-cli -s /dev/ttyUSB0 -b 115200 get transport - -meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.mode -meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.ssid -meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.pwd -meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.ap.ssid -meshcore-cli -s /dev/ttyUSB0 -b 115200 get wifi.ap.pwd +meshcli -s /dev/ttyUSB0 -b 115200 infos +meshcli -s /dev/ttyUSB0 -b 115200 set transport ble +meshcli -s /dev/ttyUSB0 -b 115200 get transport + +meshcli -s /dev/ttyUSB0 -b 115200 set wifi.mode ap +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.mode +meshcli -s /dev/ttyUSB0 -b 115200 set wifi.ap.ssid MeshCore-SampleAP +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.ap.ssid +meshcli -s /dev/ttyUSB0 -b 115200 set wifi.ap.pwd SampleAPPwd123 +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.ap.pwd + +meshcli -s /dev/ttyUSB0 -b 115200 set wifi.mode client +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.mode +meshcli -s /dev/ttyUSB0 -b 115200 set wifi.ssid SampleSSID +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.ssid +meshcli -s /dev/ttyUSB0 -b 115200 set wifi.pwd SamplePwd123 +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.pwd + +meshcli -s /dev/ttyUSB0 -b 115200 set transport tcp +meshcli -s /dev/ttyUSB0 -b 115200 get transport +meshcli -s /dev/ttyUSB0 -b 115200 set transport wifi +meshcli -s /dev/ttyUSB0 -b 115200 get transport +meshcli -s /dev/ttyUSB0 -b 115200 set transport ble +meshcli -s /dev/ttyUSB0 -b 115200 get transport + +meshcli -s /dev/ttyUSB0 -b 115200 get tcp.port +meshcli -s /dev/ttyUSB0 -b 115200 get ip +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.mode +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.ssid +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.pwd +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.ap.pwd ``` ### 3.3 CLI Rescue text commands @@ -189,6 +200,7 @@ Inside CLI Rescue terminal: ```text set transport tcp set wifi.mode ap +set tcp.port 5000 set wifi.ap.ssid MeshCore-SampleAP set wifi.ap.pwd SampleAPPwd123 @@ -199,11 +211,21 @@ set wifi.pwd SamplePwd123 set transport ble get transport +get tcp.port +get ip +get wifi.ip get wifi.mode get wifi.ssid get wifi.pwd get wifi.ap.ssid get wifi.ap.pwd +get ip +get wifi.ip +get wifi.status +get wifi.rssi +get wifi.gateway +get wifi.mask +get wifi.mac ``` ## TCP Test Example @@ -211,7 +233,7 @@ get wifi.ap.pwd If device client IP is known (example `192.168.40.55`): ```bash -meshcore-cli -t 192.168.40.55 -p 5000 infos +meshcli -t 192.168.40.55 -p 5000 infos ``` ## Notes diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index ec6d294d0..7af9983c9 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -144,6 +144,12 @@ static bool applyWifiPrefs(NodePrefs& prefs) { : CompanionTransportInterface::WIFI_MODE_AP_ONLY; return transport->setWifiMode(wifi_mode); } + +static bool applyTcpPortPref(uint16_t tcp_port) { + auto* transport = CompanionTransportInterface::instance(); + if (!transport) return false; + return transport->setTcpPort(tcp_port); +} #endif #define ERR_CODE_UNSUPPORTED_CMD 1 @@ -827,6 +833,8 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui) { _iter_started = false; _cli_rescue = false; + _pending_transport_apply = false; + _pending_transport_apply_at = 0; offline_queue_len = 0; app_target_ver = 0; clearPendingReqs(); @@ -1689,11 +1697,36 @@ void MyMesh::handleCmdFrame(size_t len) { } else if (cmd_frame[0] == CMD_GET_CUSTOM_VARS) { out_frame[0] = RESP_CODE_CUSTOM_VARS; char *dp = (char *)&out_frame[1]; + const int max_custom_vars_len = 140; + auto escapeCustomVarValue = [](const char* value, char* out, size_t out_len) { + if (!value || !out || out_len == 0) return; + size_t oi = 0; + for (size_t i = 0; value[i] != 0 && oi + 1 < out_len; i++) { + char c = value[i]; + if (c == ',' || c == ':' || c == '%') { + if (oi + 3 >= out_len) break; + out[oi++] = '%'; + if (c == ',') { + out[oi++] = '2'; + out[oi++] = 'C'; + } else if (c == ':') { + out[oi++] = '3'; + out[oi++] = 'A'; + } else { + out[oi++] = '2'; + out[oi++] = '5'; + } + } else { + out[oi++] = c; + } + } + out[oi] = 0; + }; auto appendKV = [&](const char* key, const char* value) -> bool { if (!key || !value) return false; int used = dp - (char *)&out_frame[1]; int required = strlen(key) + 1 + strlen(value) + (used > 0 ? 1 : 0); - if (used + required >= 140) return false; + if (used + required >= max_custom_vars_len) return false; if (used > 0) { *dp++ = ','; } @@ -1704,18 +1737,39 @@ void MyMesh::handleCmdFrame(size_t len) { dp = strchr(dp, 0); return true; }; + auto appendKVEscaped = [&](const char* key, const char* value) -> bool { + char escaped[96]; + escapeCustomVarValue(value, escaped, sizeof(escaped)); + return appendKV(key, escaped); + }; #if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) - appendKV("transport", _prefs.transport_mode == 1 ? "wifi" : "ble"); - appendKV("wifi.mode", _prefs.wifi_mode == 1 ? "client" : "ap"); - appendKV("wifi.ssid", _prefs.wifi_ssid); - appendKV("wifi.ap.ssid", _prefs.wifi_ap_ssid); - appendKV("wifi.pwd", _prefs.wifi_pwd); - appendKV("wifi.ap.pwd", _prefs.wifi_ap_pwd); + char tcp_port_value[8]; + char ip_value[20] = "0.0.0.0"; + uint16_t tcp_port = 5000; + if (auto* transport = CompanionTransportInterface::instance()) { + tcp_port = transport->getTcpPort(); + } + snprintf(tcp_port_value, sizeof(tcp_port_value), "%u", tcp_port); + if (WiFi.status() == WL_CONNECTED) { + snprintf(ip_value, sizeof(ip_value), "%s", WiFi.localIP().toString().c_str()); + } else if (_prefs.wifi_mode == 0) { + snprintf(ip_value, sizeof(ip_value), "%s", WiFi.softAPIP().toString().c_str()); + } + + appendKVEscaped("transport", _prefs.transport_mode == 1 ? "tcp" : "ble"); + appendKVEscaped("tcp.port", tcp_port_value); + appendKVEscaped("ip", ip_value); + appendKVEscaped("wifi.mode", _prefs.wifi_mode == 1 ? "client" : "ap"); + appendKVEscaped("wifi.ssid", _prefs.wifi_ssid); + appendKVEscaped("wifi.pwd", _prefs.wifi_pwd); + appendKVEscaped("wifi.ap.pwd", _prefs.wifi_ap_pwd); + appendKVEscaped("wifi.ip", ip_value); + appendKVEscaped("wifi.ap.ssid", _prefs.wifi_ap_ssid); #endif - for (int i = 0; i < sensors.getNumSettings() && dp - (char *)&out_frame[1] < 140; i++) { - if (!appendKV(sensors.getSettingName(i), sensors.getSettingValue(i))) break; + for (int i = 0; i < sensors.getNumSettings() && dp - (char *)&out_frame[1] < max_custom_vars_len; i++) { + if (!appendKVEscaped(sensors.getSettingName(i), sensors.getSettingValue(i))) break; } _serial->writeFrame(out_frame, dp - (char *)out_frame); } else if (cmd_frame[0] == CMD_SET_CUSTOM_VAR && len >= 4) { @@ -1730,10 +1784,14 @@ void MyMesh::handleCmdFrame(size_t len) { if (strcmp(sp, "transport") == 0) { if (strcmp(np, "ble") == 0) { _prefs.transport_mode = 0; - success = applyTransportModePref(_prefs); + _pending_transport_apply = true; + _pending_transport_apply_at = millis() + 500; + success = true; } else if (strcmp(np, "tcp") == 0 || strcmp(np, "wifi") == 0) { _prefs.transport_mode = 1; - success = applyTransportModePref(_prefs); + _pending_transport_apply = true; + _pending_transport_apply_at = millis() + 500; + success = true; } if (success) { savePrefs(); @@ -1772,6 +1830,11 @@ void MyMesh::handleCmdFrame(size_t len) { success = applyWifiPrefs(_prefs); if (success) savePrefs(); } + } else if (strcmp(sp, "tcp.port") == 0) { + long port = atol(np); + if (port >= 1 && port <= 65535) { + success = applyTcpPortPref((uint16_t)port); + } } #endif @@ -2056,8 +2119,46 @@ void MyMesh::checkCLIRescueCmd() { #if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) } else if (memcmp(cli_command, "get ", 4) == 0) { const char* key = &cli_command[4]; - if (strcmp(key, "transport") == 0) { - Serial.printf(" > %s\n", _prefs.transport_mode == 1 ? "wifi" : "ble"); + String wifi_mac_str = WiFi.macAddress(); + const char* wifi_status = "off"; + if (WiFi.status() == WL_CONNECTED) { + wifi_status = "connected"; + } else if (_prefs.wifi_mode == 0) { + wifi_status = "ap"; + } else if (_prefs.wifi_mode == 1 && _prefs.transport_mode == 1 && _prefs.wifi_ssid[0] != 0) { + wifi_status = "connecting"; + } + + if (strcmp(key, "ip") == 0 || strcmp(key, "wifi.ip") == 0) { + if (WiFi.status() == WL_CONNECTED) { + Serial.printf(" > %s\n", WiFi.localIP().toString().c_str()); + } else if (_prefs.wifi_mode == 0) { + Serial.printf(" > %s\n", WiFi.softAPIP().toString().c_str()); + } else { + Serial.println(" > 0.0.0.0"); + } + } else if (strcmp(key, "tcp.port") == 0) { + if (auto* transport = CompanionTransportInterface::instance()) { + Serial.printf(" > %u\n", transport->getTcpPort()); + } else { + Serial.println(" > 5000"); + } + } else if (strcmp(key, "wifi.gateway") == 0) { + Serial.printf(" > %s\n", WiFi.gatewayIP().toString().c_str()); + } else if (strcmp(key, "wifi.mask") == 0) { + Serial.printf(" > %s\n", WiFi.subnetMask().toString().c_str()); + } else if (strcmp(key, "wifi.status") == 0) { + Serial.printf(" > %s\n", wifi_status); + } else if (strcmp(key, "wifi.rssi") == 0) { + if (WiFi.status() == WL_CONNECTED) { + Serial.printf(" > %ld\n", (long)WiFi.RSSI()); + } else { + Serial.println(" > n/a"); + } + } else if (strcmp(key, "wifi.mac") == 0) { + Serial.printf(" > %s\n", wifi_mac_str.c_str()); + } else if (strcmp(key, "transport") == 0) { + Serial.printf(" > %s\n", _prefs.transport_mode == 1 ? "tcp" : "ble"); } else if (strcmp(key, "wifi.mode") == 0) { Serial.printf(" > %s\n", _prefs.wifi_mode == 1 ? "client" : "ap"); } else if (strcmp(key, "wifi.ssid") == 0) { @@ -2278,6 +2379,15 @@ void MyMesh::loop() { dirty_contacts_expiry = 0; } +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) + if (_pending_transport_apply && _serial && !_serial->isWriteBusy() && + millisHasNowPassed(_pending_transport_apply_at)) { + _pending_transport_apply = false; + _pending_transport_apply_at = 0; + applyTransportModePref(_prefs); // best-effort apply after command response is sent + } +#endif + #ifdef DISPLAY_CLASS if (_ui) _ui->setHasConnection(_serial->isConnected()); #endif diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 4d77b5ab7..1e9d0e400 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -201,6 +201,8 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { uint32_t _active_ble_pin; bool _iter_started; bool _cli_rescue; + bool _pending_transport_apply; + unsigned long _pending_transport_apply_at; char cli_command[80]; uint8_t app_target_ver; uint8_t *sign_data; diff --git a/src/helpers/esp32/CompanionTransportInterface.cpp b/src/helpers/esp32/CompanionTransportInterface.cpp index 3940b2590..66288d19e 100644 --- a/src/helpers/esp32/CompanionTransportInterface.cpp +++ b/src/helpers/esp32/CompanionTransportInterface.cpp @@ -93,7 +93,11 @@ bool CompanionTransportInterface::setWirelessMode(WirelessMode mode, bool persis } _mode = mode; if (_isEnabled && !persist_only) { - startWireless(); + // Keep active USB control session stable: if command/control is over USB, + // defer live wireless stack switch until after USB disconnect/reconnect. + if (!(_last_source == 0 && _usb.isConnected())) { + startWireless(); + } } return true; } @@ -134,6 +138,21 @@ bool CompanionTransportInterface::setWifiApCredentials(const char* ssid, const c return true; } +bool CompanionTransportInterface::setTcpPort(uint16_t port) { + if (port == 0) return false; + _tcp_port = port; + + // If TCP server is already running, restart it on the new port. + if (_wifi_server_started) { + _wifi.disable(); + _wifi.begin(_tcp_port); + if (_mode == WIRELESS_MODE_TCP) { + _wifi.enable(); + } + } + return true; +} + void CompanionTransportInterface::enable() { if (_isEnabled) return; _isEnabled = true; @@ -168,6 +187,14 @@ size_t CompanionTransportInterface::writeFrame(const uint8_t src[], size_t len) } size_t CompanionTransportInterface::checkRecvFrame(uint8_t dest[]) { + // Prioritise USB input so local serial control remains reliable even + // when wireless transport is active or reconnecting. + size_t usb_len = _usb.checkRecvFrame(dest); + if (usb_len > 0) { + _last_source = 0; + return usb_len; + } + if (_mode == WIRELESS_MODE_TCP) { static unsigned long last_wifi_retry_at = 0; if (_wifi_mode == WIFI_MODE_STA_CLIENT && _wifi_ssid[0] != 0 && WiFi.status() != WL_CONNECTED) { @@ -192,12 +219,6 @@ size_t CompanionTransportInterface::checkRecvFrame(uint8_t dest[]) { return ble_len; } } - - size_t usb_len = _usb.checkRecvFrame(dest); - if (usb_len > 0) { - _last_source = 0; - return usb_len; - } return 0; } diff --git a/src/helpers/esp32/CompanionTransportInterface.h b/src/helpers/esp32/CompanionTransportInterface.h index 6daf193c3..95494b002 100644 --- a/src/helpers/esp32/CompanionTransportInterface.h +++ b/src/helpers/esp32/CompanionTransportInterface.h @@ -55,6 +55,8 @@ class CompanionTransportInterface : public BaseSerialInterface { bool setWifiMode(WifiMode mode); WifiMode getWifiMode() const { return _wifi_mode; } bool setWifiApCredentials(const char* ssid, const char* pwd); + bool setTcpPort(uint16_t port); + uint16_t getTcpPort() const { return _tcp_port; } const char* getWifiSSID() const { return _wifi_ssid; } const char* getWifiApSSID() const { return _wifi_ap_ssid; } const char* getWifiApPassword() const { return _wifi_ap_pwd; } From df0cc1e9c2c9448e5ae5f74bdbe9b5e8cda5c6e7 Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Sun, 8 Mar 2026 16:18:18 -0400 Subject: [PATCH 12/20] ui: add transport reboot countdown and wifi mode transition screens --- docs/wifi_transport_command_examples.md | 2 + examples/companion_radio/MyMesh.cpp | 2 + examples/companion_radio/ui-new/UITask.cpp | 58 +++++++++++++++++++++- examples/companion_radio/ui-new/UITask.h | 10 ++++ 4 files changed, 70 insertions(+), 2 deletions(-) diff --git a/docs/wifi_transport_command_examples.md b/docs/wifi_transport_command_examples.md index d869dc4e9..80fe523e1 100644 --- a/docs/wifi_transport_command_examples.md +++ b/docs/wifi_transport_command_examples.md @@ -240,5 +240,7 @@ meshcli -t 192.168.40.55 -p 5000 infos - `transport:wifi` / `set transport tcp` enables WiFi/TCP transport and disables BLE. - `transport:ble` / `set transport ble` enables BLE and disables WiFi/TCP. +- After a transport switch, the board UI now shows a 10-second reboot countdown and auto-resets. +- The board UI also shows a temporary WiFi mode transition screen when switching AP/client mode. - AP password should be empty (open AP) or at least 8 characters. - Do not commit real SSIDs/passwords to git. diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 7af9983c9..f6167bc4e 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2025,6 +2025,7 @@ void MyMesh::checkCLIRescueCmd() { if (applyTransportModePref(_prefs)) { savePrefs(); Serial.println(" > wireless transport is now BLE"); + Serial.println(" > reboot required; board will auto-reset in 10 seconds"); } else { Serial.println(" Error: failed to switch transport"); } @@ -2033,6 +2034,7 @@ void MyMesh::checkCLIRescueCmd() { if (applyTransportModePref(_prefs)) { savePrefs(); Serial.println(" > wireless transport is now TCP/WiFi"); + Serial.println(" > reboot required; board will auto-reset in 10 seconds"); } else { Serial.println(" Error: failed to switch transport"); } diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 976d7ff56..c04e94ab9 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -832,13 +832,33 @@ void UITask::loop() { char c = 0; if (_node_prefs != NULL) { - if (_node_prefs->transport_mode != _last_transport_mode || _node_prefs->wifi_mode != _last_wifi_mode) { + if (_node_prefs->transport_mode != _last_transport_mode) { _last_transport_mode = _node_prefs->transport_mode; + _transport_reboot_pending = true; + _transport_reboot_deadline = millis() + 10000; + _next_refresh = 0; // immediate redraw after remote command changes + } + if (_node_prefs->wifi_mode != _last_wifi_mode) { _last_wifi_mode = _node_prefs->wifi_mode; + _wifi_mode_notice_active = true; + _wifi_mode_notice_value = _node_prefs->wifi_mode; + _wifi_mode_notice_until = millis() + 3000; + _last_transport_mode = _node_prefs->transport_mode; _next_refresh = 0; // immediate redraw after remote command changes } } + if (_transport_reboot_pending && millis() >= _transport_reboot_deadline) { + _transport_reboot_pending = false; + shutdown(true); + return; + } + + if (_wifi_mode_notice_active && millis() >= _wifi_mode_notice_until) { + _wifi_mode_notice_active = false; + _next_refresh = 0; + } + #if UI_HAS_JOYSTICK int ev = user_btn.check(); if (ev == BUTTON_EVENT_CLICK) { @@ -919,7 +939,41 @@ void UITask::loop() { if (_display != NULL && _display->isOn()) { if (millis() >= _next_refresh && curr) { _display->startFrame(); - int delay_millis = curr->render(*_display); + int delay_millis = 5000; + if (_transport_reboot_pending) { + int remaining_secs = (int)((_transport_reboot_deadline - millis() + 999) / 1000); + if (remaining_secs < 0) remaining_secs = 0; + + _display->setTextSize(1); + _display->setColor(DisplayDriver::YELLOW); + _display->drawTextCentered(_display->width() / 2, 8, "Transport Changed"); + _display->setColor(DisplayDriver::LIGHT); + _display->drawTextCentered(_display->width() / 2, 22, "Reboot required"); + _display->drawTextCentered(_display->width() / 2, 34, "Auto reset in:"); + + char tmp[16]; + snprintf(tmp, sizeof(tmp), "%ds", remaining_secs); + _display->setTextSize(2); + _display->setColor(DisplayDriver::RED); + _display->drawTextCentered(_display->width() / 2, 46, tmp); + _display->setTextSize(1); + delay_millis = 200; + } else if (_wifi_mode_notice_active) { + _display->setTextSize(1); + _display->setColor(DisplayDriver::YELLOW); + _display->drawTextCentered(_display->width() / 2, 10, "WiFi Mode Updated"); + _display->setColor(DisplayDriver::LIGHT); + if (_wifi_mode_notice_value == 0) { + _display->drawTextCentered(_display->width() / 2, 30, "Switching to AP mode"); + _display->drawTextCentered(_display->width() / 2, 44, "SSID: device hotspot"); + } else { + _display->drawTextCentered(_display->width() / 2, 30, "Switching to Client"); + _display->drawTextCentered(_display->width() / 2, 44, "Joining configured SSID"); + } + delay_millis = 200; + } else { + delay_millis = curr->render(*_display); + } if (millis() < _alert_expiry) { // render alert popup _display->setTextSize(1); int y = _display->height() / 3; diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 906c6ebb0..6464fb031 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -35,6 +35,11 @@ class UITask : public AbstractUITask { bool _pending_long_press; uint8_t _last_transport_mode; uint8_t _last_wifi_mode; + bool _transport_reboot_pending; + unsigned long _transport_reboot_deadline; + bool _wifi_mode_notice_active; + unsigned long _wifi_mode_notice_until; + uint8_t _wifi_mode_notice_value; NodePrefs* _node_prefs; char _alert[80]; unsigned long _alert_expiry; @@ -74,6 +79,11 @@ class UITask : public AbstractUITask { _pending_long_press = false; _last_transport_mode = 255; _last_wifi_mode = 255; + _transport_reboot_pending = false; + _transport_reboot_deadline = 0; + _wifi_mode_notice_active = false; + _wifi_mode_notice_until = 0; + _wifi_mode_notice_value = 255; curr = NULL; } void begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs); From fbd7105e07bd33de605259041b6f2cbcaae49d02 Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Sun, 8 Mar 2026 17:04:47 -0400 Subject: [PATCH 13/20] companion: finalize runtime transport/wifi UX, commands, and docs --- README.md | 34 +++++++++ docs/heltec_v3_button_actions.md | 44 +++++++++++ docs/index.md | 1 + docs/wifi_transport_command_examples.md | 24 ++++-- examples/companion_radio/MyMesh.cpp | 6 +- examples/companion_radio/ui-new/UITask.cpp | 87 +++++++++++++++++----- 6 files changed, 168 insertions(+), 28 deletions(-) create mode 100644 docs/heltec_v3_button_actions.md diff --git a/README.md b/README.md index 5a0941753..d914f3d6c 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,40 @@ For developers; The Simple Secure Chat example can be interacted with through the Serial Monitor in Visual Studio Code, or with a Serial USB Terminal on Android. +## Build And Flash This Fork (Heltec V3) + +If you want to build and flash this fork directly from terminal: + +```bash +git clone https://github.com/just-stuff-tm/MeshCore.git +cd MeshCore +git checkout feat/heltec-v3-runtime-transport-dev +``` + +Install PlatformIO Core (if not already installed): + +```bash +python3 -m pip install -U platformio +``` + +Build firmware: + +```bash +~/.platformio/penv/bin/pio run -e Heltec_v3_companion_radio_tcp_usb_ble +``` + +Flash firmware (USB serial example): + +```bash +~/.platformio/penv/bin/pio run -e Heltec_v3_companion_radio_tcp_usb_ble -t upload --upload-port /dev/ttyUSB0 +``` + +Optional serial companion check: + +```bash +meshcli -s /dev/ttyUSB0 +``` + ## ⚡️ MeshCore Flasher We have prebuilt firmware ready to flash on supported devices. diff --git a/docs/heltec_v3_button_actions.md b/docs/heltec_v3_button_actions.md new file mode 100644 index 000000000..5ce91f1ab --- /dev/null +++ b/docs/heltec_v3_button_actions.md @@ -0,0 +1,44 @@ +# Heltec V3 Button Actions + +This page documents current button behavior for `Heltec_v3_companion_radio_tcp_usb_ble` UI flows. + +## Global Click Mapping + +On single-button Heltec V3 builds: + +- `single click` = next page +- `double click` = previous page +- `long press` = enter/select action on current page +- `triple click` = `KEY_SELECT` action on current page + +## Startup Special Case + +- Long press within first ~8 seconds after boot enters CLI Rescue mode. + +## Screen Actions + +### BLE Transport Page + +- Long press: switch transport to WiFi/TCP. +- After transport switch, UI shows a 10-second reboot countdown and auto-reboots. + +### WiFi Transport Page + +- Long press: toggle WiFi mode (`AP <-> Client`). +- Triple click: switch transport back to BLE. +- UI footer alternates/scrolls usage hints: + - `Long press: AP/Client` + - `Triple click: BLE` + +### Advert Page + +- Long press: send advert. + +### Power Page + +- Long press: start hibernate/power-off flow. + +## Notes + +- Transport switches are runtime-applied, then device auto-reboots after countdown for clean handoff. +- WiFi page shows `IP:PORT` in both AP and client mode, with AP SSID/password rows alternating. diff --git a/docs/index.md b/docs/index.md index 9460a00c5..3fef7e854 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,6 +7,7 @@ Below are a few quick start guides. - [Frequently Asked Questions](./faq.md) - [CLI Commands](./cli_commands.md) - [Companion Protocol](./companion_protocol.md) +- [Heltec V3 Button Actions](./heltec_v3_button_actions.md) - [Packet Format](./packet_format.md) - [QR Codes](./qr_codes.md) diff --git a/docs/wifi_transport_command_examples.md b/docs/wifi_transport_command_examples.md index 80fe523e1..7164f8c6e 100644 --- a/docs/wifi_transport_command_examples.md +++ b/docs/wifi_transport_command_examples.md @@ -19,8 +19,7 @@ These are the custom-var keys used by firmware: - `wifi.ap.ssid:` - `wifi.ap.pwd:` - `tcp.port:` -- `ip` (runtime IP in companion mode) -- `wifi.ip` / `wifi.status` / `wifi.rssi` / `wifi.gateway` / `wifi.mask` / `wifi.mac` (runtime keys in CLI Rescue) +- `ip` / `wifi.ip` / `wifi.status` / `wifi.rssi` / `wifi.gateway` / `wifi.mask` / `wifi.mac` (runtime keys in CLI Rescue) ## 1) Raw Companion Protocol Form @@ -115,10 +114,10 @@ Read back current transport/WiFi values: ```bash meshcli -s /dev/ttyUSB0 -b 115200 get transport meshcli -s /dev/ttyUSB0 -b 115200 get tcp.port -meshcli -s /dev/ttyUSB0 -b 115200 get ip meshcli -s /dev/ttyUSB0 -b 115200 get wifi.mode meshcli -s /dev/ttyUSB0 -b 115200 get wifi.ssid meshcli -s /dev/ttyUSB0 -b 115200 get wifi.pwd +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.ap.ssid meshcli -s /dev/ttyUSB0 -b 115200 get wifi.ap.pwd ``` @@ -145,12 +144,12 @@ KD3CGK|* set transport ble Var transport set to ble KD3CGK|* get transport ble -KD3CGK|* get ip -0.0.0.0 KD3CGK|* get wifi.ssid SampleSSID KD3CGK|* get wifi.pwd SamplePwd123 +KD3CGK|* get wifi.ap.ssid +MeshCore-SampleAP KD3CGK|* get wifi.ap.pwd SampleAPPwd123 ``` @@ -186,13 +185,25 @@ meshcli -s /dev/ttyUSB0 -b 115200 set transport ble meshcli -s /dev/ttyUSB0 -b 115200 get transport meshcli -s /dev/ttyUSB0 -b 115200 get tcp.port -meshcli -s /dev/ttyUSB0 -b 115200 get ip meshcli -s /dev/ttyUSB0 -b 115200 get wifi.mode meshcli -s /dev/ttyUSB0 -b 115200 get wifi.ssid meshcli -s /dev/ttyUSB0 -b 115200 get wifi.pwd +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.ap.ssid meshcli -s /dev/ttyUSB0 -b 115200 get wifi.ap.pwd ``` +### 3.2.2 Current `meshcli` custom `get` keys + +In companion interactive mode (`meshcli -s ...`), the custom-var getter supports: + +- `transport` +- `tcp.port` +- `wifi.mode` +- `wifi.ssid` +- `wifi.pwd` +- `wifi.ap.ssid` +- `wifi.ap.pwd` + ### 3.3 CLI Rescue text commands Inside CLI Rescue terminal: @@ -242,5 +253,6 @@ meshcli -t 192.168.40.55 -p 5000 infos - `transport:ble` / `set transport ble` enables BLE and disables WiFi/TCP. - After a transport switch, the board UI now shows a 10-second reboot countdown and auto-resets. - The board UI also shows a temporary WiFi mode transition screen when switching AP/client mode. +- Heltec V3 button mappings for these UI actions: [Heltec V3 Button Actions](./heltec_v3_button_actions.md). - AP password should be empty (open AP) or at least 8 characters. - Do not commit real SSIDs/passwords to git. diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index f6167bc4e..0d1863eea 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1759,13 +1759,11 @@ void MyMesh::handleCmdFrame(size_t len) { appendKVEscaped("transport", _prefs.transport_mode == 1 ? "tcp" : "ble"); appendKVEscaped("tcp.port", tcp_port_value); - appendKVEscaped("ip", ip_value); appendKVEscaped("wifi.mode", _prefs.wifi_mode == 1 ? "client" : "ap"); - appendKVEscaped("wifi.ssid", _prefs.wifi_ssid); + appendKVEscaped("wifi.ap.ssid", _prefs.wifi_ap_ssid); appendKVEscaped("wifi.pwd", _prefs.wifi_pwd); appendKVEscaped("wifi.ap.pwd", _prefs.wifi_ap_pwd); - appendKVEscaped("wifi.ip", ip_value); - appendKVEscaped("wifi.ap.ssid", _prefs.wifi_ap_ssid); + appendKVEscaped("wifi.ssid", _prefs.wifi_ssid); #endif for (int i = 0; i < sensors.getNumSettings() && dp - (char *)&out_frame[1] < max_custom_vars_len; i++) { diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index c04e94ab9..11f492b05 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -316,42 +316,69 @@ class HomeScreen : public UIScreen { display.setTextSize(1); display.drawTextCentered(display.width() / 2, 64 - 11, "Long press: WiFi"); } else if (_page == HomePage::WIFI_TRANSPORT) { + const int row1_y = 28; + const int row2_y = 38; + const int row3_y = 48; + const int footer_y = 57; display.setColor(DisplayDriver::GREEN); display.setTextSize(1); display.drawTextCentered(display.width() / 2, 20, "WiFi transport"); + uint16_t tcp_port = 5000; +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) + if (auto* transport = CompanionTransportInterface::instance()) { + tcp_port = transport->getTcpPort(); + } +#endif + bool flip = ((millis() / 1300) % 2) == 0; + if (_node_prefs->wifi_mode == 0) { + char ap_name[40]; if (_node_prefs->wifi_ap_ssid[0]) { - snprintf(tmp, sizeof(tmp), "AP:%s", _node_prefs->wifi_ap_ssid); + snprintf(ap_name, sizeof(ap_name), "AP:%s", _node_prefs->wifi_ap_ssid); } else { - snprintf(tmp, sizeof(tmp), "AP:MeshCore-%s", _node_prefs->node_name); + snprintf(ap_name, sizeof(ap_name), "AP:MeshCore-%s", _node_prefs->node_name); + } + const char* ap_pwd = _node_prefs->wifi_ap_pwd[0] ? _node_prefs->wifi_ap_pwd : ""; + char ap_pwd_row[40]; + snprintf(ap_pwd_row, sizeof(ap_pwd_row), "PW:%s", ap_pwd); + + if (flip) { + display.drawTextEllipsized(0, row1_y, display.width(), ap_name); + display.drawTextEllipsized(0, row2_y, display.width(), ap_pwd_row); + } else { + display.drawTextEllipsized(0, row1_y, display.width(), ap_pwd_row); + display.drawTextEllipsized(0, row2_y, display.width(), ap_name); } - display.drawTextCentered(display.width() / 2, 31, tmp); #if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) IPAddress ap_ip = WiFi.softAPIP(); - snprintf(tmp, sizeof(tmp), "IP:%d.%d.%d.%d", ap_ip[0], ap_ip[1], ap_ip[2], ap_ip[3]); + snprintf(tmp, sizeof(tmp), "%d.%d.%d.%d:%u", ap_ip[0], ap_ip[1], ap_ip[2], ap_ip[3], tcp_port); #else - snprintf(tmp, sizeof(tmp), "IP:n/a"); + snprintf(tmp, sizeof(tmp), "0.0.0.0:%u", tcp_port); #endif - display.drawTextCentered(display.width() / 2, 42, tmp); - const char* ap_pwd = _node_prefs->wifi_ap_pwd[0] ? _node_prefs->wifi_ap_pwd : ""; - snprintf(tmp, sizeof(tmp), "PW:%s", ap_pwd); - display.drawTextCentered(display.width() / 2, 53, tmp); + display.drawTextCentered(display.width() / 2, row3_y, tmp); } else { - display.drawTextCentered(display.width() / 2, 31, "Mode: Client"); + display.drawTextCentered(display.width() / 2, row1_y, "Mode: Client"); snprintf(tmp, sizeof(tmp), "SSID:%s", _node_prefs->wifi_ssid[0] ? _node_prefs->wifi_ssid : ""); - display.drawTextCentered(display.width() / 2, 42, tmp); + display.drawTextEllipsized(0, row2_y, display.width(), tmp); #if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) if (WiFi.status() == WL_CONNECTED) { IPAddress ip = WiFi.localIP(); - snprintf(tmp, sizeof(tmp), "IP:%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); + snprintf(tmp, sizeof(tmp), "%d.%d.%d.%d:%u", ip[0], ip[1], ip[2], ip[3], tcp_port); } else { - snprintf(tmp, sizeof(tmp), "IP: "); + snprintf(tmp, sizeof(tmp), "0.0.0.0:%u", tcp_port); } #else - snprintf(tmp, sizeof(tmp), "IP: n/a"); + snprintf(tmp, sizeof(tmp), "0.0.0.0:%u", tcp_port); #endif - display.drawTextCentered(display.width() / 2, 53, tmp); + display.drawTextCentered(display.width() / 2, row3_y, tmp); } + + display.setColor(DisplayDriver::YELLOW); + // Keep labels short enough to fully fit 128px width with centered rendering. + const char* labels[2] = {"Hold: AP/Client", "Triple click: BLE"}; + const int label_turn_ms = 3500; + int idx = (millis() / label_turn_ms) % 2; + display.drawTextCentered(display.width() / 2, footer_y, labels[idx]); } else if (_page == HomePage::ADVERT_ACTION) { display.setColor(DisplayDriver::GREEN); display.drawXbm((display.width() - 32) / 2, 18, advert_icon, 32, 32); @@ -521,6 +548,29 @@ class HomeScreen : public UIScreen { if (c == KEY_ENTER && _page == HomePage::WIFI_TRANSPORT) { #if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) if (_task->consumeLongPress()) { + bool success = false; + uint8_t next_mode = _node_prefs->wifi_mode == 0 ? 1 : 0; + if (auto* transport = CompanionTransportInterface::instance()) { + auto mode = next_mode == 0 + ? CompanionTransportInterface::WIFI_MODE_AP_ONLY + : CompanionTransportInterface::WIFI_MODE_STA_CLIENT; + success = transport->setWifiMode(mode); + if (success) _node_prefs->wifi_mode = next_mode; + } + if (success) { + the_mesh.savePrefs(); + _task->notify(UIEventType::ack); + _task->showAlert(next_mode == 0 ? "WiFi mode: AP" : "WiFi mode: Client", 1400); + } else { + _task->showAlert("WiFi mode switch failed", 1400); + } + } +#endif + return true; + } + if (c == KEY_SELECT && _page == HomePage::WIFI_TRANSPORT) { +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) + { bool success = false; if (auto* transport = CompanionTransportInterface::instance()) { success = transport->setWirelessMode(CompanionTransportInterface::WIRELESS_MODE_BLE); @@ -922,7 +972,10 @@ void UITask::loop() { #endif if (c != 0 && curr) { - curr->handleInput(c); + bool handled = curr->handleInput(c); + if (!handled && c == KEY_SELECT) { + toggleBuzzer(); + } _pending_long_press = false; // one-shot flag _auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer _next_refresh = 100; // trigger refresh @@ -1057,8 +1110,6 @@ char UITask::handleDoubleClick(char c) { char UITask::handleTripleClick(char c) { MESH_DEBUG_PRINTLN("UITask: triple click triggered"); checkDisplayOn(c); - toggleBuzzer(); - c = 0; return c; } From dd166d2165210b1f13bec943a3d34ed739f23345 Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Sun, 8 Mar 2026 17:37:56 -0400 Subject: [PATCH 14/20] heltec_v2: add all-transports companion target and build docs --- README.md | 14 +++++++++++++- variants/heltec_v2/platformio.ini | 23 +++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d914f3d6c..c32baf4d2 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ For developers; The Simple Secure Chat example can be interacted with through the Serial Monitor in Visual Studio Code, or with a Serial USB Terminal on Android. -## Build And Flash This Fork (Heltec V3) +## Build And Flash This Fork (Heltec V3 / Heltec V2) If you want to build and flash this fork directly from terminal: @@ -69,12 +69,24 @@ Build firmware: ~/.platformio/penv/bin/pio run -e Heltec_v3_companion_radio_tcp_usb_ble ``` +For Heltec V2: + +```bash +~/.platformio/penv/bin/pio run -e Heltec_v2_companion_radio_tcp_usb_ble +``` + Flash firmware (USB serial example): ```bash ~/.platformio/penv/bin/pio run -e Heltec_v3_companion_radio_tcp_usb_ble -t upload --upload-port /dev/ttyUSB0 ``` +For Heltec V2: + +```bash +~/.platformio/penv/bin/pio run -e Heltec_v2_companion_radio_tcp_usb_ble -t upload --upload-port /dev/ttyUSB0 +``` + Optional serial companion check: ```bash diff --git a/variants/heltec_v2/platformio.ini b/variants/heltec_v2/platformio.ini index f8cc93608..515fa4a92 100644 --- a/variants/heltec_v2/platformio.ini +++ b/variants/heltec_v2/platformio.ini @@ -195,3 +195,26 @@ build_src_filter = ${Heltec_lora32_v2.build_src_filter} lib_deps = ${Heltec_lora32_v2.lib_deps} densaugeo/base64 @ ~1.4.0 + +[env:Heltec_v2_companion_radio_tcp_usb_ble] +extends = Heltec_lora32_v2 +build_flags = + ${Heltec_lora32_v2.build_flags} + -I examples/companion_radio/ui-new + -D DISPLAY_CLASS=SSD1306Display + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=4 + -D BLE_PIN_CODE=123456 + -D COMPANION_ALL_TRANSPORTS + -D OFFLINE_QUEUE_SIZE=64 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_lora32_v2.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_lora32_v2.lib_deps} + densaugeo/base64 @ ~1.4.0 From 0d0b31eb52fa794f9e2b08764baf04b039159623 Mon Sep 17 00:00:00 2001 From: Robert Ekl Date: Sun, 8 Mar 2026 21:31:08 -0600 Subject: [PATCH 15/20] docs: sync CLI and payload docs with implementation --- docs/cli_commands.md | 83 ++++++++++++++++++++++++++++++++++++-------- docs/faq.md | 10 +++--- docs/payloads.md | 35 ++++++++----------- 3 files changed, 90 insertions(+), 38 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 7ade97066..68e8fb9ef 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -53,7 +53,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore - `time ` **Parameters:** -- `epoc_seconds`: Unix epoc time +- `epoch_seconds`: Unix epoch time --- @@ -63,6 +63,12 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +### Send a zero-hop advert +**Usage:** +- `advert.zerohop` + +--- + ### Start an Over-The-Air (OTA) firmware update **Usage:** - `start ota` @@ -136,7 +142,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- -### End capture of rx log to node sotrage +### End capture of rx log to node storage **Usage:** `log stop` --- @@ -200,7 +206,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **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. Referr to the node's manual for the correct setting to use. **Setting a value too high may violate the laws in your country.** +**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.** --- @@ -230,6 +236,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Default:** `869.525` **Note:** Requires reboot to apply +**Serial Only:** `set freq ` ### System @@ -287,7 +294,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Serial Only:** - `get prv.key`: Yes -- `set prv.key`: No +- `set prv.key`: Yes **Note:** Requires reboot to take effect after setting @@ -295,16 +302,16 @@ This document provides an overview of CLI commands that can be sent to MeshCore #### Change this node's admin password **Usage:** -- `password ` +- `password ` **Parameters:** -- `password`: Admin password +- `new_password`: New admin password **Set by build flag:** `ADMIN_PASSWORD` **Default:** `password` -**Note:** Echoed back for confirmation +**Note:** Command reply echoes the updated password for confirmation. **Note:** Any node using this password will be added to the admin ACL list. @@ -354,13 +361,25 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### View this node's public key +**Usage:** `get public.key` + +--- + +#### View this node's configured role +**Usage:** `get role` + +--- + #### View or change this node's power saving flag (Repeater Only) **Usage:** -- `powersaving ` - `powersaving` +- `powersaving on` +- `powersaving off` **Parameters:** -- `state`: `on`|`off` +- `on`: enable power saving +- `off`: disable power saving **Default:** `on` @@ -769,7 +788,7 @@ region save - `gps advert ` **Parameters:** -- `policy`: `none`|`shared`|`prefs` +- `policy`: `none`|`share`|`prefs` - `none`: don't include location in adverts - `share`: share gps location (from SensorManager) - `prefs`: location stored in node's lat and lon settings @@ -803,6 +822,11 @@ region save ### Bridge (When bridge support is compiled in) +#### View the compiled bridge type +**Usage:** `get bridge.type` + +--- + #### View or change the bridge enabled flag **Usage:** - `get bridge.enabled` @@ -840,10 +864,10 @@ region save **Parameters:** - `source`: - - `rx`: bridges received packets - - `tx`: bridges transmitted packets + - `logRx`: bridges received packets + - `logTx`: bridges transmitted packets -**Default:** `tx` +**Default:** `logTx` --- @@ -875,8 +899,39 @@ region save - `set bridge.secret ` **Parameters:** -- `secret`: 16-character encryption secret +- `secret`: ESP-NOW bridge secret, up to 15 characters **Default:** Varies by board --- + +#### View the bootloader version (nRF52 only) +**Usage:** `get bootloader.ver` + +--- + +#### View power management support +**Usage:** `get pwrmgt.support` + +--- + +#### View the current power source +**Usage:** `get pwrmgt.source` + +**Note:** Returns an error on boards without power management support. + +--- + +#### View the boot reset and shutdown reasons +**Usage:** `get pwrmgt.bootreason` + +**Note:** Returns an error on boards without power management support. + +--- + +#### View the boot voltage +**Usage:** `get pwrmgt.bootmv` + +**Note:** Returns an error on boards without power management support. + +--- diff --git a/docs/faq.md b/docs/faq.md index 220b8971c..530f97013 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -221,11 +221,11 @@ MeshCore allows you to manually broadcast your name, position and public encrypt * Zero hop means your advert is broadcasted out to anyone that can hear it, and that's it. * Flooded means it's broadcasted out and then repeated by all the repeaters that hear it. -MeshCore clients only advertise themselves when the user initiates it. A repeater sends a flood advert once every 3 hours by default. This interval can be configured using the following command: +MeshCore clients only advertise themselves when the user initiates it. A repeater sends a flood advert once every 12 hours by default. This interval can be configured using the following command: -`set advert.interval {minutes}` +`set flood.advert.interval {hours}` -As of Aug 20 2025, a pending PR on github will change the flood advert to 12 hours to minimize airtime utilization caused by repeaters' flood adverts. +The separate `set advert.interval {minutes}` command controls the local zero-hop advert timer. ### 2.5. Q: Is there a hop limit? @@ -260,7 +260,9 @@ Repeater or room server can be administered with one of the options below: ### 3.2. Q: Do I need to set the location for a repeater? **A:** While not required, with location set for a repeater it will show up on the MeshCore map in the future. Set location with the following command: -`set lat set long ` +`set lat ` + +`set lon ` You can get the latitude and longitude from Google Maps by right-clicking the location you are at on the map. diff --git a/docs/payloads.md b/docs/payloads.md index 3648b6557..15fec7578 100644 --- a/docs/payloads.md +++ b/docs/payloads.md @@ -90,23 +90,17 @@ Returned path messages provide a description of the route a packet took from the ## Request -| Field | Size (bytes) | Description | -|--------------|-----------------|----------------------------| -| timestamp | 4 | send time (unix timestamp) | -| request type | 1 | see below | -| request data | rest of payload | depends on request type | +| Field | Size (bytes) | Description | +|--------------|-----------------|------------------------------------------| +| timestamp | 4 | sender time (unix timestamp) | +| request data | rest of payload | application-defined request payload body | -Request type +For the common chat/server helpers in `BaseChatMesh`, the current request type values are: | Value | Name | Description | |--------|----------------------|---------------------------------------| | `0x01` | get stats | get stats of repeater or room server | -| `0x02` | keepalive | (deprecated) | -| `0x03` | get telemetry data | TODO | -| `0x04` | get min,max,avg data | sensor nodes - get min, max, average for given time span | -| `0x05` | get access list | get node's approved access list | -| `0x06` | get neighbors | get repeater node's neighbors | -| `0x07` | get owner info | get repeater firmware-ver/name/owner info | +| `0x02` | keepalive | keep-alive request used for maintained connections | ### Get stats @@ -133,35 +127,36 @@ Gets information about the node, possibly including the following: ### Get telemetry data -Request data about sensors on the node, including battery level. +Not defined in `BaseChatMesh`. Sensor- and application-specific request payloads may be implemented by higher-level firmware. ### Get Telemetry -TODO +Not defined in `BaseChatMesh`. ### Get Min/Max/Ave (Sensor nodes) -TODO +Not defined in `BaseChatMesh`. ### Get Access List -TODO +Not defined in `BaseChatMesh`. ### Get Neighors -TODO +Not defined in `BaseChatMesh`. ### Get Owner Info -TODO +Not defined in `BaseChatMesh`. ## Response | Field | Size (bytes) | Description | |---------|-----------------|-------------| -| tag | 4 | TODO | -| content | rest of payload | TODO | +| content | rest of payload | application-defined response body | + +Response contents are opaque application data. There is no single generic response envelope beyond the encrypted payload wrapper shown above. ## Plain text message From 6677b409540843a1b4b00b11515d11f8d5357d78 Mon Sep 17 00:00:00 2001 From: Robert Ekl Date: Sun, 8 Mar 2026 21:31:08 -0600 Subject: [PATCH 16/20] docs: sync companion and kiss protocol docs --- docs/companion_protocol.md | 176 ++++++++++++------------------------ docs/kiss_modem_protocol.md | 6 +- 2 files changed, 59 insertions(+), 123 deletions(-) diff --git a/docs/companion_protocol.md b/docs/companion_protocol.md index 9d45b59ef..11ba0ab24 100644 --- a/docs/companion_protocol.md +++ b/docs/companion_protocol.md @@ -1,6 +1,6 @@ # Companion Protocol -- **Last Updated**: 2026-01-03 +- **Last Updated**: 2026-03-08 - **Protocol Version**: Companion Firmware v1.12.0+ > NOTE: This document is still in development. Some information may be inaccurate. @@ -100,7 +100,7 @@ When writing commands to the RX characteristic, specify the write type: ### MTU (Maximum Transmission Unit) -The default BLE MTU is 23 bytes (20 bytes payload). For larger commands like `SET_CHANNEL` (66 bytes), you may need to: +The default BLE MTU is 23 bytes (20 bytes payload). For larger commands like `SET_CHANNEL` (50 bytes), you may need to: 1. **Request Larger MTU**: Request MTU of 512 bytes if supported - Android: `gatt.requestMtu(512)` @@ -167,16 +167,16 @@ The first byte indicates the packet type (see [Response Parsing](#response-parsi **Command Format**: ``` Byte 0: 0x01 -Byte 1: 0x03 -Bytes 2-10: "mccli" (ASCII, null-padded to 9 bytes) +Bytes 1-7: Reserved (currently ignored by firmware) +Bytes 8+: Application name (UTF-8, optional) ``` **Example** (hex): ``` -01 03 6d 63 63 6c 69 00 00 00 00 +01 00 00 00 00 00 00 00 6d 63 63 6c 69 ``` -**Response**: `PACKET_OK` (0x00) +**Response**: `PACKET_SELF_INFO` (0x05) --- @@ -216,8 +216,6 @@ Byte 1: Channel Index (0-7) **Response**: `PACKET_CHANNEL_INFO` (0x12) with channel details -**Note**: The device does not return channel secrets for security reasons. Store secrets locally when creating channels. - --- ### 4. Set Channel @@ -229,10 +227,10 @@ Byte 1: Channel Index (0-7) Byte 0: 0x20 Byte 1: Channel Index (0-7) Bytes 2-33: Channel Name (32 bytes, UTF-8, null-padded) -Bytes 34-65: Secret (32 bytes) +Bytes 34-49: Secret (16 bytes) ``` -**Total Length**: 66 bytes +**Total Length**: 50 bytes **Channel Index**: - Index 0: Reserved for public channels (no secret) @@ -243,16 +241,18 @@ Bytes 34-65: Secret (32 bytes) - Maximum 32 bytes - Padded with null bytes (0x00) if shorter -**Secret Field** (32 bytes): -- For **private channels**: 32-byte secret +**Secret Field** (16 bytes): +- For **private channels**: 16-byte secret - For **public channels**: All zeros (0x00) **Example** (create channel "YourChannelName" at index 1 with secret): ``` 20 01 53 4D 53 00 00 ... (name padded to 32 bytes) - [32 bytes of secret] + [16 bytes of secret] ``` +**Note**: The 32-byte secret variant is unsupported and returns `PACKET_ERROR`. + **Response**: `PACKET_OK` (0x00) on success, `PACKET_ERROR` (0x01) on failure --- @@ -304,9 +304,9 @@ Byte 0: 0x0A --- -### 7. Get Battery +### 7. Get Battery and Storage -**Purpose**: Query device battery level. +**Purpose**: Query device battery voltage and storage usage. **Command Format**: ``` @@ -318,7 +318,7 @@ Byte 0: 0x14 14 ``` -**Response**: `PACKET_BATTERY` (0x0C) with battery percentage +**Response**: `PACKET_BATTERY` (0x0C) with battery millivolts and storage information --- @@ -346,7 +346,7 @@ Byte 0: 0x14 1. **Set Channel**: - Fetch all channel slots, and find one with empty name and all-zero secret - Generate or provide a 16-byte secret - - Send `CMD_SET_CHANNEL` with name and secret + - Send `CMD_SET_CHANNEL` with name and a 16-byte secret 2. **Get Channel**: - Send `CMD_GET_CHANNEL` with channel index - Parse `RESP_CODE_CHANNEL_INFO` response @@ -360,7 +360,7 @@ Byte 0: 0x14 ### Receiving Messages -Messages are received via the RX characteristic (notifications). The device sends: +Messages are received via the TX characteristic (notifications). The device sends: 1. **Channel Messages**: - `PACKET_CHANNEL_MSG_RECV` (0x08) - Standard format @@ -544,10 +544,10 @@ Byte 1: Error code (optional) Byte 0: 0x12 Byte 1: Channel Index Bytes 2-33: Channel Name (32 bytes, null-terminated) -Bytes 34-65: Secret (32 bytes, but device typically only returns 20 bytes total) +Bytes 34-49: Secret (16 bytes) ``` -**Note**: The device may not return the full 66-byte packet. Parse what is available. The secret field is typically not returned for security reasons. +**Note**: The device returns the 16-byte channel secret in this response. **PACKET_DEVICE_INFO** (0x0D): ``` @@ -562,6 +562,8 @@ Bytes 4-7: BLE PIN (32-bit little-endian) Bytes 8-19: Firmware Build (12 bytes, UTF-8, null-padded) Bytes 20-59: Model (40 bytes, UTF-8, null-padded) Bytes 60-79: Version (20 bytes, UTF-8, null-padded) +Byte 80: Client repeat enabled/preferred (firmware v9+) +Byte 81: Path hash mode (firmware v10+) ``` **Parsing Pseudocode**: @@ -587,9 +589,7 @@ def parse_device_info(data): **PACKET_BATTERY** (0x0C): ``` Byte 0: 0x0C -Bytes 1-2: Battery Level (16-bit little-endian, percentage 0-100) - -Optional (if data size > 3): +Bytes 1-2: Battery Voltage (16-bit little-endian, millivolts) Bytes 3-6: Used Storage (32-bit little-endian, KB) Bytes 7-10: Total Storage (32-bit little-endian, KB) ``` @@ -600,14 +600,12 @@ def parse_battery(data): if len(data) < 3: return None - level = int.from_bytes(data[1:3], 'little') - info = {'level': level} + mv = int.from_bytes(data[1:3], 'little') + info = {'battery_mv': mv} - if len(data) > 3: - used_kb = int.from_bytes(data[3:7], 'little') - total_kb = int.from_bytes(data[7:11], 'little') - info['used_kb'] = used_kb - info['total_kb'] = total_kb + if len(data) >= 11: + info['used_kb'] = int.from_bytes(data[3:7], 'little') + info['total_kb'] = int.from_bytes(data[7:11], 'little') return info ``` @@ -629,7 +627,7 @@ Bytes 48-51: Radio Frequency (32-bit little-endian, divided by 1000.0) Bytes 52-55: Radio Bandwidth (32-bit little-endian, divided by 1000.0) Byte 56: Radio Spreading Factor Byte 57: Radio Coding Rate -Bytes 58+: Device Name (UTF-8, variable length, null-terminated) +Bytes 58+: Device Name (UTF-8, variable length, no null terminator required) ``` **Parsing Pseudocode**: @@ -680,9 +678,9 @@ def parse_self_info(data): **PACKET_MSG_SENT** (0x06): ``` Byte 0: 0x06 -Byte 1: Message Type -Bytes 2-5: Expected ACK (4 bytes, hex) -Bytes 6-9: Suggested Timeout (32-bit little-endian, seconds) +Byte 1: Route Flag (0 = direct, 1 = flood) +Bytes 2-5: Tag / Expected ACK (4 bytes, little-endian) +Bytes 6-9: Suggested Timeout (32-bit little-endian, milliseconds) ``` **PACKET_ACK** (0x82): @@ -710,89 +708,32 @@ Bytes 1-6: ACK Code (6 bytes, hex) **Note**: Error codes may vary by firmware version. Always check byte 1 of `PACKET_ERROR` response. -### Partial Packet Handling - -BLE notifications may arrive in chunks, especially for larger packets. Implement buffering: - -**Implementation**: -```python -class PacketBuffer: - def __init__(self): - self.buffer = bytearray() - self.expected_length = None - - def add_data(self, data): - self.buffer.extend(data) - - # Check if we have a complete packet - if len(self.buffer) >= 1: - packet_type = self.buffer[0] - - # Determine expected length based on packet type - expected = self.get_expected_length(packet_type) - - if expected is not None and len(self.buffer) >= expected: - # Complete packet - packet = bytes(self.buffer[:expected]) - self.buffer = self.buffer[expected:] - return packet - elif expected is None: - # Variable length packet - try to parse what we have - # Some packets have minimum length requirements - if self.can_parse_partial(packet_type): - return self.try_parse_partial() - - return None # Incomplete packet - - def get_expected_length(self, packet_type): - # Fixed-length packets - fixed_lengths = { - 0x00: 5, # PACKET_OK (minimum) - 0x01: 2, # PACKET_ERROR (minimum) - 0x0A: 1, # PACKET_NO_MORE_MSGS - 0x14: 3, # PACKET_BATTERY (minimum) - } - return fixed_lengths.get(packet_type) - - def can_parse_partial(self, packet_type): - # Some packets can be parsed partially - return packet_type in [0x12, 0x08, 0x11, 0x07, 0x10, 0x05, 0x0D] - - def try_parse_partial(self): - # Try to parse with available data - # Return packet if successfully parsed, None otherwise - # This is packet-type specific - pass -``` +### Frame Handling -**Usage**: -```python -buffer = PacketBuffer() +BLE implementations enqueue and deliver one protocol frame per BLE write/notification at the firmware layer. -def on_notification_received(data): - packet = buffer.add_data(data) - if packet: - parse_and_handle_packet(packet) -``` +- Apps should treat each characteristic write/notification as exactly one companion protocol frame +- Apps should still validate frame lengths before parsing +- Future transports or firmware revisions may differ, so avoid assuming fixed payload sizes for variable-length responses ### Response Handling 1. **Command-Response Pattern**: - - Send command via TX characteristic - - Wait for response via RX characteristic (notification) + - Send command via RX characteristic + - Wait for response via TX characteristic (notification) - Match response to command using sequence numbers or command type - Handle timeout (typically 5 seconds) - Use command queue to prevent concurrent commands 2. **Asynchronous Messages**: - - Device may send messages at any time via RX characteristic + - Device may send messages at any time via TX characteristic - Handle `PACKET_MESSAGES_WAITING` (0x83) by polling `GET_MESSAGE` command - Parse incoming messages and route to appropriate handlers - - Buffer partial packets until complete + - Validate frame length before decoding 3. **Response Matching**: - Match responses to commands by expected packet type: - - `APP_START` → `PACKET_OK` + - `APP_START` → `PACKET_SELF_INFO` - `DEVICE_QUERY` → `PACKET_DEVICE_INFO` - `GET_CHANNEL` → `PACKET_CHANNEL_INFO` - `SET_CHANNEL` → `PACKET_OK` or `PACKET_ERROR` @@ -825,16 +766,16 @@ device = scan_for_device("MeshCore") gatt = connect_to_device(device) # 3. Discover services and characteristics -service = discover_service(gatt, "0000ff00-0000-1000-8000-00805f9b34fb") -rx_char = discover_characteristic(service, "0000ff01-0000-1000-8000-00805f9b34fb") -tx_char = discover_characteristic(service, "0000ff02-0000-1000-8000-00805f9b34fb") +service = discover_service(gatt, "6E400001-B5A3-F393-E0A9-E50E24DCCA9E") +rx_char = discover_characteristic(service, "6E400002-B5A3-F393-E0A9-E50E24DCCA9E") +tx_char = discover_characteristic(service, "6E400003-B5A3-F393-E0A9-E50E24DCCA9E") -# 4. Enable notifications on RX characteristic -enable_notifications(rx_char, on_notification_received) +# 4. Enable notifications on TX characteristic +enable_notifications(tx_char, on_notification_received) # 5. Send AppStart command -send_command(tx_char, build_app_start()) -wait_for_response(PACKET_OK) +send_command(rx_char, build_app_start()) +wait_for_response(PACKET_SELF_INFO) ``` ### Creating a Private Channel @@ -844,21 +785,16 @@ wait_for_response(PACKET_OK) secret_16_bytes = generate_secret(16) # Use CSPRNG secret_hex = secret_16_bytes.hex() -# 2. Expand secret to 32 bytes using SHA-512 -import hashlib -sha512_hash = hashlib.sha512(secret_16_bytes).digest() -secret_32_bytes = sha512_hash[:32] - -# 3. Build SET_CHANNEL command +# 2. Build SET_CHANNEL command channel_name = "YourChannelName" channel_index = 1 # Use 1-7 for private channels -command = build_set_channel(channel_index, channel_name, secret_32_bytes) +command = build_set_channel(channel_index, channel_name, secret_16_bytes) -# 4. Send command -send_command(tx_char, command) +# 3. Send command +send_command(rx_char, command) response = wait_for_response(PACKET_OK) -# 5. Store secret locally (device won't return it) +# 4. Store secret locally store_channel_secret(channel_index, secret_hex) ``` @@ -872,7 +808,7 @@ timestamp = int(time.time()) command = build_channel_message(channel_index, message, timestamp) # 2. Send command -send_command(tx_char, command) +send_command(rx_char, command) response = wait_for_response(PACKET_MSG_SENT) ``` @@ -887,7 +823,7 @@ def on_notification_received(data): handle_channel_message(message) elif packet_type == PACKET_MESSAGES_WAITING: # Poll for messages - send_command(tx_char, build_get_message()) + send_command(rx_char, build_get_message()) ``` --- diff --git a/docs/kiss_modem_protocol.md b/docs/kiss_modem_protocol.md index 6a08614fa..8fbb57e45 100644 --- a/docs/kiss_modem_protocol.md +++ b/docs/kiss_modem_protocol.md @@ -190,7 +190,7 @@ All values little-endian. | Field | Size | Description | |-------|------|-------------| | MAC | 2 bytes | HMAC-SHA256 truncated to 2 bytes | -| Ciphertext | variable | AES-128-CBC encrypted data | +| Ciphertext | variable | AES-128 block-encrypted data with zero padding | ### Airtime (Airtime response) @@ -268,7 +268,7 @@ Data returned in CayenneLPP format. See [CayenneLPP documentation](https://docs. |-----------|-----------| | Identity / Signing / Verification | Ed25519 | | Key Exchange | X25519 (ECDH) | -| Encryption | AES-128-CBC + HMAC-SHA256 (MAC truncated to 2 bytes) | +| Encryption | AES-128 block encryption with zero padding + HMAC-SHA256 (MAC truncated to 2 bytes) | | Hashing | SHA-256 | ## Notes @@ -279,4 +279,4 @@ Data returned in CayenneLPP format. See [CayenneLPP documentation](https://docs. - SNR values in RxMeta are multiplied by 4 for 0.25 dB precision - TxDone is sent as a SetHardware event after each transmission - Standard KISS clients receive only type 0x00 data frames and can safely ignore all SetHardware (0x06) frames -- See [packet_structure.md](./packet_structure.md) for packet format +- See [packet_format.md](./packet_format.md) for packet format From 095f97b3556e94dd8e32d58b2b6db20c73182478 Mon Sep 17 00:00:00 2001 From: Robert Ekl Date: Tue, 10 Mar 2026 22:12:55 -0500 Subject: [PATCH 17/20] set prv.key doc update --- docs/cli_commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 68e8fb9ef..03c7e509f 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -294,7 +294,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Serial Only:** - `get prv.key`: Yes -- `set prv.key`: Yes +- `set prv.key`: No **Note:** Requires reboot to take effect after setting From 9ef3b2b5ca1c9575579cf9eac62a364da6a9fbc5 Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Wed, 11 Mar 2026 15:15:39 -0400 Subject: [PATCH 18/20] Fix companion transport state handling --- .../esp32/CompanionTransportInterface.cpp | 74 ++++++++++++++--- .../esp32/CompanionTransportInterface.h | 7 ++ src/helpers/esp32/SerialBLEInterface.cpp | 82 ++++++++++++------- src/helpers/esp32/SerialBLEInterface.h | 6 ++ variants/heltec_v2/platformio.ini | 6 +- 5 files changed, 131 insertions(+), 44 deletions(-) diff --git a/src/helpers/esp32/CompanionTransportInterface.cpp b/src/helpers/esp32/CompanionTransportInterface.cpp index 66288d19e..e0b7e4152 100644 --- a/src/helpers/esp32/CompanionTransportInterface.cpp +++ b/src/helpers/esp32/CompanionTransportInterface.cpp @@ -5,8 +5,15 @@ CompanionTransportInterface* CompanionTransportInterface::_instance = nullptr; +namespace { +constexpr unsigned long WIFI_STA_RETRY_BASE_MS = 10000; +constexpr unsigned long WIFI_STA_RETRY_MAX_MS = 300000; +constexpr uint8_t WIFI_STA_MAX_RETRIES = 5; +} + CompanionTransportInterface::CompanionTransportInterface() : _isEnabled(false), _wifi_server_started(false), _last_source(0), _mode(WIRELESS_MODE_BLE), _wifi_started(false), + _last_usb_activity_at(0), _wifi_retry_count(0), _wifi_retry_suspended(false), _next_wifi_retry_at(0), _wifi_mode(WIFI_MODE_AP_ONLY), _tcp_port(5000) { _node_name[0] = 0; _wifi_ssid[0] = 0; @@ -16,6 +23,16 @@ CompanionTransportInterface::CompanionTransportInterface() _instance = this; } +bool CompanionTransportInterface::usbSessionActive() const { + return _last_usb_activity_at != 0 && (unsigned long)(millis() - _last_usb_activity_at) < USB_ACTIVITY_TIMEOUT_MS; +} + +void CompanionTransportInterface::resetWifiRetryState() { + _wifi_retry_count = 0; + _wifi_retry_suspended = false; + _next_wifi_retry_at = 0; +} + void CompanionTransportInterface::begin(Stream& usb_serial, const char* ble_prefix, char* node_name, uint32_t ble_pin, uint16_t tcp_port) { _tcp_port = tcp_port; snprintf(_node_name, sizeof(_node_name), "%s", node_name); @@ -29,8 +46,23 @@ void CompanionTransportInterface::begin(HardwareSerial& usb_serial, const char* } void CompanionTransportInterface::connectWifi() { + if (_wifi_ssid[0] == 0) { + _wifi_retry_suspended = true; + _next_wifi_retry_at = 0; + return; + } + WiFi.mode(WIFI_STA); + WiFi.setAutoReconnect(false); WiFi.begin(_wifi_ssid, _wifi_pwd); + + _wifi_retry_count++; + unsigned long retry_delay = WIFI_STA_RETRY_BASE_MS; + if (_wifi_retry_count > 1) { + retry_delay <<= (_wifi_retry_count - 1); + if (retry_delay > WIFI_STA_RETRY_MAX_MS) retry_delay = WIFI_STA_RETRY_MAX_MS; + } + _next_wifi_retry_at = millis() + retry_delay; } void CompanionTransportInterface::startWifiAP() { @@ -50,10 +82,15 @@ void CompanionTransportInterface::startWifiAP() { } void CompanionTransportInterface::startWifi() { - _wifi_started = true; + stopWifi(); + resetWifiRetryState(); + if (_wifi_mode == WIFI_MODE_STA_CLIENT) { + if (_wifi_ssid[0] == 0) return; + _wifi_started = true; connectWifi(); } else { + _wifi_started = true; startWifiAP(); } } @@ -77,6 +114,7 @@ void CompanionTransportInterface::startWireless() { } else { _wifi.disable(); stopWifi(); + resetWifiRetryState(); _ble.enable(); } } @@ -85,6 +123,7 @@ void CompanionTransportInterface::stopWireless() { _ble.disable(); _wifi.disable(); stopWifi(); + resetWifiRetryState(); } bool CompanionTransportInterface::setWirelessMode(WirelessMode mode, bool persist_only) { @@ -107,6 +146,7 @@ bool CompanionTransportInterface::setWifiCredentials(const char* ssid, const cha if (strlen(ssid) > 32 || strlen(pwd) > 64) return false; snprintf(_wifi_ssid, sizeof(_wifi_ssid), "%s", ssid); snprintf(_wifi_pwd, sizeof(_wifi_pwd), "%s", pwd); + resetWifiRetryState(); if (_isEnabled && _mode == WIRELESS_MODE_TCP && _wifi_mode == WIFI_MODE_STA_CLIENT) { startWifi(); @@ -119,6 +159,7 @@ bool CompanionTransportInterface::setWifiMode(WifiMode mode) { return false; } _wifi_mode = mode; + resetWifiRetryState(); if (_isEnabled && _mode == WIRELESS_MODE_TCP) { startWifi(); } @@ -156,7 +197,8 @@ bool CompanionTransportInterface::setTcpPort(uint16_t port) { void CompanionTransportInterface::enable() { if (_isEnabled) return; _isEnabled = true; - _last_source = 0; + _last_source = 0xFF; + _last_usb_activity_at = 0; _usb.enable(); startWireless(); } @@ -170,8 +212,8 @@ void CompanionTransportInterface::disable() { bool CompanionTransportInterface::isConnected() const { if (_last_source == 2) return _wifi.isConnected(); if (_last_source == 1) return _ble.isConnected(); - if (_last_source == 0) return _usb.isConnected(); - return _usb.isConnected() || _ble.isConnected() || _wifi.isConnected(); + if (_last_source == 0) return usbSessionActive(); + return _ble.isConnected() || _wifi.isConnected() || usbSessionActive(); } bool CompanionTransportInterface::isWriteBusy() const { @@ -181,6 +223,7 @@ bool CompanionTransportInterface::isWriteBusy() const { } size_t CompanionTransportInterface::writeFrame(const uint8_t src[], size_t len) { + if (_last_source == 0) _last_usb_activity_at = millis(); if (_last_source == 2 && _wifi.isConnected()) return _wifi.writeFrame(src, len); if (_last_source == 1 && _ble.isConnected()) return _ble.writeFrame(src, len); return _usb.writeFrame(src, len); @@ -192,18 +235,25 @@ size_t CompanionTransportInterface::checkRecvFrame(uint8_t dest[]) { size_t usb_len = _usb.checkRecvFrame(dest); if (usb_len > 0) { _last_source = 0; + _last_usb_activity_at = millis(); return usb_len; } if (_mode == WIRELESS_MODE_TCP) { - static unsigned long last_wifi_retry_at = 0; - if (_wifi_mode == WIFI_MODE_STA_CLIENT && _wifi_ssid[0] != 0 && WiFi.status() != WL_CONNECTED) { - unsigned long now = millis(); - if (last_wifi_retry_at == 0 || (unsigned long)(now - last_wifi_retry_at) >= 10000) { - // Keep trying to reconnect in STA mode, but avoid tight reconnect loops. - WiFi.disconnect(); - connectWifi(); - last_wifi_retry_at = now; + if (_wifi_mode == WIFI_MODE_STA_CLIENT) { + if (WiFi.status() == WL_CONNECTED) { + resetWifiRetryState(); + } else if (_wifi_started && !_wifi_retry_suspended && _wifi_ssid[0] != 0) { + unsigned long now = millis(); + if ((long)(now - _next_wifi_retry_at) >= 0) { + if (_wifi_retry_count >= WIFI_STA_MAX_RETRIES) { + stopWifi(); + _wifi_retry_suspended = true; + } else { + WiFi.disconnect(true, false); + connectWifi(); + } + } } } diff --git a/src/helpers/esp32/CompanionTransportInterface.h b/src/helpers/esp32/CompanionTransportInterface.h index 95494b002..030968f36 100644 --- a/src/helpers/esp32/CompanionTransportInterface.h +++ b/src/helpers/esp32/CompanionTransportInterface.h @@ -21,6 +21,7 @@ class CompanionTransportInterface : public BaseSerialInterface { private: static CompanionTransportInterface* _instance; + static constexpr unsigned long USB_ACTIVITY_TIMEOUT_MS = 15000; ArduinoSerialInterface _usb; SerialBLEInterface _ble; @@ -31,6 +32,10 @@ class CompanionTransportInterface : public BaseSerialInterface { uint8_t _last_source; // 0=usb,1=ble,2=wifi WirelessMode _mode; bool _wifi_started; + unsigned long _last_usb_activity_at; + uint8_t _wifi_retry_count; + bool _wifi_retry_suspended; + unsigned long _next_wifi_retry_at; char _node_name[33]; char _wifi_ssid[33]; @@ -71,6 +76,8 @@ class CompanionTransportInterface : public BaseSerialInterface { size_t checkRecvFrame(uint8_t dest[]) override; private: + bool usbSessionActive() const; + void resetWifiRetryState(); void startWireless(); void stopWireless(); void startWifi(); diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index dcfa0e1e3..f07ea1c2a 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -10,47 +10,72 @@ #define ADVERT_RESTART_DELAY 1000 // millis -void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code) { - _pin_code = pin_code; - - if (strcmp(name, "@@MAC") == 0) { - uint8_t addr[8]; - memset(addr, 0, sizeof(addr)); - esp_efuse_mac_get_default(addr); - sprintf(name, "%02X%02X%02X%02X%02X%02X", // modify (IN-OUT param) - addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]); - } - char dev_name[32+16]; - sprintf(dev_name, "%s%s", prefix, name); +void SerialBLEInterface::initStack() { + if (_stack_initialized || _device_name[0] == 0) return; - // Create the BLE Device - BLEDevice::init(dev_name); + BLEDevice::init(_device_name); BLEDevice::setSecurityCallbacks(this); BLEDevice::setMTU(MAX_FRAME_SIZE); - BLESecurity sec; - sec.setStaticPIN(pin_code); + BLESecurity sec; + sec.setStaticPIN(_pin_code); sec.setAuthenticationMode(ESP_LE_AUTH_REQ_SC_MITM_BOND); - //BLEDevice::setPower(ESP_PWR_LVL_N8); - - // Create the BLE Server pServer = BLEDevice::createServer(); pServer->setCallbacks(this); - // Create the BLE Service pService = pServer->createService(SERVICE_UUID); - // Create a BLE Characteristic - pTxCharacteristic = pService->createCharacteristic(CHARACTERISTIC_UUID_TX, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY); + pTxCharacteristic = pService->createCharacteristic( + CHARACTERISTIC_UUID_TX, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY); pTxCharacteristic->setAccessPermissions(ESP_GATT_PERM_READ_ENC_MITM); pTxCharacteristic->addDescriptor(new BLE2902()); - BLECharacteristic * pRxCharacteristic = pService->createCharacteristic(CHARACTERISTIC_UUID_RX, BLECharacteristic::PROPERTY_WRITE); + BLECharacteristic * pRxCharacteristic = + pService->createCharacteristic(CHARACTERISTIC_UUID_RX, BLECharacteristic::PROPERTY_WRITE); pRxCharacteristic->setAccessPermissions(ESP_GATT_PERM_WRITE_ENC_MITM); pRxCharacteristic->setCallbacks(this); pServer->getAdvertising()->addServiceUUID(SERVICE_UUID); + _stack_initialized = true; +} + +void SerialBLEInterface::shutdownStack() { + if (!_stack_initialized) return; + + if (pServer != NULL && pServer->getAdvertising() != NULL) { + pServer->getAdvertising()->stop(); + } + if (pServer != NULL && pServer->getConnectedCount() > 0) { + pServer->disconnect(last_conn_id); + } + if (pService != NULL) { + pService->stop(); + } + + BLEDevice::deinit(false); + + pTxCharacteristic = NULL; + pService = NULL; + pServer = NULL; + oldDeviceConnected = deviceConnected = false; + adv_restart_time = 0; + last_conn_id = 0; + _stack_initialized = false; +} + +void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code) { + _pin_code = pin_code; + + if (strcmp(name, "@@MAC") == 0) { + uint8_t addr[8]; + memset(addr, 0, sizeof(addr)); + esp_efuse_mac_get_default(addr); + sprintf(name, "%02X%02X%02X%02X%02X%02X", // modify (IN-OUT param) + addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]); + } + snprintf(_device_name, sizeof(_device_name), "%s%s", prefix, name); + initStack(); } // -------- BLESecurityCallbacks methods @@ -132,6 +157,9 @@ void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic, esp_ble_gat void SerialBLEInterface::enable() { if (_isEnabled) return; + initStack(); + if (!_stack_initialized || pService == NULL || pServer == NULL) return; + _isEnabled = true; clearBuffers(); @@ -151,12 +179,8 @@ void SerialBLEInterface::disable() { _isEnabled = false; BLE_DEBUG_PRINTLN("SerialBLEInterface::disable"); - - pServer->getAdvertising()->stop(); - pServer->disconnect(last_conn_id); - pService->stop(); - oldDeviceConnected = deviceConnected = false; - adv_restart_time = 0; + clearBuffers(); + shutdownStack(); } size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) { diff --git a/src/helpers/esp32/SerialBLEInterface.h b/src/helpers/esp32/SerialBLEInterface.h index 965e90fd1..a65fbec18 100644 --- a/src/helpers/esp32/SerialBLEInterface.h +++ b/src/helpers/esp32/SerialBLEInterface.h @@ -13,10 +13,12 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE bool deviceConnected; bool oldDeviceConnected; bool _isEnabled; + bool _stack_initialized; uint16_t last_conn_id; uint32_t _pin_code; unsigned long _last_write; unsigned long adv_restart_time; + char _device_name[49]; struct Frame { uint8_t len; @@ -30,6 +32,8 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE Frame send_queue[FRAME_QUEUE_SIZE]; void clearBuffers() { recv_queue_len = 0; send_queue_len = 0; } + void initStack(); + void shutdownStack(); protected: // BLESecurityCallbacks methods @@ -56,9 +60,11 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE oldDeviceConnected = false; adv_restart_time = 0; _isEnabled = false; + _stack_initialized = false; _last_write = 0; last_conn_id = 0; send_queue_len = recv_queue_len = 0; + _device_name[0] = 0; } /** diff --git a/variants/heltec_v2/platformio.ini b/variants/heltec_v2/platformio.ini index 515fa4a92..415eb9f50 100644 --- a/variants/heltec_v2/platformio.ini +++ b/variants/heltec_v2/platformio.ini @@ -202,11 +202,11 @@ build_flags = ${Heltec_lora32_v2.build_flags} -I examples/companion_radio/ui-new -D DISPLAY_CLASS=SSD1306Display - -D MAX_CONTACTS=100 - -D MAX_GROUP_CHANNELS=4 + -D MAX_CONTACTS=70 + -D MAX_GROUP_CHANNELS=8 -D BLE_PIN_CODE=123456 -D COMPANION_ALL_TRANSPORTS - -D OFFLINE_QUEUE_SIZE=64 + -D OFFLINE_QUEUE_SIZE=200 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v2.build_src_filter} From d07824bccd736ea8ef9bca350c88357a45b70d04 Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Wed, 11 Mar 2026 15:43:05 -0400 Subject: [PATCH 19/20] Hide BLE PIN outside BLE mode --- examples/companion_radio/ui-new/UITask.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 11f492b05..ee1729c5d 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -257,7 +257,7 @@ class HomeScreen : public UIScreen { display.setTextSize(1); display.drawTextCentered(display.width() / 2, 43, "< Connected >"); - } else if (the_mesh.getBLEPin() != 0) { // BT pin + } else if (_node_prefs->transport_mode == 0 && the_mesh.getBLEPin() != 0) { // BT pin display.setColor(DisplayDriver::RED); display.setTextSize(2); sprintf(tmp, "Pin:%d", the_mesh.getBLEPin()); From c523b27eaf3bacf81d0e7b11665ed653d101ec90 Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Wed, 11 Mar 2026 18:34:24 -0400 Subject: [PATCH 20/20] Ficx icon overlap --- docs/heltec_v3_button_actions.md | 9 +++--- examples/companion_radio/ui-new/UITask.cpp | 32 +++++++++++----------- examples/companion_radio/ui-new/icons.h | 2 +- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/docs/heltec_v3_button_actions.md b/docs/heltec_v3_button_actions.md index 5ce91f1ab..55f7ffb5a 100644 --- a/docs/heltec_v3_button_actions.md +++ b/docs/heltec_v3_button_actions.md @@ -1,6 +1,7 @@ # Heltec V3 Button Actions This page documents current button behavior for `Heltec_v3_companion_radio_tcp_usb_ble` UI flows. +The same mapping also applies to `Heltec_v2_companion_radio_tcp_usb_ble`, which uses the same `examples/companion_radio/ui-new` button/UI logic. ## Global Click Mapping @@ -24,11 +25,11 @@ On single-button Heltec V3 builds: ### WiFi Transport Page -- Long press: toggle WiFi mode (`AP <-> Client`). -- Triple click: switch transport back to BLE. +- Long press: switch transport back to BLE. +- Triple click: toggle WiFi mode (`AP <-> Client`). - UI footer alternates/scrolls usage hints: - - `Long press: AP/Client` - - `Triple click: BLE` + - `Hold: BLE` + - `Triple: AP/Client` ### Advert Page diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index ee1729c5d..a43d9515d 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -309,12 +309,12 @@ class HomeScreen : public UIScreen { display.print(tmp); } else if (_page == HomePage::BLE_TRANSPORT) { display.setColor(DisplayDriver::GREEN); - display.drawTextCentered(display.width() / 2, 20, "BLE transport"); - display.drawXbm((display.width() - 32) / 2, 28, + display.drawTextCentered(display.width() / 2, 16, "BLE transport"); + display.drawXbm((display.width() - 32) / 2, 24, _task->isSerialEnabled() ? bluetooth_on : bluetooth_off, 32, 32); display.setTextSize(1); - display.drawTextCentered(display.width() / 2, 64 - 11, "Long press: WiFi"); + display.drawTextCentered(display.width() / 2, 56, "Long press: WiFi"); } else if (_page == HomePage::WIFI_TRANSPORT) { const int row1_y = 28; const int row2_y = 38; @@ -375,7 +375,7 @@ class HomeScreen : public UIScreen { display.setColor(DisplayDriver::YELLOW); // Keep labels short enough to fully fit 128px width with centered rendering. - const char* labels[2] = {"Hold: AP/Client", "Triple click: BLE"}; + const char* labels[2] = {"Hold: BLE", "Triple: AP/Client"}; const int label_turn_ms = 3500; int idx = (millis() / label_turn_ms) % 2; display.drawTextCentered(display.width() / 2, footer_y, labels[idx]); @@ -549,20 +549,16 @@ class HomeScreen : public UIScreen { #if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) if (_task->consumeLongPress()) { bool success = false; - uint8_t next_mode = _node_prefs->wifi_mode == 0 ? 1 : 0; if (auto* transport = CompanionTransportInterface::instance()) { - auto mode = next_mode == 0 - ? CompanionTransportInterface::WIFI_MODE_AP_ONLY - : CompanionTransportInterface::WIFI_MODE_STA_CLIENT; - success = transport->setWifiMode(mode); - if (success) _node_prefs->wifi_mode = next_mode; + success = transport->setWirelessMode(CompanionTransportInterface::WIRELESS_MODE_BLE); + if (success) _node_prefs->transport_mode = 0; } if (success) { the_mesh.savePrefs(); _task->notify(UIEventType::ack); - _task->showAlert(next_mode == 0 ? "WiFi mode: AP" : "WiFi mode: Client", 1400); + _task->showAlert("BLE enabled", 1200); } else { - _task->showAlert("WiFi mode switch failed", 1400); + _task->showAlert("Transport switch failed", 1200); } } #endif @@ -572,16 +568,20 @@ class HomeScreen : public UIScreen { #if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) { bool success = false; + uint8_t next_mode = _node_prefs->wifi_mode == 0 ? 1 : 0; if (auto* transport = CompanionTransportInterface::instance()) { - success = transport->setWirelessMode(CompanionTransportInterface::WIRELESS_MODE_BLE); - if (success) _node_prefs->transport_mode = 0; + auto mode = next_mode == 0 + ? CompanionTransportInterface::WIFI_MODE_AP_ONLY + : CompanionTransportInterface::WIFI_MODE_STA_CLIENT; + success = transport->setWifiMode(mode); + if (success) _node_prefs->wifi_mode = next_mode; } if (success) { the_mesh.savePrefs(); _task->notify(UIEventType::ack); - _task->showAlert("BLE enabled", 1200); + _task->showAlert(next_mode == 0 ? "WiFi mode: AP" : "WiFi mode: Client", 1400); } else { - _task->showAlert("Transport switch failed", 1200); + _task->showAlert("WiFi mode switch failed", 1400); } } #endif diff --git a/examples/companion_radio/ui-new/icons.h b/examples/companion_radio/ui-new/icons.h index cbe237902..402552728 100644 --- a/examples/companion_radio/ui-new/icons.h +++ b/examples/companion_radio/ui-new/icons.h @@ -119,4 +119,4 @@ static const uint8_t advert_icon[] = { static const uint8_t muted_icon[] = { 0x20, 0x6a, 0xea, 0xe4, 0xe4, 0xea, 0x6a, 0x20 -}; \ No newline at end of file +};