diff --git a/src/helpers/nrf52/SerialBLEInterface.cpp b/src/helpers/nrf52/SerialBLEInterface.cpp index 75a4e3b0..345daa9a 100644 --- a/src/helpers/nrf52/SerialBLEInterface.cpp +++ b/src/helpers/nrf52/SerialBLEInterface.cpp @@ -7,6 +7,9 @@ // Magic numbers came from actual testing #define BLE_HEALTH_CHECK_INTERVAL 10000 // Advertising watchdog check every 10 seconds #define BLE_RETRY_THROTTLE_MS 250 // Throttle retries to 250ms when queue buildup detected +#define BLE_RETRY_THROTTLE_MAX_MS 2000 // Cap for backoff below — a marginal link that keeps + // failing the (blocking, up to 100ms) send shouldn't get + // re-probed every 250ms, stealing ~40% of loop time // Connection parameters (units: interval=1.25ms, timeout=10ms) #define BLE_MIN_CONN_INTERVAL 12 // 15ms @@ -205,6 +208,7 @@ void SerialBLEInterface::clearBuffers() { send_queue_len = 0; recv_queue_len = 0; _last_retry_attempt = 0; + _retry_backoff_ms = 0; bleuart.flush(); } @@ -302,7 +306,8 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) { send_queue_len = 0; } else { unsigned long now = millis(); - bool throttle_active = (_last_retry_attempt > 0 && (now - _last_retry_attempt) < BLE_RETRY_THROTTLE_MS); + unsigned long throttle_ms = _retry_backoff_ms > 0 ? _retry_backoff_ms : BLE_RETRY_THROTTLE_MS; + bool throttle_active = (_last_retry_attempt > 0 && (now - _last_retry_attempt) < throttle_ms); if (!throttle_active) { Frame frame_to_send = send_queue[0]; @@ -311,19 +316,28 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) { if (written == frame_to_send.len) { BLE_DEBUG_PRINTLN("writeBytes: sz=%u, hdr=%u", (unsigned)frame_to_send.len, (unsigned)frame_to_send.buf[0]); _last_retry_attempt = 0; + _retry_backoff_ms = 0; shiftSendQueueLeft(); } else if (written > 0) { BLE_DEBUG_PRINTLN("writeBytes: partial write, sent=%u of %u, dropping corrupted frame", (unsigned)written, (unsigned)frame_to_send.len); _last_retry_attempt = 0; + _retry_backoff_ms = 0; shiftSendQueueLeft(); } else { if (!isConnected()) { BLE_DEBUG_PRINTLN("writeBytes failed: connection lost, dropping frame"); _last_retry_attempt = 0; + _retry_backoff_ms = 0; shiftSendQueueLeft(); } else { - BLE_DEBUG_PRINTLN("writeBytes failed (buffer full), keeping frame for retry"); + // Send blocked (peer not ACKing — e.g. marginal link range). Each attempt above can + // itself block up to 100ms (Bluefruit's notify() waits on a free HVN packet slot), so + // back off exponentially instead of re-probing every fixed 250ms — otherwise a + // sustained bad link steals ~40% of loop time indefinitely. + BLE_DEBUG_PRINTLN("writeBytes failed (buffer full), keeping frame for retry, backoff=%lums", throttle_ms); _last_retry_attempt = now; + unsigned long next = throttle_ms * 2; + _retry_backoff_ms = next > BLE_RETRY_THROTTLE_MAX_MS ? BLE_RETRY_THROTTLE_MAX_MS : next; } } } diff --git a/src/helpers/nrf52/SerialBLEInterface.h b/src/helpers/nrf52/SerialBLEInterface.h index de103054..b48b6660 100644 --- a/src/helpers/nrf52/SerialBLEInterface.h +++ b/src/helpers/nrf52/SerialBLEInterface.h @@ -15,6 +15,7 @@ class SerialBLEInterface : public BaseSerialInterface { uint16_t _conn_handle; unsigned long _last_health_check; unsigned long _last_retry_attempt; + unsigned long _retry_backoff_ms; // 0 == not yet backed off; grows on repeated failed sends struct Frame { uint8_t len; @@ -49,6 +50,7 @@ public: _conn_handle = BLE_CONN_HANDLE_INVALID; _last_health_check = 0; _last_retry_attempt = 0; + _retry_backoff_ms = 0; send_queue_len = 0; recv_queue_len = 0; }