Save valid busy RX, fast-switch and Companion tuple changes once, then apply the latest accepted intent at a safe radio gap. Preserve checked-save rollback, runtime RXEN intent, TempRadio ownership and exact remote reply delivery. Repair failed preamble compensation without hiding uncertain saves. Retry WebConfig, MQTT and NimBLE startup failures automatically. Preserve explicit off choices, OTA ownership and the latest WiFi credentials through cooperative shutdown. Add regression coverage and update command guides. Validation: focused host transaction, rollback, ownership and lifecycle suites; Full RAK4631, Heltec V4 R8 TFT and RC32 packages; representative room, sensor, LR1110, LR2021, SX1276, STM32WL and ESP-NOW builds. Linked RAK crypto and OTA workspace checks pass; its complete OTA image uses 712644/712704 bytes.
446 lines
18 KiB
Python
446 lines
18 KiB
Python
#!/usr/bin/env python3
|
|
"""Execute production role hooks around the separately tested fleet receiver."""
|
|
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
from cpp_source import body
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
HARNESS = r'''
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <vector>
|
|
#include <algorithm>
|
|
#include <SHA256.h>
|
|
#include <helpers/FleetLocation.h>
|
|
#define MESH_ENABLE_CLOCK_SYNC 0
|
|
#define MAX_TKS_ENTRIES 16
|
|
static std::vector<int> events;
|
|
namespace mesh {
|
|
struct Packet {
|
|
bool scoped = false;
|
|
uint16_t transport_codes[2] = {};
|
|
uint8_t payload[32] = {}, payload_len = 32, type = 5;
|
|
bool hasTransportCodes() const { return scoped; }
|
|
uint8_t getPayloadType() const { return type; }
|
|
};
|
|
struct Mesh {
|
|
virtual ~Mesh() = default;
|
|
virtual bool supportsFleetControl() const { return false; }
|
|
virtual bool getFleetLocation(int32_t&, int32_t&) const { return false; }
|
|
virtual void onSendComplete(Packet*) { events.push_back(1); }
|
|
virtual void onSendFail(Packet*) { events.push_back(3); }
|
|
virtual void onRadioProfileCopyQueued(Packet*, const Packet*, uint8_t) { events.push_back(5); }
|
|
};
|
|
}
|
|
struct TransportKey {
|
|
uint8_t key[16] = {};
|
|
uint16_t calcTransportCode(const mesh::Packet*) const;
|
|
};
|
|
struct Region { bool wildcard = false; bool isWildcard() const { return wildcard; } };
|
|
struct Regions {
|
|
std::vector<TransportKey> keys;
|
|
unsigned calls = 0;
|
|
int getTransportKeysFor(const Region&, TransportKey* out, int capacity) {
|
|
++calls;
|
|
const int count = std::min<int>(keys.size(), capacity);
|
|
for (int i = 0; i < count; ++i) out[i] = keys[i];
|
|
return count;
|
|
}
|
|
};
|
|
@SCOPE_HELPERS@
|
|
struct CachedSensors {
|
|
bool valid = false;
|
|
double latitude = 45.5152, longitude = -122.6784;
|
|
mutable unsigned reads = 0;
|
|
bool getCachedGpsPosition(double& lat, double& lon) const {
|
|
++reads; lat = latitude; lon = longitude; return valid;
|
|
}
|
|
};
|
|
struct Profiles {void servicePendingTuning(){}};
|
|
struct Fleet {
|
|
unsigned received = 0, serviced = 0;
|
|
bool scoped = false;
|
|
TransportKey reply_scope;
|
|
uint32_t sequence = 1735689610;
|
|
char command[80] = "set radio2 off", reply[160] = {};
|
|
void receive(mesh::Packet*, mesh::Mesh&, const TransportKey* scope) {
|
|
++received; scoped = scope != nullptr;
|
|
if (scope) reply_scope = *scope;
|
|
}
|
|
void complete(mesh::Packet*, Profiles&) { events.push_back(2); }
|
|
void fail(mesh::Packet*, Profiles&) { events.push_back(4); }
|
|
void copy(mesh::Packet*, const mesh::Packet*) { events.push_back(6); }
|
|
template<typename Handler>
|
|
void service(mesh::Mesh&, Profiles&, const char* name, Handler handler) {
|
|
assert(!strcmp(name, "fleet node")); ++serviced;
|
|
handler(sequence, command, reply);
|
|
}
|
|
};
|
|
struct CLI {
|
|
Fleet fleet;
|
|
void servicePendingTxPower() {}
|
|
void servicePendingRxSettings() {}
|
|
Profiles profiles;
|
|
struct NoMorseReply {
|
|
void complete(const void*) {}
|
|
void fail(const void*) {}
|
|
void copy(const void*, const void*) {}
|
|
} morse;
|
|
NoMorseReply& morseReply() { return morse; }
|
|
bool enrolled = true;
|
|
Fleet* fleetChannel() { return enrolled ? &fleet : nullptr; }
|
|
Profiles& radioProfiles() { return profiles; }
|
|
};
|
|
struct ClockSync {
|
|
unsigned observed = 0;
|
|
void observeGroupPacket(mesh::Packet*) { ++observed; }
|
|
};
|
|
class Repeater : public mesh::Mesh {
|
|
public:
|
|
CLI _cli;
|
|
bool region_load_active = false;
|
|
Regions region_map;
|
|
Region region;
|
|
Region* recv_pkt_region = ®ion;
|
|
struct { const char* node_name = "fleet node"; double node_lat = 0, node_lon = 0; } _prefs;
|
|
CachedSensors sensors;
|
|
#if MESH_ENABLE_FLEET_CONTROL
|
|
bool getFleetLocation(int32_t&, int32_t&) const override;
|
|
#endif
|
|
unsigned commands = 0;
|
|
uint32_t timestamp = 0;
|
|
@REPEATER_CAPABILITY@
|
|
void onGroupPacketRecv(mesh::Packet*);
|
|
void serviceFleet();
|
|
void handleCommand(uint32_t value, void* sender, char* command, char* reply) {
|
|
assert(sender == nullptr && !strcmp(command, "set radio2 off"));
|
|
timestamp = value; ++commands; strcpy(reply, "OK");
|
|
}
|
|
};
|
|
class Room : public mesh::Mesh {
|
|
public:
|
|
CLI _cli;
|
|
ClockSync _clock_sync;
|
|
bool region_load_active = false;
|
|
Regions region_map;
|
|
Region region;
|
|
Region* recv_pkt_region = ®ion;
|
|
struct { const char* node_name = "fleet node"; double node_lat = 0, node_lon = 0; } _prefs;
|
|
CachedSensors sensors;
|
|
#if MESH_ENABLE_FLEET_CONTROL
|
|
bool getFleetLocation(int32_t&, int32_t&) const override;
|
|
#endif
|
|
unsigned commands = 0;
|
|
uint32_t timestamp = 0;
|
|
@ROOM_CAPABILITY@
|
|
void onGroupPacketRecv(mesh::Packet*);
|
|
void serviceFleet();
|
|
#if MESH_ENABLE_FLEET_CONTROL
|
|
void servicePendingPrimaryRadioSettings(bool) {}
|
|
void onSendComplete(mesh::Packet*) override;
|
|
void onSendFail(mesh::Packet*) override;
|
|
void onRadioProfileCopyQueued(mesh::Packet*, const mesh::Packet*, uint8_t) override;
|
|
#endif
|
|
void handleCommand(uint32_t value, char* command, char* reply) {
|
|
assert(!strcmp(command, "set radio2 off"));
|
|
timestamp = value; ++commands; strcpy(reply, "OK");
|
|
}
|
|
};
|
|
@METHODS@
|
|
|
|
template<typename Role>
|
|
void checkRole() {
|
|
Role role;
|
|
int32_t latitude = 0, longitude = 0;
|
|
assert(!role.getFleetLocation(latitude, longitude));
|
|
#if MESH_ENABLE_FLEET_CONTROL
|
|
assert(latitude == 0 && longitude == 0 && role.sensors.reads == 1);
|
|
role.sensors.valid = true;
|
|
role._prefs.node_lat = 47.6062; role._prefs.node_lon = -122.3321;
|
|
assert(role.getFleetLocation(latitude, longitude));
|
|
assert(latitude == 47606200 && longitude == -122332100 && role.sensors.reads == 1);
|
|
role._prefs.node_lat = role._prefs.node_lon = 0;
|
|
assert(role.getFleetLocation(latitude, longitude));
|
|
assert(latitude == 45515200 && longitude == -122678400 && role.sensors.reads == 2);
|
|
#else
|
|
assert(role.sensors.reads == 0);
|
|
#endif
|
|
mesh::Packet packet;
|
|
TransportKey original, rotated;
|
|
memset(original.key, 0x11, sizeof(original.key));
|
|
memset(rotated.key, 0x22, sizeof(rotated.key));
|
|
role.region_map.keys = {original, rotated};
|
|
packet.scoped = true;
|
|
packet.transport_codes[0] = rotated.calcTransportCode(&packet);
|
|
assert(role.supportsFleetControl());
|
|
role.onGroupPacketRecv(&packet);
|
|
role.serviceFleet();
|
|
#if MESH_ENABLE_FLEET_CONTROL
|
|
assert(role._cli.fleet.received == 1 && role._cli.fleet.serviced == 1);
|
|
assert(role._cli.fleet.scoped);
|
|
assert(!memcmp(role._cli.fleet.reply_scope.key, rotated.key, sizeof(rotated.key)));
|
|
assert(role.commands == 1 && role.timestamp == role._cli.fleet.sequence);
|
|
role.region_load_active = true;
|
|
role.serviceFleet();
|
|
assert(role.commands == 1);
|
|
assert(strstr(role._cli.fleet.reply, "region load active"));
|
|
#else
|
|
assert(role._cli.fleet.received == 0 && role._cli.fleet.serviced == 0);
|
|
assert(role.commands == 0);
|
|
#endif
|
|
role._cli.enrolled = false;
|
|
const unsigned before = role._cli.fleet.serviced;
|
|
role.onGroupPacketRecv(&packet);
|
|
role.serviceFleet();
|
|
assert(role._cli.fleet.serviced == before);
|
|
}
|
|
|
|
static void checkScopeSelection() {
|
|
Regions regions;
|
|
Region region;
|
|
mesh::Packet packet;
|
|
TransportKey first, rotated, selected;
|
|
memset(first.key, 0x31, sizeof(first.key));
|
|
memset(rotated.key, 0xA4, sizeof(rotated.key));
|
|
regions.keys = {first, rotated};
|
|
assert(!mesh::captureFleetReplyScope(regions, ®ion, &packet, selected));
|
|
assert(regions.calls == 0);
|
|
packet.scoped = true;
|
|
packet.payload[0] = 0x6D;
|
|
packet.transport_codes[1] = rotated.calcTransportCode(&packet);
|
|
assert(mesh::captureFleetReplyScope(regions, ®ion, &packet, selected));
|
|
assert(!memcmp(selected.key, rotated.key, sizeof(rotated.key)));
|
|
// Codes belong to the received payload. Changing it invalidates the match.
|
|
packet.payload[0] ^= 1;
|
|
assert(!mesh::captureFleetReplyScope(regions, ®ion, &packet, selected));
|
|
packet.payload[0] ^= 1;
|
|
packet.transport_codes[0] = first.calcTransportCode(&packet);
|
|
packet.transport_codes[1] = 0;
|
|
assert(mesh::captureFleetReplyScope(regions, ®ion, &packet, selected));
|
|
assert(!memcmp(selected.key, first.key, sizeof(first.key)));
|
|
region.wildcard = true;
|
|
const unsigned calls = regions.calls;
|
|
assert(!mesh::captureFleetReplyScope(regions, ®ion, &packet, selected));
|
|
assert(!mesh::captureFleetReplyScope(regions, (const Region*)nullptr, &packet, selected));
|
|
assert(!mesh::captureFleetReplyScope(regions, ®ion, nullptr, selected));
|
|
assert(regions.calls == calls);
|
|
}
|
|
|
|
int main() {
|
|
checkScopeSelection();
|
|
checkRole<Repeater>(); checkRole<Room>();
|
|
Room room; mesh::Packet first, copy;
|
|
room.onGroupPacketRecv(&first);
|
|
assert(room._clock_sync.observed == 1); // Existing clock delivery survives.
|
|
room.onSendComplete(&first);
|
|
room.onSendFail(&first);
|
|
room.onRadioProfileCopyQueued(©, &first, 7);
|
|
#if MESH_ENABLE_FLEET_CONTROL
|
|
assert((events == std::vector<int>{1,2,3,4,5,6}));
|
|
#else
|
|
assert((events == std::vector<int>{1,3,5}));
|
|
#endif
|
|
puts("Fleet role receive, deferred dispatch, busy guard and callbacks passed");
|
|
}
|
|
'''
|
|
|
|
AUTHORIZATION = r'''
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <new>
|
|
#include <initializer_list>
|
|
static unsigned configurations = 0, allocations = 0;
|
|
struct RegionMap {};
|
|
static RegionMap* enrolled_regions = nullptr;
|
|
struct FS { bool present = false; bool exists(const char*) const { return present; } };
|
|
namespace mesh {
|
|
struct FleetChannel {
|
|
explicit FleetChannel(void*, void* = nullptr, RegionMap* regions = nullptr) {
|
|
assert(regions); enrolled_regions = regions; ++allocations;
|
|
}
|
|
bool handleConfig(const char*, char* reply, size_t capacity) {
|
|
assert(capacity == 160); ++configurations; strcpy(reply, "OK"); return true;
|
|
}
|
|
};
|
|
}
|
|
struct Callbacks {
|
|
bool supported = true;
|
|
bool supportsFleetControl() const { return supported; }
|
|
};
|
|
class CommonCLI {
|
|
public:
|
|
Callbacks callbacks;
|
|
Callbacks* _callbacks = &callbacks;
|
|
void* _management_fs = nullptr;
|
|
RegionMap regions;
|
|
RegionMap* _region_map = ®ions;
|
|
mesh::FleetChannel* _fleet_channel = nullptr;
|
|
~CommonCLI() { delete _fleet_channel; }
|
|
bool handleFleetCommand(const char*, char*);
|
|
void loadExisting(FS* fs) { @BOOT_FLEET@ }
|
|
};
|
|
struct ClientInfo {
|
|
enum Role { Guest, Admin, RegionManager, FilterManager } role;
|
|
bool isAdmin() const { return role == Admin; }
|
|
bool isRegionMgr() const { return role == RegionManager; }
|
|
bool isFilterMgr() const { return role == FilterManager; }
|
|
};
|
|
@MANAGER_HELPERS@
|
|
@COMMON_FLEET@
|
|
static void dispatch(CommonCLI& cli, ClientInfo* sender, const char* command, char* reply) {
|
|
@MANAGER_GUARD@
|
|
if (!cli.handleFleetCommand(command, reply)) strcpy(reply, "unhandled");
|
|
}
|
|
int main() {
|
|
CommonCLI cli;
|
|
char reply[160] = {};
|
|
ClientInfo admin{ClientInfo::Admin};
|
|
for (const char* command : {"get fleet.channel", "get fleet.controller", "get fleet.stats",
|
|
"set fleet.channel off", "set fleet.controller 010203"}) {
|
|
for (auto role : {ClientInfo::Guest, ClientInfo::RegionManager, ClientInfo::FilterManager}) {
|
|
ClientInfo sender{role};
|
|
const unsigned before = configurations;
|
|
dispatch(cli, &sender, command, reply);
|
|
assert(!strcmp(reply, "Err - not permitted"));
|
|
assert(configurations == before);
|
|
assert(!isRegionMgrAllowed(command) && !isFilterMgrAllowed(command));
|
|
}
|
|
dispatch(cli, &admin, command, reply);
|
|
assert(!strcmp(reply, "OK"));
|
|
dispatch(cli, nullptr, command, reply);
|
|
assert(!strcmp(reply, "OK"));
|
|
}
|
|
assert(allocations == 1 && configurations == 10 && enrolled_regions == cli._region_map);
|
|
assert(isFilterMgrAllowed("set flood.filter.1 group-text 4"));
|
|
assert(isRegionMgrAllowed("set flood.channel.scope public #local"));
|
|
assert(!isFilterMgrAllowed("set radio2 off"));
|
|
assert(!isRegionMgrAllowed("get password"));
|
|
CommonCLI unsupported;
|
|
unsupported.callbacks.supported = false;
|
|
assert(unsupported.handleFleetCommand("set fleet.channel off", reply));
|
|
assert(strstr(reply, "infrastructure roles"));
|
|
assert(allocations == 1 && configurations == 10);
|
|
assert(!cli.handleFleetCommand("set fleeter.channel off", reply));
|
|
FS disk;
|
|
CommonCLI no_existing;
|
|
no_existing.loadExisting(&disk);
|
|
assert(allocations == 1 && no_existing._fleet_channel == nullptr);
|
|
disk.present = true;
|
|
CommonCLI existing;
|
|
existing.loadExisting(&disk);
|
|
assert(allocations == 2 && existing._fleet_channel && enrolled_regions == existing._region_map);
|
|
CommonCLI disabled;
|
|
disabled.callbacks.supported = false;
|
|
disabled.loadExisting(&disk);
|
|
assert(allocations == 2 && disabled._fleet_channel == nullptr);
|
|
puts("Fleet enrollment remains restricted to local/admin infrastructure dispatch");
|
|
}
|
|
'''
|
|
|
|
|
|
class FleetRoleWiringTests(unittest.TestCase):
|
|
def test_enrollment_common_cli_stays_behind_live_manager_permissions(self):
|
|
compiler = shutil.which("g++") or shutil.which("clang++")
|
|
self.assertIsNotNone(compiler)
|
|
repeater = (ROOT / "examples/simple_repeater/MyMesh.cpp").read_text()
|
|
helpers = "\n".join(body(repeater, signature) for signature in (
|
|
"static bool commandFamilyMatches(", "static bool commandListMatches(",
|
|
"static bool isCommonManagerReadOnlyAllowed(",
|
|
"static bool isRegionMgrAllowed(", "static bool isFilterMgrAllowed(",
|
|
))
|
|
handler = body(repeater, "void MyMesh::handleCommand(uint32_t sender_timestamp,")
|
|
guard_start = handler.rindex("if (sender && !sender->isAdmin())", 0,
|
|
handler.index("mesh::cli::handleACLGet("))
|
|
guard = body(handler[guard_start:], "if (sender && !sender->isAdmin())")
|
|
management = (ROOT / "src/helpers/CommonCLI_Management.cpp").read_text()
|
|
common = body(management, "bool CommonCLI::handleFleetCommand(")
|
|
boot = body(body(management, "void CommonCLI::beginManagement("),
|
|
"if (_callbacks->supportsFleetControl()")
|
|
helpers = f'#include "{(ROOT / "src/helpers/FloodCommandAliases.h").as_posix()}"\n' + helpers
|
|
harness = AUTHORIZATION.replace("@MANAGER_HELPERS@", helpers)
|
|
harness = harness.replace("@COMMON_FLEET@", common).replace("@MANAGER_GUARD@", guard)
|
|
harness = harness.replace("@BOOT_FLEET@", boot)
|
|
with tempfile.TemporaryDirectory(prefix="meshcore-fleet-authorization-") as directory:
|
|
work = Path(directory)
|
|
source = work / "fixture.cpp"
|
|
source.write_text(harness)
|
|
for extended_filters in (0, 1):
|
|
with self.subTest(extended_filters=extended_filters):
|
|
executable = work / str(extended_filters)
|
|
built = subprocess.run([
|
|
compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
|
"-DMESH_ENABLE_FLOOD_RULE_ENGINE=" + str(extended_filters),
|
|
str(source), "-o", str(executable),
|
|
], capture_output=True, text=True, timeout=60)
|
|
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
|
checked = subprocess.run([str(executable)], capture_output=True,
|
|
text=True, timeout=10)
|
|
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
|
|
|
|
def test_roles_with_feature_enabled_and_disabled(self):
|
|
compiler = shutil.which("g++") or shutil.which("clang++")
|
|
self.assertIsNotNone(compiler)
|
|
methods = []
|
|
capabilities = {}
|
|
for role, folder in (("Repeater", "simple_repeater"), ("Room", "simple_room_server")):
|
|
source = (ROOT / "examples" / folder / "MyMesh.cpp").read_text()
|
|
header = (ROOT / "examples" / folder / "MyMesh.h").read_text()
|
|
capabilities[role] = body(header, "bool supportsFleetControl() const override")
|
|
methods.append(body(source, "void MyMesh::onGroupPacketRecv(").replace("MyMesh::", role + "::", 1))
|
|
methods.append("#if MESH_ENABLE_FLEET_CONTROL\n"
|
|
+ body(source, "bool MyMesh::getFleetLocation(").replace("MyMesh::", role + "::", 1)
|
|
+ "\n#endif\n")
|
|
loop = body(source, "void MyMesh::loop()")
|
|
fleet = body(loop, "if (_cli.fleetChannel())")
|
|
methods.append("void " + role + "::serviceFleet() {\n#if MESH_ENABLE_FLEET_CONTROL\n"
|
|
+ fleet + "\n#endif\n}\n")
|
|
if role == "Room":
|
|
for signature in ("void MyMesh::onSendComplete(", "void MyMesh::onSendFail(",
|
|
"void MyMesh::onRadioProfileCopyQueued("):
|
|
methods.append("#if MESH_ENABLE_FLEET_CONTROL\n"
|
|
+ body(source, signature).replace("MyMesh::", role + "::", 1)
|
|
+ "\n#endif\n")
|
|
harness = HARNESS.replace("@METHODS@", "\n".join(methods))
|
|
scope = body((ROOT / "src/helpers/FleetChannel.h").read_text(),
|
|
"template <typename Regions, typename Region>")
|
|
transport_code = body((ROOT / "src/helpers/TransportKeyStore.cpp").read_text(),
|
|
"uint16_t TransportKey::calcTransportCode(")
|
|
# The host SHA-256 shim has a byte-pointer output API; on-device Crypto
|
|
# accepts void*. Preserve the production algorithm with that cast only.
|
|
transport_code = transport_code.replace("&code, 2", "(uint8_t*)&code, 2")
|
|
harness = harness.replace("@SCOPE_HELPERS@", transport_code + "\nnamespace mesh {\n" + scope + "\n}\n")
|
|
harness = harness.replace("@REPEATER_CAPABILITY@", capabilities["Repeater"])
|
|
harness = harness.replace("@ROOM_CAPABILITY@", capabilities["Room"])
|
|
with tempfile.TemporaryDirectory(prefix="meshcore-fleet-role-") as directory:
|
|
work = Path(directory)
|
|
fixture = work / "fixture.cpp"
|
|
fixture.write_text(harness)
|
|
for enabled in (0, 1):
|
|
with self.subTest(fleet_control=enabled):
|
|
executable = work / str(enabled)
|
|
built = subprocess.run([
|
|
compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
|
"-isystem", str(ROOT / "test/mocks"), "-I", str(ROOT / "src"),
|
|
"-DMESH_ENABLE_FLEET_CONTROL=" + str(enabled),
|
|
str(fixture), "-o", str(executable),
|
|
], capture_output=True, text=True, timeout=60)
|
|
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
|
checked = subprocess.run([str(executable)], capture_output=True,
|
|
text=True, timeout=10)
|
|
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|