From 0731b5ff673dac6e357e90028e68013587b683ea Mon Sep 17 00:00:00 2001 From: Kaj Schittecat Date: Wed, 1 Jul 2026 08:46:56 +0200 Subject: [PATCH] =?UTF-8?q?touch:=20beta=5F22=20(test)=20=E2=80=94=20trace?= =?UTF-8?q?-ping=20signal=20probe=20+=20companion=20contact-refresh=20(#73?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signal probe: replace the beta_21 zero-hop advert (which nothing replies to, so the signal never updated) with a directed TRACE ping to the nearest reachable repeater. It retransmits — a real reply we measure — without flooding the mesh. New MyMesh::uiSendSignalProbe() picks the shortest-path repeater; the reply is captured silently in onTraceRecv via _ui_sig_probe_tag. Falls back to a zero-hop advert when no repeater is known. Thanks to Tarmo for the insight. Contacts (#73 part A): a contact discovered while a companion app is connected over BLE now refreshes the device's own Contacts list + notification. onDiscoveredContact fired the UI notify only in the standalone branch — move it out so it always fires, and flag the list dirty for UITask::loop to rebuild on the LVGL thread. Co-Authored-By: Claude Opus 4.8 (1M context) --- release-notes/beta_22.txt | 8 ++++++++ src/MyMesh.cpp | 22 ++++++++++++++++---- src/MyMesh.h | 43 +++++++++++++++++++++++++++++++++++++++ src/ui-touch/UITask.cpp | 24 ++++++++++++++++++++-- src/ui-touch/UITask.h | 1 + 5 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 release-notes/beta_22.txt diff --git a/release-notes/beta_22.txt b/release-notes/beta_22.txt new file mode 100644 index 00000000..3e83033c --- /dev/null +++ b/release-notes/beta_22.txt @@ -0,0 +1,8 @@ +# beta_22 (TEST build — one user-facing note per non-blank, non-# line; # lines are section comments) + +# --- Fixed --- +The signal probe reads a real signal again. In beta_21 the probe was changed to a zero-hop advert so it could never be re-flooded across the mesh — but nothing answers a zero-hop advert, so the signal reading stopped updating. It now sends a directed trace "ping" to your nearest repeater instead: the repeater replies directly (it still never floods the mesh) and the device reads the link signal from that reply. So you get both — no mesh flooding, and a live signal again. Thanks to Tarmo for spotting this and suggesting the fix. +Contacts discovered while your phone is connected now show on the device too. If the T-Deck was connected to the MeshCore phone app over Bluetooth and heard a new node, the node appeared on the phone but not on the device's own screen, and the Contacts list did not refresh. The device now updates its own Contacts list and notification whether or not a phone app is connected. (Issue #73, reported by 1nerdherder.) + +# --- Also --- +This is a test-channel build (opt in with "Get test builds" on the About screen). Everything from beta_21, the first stable, is included. diff --git a/src/MyMesh.cpp b/src/MyMesh.cpp index ca7a595c..b4c9a385 100644 --- a/src/MyMesh.cpp +++ b/src/MyMesh.cpp @@ -1745,12 +1745,14 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE); _serial->writeFrameToAll(out_frame, 1 + PUB_KEY_SIZE); } - } else { -#ifdef DISPLAY_CLASS - if (_ui) _ui->notify(UIEventType::newContactMessage); -#endif } #ifdef DISPLAY_CLASS + // Notify the touch UI whether or not a companion app is connected. This used to + // fire only in the standalone (else) branch, so a contact discovered while the + // phone app was attached over BLE gave the device's OWN screen no indication and + // never refreshed its Contacts list (issue #73). notify() flags the list dirty so + // UITask::loop rebuilds it when the Contacts tab is showing. + if (_ui) _ui->notify(UIEventType::newContactMessage); // Mirror to the touch UI's Discovered store so the user can browse pending // adverts and manually add nodes to contacts[] (used when auto-add is off // or contacts[] is full). `is_new=true` here means the contact is NOT yet @@ -2362,6 +2364,18 @@ void MyMesh::onRawDataRecv(mesh::Packet *packet) { void MyMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags, const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) { uint8_t path_sz = flags & 0x03; // NEW v1.11+ + // Silent SIGNAL PROBE result: a directed trace we sent to a neighbouring repeater + // has come back (it retransmitted; we heard it). Capture OUR reception of that + // reply — SNR + RSSI — as the live signal, and swallow it (no popup). This is the + // non-flooding replacement for the old flood-advert probe, and must run before the + // visible-ping path below. + if (tag != 0 && tag == _ui_sig_probe_tag) { + _ui_sig_probe_tag = 0; + _ui_sig_snr_q4 = (int8_t)(packet->getSNR() * 4.0f); + _ui_sig_rssi = (int8_t)_radio->getLastRSSI(); + _ui_sig_ms = millis(); + return; + } #if defined(DISPLAY_CLASS) // If this trace's tag matches the most recent UI-initiated ping, surface // the bidirectional SNRs directly to the touch UI instead of (or in diff --git a/src/MyMesh.h b/src/MyMesh.h index 05378e94..6997422e 100644 --- a/src/MyMesh.h +++ b/src/MyMesh.h @@ -623,6 +623,48 @@ public: return tag; } + /** SIGNAL PROBE (the non-flooding way): trace-ping the best-reachable repeater so + * it retransmits — a real reply we can measure — WITHOUT flooding the mesh. A + * plain zero-hop advert gets no reply (nothing repeats it), so the signal never + * updates; a directed trace to a neighbour repeater does (it appends its RX-SNR + * and retransmits, which we hear). Picks the repeater with the shortest known + * path (0 = direct neighbour), breaking ties by most-recently-heard. The reply + * is captured silently in onTraceRecv via _ui_sig_probe_tag → _ui_sig_*. Returns + * the tag, or 0 if no repeater with a known path is available (the caller can + * fall back to a zero-hop advert). */ + uint32_t uiSendSignalProbe() { + ContactInfo best; bool have = false; ContactInfo c; + const int n = getNumContacts(); + for (int i = 0; i < n; i++) { + if (!getContactByIdx((uint32_t)i, c)) continue; + if (c.type != ADV_TYPE_REPEATER) continue; // only repeaters retransmit a trace + if (c.out_path_len == OUT_PATH_UNKNOWN) continue; // no known path → would need a flood + if (c.out_path_len > MAX_PATH_SIZE) continue; // guard a corrupt length + if (!have) { best = c; have = true; continue; } + if (c.out_path_len < best.out_path_len || + (c.out_path_len == best.out_path_len && + c.last_advert_timestamp > best.last_advert_timestamp)) { + best = c; + } + } + if (!have) return 0; + uint32_t tag = 0; + getRNG()->random((uint8_t*)&tag, sizeof(tag)); + if (tag == 0) tag = 1; + uint8_t flags = (uint8_t)(_prefs.path_hash_mode & 0x03); + mesh::Packet* pkt = createTrace(tag, 0, flags); + if (!pkt) return 0; + _ui_sig_probe_tag = tag; + if (best.out_path_len > 0 && best.out_path_len <= MAX_PATH_SIZE) { + sendDirect(pkt, best.out_path, best.out_path_len); // directed along the known path + } else { + uint8_t hash_sz = (uint8_t)(_prefs.path_hash_mode + 1); + if (hash_sz == 0 || hash_sz > 4) hash_sz = 1; + sendDirect(pkt, best.id.pub_key, hash_sz); // direct neighbour → single hop + } + return tag; + } + /** Request CayenneLPP telemetry from a remote contact. Reply is delivered * via AbstractUITask::onTelemetryReply with the raw LPP payload after the * 4-byte timestamp header. Falls back to onPingReply if the UI didn't @@ -955,6 +997,7 @@ private: // path. Single-slot is enough because the UI gates a new ping until the // last one resolves or times out. uint32_t _ui_trace_ping_tag = 0; + uint32_t _ui_sig_probe_tag = 0; // in-flight silent signal probe (updates _ui_sig_* only, no popup) }; #if defined(ESP32_PLATFORM) diff --git a/src/ui-touch/UITask.cpp b/src/ui-touch/UITask.cpp index b7978aa2..0dd08458 100644 --- a/src/ui-touch/UITask.cpp +++ b/src/ui-touch/UITask.cpp @@ -17036,7 +17036,7 @@ static void sigPollSaveCb(lv_event_t* e) { // Manual probe: send a flood advert now so nearby repeaters echo it back. static void sigProbeNowCb(lv_event_t* e) { if (lv_event_get_code(e) != LV_EVENT_CLICKED || !g_lv.task) return; - g_lv.task->sendAdvertZeroHop(); // zero-hop only — neighbours hear it; the mesh never re-floods it + g_lv.task->sendSignalProbe(); // directed trace-ping to the nearest repeater (no flood); it replies with our link signal g_lv.task->showAlert(TR("Probing neighbours\xE2\x80\xA6"), 900); } static void openSignalInfoPopup() { @@ -18569,6 +18569,7 @@ static bool s_ct_select_mode = false; static uint8_t s_ct_sel[128][6]; // pub_key prefix of currently-selected (deletable) contacts static int s_ct_sel_n = 0; static bool s_ct_list_force = false; // force refreshContactsList past its no-change cache +static volatile bool s_ct_contacts_dirty = false; // a contact was discovered/added (set from the mesh callback); UITask::loop rebuilds the visible Contacts list — issue #73 static void contactsListForceRefresh() { s_ct_list_force = true; refreshContactsList(); } static lv_obj_t* s_ct_sort_sheet = nullptr; // Sort & filter overlay static lv_obj_t* s_ct_normal_bar = nullptr; // header toolbar shown in normal mode @@ -35270,6 +35271,14 @@ bool UITask::setScreenTimeoutSecs(uint16_t seconds) { bool UITask::sendAdvertNow() { return the_mesh.advert(); } bool UITask::sendAdvertFlood() { return the_mesh.sendAdvert(true); } bool UITask::sendAdvertZeroHop() { return the_mesh.sendAdvert(false); } +// Signal probe: ask the nearest repeater to reply (a directed trace, never a flood) +// so the live signal reading updates from a real link measurement. If no repeater +// with a known path is available, fall back to a zero-hop advert (announce ourselves; +// passive RX still updates the signal from any directly-heard neighbour). +bool UITask::sendSignalProbe() { + if (the_mesh.uiSendSignalProbe() != 0) return true; + return the_mesh.sendAdvert(false); +} void UITask::persistHistoryNow() { saveThreadsToStorage(); @@ -35504,6 +35513,10 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i void UITask::notify(UIEventType t) { g_last_event = t; + // A discovered/added contact should refresh the Contacts list even mid-frame and + // even while a companion app is connected (issue #73). Flag it; UITask::loop does + // the rebuild on the LVGL thread (refreshContactsList no-op's when nothing changed). + if (t == UIEventType::newContactMessage) s_ct_contacts_dirty = true; const char* msg = nullptr; switch (t) { case UIEventType::contactMessage: msg = TR("New DM"); break; @@ -35562,6 +35575,13 @@ void UITask::loop() { // radio forever — so re-apply the mesh params and release ownership here. if (s_spectrum_active && !s_spec_root) spectrumRestoreRadio(); flushHistoryIfDue(now); + // A contact was discovered/added (possibly while a companion app was connected — + // the old code left the device screen unaware; issue #73). Rebuild the Contacts + // list if it's the visible tab; refreshContactsList no-op's when nothing changed. + if (s_ct_contacts_dirty) { + s_ct_contacts_dirty = false; + if (getActiveTab() == CONTACTS_TAB_INDEX) refreshContactsList(); + } #if defined(HAS_TANMATSU) if (s_msgled_flash_until) msgLedRefresh(getUnreadTotal() > 0); // end the one-shot envelope-LED flash on time #endif @@ -35930,7 +35950,7 @@ void UITask::loop() { s_sig_probe_at = now + poll_ms; const uint32_t sms = the_mesh.uiSignalMs(); // Skip the announce when a direct neighbour was heard within the poll window. - if (sms == 0 || (now - sms) >= poll_ms - 5000UL) sendAdvertZeroHop(); + if (sms == 0 || (now - sms) >= poll_ms - 5000UL) sendSignalProbe(); } else { s_sig_probe_at = now + 60000; // probe off: just re-check the toggle each minute } diff --git a/src/ui-touch/UITask.h b/src/ui-touch/UITask.h index ee2311c1..96944b30 100644 --- a/src/ui-touch/UITask.h +++ b/src/ui-touch/UITask.h @@ -423,6 +423,7 @@ public: bool sendAdvertNow(); // legacy: zero-hop bool sendAdvertFlood(); // multi-hop flood bool sendAdvertZeroHop(); // explicit zero-hop (same as sendAdvertNow) + bool sendSignalProbe(); // trace-ping the nearest repeater (non-flooding); falls back to zero-hop advert void rebootDevice(); // Synchronously persist chat history to flash. Call before any path that // restarts the device (Wi-Fi/BLE mode switch, etc.) so recent chat isn't