Preserve BLE command replies and respect negotiated mOTA MTU
This commit is contained in:
1 parent
6b75ad07d3
commit
e57ab1309f
18 files changed
+1111
-10
No files matched your search
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user