"""Compile and execute the production wireless command/state controller.""" 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] HARNESS = r''' #include #include #include using namespace mesh::wireless; struct Fake : Backend { uint8_t supported=All, active=All, sessions=0, wait=0, fail=0; bool busy=false; std::vector calls; uint8_t available()const override{return supported;} uint8_t enabled()const override{return active;} uint8_t clients()const override{return sessions;} bool replyBusy()const override{return busy;} Result set(uint8_t bit,bool on)override { calls.push_back(on?bit:-bit); if(wait==bit)return Result::Pending; if(fail==bit)return Result::Failed; if(on)active|=bit;else active&=~bit; return Result::Done; } }; struct Fixture { Fake b; Control c; char reply[160]={}; uint32_t now=100; Fixture(){c.begin(b);} void run(const char* text,uint8_t requester=Independent){assert(c.handle(text,reply,sizeof(reply),now,requester));} void tick(unsigned ms=250){now+=ms;c.service(now);} }; int main(int argc,char** argv) { const int test=argc>1?atoi(argv[1]):0; if(test==0){ for(const char* text:{"set wifi","set espnow on force","set wifi off force extra", "set 2.4ghz on all force","set 2.4ghz on forced","get wifi junk","set 2.4ghz off all"}) assert(parse(text).action==Action::Invalid); for(const char* text:{"set wifi.ssid x","get wifi.status","set espnow.channel 3","set bluetooth off","get 2.4ghzx"}) assert(parse(text).action==Action::None); assert(parse(nullptr).action==Action::None); assert(parse(" set espnow\t off\tforce \t").action==Action::ForceOff); assert(parse("set 2.4ghz on all").action==Action::OnAll); assert(parse("set 2.4ghz on force").action==Action::OnAll); } else if(test==1){ for(unsigned mask=0;mask<=All;++mask){ Fixture f;f.b.active=mask; f.run("set 2.4ghz on");f.tick();assert(f.b.active==mask); // on before off is idempotent f.run("set 2.4ghz off");f.tick();assert(f.b.active==0); f.run("set 2.4ghz off");f.tick(); // never overwrite the saved state f.run("set wifi on");assert(strstr(f.reply,"Error:")); assert(!f.c.allowService(Bluetooth)); f.run("set 2.4ghz on");f.tick();assert(f.b.active==mask); assert(f.c.allowService(Bluetooth)); f.run("set 2.4ghz on all");f.tick();assert(f.b.active==All); f.run("set 2.4ghz off");f.tick(); f.run("set 2.4ghz on force");f.tick();assert(f.b.active==All); } } else if(test==2){ Fixture f;f.run("set wifi off",WiFi);assert(strstr(f.reply,"Error:")); f.b.sessions=Bluetooth;f.run("set wifi off",WiFi);assert(f.c.pending()); f.tick(249);assert(f.b.active==All);f.b.sessions=0;f.tick(1); assert(f.b.active==All&&!f.c.pending());f.run("get wifi");assert(strstr(f.reply,"cancelled")); f.b.sessions=Bluetooth;f.run("set wifi off",WiFi);f.tick();assert(f.b.active==(Bluetooth|EspNow)); f.run("set wifi on");f.tick();assert(f.b.active==All); f.b.sessions=Bluetooth|WiFi;f.run("set 2.4ghz off",Bluetooth);assert(strstr(f.reply,"Error:")); f.run("set espnow off",EspNow);f.tick();assert(f.b.active==(WiFi|Bluetooth)); } else if(test==3){ Fixture f;f.b.busy=true;f.run("set 2.4ghz off force",Bluetooth); f.tick();assert(f.b.active==All);f.tick(1749);assert(f.b.active==All); f.tick(1);assert(f.b.active==0);assert(f.c.blocked(All)); assert((f.b.calls==std::vector{-WiFi,-EspNow,-Bluetooth})); f.b.calls.clear();f.b.busy=false;f.run("set 2.4ghz on");f.tick(); assert((f.b.calls==std::vector{EspNow,WiFi,Bluetooth})); } else if(test==4){ Fixture f;f.b.wait=WiFi;f.run("set 2.4ghz off");f.tick(); assert(f.c.pending()&&f.b.calls.size()==1&&f.b.active==All); f.run("set espnow off");assert(strstr(f.reply,"pending")); f.b.wait=0;f.tick(1);assert(f.b.active==0&&!f.c.pending()); f.b.fail=EspNow;f.run("set 2.4ghz on");f.tick(); assert(f.b.active==0&&!f.c.pending());f.run("get 2.4ghz");assert(strstr(f.reply,"failed")); f.b.fail=0;f.run("set 2.4ghz on");f.tick();assert(f.b.active==All); // saved state survives failure } else if(test==5){ Fixture f;f.run("set 2.4ghz off");f.tick(249); f.run("set 2.4ghz on");f.tick();assert(f.b.active==All&&!f.c.masterOff()); f.b.sessions=Independent;f.run("set 2.4ghz off",Bluetooth);f.b.sessions=0;f.tick(); assert(f.b.active==All&&!f.c.masterOff()); f.now=UINT32_MAX-249;f.run("set 2.4ghz off");f.tick(249);assert(f.b.active==All); f.tick(1);assert(f.b.active==0); } else if(test==6){ Fixture f;f.b.supported=Bluetooth;f.b.active=Bluetooth; f.run("set wifi on");assert(strstr(f.reply,"unavailable")); f.run("get 2.4ghz");assert(strstr(f.reply,"wifi=unavailable")&&strstr(f.reply,"bluetooth=on")); f.run("set 2.4ghz off");f.tick();assert(f.b.active==0); f.run("set 2.4ghz on all");f.tick();assert(f.b.active==Bluetooth); f.b.supported=0;f.run("set 2.4ghz on");assert(strstr(f.reply,"unavailable")); } else if(test==7){ Fixture f;f.b.wait=WiFi;f.run("set 2.4ghz off");f.tick(30250); assert(!f.c.pending()&&f.b.active==All&&f.b.calls.size()==1); f.run("get 2.4ghz");assert(strstr(f.reply,"failed")); f.b.wait=0;f.run("set 2.4ghz off");f.tick();assert(f.b.active==0); } else if(test==8){ Fixture f;f.b.active=WiFi|Bluetooth;f.b.sessions=WiFi|Bluetooth; f.run("set wifi off",Bluetooth);assert(f.c.pending()); f.b.active=WiFi;f.b.sessions=WiFi; // the retained requester disconnects f.tick();assert(f.b.active==WiFi&&!f.c.pending()); f.run("get wifi");assert(strstr(f.reply,"cancelled")); // Explicit force still permits shutting down the only live connection. f.run("set wifi off force",WiFi);f.tick();assert(f.b.active==0); } else if(test==9){ Fixture f;f.b.active=WiFi; f.run("set 2.4ghz off");f.tick(); f.b.fail=WiFi;f.run("set 2.4ghz on");f.tick();assert(!f.c.pending()); f.b.fail=0;f.b.active=Bluetooth;f.b.sessions=Bluetooth; // The failed restore retains a WiFi-only saved mask. Enabling BLE to // recover access must not make the next ordinary restore cut that access. f.run("set 2.4ghz on",Bluetooth); assert(strstr(f.reply,"no remaining")&&!f.c.pending()&&f.b.active==Bluetooth); f.run("set 2.4ghz on all",Bluetooth);f.tick();assert(f.b.active==All); } } ''' class WirelessControlTests(unittest.TestCase): def test_compiled_controller(self): with tempfile.TemporaryDirectory() as tmp: source = Path(tmp)/'wireless.cpp' source.write_text('#include \n'+HARNESS) binary = Path(tmp)/'wireless' subprocess.run([os.environ.get('CXX', 'g++'), '-std=c++11', '-Wall', '-Wextra', '-Werror', '-I', str(ROOT/'src'), str(source), '-o', str(binary)], check=True) for scenario in range(10): with self.subTest(scenario=scenario): subprocess.run([str(binary), str(scenario)], check=True) def test_actual_companion_transport_adapter(self): from test_companion_bluetooth_control import HARNESS as BT_HARNESS main = (ROOT/'examples/companion_radio/main.cpp').read_text() prefix = BT_HARNESS.split('@METHODS@')[0].replace( ' bool isAnyNetworkTerminalMode()', ' bool isWebConfigActiveOrStopping(){return false;}\n bool isAnyNetworkTerminalMode()') adapter = method(main, 'class CompanionWirelessBackend')+';\n' adapter += method(main, 'static void disableCompanionBluetoothForCli(')+'\n' adapter += method(main, 'bool handleCompanionWirelessCommand(') adapter += '\n'+method(main, 'static bool hasCompanionNonBluetoothClient(') adapter += '\n'+method(main, 'static void serviceCompanionBluetoothControl(') adapter += '\n'+method(main, 'bool handleCompanionBluetoothCommand(') source_text = prefix+r''' #include "examples/companion_radio/CompanionWireless.h" bool companion_wifi_requested=true,companion_wifi_active=true; bool companion_wifi_setup_requested=false,companion_wifi_disable_in_progress=false; bool stop_pending=false; struct {int mode_value=0;void mode(int value){mode_value=value;}void setAutoReconnect(bool){}} WiFi; #define WIFI_OFF 0 bool finishStoppingCompanionWiFi(){ // Production tears down sockets before waiting for the web server, while // companion_wifi_active remains true until the whole teardown completes. wifi_interface.disable(); if(stop_pending)return false; companion_wifi_active=false;return true; } void startCompanionWiFi(){ if(companion_wifi_active)return; companion_wifi_active=true;wifi_interface.enable(); } struct { bool active=true; bool isEnabled(){return active;} void end(){active=false;} void init(){active=true;} } radio_driver; static void disableCompanionBluetoothForCli(); '''+adapter+r''' int main(){ CompanionWirelessBackend backend;mesh::wireless::control().begin(backend); 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();bluetooth_interface.connected=true; assert(backend.available()==mesh::wireless::All); assert(backend.enabled()==mesh::wireless::All); // UART/HWCDC power cannot authorize cutting all wireless connections. assert(!(backend.clients()&mesh::wireless::Independent)); char reply[160]; handleCompanionWirelessCommand("set 2.4ghz off",reply,sizeof(reply),CompanionWirelessSource::Network); assert(strstr(reply,"Error:")); // A stale framed BLE route must not disguise an actual USB ASCII request. bluetooth_interface.frame=true;uint8_t frame[176];interface_manager.checkRecvFrame(frame); handleCompanionWirelessCommand("set 2.4ghz off",reply,sizeof(reply),CompanionWirelessSource::Usb); assert(strstr(reply,"requested"));stop_pending=true;now_ms+=250; mesh::wireless::control().service(now_ms); assert(radio_driver.active&&bluetooth_interface.enabled); // wait for WiFi tasks/sockets first stop_pending=false;mesh::wireless::control().service(++now_ms); assert(!radio_driver.active&&!bluetooth_interface.enabled&&!companion_wifi_active); assert(!companion_wifi_requested); handleCompanionWirelessCommand("set 2.4ghz on",reply,sizeof(reply),CompanionWirelessSource::Usb); now_ms+=250;mesh::wireless::control().service(now_ms); assert(radio_driver.active&&bluetooth_interface.enabled&&companion_wifi_active); assert(companion_wifi_requested); // Reversing an asynchronous shutdown must finish teardown and restart the // sockets, rather than treating the still-active flag as a healthy WiFi stack. for(const char* family : {"wifi", "2.4ghz"}) { char command[40];snprintf(command,sizeof(command),"set %s off",family); handleCompanionWirelessCommand(command,reply,sizeof(reply),CompanionWirelessSource::Usb); stop_pending=true;now_ms+=250;mesh::wireless::control().service(now_ms); assert(mesh::wireless::control().pending()&&companion_wifi_active&&!wifi_interface.enabled); snprintf(command,sizeof(command),"set %s on",family); handleCompanionWirelessCommand(command,reply,sizeof(reply),CompanionWirelessSource::Usb); now_ms+=250;mesh::wireless::control().service(now_ms); assert(mesh::wireless::control().pending()&&!wifi_interface.enabled); stop_pending=false;mesh::wireless::control().service(++now_ms); assert(!mesh::wireless::control().pending()&&companion_wifi_active&&wifi_interface.enabled); assert(companion_wifi_requested&&!companion_wifi_disable_in_progress); } // WiFi's framed client must not be mistaken for independent USB. wifi_interface.frame=true;wifi_interface.connected=true;bluetooth_interface.connected=false; interface_manager.checkRecvFrame(frame); handleCompanionWirelessCommand("set wifi off",reply,sizeof(reply),CompanionWirelessSource::Framed); assert(strstr(reply,"Error:")); // An actual framed USB request is proof even without DTR on a UART bridge. usb_serial_interface.frame=true;usb_serial_interface.connected=true; interface_manager.checkRecvFrame(frame); handleCompanionWirelessCommand("set espnow off",reply,sizeof(reply),CompanionWirelessSource::Framed); now_ms+=250;mesh::wireless::control().service(now_ms); assert(!radio_driver.active&&companion_wifi_active&&bluetooth_interface.enabled); // Each shutdown must not use the other pending shutdown's connection as // its safety net. Bluetooth's force override remains available explicitly. usb_serial_interface.connected=false; bluetooth_interface.connected=true;bluetooth_interface.frame=true; interface_manager.checkRecvFrame(frame); handleCompanionWirelessCommand("set wifi off",reply,sizeof(reply),CompanionWirelessSource::Framed); assert(mesh::wireless::control().pending()); handleCompanionBluetoothCommand("set bluetooth off",reply,sizeof(reply),CompanionBluetoothCommandSource::Terminal); assert(strstr(reply,"pending")&&bluetooth_interface.enabled); handleCompanionBluetoothCommand("set bluetooth off force",reply,sizeof(reply),CompanionBluetoothCommandSource::Terminal); assert(!bluetooth_interface.enabled); now_ms+=250;mesh::wireless::control().service(now_ms); assert(companion_wifi_active&&!mesh::wireless::control().pending()); handleCompanionWirelessCommand("get wifi",reply,sizeof(reply),CompanionWirelessSource::Network); assert(strstr(reply,"cancelled")); // The opposite command order is protected too, including asynchronous // WiFi teardown where its socket can stay visible during the first stop. handleCompanionBluetoothCommand("set bluetooth on",reply,sizeof(reply),CompanionBluetoothCommandSource::Terminal); bluetooth_interface.connected=true;bluetooth_interface.frame=true; interface_manager.checkRecvFrame(frame); handleCompanionBluetoothCommand("set bluetooth off",reply,sizeof(reply),CompanionBluetoothCommandSource::Framed); assert(companion_bluetooth_off_at!=0); handleCompanionWirelessCommand("set wifi off",reply,sizeof(reply),CompanionWirelessSource::Network); assert(strstr(reply,"Bluetooth change pending")&&!mesh::wireless::control().pending()); now_ms+=250;serviceCompanionBluetoothControl(); assert(!bluetooth_interface.enabled&&companion_wifi_active); // A restore can remove WiFi too. Keep a BLE-only saved snapshot after a // failed restore, then simulate both management services recovering. companion_wifi_requested=companion_wifi_active=false;wifi_interface.disable(); interface_manager.enableBluetooth(); handleCompanionWirelessCommand("set 2.4ghz off",reply,sizeof(reply),CompanionWirelessSource::Usb); now_ms+=250;mesh::wireless::control().service(now_ms); bluetooth_interface.enable_fails=true; handleCompanionWirelessCommand("set 2.4ghz on",reply,sizeof(reply),CompanionWirelessSource::Usb); now_ms+=250;mesh::wireless::control().service(now_ms); assert(!mesh::wireless::control().pending()&&!bluetooth_interface.enabled); bluetooth_interface.enable_fails=false;interface_manager.enableBluetooth(); companion_wifi_requested=companion_wifi_active=true;wifi_interface.enable(); wifi_interface.connected=true;bluetooth_interface.connected=true; bluetooth_interface.frame=true;interface_manager.checkRecvFrame(frame); handleCompanionBluetoothCommand("set bluetooth off",reply,sizeof(reply),CompanionBluetoothCommandSource::Framed); assert(companion_bluetooth_off_at!=0); handleCompanionWirelessCommand("set 2.4ghz on",reply,sizeof(reply),CompanionWirelessSource::Network); assert(strstr(reply,"Bluetooth change pending")&&!mesh::wireless::control().pending()); // Explicit all cannot remove a service and may replace the pending stop. handleCompanionWirelessCommand("set 2.4ghz on all",reply,sizeof(reply),CompanionWirelessSource::Network); assert(mesh::wireless::control().pending()); now_ms+=250;mesh::wireless::control().service(now_ms); assert(!companion_bluetooth_off_at&&bluetooth_interface.enabled&&companion_wifi_active); } ''' with tempfile.TemporaryDirectory() as tmp: folder = Path(tmp) (folder/'Arduino.h').write_text('#pragma once\n#include \n#include \n#include \n') (folder/'adapter.cpp').write_text(source_text) binary=folder/'adapter' subprocess.run([os.environ.get('CXX','g++'), '-std=c++17', '-I',str(folder),'-I',str(ROOT),'-I',str(ROOT/'src'), '-DESP32=1','-DBLE_PIN_CODE=1','-DWIFI_SSID=""', '-DWITH_WEBCONFIG=1','-DMESH_PRIMARY_ESPNOW=1', '-DENABLE_USB_INTERFACE=1','-DCOMPANION_FEATURE_TEXT_TERMINAL=1', '-DCOMPANION_FEATURE_NETWORK_TERMINAL=1', str(folder/'adapter.cpp'),'-o',str(binary)],check=True) subprocess.run([str(binary)],check=True) def test_role_and_restart_wiring(self): for role, backend in [('companion_radio','companion_wireless'), ('simple_repeater','infrastructure_wireless'), ('simple_room_server','infrastructure_wireless')]: source = (ROOT/'examples'/role/'main.cpp').read_text() self.assertIn(f'control().begin({backend})', source) self.assertIn('control().service(millis())', source[source.index('\nvoid loop()'):]) self.assertIn('&& !mesh::wireless::control().pending()', source[source.index('\nvoid loop()'):]) gates = { 'src/helpers/bridges/MQTTBridge.cpp': 'WiFi', 'src/helpers/bridges/ESPNowBridge.cpp': 'EspNow', 'src/helpers/esp32/ESPNOWRadio.cpp': 'EspNow', 'src/helpers/esp32/WebConfigServer.cpp': 'WiFi', 'src/helpers/WiFiSetupPortal.cpp': 'WiFi', 'src/helpers/MultiSerialInterface.h': 'Bluetooth', } for name, bit in gates.items(): with self.subTest(file=name): self.assertIn(f'control().blocked(mesh::wireless::{bit})', (ROOT/name).read_text()) def test_actual_infrastructure_transport_adapter(self): source = r''' #include #include enum {WIFI_STA=1,WIFI_OFF=0,WIFI_IF_STA=0}; struct { bool usb=false, ota=false; bool isUsbDataConnected(){return usb;} bool isOTAUpdateRunning(){return ota;} bool stopOTAUpdate(char*){ota=false;return true;} } board; struct { int mode_value=WIFI_STA;bool autoreconnect=true; void setAutoReconnect(bool value){autoreconnect=value;} void disconnect(bool,bool){} void mode(int value){mode_value=value;} } WiFi; namespace mesh {namespace wifi {void applyProtocolMask(int){}void restoreEspNowChannel(){}}} struct { bool active=true;bool isEnabled(){return active;} void init(){active=true;}void end(){active=false;} } radio_driver; struct { bool web=true,stopping=false,bridge=true,network=false; bool isWebConfigActive(){return web||stopping;} bool isWebConfigStopping(){return stopping;} bool hasWirelessNetworkClient(){return network;} bool isBridgeRunning(){return bridge;} bool setBridgeState(bool on){bridge=on;return true;} bool isMqttBridgeRunning(){return bridge;} bool isEspNowBridgeRunning(){return bridge;} bool setMqttBridgeState(bool on){bridge=on;return true;} bool setEspNowBridgeState(bool on){bridge=on;return true;} bool stopWebConfig(char*){web=false;stopping=true;return true;} bool startWebConfig(bool,char*){web=true;return true;} } the_mesh; #include "examples/InfrastructureWireless.h" int main(){ InfrastructureWirelessBackend b;mesh::wireless::Control c;c.begin(b); char reply[160];using namespace mesh::wireless; #ifdef WITH_MQTT_BRIDGE assert(b.available()==(mesh::wireless::WiFi|EspNow)); assert(b.enabled()==(mesh::wireless::WiFi|EspNow)); assert(b.clients()==0);the_mesh.network=true;assert(b.clients()==mesh::wireless::WiFi); c.handle("set 2.4ghz off",reply,sizeof(reply),0,mesh::wireless::WiFi);assert(strstr(reply,"Error:")); c.handle("set 2.4ghz off",reply,sizeof(reply),0,Independent);c.service(250); assert(c.pending()&&!the_mesh.bridge&&the_mesh.stopping&&radio_driver.active); the_mesh.stopping=false;c.service(251); assert(!c.pending()&&!radio_driver.active&&::WiFi.mode_value==WIFI_OFF); c.handle("set 2.4ghz on",reply,sizeof(reply),300,Independent);c.service(550); assert(the_mesh.bridge&&the_mesh.web&&radio_driver.active); // OTA alone remains visible as an active WiFi owner after WebConfig drains. the_mesh.web=the_mesh.bridge=false;board.ota=true; assert(b.enabled()&mesh::wireless::WiFi); InfrastructureWirelessBackend ota_backend; assert(ota_backend.set(mesh::wireless::WiFi,false)==Result::Done); assert(!board.ota); the_mesh.web=the_mesh.bridge=true; // Individual WiFi off retains the ESP-NOW driver and stops both its own services. c.handle("set wifi off",reply,sizeof(reply),600,Independent);c.service(850); the_mesh.stopping=false;c.service(851); assert(!the_mesh.bridge&&!the_mesh.web&&radio_driver.active&&::WiFi.mode_value==WIFI_STA); c.handle("set wifi on",reply,sizeof(reply),900,Independent);c.service(1150); assert(the_mesh.bridge&&the_mesh.web&&radio_driver.active); c.handle("set espnow off",reply,sizeof(reply),1200,Independent);c.service(1450); assert(!radio_driver.active&&the_mesh.web&&the_mesh.bridge&&::WiFi.mode_value==WIFI_STA); #else assert(b.available()==EspNow&&b.enabled()==EspNow); c.handle("set espnow off",reply,sizeof(reply),0,Independent);c.service(250);assert(!the_mesh.bridge); c.handle("set espnow on",reply,sizeof(reply),300,Independent);c.service(550);assert(the_mesh.bridge); #endif } ''' with tempfile.TemporaryDirectory() as tmp: folder=Path(tmp) (folder/'helpers/esp32').mkdir(parents=True) (folder/'WiFi.h').write_text('#pragma once\n') (folder/'helpers/esp32/WiFiRadioPolicy.h').write_text('#pragma once\n') (folder/'infrastructure.cpp').write_text(source) for defines in [('WITH_MQTT_BRIDGE','WITH_WEBCONFIG','MESH_PRIMARY_ESPNOW'),('WITH_ESPNOW_BRIDGE',)]: with self.subTest(features=defines): binary=folder/'infrastructure' subprocess.run([os.environ.get('CXX','g++'),'-std=c++17','-DESP32=1', '-I',str(folder),'-I',str(ROOT),'-I',str(ROOT/'src'), *['-D'+d+'=1' for d in defines],str(folder/'infrastructure.cpp'), '-o',str(binary)],check=True) subprocess.run([str(binary)],check=True) if __name__ == '__main__': unittest.main()