Preserve BLE command replies and respect negotiated mOTA MTU

This commit is contained in:
mikecarper committed 2026-10-15 11:05:00 -07:00
1 parent 6b75ad07d3
commit e57ab1309f
18 files changed
+1111 -10

No files matched your search

+39
View File
@@ -0,0 +1,39 @@
#pragma once
// Explicit lab images only. Normal release builds expose no echo command,
// allocate no benchmark state and retain their existing command behavior.
#if defined(MESH_HIL_BLE_BENCH) && MESH_HIL_BLE_BENCH
#if MESH_HIL_BLE_BENCH != 1
#error "MESH_HIL_BLE_BENCH must be explicitly 0 or 1"
#endif
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include "CompanionFrameLimits.h"
namespace mesh {
namespace companion {
// 0x78..0x7F already have legacy hardware aliases. 0x70 is lab-only and is
// distinct from normal responses/pushes; nonce and sequence belong to the host.
static constexpr uint8_t kBleBenchCommand = 0x70;
static constexpr size_t kBleBenchHeaderBytes = 15;
// Wire: 70, "BLE1", runIdLE32, sequenceLE32, payloadLengthLE16, payload.
// Echo the entire request exactly once, without generic OK or radio traffic.
// The host also limits frame size to its negotiated ATT MTU minus three.
inline size_t buildBleBenchEcho(const uint8_t* request, size_t length,
uint8_t* response, size_t capacity) {
if (!request || !response || length < kBleBenchHeaderBytes
|| length > MAX_FRAME_SIZE || length > capacity
|| request[0] != kBleBenchCommand || memcmp(request + 1, "BLE1", 4)) return 0;
const size_t payload_length = size_t(request[13]) | (size_t(request[14]) << 8);
if (payload_length != length - kBleBenchHeaderBytes) return 0;
memcpy(response, request, length);
return length;
}
} // namespace companion
} // namespace mesh
#endif
+31 -3
View File
@@ -977,14 +977,38 @@ size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) {
return 0;
}
#define BLE_WRITE_MIN_INTERVAL 60
#if defined(MESH_HIL_BLE_INTERVAL_MS)
#if !defined(MESH_HIL_BLE_BENCH) || MESH_HIL_BLE_BENCH != 1
#error "MESH_HIL_BLE_INTERVAL_MS requires the explicit MESH_HIL_BLE_BENCH=1 lab image"
#endif
#if MESH_HIL_BLE_INTERVAL_MS < 7 || MESH_HIL_BLE_INTERVAL_MS > 60
#error "MESH_HIL_BLE_INTERVAL_MS must be in 7..60 milliseconds"
#endif
#define BLE_WRITE_MIN_INTERVAL MESH_HIL_BLE_INTERVAL_MS
#else
// Preserve the shipped pacing until a separate lab image has measured a safe
// replacement. One value governs admission and both successful/failed sends.
#define BLE_WRITE_MIN_INTERVAL 60
#endif
bool SerialBLEInterface::isReadBusy() const {
return uxQueueMessagesWaiting(recv_queue) > 0;
}
bool SerialBLEInterface::hasRequiredReplyCapacity() const {
if (send_queue_len < FRAME_QUEUE_SIZE) return true;
// Match enqueueCompanionFrame's admission rule: a required response can
// displace best-effort traffic, but must never displace another required one.
for (int i = 0; i < send_queue_len; ++i) {
if (!mesh::companionFrameRequiresDelivery(send_queue[i].buf, send_queue[i].len))
return true;
}
return false;
}
bool SerialBLEInterface::isWriteBusy() const {
return !mesh::bleElapsedAtLeast((uint32_t)millis(), _last_write,
return !hasRequiredReplyCapacity()
|| !mesh::bleElapsedAtLeast((uint32_t)millis(), _last_write,
BLE_WRITE_MIN_INTERVAL);
}
@@ -1052,7 +1076,11 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) {
}
Frame frame;
if (deviceConnected && xQueueReceive(recv_queue, &frame, 0) == pdTRUE) {
// Service TX above even when full, then retain a command until its required
// reply can be admitted. Dequeuing while all TX slots are required causes
// one-shot replies (including CMD66) to be silently lost by writeFrame().
if (deviceConnected && hasRequiredReplyCapacity()
&& xQueueReceive(recv_queue, &frame, 0) == pdTRUE) {
memcpy(dest, frame.buf, frame.len);
BLE_DEBUG_PRINTLN("readBytes: sz=%d, hdr=%d", (uint32_t) frame.len, (uint32_t) dest[0]);
return frame.len;
+1
View File
@@ -67,6 +67,7 @@ class SerialBLEInterface : public BaseSerialInterface,
Frame send_queue[FRAME_QUEUE_SIZE];
void clearBuffers();
bool hasRequiredReplyCapacity() const;
void servicePendingTxReset();
void scheduleAdvertisingRestart(uint32_t now);
void recoverStalledTx(const char* cause);
+25 -1
View File
@@ -1194,6 +1194,17 @@ void SerialBLEInterface::loop() {
}
}
bool SerialBLEInterface::hasRequiredReplyCapacity() const {
if (send_queue_len < FRAME_QUEUE_SIZE) return true;
// Required responses may evict an unsent packet log, never another required
// reply. Match enqueueCompanionFrame without growing either bounded queue.
for (uint8_t i = 0; i < send_queue_len; ++i) {
if (!mesh::companionFrameRequiresDelivery(send_queue[i].buf, send_queue[i].len))
return true;
}
return false;
}
size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) {
if (len > MAX_FRAME_SIZE) {
BLE_DEBUG_PRINTLN("writeFrame(), frame too big, len=%u", (unsigned)len);
@@ -1285,7 +1296,9 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) {
_tx_stall_watchdog.reset();
}
if (recv_queue_len > 0) {
// TX/recovery above must run even when full. Retain RX until a required
// command reply can be admitted instead of silently losing that reply.
if (recv_queue_len > 0 && hasRequiredReplyCapacity()) {
size_t len = recv_queue[0].len;
memcpy(dest, recv_queue[0].buf, len);
@@ -1366,6 +1379,17 @@ bool SerialBLEInterface::isMotaChannelReady() {
&& _conn_handle != BLE_CONN_HANDLE_INVALID
&& _mota_request.notifyEnabled(_conn_handle);
}
uint16_t SerialBLEInterface::motaResponseCapacity() {
if (!isMotaChannelReady() || !_mota_stream.isActive()) return 0;
BLEConnection* connection = Bluefruit.Connection(_conn_handle);
if (!connection || !connection->connected()) return 0;
const uint16_t mtu = connection->getMtu();
if (mtu < 23) return 0;
const uint16_t capacity = (uint16_t)(mtu - 3u);
return capacity < mesh::ota::BLE_MOTA_RESPONSE_MAX
? capacity : mesh::ota::BLE_MOTA_RESPONSE_MAX;
}
#endif
bool SerialBLEInterface::isReadBusy() const {
+2
View File
@@ -109,6 +109,7 @@ class SerialBLEInterface : public BaseSerialInterface {
Frame recv_queue[FRAME_QUEUE_SIZE];
void clearBuffers();
bool hasRequiredReplyCapacity() const;
void shiftSendQueueLeft();
void shiftRecvQueueLeft();
size_t writeBleUartFrame(const Frame& frame);
@@ -209,6 +210,7 @@ public:
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
Stream& motaStream() { return _mota_stream; }
bool isMotaChannelReady();
uint16_t motaResponseCapacity();
bool isMotaStreamActive() const { return _mota_stream.isActive(); }
void setMotaStreamActive(bool active) {
_mota_stream.setActive(active);
+16 -4
View File
@@ -202,13 +202,21 @@ bool SerialMotaSource::describe(uint8_t idx, MotaDesc& out) {
return true;
}
uint16_t SerialMotaSource::responsePayloadCapacity() const {
uint16_t frame = responseFrameCapacity();
if (frame < 8) return 0; // even a nonempty DEFLATE response must fit
if (frame > MOTA_SEEDER_READ_MAX + 5u) frame = MOTA_SEEDER_READ_MAX + 5u;
return (uint16_t)(frame - 5u);
}
bool SerialMotaSource::read(uint8_t idx, uint32_t off, uint8_t* buf, uint32_t len) {
if (!buf && len != 0) return false;
if (len > UINT32_MAX - off) return false;
uint32_t done = 0;
while (done < len) {
uint16_t chunk = (uint16_t)(len - done > MOTA_SEEDER_READ_MAX
? MOTA_SEEDER_READ_MAX : len - done);
const uint16_t maximum = responsePayloadCapacity();
if (maximum == 0) return false; // disconnected/invalid BLE session
uint16_t chunk = (uint16_t)(len - done > maximum ? maximum : len - done);
uint8_t args[7];
args[0] = idx;
wr_u32le(args + 1, off + done);
@@ -223,6 +231,7 @@ bool SerialMotaSource::read_deflated_block(uint8_t idx, uint16_t block, uint8_t*
uint16_t cap, uint16_t* len) {
if (!buf || !len || cap == 0) return false;
*len = 0;
if (responsePayloadCapacity() < 3) return false;
uint8_t args[7] = { idx };
args[1] = (uint8_t)(block & 0xFF); args[2] = (uint8_t)(block >> 8);
@@ -235,8 +244,11 @@ bool SerialMotaSource::read_deflated_block(uint8_t idx, uint16_t block, uint8_t*
uint16_t done = 0;
while (done < total) {
const uint16_t chunk = (uint16_t)((total - done > MOTA_SEEDER_DEFLATE_CHUNK_MAX)
? MOTA_SEEDER_DEFLATE_CHUNK_MAX : total - done);
const uint16_t payload_capacity = responsePayloadCapacity();
if (payload_capacity < 3) return false;
const uint16_t maximum = (uint16_t)(payload_capacity - 2u);
const uint16_t chunk = (uint16_t)((total - done > maximum)
? maximum : total - done);
args[3] = (uint8_t)(done & 0xFF); args[4] = (uint8_t)(done >> 8);
args[5] = (uint8_t)(chunk & 0xFF); args[6] = (uint8_t)(chunk >> 8);
uint8_t response[2 + MOTA_SEEDER_DEFLATE_CHUNK_MAX];
+7
View File
@@ -38,7 +38,14 @@ public:
bool hasPendingResponse() const { return _response_pending; }
void resetSessionState();
protected:
// Full framed response capacity, including magic/op/status/checksum. The
// default preserves USB/UART/TCP geometry. A BLE source can override this
// using its current ATT MTU without storing another buffer or callback.
virtual uint16_t responseFrameCapacity() const { return 197; }
private:
uint16_t responsePayloadCapacity() const;
// Send a request (op+args) and read its response header; on OK, `payload` (if non-null) receives
// `payload_len` bytes. Returns true iff a well-formed OK response for `op` arrived in time.
bool txn(uint8_t op, const uint8_t* args, uint8_t arglen, uint8_t* payload, uint32_t payload_len);