Files
HaloKeymind/test/test_wireless_control.py
T

391 lines
22 KiB
Python

"""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 <helpers/WirelessControl.h>
#include <cassert>
#include <vector>
using namespace mesh::wireless;
struct Fake : Backend {
uint8_t supported=All, active=All, sessions=0, wait=0, fail=0;
bool busy=false;
std::vector<int> 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<int>{-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<int>{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 <cstdlib>\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,&ethernet_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 <cstdint>\n#include <cstddef>\n#include <initializer_list>\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 <helpers/WirelessControl.h>
#include <cassert>
enum {WIFI_STA=1,WIFI_OFF=0,WIFI_IF_STA=0};
struct {bool usb=false;bool isUsbDataConnected(){return usb;}} 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 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);
// 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()