mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-22 13:01:37 +00:00
Keep ESP32 USB startup responsive during optional GPS discovery
This commit is contained in:
1 parent
163364e528
commit
c8c5f87ab9
4 files changed
+464
-1
No files matched your search
@@ -262,6 +262,7 @@ jobs:
|
||||
run: |
|
||||
python3 -B test/test_gps_status.py
|
||||
python3 -B test/test_gps_upstream_adaptations.py -v
|
||||
python3 -B test/test_esp32_gps_discovery.py -v
|
||||
python3 -B test/test_gps_sync_interval.py -v
|
||||
python3 -B test/test_gps_sync_preferences.py -v
|
||||
python3 -B test/test_t1000e_sensor_power.py -v
|
||||
|
||||
@@ -1112,7 +1112,16 @@ const char* EnvironmentSensorManager::getSettingValue(int i) const {
|
||||
|
||||
bool EnvironmentSensorManager::setSettingValue(const char* name, const char* value) {
|
||||
#if ENV_INCLUDE_GPS
|
||||
if (gps_detected && strcmp(name, "gps") == 0) {
|
||||
bool gps_setting_available = gps_detected;
|
||||
#if defined(ESP32_PLATFORM) && !defined(RAK_WISBLOCK_GPS)
|
||||
gps_setting_available = gps_setting_available || gps_discovery_pending;
|
||||
#endif
|
||||
if (gps_setting_available && strcmp(name, "gps") == 0) {
|
||||
#if defined(ESP32_PLATFORM) && !defined(RAK_WISBLOCK_GPS)
|
||||
// Preferences load before a cold receiver finishes its bounded probe.
|
||||
// Retain intent so late discovery cannot replace a saved off/on choice.
|
||||
gps_discovery_preference_known = true;
|
||||
#endif
|
||||
bool enabled = strcmp(value, "0") != 0;
|
||||
#if defined(RAK_WISBLOCK_GPS) && defined(FORCE_GPS_ALIVE)
|
||||
// A UART L76K cannot remove power from the shared 3V3_S rail. "Off" stops
|
||||
@@ -1172,6 +1181,51 @@ void EnvironmentSensorManager::setPowerSavingEnabled(bool enabled) {
|
||||
}
|
||||
|
||||
#if ENV_INCLUDE_GPS
|
||||
#if defined(ESP32_PLATFORM) && !defined(RAK_WISBLOCK_GPS)
|
||||
void EnvironmentSensorManager::finishBasicGpsDiscovery(bool found) {
|
||||
gps_discovery_pending = false;
|
||||
gps_detected = found;
|
||||
gps_serial_transport = found;
|
||||
if (found) {
|
||||
MESH_DEBUG_PRINTLN("GPS detected");
|
||||
#ifdef PERSISTANT_GPS
|
||||
// Keep the established always-on default only when no loaded preference
|
||||
// has superseded it. The receiver is already awake from the probe.
|
||||
if (!gps_discovery_preference_known || isGpsTelemetryUserEnabled()) {
|
||||
gps_active = true;
|
||||
if (!gps_discovery_preference_known) setGpsTelemetryUserEnabled(true);
|
||||
_location->setGPSPowerSaving(powersaving_enabled);
|
||||
armGpsPowerSavingCycle();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("No GPS detected");
|
||||
}
|
||||
_location->stop();
|
||||
gps_active = false;
|
||||
if (found && isGpsTelemetryUserEnabled()) {
|
||||
_location->setGPSPowerSaving(powersaving_enabled);
|
||||
start_gps();
|
||||
}
|
||||
}
|
||||
|
||||
void EnvironmentSensorManager::serviceBasicGpsDiscovery() {
|
||||
// A bridge may own Serial1 until it explicitly releases the receiver. Keep
|
||||
// its pending probe reserved, but never poll or power it while blocked.
|
||||
if (!gps_discovery_pending || gps_serial_transport_blocked) return;
|
||||
#ifdef ENV_SKIP_GPS_DETECT
|
||||
const bool found = true;
|
||||
#else
|
||||
const bool found = Serial1.available() > 0;
|
||||
#endif
|
||||
// Unsigned elapsed time keeps the same five-second cold-start window even
|
||||
// when the millisecond clock wraps. No startup or command loop waits here.
|
||||
if (!found && static_cast<uint32_t>(millis() - gps_discovery_started_at) < 5000UL) return;
|
||||
finishBasicGpsDiscovery(found);
|
||||
}
|
||||
#endif
|
||||
|
||||
void EnvironmentSensorManager::initBasicGPS() {
|
||||
|
||||
// A repeated discovery pass must not steal a UART which an active bridge
|
||||
@@ -1184,6 +1238,10 @@ void EnvironmentSensorManager::initBasicGPS() {
|
||||
resetGpsTelemetryTransportState();
|
||||
gps_serial_transport = false;
|
||||
gps_serial_transport_blocked = false;
|
||||
#if defined(ESP32_PLATFORM) && !defined(RAK_WISBLOCK_GPS)
|
||||
gps_detected = false;
|
||||
gps_active = false;
|
||||
#endif
|
||||
|
||||
Serial1.setPins(PIN_GPS_TX, PIN_GPS_RX);
|
||||
|
||||
@@ -1201,6 +1259,15 @@ void EnvironmentSensorManager::initBasicGPS() {
|
||||
MESH_DEBUG_PRINTLN("No GPS wake/reset pin found for this board. Continuing on...");
|
||||
#endif
|
||||
|
||||
#if defined(ESP32_PLATFORM) && !defined(RAK_WISBLOCK_GPS)
|
||||
// Reserve the configured UART immediately, including while presence is
|
||||
// still unknown. Continue discovery from loop() so an absent optional GPS
|
||||
// cannot consume the website's entire five-second USB bootstrap deadline.
|
||||
gps_serial_transport = true;
|
||||
gps_discovery_pending = true;
|
||||
gps_discovery_started_at = millis();
|
||||
serviceBasicGpsDiscovery();
|
||||
#else
|
||||
// Cold-start receivers can take several seconds to emit their first NMEA
|
||||
// data. Check immediately, then wait at most five seconds; an already awake
|
||||
// receiver and the explicit bypass need no extra startup delay.
|
||||
@@ -1227,6 +1294,7 @@ void EnvironmentSensorManager::initBasicGPS() {
|
||||
}
|
||||
_location->stop();
|
||||
gps_active = false; //Set GPS visibility off until setting is changed
|
||||
#endif
|
||||
}
|
||||
|
||||
// gps code for rak might be moved to MicroNMEALoactionProvider
|
||||
@@ -1472,7 +1540,12 @@ void EnvironmentSensorManager::stop_gps() {
|
||||
|
||||
bool EnvironmentSensorManager::gpsUsesSerialUart(uint8_t uart) const {
|
||||
#if ENV_INCLUDE_GPS
|
||||
#if defined(ESP32_PLATFORM) && !defined(RAK_WISBLOCK_GPS)
|
||||
// The pending probe owns this UART before presence has been established.
|
||||
return uart == 1 && gps_serial_transport;
|
||||
#else
|
||||
return uart == 1 && gps_detected && gps_serial_transport;
|
||||
#endif
|
||||
#else
|
||||
(void)uart;
|
||||
return false;
|
||||
@@ -1530,10 +1603,21 @@ bool EnvironmentSensorManager::setGpsSerialTransportBlocked(uint8_t uart,
|
||||
// Cancel remote-query acquisition/hold state before releasing the UART.
|
||||
setGpsTelemetryTransportAvailable(false);
|
||||
gps_serial_transport_blocked = true;
|
||||
#if defined(ESP32_PLATFORM) && !defined(RAK_WISBLOCK_GPS)
|
||||
if (gps_discovery_pending) _location->stop();
|
||||
#endif
|
||||
if (gps_active) stop_gps();
|
||||
Serial1.end();
|
||||
} else {
|
||||
gps_serial_transport_blocked = false;
|
||||
#if defined(ESP32_PLATFORM) && !defined(RAK_WISBLOCK_GPS)
|
||||
if (gps_discovery_pending) {
|
||||
// The bridge's time cannot count as a GPS absence timeout. Reclaim only
|
||||
// after release and give the newly awakened receiver its full window.
|
||||
initBasicGPS();
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
// Restoring availability restarts the receiver only when the user's GPS
|
||||
// preference is on. Otherwise a future authorized location query may
|
||||
// acquire it on demand.
|
||||
@@ -1553,6 +1637,9 @@ void EnvironmentSensorManager::loop() {
|
||||
#if ENV_INCLUDE_GPS
|
||||
static unsigned long next_gps_update = 0;
|
||||
unsigned long now = millis();
|
||||
#if defined(ESP32_PLATFORM) && !defined(RAK_WISBLOCK_GPS)
|
||||
serviceBasicGpsDiscovery();
|
||||
#endif
|
||||
loopGpsTelemetry(now);
|
||||
|
||||
if (powersaving_enabled && gps_detected && _location->getGPSPowerSaving()) {
|
||||
|
||||
@@ -26,6 +26,13 @@ protected:
|
||||
bool gps_serial_transport_blocked = false;
|
||||
|
||||
#if ENV_INCLUDE_GPS
|
||||
#if defined(ESP32_PLATFORM) && !defined(RAK_WISBLOCK_GPS)
|
||||
bool gps_discovery_pending = false;
|
||||
bool gps_discovery_preference_known = false;
|
||||
uint32_t gps_discovery_started_at = 0;
|
||||
void finishBasicGpsDiscovery(bool found);
|
||||
void serviceBasicGpsDiscovery();
|
||||
#endif
|
||||
LocationProvider* _location;
|
||||
LocationProvider* _configured_location;
|
||||
void armGpsPowerSavingCycle();
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute cooperative ESP32 GPS discovery with real preference/UART policy.
|
||||
|
||||
Only the UART, clock and GPS hardware are peripheral doubles. Discovery,
|
||||
completion, loop servicing, settings, power transitions and UART handoff come
|
||||
from EnvironmentSensorManager.cpp; SensorManager.cpp and LocationProvider's
|
||||
power/timing policy compile unchanged. This does not qualify physical GPS/USB.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_gps_upstream_adaptations import ARDUINO, CAYENNE, MESH
|
||||
from test_replay_reset_integration import extract_braced
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE = ROOT / "src/helpers/sensors/EnvironmentSensorManager.cpp"
|
||||
HEADER = ROOT / "src/helpers/sensors/EnvironmentSensorManager.h"
|
||||
|
||||
HARNESS = r'''
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <helpers/SensorManager.h>
|
||||
#define PIN_GPS_TX 39
|
||||
#define PIN_GPS_RX 38
|
||||
#define PIN_GPS_EN 34
|
||||
#define MESH_DEBUG_PRINTLN(...) ((void)0)
|
||||
#define POWERSAVING_DEBUG_PRINTLN(...) ((void)0)
|
||||
void LocationProvider::sendSentence(const char*) {}
|
||||
|
||||
struct SerialMock {
|
||||
unsigned begins=0, ends=0, polls=0, baud=0;
|
||||
int first_pin=-1, second_pin=-1, bytes=0;
|
||||
bool installed=false;
|
||||
void setPins(int first, int second) { first_pin=first; second_pin=second; }
|
||||
void begin(unsigned speed) { installed=true; baud=speed; ++begins; }
|
||||
void end() { installed=false; ++ends; }
|
||||
int available() { ++polls; return bytes; }
|
||||
} Serial1;
|
||||
|
||||
struct TestLocation : LocationProvider {
|
||||
unsigned begins=0, resets=0, stops=0, loops=0;
|
||||
int enable_pin=PIN_GPS_EN;
|
||||
bool powered=false;
|
||||
void begin() override { powered=true; ++begins; }
|
||||
void reset() override { ++resets; }
|
||||
void stop() override { powered=false; ++stops; }
|
||||
void loop() override { ++loops; }
|
||||
int getPinEn() override { return enable_pin; }
|
||||
bool isEnabled() override { return powered; }
|
||||
bool isValid() override { return false; }
|
||||
long getLatitude() override { return 0; }
|
||||
long getLongitude() override { return 0; }
|
||||
long getAltitude() override { return 0; }
|
||||
long satellitesCount() override { return 0; }
|
||||
long getTimestamp() override { return 0; }
|
||||
};
|
||||
|
||||
struct EnvironmentSensorManager : SensorManager {
|
||||
@FIELDS@
|
||||
TestLocation* _location;
|
||||
explicit EnvironmentSensorManager(TestLocation& location) : _location(&location) {}
|
||||
bool telemetryGpsDetected() const override { return gps_detected; }
|
||||
bool telemetryGpsActive() const override { return gps_active; }
|
||||
void telemetryGpsStart() override { start_gps(); }
|
||||
void telemetryGpsStop() override { stop_gps(); }
|
||||
LocationProvider* getLocationProvider() override { return _location; }
|
||||
bool userEnabled() const { return isGpsTelemetryUserEnabled(); }
|
||||
bool transportAvailable() const { return isGpsTelemetryTransportAvailable(); }
|
||||
void initBasicGPS();
|
||||
void finishBasicGpsDiscovery(bool found);
|
||||
void serviceBasicGpsDiscovery();
|
||||
void armGpsPowerSavingCycle();
|
||||
void start_gps();
|
||||
void stop_gps();
|
||||
void loop() override;
|
||||
bool setSettingValue(const char* name, const char* value) override;
|
||||
void setPowerSavingEnabled(bool enabled) override;
|
||||
bool gpsUsesSerialUart(uint8_t uart) const override;
|
||||
bool gpsSerialTransportMayConflict(uint8_t uart) const override;
|
||||
bool gpsSerialTransportCanYield(uint8_t uart) const override;
|
||||
bool setGpsSerialTransportBlocked(uint8_t uart, bool blocked) override;
|
||||
};
|
||||
@METHODS@
|
||||
|
||||
static void fresh(uint32_t now=100) {
|
||||
now_ms=now; delay_count=0; Serial1=SerialMock{};
|
||||
}
|
||||
static void requireImmediateInit(EnvironmentSensorManager& sensors) {
|
||||
const uint32_t before=millis();
|
||||
sensors.initBasicGPS();
|
||||
assert(millis()==before && delay_count==0);
|
||||
assert(Serial1.installed && Serial1.first_pin==PIN_GPS_TX && Serial1.second_pin==PIN_GPS_RX);
|
||||
#ifdef GPS_BAUD_RATE
|
||||
assert(Serial1.baud==GPS_BAUD_RATE);
|
||||
#else
|
||||
assert(Serial1.baud==9600);
|
||||
#endif
|
||||
}
|
||||
|
||||
static void absent() {
|
||||
fresh(); TestLocation location; EnvironmentSensorManager sensors(location);
|
||||
requireImmediateInit(sensors);
|
||||
assert(sensors.gps_discovery_pending && !sensors.gps_detected && !sensors.gps_active);
|
||||
assert(location.powered && location.begins==1 && location.resets==1 && location.stops==0);
|
||||
assert(sensors.gpsUsesSerialUart(1) && sensors.gpsSerialTransportMayConflict(1));
|
||||
assert(!sensors.gpsUsesSerialUart(0) && !sensors.gpsUsesSerialUart(2));
|
||||
assert(!sensors.setGpsSerialTransportBlocked(2, true));
|
||||
now_ms=5099; sensors.loop();
|
||||
assert(sensors.gps_discovery_pending && location.powered && delay_count==0);
|
||||
now_ms=5100; sensors.loop();
|
||||
assert(!sensors.gps_discovery_pending && !sensors.gps_detected && !sensors.gps_active);
|
||||
assert(!location.powered && location.stops==1 && !sensors.gpsUsesSerialUart(1));
|
||||
const unsigned polls=Serial1.polls;
|
||||
Serial1.bytes=8; now_ms=6100; sensors.loop();
|
||||
assert(Serial1.polls==polls && !sensors.gps_detected && location.stops==1);
|
||||
assert(delay_count==0);
|
||||
}
|
||||
|
||||
static void latePreferences() {
|
||||
fresh(); TestLocation location; EnvironmentSensorManager sensors(location);
|
||||
requireImmediateInit(sensors);
|
||||
// Common preferences load after begin() while the optional receiver is cold.
|
||||
assert(sensors.setSettingValue("gps", "1"));
|
||||
sensors.setPowerSavingEnabled(true);
|
||||
assert(sensors.gps_discovery_preference_known && sensors.userEnabled());
|
||||
assert(!sensors.gps_active && location.begins==1);
|
||||
now_ms=1100; Serial1.bytes=8; sensors.loop();
|
||||
assert(!sensors.gps_discovery_pending && sensors.gps_detected && sensors.gps_active);
|
||||
assert(sensors.userEnabled() && location.powered && location.getGPSPowerSaving());
|
||||
assert(location.waitingTimeSync() && location.getNextGPSOff()>millis());
|
||||
assert(location.loops==1 && delay_count==0);
|
||||
#ifdef PERSISTANT_GPS
|
||||
assert(location.stops==0 && location.begins==1 && location.resets==1);
|
||||
#else
|
||||
assert(location.stops==1 && location.begins==2 && location.resets==2);
|
||||
#endif
|
||||
sensors.setPowerSavingEnabled(false);
|
||||
assert(sensors.gps_active && !location.getGPSPowerSaving());
|
||||
assert(location.getNextGPSOff()==0 && location.getNextGPSOn()==0);
|
||||
assert(sensors.setSettingValue("gps", "0"));
|
||||
assert(!sensors.userEnabled() && !sensors.gps_active && !location.powered);
|
||||
assert(sensors.setSettingValue("gps", "1"));
|
||||
assert(sensors.userEnabled() && sensors.gps_active && location.powered);
|
||||
}
|
||||
|
||||
static void latestPreferenceWins() {
|
||||
fresh(); TestLocation location; EnvironmentSensorManager sensors(location);
|
||||
requireImmediateInit(sensors);
|
||||
assert(sensors.setSettingValue("gps", "1"));
|
||||
sensors.setPowerSavingEnabled(true);
|
||||
assert(sensors.setSettingValue("gps", "0"));
|
||||
now_ms=900; Serial1.bytes=8; sensors.loop();
|
||||
assert(sensors.gps_detected && !sensors.gps_discovery_pending);
|
||||
assert(sensors.gps_discovery_preference_known && !sensors.userEnabled());
|
||||
assert(!sensors.gps_active && !location.powered && !location.getGPSPowerSaving());
|
||||
// Reprobe preserves an explicit user choice; the current node power policy
|
||||
// also wins over the policy in force when discovery originally started.
|
||||
Serial1.bytes=0; sensors.initBasicGPS();
|
||||
assert(sensors.gps_discovery_pending);
|
||||
assert(sensors.setSettingValue("gps", "1"));
|
||||
sensors.setPowerSavingEnabled(false);
|
||||
now_ms=1200; Serial1.bytes=8; sensors.loop();
|
||||
assert(sensors.gps_active && sensors.userEnabled() && location.powered);
|
||||
assert(!location.getGPSPowerSaving() && location.getNextGPSOff()==0);
|
||||
}
|
||||
|
||||
static void awakeOrBypass() {
|
||||
fresh(); TestLocation location; EnvironmentSensorManager sensors(location);
|
||||
#ifndef ENV_SKIP_GPS_DETECT
|
||||
Serial1.bytes=8;
|
||||
#endif
|
||||
requireImmediateInit(sensors);
|
||||
assert(sensors.gps_detected && !sensors.gps_discovery_pending && sensors.gpsUsesSerialUart(1));
|
||||
assert(location.begins==1 && location.resets==1);
|
||||
#ifdef ENV_SKIP_GPS_DETECT
|
||||
assert(Serial1.polls==0);
|
||||
#else
|
||||
assert(Serial1.polls==1);
|
||||
#endif
|
||||
#ifdef PERSISTANT_GPS
|
||||
assert(sensors.gps_active && sensors.userEnabled() && location.powered && location.stops==0);
|
||||
#else
|
||||
assert(!sensors.gps_active && !sensors.userEnabled() && !location.powered && location.stops==1);
|
||||
#endif
|
||||
assert(sensors.setSettingValue("gps", "0"));
|
||||
assert(!sensors.gps_active && !sensors.userEnabled() && !location.powered);
|
||||
}
|
||||
|
||||
static void wrap() {
|
||||
fresh(UINT32_MAX-1000U); TestLocation location; EnvironmentSensorManager sensors(location);
|
||||
const uint32_t start=millis(); requireImmediateInit(sensors);
|
||||
now_ms=start+4999U; sensors.loop();
|
||||
assert(sensors.gps_discovery_pending && location.powered);
|
||||
now_ms=start+5000U; sensors.loop();
|
||||
assert(!sensors.gps_discovery_pending && !sensors.gps_detected && !location.powered);
|
||||
assert(delay_count==0);
|
||||
}
|
||||
|
||||
static void blockedRelease() {
|
||||
fresh(); TestLocation location; EnvironmentSensorManager sensors(location);
|
||||
requireImmediateInit(sensors);
|
||||
assert(sensors.setSettingValue("gps", "1"));
|
||||
sensors.setPowerSavingEnabled(true);
|
||||
assert(sensors.gpsSerialTransportCanYield(1));
|
||||
assert(sensors.setGpsSerialTransportBlocked(1, true));
|
||||
assert(sensors.gps_discovery_pending && sensors.gps_serial_transport_blocked);
|
||||
assert(sensors.gpsUsesSerialUart(1) && !sensors.transportAvailable());
|
||||
assert(!location.powered && location.stops==1 && Serial1.ends==1 && !Serial1.installed);
|
||||
const unsigned polls=Serial1.polls, begins=Serial1.begins, starts=location.begins;
|
||||
// These are bytes belonging to the bridge, not evidence of a late GPS.
|
||||
now_ms=20100; Serial1.bytes=8; sensors.loop();
|
||||
assert(Serial1.polls==polls && sensors.gps_discovery_pending && !sensors.gps_detected);
|
||||
sensors.initBasicGPS(); sensors.start_gps();
|
||||
assert(Serial1.begins==begins && location.begins==starts && !location.powered);
|
||||
assert(sensors.setSettingValue("gps", "0"));
|
||||
assert(sensors.setSettingValue("gps", "1"));
|
||||
assert(sensors.userEnabled() && !sensors.gps_active && !location.powered);
|
||||
Serial1.bytes=0;
|
||||
assert(sensors.setGpsSerialTransportBlocked(1, false));
|
||||
assert(sensors.gps_discovery_pending && !sensors.gps_serial_transport_blocked);
|
||||
assert(sensors.gps_discovery_started_at==20100 && sensors.transportAvailable());
|
||||
assert(Serial1.begins==begins+1 && location.begins==starts+1 && location.powered);
|
||||
now_ms=25099; sensors.loop();
|
||||
assert(sensors.gps_discovery_pending); // Bridge time cannot consume this window.
|
||||
now_ms=25100; Serial1.bytes=8; sensors.loop();
|
||||
assert(sensors.gps_detected && !sensors.gps_discovery_pending && sensors.gps_active);
|
||||
assert(sensors.userEnabled() && location.powered && location.getGPSPowerSaving());
|
||||
assert(delay_count==0);
|
||||
}
|
||||
|
||||
static void noEnablePin() {
|
||||
fresh(); TestLocation location; location.enable_pin=-1;
|
||||
EnvironmentSensorManager sensors(location); requireImmediateInit(sensors);
|
||||
assert(sensors.gpsUsesSerialUart(1) && !sensors.gpsSerialTransportCanYield(1));
|
||||
assert(!sensors.setGpsSerialTransportBlocked(1, true));
|
||||
assert(sensors.gps_discovery_pending && !sensors.gps_serial_transport_blocked);
|
||||
assert(location.powered && Serial1.installed && location.stops==0 && Serial1.ends==0);
|
||||
now_ms=5100; sensors.loop();
|
||||
assert(!sensors.gps_discovery_pending && !sensors.gps_detected && !location.powered);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
assert(argc==2);
|
||||
if (!strcmp(argv[1], "absent")) absent();
|
||||
else if (!strcmp(argv[1], "late")) latePreferences();
|
||||
else if (!strcmp(argv[1], "latest")) latestPreferenceWins();
|
||||
else if (!strcmp(argv[1], "awake")) awakeOrBypass();
|
||||
else if (!strcmp(argv[1], "wrap")) wrap();
|
||||
else if (!strcmp(argv[1], "blocked")) blockedRelease();
|
||||
else if (!strcmp(argv[1], "no_enable")) noEnablePin();
|
||||
else assert(false && "unknown scenario");
|
||||
std::printf("PASS %s\n", argv[1]);
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
def harness(*, negative=None):
|
||||
source = SOURCE.read_text(encoding="utf-8")
|
||||
header = HEADER.read_text(encoding="utf-8")
|
||||
names = ("gps_detected", "gps_active", "gps_serial_transport",
|
||||
"gps_serial_transport_blocked", "gps_discovery_pending",
|
||||
"gps_discovery_preference_known", "gps_discovery_started_at")
|
||||
fields = "\n".join(re.search(r"^\s*(?:bool|uint32_t)\s+" + name + r"\s*=[^;]*;",
|
||||
header, re.MULTILINE)[0] for name in names)
|
||||
signatures = (
|
||||
"void EnvironmentSensorManager::initBasicGPS()",
|
||||
"void EnvironmentSensorManager::finishBasicGpsDiscovery(bool found)",
|
||||
"void EnvironmentSensorManager::serviceBasicGpsDiscovery()",
|
||||
"void EnvironmentSensorManager::armGpsPowerSavingCycle()",
|
||||
"void EnvironmentSensorManager::start_gps()",
|
||||
"void EnvironmentSensorManager::stop_gps()",
|
||||
"bool EnvironmentSensorManager::setSettingValue(",
|
||||
"void EnvironmentSensorManager::setPowerSavingEnabled(",
|
||||
"bool EnvironmentSensorManager::gpsUsesSerialUart(",
|
||||
"bool EnvironmentSensorManager::gpsSerialTransportMayConflict(",
|
||||
"bool EnvironmentSensorManager::gpsSerialTransportCanYield(",
|
||||
"bool EnvironmentSensorManager::setGpsSerialTransportBlocked(",
|
||||
"void EnvironmentSensorManager::loop()",
|
||||
)
|
||||
methods = "\n".join(extract_braced(source, signature) for signature in signatures)
|
||||
mutations = {
|
||||
"blocking": (" gps_discovery_started_at = millis();",
|
||||
" gps_discovery_started_at = millis();\n delay(5000);"),
|
||||
"blocked_poll": ("if (!gps_discovery_pending || gps_serial_transport_blocked) return;",
|
||||
"if (!gps_discovery_pending) return;"),
|
||||
"cancellation": (" if (gps_discovery_pending) _location->stop();",
|
||||
" // Negative control: leave the pending receiver powered."),
|
||||
"preference_loss": (" gps_discovery_preference_known = true;",
|
||||
" // Negative control: forget that an explicit preference was loaded."),
|
||||
}
|
||||
if negative:
|
||||
before, after = mutations[negative]
|
||||
assert methods.count(before) == 1, "negative-control anchor changed"
|
||||
methods = methods.replace(before, after, 1)
|
||||
return HARNESS.replace("@FIELDS@", fields).replace("@METHODS@", methods)
|
||||
|
||||
|
||||
class Esp32GpsDiscoveryTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.compiler = shutil.which("g++") or shutil.which("clang++")
|
||||
if cls.compiler is None:
|
||||
raise unittest.SkipTest("a host C++17 compiler is required")
|
||||
|
||||
def compile_run(self, cases, *, defines=(), negative=None):
|
||||
with tempfile.TemporaryDirectory(prefix="meshcore-esp32-gps-") as directory:
|
||||
work = Path(directory)
|
||||
for name, text in {
|
||||
"Arduino.h": ARDUINO, "Mesh.h": MESH, "CayenneLPP.h": CAYENNE,
|
||||
"Wire.h": "#pragma once\nclass TwoWire {};\n",
|
||||
}.items():
|
||||
(work / name).write_text(text, encoding="utf-8")
|
||||
cpp, binary = work / "discovery.cpp", work / "discovery"
|
||||
cpp.write_text(harness(negative=negative), encoding="utf-8")
|
||||
command = [self.compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
||||
"-Wno-unused-parameter", "-DENV_INCLUDE_GPS=1", "-DESP32_PLATFORM=1",
|
||||
*["-D" + define for define in defines],
|
||||
*(["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
|
||||
"-fno-pie", "-no-pie"] if sys.platform.startswith("linux") else []),
|
||||
"-I" + str(work), "-I" + str(ROOT / "src"), str(cpp),
|
||||
str(ROOT / "src/helpers/SensorManager.cpp"), "-o", str(binary)]
|
||||
compiled = subprocess.run(command, capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr)
|
||||
for case in cases:
|
||||
with self.subTest(case=case, defines=defines, negative=negative):
|
||||
result = subprocess.run([str(binary), case], capture_output=True,
|
||||
text=True, timeout=10)
|
||||
if negative:
|
||||
self.assertNotEqual(result.returncode, 0, "negative control escaped assertions")
|
||||
self.assertIn("Assertion", result.stderr)
|
||||
else:
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn("PASS " + case, result.stdout)
|
||||
|
||||
def test_boot_is_cooperative_and_loop_finishes_cold_gps(self):
|
||||
self.compile_run(("absent", "late", "latest", "awake", "wrap", "blocked", "no_enable"),
|
||||
defines=("GPS_BAUD_RATE=38400",))
|
||||
|
||||
def test_persistent_default_cannot_replace_loaded_user_preference(self):
|
||||
self.compile_run(("absent", "late", "latest", "awake", "wrap", "blocked", "no_enable"),
|
||||
defines=("PERSISTANT_GPS=1",))
|
||||
|
||||
def test_explicit_detection_bypass_is_immediate(self):
|
||||
for defines in (("ENV_SKIP_GPS_DETECT=1",),
|
||||
("ENV_SKIP_GPS_DETECT=1", "PERSISTANT_GPS=1")):
|
||||
self.compile_run(("awake",), defines=defines)
|
||||
|
||||
def test_boot_blocking_negative_control(self):
|
||||
self.compile_run(("absent",), negative="blocking")
|
||||
|
||||
def test_bridge_bytes_cannot_finish_pending_discovery_negative_control(self):
|
||||
self.compile_run(("blocked",), negative="blocked_poll")
|
||||
|
||||
def test_uart_handoff_must_stop_pending_receiver_negative_control(self):
|
||||
self.compile_run(("blocked",), negative="cancellation")
|
||||
|
||||
def test_loaded_preference_must_be_retained_negative_control(self):
|
||||
self.compile_run(("latest",), defines=("PERSISTANT_GPS=1",), negative="preference_loss")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
Reference in new issue
Block a user