diff --git a/zephcore/Kconfig b/zephcore/Kconfig index ed0dd75..4e64ce6 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -120,9 +120,10 @@ config ZEPHCORE_MAX_CHANNELS config ZEPHCORE_OFFLINE_QUEUE_SIZE int "Offline message queue size" - default 16 + default 256 help Size of the offline message queue for incoming messages. + Arduino MeshCore uses 256 for both T1000-E and Wio Tracker. config ZEPHCORE_ACK_TABLE_SIZE int "ACK tracking table size" diff --git a/zephcore/adapters/ble/ZephyrBLE.cpp b/zephcore/adapters/ble/ZephyrBLE.cpp index e3739ff..608fb2f 100644 --- a/zephcore/adapters/ble/ZephyrBLE.cpp +++ b/zephcore/adapters/ble/ZephyrBLE.cpp @@ -121,6 +121,7 @@ static bool dle_requested; /* Advertising state */ static bool adv_switching = false; static bool adv_is_slow = false; +static bool adv_post_disconnect = false; /* skip fast advert after disconnect */ /* Runtime BLE passkey */ static uint32_t ble_passkey = CONFIG_ZEPHCORE_BLE_PASSKEY; @@ -340,6 +341,10 @@ static void disconnected(struct bt_conn *conn, uint8_t reason) k_work_cancel_delayable(&tx_drain_work); k_work_cancel_delayable(&overflow_retry_work); + /* Skip fast advertising on reconnect — go straight to slow. + * Prevents tight reconnect flapping loops on flaky links. */ + adv_post_disconnect = true; + /* Notify main of BLE disconnection */ if (ble_cbs && ble_cbs->on_disconnected) { ble_cbs->on_disconnected(); @@ -354,6 +359,16 @@ static void recycled(void) adv_switching, adv_is_slow); return; } + + if (adv_post_disconnect) { + /* After disconnect, skip fast advertising — go straight to slow. + * Prevents rapid reconnect flapping on flaky BLE links. */ + adv_post_disconnect = false; + LOG_INF("post-disconnect: starting slow advertising directly"); + adv_slow_work_fn(NULL); + return; + } + LOG_DBG("restart advertising"); start_adv(); } diff --git a/zephcore/app/CompanionMesh.cpp b/zephcore/app/CompanionMesh.cpp index 97e0d1b..a560b99 100644 --- a/zephcore/app/CompanionMesh.cpp +++ b/zephcore/app/CompanionMesh.cpp @@ -158,6 +158,7 @@ CompanionMesh::CompanionMesh(mesh::Radio &radio, mesh::MillisecondClock &ms, mes _offline_queue_head = 0; _offline_queue_tail = 0; _offline_queue_count = 0; + _sync_pending = false; memset(_ack_table, 0, sizeof(_ack_table)); _ack_next_overwrite = 0; memset(_advert_paths, 0, sizeof(_advert_paths)); @@ -392,6 +393,22 @@ bool CompanionMesh::dequeueOfflineMessage(uint8_t *dest, size_t &len) return true; } +bool CompanionMesh::peekOfflineMessage(uint8_t *dest, size_t &len) +{ + if (_offline_queue_count == 0) return false; + QueuedFrame *f = &_offline_queue[_offline_queue_head]; + len = f->len; + memcpy(dest, f->buf, len); + return true; /* head NOT advanced — message stays in queue */ +} + +void CompanionMesh::confirmOfflineMessage() +{ + if (_offline_queue_count == 0) return; + _offline_queue_head = (_offline_queue_head + 1) % OFFLINE_QUEUE_SIZE; + _offline_queue_count--; +} + void CompanionMesh::resetContactIterator() { if (_contact_iter_active) { @@ -1677,12 +1694,21 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len) } case CMD_SYNC_NEXT_MESSAGE: { - LOG_INF("CMD_SYNC_NEXT_MESSAGE: queue_count=%d", _offline_queue_count); + LOG_INF("CMD_SYNC_NEXT_MESSAGE: queue_count=%d pending=%d", + _offline_queue_count, _sync_pending); + + /* Phone asking for next = implicit ACK for the previously-peeked message */ + if (_sync_pending) { + confirmOfflineMessage(); + _sync_pending = false; + } + uint8_t buf[MAX_FRAME_SIZE]; size_t msg_len; - if (dequeueOfflineMessage(buf, msg_len)) { - LOG_INF("CMD_SYNC_NEXT_MESSAGE: dequeued msg_len=%u type=0x%02x", (unsigned)msg_len, buf[0]); + if (peekOfflineMessage(buf, msg_len)) { + LOG_INF("CMD_SYNC_NEXT_MESSAGE: peeked msg_len=%u type=0x%02x", (unsigned)msg_len, buf[0]); writeFrame(buf, msg_len); + _sync_pending = true; /* will be confirmed on next request or lost on disconnect */ } else { LOG_INF("CMD_SYNC_NEXT_MESSAGE: queue empty, sending NO_MORE_MSGS"); uint8_t rsp[] = { PACKET_NO_MORE_MSGS }; diff --git a/zephcore/app/CompanionMesh.h b/zephcore/app/CompanionMesh.h index c9ea799..26b14a1 100644 --- a/zephcore/app/CompanionMesh.h +++ b/zephcore/app/CompanionMesh.h @@ -162,9 +162,22 @@ public: /** * 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; } + + /** + * Cancel pending message sync. Un-ACKed message stays in queue. + * Call on BLE disconnect so the message is re-sent on reconnect. + */ + void cancelSyncPending() { _sync_pending = false; } + /** * Get BLE device name for advertising. */ @@ -270,6 +283,7 @@ private: int _offline_queue_head; int _offline_queue_tail; int _offline_queue_count; + bool _sync_pending; /* true = last peeked message not yet ACKed by phone */ /* ACK tracking table */ struct AckEntry { @@ -328,6 +342,8 @@ private: void queueOfflineMessage(const uint8_t *data, size_t len); bool dequeueOfflineMessage(uint8_t *dest, size_t &len); + bool peekOfflineMessage(uint8_t *dest, size_t &len); + void confirmOfflineMessage(); void queueContactMessage(const ContactInfo &contact, mesh::Packet *pkt, uint8_t txt_type, uint32_t sender_timestamp, const uint8_t *extra, int extra_len, const char *text); diff --git a/zephcore/src/main_companion.cpp b/zephcore/src/main_companion.cpp index 31d625f..b10b9b5 100644 --- a/zephcore/src/main_companion.cpp +++ b/zephcore/src/main_companion.cpp @@ -141,11 +141,21 @@ static void ble_on_connected(void) ui_notify(UI_EVENT_BLE_CONNECTED); } -/* BLE disconnected callback — notify UI */ +/* BLE disconnected callback — clean up state and notify UI */ static void ble_on_disconnected(void) { #if IS_ENABLED(CONFIG_LOG) zephcore_usb_companion_reset_rx(); +#endif +#ifdef ZEPHCORE_LORA + /* Silently cancel any in-progress contact iteration. + * Without this, reconnect triggers resetContactIterator() which sends + * a stale PACKET_CONTACT_END to the NEW connection, confusing the + * phone's sync state machine. */ + companion_mesh_ptr->cancelContactIterator(); + /* Reset message sync — un-ACKed peeked message stays in queue + * and will be re-sent on next CMD_SYNC_NEXT_MESSAGE. */ + companion_mesh_ptr->cancelSyncPending(); #endif ui_notify(UI_EVENT_BLE_DISCONNECTED); } @@ -205,8 +215,10 @@ static void rx_process_work_fn(struct k_work *work) /* Process all queued frames */ while (k_msgq_get(zephcore_ble_get_recv_queue(), &f, K_NO_WAIT) == 0) { #ifdef ZEPHCORE_LORA - /* Reset contact iterator when new command received */ - companion_mesh_ptr->resetContactIterator(); + /* 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. */ 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 */