(greatly) improve our BLE

This commit is contained in:
liquidraver
2026-04-29 12:09:21 +02:00
parent 4506280162
commit b84830b3b3
4 changed files with 99 additions and 24 deletions
+18
View File
@@ -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). */
+71 -23
View File
@@ -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
+9 -1
View File
@@ -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;
};
+1
View File
@@ -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 */