#pragma once #include "BaseSerialInterface.h" #include "CompanionFrameQueue.h" #include "WirelessControl.h" #ifndef MAX_INTERFACES // ble, usb, wifi, ethernet #define MAX_INTERFACES 4 #endif enum class InterfaceType : uint8_t { NONE, Bluetooth, USB, WiFi, Ethernet, HardwareSerial }; class MultiSerialInterface : public BaseSerialInterface { private: struct RegisteredInterface { InterfaceType type = InterfaceType::NONE; BaseSerialInterface* instance = nullptr; }; bool _enabled = false; bool _bluetooth_auto_enable = true; bool _bluetooth_requested = false; bool _bluetooth_inhibited = false; uint8_t _nextRxIndex = 0; 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: // The Companion's saved preference controls blanket startup/wake enables. // Explicit Bluetooth/wireless controls remain able to enable it temporarily. void setBluetoothAutoEnable(bool enabled) { _bluetooth_auto_enable = enabled; } // A temporary transport priority must not change the saved preference or // lose explicit on/off requests made while Bluetooth is unavailable. bool isBluetoothInhibited() const { return _bluetooth_inhibited; } void setBluetoothInhibited(bool inhibited) { if (_bluetooth_inhibited == inhibited) return; _bluetooth_inhibited = inhibited; if (inhibited) { const bool requested = _bluetooth_requested; disableBluetooth(); _bluetooth_requested = requested; } else if (_enabled && _bluetooth_requested) { enableBluetooth(); } } bool addInterface(InterfaceType type, BaseSerialInterface* iface) { // make sure an interface was provided if(iface == nullptr){ return false; } // put it in the first free slot for(int i = 0; i < MAX_INTERFACES; i++){ if(_interfaces[i].instance == nullptr){ _interfaces[i].instance = iface; _interfaces[i].type = type; return true; } } // no free slots available return false; } bool removeInterface(BaseSerialInterface* iface) { // make sure an interface was provided if(iface == nullptr){ return false; } // find and remove interface for(int i = 0; i < MAX_INTERFACES; i++){ if(_interfaces[i].instance == iface){ clearReplyRouteFor(iface); _interfaces[i] = {}; return true; } } // interface not found return false; } void enableBluetooth() { _bluetooth_requested = true; if (_bluetooth_inhibited) return; if (mesh::wireless::control().blocked(mesh::wireless::Bluetooth)) return; for(auto iface : _interfaces){ if(iface.instance && iface.type == InterfaceType::Bluetooth){ iface.instance->enable(); } } } void disableBluetooth() { _bluetooth_requested = false; for(auto iface : _interfaces){ if(iface.instance && iface.type == InterfaceType::Bluetooth){ iface.instance->disable(); clearReplyRouteFor(iface.instance); } } } bool isBluetoothEnabled() { for(auto iface : _interfaces){ if(iface.instance && iface.type == InterfaceType::Bluetooth){ return iface.instance->isEnabled(); } } return false; } bool isBluetoothConnected() const { return isInterfaceConnected(InterfaceType::Bluetooth); } bool isInterfaceConnected(InterfaceType type) const { if (!_enabled) return false; for (auto iface : _interfaces) { if (iface.instance && iface.type == type && iface.instance->isEnabled() && iface.instance->isConnected()) { return true; } } return false; } // enable all interfaces void enable() override { _enabled = true; _bluetooth_requested = _bluetooth_auto_enable; _nextRxIndex = 0; _lastRxInterface = nullptr; _lockedReplyInterface = nullptr; for(auto iface : _interfaces){ if(iface.instance){ if (iface.type == InterfaceType::Bluetooth && (!_bluetooth_auto_enable || _bluetooth_inhibited || mesh::wireless::control().blocked(mesh::wireless::Bluetooth))) continue; iface.instance->enable(); } } } // disable all interfaces void disable() override { _enabled = false; _nextRxIndex = 0; _lastRxInterface = nullptr; _lockedReplyInterface = nullptr; for(auto iface : _interfaces){ if(iface.instance){ iface.instance->disable(); } } } bool isEnabled() const override { return _enabled; } bool isConnected() const override { // not connected when disabled if(!_enabled){ return false; } // 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->isEnabled() && iface.instance->isConnected()) { return true; } } // nothing connected return false; } // loop all interfaces void loop() override { for(auto iface : _interfaces){ if(iface.instance){ iface.instance->loop(); } } } bool isWriteBusy() const override { // not busy when disabled if(!_enabled){ return false; } // 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; } } // nothing busy return false; } bool isReadBusy() const override { if (!_enabled) return false; for (auto iface : _interfaces) { if (iface.instance && iface.instance->isEnabled() && iface.instance->isReadBusy()) { return true; } } return false; } bool hasPendingIO() const override { if (!_enabled) return false; for (auto iface : _interfaces) { if (iface.instance && iface.instance->isEnabled() && iface.instance->hasPendingIO()) { return true; } } return false; } bool takePairingRequest() override { for (auto iface : _interfaces) { if (iface.instance && iface.type == InterfaceType::Bluetooth && iface.instance->isEnabled() && iface.instance->takePairingRequest()) { return true; } } return false; } // Exact transport-session resets can arrive independently of // isConnected() polling (for example, a close/reopen between two loop // iterations). Let the application abort only work which belongs to that // transport, without disturbing an active BLE/WiFi response transaction. bool isReplyRouteFor(BaseSerialInterface* target) const { return target != nullptr && replyTarget() == target; } BaseSerialInterface* captureReplyRoute() override { return replyTarget(); } bool isReplyRouteAvailable(BaseSerialInterface* route) const override { return _enabled && isAvailableReplyTarget(route); } bool isReplyRouteWriteBusy(BaseSerialInterface* route) const override { return !_enabled || !isAvailableReplyTarget(route) || route->isWriteBusy(); } size_t writeFrameToRoute(BaseSerialInterface* route, const uint8_t src[], size_t len) override { if (!_enabled || src == nullptr || len == 0 || !isAvailableReplyTarget(route)) { return 0; } return route->writeFrame(src, len); } // Call this only after the response producer for target has been stopped. // Forgetting both pointers prevents a new host on the same transport from // inheriting the prior host's reply destination. void forgetReplyRouteForDisconnected(BaseSerialInterface* target) { if (target == nullptr) return; if (_lockedReplyInterface == target) _lockedReplyInterface = nullptr; if (_lastRxInterface == target) _lastRxInterface = nullptr; } 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 || src == nullptr || len == 0){ return 0; } // Responses and command-completion pushes belong to their requester. // Unsolicited pushes remain broadcast regardless of queue priority so // passive connected apps can keep their view fresh. if (mesh::companionFrameUsesRequesterRoute(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()){ if(iface.instance->writeFrame(src, len) != len){ allSuccessful = false; } } } // report success if all writes completed successfully return allSuccessful ? len : 0; } size_t checkRecvFrame(uint8_t dest[]) override { // don't read when disabled 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; // A backend may cancel the previous host's stream inside this call // (e.g. WiFi IP takeover), clearing the lock before returning a new // host's first command. Preserve the actual source of that command. BaseSerialInterface* target = _lockedReplyInterface; size_t frameSize = target->checkRecvFrame(dest); if (frameSize > 0) _lastRxInterface = target; return frameSize; } if (_lastRxInterface != nullptr && !isAvailableReplyTarget(_lastRxInterface)) { _lastRxInterface = nullptr; } // Start after the last command so a pipelined client cannot starve the // other transports. Locked response streams retain their exclusive poll. for (size_t offset = 0; offset < MAX_INTERFACES; ++offset) { const size_t index = (_nextRxIndex + offset) % MAX_INTERFACES; auto iface = _interfaces[index]; if(iface.instance && iface.instance->isEnabled()){ size_t frameSize = iface.instance->checkRecvFrame(dest); if(frameSize > 0){ _nextRxIndex = (index + 1) % MAX_INTERFACES; _lastRxInterface = iface.instance; return frameSize; } } } // no frame received return 0; } };