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 0d4274ceea
commit 818f610ba3
59 files changed
+3946 -2437

No files matched your search

+88 -6
View File
@@ -1,6 +1,7 @@
#pragma once
#include "BaseSerialInterface.h"
#include "CompanionFrameQueue.h"
#ifndef MAX_INTERFACES
// ble, usb, wifi, ethernet
@@ -26,6 +27,31 @@ private:
bool _enabled = false;
RegisteredInterface _interfaces[MAX_INTERFACES] = {};
BaseSerialInterface* _lastRxInterface = nullptr;
BaseSerialInterface* _lockedReplyInterface = nullptr;
bool isAvailableReplyTarget(BaseSerialInterface* target) const {
if (target == nullptr) return false;
for (auto iface : _interfaces) {
if (iface.instance == target) {
return target->isEnabled() && target->isConnected();
}
}
return false;
}
void clearReplyRouteFor(BaseSerialInterface* target) {
// Keep a locked pointer until the response producer observes that its
// target is unavailable and aborts the transaction. Clearing it here would
// make the remaining frames fall back to broadcast on another interface.
if (_lockedReplyInterface == target) return;
if (_lastRxInterface == target) _lastRxInterface = nullptr;
}
BaseSerialInterface* replyTarget() const {
return _lockedReplyInterface != nullptr
? _lockedReplyInterface : _lastRxInterface;
}
public:
bool addInterface(InterfaceType type, BaseSerialInterface* iface) {
@@ -56,6 +82,7 @@ public:
// find and remove interface
for(int i = 0; i < MAX_INTERFACES; i++){
if(_interfaces[i].instance == iface){
clearReplyRouteFor(iface);
_interfaces[i] = {};
return true;
}
@@ -77,6 +104,7 @@ public:
for(auto iface : _interfaces){
if(iface.instance && iface.type == InterfaceType::Bluetooth){
iface.instance->disable();
clearReplyRouteFor(iface.instance);
}
}
}
@@ -105,6 +133,8 @@ public:
// enable all interfaces
void enable() override {
_enabled = true;
_lastRxInterface = nullptr;
_lockedReplyInterface = nullptr;
for(auto iface : _interfaces){
if(iface.instance){
iface.instance->enable();
@@ -115,6 +145,8 @@ public:
// disable all interfaces
void disable() override {
_enabled = false;
_lastRxInterface = nullptr;
_lockedReplyInterface = nullptr;
for(auto iface : _interfaces){
if(iface.instance){
iface.instance->disable();
@@ -132,9 +164,17 @@ public:
return false;
}
// check if any interface is connected
// A locked multi-frame reply belongs to one client. Treat losing that
// client as a disconnect even if another transport remains connected, so
// the producer aborts instead of leaking the remainder to somebody else.
if (_lockedReplyInterface != nullptr) {
return isAvailableReplyTarget(_lockedReplyInterface);
}
// check if any enabled interface is connected
for(auto iface : _interfaces){
if(iface.instance && iface.instance->isConnected()) {
if(iface.instance && iface.instance->isEnabled()
&& iface.instance->isConnected()) {
return true;
}
}
@@ -158,7 +198,12 @@ public:
return false;
}
// check if any interface is busy
// Pace a response stream against its destination. A slow inactive BLE or
// WiFi client must not stall a contact sync running over USB (or vice versa).
BaseSerialInterface* target = replyTarget();
if (isAvailableReplyTarget(target)) return target->isWriteBusy();
// With no requester yet, preserve the aggregate behavior used for pushes.
for(auto iface : _interfaces){
if(iface.instance && iface.instance->isEnabled() && iface.instance->isWriteBusy()){
return true;
@@ -202,13 +247,35 @@ public:
return false;
}
void lockReplyRoute() override {
if (isAvailableReplyTarget(_lastRxInterface)) {
_lockedReplyInterface = _lastRxInterface;
}
}
void unlockReplyRoute() override {
_lockedReplyInterface = nullptr;
}
size_t writeFrame(const uint8_t src[], size_t len) override {
// don't write when disabled or nothing provided
if(!_enabled || len == 0){
if(!_enabled || src == nullptr || len == 0){
return 0;
}
// write frame to all enabled interfaces
// Responses and delivery-required completion pushes belong to the client
// which supplied the latest command. Best-effort asynchronous observations
// remain broadcast so passive connected apps can keep their view fresh.
if (mesh::companionFrameRequiresDelivery(src, len)) {
BaseSerialInterface* target = replyTarget();
if (target != nullptr) {
if (!isAvailableReplyTarget(target)) return 0;
return target->writeFrame(src, len);
}
}
// Before any command establishes a reply route, or for best-effort pushes,
// write the frame to all enabled interfaces.
bool allSuccessful = true;
for(auto iface : _interfaces){
if(iface.instance && iface.instance->isEnabled()){
@@ -224,15 +291,30 @@ public:
size_t checkRecvFrame(uint8_t dest[]) override {
// don't read when disabled
if(!_enabled){
if(!_enabled || dest == nullptr){
return 0;
}
// Keep a multi-frame response transaction on its originating transport.
// Other interfaces retain their input until the producer unlocks the route.
if (_lockedReplyInterface != nullptr) {
if (!isAvailableReplyTarget(_lockedReplyInterface)) return 0;
size_t frameSize = _lockedReplyInterface->checkRecvFrame(dest);
if (frameSize > 0) _lastRxInterface = _lockedReplyInterface;
return frameSize;
}
if (_lastRxInterface != nullptr
&& !isAvailableReplyTarget(_lastRxInterface)) {
_lastRxInterface = nullptr;
}
// try to read a frame from any enabled interface
for(auto iface : _interfaces){
if(iface.instance && iface.instance->isEnabled()){
size_t frameSize = iface.instance->checkRecvFrame(dest);
if(frameSize > 0){
_lastRxInterface = iface.instance;
return frameSize;
}
}