Include runtime ESP-NOW in Full ESP32 repeaters

This commit is contained in:
mikecarper committed 2026-10-05 17:33:02 -07:00
1 parent 6cd1289c8b
commit 69d9ac84bc
21 files changed
+1098 -104

No files matched your search

+219
View File
@@ -0,0 +1,219 @@
// Production methods and dispatch branches are inserted by test_espnow_runtime.py.
// The bridge driver is mocked to inject SDK failures without ESP32 hardware.
#include <cassert>
#include <cstdio>
#include <cstring>
#include <cstdint>
#include <initializer_list>
#include <helpers/CLICommandUtils.h>
#include <helpers/bridges/ESPNowBridgeFormat.h>
#define MESH_DEBUG_PRINTLN(...) ((void)0)
namespace mesh { struct Packet {}; }
struct StrHelper {
static void strncpy(char* out, const char* in, size_t size) {
assert(size); std::strncpy(out, in, size - 1); out[size - 1] = 0;
}
};
struct NodePrefs {
uint8_t bridge_enabled = 0, espnow_bridge_enabled = 0;
uint16_t bridge_delay = 0;
uint8_t bridge_pkt_src = 0, bridge_uart = 0, bridge_channel = 0, bridge_format = 0;
uint32_t bridge_baud = 0;
char bridge_secret[32] = {};
};
class AbstractBridge {
public:
virtual ~AbstractBridge() = default;
virtual void begin() = 0;
virtual void end() = 0;
virtual bool isRunning() const = 0;
virtual void loop() = 0;
virtual void sendPacket(mesh::Packet*) = 0;
};
class ESPNowBridge : public AbstractBridge {
bool running = false;
public:
unsigned begins = 0, ends = 0, fail_starts = 0, sent = 0, loops = 0;
void begin() override {
++begins;
if (fail_starts) { --fail_starts; return; }
running = true;
}
void end() override { ++ends; running = false; }
bool isRunning() const override { return running; }
void loop() override { ++loops; }
void sendPacket(mesh::Packet*) override { ++sent; }
};
static uint32_t fake_millis = 100;
static uint32_t millis() { return fake_millis; }
static bool millisHasNowPassed(uint32_t deadline) {
return static_cast<int32_t>(fake_millis - deadline) >= 0;
}
struct FakeBoard { bool ota = false; bool isOTAUpdateRunning() const { return ota; } };
struct FakeCLI { FakeBoard board; FakeBoard* getBoard() { return &board; } };
struct Callbacks {
virtual ~Callbacks() = default;
virtual bool isBridgeRunning() const = 0;
virtual bool setBridgeState(bool) = 0;
virtual bool restartBridge() = 0;
@CALLBACK_FORWARDS@
};
class MyMesh : public Callbacks {
public:
NodePrefs _prefs;
ESPNowBridge bridge;
uint32_t shared_espnow_retry_at = 0;
FakeCLI _cli;
void configureBridgeFilter(AbstractBridge*) {}
void defaults() { @DEFAULTS@ }
void boot() {
@BOOT@
}
void routeRx(mesh::Packet* pkt) {
@ROUTE_RX@
}
void routeTx(mesh::Packet* pkt) {
@ROUTE_TX@
}
void serviceBridges() {
@SERVICE@
}
bool bridgesPreventSleep() const {
@PENDING@
return false;
}
@LIFECYCLE@
};
@PARSERS@
class CommonCLI {
public:
NodePrefs* _prefs;
MyMesh* _callbacks;
NodePrefs stored;
unsigned saves = 0;
explicit CommonCLI(MyMesh& mesh) : _prefs(&mesh._prefs), _callbacks(&mesh) {}
void savePrefs() { ++saves; stored = *_prefs; }
void set(const char* input, char* reply) {
char config[256] = {};
assert(strlen(input) < sizeof(config)); strcpy(config, input);
@SET_BRANCHES@
}
void get(const char* config, char* reply) { @GET_BRANCHES@ }
};
static void expect_get(CommonCLI& cli, const char* key, const char* expected) {
char reply[256] = {}; cli.get(key, reply); assert(strcmp(reply, expected) == 0);
}
static void assert_aliases(CommonCLI& cli, bool enabled, bool running) {
expect_get(cli, "bridge.enabled", enabled ? "> on" : "> off");
expect_get(cli, "espnow.enabled", enabled ? "> on" : "> off");
expect_get(cli, "bridge.running", running ? "> on" : "> off");
expect_get(cli, "espnow.running", running ? "> on" : "> off");
}
int main() {
{
MyMesh mesh; mesh.defaults();
CommonCLI cli(mesh);
#if defined(ESPNOW_BRIDGE_MERGED) && !defined(ESPNOW_BRIDGE_DEFAULT_ON)
const bool initial_enabled = false;
#else
const bool initial_enabled = true;
#endif
assert(mesh._prefs.bridge_enabled == initial_enabled);
assert(mesh._prefs.espnow_bridge_enabled == initial_enabled);
mesh.boot();
assert(mesh.bridge.begins == (initial_enabled ? 1 : 0));
assert_aliases(cli, initial_enabled, initial_enabled);
expect_get(cli, "bridge.type", "> espnow");
}
MyMesh mesh; mesh.defaults();
CommonCLI cli(mesh);
char reply[256] = {};
cli.set("bridge.enabled off", reply);
assert(strcmp(reply, "OK") == 0 && !mesh.bridgesPreventSleep());
assert_aliases(cli, false, false);
const unsigned initial_begins = mesh.bridge.begins;
cli.set("bridge.channel 6", reply);
assert(strcmp(reply, "OK") == 0 && mesh.bridge.begins == initial_begins);
cli.set("bridge.secret combined-secret", reply);
assert(strcmp(reply, "OK") == 0 && mesh.bridge.begins == initial_begins);
cli.set("bridge.format raw", reply);
assert(strncmp(reply, "OK", 2) == 0 && mesh.bridge.begins == initial_begins);
cli.set("espnow.enabled on", reply);
assert(strcmp(reply, "OK") == 0 && mesh.bridgesPreventSleep());
assert_aliases(cli, true, true);
mesh::Packet packet;
mesh.routeRx(&packet); mesh.routeTx(&packet); mesh.serviceBridges();
assert(mesh.bridge.sent == 1 && mesh.bridge.loops == 1); // RX is routed once.
cli.set("bridge.source tx", reply);
assert(strcmp(reply, "OK") == 0);
mesh.routeRx(&packet); mesh.routeTx(&packet);
assert(mesh.bridge.sent == 2); // TX is routed once, RX no longer feeds bridge.
const unsigned configured_begins = mesh.bridge.begins;
cli.set("bridge.channel 11", reply);
cli.set("bridge.secret updated-secret", reply);
cli.set("bridge.format wrapped", reply);
assert(mesh.bridge.begins == configured_begins + 3);
expect_get(cli, "bridge.channel", "> 11");
expect_get(cli, "bridge.secret", "> updated-secret");
expect_get(cli, "bridge.format", "> wrapped");
const unsigned saves = cli.saves;
for (const char* invalid : {"bridge.enabled onjunk", "espnow.enabled onjunk",
"bridge.channel 0", "bridge.channel 14", "bridge.channel 6junk",
"bridge.secret ", "bridge.format unsupported", "bridge.source rxjunk"}) {
cli.set(invalid, reply);
assert(strncmp(reply, "Error", 5) == 0 && cli.saves == saves);
assert_aliases(cli, true, true);
}
// The persisted aliases reconstruct exactly the enabled sole runtime.
MyMesh restarted; restarted.defaults(); restarted._prefs = cli.stored;
restarted.boot();
assert(restarted.bridge.begins == 1 && restarted.isBridgeRunning());
// Failed SDK initialization preserves saved intent and retries cooperatively.
cli.set("espnow.enabled off", reply);
mesh.bridge.fail_starts = 2;
cli.set("bridge.enabled on", reply);
assert(strstr(reply, "setting saved") && !mesh.isBridgeRunning());
assert_aliases(cli, true, false);
mesh.serviceBridges();
const unsigned after_first_retry = mesh.bridge.begins;
mesh.serviceBridges();
assert(mesh.bridge.begins == after_first_retry && !mesh.bridgesPreventSleep());
fake_millis += 5001;
mesh.serviceBridges();
assert(mesh.bridge.begins == after_first_retry + 1 && mesh.isBridgeRunning());
// Paused browser OTA must block direct enable, restart, and timed retries.
assert(mesh.setEspNowBridgeState(false));
mesh._cli.board.ota = true;
const unsigned before_ota = mesh.bridge.begins;
cli.set("espnow.enabled on", reply);
assert(strstr(reply, "setting saved") && !mesh.isEspNowBridgeRunning());
cli.set("bridge.channel 3", reply);
assert(strstr(reply, "setting saved") && !mesh.isBridgeRunning());
for (unsigned tick = 0; tick < 10; ++tick) {
fake_millis += 5001;
mesh.serviceBridges();
}
assert(mesh.bridge.begins == before_ota && !mesh.bridgesPreventSleep());
mesh._cli.board.ota = false;
mesh.serviceBridges();
assert(mesh.bridge.begins == before_ota + 1 && mesh.isBridgeRunning());
// Runtime switches through either spelling must not leave the radio running.
for (unsigned cycle = 0; cycle < 100; ++cycle) {
cli.set("bridge.enabled off", reply);
assert(strcmp(reply, "OK") == 0 && !mesh.bridgesPreventSleep());
assert_aliases(cli, false, false);
cli.set("espnow.enabled on", reply);
assert(strcmp(reply, "OK") == 0 && mesh.bridgesPreventSleep());
assert_aliases(cli, true, true);
cli.set("espnow.enabled off", reply);
assert(strcmp(reply, "OK") == 0 && !mesh.bridgesPreventSleep());
cli.set("bridge.enabled on", reply);
assert(strcmp(reply, "OK") == 0 && mesh.isBridgeRunning());
}
cli.set("espnow.enabled off", reply);
const unsigned sent_when_off = mesh.bridge.sent;
mesh.routeRx(&packet); mesh.routeTx(&packet); mesh.serviceBridges();
assert(mesh.bridge.sent == sent_when_off && !mesh.bridgesPreventSleep());
puts("ESP-NOW runtime checks passed");
}
+293
View File
@@ -0,0 +1,293 @@
// Production bridge methods are injected by test_espnow_radio_lifecycle.py.
// Keep Arduino's private caches distinct from the underlying SDK driver.
#include <cassert>
#include <cstdint>
#include <cstring>
#include <initializer_list>
#include <helpers/WirelessControl.h>
#include <helpers/ESPNowRawFragmentation.h>
#include <helpers/bridges/ESPNowBridgeFormat.h>
#define ESP_IDF_VERSION_VAL(a,b,c) ((a)*10000+(b)*100+(c))
#define BRIDGE_DEBUG_PRINTLN(...) ((void)0)
#define portENTER_CRITICAL(x) ((void)0)
#define portEXIT_CRITICAL(x) ((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;
using esp_err_t = int;
enum wifi_mode_t { WIFI_OFF=0, WIFI_STA=1, WIFI_AP=2, WIFI_AP_STA=3 };
constexpr wifi_mode_t WIFI_MODE_NULL = WIFI_OFF, WIFI_MODE_STA = WIFI_STA;
constexpr int ESP_OK=0, WIFI_STORAGE_RAM=0, WIFI_IF_STA=0, WIFI_SECOND_CHAN_NONE=0;
constexpr int WIFI_PROTOCOL_11B=1, WIFI_PROTOCOL_11G=2, WIFI_PROTOCOL_11N=4, WIFI_PROTOCOL_LR=8;
constexpr int WIFI_PHY_MODE_LR=1, WIFI_PHY_RATE_LORA_250K=2;
enum esp_now_send_status_t { ESP_NOW_SEND_SUCCESS=0, ESP_NOW_SEND_FAIL=1 };
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, wifi_starts=0, wifi_stops=0, wifi_deinits=0, facade_initializations=0;
bool sdk_initialized=false, sdk_started=false, espnow_active=false;
uint8_t wifi_channel=6;
wifi_mode_t sdk_mode=WIFI_OFF;
int esp_wifi_init(wifi_init_config_t*) {
if (sdk_initialized) return ESP_OK; // esp-idf init is idempotent.
if (failure==11) return -1;
sdk_initialized=true;
return ESP_OK;
}
int esp_wifi_set_storage(int) { return !sdk_initialized || failure==12 ? -1 : ESP_OK; }
int esp_wifi_set_mode(wifi_mode_t mode) {
if (!sdk_initialized || failure==13) return -1;
sdk_mode=mode;
return ESP_OK;
}
int esp_wifi_get_mode(wifi_mode_t* mode) {
if (!sdk_initialized) return -1;
*mode=sdk_mode;
return ESP_OK;
}
int esp_wifi_start() {
if (!sdk_initialized || failure==14) return -1;
sdk_started=true;
++wifi_starts;
return ESP_OK;
}
int esp_wifi_stop() {
if (!sdk_initialized || failure==16) return -1;
assert(!espnow_active);
sdk_started=false;
++wifi_stops;
return ESP_OK;
}
int esp_wifi_deinit() {
if (!sdk_initialized || sdk_started || failure==17) return -1;
assert(!espnow_active);
sdk_initialized=false;
sdk_mode=WIFI_OFF;
++wifi_deinits;
return ESP_OK;
}
int esp_wifi_get_channel(uint8_t* channel, wifi_second_chan_t*) {
if (!sdk_initialized) return -1;
*channel=wifi_channel;
return ESP_OK;
}
int esp_wifi_set_channel(int channel, int) {
if (!sdk_started || failure==15) return -1;
wifi_channel=channel;
return ESP_OK;
}
int esp_wifi_set_protocol(int, int) { return !sdk_started || failure==2 ? -1 : ESP_OK; }
int esp_now_init() {
if (!sdk_started || failure==4) return -1;
assert(!espnow_active);
espnow_active=true;
return ESP_OK;
}
int esp_now_deinit() { assert(espnow_active); espnow_active=false; return ESP_OK; }
template<typename T> int esp_now_register_send_cb(T) { return failure==7 ? -1 : ESP_OK; }
template<typename T> int esp_now_register_recv_cb(T) { return failure==8 ? -1 : ESP_OK; }
int esp_now_add_peer(esp_now_peer_info_t* peer) {
assert(peer->channel==6);
return failure==9 ? -1 : ESP_OK;
}
int esp_now_del_peer(const uint8_t*) { return ESP_OK; }
int esp_now_set_peer_rate_config(const uint8_t*, esp_now_rate_config_t*) { return failure==10 ? -1 : ESP_OK; }
int esp_wifi_config_espnow_rate(int, int) { return failure==10 ? -1 : ESP_OK; }
struct Facade {
bool initialized=false, started=false;
wifi_mode_t getMode() const {
// Pinned Arduino 2.x and 3.x both gate this getter on both caches.
if (!initialized || !started) return WIFI_OFF;
wifi_mode_t mode=WIFI_OFF;
return esp_wifi_get_mode(&mode)==ESP_OK ? mode : WIFI_OFF;
}
int channel() const {
// channel() only checks initialized, not started, in both pinned cores.
if (!initialized) return 0;
uint8_t channel=0;
wifi_second_chan_t secondary=0;
return esp_wifi_get_channel(&channel, &secondary)==ESP_OK ? channel : 0;
}
bool mode(wifi_mode_t requested) {
const wifi_mode_t current=getMode();
if (current==requested) return true;
if (!current && requested && !initialized) {
wifi_init_config_t config=0;
if (esp_wifi_init(&config)!=ESP_OK) return false;
initialized=true;
++facade_initializations;
if (esp_wifi_set_storage(WIFI_STORAGE_RAM)!=ESP_OK) {
initialized=false;
return false;
}
}
if (current && !requested) {
if (!started) return true;
started=false;
if (esp_wifi_stop()!=ESP_OK) { started=true; return false; }
if (initialized && esp_wifi_deinit()!=ESP_OK) return false;
initialized=false;
return true;
}
if (esp_wifi_set_mode(requested)!=ESP_OK) return false;
if (started) return true;
started=true;
if (esp_wifi_start()!=ESP_OK) { started=false; return false; }
return true;
}
} WiFi;
namespace mesh { namespace wifi {
std::atomic<uint8_t>& bridgeEspNowChannel() { static std::atomic<uint8_t> channel{0}; return channel; }
} }
struct Backend : mesh::wireless::Backend {
bool wifi=false;
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;
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@
void coherent() {
// SDK ownership may exist without Arduino, but never the reverse.
assert(!WiFi.initialized || sdk_initialized);
assert(!WiFi.started || (sdk_initialized && sdk_started));
}
void off() {
coherent();
assert(!sdk_initialized && !sdk_started && !espnow_active);
assert(!WiFi.initialized && !WiFi.started);
assert(mesh::wifi::bridgeEspNowChannel().load()==0);
}
int main() {
Backend backend;
mesh::wireless::control().begin(backend);
Bridge bridge;
// Stopping a bridge that only used IDF must not allocate the Arduino facade.
for (int cycle=0; cycle<100; ++cycle) {
bridge.begin();
assert(bridge._initialized && sdk_started && !WiFi.initialized);
bridge.end();
bridge.end();
off();
assert(facade_initializations==0);
}
// Every driver startup failure releases pure-IDF resources without a facade.
for (int stage : {11,12,13,14,15,2,4,7,8,9,10}) {
failure=stage;
bridge.begin();
assert(!bridge._initialized);
bridge.end();
off();
assert(facade_initializations==0);
}
failure=0;
for (int cycle=0; cycle<100; ++cycle) {
// ESP-NOW -> WebConfig -> stop WebConfig -> stop ESP-NOW -> WebConfig.
bridge.begin();
assert(bridge._initialized);
assert(WiFi.mode(WIFI_AP_STA));
backend.wifi=true;
coherent();
assert(WiFi.mode(WIFI_STA)); // WebConfig keeps STA for the running bridge.
backend.wifi=false;
bridge.end();
off();
assert(WiFi.mode(WIFI_AP_STA)); // Must initialize again, rather than use stale caches.
coherent();
assert(WiFi.mode(WIFI_OFF));
off();
// Browser OTA pauses ESP-NOW, initializes Arduino, then leaves STA alive.
bridge.begin();
bridge.end();
off();
assert(WiFi.mode(WIFI_AP_STA));
backend.wifi=true;
coherent();
assert(WiFi.mode(WIFI_STA));
backend.wifi=false;
bridge.begin();
assert(bridge._initialized && WiFi.started);
bridge.end();
off();
}
// An active infrastructure owner survives all bridge failures and stops.
assert(WiFi.mode(WIFI_AP_STA));
backend.wifi=true;
const int stops=wifi_stops, deinits=wifi_deinits;
for (int stage : {2,4,7,8,9,10}) {
failure=stage;
bridge.begin();
assert(!bridge._initialized && !espnow_active);
bridge.end();
coherent();
assert(sdk_started && WiFi.started && WiFi.getMode()==WIFI_AP_STA);
assert(wifi_stops==stops && wifi_deinits==deinits);
}
failure=0;
bridge.begin();
bridge.end();
assert(wifi_stops==stops && wifi_deinits==deinits);
backend.wifi=false;
assert(WiFi.mode(WIFI_OFF));
off();
// A failed Arduino start retains initialized=true and started=false.
// Driver teardown must repair this partial cache, including its own failures.
for (int stage : {0,12,13,14,15,2,4,7,8,9,10}) {
failure=14;
assert(!WiFi.mode(WIFI_STA));
assert(WiFi.initialized && !WiFi.started && sdk_initialized);
assert(WiFi.getMode()==WIFI_OFF && WiFi.channel()==6);
failure=stage;
bridge.begin();
if (!stage) assert(bridge._initialized);
else assert(!bridge._initialized);
bridge.end();
coherent(); // A failed repair must not destroy the SDK under a cached facade.
failure=0;
stopBridgeWiFiIfUnused();
off();
assert(WiFi.mode(WIFI_AP_STA));
coherent();
assert(WiFi.mode(WIFI_OFF));
off();
}
// SDK shutdown errors also leave facade caches coherent for a safe retry.
for (int stage : {16,17}) {
assert(WiFi.mode(WIFI_STA));
bridge.begin();
assert(bridge._initialized);
failure=stage;
bridge.end();
assert(!espnow_active);
coherent();
failure=0;
stopBridgeWiFiIfUnused();
off();
}
}
+13 -1
View File
@@ -66,7 +66,10 @@ int main() {
FakeFS fs{json};
assert(cli.import(&fs) && cli.saves == 1);
assert(strcmp(cli.prefs.node_name, "imported node") == 0);
#if defined(RS232_BRIDGE_MERGED) && !defined(RS232_BRIDGE_DEFAULT_ON)
#if defined(ESPNOW_BRIDGE_MERGED) && !defined(ESPNOW_BRIDGE_DEFAULT_ON) \
&& !defined(WITH_RS232_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
assert(cli.prefs.bridge_enabled == 0);
#elif defined(RS232_BRIDGE_MERGED) && !defined(RS232_BRIDGE_DEFAULT_ON)
const bool explicit_uart = strstr(json, "uart:1") || strstr(json, "uart:2");
const bool enabled = explicit_uart && strstr(json, "en:1");
assert(cli.prefs.bridge_enabled == enabled && cli.prefs.bridge_uart == 2);
@@ -77,6 +80,8 @@ int main() {
#endif
#if defined(ESPNOW_BRIDGE_MERGED) && !defined(ESPNOW_BRIDGE_DEFAULT_ON)
assert(cli.prefs.espnow_bridge_enabled == 0);
#elif defined(ESPNOW_BRIDGE_MERGED) && !defined(WITH_RS232_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
assert(cli.prefs.espnow_bridge_enabled == cli.prefs.bridge_enabled);
#else
assert(cli.prefs.espnow_bridge_enabled == 1);
#endif
@@ -85,9 +90,16 @@ int main() {
}
// Malformed JSON may partially apply scalar fields before the parser rejects it.
CLI broken;
broken.prefs.espnow_bridge_enabled = 1;
FakeFS malformed{"{bridge:{en:1,uart:1}"};
assert(!broken.import(&malformed) && broken.saves == 0);
#if defined(RS232_BRIDGE_MERGED) && !defined(RS232_BRIDGE_DEFAULT_ON)
assert(broken.prefs.bridge_enabled == 0 && broken.prefs.bridge_uart == 2);
#endif
#if defined(ESPNOW_BRIDGE_MERGED) && !defined(ESPNOW_BRIDGE_DEFAULT_ON)
assert(broken.prefs.espnow_bridge_enabled == 1);
#if !defined(WITH_RS232_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
assert(broken.prefs.bridge_enabled == 0);
#endif
#endif
}
+19 -3
View File
@@ -46,7 +46,13 @@ int main() {
CommonCLI reader;
reader.fs.files["/com_prefs"] = image;
reader.loadPrefsInt(&reader.fs, "/com_prefs");
#if defined(ESPNOW_BRIDGE_MERGED) && !defined(WITH_RS232_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
// The new sole bridge stores independent ESP-NOW intent and mirrors the
// legacy primary alias, even if an older caller supplied different flags.
assert(reader.prefs.bridge_enabled == secondary);
#else
assert(reader.prefs.bridge_enabled == primary);
#endif
assert(reader.prefs.espnow_bridge_enabled == secondary);
#ifdef WITH_RS232_BRIDGE
assert(reader.prefs.bridge_uart == 2);
@@ -85,7 +91,7 @@ int main() {
assert(legacy_reader.prefs.bridge_enabled == 1 && legacy_reader.prefs.bridge_uart == 2);
#endif
#ifdef ESPNOW_BRIDGE_MERGED
#if defined(ESPNOW_BRIDGE_MERGED) && !defined(ESPNOW_BRIDGE_DEFAULT_ON)
for (unsigned invalid = 0; invalid <= 255; ++invalid) {
if (invalid == 0xA1) continue;
auto old = on.bytes;
@@ -94,7 +100,12 @@ int main() {
reader.fs.files["/com_prefs"] = old;
reader.loadPrefsInt(&reader.fs, "/com_prefs");
assert(reader.prefs.espnow_bridge_enabled == 0);
assert(reader.prefs.bridge_enabled == 1 && reader._com_prefs_needs_upgrade);
#if !defined(WITH_RS232_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
assert(reader.prefs.bridge_enabled == 0);
#else
assert(reader.prefs.bridge_enabled == 1);
#endif
assert(reader._com_prefs_needs_upgrade);
}
// Old 874-byte profiles saved the unused secondary flag as one. It is not intent.
auto old = on.bytes;
@@ -102,7 +113,12 @@ int main() {
CommonCLI reader;
reader.fs.files["/com_prefs"] = old;
reader.loadPrefsInt(&reader.fs, "/com_prefs");
assert(reader.prefs.espnow_bridge_enabled == 0 && reader.prefs.bridge_enabled == 1);
assert(reader.prefs.espnow_bridge_enabled == 0);
#if !defined(WITH_RS232_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
assert(reader.prefs.bridge_enabled == 0);
#else
assert(reader.prefs.bridge_enabled == 1);
#endif
reader.fs.files["/com_prefs"] = on.bytes;
reader.fs.fail_read_after = static_cast<int>(marker_offset);
reader.loadPrefsInt(&reader.fs, "/com_prefs");