"""Execute Bluetooth CLI parsing, real transport routing, and shutdown guards.""" from pathlib import Path import os import subprocess import tempfile import unittest from test_radio_receive_contract import method ROOT = Path(__file__).resolve().parents[1] MAIN = (ROOT / 'examples/companion_radio/main.cpp').read_text() HARNESS = r''' #include #include #include #include #include #include "examples/companion_radio/CompanionBluetooth.h" uint32_t now_ms=100; uint32_t millis(){return now_ms;} bool usb_open=false, usb_power=true, network_terminal=false; bool companion_bluetooth_initialized=true, usb_mota_mode=false; char companion_bluetooth_start_failure[96]="Bluetooth has not started"; uint32_t companion_bluetooth_off_at=0; bool companion_bluetooth_force_off=false; struct Transport : BaseSerialInterface { bool enabled=false,connected=false,busy=false,frame=false,enable_fails=false; unsigned disabled=0,enables=0; void enable() override {++enables;enabled=!enable_fails;} void disable() override {enabled=false;connected=false;++disabled;} bool isEnabled() const override{return enabled;} bool isConnected() const override{return connected;} bool isReadBusy() const override{return false;} bool isWriteBusy() const override{return busy;} bool hasPendingIO() const override{return busy;} size_t writeFrame(const uint8_t*,size_t n) override{return n;} size_t checkRecvFrame(uint8_t* out) override{ if(!frame)return 0; frame=false;out[0]=0x42;return 1; } } bluetooth_interface,usb_serial_interface,wifi_interface,ethernet_interface; MultiSerialInterface interface_manager; bool isUsbTerminalDataConnected(){return usb_open;} unsigned cancelled=0,logs=0; struct { bool bluetooth_preference=true,durable_bluetooth_preference=true,storage_accepts=true; unsigned saves=0,fail_save_at=0; bool isBluetoothEnabledPreference() const {return bluetooth_preference;} bool setBluetoothEnabledPreference(bool enabled){ ++saves;if(!storage_accepts||saves==fail_save_at)return false; bluetooth_preference=durable_bluetooth_preference=enabled;return true; } bool isAnyNetworkTerminalMode(){return network_terminal;} void cancelSerialResponseStream(BaseSerialInterface*) {} void cancelSerialOperationsForRoute(BaseSerialInterface* route){ assert(route==&bluetooth_interface);++cancelled; } } the_mesh; namespace mesh { struct Logger {void println(const char*){++logs;}} logger; Logger& usbLoggingPort(){return logger;} } @METHODS@ char reply[160]; bool command(const char* text,CompanionBluetoothCommandSource source=CompanionBluetoothCommandSource::Framed){ memset(reply,0,sizeof(reply)); return handleCompanionBluetoothCommand(text,reply,sizeof(reply),source); } void route(Transport& target){ target.connected=true;target.frame=true; uint8_t frame[176];assert(interface_manager.checkRecvFrame(frame)==1); assert(interface_manager.captureReplyRoute()==&target); } int main(){ #if defined(BLE_PIN_CODE) interface_manager.addInterface(InterfaceType::Bluetooth,&bluetooth_interface); interface_manager.addInterface(InterfaceType::USB,&usb_serial_interface); interface_manager.addInterface(InterfaceType::WiFi,&wifi_interface); interface_manager.addInterface(InterfaceType::Ethernet,ðernet_interface); interface_manager.enable();route(bluetooth_interface); assert(command("get bluetooth")&&strstr(reply,"bluetooth on")); for(const char* invalid : {"set ble","set bluetooth on force","set ble off force extra", "set ble off forced","set bluetooth off junk","set ble yes"}){ assert(command(invalid)&&strstr(reply,"Error:")); assert(bluetooth_interface.disabled==0&&companion_bluetooth_off_at==0); } assert(!command("get bluetooth.mac")); assert(!command("set ble.stealth on")); assert(command("set bluetooth off")&&strstr(reply,"no other active")); assert(usb_power&&bluetooth_interface.enabled&&!companion_bluetooth_off_at); assert(the_mesh.saves==0&&the_mesh.durable_bluetooth_preference); // A disabled TCP interface and a socket that is merely listening don't count. wifi_interface.connected=true;wifi_interface.enabled=false; assert(command("set ble off")&&strstr(reply,"Error:")); wifi_interface.enabled=true;wifi_interface.connected=false; assert(command("set ble off")&&strstr(reply,"Error:")); // A real TCP client allows a delayed BLE response. Losing it cancels shutdown. wifi_interface.connected=true; assert(command("set ble off")&&strstr(reply,"requested")); assert(command("get ble")&&strstr(reply,"off pending")); assert(the_mesh.durable_bluetooth_preference); // Requested is not yet committed. now_ms+=249;serviceCompanionBluetoothControl();assert(bluetooth_interface.enabled); wifi_interface.connected=false;++now_ms;serviceCompanionBluetoothControl(); assert(bluetooth_interface.enabled&&!companion_bluetooth_off_at&&logs==1); assert(the_mesh.saves==0&&the_mesh.durable_bluetooth_preference); // Force may disconnect the sole client, but waits for queued notifications. assert(command(" set ble\t off\tforce \t")&&strstr(reply,"requested")); bluetooth_interface.busy=true;now_ms+=250;serviceCompanionBluetoothControl(); assert(bluetooth_interface.enabled); now_ms+=1749;serviceCompanionBluetoothControl();assert(bluetooth_interface.enabled); ++now_ms;serviceCompanionBluetoothControl(); assert(!bluetooth_interface.enabled&&cancelled==1); assert(!the_mesh.durable_bluetooth_preference); assert(interface_manager.captureReplyRoute()==nullptr); assert(command("set ble off")&&strstr(reply,"already off")); assert(command("get ble")&&strcmp(reply,"bluetooth off")==0); bluetooth_interface.busy=false; assert(command("set bluetooth on")&&strstr(reply,"OK"));route(bluetooth_interface); assert(the_mesh.durable_bluetooth_preference); // Another native CDC session counts; HWCDC/bridge presence alone doesn't. usb_open=true;usb_serial_interface.connected=true; assert(command("set ble off")); #if defined(NRF52_PLATFORM) || (defined(ARDUINO_USB_MODE) && ARDUINO_USB_MODE == 0) assert(strstr(reply,"requested")); usb_open=false;usb_serial_interface.connected=false; now_ms+=250;serviceCompanionBluetoothControl();assert(bluetooth_interface.enabled); #else assert(strstr(reply,"Error:"));usb_open=false;usb_serial_interface.connected=false; #endif // USB MOTA ownership cannot count as a separate management terminal. usb_mota_mode=true;usb_open=true; assert(command("set ble off")&&strstr(reply,"Error:")); usb_mota_mode=false;usb_open=false; // An actual USB command proves that requester is present on every backend. route(usb_serial_interface); assert(command("set ble off")&&strstr(reply,"Bluetooth off")); assert(!bluetooth_interface.enabled&&interface_manager.captureReplyRoute()==&usb_serial_interface); assert(!the_mesh.durable_bluetooth_preference); assert(usb_serial_interface.enabled); // Persisting BLE off never disables USB. const unsigned saved_off_enables=bluetooth_interface.enables; interface_manager.disable();interface_manager.enable(); assert(!bluetooth_interface.enabled&&bluetooth_interface.enables==saved_off_enables); assert(usb_serial_interface.enabled);route(usb_serial_interface); assert(command("set ble on")&&bluetooth_interface.enabled); assert(the_mesh.durable_bluetooth_preference); interface_manager.disable();interface_manager.enable(); assert(bluetooth_interface.enabled); // Successful on also restores the aggregate boot policy. // ASCII terminal commands cannot inherit the previous framed BLE route. route(bluetooth_interface); assert(command("set ble off",CompanionBluetoothCommandSource::Terminal)); assert(!bluetooth_interface.enabled); assert(command("set ble on"));route(bluetooth_interface); // Active browser/TCP terminal and Ethernet sessions each qualify. network_terminal=true; assert(command("set ble off")&&strstr(reply,"requested")); assert(command("set ble on")&&!companion_bluetooth_off_at); now_ms+=300;serviceCompanionBluetoothControl();assert(bluetooth_interface.enabled); network_terminal=false;ethernet_interface.connected=true; assert(command("set ble off")&&strstr(reply,"requested")); now_ms+=250;serviceCompanionBluetoothControl();assert(!bluetooth_interface.enabled); assert(command("set ble on"));route(bluetooth_interface);ethernet_interface.connected=false; // Failed delayed saves leave the requesting BLE session alive and cancellable. assert(command("set ble off force")); const unsigned delayed_disabled=bluetooth_interface.disabled; const uint32_t pending_off=companion_bluetooth_off_at; the_mesh.storage_accepts=false; assert(command("set ble on")&&strstr(reply,"Error:")); assert(companion_bluetooth_off_at==pending_off); // Failed reversal does not cancel a prior request. now_ms+=250;serviceCompanionBluetoothControl(); assert(bluetooth_interface.enabled&&the_mesh.durable_bluetooth_preference); assert(!companion_bluetooth_off_at&&bluetooth_interface.disabled==delayed_disabled); the_mesh.storage_accepts=true; // Failed immediate off saves must not disconnect the USB recovery command. route(usb_serial_interface);the_mesh.storage_accepts=false; assert(command("set ble off")&&strstr(reply,"Error:")); assert(bluetooth_interface.enabled&&the_mesh.durable_bluetooth_preference); assert(interface_manager.captureReplyRoute()==&usb_serial_interface); the_mesh.storage_accepts=true; assert(command("set ble off")&&!bluetooth_interface.enabled); assert(!the_mesh.durable_bluetooth_preference); // Failed on saves restore the off runtime state as well as durable preference. the_mesh.storage_accepts=false; const unsigned failed_on_enables=bluetooth_interface.enables; assert(command("set ble on")&&strstr(reply,"Error:")); assert(!bluetooth_interface.enabled&&!the_mesh.durable_bluetooth_preference); assert(bluetooth_interface.enables==failed_on_enables); // No transient advertising. the_mesh.storage_accepts=true; assert(command("set ble on")&&bluetooth_interface.enabled); assert(the_mesh.durable_bluetooth_preference);route(bluetooth_interface); // Pending deadlines work across millis() wrap, including the zero sentinel. for(uint32_t start : {0xffffff00u,0xffffff06u}){ now_ms=start;assert(command("set ble off force")); now_ms=start+249;serviceCompanionBluetoothControl();assert(bluetooth_interface.enabled); now_ms=start+251;serviceCompanionBluetoothControl();assert(!bluetooth_interface.enabled); assert(command("set ble on"));route(bluetooth_interface); } companion_bluetooth_initialized=false; bluetooth_interface.enabled=false; assert(command("get ble")&&strstr(reply,"bluetooth off")); assert(command("set ble on")&&strstr(reply,"unavailable")); assert(command("set ble off force")&&strstr(reply,"already off")); assert(!the_mesh.durable_bluetooth_preference); companion_bluetooth_initialized=true; assert(command("set ble off",CompanionBluetoothCommandSource::Terminal)); bluetooth_interface.enable_fails=true; assert(command("set ble on")&&strstr(reply,"enable failed")); assert(!the_mesh.durable_bluetooth_preference&&!bluetooth_interface.enabled); assert(command("get ble")&&strcmp(reply,"bluetooth off")==0); bluetooth_interface.enable_fails=false; interface_manager.disable();interface_manager.enable(); assert(!bluetooth_interface.enabled&&usb_serial_interface.enabled); // Policy rollback matches saved off. // If both enable and its rollback save fail, report the durable on choice honestly. bluetooth_interface.enable_fails=true;the_mesh.fail_save_at=the_mesh.saves+2; assert(command("set ble on")&&strstr(reply,"rollback save failed")); assert(the_mesh.durable_bluetooth_preference&&!bluetooth_interface.enabled); assert(command("get ble")&&strcmp(reply,"bluetooth off")==0); // Status remains the live state. the_mesh.fail_save_at=0;bluetooth_interface.enable_fails=false; interface_manager.disable();interface_manager.enable(); assert(bluetooth_interface.enabled); // Aggregate policy remains consistent with the durable on value. #else assert(command("get ble")&&strstr(reply,"not supported")); assert(command("set ble off force")&&strstr(reply,"not supported")); #endif assert(!handleCompanionBluetoothCommand(nullptr,reply,sizeof(reply))); assert(!handleCompanionBluetoothCommand("get ble",nullptr,sizeof(reply))); assert(!handleCompanionBluetoothCommand("set ble off force",reply,0)); } ''' class BluetoothControlTests(unittest.TestCase): def test_actual_commands_and_connection_guards(self): guards = '\n'.join(method(MAIN, signature) for signature in ( 'static bool hasCompanionNonBluetoothClient(', 'static void disableCompanionBluetoothForCli(', 'static void serviceCompanionBluetoothControl(')) methods = '#if defined(BLE_PIN_CODE)\n' + guards + '\n#endif\n' + method( MAIN, 'bool handleCompanionBluetoothCommand(') configurations = { 'no_ble': [], 'nrf52': ['BLE_PIN_CODE=123456', 'NRF52_PLATFORM=1'], 'esp32_tinyusb': ['BLE_PIN_CODE=123456', 'ESP32=1', 'ARDUINO_USB_CDC_ON_BOOT=1', 'ARDUINO_USB_MODE=0'], 'esp32_hwcdc': ['BLE_PIN_CODE=123456', 'ESP32=1', 'ARDUINO_USB_CDC_ON_BOOT=1', 'ARDUINO_USB_MODE=1'], 'esp32_uart': ['BLE_PIN_CODE=123456', 'ESP32=1'], } with tempfile.TemporaryDirectory() as directory: folder = Path(directory) (folder/'Arduino.h').write_text( '#pragma once\n#include \n#include \n#include \n') source = folder/'bluetooth.cpp' source.write_text(HARNESS.replace('@METHODS@', methods)) for name, defines in configurations.items(): with self.subTest(platform=name): binary = folder/name built = subprocess.run([ os.environ.get('CXX','g++'), '-std=c++17', '-Wall', '-Wextra', '-I',str(folder),'-I',str(ROOT),'-I',str(ROOT/'src'), '-DENABLE_USB_INTERFACE=1', '-DCOMPANION_FEATURE_TEXT_TERMINAL=1', '-DCOMPANION_FEATURE_NETWORK_TERMINAL=1', '-DCOMPANION_FEATURE_USB_MOTA_SOURCE=1', *['-D'+flag for flag in defines],str(source),'-o',str(binary)], capture_output=True,text=True) self.assertEqual(built.returncode,0,built.stderr) ran = subprocess.run([str(binary)],capture_output=True,text=True) self.assertEqual(ran.returncode,0,ran.stderr) def test_saved_off_policy_keeps_usb_recovery_and_explicit_runtime_on(self): # Execute the actual multi-transport manager. A saved-off boot must # not briefly advertise when the aggregate interface is enabled. source_text = HARNESS.split('@METHODS@')[0] + r''' int main(){ interface_manager.addInterface(InterfaceType::Bluetooth,&bluetooth_interface); interface_manager.addInterface(InterfaceType::USB,&usb_serial_interface); interface_manager.addInterface(InterfaceType::WiFi,&wifi_interface); interface_manager.addInterface(InterfaceType::Ethernet,ðernet_interface); interface_manager.setBluetoothAutoEnable(false); interface_manager.enable(); assert(!bluetooth_interface.enabled&&bluetooth_interface.enables==0); assert(usb_serial_interface.enabled&&wifi_interface.enabled&ðernet_interface.enabled); interface_manager.disable();interface_manager.enable(); assert(!bluetooth_interface.enabled&&bluetooth_interface.enables==0); // USB can explicitly enable an already-initialized stack while saved off. interface_manager.enableBluetooth();assert(bluetooth_interface.enabled); assert(bluetooth_interface.enables==1); interface_manager.disable();interface_manager.enable(); assert(!bluetooth_interface.enabled&&bluetooth_interface.enables==1); interface_manager.setBluetoothAutoEnable(true); interface_manager.disable();interface_manager.enable(); assert(bluetooth_interface.enabled&&bluetooth_interface.enables==2); // The legacy default remains automatic on. MultiSerialInterface legacy;Transport ble; legacy.addInterface(InterfaceType::Bluetooth,&ble);legacy.enable(); assert(ble.enabled&&ble.enables==1); } ''' with tempfile.TemporaryDirectory() as directory: folder = Path(directory) (folder/'Arduino.h').write_text('#pragma once\n#include \n#include \n') source = folder/'startup_policy.cpp' source.write_text(source_text) binary = folder/'startup_policy' built = subprocess.run([os.environ.get('CXX','g++'), '-std=c++17', '-I',str(folder),'-I',str(ROOT),'-I',str(ROOT/'src'), str(source),'-o',str(binary)],capture_output=True,text=True) self.assertEqual(built.returncode,0,built.stderr) ran = subprocess.run([str(binary)],capture_output=True,text=True) self.assertEqual(ran.returncode,0,ran.stderr) def test_wireless_restore_does_not_change_the_saved_bluetooth_choice(self): adapter = method(MAIN, 'class CompanionWirelessBackend')+';\n' source_text = HARNESS.split('@METHODS@')[0] + '\n' + method( MAIN, 'static void disableCompanionBluetoothForCli(') + '\n' + adapter + r''' int main(){ using namespace mesh::wireless; interface_manager.addInterface(InterfaceType::Bluetooth,&bluetooth_interface); interface_manager.addInterface(InterfaceType::USB,&usb_serial_interface); the_mesh.bluetooth_preference=the_mesh.durable_bluetooth_preference=false; interface_manager.setBluetoothAutoEnable(false);interface_manager.enable(); CompanionWirelessBackend backend;control().begin(backend); assert(backend.available()==Bluetooth&&!bluetooth_interface.enabled); char reply[160]; control().handle("set 2.4ghz off",reply,sizeof(reply),now_ms,Independent); now_ms+=250;control().service(now_ms); control().handle("set 2.4ghz on",reply,sizeof(reply),now_ms,Independent); now_ms+=250;control().service(now_ms); assert(!bluetooth_interface.enabled&&bluetooth_interface.enables==0); assert(!the_mesh.durable_bluetooth_preference&&the_mesh.saves==0); // Explicit all is a runtime-only override; the saved-off boot policy remains. control().handle("set 2.4ghz on all",reply,sizeof(reply),now_ms,Independent); now_ms+=250;control().service(now_ms); assert(bluetooth_interface.enabled&&!the_mesh.durable_bluetooth_preference); assert(the_mesh.saves==0); interface_manager.disable();interface_manager.enable(); assert(!bluetooth_interface.enabled&&usb_serial_interface.enabled); } ''' with tempfile.TemporaryDirectory() as directory: folder = Path(directory) (folder/'Arduino.h').write_text('#pragma once\n#include \n#include \n') source = folder/'wireless_policy.cpp' source.write_text(source_text) binary = folder/'wireless_policy' built = subprocess.run([os.environ.get('CXX','g++'), '-std=c++17', '-DBLE_PIN_CODE=123456','-I',str(folder),'-I',str(ROOT),'-I',str(ROOT/'src'), str(source),'-o',str(binary)],capture_output=True,text=True) self.assertEqual(built.returncode,0,built.stderr) ran = subprocess.run([str(binary)],capture_output=True,text=True) self.assertEqual(ran.returncode,0,ran.stderr) def test_cli_entry_points_pass_the_current_source(self): mesh = (ROOT/'examples/companion_radio/MyMesh.cpp').read_text() terminal = method(mesh,'void MyMesh::handleTerminalCommand(') framed = method(mesh,'void MyMesh::handleCmdFrame(') local = method(mesh,'bool MyMesh::handleLocalControlCommand(') self.assertIn('CompanionBluetoothCommandSource::Terminal',terminal) self.assertIn('CompanionBluetoothCommandSource::Framed',framed) self.assertIn('handleCompanionBluetoothCommand(command, reply, reply_size)',local) loop = method(MAIN,'\nvoid loop()') self.assertIn('serviceCompanionBluetoothControl();',loop) if __name__ == '__main__': unittest.main()