Fix Cascade power saving, USB Companion, and mOTA flows

This commit is contained in:
mikecarper committed 2026-08-18 14:02:10 -07:00
1 parent 21bfc97712
commit 7a4da3ccfb
59 files changed
+3946 -2437

No files matched your search

+133 -18
View File
@@ -1,4 +1,5 @@
#include "ArduinoSerialInterface.h"
#include "CompanionFrameQueue.h"
#define RECV_STATE_IDLE 0
#define RECV_STATE_HDR_FOUND 1
@@ -11,6 +12,103 @@ void ArduinoSerialInterface::resetReceiveState() {
_secondaryControlSequencePos = 0;
_frame_len = 0;
rx_len = 0;
_last_rx_byte_ms = 0;
}
void ArduinoSerialInterface::serviceReceiveTimeout() {
if (_state != RECV_STATE_IDLE && _serial != nullptr
&& _serial->available() == 0
&& (uint32_t)(millis() - _last_rx_byte_ms) >= RX_FRAME_TIMEOUT_MS) {
// A truncated length-prefixed frame must not hold the MCU awake forever or
// turn bytes from a later session into the missing tail of the old frame.
resetReceiveState();
}
}
void ArduinoSerialInterface::resetTransmitState() {
_tx_queue_len = 0;
_tx_offset = 0;
}
bool ArduinoSerialInterface::enqueueFrame(const uint8_t src[], size_t len) {
if (src == nullptr || len == 0 || len > MAX_FRAME_SIZE) return false;
// MSG_WAITING is level-triggered. Retaining more than one copy only takes
// space away from command replies while a USB host is backpressuring us.
if (src[0] == 0x83) {
for (uint8_t i = 0; i < _tx_queue_len; ++i) {
if (_tx_queue[i].len > 3 && _tx_queue[i].buf[3] == 0x83) return true;
}
}
const bool delivery_required = mesh::companionFrameRequiresDelivery(src, len);
if (!delivery_required && _tx_queue_len >= TX_QUEUE_SIZE - 1) {
return false; // reserve one slot for a response or required push
}
if (_tx_queue_len == TX_QUEUE_SIZE) {
if (!delivery_required) return false;
// A required frame may replace queued best-effort traffic. Never replace
// the head after any of it has reached the host: doing so would splice two
// frames together on the byte stream.
const uint8_t first_evictable = _tx_offset == 0 ? 0 : 1;
int evict = -1;
for (int i = TX_QUEUE_SIZE - 1; i >= first_evictable; --i) {
const TxFrame& queued = _tx_queue[i];
if (!mesh::companionFrameRequiresDelivery(&queued.buf[3], queued.len - 3)) {
evict = i;
break;
}
}
if (evict < 0) return false;
for (uint8_t i = (uint8_t)evict; i + 1 < _tx_queue_len; ++i) {
_tx_queue[i] = _tx_queue[i + 1];
}
--_tx_queue_len;
}
TxFrame& frame = _tx_queue[_tx_queue_len++];
frame.len = (uint16_t)(len + 3);
frame.buf[0] = '>';
frame.buf[1] = (uint8_t)(len & 0xFF);
frame.buf[2] = (uint8_t)(len >> 8);
memcpy(&frame.buf[3], src, len);
return true;
}
void ArduinoSerialInterface::serviceTransmit() {
if (!_flow_ctl || _passthroughMode || _serial == nullptr) return;
if (!isConnected()) {
// A disconnected USB endpoint cannot make progress. Its host also loses
// any partial endpoint data on disconnect, so begin the next session clean.
resetTransmitState();
return;
}
while (_tx_queue_len > 0) {
TxFrame& frame = _tx_queue[0];
const size_t remaining = frame.len - _tx_offset;
int available = _serial->availableForWrite();
if (available <= 0) return;
// A UART or CDC FIFO can be smaller than MAX_FRAME_SIZE. Drain the frame in
// bounded chunks, retaining the offset and never allowing another frame to
// interleave until this header and body are complete.
const size_t attempt = (size_t)available < remaining
? (size_t)available : remaining;
size_t written = _serial->write(&frame.buf[_tx_offset], attempt);
if (written > attempt) written = attempt;
_tx_offset += (uint16_t)written;
if (_tx_offset < frame.len) return;
for (uint8_t i = 0; i + 1 < _tx_queue_len; ++i) {
_tx_queue[i] = _tx_queue[i + 1];
}
--_tx_queue_len;
_tx_offset = 0;
if (written < attempt) return;
}
}
bool ArduinoSerialInterface::checkControlSequence(uint8_t c,
@@ -39,6 +137,7 @@ void ArduinoSerialInterface::setPassthroughMode(bool enabled) {
_controlSequenceReceived = false;
_secondaryControlSequenceReceived = false;
resetReceiveState();
resetTransmitState();
}
bool ArduinoSerialInterface::takeControlSequence() {
@@ -57,37 +156,52 @@ void ArduinoSerialInterface::enable() {
_isEnabled = true;
_controlSequenceReceived = false;
_secondaryControlSequenceReceived = false;
_has_received_frame = false;
resetReceiveState();
resetTransmitState();
}
void ArduinoSerialInterface::disable() {
_isEnabled = false;
_controlSequenceReceived = false;
_secondaryControlSequenceReceived = false;
_has_received_frame = false;
resetReceiveState();
resetTransmitState();
}
bool ArduinoSerialInterface::isConnected() const {
if (_serial == nullptr) return false;
if (_conn_check) return _conn_check();
return true; // no way of knowing, so assume yes
}
void ArduinoSerialInterface::loop() {
serviceReceiveTimeout();
serviceTransmit();
}
bool ArduinoSerialInterface::isReadBusy() const {
return false;
return _state != RECV_STATE_IDLE;
}
bool ArduinoSerialInterface::isWriteBusy() const {
if (_passthroughMode) return false;
if (_passthroughMode || _serial == nullptr) return false;
if (_flow_ctl && isConnected()) {
return const_cast<Stream*>(_serial)->availableForWrite() < (int)(MAX_FRAME_SIZE + 3);
if (_tx_queue_len > 0) return true;
return const_cast<Stream*>(_serial)->availableForWrite() <= 0;
}
// while nobody drains the port the TX buffer stays full, so never report
// busy in that case: it would stall the paced streams on all interfaces
return false;
}
bool ArduinoSerialInterface::hasPendingIO() const {
return isReadBusy() || _tx_queue_len > 0;
}
size_t ArduinoSerialInterface::writeFrame(const uint8_t src[], size_t len) {
if (len > MAX_FRAME_SIZE) {
// frame is too big!
if (src == nullptr || len == 0 || len > MAX_FRAME_SIZE || _serial == nullptr) {
// invalid frame or an interface which has not begun yet
return 0;
}
if (_passthroughMode) return len;
@@ -95,29 +209,29 @@ size_t ArduinoSerialInterface::writeFrame(const uint8_t src[], size_t len) {
if (!isConnected()) {
return len; // nobody is listening, drop instead of filling the TX buffer
}
if (_serial->availableForWrite() < (int)(len + 3)) {
// a short write would tear the length prefixed framing, and as there is
// neither a checksum nor a resync marker the receiver would stay out of
// sync forever - so drop the whole frame instead
return 0;
}
if (!enqueueFrame(src, len)) return 0;
serviceTransmit();
return len;
}
uint8_t hdr[3];
hdr[0] = '>';
hdr[1] = (len & 0xFF); // LSB
hdr[2] = (len >> 8); // MSB
_serial->write(hdr, 3);
return _serial->write(src, len);
uint8_t frame[MAX_FRAME_SIZE + 3];
frame[0] = '>';
frame[1] = (uint8_t)(len & 0xFF); // LSB
frame[2] = (uint8_t)(len >> 8); // MSB
memcpy(&frame[3], src, len);
return _serial->write(frame, len + 3) == len + 3 ? len : 0;
}
size_t ArduinoSerialInterface::checkRecvFrame(uint8_t dest[]) {
if (_serial == nullptr || dest == nullptr) return 0;
serviceReceiveTimeout();
serviceTransmit();
if (_passthroughMode) return 0;
while (_serial->available()) {
int c = _serial->read();
if (c < 0) break;
_last_rx_byte_ms = millis();
switch (_state) {
case RECV_STATE_IDLE:
@@ -152,6 +266,7 @@ size_t ArduinoSerialInterface::checkRecvFrame(uint8_t dest[]) {
memcpy(dest, rx_buf, _frame_len);
_state = RECV_STATE_IDLE; // reset state, for next frame
_last_frame_ms = millis(); // a real client is talking to us
_has_received_frame = true;
return _frame_len;
}
}