From b84830b3b3b2d278576874efeb8694627793bb8d Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:50:55 +0200 Subject: [PATCH] (greatly) improve our BLE --- zephcore/adapters/ble/ZephyrBLE.cpp | 18 ++++++ zephcore/app/CompanionMesh.cpp | 94 ++++++++++++++++++++++------- zephcore/app/CompanionMesh.h | 10 ++- zephcore/src/main_companion.cpp | 1 + 4 files changed, 99 insertions(+), 24 deletions(-) diff --git a/zephcore/adapters/ble/ZephyrBLE.cpp b/zephcore/adapters/ble/ZephyrBLE.cpp index eb9cfb8..95e5021 100644 --- a/zephcore/adapters/ble/ZephyrBLE.cpp +++ b/zephcore/adapters/ble/ZephyrBLE.cpp @@ -53,6 +53,9 @@ LOG_MODULE_REGISTER(zephcore_ble, CONFIG_ZEPHCORE_BLE_LOG_LEVEL); * BLE stack when the link is marginal, fast enough to recover quickly. */ #define BLE_TX_OVERFLOW_RETRY_MS 250 +/* App push notifications are 0x80+; protocol response packets are < 0x80. */ +#define PUSH_CODE_BASE 0x80 + /* Advertising intervals (Apple Accessory Design Guidelines §5.5) */ #define BT_ADV_FAST_INTERVAL 32 /* 20ms in 0.625ms units */ #define BT_ADV_FAST_DURATION_MS (60 * 1000) /* fast window after boot/disconnect */ @@ -139,6 +142,14 @@ static uint32_t ble_passkey = CONFIG_ZEPHCORE_BLE_PASSKEY; /* NUS TX characteristic attribute — resolved at init, avoids hard-coded offset */ static const struct bt_gatt_attr *nus_tx_attr; +static bool is_lossless_protocol_frame(const uint8_t *data, uint16_t len) +{ + if (!data || len == 0) { + return false; + } + return data[0] < PUSH_CODE_BASE; +} + /* ========== Forward declarations ========== */ static void ble_tx_complete_cb(struct bt_conn *conn, void *user_data); @@ -847,6 +858,13 @@ size_t zephcore_ble_send(const uint8_t *data, uint16_t len) ble_tx_congested = true; } + /* Protocol response frames are lossless. If queue is full, report + * failure so the caller can retry instead of overflow replacement. */ + if (is_lossless_protocol_frame(data, len)) { + LOG_DBG("TX queue full for lossless protocol frame hdr=0x%02x, retry later", data[0]); + return 0; + } + /* Save to overflow — retried at 250ms intervals. * If overflow already pending, replace with newest frame * (push notifications are idempotent MSG_WAITING signals). */ diff --git a/zephcore/app/CompanionMesh.cpp b/zephcore/app/CompanionMesh.cpp index 69c3783..ba8721a 100644 --- a/zephcore/app/CompanionMesh.cpp +++ b/zephcore/app/CompanionMesh.cpp @@ -182,6 +182,9 @@ CompanionMesh::CompanionMesh(mesh::Radio &radio, mesh::MillisecondClock &ms, mes _pending_telemetry = 0; _pending_discovery = 0; _pending_req = 0; + _pending_channel_head = 0; + _pending_channel_tail = 0; + _pending_channel_count = 0; _app_target_ver = 0; _dirty_contacts_expiry = 0; _dirty_channels_expiry = 0; @@ -278,6 +281,7 @@ bool CompanionMesh::onContactPathRecv(ContactInfo &from, uint8_t *in_path, uint8 void CompanionMesh::loop() { BaseChatMesh::loop(); + drainPendingChannelInfos(); /* Check for pending lazy contact/channel writes */ int64_t now = _ms->getMillis(); @@ -300,9 +304,9 @@ void CompanionMesh::markContactsDirty() void CompanionMesh::markChannelsDirty() { - if (!_dirty_channels_expiry) { - _dirty_channels_expiry = _ms->getMillis() + LAZY_WRITE_DELAY_MS; - } + /* Channels import arrives as a burst of CMD_SET_CHANNEL writes. + * Keep pushing the flush deadline so we save once after the burst. */ + _dirty_channels_expiry = _ms->getMillis() + LAZY_WRITE_DELAY_MS; } void CompanionMesh::flushDirtyContacts() @@ -323,12 +327,13 @@ void CompanionMesh::flushDirtyChannels() } } -void CompanionMesh::writeFrame(const uint8_t *data, size_t len) +bool CompanionMesh::writeFrame(const uint8_t *data, size_t len) { LOG_DBG("RSP: 0x%02x len=%u", data[0], (unsigned)len); if (_write_cb) { - _write_cb(data, len); + return _write_cb(data, len) == len; } + return false; } void CompanionMesh::sendPacketOk() @@ -378,6 +383,46 @@ static bool isChannelMessage(const uint8_t *buf) return buf[0] == PACKET_CHANNEL_MSG_V3 || buf[0] == PACKET_CHANNEL_MSG_RECV || buf[0] == PACKET_CHANNEL_DATA_RECV; } +bool CompanionMesh::enqueuePendingChannelInfo(uint8_t idx) +{ + if (_pending_channel_count >= MAX_GROUP_CHANNELS) { + return false; + } + _pending_channel_idx[_pending_channel_tail] = idx; + _pending_channel_tail = (uint8_t)((_pending_channel_tail + 1) % MAX_GROUP_CHANNELS); + _pending_channel_count++; + return true; +} + +bool CompanionMesh::sendChannelInfoFrame(uint8_t idx) +{ + ChannelDetails ch; + if (!getChannel(idx, ch)) { + return false; + } + uint8_t rsp[50]; + int i = 0; + rsp[i++] = PACKET_CHANNEL_INFO; + rsp[i++] = idx; + memcpy(&rsp[i], ch.name, 32); + i += 32; + memcpy(&rsp[i], ch.channel.secret, 16); + i += 16; + return writeFrame(rsp, i); +} + +void CompanionMesh::drainPendingChannelInfos() +{ + while (_pending_channel_count > 0) { + uint8_t idx = _pending_channel_idx[_pending_channel_head]; + if (!sendChannelInfoFrame(idx)) { + return; + } + _pending_channel_head = (uint8_t)((_pending_channel_head + 1) % MAX_GROUP_CHANNELS); + _pending_channel_count--; + } +} + void CompanionMesh::queueOfflineMessage(const uint8_t *data, size_t len) { LOG_DBG("queueOfflineMessage: len=%u type=0x%02x count_before=%d", (unsigned)len, data[0], _offline_queue_count); @@ -465,7 +510,9 @@ bool CompanionMesh::continueContactIteration() } uint8_t rsp[CONTACT_FRAME_SIZE]; size_t n = serializeContact(rsp, c, PACKET_CONTACT); - writeFrame(rsp, n); + if (!writeFrame(rsp, n)) { + return true; + } } } _contact_iter_idx++; @@ -475,7 +522,9 @@ bool CompanionMesh::continueContactIteration() uint8_t rsp[5]; rsp[0] = PACKET_CONTACT_END; put_le32(&rsp[1], _contact_iter_lastmod); - writeFrame(rsp, sizeof(rsp)); + if (!writeFrame(rsp, sizeof(rsp))) { + return true; + } _contact_iter_active = false; return false; } @@ -1511,20 +1560,16 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len) // Response: [code][idx][32 name][16 secret] = 50 bytes (matches Arduino) // Arduino returns channel info even if slot is empty (name[0]=='\0') if (len >= 2 && data[1] < MAX_GROUP_CHANNELS) { - ChannelDetails ch; - if (getChannel(data[1], ch)) { - uint8_t rsp[50]; - int i = 0; - rsp[i++] = PACKET_CHANNEL_INFO; - rsp[i++] = data[1]; - memcpy(&rsp[i], ch.name, 32); - i += 32; - memcpy(&rsp[i], ch.channel.secret, 16); // return 128-bit secret - i += 16; - writeFrame(rsp, i); - } else { - sendPacketError(ERR_NOT_FOUND); // Only if index out of range + static int64_t last_get_channel_ms; + int64_t now_ms = _ms->getMillis(); + uint32_t dt_ms = last_get_channel_ms ? (uint32_t)(now_ms - last_get_channel_ms) : 0; + last_get_channel_ms = now_ms; + LOG_DBG("CMD_GET_CHANNEL idx=%u dt=%ums", data[1], dt_ms); + if (!enqueuePendingChannelInfo(data[1])) { + sendPacketError(ERR_BAD_STATE); + return true; } + drainPendingChannelInfos(); return true; } break; @@ -1826,12 +1871,15 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len) size_t msg_len; if (peekOfflineMessage(buf, msg_len)) { LOG_DBG("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 */ + if (writeFrame(buf, msg_len)) { + _sync_pending = true; /* confirmed on next request or lost on disconnect */ + } } else { LOG_DBG("CMD_SYNC_NEXT_MESSAGE: queue empty, sending NO_MORE_MSGS"); uint8_t rsp[] = { PACKET_NO_MORE_MSGS }; - writeFrame(rsp, sizeof(rsp)); + if (!writeFrame(rsp, sizeof(rsp))) { + return true; + } /* Initial sync is done — safe to apply deferred * connection parameters now without disrupting diff --git a/zephcore/app/CompanionMesh.h b/zephcore/app/CompanionMesh.h index e6f61bb..52e9581 100644 --- a/zephcore/app/CompanionMesh.h +++ b/zephcore/app/CompanionMesh.h @@ -339,7 +339,7 @@ private: _pending_login = _pending_status = _pending_telemetry = _pending_discovery = _pending_req = 0; } - void writeFrame(const uint8_t *data, size_t len); + bool writeFrame(const uint8_t *data, size_t len); void sendPacketOk(); void sendPacketError(uint8_t code); void sendPush(uint8_t code, const uint8_t *data = nullptr, size_t len = 0); @@ -352,10 +352,18 @@ private: bool dequeueOfflineMessage(uint8_t *dest, size_t &len); bool peekOfflineMessage(uint8_t *dest, size_t &len); void confirmOfflineMessage(); + bool enqueuePendingChannelInfo(uint8_t idx); + bool sendChannelInfoFrame(uint8_t idx); + void drainPendingChannelInfos(); 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); void addPendingAck(uint32_t expected, int contact_idx); int findAndRemoveAck(uint32_t ack, uint32_t *out_sent_time = nullptr); + + uint8_t _pending_channel_idx[MAX_GROUP_CHANNELS]; + uint8_t _pending_channel_head; + uint8_t _pending_channel_tail; + uint8_t _pending_channel_count; }; diff --git a/zephcore/src/main_companion.cpp b/zephcore/src/main_companion.cpp index fb47093..eeaa664 100644 --- a/zephcore/src/main_companion.cpp +++ b/zephcore/src/main_companion.cpp @@ -129,6 +129,7 @@ static void ble_on_rx_frame(const uint8_t *data, uint16_t len) static void ble_on_tx_idle(void) { k_work_submit(&contact_iter_work); + k_event_post(&mesh_events, MESH_EVENT_TX_DRAIN); } /* BLE connected callback — notify UI, clear USB state if needed */