Add staggered transmit-only channel text beacons

This commit is contained in:
mikecarper committed 2026-10-14 09:58:24 -07:00
1 parent 8049754de9
commit 009bfff383
35 files changed
+3389 -19

No files matched your search

+364
View File
@@ -0,0 +1,364 @@
#include <helpers/BeaconManager.h>
#include <Mesh.h>
#include <algorithm>
#include <cassert>
#include <cstdio>
#include <cstring>
#include <initializer_list>
#include <string>
uint64_t beacon_test_time = 0;
constexpr uint64_t H = mesh::BeaconManager::HourMillis;
struct Fixture {
MemoryFS fs;
mesh::Mesh mesh;
mesh::RTCClock clock;
mesh::BeaconManager manager;
char reply[256] = {};
char name[32] = "Beacon test";
uint64_t now = 0;
explicit Fixture(uint32_t boot = 0) {
g_mock_millis = boot; beacon_test_time = 0;
manager.begin(&fs, &mesh, &clock, name);
}
void tick(uint64_t elapsed) {
now += elapsed; beacon_test_time = now; g_mock_millis += uint32_t(elapsed);
manager.loop(g_mock_millis);
}
const char* cmd(const std::string& command, bool ok = true) {
std::memset(reply, 0x55, sizeof(reply));
const bool handled = manager.handle(command.c_str(), reply, sizeof(reply));
assert(handled && std::memchr(reply, 0, sizeof(reply)));
const bool actual_ok = std::strncmp(reply, "Err", 3) != 0;
if (actual_ok != ok) { std::fprintf(stderr, "%s => %s\n", command.c_str(), reply); assert(false); }
return reply;
}
void mc(unsigned slot, const char* text = "Hello") {
cmd("set beacon " + std::to_string(slot) + " meshcore 906.5,62.5,7,5,14 " + text);
}
void all(unsigned hours) {
cmd("set beacon.interval " + std::to_string(hours));
for (unsigned i = 1; i <= 12; ++i) mc(i, std::to_string(i).c_str());
}
void mt(unsigned slot = 1) { cmd("set beacon " + std::to_string(slot) + " meshtext US LongFast 14 Hello MT"); }
};
uint32_t crc(const uint8_t* bytes, size_t size) {
uint32_t value = 0xffffffffU;
while (size--) {
value ^= *bytes++;
for (unsigned i = 0; i < 8; ++i) value = (value >> 1) ^ ((value & 1) ? 0xedb88320U : 0);
}
return value;
}
void put32(uint8_t* bytes, uint32_t value) {
for (unsigned i = 0; i < 4; ++i) bytes[i] = uint8_t(value >> (8 * i));
}
void fixCrc(std::vector<uint8_t>& bytes) { put32(bytes.data() + bytes.size() - 4, crc(bytes.data(), bytes.size() - 4)); }
void commands() {
Fixture f;
assert(!f.mesh.wake_enabled && f.fs.files.empty());
f.tick(48 * H);
assert(f.mesh.sends.empty() && f.fs.files.empty());
assert(std::strstr(f.cmd("get beacon.interval"), "24"));
assert(!mesh::BeaconManager::isCommand(nullptr));
for (const char* command : {"set beacons 1", "get beaconry", "set beacon.intervalx 12", "get radio", "send beacon 1"})
assert(!mesh::BeaconManager::isCommand(command));
for (const char* command : {"get beacon", "get beacon 1", "set beacon", "set beacon.interval", "delete beacon all", "del beacon 1"})
assert(mesh::BeaconManager::isCommand(command));
for (const char* command : {
"set beacon", "set beacon.interval", "get beacon.interval garbage", "set beacon.interval 0",
"set beacon.interval 13", "set beacon.interval 4294967296", "set beacon.interval 12 garbage",
"get beacon 0", "get beacon 13", "get beacon -1", "get beacon 4294967296", "get beacon 1 extra",
"delete beacon 1 extra", "set beacon 1 unknown 1 Hello",
"set beacon 1 meshcore nan,62.5,7,5,14 Hello", "set beacon 1 meshcore inf,62.5,7,5,14 Hello",
"set beacon 1 meshcore 906.5,nan,7,5,14 Hello", "set beacon 1 meshcore 906.5,62.5,256,5,14 Hello",
"set beacon 1 meshcore 906.5,62.5,7,5,128 Hello", "set beacon 1 meshcore 906.5,62.5,7,5,23 Hello",
"set beacon 1 meshcore 906.5,62.5,7,5,14,7 Hello", "set beacon 1 meshcore 906.5,62.5,7,5,14,65536 Hello",
"set beacon 1 meshcore 906.5,62.5,7,5,14,32,1 Hello", "set beacon 1 meshcore 906.5,62.5,7,5,14",
"set beacon 1 meshtext bad LongFast 14 Hello", "set beacon 1 meshtext US bad 14 Hello",
"set beacon 1 meshtext US LongFast 128 Hello"}) f.cmd(command, false);
f.cmd("set beacon 1 meshcore 906.5,62.5,7,5,14 " + std::string(80, 'A'));
assert(std::strstr(f.cmd("get beacon 1"), std::string(80, 'A').c_str()));
f.cmd("set beacon 1 meshcore 906.5,62.5,7,5,14 " + std::string(81, 'A'), false);
f.cmd("set beacon 1 meshcore 906.5,62.5,7,5,14 Hello\nthere", false);
f.cmd("set beacon 1 meshcore 906.5,62.5,7,5,14 \xc3\xa9");
assert(std::strstr(f.cmd("get beacon"), "1"));
f.cmd("delete beacon all");
assert(!f.mesh.wake_enabled);
char tiny[2] = {'x', 'x'};
assert(f.manager.handle("get beacon", tiny, sizeof(tiny)) && tiny[1] == 0);
assert(!f.manager.handle("get beacon", tiny, 0));
}
void schedule() {
for (unsigned cycle : {12U, 24U, 36U, 48U}) {
for (bool wrap : {false, true}) {
Fixture f(wrap ? UINT32_MAX - uint32_t(H) : 0);
f.all(cycle);
const uint64_t spacing = cycle / 12 * H;
assert(f.mesh.wake_enabled && f.mesh.wake_delay == spacing);
for (unsigned minute = 1; minute <= 48 * 60; ++minute) f.tick(60000);
const unsigned expected = 48 / (cycle / 12);
assert(f.mesh.sends.size() == expected);
for (unsigned i = 0; i < expected; ++i) {
const auto& send = f.mesh.sends[i];
assert(send.time == (i + 1) * spacing);
assert(std::string(send.slot.text) == std::to_string(i % 12 + 1));
assert(send.slot.radio.preamble == 32 && send.slot.radio.protocol == mesh::RadioProfileProtocol::MeshCore);
if (i) assert(send.time - f.mesh.sends[i - 1].time >= H);
if (i >= 12) assert(send.time - f.mesh.sends[i - 12].time >= cycle * H);
}
}
}
// Sparse slots retain their hour positions; a long sleep never catches up
// all missed slots in a burst.
Fixture sparse;
sparse.mc(1); sparse.mc(12);
sparse.tick(13 * H);
assert(sparse.mesh.sends.empty());
assert(sparse.mesh.wake_delay == 11 * H);
sparse.tick(11 * H);
assert(sparse.mesh.sends.size() == 1);
sparse.tick(24 * H);
assert(sparse.mesh.sends.size() == 2);
}
void rolling() {
Fixture f;
f.all(12);
f.tick(H + 59 * 60000);
assert(f.mesh.sends.size() == 1);
f.tick(60000);
assert(f.mesh.sends.size() == 1 && f.mesh.wake_delay == 59 * 60000);
for (unsigned minute = 1; minute <= 24 * 60; ++minute) f.tick(60000);
for (unsigned i = 1; i < f.mesh.sends.size(); ++i) {
assert(f.mesh.sends[i].time - f.mesh.sends[i - 1].time >= H);
if (i >= 12) assert(f.mesh.sends[i].time - f.mesh.sends[i - 12].time >= 12 * H);
}
// Failed queue admission consumes this window and causes no fast retry.
Fixture failed;
failed.mc(1); failed.mesh.accept = false;
failed.tick(2 * H);
assert(failed.mesh.attempts == 1 && failed.mesh.sends.empty());
for (unsigned i = 0; i < 100; ++i) failed.tick(1000);
assert(failed.mesh.attempts == 1);
failed.mesh.accept = true; failed.tick(24 * H - 100000);
assert(failed.mesh.attempts == 2 && failed.mesh.sends.size() == 1);
// Interval changes start a new anchor, rather than sending immediately.
failed.cmd("set beacon.interval 12");
failed.tick(H - 1); assert(failed.mesh.sends.size() == 1);
failed.tick(1); assert(failed.mesh.sends.size() == 1); // per-slot rolling guard
const unsigned cancellations = failed.mesh.cancellations;
failed.manager.suspend();
assert(!failed.mesh.wake_enabled && failed.mesh.cancellations == cancellations + 1);
failed.tick(100 * H); assert(failed.mesh.sends.size() == 1);
failed.cmd("set beacon 1 meshcore 906.5,62.5,7,5,14 Gone", false);
// An object created by an early get must not transmit when a later set
// enables a slot whose current window is already due, before the CLI ack.
Fixture edit;
edit.cmd("set beacon.interval 12"); edit.tick(H + 10 * 60000);
edit.mc(1); assert(edit.mesh.sends.empty());
edit.tick(50 * 60000); assert(edit.mesh.sends.empty());
edit.tick(11 * H); assert(edit.mesh.sends.size() == 1);
}
void storage() {
Fixture original;
original.mc(1, "Old");
const auto baseline = original.fs.files;
const size_t size = baseline.at("/beacons").size();
assert(size == 1188);
for (size_t boundary = 0; boundary < size; ++boundary) {
Fixture f; f.fs.files = baseline; f.manager.begin(&f.fs, &f.mesh, &f.clock, f.name);
f.fs.fail_write_after = int(boundary);
f.cmd("set beacon 1 meshcore 907.5,125,8,6,10 New", false);
assert(std::strstr(f.cmd("get beacon 1"), "Old"));
f.fs.fail_write_after = -1;
f.manager.begin(&f.fs, &f.mesh, &f.clock, f.name);
assert(std::strstr(f.cmd("get beacon 1"), "Old"));
}
for (int boundary : {0, 7, 8, 105, 1187}) {
Fixture f; f.fs.files = baseline; f.manager.begin(&f.fs, &f.mesh, &f.clock, f.name);
f.fs.fail_read_after = boundary;
f.cmd("set beacon 1 meshcore 907.5,125,8,6,10 New", false);
assert(std::strstr(f.cmd("get beacon 1"), "Old"));
}
for (int rename : {1, 2}) {
Fixture f; f.fs.files = baseline; f.manager.begin(&f.fs, &f.mesh, &f.clock, f.name);
f.fs.fail_rename = rename;
f.cmd("set beacon 1 meshcore 907.5,125,8,6,10 New", false);
f.manager.begin(&f.fs, &f.mesh, &f.clock, f.name);
assert(std::strstr(f.cmd("get beacon 1"), "Old"));
}
{
Fixture f; f.fs.files = baseline;
f.fs.files["/beacons.tmp"] = baseline.at("/beacons");
f.fs.files.erase("/beacons");
f.manager.begin(&f.fs, &f.mesh, &f.clock, f.name);
assert(!std::strstr(f.cmd("get beacon 1"), "Old")); // temp is not committed
}
for (bool future : {false, true}) {
Fixture f; f.fs.files = baseline;
f.fs.files["/beacons"][future ? 4 : 40] ^= future ? 3 : 1;
f.manager.begin(&f.fs, &f.mesh, &f.clock, f.name);
f.cmd("get beacon", false); f.cmd("get beacon.interval", false); f.cmd("delete beacon all", false);
f.tick(48 * H); assert(f.mesh.sends.empty());
}
{
Fixture f; f.fs.files = baseline;
f.fs.files["/beacons.bak"] = baseline.at("/beacons");
f.fs.files["/beacons"][40] ^= 1;
f.manager.begin(&f.fs, &f.mesh, &f.clock, f.name);
assert(std::strstr(f.cmd("get beacon 1"), "Old"));
}
{
Fixture f; f.fs.files = baseline;
f.manager.begin(&f.fs, &f.mesh, &f.clock, f.name);
f.fs.fail_rename_from = {"/beacons.tmp", "/beacons.bak"};
f.cmd("set beacon 1 meshcore 907.5,125,8,6,10 New", false);
f.cmd("get beacon", false);
f.tick(48 * H); assert(f.mesh.sends.empty());
}
{
Fixture f; f.fs.files = baseline; f.fs.fail_read_open = true;
f.manager.begin(&f.fs, &f.mesh, &f.clock, f.name);
f.cmd("delete beacon all", false);
assert(f.fs.files.at("/beacons") == baseline.at("/beacons"));
}
// CRC-valid but semantically invalid configuration is held as a unit.
{
Fixture f; f.fs.files = baseline;
auto& bytes = f.fs.files["/beacons"];
bytes[8 + 10] = 255; fixCrc(bytes);
f.manager.begin(&f.fs, &f.mesh, &f.clock, f.name);
f.cmd("get beacon", false);
}
}
void ids() {
Fixture f;
f.mt();
assert(f.fs.files.count("/beacon_ids"));
f.tick(2 * H);
assert(f.mesh.sends.size() == 1 && f.mesh.sends[0].id == 0x1234567a);
assert(f.mesh.sends[0].from == 0x88776655);
assert(f.mesh.sends[0].slot.radio.preamble == 16 && f.mesh.sends[0].slot.hops == 0);
const auto baseline = f.fs.files;
f.manager.begin(&f.fs, &f.mesh, &f.clock, f.name); // reboots use the durable counter
f.tick(2 * H);
assert(f.mesh.sends.size() == 2 && f.mesh.sends.back().id == 0x1234567b);
f.cmd("delete beacon all"); f.mt();
f.tick(24 * H);
assert(f.mesh.sends.back().id == 0x1234567c);
for (unsigned fault = 0; fault < 4; ++fault) {
Fixture bad; bad.fs.files = baseline;
if (fault == 0) bad.fs.files.erase("/beacon_ids");
if (fault == 1) bad.fs.files["/beacon_ids"][8] ^= 1;
if (fault == 2) bad.fs.files["/beacon_ids"][4] = 2;
if (fault == 3) {
auto& bytes = bad.fs.files["/beacon_ids"];
put32(bytes.data() + 8, UINT32_MAX); fixCrc(bytes);
}
bad.manager.begin(&bad.fs, &bad.mesh, &bad.clock, bad.name);
bad.tick(2 * H); assert(bad.mesh.sends.empty());
bad.cmd("delete beacon all");
bad.cmd("set beacon 1 meshtext US LongFast 14 Hello MT", false);
// A broken MT counter must not disable independent MeshCore slots.
bad.mc(2); bad.tick(2 * H); assert(bad.mesh.sends.size() == 1);
}
// Every counter short-write boundary fails closed for this boot. Previous
// durable state is unchanged, so the next boot cannot repeat an on-air ID.
for (int boundary = 0; boundary < 16; ++boundary) {
Fixture bad; bad.fs.files = baseline; bad.manager.begin(&bad.fs, &bad.mesh, &bad.clock, bad.name);
bad.fs.fail_write_after = boundary;
bad.tick(2 * H); assert(bad.mesh.sends.empty());
bad.fs.fail_write_after = -1;
bad.tick(24 * H); assert(bad.mesh.sends.empty());
bad.manager.begin(&bad.fs, &bad.mesh, &bad.clock, bad.name);
bad.tick(2 * H); assert(bad.mesh.sends.size() == 1 && bad.mesh.sends[0].id == 0x1234567b);
}
{
Fixture rejected; rejected.mt(); rejected.mesh.accept = false;
rejected.tick(2 * H); assert(rejected.mesh.attempts == 1);
rejected.mesh.accept = true; rejected.tick(24 * H);
assert(rejected.mesh.sends.size() == 1 && rejected.mesh.sends[0].id == 0x1234567b);
}
}
void mutations() {
for (const char* command : {
"set beacon 1 meshcore 907.5,125,8,6,10 New",
"set beacon 2 meshcore 907.5,125,8,6,10 Other",
"delete beacon 1", "delete beacon all", "set beacon.interval 12"}) {
for (bool rename_failure : {false, true}) {
Fixture f;
f.mc(1, "Old"); f.tick(2 * H);
assert(f.mesh.pending && f.mesh.sends.size() == 1);
const unsigned cancellations = f.mesh.cancellations;
f.cmd("get beacon 1");
assert(f.mesh.pending && f.mesh.cancellations == cancellations);
if (rename_failure) f.fs.fail_rename = 2;
else f.fs.fail_write = true;
f.cmd(command, false);
assert(f.mesh.pending && f.mesh.cancellations == cancellations);
f.tick(1000);
assert(f.mesh.pending && f.mesh.sends.size() == 1);
f.fs.fail_write = false;
f.cmd(command);
assert(!f.mesh.pending && f.mesh.cancellations == cancellations + 1);
f.tick(10000);
assert(!f.mesh.pending && f.mesh.sends.size() == 1);
}
}
}
void staleBackup() {
Fixture f;
f.mt();
const auto stale = f.fs.files.at("/beacon_ids");
f.tick(2 * H);
assert(f.mesh.sends.size() == 1 && f.mesh.sends[0].id == 0x1234567a);
assert(stale != f.fs.files.at("/beacon_ids"));
// A previous backup-remove failure can leave this lower ID behind. Losing
// only the active directory entry must not make the on-air ID reusable.
f.fs.files["/beacon_ids.bak"] = stale;
f.fs.files.erase("/beacon_ids");
f.manager.begin(&f.fs, &f.mesh, &f.clock, f.name);
f.tick(2 * H);
assert(f.mesh.sends.size() == 1);
assert(!f.fs.files.count("/beacon_ids") && f.fs.files.at("/beacon_ids.bak") == stale);
f.cmd("delete beacon all");
f.cmd("set beacon 1 meshtext US LongFast 14 Hello MT", false);
f.mc(2); f.tick(2 * H);
assert(f.mesh.sends.size() == 2 && f.mesh.sends.back().slot.radio.protocol == mesh::RadioProfileProtocol::MeshCore);
// Absence of the config marker is not evidence that an independent backup
// has never been used. A fresh MT setter must remain fail closed too.
Fixture uninitialized;
uninitialized.mc(2);
assert(uninitialized.fs.files.at("/beacons")[6] == 0);
uninitialized.fs.files["/beacon_ids.bak"] = stale;
uninitialized.manager.begin(&uninitialized.fs, &uninitialized.mesh, &uninitialized.clock, uninitialized.name);
uninitialized.cmd("set beacon 1 meshtext US LongFast 14 Hello MT", false);
uninitialized.tick(4 * H);
assert(uninitialized.mesh.sends.size() == 1
&& uninitialized.mesh.sends[0].slot.radio.protocol == mesh::RadioProfileProtocol::MeshCore);
assert(!uninitialized.fs.files.count("/beacon_ids")
&& uninitialized.fs.files.at("/beacon_ids.bak") == stale);
}
int main(int argc, char** argv) {
assert(argc == 2);
if (!std::strcmp(argv[1], "commands")) commands();
else if (!std::strcmp(argv[1], "schedule")) schedule();
else if (!std::strcmp(argv[1], "rolling")) rolling();
else if (!std::strcmp(argv[1], "storage")) storage();
else if (!std::strcmp(argv[1], "ids")) ids();
else if (!std::strcmp(argv[1], "mutations")) mutations();
else if (!std::strcmp(argv[1], "stale_backup")) staleBackup();
else assert(false);
std::printf("beacon manager %s passed\n", argv[1]);
}
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <cstring>
// BeaconManager uses only the production preset parser/modem calculator.
// Encryption has its independent real-AES oracle in test_meshtext_chat.py.
class BeaconUnusedAES {
public:
bool setKey(const uint8_t*, size_t) { return false; }
void encryptBlock(uint8_t* output, const uint8_t* input) { std::memcpy(output, input, 16); }
void clear() {}
};
using AES128 = BeaconUnusedAES;
using AES256 = BeaconUnusedAES;
+49
View File
@@ -0,0 +1,49 @@
#pragma once
#include <helpers/BeaconConfig.h>
#include <Arduino.h>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <vector>
extern uint64_t beacon_test_time;
namespace mesh {
class RTCClock {};
class RNG {
public:
void random(uint8_t* output, size_t size) {
const uint32_t value = 0x12345678;
std::memcpy(output, &value, size);
}
};
class Mesh {
public:
struct Send { uint64_t time; BeaconSlot slot; uint32_t from, id; };
std::vector<Send> sends;
bool accept = true, wake_enabled = false, wire_supported = true, pending = false;
uint32_t wake_delay = 0, wake_now = 0, attempts = 0, cancellations = 0;
bool validateTextBeacon(const BeaconSlot& slot) const {
const auto& p = slot.radio;
return slot.enabled && RadioProfiles::valid(p) && p.freq >= 300 && p.freq <= 1000
&& (p.bw == 31.25f || p.bw == 62.5f || p.bw == 125 || p.bw == 250 || p.bw == 500)
&& slot.power >= -9 && slot.power <= 22 && slot.text[0]
&& (p.protocol != RadioProfileProtocol::Meshtext || wire_supported);
}
bool queueTextBeacon(const BeaconSlot& slot, const char*, uint32_t from, uint32_t id) {
++attempts;
if (!accept) return false;
pending = true;
sends.push_back({beacon_test_time, slot, from, id});
return true;
}
void setBeaconWakeDelay(uint32_t now, uint32_t delay, bool enabled) {
wake_now = now; wake_delay = delay; wake_enabled = enabled;
}
uint32_t beaconNodeId() const { return 0x88776655; }
RNG* getRNG() { return &rng_; }
void cancelBeaconRadioFrame() { ++cancellations; pending = false; }
private:
RNG rng_;
};
} // namespace mesh
+446
View File
@@ -0,0 +1,446 @@
"""Execute the production TX-only beacon owner and Dispatcher lifecycle.
The test uses the actual Dispatcher, packet serializer, and static pool. Radio
and clock boundaries are counted adapters. No PlatformIO or hardware is used.
"""
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
HARNESS = r'''
#include <Dispatcher.h>
#include <helpers/StaticPoolPacketManager.h>
#include <cassert>
#include <cstring>
#include <cstdio>
#include <vector>
using namespace mesh;
class Clock : public MillisecondClock {
public:
uint32_t now;
explicit Clock(uint32_t initial = 1000) : now(initial) {}
unsigned long getMillis() override { return now; }
};
class TestRadio : public Radio {
public:
RadioProfiles p;
RadioProfileParams physical, saved;
int8_t power = 22, saved_power = 22;
uint8_t active = 0;
bool owned = false, receiving = false, complete = false, sending = false;
bool start_ok = true, carrier = false, morse_tx = false, morse_rx = false;
bool calibrating = false, incoming = false, channel_busy = false;
bool valid = true, busy_owns = false, fail_owns = false;
unsigned sends = 0, beacon_sends = 0, normal_sends = 0, finishes = 0, recovery = 0;
unsigned processed = 0, prepare_calls = 0, restore_calls = 0, loops = 0;
unsigned agc = 0, noise = 0, begins = 0, begin_while_owned = 0;
uint32_t airtime = 100;
Clock* clock = nullptr;
uint32_t cad_delay = 0;
RadioParamApplyResult prepare = RadioParamApplyResult::APPLIED;
RadioParamApplyResult restore = RadioParamApplyResult::APPLIED;
std::vector<uint8_t> sent;
std::vector<uint32_t> beacon_starts;
TestRadio() {
p.primary.freq = 909.5f; p.primary.bw = 125; p.primary.sf = 7; p.primary.cr = 5;
p.primary.preamble = 32;
p.secondary.params = p.primary; p.secondary.params.freq = 910;
physical = saved = p.primary;
}
RadioProfiles* profiles() override { return &p; }
const RadioProfiles* profiles() const override { return &p; }
bool supportsMeshtext() const override { return true; }
bool validateProfile(const RadioProfileParams& params) const override {
return valid && RadioProfiles::valid(params);
}
uint8_t receiveProfile() const override { return active; }
int recvRaw(uint8_t* bytes, int) override {
assert(!owned);
if (!incoming) return 0;
incoming = false; bytes[0] = 0x15; bytes[1] = 0; bytes[2] = 0x23; return 3;
}
void onReceiveProcessed() override { ++processed; }
uint32_t getEstAirtimeFor(int) override { return 100; }
uint32_t getProfileAirtime(uint8_t, int, uint8_t = 0) override { return 100; }
uint32_t getBeaconAirtime(const RadioProfileParams&, int) override { return airtime; }
float packetScore(float, int) override { return 1; }
RadioParamApplyResult prepareTransmitProfile(uint8_t profile) override {
assert(!owned); active = profile; physical = p.params(profile); return RadioParamApplyResult::APPLIED;
}
RadioParamApplyResult prepareBeaconTransmit(const RadioProfileParams& params, int8_t requested_power) override {
++prepare_calls;
if (prepare == RadioParamApplyResult::APPLIED || busy_owns || fail_owns) {
if (!owned) { saved = physical; saved_power = power; }
owned = true; physical = params; power = requested_power;
}
return prepare;
}
RadioParamApplyResult restoreBeaconReceive() override {
++restore_calls;
if (restore == RadioParamApplyResult::APPLIED) {
if (owned) { physical = saved; power = saved_power; }
owned = false;
}
return restore;
}
bool isBeaconRadioOwned() const override { return owned; }
bool isBeaconChannelBusy() override {
assert(owned);
if (clock) clock->now += cad_delay;
return channel_busy;
}
bool startSendRaw(const uint8_t* bytes, int length) override {
++sends;
if (owned) {
++beacon_sends;
if (clock) beacon_starts.push_back(clock->now);
} else ++normal_sends;
sent.assign(bytes, bytes + length); sending = start_ok; complete = false; return start_ok;
}
bool isSendComplete() override { return complete; }
void onSendFinished() override { ++finishes; sending = false; }
bool isInRecvMode() const override { return !sending; }
bool isReceiving() override { return receiving; }
bool isCarrierWaveActive() const override { return carrier; }
bool isMorseTxActive() const override { return morse_tx; }
bool isMorseRxActive() const override { return morse_rx; }
bool isCalibratingNoiseFloor() const override { return calibrating; }
bool recoverRadio(bool) override { ++recovery; return true; }
uint32_t getRecoveryCount() const override { return recovery; }
void loop() override { assert(!owned); ++loops; }
void begin() override { ++begins; if (owned) ++begin_while_owned; }
void resetAGC() override { assert(!owned); ++agc; }
bool tryResetAGC() override { resetAGC(); return true; }
void triggerNoiseFloorCalibrate(int) override { assert(!owned); ++noise; }
};
class TestDispatcher : public Dispatcher {
public:
unsigned received = 0, mc_sent = 0, failed = 0;
unsigned long duty_window = 3600000;
int interval = 0;
bool adaptive = false;
TestDispatcher(TestRadio& radio, Clock& clock, StaticPoolPacketManager& manager)
: Dispatcher(radio, clock, manager) {}
using Dispatcher::setRadioAvailable;
using Dispatcher::getNextQueueWakeDelay;
using Dispatcher::queueForeignRadioFrame;
DispatcherAction onRecvPacket(Packet*) override { ++received; return ACTION_RELEASE; }
void onSendComplete(Packet*) override { ++mc_sent; }
void onSendFail(Packet*) override { ++failed; }
unsigned long getDutyCycleWindowMs() const override { return duty_window; }
int getAGCResetInterval() const override { return interval; }
bool useAdaptiveAGC() const override { return adaptive; }
};
struct Fixture {
Clock clock;
TestRadio radio;
StaticPoolPacketManager manager{8};
TestDispatcher d{radio, clock, manager};
explicit Fixture(uint32_t initial = 1000) : clock(initial) { radio.clock = &clock; d.begin(); }
void tick(uint32_t delta = 1) { clock.now += delta; d.loop(); }
};
static const uint8_t bytes[] = {0x15, 0, 0x23};
static RadioProfileParams beacon_params() {
RadioProfileParams p;
p.freq = 906.875f; p.bw = 250; p.sf = 11; p.cr = 5; p.preamble = 16;
p.protocol = RadioProfileProtocol::Meshtext;
return p;
}
static bool queue(Fixture& f) {
return f.d.queueBeaconRadioFrame(bytes, sizeof(bytes), beacon_params(), 17);
}
static Packet* normal(Fixture& f, uint32_t delay = 0) {
Packet* packet = f.d.obtainNewPacket(); assert(packet);
packet->header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_GRP_TXT << PH_TYPE_SHIFT);
packet->tx_radio = RADIO_TX_PRIMARY; packet->payload[0] = 0x12; packet->payload_len = 1;
assert(f.d.sendPacket(packet, 0, delay));
return packet;
}
static void basic() {
Fixture f;
f.radio.p.secondary.mode = RadioProfileMode::RxTx;
f.radio.p.secondary_temporary = true;
const auto saved = f.radio.p;
assert(queue(f)); assert(f.d.hasOutbound() && !queue(f));
uint32_t delay = 99;
assert(f.d.getNextQueueWakeDelay(delay) && delay == 0);
f.tick();
assert(f.radio.beacon_sends == 1 && f.radio.owned && f.radio.physical == beacon_params());
assert(f.radio.sent == std::vector<uint8_t>(bytes, bytes + sizeof(bytes)));
assert(f.radio.power == 17 && !f.d.queueForeignRadioFrame(bytes, sizeof(bytes), 1, 1));
f.tick(20); assert(f.d.hasOutbound() && f.radio.finishes == 0);
f.radio.complete = true; f.tick();
assert(!f.d.hasOutbound() && !f.radio.owned && f.radio.finishes == 1);
assert(f.radio.physical == saved.primary && f.radio.power == 22);
assert(f.radio.p.primary == saved.primary && f.radio.p.secondary.params == saved.secondary.params);
assert(f.radio.p.secondary.mode == saved.secondary.mode && f.radio.p.secondary_temporary);
assert(f.radio.p.generation[0] == saved.generation[0] && f.radio.p.generation[1] == saved.generation[1]);
assert(!f.d.getNumSentFlood() && !f.d.getNumSentDirect() && !f.d.mc_sent);
assert(f.d.getTotalAirTime() == 21);
}
static void preparation() {
{ Fixture f; f.radio.prepare = RadioParamApplyResult::BUSY;
assert(queue(f)); f.tick(); assert(f.radio.prepare_calls == 1 && !f.radio.owned && !f.radio.sends);
f.tick(10); assert(f.radio.prepare_calls == 1);
f.tick(1); assert(f.radio.prepare_calls == 2);
f.tick(29987); assert(f.d.hasBeaconRadioFrame());
f.tick(1); assert(!f.d.hasBeaconRadioFrame() && !f.radio.sends && !f.radio.owned);
assert(queue(f)); // No RF attempt means the one-hour guard was not consumed.
f.d.cancelBeaconRadioFrame(); }
{ Fixture f; f.radio.prepare = RadioParamApplyResult::BUSY; f.radio.busy_owns = true;
f.radio.restore = RadioParamApplyResult::BUSY; assert(queue(f)); f.tick();
assert(f.d.hasBeaconRadioFrame() && f.radio.owned && f.radio.restore_calls == 1);
const unsigned loops = f.radio.loops; f.tick(500);
assert(f.radio.loops == loops && !f.radio.sends);
f.radio.restore = RadioParamApplyResult::APPLIED; f.tick();
assert(!f.d.hasBeaconRadioFrame() && !f.radio.owned); }
{ Fixture f; f.radio.prepare = RadioParamApplyResult::FAILED; f.radio.fail_owns = true;
assert(queue(f)); f.tick(); assert(!f.radio.owned && !f.d.hasBeaconRadioFrame() && !f.radio.sends); }
}
static void failures() {
{ Fixture f; f.radio.channel_busy = true; assert(queue(f)); f.tick();
assert(!f.radio.sends && !f.d.hasBeaconRadioFrame() && !f.radio.owned);
f.radio.channel_busy = false; assert(queue(f)); f.d.cancelBeaconRadioFrame(); }
{ Fixture f; f.radio.start_ok = false; assert(queue(f)); f.tick();
assert(f.radio.sends == 1 && !f.d.hasBeaconRadioFrame() && !f.radio.owned);
assert(queue(f)); f.tick(); assert(f.radio.sends == 1);
f.tick(30000); assert(!f.d.hasBeaconRadioFrame() && f.radio.sends == 1);
f.tick(3600000); assert(queue(f)); f.d.cancelBeaconRadioFrame(); }
{ Fixture f; f.radio.airtime = 0; assert(queue(f)); f.tick();
assert(!f.d.hasBeaconRadioFrame() && !f.radio.sends); }
{ Fixture f; f.radio.airtime = UINT32_MAX; assert(queue(f)); f.tick();
assert(!f.d.hasBeaconRadioFrame() && !f.radio.sends); }
{ Fixture f; assert(queue(f)); f.tick(); f.tick(1150);
assert(!f.d.hasBeaconRadioFrame() && !f.radio.owned && f.radio.finishes == 1 && f.radio.recovery == 1);
assert(f.d.getErrFlags() & ERR_EVENT_RADIO_WATCHDOG);
f.tick(10000); assert(f.radio.sends == 1); }
}
static void restoration() {
Fixture f; f.d.interval = 8; f.d.adaptive = true;
assert(queue(f)); f.tick();
const unsigned loops = f.radio.loops, agc = f.radio.agc, noise = f.radio.noise;
f.radio.restore = RadioParamApplyResult::BUSY;
f.radio.complete = true; f.radio.incoming = true;
normal(f); f.tick(20);
assert(f.radio.owned && f.d.hasBeaconRadioFrame() && f.radio.finishes == 1);
f.tick(30000);
assert(f.radio.loops == loops && f.radio.agc == agc && f.radio.noise == noise);
assert(f.radio.normal_sends == 0 && f.radio.incoming && f.manager.getOutboundTotal() == 1);
f.radio.restore = RadioParamApplyResult::FAILED; f.tick();
assert(f.radio.owned && f.radio.loops == loops && !f.radio.normal_sends);
f.radio.restore = RadioParamApplyResult::APPLIED; f.tick();
assert(!f.radio.owned && !f.d.hasBeaconRadioFrame() && !f.radio.incoming);
assert(f.radio.normal_sends == 1 && f.radio.agc > agc);
f.radio.complete = true; f.tick();
}
static void availability() {
{ Fixture f; assert(queue(f)); f.d.setRadioAvailable(false);
assert(!f.d.hasBeaconRadioFrame() && !queue(f));
const unsigned restores = f.radio.restore_calls, loops = f.radio.loops;
f.tick(10000); assert(f.radio.restore_calls == restores && f.radio.loops == loops);
f.d.setRadioAvailable(true); assert(queue(f)); f.d.cancelBeaconRadioFrame(); }
{ Fixture f; assert(queue(f)); f.tick(); f.radio.restore = RadioParamApplyResult::BUSY;
f.d.setRadioAvailable(false); assert(f.radio.owned && f.d.hasBeaconRadioFrame());
const unsigned restores = f.radio.restore_calls, finishes = f.radio.finishes, loops = f.radio.loops;
f.tick(10000);
assert(f.radio.restore_calls == restores && f.radio.finishes == finishes && f.radio.loops == loops);
f.d.setRadioAvailable(true);
assert(!f.radio.begin_while_owned); // Never initialize through an outstanding beacon lease.
f.radio.restore = RadioParamApplyResult::APPLIED; f.tick();
assert(!f.radio.owned && !f.d.hasBeaconRadioFrame()); }
}
static void priority() {
{ Fixture f; assert(queue(f)); normal(f); f.tick();
assert(f.radio.normal_sends == 1 && !f.radio.beacon_sends && !f.radio.owned);
assert(f.d.hasBeaconRadioFrame()); f.radio.complete = true; f.tick(); f.tick(101);
assert(f.radio.beacon_sends == 1 && f.radio.owned);
f.radio.complete = true; f.tick(); }
{ Fixture f; normal(f, 5000); assert(queue(f)); f.tick();
assert(f.radio.beacon_sends == 1 && !f.radio.normal_sends);
f.radio.complete = true; f.tick(); f.tick(5000);
assert(f.radio.normal_sends == 1); f.radio.complete = true; f.tick(); }
{ Fixture f; normal(f); f.tick(); assert(!queue(f)); f.radio.complete = true; f.tick(); }
{ Fixture f; f.radio.p.secondary.params.protocol = RadioProfileProtocol::Meshtext;
f.radio.p.secondary.mode = RadioProfileMode::RxTx;
assert(f.d.queueForeignRadioFrame(bytes, sizeof(bytes), 1, f.radio.p.generation[1]));
assert(!queue(f)); f.tick(); f.radio.complete = true; f.tick(); }
}
static void budget_and_hour() {
{ Fixture f; f.d.duty_window = 200; f.d.begin();
assert(queue(f)); f.tick(); f.radio.complete = true; f.tick(100);
assert(f.d.getRemainingTxBudget() == 0 && f.d.getTotalAirTime() == 100);
assert(queue(f)); f.tick(); assert(f.radio.beacon_sends == 1);
f.d.cancelBeaconRadioFrame();
f.clock.now = 3601000; assert(queue(f)); // Admission just before the actual RF deadline is valid.
f.d.loop(); assert(f.radio.beacon_sends == 1);
f.clock.now = 3601001; f.d.loop(); assert(f.radio.beacon_sends == 1);
f.clock.now = 3601101; f.d.loop(); assert(f.radio.beacon_sends == 2);
f.radio.complete = true; f.tick(100); }
{ Fixture f; f.d.duty_window = 20; f.d.begin(); assert(queue(f)); f.tick();
assert(!f.radio.sends && f.d.hasBeaconRadioFrame());
f.tick(30000); assert(!f.d.hasBeaconRadioFrame() && !f.radio.sends); }
{ Fixture f(UINT32_MAX - 500); assert(queue(f)); f.tick();
f.radio.complete = true; f.tick(50); assert(queue(f));
f.tick(); assert(f.radio.beacon_sends == 1); f.d.cancelBeaconRadioFrame();
f.clock.now = uint32_t(UINT32_MAX - 499 + uint64_t(3600000));
assert(queue(f)); f.tick(); assert(f.radio.beacon_sends == 2);
f.radio.complete = true; f.tick(); }
}
static void hourly_cad_delay() {
Fixture f;
f.radio.cad_delay = 30;
const uint32_t first_admission = f.clock.now;
assert(queue(f)); f.tick();
assert(f.radio.beacon_sends == 1 && f.radio.beacon_starts[0] == first_admission + 31);
f.radio.complete = true; f.tick();
// The manager staggers admission, not the end of synchronous CAD. A new
// hourly slot must wait for the RF guard instead of being dropped here.
f.clock.now = first_admission + 3600000;
assert(queue(f));
f.tick(); assert(f.radio.beacon_sends == 1 && !f.radio.owned);
f.tick(30); assert(f.radio.beacon_sends == 1 && !f.radio.owned);
f.tick(71); assert(f.radio.beacon_sends == 2 && f.radio.owned);
assert(uint32_t(f.radio.beacon_starts[1] - f.radio.beacon_starts[0]) >= 3600000);
f.radio.complete = true; f.tick();
}
static void sleep_deadlines() {
Fixture f; f.d.interval = 8000; f.d.adaptive = true; f.tick();
uint32_t delay = 0;
f.d.setBeaconWakeDelay(f.clock.now, 3600000, true);
assert(f.d.getNextQueueWakeDelay(delay) && delay == 8000);
f.d.setBeaconWakeDelay(f.clock.now, 1000, true);
assert(f.d.getNextQueueWakeDelay(delay) && delay == 1000);
f.d.setBeaconWakeDelay(f.clock.now, 1000, false);
assert(f.d.getNextQueueWakeDelay(delay) && delay == 8000);
assert(queue(f)); assert(f.d.getNextQueueWakeDelay(delay) && delay == 0);
f.d.cancelBeaconRadioFrame();
f.d.setBeaconWakeDelay(f.clock.now, 0, true);
assert(f.d.getNextQueueWakeDelay(delay) && delay == 0);
f.d.setRadioAvailable(false);
assert(!f.d.getNextQueueWakeDelay(delay));
}
static void begin_lifecycle() {
Fixture f; assert(queue(f)); f.tick();
assert(f.radio.owned);
f.d.begin();
assert(!f.radio.begin_while_owned && !f.radio.owned && !f.d.hasBeaconRadioFrame());
assert(f.radio.finishes == 1 && f.radio.restore_calls == 1);
}
int main(int argc, char** argv) {
assert(argc == 2);
if (!strcmp(argv[1], "basic")) basic();
else if (!strcmp(argv[1], "preparation")) preparation();
else if (!strcmp(argv[1], "failures")) failures();
else if (!strcmp(argv[1], "restoration")) restoration();
else if (!strcmp(argv[1], "availability")) availability();
else if (!strcmp(argv[1], "priority")) priority();
else if (!strcmp(argv[1], "budget")) budget_and_hour();
else if (!strcmp(argv[1], "hourly")) hourly_cad_delay();
else if (!strcmp(argv[1], "sleep")) sleep_deadlines();
else if (!strcmp(argv[1], "begin")) begin_lifecycle();
else assert(false);
puts("PASS");
}
'''
class BeaconDispatcherTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
if not compiler:
raise unittest.SkipTest("a host C++ compiler is required")
cls.temp = tempfile.TemporaryDirectory(prefix="meshcore-beacon-dispatcher-")
cls.addClassCleanup(cls.temp.cleanup)
work = Path(cls.temp.name)
cpp = work / "dispatcher.cpp"
cpp.write_text(HARNESS, encoding="ascii")
cls.binary = work / ("dispatcher.exe" if sys.platform == "win32" else "dispatcher")
flags = ["-std=c++17", "-Wall", "-Wextra", "-Wno-unused-parameter",
"-DMESH_ENABLE_TX_BEACONS=1", "-DMESH_ENABLE_MESHTEXT_CHAT=1"]
if sys.platform.startswith("linux"):
flags += ["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
"-fno-omit-frame-pointer", "-fno-pie", "-no-pie"]
cls.compiler, cls.flags, cls.cpp, cls.work = compiler, flags, cpp, work
result = cls.compile_variant(ROOT / "src/DispatcherBeacon.cpp", cls.binary)
if result.returncode:
raise AssertionError(result.stdout + result.stderr)
@classmethod
def compile_variant(cls, beacon_source, binary):
return subprocess.run(
[cls.compiler, *cls.flags, "-I" + str(ROOT / "test/mocks"), "-I" + str(ROOT / "src"),
str(ROOT / "src/Dispatcher.cpp"), str(beacon_source),
str(ROOT / "src/Packet.cpp"), str(ROOT / "src/helpers/StaticPoolPacketManager.cpp"),
str(cls.cpp), "-o", str(binary)], capture_output=True, text=True, timeout=90)
def run_case(self, name):
env = os.environ.copy()
# The legacy static pool intentionally lives for the entire firmware
# lifetime and has no teardown. Keep ASan/UBSan fatal, exclude only LSAN.
env["ASAN_OPTIONS"] = env.get("ASAN_OPTIONS", "") + ":detect_leaks=0"
result = subprocess.run(
[str(self.binary), name], capture_output=True, text=True,
env=env, cwd=self.work, timeout=30)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("PASS", result.stdout)
def test_one_tx_uses_private_tuple_preserves_radio2_and_accounts_airtime(self):
self.run_case("basic")
def test_prepare_busy_is_bounded_and_owned_failures_restore_exclusively(self):
self.run_case("preparation")
def test_cad_start_failure_invalid_airtime_and_tx_timeout_do_not_retry(self):
self.run_case("failures")
def test_restore_busy_blocks_normal_tx_rx_agc_and_noise_until_repaired(self):
self.run_case("restoration")
def test_suspension_and_reactivation_do_not_initialize_through_a_beacon_lease(self):
self.run_case("availability")
def test_due_normal_traffic_wins_and_meshtext_ownership_is_exclusive(self):
self.run_case("priority")
def test_budget_hour_guard_and_millisecond_rollover(self):
self.run_case("budget")
def test_hourly_admission_waits_for_actual_rf_guard_after_cad_delay(self):
self.run_case("hourly")
def test_old_hour_guard_at_admission_is_rejected_by_regression(self):
source = (ROOT / "src/DispatcherBeacon.cpp").read_text(encoding="utf-8")
marker = " const uint32_t now = _ms->getMillis();\n"
old_guard = " if (beacon_has_started && uint32_t(now - beacon_last_start) < 3600000UL) return false;\n"
self.assertIn(marker, source)
broken = self.work / "old-admission.cpp"
broken.write_text(source.replace(marker, marker + old_guard, 1), encoding="ascii")
binary = self.work / ("old-admission.exe" if sys.platform == "win32" else "old-admission")
compile_result = self.compile_variant(broken, binary)
self.assertEqual(compile_result.returncode, 0, compile_result.stdout + compile_result.stderr)
env = os.environ.copy()
env["ASAN_OPTIONS"] = env.get("ASAN_OPTIONS", "") + ":detect_leaks=0"
result = subprocess.run([str(binary), "hourly"], capture_output=True, text=True,
env=env, cwd=self.work, timeout=30)
self.assertNotEqual(result.returncode, 0, "old admission guard unexpectedly passed")
self.assertIn("queue(f)", result.stderr)
def test_beacon_sleep_deadline_is_merged_with_adaptive_agc(self):
self.run_case("sleep")
def test_begin_retires_an_in_flight_beacon_before_radio_initialization(self):
self.run_case("begin")
if __name__ == "__main__":
unittest.main()
+370
View File
@@ -0,0 +1,370 @@
"""Test TX-only beacon frames with real AES/SHA and independent wire oracles.
Only the radio queue, clock and packet-pool boundary are substituted. The
production group-datagram builder, packet serializer and Mesh adapter run as
written; no PlatformIO, hardware or downloaded dependency is needed.
"""
import hashlib
import hmac
import os
from pathlib import Path
import shutil
import struct
import subprocess
import sys
import tempfile
import unittest
from cpp_source import body
from test_meshtext_chat import DEFAULT_KEY, data_text, reference_frame
ROOT = Path(__file__).resolve().parents[1]
PUBLIC_KEY = bytes.fromhex("8b3387e9c5cdea6ac9e5edbaa115cd72")
ADAPTER = r'''
#pragma once
#include <Packet.h>
#include <Utils.h>
#include <helpers/BeaconConfig.h>
#include <cassert>
#include <vector>
namespace mesh {
struct GroupChannel {
uint8_t hash[PATH_HASH_SIZE] = {};
uint8_t secret[PUB_KEY_SIZE] = {};
uint8_t tx_radio = RADIO_TX_AUTO;
};
class TestRadio {
public:
bool valid = true, meshtext = true;
RadioProfiles profiles;
bool validateProfile(const RadioProfileParams& p) const { return valid && RadioProfiles::valid(p); }
bool supportsMeshtext() const { return meshtext; }
};
class TestClock {
public:
uint32_t now = 0x12345678;
uint32_t getCurrentTimeUnique() { return now++; }
};
class TestTables {
public:
unsigned sent = 0;
std::vector<uint8_t> marked;
void markSent(const Packet* packet) {
++sent;
uint8_t raw[MAX_TRANS_UNIT];
const size_t length = packet->writeTo(raw);
marked.assign(raw, raw + length);
}
};
class Mesh {
public:
TestRadio radio;
TestRadio* _radio = &radio;
TestClock clock;
TestTables tables;
struct { uint8_t pub_key[32] = {}; } self_id;
bool temporary = false, accept = true, pool_available = true;
unsigned obtained = 0, released = 0, queues = 0;
std::vector<uint8_t> queued;
RadioProfileParams queued_params;
int8_t queued_power = 0;
Packet pool_packet;
bool validateTextBeacon(const BeaconSlot&) const;
bool queueTextBeacon(const BeaconSlot&, const char*, uint32_t, uint32_t);
uint32_t beaconNodeId() const;
bool isAnyTempRadioActive() const { return temporary; }
TestClock* getRTCClock() { return &clock; }
TestTables* getTables() { return &tables; }
Packet* obtainNewPacket() {
if (!pool_available) return nullptr;
++obtained;
pool_packet = Packet();
return &pool_packet;
}
void releasePacket(Packet* p) { assert(p == &pool_packet); ++released; }
Packet* createGroupDatagram(uint8_t, const GroupChannel&, const uint8_t*, size_t);
bool queueBeaconRadioFrame(const uint8_t* bytes, size_t size,
const RadioProfileParams& params, int8_t power) {
++queues;
if (!accept) return false;
queued.assign(bytes, bytes + size);
queued_params = params;
queued_power = power;
return true;
}
};
}
'''
HARNESS = r'''
#include <Mesh.h>
#include <helpers/BeaconFrames.h>
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
using namespace mesh;
static std::vector<uint8_t> unhex(const char* input) {
const std::string text(input);
assert(text.size() % 2 == 0);
std::vector<uint8_t> bytes;
for (size_t i = 0; i < text.size(); i += 2)
bytes.push_back(uint8_t(std::stoul(text.substr(i, 2), nullptr, 16)));
return bytes;
}
static BeaconSlot slot(bool mt = false) {
BeaconSlot result;
result.radio.freq = 906.875f;
result.radio.bw = 250;
result.radio.sf = 11;
result.radio.cr = 5;
result.radio.preamble = mt ? 16 : 32;
result.radio.protocol = mt ? RadioProfileProtocol::Meshtext : RadioProfileProtocol::MeshCore;
result.power = 17;
result.preset = static_cast<uint8_t>(mt::Preset::LongFast);
result.hops = 3;
result.enabled = true;
strcpy(result.text, "hello");
return result;
}
static void public_frame(const char* expected) {
Mesh mesh;
const auto before = mesh.radio.profiles;
auto config = slot();
assert(mesh.validateTextBeacon(config));
assert(mesh.queueTextBeacon(config, "Node:name", 0, 0));
assert(mesh.queued == unhex(expected));
assert(mesh.tables.sent == 1 && mesh.tables.marked == mesh.queued);
assert(mesh.obtained == 1 && mesh.released == 1 && mesh.queues == 1);
assert(mesh.queued_params == config.radio && mesh.queued_power == 17);
assert(mesh.radio.profiles.primary == before.primary);
assert(mesh.radio.profiles.secondary.params == before.secondary.params);
assert(mesh.radio.profiles.secondary.mode == before.secondary.mode);
assert(mesh.radio.profiles.generation[0] == before.generation[0]);
assert(mesh.radio.profiles.generation[1] == before.generation[1]);
}
static void meshtext_frame(const char* expected, bool medium) {
Mesh mesh;
auto config = slot(true);
if (medium) {
config.preset = static_cast<uint8_t>(mt::Preset::MediumFast);
config.radio.sf = 9;
config.hops = 7;
}
strcpy(config.text, "caf\xc3\xa9");
mesh.radio.profiles.secondary.params.freq = 915;
mesh.radio.profiles.secondary.mode = RadioProfileMode::RxTx;
const auto before = mesh.radio.profiles;
assert(mesh.queueTextBeacon(config, "Node", 0x11223344, 0x89abcdef));
assert(mesh.queued == unhex(expected));
assert(mesh.obtained == 0 && mesh.released == 0 && mesh.tables.sent == 0);
assert(mesh.queues == 1 && mesh.queued_params == config.radio);
assert(mesh.radio.profiles.secondary.params == before.secondary.params);
assert(mesh.radio.profiles.secondary.mode == before.secondary.mode);
assert(mesh.radio.profiles.generation[0] == before.generation[0]);
assert(mesh.radio.profiles.generation[1] == before.generation[1]);
assert((mesh.queued[12] & 0x18) == 0); // No want_ack/via_mqtt bits.
mt::DecodedText decoded;
assert(mt::decodeText(mesh.queued.data(), mesh.queued.size(), medium ? "MediumFast" : "LongFast",
mt::DefaultPsk, sizeof(mt::DefaultPsk), 0x99887766, decoded));
assert(!strcmp(decoded.text, "Node: caf\xc3\xa9"));
}
static void bounds() {
char output[beacon::MaxFormattedBytes + 2];
uint8_t raw[mt::MaxFrameSize + 1];
char name[33], text[82];
memset(name, 'n', sizeof(name)); name[31] = 0;
memset(text, 't', sizeof(text)); text[80] = 0;
memset(output, 0xa5, sizeof(output));
assert(beacon::formatText(name, text, output, sizeof(output)) == beacon::MaxFormattedBytes);
assert(output[beacon::MaxFormattedBytes] == 0);
assert(uint8_t(output[beacon::MaxFormattedBytes + 1]) == 0xa5);
assert(!beacon::formatText(name, text, output, beacon::MaxFormattedBytes));
assert(!beacon::formatText(nullptr, text, output, sizeof(output)));
assert(!beacon::formatText("", text, output, sizeof(output)));
assert(!beacon::formatText(name, text, nullptr, sizeof(output)));
name[31] = 'n'; name[32] = 0;
assert(!beacon::formatText(name, text, output, sizeof(output)));
name[31] = 0;
text[80] = 't'; text[81] = 0;
assert(!beacon::validText(text));
assert(!beacon::validText(nullptr) && !beacon::validText(""));
for (unsigned control = 1; control <= 127; ++control) {
if (control >= 32 && control != 127) continue;
const char controlled[] = {'t', static_cast<char>(control), 'x', 0};
assert(!beacon::validText(controlled));
assert(!beacon::formatText("node", controlled, output, sizeof(output)));
}
for (const char* invalid : {"\x80", "\xc0\xaf", "\xe0\x80\x80", "\xed\xa0\x80",
"\xf0\x80\x80\x80", "\xf4\x90\x80\x80", "\xc2", "\xe2\x82"}) {
assert(!beacon::validText(invalid));
assert(!beacon::formatText(invalid, "text", output, sizeof(output)));
}
// Exact maximum byte lengths, including multibyte scalars, stay intact.
memset(text, 't', 77); memcpy(text + 77, "\xe2\x82\xac", 4);
assert(beacon::validText(text));
memset(name, 'n', 27); memcpy(name + 27, "\xf0\x9f\x9a\x80", 5);
assert(beacon::formatText(name, text, output, sizeof(output)) == beacon::MaxFormattedBytes);
memset(raw, 0xa5, sizeof(raw));
assert(!beacon::meshtextFrame(static_cast<mt::Preset>(255), 4, 1, 0, "n", "text", raw));
for (uint32_t from : {0U, UINT32_MAX})
assert(!beacon::meshtextFrame(mt::Preset::LongFast, from, 1, 0, "n", "text", raw));
assert(!beacon::meshtextFrame(mt::Preset::LongFast, 4, 0, 0, "n", "text", raw));
assert(!beacon::meshtextFrame(mt::Preset::LongFast, 4, 1, 8, "n", "text", raw));
for (uint8_t byte : raw) assert(byte == 0xa5); // Rejection produces no partial frame.
assert(beacon::meshtextFrame(mt::Preset::LongFast, 4, 1, 0, name, text, raw));
assert(raw[mt::MaxFrameSize] == 0xa5);
uint8_t payload[beacon::MaxPublicPayloadBytes + 1];
memset(payload, 0xa5, sizeof(payload));
assert(!beacon::publicPayload(1, name, text, payload, beacon::MaxPublicPayloadBytes - 1));
for (uint8_t byte : payload) assert(byte == 0xa5);
assert(beacon::publicPayload(1, name, text, payload, sizeof(payload)) == beacon::MaxPublicPayloadBytes);
assert(payload[beacon::MaxPublicPayloadBytes] == 0xa5);
}
static void admission() {
auto config = slot();
{ Mesh mesh; mesh.accept = false;
assert(!mesh.queueTextBeacon(config, "Node", 0, 0));
assert(mesh.obtained == 1 && mesh.released == 1 && mesh.tables.sent == 0 && mesh.queues == 1); }
{ Mesh mesh; mesh.pool_available = false;
assert(!mesh.queueTextBeacon(config, "Node", 0, 0));
assert(mesh.obtained == 0 && mesh.released == 0 && mesh.queues == 0); }
{ Mesh mesh; mesh.temporary = true;
assert(mesh.queueTextBeacon(config, "Node", 0, 0));
assert(mesh.queues == 1 && mesh.obtained == 1 && mesh.released == 1); }
{ Mesh mesh; mesh.radio.valid = false;
assert(!mesh.validateTextBeacon(config));
assert(!mesh.queueTextBeacon(config, "Node", 0, 0) && !mesh.queues); }
{ Mesh mesh; config.radio.sf = 0; assert(!mesh.validateTextBeacon(config)); }
{ Mesh mesh; config = slot(); config.radio.preamble = 7; assert(!mesh.validateTextBeacon(config)); }
{ Mesh mesh; config = slot(); config.radio.preamble = 65528; config.radio.sf = 12;
config.radio.bw = 7.8f; assert(!mesh.validateTextBeacon(config)); }
{ Mesh mesh; config = slot(); config.power = -10; assert(!mesh.validateTextBeacon(config)); }
{ Mesh mesh; config = slot(); config.power = 23; assert(!mesh.validateTextBeacon(config)); }
{ Mesh mesh; config = slot(); config.hops = 8; assert(!mesh.validateTextBeacon(config)); }
{ Mesh mesh; config = slot(); config.radio.protocol = static_cast<RadioProfileProtocol>(2);
assert(!mesh.validateTextBeacon(config)); }
{ Mesh mesh; config = slot(true); mesh.radio.meshtext = false; assert(!mesh.validateTextBeacon(config)); }
{ Mesh mesh; config = slot(true); config.preset = 255; assert(!mesh.validateTextBeacon(config)); }
{ Mesh mesh; config = slot(true); assert(!mesh.queueTextBeacon(config, "Node", 4, 0));
assert(!mesh.queues && !mesh.obtained); }
{ Mesh mesh; config = slot(); assert(!mesh.queueTextBeacon(config, "", 0, 0));
assert(!mesh.queues && !mesh.obtained); }
}
static void identity(const char* public_hex, const char* expected) {
const auto public_key = unhex(public_hex);
assert(public_key.size() == 32);
const uint32_t wanted = static_cast<uint32_t>(strtoul(expected, nullptr, 10));
assert(beacon::nodeId(public_key.data()) == wanted);
assert(beacon::nodeId(nullptr) == 0);
Mesh mesh;
memcpy(mesh.self_id.pub_key, public_key.data(), 32);
assert(mesh.beaconNodeId() == wanted && wanted >= 4 && wanted != UINT32_MAX);
}
int main(int argc, char** argv) {
assert(argc >= 2);
if (!strcmp(argv[1], "public")) { assert(argc == 3); public_frame(argv[2]); }
else if (!strcmp(argv[1], "longfast")) { assert(argc == 3); meshtext_frame(argv[2], false); }
else if (!strcmp(argv[1], "mediumfast")) { assert(argc == 3); meshtext_frame(argv[2], true); }
else if (!strcmp(argv[1], "bounds")) bounds();
else if (!strcmp(argv[1], "admission")) admission();
else if (!strcmp(argv[1], "identity")) { assert(argc == 4); identity(argv[2], argv[3]); }
else assert(false);
puts("PASS");
}
'''
class BeaconFramesTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
try:
from cryptography.hazmat.primitives.ciphers import Cipher # noqa: F401
except ImportError:
raise unittest.SkipTest("Python cryptography is required for independent frame vectors")
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
if not compiler:
raise unittest.SkipTest("a host C++ compiler is required")
required = ["AESCommon.cpp", "AES128.cpp", "AES256.cpp", "BlockCipher.cpp",
"Crypto.cpp", "SHA256.cpp", "Hash.cpp"]
override = os.environ.get("MESHCORE_CRYPTO_DIR")
candidates = [Path(override)] if override else sorted((ROOT / ".pio/libdeps").glob("*/Crypto"))
crypto = next((path for path in candidates if all((path / name).is_file() for name in required)), None)
if not crypto:
raise unittest.SkipTest("an existing rweather/Crypto checkout is required (MESHCORE_CRYPTO_DIR)")
cls.temp = tempfile.TemporaryDirectory(prefix="meshcore-beacon-frames-")
cls.addClassCleanup(cls.temp.cleanup)
work = Path(cls.temp.name)
(work / "Stream.h").write_text(
"#pragma once\nclass Stream { public: void print(char) {} };\n", encoding="ascii")
(work / "Mesh.h").write_text(ADAPTER, encoding="ascii")
group_builder = body((ROOT / "src/Mesh.cpp").read_text(encoding="utf-8"),
"Packet* Mesh::createGroupDatagram(")
cpp = work / "beacon.cpp"
cpp.write_text(HARNESS + "\nnamespace mesh {\n" + group_builder + "\n}\n", encoding="ascii")
cls.binary = work / ("beacon.exe" if sys.platform == "win32" else "beacon")
sanitizers = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
if sys.platform.startswith("linux") else [])
result = subprocess.run(
[compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror", "-Wno-unused-parameter",
"-DMESH_ENABLE_TX_BEACONS=1",
"-DMESH_ENABLE_MESHTEXT_WIRE=1", "-DMESH_ENABLE_MESHTEXT_CHAT=0",
"-DMIN_LORA_TX_POWER=-9", "-DMAX_LORA_TX_POWER=22", *sanitizers,
"-I" + str(work), "-I" + str(crypto), "-I" + str(ROOT / "src"), str(cpp),
str(ROOT / "src/helpers/BeaconFrames.cpp"), str(ROOT / "src/MeshBeacon.cpp"),
str(ROOT / "src/helpers/MeshtextChat.cpp"), str(ROOT / "src/Utils.cpp"),
str(ROOT / "src/Packet.cpp"), *[str(crypto / name) for name in required],
"-o", str(cls.binary)], capture_output=True, text=True, timeout=90)
if result.returncode:
raise AssertionError(result.stdout + result.stderr)
def run_case(self, *arguments):
result = subprocess.run([str(self.binary), *arguments], capture_output=True, text=True, timeout=30)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("PASS", result.stdout)
def test_public_frame_matches_independent_aes_ecb_hmac_oracle_and_dedup(self):
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
plain = struct.pack("<I", 0x12345678) + b"\0Node;name: hello"
plain += bytes((-len(plain)) % 16)
encryptor = Cipher(algorithms.AES(PUBLIC_KEY), modes.ECB()).encryptor()
encrypted = encryptor.update(plain) + encryptor.finalize()
mac = hmac.digest(PUBLIC_KEY + bytes(16), encrypted, "sha256")[:2]
expected = b"\x15\0" + hashlib.sha256(PUBLIC_KEY).digest()[:1] + mac + encrypted
self.run_case("public", expected.hex())
def test_longfast_and_mediumfast_match_independent_aes_ctr_wire_oracle(self):
for preset, hops in (("LongFast", 3), ("MediumFast", 7)):
with self.subTest(preset=preset):
frame = reference_frame(data_text("Node: caf\u00e9".encode()), preset, DEFAULT_KEY,
flags=hops | (hops << 5))
self.run_case(preset.lower(), frame.hex())
def test_utf8_size_null_sender_preset_and_no_partial_frame_bounds(self):
self.run_case("bounds")
def test_queue_failure_pool_cleanup_temporary_radio_and_parameter_validation(self):
self.run_case("admission")
def test_beacon_identity_is_domain_separated_from_normal_chat(self):
for first in range(16):
public = bytes([first]) + bytes(range(1, 32))
raw = int.from_bytes(hashlib.sha256(public + b"beacon").digest()[:4], "little")
expected = raw if 4 <= raw < 0xFFFFFFFF else 0x0BADF00D
chat_raw = int.from_bytes(hashlib.sha256(public).digest()[:4], "little")
chat = chat_raw if 4 <= chat_raw < 0xFFFFFFFF else 0x0BADF00D
if expected == chat:
expected = 4 if expected == 0xFFFFFFFE else expected + 1
self.assertNotEqual(expected, chat)
self.run_case("identity", public.hex(), str(expected))
if __name__ == "__main__":
unittest.main()
+102
View File
@@ -0,0 +1,102 @@
"""Exercise production beacon CLI, scheduling, and transactional persistence.
The preset/modem helpers are production code; AES is deliberately not used by
this fixture. Real-AES wire vectors have their separate meshtext codec suite.
"""
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
class BeaconManagerTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
compiler = shutil.which("g++") or shutil.which("clang++")
if not compiler:
raise AssertionError("a C++ compiler is required")
cls.work = tempfile.TemporaryDirectory(prefix="meshcore-beacon-manager-")
cls.exe = Path(cls.work.name) / "manager"
command = [compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror", "-O1", "-g",
*(["-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
if sys.platform.startswith("linux") else []),
"-DMESH_ENABLE_TX_BEACONS=1", "-DMESH_ENABLE_MESHTEXT_CHAT=1",
"-DMESH_ENABLE_MESHTEXT_WIRE=1",
"-I", str(ROOT / "test/fixtures/beacons/mocks"),
"-I", str(ROOT / "test/fixtures/radio_profiles/mocks"),
"-isystem", str(ROOT / "test/mocks"), "-I", str(ROOT / "src"),
str(ROOT / "src/helpers/BeaconManager.cpp"),
str(ROOT / "src/helpers/MeshtextChat.cpp"),
str(ROOT / "test/fixtures/beacons/manager_test.cpp"), "-o", str(cls.exe)]
cls.build_command = command
result = subprocess.run(command, capture_output=True, text=True)
if result.returncode:
raise AssertionError(result.stdout + result.stderr)
@classmethod
def tearDownClass(cls):
cls.work.cleanup()
def run_scenario(self, scenario):
result = subprocess.run([str(self.exe), scenario], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def test_commands_and_bounds(self):
self.run_scenario("commands")
def test_all_intervals_and_wrap(self):
self.run_scenario("schedule")
def test_rolling_guards_and_no_retries(self):
self.run_scenario("rolling")
def test_every_config_short_write_and_recovery(self):
self.run_scenario("storage")
def test_durable_meshtext_ids_fail_closed(self):
self.run_scenario("ids")
def test_only_committed_mutations_cancel_queued_frames(self):
self.run_scenario("mutations")
def test_missing_active_counter_never_rewinds_to_backup(self):
self.run_scenario("stale_backup")
def negative_control(self, name, source, scenario):
generated = Path(self.work.name) / (name + ".cpp")
generated.write_text(source, encoding="ascii")
executable = Path(self.work.name) / name
command = list(self.build_command)
command[command.index(str(ROOT / "src/helpers/BeaconManager.cpp"))] = str(generated)
command[-1] = str(executable)
command[1:1] = ["-I", str(ROOT / "src/helpers")]
compiled = subprocess.run(command, capture_output=True, text=True)
self.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr)
result = subprocess.run([str(executable), scenario], capture_output=True, text=True)
self.assertNotEqual(result.returncode, 0, "old behavior unexpectedly passed the regression")
self.assertIn("Assertion", result.stderr)
def test_negative_control_uncancelled_old_frame_is_detected(self):
source = (ROOT / "src/helpers/BeaconManager.cpp").read_text(encoding="ascii")
cancellation = "if (mesh_) mesh_->cancelBeaconRadioFrame();"
self.assertEqual(source.count(cancellation), 4)
self.negative_control("uncancelled", source.replace(cancellation, ""), "mutations")
def test_negative_control_counter_backup_rewind_is_detected(self):
source = (ROOT / "src/helpers/BeaconManager.cpp").read_text(encoding="ascii")
held = """ // Missing active metadata does not prove an interrupted rename. A
// stale backup can survive a cleanup failure after a newer ID was
// transmitted. Never rewind to it, even before the first MT setting.
ids_hold_ = true;"""
restored = """ if (result != ReadResult::Missing || !fs_->rename(IdBackup, IdPath)) ids_hold_ = true;
else result = ReadResult::Valid;"""
self.assertEqual(source.count(held), 1)
self.negative_control("rewound", source.replace(held, restored), "stale_backup")
if __name__ == "__main__":
unittest.main()
+402
View File
@@ -0,0 +1,402 @@
#!/usr/bin/env python3
"""Execute the actual transient beacon radio methods against a faultable modem."""
from pathlib import Path
import os
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
def method(source, signature):
start = source.index(signature)
end = source.index("{", start) + 1
depth = 1
while depth:
depth += (source[end] == "{") - (source[end] == "}")
end += 1
return source[start:end]
HARNESS = r'''
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <initializer_list>
#include <RadioProfiles.h>
#define MESH_ENABLE_TX_BEACONS 1
#define MESH_ENABLE_MESHTEXT_WIRE 1
#define STATE_IDLE 0
#define STATE_RX 1
#define STATE_TX_WAIT 3
#define RADIOLIB_ERR_NONE 0
#define RADIOLIB_ERR_SPI_CMD_TIMEOUT -705
#define RADIOLIB_CHANNEL_FREE -15
#define MAX_TRANS_UNIT 255
#define NF_CALIB_SETTLE_MS 7
#define MESH_DEBUG_PRINTLN(...) ((void)0)
namespace mesh { enum class RadioParamApplyResult { APPLIED, BUSY, FAILED }; }
using Result = mesh::RadioParamApplyResult;
uint32_t now_ms = 100;
uint32_t millis() { return now_ms; }
uint32_t profileTimestamp(bool) { return now_ms * 1000; }
struct Board {
unsigned before = 0, after = 0;
void onBeforeTransmit() { ++before; }
void onAfterTransmit() { ++after; }
};
struct Modem {
bool standby_fail = false, clear_fail = false, param_fail = false, arm_fail = false;
int tx_result = 0, scan_result = RADIOLIB_CHANNEL_FREE;
float freq = 906, bw = 62.5;
uint8_t sf = 7, cr = 5;
uint16_t preamble = 32;
mesh::RadioProfileProtocol protocol = mesh::RadioProfileProtocol::MeshCore;
int8_t power = 20;
unsigned standbys = 0, clears = 0, finishes = 0;
int standby() { ++standbys; return standby_fail ? -1 : 0; }
int clearIrqFlags(uint32_t) { ++clears; return clear_fail ? -1 : 0; }
int finishTransmit() { ++finishes; return 0; }
int startTransmit(uint8_t*, int) { return tx_result; }
int setPreambleLength(uint16_t value) { preamble = value; return 0; }
};
struct RadioLibWrapper {
Modem modem;
Modem* _radio = &modem;
Board board;
Board* _board = &board;
mesh::RadioProfiles _profiles;
bool _beacon_owned = false, _beacon_restoring = false;
uint8_t _beacon_restore_profile = 0;
uint32_t _beacon_restore_at = 0, _beacon_cad_timeout = 0;
uint8_t state = STATE_RX, _active_profile = 0;
uint32_t _profile_generation = 1, _profile_visit_stamp = 0;
bool _profile_refresh_required = false;
uint16_t _physical_preamble = 32;
mesh::RadioProfileProtocol _physical_protocol = mesh::RadioProfileProtocol::MeshCore;
float _cur_freq = 906, _cur_bw = 62.5;
uint8_t _cur_sf = 7, _cur_cr = 5;
int8_t _cur_dbm = 20;
bool _params_valid = true, _dbm_valid = true;
bool _cw_active = false, pending = false, packet = false, busy = false, calibrating = false;
bool _rx_ps_enabled = false, _rx_ps_armed = false, _rx_hold_continuous = false;
bool _rx_ps_continuous_fallback = false, _nf_calib_active = false;
bool _rx_boosted_gain_valid = true, _cur_rx_boosted_gain = true;
bool _wd_last_busy = false;
uint8_t _startrx_fails = 0, _irq_probe_failures = 0;
uint32_t _rx_ps_rx_us = 50000, _rx_ps_sleep_us = 50000;
uint32_t _wd_last_transition = 0, _wd_stuck_thresh = 0, _wd_observe_ms = 0;
uint32_t n_wd_soft = 0, n_wd_hard = 0;
unsigned long _nf_sample_from = 0;
unsigned applications = 0, protocols = 0, power_sets = 0, arms = 0, stops = 0;
unsigned deep_calls = 0, resets = 0, recalibrations = 0;
unsigned retune_begins = 0, retune_ends = 0, scans = 0;
bool deep_ok = true, repair_on_deep = false, power_fail = false, protocol_fail = false;
RadioLibWrapper() {
_profiles.primary.freq = 906; _profiles.primary.bw = 62.5;
_profiles.primary.sf = 7; _profiles.primary.cr = 5;
}
bool validateProfile(const mesh::RadioProfileParams& p) const {
return mesh::RadioProfiles::valid(p) && p.freq <= 960 && p.bw <= 500;
}
bool isInRecvMode() const { return state == STATE_RX; }
bool isCalibratingNoiseFloor() const { return calibrating; }
bool isChipBusy() { return busy; }
bool isReceivingPacket() { return packet; }
bool hasPendingRadioInterrupt() const { return pending; }
bool claimRadioInterrupt() { bool had = pending; pending = false; return had; }
void beginProfileRetune(bool) { ++retune_begins; }
void endProfileRetune(bool) { ++retune_ends; }
void cacheParams(float f, float b, uint8_t s, uint8_t c) {
_cur_freq = f; _cur_bw = b; _cur_sf = s; _cur_cr = c; _params_valid = true;
}
uint16_t profilePreamble(uint8_t profile) const { return _profiles.preamble(profile, 32); }
uint16_t currentPreambleLength() const { return _physical_preamble; }
bool applyParams(float f, float b, uint8_t s, uint8_t c) {
++applications;
if (modem.param_fail) return false;
modem.freq = f; modem.bw = b; modem.sf = s; modem.cr = c;
modem.preamble = _physical_preamble;
return true;
}
bool applyProtocol(mesh::RadioProfileProtocol p) {
++protocols;
if (protocol_fail) return false;
modem.protocol = p; return true;
}
int applyCachedTxPower(int8_t power) {
++power_sets; if (power_fail) return -1; modem.power = power; return 0;
}
uint32_t cadScanTimeoutMillis(uint8_t sf, float) const { return sf * 10; }
int performChannelScanWithTimeout(uint32_t timeout) {
assert(timeout == _beacon_cad_timeout); ++scans; pending = true;
return modem.scan_result;
}
void stopReceiveDutyCycle() { ++stops; _rx_ps_armed = false; }
int startReceiveMode() {
++arms;
if (modem.arm_fail) return -1;
_rx_ps_armed = _rx_ps_enabled; return 0;
}
uint32_t getEstAirtimeFor(int) { return 100; }
bool registerInterruptAction() { return true; }
void recalibrateNoiseFloor() { ++recalibrations; }
bool applyRxBoostedGainMode(bool) { return true; }
bool supportsRadioDeepInit() const { return true; }
bool radioDeepInit() {
++deep_calls;
if (!deep_ok) return false;
if (repair_on_deep) {
busy = false; modem.standby_fail = modem.clear_fail = modem.param_fail = modem.arm_fail = false;
power_fail = protocol_fail = false;
}
return true;
}
void resetAGC() { ++resets; }
void idle() { modem.standby(); state = STATE_IDLE; }
uint8_t beginReconfigure();
bool restoreAfterDeepInit();
bool recoverRadio(bool hard);
void startRecv();
bool startSendRaw(const uint8_t*, int);
void onSendFinished();
uint32_t getBeaconAirtime(const mesh::RadioProfileParams&, int);
void cacheBeaconRestoreParams();
Result prepareBeaconTransmit(const mesh::RadioProfileParams&, int8_t);
Result restoreBeaconReceive();
bool isBeaconChannelBusy();
};
@METHODS@
mesh::RadioProfileParams beacon(bool meshtext = false) {
mesh::RadioProfileParams p;
p.freq = 915; p.bw = 250; p.sf = 11; p.cr = 5;
if (meshtext) p.protocol = mesh::RadioProfileProtocol::Meshtext;
return p;
}
int main() {
unsigned cases = 0;
for (bool dual : {false, true}) for (bool rxps : {false, true}) for (bool mt : {false, true}) {
RadioLibWrapper w; now_ms = 100;
if (dual) {
w._profiles.secondary.params = w._profiles.primary;
w._profiles.secondary.params.freq = 908;
w._profiles.secondary.mode = mesh::RadioProfileMode::RxTx;
w._active_profile = 1; w._cur_freq = 908; w.modem.freq = 908;
}
w._rx_ps_enabled = w._rx_ps_armed = rxps;
const auto before = w._profiles;
assert(w.prepareBeaconTransmit(beacon(mt), 10) == Result::APPLIED);
assert(w._beacon_owned && w.state == STATE_IDLE && w.arms == 0);
assert(w.modem.freq == 915 && w.modem.power == 10);
assert(w.modem.preamble == (mt ? 16 : 32));
assert(w._cur_freq == (dual ? 908 : 906) && w._cur_dbm == 20);
assert(w._profiles.primary == before.primary && w._profiles.secondary.params == before.secondary.params);
assert(w._profiles.generation[0] == before.generation[0] && w._profiles.generation[1] == before.generation[1]);
assert(w._profiles.switches == 0 && w._profiles.switch_test_samples[0] == 0);
assert(w.beginReconfigure() == 2);
w.startRecv(); assert(w.arms == 0);
assert(w.recoverRadio(true) == false && w.deep_calls == 0);
assert(!w.isBeaconChannelBusy() && !w.pending && w.state == STATE_IDLE);
assert(w.restoreBeaconReceive() == Result::APPLIED);
assert(!w._beacon_owned && w.state == STATE_RX && w._rx_ps_armed == rxps);
assert(w.modem.freq == (dual ? 908 : 906) && w.modem.power == 20);
assert(w.modem.protocol == mesh::RadioProfileProtocol::MeshCore);
assert(w.recalibrations == 0 && w.deep_calls == 0);
++cases;
}
for (unsigned reason = 0; reason < 5; ++reason) {
RadioLibWrapper w;
if (reason == 0) w.pending = true;
if (reason == 1) w.packet = true;
if (reason == 2) w.busy = true;
if (reason == 3) w._cw_active = true;
if (reason == 4) w.calibrating = true;
assert(w.prepareBeaconTransmit(beacon(), 10) == Result::BUSY);
assert(!w._beacon_owned && w.applications == 0 && w.power_sets == 0);
++cases;
}
for (unsigned failure = 0; failure < 4; ++failure) {
RadioLibWrapper w;
if (failure == 0) w.modem.param_fail = true;
if (failure == 1) w.protocol_fail = true;
if (failure == 2) w.power_fail = true;
if (failure == 3) w.modem.standby_fail = true;
assert(w.prepareBeaconTransmit(beacon(true), 10) == Result::FAILED && w._beacon_owned);
w.repair_on_deep = true;
assert(w.restoreBeaconReceive() == Result::APPLIED && !w._beacon_owned);
assert(w.modem.freq == 906 && w.modem.protocol == mesh::RadioProfileProtocol::MeshCore && w.modem.power == 20);
++cases;
}
for (unsigned changed = 0; changed < 2; ++changed) {
RadioLibWrapper w;
w._profiles.secondary.params = w._profiles.primary;
w._profiles.secondary.params.freq = 908;
w._profiles.secondary.mode = mesh::RadioProfileMode::RxTx;
w._active_profile = 1;
assert(w.prepareBeaconTransmit(beacon(), 10) == Result::APPLIED);
if (changed == 0) {
w._profiles.secondary.params.freq = 909; ++w._profiles.generation[1];
} else w._profiles.secondary.mode = mesh::RadioProfileMode::Off;
assert(w.restoreBeaconReceive() == Result::APPLIED);
assert(w.modem.freq == (changed == 0 ? 909 : 906));
assert(w._active_profile == (changed == 0 ? 1 : 0));
++cases;
}
for (int cad_result : {RADIOLIB_CHANNEL_FREE, -6, -16, -702}) {
RadioLibWrapper w;
assert(w.prepareBeaconTransmit(beacon(), 10) == Result::APPLIED);
w.modem.scan_result = cad_result;
assert(w.isBeaconChannelBusy() == (cad_result != RADIOLIB_CHANNEL_FREE));
assert(!w.pending && w.arms == 0 && w.state == STATE_IDLE);
assert(w.restoreBeaconReceive() == Result::APPLIED);
++cases;
}
for (bool spi_timeout : {false, true}) {
RadioLibWrapper w;
assert(w.prepareBeaconTransmit(beacon(true), 10) == Result::APPLIED);
w.modem.tx_result = spi_timeout ? RADIOLIB_ERR_SPI_CMD_TIMEOUT : -1;
const uint8_t bytes[] = {1, 2};
assert(!w.startSendRaw(bytes, 2) && w._beacon_owned && w.deep_calls == 0);
assert(w._cur_freq == 906 && w._cur_dbm == 20);
assert(w.board.before == 1 && w.board.after == 1);
assert(w.restoreBeaconReceive() == Result::APPLIED && w.modem.freq == 906);
++cases;
}
{
RadioLibWrapper w;
assert(w.prepareBeaconTransmit(beacon(), 10) == Result::APPLIED);
const uint8_t bytes[] = {1, 2};
assert(w.startSendRaw(bytes, 2));
assert(w.restoreBeaconReceive() == Result::BUSY && w._beacon_owned);
w.onSendFinished();
assert(w.restoreBeaconReceive() == Result::APPLIED);
assert(w.board.before == 1 && w.board.after == 1 && w.modem.finishes == 1);
++cases;
}
for (unsigned failure = 0; failure < 5; ++failure) {
RadioLibWrapper w; now_ms = UINT32_MAX - 50;
assert(w.prepareBeaconTransmit(beacon(true), 10) == Result::APPLIED);
if (failure == 0) w.modem.standby_fail = true;
if (failure == 1) w.modem.param_fail = true;
if (failure == 2) w.modem.clear_fail = true;
if (failure == 3) w.modem.arm_fail = true;
if (failure == 4) w.busy = true;
w.deep_ok = false;
assert(w.restoreBeaconReceive() == Result::BUSY && w._beacon_owned);
const unsigned before = w.applications;
++now_ms;
assert(w.restoreBeaconReceive() == Result::BUSY && w.applications == before);
now_ms += 100; w.deep_ok = w.repair_on_deep = true;
assert(w.restoreBeaconReceive() == Result::APPLIED && !w._beacon_owned);
assert(w.modem.freq == 906 && w.modem.power == 20 && w.state == STATE_RX);
++cases;
}
{
RadioLibWrapper w;
assert(w.prepareBeaconTransmit(beacon(true), 10) == Result::APPLIED);
assert(w.restoreAfterDeepInit());
assert(w.modem.freq == 906 && w.modem.power == 20);
assert(w.modem.protocol == mesh::RadioProfileProtocol::MeshCore && w._beacon_owned);
assert(w.restoreBeaconReceive() == Result::APPLIED);
++cases;
}
{
RadioLibWrapper w;
auto p = beacon();
assert(w.getBeaconAirtime(p, 255) > 0);
assert(!w.getBeaconAirtime(p, 0) && !w.getBeaconAirtime(p, 256));
p.freq = 1000;
assert(!w.getBeaconAirtime(p, 20));
assert(w.prepareBeaconTransmit(p, 10) == Result::FAILED && !w._beacon_owned);
++cases;
}
std::printf("PASS: %u actual-method beacon radio cases\n", cases);
}
'''
class BeaconRadioTest(unittest.TestCase):
def source(self):
source = (ROOT / "src/helpers/radiolib/RadioLibWrappers.cpp").read_text()
signatures = [
"uint8_t RadioLibWrapper::beginReconfigure()",
"bool RadioLibWrapper::restoreAfterDeepInit()",
"bool RadioLibWrapper::recoverRadio(bool hard)",
"void RadioLibWrapper::startRecv()",
"bool RadioLibWrapper::startSendRaw(const uint8_t* bytes, int len)",
"void RadioLibWrapper::onSendFinished()",
"uint32_t RadioLibWrapper::getBeaconAirtime(",
"void RadioLibWrapper::cacheBeaconRestoreParams()",
"mesh::RadioParamApplyResult RadioLibWrapper::prepareBeaconTransmit(",
"bool RadioLibWrapper::isBeaconChannelBusy()",
"mesh::RadioParamApplyResult RadioLibWrapper::restoreBeaconReceive()",
]
return HARNESS.replace("@METHODS@", "\n\n".join(method(source, name) for name in signatures))
def run_harness(self, code, expect_pass=True):
with tempfile.TemporaryDirectory(prefix="meshcore-beacon-radio-") as directory:
cpp = Path(directory) / "test.cpp"
binary = Path(directory) / "test"
cpp.write_text(code, encoding="ascii")
command = ["g++", "-std=c++17", "-Wall", "-Wextra", "-Werror", "-I", str(ROOT / "src"),
"-fsanitize=address,undefined", "-fno-omit-frame-pointer", "-fno-pie", "-no-pie",
str(cpp), "-o", str(binary)]
build = subprocess.run(command, capture_output=True, text=True)
self.assertEqual(build.returncode, 0, build.stdout + build.stderr)
env = dict(os.environ, ASAN_OPTIONS="detect_leaks=1:halt_on_error=1",
UBSAN_OPTIONS="halt_on_error=1:print_stacktrace=1")
result = subprocess.run([str(binary)], capture_output=True, text=True, env=env)
if expect_pass:
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("PASS:", result.stdout)
print(result.stdout.strip())
else:
self.assertNotEqual(result.returncode, 0, "negative control unexpectedly passed")
self.assertIn("Assertion", result.stderr)
def test_actual_radio_transaction(self):
self.run_harness(self.source())
def test_ownership_and_snapshot_negative_controls(self):
source = self.source()
mutations = [
("if (_beacon_owned && !_beacon_restoring) return 2;", "/* remove ownership guard */"),
("if (_beacon_owned && !_beacon_restoring) return;", "/* permit RX on beacon tuple */"),
("bool applied = _radio->standby() == RADIOLIB_ERR_NONE",
"cacheParams(p.freq, p.bw, p.sf, p.cr); bool applied = _radio->standby() == RADIOLIB_ERR_NONE"),
]
for old, new in mutations:
with self.subTest(mutation=old):
self.assertIn(old, source)
self.run_harness(source.replace(old, new, 1), expect_pass=False)
def test_feature_gate_matrix(self):
cases = [
([], 0, 0),
(["STM32_PLATFORM"], 0, 0),
(["USE_SX1262"], 0, 0),
(["NRF52_PLATFORM", "USE_SX1262"], 0, 1),
# The codec is stable across translation units; the concrete
# unqualified Indicator driver still declines Meshtext TX.
(["NRF52_PLATFORM", "USE_SX1262", "SENSECAP_INDICATOR_LORA"], 0, 1),
(["USE_SX1262", "MESH_ENABLE_TX_BEACONS=0"], 0, 0),
(["USE_SX1262", "MESH_PRIMARY_ESPNOW=1"], 0, 0),
(["ESP32_PLATFORM", "CONFIG_IDF_TARGET_ESP32S3", "COMPANION_RADIO_FULL", "USE_SX1262"], 1, 1),
(["ESP32_PLATFORM", "CONFIG_IDF_TARGET_ESP32S3", "COMPANION_RADIO_FULL", "USE_SX1262",
"MESH_ENABLE_TX_BEACONS=0"], 1, 1),
(["NRF52_PLATFORM", "RAK_4631", "COMPANION_RADIO_FULL", "USE_SX1262"], 1, 1),
]
for flags, chat, wire in cases:
with self.subTest(flags=flags):
code = '#include <helpers/MeshtextConfig.h>\n'
code += f'static_assert(MESH_ENABLE_MESHTEXT_CHAT == {chat}, "chat gate");\n'
code += f'static_assert(MESH_ENABLE_MESHTEXT_WIRE == {wire}, "wire gate");\n'
command = ["g++", "-std=c++17", "-fsyntax-only", "-x", "c++", "-I", str(ROOT / "src")]
command += ["-D" + flag for flag in flags] + ["-"]
result = subprocess.run(command, input=code, capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
if __name__ == "__main__":
unittest.main()
+111 -2
View File
@@ -22,7 +22,8 @@ class MeshtextConfigTest(unittest.TestCase):
if self.compiler is None:
self.skipTest("a C++ preprocessor is required")
def effective(self, defines, *, arduino_first=False, sdk_target=None):
def effective(self, defines, *, arduino_first=False, sdk_target=None,
feature="MESH_ENABLE_MESHTEXT_CHAT"):
with tempfile.TemporaryDirectory(prefix="meshcore-mt-config-") as folder:
work = Path(folder)
(work / "Arduino.h").write_text(
@@ -37,7 +38,7 @@ class MeshtextConfigTest(unittest.TestCase):
if arduino_first:
includes.reverse()
source = "".join("#include <" + name + ">\n" for name in includes)
source += "MESH_ENABLE_MESHTEXT_CHAT\n"
source += feature + "\n"
result = subprocess.run(
[self.compiler, "-E", "-P", "-x", "c++", "-",
"-I", str(work), "-I", str(ROOT / "src"),
@@ -85,6 +86,114 @@ class MeshtextConfigTest(unittest.TestCase):
"USE_SX1262", "MESH_ENABLE_MESHTEXT_CHAT=0"], 0,
sdk_target="CONFIG_IDF_TARGET_ESP32S3")
def test_tx_beacon_wire_support_does_not_enable_companion_chat(self):
for platform in ("ESP32_PLATFORM", "NRF52_PLATFORM", "RP2040_PLATFORM"):
flags = [platform, "USE_SX1262"]
self.assert_orders(flags, 0)
self.assert_orders(flags, 1, feature="MESH_ENABLE_MESHTEXT_WIRE")
self.assert_orders(flags + ["MESH_ENABLE_TX_BEACONS=0"], 0,
feature="MESH_ENABLE_MESHTEXT_WIRE")
self.assert_orders(flags + ["MESH_PRIMARY_ESPNOW=1"], 1,
feature="MESH_ENABLE_MESHTEXT_WIRE")
self.assert_orders(flags + ["SENSECAP_INDICATOR_LORA"], 1,
feature="MESH_ENABLE_MESHTEXT_WIRE")
self.assert_orders([platform, "USE_LR1110"], 1,
feature="MESH_ENABLE_MESHTEXT_WIRE")
for flags in (["STM32_PLATFORM", "USE_SX1262"], []):
self.assert_orders(flags, 0, feature="MESH_ENABLE_MESHTEXT_WIRE")
def test_late_variant_chip_discovery_does_not_change_wire_layout(self):
with tempfile.TemporaryDirectory(prefix="meshcore-mt-late-variant-") as folder:
work = Path(folder)
# Nano G2 Ultra declares USE_SX1262 in its Arduino variant header,
# not in global flags. The codec translation unit omits Arduino.
(work / "Arduino.h").write_text(
"#pragma once\n#define USE_SX1262 1\n", encoding="ascii")
for arduino_first in (False, True):
source = "#include <Arduino.h>\n" if arduino_first else ""
source += "#include <helpers/MeshtextConfig.h>\n"
source += "static_assert(MESH_ENABLE_MESHTEXT_CHAT == 0, \"chat stays disabled\");\n"
source += "static_assert(MESH_ENABLE_MESHTEXT_WIRE == 1, \"consistent wire layout\");\n"
result = subprocess.run(
[self.compiler, "-std=c++17", "-fsyntax-only", "-x", "c++", "-",
"-DNRF52_PLATFORM", "-I", str(work), "-I", str(ROOT / "src")],
input=source, capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def test_actual_driver_capability_and_mc_only_no_spi(self):
source = (ROOT / "src/helpers/radiolib/CustomSX1262Wrapper.h").read_text()
support = source.index(" bool supportsMeshtext() const override")
start = source.rfind("#if", 0, support)
end = source.index("#endif", support) + len("#endif")
capability = source[start:end]
start = source.index("bool applyProtocol(")
end = source.index("{", start) + 1
depth = 1
while depth:
depth += (source[end] == "{") - (source[end] == "}")
end += 1
protocol = source[start:end].replace(" override", "")
code = r'''
#include <helpers/MeshtextConfig.h>
#include <cstdint>
#include <cassert>
#define RADIOLIB_ERR_NONE 0
namespace mesh { enum class RadioProfileProtocol { MeshCore, Meshtext }; }
struct Base { virtual bool supportsMeshtext() const { return false; } };
struct CustomSX1262 {
unsigned writes = 0;
int setProfileProtocol(uint8_t) { ++writes; return 0; }
};
struct Actual : Base {
CustomSX1262 chip;
CustomSX1262* _radio = &chip;
@CAPABILITY@
#if MESH_ENABLE_MESHTEXT_WIRE
@PROTOCOL@
#endif
};
int main() {
Actual driver;
assert(driver.supportsMeshtext() == EXPECTED);
#if MESH_ENABLE_MESHTEXT_WIRE
assert(driver.applyProtocol(mesh::RadioProfileProtocol::MeshCore));
assert(driver.applyProtocol(mesh::RadioProfileProtocol::Meshtext) == EXPECTED);
assert(driver.chip.writes == (EXPECTED ? 2U : 0U));
#endif
}
'''.replace("@CAPABILITY@", capability).replace("@PROTOCOL@", protocol)
cases = [
(["NRF52_PLATFORM", "USE_SX1262"], True),
(["NRF52_PLATFORM", "USE_SX1262", "SENSECAP_INDICATOR_LORA"], False),
(["NRF52_PLATFORM", "USE_SX1262", "SENSECAP_INDICATOR_LORA",
"MESH_ENABLE_MESHTEXT_CHAT=1"], True),
(["NRF52_PLATFORM", "USE_SX1262", "MESH_ENABLE_MESHTEXT_WIRE=0"], False),
(["NRF52_PLATFORM", "USE_SX1262", "MESH_ENABLE_MESHTEXT_CHAT=1",
"MESH_ENABLE_MESHTEXT_WIRE=0"], False),
]
with tempfile.TemporaryDirectory(prefix="meshcore-mt-driver-gate-") as folder:
binary = Path(folder) / "test"
for flags, expected in cases:
with self.subTest(flags=flags):
command = [self.compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-x", "c++", "-", "-I", str(ROOT / "src"), "-o", str(binary)]
command += ["-D" + flag for flag in flags] + ["-DEXPECTED=" + str(int(expected))]
result = subprocess.run(command, input=code, capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
result = subprocess.run([str(binary)], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def test_chat_keeps_wire_support_with_beacons_disabled(self):
flags = ["NRF52_PLATFORM", "RAK_4631", "COMPANION_RADIO_FULL", "USE_SX1262",
"MESH_ENABLE_TX_BEACONS=0"]
self.assert_orders(flags, 1)
self.assert_orders(flags, 1, feature="MESH_ENABLE_MESHTEXT_WIRE")
flags = ["ESP32_PLATFORM", "ARDUINO=10819", "COMPANION_RADIO_FULL", "USE_SX1262",
"MESH_ENABLE_TX_BEACONS=0"]
self.assert_orders(flags, 1, sdk_target="CONFIG_IDF_TARGET_ESP32S3")
self.assert_orders(flags, 1, feature="MESH_ENABLE_MESHTEXT_WIRE",
sdk_target="CONFIG_IDF_TARGET_ESP32S3")
if __name__ == "__main__":
unittest.main()
+8 -2
View File
@@ -277,7 +277,8 @@ int main() {
}
}
'''
self.compile_and_run(harness + methods + cases, ['-DMESH_ENABLE_MESHTEXT_CHAT=1'])
self.compile_and_run(harness + methods + cases,
['-DMESH_ENABLE_MESHTEXT_CHAT=1', '-DMESH_ENABLE_MESHTEXT_WIRE=1'])
def test_sx1262_wire_settings_and_deep_recovery(self):
wrapper = (ROOT/'src/helpers/radiolib/CustomSX1262Wrapper.h').read_text()
@@ -314,6 +315,8 @@ struct CustomSX1262: SX1262 {
};
struct RadioLibWrapper {
CustomSX1262 chip;CustomSX1262* _radio=&chip;
bool supported=true;
bool supportsMeshtext() const { return supported; }
bool _rx_ps_armed=true,_params_valid=true,_dbm_valid=false,_rx_boosted_gain_valid=false;
int state=1;uint8_t _cur_sf=11,_cur_cr=5;float _cur_freq=906.875,_cur_bw=250;
int8_t _cur_dbm=22;bool _cur_rx_boosted_gain=false;
@@ -343,11 +346,14 @@ int main() {
assert(w.chip.sync==0x2b && w.chip.crc && w.chip.explicit_header && !w.chip.iq);
assert(!w._rx_ps_armed && w.state==STATE_IDLE);
w._physical_protocol=Protocol::MeshCore;assert(w.restoreAfterDeepInit());assert(w.chip.sync==0x12);
RadioLibWrapper disabled;disabled.supported=false;
assert(disabled.applyProtocol(Protocol::MeshCore));
assert(!disabled.applyProtocol(Protocol::Meshtext) && disabled.chip.step==0);
}
'''
self.compile_and_run('#include <initializer_list>\n' + harness.replace('@WIRE@',wire)
.replace('@PROTOCOL@',protocol).replace('@RESTORE@',restore),
['-DMESH_ENABLE_MESHTEXT_CHAT=1'])
['-DMESH_ENABLE_MESHTEXT_CHAT=1', '-DMESH_ENABLE_MESHTEXT_WIRE=1'])
if __name__ == '__main__':