Add station-only WiFi UDP bridge with eight authenticated peers
This commit is contained in:
1 parent
7bc3aeb234
commit
5e25faa3d0
45 files changed
+2699
-35
No files matched your search
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
inline uint32_t test_now = 0;
|
||||
inline uint32_t millis() { return test_now; }
|
||||
Vendored
+22
@@ -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; }
|
||||
};
|
||||
Vendored
+93
@@ -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
@@ -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; }
|
||||
@@ -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
@@ -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
|
||||
Vendored
+58
@@ -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
@@ -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));
|
||||
}
|
||||
Vendored
+245
@@ -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]);
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user