"""Execute production ESP-NOW driver startup/shutdown with fault-injected SDK APIs.""" from pathlib import Path import os import re import subprocess import tempfile import unittest from test_radio_receive_contract import method ROOT = Path(__file__).resolve().parents[1] STUBS = r''' #include #include #include #include #include #define ESP_IDF_VERSION_VAL(a,b,c) ((a)*10000+(b)*100+(c)) #define ESPNOW_DEBUG_PRINTLN(...) ((void)0) #define portENTER_CRITICAL(x) ((void)0) #define portEXIT_CRITICAL(x) ((void)0) #define portMUX_INITIALIZER_UNLOCKED 0 using portMUX_TYPE=int; using esp_err_t=int; constexpr int ESP_OK=0,WIFI_IF_STA=0,WIFI_PROTOCOL_LR=8,WIFI_PHY_MODE_LR=1,WIFI_PHY_RATE_LORA_250K=2; enum wifi_mode_t {WIFI_OFF=0,WIFI_STA=1,WIFI_AP=2,WIFI_AP_STA=3}; enum esp_now_send_status_t {ESP_NOW_SEND_SUCCESS=0,ESP_NOW_SEND_FAIL=1}; struct esp_now_send_info_t {}; struct esp_now_recv_info_t {const uint8_t* src_addr;}; struct esp_now_peer_info_t {uint8_t peer_addr[6];int channel,ifidx;bool encrypt;}; struct esp_now_rate_config_t {int phymode,rate;bool ersu,dcm;}; int failure=0,init_calls=0,deinit_calls=0,wake_refs=0,send_calls=0,power=0; bool sdk_active=false;uint32_t millis(){return 1234;} struct FakeWiFi { wifi_mode_t current=WIFI_OFF;bool autoreconnect=true; void persistent(bool){} wifi_mode_t getMode(){return current;} int channel(){return current==WIFI_OFF?0:6;} void setAutoReconnect(bool value){autoreconnect=value;} bool mode(wifi_mode_t value){if(failure==1)return false;current=value;return true;} } WiFi; namespace mesh { enum class RadioParamApplyResult {APPLIED}; namespace wifi { constexpr uint8_t kProtocolMask=15,kLongRangeRadioOwner=1,kLongRangeBridgeOwner=2; void setLongRangeOwner(uint8_t,bool){} int checkLongRangeRadioStart(){return WiFi.current&WIFI_AP?-1:0;} int applyStationProtocolMask(uint8_t,bool){return failure==2?-1:0;} int applyProtocolMask(int){return failure==2?-1:0;} int restoreEspNowChannel(){return failure==3?-1:0;} } } int esp_wifi_set_protocol(int,int){return failure==2?-1:0;} int esp_now_init(){++init_calls;if(failure==4)return -1;sdk_active=true;return 0;} int esp_now_deinit(){assert(sdk_active);sdk_active=false;++deinit_calls;return 0;} int esp_wifi_force_wakeup_acquire(){if(failure==5)return -1;++wake_refs;return 0;} int esp_wifi_force_wakeup_release(){assert(wake_refs==1);--wake_refs;return 0;} int esp_wifi_set_max_tx_power(int value){if(failure==6)return -1;power=value;return 0;} template int esp_now_register_send_cb(T){return failure==7?-1:0;} template int esp_now_register_recv_cb(T){return failure==8?-1:0;} int esp_now_unregister_send_cb(){return 0;} int esp_now_unregister_recv_cb(){return 0;} int esp_now_add_peer(esp_now_peer_info_t* peer){assert(peer->channel==0);return failure==9?-1:0;} int esp_now_set_peer_rate_config(const uint8_t*,esp_now_rate_config_t*){return failure==10?-1:0;} int esp_wifi_config_espnow_rate(int,int){return failure==10?-1:0;} int esp_now_send(const uint8_t*,const uint8_t*,size_t){assert(sdk_active);++send_calls;return 0;} void esp_efuse_mac_get_default(uint8_t* mac){memset(mac,1,6);} ''' CHECKS = r''' void deliver(const uint8_t* bytes,int len){ static uint8_t mac[6]={1,2,3,4,5,6}; #if ESP_ARDUINO_VERSION_MAJOR >= 3 esp_now_recv_info_t info{mac};OnDataRecv(&info,bytes,len); #else OnDataRecv(mac,bytes,len); #endif } int main(){ ESPNOWRadio radio; assert(!radio.isEnabled()); for(int stage=1;stage<=10;++stage){ failure=stage;radio.init();assert(!radio.isEnabled()); assert(!sdk_active&&wake_refs==0); assert(!radio.isInRecvMode()&&!radio.isSendComplete()); radio.end();assert(wake_refs==0); // failed startup and repeated off are safe } failure=0;WiFi.current=WIFI_OFF;radio.init(); assert(radio.isEnabled()&&sdk_active&&wake_refs==1&&!WiFi.autoreconnect); const int started=init_calls;radio.init();assert(init_calls==started&&wake_refs==1); assert(radio.setTxPower(13)&&power==52); uint8_t raw[255]={0x15,1,2,3},out[255]; deliver(raw,4);assert(radio.recvRaw(out,sizeof(out))==4&&!memcmp(raw,out,4)); // Stop in the middle of a fragmented TX: do not report success or send fragment 2. assert(radio.startSendRaw(raw,sizeof(raw))&&tx_second_pending); OnDataSent(nullptr,ESP_NOW_SEND_SUCCESS); radio.end();const int sent=send_calls; assert(!radio.isSendComplete()&&!radio.startSendRaw(raw,4)&&send_calls==sent); OnDataSent(nullptr,ESP_NOW_SEND_SUCCESS); // callback arriving while disabled deliver(raw,4);assert(radio.recvRaw(out,sizeof(out))==0); radio.end();assert(wake_refs==0&&!sdk_active); // LR cannot restart alongside a conventional setup AP on the shared PHY. WiFi.current=WIFI_AP_STA;WiFi.autoreconnect=true;radio.init(); assert(!radio.isEnabled()&&!sdk_active&&WiFi.current==WIFI_AP_STA); WiFi.current=WIFI_STA;radio.init(); assert(WiFi.current==WIFI_STA&&WiFi.autoreconnect&&power==52); assert(!radio.isSendComplete()); // restarting before Dispatcher's timeout cannot fake TX success radio.onSendFinished(); // Dispatcher acknowledges the cancelled operation assert(radio.recvRaw(out,sizeof(out))==0&&radio.isInRecvMode()); // Incomplete RX fragments cannot cross a stop/start boundary. mesh::espnow::ESPNowRawFrames frames; assert(mesh::espnow::encodeEspNowRawFrames(raw,sizeof(raw),frames)); deliver(frames.data[0],frames.lengths[0]);assert(radio.recvRaw(out,sizeof(out))==0); radio.end();radio.init(); deliver(frames.data[1],frames.lengths[1]);assert(radio.recvRaw(out,sizeof(out))==0); // A fresh two-fragment packet is received normally after restarting. deliver(frames.data[0],frames.lengths[0]); assert(radio.recvRaw(out,sizeof(out))==255&&!memcmp(raw,out,255)); radio.end();assert(wake_refs==0&&!sdk_active); } ''' class EspNowLifecycleTests(unittest.TestCase): def test_combined_full_transport_controls_are_independent(self): common = (ROOT/'src/helpers/CommonCLI.cpp').read_text() observer = (ROOT/'src/helpers/CommonCLI_Observer.cpp').read_text() prefs = (ROOT/'src/helpers/CommonCLI.h').read_text() self.assertIn('uint8_t espnow_bridge_enabled = 1;', prefs) self.assertIn('setEspNowBridgeState(enable)', common) self.assertIn('_prefs->espnow_bridge_enabled', common) mqtt_switch = method(common, 'if (strncmp(config, "mqtt.enabled ", 13) == 0)') self.assertIn('enable ? _callbacks->setMqttBridgeState(true)', mqtt_switch) self.assertIn(': _callbacks->requestMqttBridgeStop()', mqtt_switch) # Reject an unsafe restart before changing saved intent. An accepted # OFF command saves successfully before signalling cooperative stop. self.assertLess(mqtt_switch.index('isMqttBridgeStopping()'), mqtt_switch.index('_prefs->bridge_enabled =')) self.assertLess(mqtt_switch.index('if (!trySavePrefs())'), mqtt_switch.index('setMqttBridgeState(true)')) self.assertLess(mqtt_switch.index('if (!trySavePrefs())'), mqtt_switch.index('requestMqttBridgeStop()')) self.assertIn('isMqttBridgeRunning()', observer) for role in ('simple_repeater', 'simple_room_server'): header = (ROOT/f'examples/{role}/MyMesh.h').read_text() combined = method(header, 'bool setBridgeState(bool enable) override') self.assertIn('setMqttBridgeState(true)', combined) self.assertIn('setEspNowBridgeState(true)', combined) mqtt = method(header, 'bool setMqttBridgeState(bool enable) override') stop = method(header, 'bool requestMqttBridgeStop() override') espnow = method(header, 'bool setEspNowBridgeState(bool enable) override') self.assertIn('if (espnow_bridge.isRunning()) espnow_bridge.end();', mqtt) self.assertLess(mqtt.index('isStopping()'), mqtt.index('espnow_bridge.end()')) self.assertIn('->requestStop()', stop) self.assertNotIn('->end()', stop) self.assertIn('if (mqtt_bridge' if role == 'simple_repeater' else 'if (bridge', espnow) def test_sdk_failures_stop_and_restart(self): header=(ROOT/'src/helpers/esp32/ESPNOWRadio.h').read_text() source=(ROOT/'src/helpers/esp32/ESPNOWRadio.cpp').read_text() # Substitute only the abstract radio base and SDK includes; all driver # state, callbacks, queues, fragmentation, and method bodies are production. header=header[:header.index('#if ESPNOW_DEBUG_LOGGING')] header=re.sub(r'^\s*#(?:include|pragma).*$', '', header, flags=re.MULTILINE) header=header.replace(' : public mesh::Radio','').replace(' override','') source=re.sub(r'^\s*#include.*$', '', source, flags=re.MULTILINE) with tempfile.TemporaryDirectory() as tmp: folder=Path(tmp) (folder/'driver.cpp').write_text(STUBS+header+source+CHECKS) for arduino,idf in [(2,40400),(3,50200),(3,50500)]: with self.subTest(arduino=arduino,idf=idf): binary=folder/f'driver-{idf}' subprocess.run([os.environ.get('CXX','g++'),'-std=c++17', '-I',str(ROOT/'src'),'-DMESH_PRIMARY_ESPNOW=1','-DMESH_ESPNOW_RADIO=1', f'-DESP_ARDUINO_VERSION_MAJOR={arduino}',f'-DESP_IDF_VERSION={idf}', str(folder/'driver.cpp'),'-o',str(binary)],check=True) subprocess.run([str(binary)],check=True) def test_bridge_preserves_infrastructure_wifi(self): source=(ROOT/'src/helpers/bridges/ESPNowBridge.cpp').read_text() methods=method(source,'static void stopBridgeWiFiIfUnused(')+'\n' methods+='\n'.join(method(source,f'void ESPNowBridge::{name}(').replace('ESPNowBridge::','Bridge::') for name in ('begin','end')) harness=STUBS.replace('assert(peer->channel==0)', 'assert(peer->channel==6)')+r''' #include #include #define BRIDGE_DEBUG_PRINTLN(...) ((void)0) #define ESP_NOW_ETH_ALEN 6 #define WIFI_INIT_CONFIG_DEFAULT() 0 using wifi_init_config_t=int; using wifi_second_chan_t=int; constexpr int WIFI_STORAGE_RAM=0,WIFI_MODE_STA=1,WIFI_SECOND_CHAN_NONE=0; constexpr wifi_mode_t WIFI_MODE_NULL=WIFI_OFF; constexpr int WIFI_PROTOCOL_11B=1,WIFI_PROTOCOL_11G=2,WIFI_PROTOCOL_11N=4; int wifi_stops=0,wifi_starts=0;uint8_t wifi_channel=6;bool wifi_initialized=false; int esp_wifi_init(wifi_init_config_t*){wifi_initialized=true;return 0;} int esp_wifi_set_storage(int){return 0;} int esp_wifi_set_mode(int){return 0;} int esp_wifi_start(){++wifi_starts;return 0;} int esp_wifi_stop(){++wifi_stops;return 0;} int esp_wifi_deinit(){wifi_initialized=false;return 0;} int esp_wifi_get_mode(wifi_mode_t* mode){*mode=WIFI_STA;return wifi_initialized?0:-1;} int esp_wifi_get_channel(uint8_t* channel,wifi_second_chan_t*){*channel=wifi_channel;return 0;} int esp_wifi_set_channel(int channel,int){wifi_channel=channel;return 0;} int esp_now_del_peer(const uint8_t*){return 0;} namespace mesh {namespace wifi { std::atomic& bridgeEspNowChannel(){static std::atomic channel{0};return channel;} }} struct Backend : mesh::wireless::Backend { bool wifi=true; uint8_t available()const override{return mesh::wireless::WiFi|mesh::wireless::EspNow;} uint8_t enabled()const override{return wifi?mesh::wireless::WiFi:0;} uint8_t clients()const override{return mesh::wireless::Independent;} mesh::wireless::Result set(uint8_t,bool)override{return mesh::wireless::Result::Done;} }; class Bridge { public: struct Prefs {int bridge_format=mesh::bridge::ESPNOW_FORMAT_RAW;uint8_t bridge_channel=6;} prefs; Prefs* _prefs=&prefs;bool _initialized=false; int _active_format=0,_rx_mux=0,_rx_head=0,_rx_tail=0,_rx_count=0,_rx_dropped=0,_rx_dropped_reported=0; int _tx_mux=0,_tx_head=0,_tx_tail=0,_tx_count=0,_tx_waiting=0,_tx_callback_done=0; int _tx_callback_status=0,_tx_dropped=0,_tx_dropped_reported=0; uint32_t _tx_started_at=0,_tx_retry_at=0;bool _sdk_teardown_failed=false; mesh::espnow::ESPNowRawReassembler _raw_reassembler; struct QueuedTransmit {bool started=false;int packet=0;}; static constexpr int TX_QUEUE_DEPTH=2; QueuedTransmit _tx_queue[TX_QUEUE_DEPTH]; struct {void clear(const int*){}} _seen_packets; static void recv_cb(){}static void send_cb(){} void begin();void end(); }; '''+methods+r''' int main(){ Backend backend;mesh::wireless::control().begin(backend);Bridge bridge; // A channel conflict must leave the active WiFi connection untouched. wifi_channel=3;bridge.begin();assert(!bridge._initialized&&init_calls==0&&wifi_stops==0); wifi_channel=6; for(int stage:{2,4,7,8,9,10}){ failure=stage;bridge.begin();assert(!bridge._initialized&&!sdk_active&&wifi_stops==0); bridge.end();assert(wifi_stops==0); } failure=0;bridge.begin();assert(bridge._initialized&&sdk_active&&wifi_starts==0); assert(mesh::wifi::bridgeEspNowChannel().load()==6); bridge.end();assert(!sdk_active&&wifi_stops==0&&mesh::wifi::bridgeEspNowChannel().load()==0); // When no infrastructure service owns WiFi, the bridge owns driver shutdown. backend.wifi=false;bridge.begin();assert(bridge._initialized&&wifi_starts==1); bridge.end();assert(!sdk_active&&wifi_stops==1); bridge.end();assert(wifi_stops==1); } ''' with tempfile.TemporaryDirectory() as tmp: folder=Path(tmp) (folder/'bridge.cpp').write_text(harness) binary=folder/'bridge' subprocess.run([os.environ.get('CXX','g++'),'-std=c++17','-I',str(ROOT/'src'), '-DESP_IDF_VERSION=50200',str(folder/'bridge.cpp'),'-o',str(binary)],check=True) subprocess.run([str(binary)],check=True) def test_bridge_channel_constraint_tracks_runtime_state(self): with tempfile.TemporaryDirectory() as tmp: folder=Path(tmp) (folder/'esp_err.h').write_text('#pragma once\nusing esp_err_t=int;\nconstexpr int ESP_OK=0,ESP_ERR_INVALID_STATE=-2,ESP_ERR_WIFI_NOT_INIT=-3;\n') (folder/'esp_wifi.h').write_text(r''' #pragma once #include #include using wifi_interface_t=int;using wifi_second_chan_t=int;using wifi_mode_t=int; constexpr int WIFI_MODE_NULL=0,WIFI_MODE_STA=1,WIFI_MODE_AP=2,WIFI_STORAGE_RAM=0; constexpr int WIFI_IF_STA=0,WIFI_IF_AP=1,WIFI_SECOND_CHAN_NONE=0; constexpr int WIFI_PROTOCOL_11B=1,WIFI_PROTOCOL_11G=2,WIFI_PROTOCOL_11N=4,WIFI_PROTOCOL_LR=8; uint8_t current_channel=1,last_protocol=0; int esp_wifi_set_protocol(int,uint8_t p){last_protocol=p;return 0;} int esp_wifi_get_mode(int* p){*p=WIFI_MODE_STA;return 0;} int esp_wifi_get_protocol(int,uint8_t* p){*p=7;return 0;} int esp_wifi_set_storage(int){return 0;} int esp_wifi_get_channel(uint8_t* p,int*){*p=current_channel;return 0;} int esp_wifi_set_channel(uint8_t p,int){current_channel=p;return 0;} ''') (folder/'channel.cpp').write_text(r''' #include #include #include int main(){using namespace mesh::wifi; assert(!espNowChannelConstrained()&&stationChannelHint()==0&&stationScanChannel()==0); bridgeEspNowChannel().store(6); assert(espNowChannelConstrained()&&stationChannelHint()==6&&stationScanChannel()==6&&accessPointChannel()==6); assert(restoreEspNowChannel()==ESP_OK&¤t_channel==6); applyProtocolMask(WIFI_IF_STA);assert(last_protocol==15); setStationAutoReconnect(true);assert(!WiFi.auto_reconnect); assert(beginStation("mesh","password")==WL_CONNECTED); assert(WiFi.scan_channel==6&&WiFi.join_channel==6); current_channel=11;assert(!enforceStationChannel()&¤t_channel==6); bridgeEspNowChannel().store(0);current_channel=11; assert(!espNowChannelConstrained()&&stationChannelHint()==0&&stationScanChannel()==0); assert(restoreEspNowChannel()==ESP_OK&¤t_channel==11); setStationAutoReconnect(true);assert(WiFi.auto_reconnect); assert(beginStation("mesh","password")==WL_CONNECTED&&WiFi.join_channel==0); } ''') (folder/'WiFi.h').write_text(r''' #pragma once #include #include enum wl_status_t {WL_IDLE_STATUS,WL_CONNECTED,WL_NO_SSID_AVAIL,WL_CONNECT_FAILED}; constexpr int WIFI_SCAN_RUNNING=-1; struct { bool auto_reconnect=true;uint8_t scan_channel=0;int join_channel=0; wl_status_t state=WL_IDLE_STATUS; void setAutoReconnect(bool value){auto_reconnect=value;} wl_status_t begin(const char*,const char*,int channel=0,const uint8_t* =nullptr){ join_channel=channel;state=WL_CONNECTED;return state; } int scanComplete(){return -2;}void scanDelete(){} int scanNetworks(bool,bool,bool,int,uint8_t channel,const char*,const uint8_t*){scan_channel=channel;return 2;} int channel(int index=-1){return index<0?current_channel:index==0?1:6;} std::string SSID(int){return "mesh";}int RSSI(int){return -50;} const uint8_t* BSSID(int){static uint8_t mac[6]={1};return mac;} wl_status_t status(){return state;} void disconnect(bool,bool){state=WL_IDLE_STATUS;} } WiFi; ''') binary=folder/'channel' subprocess.run([os.environ.get('CXX','g++'),'-std=c++17','-DWITH_ESPNOW_BRIDGE=1', '-I',str(folder),'-I',str(ROOT/'src'),str(folder/'channel.cpp'),'-o',str(binary)],check=True) subprocess.run([str(binary)],check=True) def test_bridge_arduino_sdk_handoffs_and_partial_startup(self): source = (ROOT/'src/helpers/bridges/ESPNowBridge.cpp').read_text() methods = method(source, 'static void stopBridgeWiFiIfUnused(') + '\n' methods += '\n'.join( method(source, f'void ESPNowBridge::{name}(').replace('ESPNowBridge::', 'Bridge::') for name in ('begin', 'end') ) fixture = (ROOT/'test/fixtures/espnow_wifi_handoff.cpp').read_text() with tempfile.TemporaryDirectory() as tmp: folder = Path(tmp) (folder/'handoff.cpp').write_text(fixture.replace('@METHODS@', methods)) for arduino, idf in [(2,40400), (3,50200), (3,50500)]: with self.subTest(arduino=arduino, idf=idf): binary = folder/f'handoff-{idf}' subprocess.run([ os.environ.get('CXX', 'g++'), '-std=c++17', '-fsanitize=address,undefined', '-fno-omit-frame-pointer', '-fno-pie', '-no-pie', '-I', str(ROOT/'src'), f'-DESP_IDF_VERSION={idf}', f'-DESP_ARDUINO_VERSION_MAJOR={arduino}', str(folder/'handoff.cpp'), '-o', str(binary), ], check=True) subprocess.run([str(binary)], check=True) if __name__=='__main__': unittest.main()