Retain independent RS232 in Full MQTT repeaters
This commit is contained in:
1 parent
28023c7302
commit
51d74a200e
37 files changed
+1572
-196
No files matched your search
Vendored
+387
@@ -0,0 +1,387 @@
|
||||
// Real repeater methods and CLI dispatch are inserted by the Python harness.
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cstdint>
|
||||
#include <initializer_list>
|
||||
#include <new>
|
||||
#include <helpers/CLICommandUtils.h>
|
||||
#include <helpers/bridges/ESPNowBridgeFormat.h>
|
||||
#define BRIDGE_MAX_BAUD 115200
|
||||
#define PUB_KEY_SIZE 32
|
||||
#define MESH_DEBUG_PRINTLN(...) ((void)0)
|
||||
|
||||
namespace mesh {
|
||||
struct Packet { bool allowed = true; };
|
||||
struct LocalIdentity { uint8_t pub_key[32] = {}; };
|
||||
struct Utils { static void toHex(char* out, const uint8_t*, unsigned) { strcpy(out, "mock-node"); } };
|
||||
}
|
||||
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, rs232_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;
|
||||
float freq = 915, bw = 125;
|
||||
uint8_t sf = 7, cr = 5, disable_fwd = 0;
|
||||
char bridge_secret[32] = {}, node_name[32] = "triple bridge";
|
||||
};
|
||||
struct FakeUart {
|
||||
unsigned begins = 0, ends = 0;
|
||||
int rx = -1, tx = -1;
|
||||
uint32_t baud = 0;
|
||||
bool running = false;
|
||||
} Serial2;
|
||||
struct FakeSensors {
|
||||
unsigned yield_calls = 0;
|
||||
bool gpsUsesSerialUart(uint8_t uart) const { return uart == 1; }
|
||||
bool gpsSerialTransportMayConflict(uint8_t uart) const { return uart == 1; }
|
||||
bool gpsSerialTransportCanYield(uint8_t) const { return true; }
|
||||
bool setGpsSerialTransportBlocked(uint8_t, bool) { ++yield_calls; return true; }
|
||||
};
|
||||
class AbstractBridge {
|
||||
bool (*filter)(void*, const mesh::Packet*) = nullptr;
|
||||
void* filter_context = nullptr;
|
||||
protected:
|
||||
bool allow(mesh::Packet* packet) { return !filter || filter(filter_context, packet); }
|
||||
public:
|
||||
unsigned filters = 0;
|
||||
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;
|
||||
void setPacketFilter(bool (*check)(void*, const mesh::Packet*), void* context) {
|
||||
filter = check; filter_context = context; ++filters;
|
||||
}
|
||||
};
|
||||
class RS232Bridge : public AbstractBridge {
|
||||
NodePrefs* prefs;
|
||||
FakeUart& uart;
|
||||
int rx, tx;
|
||||
bool running = false;
|
||||
public:
|
||||
inline static unsigned live = 0, fail_starts = 0, fail_allocations = 0;
|
||||
static void* operator new(size_t size, const std::nothrow_t&) noexcept {
|
||||
if (fail_allocations) { --fail_allocations; return nullptr; }
|
||||
return ::operator new(size, std::nothrow);
|
||||
}
|
||||
static void operator delete(void* pointer) noexcept { ::operator delete(pointer); }
|
||||
unsigned loops = 0, sent = 0;
|
||||
RS232Bridge(NodePrefs* p, FakeUart& u, int r, int t, void*, void*)
|
||||
: prefs(p), uart(u), rx(r), tx(t) { ++live; }
|
||||
~RS232Bridge() override { --live; }
|
||||
void begin() override {
|
||||
assert(filters); ++uart.begins;
|
||||
if (fail_starts) { --fail_starts; return; }
|
||||
uart.rx = rx; uart.tx = tx; uart.baud = prefs->bridge_baud;
|
||||
running = uart.running = true;
|
||||
}
|
||||
void end() override { ++uart.ends; running = uart.running = false; }
|
||||
bool isRunning() const override { return running; }
|
||||
void loop() override { ++loops; }
|
||||
void sendPacket(mesh::Packet* p) override { if (allow(p)) ++sent; }
|
||||
};
|
||||
class ESPNowBridge : public AbstractBridge {
|
||||
bool running = false;
|
||||
public:
|
||||
unsigned begins = 0, ends = 0, fail_starts = 0, loops = 0, sent = 0;
|
||||
void begin() override {
|
||||
assert(filters); ++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* p) override { if (allow(p)) ++sent; }
|
||||
};
|
||||
struct MQTTNodeInfo {
|
||||
const char* node_name;
|
||||
float* freq;
|
||||
float* bw;
|
||||
uint8_t* sf;
|
||||
uint8_t* cr;
|
||||
uint8_t* repeat_flag;
|
||||
bool repeat_when_nonzero;
|
||||
};
|
||||
class MQTTBridge : public AbstractBridge {
|
||||
bool running = false;
|
||||
public:
|
||||
inline static unsigned live = 0, fail_starts = 0;
|
||||
unsigned begins = 0, ends = 0, loops = 0, sent = 0, received = 0;
|
||||
MQTTBridge(MQTTNodeInfo, void*, void*, mesh::LocalIdentity*) { ++live; }
|
||||
~MQTTBridge() override { --live; }
|
||||
void setDeviceID(const char*) {}
|
||||
void setFirmwareVersion(const char*) {}
|
||||
void setBoardModel(const char*) {}
|
||||
void setBuildDate(const char*) {}
|
||||
void setStatsSources(void*, void*, void*, void*) {}
|
||||
void begin() override {
|
||||
assert(filters); ++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* p) override { if (allow(p)) ++sent; }
|
||||
void onPacketReceived(mesh::Packet* p) { if (allow(p)) ++received; }
|
||||
};
|
||||
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 FakeWiFi { bool connected = true; bool isConnected() const { return connected; } } WiFi;
|
||||
struct FakeBoard {
|
||||
bool ota = false, allow_start = true, allow_stop = true;
|
||||
bool isOTAUpdateRunning() const { return ota; }
|
||||
const char* getManufacturerName() const { return "synthetic ESP32"; }
|
||||
bool startOTAUpdate(const char*, char* reply, bool) {
|
||||
ota = allow_start; strcpy(reply, allow_start ? "Started: http://192.168.4.1/update" : "ERR: OTA failed");
|
||||
return allow_start;
|
||||
}
|
||||
bool stopOTAUpdate(char* reply) {
|
||||
if (allow_stop) ota = false;
|
||||
strcpy(reply, allow_stop ? "OK - OTA stopped" : "ERR: upload active"); return allow_stop;
|
||||
}
|
||||
};
|
||||
struct FakeCLI {
|
||||
FakeBoard board;
|
||||
FakeBoard* getBoard() { return &board; }
|
||||
void* getObserverPrefs() { return nullptr; }
|
||||
};
|
||||
struct FakeAlerter { void setBridge(MQTTBridge*) {} };
|
||||
struct Callbacks {
|
||||
virtual ~Callbacks() = default;
|
||||
virtual bool isBridgeRunning() const = 0;
|
||||
virtual bool setBridgeState(bool) = 0;
|
||||
virtual bool restartBridge() = 0;
|
||||
virtual bool isRs232BridgeRunning() const = 0;
|
||||
virtual bool setRs232BridgeState(bool) = 0;
|
||||
virtual bool restartRs232Bridge() = 0;
|
||||
virtual bool isEspNowBridgeRunning() = 0;
|
||||
virtual bool setEspNowBridgeState(bool) = 0;
|
||||
virtual bool restartEspNowBridge() = 0;
|
||||
virtual bool isMqttBridgeRunning() = 0;
|
||||
virtual bool setMqttBridgeState(bool) = 0;
|
||||
virtual bool restartMqttBridge() = 0;
|
||||
};
|
||||
class MyMesh : public Callbacks {
|
||||
public:
|
||||
NodePrefs _prefs;
|
||||
RS232Bridge* bridge = nullptr;
|
||||
MQTTBridge* mqtt_bridge = nullptr;
|
||||
ESPNowBridge espnow_bridge;
|
||||
uint8_t active_rs232_bridge_uart = 0;
|
||||
uint32_t shared_espnow_retry_at = 0;
|
||||
unsigned filter_checks = 0;
|
||||
FakeSensors sensors;
|
||||
FakeCLI _cli;
|
||||
FakeAlerter _alerter;
|
||||
mesh::LocalIdentity self_id;
|
||||
void* _mgr = nullptr; void* _radio = nullptr; void* _ms = nullptr;
|
||||
void* getRTCClock() { return nullptr; }
|
||||
mesh::LocalIdentity getSelfId() { return self_id; }
|
||||
const char* getFirmwareVer() { return "test"; }
|
||||
const char* getBuildDate() { return "test"; }
|
||||
bool allowTransportPacket(const mesh::Packet* p, unsigned mode) {
|
||||
assert(mode == 2); ++filter_checks; return p->allowed;
|
||||
}
|
||||
@FILTER@
|
||||
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;
|
||||
}
|
||||
~MyMesh() override {
|
||||
assert(setRs232BridgeState(false));
|
||||
assert(setBridgeState(false));
|
||||
delete mqtt_bridge;
|
||||
}
|
||||
@LIFECYCLE@
|
||||
};
|
||||
@PARSERS@
|
||||
class CommonCLI {
|
||||
public:
|
||||
NodePrefs* _prefs;
|
||||
MyMesh* _callbacks;
|
||||
FakeSensors* _sensors;
|
||||
FakeBoard* _board;
|
||||
NodePrefs stored;
|
||||
unsigned saves = 0;
|
||||
bool fail_save = false;
|
||||
explicit CommonCLI(MyMesh& mesh) : _prefs(&mesh._prefs), _callbacks(&mesh),
|
||||
_sensors(&mesh.sensors), _board(mesh._cli.getBoard()) {}
|
||||
void savePrefs() { ++saves; stored = *_prefs; }
|
||||
bool trySavePrefs() { if (fail_save) return false; savePrefs(); return true; }
|
||||
void set(const char* input, char* reply) {
|
||||
char config[256] = {}; assert(strlen(input) < sizeof(config)); strcpy(config, input);
|
||||
@SET_BRANCHES@
|
||||
}
|
||||
bool get(const char* config, char* reply) {
|
||||
@GET_BRANCHES@
|
||||
return false;
|
||||
}
|
||||
void run(const char* command, char* reply) {
|
||||
if (false) {
|
||||
@OTA@
|
||||
}
|
||||
}
|
||||
};
|
||||
static void expect_get(CommonCLI& cli, const char* key, const char* expected) {
|
||||
char reply[256] = {}; cli.get(key, reply); assert(strcmp(reply, expected) == 0);
|
||||
}
|
||||
int main() {
|
||||
{
|
||||
MyMesh fresh; fresh.defaults(); CommonCLI cli(fresh);
|
||||
assert(fresh._prefs.bridge_enabled && fresh._prefs.espnow_bridge_enabled);
|
||||
assert(!fresh._prefs.rs232_bridge_enabled);
|
||||
const unsigned before = Serial2.begins;
|
||||
fresh.boot();
|
||||
assert(fresh.isMqttBridgeRunning() && fresh.isEspNowBridgeRunning());
|
||||
assert(!fresh.isRs232BridgeRunning() && !fresh.bridge && Serial2.begins == before);
|
||||
expect_get(cli, "bridge.type", "> mqtt+rs232+espnow");
|
||||
expect_get(cli, "rs232.enabled", "> off");
|
||||
expect_get(cli, "rs232.running", "> off");
|
||||
}
|
||||
MyMesh mesh; mesh.defaults(); mesh.boot(); CommonCLI cli(mesh);
|
||||
char reply[256] = {};
|
||||
cli.set("rs232.enabled on", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && mesh.isRs232BridgeRunning());
|
||||
assert(Serial2.rx == 5 && Serial2.tx == 6 && Serial2.baud == 115200);
|
||||
assert(mesh.sensors.yield_calls == 0 && mesh.bridgesPreventSleep());
|
||||
expect_get(cli, "rs232.enabled", "> on"); expect_get(cli, "rs232.running", "> on");
|
||||
mesh::Packet packet;
|
||||
mesh.routeRx(&packet); mesh.routeTx(&packet); mesh.serviceBridges();
|
||||
assert(mesh.bridge->sent == 1 && mesh.bridge->loops == 1);
|
||||
assert(mesh.espnow_bridge.sent == 1 && mesh.espnow_bridge.loops == 1);
|
||||
assert(mesh.mqtt_bridge->received == 1 && mesh.mqtt_bridge->sent == 1);
|
||||
assert(mesh.mqtt_bridge->loops == 0); // Its actual task services MQTT independently.
|
||||
assert(mesh.filter_checks == 4);
|
||||
packet.allowed = false;
|
||||
mesh.routeRx(&packet); mesh.routeTx(&packet);
|
||||
assert(mesh.filter_checks == 8 && mesh.bridge->sent == 1 && mesh.espnow_bridge.sent == 1);
|
||||
assert(mesh.mqtt_bridge->received == 1 && mesh.mqtt_bridge->sent == 1);
|
||||
packet.allowed = true;
|
||||
const unsigned wifi_starts = mesh.mqtt_bridge->begins, wireless_starts = mesh.espnow_bridge.begins;
|
||||
cli.set("bridge.baud 57600", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && Serial2.baud == 57600);
|
||||
cli.set("bridge.uart 2", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && mesh.isRs232BridgeRunning());
|
||||
assert(mesh.mqtt_bridge->begins == wifi_starts && mesh.espnow_bridge.begins == wireless_starts);
|
||||
const unsigned saves = cli.saves;
|
||||
for (const char* invalid : {"rs232.enabled onjunk", "bridge.uart 1", "bridge.uart 2junk",
|
||||
"bridge.baud 9599", "bridge.baud 115201", "bridge.baud 57600junk"}) {
|
||||
cli.set(invalid, reply); assert(strncmp(reply, "Error", 5) == 0 && cli.saves == saves);
|
||||
assert(mesh.isRs232BridgeRunning() && mesh.isMqttBridgeRunning() && mesh.isEspNowBridgeRunning());
|
||||
}
|
||||
RS232Bridge::fail_starts = 1;
|
||||
cli.set("bridge.baud 19200", reply);
|
||||
assert(strstr(reply, "baud unchanged") && cli.saves == saves);
|
||||
assert(mesh._prefs.bridge_baud == 57600 && Serial2.baud == 57600 && mesh.isRs232BridgeRunning());
|
||||
assert(mesh.mqtt_bridge->begins == wifi_starts && mesh.espnow_bridge.begins == wireless_starts);
|
||||
cli.set("rs232.enabled off", reply); assert(strcmp(reply, "OK") == 0 && !mesh.bridge);
|
||||
const unsigned disabled_saves = cli.saves;
|
||||
RS232Bridge::fail_starts = 1;
|
||||
cli.set("rs232.enabled on", reply);
|
||||
assert(strstr(reply, "setting unchanged") && cli.saves == disabled_saves && !mesh.bridge);
|
||||
assert(!mesh._prefs.rs232_bridge_enabled && mesh.isMqttBridgeRunning() && mesh.isEspNowBridgeRunning());
|
||||
RS232Bridge::fail_allocations = 1;
|
||||
cli.set("rs232.enabled on", reply);
|
||||
assert(strstr(reply, "setting unchanged") && !mesh.bridge && !mesh._prefs.rs232_bridge_enabled);
|
||||
assert(cli.saves == disabled_saves && mesh.isMqttBridgeRunning() && mesh.isEspNowBridgeRunning());
|
||||
cli.fail_save = true;
|
||||
cli.set("rs232.enabled on", reply);
|
||||
assert(strstr(reply, "setting unchanged") && !mesh.bridge && !mesh._prefs.rs232_bridge_enabled);
|
||||
assert(cli.saves == disabled_saves && mesh.isMqttBridgeRunning() && mesh.isEspNowBridgeRunning());
|
||||
cli.fail_save = false;
|
||||
cli.set("rs232.enabled on", reply); assert(strcmp(reply, "OK") == 0);
|
||||
const unsigned saved_after_enable = cli.saves;
|
||||
cli.fail_save = true;
|
||||
cli.set("rs232.enabled off", reply);
|
||||
assert(strstr(reply, "setting unchanged") && mesh.isRs232BridgeRunning() && mesh._prefs.rs232_bridge_enabled);
|
||||
cli.set("bridge.baud 19200", reply);
|
||||
assert(strstr(reply, "baud unchanged") && mesh._prefs.bridge_baud == 57600 && Serial2.baud == 57600);
|
||||
cli.set("bridge.uart 2", reply);
|
||||
assert(strstr(reply, "UART unchanged") && mesh._prefs.bridge_uart == 2 && mesh.isRs232BridgeRunning());
|
||||
assert(cli.saves == saved_after_enable && mesh.mqtt_bridge->begins == wifi_starts);
|
||||
assert(mesh.espnow_bridge.begins == wireless_starts);
|
||||
cli.fail_save = false;
|
||||
// Saved UART settings recreate a running independent bridge after reboot.
|
||||
{
|
||||
MyMesh restarted; restarted.defaults(); restarted._prefs = cli.stored; restarted.boot();
|
||||
assert(restarted.isRs232BridgeRunning() && restarted.isMqttBridgeRunning());
|
||||
assert(restarted._prefs.bridge_baud == 57600 && restarted.isEspNowBridgeRunning());
|
||||
}
|
||||
// ESP-NOW format/channel changes and its legacy alias never restart the UART.
|
||||
const unsigned uart_before_wireless = Serial2.begins;
|
||||
cli.set("bridge.channel 6", reply); cli.set("bridge.secret three-transports", reply);
|
||||
cli.set("bridge.format raw", reply); assert(strncmp(reply, "OK", 2) == 0);
|
||||
assert(Serial2.begins == uart_before_wireless && mesh.mqtt_bridge->begins == wifi_starts);
|
||||
cli.set("bridge.enabled off", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && !mesh.isEspNowBridgeRunning());
|
||||
assert(mesh.isMqttBridgeRunning() && mesh.isRs232BridgeRunning());
|
||||
cli.set("mqtt.enabled off", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && !mesh.isMqttBridgeRunning());
|
||||
assert(mesh.isRs232BridgeRunning() && mesh.bridgesPreventSleep());
|
||||
const unsigned uart_tx_before = mesh.bridge->sent;
|
||||
mesh.routeRx(&packet); mesh.serviceBridges();
|
||||
assert(mesh.bridge->sent == uart_tx_before + 1);
|
||||
cli.set("espnow.enabled on", reply); assert(strcmp(reply, "OK") == 0);
|
||||
cli.set("mqtt.enabled on", reply); assert(strcmp(reply, "OK") == 0);
|
||||
mesh.serviceBridges(); assert(mesh.isEspNowBridgeRunning());
|
||||
// Browser OTA releases only WiFi users and leaves the enabled UART running.
|
||||
const unsigned uart_before_ota = Serial2.begins;
|
||||
cli.run("start ota ap", reply);
|
||||
assert(mesh._cli.board.ota && !mesh.isMqttBridgeRunning() && !mesh.isEspNowBridgeRunning());
|
||||
assert(mesh.isRs232BridgeRunning() && Serial2.begins == uart_before_ota);
|
||||
for (unsigned tick = 0; tick < 5; ++tick) { fake_millis += 5001; mesh.serviceBridges(); }
|
||||
assert(!mesh.isEspNowBridgeRunning() && mesh.isRs232BridgeRunning());
|
||||
mesh._cli.board.allow_stop = false; cli.run("stop ota", reply);
|
||||
assert(strcmp(reply, "ERR: upload active") == 0 && !mesh.isMqttBridgeRunning());
|
||||
mesh._cli.board.allow_stop = true; cli.run("stop ota", reply); mesh.serviceBridges();
|
||||
assert(!mesh._cli.board.ota && mesh.isMqttBridgeRunning() && mesh.isEspNowBridgeRunning());
|
||||
assert(mesh.isRs232BridgeRunning() && Serial2.begins == uart_before_ota);
|
||||
// Wireless startup failure retries without stopping or reallocating UART.
|
||||
cli.set("espnow.enabled off", reply); mesh.espnow_bridge.fail_starts = 2;
|
||||
cli.set("espnow.enabled on", reply); mesh.serviceBridges();
|
||||
const unsigned retry_starts = mesh.espnow_bridge.begins;
|
||||
mesh.serviceBridges(); assert(mesh.espnow_bridge.begins == retry_starts);
|
||||
fake_millis += 5001; mesh.serviceBridges(); assert(!mesh.isEspNowBridgeRunning());
|
||||
fake_millis += 5001; mesh.serviceBridges(); assert(mesh.isEspNowBridgeRunning());
|
||||
assert(mesh.isRs232BridgeRunning() && Serial2.begins == uart_before_ota);
|
||||
// Every switch, parameter change and rollback leaves other transports intact.
|
||||
for (unsigned cycle = 0; cycle < 100; ++cycle) {
|
||||
cli.set("rs232.enabled off", reply);
|
||||
assert(!mesh.bridge && mesh.isMqttBridgeRunning() && mesh.isEspNowBridgeRunning());
|
||||
cli.set("rs232.enabled on", reply);
|
||||
assert(mesh.isRs232BridgeRunning() && RS232Bridge::live == 1);
|
||||
}
|
||||
cli.set("rs232.enabled off", reply); cli.set("espnow.enabled off", reply); cli.set("mqtt.enabled off", reply);
|
||||
assert(!mesh.bridgesPreventSleep() && RS232Bridge::live == 0);
|
||||
cli.run("start ota", reply); cli.run("stop ota", reply); mesh.serviceBridges();
|
||||
assert(!mesh.isRs232BridgeRunning() && !mesh.isMqttBridgeRunning() && !mesh.isEspNowBridgeRunning());
|
||||
assert(mesh.sensors.yield_calls == 0);
|
||||
puts("Three transport runtime checks passed");
|
||||
}
|
||||
Vendored
+21
-3
@@ -6,7 +6,7 @@
|
||||
#include <helpers/ConfigSerializer.h>
|
||||
|
||||
struct NodePrefs : ConfigSerializer {
|
||||
uint8_t bridge_enabled = 0, bridge_uart = 2, espnow_bridge_enabled = 0;
|
||||
uint8_t bridge_enabled = 0, bridge_uart = 2, espnow_bridge_enabled = 0, rs232_bridge_enabled = 0;
|
||||
uint16_t bridge_delay = 500;
|
||||
uint8_t bridge_pkt_src = 1, bridge_channel = 1, bridge_format = 0;
|
||||
uint32_t bridge_baud = 115200;
|
||||
@@ -47,8 +47,14 @@ struct CLI {
|
||||
}
|
||||
bool import(FakeFS* fs) {
|
||||
bool loaded = false, is_upgrade = false;
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
bool node_prefs_needs_migration = false;
|
||||
#endif
|
||||
@IMPORT@
|
||||
assert(loaded == is_upgrade);
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
assert(node_prefs_needs_migration == loaded);
|
||||
#endif
|
||||
return loaded;
|
||||
}
|
||||
};
|
||||
@@ -60,17 +66,27 @@ int main() {
|
||||
"{name:\"imported node\",bridge:{en:1,uart:1}}",
|
||||
"{name:\"imported node\",bridge:{en:1,uart:2}}",
|
||||
"{name:\"imported node\",bridge:{en:0,uart:2}}",
|
||||
"{name:\"imported node\",bridge:{en:1,uart:2,rs232:1}}",
|
||||
}) {
|
||||
CLI cli;
|
||||
cli.prefs.espnow_bridge_enabled = 1; // An old unused flag cannot prove intent.
|
||||
cli.prefs.rs232_bridge_enabled = 1; // Reused objects cannot donate UART intent.
|
||||
FakeFS fs{json};
|
||||
assert(cli.import(&fs) && cli.saves == 1);
|
||||
assert(cli.import(&fs));
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
assert(cli.saves == 0);
|
||||
#else
|
||||
assert(cli.saves == 1);
|
||||
#endif
|
||||
assert(strcmp(cli.prefs.node_name, "imported node") == 0);
|
||||
#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(WITH_MQTT_BRIDGE) && defined(WITH_RS232_BRIDGE)
|
||||
assert(cli.prefs.bridge_enabled == (strstr(json, "en:1") != nullptr));
|
||||
assert(cli.prefs.rs232_bridge_enabled == 0 && cli.prefs.bridge_uart == 2);
|
||||
#elif defined(RS232_BRIDGE_MERGED) && !defined(RS232_BRIDGE_DEFAULT_ON)
|
||||
const bool explicit_uart = strstr(json, "uart:1") || strstr(json, "uart:2");
|
||||
const bool explicit_uart = strstr(json, "uart:2");
|
||||
const bool enabled = explicit_uart && strstr(json, "en:1");
|
||||
assert(cli.prefs.bridge_enabled == enabled && cli.prefs.bridge_uart == 2);
|
||||
#else
|
||||
@@ -85,8 +101,10 @@ int main() {
|
||||
#else
|
||||
assert(cli.prefs.espnow_bridge_enabled == 1);
|
||||
#endif
|
||||
#ifndef WITH_MQTT_BRIDGE
|
||||
assert(cli.saved_uart == cli.prefs.bridge_uart && cli.saved_enabled == cli.prefs.bridge_enabled);
|
||||
assert(cli.saved_secondary == cli.prefs.espnow_bridge_enabled);
|
||||
#endif
|
||||
}
|
||||
// Malformed JSON may partially apply scalar fields before the parser rejects it.
|
||||
CLI broken;
|
||||
|
||||
Vendored
+7
-3
@@ -29,7 +29,7 @@ template<typename T> T constrain(T value, int low, int high) {
|
||||
}
|
||||
|
||||
struct NodePrefs {
|
||||
uint8_t bridge_enabled = 0, espnow_bridge_enabled = 0;
|
||||
uint8_t bridge_enabled = 0, espnow_bridge_enabled = 0, rs232_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;
|
||||
@@ -117,6 +117,9 @@ struct Callbacks {
|
||||
virtual bool isBridgeRunning() const = 0;
|
||||
virtual bool setBridgeState(bool) = 0;
|
||||
virtual bool restartBridge() = 0;
|
||||
virtual bool isRs232BridgeRunning() const = 0;
|
||||
virtual bool setRs232BridgeState(bool) = 0;
|
||||
virtual bool restartRs232Bridge() = 0;
|
||||
#ifdef WITH_ESPNOW_BRIDGE
|
||||
virtual bool isEspNowBridgeRunning() = 0;
|
||||
virtual bool setEspNowBridgeState(bool) = 0;
|
||||
@@ -186,6 +189,7 @@ public:
|
||||
explicit CommonCLI(MyMesh& mesh)
|
||||
: _prefs(&mesh._prefs), _callbacks(&mesh), _sensors(&mesh.sensors) {}
|
||||
void savePrefs() { ++saves; stored = *_prefs; }
|
||||
bool trySavePrefs() { savePrefs(); return true; }
|
||||
void load(FakeFile file) {
|
||||
@INITIAL_UART@
|
||||
@LOAD_UART@
|
||||
@@ -212,7 +216,7 @@ static void check_upgrade(bool has_tail, uint8_t saved_uart, uint8_t enabled) {
|
||||
CommonCLI cli(mesh);
|
||||
cli.load(FakeFile{has_tail, saved_uart});
|
||||
#if defined(RS232_BRIDGE_MERGED) && !defined(RS232_BRIDGE_DEFAULT_ON)
|
||||
const bool normal_upgrade = !has_tail || saved_uart == 0;
|
||||
const bool normal_upgrade = !has_tail || saved_uart != 2;
|
||||
#else
|
||||
const bool normal_upgrade = false;
|
||||
#endif
|
||||
@@ -241,7 +245,7 @@ int main() {
|
||||
assert(Serial2.begins == begins + (mesh._prefs.bridge_enabled ? 1 : 0));
|
||||
}
|
||||
// Missing old tails and current normal-repeater zero sentinels fail safe.
|
||||
// Legacy dedicated UART1 is intent, and normalizes to the board's UART2.
|
||||
// Only a supported legacy UART proves intent; unsupported ports fail closed.
|
||||
for (uint8_t enabled : {uint8_t{0}, uint8_t{1}}) {
|
||||
check_upgrade(false, 0, enabled);
|
||||
check_upgrade(true, 0, enabled);
|
||||
|
||||
+141
-63
@@ -1,3 +1,4 @@
|
||||
#include <fstream>
|
||||
struct Capture {
|
||||
std::vector<uint8_t> bytes;
|
||||
std::map<const void*, size_t> offsets;
|
||||
@@ -9,9 +10,39 @@ struct Capture {
|
||||
size_t offset(const void* field) const { return offsets.at(field); }
|
||||
};
|
||||
|
||||
int main() {
|
||||
// The same executable can exchange actual serialized files with another profile.
|
||||
// This catches role transitions which cannot be represented by a single macro set.
|
||||
int main(int argc, char** argv) {
|
||||
if (argc > 1) {
|
||||
assert(argc == 7);
|
||||
CommonCLI cli;
|
||||
const bool writing = strcmp(argv[1], "write") == 0;
|
||||
if (writing) {
|
||||
cli.prefs.bridge_enabled = atoi(argv[3]);
|
||||
cli.prefs.espnow_bridge_enabled = atoi(argv[4]);
|
||||
cli.prefs.rs232_bridge_enabled = atoi(argv[5]);
|
||||
cli.prefs.bridge_uart = 2;
|
||||
cli.savePrefs(&cli.fs, PrefsSaveRouting::Scope::Common);
|
||||
assert(cli._common_save_succeeded);
|
||||
const auto& image = cli.fs.files.at("/com_prefs");
|
||||
std::ofstream stream(argv[2], std::ios::binary);
|
||||
stream.write(reinterpret_cast<const char*>(image.data()), image.size());
|
||||
assert(stream.good());
|
||||
} else {
|
||||
std::ifstream stream(argv[2], std::ios::binary);
|
||||
assert(stream.good());
|
||||
cli.fs.files["/com_prefs"] = std::vector<uint8_t>(
|
||||
std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>());
|
||||
cli.loadPrefsInt(&cli.fs, "/com_prefs");
|
||||
assert(cli.prefs.bridge_enabled == atoi(argv[3]));
|
||||
assert(cli.prefs.espnow_bridge_enabled == atoi(argv[4]));
|
||||
assert(cli.prefs.rs232_bridge_enabled == atoi(argv[5]));
|
||||
assert(cli.prefs.bridge_uart == atoi(argv[6]));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
CommonCLI writer;
|
||||
strcpy(writer.prefs.node_name, "MKE combined repeater");
|
||||
strcpy(writer.prefs.node_name, "Combined repeater");
|
||||
strcpy(writer.prefs.password, "test-password");
|
||||
writer.prefs.bridge_uart = 2;
|
||||
writer.prefs.bridge_baud = 57600;
|
||||
@@ -22,80 +53,134 @@ int main() {
|
||||
const size_t uart_offset = initial.offset(&writer.prefs.bridge_uart);
|
||||
const size_t enabled_offset = initial.offset(&writer.prefs.bridge_enabled);
|
||||
const size_t channel_offset = initial.offset(&writer.prefs.ota_channel);
|
||||
const size_t esp_marker_offset = channel_offset + 1;
|
||||
const size_t rs_intent_offset = initial.offset(&writer.prefs.rs232_bridge_enabled);
|
||||
const size_t rs_marker_offset = channel_offset + 3;
|
||||
assert(initial.bytes.size() == 877 && channel_offset == 873);
|
||||
assert(rs_intent_offset == channel_offset + 2);
|
||||
#ifdef ESPNOW_BRIDGE_MERGED
|
||||
assert(initial.bytes.size() == 875 && initial.bytes.back() == 0xA1);
|
||||
const size_t marker_offset = channel_offset + 1;
|
||||
assert(marker_offset == initial.bytes.size() - 1);
|
||||
assert(initial.bytes[esp_marker_offset] == 0xA1);
|
||||
#else
|
||||
assert(initial.bytes.size() == 874 && channel_offset == initial.bytes.size() - 1);
|
||||
assert(initial.bytes[esp_marker_offset] == 0);
|
||||
#endif
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
assert(initial.bytes[rs_marker_offset] == 0xB1);
|
||||
#else
|
||||
assert(initial.bytes[rs_marker_offset] == 0);
|
||||
#endif
|
||||
assert(initial.offset(writer.prefs.node_name) == 4);
|
||||
assert(initial.offset(&writer.prefs.freq) == 72);
|
||||
assert(initial.offset(&writer.prefs.bridge_enabled) == 127);
|
||||
assert(enabled_offset == 127);
|
||||
|
||||
for (uint8_t primary : {uint8_t{0}, uint8_t{1}}) {
|
||||
for (uint8_t secondary : {uint8_t{0}, uint8_t{1}}) {
|
||||
writer.prefs.bridge_enabled = primary;
|
||||
writer.prefs.espnow_bridge_enabled = secondary;
|
||||
writer.savePrefs(&writer.fs, PrefsSaveRouting::Scope::Common);
|
||||
assert(writer._common_save_succeeded);
|
||||
const auto image = writer.fs.files.at("/com_prefs");
|
||||
Capture checked;
|
||||
assert(writeCommonPrefsImage(checked, &writer.prefs));
|
||||
assert(checked.bytes == image); // Checked and ordinary writers agree.
|
||||
CommonCLI reader;
|
||||
reader.fs.files["/com_prefs"] = image;
|
||||
reader.loadPrefsInt(&reader.fs, "/com_prefs");
|
||||
for (uint8_t uart_intent : {uint8_t{0}, uint8_t{1}}) {
|
||||
writer.prefs.bridge_enabled = primary;
|
||||
writer.prefs.espnow_bridge_enabled = secondary;
|
||||
writer.prefs.rs232_bridge_enabled = uart_intent;
|
||||
writer.savePrefs(&writer.fs, PrefsSaveRouting::Scope::Common);
|
||||
assert(writer._common_save_succeeded);
|
||||
const auto image = writer.fs.files.at("/com_prefs");
|
||||
Capture checked;
|
||||
assert(writeCommonPrefsImage(checked, &writer.prefs));
|
||||
assert(checked.bytes == image); // Checked and ordinary writers agree.
|
||||
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);
|
||||
assert(reader.prefs.bridge_enabled == secondary);
|
||||
#else
|
||||
assert(reader.prefs.bridge_enabled == primary);
|
||||
assert(reader.prefs.bridge_enabled == primary);
|
||||
#endif
|
||||
assert(reader.prefs.espnow_bridge_enabled == secondary);
|
||||
assert(reader.prefs.espnow_bridge_enabled == secondary);
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
assert(reader.prefs.bridge_uart == 2);
|
||||
assert(reader.prefs.bridge_uart == 2);
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
assert(reader.prefs.rs232_bridge_enabled == uart_intent);
|
||||
#else
|
||||
assert(reader.prefs.bridge_uart == 0);
|
||||
assert(reader.prefs.rs232_bridge_enabled == primary);
|
||||
#endif
|
||||
assert(reader.prefs.bridge_baud == 57600 && reader.prefs.bridge_channel == 6);
|
||||
assert(strcmp(reader.prefs.node_name, writer.prefs.node_name) == 0);
|
||||
assert(strcmp(reader.prefs.bridge_secret, "test-secret") == 0);
|
||||
#else
|
||||
assert(reader.prefs.bridge_uart == 0 && reader.prefs.rs232_bridge_enabled == 0);
|
||||
#endif
|
||||
assert(reader.prefs.bridge_baud == 57600 && reader.prefs.bridge_channel == 6);
|
||||
assert(strcmp(reader.prefs.node_name, writer.prefs.node_name) == 0);
|
||||
assert(strcmp(reader.prefs.bridge_secret, "test-secret") == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
writer.prefs.bridge_enabled = 1;
|
||||
writer.prefs.espnow_bridge_enabled = 1;
|
||||
Capture on;
|
||||
assert(writeCommonPrefsImage(on, &writer.prefs));
|
||||
writer.prefs.rs232_bridge_enabled = 1;
|
||||
writer.savePrefs(&writer.fs, PrefsSaveRouting::Scope::Common);
|
||||
assert(writer._common_save_succeeded);
|
||||
const auto on = writer.fs.files.at("/com_prefs");
|
||||
|
||||
#ifdef RS232_BRIDGE_MERGED
|
||||
// Missing UART tails and current no-bridge zero sentinels both fail safe.
|
||||
for (bool missing_tail : {false, true}) {
|
||||
auto old = on.bytes;
|
||||
if (missing_tail) old.resize(uart_offset);
|
||||
else old[uart_offset] = 0;
|
||||
assert(old[enabled_offset] == 1);
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
// Unmarked previous MQTT files cannot prove UART intent, even when they
|
||||
// contain UART2 and primary enabled. Dedicated UART profiles retain context.
|
||||
for (size_t length : {uart_offset, esp_marker_offset, rs_intent_offset}) {
|
||||
auto old = on; old.resize(length);
|
||||
CommonCLI reader;
|
||||
reader.fs.files["/com_prefs"] = old;
|
||||
reader.loadPrefsInt(&reader.fs, "/com_prefs");
|
||||
assert(reader.prefs.bridge_enabled == 0 && reader.prefs.bridge_uart == 2);
|
||||
assert(reader._com_prefs_needs_upgrade);
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
assert(reader.prefs.bridge_enabled == 1 && reader.prefs.rs232_bridge_enabled == 0);
|
||||
#elif defined(RS232_BRIDGE_MERGED) && !defined(RS232_BRIDGE_DEFAULT_ON)
|
||||
const bool legacy_uart_present = length > uart_offset;
|
||||
assert(reader.prefs.bridge_enabled == legacy_uart_present);
|
||||
assert(reader.prefs.rs232_bridge_enabled == legacy_uart_present);
|
||||
#endif
|
||||
assert(reader.prefs.bridge_uart == 2 && reader._com_prefs_needs_upgrade);
|
||||
}
|
||||
// Older dedicated MKE builds selected Serial2 but persisted the UART1 fallback.
|
||||
auto dedicated = on.bytes;
|
||||
dedicated[uart_offset] = 1;
|
||||
// A partial new tail, invalid marker or invalid boolean must never fall back
|
||||
// to a different transport's primary setting. Short reads fail closed too.
|
||||
for (unsigned invalid = 0; invalid <= 255; ++invalid) {
|
||||
if (invalid == 0xB1) continue;
|
||||
CommonCLI reader;
|
||||
auto corrupt = on; corrupt[rs_marker_offset] = static_cast<uint8_t>(invalid);
|
||||
reader.fs.files["/com_prefs"] = corrupt;
|
||||
reader.loadPrefsInt(&reader.fs, "/com_prefs");
|
||||
assert(reader.prefs.rs232_bridge_enabled == 0 && reader._com_prefs_needs_upgrade);
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
assert(reader.prefs.bridge_enabled == 1);
|
||||
#else
|
||||
assert(reader.prefs.bridge_enabled == 0);
|
||||
#endif
|
||||
}
|
||||
for (unsigned invalid = 2; invalid <= 255; ++invalid) {
|
||||
CommonCLI reader;
|
||||
auto corrupt = on; corrupt[rs_intent_offset] = static_cast<uint8_t>(invalid);
|
||||
reader.fs.files["/com_prefs"] = corrupt;
|
||||
reader.loadPrefsInt(&reader.fs, "/com_prefs");
|
||||
assert(reader.prefs.rs232_bridge_enabled == 0);
|
||||
}
|
||||
for (size_t fault : {rs_intent_offset, rs_marker_offset}) {
|
||||
CommonCLI reader;
|
||||
reader.fs.files["/com_prefs"] = on;
|
||||
reader.fs.fail_read_after = static_cast<int>(fault);
|
||||
reader.loadPrefsInt(&reader.fs, "/com_prefs");
|
||||
assert(reader.prefs.rs232_bridge_enabled == 0);
|
||||
}
|
||||
CommonCLI torn;
|
||||
torn.fs.files["/com_prefs"] = {on.begin(), on.begin() + rs_marker_offset};
|
||||
torn.loadPrefsInt(&torn.fs, "/com_prefs");
|
||||
assert(torn.prefs.rs232_bridge_enabled == 0);
|
||||
#if defined(RS232_BRIDGE_MERGED) && !defined(WITH_MQTT_BRIDGE)
|
||||
// Old Serial2 builds saved the generic UART1 fallback; known UART2 remains
|
||||
// the safe resolved port, without losing a valid new independent intent.
|
||||
auto dedicated = on; dedicated[uart_offset] = 1;
|
||||
CommonCLI legacy_reader;
|
||||
legacy_reader.fs.files["/com_prefs"] = dedicated;
|
||||
legacy_reader.loadPrefsInt(&legacy_reader.fs, "/com_prefs");
|
||||
assert(legacy_reader.prefs.bridge_enabled == 1 && legacy_reader.prefs.bridge_uart == 2);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#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;
|
||||
old[marker_offset] = static_cast<uint8_t>(invalid);
|
||||
auto old = on; old[esp_marker_offset] = static_cast<uint8_t>(invalid);
|
||||
CommonCLI reader;
|
||||
reader.fs.files["/com_prefs"] = old;
|
||||
reader.loadPrefsInt(&reader.fs, "/com_prefs");
|
||||
@@ -107,30 +192,23 @@ int main() {
|
||||
#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;
|
||||
old.resize(marker_offset);
|
||||
auto old = on; old.resize(esp_marker_offset);
|
||||
CommonCLI reader;
|
||||
reader.fs.files["/com_prefs"] = old;
|
||||
reader.loadPrefsInt(&reader.fs, "/com_prefs");
|
||||
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.fs.files["/com_prefs"] = on;
|
||||
reader.fs.fail_read_after = static_cast<int>(esp_marker_offset);
|
||||
reader.loadPrefsInt(&reader.fs, "/com_prefs");
|
||||
assert(reader.prefs.espnow_bridge_enabled == 0);
|
||||
|
||||
// A torn append must retain the previous durable settings image.
|
||||
writer.savePrefs(&writer.fs, PrefsSaveRouting::Scope::Common);
|
||||
assert(writer._common_save_succeeded);
|
||||
const auto previous = writer.fs.files.at("/com_prefs");
|
||||
writer.fs.fail_write_after = static_cast<int>(marker_offset);
|
||||
writer.prefs.espnow_bridge_enabled = 0;
|
||||
assert(!writer.trySavePrefs());
|
||||
assert(writer.fs.files.at("/com_prefs") == previous);
|
||||
#endif
|
||||
// Every append boundary must commit atomically for observer and plain writers.
|
||||
for (size_t fault : {esp_marker_offset, rs_intent_offset, rs_marker_offset}) {
|
||||
const auto previous = writer.fs.files.at("/com_prefs");
|
||||
writer.fs.fail_write_after = static_cast<int>(fault);
|
||||
writer.prefs.rs232_bridge_enabled = 0;
|
||||
assert(!writer.trySavePrefs());
|
||||
assert(writer.fs.files.at("/com_prefs") == previous);
|
||||
writer.fs.fail_write_after = -1;
|
||||
}
|
||||
}
|
||||
@@ -341,6 +341,23 @@ require("RAK_4631_repeater", "build_flags", "WITH_RS232_BRIDGE_ALT=Serial1")
|
||||
require("RAK_4631_repeater", "build_flags", "WITH_RS232_BRIDGE_UART=2")
|
||||
reject("wio-e5_repeater", "build_flags", "WITH_RS232_BRIDGE=")
|
||||
|
||||
# Full source substitution must preserve the normal repeater UART rather
|
||||
# than retiring its legacy bridge while compiling an MQTT-only recipe.
|
||||
for env_name, rx, tx in (
|
||||
("Heltec_v3_repeater_observer_mqtt", 5, 6),
|
||||
("Heltec_WSL3_repeater_observer_mqtt", 5, 6),
|
||||
("RAK_3112_repeater_observer_mqtt", 5, 6),
|
||||
("LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_", 34, 25),
|
||||
):
|
||||
require(env_name, "build_flags", "WITH_MQTT_BRIDGE=1")
|
||||
require(env_name, "build_flags", "WITH_RS232_BRIDGE=Serial2")
|
||||
require(env_name, "build_flags", "WITH_RS232_BRIDGE_UART=2")
|
||||
require(env_name, "build_flags", f"WITH_RS232_BRIDGE_RX={rx}")
|
||||
require(env_name, "build_flags", f"WITH_RS232_BRIDGE_TX={tx}")
|
||||
require(env_name, "build_flags", "RS232_BRIDGE_MERGED=1")
|
||||
require(env_name, "build_src_filter", "helpers/bridges/RS232Bridge.cpp")
|
||||
reject(env_name, "build_flags", "RS232_BRIDGE_DEFAULT_ON")
|
||||
|
||||
# MKE keeps its ordinary role and UART wiring while exposing the existing
|
||||
# persistent bridge controls. The merged marker selects the disabled default;
|
||||
# the explicit legacy bridge remains enabled for deliberate direct builds.
|
||||
@@ -1606,6 +1623,19 @@ if is_redundant_bulk_build_target wio-e5-repeater_bridge_rs232; then
|
||||
fail "capacity-constrained Wio-E5 RS232 bridge was incorrectly merged"
|
||||
fi
|
||||
|
||||
(
|
||||
BUILD_PROFILE_FOR_TARGET=full
|
||||
for uart_target in Heltec_v3_repeater Heltec_WSL3_repeater RAK_3112_repeater \
|
||||
LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_; do
|
||||
BUILD_EXPECTATIONS=()
|
||||
declare_build_capability_contract "$uart_target" ESP32_PLATFORM
|
||||
[[ " ${BUILD_EXPECTATIONS[*]} " == *"bridge.rs232=_ZN11RS232Bridge5beginEv"* ]] \
|
||||
|| fail "$uart_target Full contract omitted the linked UART driver"
|
||||
[[ " ${BUILD_EXPECTATIONS[*]} " == *"bridge.espnow=_ZN12ESPNowBridge5beginEv"* ]] \
|
||||
|| fail "$uart_target Full contract omitted the linked ESP-NOW driver"
|
||||
done
|
||||
)
|
||||
|
||||
# A WiFi base's final -UENABLE_OTA must not win over the Full Companion's
|
||||
# source-only OTA overlay. That mismatch compiles out both the TCP terminal and
|
||||
# seeder while still leaving a superficially valid binary.
|
||||
|
||||
@@ -49,6 +49,7 @@ COMMON_QUERY_EXCEPTIONS = {
|
||||
'bootloader.ver': 'Installed bootloader identity',
|
||||
'bridge.running': 'Live bridge state; set bridge.enabled controls intent',
|
||||
'espnow.running': 'Live ESP-NOW bridge state; set espnow.enabled controls intent',
|
||||
'rs232.running': 'Live UART bridge state; set rs232.enabled controls intent',
|
||||
'bridge.type': 'Build-selected bridge transport',
|
||||
'password': 'Written by the password command, not set password',
|
||||
'public.key': 'Derived from identity; written through set prv.key',
|
||||
@@ -87,6 +88,10 @@ class CLISettingsContractTest(unittest.TestCase):
|
||||
'rs232_gps': ['WITH_BRIDGE', 'WITH_RS232_BRIDGE', 'ENV_INCLUDE_GPS'],
|
||||
'rs232_espnow': ['ESP_PLATFORM', 'ESP32_PLATFORM', 'WITH_BRIDGE',
|
||||
'WITH_RS232_BRIDGE', 'WITH_ESPNOW_BRIDGE'],
|
||||
'mqtt_rs232_espnow': ['ESP_PLATFORM', 'ESP32_PLATFORM', 'WITH_BRIDGE',
|
||||
'WITH_MQTT_BRIDGE', 'WITH_RS232_BRIDGE', 'WITH_ESPNOW_BRIDGE'],
|
||||
'mqtt_rs232': ['ESP_PLATFORM', 'ESP32_PLATFORM', 'WITH_BRIDGE',
|
||||
'WITH_MQTT_BRIDGE', 'WITH_RS232_BRIDGE'],
|
||||
'espnow': ['ESP_PLATFORM', 'ESP32_PLATFORM', 'WITH_BRIDGE', 'WITH_ESPNOW_BRIDGE', 'MESH_PRIMARY_ESPNOW'],
|
||||
'lr2021': ['USE_LR2021'],
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ int main() {
|
||||
auto original=cli.fs.files["/com_prefs"];
|
||||
// Appending preferences must not move the established preamble bytes.
|
||||
const size_t preamble_offset=866;
|
||||
assert(original.size()==874);
|
||||
assert(original.size()==877);
|
||||
assert(original[preamble_offset]==48 && original[preamble_offset+1]==0);
|
||||
assert(original[preamble_offset+2]==1);
|
||||
assert(original[preamble_offset+3]==0x50 && original[preamble_offset+4]==0x01);
|
||||
|
||||
@@ -159,7 +159,7 @@ int main() {
|
||||
cli.savePrefs(&cli.fs, PrefsSaveRouting::Scope::Common);
|
||||
assert(cli._common_save_succeeded);
|
||||
const auto baseline = cli.fs.files["/com_prefs"];
|
||||
assert(baseline.size() == 874);
|
||||
assert(baseline.size() == 877);
|
||||
const size_t debug_offset = 871, trace_offset = 872;
|
||||
const size_t logging_offset = 863, preamble_offset = 866;
|
||||
const size_t espnow_offset = 868, gps_offset = 869;
|
||||
|
||||
@@ -1284,6 +1284,40 @@ assert(!picker.runtimeDirections(staleBridgeMetadata.profiles.find(profile =>
|
||||
profile.target === bridgeTargets[0]), {mode: 'espnow'}).some(section => section.title === 'ESP-NOW bridge'));
|
||||
console.log('combined UART/ESP-NOW picker modes, independent commands and historical compatibility passed');
|
||||
|
||||
// MQTT, ESP-NOW and UART coexist in the same qualified Full artifact. UART
|
||||
// instructions must never use MQTT's historical ESP-NOW bridge alias.
|
||||
const tripleTargets = ['Heltec_v3_repeater', 'Heltec_WSL3_repeater',
|
||||
'RAK_3112_repeater', 'LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_']
|
||||
.map(target => target + '-full-usb-wifi');
|
||||
const tripleCatalog = picker.buildCatalog([
|
||||
release(bridgeFamily, '2026-10-05T00:00:00Z', tripleTargets.map(target =>
|
||||
asset(target + '-ota-' + bridgeFamily + '.bin'))),
|
||||
], {
|
||||
familyTag: bridgeFamily,
|
||||
profiles: Object.fromEntries(tripleTargets.map(target => [target, {
|
||||
platform: 'ESP32_PLATFORM', rs232: true, mqtt: true, espnowBridge: true,
|
||||
updateMethods: ['wifi', 'lora'],
|
||||
}])),
|
||||
});
|
||||
for (const profile of tripleCatalog.profiles) {
|
||||
assert.deepStrictEqual(picker.profileFieldValues(profile, 'mode'),
|
||||
['standard', 'rs232', 'espnow']);
|
||||
const directions = picker.runtimeDirections(profile, {mode: 'rs232'});
|
||||
const uart = directions.find(section => /^RS232 bridge/.test(section.title));
|
||||
const wireless = directions.find(section => section.title === 'ESP-NOW bridge');
|
||||
const mqtt = directions.find(section => section.title === 'MQTT broker connections');
|
||||
assert(uart && wireless && mqtt);
|
||||
assert.deepStrictEqual(uart.actions.map(action => action.commands), [
|
||||
['set rs232.enabled on', 'get rs232.running'], ['set rs232.enabled off'],
|
||||
['set rs232.enabled off', 'set bridge.baud 115200', 'set rs232.enabled on'],
|
||||
]);
|
||||
assert(commands([wireless]).includes('set espnow.enabled on'));
|
||||
assert(commands([mqtt]).includes('set mqtt.enabled on'));
|
||||
assert(!commands([uart, wireless, mqtt]).some(command => command.includes('bridge.enabled')));
|
||||
}
|
||||
assert.strictEqual(tripleCatalog.profiles.length, tripleTargets.length);
|
||||
console.log('three-transport Full picker uses independent UART, MQTT and ESP-NOW controls');
|
||||
|
||||
// Partition expansion uses the packager's lookup and published utility assets,
|
||||
// not hardware labels or guessed slugs (several historical names differ).
|
||||
const migrationFamily = currentControls.familyTag;
|
||||
|
||||
@@ -173,6 +173,21 @@ class FirmwareRamTest(unittest.TestCase):
|
||||
}, "v4_companion")
|
||||
self.assertEqual(default, expanded)
|
||||
|
||||
def test_combined_uart_reserves_heap_beside_mqtt_before_enable(self):
|
||||
defines = {"WITH_MQTT_BRIDGE": 1, "WIFI_OTA_SEEDER": 1}
|
||||
mqtt = ram.requirements("ESP32_PLATFORM", defines, "v3_repeater")
|
||||
combined = ram.requirements("ESP32_PLATFORM", {
|
||||
**defines, "WITH_RS232_BRIDGE": "Serial2", "RS232_BRIDGE_MERGED": 1,
|
||||
}, "v3_repeater")
|
||||
self.assertEqual(combined["required_heap_bytes"] - mqtt["required_heap_bytes"], 8192)
|
||||
self.assertEqual(combined["components"]["uart_bridge_and_driver"], 8192)
|
||||
# A selectable alternate port does not create two UART owners.
|
||||
alternate = ram.requirements("ESP32_PLATFORM", {
|
||||
**defines, "WITH_RS232_BRIDGE": "Serial2", "RS232_BRIDGE_MERGED": 1,
|
||||
"WITH_RS232_BRIDGE_ALT": "Serial1",
|
||||
}, "v3_repeater")
|
||||
self.assertEqual(alternate, combined)
|
||||
|
||||
def test_published_image_tables_match_elf_and_use_its_own_reservations(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
path = Path(temp) / "reference.elf"
|
||||
|
||||
@@ -20,6 +20,38 @@ def record(target: str, profile: str = "full", platform: str = "ESP32_PLATFORM",
|
||||
|
||||
|
||||
class ReleaseFullSelectionTest(unittest.TestCase):
|
||||
def test_full_uart_source_substitution_requires_linked_driver_proof(self):
|
||||
for base in ("Heltec_v3_repeater", "Heltec_WSL3_repeater", "RAK_3112_repeater",
|
||||
"LilyGo_TLora_V2_1_1_6_repeater"):
|
||||
for suffix in ("", "_observer_mqtt", "_observer_mqtt_"):
|
||||
for fault in ("missing_capability", "missing_proof", "false_proof", "unverified"):
|
||||
image = record(base + suffix, capabilities=("bridge.rs232", "bridge.espnow"))
|
||||
manifest = image["manifest"]
|
||||
if fault == "missing_capability":
|
||||
manifest["capabilities"].remove("bridge.rs232")
|
||||
elif fault == "missing_proof":
|
||||
manifest["verification"] = []
|
||||
elif fault == "false_proof":
|
||||
manifest["verification"][0]["present"] = False
|
||||
else:
|
||||
manifest["verified"] = False
|
||||
with self.subTest(base=base, suffix=suffix, fault=fault):
|
||||
with self.assertRaisesRegex(ValueError, "verified RS232 bridge"):
|
||||
select_ordinary_full_records([image])
|
||||
|
||||
def test_new_combined_full_displaces_stale_plain_and_both_dedicated_bridges(self):
|
||||
for base in ("Heltec_v3_repeater", "Heltec_WSL3_repeater", "RAK_3112_repeater",
|
||||
"LilyGo_TLora_V2_1_1_6_repeater"):
|
||||
stale = record(base, capabilities=("bridge.espnow",))
|
||||
primary = record(base + "_observer_mqtt_",
|
||||
capabilities=("bridge.rs232", "bridge.espnow"))
|
||||
for profile in ("standard", "full"):
|
||||
old = [record(base + "_bridge_" + bridge, profile)
|
||||
for bridge in ("rs232", "espnow")]
|
||||
for inputs in ([stale, primary] + old, old + [primary, stale]):
|
||||
with self.subTest(base=base, profile=profile):
|
||||
self.assertEqual(select_ordinary_full_records(inputs), [primary])
|
||||
|
||||
def test_meshtower_sd_primary_filters_stale_internal_images_without_relabeling(self):
|
||||
primary = "Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors"
|
||||
variants = []
|
||||
@@ -63,7 +95,7 @@ class ReleaseFullSelectionTest(unittest.TestCase):
|
||||
inputs = [
|
||||
record("Heltec_v3_repeater_observer_mqtt"),
|
||||
record("Heltec_v3_repeater_bridge_espnow"),
|
||||
record("Heltec_v3_repeater", capabilities=("bridge.espnow",)),
|
||||
record("Heltec_v3_repeater", capabilities=("bridge.espnow", "bridge.rs232")),
|
||||
record("Heltec_v3_repeater", "standard"),
|
||||
record("Heltec_v3_room_server"),
|
||||
record("Station_G2_repeater"),
|
||||
@@ -71,7 +103,7 @@ class ReleaseFullSelectionTest(unittest.TestCase):
|
||||
record("Tbeam_SX1262_repeater_bridge_espnow"),
|
||||
record("Tbeam_SX1262_repeater_observer_mqtt", capabilities=("bridge.espnow",)),
|
||||
record("LilyGo_TLora_V2_1_1_6_repeater"),
|
||||
record("LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_"),
|
||||
record("LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_", capabilities=("bridge.rs232",)),
|
||||
record("Meshadventurer_sx1262_repeater"),
|
||||
record("Meshadventurer_sx1262_repeater_bridge_espnow"),
|
||||
record("RAK_4631_repeater", platform="NRF52_PLATFORM"),
|
||||
@@ -158,7 +190,7 @@ class ReleaseFullSelectionTest(unittest.TestCase):
|
||||
self.assertEqual(select_ordinary_full_records(ordered), ordered)
|
||||
|
||||
def test_qualified_combined_plain_image_replaces_only_its_matching_espnow_image(self):
|
||||
primary = record("Heltec_v3_repeater", capabilities=("bridge.espnow",))
|
||||
primary = record("Heltec_v3_repeater", capabilities=("bridge.espnow", "bridge.rs232"))
|
||||
other_board = record("heltec_v4_tft_repeater_bridge_espnow", capabilities=("bridge.espnow",))
|
||||
for profile in ("full", "standard"):
|
||||
legacy = record("Heltec_v3_repeater_bridge_espnow", profile, capabilities=("bridge.espnow",))
|
||||
@@ -221,7 +253,7 @@ class ReleaseFullSelectionTest(unittest.TestCase):
|
||||
|
||||
def test_combined_observer_with_trailing_underscore_replaces_matching_espnow(self):
|
||||
observer = record("LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_",
|
||||
capabilities=("bridge.espnow",))
|
||||
capabilities=("bridge.espnow", "bridge.rs232"))
|
||||
legacy = record("LilyGo_TLora_V2_1_1_6_repeater_bridge_espnow",
|
||||
capabilities=("bridge.espnow",))
|
||||
for inputs in ([observer, legacy], [legacy, observer]):
|
||||
|
||||
@@ -398,7 +398,7 @@ class Nrf52VariantContractsTest(unittest.TestCase):
|
||||
cli = (ROOT / "src/helpers/CommonCLI.cpp").read_text()
|
||||
|
||||
self.assertIn("bool isBridgeRunning() const override", header)
|
||||
self.assertIn("if (isBridgeRunning()) reply_data[8] |= 0x01", implementation)
|
||||
self.assertIn("if (isRs232BridgeRunning()) reply_data[8] |= 0x01", implementation)
|
||||
self.assertIn("if (isBridgeRunning()) reply_data[8] |= 0x03", implementation)
|
||||
boot_failure = implementation[
|
||||
implementation.index("if (!bridge || !beginRS232Bridge())"):
|
||||
@@ -406,7 +406,7 @@ class Nrf52VariantContractsTest(unittest.TestCase):
|
||||
"if (!bridge || !beginRS232Bridge())"
|
||||
))
|
||||
]
|
||||
self.assertIn("setBridgeState(false)", boot_failure)
|
||||
self.assertIn("setRs232BridgeState(false)", boot_failure)
|
||||
self.assertNotIn("_prefs.bridge_enabled", boot_failure)
|
||||
self.assertIn("parseOnOffStrict(&config[15], enable)", cli)
|
||||
self.assertIn("parseUnsignedIntegerStrict(&config[12], baud)", cli)
|
||||
@@ -417,7 +417,7 @@ class Nrf52VariantContractsTest(unittest.TestCase):
|
||||
self.assertIn('_prefs->bridge_pkt_src ? "rx" : "tx"', cli)
|
||||
self.assertIn("!defined(RS232_BRIDGE_DEFAULT_ON)", cli)
|
||||
|
||||
def test_default_on_rs232_profile_skips_default_off_tail_migration(self) -> None:
|
||||
def test_default_on_rs232_profile_retains_legacy_alias_and_checks_new_intent(self) -> None:
|
||||
cli = (ROOT / "src/helpers/CommonCLI.cpp").read_text()
|
||||
xiao = (ROOT / "variants/xiao_nrf52/platformio.ini").read_text()
|
||||
dedicated = ini_section(
|
||||
@@ -426,17 +426,18 @@ class Nrf52VariantContractsTest(unittest.TestCase):
|
||||
|
||||
self.assertIn("extends = env:solarxiao_30S_repeater", dedicated)
|
||||
self.assertRegex(dedicated, r"-D\s+RS232_BRIDGE_DEFAULT_ON=1\b")
|
||||
guard = (
|
||||
"#if defined(WITH_RS232_BRIDGE) && defined(RS232_BRIDGE_MERGED) \\\n"
|
||||
" && !defined(RS232_BRIDGE_DEFAULT_ON)"
|
||||
)
|
||||
# Declaration, persisted-tail detection, and migration must share the
|
||||
# same guard. A broader use breaks dedicated default-on bridge builds.
|
||||
# Dedicated compatibility profiles retain their old primary UART alias;
|
||||
# every current UART profile checks the independent marker before a
|
||||
# newer MQTT image can donate intent. Executable nRF52/default-on and
|
||||
# cross-profile round trips are covered by test_rs232_runtime.py.
|
||||
loader_start = cli.index("void CommonCLI::loadPrefsInt(")
|
||||
loader_end = cli.index("static bool writeCommonPrefsImage(", loader_start)
|
||||
binary_loader = cli[loader_start:loader_end]
|
||||
self.assertEqual(binary_loader.count(guard), 3)
|
||||
self.assertEqual(binary_loader.count("has_runtime_bridge_uart"), 3)
|
||||
self.assertTrue("has_runtime_rs232_intent = rs232_profile == 0xB1" in binary_loader)
|
||||
self.assertTrue("&& rs232_intent <= 1" in binary_loader)
|
||||
self.assertTrue("if (!has_runtime_rs232_intent)" in binary_loader)
|
||||
self.assertTrue("legacy_uart_intent" in binary_loader)
|
||||
self.assertTrue("_prefs->bridge_enabled = _prefs->rs232_bridge_enabled;" in binary_loader)
|
||||
|
||||
def test_gat562_30s_gps_enable_and_buzzer_pins_are_distinct(self) -> None:
|
||||
recipe = (
|
||||
|
||||
@@ -66,7 +66,10 @@ void testRoundTripsAndLayout() {
|
||||
const size_t channel_offset = original.offset(&cli.prefs.ota_channel);
|
||||
const size_t trace_offset = original.offset(&cli.prefs.trace_when_repeat_off);
|
||||
assert(channel_offset == trace_offset + sizeof(cli.prefs.trace_when_repeat_off));
|
||||
assert(channel_offset + sizeof(cli.prefs.ota_channel) == original.bytes.size());
|
||||
assert(channel_offset + sizeof(cli.prefs.ota_channel) + 3 == original.bytes.size());
|
||||
assert(original.bytes[channel_offset + 1] == 0); // reserved ESP-NOW profile
|
||||
assert(original.offset(&cli.prefs.rs232_bridge_enabled) == channel_offset + 2);
|
||||
assert(original.bytes[channel_offset + 2] == 0 && original.bytes.back() == 0);
|
||||
// The established common core remains at its public file offsets.
|
||||
assert(original.offset(cli.prefs.node_name) == 4);
|
||||
assert(original.offset(&cli.prefs.freq) == 72);
|
||||
|
||||
@@ -21,7 +21,7 @@ int main() {
|
||||
cli.savePrefs(&cli.fs, PrefsSaveRouting::Scope::Common);
|
||||
assert(cli._common_save_succeeded);
|
||||
const auto baseline = cli.fs.files["/com_prefs"];
|
||||
assert(baseline.size() == 874);
|
||||
assert(baseline.size() == 877);
|
||||
const size_t trace_offset = 872, debug_offset = 871, gps_offset = 869;
|
||||
assert(baseline[trace_offset] == 0 && baseline[debug_offset] == 1);
|
||||
assert(baseline[866] == 48 && baseline[867] == 0 && baseline[868] == 1);
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute the production ESP32 UART begin/end and bridge state checks."""
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_replay_reset_integration import extract_braced
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
FIXTURE = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#define ESP32 1
|
||||
#define BRIDGE_DEBUG_PRINTLN(...) ((void)0)
|
||||
struct NodePrefs { uint32_t bridge_baud = 57600; };
|
||||
struct Stream { virtual ~Stream() = default; };
|
||||
struct HardwareSerial : Stream {
|
||||
bool pins_ok = true, begin_ok = true, installed = false;
|
||||
unsigned pin_calls = 0, begin_calls = 0, end_calls = 0;
|
||||
int16_t rx = -1, tx = -1;
|
||||
uint32_t baud = 0;
|
||||
bool setPins(int16_t selected_rx, int16_t selected_tx) {
|
||||
++pin_calls; rx = selected_rx; tx = selected_tx; return pins_ok;
|
||||
}
|
||||
void begin(uint32_t selected_baud) { ++begin_calls; baud = selected_baud; installed = begin_ok; }
|
||||
explicit operator bool() const { return installed; }
|
||||
void end() { ++end_calls; installed = false; }
|
||||
};
|
||||
struct BridgeBase {
|
||||
bool _initialized = false;
|
||||
@RUNNING@
|
||||
};
|
||||
struct RS232Bridge : BridgeBase {
|
||||
NodePrefs* _prefs;
|
||||
Stream* _serial;
|
||||
int16_t _rx_pin, _tx_pin;
|
||||
RS232Bridge(NodePrefs& prefs, HardwareSerial& serial)
|
||||
: _prefs(&prefs), _serial(&serial), _rx_pin(5), _tx_pin(6) {}
|
||||
void begin();
|
||||
void end();
|
||||
};
|
||||
@BEGIN@
|
||||
@END@
|
||||
int main() {
|
||||
NodePrefs prefs;
|
||||
HardwareSerial serial;
|
||||
RS232Bridge bridge(prefs, serial);
|
||||
assert(!bridge.isRunning());
|
||||
serial.pins_ok = false;
|
||||
bridge.begin();
|
||||
assert(!bridge.isRunning() && serial.pin_calls == 1 && serial.begin_calls == 0);
|
||||
serial.pins_ok = true; serial.begin_ok = false;
|
||||
bridge.begin();
|
||||
assert(!bridge.isRunning() && !serial.installed && serial.begin_calls == 1);
|
||||
serial.begin_ok = true;
|
||||
bridge.begin();
|
||||
assert(bridge.isRunning() && serial.installed && serial.begin_calls == 2);
|
||||
assert(serial.rx == 5 && serial.tx == 6 && serial.baud == 57600);
|
||||
bridge.end();
|
||||
assert(!bridge.isRunning() && !serial.installed && serial.end_calls == 1);
|
||||
// A failed restart must clear its former state, and a later retry can recover.
|
||||
bridge.begin(); assert(bridge.isRunning());
|
||||
serial.pins_ok = false;
|
||||
bridge.begin(); assert(!bridge.isRunning());
|
||||
bridge.end(); serial.pins_ok = true; prefs.bridge_baud = 19200;
|
||||
bridge.begin(); assert(bridge.isRunning() && serial.baud == 19200);
|
||||
for (unsigned cycle = 0; cycle < 100; ++cycle) {
|
||||
bridge.end(); assert(!bridge.isRunning());
|
||||
bridge.begin(); assert(bridge.isRunning());
|
||||
}
|
||||
bridge.end(); assert(!bridge.isRunning());
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class RS232DriverLifecycleTests(unittest.TestCase):
|
||||
def test_failed_pins_or_driver_start_never_report_running_and_retry_recovers(self):
|
||||
compiler = shutil.which("g++") or shutil.which("clang++")
|
||||
self.assertIsNotNone(compiler, "a host C++ compiler is required")
|
||||
driver = (ROOT / "src/helpers/bridges/RS232Bridge.cpp").read_text(encoding="ascii")
|
||||
base = (ROOT / "src/helpers/bridges/BridgeBase.cpp").read_text(encoding="ascii")
|
||||
code = FIXTURE.replace("@RUNNING@", extract_braced(base, "bool BridgeBase::isRunning() const")
|
||||
.replace("BridgeBase::", "").replace(" override", ""))
|
||||
code = code.replace("@BEGIN@", extract_braced(driver, "void RS232Bridge::begin()"))
|
||||
code = code.replace("@END@", extract_braced(driver, "void RS232Bridge::end()"))
|
||||
with tempfile.TemporaryDirectory(prefix="rs232-driver-lifecycle-") as directory:
|
||||
work = Path(directory)
|
||||
source, binary = work / "main.cpp", work / "test"
|
||||
source.write_text(code, encoding="ascii")
|
||||
built = subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
||||
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
|
||||
str(source), "-o", str(binary)], capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
||||
checked = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
|
||||
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute real UART/MQTT/ESP-NOW repeater lifecycle and CLI independently."""
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_replay_reset_integration import extract_braced
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class RS232MQTTRuntimeTests(unittest.TestCase):
|
||||
def test_independent_transports_route_retry_restart_and_survive_browser_ota(self):
|
||||
compiler = shutil.which("g++") or shutil.which("clang++")
|
||||
self.assertIsNotNone(compiler, "a host C++ compiler is required")
|
||||
cli = (ROOT / "src/helpers/CommonCLI.cpp").read_text(encoding="ascii")
|
||||
observer = (ROOT / "src/helpers/CommonCLI_Observer.cpp").read_text(encoding="ascii")
|
||||
header = (ROOT / "examples/simple_repeater/MyMesh.h").read_text(encoding="ascii")
|
||||
implementation = (ROOT / "examples/simple_repeater/MyMesh.cpp").read_text(encoding="ascii")
|
||||
flags = ["-DWITH_RS232_BRIDGE=Serial2", "-DWITH_RS232_BRIDGE_UART=2",
|
||||
"-DWITH_RS232_BRIDGE_RX=5", "-DWITH_RS232_BRIDGE_TX=6",
|
||||
"-DRS232_BRIDGE_MERGED=1", "-DWITH_MQTT_BRIDGE=1", "-DWITH_ESPNOW_BRIDGE=1", "-DWITH_BRIDGE=1",
|
||||
"-DENV_INCLUDE_GPS=1", "-DLIGHTWEIGHT_WIFI_OTA=1", "-DMESH_ENABLE_FLOOD_RULE_ENGINE=1"]
|
||||
def preprocess(source, extra=()):
|
||||
return subprocess.run([compiler, "-E", "-P", "-x", "c++", *flags, *extra, "-"],
|
||||
input=source, capture_output=True, text=True, check=True).stdout
|
||||
processed_header = preprocess(re.sub(r'^\s*#include[^\n]*', '', header, flags=re.M))
|
||||
lifecycle = "\n".join(extract_braced(processed_header, signature) for signature in (
|
||||
"AbstractBridge* activeBridge() {", "const AbstractBridge* activeBridge() const {",
|
||||
"RS232Bridge* createRS232Bridge()", "bool beginRS232Bridge()", "bool endRS232Bridge()",
|
||||
"bool isBridgeRunning() const override", "bool rs232BridgeEnabled() const", "bool isRs232BridgeRunning()",
|
||||
"bool setRs232BridgeState(bool enable)", "bool restartRs232Bridge()",
|
||||
"bool startSharedEspNowBridgeIfReady()", "bool isEspNowBridgeRunning()",
|
||||
"bool setEspNowBridgeState(bool enable)", "bool restartEspNowBridge()",
|
||||
"bool isMqttBridgeRunning()", "bool setMqttBridgeState(bool enable)",
|
||||
"bool restartMqttBridge()", "bool setBridgeState(bool enable)", "bool restartBridge()",
|
||||
))
|
||||
defaults_start = implementation.index("// bridge defaults")
|
||||
defaults_end = implementation.index("// GPS defaults", defaults_start)
|
||||
boot_start = implementation.index('mesh::hilStartupTrace("mesh_bridge_begin");')
|
||||
boot_start = implementation.index("#if defined(WITH_BRIDGE)", boot_start)
|
||||
boot_end = implementation.index('mesh::hilStartupTrace("mesh_bridge_ready");', boot_start)
|
||||
routing = {}
|
||||
for name in ("logRx", "logTx"):
|
||||
start = implementation.index("void MyMesh::" + name + "(")
|
||||
start = implementation.index("#ifdef WITH_MQTT_BRIDGE\n", start)
|
||||
end = implementation.index(" if (_logging)", start)
|
||||
routing[name] = implementation[start:end]
|
||||
service_start = implementation.index("#if defined(WITH_ESPNOW_BRIDGE)",
|
||||
implementation.index("MyMesh::servicePostMeshLoop()"))
|
||||
service_end = implementation.index(" if (next_flood_advert", service_start)
|
||||
pending_start = implementation.index("#if defined(WITH_BRIDGE)",
|
||||
implementation.index("bool MyMesh::hasPendingWork()"))
|
||||
pending_end = implementation.index(" if (radio_driver.isWatchdogObserving()", pending_start)
|
||||
commands = preprocess("\n".join(extract_braced(cli, signature) for signature in (
|
||||
"void CommonCLI::handleSetCmd(", "void CommonCLI::handleGetCmd(")))
|
||||
sets = "\nelse ".join(extract_braced(commands, signature) for signature in (
|
||||
'if (strncmp(config, "mqtt.enabled ", 13) == 0)',
|
||||
'if (strncmp(config, "rs232.enabled ", 14) == 0)',
|
||||
'if (memcmp(config, "espnow.enabled ", 15) == 0)',
|
||||
'if (memcmp(config, "bridge.enabled ", 15) == 0)',
|
||||
'if (memcmp(config, "bridge.baud ", 12) == 0)',
|
||||
'if (memcmp(config, "bridge.uart ", 12) == 0)',
|
||||
'if (memcmp(config, "bridge.channel ", 15) == 0)',
|
||||
'if (memcmp(config, "bridge.secret ", 14) == 0)',
|
||||
'if (memcmp(config, "bridge.format ", 14) == 0)',
|
||||
))
|
||||
gets = "\nelse ".join(extract_braced(commands, signature) for signature in (
|
||||
'if (configKeyEquals(config, "bridge.type"))',
|
||||
'if (configKeyEquals(config, "rs232.enabled"))',
|
||||
'if (configKeyEquals(config, "rs232.running"))',
|
||||
'if (configKeyEquals(config, "bridge.enabled"))',
|
||||
'if (configKeyEquals(config, "bridge.running"))',
|
||||
'if (configKeyEquals(config, "espnow.enabled"))',
|
||||
'if (configKeyEquals(config, "espnow.running"))',
|
||||
'if (configKeyEquals(config, "bridge.baud"))',
|
||||
'if (configKeyEquals(config, "bridge.uart"))',
|
||||
))
|
||||
gets += "\nelse " + "\nelse ".join(extract_braced(observer, signature) for signature in (
|
||||
'if (strcmp(config, "mqtt.enabled") == 0)', 'if (strcmp(config, "mqtt.running") == 0)'))
|
||||
ota_start = cli.index(' } else if (memcmp(command, "start ota", 9)')
|
||||
ota_end = cli.index(' } else if (memcmp(command, "clock", 5)', ota_start)
|
||||
code = (ROOT / "test/fixtures/rs232_mqtt_runtime.cpp").read_text(encoding="ascii")
|
||||
for key, value in {
|
||||
"LIFECYCLE": lifecycle, "DEFAULTS": implementation[defaults_start:defaults_end],
|
||||
"FILTER": extract_braced(header, "void configureBridgeFilter(AbstractBridge* active_bridge)"),
|
||||
"BOOT": implementation[boot_start:boot_end], "ROUTE_RX": routing["logRx"],
|
||||
"ROUTE_TX": routing["logTx"], "SERVICE": implementation[service_start:service_end],
|
||||
"PENDING": implementation[pending_start:pending_end], "SET_BRANCHES": sets,
|
||||
"GET_BRANCHES": gets, "OTA": cli[ota_start:ota_end],
|
||||
"PARSERS": "\n".join(extract_braced(cli, signature) for signature in (
|
||||
"static bool parseOnOffStrict(", "static bool configKeyEquals(")),
|
||||
}.items():
|
||||
code = code.replace("@" + key + "@", value)
|
||||
with tempfile.TemporaryDirectory(prefix="rs232-mqtt-runtime-") as temporary:
|
||||
work = Path(temporary)
|
||||
source, binary = work / "main.cpp", work / "test"
|
||||
source.write_text(code, encoding="ascii")
|
||||
result = subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
||||
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
|
||||
*flags, "-I" + str(ROOT / "src"), str(source), "-o", str(binary)],
|
||||
capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn("Three transport runtime checks passed", result.stdout)
|
||||
|
||||
def test_uart_and_mqtt_without_espnow_preserve_primary_mqtt_alias(self):
|
||||
compiler = shutil.which("g++") or shutil.which("clang++")
|
||||
self.assertIsNotNone(compiler, "a host C++ compiler is required")
|
||||
cli = (ROOT / "src/helpers/CommonCLI.cpp").read_text(encoding="ascii")
|
||||
callback_header = (ROOT / "src/helpers/CommonCLI.h").read_text(encoding="ascii")
|
||||
header = (ROOT / "examples/simple_repeater/MyMesh.h").read_text(encoding="ascii")
|
||||
implementation = (ROOT / "examples/simple_repeater/MyMesh.cpp").read_text(encoding="ascii")
|
||||
flags = ["-DWITH_RS232_BRIDGE=Serial2", "-DWITH_RS232_BRIDGE_UART=2",
|
||||
"-DWITH_RS232_BRIDGE_RX=5", "-DWITH_RS232_BRIDGE_TX=6", "-DRS232_BRIDGE_MERGED=1",
|
||||
"-DWITH_MQTT_BRIDGE=1", "-DWITH_BRIDGE=1", "-DENV_INCLUDE_GPS=1",
|
||||
"-DLIGHTWEIGHT_WIFI_OTA=1", "-DMESH_ENABLE_FLOOD_RULE_ENGINE=1"]
|
||||
def preprocess(source):
|
||||
return subprocess.run([compiler, "-E", "-P", "-x", "c++", *flags, "-"],
|
||||
input=source, capture_output=True, text=True, check=True).stdout
|
||||
processed_header = preprocess(re.sub(r'^\s*#include[^\n]*', '', header, flags=re.M))
|
||||
lifecycle = "\n".join(extract_braced(processed_header, signature) for signature in (
|
||||
"AbstractBridge* activeBridge() {", "const AbstractBridge* activeBridge() const {",
|
||||
"RS232Bridge* createRS232Bridge()", "bool beginRS232Bridge()", "bool endRS232Bridge()",
|
||||
"bool rs232BridgeEnabled() const", "bool isBridgeRunning() const override",
|
||||
"bool isRs232BridgeRunning()", "bool setRs232BridgeState(bool enable)",
|
||||
"bool restartRs232Bridge()", "bool isMqttBridgeRunning()", "bool setBridgeState(bool enable)",
|
||||
"bool restartBridge()"))
|
||||
code = (ROOT / "test/fixtures/rs232_mqtt_runtime.cpp").read_text(encoding="ascii")
|
||||
code = code[:code.index("int main() {")]
|
||||
for signature in ("virtual bool isEspNowBridgeRunning() = 0;",
|
||||
"virtual bool setEspNowBridgeState(bool) = 0;",
|
||||
"virtual bool restartEspNowBridge() = 0;"):
|
||||
code = code.replace(signature, "")
|
||||
for short, full in (("virtual bool setMqttBridgeState(bool) = 0;", "virtual bool setMqttBridgeState(bool enable)"),
|
||||
("virtual bool restartMqttBridge() = 0;", "virtual bool restartMqttBridge()")):
|
||||
code = code.replace(short, extract_braced(callback_header, full))
|
||||
commands = preprocess("\n".join(extract_braced(cli, signature) for signature in (
|
||||
"void CommonCLI::handleSetCmd(", "void CommonCLI::handleGetCmd(")))
|
||||
boot_start = implementation.index('mesh::hilStartupTrace("mesh_bridge_begin");')
|
||||
boot_start = implementation.index("#if defined(WITH_BRIDGE)", boot_start)
|
||||
boot_end = implementation.index('mesh::hilStartupTrace("mesh_bridge_ready");', boot_start)
|
||||
service_start = implementation.index("#if defined(WITH_ESPNOW_BRIDGE)", implementation.index("MyMesh::servicePostMeshLoop()"))
|
||||
service_end = implementation.index(" if (next_flood_advert", service_start)
|
||||
pending_start = implementation.index("#if defined(WITH_BRIDGE)", implementation.index("bool MyMesh::hasPendingWork()"))
|
||||
pending_end = implementation.index(" if (radio_driver.isWatchdogObserving()", pending_start)
|
||||
route = {}
|
||||
for name in ("logRx", "logTx"):
|
||||
start = implementation.index("#ifdef WITH_MQTT_BRIDGE\n", implementation.index("void MyMesh::" + name + "("))
|
||||
route[name] = implementation[start:implementation.index(" if (_logging)", start)]
|
||||
ota_start = cli.index(' } else if (memcmp(command, "start ota", 9)')
|
||||
defaults_start = implementation.index("// bridge defaults")
|
||||
substitutions = {
|
||||
"LIFECYCLE": lifecycle, "BOOT": implementation[boot_start:boot_end],
|
||||
"DEFAULTS": implementation[defaults_start:implementation.index("// GPS defaults", defaults_start)],
|
||||
"FILTER": extract_braced(header, "void configureBridgeFilter(AbstractBridge* active_bridge)"),
|
||||
"ROUTE_RX": route["logRx"], "ROUTE_TX": route["logTx"],
|
||||
"SERVICE": implementation[service_start:service_end], "PENDING": implementation[pending_start:pending_end],
|
||||
"SET_BRANCHES": "\nelse ".join(extract_braced(commands, signature) for signature in (
|
||||
'if (strncmp(config, "mqtt.enabled ", 13) == 0)', 'if (strncmp(config, "rs232.enabled ", 14) == 0)',
|
||||
'if (memcmp(config, "bridge.enabled ", 15) == 0)', 'if (memcmp(config, "bridge.baud ", 12) == 0)',
|
||||
'if (memcmp(config, "bridge.uart ", 12) == 0)')),
|
||||
"GET_BRANCHES": extract_braced(commands, 'if (configKeyEquals(config, "bridge.type"))'),
|
||||
"PARSERS": "\n".join(extract_braced(cli, signature) for signature in (
|
||||
"static bool parseOnOffStrict(", "static bool configKeyEquals(")),
|
||||
"OTA": cli[ota_start:cli.index(' } else if (memcmp(command, "clock", 5)', ota_start)],
|
||||
}
|
||||
for key, value in substitutions.items():
|
||||
code = code.replace("@" + key + "@", value)
|
||||
code += r'''int main() {
|
||||
assert(millisHasNowPassed(millis()));
|
||||
MyMesh mesh; mesh.defaults(); mesh.boot(); CommonCLI cli(mesh);
|
||||
expect_get(cli, "bridge.type", "> mqtt+rs232");
|
||||
assert(mesh.isMqttBridgeRunning() && !mesh.isRs232BridgeRunning());
|
||||
char reply[256] = {};
|
||||
cli.set("rs232.enabled on", reply); assert(strcmp(reply, "OK") == 0);
|
||||
assert(mesh.isRs232BridgeRunning() && mesh.isMqttBridgeRunning());
|
||||
const unsigned starts = mesh.mqtt_bridge->begins;
|
||||
cli.set("bridge.baud 57600", reply); assert(strcmp(reply, "OK") == 0);
|
||||
assert(mesh.isRs232BridgeRunning() && mesh.mqtt_bridge->begins == starts);
|
||||
cli.set("bridge.enabled off", reply); assert(strcmp(reply, "OK") == 0);
|
||||
assert(!mesh._prefs.bridge_enabled && mesh._prefs.rs232_bridge_enabled);
|
||||
assert(!mesh.isMqttBridgeRunning() && mesh.isRs232BridgeRunning());
|
||||
assert(mesh.isBridgeRunning() && mesh.bridgesPreventSleep());
|
||||
cli.set("bridge.enabled on", reply); assert(strcmp(reply, "OK") == 0);
|
||||
assert(mesh.isMqttBridgeRunning() && mesh.isRs232BridgeRunning());
|
||||
mesh::Packet packet;
|
||||
mesh.routeRx(&packet); mesh.routeTx(&packet); mesh.serviceBridges();
|
||||
assert(mesh.bridge->sent == 1 && mesh.bridge->loops == 1);
|
||||
assert(mesh.mqtt_bridge->received == 1 && mesh.mqtt_bridge->sent == 1);
|
||||
assert(mesh.filter_checks == 3);
|
||||
const unsigned uart_starts = Serial2.begins;
|
||||
cli.run("start ota ap", reply);
|
||||
assert(mesh._cli.board.ota && !mesh.isMqttBridgeRunning() && mesh.isRs232BridgeRunning());
|
||||
cli.run("stop ota", reply);
|
||||
assert(!mesh._cli.board.ota && mesh.isMqttBridgeRunning() && Serial2.begins == uart_starts);
|
||||
cli.set("rs232.enabled off", reply); cli.set("mqtt.enabled off", reply);
|
||||
assert(!mesh.bridge && !mesh.isMqttBridgeRunning() && !mesh.bridgesPreventSleep());
|
||||
cli.set("rs232.enabled on", reply); mesh.mqtt_bridge->fail_starts = 1;
|
||||
cli.set("mqtt.enabled on", reply);
|
||||
assert(strstr(reply, "setting saved") && !mesh.isMqttBridgeRunning());
|
||||
assert(mesh.isRs232BridgeRunning() && mesh.bridgesPreventSleep());
|
||||
cli.set("mqtt.enabled on", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && mesh.isMqttBridgeRunning());
|
||||
}'''
|
||||
with tempfile.TemporaryDirectory(prefix="rs232-mqtt-only-") as directory:
|
||||
work = Path(directory)
|
||||
source, binary = work / "main.cpp", work / "test"
|
||||
source.write_text(code, encoding="ascii")
|
||||
built = subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
||||
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
|
||||
*flags, "-I" + str(ROOT / "src"), str(source), "-o", str(binary)],
|
||||
capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
||||
checked = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
|
||||
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+86
-14
@@ -35,6 +35,11 @@ class RS232RuntimeTests(unittest.TestCase):
|
||||
("merged_composite", ["WITH_RS232_BRIDGE=Serial2", "WITH_RS232_BRIDGE_UART=2",
|
||||
"RS232_BRIDGE_MERGED=1", "WITH_ESPNOW_BRIDGE=1",
|
||||
"ESPNOW_BRIDGE_MERGED=1"]),
|
||||
("mqtt_rs232", ["WITH_RS232_BRIDGE=Serial2", "WITH_RS232_BRIDGE_UART=2",
|
||||
"RS232_BRIDGE_MERGED=1", "WITH_MQTT_BRIDGE=1"]),
|
||||
("mqtt_rs232_espnow", ["WITH_RS232_BRIDGE=Serial2", "WITH_RS232_BRIDGE_UART=2",
|
||||
"RS232_BRIDGE_MERGED=1", "WITH_MQTT_BRIDGE=1",
|
||||
"WITH_ESPNOW_BRIDGE=1"]),
|
||||
("sole_espnow_merged", ["WITH_ESPNOW_BRIDGE=1", "ESPNOW_BRIDGE_MERGED=1"]),
|
||||
("sole_espnow_dedicated", ["WITH_ESPNOW_BRIDGE=1"]),
|
||||
("sole_espnow_default_on", ["WITH_ESPNOW_BRIDGE=1", "ESPNOW_BRIDGE_MERGED=1",
|
||||
@@ -71,11 +76,21 @@ class RS232RuntimeTests(unittest.TestCase):
|
||||
code += (ROOT / "test/fixtures/rs232_runtime/prefs.cpp").read_text(encoding="ascii")
|
||||
for profile, macros in (
|
||||
("ordinary_unmerged", []),
|
||||
("rs232_dedicated", ["WITH_RS232_BRIDGE=Serial2", "WITH_RS232_BRIDGE_UART=2"]),
|
||||
("rs232_default_on", ["WITH_RS232_BRIDGE=Serial2", "WITH_RS232_BRIDGE_UART=2",
|
||||
"RS232_BRIDGE_MERGED=1", "RS232_BRIDGE_DEFAULT_ON=1"]),
|
||||
("nrf52_rs232_default_on", ["WITH_RS232_BRIDGE=Serial2", "WITH_RS232_BRIDGE_UART=2",
|
||||
"RS232_BRIDGE_MERGED=1", "RS232_BRIDGE_DEFAULT_ON=1"]),
|
||||
("rs232_merged", ["WITH_RS232_BRIDGE=Serial2", "WITH_RS232_BRIDGE_UART=2",
|
||||
"RS232_BRIDGE_MERGED=1"]),
|
||||
("rs232_espnow_merged", ["WITH_RS232_BRIDGE=Serial2", "WITH_RS232_BRIDGE_UART=2",
|
||||
"RS232_BRIDGE_MERGED=1", "WITH_ESPNOW_BRIDGE=1",
|
||||
"ESPNOW_BRIDGE_MERGED=1"]),
|
||||
("mqtt_rs232", ["WITH_RS232_BRIDGE=Serial2", "WITH_RS232_BRIDGE_UART=2",
|
||||
"RS232_BRIDGE_MERGED=1", "WITH_MQTT_BRIDGE=1"]),
|
||||
("mqtt_rs232_espnow", ["WITH_RS232_BRIDGE=Serial2", "WITH_RS232_BRIDGE_UART=2",
|
||||
"RS232_BRIDGE_MERGED=1", "WITH_MQTT_BRIDGE=1",
|
||||
"WITH_ESPNOW_BRIDGE=1"]),
|
||||
("sole_espnow_merged", ["WITH_ESPNOW_BRIDGE=1", "ESPNOW_BRIDGE_MERGED=1"]),
|
||||
("sole_espnow_dedicated", ["WITH_ESPNOW_BRIDGE=1"]),
|
||||
("sole_espnow_default_on", ["WITH_ESPNOW_BRIDGE=1", "ESPNOW_BRIDGE_MERGED=1",
|
||||
@@ -89,7 +104,7 @@ class RS232RuntimeTests(unittest.TestCase):
|
||||
source, binary = work / "main.cpp", work / "test"
|
||||
source.write_text(code, encoding="ascii")
|
||||
result = subprocess.run([
|
||||
compiler, "-std=c++17", "-DESP32_PLATFORM=1", "-DENABLE_OTA=1",
|
||||
compiler, "-std=c++17", "-D" + ("NRF52_PLATFORM=1" if profile.startswith("nrf52") else "ESP32_PLATFORM=1"), "-DENABLE_OTA=1",
|
||||
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
|
||||
*["-D" + macro for macro in macros], "-I" + str(work),
|
||||
"-I" + str(ROOT / "test/fixtures/radio_profiles/mocks"),
|
||||
@@ -100,27 +115,82 @@ class RS232RuntimeTests(unittest.TestCase):
|
||||
result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
def test_uart_intent_survives_cross_profile_upgrades_without_claiming_mqtt_intent(self):
|
||||
compiler = shutil.which("g++") or shutil.which("clang++")
|
||||
self.assertIsNotNone(compiler, "a host C++ compiler is required")
|
||||
base = persistence_harness().replace("#define WITH_RS232_BRIDGE_UART 1", "")
|
||||
code = base[:base.index("struct Capture {")]
|
||||
code += (ROOT / "test/fixtures/rs232_runtime/prefs.cpp").read_text(encoding="ascii")
|
||||
common = ["WITH_RS232_BRIDGE=Serial2", "WITH_RS232_BRIDGE_UART=2", "RS232_BRIDGE_MERGED=1"]
|
||||
profiles = {"normal": [*common, "WITH_ESPNOW_BRIDGE=1", "ESPNOW_BRIDGE_MERGED=1"],
|
||||
"dedicated": common[:2],
|
||||
"triple": [*common, "WITH_ESPNOW_BRIDGE=1", "WITH_MQTT_BRIDGE=1"],
|
||||
"mqtt_uart": [*common, "WITH_MQTT_BRIDGE=1"]}
|
||||
with tempfile.TemporaryDirectory(prefix="rs232-profile-upgrade-") as directory:
|
||||
work = Path(directory)
|
||||
transaction = (ROOT / "src/helpers/ContactFileTransaction.h").read_text(encoding="ascii")
|
||||
(work / "ContactFileTransaction.h").write_text(transaction.replace(
|
||||
'#include "IdentityStore.h"', '#include <helpers/IdentityStore.h>'), encoding="ascii")
|
||||
source = work / "main.cpp"
|
||||
source.write_text(code, encoding="ascii")
|
||||
for name, macros in profiles.items():
|
||||
result = subprocess.run([compiler, "-std=c++17", "-DESP32_PLATFORM=1", "-DENABLE_OTA=1",
|
||||
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
|
||||
*["-D" + macro for macro in macros], "-I" + str(work),
|
||||
"-I" + str(ROOT / "test/fixtures/radio_profiles/mocks"),
|
||||
"-I" + str(ROOT / "src"), "-I" + str(ROOT / "src/helpers"),
|
||||
str(source), "-o", str(work / name)], capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
def run(profile, operation, image, primary, espnow, uart, port=2):
|
||||
result = subprocess.run([str(work / profile), operation, str(image), str(primary),
|
||||
str(espnow), str(uart), str(port)], capture_output=True, text=True, timeout=10)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
for origin in ("triple", "mqtt_uart"):
|
||||
for uart in (0, 1):
|
||||
image = work / f"{origin}-{uart}.bin"
|
||||
run(origin, "write", image, 1, 0, uart)
|
||||
self.assertEqual(len(image.read_bytes()), 877)
|
||||
for target in ("normal", "dedicated"):
|
||||
with self.subTest(origin=origin, target=target, uart=uart):
|
||||
run(target, "read", image, uart, 0, uart)
|
||||
# Old MQTT files persisted primary enabled plus UART2.
|
||||
# Without the new independent marker this remains MQTT only.
|
||||
old = work / "old-mqtt.bin"
|
||||
old.write_bytes(image.read_bytes()[:874])
|
||||
run(origin, "read", old, 1, 0, 0)
|
||||
for origin in ("normal", "dedicated"):
|
||||
for uart in (0, 1):
|
||||
image = work / f"{origin}-{uart}.bin"
|
||||
run(origin, "write", image, uart, 0, uart)
|
||||
for target in ("triple", "mqtt_uart"):
|
||||
with self.subTest(origin=origin, target=target, uart=uart):
|
||||
run(target, "read", image, uart, 0, uart)
|
||||
image = work / "corrupt-triple.bin"
|
||||
run("triple", "write", image, 1, 0, 1)
|
||||
saved = image.read_bytes()
|
||||
for tail in (saved[:875] + bytes([1]), saved[:876], saved[:876] + bytes([0])):
|
||||
image.write_bytes(tail)
|
||||
for target in ("normal", "dedicated"):
|
||||
run(target, "read", image, 0, 0, 0)
|
||||
for target in ("triple", "mqtt_uart"):
|
||||
run(target, "read", image, 1, 0, 0)
|
||||
|
||||
def test_production_defaults_upgrade_and_runtime_controls(self):
|
||||
compiler = shutil.which("g++") or shutil.which("clang++")
|
||||
self.assertIsNotNone(compiler, "a host C++ compiler is required")
|
||||
cli = (ROOT / "src/helpers/CommonCLI.cpp").read_text(encoding="ascii")
|
||||
header = (ROOT / "examples/simple_repeater/MyMesh.h").read_text(encoding="ascii")
|
||||
implementation = (ROOT / "examples/simple_repeater/MyMesh.cpp").read_text(encoding="ascii")
|
||||
guard = (
|
||||
"#if defined(WITH_RS232_BRIDGE) && defined(RS232_BRIDGE_MERGED) \\\n"
|
||||
" && !defined(RS232_BRIDGE_DEFAULT_ON)"
|
||||
)
|
||||
declaration_start = cli.index(guard, cli.index('if (file) {'))
|
||||
declaration = cli[declaration_start:cli.index("#endif", declaration_start) + len("#endif")]
|
||||
esp_guard = "#if defined(ESPNOW_BRIDGE_MERGED) && !defined(ESPNOW_BRIDGE_DEFAULT_ON)"
|
||||
esp_declaration = cli.index(esp_guard, cli.index('if (file) {'))
|
||||
declaration += "\n" + cli[esp_declaration:cli.index("#endif", esp_declaration) + len("#endif")]
|
||||
declaration_start = cli.index("#if defined(ESPNOW_BRIDGE_MERGED)", cli.index("mesh::hilStartupTrace(\"prefs_image_open_ready\")"))
|
||||
declaration_end = cli.index(" // Every supported layout", declaration_start)
|
||||
declaration = cli[declaration_start:declaration_end]
|
||||
read_start = cli.index("if (file.available() >= (int)sizeof(_prefs->bridge_uart))")
|
||||
read_end = cli.index("if (file.available() >= (int)sizeof(_prefs->bridge_format))", read_start)
|
||||
# The fixture supplies a file positioned at the appended UART field.
|
||||
uart_read = cli[read_start:read_end] + "}\n"
|
||||
marker_read_start = cli.index(esp_guard, cli.index("if (file.available() >= (int)sizeof(_prefs->ota_channel))"))
|
||||
uart_read += cli[marker_read_start:cli.index("#endif", marker_read_start) + len("#endif")]
|
||||
# The old image mock has just its UART byte, so marker reads return
|
||||
# zero; execute the real marker guard rather than inventing a fallback.
|
||||
marker_start = cli.index("uint8_t bridge_profile = 0;", cli.index("if (file.available() >= (int)sizeof(_prefs->ota_channel))"))
|
||||
uart_read += "uint8_t bridge_profile = 0;\n" + extract_braced(cli[marker_start:], "if (file.read(&bridge_profile,")
|
||||
migrate_start = cli.index("// sanitise bad bridge pref values")
|
||||
migrate_end = cli.index("if (!mesh::bridge::isValidEspNowFormat", migrate_start)
|
||||
migration = cli[migrate_start:migrate_end]
|
||||
@@ -137,7 +207,9 @@ class RS232RuntimeTests(unittest.TestCase):
|
||||
lifecycle = "\n".join(extract_braced(header, signature) for signature in (
|
||||
"AbstractBridge* activeBridge() {", "const AbstractBridge* activeBridge() const {",
|
||||
"RS232Bridge* createRS232Bridge()", "bool beginRS232Bridge()",
|
||||
"bool endRS232Bridge()", "bool isBridgeRunning() const override",
|
||||
"bool endRS232Bridge()", "bool rs232BridgeEnabled() const",
|
||||
"bool isRs232BridgeRunning() const override", "bool setRs232BridgeState(bool enable)",
|
||||
"bool restartRs232Bridge()", "bool isBridgeRunning() const override",
|
||||
"bool setBridgeState(bool enable) override", "bool restartBridge() override",
|
||||
))
|
||||
lifecycle += "\n#ifdef WITH_ESPNOW_BRIDGE\n" + "\n".join(
|
||||
|
||||
@@ -37,7 +37,10 @@ STUBS = r'''
|
||||
#define WITH_ESPNOW_BRIDGE 1
|
||||
#define MESH_DEBUG_PRINTLN(...) ((void)0)
|
||||
constexpr int WL_CONNECTED = 3, WL_DISCONNECTED = 6, HTTP_GET = 0;
|
||||
constexpr int WIFI_AP = 2;
|
||||
enum wifi_mode_t { WIFI_OFF = 0, WIFI_STA = 1, WIFI_AP = 2 };
|
||||
constexpr wifi_mode_t WIFI_MODE_NULL = WIFI_OFF;
|
||||
int esp_wifi_stop();
|
||||
int esp_wifi_deinit();
|
||||
constexpr int ESP_OK = 0;
|
||||
constexpr int LISTEN = 10;
|
||||
|
||||
@@ -65,7 +68,20 @@ struct MockWiFi {
|
||||
IPAddress station_ip, ap_ip, configured_ap_ip;
|
||||
int ap_protocol = 15;
|
||||
int status() const { return station_status; }
|
||||
int getMode() const { return sdk_ap_active ? WIFI_AP : 0; }
|
||||
wifi_mode_t getMode() const {
|
||||
return started_cache && sdk_started ? (sdk_ap_active ? WIFI_AP : WIFI_STA) : WIFI_OFF;
|
||||
}
|
||||
int channel() const { return initialized_cache && sdk_initialized ? 1 : 0; }
|
||||
bool mode(wifi_mode_t requested) {
|
||||
if (requested == getMode()) return true;
|
||||
if (requested == WIFI_OFF) {
|
||||
esp_wifi_stop(); esp_wifi_deinit();
|
||||
initialized_cache = started_cache = false;
|
||||
} else {
|
||||
sdk_initialized = sdk_started = initialized_cache = started_cache = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
IPAddress localIP() const { return station_ip; }
|
||||
IPAddress softAPIP() const { return ap_ip; }
|
||||
bool softAPConfig(IPAddress ip, IPAddress, IPAddress) {
|
||||
@@ -99,6 +115,11 @@ int esp_wifi_stop() {
|
||||
return 0;
|
||||
}
|
||||
int esp_wifi_deinit() { ++wifi_deinit_calls; sdk_initialized = false; return 0; }
|
||||
int esp_wifi_get_mode(wifi_mode_t* mode) {
|
||||
if (!sdk_initialized) return -1;
|
||||
*mode = sdk_ap_active ? WIFI_AP : WIFI_STA;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
int server_count = 0, server_begin_calls = 0;
|
||||
struct AsyncWebServerRequest {
|
||||
@@ -268,10 +289,12 @@ class WifiOtaLifecycleTests(unittest.TestCase):
|
||||
source = folder / "lifecycle.cpp"
|
||||
binary = folder / ("lifecycle.exe" if os.name == "nt" else "lifecycle")
|
||||
source.write_text(production_harness())
|
||||
subprocess.run([
|
||||
built = subprocess.run([
|
||||
os.environ.get("CXX", "g++"), "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
||||
"-I", str(ROOT / "src"), str(source), "-o", str(binary),
|
||||
], check=True, capture_output=True, text=True, timeout=60)
|
||||
], capture_output=True, text=True, timeout=60)
|
||||
if built.returncode:
|
||||
raise RuntimeError(built.stdout + built.stderr)
|
||||
cls.observations = {}
|
||||
for scenario in SCENARIOS:
|
||||
result = subprocess.run([str(binary), scenario], check=True,
|
||||
@@ -317,7 +340,8 @@ class WifiOtaLifecycleTests(unittest.TestCase):
|
||||
self.assertEqual(observation["deinit_calls"], count)
|
||||
self.assertEqual(observation["sdk_initialized"], 1 - count)
|
||||
self.assertEqual(observation["sdk_started"], 1 - count)
|
||||
self.assertTrue(observation["initialized_cache"] and observation["started_cache"])
|
||||
self.assertEqual(observation["initialized_cache"], 1 - count)
|
||||
self.assertEqual(observation["started_cache"], 1 - count)
|
||||
self.assertEqual(observation["endpoint_usable"], 1 - count)
|
||||
if count == 0:
|
||||
if scenario == "cleanup_web":
|
||||
|
||||
Reference in new issue
Block a user