mirror of
https://github.com/meshtastic/firmware.git
synced 2026-11-07 23:04:49 -05:00
* test(harness): make run-tests.sh drivable by a caller that cannot see the terminal A tool call or a fresh session reads a captured output file, gets interrupted mid-run, and starts cold. Two things in the wrapper tripped that caller: an interrupt left pio's build tree running with no record of it (the next invocation started a second build into the same .pio/build/, or pgrep'd and matched itself); and pio's own "[PASSED]" / "N succeeded" lines made a half-finished output file read as green. run-tests.sh: - Run record in .pio/runtests/current.tsv for the life of a run. A second invocation prints RESULT: BUSY and exits 4 without touching the build directory. Valid while the holder pid OR the recorded process group is alive, so a SIGKILLed wrapper with live scons children reads ORPHANED rather than clear. - --status / --wait / --abort. --abort kills the whole tree by pgid. - pio runs under setsid with its pgid recorded; INT/TERM/HUP kill the tree and record RESULT: ABORTED (exit 5), log kept. - One result() for every verdict: prints to the stdout the script started with (a signal can arrive inside a redirected pio call) and writes .pio/runtests/last-result.tsv with head, args, env, finish time, kept log and a tree fingerprint (HEAD + working-tree diff + untracked files). --status marks the last verdict STALE when the tree has changed since. - Banner naming the final RESULT: line as the only verdict. - Non-Linux host: RESULT: UNSUPPORTED, exit 6, instead of the AMBER code. - A failed build removes .pio/build/<env>/meshtasticd, which run bare would reprint the last good run. - FILTERED lists the not-run count, not 77 suite names. bin/run-tests.cmd forwards into WSL with the exit code passed through, so the same command line works from cmd.exe and PowerShell; no logic is duplicated. test/README.md, copilot-instructions.md and the mirrors document the new codes and the rule. * test(harness): one-suite warm-up, and name the build phase instead of freezing the counter Measured on a full native run: the warm-up, `pio test --without-testing` with no filter, builds AND links every suite - 78 links, 2320 s, 29.7 s each, 39 minutes before the first test ran - and prints a "[PASSED]" line for each program it merely linked. CI never did this; its warm-up is one `platformio run`. The shared src objects are the same whichever suite links them, so the warm-up now links one: the filtered suite when there is one, else test_utf8. The run itself still builds every suite, as it must. The heartbeat counted objects newer than its marker, which sits still through PlatformIO's single-threaded scons dependency scan and through each link - twelve minutes at "430/754 objs, ETA 17m" on that run, which reads as a hung build to a caller who cannot run ps. It now names the phase from the processes in the recorded group: [scons] / [compile] (with the ETA) / [link] / [test], and --status prints the same phase word. * test(harness): define phase_of_run before --status can call it * test(harness): bind --wait to the run it observed; serialize the run-record publish Review findings on #11862, all three valid: - --wait stored a state and then waited for any record to clear, so a run that finished and a second that started between polls would be followed to the second's verdict, and a run that turned ORPHANED mid-wait could print a stale last-result. Each run now has an id (pid-start) in current.tsv and last-result.tsv; --wait captures it and reports only a matching verdict, else ABORTED-without-verdict. - run_state() then the current.tsv write was a check-then-act pair: two invocations in the same instant could both see IDLE. The pair is now one critical section under a short-lived flock, and both records are published by rename so no reader can see a partial file. The record stays the ownership token; the lock only serializes the handoff (a SIGKILLed holder releases flock but not the record, which is why flock alone was rejected). - The FILTERED and AMBER examples in copilot-instructions.md carried a literal suite count, which the same document says never to do. Verified on a live run: --status RUNNING, a concurrent start refused BUSY, a --wait started before --abort reported the aborted run's own verdict with the matching id, exit 5; no build process survived. * fix(waypoints): do not create an empty store file on clear clearAllWaypoints() wrote a two-byte empty store unconditionally, so test_waypoint_expiry left Waypoints_default.wpts behind in every run and the suite has read AMBER (undeclared shared state) since it landed. An existing file - stale or unreadable included - is still rewritten as empty, so a reset after a failed load clears flash as before; a file that is not there is left not there.
410 lines
12 KiB
C++
410 lines
12 KiB
C++
#include "configuration.h"
|
|
|
|
#if !MESHTASTIC_EXCLUDE_WAYPOINT
|
|
|
|
#include "FSCommon.h"
|
|
#include "SPILock.h"
|
|
#include "SafeFile.h"
|
|
#include "Throttle.h"
|
|
#include "UptimeClock.h"
|
|
#include "WaypointStore.h"
|
|
#include "concurrency/LockGuard.h"
|
|
#include "gps/RTC.h"
|
|
#include "meshUtils.h"
|
|
#include <cstring>
|
|
#include <pb_decode.h>
|
|
#include <pb_encode.h>
|
|
|
|
namespace
|
|
{
|
|
|
|
constexpr uint8_t WAYPOINT_STORE_VERSION = 3;
|
|
constexpr const char *WAYPOINT_STORE_FILENAME = "/Waypoints_default.wpts";
|
|
|
|
#ifndef WAYPOINT_AUTOSAVE_INTERVAL_SEC
|
|
#define WAYPOINT_AUTOSAVE_INTERVAL_SEC (2 * 60 * 60)
|
|
#endif
|
|
|
|
struct __attribute__((packed)) StoredWaypointRecord {
|
|
uint32_t creatorNodeNum;
|
|
uint32_t receivedTime;
|
|
uint8_t notificationPreferences;
|
|
uint16_t payloadLength;
|
|
uint8_t payload[meshtastic_Waypoint_size];
|
|
};
|
|
|
|
bool decodeWaypointPayload(const uint8_t *payload, size_t payloadLength, meshtastic_Waypoint &wp)
|
|
{
|
|
memset(&wp, 0, sizeof(wp));
|
|
return pb_decode_from_bytes(payload, payloadLength, &meshtastic_Waypoint_msg, &wp);
|
|
}
|
|
|
|
uint16_t encodeWaypointPayload(const meshtastic_Waypoint &wp, uint8_t *payload, size_t payloadCapacity)
|
|
{
|
|
return (uint16_t)pb_encode_to_bytes(payload, payloadCapacity, &meshtastic_Waypoint_msg, &wp);
|
|
}
|
|
|
|
static bool g_waypointStoreHasUnsavedChanges = false;
|
|
static uint32_t g_lastWaypointAutoSaveMs = 0;
|
|
|
|
uint32_t autosaveIntervalMs()
|
|
{
|
|
uint32_t sec = (uint32_t)WAYPOINT_AUTOSAVE_INTERVAL_SEC;
|
|
if (sec < 60)
|
|
sec = 60;
|
|
return sec * 1000UL;
|
|
}
|
|
|
|
void markWaypointStoreUnsaved()
|
|
{
|
|
g_waypointStoreHasUnsavedChanges = true;
|
|
if (g_lastWaypointAutoSaveMs == 0)
|
|
g_lastWaypointAutoSaveMs = Time::getMillis();
|
|
}
|
|
|
|
void persistWaypointStore()
|
|
{
|
|
LOG_INFO("Autosaving WaypointStore to flash");
|
|
waypointStore.saveToFlash();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
WaypointStore waypointStore;
|
|
|
|
void WaypointStore::notifyChanged()
|
|
{
|
|
notifyObservers(this);
|
|
}
|
|
|
|
bool WaypointStore::isExpired(const meshtastic_Waypoint &wp, uint32_t now)
|
|
{
|
|
// getTime() counts from boot until the RTC is set, which reads every real expiry as future.
|
|
if (now == 0)
|
|
now = getValidTime(RTCQuality::RTCQualityDevice);
|
|
|
|
return !waypointIsActive(wp.expire, now);
|
|
}
|
|
|
|
bool WaypointStore::isExpired(const StoredWaypoint &entry, uint32_t now)
|
|
{
|
|
return isExpired(entry.waypoint, now);
|
|
}
|
|
|
|
uint8_t WaypointStore::notificationPreferencesFromWaypoint(const meshtastic_Waypoint &wp)
|
|
{
|
|
uint8_t preferences = 0;
|
|
if (wp.notify_on_enter)
|
|
preferences |= WAYPOINT_NOTIFY_ENTER;
|
|
if (wp.notify_on_exit)
|
|
preferences |= WAYPOINT_NOTIFY_EXIT;
|
|
if (wp.notify_favorites_only)
|
|
preferences |= WAYPOINT_NOTIFY_FAVORITES_ONLY;
|
|
return preferences;
|
|
}
|
|
|
|
uint8_t WaypointStore::mergeNotificationPreferences(bool locallyAuthored, bool hasExisting, uint8_t existingPreferences,
|
|
const meshtastic_Waypoint &incoming)
|
|
{
|
|
if (locallyAuthored)
|
|
return notificationPreferencesFromWaypoint(incoming);
|
|
return hasExisting ? existingPreferences : 0;
|
|
}
|
|
|
|
void WaypointStore::clearWireNotificationPreferences(meshtastic_Waypoint &wp)
|
|
{
|
|
wp.notify_on_enter = false;
|
|
wp.notify_on_exit = false;
|
|
wp.notify_favorites_only = false;
|
|
}
|
|
|
|
const StoredWaypoint *WaypointStore::findWaypoint(uint32_t id) const
|
|
{
|
|
for (const StoredWaypoint &entry : waypoints) {
|
|
if (entry.waypoint.id == id)
|
|
return &entry;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
bool WaypointStore::removeWaypointById(uint32_t id)
|
|
{
|
|
for (auto it = waypoints.begin(); it != waypoints.end(); ++it) {
|
|
if (it->waypoint.id == id) {
|
|
waypoints.erase(it);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool WaypointStore::removeWaypoint(uint32_t id)
|
|
{
|
|
const bool removed = removeWaypointById(id);
|
|
if (!removed)
|
|
return false;
|
|
|
|
#if ENABLE_WAYPOINT_PERSISTENCE
|
|
markWaypointStoreUnsaved();
|
|
#endif
|
|
notifyChanged();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool WaypointStore::setNotificationPreference(uint32_t id, WaypointNotificationPreference preference, bool enabled)
|
|
{
|
|
for (StoredWaypoint &entry : waypoints) {
|
|
if (entry.waypoint.id != id)
|
|
continue;
|
|
|
|
const uint8_t previous = entry.notificationPreferences;
|
|
if (enabled)
|
|
entry.notificationPreferences |= preference;
|
|
else
|
|
entry.notificationPreferences &= ~preference;
|
|
if (entry.notificationPreferences == previous)
|
|
return true;
|
|
|
|
#if ENABLE_WAYPOINT_PERSISTENCE
|
|
markWaypointStoreUnsaved();
|
|
#endif
|
|
notifyChanged();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void WaypointStore::addStoredWaypoint(const StoredWaypoint &entry)
|
|
{
|
|
removeWaypointById(entry.waypoint.id);
|
|
|
|
waypoints.push_front(entry);
|
|
while (waypoints.size() > WAYPOINT_HISTORY_LIMIT)
|
|
waypoints.pop_back();
|
|
}
|
|
|
|
bool WaypointStore::addFromPacket(const meshtastic_MeshPacket &packet, bool locallyAuthored, StoredWaypoint *stored)
|
|
{
|
|
StoredWaypoint entry;
|
|
if (!decodeWaypointPayload(packet.decoded.payload.bytes, packet.decoded.payload.size, entry.waypoint))
|
|
return false;
|
|
|
|
const StoredWaypoint *existing = findWaypoint(entry.waypoint.id);
|
|
entry.notificationPreferences = mergeNotificationPreferences(
|
|
locallyAuthored, existing != nullptr, existing ? existing->notificationPreferences : 0, entry.waypoint);
|
|
clearWireNotificationPreferences(entry.waypoint);
|
|
entry.receivedTime = packet.rx_time ? packet.rx_time : getTime();
|
|
entry.creatorNodeNum = getFrom(&packet);
|
|
|
|
if (stored)
|
|
*stored = entry;
|
|
|
|
// rx_time holds uptime, not an epoch, when has_rx_time is false; pass 0 so isExpired() resolves
|
|
// the clock itself rather than comparing an expiry against seconds since boot.
|
|
if (isExpired(entry, packet.has_rx_time ? packet.rx_time : 0)) {
|
|
// Respect the lock: only the node a waypoint is locked to may delete it on our device.
|
|
// An unauthorized deletion attempt is ignored entirely, rather than applied locally.
|
|
for (const auto &storedEntry : waypoints) {
|
|
if (storedEntry.waypoint.id != entry.waypoint.id)
|
|
continue;
|
|
if (storedEntry.waypoint.locked_to != 0 && storedEntry.waypoint.locked_to != entry.creatorNodeNum)
|
|
return true; // Packet handled, but the deletion is not honored
|
|
break;
|
|
}
|
|
|
|
removeWaypoint(entry.waypoint.id);
|
|
return true;
|
|
}
|
|
|
|
addStoredWaypoint(entry);
|
|
|
|
#if ENABLE_WAYPOINT_PERSISTENCE
|
|
markWaypointStoreUnsaved();
|
|
#endif
|
|
notifyChanged();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool WaypointStore::purgeExpired(uint32_t now)
|
|
{
|
|
// No local clock normalization: isExpired() owns that policy, including the delete convention.
|
|
bool changed = false;
|
|
for (auto it = waypoints.begin(); it != waypoints.end();) {
|
|
if (!isExpired(*it, now)) {
|
|
++it;
|
|
continue;
|
|
}
|
|
|
|
it = waypoints.erase(it);
|
|
changed = true;
|
|
}
|
|
|
|
if (changed) {
|
|
#if ENABLE_WAYPOINT_PERSISTENCE
|
|
markWaypointStoreUnsaved();
|
|
#endif
|
|
notifyChanged();
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
void WaypointStore::saveToFlash()
|
|
{
|
|
purgeExpired();
|
|
|
|
#if ENABLE_WAYPOINT_PERSISTENCE && defined(FSCom)
|
|
if (!g_waypointStoreHasUnsavedChanges)
|
|
return;
|
|
|
|
spiLock->lock();
|
|
FSCom.mkdir("/");
|
|
spiLock->unlock();
|
|
|
|
SafeFile f(WAYPOINT_STORE_FILENAME, false);
|
|
|
|
spiLock->lock();
|
|
const uint8_t version = WAYPOINT_STORE_VERSION;
|
|
size_t countFull = waypoints.size();
|
|
if (countFull > WAYPOINT_HISTORY_LIMIT)
|
|
countFull = WAYPOINT_HISTORY_LIMIT;
|
|
if (countFull > UINT8_MAX)
|
|
countFull = UINT8_MAX;
|
|
const uint8_t count = (uint8_t)countFull;
|
|
|
|
f.write(&version, 1);
|
|
f.write(&count, 1);
|
|
|
|
for (uint8_t i = 0; i < count; ++i) {
|
|
StoredWaypointRecord rec = {};
|
|
rec.creatorNodeNum = waypoints[i].creatorNodeNum;
|
|
rec.receivedTime = waypoints[i].receivedTime;
|
|
rec.notificationPreferences = waypoints[i].notificationPreferences;
|
|
rec.payloadLength = encodeWaypointPayload(waypoints[i].waypoint, rec.payload, sizeof(rec.payload));
|
|
f.write(reinterpret_cast<const uint8_t *>(&rec), sizeof(rec));
|
|
}
|
|
spiLock->unlock();
|
|
f.close();
|
|
#endif
|
|
|
|
#if ENABLE_WAYPOINT_PERSISTENCE
|
|
g_waypointStoreHasUnsavedChanges = false;
|
|
g_lastWaypointAutoSaveMs = Time::getMillis();
|
|
#endif
|
|
}
|
|
|
|
void WaypointStore::loadFromFlash()
|
|
{
|
|
std::deque<StoredWaypoint>().swap(waypoints);
|
|
|
|
#if ENABLE_WAYPOINT_PERSISTENCE && defined(FSCom)
|
|
{
|
|
concurrency::LockGuard guard(spiLock);
|
|
|
|
if (FSCom.exists(WAYPOINT_STORE_FILENAME)) {
|
|
auto f = FSCom.open(WAYPOINT_STORE_FILENAME, FILE_O_READ);
|
|
if (f) {
|
|
uint8_t version = 0;
|
|
uint8_t count = 0;
|
|
f.readBytes(reinterpret_cast<char *>(&version), 1);
|
|
f.readBytes(reinterpret_cast<char *>(&count), 1);
|
|
|
|
if (version != WAYPOINT_STORE_VERSION) {
|
|
LOG_WARN("WaypointStore version mismatch (%u)", version);
|
|
f.close();
|
|
} else {
|
|
if (count > WAYPOINT_HISTORY_LIMIT)
|
|
count = WAYPOINT_HISTORY_LIMIT;
|
|
|
|
for (uint8_t i = 0; i < count; ++i) {
|
|
StoredWaypoint entry;
|
|
StoredWaypointRecord rec = {};
|
|
if (f.readBytes(reinterpret_cast<char *>(&rec), sizeof(rec)) != sizeof(rec))
|
|
break;
|
|
if (rec.payloadLength == 0 || rec.payloadLength > sizeof(rec.payload)) {
|
|
LOG_WARN("WaypointStore skipping corrupt record %u", i);
|
|
continue;
|
|
}
|
|
if (!decodeWaypointPayload(rec.payload, rec.payloadLength, entry.waypoint))
|
|
continue;
|
|
entry.receivedTime = rec.receivedTime;
|
|
entry.creatorNodeNum = rec.creatorNodeNum;
|
|
entry.notificationPreferences = rec.notificationPreferences;
|
|
|
|
if (isExpired(entry.waypoint))
|
|
continue;
|
|
waypoints.push_back(entry);
|
|
}
|
|
f.close();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#if ENABLE_WAYPOINT_PERSISTENCE
|
|
g_waypointStoreHasUnsavedChanges = false;
|
|
g_lastWaypointAutoSaveMs = Time::getMillis();
|
|
#endif
|
|
}
|
|
|
|
void WaypointStore::clearAllWaypoints()
|
|
{
|
|
const bool hadWaypoints = !waypoints.empty();
|
|
|
|
std::deque<StoredWaypoint>().swap(waypoints);
|
|
|
|
#if ENABLE_WAYPOINT_PERSISTENCE && defined(FSCom)
|
|
// Rewrite an existing store (stale or unreadable included) as empty; never create one just to
|
|
// say so. Checked before SafeFile, which takes spiLock itself.
|
|
bool onFlash;
|
|
{
|
|
concurrency::LockGuard guard(spiLock);
|
|
onFlash = FSCom.exists(WAYPOINT_STORE_FILENAME);
|
|
}
|
|
if (onFlash) {
|
|
SafeFile f(WAYPOINT_STORE_FILENAME, false);
|
|
{
|
|
concurrency::LockGuard guard(spiLock);
|
|
const uint8_t version = WAYPOINT_STORE_VERSION;
|
|
const uint8_t count = 0;
|
|
f.write(&version, 1);
|
|
f.write(&count, 1);
|
|
}
|
|
f.close();
|
|
}
|
|
#endif
|
|
|
|
#if ENABLE_WAYPOINT_PERSISTENCE
|
|
g_waypointStoreHasUnsavedChanges = false;
|
|
g_lastWaypointAutoSaveMs = Time::getMillis();
|
|
#endif
|
|
|
|
if (hadWaypoints)
|
|
notifyChanged();
|
|
}
|
|
|
|
#if ENABLE_WAYPOINT_PERSISTENCE
|
|
void waypointStoreAutosaveTick()
|
|
{
|
|
if (!g_waypointStoreHasUnsavedChanges) {
|
|
if (g_lastWaypointAutoSaveMs == 0)
|
|
g_lastWaypointAutoSaveMs = Time::getMillis();
|
|
return;
|
|
}
|
|
|
|
if (g_lastWaypointAutoSaveMs == 0) {
|
|
g_lastWaypointAutoSaveMs = Time::getMillis();
|
|
return;
|
|
}
|
|
|
|
Throttle::execute(&g_lastWaypointAutoSaveMs, autosaveIntervalMs(), persistWaypointStore);
|
|
}
|
|
#endif
|
|
|
|
#endif
|