Files
MeshCore/test/fixtures/ota_folder_ready.cpp
T

234 lines
9.1 KiB
C++

#include <helpers/ota/OtaManager.h>
#include <helpers/ota/MotaSourceSerial.h>
#include <helpers/ota/MotaSeederProto.h>
#include <helpers/ota/OtaByteIO.h>
#include <helpers/ota/OtaTiming.h>
#include <algorithm>
#include <cassert>
#include <cstdio>
#include <vector>
using namespace mesh::ota;
namespace ota = mesh::ota;
// One ordinary data stream answers the real device COUNT/DESCRIBE protocol.
// No text CLI, second socket or explicit announcement command exists here.
class FolderHost : public Stream {
std::vector<uint8_t> response;
size_t position = 0;
public:
using Stream::write;
std::vector<uint32_t> mids = {0x12345678};
std::vector<uint8_t> operations;
int failed_descriptor = -1;
bool fail_count = false;
bool invalid_geometry = false;
unsigned flushes = 0;
size_t write(const uint8_t* request, size_t length) override {
assert(length >= 4 && request[0] == 'M' && request[1] == 'S');
uint8_t checksum = 0;
for (size_t i = 2; i < length; ++i) checksum ^= request[i];
assert(checksum == 0);
const uint8_t op = request[2];
operations.push_back(op);
std::vector<uint8_t> payload;
bool ok = true;
if (op == MS_OP_COUNT) {
assert(length == 4);
ok = !fail_count;
payload.push_back(static_cast<uint8_t>(mids.size()));
} else {
assert(op == MS_OP_DESCRIBE && length == 5);
const uint8_t index = request[3];
ok = index < mids.size() && index != failed_descriptor;
if (ok) {
payload.assign(MOTA_DESC_WIRE, 0);
wr_u32le(payload.data(), mids[index]);
wr_u32le(payload.data() + 4, 1234);
wr_u32le(payload.data() + 8, 0x01120001);
payload[12] = CODEC_FULL; payload[13] = MFLAG_FULL;
wr_u32le(payload.data() + 14, 8 + MOTA_MFL + 4 + 128 + 5);
wr_u32le(payload.data() + 18, 8 + MOTA_MFL);
wr_u32le(payload.data() + 22, invalid_geometry ? 0 : 1);
wr_u32le(payload.data() + 26, 8 + MOTA_MFL + 4);
wr_u32le(payload.data() + 30, 128);
payload[34] = 7;
}
}
response = {'m', 's', op, uint8_t(ok ? MS_STATUS_OK : MS_STATUS_ERR)};
if (ok) response.insert(response.end(), payload.begin(), payload.end());
checksum = 0;
for (uint8_t value : response) checksum ^= value;
response.push_back(checksum);
position = 0;
return length;
}
int available() override { return static_cast<int>(response.size() - position); }
int read() override {
if (position < response.size()) return response[position++];
++g_mock_millis;
return -1;
}
void flush() override { ++flushes; }
};
struct Sent {
std::vector<std::vector<uint8_t>> frames;
std::vector<uint32_t> times;
uint32_t now = 0;
};
static bool capture(void* pointer, const uint8_t* data, uint16_t length, bool) {
auto& sent = *static_cast<Sent*>(pointer);
sent.frames.emplace_back(data, data + length);
sent.times.push_back(sent.now);
return true;
}
namespace mesh { namespace ota {
struct OtaContext {
OtaManager manager;
bool self_serve_supported = false, serving = false;
};
static OtaContext* active_context = nullptr;
OtaContext& ota_ctx() { assert(active_context); return *active_context; }
bool ota_serve_self(OtaContext&, unsigned) { return false; }
}}
// @PRODUCTION_DEFINES@
struct Clock { uint32_t now; uint32_t getMillis() const { return now; } };
struct Fixture {
ota::OtaContext context;
Sent sent;
Clock clock;
Clock* _ms = &clock;
uint8_t _ota_announce_count = OTA_ANNOUNCE_BURST;
ota::LongTimer _ota_announce_timer;
float speed;
bool active = true;
Fixture(uint32_t start, float rate) : clock{start}, speed(rate) {
ota::active_context = &context;
context.manager.begin(1234, capture, &sent);
const uint8_t sid[4] = {1, 2, 3, 4};
context.manager.set_seeder_id(sid);
context.manager.set_advert_mins(0);
_ota_announce_timer.arm(start, OTA_ANNOUNCE_DISABLED_POLL_MS, speed);
}
void step() {
if (!active) return; // Same admission boundary as the production Mesh loop.
sent.now = clock.now;
context.manager.set_clock(clock.now);
const float ota_speed = speed;
// @PRODUCTION_READY@
// @PRODUCTION_ANNOUNCE@
}
void advance(uint32_t milliseconds) { clock.now += milliseconds; step(); }
};
static void four_ready_beacons(MotaStreamWritePolicy policy, uint32_t start, float speed) {
Fixture f(start, speed);
FolderHost host;
SerialMotaSource source(host, policy, 5);
// Host preparation can outlast the entire existing boot burst.
f.advance(120000);
assert(f.sent.frames.empty());
assert(f.context.manager.add_source(&source));
assert((host.operations == std::vector<uint8_t>{MS_OP_COUNT, MS_OP_DESCRIBE}));
assert(f.sent.frames.empty()); // Nothing is emitted from the data callback.
f.step();
assert(f.sent.frames.size() == 1 && "ready catalog must discover without a control channel");
AdvMsg advert{};
assert(decode_adv(f.sent.frames[0].data(), f.sent.frames[0].size(), advert));
assert(advert.n_motas == 1);
const uint32_t gap = scaleDelay(OTA_ANNOUNCE_BURST_MS, speed);
for (unsigned count = 1; count < OTA_ANNOUNCE_BURST; ++count) {
f.advance(gap - 1);
assert(f.sent.frames.size() == count);
f.context.manager.refresh_sources(); // Unchanged polls must not restart it.
f.advance(1);
assert(f.sent.frames.size() == count + 1);
assert(f.sent.times.back() - f.sent.times[f.sent.times.size() - 2] == gap);
}
f.context.manager.refresh_sources();
f.advance(gap);
f.advance(OTA_ANNOUNCE_DISABLED_POLL_MS);
assert(f.sent.frames.size() == OTA_ANNOUNCE_BURST);
assert(host.flushes == (policy == MotaStreamWritePolicy::FlushTransmit ? host.operations.size() : 0));
// A fresh receiver with no known source learns and retrieves the exact MID.
OtaManager receiver;
Sent requests;
receiver.begin(1234, capture, &requests);
receiver.queryAll();
assert(requests.frames.empty());
receiver.on_message(f.sent.frames[0].data(), f.sent.frames[0].size());
receiver.queryAll();
assert(requests.frames.size() == 1);
const size_t before = f.sent.frames.size();
f.context.manager.on_message(requests.frames[0].data(), requests.frames[0].size());
assert(f.sent.frames.size() == before + 1);
receiver.on_message(f.sent.frames.back().data(), f.sent.frames.back().size());
assert(receiver.catalogCount() == 1 && rd_u32le(receiver.catalogRow(0)->mid) == host.mids[0]);
}
static void readiness_and_coalescing() {
Fixture f(1000, 1);
FolderHost host;
host.mids.clear();
SerialMotaSource source(host, MotaStreamWritePolicy::NoFlush, 5);
assert(f.context.manager.add_source(&source));
f.step();
assert(f.sent.frames.empty());
host.mids = {17, 18}; host.failed_descriptor = 1;
f.context.manager.refresh_sources();
assert(f.context.manager.servedCount() == 1 && !f.context.manager.sourceCatalogComplete());
f.step();
assert(f.sent.frames.empty());
// A complete rescan may have exactly the same deduplicated MID set.
host.mids = {17, 17}; host.failed_descriptor = -1;
f.context.manager.refresh_sources();
assert(f.context.manager.sourceCatalogComplete());
f.step();
assert(f.sent.frames.size() == 1);
// Multiple changes before the next mesh tick collapse onto the newest set.
host.mids = {19}; f.context.manager.refresh_sources();
host.mids = {20, 21}; f.context.manager.refresh_sources();
f.step();
assert(f.sent.frames.size() == 2);
std::reverse(host.mids.begin(), host.mids.end());
f.context.manager.refresh_sources(); f.advance(1);
assert(f.sent.frames.size() == 2); // Canonical digest ignores enumeration order.
// Metadata failure cancels an unsent ready signal and automatic burst sends.
host.mids = {22, 23}; f.context.manager.refresh_sources();
host.failed_descriptor = 1; f.context.manager.refresh_sources();
f.advance(OTA_ANNOUNCE_BURST_MS);
assert(f.sent.frames.size() == 2);
host.failed_descriptor = -1; f.context.manager.refresh_sources();
f.step();
assert(f.sent.frames.size() == 3);
// Empty or invalid offerings never announce an empty ready catalog.
host.mids.clear(); f.context.manager.refresh_sources();
f.advance(OTA_ANNOUNCE_BURST_MS);
assert(f.sent.frames.size() == 3);
host.mids = {24}; host.invalid_geometry = true; f.context.manager.refresh_sources();
f.advance(OTA_ANNOUNCE_BURST_MS);
assert(f.sent.frames.size() == 3);
host.invalid_geometry = false; f.context.manager.refresh_sources();
f.active = false; f.advance(120000);
assert(f.sent.frames.size() == 3); // Normal-radio admission retains readiness.
f.active = true; f.step();
assert(f.sent.frames.size() == 4);
// Removing the source before admission must retire its pending signal.
host.mids = {25}; f.context.manager.refresh_sources();
f.context.manager.clear_sources(); f.step();
assert(f.sent.frames.size() == 4 && !f.context.manager.takeSourceReadyAnnouncement());
}
int main() {
for (auto policy : {MotaStreamWritePolicy::FlushTransmit, MotaStreamWritePolicy::NoFlush})
for (uint32_t start : {0U, 0xffff0000U})
for (float speed : {0.05f, 1.0f, 3.0f}) four_ready_beacons(policy, start, speed);
readiness_and_coalescing();
puts("PASS: single-data-session folder readiness, exact four-beacon bursts, cold peer discovery and coalescing");
}