186 lines
9.1 KiB
C++
186 lines
9.1 KiB
C++
// Only application boundaries are stubbed. The Companion declarations,
|
|
// callbacks, inherited Mesh direct hooks and Dispatcher are production code.
|
|
@RADIO@
|
|
#include <helpers/ota/OtaManager.h>
|
|
#include <Arduino.h>
|
|
#include <cstdio>
|
|
|
|
struct ChannelDetails { struct { uint8_t secret[32] = {}; } channel; };
|
|
namespace mesh { namespace mt {
|
|
constexpr unsigned DeliverySlots = 2;
|
|
struct PendingFrame {
|
|
uint32_t generation = 0, due = 0;
|
|
bool heard = false, cancelled = false, relayed = false, nodeinfo = false, sent = false;
|
|
int channel = 0;
|
|
unsigned retries = 0;
|
|
bool matchesRelayCopy(const uint8_t*, size_t) const { return false; }
|
|
bool matchesEcho(const uint8_t*, size_t) const { return false; }
|
|
void clear() { *this = {}; }
|
|
};
|
|
struct Channel { const char* name = "test"; uint8_t key[32] = {}; size_t key_length = 16; };
|
|
struct DecodedText { uint32_t from = 0, id = 0; uint8_t hops = 0; size_t text_len = 0; char text[32] = {}; };
|
|
inline bool decodeText(const uint8_t*, size_t, const char*, const uint8_t*, size_t,
|
|
uint32_t, DecodedText&) { return false; }
|
|
} }
|
|
static bool describe(const ChannelDetails&, mesh::mt::Channel&) { return false; }
|
|
static unsigned meshtextRetryLimit(const mesh::mt::PendingFrame&) { return 0; }
|
|
static uint32_t meshtextRetryDelay(const mesh::mt::PendingFrame&) { return 100; }
|
|
namespace mesh { namespace ota {
|
|
struct BoundaryManager {
|
|
unsigned calls = 0;
|
|
bool enabled = false;
|
|
uint8_t hops = 255;
|
|
OtaReplyRoute route;
|
|
uint32_t directTransportEpoch() const { return 42; }
|
|
void set_direct_transport_enabled(bool value) { enabled = value; }
|
|
void note_rx_path_hops(uint8_t value) { hops = value; }
|
|
bool on_message(const uint8_t*, uint16_t, OtaReplyRoute value) { ++calls; route = value; return true; }
|
|
uint8_t fetchState() const { return 0; }
|
|
};
|
|
struct BoundaryContext {
|
|
BoundaryManager manager;
|
|
void track_session(uint8_t, unsigned long) {}
|
|
} context;
|
|
inline BoundaryContext* ota_context_if_active() { return &context; }
|
|
} }
|
|
namespace mesh {
|
|
class Mesh : public Dispatcher {
|
|
public:
|
|
bool temp = false, primary = false;
|
|
mutable unsigned channel_queries = 0;
|
|
Mesh(Radio& r, MillisecondClock& c, PacketManager& m) : Dispatcher(r, c, m) {}
|
|
bool isAnyTempRadioActive() const { return temp; }
|
|
bool isTempRadioActive() const { return primary; }
|
|
bool getChannel(int, ChannelDetails&) const { ++channel_queries; return false; }
|
|
void syncOtaTiming(ota::BoundaryManager&) {}
|
|
struct RTC { uint32_t getCurrentTime() const { return 1000; } } rtc;
|
|
RTC* getRTCClock() { return &rtc; }
|
|
@DIRECT_DECLARATIONS@
|
|
};
|
|
@DIRECT_METHODS@
|
|
}
|
|
using BaseChatMesh = mesh::Mesh;
|
|
#define MAX_GROUP_CHANNELS 1
|
|
#define MAX_FRAME_SIZE 176
|
|
class MyMesh : public BaseChatMesh {
|
|
public:
|
|
bool _radio_available = true;
|
|
uint32_t _scheduled_reboot_at = 0;
|
|
bool _mt_active = false, _ethernet_active = false;
|
|
unsigned _mt_received = 0, _mt_sent = 0, _mt_failed = 0, _mt_heard = 0, _mt_retried = 0;
|
|
unsigned _ethernet_received = 0, _ethernet_sent = 0, _ethernet_failed = 0;
|
|
uint32_t _mt_last_tx_completed_at = 0, _mt_node = 1, _mt_nodeinfo_generation = 0;
|
|
int _mt_tx_slot = -1;
|
|
bool _mt_nodeinfo_pending[1] = {}, _mt_nodeinfo_announced[1] = {};
|
|
mesh::mt::PendingFrame _mt_delivery[mesh::mt::DeliverySlots];
|
|
struct Seen {
|
|
bool seen(uint32_t, uint32_t) const { return false; }
|
|
void remember(uint32_t, uint32_t) {}
|
|
} _mt_seen;
|
|
struct Kiss {
|
|
unsigned calls = 0;
|
|
bool queueReceive(const uint8_t* bytes, size_t length) { assert(bytes && length); ++calls; return true; }
|
|
} _ethernet_kiss;
|
|
bool meshtextDeliveryCurrent(const mesh::mt::PendingFrame&) { return false; }
|
|
bool getChannel(int index, ChannelDetails& channel) { return BaseChatMesh::getChannel(index, channel); }
|
|
template <typename... Args> bool queueChannelText(Args&&...) { return false; }
|
|
void maybeRebroadcastMeshtext(const ChannelDetails&, const uint8_t*, size_t) {}
|
|
MyMesh(TestRadio& r, Clock& c, StaticPoolPacketManager& p) : BaseChatMesh(r, c, p) {}
|
|
unsigned mesh_rx = 0, raw_logs = 0;
|
|
DispatcherAction onRecvPacket(Packet*) override { ++mesh_rx; return ACTION_RELEASE; }
|
|
void logRxRaw(float, float, const uint8_t*, int) override { ++raw_logs; }
|
|
@COMPANION_DECLARATIONS@
|
|
#if MESH_ENABLE_ETHERNET_TUNNEL
|
|
bool onEthernetFrame(const uint8_t*, size_t, uint8_t, uint32_t);
|
|
#endif
|
|
#if MESH_ENABLE_FOREIGN_RADIO
|
|
using BaseChatMesh::queueForeignRadioFrame;
|
|
using BaseChatMesh::discardQueuedForeignRadioFrame;
|
|
bool foreignAllowed() const { return allowForeignRadioTransmit(); }
|
|
bool foreignReceive(const uint8_t* bytes, size_t n, uint8_t profile, uint32_t generation) {
|
|
// Explicit lookup also makes link failures identify the missing hook,
|
|
// instead of only the vtable. Dispatcher RX/TX below still dispatch virtually.
|
|
return MyMesh::onForeignRadioFrame(bytes, n, profile, generation, -4, -100);
|
|
}
|
|
void foreignComplete(bool okay) { MyMesh::onForeignRadioSendComplete(okay); }
|
|
#endif
|
|
#if MESH_ENABLE_OTA_KISS_DIRECT
|
|
using BaseChatMesh::queueDirectOtaRadioFrame;
|
|
#endif
|
|
};
|
|
@CALLBACK_METHODS@
|
|
|
|
struct Fixture {
|
|
Clock clock;
|
|
TestRadio radio;
|
|
StaticPoolPacketManager pool{8};
|
|
MyMesh node{radio, clock, pool};
|
|
Fixture() { node.begin(); }
|
|
void tick(unsigned delta = 1) { clock.now += delta; node.loop(); }
|
|
};
|
|
static void transmit(Fixture& f) {
|
|
f.tick(); assert(f.radio.sending); f.radio.complete = true; f.tick(10);
|
|
assert(!f.node.hasOutbound());
|
|
}
|
|
int main() {
|
|
using Protocol = mesh::RadioProfileProtocol;
|
|
static_assert(bool(MESH_ENABLE_FOREIGN_RADIO) == bool(
|
|
MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL || MESH_ENABLE_OTA_KISS_DIRECT));
|
|
const uint8_t bytes[] = {0x11, 0, 0x23};
|
|
Fixture f;
|
|
#if MESH_ENABLE_FOREIGN_RADIO
|
|
assert(!f.node.foreignAllowed() && "Companion must retain inactive-app admission rejection");
|
|
#if !MESH_ENABLE_MESHTEXT_CHAT && !MESH_ENABLE_ETHERNET_TUNNEL
|
|
assert(!f.node.foreignReceive(bytes, sizeof(bytes), 1, f.radio.p.generation[1]));
|
|
f.node.foreignComplete(true); f.node.foreignComplete(false);
|
|
assert(!f.node._mt_sent && !f.node._mt_failed && !f.node._ethernet_sent && !f.node._ethernet_failed);
|
|
#endif
|
|
#endif
|
|
#if MESH_ENABLE_OTA_KISS_DIRECT
|
|
// The actual Mesh inherited hooks and Dispatcher direct owner bypass
|
|
// inactive foreign apps, without falling through to foreign callbacks.
|
|
f.node.temp = true; f.radio.p.secondary_temporary = true;
|
|
f.radio.p.secondary.params.protocol = Protocol::MeshCore;
|
|
const auto gen = f.radio.p.generation[1];
|
|
const uint8_t message[] = {mesh::ota::OTA_REQ, 0xc0, 0xdb};
|
|
assert(f.node.queueDirectOtaRadioFrame(message, sizeof(message), 1, gen));
|
|
transmit(f);
|
|
assert(f.node.getNumSentDirect() == 1 && !f.node._mt_sent && !f.node._ethernet_sent);
|
|
uint8_t raw[16]; const auto n = mesh::ota::ota_direct_wire_encode(raw, sizeof(raw), message, sizeof(message));
|
|
f.radio.active = 1; f.radio.rx.assign(raw, raw + n); f.radio.incoming = true; f.tick();
|
|
assert(mesh::ota::context.manager.calls == 1 && mesh::ota::context.manager.enabled);
|
|
assert(mesh::ota::context.manager.hops == 0 && mesh::ota::context.manager.route.direct);
|
|
assert(mesh::ota::context.manager.route.profile == 1 && mesh::ota::context.manager.route.generation == gen);
|
|
assert(!f.node.mesh_rx && !f.node.raw_logs && !f.node._mt_received && !f.node._ethernet_received);
|
|
#endif
|
|
#if MESH_ENABLE_MESHTEXT_CHAT
|
|
f.node._mt_active = true;
|
|
f.radio.p.secondary.params.protocol = Protocol::Meshtext;
|
|
assert(f.node.foreignAllowed());
|
|
assert(!f.node.foreignReceive(bytes, sizeof(bytes), 1, f.radio.p.generation[1]));
|
|
assert(f.node.channel_queries == 1); // Real Meshtext callback reached its channel boundary.
|
|
assert(f.node.queueForeignRadioFrame(bytes, sizeof(bytes), 1, f.radio.p.generation[1], Protocol::Meshtext));
|
|
transmit(f); assert(f.node._mt_sent == 1 && !f.node._ethernet_sent);
|
|
assert(f.node.queueForeignRadioFrame(bytes, sizeof(bytes), 1, f.radio.p.generation[1], Protocol::Meshtext));
|
|
assert(f.node.discardQueuedForeignRadioFrame(Protocol::Meshtext)); assert(f.node._mt_failed == 1);
|
|
#endif
|
|
#if MESH_ENABLE_ETHERNET_TUNNEL
|
|
f.node._ethernet_active = true; f.node._mt_active = false;
|
|
f.radio.p.secondary.params.protocol = Protocol::Ethernet;
|
|
assert(f.node.foreignAllowed());
|
|
assert(f.node.foreignReceive(bytes, sizeof(bytes), 1, f.radio.p.generation[1]));
|
|
assert(f.node._ethernet_received == 1 && f.node._ethernet_kiss.calls == 1 && !f.node._mt_received);
|
|
assert(!f.node.foreignReceive(bytes, sizeof(bytes), 1, f.radio.p.generation[1] + 1));
|
|
const auto mt_sent = f.node._mt_sent;
|
|
assert(f.node.queueForeignRadioFrame(bytes, sizeof(bytes), 1, f.radio.p.generation[1], Protocol::Ethernet));
|
|
transmit(f); assert(f.node._ethernet_sent == 1 && f.node._mt_sent == mt_sent);
|
|
assert(f.node.queueForeignRadioFrame(bytes, sizeof(bytes), 1, f.radio.p.generation[1], Protocol::Ethernet));
|
|
assert(f.node.discardQueuedForeignRadioFrame(Protocol::Ethernet)); assert(f.node._ethernet_failed == 1);
|
|
#endif
|
|
#if MESH_ENABLE_FOREIGN_RADIO
|
|
f.node._scheduled_reboot_at = 1; assert(!f.node.foreignAllowed());
|
|
f.node._scheduled_reboot_at = 0; f.node._radio_available = false; assert(!f.node.foreignAllowed());
|
|
#endif
|
|
std::puts("PASS: Companion foreign-hook feature matrix");
|
|
}
|