Save valid busy RX, fast-switch and Companion tuple changes once, then apply the latest accepted intent at a safe radio gap. Preserve checked-save rollback, runtime RXEN intent, TempRadio ownership and exact remote reply delivery. Repair failed preamble compensation without hiding uncertain saves. Retry WebConfig, MQTT and NimBLE startup failures automatically. Preserve explicit off choices, OTA ownership and the latest WiFi credentials through cooperative shutdown. Add regression coverage and update command guides. Validation: focused host transaction, rollback, ownership and lifecycle suites; Full RAK4631, Heltec V4 R8 TFT and RC32 packages; representative room, sensor, LR1110, LR2021, SX1276, STM32WL and ESP-NOW builds. Linked RAK crypto and OTA workspace checks pass; its complete OTA image uses 712644/712704 bytes.
211 lines
10 KiB
C++
211 lines
10 KiB
C++
#include <atomic>
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
#include <functional>
|
|
#include <string>
|
|
#include <vector>
|
|
#include "helpers/AlertFaultPolicy.h"
|
|
#include "helpers/MQTTConnectionPolicy.h"
|
|
#include "helpers/MQTTTransportHealth.h"
|
|
#include "helpers/WiFiPowerSave.h"
|
|
#include "helpers/WiFiNetworkPowerPolicy.h"
|
|
#define MQTT_DEBUG_PRINTLN(...) ((void)0)
|
|
#define pdMS_TO_TICKS(value) (value)
|
|
using TickType_t=uint32_t;
|
|
using wifi_mode_t=int;
|
|
using esp_err_t=int;
|
|
using wl_status_t=int;
|
|
using wifi_ps_type_t=int;
|
|
constexpr int ESP_OK=0,ESP_FAIL=-1,ESP_ERR_WIFI_NOT_INIT=-2;
|
|
constexpr int WIFI_MODE_NULL=0,WIFI_STA=1,WIFI_AP=2,WIFI_AP_STA=3,WIFI_MODE_AP=2;
|
|
constexpr int WL_CONNECTED=3,WL_DISCONNECTED=0,RUNTIME_MQTT_SLOTS=2;
|
|
constexpr int WIFI_PS_NONE=0,WIFI_PS_MIN_MODEM=1,WIFI_PS_MAX_MODEM=2,WIFI_POWER_11dBm=44;
|
|
int sdk_mode=WIFI_STA,mode_reads=0,enable_calls=0,mode_writes=0;
|
|
int reconnect_writes=0,autoconnect_writes=0,event_registers=0,station_begins=0;
|
|
int station_disconnects=0,channel_checks=0,power_writes=0,tx_power_writes=0;
|
|
int read_failure_at=0;
|
|
bool driver_initialized=true,facade_started=true,enable_failure=false,race_ap=false;
|
|
uint32_t now_ms=0;std::function<void()> tick;
|
|
int esp_wifi_get_mode(wifi_mode_t* mode){
|
|
++mode_reads;if(mode_reads==read_failure_at)return ESP_FAIL;
|
|
if(!driver_initialized)return ESP_ERR_WIFI_NOT_INIT;
|
|
*mode=sdk_mode;return ESP_OK;
|
|
}
|
|
uint32_t millis(){return now_ms;}
|
|
TickType_t xTaskGetTickCount(){return now_ms;}
|
|
void vTaskDelay(TickType_t value){now_ms+=value;if(tick)tick();}
|
|
int esp_wifi_set_ps(int){++power_writes;return ESP_OK;}
|
|
using WiFiEvent_t=int;
|
|
constexpr int ARDUINO_EVENT_WIFI_STA_GOT_IP=1,ARDUINO_EVENT_WIFI_STA_DISCONNECTED=2;
|
|
struct WiFiEventInfo_t{
|
|
struct{struct{struct{uint32_t addr=1;}ip;}ip_info;}got_ip;
|
|
struct{uint8_t reason=1;}wifi_sta_disconnected;
|
|
};
|
|
struct WiFiClass{
|
|
int state=WL_CONNECTED;
|
|
// Match Arduino-ESP32 2.0.17 enableSTA's getMode snapshot plus mode call.
|
|
// This is not an atomic SDK OR. An AP observed before the snapshot survives;
|
|
// stable AP deferral must use the SDK guard even with a stale OFF facade.
|
|
int getMode(){if(race_ap)sdk_mode|=WIFI_AP;return facade_started?sdk_mode:WIFI_MODE_NULL;}
|
|
bool enableSTA(bool enable){
|
|
assert(enable);++enable_calls;if(enable_failure)return false;
|
|
const wifi_mode_t currentMode=getMode();
|
|
const bool isEnabled=(currentMode&WIFI_STA)!=0;
|
|
if(isEnabled!=enable)return mode(enable?(currentMode|WIFI_STA):(currentMode&~WIFI_STA));
|
|
return true;
|
|
}
|
|
bool mode(int value){++mode_writes;driver_initialized=facade_started=true;sdk_mode=value;return true;}
|
|
void setAutoConnect(bool){++autoconnect_writes;}
|
|
template<class T>void onEvent(T){++event_registers;}
|
|
int status(){return state;}
|
|
void disconnect(){++station_disconnects;state=WL_DISCONNECTED;}
|
|
void setTxPower(int){++tx_power_writes;}
|
|
}WiFi;
|
|
namespace mesh{namespace wifi{
|
|
@STATION_GUARD@
|
|
constexpr bool kPrimaryEspNowRadio=false;
|
|
void setStationAutoReconnect(bool){++reconnect_writes;}
|
|
void beginStation(const char*,const char*){++station_begins;}
|
|
bool enforceStationChannel(){++channel_checks;return true;}
|
|
}}
|
|
int s_wifi_disconnect_reason=0;uint32_t s_wifi_disconnect_time=0;
|
|
unsigned long s_wifi_connected_at=0;
|
|
struct MQTTBridge{
|
|
mesh::wifi::NetworkPowerPolicy _network_power_policy;
|
|
std::atomic<bool>_network_suspended{false};
|
|
bool _network_espnow_restore_pending=false;
|
|
bool _wifi_event_registered=false,_manage_wifi=true,_ntp_sync_pending=false;
|
|
bool _wifi_credentials_changed=false;
|
|
int _wifi_reconnect_backoff_attempt=0;
|
|
std::atomic<bool>_ntp_synced{true},_stop_requested{false},_stop_acked{false};
|
|
struct{void noteGotIp(){}}_ntp_reconnect_latch;
|
|
AlertFaultPolicy::OutageSnapshot outage{true,1,0};
|
|
AlertFaultPolicy::OutageSnapshot wifiOutage(){return outage;}
|
|
void setWifiOutage(AlertFaultPolicy::OutageSnapshot value){outage=value;}
|
|
char _wifi_ssid[8]="mesh",_wifi_password[8]="secret";
|
|
void beginWiFiStation();
|
|
void applyTimezoneRules() {}
|
|
bool _wifi_status_initialized=true;
|
|
int _last_wifi_status=WL_DISCONNECTED;
|
|
unsigned long _last_wifi_check=0,_last_wifi_reconnect_attempt=0;
|
|
uint8_t _wifi_power_save=1;
|
|
struct{bool canonical_wifi=true;}_node_info;
|
|
struct{uint8_t wifi_power_save=1;}prefs,*_obs=&prefs;
|
|
struct Client{void disconnect(){}bool isStarted()const{return false;}};
|
|
struct Slot{
|
|
Client* client=nullptr;bool connected=false;
|
|
uint32_t connected_at_ms=0,current_outage_started_ms=0;
|
|
MQTTTransportHealth::State transport_health;
|
|
}_slots[RUNTIME_MQTT_SLOTS];
|
|
bool _slot_attempt_pending[RUNTIME_MQTT_SLOTS]={};
|
|
std::atomic<bool> _slot_transport_fault_pending[RUNTIME_MQTT_SLOTS]={};
|
|
std::atomic<bool> _slot_health_reset_pending[RUNTIME_MQTT_SLOTS]={};
|
|
void updateSlotTransportDiag(int,uint32_t){}
|
|
void updateCachedConnectionStatus(){}
|
|
std::vector<std::string> cleanup;
|
|
void cancelNtpRefresh(){cleanup.push_back("ntp");}
|
|
void teardownSlot(int index){cleanup.push_back("slot"+std::to_string(index));}
|
|
void destroySlotClients(){cleanup.push_back("clients");}
|
|
int slot_work=0;
|
|
bool initializeWiFiInTask();bool waitUnlessStopping(uint32_t delay_ms);
|
|
bool handleWiFiConnection(unsigned long now);
|
|
void mqttTaskLoop(){
|
|
@TASK_START@
|
|
while(true){
|
|
@TASK_STOP@
|
|
++slot_work;
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
@INITIALIZE@
|
|
@WAIT@
|
|
void assert_ap_untouched(){
|
|
assert(sdk_mode&WIFI_AP);
|
|
assert(enable_calls==0&&mode_writes==0&&reconnect_writes==0&&autoconnect_writes==0);
|
|
assert(event_registers==0&&station_begins==0);
|
|
}
|
|
int main(int argc,char** argv){
|
|
assert(argc==2);const std::string scenario=argv[1];MQTTBridge bridge;
|
|
if(scenario=="ap"){
|
|
for(int mode:{WIFI_AP,WIFI_AP_STA})for(bool manage:{false,true}){
|
|
sdk_mode=mode;bridge._manage_wifi=manage;facade_started=false;
|
|
for(int attempt=0;attempt<3;++attempt)assert(!bridge.initializeWiFiInTask());
|
|
assert_ap_untouched();assert(!bridge._wifi_event_registered&&!bridge._ntp_sync_pending);
|
|
}
|
|
}else if(scenario=="stop_waiting"){
|
|
sdk_mode=WIFI_AP_STA;
|
|
tick=[&](){if(now_ms>=65)bridge._stop_requested=true;};
|
|
bridge.mqttTaskLoop();assert_ap_untouched();
|
|
assert(now_ms==80&&bridge._stop_acked&&bridge.slot_work==0);
|
|
assert((bridge.cleanup==std::vector<std::string>{"ntp","slot0","slot1","clients"}));
|
|
}else if(scenario=="stop_before_start"){
|
|
sdk_mode=WIFI_AP;bridge._stop_requested=true;
|
|
bridge.mqttTaskLoop();assert_ap_untouched();
|
|
assert(now_ms==0&&mode_reads==0&&bridge._stop_acked&&bridge.slot_work==0);
|
|
}else if(scenario=="handoff"||scenario=="companion_handoff"){
|
|
sdk_mode=WIFI_AP_STA;WiFi.state=WL_DISCONNECTED;
|
|
bridge._manage_wifi=scenario!="companion_handoff";
|
|
tick=[&](){if(now_ms<100){assert_ap_untouched();assert(!bridge._wifi_event_registered);}
|
|
else sdk_mode=WIFI_STA;};
|
|
bridge.mqttTaskLoop();
|
|
assert(now_ms==1100&&sdk_mode==WIFI_STA&&bridge.slot_work==1&&!bridge._stop_acked);
|
|
assert(enable_calls==1&&event_registers==1&&reconnect_writes==1);
|
|
assert(station_begins==(bridge._manage_wifi?1:0));
|
|
}else if(scenario=="cold_and_restart"){
|
|
driver_initialized=false;sdk_mode=WIFI_MODE_NULL;bridge._ntp_synced=false;
|
|
assert(bridge.initializeWiFiInTask());
|
|
assert(driver_initialized&&sdk_mode==WIFI_STA&&event_registers==1&&bridge._ntp_sync_pending);
|
|
sdk_mode=WIFI_AP_STA;const int writes=mode_writes;
|
|
assert(!bridge.initializeWiFiInTask()&&mode_writes==writes&&event_registers==1);
|
|
sdk_mode=WIFI_STA;assert(bridge.initializeWiFiInTask());assert(event_registers==1);
|
|
}else if(scenario=="failures"){
|
|
read_failure_at=1;assert(!bridge.initializeWiFiInTask());assert(enable_calls==0);
|
|
read_failure_at=0;enable_failure=true;assert(!bridge.initializeWiFiInTask());
|
|
assert(mode_writes==0&&event_registers==0&&reconnect_writes==0);
|
|
enable_failure=false;read_failure_at=mode_reads+2;
|
|
assert(!bridge.initializeWiFiInTask());assert(event_registers==0&&reconnect_writes==0);
|
|
read_failure_at=0;assert(bridge.initializeWiFiInTask());assert(event_registers==1);
|
|
}else if(scenario=="race"){
|
|
race_ap=true;assert(!bridge.initializeWiFiInTask());
|
|
assert(sdk_mode==WIFI_AP_STA&&event_registers==0&&reconnect_writes==0&&station_begins==0);
|
|
race_ap=false;sdk_mode=WIFI_STA;assert(bridge.initializeWiFiInTask());assert(event_registers==1);
|
|
}else if(scenario=="changed_credentials"){
|
|
// A different saved tuple must replace an old, still-connected station.
|
|
bridge._wifi_credentials_changed=true;
|
|
assert(bridge.initializeWiFiInTask());
|
|
assert(station_disconnects==1&&station_begins==1&&!bridge._wifi_credentials_changed);
|
|
WiFi.state=WL_CONNECTED;
|
|
assert(bridge.initializeWiFiInTask());
|
|
assert(station_disconnects==1&&station_begins==1);
|
|
// A setup/OTA owner defers that reconnect without consuming the intent.
|
|
sdk_mode=WIFI_AP_STA;WiFi.state=WL_CONNECTED;bridge._wifi_credentials_changed=true;
|
|
assert(!bridge.initializeWiFiInTask()&&bridge._wifi_credentials_changed);
|
|
assert(!bridge.handleWiFiConnection(20000)&&bridge._wifi_credentials_changed);
|
|
assert(station_disconnects==1&&station_begins==1);
|
|
sdk_mode=WIFI_STA;
|
|
now_ms=20000;
|
|
bridge.handleWiFiConnection(20000);
|
|
assert(!bridge._wifi_credentials_changed&&station_disconnects==2&&station_begins==2);
|
|
}else if(scenario=="recovery"||scenario=="companion_recovery"){
|
|
bridge._manage_wifi=scenario!="companion_recovery";
|
|
for(int mode:{WIFI_AP,WIFI_AP_STA}){
|
|
sdk_mode=mode;WiFi.state=WL_DISCONNECTED;
|
|
bridge.beginWiFiStation();assert(station_begins==0);
|
|
assert(!bridge.handleWiFiConnection(20000));
|
|
assert(station_disconnects==0&&station_begins==0&&bridge._last_wifi_reconnect_attempt==0);
|
|
assert(bridge._wifi_reconnect_backoff_attempt==0&&bridge._last_wifi_check==0);
|
|
WiFi.state=WL_CONNECTED;assert(!bridge.handleWiFiConnection(40000));
|
|
assert(channel_checks==0&&power_writes==0&&tx_power_writes==0);
|
|
}
|
|
// Unknown SDK mode also defers, without consuming the due reconnect rung.
|
|
sdk_mode=WIFI_STA;WiFi.state=WL_DISCONNECTED;read_failure_at=mode_reads+1;
|
|
assert(!bridge.handleWiFiConnection(20000)&&station_disconnects==0);
|
|
read_failure_at=0;assert(!bridge.handleWiFiConnection(20000));
|
|
assert(station_begins==(bridge._manage_wifi?1:0));
|
|
assert(station_disconnects==(bridge._manage_wifi?1:0));
|
|
WiFi.state=WL_CONNECTED;assert(bridge.handleWiFiConnection(40000));
|
|
assert(power_writes==1&&tx_power_writes==1&&channel_checks==1);
|
|
}else assert(false);
|
|
}
|