fix contact sync freeze on cold boot

This commit is contained in:
liquidraver
2026-07-16 11:30:25 +02:00
parent 5cb38b13dc
commit c571bae013
4 changed files with 58 additions and 33 deletions
+6 -1
View File
@@ -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;
}
+18 -22
View File
@@ -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);
+12 -6
View File
@@ -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 */
+22 -4
View File
@@ -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