Files
MeshCore/test/fixtures/sensor_tracker_client.h
T
mikecarper b0c49a4cc3 Fit STM32 release images and repair stale CI fixtures
Fix the STM32 flash overflows and fleet fixture compile failure from
https://github.com/mikecarper/MeshCore/actions/runs/40241742182.

Fold already-unsupported RX control paths only for the integrated STM32WL
wrapper. Preserve RXPS-off validation, persistence, pending application and
rollback. External radios retain their supported controls.

Use measured GCC 14 code sharing in the four small STM32 USB Companion
recipes. Keep firmware features, contact/channel capacities, filesystem
boundaries and flash/RAM admission checks unchanged.

Refresh downstream host fixtures for saved radio intent, deferred service
restarts, WebConfig ownership, RX recovery and color tint state. Retain or
strengthen behavior checks rather than skipping failing coverage.

Validation: canonical RAK3x72, Wio Mini, Wio Dev and Tiny Relay USB Companion
packages and Mini Repeater build passed at release-version length. All
post-failure host checks, compact arithmetic/float checks, build-profile and
real PlatformIO recipe/path/RAM-policy checks passed locally.
2026-10-25 22:00:24 -07:00

304 lines
13 KiB
C++

#pragma once
// Hardware adapter for the complete production Tracker.cpp. The wire codec,
// route policy, durable store, Packet layout and packet SHA256 stay production.
#include "tracker_storage_fs.h"
#include <Packet.h>
#include <helpers/SensorTrackerStore.h>
#include <helpers/TrackerProtocol.h>
#include <array>
#include <cmath>
#include <limits>
#define MESH_ENABLE_SENSOR_TRACKER 1
#define MAX_SEARCH_RESULTS 24
namespace mesh {
static bool test_logging_watchdog = false;
bool isUsbLoggingWatchdogArmed() { return test_logging_watchdog; }
struct Identity {
uint8_t pub_key[32] = {};
Identity() = default;
explicit Identity(const uint8_t* key) { memcpy(pub_key, key, sizeof(pub_key)); }
bool matches(const uint8_t* key) const { return memcmp(pub_key, key, sizeof(pub_key)) == 0; }
bool isHashMatch(const uint8_t* hash) const { return memcmp(pub_key, hash, PATH_HASH_SIZE) == 0; }
};
struct LocalIdentity : Identity {
// Deterministic transport adapter preserves all 32 key bytes. Production
// encrypted transport authenticates one of the selected full-key secrets.
void calcSharedSecret(uint8_t* out, const uint8_t* owner) const {
for (size_t i = 0; i < 32; ++i) out[i] = owner[i] ^ pub_key[i] ^ 0xa7;
}
};
class Mesh {
public:
unsigned base_send_complete = 0, base_send_fail = 0;
unsigned base_loops = 0;
void loop() { ++base_loops; }
void onSendComplete(Packet*) { ++base_send_complete; }
void onSendFail(Packet*) { ++base_send_fail; }
};
}
struct FakeRTC : mesh::RTCClock {
uint32_t now = 1000000;
uint32_t getCurrentTime() override { return now; }
void setCurrentTime(uint32_t value) override { now = value; }
};
struct FakeBoard {
bool usb = false, ota = false, test = false;
bool isUsbDataConnected() const { return usb; }
bool isOTAUpdateRunning() const { return ota; }
bool isRadioTestActive() const { return test; }
uint16_t getBattMilliVolts() const { return 3765; }
};
struct FakeRadio {
bool permit_sleep = true, permit_wake = true, permit_recovery = true, sleeping = false;
bool morse_tx = false, morse_rx = false;
bool isMorseTxActive() const { return morse_tx; }
bool isMorseRxActive() const { return morse_rx; }
unsigned sleeps = 0, wakes = 0, wake_attempts = 0, hard_recoveries = 0;
bool setTrackerSleep(bool value) {
if (!value) ++wake_attempts;
if ((value && !permit_sleep) || (!value && !permit_wake)) return false;
sleeping = value;
if (value) ++sleeps; else ++wakes;
return true;
}
bool recoverRadio(bool hard) {
assert(hard); ++hard_recoveries;
if (!permit_recovery) return false;
sleeping = false; return true;
}
};
struct FakeDriver {
bool watchdog = false, calibrating = false;
bool isWatchdogObserving() const { return watchdog; }
bool isCalibratingNoiseFloor() const { return calibrating; }
};
struct FakeSensors {
bool detected = true, mode = false, power_save = false;
bool pending = false, fresh = false;
double latitude = 0, longitude = 0;
uint32_t* clock = nullptr;
uint32_t acquisition_until = 0;
bool hold_window = false;
unsigned starts = 0;
void setTrackerGpsModeEnabled(bool value) { mode = value; if (!value) pending = fresh = false; }
void setPowerSavingEnabled(bool value) { power_save = value; }
bool isGPSDetected() const { return detected; }
bool beginTrackerGpsAcquisition() {
assert(clock); ++starts; pending = true; fresh = false;
acquisition_until = *clock + 120000; return detected;
}
bool isTrackerGpsAcquisitionPending() {
// SensorManager owns finalizing its acquisition, including receiver stop.
if (pending && !hold_window && int32_t(*clock - acquisition_until) >= 0) pending = fresh = false;
return pending;
}
bool takeTrackerGpsPosition(double& lat, double& lon) {
if (!fresh) return false;
lat = latitude; lon = longitude; fresh = false; return true;
}
void fix(double lat = 47.1234567, double lon = -122.2345678) {
latitude = lat; longitude = lon; pending = false; fresh = true;
}
};
static FakeSensors sensors;
struct FakePool {
std::set<mesh::Packet*> allocated;
std::vector<mesh::Packet*> queued;
mesh::Packet* in_flight = nullptr;
unsigned frees = 0;
int free_limit = 32;
~FakePool() { for (auto* packet : allocated) delete packet; }
int getOutboundTotal() const { return static_cast<int>(queued.size()); }
const mesh::Packet* getOutboundByIdx(int i) const { return queued.at(i); }
mesh::Packet* removeOutboundByIdx(int i) {
auto* packet = queued.at(i); queued.erase(queued.begin() + i); return packet;
}
int getFreeCount() const { return free_limit - static_cast<int>(allocated.size()); }
mesh::Packet* alloc() { auto* p = new mesh::Packet; assert(allocated.insert(p).second); return p; }
void free(mesh::Packet* p) {
assert(p && p != in_flight);
assert(std::find(queued.begin(), queued.end(), p) == queued.end());
assert(allocated.erase(p) == 1); ++frees; delete p;
}
};
struct ClientInfo {
mesh::Identity id;
uint8_t shared_secret[32] = {};
};
struct FakeACL {
std::vector<ClientInfo> clients;
int getNumClients() const { return static_cast<int>(clients.size()); }
ClientInfo* getClientByIdx(int index) { return &clients.at(index); }
};
struct FakeTrigger { char text[8] = "alert"; };
class SensorMesh : public mesh::Mesh {
public:
MemoryFS fs;
MemoryFS* _fs = &fs;
FakeRTC rtc;
uint32_t now_ms = 100;
struct Milliseconds { uint32_t* value; uint32_t getMillis() const { return *value; } } clock_ms{&now_ms};
Milliseconds* _ms = &clock_ms;
FakeBoard board;
FakeRadio radio;
FakeRadio* _radio = &radio;
FakeDriver radio_driver;
FakePool pool;
FakePool* _mgr = &pool;
mesh::LocalIdentity self_id;
mesh::tracker::TrackerRecord tracker;
uint8_t tracker_secret[32] = {};
bool tracker_storage_available = true, tracker_storage_fault = false;
bool tracker_radio_asleep = false, tracker_acquiring = false;
bool tracker_pending = false, tracker_sent = false;
uint8_t tracker_packet_hash[MAX_HASH_SIZE] = {};
uint32_t tracker_next_check = 0, tracker_pending_tag = 0;
unsigned long tracker_reply_until = 0, tracker_awake_until = 0;
uint32_t tracker_boot_anchor = 1000000;
uint32_t tracker_clock_rtc = 0;
unsigned long tracker_clock_millis = 0;
unsigned long next_local_advert = 123, next_flood_advert = 456;
struct Prefs { uint8_t powersaving_enabled = 0, path_hash_mode = 0;
bool disable_fwd = false; unsigned flood_max = 64; } _prefs;
FakeTrigger alert1, alert2;
FakeTrigger* alert_tasks[4] = {&alert1, &alert2};
int num_alert_tasks = 2;
struct CLI {
unsigned tx_services = 0, rx_services = 0;
bool pending_radio_settings = false;
void servicePendingTxPower() { ++tx_services; }
void servicePendingRxSettings() { ++rx_services; }
struct Profiles {
unsigned tuning_services = 0;
void servicePendingTuning() { ++tuning_services; }
} profiles;
Profiles& radioProfiles() { return profiles; }
bool batteryAllowsForward(bool) const { return true; }
bool batteryAllowsTransmit(const mesh::Packet*) const { return true; }
struct NoMorseReply { void complete(const void*) {} void fail(const void*) {} } morse;
NoMorseReply& morseReply() { return morse; }
bool active = false;
bool local_command = false;
unsigned loops = 0;
SensorMesh* owner = nullptr;
void loop();
bool hasActiveUserGpioTimer() const { return active; }
bool hasPendingRadioSettings() const { return pending_radio_settings; }
} _cli;
struct ClockSync { unsigned observed = 0, loops = 0;
void loop() { ++loops; }
void observeAcceptedFlood(const mesh::Packet*) { ++observed; } } _clock_sync;
uint32_t set_radio_at = 0, revert_radio_at = 0;
bool saved_radio_apply_pending = false, temporary = false, pending_ota = false, dual = false;
unsigned primary_radio_services = 0;
void servicePendingPrimaryRadioSettings(bool tx_gap) { assert(tx_gap); ++primary_radio_services; }
bool queue_wake = false, retry_wake = false, available = true;
bool allocation_failure = false, admission_failure = false;
unsigned created = 0, direct_sends = 0, flood_sends = 0, adverts = 0, zero_hops = 0;
unsigned cancel_active = 0, cancel_queued = 0, cancel_inflight = 0;
bool cancel_delivered = false;
unsigned advert_timer_updates = 0, flood_timer_updates = 0;
std::vector<uint8_t> last_payload;
uint8_t last_dest[32] = {}, last_secret[32] = {};
int default_scope = 0;
FakeACL acl;
int matching_peer_indexes[MAX_SEARCH_RESULTS] = {};
unsigned ordinary_data = 0, ordinary_paths = 0;
SensorMesh() {
sensors = FakeSensors(); mesh::test_logging_watchdog = false;
sensors.clock = &now_ms;
metadata_filesystem = &fs;
self_id.pub_key[0] = 0xf1;
_cli.owner = this;
}
FakeRTC* getRTCClock() const { return const_cast<FakeRTC*>(&rtc); }
unsigned long futureMillis(int delta) const { return uint32_t(now_ms + uint32_t(delta)); }
bool millisHasNowPassed(unsigned long value) const { return int32_t(now_ms - uint32_t(value)) > 0; }
bool hasOutbound() const { return pool.in_flight != nullptr; }
bool isAnyTempRadioActive() const { return temporary; }
bool hasPendingOtaApply() const { return pending_ota; }
bool isDualRadioActive() const { return dual; }
bool getNextQueueWakeDelay(uint32_t& value) const { value = 1; return queue_wake; }
bool hasPendingRadioWork() const { return queue_wake || radio.morse_tx || radio.morse_rx; }
bool getNextBeaconWakeDelay(uint32_t&) const { return false; }
bool getNextRetryWakeDelay(uint32_t& value) const { value = 1; return retry_wake; }
void setRadioAvailable(bool value) { available = value; }
void updateAdvertTimer() { ++advert_timer_updates; }
void updateFloodAdvertTimer() { ++flood_timer_updates; }
void releasePacket(mesh::Packet* p) { pool.free(p); }
const mesh::Packet* getOutboundInFlight() const { return pool.in_flight; }
void cancelActiveRetries(const uint8_t* hash) {
assert(memcmp(hash, tracker_packet_hash, MAX_HASH_SIZE) == 0); ++cancel_active;
}
void cancelOutboundRadioRetry(const mesh::Packet* p, bool delivered) {
assert(pool.allocated.count(const_cast<mesh::Packet*>(p)));
if (p == pool.in_flight) ++cancel_inflight; else ++cancel_queued;
cancel_delivered = delivered;
}
mesh::Packet* createDatagram(uint8_t type, const mesh::Identity& dest,
const uint8_t* secret, const uint8_t* data, size_t length) {
++created;
assert(length <= MAX_PACKET_PAYLOAD);
memcpy(last_dest, dest.pub_key, 32); memcpy(last_secret, secret, 32);
last_payload.assign(data, data + length);
if (allocation_failure) return nullptr;
auto* packet = pool.alloc(); packet->header = type << PH_TYPE_SHIFT;
packet->payload_len = length; memcpy(packet->payload, data, length); return packet;
}
bool admit(mesh::Packet* packet) {
assert(packet->tx_radio == mesh::RADIO_TX_PRIMARY);
assert(packet->flood_retry_policy == mesh::FLOOD_RETRY_POLICY_DENY);
if (admission_failure) { onSendFail(packet); releasePacket(packet); return false; }
pool.queued.push_back(packet); return true;
}
bool sendDirect(mesh::Packet* packet, const uint8_t* path, uint8_t length) {
++direct_sends; assert(mesh::Packet::isValidPathLen(length));
packet->header = (packet->header & ~PH_ROUTE_MASK) | ROUTE_TYPE_DIRECT;
packet->path_len = length;
if (length) memcpy(packet->path, path, mesh::tracker::trackerPathByteLength(length));
return admit(packet);
}
bool sendFloodScoped(int, mesh::Packet* packet, unsigned = 0, uint8_t width = 1) {
if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT) { ++adverts; releasePacket(packet); return true; }
++flood_sends; assert(width >= 1 && width <= 3);
packet->header = (packet->header & ~PH_ROUTE_MASK) | ROUTE_TYPE_FLOOD;
packet->setPathHashSizeAndCount(width, 0); return admit(packet);
}
mesh::Packet* createSelfAdvert() { auto* p = pool.alloc(); p->header = PAYLOAD_TYPE_ADVERT << PH_TYPE_SHIFT; return p; }
void sendZeroHop(mesh::Packet* p, int) { ++adverts; ++zero_hops; releasePacket(p); }
bool isTrackerModeEnabled() const { return tracker.enabled; }
bool handleTrackerCommand(uint32_t, const char*, char*);
void localConsoleLease(uint32_t);
void loop();
void configureTrackerRuntime();
bool saveTrackerRecord(mesh::tracker::TrackerRecord&);
bool trackerNeedsRadio() const;
void wakeTrackerRadio();
void cancelTrackerPending(bool delivered = false);
void serviceTracker();
bool handleTrackerResponse(mesh::Packet*, uint8_t, const uint8_t*, size_t,
const uint8_t* path = nullptr, uint8_t path_len = 0xff);
bool handleTrackerPath(mesh::Packet*, uint8_t*, uint8_t, uint8_t, uint8_t*, size_t);
void onSendComplete(mesh::Packet*);
void onSendFail(mesh::Packet*);
int searchPeersByHash(const uint8_t*);
void getPeerSharedSecret(uint8_t*, int);
void onPeerDataRecv(mesh::Packet*, uint8_t, int, const uint8_t*, uint8_t*, size_t);
bool onPeerPathRecv(mesh::Packet*, int, const uint8_t*, uint8_t*, uint8_t, uint8_t, uint8_t*, uint8_t);
bool allowPacketForward(const mesh::Packet*);
void sendSelfAdvertisement(int, bool);
};
inline void SensorMesh::CLI::loop() {
++loops;
if (local_command) {
owner->localConsoleLease(0);
local_command = false;
}
}