From c571bae013b3f162ca59b61837c2b9cf20a0425d Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Thu, 16 Jul 2026 11:30:25 +0200 Subject: [PATCH] fix contact sync freeze on cold boot --- zephcore/adapters/ble/ZephyrBLE.cpp | 7 ++++- zephcore/app/CompanionMesh.cpp | 40 +++++++++++++---------------- zephcore/app/CompanionMesh.h | 18 ++++++++----- zephcore/src/main_companion.cpp | 26 ++++++++++++++++--- 4 files changed, 58 insertions(+), 33 deletions(-) diff --git a/zephcore/adapters/ble/ZephyrBLE.cpp b/zephcore/adapters/ble/ZephyrBLE.cpp index 5367651..db977bd 100644 --- a/zephcore/adapters/ble/ZephyrBLE.cpp +++ b/zephcore/adapters/ble/ZephyrBLE.cpp @@ -1156,9 +1156,14 @@ static void tx_drain_work_fn(struct k_work *work) bt_conn_unref(conn); return; } else { - /* Other error - drop frame */ + /* Other error - drop frame. Re-kick rather than returning: on_tx_idle + * only fires from the queue-empty path below, and it is the sole re-arm + * for the contact dump — bailing out here stranded the dump forever. + * This terminates: the frame is already off the queue, so each pass + * either drains one or reaches empty and signals idle. */ ble_tx_in_progress = false; LOG_WRN("tx_drain[BLE]: send failed err=%d, dropped frame", err); + kick_tx_drain(); bt_conn_unref(conn); return; } diff --git a/zephcore/app/CompanionMesh.cpp b/zephcore/app/CompanionMesh.cpp index c13e18a..f591b27 100644 --- a/zephcore/app/CompanionMesh.cpp +++ b/zephcore/app/CompanionMesh.cpp @@ -178,6 +178,8 @@ CompanionMesh::CompanionMesh(mesh::Radio &radio, mesh::MillisecondClock &ms, mes _pin_change_cb = nullptr; _contact_iter_active = false; _contact_iter_idx = 0; + _contact_iter_num = 0; + _contact_iter_vc = false; _contact_iter_lastmod = 0; _contact_iter_since = 0; _offline_queue_head = 0; @@ -588,24 +590,14 @@ void CompanionMesh::confirmOfflineMessage() _offline_queue_count--; } -void CompanionMesh::resetContactIterator() -{ - if (_contact_iter_active) { - // Send contact end if interrupted - uint8_t rsp[5]; - rsp[0] = PACKET_CONTACT_END; - put_le32(&rsp[1], _contact_iter_lastmod); - writeFrame(rsp, sizeof(rsp)); - } - _contact_iter_active = false; -} - bool CompanionMesh::continueContactIteration() { if (!_contact_iter_active) return false; - if (_contact_iter_idx < getNumContacts()) { + if (_contact_iter_idx < _contact_iter_num) { ContactInfo c; + /* getContactByIdx bounds-checks against the live table, so a slot that + * disappeared under us is skipped rather than read stale. */ if (getContactByIdx(_contact_iter_idx, c)) { // Skip transient/anon contacts (ADV_TYPE_NONE) — never synced to the app. // Apply 'since' filter - only send contacts modified after the timestamp @@ -622,11 +614,11 @@ bool CompanionMesh::continueContactIteration() } _contact_iter_idx++; return true; - } else if (_contact_iter_idx == getNumContacts()) { + } else if (_contact_iter_idx == _contact_iter_num) { // Virtual tail entry: the v-contact (never in the real table). - // vcontactReady() implies lastmod != 0 — deferred (clock-invalid) + // _contact_iter_vc implies lastmod != 0 — deferred (clock-invalid) // state is excluded so the app never sees a 1970 timestamp. - if (vcontactReady() && _vcontact_lastmod > _contact_iter_since) { + if (_contact_iter_vc && _vcontact_lastmod > _contact_iter_since) { if (_vcontact_lastmod > _contact_iter_lastmod) { _contact_iter_lastmod = _vcontact_lastmod; } @@ -2018,10 +2010,12 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len) /* Debug: log all incoming commands */ LOG_DBG("CMD: 0x%02x len=%u", data[0], (unsigned)len); - // Reset contact iterator when any new command is received (except during iteration) - if (data[0] != CMD_GET_CONTACTS && _contact_iter_active) { - resetContactIterator(); - } + /* An active contact dump survives interleaved commands — the app is free to + * talk to us mid-sync and does (it sets the clock on a cold boot, and + * pipelines CMD_GET_CHANNEL bursts). Aborting the iterator here truncated + * the dump with a premature PACKET_CONTACT_END after whatever had streamed, + * so the app waited forever for the rest. Upstream interleaves the same way + * (MyMesh::checkSerialInterface); CMD_APP_START remains the only reset. */ /* V-contact interception — must run before any contact lookup so a frame * addressed to the loopback contact can never create a radio packet. */ @@ -2096,12 +2090,14 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len) // Send PACKET_CONTACT_START with total count (unfiltered, but excluding // transient anon slots -- continueContactIteration() never streams those) + _contact_iter_num = getNumContacts(); + _contact_iter_vc = vcontactReady(); /* virtual tail entry (post time sync) */ uint32_t total = 0; - for (int i = 0; i < getNumContacts(); i++) { + for (int i = 0; i < _contact_iter_num; i++) { ContactInfo c; if (getContactByIdx(i, c) && c.type != ADV_TYPE_NONE) total++; } - if (vcontactReady()) total++; /* virtual tail entry (post time sync) */ + if (_contact_iter_vc) total++; uint8_t rsp[5]; rsp[0] = PACKET_CONTACT_START; put_le32(&rsp[1], total); diff --git a/zephcore/app/CompanionMesh.h b/zephcore/app/CompanionMesh.h index cc3b750..48c050c 100644 --- a/zephcore/app/CompanionMesh.h +++ b/zephcore/app/CompanionMesh.h @@ -214,18 +214,18 @@ public: */ bool continueContactIteration(); - /** - * Reset contact iterator (call when new command received). - * Sends PACKET_CONTACT_END if iteration was in progress. - */ - void resetContactIterator(); - /** * Cancel contact iteration silently (no frame sent). * Call on BLE disconnect — there's nobody to send CONTACT_END to. */ void cancelContactIterator() { _contact_iter_active = false; } + /** True while a contact dump is in progress (drives the stall watchdog). */ + bool isContactIterActive() const { return _contact_iter_active; } + + /** Dump progress cursor — the watchdog re-kicks only if this stops moving. */ + int getContactIterIdx() const { return _contact_iter_idx; } + /** * Cancel pending message sync. Un-ACKed message stays in queue. * Call on BLE disconnect so the message is re-sent on reconnect. @@ -384,6 +384,12 @@ private: /* Contact iteration state */ bool _contact_iter_active; int _contact_iter_idx; + /* Table bound and v-contact inclusion are snapshotted at PACKET_CONTACT_START + * so the dump can never stream more entries than the total it promised: the + * table grows from inbound adverts mid-dump, and vcontactReady() flips false + * ->true the moment a cold-booted clock goes valid (CMD_SET_DEVICE_TIME). */ + int _contact_iter_num; + bool _contact_iter_vc; uint32_t _contact_iter_lastmod; uint32_t _contact_iter_since; /* Filter: only send contacts with lastmod > this */ diff --git a/zephcore/src/main_companion.cpp b/zephcore/src/main_companion.cpp index 292eea9..ca3f584 100644 --- a/zephcore/src/main_companion.cpp +++ b/zephcore/src/main_companion.cpp @@ -373,10 +373,10 @@ static void process_companion_rx(void) /* Process all queued frames */ while (k_msgq_get(zephcore_ble_get_recv_queue(), &f, K_NO_WAIT) == 0) { #ifdef ZEPHCORE_LORA - /* handleProtocolFrame() resets the contact iterator internally - * (line 1229) for any non-CMD_GET_CONTACTS command — no need - * to call resetContactIterator() here. Doing so sent a stale - * PACKET_CONTACT_END before the command was even processed. */ + /* An in-flight contact dump deliberately survives commands parsed + * here — it is only cancelled by CMD_APP_START (new session) or by + * disconnect. Do not abort it on inbound traffic: the app talks to + * us mid-sync, and truncating the dump left it waiting forever. */ if (!companion_mesh_ptr->handleProtocolFrame(f.buf, f.len)) { LOG_DBG("rx_process: unknown cmd 0x%02x len=%u", f.buf[0], (unsigned)f.len); uint8_t err_rsp[] = { 0x01, 0x01 }; /* PACKET_ERROR, ERR_UNSUPPORTED */ @@ -516,6 +516,24 @@ static void mesh_event_loop(void) companion_mesh_ptr->maintenanceLoop(); } + /* Contact-dump watchdog — the dump is pumped solely by the + * BLE/USB tx-idle callback, so a single lost kick strands it + * silently and the app waits for contacts that never arrive. + * Only re-kick when the cursor has not moved across a whole + * tick: a large dump legitimately spans several ticks, and + * back-pressure resolves itself. */ + static int wd_last_iter_idx = -1; + if (companion_mesh_ptr && companion_mesh_ptr->isContactIterActive()) { + int idx = companion_mesh_ptr->getContactIterIdx(); + if (idx == wd_last_iter_idx) { + LOG_WRN("contact dump watchdog: no progress at %d, resuming", idx); + k_event_post(&mesh_events, MESH_EVENT_CONTACT_ITER); + } + wd_last_iter_idx = idx; + } else { + wd_last_iter_idx = -1; + } + /* BLE advertising watchdog — if bt_le_adv_start failed * transiently (HCI timeout, controller pacing) the device * would silently stop advertising and be undiscoverable