Add station-only WiFi UDP bridge with eight authenticated peers

This commit is contained in:
mikecarper committed 2026-10-22 12:20:43 -07:00
1 parent 7bc3aeb234
commit 5e25faa3d0
45 files changed
+2699 -35

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
#pragma once
#include <stdint.h>
inline uint32_t test_now = 0;
inline uint32_t millis() { return test_now; }
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include <cstddef>
#include <cstdint>
// Print/Arduino defines these before ordinary firmware headers. Keep them in
// the host build too: a local variable named HEX would otherwise compile here
// yet be expanded to an invalid numeric token on the actual board.
#define HEX 16
#define DEC 10
#define OCT 8
#define BIN 2
uint32_t millis();
class Stream {
public:
virtual ~Stream() = default;
virtual int available() = 0;
virtual int read() = 0;
virtual size_t write(const uint8_t*, size_t) = 0;
virtual int availableForWrite() { return 4096; }
};
+93
View File
@@ -0,0 +1,93 @@
#include <helpers/UDPBridgePrefs.h>
#include <cassert>
#include <string>
#include <initializer_list>
static uint32_t millis() { return 0x11223344; }
struct Callbacks {
virtual ~Callbacks() = default;
CALLBACKS_HERE
};
struct Fake : Callbacks {
mesh::bridge::udp::Prefs prefs;
unsigned writes = 0, statuses = 0, stats = 0, sends = 0, resets = 0;
uint8_t peer = 255;
uint32_t nonce = 0;
bool save = true, available = true;
std::string page;
const mesh::bridge::udp::Prefs* getUDPBridgePrefs() const override { return available ? &prefs : nullptr; }
bool setUDPBridgePrefs(const mesh::bridge::udp::Prefs& value) override {
++writes; if (!save) return false; prefs = value; return true;
}
void formatUDPBridgeStatus(char* reply, size_t capacity) override {
++statuses; assert(capacity == 157); snprintf(reply, capacity, "%s", std::string(300, 'x').c_str());
}
void formatUDPBridgeStats(const char* value, char* reply, size_t capacity) override {
++stats; page = value; snprintf(reply, capacity, "stats");
}
bool sendUDPBridgeTest(uint8_t value, uint32_t id) override { ++sends; peer = value; nonce = id; return true; }
bool resetUDPBridgeStats() override { ++resets; return true; }
};
struct CommonCLI {
Callbacks* _callbacks;
bool handleUDPBridgeCommand(uint32_t, const char*, char*);
};
HANDLER_HERE
int main() {
Fake fake; CommonCLI cli{&fake};
char reply[160];
auto command = [&](const char* value, uint32_t remote = 0) {
memset(reply, 'Z', sizeof(reply));
const bool handled = cli.handleUDPBridgeCommand(remote, value, reply);
assert(reply[157] == 'Z' && reply[158] == 'Z' && reply[159] == 'Z');
if (handled) assert(memchr(reply, 0, 157));
return handled;
};
assert(command("get udp.status", 1) && strlen(reply) == 156);
for (const char* page : {"", "rx", "tx", "drops", "test"}) {
assert(command((std::string("get udp.stats") + (*page ? " " : "") + page).c_str(), 1));
assert(fake.page == page);
}
assert(command("get udp.enabled") && !strcmp(reply, "off"));
assert(command("get udp.port") && !strcmp(reply, "4404"));
for (char index = '1'; index <= '8'; ++index) {
std::string key = "udp.peer."; key += index;
assert(command(("get " + key).c_str()) && !strcmp(reply, "off"));
const std::string value = "192.168.1." + std::to_string(index - '0') + ":4404";
assert(command(("set " + key + " " + value).c_str(), 1) && strstr(reply, "saved"));
assert(command(("get " + key).c_str()) && value == reply);
assert(command(("set " + key + " off").c_str()));
}
assert(command("set udp.enabled on", 5) && fake.prefs.enabled == 1);
assert(command("set udp.port 65535", 5) && fake.prefs.port == 65535);
assert(command("set udp.peer.1 192.168.1.1:4404"));
unsigned writes = fake.writes;
assert(command("set udp.peer.2 192.168.1.1:4404") && strstr(reply, "duplicate") && fake.writes == writes);
for (const char* value : {"get udp.status x", "get udp.enabled ", "get udp.port 1", "get udp.statsrx",
"get udp.stats ", "get udp.stats xxx", "set udp.enabled ON", "set udp.enabled on ",
"set udp.port 0", "set udp.port -1", "set udp.port 65536", "set udp.port 1 ",
"get udp.peer.0", "get udp.peer.9", "get udp.peer.10", "get udp.peer.1 x",
"set udp.peer.1 off ", "set udp.peer.1 127.0.0.1:4404", "set udp.peer.1 224.1.2.3:1",
"udp", "udp test", "udp test 0", "udp test 9", "udp test 10", "udp test 1 ",
"udp test 1 1234567", "udp test 1 123456789", "udp test 1 ABCDEFGH", "udp test 1 12345678 ",
"udp resetstats ", "udp resetstats x"}) {
assert(command(value) && !strncmp(reply, "Error:", 6));
assert(fake.writes == writes && fake.sends == 0 && fake.resets == 0);
}
for (const char* value : {"udptest", "udpx test", "get repeat", "get espnow.status"}) assert(!command(value));
for (const char* value : {"udp test 1", "udp test 8 A1B2C3D4", "udp resetstats"}) {
assert(command(value, 1) && strstr(reply, "local connection"));
assert(fake.sends == 0 && fake.resets == 0);
}
assert(command("udp test 8 a1b2C3d4") && fake.peer == 7 && fake.nonce == 0xA1B2C3D4);
assert(command("udp test 1") && fake.peer == 0 && fake.nonce == millis());
assert(command("udp resetstats") && fake.resets == 1);
fake.save = false; writes = fake.writes;
assert(command("set udp.port 1234") && strstr(reply, "not saved") && fake.writes == writes + 1);
assert(fake.prefs.port == 65535);
fake.available = false;
assert(command("get udp.port") && strstr(reply, "unavailable"));
Callbacks unsupported; cli._callbacks = &unsupported;
assert(command("get udp.status") && strstr(reply, "unavailable"));
assert(command("get udp.stats") && strstr(reply, "unavailable"));
assert(command("udp test 1") && strstr(reply, "not queued"));
}
+4
View File
@@ -0,0 +1,4 @@
#pragma once
#include <stdint.h>
inline uint32_t random_state = 0x1678abcd;
inline uint32_t esp_random() { random_state ^= random_state << 13; random_state ^= random_state >> 17; random_state ^= random_state << 5; return random_state; }
+125
View File
@@ -0,0 +1,125 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <vector>
#if defined(ARDUINO)
#include <Stream.h>
#include <cstdarg>
#include <cstdio>
#endif
#define MAX_TRANS_UNIT 255
#define PAYLOAD_TYPE_OTA 0x0c
#define BRIDGE_DEBUG_PRINTLN(...) ((void)0)
struct NodePrefs { uint8_t bridge_pkt_src = 1; uint32_t bridge_delay = 0; char bridge_secret[16] = "unit-test-key"; };
namespace mesh {
#if defined(ARDUINO)
inline bool logging_enabled = false, debug_enabled = false;
class DebugStream : public Stream {
public:
int capacity = 4096;
unsigned writes = 0;
std::vector<uint8_t> bytes;
int available() override { return 0; }
int read() override { return -1; }
int availableForWrite() override { return capacity; }
size_t write(const uint8_t* data, size_t size) override {
++writes;
if (size > static_cast<size_t>(capacity)) return 0;
bytes.insert(bytes.end(), data, data + size); return size;
}
};
inline DebugStream debug_stream;
inline bool isUsbDebugLoggingEnabled() { return logging_enabled && debug_enabled; }
inline Stream& usbLoggingPort() { return debug_stream; }
inline size_t nrf52DebugPrintf(const char* format, ...) {
if (!isUsbDebugLoggingEnabled()) return 0;
char record[256];
va_list args; va_start(args, format);
int size = vsnprintf(record, sizeof(record), format, args); va_end(args);
if (size <= 0) return 0;
if (size >= static_cast<int>(sizeof(record))) size = sizeof(record) - 1;
return debug_stream.write(reinterpret_cast<const uint8_t*>(record), size);
}
#endif
struct Packet {
std::vector<uint8_t> payload{1, 2, 3};
bool allowed = true; bool udp_bridge_ingress = false;
uint8_t getPayloadType() const { return payload.empty() ? 0 : (payload[0] >> 2) & 0x0f; }
int getRawLength() const { return static_cast<int>(payload.size()); }
uint16_t writeTo(uint8_t* out) const {
for (size_t i = 0; i < payload.size(); ++i) out[i] = payload[i];
return static_cast<uint16_t>(payload.size());
}
bool readFrom(const uint8_t* bytes, size_t size) {
if (size < 2 || size > MAX_TRANS_UNIT || bytes[0] == 0xff) return false;
udp_bridge_ingress = false;
payload.assign(bytes, bytes + size); return true;
}
};
struct RTCClock {};
struct PacketManager {
bool starved = false;
unsigned allocations = 0, frees = 0;
std::vector<std::vector<uint8_t>> received;
Packet* allocNew() {
++allocations;
return starved ? nullptr : new Packet;
}
void free(Packet* packet) { ++frees; delete packet; }
};
}
struct SeenPackets {
std::vector<std::vector<uint8_t>> seen;
bool wasSeen(const mesh::Packet* packet) const {
for (const auto& bytes : seen) if (bytes == packet->payload) return true;
return false;
}
void markSeen(mesh::Packet* packet) { seen.push_back(packet->payload); }
};
class BridgeBase {
public:
static constexpr uint16_t BRIDGE_PACKET_MAGIC = 0xC03E;
static constexpr uint16_t BRIDGE_MAGIC_SIZE = 2, BRIDGE_LENGTH_SIZE = 2, BRIDGE_CHECKSUM_SIZE = 2;
virtual ~BridgeBase() = default;
virtual void begin() = 0;
virtual void end() = 0;
virtual void loop() = 0;
virtual void sendPacket(mesh::Packet*) = 0;
virtual void onPacketReceived(mesh::Packet*) = 0;
bool isRunning() const { return _initialized; }
size_t seenCount() const { return _seen_packets.seen.size(); }
protected:
enum class IngressResult : uint8_t { Stopped, Filtered, Duplicate, Queued };
virtual void onIngressResult(IngressResult) {}
bool _initialized = false;
NodePrefs* _prefs;
mesh::PacketManager* _mgr;
mesh::RTCClock* _rtc;
SeenPackets _seen_packets;
BridgeBase(NodePrefs* prefs, mesh::PacketManager* mgr, mesh::RTCClock* rtc)
: _prefs(prefs), _mgr(mgr), _rtc(rtc) {}
bool allowsPacket(const mesh::Packet* packet) { return packet->allowed; }
static uint16_t fletcher16(const uint8_t* bytes, size_t size) {
uint16_t a = 0, b = 0;
for (size_t i = 0; i < size; ++i) { a = (a + bytes[i]) % 255; b = (b + a) % 255; }
return (b << 8) | a;
}
bool validateChecksum(const uint8_t* bytes, size_t size, uint16_t checksum) {
return fletcher16(bytes, size) == checksum;
}
void handleReceivedPacket(mesh::Packet* packet) {
if (!allowsPacket(packet)) {
onIngressResult(IngressResult::Filtered); _mgr->free(packet); return;
}
if (packet->getPayloadType() != PAYLOAD_TYPE_OTA && _seen_packets.wasSeen(packet)) {
onIngressResult(IngressResult::Duplicate); _mgr->free(packet); return;
}
if (packet->getPayloadType() != PAYLOAD_TYPE_OTA) _seen_packets.markSeen(packet);
onIngressResult(IngressResult::Queued);
_mgr->received.push_back(packet->payload); _mgr->free(packet);
}
};
+69
View File
@@ -0,0 +1,69 @@
#pragma once
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <cstdarg>
#include <vector>
#include <deque>
#include <cstring>
namespace udp_test {
struct Datagram { sockaddr_in source{}, target{}; std::vector<uint8_t> bytes; };
struct Socket { bool open = false; uint32_t ip = 0; uint16_t port = 0; std::deque<Datagram> rx; };
inline Socket sockets[64];
inline int next_socket = 0, opens = 0, receives = 0, sends = 0, receive_cap = 0;
inline uint32_t creating_ip = 0;
inline bool fail_send = false, fail_rcvbuf = false;
inline std::vector<Datagram> sent;
inline void inject(int index, const Datagram& packet) { sockets[index].rx.push_back(packet); }
}
inline int udpTestSocket(int, int, int) {
const int fd = udp_test::next_socket++;
if (fd >= 64) { errno = EMFILE; return -1; }
udp_test::sockets[fd] = {}; udp_test::sockets[fd].open = true;
udp_test::sockets[fd].ip = udp_test::creating_ip; ++udp_test::opens;
return fd;
}
inline int udpTestClose(int fd) { udp_test::sockets[fd].open = false; udp_test::sockets[fd].rx.clear(); return 0; }
inline int udpTestFcntl(int, int, ...) { return 0; }
inline int udpTestSetsockopt(int, int, int, const void* value, socklen_t) {
if (udp_test::fail_rcvbuf) { errno = ENOPROTOOPT; return -1; }
udp_test::receive_cap = *static_cast<const int*>(value); return 0;
}
inline int udpTestBind(int fd, const sockaddr* address, socklen_t) {
udp_test::sockets[fd].port = reinterpret_cast<const sockaddr_in*>(address)->sin_port; return 0;
}
inline int udpTestSendto(int fd, const void* bytes, size_t size, int, const sockaddr* address, socklen_t) {
++udp_test::sends;
if (udp_test::fail_send) { udp_test::fail_send = false; errno = ENOMEM; return -1; }
udp_test::Datagram message;
message.source.sin_family = AF_INET; message.source.sin_port = udp_test::sockets[fd].port;
message.source.sin_addr.s_addr = udp_test::sockets[fd].ip;
message.target = *reinterpret_cast<const sockaddr_in*>(address);
message.bytes.assign(static_cast<const uint8_t*>(bytes), static_cast<const uint8_t*>(bytes) + size);
udp_test::sent.push_back(message);
for (auto& socket : udp_test::sockets)
if (socket.open && socket.ip == message.target.sin_addr.s_addr && socket.port == message.target.sin_port) {
size_t queued = 0; for (const auto& item : socket.rx) queued += item.bytes.size();
if (socket.rx.size() < 6 && queued + size <= 768) socket.rx.push_back(message);
}
return static_cast<int>(size);
}
inline int udpTestRecvfrom(int fd, void* out, size_t size, int, sockaddr* address, socklen_t* address_size) {
++udp_test::receives;
auto& queue = udp_test::sockets[fd].rx;
if (queue.empty()) { errno = EAGAIN; return -1; }
const auto message = queue.front(); queue.pop_front();
const size_t count = message.bytes.size() < size ? message.bytes.size() : size;
memcpy(out, message.bytes.data(), count);
memcpy(address, &message.source, sizeof(message.source)); *address_size = sizeof(message.source);
return static_cast<int>(count);
}
#define socket udpTestSocket
#define close udpTestClose
#define fcntl udpTestFcntl
#define setsockopt udpTestSetsockopt
#define bind udpTestBind
#define sendto udpTestSendto
#define recvfrom udpTestRecvfrom
+58
View File
@@ -0,0 +1,58 @@
#include <helpers/UDPBridgePrefs.h>
#include <cassert>
#include <cstring>
#include <cstdio>
struct Callbacks {
mesh::bridge::udp::Prefs prefs;
bool network = true;
unsigned closes = 0, resumes = 0;
bool isUDPBridgeNetworkEnabled() const { return network; }
bool setUDPBridgeNetworkState(bool enabled) { network = enabled; if (enabled) ++resumes; else ++closes; return true; }
const mesh::bridge::udp::Prefs* getUDPBridgePrefs() const { return &prefs; }
} callbacks;
struct Board {
bool running = false, allow_start = true, allow_stop = true;
unsigned starts = 0;
bool isOTAUpdateRunning() const { return running; }
bool startOTAUpdate(const char*, char* reply, bool) {
assert(!callbacks.network); ++starts;
if (allow_start) running = true;
strcpy(reply, allow_start ? "Started" : "Failed"); return allow_start;
}
bool stopOTAUpdate(char* reply) { if (allow_stop) running = false; strcpy(reply, "Stopped"); return allow_stop; }
} board;
struct NodePrefs { const char* node_name = "test"; } prefs;
struct CLI {
Board* _board = &board;
NodePrefs* _prefs = &prefs;
Callbacks* _callbacks = &callbacks;
bool _wifi_ota_resume_udp = false;
void run(const char* command, char* reply) {
if (false) {
OTA_HERE
}
}
};
int main() {
callbacks.prefs.enabled = 1;
CLI cli; char reply[160] = {};
cli.run("start ota invalid", reply); assert(board.starts == 0 && callbacks.closes == 0);
board.allow_start = false;
cli.run("start ota", reply);
assert(callbacks.closes == 1 && callbacks.resumes == 1 && callbacks.network && !cli._wifi_ota_resume_udp);
board.allow_start = true;
cli.run("start ota ap", reply);
assert(!callbacks.network && cli._wifi_ota_resume_udp);
cli.run("start ota ap", reply); // Repeating a start retains the original pause.
board.allow_stop = false;
cli.run("stop ota", reply); assert(!callbacks.network && cli._wifi_ota_resume_udp);
board.allow_stop = true;
cli.run("stop ota", reply); assert(callbacks.network && !cli._wifi_ota_resume_udp);
cli.run("start ota", reply); callbacks.prefs.enabled = 0;
cli.run("stop ota", reply); assert(!callbacks.network && !cli._wifi_ota_resume_udp);
callbacks.prefs.enabled = 1;
const unsigned resumes = callbacks.resumes;
cli.run("stop ota", reply); assert(!callbacks.network && callbacks.resumes == resumes);
cli.run("start ota", reply); cli.run("stop ota", reply);
assert(!callbacks.network && callbacks.resumes == resumes); // An initially stopped session remains stopped.
}
+102
View File
@@ -0,0 +1,102 @@
#include <helpers/UDPBridgeSettings.h>
#include <cassert>
#include <vector>
#include <initializer_list>
using namespace mesh::bridge::udp;
using Bytes = std::vector<uint8_t>;
constexpr char Path[] = "/udp_bridge";
Bytes image(const Prefs& prefs) {
Bytes result(PREFS_IMAGE_SIZE); encodePrefs(result.data(), prefs); return result;
}
Prefs configured() {
Prefs prefs; prefs.enabled = 1;
assert(parsePeer("192.168.1.22:4404", prefs.peers[0]));
assert(parsePeer("10.2.3.4:65535", prefs.peers[7]));
return prefs;
}
int main() {
Prefs defaults; assert(validPrefs(defaults) && defaults.port == 4404 && !defaults.enabled);
Peer peer;
assert(parsePeer("192.168.1.10:1", peer));
const Peer before = peer;
for (const char* value : {"", "off ", "0.1.2.3:1", "127.1.2.3:2", "224.1.2.3:1",
"255.255.255.255:3", "192.168.1.2", "1.2.3.4:0", "1.2.3.4:65536", "1.2.3.4:-1",
"1.2.3.4:123456789123456789123456789", "1.2.3.4:1 ", "1.2.3.4:1x", "1.2.3.4:1:2",
"1.2.3:1", "1.2.3.256:1", "999999999999999999999.1.2.3:2", "a.b.c.d:2", "1..3.4:2"}) {
assert(!parsePeer(value, peer) && equalPeer(peer, before));
}
char text[24]; formatPeer(peer, text, sizeof(text)); assert(!strcmp(text, "192.168.1.10:1"));
assert(parsePeer("off", peer) && !peer.enabled());
formatPeer(peer, text, sizeof(text)); assert(!strcmp(text, "off"));
Prefs good = configured(), loaded;
assert(decodePrefs(image(good).data(), PREFS_IMAGE_SIZE, loaded));
assert(image(loaded) == image(good));
for (size_t size = 0; size <= 128; ++size) {
if (size == PREFS_IMAGE_SIZE) continue;
Bytes bad = image(good); bad.resize(size);
loaded = defaults;
assert(!decodePrefs(bad.data(), bad.size(), loaded) && image(loaded) == image(defaults));
}
for (size_t byte = 0; byte < PREFS_IMAGE_SIZE; ++byte) {
Bytes bad = image(good); bad[byte] ^= 1;
assert(!decodePrefs(bad.data(), bad.size(), loaded));
}
Prefs invalid = good; invalid.peers[1] = invalid.peers[0]; assert(!validPrefs(invalid));
invalid = defaults; invalid.peers[0].ip[0] = 1; assert(!validPrefs(invalid));
invalid = defaults; invalid.enabled = 2; assert(!validPrefs(invalid));
invalid = defaults; invalid.port = 0; assert(!validPrefs(invalid));
GuestMemoryFS fs;
fs.files["/com_prefs"] = {1, 2, 3}; // The existing binary image is never rewritten.
Settings store;
assert(store.begin(&fs) && store.healthy() && !store.prefs().enabled && fs.writes == 0);
assert(store.setPrefs(good) && image(store.prefs()) == image(good));
assert(fs.files.at("/com_prefs") == Bytes({1, 2, 3}));
Settings reboot; assert(reboot.begin(&fs) && image(reboot.prefs()) == image(good));
for (unsigned fault = 0; fault < 10; ++fault) {
GuestMemoryFS faulty; faulty.files[Path] = image(good);
Settings current; assert(current.begin(&faulty));
switch (fault) {
case 0: faulty.fail_open_write = true; break;
case 1: faulty.short_write = 5; break;
case 2: faulty.corrupt_stage = true; break;
case 3: faulty.fail_stage_open = true; break;
case 4: faulty.fail_stage_read = true; break;
case 5: faulty.fail_publish = true; break;
case 6: faulty.false_publish = true; break;
case 7: faulty.fail_backup = true; break;
case 8: faulty.files["/udp_bridge.bak"] = image(defaults); faulty.fail_remove_backup = true; break;
case 9: faulty.files["/udp_bridge.tmp"] = image(defaults); faulty.fail_remove_temp = true; break;
}
assert(!current.setPrefs(defaults));
assert(image(current.prefs()) == image(good) && faulty.files.at(Path) == image(good));
GuestMemoryFS after; after.files = faulty.files;
Settings restored; assert(restored.begin(&after) && image(restored.prefs()) == image(good));
}
for (size_t size : {size_t(0), size_t(1), size_t(63), size_t(65), size_t(128)}) {
GuestMemoryFS broken; broken.files[Path] = image(good); broken.files[Path].resize(size);
Settings current; assert(!current.begin(&broken) && !current.prefs().enabled && current.healthy());
assert(current.setPrefs(defaults));
}
GuestMemoryFS uncertain; uncertain.files[Path] = image(good);
Settings previous; assert(previous.begin(&uncertain)); uncertain.fail_public_read = true;
assert(!previous.setPrefs(defaults) && !previous.healthy() && image(previous.prefs()) == image(good));
assert(!previous.setPrefs(good));
uncertain.fail_public_read = false;
assert(previous.begin(&uncertain) && !previous.prefs().enabled);
GuestMemoryFS interrupted; interrupted.files[Path] = image(good);
Settings writer; assert(writer.begin(&interrupted)); interrupted.capture = true;
assert(writer.setPrefs(defaults));
const auto snapshots = interrupted.snapshots;
assert(snapshots.size() > 10);
for (const auto& snapshot : snapshots) {
GuestMemoryFS after; after.files = snapshot;
Settings recovered; assert(recovered.begin(&after));
assert(image(recovered.prefs()) == image(good) || image(recovered.prefs()) == image(defaults));
}
GuestMemoryFS staged; staged.files["/udp_bridge.tmp"] = image(good);
Settings scratch; assert(scratch.begin(&staged) && !scratch.prefs().enabled);
GuestMemoryFS unreadable; unreadable.files[Path] = image(good); unreadable.fail_metadata = true;
Settings unavailable; assert(!unavailable.begin(&unreadable) && !unavailable.healthy());
assert(!unavailable.setPrefs(defaults));
}
+245
View File
@@ -0,0 +1,245 @@
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <vector>
#include <memory>
#include "helpers/bridges/BridgeBase.h"
#include "Arduino.h"
#include "lwip/sockets.h"
#include "helpers/bridges/UDPBridge.h"
#include <SHA256.h>
namespace udp = mesh::bridge::udp;
static bool hmac(void* context, const uint8_t* bytes, size_t size, uint8_t digest[32]) {
const char* key = static_cast<const char*>(context);
SHA256 sha; sha.resetHMAC(reinterpret_cast<const uint8_t*>(key), strlen(key));
static const uint8_t domain[] = "MeshCore UDP bridge v1";
sha.update(domain, sizeof(domain)); sha.update(bytes, size);
sha.finalizeHMAC(reinterpret_cast<const uint8_t*>(key), strlen(key), digest, 32); return true;
}
static uint32_t address(uint8_t last) { uint8_t bytes[] = {10, 11, 12, last}; uint32_t value; memcpy(&value, bytes, 4); return value; }
static udp::Peer peer(uint8_t last) { udp::Peer value; value.ip[0] = 10; value.ip[1] = 11; value.ip[2] = 12; value.ip[3] = last; value.port = 4404; return value; }
struct Node {
NodePrefs prefs; mesh::PacketManager manager; mesh::RTCClock rtc; udp::Prefs config; UDPBridge bridge;
uint8_t id; int fd = -1;
Node(uint8_t number, uint8_t other) : bridge(&prefs, &manager, &rtc, &config), id(number) {
config.enabled = 1; config.peers[0] = peer(other);
}
void start() { udp_test::creating_ip = address(id); fd = udp_test::next_socket; bridge.serviceNetwork(true, address(id), 0x00ffffff); }
std::string status() { char value[157]; bridge.formatStatus(value, sizeof(value)); return value; }
std::string stats(const char* page) { char value[157]; bridge.formatStats(page, value, sizeof(value)); return value; }
};
static void run(Node& a, Node& b, unsigned count = 60) {
for (unsigned i = 0; i < count; ++i) { test_now += 100; a.bridge.loop(); b.bridge.loop(); }
}
static udp_test::Datagram lastKind(udp::Kind kind, uint8_t sender) {
for (auto it = udp_test::sent.rbegin(); it != udp_test::sent.rend(); ++it)
if (it->source.sin_addr.s_addr == address(sender) && it->bytes[5] == uint8_t(kind)) return *it;
assert(false); return {};
}
static void codec() {
const char* key = "unit-test-key"; uint8_t payload[255]; memset(payload, 7, sizeof(payload));
uint8_t bytes[udp::MAX_FRAME + 1];
udp::Frame frame{udp::Kind::Mesh, 17, 29, 1, payload, sizeof(payload)};
const size_t size = udp::encode(bytes, sizeof(bytes), frame, hmac, const_cast<char*>(key));
assert(size == 299);
// Independently calculated with Python hmac/hashlib, not this codec.
static const uint8_t expected_tag[] = {0x98,0xee,0xa8,0xf4,0x80,0x8e,0x94,0x55,
0x73,0x63,0xf1,0xf3,0xdb,0xbe,0xac,0x29};
assert(!memcmp(bytes + size - udp::TAG_SIZE, expected_tag, sizeof(expected_tag)));
udp::Frame decoded;
assert(udp::decode(bytes, size, decoded, hmac, const_cast<char*>(key)) == udp::Decode::Ok);
assert(decoded.length == 255 && decoded.sender == 17 && decoded.recipient == 29);
for (size_t i = 0; i < size; ++i) {
bytes[i] ^= 1;
udp::Frame untouched; untouched.sequence = 789;
assert(udp::decode(bytes, size, untouched, hmac, const_cast<char*>(key)) != udp::Decode::Ok);
assert(untouched.sequence == 789); bytes[i] ^= 1;
}
for (size_t n = 0; n < size; ++n)
assert(udp::decode(bytes, n, decoded, hmac, const_cast<char*>(key)) != udp::Decode::Ok);
assert(udp::decode(bytes, size + 1, decoded, hmac, const_cast<char*>(key)) != udp::Decode::Ok);
assert(udp::decode(bytes, size, decoded, hmac, const_cast<char*>("other-key")) == udp::Decode::Authentication);
assert(!udp::encode(bytes, size - 1, frame, hmac, const_cast<char*>(key)));
udp::Session session;
assert(session.request(11, 77, 0)); assert(!session.prove(11, 78, 1)); assert(session.prove(11, 77, 2));
assert(session.admit(11, 33, 33, 100)); assert(!session.admit(11, 33, 33, 100));
assert(!session.admit(11, 33, 33, 99)); assert(!session.admit(11, 34, 33, 101));
assert(session.request(22, 88, 3)); assert(!session.prove(22, 77, 4));
assert(session.remote == 11 && session.received == 100);
assert(session.prove(22, 88, 5)); assert(session.remote == 22 && !session.received);
assert(session.request(11, 99, 6)); assert(!session.prove(11, 77, 7));
assert(session.remote == 22); // Recorded retired sessions cannot flip it back.
assert(session.prove(11, 99, 8)); assert(session.admit(11, 33, 33, 5));
assert(session.request(11, 42, 9)); assert(session.prove(11, 42, 10)); assert(session.received == 5);
assert(session.request(22, 123, 11)); assert(!session.prove(22, 123, 5011));
uint32_t saturated = UINT32_MAX; udp::increment(saturated); assert(saturated == UINT32_MAX);
}
static void isolation() {
Node a(1,2), b(2,1); a.start(); b.start();
assert(udp_test::receive_cap == 768); assert(!a.bridge.sendTest(0, 1));
run(a,b); assert(a.status().find("ready=1") != std::string::npos);
assert(b.status().find("ready=1") != std::string::npos);
const unsigned allocations = b.manager.allocations;
assert(a.bridge.sendTest(0,0xa1b2c3d4)); run(a,b,5);
assert(b.stats("test").find("rx=1") != std::string::npos);
assert(b.stats("test").find("rxid=A1B2C3D4") != std::string::npos);
assert(b.manager.allocations == allocations && b.manager.received.empty());
const auto packet = lastKind(udp::Kind::Probe,1);
const auto before = udp_test::sent.size(); udp_test::inject(b.fd,packet); b.bridge.loop();
assert(udp_test::sent.size() == before); assert(b.stats("test").find("rx=1") != std::string::npos);
assert(b.stats("drops").find("replay=1") != std::string::npos);
auto forged = packet; forged.bytes.back() ^= 1; udp_test::inject(b.fd,forged); b.bridge.loop();
assert(b.stats("drops").find("auth=1") != std::string::npos);
forged = packet; forged.source.sin_port = htons(4405); udp_test::inject(b.fd,forged); b.bridge.loop();
assert(b.stats("drops").find("foreign=1") != std::string::npos);
assert(b.manager.allocations == allocations);
}
static void recovery() {
Node a(1,2), b(2,1); a.start(); b.start(); run(a,b);
assert(a.bridge.sendTest(0,1)); run(a,b,5); const auto old = lastKind(udp::Kind::Probe,1);
const auto old_hello = lastKind(udp::Kind::Hello,1);
const auto old_proof = lastKind(udp::Kind::Proof,1);
a.bridge.end(); a.start(); run(a,b,120);
assert(a.bridge.sendTest(0,2)); run(a,b,5);
assert(b.stats("test").find("rx=2") != std::string::npos);
udp_test::inject(b.fd,old); udp_test::inject(b.fd,old_hello); b.bridge.loop();
udp_test::inject(b.fd,old_proof); b.bridge.loop();
assert(b.stats("test").find("rx=2") != std::string::npos);
assert(a.bridge.sendTest(0,3)); run(a,b,5);
assert(b.stats("test").find("rx=3") != std::string::npos);
// A fresh receiver boot rejects all recorded old-recipient frames.
b.bridge.end(); b.start(); run(a,b,120);
udp_test::inject(b.fd,old); b.bridge.loop();
assert(b.stats("test").find("rx=3") != std::string::npos);
assert(a.bridge.sendTest(0,4)); run(a,b,5);
assert(b.stats("test").find("rx=4") != std::string::npos);
}
static void queues() {
Node a(1,2), b(2,1); a.start(); b.start(); run(a,b);
mesh::Packet packet; packet.payload = {1,0,3,4};
a.bridge.sendPacket(&packet); a.bridge.sendPacket(&packet); run(a,b,5);
assert(b.manager.received.size() == 1);
assert(a.stats("tx").find("queued=1") != std::string::npos);
// Same payload received through LoRa is deduplicated; received UDP packets
// are marked locally and cannot re-export even for OTA.
packet.udp_bridge_ingress = true; packet.payload[0] = PAYLOAD_TYPE_OTA << 2;
a.bridge.sendPacket(&packet); run(a,b,2); assert(b.manager.received.size() == 1);
packet.udp_bridge_ingress = false;
b.manager.starved = true;
a.bridge.sendPacket(&packet); run(a,b,3);
const unsigned held = b.manager.allocations; assert(held > 1 && b.manager.received.size() == 1);
const int reads = udp_test::receives; b.bridge.loop();
assert(udp_test::receives == reads); // Held authenticated bytes own the RX slot.
b.manager.starved = false; b.bridge.loop(); assert(b.manager.received.size() == 2);
// Failed UDP send is final, never automatically retried.
packet.payload[3]++; a.bridge.sendPacket(&packet); udp_test::fail_send = true;
a.bridge.loop(); const int sends = udp_test::sends; run(a,b,2);
assert(udp_test::sends == sends); assert(a.stats("tx").find("err=1") != std::string::npos);
// Unauthenticated noise, including oversized frames, never borrows the pool.
auto noise = lastKind(udp::Kind::Mesh,1); noise.bytes.assign(600,0xaa);
const unsigned allocations = b.manager.allocations; udp_test::inject(b.fd,noise); b.bridge.loop();
assert(b.manager.allocations == allocations);
// A valid maximum-size HMAC prefix followed by extra bytes must not become
// a valid datagram when recvfrom truncates the oversized UDP payload.
uint8_t payload[255]{}; payload[0] = 1;
udp::Frame shape{udp::Kind::Mesh,1,2,1,payload,sizeof(payload)};
uint8_t encoded[udp::MAX_FRAME];
const auto size = udp::encode(encoded,sizeof(encoded),shape,hmac,a.prefs.bridge_secret);
noise.bytes.assign(encoded,encoded+size); noise.bytes.push_back(0); noise.bytes.resize(600,0);
udp_test::inject(b.fd,noise); b.bridge.loop(); assert(b.manager.allocations == allocations);
packet.allowed = false; packet.payload[3]++; a.bridge.sendPacket(&packet);
assert(a.stats("drops").find("txfilter=2") != std::string::npos);
}
static void fanout() {
Node source(1,2); std::vector<std::unique_ptr<Node>> peers;
for (uint8_t i = 0; i < udp::MAX_PEERS; ++i) {
source.config.peers[i] = peer(i+2);
peers.emplace_back(new Node(i+2,1)); peers.back()->start();
}
source.start();
auto cycle = [&]() { test_now += 100; source.bridge.loop(); for (auto& peer : peers) peer->bridge.loop(); };
for (unsigned i=0;i<100;++i) cycle();
assert(source.status().find("ready=8") != std::string::npos);
mesh::Packet packet; packet.payload = {1,0,12}; source.bridge.sendPacket(&packet);
// New data cannot overwrite the bytes already owned by the fan-out slot.
packet.payload[2]++; source.bridge.sendPacket(&packet);
assert(source.stats("tx").find("busy=1") != std::string::npos);
for (unsigned i=0;i<10;++i) cycle();
for (auto& peer : peers) assert(peer->manager.received.size() == 1 && peer->manager.received[0][2] == 12);
assert(source.stats("tx").find("sent=8") != std::string::npos);
assert(source.bridge.sendTest(7,0x76543210));
for (unsigned i=0;i<4;++i) cycle();
for (unsigned i=0;i<8;++i) {
assert(peers[i]->stats("test").find(i==7 ? "rx=1" : "rx=0") != std::string::npos);
assert(peers[i]->manager.received.size() == 1);
}
}
static void rotation() {
Node a(1,2), b(2,1); a.start(); b.start(); run(a,b);
assert(a.bridge.sendTest(0,1)); run(a,b,5);
const auto recorded = lastKind(udp::Kind::Probe,1);
assert(a.bridge.sendTest(0,2)); // Old-key queued probe must be canceled.
strcpy(a.prefs.bridge_secret,"changed-key");
udp_test::creating_ip=address(a.id); a.bridge.serviceNetwork(true,address(a.id),0x00ffffff); a.fd=udp_test::next_socket-1;
run(a,b,60);
assert(b.stats("test").find("rx=1") != std::string::npos);
assert(a.status().find("ready=0") != std::string::npos);
strcpy(b.prefs.bridge_secret,"changed-key");
udp_test::creating_ip=address(b.id); b.bridge.serviceNetwork(true,address(b.id),0x00ffffff); b.fd=udp_test::next_socket-1; run(a,b,120);
assert(a.status().find("ready=1") != std::string::npos);
assert(a.bridge.sendTest(0,3)); run(a,b,5);
assert(b.stats("test").find("rx=2") != std::string::npos);
const auto allocations = b.manager.allocations;
b.bridge.resetStats();
udp_test::inject(b.fd,recorded); b.bridge.loop();
assert(b.stats("test").find("rx=0") != std::string::npos && b.manager.allocations == allocations);
assert(b.stats("drops").find("auth=1") != std::string::npos);
assert(a.bridge.sendTest(0,4)); a.prefs.bridge_secret[0]=0;
udp_test::creating_ip=address(a.id); a.bridge.serviceNetwork(true,address(a.id),0x00ffffff); a.fd=udp_test::next_socket-1;
assert(!a.bridge.isRunning()); run(a,b,5);
assert(b.stats("test").find("rx=0") != std::string::npos);
}
static void network() {
Node a(1,2), b(2,1);
a.bridge.begin(); a.bridge.serviceNetwork(false); assert(!a.bridge.isRunning() && !udp_test::opens);
a.start(); b.start(); run(a,b);
assert(a.bridge.sendTest(0,1)); a.bridge.serviceNetwork(false);
assert(!a.bridge.isRunning()); a.bridge.loop(); assert(a.stats("test").find("sent=0") != std::string::npos);
a.start(); run(a,b,120); assert(a.status().find("ready=1") != std::string::npos);
a.bridge.end(); a.prefs.bridge_secret[0] = 0; const int opened = udp_test::opens;
a.start(); assert(!a.bridge.isRunning() && udp_test::opens == opened);
strcpy(a.prefs.bridge_secret,"unit-test-key");
test_now += 5000; udp_test::fail_rcvbuf = true; a.start();
assert(!a.bridge.isRunning()); udp_test::fail_rcvbuf = false;
a.config.peers[0] = peer(255); test_now += 5000; a.start(); assert(!a.bridge.isRunning());
a.config.peers[0] = peer(1); test_now += 5000; a.start(); assert(!a.bridge.isRunning());
a.config.peers[0] = peer(2); test_now = 0xf0000000; a.bridge.serviceNetwork(false); a.start();
assert(a.bridge.isRunning()); const auto sends = udp_test::sends; test_now += 1000; a.bridge.loop(); assert(udp_test::sends > sends);
}
static void bounds() {
Node a(1,2), b(2,1); a.start(); b.start(); run(a,b);
for (const char* page : {"", "rx", "tx", "drops", "test", "unknown"}) {
for (size_t capacity = 1; capacity <= 157; ++capacity) {
char bytes[160]; memset(bytes,0x55,sizeof(bytes));
a.bridge.formatStats(page,bytes + 1,capacity);
assert(bytes[0] == 0x55 && bytes[capacity+1] == 0x55 && bytes[capacity] == 0);
a.bridge.formatStatus(bytes + 1,capacity);
assert(bytes[0] == 0x55 && bytes[capacity+1] == 0x55 && bytes[capacity] == 0);
}
}
mesh::logging_enabled = true; mesh::debug_enabled = false;
test_now += 1000; a.bridge.loop(); assert(!mesh::debug_stream.writes);
mesh::debug_enabled = true; a.bridge.loop(); assert(mesh::debug_stream.writes == 1);
const auto bytes = std::string(mesh::debug_stream.bytes.begin(),mesh::debug_stream.bytes.end());
assert(bytes.find("unit-test-key") == std::string::npos);
const unsigned count = mesh::debug_stream.writes; a.bridge.loop(); assert(mesh::debug_stream.writes == count);
a.bridge.resetStats(); assert(a.stats("test").find("rx=0") != std::string::npos);
}
int main(int argc, char** argv) {
assert(argc == 2); const std::string name = argv[1];
if (name == "codec") codec(); else if (name == "isolation") isolation();
else if (name == "recovery") recovery(); else if (name == "queues") queues();
else if (name == "rotation") rotation(); else if (name == "network") network(); else if (name == "fanout") fanout(); else if (name == "bounds") bounds(); else assert(false);
printf("%s passed\n",argv[1]);
}
+5 -4
View File
@@ -10,7 +10,7 @@ ROOT = Path(__file__).resolve().parents[1]
class BridgeDebugFormatterTests(unittest.TestCase):
def test_rs232_only_native_usb_has_bounded_runtime_gated_output(self):
def test_bridge_only_native_usb_has_bounded_runtime_gated_output(self):
compiler = shutil.which("g++") or shutil.which("clang++")
self.assertIsNotNone(compiler)
header = (ROOT / "src/MeshCore.h").read_text(encoding="ascii")
@@ -68,15 +68,16 @@ int main() {
source = work / "main.cpp"
source.write_text(harness, encoding="ascii")
for name, flags in (
("nrf52", ["-DNRF52_PLATFORM=1"]),
("esp32", ["-DMESH_ESP32_USB_CONSOLE_COOPERATIVE=1"]),
("nrf52-rs232", ["-DNRF52_PLATFORM=1", "-DWITH_RS232_BRIDGE=1"]),
("esp32-rs232", ["-DMESH_ESP32_USB_CONSOLE_COOPERATIVE=1", "-DWITH_RS232_BRIDGE=1"]),
("esp32-udp", ["-DMESH_ESP32_USB_CONSOLE_COOPERATIVE=1", "-DWITH_UDP_BRIDGE=1"]),
):
with self.subTest(platform=name):
binary = work / name
built = subprocess.run([
compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-fsanitize=address,undefined", "-fno-sanitize-recover=all",
"-fno-pie", "-no-pie", "-DARDUINO=1", "-DWITH_RS232_BRIDGE=1",
"-fno-pie", "-no-pie", "-DARDUINO=1",
"-DMESH_DEBUG=0", "-DBRIDGE_DEBUG=0", "-DPOWERSAVING_DEBUG=0",
*flags, "-I" + str(work), str(source), "-o", str(binary),
], capture_output=True, text=True, timeout=30)
+1 -1
View File
@@ -139,7 +139,7 @@ def code():
"int esp_now_deinit() { assert(espnow_active); if(failure==18)return -1; espnow_active=false; return ESP_OK; }")
base = (ROOT / "src/helpers/bridges/BridgeBase.cpp").read_text()
base_header = (ROOT / "src/helpers/bridges/BridgeBase.h").read_text()
ingress_hook = "#if defined(WITH_ESPNOW_BRIDGE) || defined(WITH_RS232_BRIDGE)\n"
ingress_hook = "#if defined(WITH_ESPNOW_BRIDGE) || defined(WITH_RS232_BRIDGE) || defined(WITH_UDP_BRIDGE)\n"
ingress_hook += method(base_header, "enum class IngressResult") + ";\n"
ingress_hook += method(base_header, "virtual void onIngressResult(") + "\n#endif\n"
base_methods = "\n".join(method(base, signature) for signature in (
+25
View File
@@ -1364,6 +1364,23 @@ apply_esp32_full_shared_bridge_profile Station_G2_repeater_observer_mqtt
|| fail "Station G2 Full MQTT image did not report ESP-NOW capability"
[[ "$PLATFORMIO_BUILD_FLAGS" != *"ESPNOW_BRIDGE_MERGED"* ]] \
|| fail "Station G2 existing MQTT default changed during ESP-NOW composition"
[[ "$PLATFORMIO_BUILD_FLAGS" == *"-DWITH_UDP_BRIDGE=1"* ]] \
|| fail "Station G2 Full repeater did not enable UDP bridge support"
[[ "$PLATFORMIO_BUILD_SRC_FILTER" == *"helpers/bridges/UDPBridge.cpp"* \
&& "$PLATFORMIO_BUILD_SRC_FILTER" == *"helpers/UDPBridgeSettings.cpp"* ]] \
|| fail "Station G2 Full repeater did not include UDP bridge and settings sources"
[[ " ${BUILD_CAPABILITIES[*]} " == *" bridge.udp "* ]] \
|| fail "Station G2 Full repeater omitted the UDP capability"
(
PLATFORMIO_BUILD_FLAGS=""
PLATFORMIO_BUILD_SRC_FILTER=""
BUILD_CAPABILITIES=()
apply_esp32_full_shared_bridge_profile Station_G2_room_server_observer_mqtt
[[ "$PLATFORMIO_BUILD_FLAGS" != *"WITH_UDP_BRIDGE"* \
&& "$PLATFORMIO_BUILD_SRC_FILTER" != *"UDPBridge"* ]] \
|| fail "room server acquired unsupported UDP repeater wiring"
)
[[ "$(get_unified_full_infrastructure_target Station_G2_repeater_bridge_espnow)" \
= "Station_G2_repeater_observer_mqtt" ]] \
|| fail "Station G2 Full ESP-NOW target did not resolve to the combined MQTT recipe"
@@ -1389,12 +1406,20 @@ for plain_target in heltec_v4_tft_repeater Heltec_v2_repeater \
|| fail "$plain_target Full image omitted the actual ESP-NOW driver source"
[[ " ${BUILD_CAPABILITIES[*]} " == *" bridge.espnow "* ]] \
|| fail "$plain_target Full image omitted the ESP-NOW capability"
[[ "$PLATFORMIO_BUILD_FLAGS" == *"-DWITH_UDP_BRIDGE=1"* \
&& "$PLATFORMIO_BUILD_SRC_FILTER" == *"helpers/bridges/UDPBridge.cpp"* ]] \
|| fail "$plain_target Full image omitted default-off UDP support"
[[ " ${BUILD_CAPABILITIES[*]} " == *" bridge.udp "* ]] \
|| fail "$plain_target Full image omitted UDP capability"
(
BUILD_PROFILE_FOR_TARGET=full
BUILD_EXPECTATIONS=()
declare_build_capability_contract "$plain_target" ESP32_PLATFORM
[[ " ${BUILD_EXPECTATIONS[*]} " == *"bridge.espnow=_ZN12ESPNowBridge5beginEv"* ]] \
|| fail "$plain_target Full image does not require a linked ESP-NOW driver"
[[ " ${BUILD_EXPECTATIONS[*]} " == *"bridge.udp=_ZN9UDPBridge5beginEv"* ]] \
|| fail "$plain_target Full image does not require a linked UDP driver"
)
done
for existing_target in Heltec_v2_repeater_bridge_espnow \
+21 -4
View File
@@ -516,6 +516,19 @@ int main() {
}, "v3_repeater")
self.assertEqual(alternate, combined)
def test_udp_bridge_budget_counts_enabled_case_alongside_existing_owners(self):
flags = {"WITH_MQTT_BRIDGE": 1, "WITH_RS232_BRIDGE": "Serial2"}
previous = ram.requirements("ESP32_PLATFORM", flags, "v3_repeater")
udp = ram.requirements("ESP32_PLATFORM", {**flags, "WITH_UDP_BRIDGE": 1}, "v3_repeater")
self.assertEqual(udp["required_heap_bytes"] - previous["required_heap_bytes"], 5120)
self.assertEqual(udp["components"]["udp_bridge_and_socket"], 3072 + 2048)
self.assertEqual(udp["components"]["wireless_stacks"], previous["components"]["wireless_stacks"])
self.assertEqual(udp["components"]["uart_bridge_and_driver"], previous["components"]["uart_bridge_and_driver"])
# UDP without MQTT must still reserve the shared WiFi stack.
udp_only = ram.requirements("ESP32_PLATFORM", {"WITH_UDP_BRIDGE": 1}, "v4_repeater")
self.assertEqual(udp_only["components"]["wireless_stacks"], 49152)
self.assertEqual(udp_only["components"]["udp_bridge_and_socket"], 5120)
def test_mqtt_startup_gate_distinguishes_maximum_slot_stack_buffer_capacity(self):
flags = {"WITH_MQTT_BRIDGE": 1}
plain = ram.requirements("ESP32_PLATFORM", flags, "v4_companion")
@@ -890,6 +903,10 @@ int main() {
("src/helpers/bridges/RS232Bridge.cpp", "RS232Bridge::RS232Bridge(",
"struct RS232Bridge { unsigned char bytes[OBJECT_BYTES]; };\n",
((2816, 0, True), (2817, 0, False))),
("src/helpers/bridges/UDPBridge.cpp", "UDPBridge::UDPBridge(",
"namespace mesh { namespace bridge { namespace udp {} } }\n"
"struct UDPBridge { unsigned char bytes[OBJECT_BYTES]; };\n",
((3072, 0, True), (3073, 0, False))),
("src/helpers/ui/SSD1306Display.cpp", "bool SSD1306Display::i2c_probe(",
"struct SSD1306Display { unsigned char bytes[OBJECT_BYTES]; "
"static constexpr unsigned FRAMEBUFFER_BYTES = FRAME_BYTES; };\n",
@@ -904,8 +921,8 @@ int main() {
prefix = (ROOT / path).read_text().split(end, 1)[0]
prefix = "\n".join(line for line in prefix.splitlines()
if not line.lstrip().startswith("#include"))
if path.endswith("RS232Bridge.cpp"):
prefix += "\n#endif\n" # WITH_RS232_BRIDGE also encloses the methods below.
if path.endswith(("RS232Bridge.cpp", "UDPBridge.cpp")):
prefix += "\n#endif\n" # The feature guard also encloses the methods below.
for platform in (("ESP32=1", "ESP32_PLATFORM=1"),
("NRF52_PLATFORM=1", "RAK4631_COMBINED_ETHERNET=1"),
("NRF52_PLATFORM=1",)):
@@ -913,12 +930,12 @@ int main() {
with self.subTest(path=path, platform=platform, size=size, frame=frame):
result = subprocess.run([
"c++", "-std=c++17", "-fsyntax-only", "-x", "c++", "-",
"-DWITH_RS232_BRIDGE=1", f"-DOBJECT_BYTES={size}",
"-DWITH_RS232_BRIDGE=1", "-DWITH_UDP_BRIDGE=1", f"-DOBJECT_BYTES={size}",
f"-DFRAME_BYTES={frame}", *["-D" + flag for flag in platform],
], input=stand_in + prefix, text=True, capture_output=True)
enforced = "ESP32=1" in platform or (
"RAK4631_COMBINED_ETHERNET=1" in platform
and not path.endswith("SH1106Display.cpp"))
and not path.endswith(("SH1106Display.cpp", "UDPBridge.cpp")))
self.assertEqual(result.returncode == 0, bounded or not enforced,
result.stderr)
+2 -2
View File
@@ -216,7 +216,7 @@ int main() {
# Importers use this shared harness directly, so expand its production hook
# here rather than leaving fixture setup to one test's compilation path.
_base_header = (ROOT / 'src/helpers/bridges/BridgeBase.h').read_text(encoding='ascii')
_ingress_hook = '#if defined(WITH_ESPNOW_BRIDGE) || defined(WITH_RS232_BRIDGE)\n'
_ingress_hook = '#if defined(WITH_ESPNOW_BRIDGE) || defined(WITH_RS232_BRIDGE) || defined(WITH_UDP_BRIDGE)\n'
_ingress_hook += method(_base_header, 'enum class IngressResult') + ';\n'
_ingress_hook += method(_base_header, 'virtual void onIngressResult(') + '\n#endif\n'
HARNESS = HARNESS.replace('@INGRESS_HOOK@', _ingress_hook)
@@ -410,7 +410,7 @@ int main() {
(work/'Mesh.h').write_text(PACKET)
code = '#include <initializer_list>\n' + HARNESS.replace('@METHODS@', '\n'.join(methods))
(work/'test.cpp').write_text(code)
for macros in [[], ['-DWITH_ESPNOW_BRIDGE=1']]:
for macros in [[], ['-DWITH_ESPNOW_BRIDGE=1'], ['-DWITH_UDP_BRIDGE=1']]:
with self.subTest(macros=macros):
result = subprocess.run(['g++', '-std=c++17', '-Wall', '-Wextra', '-Werror',
*macros, '-I', str(work), '-I', str(ROOT/'src'), str(work/'test.cpp'),
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env python3
"""Sanitized production UDP bridge protocol, sessions, queues and diagnostics."""
from pathlib import Path
import os
import shutil
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
FIXTURES = ROOT / 'test/fixtures/udp_bridge'
class UDPBridgeTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
compiler = shutil.which('g++') or shutil.which('clang++')
if not compiler:
raise AssertionError('a host C++ compiler is required')
cls.temporary = tempfile.TemporaryDirectory(prefix='udp-bridge-')
cls.binaries = []
for cooperative in (0, 1):
binary = Path(cls.temporary.name) / f'bridge-{cooperative}'
built = subprocess.run([
compiler, '-std=c++17', '-Wall', '-Wextra', '-Werror', '-O1',
'-fsanitize=address,undefined', '-fno-sanitize-recover=all',
'-fno-pie', '-no-pie', '-DARDUINO=1', '-DESP32=1', '-DWITH_UDP_BRIDGE=1',
f'-DMESH_ESP32_USB_CONSOLE_COOPERATIVE={cooperative}',
'-I' + str(FIXTURES), '-I' + str(ROOT / 'test/mocks'), '-I' + str(ROOT / 'src'),
str(ROOT / 'test/fixtures/udp_bridge_runtime.cpp'),
str(ROOT / 'src/helpers/bridges/UDPBridge.cpp'), '-o', str(binary),
], capture_output=True, text=True, timeout=60)
if built.returncode:
cls.temporary.cleanup()
raise AssertionError(built.stdout + built.stderr)
cls.binaries.append(binary)
@classmethod
def tearDownClass(cls):
cls.temporary.cleanup()
def check(self, name):
for binary in self.binaries:
with self.subTest(console=binary.name):
result = subprocess.run([str(binary), name], capture_output=True, text=True, timeout=30)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn(name + ' passed', result.stdout)
def test_strict_authenticated_codec_and_fresh_challenge_replay_guard(self):
self.check('codec')
def test_authenticated_probes_never_allocate_echo_or_reach_lora(self):
self.check('isolation')
def test_peer_and_receiver_restart_reject_retired_sessions_and_recordings(self):
self.check('recovery')
def test_owned_queues_pool_pressure_filter_and_no_retry(self):
self.check('queues')
def test_eight_peer_fanout_owned_bytes_and_targeted_probe(self):
self.check('fanout')
def test_secret_rotation_cancels_queued_work_refreshes_sessions_and_rejects_old_key(self):
self.check('rotation')
def test_network_gate_disconnect_secret_broadcast_and_socket_budget_failure(self):
self.check('network')
def test_bounded_status_pages_and_both_usb_debug_gates(self):
self.check('bounds')
if __name__ == '__main__':
unittest.main()
+65
View File
@@ -0,0 +1,65 @@
"""Exercise production UDP preference parsing, atomic persistence and CLI."""
from pathlib import Path
import os
import shutil
import subprocess
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
FIXTURE = ROOT / "test/fixtures/udp_bridge"
class UDPBridgeConfigTests(unittest.TestCase):
def compile_run(self, source, settings=False, extra_flags=()):
compiler = shutil.which("g++") or shutil.which("clang++")
self.assertIsNotNone(compiler)
with tempfile.TemporaryDirectory(prefix="udp-config-") as directory:
work = Path(directory)
flags = []
sources = []
if settings:
original = ROOT / "test/fixtures/guest_login/mocks"
mocks = work / "mocks"
shutil.copytree(original, mocks)
for path in mocks.rglob("*"):
if path.is_file():
path.write_text(path.read_text().replace("/guest_login", "/udp_bridge"))
flags = ["-DWITH_UDP_BRIDGE=1", "-DESP32_PLATFORM", "-DESP32", "-I", str(mocks)]
sources = [str(ROOT / "src/helpers/UDPBridgeSettings.cpp")]
main = work / "main.cpp"
main.write_text(source, encoding="ascii")
binary = work / "check"
result = subprocess.run([compiler, "-std=c++17", "-O1", "-g", "-Wall", "-Wextra", "-Werror",
"-Wno-misleading-indentation", "-Wno-sign-compare", "-fsanitize=address,undefined",
"-fno-omit-frame-pointer", "-no-pie", *flags, *extra_flags, "-I", str(ROOT / "src"), str(main), *sources,
"-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=30)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def test_parser_codec_and_transaction_boundaries(self):
self.compile_run((FIXTURE / "settings.cpp").read_text(encoding="ascii"), settings=True)
def test_actual_cli_dispatch(self):
header = (ROOT / "src/helpers/CommonCLI.h").read_text(encoding="ascii")
source = (ROOT / "src/helpers/CommonCLI.cpp").read_text(encoding="ascii")
body = extract_braced(source, "bool CommonCLI::handleUDPBridgeCommand(")
callbacks = "\n".join(extract_braced(header, signature) for signature in (
"virtual const mesh::bridge::udp::Prefs* getUDPBridgePrefs(",
"virtual bool setUDPBridgePrefs(", "virtual void formatUDPBridgeStatus(",
"virtual void formatUDPBridgeStats(", "virtual bool sendUDPBridgeTest(",
"virtual bool resetUDPBridgeStats("))
self.assertIn("if (handleUDPBridgeCommand(sender_timestamp, command, reply)) return;", source)
fixture = (FIXTURE / "cli.cpp").read_text(encoding="ascii")
self.compile_run(fixture.replace("CALLBACKS_HERE", callbacks).replace("HANDLER_HERE", body))
def test_browser_ota_closes_udp_before_start_and_restores_only_saved_intent(self):
source = (ROOT / "src/helpers/CommonCLI.cpp").read_text(encoding="ascii")
start = source.index(' } else if (memcmp(command, "start ota", 9)')
end = source.index(' } else if (memcmp(command, "clock", 5)', start)
fixture = (FIXTURE / "ota.cpp").read_text(encoding="ascii")
self.compile_run(fixture.replace("OTA_HERE", source[start:end]), extra_flags=["-DWITH_UDP_BRIDGE=1"])
if __name__ == "__main__":
unittest.main()
+243
View File
@@ -0,0 +1,243 @@
#!/usr/bin/env python3
"""Execute production repeater UDP lifecycle with shared station owners."""
from pathlib import Path
import os
import subprocess
import tempfile
import unittest
from cpp_source import body
ROOT = Path(__file__).resolve().parents[1]
HARNESS = r'''
#include <cassert>
#include <cstdio>
#include <cstring>
#include <cstdint>
#include <new>
#include <initializer_list>
#include <helpers/UDPBridgePrefs.h>
#include <helpers/WiFiNetworkPowerPolicy.h>
#include <helpers/WirelessControl.h>
uint32_t now_ms=1000;
uint32_t millis(){return now_ms;}
bool millisHasNowPassed(uint32_t value){return static_cast<int32_t>(now_ms-value)>=0;}
enum {WIFI_OFF=0,WIFI_STA=1,WIFI_AP=2,WIFI_AP_STA=3,WL_IDLE_STATUS=0,WL_CONNECTED=3};
enum {WIFI_IF_STA=0,ESP_OK=0,WIFI_PS_NONE=0,WIFI_PS_MIN_MODEM=1,WIFI_PS_MAX_MODEM=2};
struct {
int current=WIFI_OFF,state=WL_IDLE_STATUS,begins=0,modes=0; uint32_t ip=0,mask=0x00ffffff;
int getMode() const{return current;}
int status()const{return state;}
uint32_t localIP()const{return ip;}
uint32_t subnetMask()const{return mask;}
void persistent(bool){}
bool mode(int value){++modes;current=value;return true;}
} WiFi;
using wifi_mode_t=int;
constexpr int WIFI_MODE_NULL=0,WIFI_MODE_STA=1;
int sdk_override=-1;
int esp_wifi_get_mode(wifi_mode_t*value){*value=sdk_override<0?WiFi.current:sdk_override;return ESP_OK;}
void esp_wifi_set_ps(int){}
namespace mesh { namespace wifi {
constexpr uint8_t kDefaultPowerSave=1,kPowerSaveNone=1,kPowerSaveMax=2;
struct Credentials {char ssid[32]="test";char password[65]="";uint8_t power_save=1;};
enum class CredentialState {Ready,Absent};
bool have_credentials=true;
CredentialState resolveCredentials(Credentials&,const char* =nullptr,const char* =nullptr,uint8_t=1){
return have_credentials?CredentialState::Ready:CredentialState::Absent;
}
bool stationMutationAllowed(){return !(WiFi.current&WIFI_AP);}
int applyProtocolMask(int){return ESP_OK;}
void setStationAutoReconnect(bool){}
void beginStation(const char*,const char*){++WiFi.begins;}
uint8_t effectivePowerSave(uint8_t value,bool,bool){return value;}
bool espNowChannelConstrained(){return false;}
}}
struct NodePrefs {char bridge_secret[16]="test-secret";};
struct FakeSettings {
mesh::bridge::udp::Prefs data;
bool health=true,save_ok=true;unsigned writes=0;
bool healthy()const{return health;}
const mesh::bridge::udp::Prefs& prefs()const{return data;}
bool setPrefs(const mesh::bridge::udp::Prefs& value){++writes;if(!save_ok)return false;data=value;return true;}
};
struct Board {bool ota=false;bool isOTAUpdateRunning()const{return ota;}};
struct ObserverPrefs {const char*wifi_ssid="test";const char*wifi_password="";uint8_t wifi_power_save=1;};
struct CLI {Board board;ObserverPrefs observer;Board*getBoard(){return &board;}const ObserverPrefs*getObserverPrefs(){return &observer;}};
struct MQTT {bool running=false,stopping=false;bool isRunning()const{return running;}bool isStopping()const{return stopping;}};
struct UDPBridge {
static int allocations;bool running=false;int loops=0,closes=0;
uint32_t address=0,mask=0;
UDPBridge(NodePrefs*,void*,void*,const mesh::bridge::udp::Prefs*){++allocations;}
~UDPBridge(){--allocations;}
bool isRunning()const{return running;}
void serviceNetwork(bool ready,uint32_t ip=0,uint32_t netmask=0){running=ready;address=ip;mask=netmask;if(!ready)++closes;}
void loop(){assert(running);++loops;}
void end(){running=false;++closes;}
bool sendTest(uint8_t,uint32_t){return running;}
void resetStats(){}
void formatStatus(char*out,size_t cap){snprintf(out,cap,"> ready ip=10.0.0.1");}
void formatStats(const char*,char*,size_t){}
}; int UDPBridge::allocations=0;
struct MyMesh {
NodePrefs _prefs;void* _mgr=nullptr;CLI _cli;FakeSettings udp_bridge_settings;
UDPBridge*udp_bridge=nullptr;uint32_t udp_station_retry_at=0;bool udp_network_paused=false;
MQTT* mqtt_bridge=nullptr;bool web_active=false,web_stopping=false;
~MyMesh(){delete udp_bridge;}
bool isWebConfigActive(){return web_active;}
bool isWebConfigStopping(){return web_stopping;}
void*getRTCClock(){return nullptr;}
void configureBridgeFilter(UDPBridge*){}
bool ensureUDPBridge();bool isUDPBridgeNetworkEnabled()const;
bool setUDPBridgeNetworkState(bool);bool setUDPBridgePrefs(const mesh::bridge::udp::Prefs&);
void serviceUDPBridge();void formatUDPBridgeStatus(char*,size_t);
};
@METHODS@
struct Backend:mesh::wireless::Backend {
uint8_t available()const override{return mesh::wireless::WiFi;}
uint8_t enabled()const override{return mesh::wireless::WiFi;}
uint8_t clients()const override{return mesh::wireless::Independent;}
mesh::wireless::Result set(uint8_t,bool)override{return mesh::wireless::Result::Done;}
};
int main(){
MyMesh node;
char status[157];node.formatUDPBridgeStatus(status,sizeof(status));assert(strstr(status,"off ip=0.0.0.0"));
node.serviceUDPBridge();
assert(!node.udp_bridge&&!mesh::wifi::networkPower().foregroundActive()&&WiFi.begins==0);
auto prefs=node.udp_bridge_settings.data;prefs.enabled=1;
for(unsigned i=0;i<8;++i){prefs.peers[i].ip[0]=10;prefs.peers[i].ip[3]=i+2;prefs.peers[i].port=4404;}
assert(node.setUDPBridgePrefs(prefs));
assert(node.udp_bridge&&UDPBridge::allocations==1&&WiFi.begins==1&&WiFi.current==WIFI_STA);
assert(!node.udp_bridge->running&&mesh::wifi::networkPower().foregroundActive());
WiFi.state=WL_CONNECTED;WiFi.ip=0x0100000a;
node.serviceUDPBridge();
node.formatUDPBridgeStatus(status,sizeof(status));assert(strstr(status,"ready ip=10.0.0.1"));
assert(node.udp_bridge->running&&node.udp_bridge->loops==1&&WiFi.begins==1);
assert(node.udp_bridge->address==WiFi.ip&&node.udp_bridge->mask==WiFi.mask);
for(int mode:{WIFI_AP,WIFI_AP_STA}){
WiFi.current=mode;const int starts=WiFi.begins,modes=WiFi.modes;
now_ms+=16000;node.serviceUDPBridge();
assert(!node.udp_bridge->running&&WiFi.begins==starts&&WiFi.modes==modes);
}
WiFi.current=WIFI_STA;sdk_override=WIFI_AP_STA;node.serviceUDPBridge();assert(!node.udp_bridge->running);
sdk_override=-1;WiFi.ip=0;now_ms+=16000;node.serviceUDPBridge();
assert(!node.udp_bridge->running); // Association alone is not a usable IP.
WiFi.ip=0x0100000a;node.serviceUDPBridge();assert(node.udp_bridge->running);
node._cli.board.ota=true;node.serviceUDPBridge();
assert(!node.udp_bridge->running&&!mesh::wifi::networkPower().foregroundActive());
node._cli.board.ota=false;node.serviceUDPBridge();assert(node.udp_bridge->running);
assert(node.setUDPBridgeNetworkState(false));node.serviceUDPBridge();assert(!node.udp_bridge->running);
assert(node.setUDPBridgeNetworkState(true)&&node.udp_bridge->running);
WiFi.state=WL_IDLE_STATUS;now_ms+=16000;const int before_owner=WiFi.begins;
#ifdef WITH_WEBCONFIG
node.web_active=true;node.serviceUDPBridge();assert(WiFi.begins==before_owner&&!node.udp_bridge->running);
node.web_active=false;node.web_stopping=true;node.serviceUDPBridge();assert(WiFi.begins==before_owner);
node.web_stopping=false;
#endif
#ifdef WITH_MQTT_BRIDGE
MQTT mqtt;node.mqtt_bridge=&mqtt;mqtt.running=true;node.serviceUDPBridge();
assert(WiFi.begins==before_owner&&!node.udp_bridge->running);
mqtt.running=false;mqtt.stopping=true;node.serviceUDPBridge();assert(WiFi.begins==before_owner);
mqtt.stopping=false;
#endif
node.serviceUDPBridge();assert(WiFi.begins==before_owner+1);
WiFi.state=WL_CONNECTED;node.serviceUDPBridge();assert(node.udp_bridge->running);
// A failed MQTT destination must not park a healthy independent LAN bridge.
mesh::wifi::NetworkPowerPolicy policy;
assert(policy.update(now_ms,false,mesh::wifi::networkPower().foregroundActive())==mesh::wifi::NetworkPowerPolicy::Action::Run);
assert(policy.update(now_ms+600000,false,mesh::wifi::networkPower().foregroundActive())==mesh::wifi::NetworkPowerPolicy::Action::Run);
Backend backend;auto& control=mesh::wireless::control();control.begin(backend);char reply[160];
assert(control.handle("set wifi off force",reply,sizeof(reply),now_ms,mesh::wireless::Independent));
node.serviceUDPBridge();assert(!node.udp_bridge->running); // Honor off before reply-drain grace ends.
node.formatUDPBridgeStatus(status,sizeof(status));assert(strstr(status,"wifi-paused"));
now_ms+=250;control.service(now_ms);assert(control.blocked(mesh::wireless::WiFi));
node.serviceUDPBridge();assert(!node.udp_bridge->running&&!mesh::wifi::networkPower().foregroundActive());
node.setUDPBridgeNetworkState(true);assert(!node.udp_bridge->running); // No silent master-off bypass.
assert(control.handle("set wifi on",reply,sizeof(reply),now_ms,mesh::wireless::Independent));
now_ms+=250;control.service(now_ms);node.serviceUDPBridge();assert(node.udp_bridge->running);
assert(control.handle("set 2.4ghz off force",reply,sizeof(reply),now_ms,mesh::wireless::Independent));
node.serviceUDPBridge();assert(!node.udp_bridge->running&&control.masterOff());
now_ms+=250;control.service(now_ms);
assert(control.handle("set 2.4ghz on",reply,sizeof(reply),now_ms,mesh::wireless::Independent));
now_ms+=250;control.service(now_ms);node.serviceUDPBridge();assert(node.udp_bridge->running);
node._prefs.bridge_secret[0]=0;node.formatUDPBridgeStatus(status,sizeof(status));assert(strstr(status,"no-secret"));
node.serviceUDPBridge();assert(!node.udp_bridge->running);
strcpy(node._prefs.bridge_secret,"test-secret");node.serviceUDPBridge();assert(node.udp_bridge->running);
const auto before=node.udp_bridge_settings.data;node.udp_bridge_settings.save_ok=false;
auto failed=before;failed.port=1234;assert(!node.setUDPBridgePrefs(failed));
assert(node.udp_bridge_settings.data.port==before.port&&node.udp_bridge->running);
node.udp_bridge_settings.save_ok=true;prefs.enabled=0;
assert(node.setUDPBridgePrefs(prefs)&&!node.udp_bridge&&UDPBridge::allocations==0);
assert(!mesh::wifi::networkPower().foregroundActive());
}
'''
class UDPBridgeIntegrationTest(unittest.TestCase):
def test_wireless_master_restores_udp_station_without_starting_setup_ap(self):
backend=body((ROOT/'examples/InfrastructureWireless.h').read_text(),
'class InfrastructureWirelessBackend')+';'
fixture=r'''
#include <cassert>
#include <cstdint>
#include <vector>
#include <helpers/WirelessControl.h>
enum {WIFI_OFF=0,WIFI_STA=1,WIFI_IF_STA=0};
std::vector<int> events;
struct {bool ota=false;bool isOTAUpdateRunning()const{return ota;}
bool stopOTAUpdate(char*){ota=false;return true;}} board;
struct {int mode_=WIFI_STA;void setAutoReconnect(bool){}
void disconnect(bool,bool){events.push_back(2);}
void mode(int value){mode_=value;events.push_back(value==WIFI_OFF?3:4);}} WiFi;
namespace mesh {namespace wifi {void applyProtocolMask(int){} void restoreEspNowChannel(){}}}
struct {
bool paused=false,configured=true,mqtt=false,web=false;unsigned web_starts=0;
bool isUDPBridgeNetworkEnabled()const{return configured&&!paused;}
bool isUDPBridgeConfigured()const{return configured;}
bool setUDPBridgeNetworkState(bool enabled){paused=!enabled;events.push_back(enabled?5:1);if(enabled)WiFi.mode(WIFI_STA);return true;}
bool isMqttBridgeRunning()const{return mqtt;}
bool setMqttBridgeState(bool value){mqtt=value;return true;}
bool isWebConfigActive()const{return web;}
bool isWebConfigStopping()const{return false;}
bool hasWirelessNetworkClient()const{return false;}
bool stopWebConfig(char*){web=false;return true;}
bool startWebConfig(bool,char*){++web_starts;web=true;return true;}
} the_mesh;
@BACKEND@
int main(){
InfrastructureWirelessBackend backend;
assert(backend.enabled()&mesh::wireless::WiFi);
assert(backend.set(mesh::wireless::WiFi,false)==mesh::wireless::Result::Done);
assert(the_mesh.paused&&WiFi.mode_==WIFI_OFF&&events.front()==1);
assert(backend.set(mesh::wireless::WiFi,true)==mesh::wireless::Result::Done);
assert(!the_mesh.paused&&WiFi.mode_==WIFI_STA&&the_mesh.web_starts==0);
// An explicit first WiFi-on with configured UDP must not launch a setup AP.
InfrastructureWirelessBackend initial;
assert(initial.set(mesh::wireless::WiFi,true)==mesh::wireless::Result::Done);
assert(the_mesh.web_starts==0);
}
'''.replace('@BACKEND@',backend)
with tempfile.TemporaryDirectory() as tmp:
path=Path(tmp);(path/'backend.cpp').write_text(fixture)
binary=path/'backend'
subprocess.run([os.environ.get('CXX','g++'),'-std=c++17','-DESP32=1','-DWITH_UDP_BRIDGE=1',
'-DWITH_WEBCONFIG=1','-DWITH_MQTT_BRIDGE=1','-fsanitize=address,undefined',
'-fno-omit-frame-pointer','-fno-pie','-no-pie','-I',str(ROOT/'src'),
str(path/'backend.cpp'),'-o',str(binary)],check=True)
subprocess.run([str(binary)],check=True)
def test_station_owners_master_off_reconnect_and_broker_independence(self):
source=(ROOT/'examples/simple_repeater/MyMesh.cpp').read_text()
methods='\n'.join(body(source,signature) for signature in (
'bool MyMesh::ensureUDPBridge(', 'bool MyMesh::isUDPBridgeNetworkEnabled(',
'bool MyMesh::setUDPBridgeNetworkState(', 'bool MyMesh::setUDPBridgePrefs(',
'void MyMesh::serviceUDPBridge(', 'void MyMesh::formatUDPBridgeStatus('))
for flags in ((),('WITH_MQTT_BRIDGE',),('WITH_WEBCONFIG',),('WITH_MQTT_BRIDGE','WITH_WEBCONFIG')):
with self.subTest(flags=flags),tempfile.TemporaryDirectory() as tmp:
path=Path(tmp);(path/'test.cpp').write_text(HARNESS.replace('@METHODS@',methods))
binary=path/'test'
subprocess.run([os.environ.get('CXX','g++'),'-std=c++17','-fsanitize=address,undefined',
'-fno-omit-frame-pointer','-fno-pie','-no-pie','-I',str(ROOT/'src'),
*['-D'+flag+'=1' for flag in flags],str(path/'test.cpp'),'-o',str(binary)],check=True)
subprocess.run([str(binary)],check=True)
if __name__=='__main__':unittest.main()
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""Exercise the UDP origin marker through real packet, pool and radio paths."""
from pathlib import Path
import os
import shutil
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
class UDPPacketOriginTests(unittest.TestCase):
def test_origin_is_local_copies_with_retries_and_clears_on_reuse(self):
compiler = shutil.which("g++") or shutil.which("clang++")
self.assertIsNotNone(compiler)
fixture = (ROOT / "test/fixtures/dispatcher/startup_watchdog_test.cpp").read_text()
support = fixture[:fixture.index("static void delayedSetup()")]
support = support.replace(" using Dispatcher::setRadioAvailable;",
" using Dispatcher::setRadioAvailable;\n"
" using Dispatcher::tryParsePacket;\n"
" using Dispatcher::obtainNewPacket;")
checks = r'''
int main() {
mesh::Packet original;
original.header = (PAYLOAD_TYPE_OTA << PH_TYPE_SHIFT) | ROUTE_TYPE_FLOOD;
original.path_len = 0;
original.payload_len = 1;
original.payload[0] = 42;
uint8_t before[MAX_TRANS_UNIT], after[MAX_TRANS_UNIT];
const auto length = original.writeTo(before);
#if defined(WITH_UDP_BRIDGE)
assert(!original.udp_bridge_ingress);
original.udp_bridge_ingress = true;
mesh::Packet retry = original;
assert(retry.udp_bridge_ingress);
assert(original.writeTo(after) == length && memcmp(before, after, length) == 0);
assert(retry.readFrom(before, length) && !retry.udp_bridge_ingress);
retry.udp_bridge_ingress = true;
const uint8_t invalid[] = {0xC1, 0};
assert(!retry.readFrom(invalid, sizeof(invalid)) && !retry.udp_bridge_ingress);
StaticPoolPacketManager pool(1);
auto* packet = pool.allocNew();
assert(packet && !packet->udp_bridge_ingress);
packet->udp_bridge_ingress = true;
pool.free(packet);
auto* recycled = pool.allocNew();
assert(recycled == packet && !recycled->udp_bridge_ingress);
assert(pool.allocNew() == nullptr);
pool.free(recycled);
TestClock clock; TestRadio radio;
TestDispatcher dispatcher(radio, clock, pool);
auto* local = dispatcher.obtainNewPacket();
assert(local && !local->udp_bridge_ingress);
local->udp_bridge_ingress = true;
assert(dispatcher.tryParsePacket(local, before, length));
assert(!local->udp_bridge_ingress);
pool.free(local);
#else
assert(original.writeTo(after) == length && memcmp(before, after, length) == 0);
#endif
printf("%zu\n", sizeof(mesh::Packet));
}
'''
with tempfile.TemporaryDirectory(prefix="udp-packet-origin-") as temporary:
work = Path(temporary)
source = work / "main.cpp"
source.write_text(support + checks, encoding="ascii")
sizes = []
for enabled in (False, True):
with self.subTest(udp=enabled):
binary = work / ("udp" if enabled else "ordinary")
built = subprocess.run([
compiler, "-std=c++17", "-Wall", "-Wextra",
"-fsanitize=address,undefined", "-fno-sanitize-recover=all",
"-fno-pie", "-no-pie", *(["-DWITH_UDP_BRIDGE=1"] if enabled else []),
"-I" + str(ROOT / "test/mocks"), "-I" + str(ROOT / "src"),
str(ROOT / "src/Dispatcher.cpp"), str(ROOT / "src/Packet.cpp"),
str(ROOT / "src/helpers/StaticPoolPacketManager.cpp"),
str(source), "-o", str(binary),
], capture_output=True, text=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
environment = os.environ.copy()
# Firmware packet pools live for the entire boot.
environment["ASAN_OPTIONS"] = environment.get("ASAN_OPTIONS", "") + ":detect_leaks=0"
checked = subprocess.run([str(binary)], capture_output=True, text=True,
env=environment, timeout=10)
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
sizes.append(int(checked.stdout.strip()))
self.assertEqual(sizes[0], sizes[1], "UDP metadata must fit existing packet padding")
if __name__ == "__main__":
unittest.main()