Files
MeshCore/test/test_cardputer_gps.py
T

219 lines
12 KiB
Python

"""Execute Cardputer GPS policy with real SensorManager and NMEA provider code."""
from pathlib import Path
import os
import shutil
import subprocess
import tempfile
import unittest
from test_gps_upstream_adaptations import ARDUINO, MESH, RTC, CAYENNE
ROOT = Path(__file__).resolve().parents[1]
VARIANT = ROOT / "variants/m5stack_cardputer_adv"
ARDUINO = ARDUINO.replace("#include <deque>", "#include <deque>\n#include <string>\n#include <cstdlib>\n#include <cctype>")
ARDUINO = ARDUINO.replace(" virtual size_t write(uint8_t){return 1;}", r'''
virtual size_t write(uint8_t){return 1;}
size_t print(const char* text){size_t n=0;while(*text)n+=write(uint8_t(*text++));return n;}
size_t print(char c){return write(uint8_t(c));}
''') + r'''
class HardwareSerial:public Stream {
public:
std::deque<char> incoming;
std::string output;
int rx=-1,tx=-1;unsigned baud=0,starts=0,ends=0;
void setPins(int input,int out){rx=input;tx=out;}
void begin(unsigned rate){baud=rate;++starts;}
void end(){++ends;}
int available()override{return int(incoming.size());}
int read()override{if(incoming.empty())return -1;char c=incoming.front();incoming.pop_front();return uint8_t(c);}
size_t write(uint8_t c)override{output+=char(c);return 1;}
void feed(const std::string& data){for(char c:data)incoming.push_back(c);}
};
inline HardwareSerial Serial1;
'''
HARNESS = r'''
#include <cassert>
#include <cstring>
#include <limits>
#include <string>
#include <MicroNMEA.h>
#include "CardputerSensorManager.h"
#include <helpers/sensors/MicroNMEALocationProvider.h>
void LocationProvider::sendSentence(const char*){}
struct Provider:LocationProvider {
bool fix=false;long lat=47610000,lon=-122330000,alt=125000;
unsigned polls=0,starts=0,stops=0,clears=0;
long getLatitude()override{return lat;}
long getLongitude()override{return lon;}
long getAltitude()override{return alt;}
long satellitesCount()override{return fix?8:0;}
bool isValid()override{return fix&&lat>=-90000000&&lat<=90000000&&lon>=-180000000&&lon<=180000000;}
long getTimestamp()override{return 0;}
void reset()override{}
void begin()override{++starts;}
void stop()override{++stops;}
void loop()override{++polls;}
// This is what the real pinless MicroNMEA provider reports even in standby.
bool isEnabled()override{return true;}
bool clearPositionFix()override{++clears;fix=false;return true;}
void sendSentence(const char* sentence)override{MicroNMEA::sendSentence(Serial1,sentence);}
void synced(){markTimeSyncApplied();stopTimeSync();}
};
static unsigned occurrences(const std::string& haystack,const char* needle) {
unsigned count=0;size_t at=0;
while((at=haystack.find(needle,at))!=std::string::npos){++count;at+=std::strlen(needle);}
return count;
}
static void reset(uint32_t time=100) {now_ms=time;delay_count=0;Serial1=HardwareSerial();}
static void setup(CardputerSensorManager& manager,Provider& provider) {
provider.setPowerSavingProfile(600,1800);assert(manager.begin());
assert(Serial1.rx==15&&Serial1.tx==13&&Serial1.baud==115200);
assert(manager.isGPSDetected()&&manager.gpsUsesSerialUart(1)&&!manager.gpsUsesSerialUart(2));
assert(!manager.isGpsPowered()&&provider.isEnabled());
assert(Serial1.output=="$PCAS12,65535*1E\r\n");
assert(!std::strcmp(manager.getSettingValue(0),"0"));
assert(manager.begin()&&Serial1.starts==1&&delay_count==0);
}
static void bootAndManualOff() {
reset();Serial1.feed("startup bytes");Provider provider;CardputerSensorManager manager(provider);
setup(manager,provider);manager.setPowerSavingEnabled(true);
for(unsigned tick=1;tick<=20;++tick) {
now_ms=100+tick*3600000U;provider.syncTime();manager.loop();
assert(!manager.isGpsPowered()&&provider.polls==0);
}
assert(occurrences(Serial1.output,"$PCAS10")==0);
assert(occurrences(Serial1.output,"$PCAS12")>=20&&delay_count==0);
}
static void cycleAndWrap() {
for(uint32_t start:{100U,UINT32_MAX-599000U,UINT32_MAX-599999U}) {
reset(start);Provider p;CardputerSensorManager m(p);setup(m,p);
m.setPowerSavingEnabled(true);assert(m.setSettingValue("gps","1"));
uint32_t off=p.getNextGPSOff();assert(off==(uint32_t(start+600000U)?uint32_t(start+600000U):1U));
assert(m.isGpsPowered()&&p.waitingTimeSync()&&!std::strcmp(m.getSettingValue(0),"1"));
now_ms=off-1;m.loop();assert(m.isGpsPowered());
now_ms=off;m.loop();assert(!m.isGpsPowered());
assert(!std::strcmp(m.getSettingValue(0),"1"));
uint32_t on=p.getNextGPSOn();assert(on==uint32_t(off+1800000U));
now_ms=on-1;m.loop();assert(!m.isGpsPowered());
now_ms=on;m.loop();assert(m.isGpsPowered()&&p.clears==2);
assert(uint32_t(p.getNextGPSOff())==uint32_t(now_ms+600000U));
assert(delay_count==0&&occurrences(Serial1.output,"$PCAS10,0*1C\r\n")==2);
p.synced();m.loop();assert(!m.isGpsPowered()); // common early sleep after clock acquisition
}
}
static void toggles() {
reset();Provider p;CardputerSensorManager m(p);setup(m,p);
assert(m.setSettingValue("gps","1")&&m.isGpsPowered());
m.setPowerSavingEnabled(true);const auto off=p.getNextGPSOff();
++now_ms;m.setPowerSavingEnabled(true);assert(p.getNextGPSOff()==off);
now_ms=uint32_t(off);m.loop();assert(!m.isGpsPowered());
m.setPowerSavingEnabled(false);assert(m.isGpsPowered()&&!p.getGPSPowerSaving());
assert(p.getNextGPSOn()==0&&p.getNextGPSOff()==0);
m.setPowerSavingEnabled(true);assert(p.getNextGPSOff()!=0);
assert(m.setSettingValue("gps","0")&&!m.isGpsPowered());
assert(!p.getGPSPowerSaving()&&!p.waitingTimeSync()&&p.getNextGPSOn()==0&&p.getNextGPSOff()==0);
for(bool save:{false,true,false,true}) {m.setPowerSavingEnabled(save);m.loop();assert(!m.isGpsPowered());}
assert(m.setSettingValue("gps","1")&&m.isGpsPowered());
m.shutdownGpsTransportForDeepSleep();assert(!m.isGpsPowered()&&!m.isGPSDetected()&&Serial1.ends==1);
m.loop();assert(!m.isGpsPowered());
}
static void intervalAndValidity() {
reset();Provider p;CardputerSensorManager m(p);setup(m,p);
assert(m.setSettingValue("gps","1"));assert(m.setSettingValue("gps_interval","2"));
p.fix=true;m.loop();assert(m.node_lat==47.61&&m.node_lon==-122.33&&m.node_altitude==125);
p.lat=48000000;now_ms+=1999;m.loop();assert(m.node_lat==47.61);
++now_ms;m.loop();assert(m.node_lat==48);
p.lat=91000000;now_ms+=2000;m.loop();assert(m.node_lat==48);
assert(m.setSettingValue("gps_interval","86400"));p.lat=49000000;m.loop();assert(m.node_lat==49);
assert(m.setSettingValue("gps_interval","1"));p.lat=50000000;m.loop();assert(m.node_lat==50);
for(const char* bad:{"","-1","86401","1x"})assert(!m.setSettingValue("gps_interval",bad));
assert(!m.setSettingValue(nullptr,"1")&&!m.setSettingValue("gps",nullptr));
assert(!m.setSettingValue("unknown","1"));
}
static void telemetry() {
reset();Provider p;CardputerSensorManager m(p);setup(m,p);CayenneLPP data(64);
assert(m.querySensors(TELEM_PERM_BASE,data)&&data.gps_count==0&&!m.isGpsPowered());
const auto commands=Serial1.output;double lat=0,lon=0;
m.setTelemetryLocationAccessAvailable(false);m.loop();assert(Serial1.output==commands);
assert(!m.getCachedGpsPosition(lat,lon));
assert(m.querySensors(TELEM_PERM_LOCATION,data)&&data.gps_count==0&&m.isGpsPowered());
assert(!std::strcmp(m.getSettingValue(0),"0")); // telemetry did not change saved intent
p.fix=true;m.loop();now_ms+=31000;m.loop();
assert(m.getCachedGpsPosition(lat,lon));
assert(m.querySensors(TELEM_PERM_LOCATION,data)&&data.gps_count==1);
assert(m.setSettingValue("gps","0")&&!m.isGpsPowered());
const auto polls=p.polls;now_ms+=1800000;m.loop();assert(!m.isGpsPowered()&&p.polls==polls);
data.reset();const auto before=Serial1.output;
assert(m.querySensors(TELEM_PERM_BASE,data)&&data.gps_count==1&&Serial1.output==before);
assert(m.querySensors(TELEM_PERM_LOCATION,data)&&data.gps_count==2); // cached, authorized, explicit hold
assert(m.isGpsPowered());m.setSettingValue("gps","0");
}
static std::string sentence(const char* text) {
HardwareSerial output;MicroNMEA::sendSentence(output,text);return output.output;
}
static void actualNmeaAndClock() {
reset(10000);mesh::RTCClock clock;MicroNMEALocationProvider p(Serial1,&clock);
CardputerSensorManager m(p);p.setPowerSavingProfile(600,1800);assert(m.begin());
assert(m.setSettingValue("gps","1")&&p.waitingTimeSync());
for(unsigned second=0;second<4;++second) {
char rmc[160];std::snprintf(rmc,sizeof(rmc),
"$GPRMC,1200%02u.00,A,4736.600,N,12219.800,W,0.0,0.0,281026,,,A",second);
Serial1.feed(sentence("$GPGGA,120000.00,4736.600,N,12219.800,W,1,08,0.9,125.0,M,0.0,M,,"));
Serial1.feed(sentence(rmc));now_ms+=1100;
// A real foreground loop drains its bounded 96-byte NMEA batches often.
// Keep one timestamp per emitted second and never bypass that byte limit.
for(unsigned poll=0;poll<4&&Serial1.available();++poll)m.loop();
assert(Serial1.available()==0);
}
assert(p.satellitesCount()==8&&p.isValid());
assert(m.node_lat>47.60&&m.node_lat<47.62&&m.node_lon< -122.32&&m.node_lon> -122.34);
assert(p.getTimeSyncGeneration()>0&&p.getLastValidTimeSync()==clock.getCurrentTime());
const double retained=m.node_lat;
Serial1.feed(sentence("$GPGGA,120005.00,,,,,0,00,99.9,,,,,,"));now_ms+=1100;m.loop();
assert(!p.isValid()&&p.satellitesCount()==0&&m.node_lat==retained);
m.setSettingValue("gps","0");const auto generation=p.getTimeSyncGeneration();
Serial1.feed(sentence("$GPRMC,120010.00,A,4736.600,N,12219.800,W,0.0,0.0,281026,,,A"));
now_ms+=1100;m.loop();assert(p.getTimeSyncGeneration()==generation&&!m.isGpsPowered());
}
int main(int argc,char** argv) {
assert(argc==2);const std::string scenario=argv[1];
if(scenario=="boot")bootAndManualOff();else if(scenario=="cycle")cycleAndWrap();
else if(scenario=="toggles")toggles();else if(scenario=="interval")intervalAndValidity();
else if(scenario=="telemetry")telemetry();else if(scenario=="nmea")actualNmeaAndClock();else assert(false);
}
'''
class CardputerGpsTests(unittest.TestCase):
def test_actual_manager_policy_cache_nmea_and_clock(self):
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
self.assertIsNotNone(compiler, "a native C++ compiler is required")
nmea = next((ROOT / ".pio/libdeps").glob("*/MicroNMEA/src/MicroNMEA.cpp"), None)
self.assertIsNotNone(nmea, "pinned MicroNMEA installed by native tests is required")
with tempfile.TemporaryDirectory(prefix="cardputer-gps-") as directory:
work = Path(directory)
for name, text in {"Arduino.h": ARDUINO, "Mesh.h": MESH, "RTClib.h": RTC,
"CayenneLPP.h": CAYENNE, "Wire.h": "#pragma once\nstruct TwoWire {};\n",
"main.cpp": HARNESS}.items():
(work / name).write_text(text, encoding="ascii")
binary = work / ("gps.exe" if os.name == "nt" else "gps")
sanitizer = [] if os.name == "nt" else ["-fsanitize=address,undefined",
"-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
built = subprocess.run([compiler, "-std=c++17", "-O1", "-g", "-Wall", "-Wextra", "-Werror",
"-Wno-unused-parameter", *sanitizer, "-DENV_INCLUDE_GPS=1", "-DCARDPUTER_ADV_UI=1",
"-DPIN_GPS_TX=15", "-DPIN_GPS_RX=13", "-DGPS_BAUD_RATE=115200",
"-I", str(work), "-I", str(ROOT / "src"), "-I", str(VARIANT),
"-I", str(nmea.parent), str(work / "main.cpp"), str(nmea),
str(ROOT / "src/helpers/SensorManager.cpp"),
str(VARIANT / "CardputerSensorManager.cpp"), "-o", str(binary)],
capture_output=True, text=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
for scenario in ("boot", "cycle", "toggles", "interval", "telemetry", "nmea"):
with self.subTest(scenario=scenario):
run = subprocess.run([str(binary), scenario], capture_output=True, text=True, timeout=10)
self.assertEqual(run.returncode, 0, run.stdout + run.stderr)
if __name__ == "__main__":
unittest.main()