From e749010bbc9ab61215c3d73d3e35d60058d23929 Mon Sep 17 00:00:00 2001 From: entr0p1 <1475255+entr0p1@users.noreply.github.com> Date: Mon, 17 Aug 2026 23:01:52 +1000 Subject: [PATCH 1/3] Duplicate MAX_CLIENTS definition MAX_CLIENTS is defined in both src/helpers/ClientACL.h and examples/simple_repeater/MyMesh.h. Appears it was centralised in the former header file some time ago. Both are included in some places and, depending on which order they're in, either value can win. This change drops the duplicate entry from the repeater firmware and bumps the central limit up to 32 (per the original repeater firmware value). Changes: - Remove MAX_CLIENTS from repeater MyMesh.h - Increase MAX_CLIENTS limit in ClientACL.h to 32 --- examples/simple_repeater/MyMesh.h | 4 ---- src/helpers/ClientACL.h | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 04bd4fb928..cac6c4a281 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -59,10 +59,6 @@ struct RepeaterStats { uint32_t n_recv_errors; }; -#ifndef MAX_CLIENTS - #define MAX_CLIENTS 32 -#endif - struct NeighbourInfo { mesh::Identity id; uint32_t advert_timestamp; diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index b758f7068d..e065446476 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -34,7 +34,7 @@ struct ClientInfo { }; #ifndef MAX_CLIENTS - #define MAX_CLIENTS 20 + #define MAX_CLIENTS 32 #endif class ClientACL { From 52f0362c09aec05323803cada70f17c8efb83eee Mon Sep 17 00:00:00 2001 From: Florent Date: Thu, 20 Aug 2026 21:17:17 -0400 Subject: [PATCH 2/3] ui: repeater discover screen --- examples/companion_radio/MyMesh.cpp | 50 ++++++++++++++++++++ examples/companion_radio/MyMesh.h | 18 ++++++++ examples/companion_radio/ui-new/UITask.cpp | 53 +++++++++++++++++++++- 3 files changed, 120 insertions(+), 1 deletion(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index fdece48290..01512b163a 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -134,6 +134,10 @@ #define ERR_CODE_FILE_IO_ERROR 5 #define ERR_CODE_ILLEGAL_ARG 6 +// Copied from simple_repeater (could probably be shared) +#define CTL_TYPE_NODE_DISCOVER_REQ 0x80 +#define CTL_TYPE_NODE_DISCOVER_RESP 0x90 + #define MAX_SIGN_DATA_LEN (8 * 1024) // 8K // Auto-add config bitmask @@ -403,6 +407,51 @@ int MyMesh::getRecentlyHeard(AdvertPath dest[], int max_num) { return max_num; } +int MyMesh::getDiscoveredNodes(DiscoveredNode nodes[], int max_num) { + if (max_num > DISCOVERED_NODES_TABLE_SIZE) max_num = DISCOVERED_NODES_TABLE_SIZE; + if (max_num > disc_nodes_count) max_num = disc_nodes_count; + + for (int i = 0; i < max_num; i++) { + nodes[i] = discovered_nodes[i]; + } + return max_num; +} + +bool MyMesh::requestRepeatersDiscovery() { + uint8_t cmd_bytes[6]; + cmd_bytes[0] = CTL_TYPE_NODE_DISCOVER_REQ | 1; // DISCOVER_REQ | prefix only + cmd_bytes[1] = 0xFF; // Repeaters + getRNG()->random(&cmd_bytes[2], 4); // tag + disc_nodes_count = 0; + disc_node_req_tag = *((uint32_t*)&cmd_bytes[2]); + mesh::Packet* req = createControlData(cmd_bytes, sizeof(cmd_bytes)); + if (req) { + sendZeroHop(req); + return true; + } + return false; +} + +void MyMesh::checkControlDataForPendingDiscovery(uint8_t payload[], size_t p_len) { + if ((p_len < 12) + || (payload[0] & 0xF0 != CTL_TYPE_NODE_DISCOVER_RESP) + || (disc_nodes_count >= DISCOVERED_NODES_TABLE_SIZE) + || (memcmp(&payload[2], &disc_node_req_tag, 4))) { + return; + } + memcpy(&discovered_nodes[disc_nodes_count].pubkey_prefix, &payload[6], 8); + discovered_nodes[disc_nodes_count].type = payload[0] & 0xF; + discovered_nodes[disc_nodes_count].snr_out = ((int8_t)payload[1]) / 4.0; + discovered_nodes[disc_nodes_count].snr_in = _radio->getLastSNR(); + ContactInfo* c = lookupContactByPubKey(&payload[6], 8); + if (c != NULL) { + strncpy(discovered_nodes[disc_nodes_count].name, c->name, 32); + } else { + discovered_nodes[disc_nodes_count].name[0] = 0; + } + disc_nodes_count ++; +} + void MyMesh::onContactPathUpdated(const ContactInfo &contact) { out_frame[0] = PUSH_CODE_PATH_UPDATED; memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE); @@ -783,6 +832,7 @@ void MyMesh::onControlDataRecv(mesh::Packet *packet) { MESH_DEBUG_PRINTLN("onControlDataRecv(), payload_len too long: %d", packet->payload_len); return; } + checkControlDataForPendingDiscovery(packet->payload, packet->payload_len); int i = 0; out_frame[i++] = PUSH_CODE_CONTROL_DATA; out_frame[i++] = (int8_t)(_radio->getLastSNR() * 4); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 238adada90..848d21c052 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -84,6 +84,14 @@ struct AdvertPath { uint8_t path[MAX_PATH_SIZE]; }; +struct DiscoveredNode { + uint8_t pubkey_prefix[9]; + float snr_in; + float snr_out; + char name[32]; + uint8_t type; +}; + class MyMesh : public BaseChatMesh, public DataStoreHost { public: MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui=NULL); @@ -102,6 +110,9 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { int getRecentlyHeard(AdvertPath dest[], int max_num); + bool requestRepeatersDiscovery(); + int getDiscoveredNodes(DiscoveredNode nodes[], int max_num); + protected: float getAirtimeBudgetFactor() const override; int getInterferenceThreshold() const override; @@ -256,6 +267,13 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { #define ADVERT_PATH_TABLE_SIZE 16 AdvertPath advert_paths[ADVERT_PATH_TABLE_SIZE]; // circular table + + #define DISCOVERED_NODES_TABLE_SIZE 10 + DiscoveredNode discovered_nodes[DISCOVERED_NODES_TABLE_SIZE]; // not circular, latest discovered nodes are not kept + uint32_t disc_node_req_tag = 0; + uint32_t disc_nodes_count = 0; + + void checkControlDataForPendingDiscovery(uint8_t payload[], size_t p_len); }; extern MyMesh the_mesh; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 55755ef172..355250282a 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -24,6 +24,7 @@ #define LONG_PRESS_MILLIS 1200 +// Used both for recent adverts and discovered nodes #ifndef UI_RECENT_LIST_SIZE #define UI_RECENT_LIST_SIZE 4 #endif @@ -102,6 +103,7 @@ class HomeScreen : public UIScreen { #if UI_SENSORS_PAGE == 1 SENSORS, #endif + DISCOVERY, SHUTDOWN, Count // keep as last }; @@ -113,7 +115,9 @@ class HomeScreen : public UIScreen { uint8_t _page; bool _shutdown_init; AdvertPath recent[UI_RECENT_LIST_SIZE]; - + DiscoveredNode discovered[UI_RECENT_LIST_SIZE]; + uint32_t discovery_req_time = 0; + bool discovery_disp_names = true; // by default desplay names if available (removes SNR_O) void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) { // Convert millivolts to percentage @@ -459,6 +463,39 @@ 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::DISCOVERY) { + int count = the_mesh.getDiscoveredNodes(discovered, UI_RECENT_LIST_SIZE); + display.setColor(UIColor::primary_txt); + int y = 20; + for (int i = 0; i < count; i++, y += 11) { + char name[32]; + auto a = &discovered[i]; + if ((a->name[0] == 0) || !discovery_disp_names) { + mesh::Utils::toHex(name, a->pubkey_prefix, 4); + } else { + strncpy(name, a->name, 32); + } + char filtered_name[sizeof(name)]; + char snr_s[12]; + if (strlen(name) <= 8) { // display snr_o + sprintf(snr_s, "%02.1f>%02.1f", a->snr_out, a->snr_in); + } else { + sprintf(snr_s, "%02.1f", a->snr_in); + } + int snr_width = display.getTextWidth(snr_s); + int max_name_width = display.width() - snr_width - 1; + display.translateUTF8ToBlocks(filtered_name, name, sizeof(filtered_name)); + display.drawTextEllipsized(0, y, max_name_width, filtered_name); + display.setCursor(display.width() - snr_width - 1, y); + display.print(snr_s); + } + if (millis() < discovery_req_time + 5000) { + return 1000; // more frequent updates just after req + } else if (count < UI_RECENT_LIST_SIZE -1) { // show only 5 sec after last disc + y = 10 + 11 * UI_RECENT_LIST_SIZE; + display.drawTextCentered(display.width() / 2, y, "discover: " PRESS_LABEL); + } + } else if (_page == HomePage::SHUTDOWN) { display.setColor(UIColor::corp_blue); display.setTextSize(1); @@ -484,6 +521,9 @@ class HomeScreen : public UIScreen { if (_page == HomePage::RECENT) { _task->showAlert("Recent adverts", 800); } + if (_page == HomePage::DISCOVERY) { + _task->showAlert("Repeater disc", 800); + } return true; } if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) { @@ -516,6 +556,17 @@ class HomeScreen : public UIScreen { return true; } #endif + if (c == KEY_ENTER && _page == HomePage::DISCOVERY) { + if (millis() > discovery_req_time + 5000) { // rate limiter + the_mesh.requestRepeatersDiscovery(); + discovery_req_time = millis(); + } + return true; + } + if (c == KEY_SELECT && _page == HomePage::DISCOVERY) { + discovery_disp_names = !discovery_disp_names; + return true; + } if (c == KEY_ENTER && _page == HomePage::SHUTDOWN) { _shutdown_init = true; // need to wait for button to be released return true; From 088ae3caebceb79b7df02e86475ca874eb122ec0 Mon Sep 17 00:00:00 2001 From: Florent Date: Sun, 23 Aug 2026 10:42:00 -0400 Subject: [PATCH 3/3] ui: opt-out for discover screen --- examples/companion_radio/MyMesh.cpp | 4 ++++ examples/companion_radio/MyMesh.h | 12 +++++++++++- examples/companion_radio/ui-new/UITask.cpp | 11 ++++++++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 01512b163a..75566448f6 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -407,6 +407,7 @@ int MyMesh::getRecentlyHeard(AdvertPath dest[], int max_num) { return max_num; } +#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) int MyMesh::getDiscoveredNodes(DiscoveredNode nodes[], int max_num) { if (max_num > DISCOVERED_NODES_TABLE_SIZE) max_num = DISCOVERED_NODES_TABLE_SIZE; if (max_num > disc_nodes_count) max_num = disc_nodes_count; @@ -451,6 +452,7 @@ void MyMesh::checkControlDataForPendingDiscovery(uint8_t payload[], size_t p_len } disc_nodes_count ++; } +#endif void MyMesh::onContactPathUpdated(const ContactInfo &contact) { out_frame[0] = PUSH_CODE_PATH_UPDATED; @@ -832,7 +834,9 @@ void MyMesh::onControlDataRecv(mesh::Packet *packet) { MESH_DEBUG_PRINTLN("onControlDataRecv(), payload_len too long: %d", packet->payload_len); return; } +#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) checkControlDataForPendingDiscovery(packet->payload, packet->payload_len); +#endif int i = 0; out_frame[i++] = PUSH_CODE_CONTROL_DATA; out_frame[i++] = (int8_t)(_radio->getLastSNR() * 4); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 848d21c052..25b5693023 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -84,6 +84,7 @@ struct AdvertPath { uint8_t path[MAX_PATH_SIZE]; }; +#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) struct DiscoveredNode { uint8_t pubkey_prefix[9]; float snr_in; @@ -91,6 +92,7 @@ struct DiscoveredNode { char name[32]; uint8_t type; }; +#endif class MyMesh : public BaseChatMesh, public DataStoreHost { public: @@ -110,8 +112,10 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { int getRecentlyHeard(AdvertPath dest[], int max_num); +#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) bool requestRepeatersDiscovery(); int getDiscoveredNodes(DiscoveredNode nodes[], int max_num); +#endif protected: float getAirtimeBudgetFactor() const override; @@ -268,12 +272,18 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { #define ADVERT_PATH_TABLE_SIZE 16 AdvertPath advert_paths[ADVERT_PATH_TABLE_SIZE]; // circular table - #define DISCOVERED_NODES_TABLE_SIZE 10 +#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) + #ifdef UI_RECENT_LIST_SIZE + #define DISCOVERED_NODES_TABLE_SIZE UI_RECENT_LIST_SIZE + #else + #define DISCOVERED_NODES_TABLE_SIZE 4 + #endif DiscoveredNode discovered_nodes[DISCOVERED_NODES_TABLE_SIZE]; // not circular, latest discovered nodes are not kept uint32_t disc_node_req_tag = 0; uint32_t disc_nodes_count = 0; void checkControlDataForPendingDiscovery(uint8_t payload[], size_t p_len); +#endif }; extern MyMesh the_mesh; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 355250282a..0193ebbc69 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -103,7 +103,9 @@ class HomeScreen : public UIScreen { #if UI_SENSORS_PAGE == 1 SENSORS, #endif +#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) DISCOVERY, +#endif SHUTDOWN, Count // keep as last }; @@ -115,9 +117,11 @@ class HomeScreen : public UIScreen { uint8_t _page; bool _shutdown_init; AdvertPath recent[UI_RECENT_LIST_SIZE]; +#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) DiscoveredNode discovered[UI_RECENT_LIST_SIZE]; uint32_t discovery_req_time = 0; bool discovery_disp_names = true; // by default desplay names if available (removes SNR_O) +#endif void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) { // Convert millivolts to percentage @@ -463,6 +467,7 @@ class HomeScreen : public UIScreen { if (sensors_scroll) sensors_scroll_offset = (sensors_scroll_offset+1)%sensors_nb; else sensors_scroll_offset = 0; #endif +#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) } else if (_page == HomePage::DISCOVERY) { int count = the_mesh.getDiscoveredNodes(discovered, UI_RECENT_LIST_SIZE); display.setColor(UIColor::primary_txt); @@ -495,7 +500,7 @@ class HomeScreen : public UIScreen { y = 10 + 11 * UI_RECENT_LIST_SIZE; display.drawTextCentered(display.width() / 2, y, "discover: " PRESS_LABEL); } - +#endif } else if (_page == HomePage::SHUTDOWN) { display.setColor(UIColor::corp_blue); display.setTextSize(1); @@ -521,9 +526,11 @@ class HomeScreen : public UIScreen { if (_page == HomePage::RECENT) { _task->showAlert("Recent adverts", 800); } +#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) if (_page == HomePage::DISCOVERY) { _task->showAlert("Repeater disc", 800); } +#endif return true; } if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) { @@ -556,6 +563,7 @@ class HomeScreen : public UIScreen { return true; } #endif +#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) if (c == KEY_ENTER && _page == HomePage::DISCOVERY) { if (millis() > discovery_req_time + 5000) { // rate limiter the_mesh.requestRepeatersDiscovery(); @@ -567,6 +575,7 @@ class HomeScreen : public UIScreen { discovery_disp_names = !discovery_disp_names; return true; } +#endif if (c == KEY_ENTER && _page == HomePage::SHUTDOWN) { _shutdown_init = true; // need to wait for button to be released return true;