mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-22 14:11:18 +00:00
Acquire fresh GPS and validated UTC, check an authenticated owner Companion, and apply saved lost-status cadence with bounded awake windows. Persist request tags, routes, and six-hour flood reservations; preserve the four-day cutoff across sleep and restart. Keep Dispatcher recovery active after a failed radio wake, honor denied flood retries after lost TxDone, and qualify GPS ownership and clock corrections. Add executable regression tests and nRF52/ESP32 Sensor build jobs. Validation: six firmware builds, 1829 native test cases, sanitized tracker/storage/GPS/sleep/recovery tests, and strict documentation build. Bluetooth Find My advertising and automatic Room mailbox polling remain separate work.
343 lines
15 KiB
Python
343 lines
15 KiB
Python
#!/usr/bin/env python3
|
|
"""Exercise actual Companion tracker callbacks and the shared wire codec."""
|
|
|
|
from pathlib import Path
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
from test_replay_reset_integration import extract_braced
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SANITIZERS = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
|
|
"-fno-pie", "-no-pie"] if sys.platform.startswith("linux") else [])
|
|
|
|
HARNESS = r'''
|
|
#include <cassert>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <climits>
|
|
#include <vector>
|
|
#include "examples/companion_radio/CompanionFeatures.h"
|
|
#include <helpers/TrackerProtocol.h>
|
|
#if MESH_ENABLE_LOST_REPLY
|
|
#include <helpers/CompanionLostReply.h>
|
|
#endif
|
|
#define PUB_KEY_SIZE 32
|
|
#define ADV_TYPE_CHAT 1
|
|
#define ADV_TYPE_REPEATER 2
|
|
#define ADV_TYPE_ROOM 3
|
|
#define ADV_TYPE_SENSOR 4
|
|
#define REQ_TYPE_GET_TELEMETRY_DATA 3
|
|
#define TELEM_MODE_ALLOW_FLAGS 1
|
|
#define TELEM_MODE_ALLOW_ALL 2
|
|
#define TELEM_PERM_BASE 1
|
|
#define TELEM_PERM_LOCATION 2
|
|
#define TELEM_PERM_ENVIRONMENT 4
|
|
#define TELEM_CHANNEL_SELF 1
|
|
using std::isnan;
|
|
|
|
struct ContactInfo {
|
|
struct Identity { uint8_t pub_key[32] = {}; } id;
|
|
uint8_t type = ADV_TYPE_SENSOR, flags = 0;
|
|
bool transient = false;
|
|
int32_t gps_lat = 100, gps_lon = 200;
|
|
uint32_t last_advert_timestamp = 900, lastmod = 800;
|
|
};
|
|
struct Clock { uint32_t now = 0; uint32_t getMillis() const { return now; } };
|
|
struct RTC { uint32_t now = 1000; uint32_t getCurrentTime() const { return now; } };
|
|
struct Board {
|
|
uint16_t getBattMilliVolts() { return 3700; }
|
|
float getMCUTemperature() { return NAN; }
|
|
};
|
|
struct Sensors { void querySensors(uint8_t, ...) {} };
|
|
struct Telemetry {
|
|
void reset() {} void addVoltage(uint8_t, float) {} void addTemperature(uint8_t, float) {}
|
|
uint8_t getSize() { return 0; } const uint8_t* getBuffer() { return nullptr; }
|
|
};
|
|
class MyMesh {
|
|
public:
|
|
struct Prefs {
|
|
uint8_t lost_reply = 0, telemetry_mode_base = 0;
|
|
uint8_t telemetry_mode_loc = 0, telemetry_mode_env = 0;
|
|
} _prefs;
|
|
Clock clock; Clock* _ms = &clock; RTC rtc;
|
|
Board board; Sensors sensors; Telemetry telemetry;
|
|
std::vector<ContactInfo> contacts;
|
|
bool mutable_contacts = true;
|
|
unsigned notifications = 0, queries = 0;
|
|
uint32_t user_ack_table[8] = {11,22,33,44,55,66,77,88};
|
|
uint32_t user_ack_timeout = 7654321;
|
|
#if MESH_ENABLE_LOST_REPLY
|
|
mesh::companion::LostReplyLimiter lost_reply_limiter;
|
|
uint8_t handleTrackerStatusRequest(const ContactInfo&, uint32_t, const uint8_t*, uint8_t, uint8_t*);
|
|
#endif
|
|
uint8_t onContactRequest(const ContactInfo&, uint32_t, const uint8_t*, uint8_t, uint8_t*);
|
|
bool isTransientContact(const ContactInfo& contact) { return contact.transient; }
|
|
bool canMutateContacts() { return mutable_contacts; }
|
|
RTC* getRTCClock() { return &rtc; }
|
|
ContactInfo* lookupPersistentContactByPubKey(const uint8_t* key, size_t length) {
|
|
assert(length == 32);
|
|
++queries;
|
|
for (auto& contact : contacts) {
|
|
if (!contact.transient && memcmp(contact.id.pub_key, key, 32) == 0) return &contact;
|
|
}
|
|
return nullptr;
|
|
}
|
|
void onDiscoveredContact(ContactInfo& contact, bool added, uint8_t length, const uint8_t* path) {
|
|
assert(!added && length == 0 && path == nullptr);
|
|
assert(contact.lastmod == rtc.now);
|
|
++notifications;
|
|
}
|
|
ContactInfo& saved(uint8_t key = 1) {
|
|
contacts.emplace_back();
|
|
contacts.back().id.pub_key[0] = key;
|
|
return contacts.back();
|
|
}
|
|
};
|
|
@METHODS@
|
|
|
|
static mesh::tracker::StatusRequest report(bool gps = false) {
|
|
mesh::tracker::StatusRequest value;
|
|
value.battery_mv = 3712;
|
|
if (gps) {
|
|
value.flags = mesh::tracker::FreshGps;
|
|
value.latitude = 471234567;
|
|
value.longitude = -1222345678;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
static uint8_t query(MyMesh& mesh, const ContactInfo& peer, uint32_t tag,
|
|
const mesh::tracker::StatusRequest& value,
|
|
uint8_t expected, bool padded = true) {
|
|
uint8_t bytes[28] = {};
|
|
assert(mesh::tracker::makeStatusRequest(value, bytes, sizeof(bytes)) == 13);
|
|
uint8_t answer[18]; memset(answer, 0xa5, sizeof(answer));
|
|
uint32_t ack_before[8]; memcpy(ack_before, mesh.user_ack_table, sizeof(ack_before));
|
|
const uint32_t timeout_before = mesh.user_ack_timeout;
|
|
const auto prefs_before = mesh._prefs;
|
|
const uint8_t length = mesh.onContactRequest(peer, tag, bytes,
|
|
padded ? 28 : 13, answer + 1);
|
|
assert(length == expected && answer[0] == 0xa5);
|
|
for (size_t i = 1 + length; i < sizeof(answer); ++i) assert(answer[i] == 0xa5);
|
|
assert(memcmp(ack_before, mesh.user_ack_table, sizeof(ack_before)) == 0);
|
|
assert(mesh.user_ack_timeout == timeout_before);
|
|
assert(memcmp(&prefs_before, &mesh._prefs, sizeof(prefs_before)) == 0);
|
|
if (length) {
|
|
uint8_t status = 99; uint16_t awake = 999;
|
|
assert(mesh::tracker::parseStatusResponse(answer + 1, length, tag, status, awake));
|
|
const uint8_t expected_status = mesh._prefs.lost_reply <= 2 ? mesh._prefs.lost_reply : 0;
|
|
assert(status == expected_status && awake == (status == 2 ? 120 : 0));
|
|
}
|
|
return length;
|
|
}
|
|
|
|
static void codec() {
|
|
const auto original = report(true);
|
|
uint8_t bytes[40] = {};
|
|
assert(mesh::tracker::makeStatusRequest(original, bytes, sizeof(bytes)) == 13);
|
|
assert(bytes[0] == 0x0c && bytes[1] == 1 && bytes[2] == 1);
|
|
assert(bytes[11] == 0x80 && bytes[12] == 0x0e);
|
|
for (size_t length = 0; length <= sizeof(bytes); ++length) {
|
|
mesh::tracker::StatusRequest parsed;
|
|
parsed.latitude = 77;
|
|
const bool valid = mesh::tracker::parseStatusRequest(bytes, length, parsed);
|
|
assert(valid == (length == 13 || length == 28));
|
|
if (valid) {
|
|
assert(parsed.flags == original.flags && parsed.latitude == original.latitude);
|
|
assert(parsed.longitude == original.longitude && parsed.battery_mv == original.battery_mv);
|
|
} else assert(parsed.latitude == 77);
|
|
}
|
|
mesh::tracker::StatusRequest parsed;
|
|
assert(!mesh::tracker::parseStatusRequest(nullptr, 28, parsed));
|
|
for (size_t i = 13; i < 28; ++i) {
|
|
bytes[i] = 1;
|
|
assert(!mesh::tracker::parseStatusRequest(bytes, 28, parsed));
|
|
bytes[i] = 0;
|
|
}
|
|
for (uint8_t flags : {uint8_t(3), uint8_t(4), uint8_t(255)}) {
|
|
bytes[2] = flags;
|
|
assert(!mesh::tracker::parseStatusRequest(bytes, 13, parsed));
|
|
}
|
|
for (int32_t bad : {INT32_MIN, INT32_MAX, -900000001, 900000001}) {
|
|
auto value = original; value.latitude = bad;
|
|
assert(mesh::tracker::makeStatusRequest(value, bytes, sizeof(bytes)) == 0);
|
|
assert(mesh::tracker::makeStatusRequest(original, bytes, sizeof(bytes)) == 13);
|
|
mesh::tracker::put32(bytes + 3, uint32_t(bad));
|
|
assert(!mesh::tracker::parseStatusRequest(bytes, 13, parsed));
|
|
}
|
|
for (int32_t bad : {INT32_MIN, INT32_MAX, -1800000001, 1800000001}) {
|
|
auto value = original; value.longitude = bad;
|
|
assert(mesh::tracker::makeStatusRequest(value, bytes, sizeof(bytes)) == 0);
|
|
assert(mesh::tracker::makeStatusRequest(original, bytes, sizeof(bytes)) == 13);
|
|
mesh::tracker::put32(bytes + 7, uint32_t(bad));
|
|
assert(!mesh::tracker::parseStatusRequest(bytes, 13, parsed));
|
|
}
|
|
for (int32_t edge : {-900000000, 900000000}) {
|
|
auto value = original; value.latitude = edge;
|
|
assert(mesh::tracker::makeStatusRequest(value, bytes, sizeof(bytes)) == 13);
|
|
assert(mesh::tracker::parseStatusRequest(bytes, 13, parsed) && parsed.latitude == edge);
|
|
}
|
|
auto no_fix = report(); no_fix.flags = mesh::tracker::GpsAttemptFailed;
|
|
assert(mesh::tracker::makeStatusRequest(no_fix, bytes, sizeof(bytes)) == 13);
|
|
no_fix.latitude = 1;
|
|
assert(mesh::tracker::makeStatusRequest(no_fix, bytes, sizeof(bytes)) == 0);
|
|
assert(mesh::tracker::makeStatusRequest(original, nullptr, 40) == 0);
|
|
assert(mesh::tracker::makeStatusRequest(original, bytes, 12) == 0);
|
|
|
|
uint8_t answer[32] = {};
|
|
assert(mesh::tracker::makeStatusResponse(0x87654321, 2, 120, answer, 32) == 9);
|
|
assert(answer[0] == 0x21 && answer[1] == 0x43 && answer[2] == 0x65 && answer[3] == 0x87);
|
|
assert(answer[4] == 12 && answer[5] == 1 && answer[6] == 2 && answer[7] == 120 && answer[8] == 0);
|
|
for (size_t length = 0; length <= sizeof(answer); ++length) {
|
|
uint8_t status = 99; uint16_t awake = 999;
|
|
const bool valid = mesh::tracker::parseStatusResponse(answer, length, 0x87654321, status, awake);
|
|
assert(valid == (length == 9 || length == 16));
|
|
assert(status == (valid ? 2 : 99) && awake == (valid ? 120 : 999));
|
|
}
|
|
uint8_t status = 99; uint16_t awake = 999;
|
|
assert(!mesh::tracker::parseStatusResponse(answer, 16, 0x87654322, status, awake));
|
|
for (size_t i = 9; i < 16; ++i) {
|
|
answer[i] = 1;
|
|
assert(!mesh::tracker::parseStatusResponse(answer, 16, 0x87654321, status, awake));
|
|
answer[i] = 0;
|
|
}
|
|
for (size_t index : {size_t(4), size_t(5), size_t(6)}) {
|
|
const uint8_t saved = answer[index]; answer[index] = 255;
|
|
assert(!mesh::tracker::parseStatusResponse(answer, 9, 0x87654321, status, awake));
|
|
answer[index] = saved;
|
|
}
|
|
assert(!mesh::tracker::parseStatusResponse(nullptr, 16, 1, status, awake));
|
|
assert(mesh::tracker::makeStatusResponse(1, 3, 0, answer, 32) == 0);
|
|
assert(mesh::tracker::makeStatusResponse(1, 2, 120, answer, 8) == 0);
|
|
assert(mesh::tracker::makeStatusResponse(1, 2, 120, nullptr, 32) == 0);
|
|
}
|
|
|
|
static void callbacks() {
|
|
#if MESH_ENABLE_LOST_REPLY
|
|
for (uint8_t state : {uint8_t(0), uint8_t(1), uint8_t(2), uint8_t(255)}) {
|
|
for (uint8_t type : {uint8_t(ADV_TYPE_SENSOR), uint8_t(ADV_TYPE_CHAT)}) {
|
|
MyMesh mesh; auto& peer = mesh.saved(); peer.type = type; mesh._prefs.lost_reply = state;
|
|
query(mesh, peer, 1000, report(), 9);
|
|
assert(mesh.notifications == 0 && peer.gps_lat == 100 && peer.gps_lon == 200);
|
|
}
|
|
}
|
|
for (uint8_t type : {uint8_t(0), uint8_t(ADV_TYPE_ROOM), uint8_t(ADV_TYPE_REPEATER), uint8_t(255)}) {
|
|
MyMesh mesh; auto& peer = mesh.saved(); peer.type = type;
|
|
query(mesh, peer, 1000, report(true), 0);
|
|
}
|
|
{
|
|
MyMesh mesh; auto& peer = mesh.saved(); peer.transient = true;
|
|
query(mesh, peer, 1000, report(true), 0);
|
|
peer.transient = false;
|
|
ContactInfo unknown = peer; unknown.id.pub_key[31] = 1;
|
|
query(mesh, unknown, 1000, report(true), 0);
|
|
assert(mesh.notifications == 0);
|
|
}
|
|
{
|
|
MyMesh mesh; auto& peer = mesh.saved(); mesh._prefs.lost_reply = 2;
|
|
uint8_t bytes[40] = {};
|
|
assert(mesh::tracker::makeStatusRequest(report(true), bytes, 40) == 13);
|
|
uint8_t answer[16]; memset(answer, 0xa5, sizeof(answer));
|
|
assert(mesh.onContactRequest(peer, 1000, nullptr, 13, answer) == 0);
|
|
assert(mesh.onContactRequest(peer, 1000, bytes, 0, answer) == 0);
|
|
assert(mesh.onContactRequest(peer, 1000, bytes, 13, nullptr) == 0);
|
|
for (uint8_t length : {uint8_t(1), uint8_t(12), uint8_t(14), uint8_t(27), uint8_t(29), uint8_t(40)})
|
|
assert(mesh.onContactRequest(peer, 1000, bytes, length, answer) == 0);
|
|
bytes[1] = 2;
|
|
assert(mesh.onContactRequest(peer, 1000, bytes, 28, answer) == 0); bytes[1] = 1;
|
|
bytes[13] = 1;
|
|
assert(mesh.onContactRequest(peer, 1000, bytes, 28, answer) == 0); bytes[13] = 0;
|
|
assert(mesh.notifications == 0);
|
|
query(mesh, peer, 1000, report(true), 9);
|
|
assert(peer.gps_lat == 47123456 && peer.gps_lon == -122234567);
|
|
assert(peer.last_advert_timestamp == 1000 && peer.lastmod == 1000 && mesh.notifications == 1);
|
|
query(mesh, peer, 1000, report(true), 0); // Same logical request cannot produce a second reply.
|
|
mesh.clock.now = 60000;
|
|
query(mesh, peer, 1000, report(true), 0); // Exact retry remains suppressed beyond cooldown.
|
|
query(mesh, peer, 1001, report(true), 9, false);
|
|
assert(peer.last_advert_timestamp == 1001 && mesh.notifications == 2);
|
|
}
|
|
for (uint32_t tag : {uint32_t(699), uint32_t(700), uint32_t(1300), uint32_t(1301), UINT32_MAX}) {
|
|
MyMesh mesh; auto& peer = mesh.saved(); peer.last_advert_timestamp = 1;
|
|
query(mesh, peer, tag, report(true), 9);
|
|
const bool accepted = tag >= 700 && tag <= 1300;
|
|
assert(mesh.notifications == (accepted ? 1U : 0U));
|
|
assert(peer.last_advert_timestamp == (accepted ? tag : 1));
|
|
}
|
|
for (uint32_t floor : {uint32_t(999), uint32_t(1000), uint32_t(1001)}) {
|
|
MyMesh mesh; auto& peer = mesh.saved(); peer.last_advert_timestamp = floor;
|
|
query(mesh, peer, 1000, report(true), 9);
|
|
assert(mesh.notifications == (floor < 1000 ? 1U : 0U));
|
|
assert(peer.last_advert_timestamp == (floor < 1000 ? 1000 : floor));
|
|
}
|
|
{
|
|
MyMesh mesh; auto& peer = mesh.saved(); mesh.mutable_contacts = false;
|
|
query(mesh, peer, 1000, report(true), 9);
|
|
assert(mesh.notifications == 0 && peer.last_advert_timestamp == 900);
|
|
}
|
|
{
|
|
MyMesh mesh; mesh.contacts.reserve(2);
|
|
auto& first = mesh.saved(1); auto& second = mesh.saved(2);
|
|
query(mesh, first, 1000, report(), 9);
|
|
query(mesh, second, 1000, report(), 0);
|
|
mesh.clock.now = 10000;
|
|
query(mesh, second, 1000, report(), 9);
|
|
mesh.clock.now = 59999;
|
|
query(mesh, first, 1001, report(), 0);
|
|
mesh.clock.now = 60000;
|
|
query(mesh, first, 1001, report(), 9);
|
|
}
|
|
#else
|
|
MyMesh mesh; auto& peer = mesh.saved(); mesh._prefs.lost_reply = 2;
|
|
query(mesh, peer, 1000, report(true), 0);
|
|
assert(mesh.notifications == 0 && peer.last_advert_timestamp == 900);
|
|
#endif
|
|
}
|
|
|
|
int main() {
|
|
codec(); callbacks();
|
|
puts("PASS: tracker owner production callback and wire codec");
|
|
}
|
|
'''
|
|
|
|
|
|
class TrackerOwnerResponseTests(unittest.TestCase):
|
|
def test_actual_owner_callback_and_codec(self):
|
|
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
|
|
if compiler is None:
|
|
self.skipTest("a host C++ compiler is required")
|
|
source = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
|
|
methods = extract_braced(source, "uint8_t MyMesh::onContactRequest(")
|
|
methods += "\n#if MESH_ENABLE_LOST_REPLY\n" + extract_braced(
|
|
source, "uint8_t MyMesh::handleTrackerStatusRequest(") + "\n#endif\n"
|
|
with tempfile.TemporaryDirectory(prefix="meshcore-tracker-owner-") as directory:
|
|
work = Path(directory)
|
|
cpp = work / "owner.cpp"
|
|
cpp.write_text(HARNESS.replace("@METHODS@", methods), encoding="ascii")
|
|
for flags in ([], ["-DSTM32_PLATFORM=1"],
|
|
["-DSTM32_PLATFORM=1", "-DMESH_ENABLE_LOST_REPLY=1"],
|
|
["-DMESH_ENABLE_LOST_REPLY=0"]):
|
|
with self.subTest(flags=flags):
|
|
binary = work / "owner"
|
|
built = subprocess.run([
|
|
compiler, "-std=c++11", "-Wall", "-Wextra", "-Werror",
|
|
"-Wno-unused-parameter", *SANITIZERS, *flags,
|
|
"-I", str(ROOT / "src"), "-I", str(ROOT),
|
|
str(cpp), "-o", str(binary),
|
|
], text=True, capture_output=True, timeout=60)
|
|
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
|
ran = subprocess.run([str(binary)], text=True, capture_output=True, timeout=20)
|
|
self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr)
|
|
self.assertIn("PASS: tracker owner production callback and wire codec", ran.stdout)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|