diff --git a/docs/companion_radio_full.md b/docs/companion_radio_full.md index 4a7b6b2bd..6a6ffbbcf 100644 --- a/docs/companion_radio_full.md +++ b/docs/companion_radio_full.md @@ -701,6 +701,12 @@ a fixed pairing PIN, then `reboot` to activate it. `set pin 0` restores the default/generated PIN policy. A PIN saved through the Companion protocol also takes effect after reboot. See the [PIN command reference](cli_commands.md#set-the-bluetooth-pairing-pin-companion). +On T1000-E, a USB data host automatically pauses Bluetooth, including an +existing phone connection. Unplug USB to resume Bluetooth after 500 ms; a +saved off setting stays off. A wall charger without a USB data host does not +pause Bluetooth. See the [T1000-E transport policy](role_feature_switches.md) +for status and saved-setting behavior. + The Bluetooth device name is independently configurable. In the text terminal, use `get bluetooth.name` and `set bluetooth.name `; use `set bluetooth.name default` to restore `MeshCore-`. The WebConfig diff --git a/docs/role_feature_switches.md b/docs/role_feature_switches.md index 9d05bb531..455391ca1 100644 --- a/docs/role_feature_switches.md +++ b/docs/role_feature_switches.md @@ -178,6 +178,14 @@ See [Bluetooth controls](cli_commands.md#turn-bluetooth-on-or-off-companion) for reply draining and boot-transport restrictions. Ethernet controls remain specific to the board's recipe; there is no universal Ethernet-off command. +**T1000-E Companion:** USB data takes priority over Bluetooth. Once a computer +enumerates USB, the firmware stops Bluetooth advertising and disconnects any +Bluetooth client, even if no serial application is open. Bluetooth resumes +500 ms after USB disconnects, unless it was switched off manually. Battery +power and a charge-only wall supply leave Bluetooth available. The saved +Bluetooth setting, pairing PIN and bonds are preserved. `get bluetooth` +reports the USB pause; `set bluetooth on` saves the choice for after unplugging. + **SenseCAP Indicator Full only:** `set companion.transport wifi` followed by `reboot` selects WiFi; `set companion.transport ble` followed by `reboot` selects Bluetooth. Check `get companion.transport`. USB remains available. diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 14c7d18ab..cd8a4650b 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -63,6 +63,7 @@ static uint32_t _atoi(const char* sp) { // interface manager #include +#include MultiSerialInterface interface_manager; // include bluetooth interface @@ -2914,6 +2915,23 @@ void halt() { if (companion_bluetooth_start_at == 0) companion_bluetooth_start_at = 1; } + static void serviceCompanionUsbBluetoothPriority() { +#if defined(T1000_E) && defined(USE_TINYUSB) + static mesh::UsbBluetoothPriority priority; + const bool inhibited = priority.update(board.isUsbHostConnected(), millis()); + if (inhibited && !interface_manager.isBluetoothInhibited()) { + // Abandon only this transport's work; USB must not inherit a partially + // streamed Bluetooth reply when the link is disconnected. + the_mesh.cancelSerialResponseStream(&bluetooth_interface); + the_mesh.cancelSerialOperationsForRoute(&bluetooth_interface); + } + interface_manager.setBluetoothInhibited(inhibited); + if (inhibited) { + interface_manager.forgetReplyRouteForDisconnected(&bluetooth_interface); + } +#endif + } + static void startCompanionBluetooth() { if (companion_bluetooth_initialized) return; companion_bluetooth_start_at = 0; @@ -3009,8 +3027,9 @@ void halt() { #endif companion_bluetooth_initialized = true; companion_bluetooth_start_failure[0] = 0; + serviceCompanionUsbBluetoothPriority(); if (interface_manager.isEnabled() && the_mesh.isBluetoothEnabledPreference()) { - bluetooth_interface.enable(); + interface_manager.enableBluetooth(); } } @@ -3236,7 +3255,9 @@ bool handleCompanionBluetoothCommand(const char* command, char* reply, if (action == CompanionBluetoothCommand::Get) { snprintf(reply, reply_size, "bluetooth %s%s", interface_manager.isBluetoothEnabled() ? "on" : "off", - companion_bluetooth_off_at ? " (off pending)" : ""); + companion_bluetooth_off_at ? " (off pending)" + : interface_manager.isBluetoothInhibited() + ? " (USB data connected)" : ""); return true; } if (action == CompanionBluetoothCommand::On) { @@ -3257,7 +3278,11 @@ bool handleCompanionBluetoothCommand(const char* command, char* reply, } interface_manager.setBluetoothAutoEnable(true); interface_manager.enableBluetooth(); - if (interface_manager.isBluetoothEnabled()) { + if (interface_manager.isBluetoothInhibited()) { + companion_bluetooth_off_at = 0; + snprintf(reply, reply_size, + "OK - Bluetooth on (saved); paused while USB data is connected"); + } else if (interface_manager.isBluetoothEnabled()) { companion_bluetooth_off_at = 0; snprintf(reply, reply_size, "OK - Bluetooth on (saved)"); } else if (the_mesh.setBluetoothEnabledPreference(previous_preference)) { @@ -3276,6 +3301,7 @@ bool handleCompanionBluetoothCommand(const char* command, char* reply, } companion_bluetooth_off_at = 0; interface_manager.setBluetoothAutoEnable(false); + disableCompanionBluetoothForCli(); snprintf(reply, reply_size, "OK - Bluetooth already off (saved)"); return true; } @@ -3394,6 +3420,7 @@ public: companion_bluetooth_off_at = 0; if (on) interface_manager.enableBluetooth(); else disableCompanionBluetoothForCli(); + if (on && interface_manager.isBluetoothInhibited()) return Result::Done; return interface_manager.isBluetoothEnabled() == on ? Result::Done : Result::Failed; } #endif @@ -3821,6 +3848,7 @@ void setup() { // Keep stack registration available for a later USB `set bluetooth on`, // without advertising even briefly when startup enables all interfaces. interface_manager.setBluetoothAutoEnable(the_mesh.isBluetoothEnabledPreference()); + serviceCompanionUsbBluetoothPriority(); #endif // Lock the saved transport selection before bringing up either wireless stack. @@ -4116,6 +4144,9 @@ static bool usbLoggingRecoverySafe(void*) { } void loop() { +#if defined(BLE_PIN_CODE) + serviceCompanionUsbBluetoothPriority(); +#endif #if defined(NRF52_PLATFORM) board.prepareSleepCycle(); #endif diff --git a/src/helpers/MultiSerialInterface.h b/src/helpers/MultiSerialInterface.h index d5a4ec8c1..1b15d3c4a 100644 --- a/src/helpers/MultiSerialInterface.h +++ b/src/helpers/MultiSerialInterface.h @@ -28,6 +28,8 @@ private: 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; @@ -61,6 +63,21 @@ public: // 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){ @@ -100,6 +117,8 @@ public: } 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){ @@ -109,6 +128,7 @@ public: } void disableBluetooth() { + _bluetooth_requested = false; for(auto iface : _interfaces){ if(iface.instance && iface.type == InterfaceType::Bluetooth){ iface.instance->disable(); @@ -145,12 +165,15 @@ public: // 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) continue; + if (iface.type == InterfaceType::Bluetooth + && (!_bluetooth_auto_enable || _bluetooth_inhibited + || mesh::wireless::control().blocked(mesh::wireless::Bluetooth))) continue; iface.instance->enable(); } } diff --git a/src/helpers/UsbBluetoothPriority.h b/src/helpers/UsbBluetoothPriority.h new file mode 100644 index 000000000..1837fd874 --- /dev/null +++ b/src/helpers/UsbBluetoothPriority.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +namespace mesh { + +// USB enumeration, rather than VBUS or an open terminal, identifies a data +// host. Hold priority across brief bus resets so Bluetooth does not flap as +// the same host re-enumerates. A wall charger never acquires priority. +class UsbBluetoothPriority { + bool inhibited_ = false; + bool disconnect_pending_ = false; + uint32_t disconnected_at_ = 0; + +public: + bool update(bool host_connected, uint32_t now) { + if (host_connected) { + inhibited_ = true; + disconnect_pending_ = false; + } else if (inhibited_) { + if (!disconnect_pending_) { + disconnected_at_ = now; + disconnect_pending_ = true; + } else if (uint32_t(now - disconnected_at_) >= 500) { + inhibited_ = false; + disconnect_pending_ = false; + } + } + return inhibited_; + } +}; + +} // namespace mesh diff --git a/src/helpers/nrf52/SerialBLEInterface.cpp b/src/helpers/nrf52/SerialBLEInterface.cpp index e0552d8ba..b9d20c303 100644 --- a/src/helpers/nrf52/SerialBLEInterface.cpp +++ b/src/helpers/nrf52/SerialBLEInterface.cpp @@ -316,6 +316,12 @@ void SerialBLEInterface::onConnect(uint16_t connection_handle) { instance->_pairingRequestPending.store(false, std::memory_order_release); instance->_conn_handle = connection_handle; instance->_isDeviceConnected = false; + // A queued connect callback can arrive after disable() stopped advertising + // and sampled an invalid handle. Close that late link as well. + if (!instance->_isEnabled) { + instance->disconnect(); + return; + } instance->_security_timer.start(millis()); instance->_companion_start_timer.cancel(); instance->_companionDataSeen.store(false, std::memory_order_release); @@ -763,7 +769,7 @@ bool SerialBLEInterface::configureBondedOnlyAdvertising( Bluefruit.Advertising.setType( BLE_GAP_ADV_TYPE_CONNECTABLE_NONSCANNABLE_DIRECTED); Bluefruit.Advertising.setPeerAddress(native_peer); - Bluefruit.Advertising.restartOnDisconnect(true); + Bluefruit.Advertising.restartOnDisconnect(_isEnabled); _bonded_only = true; _stealth_pair_once = false; _advertisingSuppressed.store(false, std::memory_order_release); @@ -1016,7 +1022,7 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name, Bluefruit.Advertising.setInterval(BLE_ADV_INTERVAL_MIN, BLE_ADV_INTERVAL_MAX); Bluefruit.Advertising.setFastTimeout(BLE_ADV_FAST_TIMEOUT); - Bluefruit.Advertising.restartOnDisconnect(true); + Bluefruit.Advertising.restartOnDisconnect(_isEnabled); if (bonded_only_peer != nullptr) { configureBondedOnlyAdvertising(*bonded_only_peer, true); @@ -1119,7 +1125,7 @@ void SerialBLEInterface::cancelStealthPairingTransition() { _advertisingSuppressed.store(false, std::memory_order_release); Bluefruit.Advertising.setType( BLE_GAP_ADV_TYPE_CONNECTABLE_SCANNABLE_UNDIRECTED); - Bluefruit.Advertising.restartOnDisconnect(true); + Bluefruit.Advertising.restartOnDisconnect(_isEnabled); if (_isEnabled && _conn_handle == BLE_CONN_HANDLE_INVALID && !isAdvertising()) { startAdvertising("advertising failed after stealth cancellation"); @@ -1275,7 +1281,7 @@ void SerialBLEInterface::recoverStalledTx(const char* cause, } bool SerialBLEInterface::isValidConnection(uint16_t handle, bool requireWaitingForSecurity) const { - if (_conn_handle != handle) { + if (!_isEnabled || _conn_handle != handle) { return false; } BLEConnection* conn = Bluefruit.Connection(handle); diff --git a/test/fixtures/nrf52_ble_rx_queue.cpp b/test/fixtures/nrf52_ble_rx_queue.cpp index 9efa87d8d..0725f2247 100644 --- a/test/fixtures/nrf52_ble_rx_queue.cpp +++ b/test/fixtures/nrf52_ble_rx_queue.cpp @@ -114,7 +114,12 @@ static bool cccd_written_this_connection = false; static struct { std::atomic valid{false}; } cccd_ram_cache; static void captureCccdDeferred(uint16_t, ble_gap_addr_t*) {} template static bool ada_callback(Args...) { return true; } -static uint32_t sd_ble_gap_disconnect(uint16_t, uint8_t) { return NRF_SUCCESS; } +static std::vector> disconnect_requests; +static uint32_t sd_ble_gap_disconnect(uint16_t handle, uint8_t reason) { + assert(!critical_depth); + disconnect_requests.emplace_back(handle, reason); + return NRF_SUCCESS; +} static uint32_t sd_ble_gap_conn_param_update(uint16_t, const void*) { return NRF_SUCCESS; } static uint32_t millis() { return 100; } @@ -146,6 +151,7 @@ struct SerialBLEInterface { ble_gap_addr_t _peer_address; bool _peer_address_valid = false, _bond_removed_for_connection = false; bool _bonded_only = false, _isDeviceConnected = false; + bool _isEnabled = true; uint16_t _conn_handle = BLE_CONN_HANDLE_INVALID; unsigned removed_bonds = 0, bonded_recoveries = 0; bool connected = true; @@ -204,6 +210,7 @@ struct SerialBLEInterface { ++bonded_recoveries; } void clearBuffers(); + void disconnect(); void resetTxState(); bool servicePendingTxReset(); static void onBLEEvent(ble_evt_t*); @@ -476,9 +483,37 @@ static void production_connection_events_preserve_disconnect_cleanup() { assert(link.recv_queue.snapshot(7, generation)); } +static void late_connect_after_disable_is_disconnected() { + SerialBLEInterface link; + instance = &link; + link._isEnabled = false; + link.recv_queue.setEnabled(false); + // The physical event and deferred callback may already be queued when USB + // priority disables Bluetooth. It must not leave an occupied hidden link. + ble_evt_t event; + event.header.evt_id = BLE_GAP_EVT_CONNECTED; + SerialBLEInterface::onBLEEvent(&event); + const size_t requests_before = disconnect_requests.size(); + SerialBLEInterface::onConnect(7); + assert(disconnect_requests.size() == requests_before + 1); + assert(disconnect_requests.back().first == 7); + assert(disconnect_requests.back().second == BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION); + assert(!link._security_timer.pending() && !link._companion_start_timer.pending()); + assert(!link._isDeviceConnected && !link._pairingRequestPending); + link.recv_queue.secure(7); + uint32_t generation; + assert(!link.recv_queue.snapshot(7, generation)); + assert(link.removed_bonds == 0 && link.bonded_recoveries == 0); + // Keep the actual handle until the confirmed disconnect cleans it up. + assert(link._conn_handle == 7); + SerialBLEInterface::onDisconnect(7, BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION); + assert(link._conn_handle == BLE_CONN_HANDLE_INVALID); +} + int main() { ring_order_capacity_and_interleaved_copy(); session_reset_and_stale_handles(); callback_read_reset_and_reconnect(); production_connection_events_preserve_disconnect_cleanup(); + late_connect_after_disable_is_disconnected(); } diff --git a/test/test_companion_bluetooth_persistence.py b/test/test_companion_bluetooth_persistence.py index d1054115e..779916dc0 100644 --- a/test/test_companion_bluetooth_persistence.py +++ b/test/test_companion_bluetooth_persistence.py @@ -194,6 +194,7 @@ class BluetoothPersistenceTests(unittest.TestCase): main = (ROOT/'examples/companion_radio/main.cpp').read_text() methods = '\n'.join(method(main, signature) for signature in ( 'static void scheduleCompanionBluetoothRetry(', + 'static void serviceCompanionUsbBluetoothPriority(', 'static void startCompanionBluetooth(', 'static void serviceDeferredCompanionBluetooth(')) with tempfile.TemporaryDirectory() as directory: diff --git a/test/test_companion_usb_bluetooth_priority.py b/test/test_companion_usb_bluetooth_priority.py new file mode 100644 index 000000000..acd67b0f6 --- /dev/null +++ b/test/test_companion_usb_bluetooth_priority.py @@ -0,0 +1,326 @@ +"""Execute USB/Bluetooth priority transitions with the real transport manager.""" + +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + +from test_companion_bluetooth_control import HARNESS as BLUETOOTH_HARNESS +from test_radio_receive_contract import method + + +ROOT = Path(__file__).resolve().parents[1] + + +TRANSPORT_HARNESS = r''' +#include +#include +#include +#include + +struct Transport : BaseSerialInterface { + bool enabled=false,connected=false,frame=false; + unsigned enables=0,disables=0,writes=0; + void enable() override {enabled=true;++enables;} + void disable() override {enabled=false;connected=false;++disables;} + bool isEnabled() const override {return enabled;} + bool isConnected() const override {return connected;} + bool isReadBusy() const override {return false;} + bool isWriteBusy() const override {return false;} + size_t writeFrame(const uint8_t*,size_t n) override {++writes;return n;} + size_t checkRecvFrame(uint8_t* out) override { + if(!frame)return 0;frame=false;out[0]=0x42;return 1; + } +}; +struct Fixture { + MultiSerialInterface manager; + Transport ble,usb,wifi,ethernet; + Fixture(){ + assert(manager.addInterface(InterfaceType::Bluetooth,&ble)); + assert(manager.addInterface(InterfaceType::USB,&usb)); + assert(manager.addInterface(InterfaceType::WiFi,&wifi)); + assert(manager.addInterface(InterfaceType::Ethernet,ðernet)); + } + void route(Transport& target){ + target.connected=true;target.frame=true; + uint8_t data[176];assert(manager.checkRecvFrame(data)==1); + assert(manager.captureReplyRoute()==&target); + } +}; +''' + + +class UsbBluetoothPriorityTests(unittest.TestCase): + def compile_and_run(self, source_text, flags=()): + with tempfile.TemporaryDirectory() as directory: + folder = Path(directory) + (folder / "Arduino.h").write_text( + "#pragma once\n#include \n#include \n" + "#include \n" + ) + source = folder / "usb_ble_priority.cpp" + source.write_text(source_text) + binary = folder / "usb_ble_priority" + built = subprocess.run( + [ + os.environ.get("CXX", "g++"), + "-std=c++17", "-Wall", "-Wextra", + "-I", str(folder), "-I", str(ROOT), "-I", str(ROOT / "src"), + *["-D" + flag for flag in flags], + str(source), "-o", str(binary), + ], + capture_output=True, text=True, + ) + self.assertEqual(built.returncode, 0, built.stdout + built.stderr) + ran = subprocess.run([str(binary)], capture_output=True, text=True) + self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr) + + def test_data_host_priority_and_disconnect_hold(self): + self.compile_and_run(TRANSPORT_HARNESS + r''' +int main(){ + mesh::UsbBluetoothPriority policy; + // Battery and a charge-only cable have no enumerated data host. + assert(!policy.update(false,0)); + assert(!policy.update(false,60000)); + assert(policy.update(true,60001)); + assert(policy.update(true,90000)); + // A bus reset cannot briefly advertise Bluetooth to a nearby phone. + assert(policy.update(false,90001)); + assert(policy.update(false,90500)); + assert(!policy.update(false,90501)); + assert(!policy.update(false,100000)); + // Re-enumeration cancels the pending release, without inheriting its timer. + assert(policy.update(true,100001)); + assert(policy.update(false,100010)); + assert(policy.update(true,100509)); + assert(policy.update(false,100510)); + assert(policy.update(false,101009)); + assert(!policy.update(false,101010)); + // The absence hold has the same duration when millis() wraps. + mesh::UsbBluetoothPriority wrapped; + assert(wrapped.update(true,0xfffffe00u)); + assert(wrapped.update(false,0xffffff00u)); + assert(wrapped.update(false,0x000000f3u)); + assert(!wrapped.update(false,0x000000f4u)); +} +''') + + def test_boot_wake_and_explicit_requests_during_usb_priority(self): + self.compile_and_run(TRANSPORT_HARNESS + r''' +int main(){ + Fixture f; + f.manager.setBluetoothInhibited(true); + assert(f.manager.isBluetoothInhibited()); + f.manager.enable(); + assert(!f.ble.enabled&&f.ble.enables==0); + assert(f.usb.enabled&&f.wifi.enabled&&f.ethernet.enabled); + // Opening/closing a serial application is not an enable route around USB. + f.manager.enableBluetooth(); + assert(!f.ble.enabled&&f.ble.enables==0); + const unsigned stopped=f.ble.disables; + f.manager.setBluetoothInhibited(true); + assert(f.ble.disables==stopped); // Repeated mounted polls are idempotent. + f.manager.disable();f.manager.enable(); + assert(!f.ble.enabled&&f.ble.enables==0&&f.usb.enabled); + f.manager.setBluetoothInhibited(false); + assert(f.ble.enabled&&f.ble.enables==1); + f.manager.setBluetoothInhibited(false); + assert(f.ble.enables==1); + // An off request while paused survives unplugging. + f.manager.setBluetoothInhibited(true); + f.manager.disableBluetooth(); + f.manager.setBluetoothInhibited(false); + assert(!f.ble.enabled&&f.ble.enables==1); + // An explicit on request is remembered, without advertising during USB. + f.manager.setBluetoothInhibited(true); + f.manager.enableBluetooth(); + assert(!f.ble.enabled&&f.ble.enables==1); + f.manager.setBluetoothInhibited(false); + assert(f.ble.enabled&&f.ble.enables==2); + // Saved-off startup/wake remains off after the temporary policy clears. + Fixture off; + off.manager.setBluetoothAutoEnable(false); + off.manager.setBluetoothInhibited(true);off.manager.enable(); + off.manager.setBluetoothInhibited(false); + assert(!off.ble.enabled&&off.ble.enables==0&&off.usb.enabled); + off.manager.setBluetoothInhibited(true);off.manager.enableBluetooth(); + off.manager.setBluetoothInhibited(false); + assert(off.ble.enabled&&off.ble.enables==1); // Runtime-only override. + off.manager.disable();off.manager.enable(); + assert(!off.ble.enabled&&off.ble.enables==1); + // An unplug cannot wake the aggregate transport when it is disabled. + Fixture sleeping; + sleeping.manager.enable();sleeping.manager.setBluetoothInhibited(true); + sleeping.manager.disable(); + const unsigned sleeping_enables=sleeping.ble.enables; + sleeping.manager.setBluetoothInhibited(false); + assert(!sleeping.ble.enabled&&sleeping.ble.enables==sleeping_enables); +} +''') + + def test_actual_t1000_service_uses_enumeration_and_cancels_only_bluetooth(self): + main = (ROOT / "examples/companion_radio/main.cpp").read_text() + service = method(main, "static void serviceCompanionUsbBluetoothPriority(") + source = TRANSPORT_HARNESS + r''' +uint32_t now_ms=0; +uint32_t millis(){return now_ms;} +MultiSerialInterface interface_manager; +Transport bluetooth_interface,usb_interface; +struct { + bool host=false,charging=true,reader=false; + unsigned samples=0; + bool isUsbHostConnected(){++samples;return host;} +} board; +struct { + unsigned streams=0,operations=0; + void cancelSerialResponseStream(BaseSerialInterface* route){ + assert(route==&bluetooth_interface);++streams; + } + void cancelSerialOperationsForRoute(BaseSerialInterface* route){ + assert(route==&bluetooth_interface);++operations; + } +} the_mesh; +''' + service + r''' +int main(){ + interface_manager.addInterface(InterfaceType::Bluetooth,&bluetooth_interface); + interface_manager.addInterface(InterfaceType::USB,&usb_interface); + serviceCompanionUsbBluetoothPriority();interface_manager.enable(); + assert(bluetooth_interface.enabled&&board.charging&&!board.reader); + board.host=true;now_ms=100;serviceCompanionUsbBluetoothPriority(); +#if defined(T1000_E) && defined(USE_TINYUSB) + assert(interface_manager.isBluetoothInhibited()&&!bluetooth_interface.enabled); + assert(usb_interface.enabled&&the_mesh.streams==1&&the_mesh.operations==1); + // The host stays enumerated even with both serial readers closed. + now_ms=10000;serviceCompanionUsbBluetoothPriority(); + assert(!bluetooth_interface.enabled&&the_mesh.streams==1); + board.reader=true;now_ms=10001;serviceCompanionUsbBluetoothPriority(); + board.reader=false;now_ms=10002;serviceCompanionUsbBluetoothPriority(); + assert(!bluetooth_interface.enabled&&the_mesh.operations==1); + board.host=false;now_ms=10003;serviceCompanionUsbBluetoothPriority(); + now_ms=10502;serviceCompanionUsbBluetoothPriority(); + assert(!bluetooth_interface.enabled); + now_ms=10503;serviceCompanionUsbBluetoothPriority(); + assert(bluetooth_interface.enabled&&!interface_manager.isBluetoothInhibited()); + assert(the_mesh.streams==1&&the_mesh.operations==1&&usb_interface.enabled); +#else + assert(bluetooth_interface.enabled&&!interface_manager.isBluetoothInhibited()); + assert(board.samples==0&&the_mesh.streams==0&&the_mesh.operations==0); +#endif +} +''' + for flags in ( + ("T1000_E=1", "USE_TINYUSB=1"), + ("USE_TINYUSB=1",), + ("T1000_E=1",), + ): + with self.subTest(configuration=flags): + self.compile_and_run(source, flags=flags) + + def test_priority_preserves_usb_routes_and_rejects_old_bluetooth_replies(self): + self.compile_and_run(TRANSPORT_HARNESS + r''' +int main(){ + const uint8_t reply[]={1,2,3}; + Fixture f;f.manager.enable();f.route(f.usb); + const unsigned usb_enables=f.usb.enables,usb_disables=f.usb.disables; + f.manager.setBluetoothInhibited(true); + assert(f.manager.captureReplyRoute()==&f.usb); + assert(f.manager.isReplyRouteAvailable(&f.usb)); + assert(f.manager.writeFrame(reply,sizeof(reply))==sizeof(reply)); + assert(f.usb.writes==1&&f.ble.writes==0); + assert(f.usb.enabled&&f.wifi.enabled&&f.ethernet.enabled); + assert(f.usb.enables==usb_enables&&f.usb.disables==usb_disables); + // An interrupted multi-frame BLE reply must not fall back to USB. + f.manager.setBluetoothInhibited(false);f.route(f.ble); + f.manager.lockReplyRoute();f.manager.setBluetoothInhibited(true); + assert(!f.manager.isReplyRouteAvailable(&f.ble)); + assert(f.manager.writeFrameToRoute(&f.ble,reply,sizeof(reply))==0); + assert(f.manager.writeFrame(reply,sizeof(reply))==0); + assert(f.usb.writes==1&&f.ble.writes==0); + f.manager.unlockReplyRoute();f.route(f.usb); + assert(f.manager.writeFrame(reply,sizeof(reply))==sizeof(reply)); + assert(f.usb.writes==2); +} +''') + + def test_wireless_master_off_remains_off_after_usb_disconnect(self): + main = (ROOT / "examples/companion_radio/main.cpp").read_text() + prefix = BLUETOOTH_HARNESS.split("@METHODS@")[0] + adapter = method(main, "class CompanionWirelessBackend") + ";\n" + disable = method(main, "static void disableCompanionBluetoothForCli(") + self.compile_and_run(prefix + disable + adapter + r''' +int main(){ + using namespace mesh::wireless; + interface_manager.addInterface(InterfaceType::Bluetooth,&bluetooth_interface); + interface_manager.addInterface(InterfaceType::USB,&usb_serial_interface); + interface_manager.enable(); + CompanionWirelessBackend backend;control().begin(backend); + interface_manager.setBluetoothInhibited(true); + assert(!bluetooth_interface.enabled&&the_mesh.durable_bluetooth_preference); + char status[192]; + assert(control().handle("set 2.4ghz off",status,sizeof(status),now_ms,Independent)); + now_ms+=250;control().service(now_ms); + assert(control().masterOff()&&control().blocked(Bluetooth)); + assert(!control().pending()); + const unsigned previous_enables=bluetooth_interface.enables; + interface_manager.setBluetoothInhibited(false); + interface_manager.enableBluetooth(); // Master-off is still authoritative. + assert(!bluetooth_interface.enabled&&bluetooth_interface.enables==previous_enables); + interface_manager.disable();interface_manager.enable(); + assert(!bluetooth_interface.enabled&&usb_serial_interface.enabled); + assert(the_mesh.durable_bluetooth_preference&&the_mesh.saves==0); + // Explicit restore-all is accepted while USB owns the transport. Reporting + // a startup failure here would contradict the remembered resume request. + interface_manager.setBluetoothInhibited(true); + assert(control().handle("set 2.4ghz on all",status,sizeof(status),now_ms,Independent)); + now_ms+=250;control().service(now_ms); + assert(!control().pending()&&!control().masterOff()&&!control().blocked(Bluetooth)); + assert(!bluetooth_interface.enabled); // No bypass during USB priority. + assert(control().handle("get 2.4ghz",status,sizeof(status),now_ms,Independent)); + assert(!strstr(status,"failed")); + interface_manager.setBluetoothInhibited(false); + assert(bluetooth_interface.enabled&&usb_serial_interface.enabled); + assert(the_mesh.durable_bluetooth_preference&&the_mesh.saves==0); +} +''', flags=("BLE_PIN_CODE=1", "NRF52_PLATFORM=1", "ENABLE_USB_INTERFACE=1")) + + def test_cli_saved_on_pauses_for_usb_and_saved_off_survives_unplug(self): + main = (ROOT / "examples/companion_radio/main.cpp").read_text() + prefix = BLUETOOTH_HARNESS.split("@METHODS@")[0] + methods = "\n".join(method(main, signature) for signature in ( + "static bool hasCompanionNonBluetoothClient(", + "static void disableCompanionBluetoothForCli(", + "bool handleCompanionBluetoothCommand(", + )) + self.compile_and_run(prefix + methods + r''' +int main(){ + interface_manager.addInterface(InterfaceType::Bluetooth,&bluetooth_interface); + interface_manager.addInterface(InterfaceType::USB,&usb_serial_interface); + interface_manager.enable();interface_manager.setBluetoothInhibited(true); + char reply[192]; + assert(handleCompanionBluetoothCommand("get bluetooth",reply,sizeof(reply))); + assert(strstr(reply,"off")&&strstr(reply,"USB")); + const unsigned previous_enables=bluetooth_interface.enables; + assert(handleCompanionBluetoothCommand("set bluetooth on",reply,sizeof(reply))); + assert(strstr(reply,"OK")&&strstr(reply,"USB")); + assert(the_mesh.durable_bluetooth_preference&&!bluetooth_interface.enabled); + assert(bluetooth_interface.enables==previous_enables); + interface_manager.setBluetoothInhibited(false); + assert(bluetooth_interface.enabled); + interface_manager.setBluetoothInhibited(true); + assert(handleCompanionBluetoothCommand("set bluetooth off",reply,sizeof(reply))); + assert(strstr(reply,"OK")&&!the_mesh.durable_bluetooth_preference); + interface_manager.setBluetoothInhibited(false); + assert(!bluetooth_interface.enabled); + // Failed preference persistence cannot turn a saved-off device on later. + interface_manager.setBluetoothInhibited(true);the_mesh.storage_accepts=false; + assert(handleCompanionBluetoothCommand("set bluetooth on",reply,sizeof(reply))); + assert(strstr(reply,"Error")&&!the_mesh.durable_bluetooth_preference); + interface_manager.setBluetoothInhibited(false); + assert(!bluetooth_interface.enabled&&usb_serial_interface.enabled); +} +''', flags=("BLE_PIN_CODE=1", "NRF52_PLATFORM=1", "ENABLE_USB_INTERFACE=1")) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_nrf52_ble_rx_queue.py b/test/test_nrf52_ble_rx_queue.py index 9ee99168b..4ca9129bb 100644 --- a/test/test_nrf52_ble_rx_queue.py +++ b/test/test_nrf52_ble_rx_queue.py @@ -32,6 +32,7 @@ class Nrf52BleRxQueueTests(unittest.TestCase): "static bool isBondAuthenticationFailure(", "void SerialBLEInterface::onBLEEvent(", "void SerialBLEInterface::onConnect(", + "void SerialBLEInterface::disconnect()", "void SerialBLEInterface::onDisconnect(", "void SerialBLEInterface::onBleUartRX(", "bool SerialBLEInterface::isReadBusy() const", diff --git a/test/test_nrf52_ble_startup.py b/test/test_nrf52_ble_startup.py index 20c98cd34..5c5875f9d 100644 --- a/test/test_nrf52_ble_startup.py +++ b/test/test_nrf52_ble_startup.py @@ -151,6 +151,7 @@ struct MotaStream { @FORMAT_DEVICE_NAME@ struct SerialBLEInterface { @STARTUP_FIELDS@ + bool _isEnabled = false; std::atomic _companionDataSeen{false}; SecuritySessionTimer _companion_start_timer; std::atomic _successfulConnectionPending{false};