Fix WisBlock GPS power policy and preserve shared peripherals

This commit is contained in:
mikecarper committed 2026-10-13 09:55:32 -07:00
1 parent af08c02a00
commit a2f51253d8
26 files changed
+1830 -107

No files matched your search

+444
View File
@@ -0,0 +1,444 @@
#include <helpers/sensors/EnvironmentSensorManager.h>
#include <helpers/sensors/I2CAddressClaimPolicy.h>
#include <helpers/sensors/NmeaSentenceProbe.h>
#include <cassert>
#include <cstring>
#include <cstdio>
#include <initializer_list>
#define WB_IO2 21
#define PIN_GPS_TX 15
#define PIN_GPS_RX 16
#define TELEM_RAK12500_ADDRESS 0x42
#define RAK_UART_GPS_PROBE_TIMEOUT_MS 1200UL
#define TELEM_WIRE (&Wire)
#define MESH_DEBUG_PRINTLN(...) ((void)0)
#define POWERSAVING_DEBUG_PRINTLN(...) ((void)0)
#define COM_TYPE_UBX 1
#define SFE_UBLOX_GNSS_ID_GPS 0
#define SFE_UBLOX_GNSS_ID_GALILEO 1
#define SFE_UBLOX_GNSS_ID_GLONASS 2
#define SFE_UBLOX_GNSS_ID_SBAS 3
#define SFE_UBLOX_GNSS_ID_BEIDOU 4
#define SFE_UBLOX_GNSS_ID_IMES 5
#define SFE_UBLOX_GNSS_ID_QZSS 6
#define VAL_CFG_SUBSEC_IOPORT 1
void LocationProvider::sendSentence(const char*) {}
static uint32_t gpsResetPin = static_cast<uint32_t>(-1);
static bool i2cGPSFlag = false, serialGPSFlag = false;
static const char meshcore_capability_rak_wisblock_gps[] = "linked-provider";
struct SerialMock : Stream {
unsigned begins=0, ends=0, polls=0;
bool installed=false;
std::deque<uint8_t> incoming;
void setPins(int tx,int rx) { event("uartPins",tx,rx); }
void begin(unsigned baud) { installed=true; ++begins; event("uartBegin",baud); }
void end() { assert(installed&&"pinned UARTE end must not be called twice"); installed=false; ++ends; event("uartEnd"); }
int available() override { ++polls; return installed ? int(incoming.size()) : 0; }
int read() override { if(incoming.empty()) return -1; int c=incoming.front(); incoming.pop_front(); return c; }
void nmea() {
const char* body="GPRMC,valid";
uint8_t checksum=0;
for(const char* c=body;*c;++c) checksum^=uint8_t(*c);
char sentence[64]; std::snprintf(sentence,sizeof(sentence),"$%s*%02X\r\n",body,checksum);
for(const char* c=sentence;*c;++c) incoming.push_back(uint8_t(*c));
}
} Serial1;
namespace mesh { namespace ota {
struct OtaStoreQspiNrf52 {
inline static uint8_t selected=0;
inline static uint32_t rak_id=0,w25_id=0;
inline static bool header_present=false;
inline static bool cached=false;
inline static unsigned probes=0;
static bool headerW25Detected() { autoDetect(); event("headerProbe"); return header_present; }
static uint8_t autoDetect() { if(!cached) { cached=true; ++probes; event("norProbe"); assert(digitalRead(WB_IO2)==HIGH); } return selected; }
static void autoProbeIds(uint32_t& rak,uint32_t& w25) { autoDetect(); rak=rak_id; w25=w25_id; }
};
}}
static bool bus_available=true;
static bool ensureI2cBusReleased(TwoWire*) { event("i2cBus"); return bus_available; }
struct GnssMock {
bool present=false, begin_ok=true, output_ok=true, rate_ok=true, psm_ok=true, valid=false;
unsigned begins=0, polls=0, flushes=0, output_calls=0, rate_calls=0, nv_writes=0;
std::vector<bool> psm;
bool begin(TwoWire&,uint8_t address) { ++begins; event("gnssBegin",address); return present&&begin_ok; }
bool powerSaveMode(bool enabled) { psm.push_back(enabled); event("gnssPsm",enabled); return psm_ok; }
bool setI2COutput(unsigned output) { ++output_calls; event("gnssOutput",output); return output_ok; }
void enableGNSS(bool,unsigned) {}
bool setMeasurementRate(unsigned rate) { ++rate_calls; event("gnssRate",rate); return rate_ok; }
void saveConfigSelective(unsigned) { ++nv_writes; }
void flushPVT() { ++flushes; }
bool getPVT(unsigned) { ++polls; return valid; }
bool getGnssFixOk(unsigned) { ++polls; return valid; }
long getLatitude(unsigned) { return 476100000; }
long getLongitude(unsigned) { return -1223300000; }
long getAltitude(unsigned) { return 125000; }
int getSIV(unsigned) { return valid?8:0; }
bool getDateValid(unsigned) { return valid; }
bool getTimeValid(unsigned) { return valid; }
long getUnixEpoch(unsigned) { return 1790942400; }
} ublox_GNSS;
@HELPERS@
@PROVIDER@
static RAK12500LocationProvider RAK12500_provider;
struct SensorDef {
uint32_t address;
const char* name;
uint8_t (*init)(TwoWire*,uint8_t);
void (*query)(uint8_t,uint8_t,CayenneLPP&);
bool (*query_voltage)(uint8_t,float&);
};
static const SensorDef SENSOR_TABLE[1] = {};
static const size_t SENSOR_TABLE_SIZE=0;
bool EnvironmentSensorManager::i2c_probe(TwoWire&,uint8_t) { return false; }
@METHODS@
struct TestLocation : LocationProvider {
unsigned loops=0,resets=0,clears=0;
int pin=-1;
mesh::RTCClock clock;
bool valid=false;
void reset() override { ++resets; valid=false; }
void begin() override {}
void stop() override {}
void loop() override { ++loops; }
bool isEnabled() override { return pin<0 || digitalRead(pin)==HIGH; }
bool clearPositionFix() override { valid=false; ++clears; return true; }
void setPinEn(int value) override { pin=value; }
int getPinEn() override { return pin; }
mesh::RTCClock* getRTCClock() override { return &clock; }
long getLatitude() override { return 47610000; }
long getLongitude() override { return -122330000; }
long getAltitude() override { return 125000; }
long satellitesCount() override { return valid?8:0; }
bool isValid() override { return valid; }
long getTimestamp() override { return 1790942400; }
void acceptedClock() { markTimeSyncApplied(); stopTimeSync(); }
};
struct TestEnvironment : EnvironmentSensorManager {
explicit TestEnvironment(TestLocation& location):EnvironmentSensorManager(location) {}
bool active() const { return gps_active; }
bool userEnabled() const { return isGpsTelemetryUserEnabled(); }
bool transportAvailable() const { return isGpsTelemetryTransportAvailable(); }
using EnvironmentSensorManager::start_gps;
using EnvironmentSensorManager::stop_gps;
using EnvironmentSensorManager::rakGPSInit;
using EnvironmentSensorManager::_wisblock_power;
};
static constexpr bool compile_held = mesh::gps::wisBlockCompileRailHolds()!=0;
static void fresh() {
now_ms=100; delay_count=0; hardware_events.clear();
std::memset(pin_values,0,sizeof(pin_values)); pin_values[WB_IO2]=HIGH;
Serial1=SerialMock{}; ublox_GNSS=GnssMock{}; RAK12500_provider=RAK12500LocationProvider{};
mesh::ota::OtaStoreQspiNrf52::selected=0;
mesh::ota::OtaStoreQspiNrf52::rak_id=mesh::ota::OtaStoreQspiNrf52::w25_id=0;
mesh::ota::OtaStoreQspiNrf52::header_present=false;
mesh::ota::OtaStoreQspiNrf52::cached=false;
mesh::ota::OtaStoreQspiNrf52::probes=0;
i2cGPSFlag=serialGPSFlag=false; gpsResetPin=uint32_t(-1); bus_available=true;
}
static size_t firstEvent(const char* name,int a=-999,int b=-999,size_t after=0) {
for(size_t i=after;i<hardware_events.size();++i) {
const auto& e=hardware_events[i];
if(e.name==name&&(a==-999||e.a==a)&&(b==-999||e.b==b)) return i;
}
return hardware_events.size();
}
static void bootUart(TestEnvironment& sensors,TestLocation& location,bool shared=false) {
Serial1.nmea(); assert(sensors.begin());
assert(sensors.isGPSDetected()&&sensors.active()&&!sensors.userEnabled());
assert(location.pin<0 && sensors.isGpsPowered());
assert(digitalRead(WB_IO2)==HIGH);
sensors.configureSharedPeripheralMode(shared);
assert(sensors._wisblock_power.isConfigured()&&!sensors._wisblock_power.discoveryActive());
assert(sensors.setSettingValue("gps","0"));
assert(!sensors.active()&&!sensors.userEnabled()&&!Serial1.installed);
}
static void requireNoLow() {
for(const auto& e:hardware_events) assert(!(e.name=="write"&&e.a==WB_IO2&&e.b==LOW));
}
static void requireUartReleasedBeforePowerOff(int pin=WB_IO2,int value=LOW) {
const size_t end=firstEvent("uartEnd");
const size_t tx=firstEvent("pinMode",PIN_GPS_TX,INPUT,end);
const size_t rx=firstEvent("pinMode",PIN_GPS_RX,INPUT,end);
const size_t off=firstEvent("write",pin,value,end);
assert(off<hardware_events.size());
assert(end<tx&&tx<off&&end<rx&&rx<off);
}
static void uart(bool shared) {
fresh(); TestLocation location; TestEnvironment sensors(location); bootUart(sensors,location,shared);
const bool held=shared||compile_held;
assert(sensors.isGpsPowered()==held && digitalRead(WB_IO2)==int(held));
if(!held) requireUartReleasedBeforePowerOff();
else requireNoLow();
assert(sensors.gpsUsesSerialUart(1)&&sensors.gpsSerialTransportMayConflict(1));
assert(!sensors.gpsSerialTransportCanYield(1));
assert(!sensors.gpsUsesSerialUart(2)&&!sensors.gpsSerialTransportMayConflict(2));
const unsigned begins=Serial1.begins,ends=Serial1.ends,loops=location.loops;
assert(!sensors.setGpsSerialTransportBlocked(1,true));
assert(!sensors.setGpsSerialTransportBlocked(2,true));
assert(sensors.transportAvailable()&&Serial1.begins==begins&&Serial1.ends==ends);
now_ms+=1000; sensors.loop(); assert(location.loops==loops&&!sensors.active());
hardware_events.clear(); const uint32_t started=millis();
assert(sensors.setSettingValue("gps","1"));
assert(sensors.active()&&sensors.userEnabled()&&Serial1.installed&&sensors.isGpsPowered());
const size_t high=firstEvent("write",WB_IO2,HIGH),opened=firstEvent("uartBegin");
assert(high<opened);
assert(hardware_events[opened].time>=started+(held?0:500));
assert(location.clears==1&&location.pin<0);
sensors.setPowerSavingEnabled(false); now_ms+=600001; sensors.loop();
assert(sensors.active()); // Manual on plus power-saving off is continuous.
hardware_events.clear(); assert(sensors.setSettingValue("gps","0"));
assert(!sensors.active()&&!Serial1.installed&&sensors.isGpsPowered()==held);
if(!held) requireUartReleasedBeforePowerOff();
else requireNoLow();
sensors.setPowerSavingEnabled(true); now_ms+=1000; sensors.loop();
sensors.setPowerSavingEnabled(false); assert(!sensors.active());
const unsigned saved_begins=Serial1.begins;
sensors.configureSharedPeripheralMode(!shared); // Boot-only mode cannot revoke/reconfigure owners.
assert(sensors.isGpsPowered()==held&&Serial1.begins==saved_begins);
}
static void hourly() {
fresh(); TestLocation location; TestEnvironment sensors(location); bootUart(sensors,location);
sensors.applyGpsTimeSyncInterval(1); sensors.setPowerSavingEnabled(true);
assert(sensors.setSettingValue("gps","1"));
assert(sensors.active()&&location.getGPSPowerSaving());
assert(location.getNextGPSOff()!=0);
now_ms+=100; location.acceptedClock(); sensors.loop();
assert(!sensors.active()&&!sensors.isGpsPowered());
const uint32_t deadline=uint32_t(location.getNextGPSOn());
assert(deadline==millis()+3600000U);
now_ms=deadline-1; sensors.loop(); assert(!sensors.active());
now_ms=deadline; hardware_events.clear(); sensors.loop();
assert(sensors.active()&&Serial1.installed);
assert(firstEvent("write",WB_IO2,HIGH)<firstEvent("uartBegin"));
// Explicit off cancels both clock/duty deadlines and must not be undone by them.
assert(sensors.setSettingValue("gps","0"));
assert(!location.getGPSPowerSaving()&&location.getNextGPSOn()==0&&location.getNextGPSOff()==0);
const unsigned begins=Serial1.begins; now_ms+=3600100; sensors.loop();
assert(!sensors.active()&&!sensors.isGpsPowered()&&Serial1.begins==begins);
}
static void repeatedDiscovery() {
fresh(); TestLocation location; TestEnvironment sensors(location); bootUart(sensors,location);
hardware_events.clear(); Serial1.nmea();
assert(sensors.begin());
assert(!sensors.active()&&!sensors.userEnabled()&&!sensors.isGpsPowered());
assert(!Serial1.installed&&!sensors._wisblock_power.discoveryActive());
assert(firstEvent("write",WB_IO2,HIGH)<firstEvent("uartBegin"));
requireUartReleasedBeforePowerOff();
assert(sensors.setSettingValue("gps","1"));
hardware_events.clear(); Serial1.nmea(); assert(sensors.begin());
assert(sensors.active()&&sensors.userEnabled()&&sensors.isGpsPowered()&&Serial1.installed);
requireNoLow();
}
static void preferenceBeforeDiscovery() {
for(bool shared:{false,true}) {
for(bool enabled:{false,true}) {
fresh(); TestLocation location; TestEnvironment sensors(location);
sensors.configureSharedPeripheralMode(shared);
// Companion loads mode before sensors.begin(): saved GPS-only must not
// cut supply until the first discovery pass has actually completed.
assert(sensors._wisblock_power.isConfigured());
assert(sensors._wisblock_power.discoveryActive());
assert(sensors.isGpsPowered()&&digitalRead(WB_IO2)==HIGH); requireNoLow();
Serial1.nmea(); assert(sensors.begin());
assert(sensors.isGPSDetected()&&sensors.active()&&!sensors.userEnabled());
assert(sensors.isGpsPowered()&&Serial1.installed); requireNoLow();
assert(!sensors._wisblock_power.discoveryActive());
assert(sensors.setSettingValue("gps",enabled?"1":"0"));
assert(sensors.active()==enabled&&sensors.userEnabled()==enabled);
const bool powered=enabled||shared||compile_held;
assert(sensors.isGpsPowered()==powered&&digitalRead(WB_IO2)==int(powered));
if(!powered) requireUartReleasedBeforePowerOff();
else requireNoLow();
}
}
}
static void actualPoweredStatus() {
fresh(); TestLocation location; TestEnvironment sensors(location); bootUart(sensors,location);
assert(!sensors.isGpsPowered());
// Status must read the applied GPIO, not a provider whose absent enable pin
// means 'always powered', and not merely the desired policy state.
assert(location.isEnabled());
pin_values[WB_IO2]=HIGH; assert(sensors.isGpsPowered());
pin_values[WB_IO2]=LOW; assert(!sensors.isGpsPowered());
}
static void absent() {
fresh(); TestLocation location; TestEnvironment sensors(location);
assert(sensors.begin()); assert(!sensors.isGPSDetected()&&!sensors.active());
assert(digitalRead(WB_IO2)==HIGH&&!sensors._wisblock_power.isConfigured());
requireNoLow(); sensors.configureSharedPeripheralMode(false);
assert(digitalRead(WB_IO2)==int(compile_held));
const unsigned polls=Serial1.polls,begins=Serial1.begins;
now_ms+=1000; sensors.loop(); assert(Serial1.polls==polls&&Serial1.begins==begins);
assert(!sensors.setSettingValue("gps","1"));
assert(!sensors.gpsSerialTransportCanYield(1)&&sensors.gpsSerialTransportMayConflict(1));
}
static void storage() {
fresh(); TestLocation location; TestEnvironment sensors(location);
mesh::ota::OtaStoreQspiNrf52::header_present=true;
mesh::ota::OtaStoreQspiNrf52::selected=2;
mesh::ota::OtaStoreQspiNrf52::w25_id=0xef4015;
assert(sensors.begin()); assert(!sensors.isGPSDetected()&&Serial1.begins==0);
sensors.configureSharedPeripheralMode(false);
assert(sensors._wisblock_power.holds()&mesh::gps::ExternalStorage);
assert(digitalRead(WB_IO2)==HIGH); requireNoLow();
sensors.configureSharedPeripheralMode(true);
assert(digitalRead(WB_IO2)==HIGH); requireNoLow();
}
static void fixedStorage() {
fresh(); TestLocation location; TestEnvironment sensors(location); bootUart(sensors,location);
assert(sensors._wisblock_power.holds()&mesh::gps::ExternalStorage);
assert(sensors.isGpsPowered()&&digitalRead(WB_IO2)==HIGH);
assert(!sensors.active()&&!Serial1.installed); requireNoLow();
assert(mesh::ota::OtaStoreQspiNrf52::probes==0);
assert(sensors.setSettingValue("gps","1"));
assert(sensors.active()&&sensors.isGpsPowered());
assert(sensors.setSettingValue("gps","0"));
assert(!sensors.active()&&sensors.isGpsPowered()); requireNoLow();
}
static void storageIds() {
for(uint8_t selected: {uint8_t(0),uint8_t(1),uint8_t(2),uint8_t(3)}) {
for(uint32_t id: {0U,0xffffffU,0x123456U,0xef4015U}) {
for(bool header: {false,true}) {
fresh(); TestLocation location; TestEnvironment sensors(location);
mesh::ota::OtaStoreQspiNrf52::selected=selected;
if(header) mesh::ota::OtaStoreQspiNrf52::w25_id=id;
else mesh::ota::OtaStoreQspiNrf52::rak_id=id;
// Supply is protected while the read-only probe identifies a physical owner.
assert(sensors.begin()); requireNoLow();
sensors.configureSharedPeripheralMode(false);
const bool storage_held=selected!=0||(id!=0&&id!=0xffffffU);
assert(bool(sensors._wisblock_power.holds()&mesh::gps::ExternalStorage)==storage_held);
assert(digitalRead(WB_IO2)==int(storage_held||compile_held));
assert(mesh::ota::OtaStoreQspiNrf52::probes>0);
}
}
}
}
static void i2c(bool shared,unsigned wake_failure=0) {
fresh(); TestLocation location; TestEnvironment sensors(location); ublox_GNSS.present=true;
assert(sensors.begin()); assert(sensors.isGPSDetected()&&sensors.active()&&!sensors.userEnabled());
assert(i2cGPSFlag&&!serialGPSFlag&&sensors.getLocationProvider()==&RAK12500_provider);
assert(RAK12500_provider.getPinEn()<0);
sensors.configureSharedPeripheralMode(shared);
assert(sensors.setSettingValue("gps","0"));
const bool held=shared||compile_held;
assert(!sensors.active()&&sensors.isGpsPowered()==held);
if(held) { requireNoLow(); assert(ublox_GNSS.psm.back()); }
else assert(ublox_GNSS.psm.empty());
const unsigned begins=ublox_GNSS.begins,polls=ublox_GNSS.polls;
now_ms+=1000; sensors.loop(); assert(ublox_GNSS.polls==polls);
hardware_events.clear(); const uint32_t started=millis();
const unsigned writes=ublox_GNSS.nv_writes;
if(wake_failure==1) ublox_GNSS.begin_ok=false;
if(wake_failure==2) ublox_GNSS.output_ok=false;
if(wake_failure==3) ublox_GNSS.rate_ok=false;
if(wake_failure==4) bus_available=false;
assert(sensors.setSettingValue("gps","1"));
if(wake_failure) {
assert(!held&&!sensors.active()&&!sensors.isGPSDetected()&&!sensors.isGpsPowered());
assert(sensors.userEnabled()); // Persisted intent is not silently rewritten after failure.
const unsigned failed_begins=ublox_GNSS.begins;
for(unsigned i=0;i<10;++i) { now_ms+=1000; sensors.loop(); }
assert(ublox_GNSS.begins==failed_begins&&ublox_GNSS.polls==polls);
assert(!sensors.setSettingValue("gps","1"));
return;
}
assert(ublox_GNSS.nv_writes==writes); // Cold duty wake must not write receiver flash/BBR again.
assert(sensors.active()&&sensors.isGpsPowered());
if(held) {
assert(ublox_GNSS.begins==begins&&!ublox_GNSS.psm.back()); requireNoLow();
} else {
assert(ublox_GNSS.begins==begins+1);
assert(firstEvent("write",WB_IO2,HIGH)<firstEvent("gnssBegin"));
assert(hardware_events[firstEvent("gnssBegin")].time>=started+500);
}
assert(ublox_GNSS.flushes>0); // A cold wake never reuses a previous position as fresh.
assert(sensors.setSettingValue("gps","0"));
assert(!sensors.active()&&sensors.isGpsPowered()==held);
}
static void initialConfigFailure() {
for(unsigned failed_step:{1U,2U}) {
fresh(); TestLocation location; TestEnvironment sensors(location); ublox_GNSS.present=true;
if(failed_step==1) ublox_GNSS.output_ok=false;
else ublox_GNSS.rate_ok=false;
assert(sensors.begin());
assert(!sensors.isGPSDetected()&&!sensors.active()&&!i2cGPSFlag&&!serialGPSFlag);
assert(sensors.getLocationProvider()==&location&&!Serial1.installed);
assert(ublox_GNSS.output_calls==1&&ublox_GNSS.rate_calls==(failed_step==1?0U:1U));
assert(ublox_GNSS.nv_writes==0&&!sensors.userEnabled());
requireNoLow(); sensors.configureSharedPeripheralMode(false);
assert(!sensors.isGpsPowered()&&digitalRead(WB_IO2)==LOW);
const unsigned begins=ublox_GNSS.begins,polls=ublox_GNSS.polls;
for(unsigned i=0;i<10;++i) { now_ms+=1000; sensors.loop(); }
assert(ublox_GNSS.begins==begins&&ublox_GNSS.polls==polls);
assert(!sensors.setSettingValue("gps","1"));
}
}
static void dedicated() {
#ifdef PIN_GPS_EN
fresh(); TestLocation location; TestEnvironment sensors(location); Serial1.nmea();
assert(sensors.begin()&&sensors.isGPSDetected()&&location.pin==PIN_GPS_EN);
sensors.configureSharedPeripheralMode(true);
assert(sensors.setSettingValue("gps","0"));
requireUartReleasedBeforePowerOff(PIN_GPS_EN,!PIN_GPS_EN_ACTIVE);
assert(!sensors.active()&&!sensors.isGpsPowered());
assert(digitalRead(PIN_GPS_EN)==!PIN_GPS_EN_ACTIVE);
assert(digitalRead(WB_IO2)==HIGH); requireNoLow();
assert(sensors.gpsSerialTransportCanYield(1));
const unsigned off_ends=Serial1.ends;
assert(sensors.setGpsSerialTransportBlocked(1,true));
assert(Serial1.ends==off_ends); // Already-off GPS must not close UARTE twice.
assert(!sensors.transportAvailable()&&!sensors.isGpsPowered());
assert(sensors.setSettingValue("gps","1"));
assert(sensors.userEnabled()&&!sensors.active()&&!sensors.isGpsPowered());
hardware_events.clear(); assert(sensors.setGpsSerialTransportBlocked(1,false));
assert(sensors.active()&&Serial1.installed&&sensors.isGpsPowered());
assert(firstEvent("write",PIN_GPS_EN,PIN_GPS_EN_ACTIVE)<firstEvent("uartBegin"));
assert(digitalRead(WB_IO2)==HIGH); requireNoLow();
const unsigned on_ends=Serial1.ends;
hardware_events.clear(); assert(sensors.setGpsSerialTransportBlocked(1,true));
assert(!sensors.active()&&!Serial1.installed&&Serial1.ends==on_ends+1);
requireUartReleasedBeforePowerOff(PIN_GPS_EN,!PIN_GPS_EN_ACTIVE);
assert(!sensors.isGpsPowered()&&sensors.userEnabled()); requireNoLow();
assert(sensors.setGpsSerialTransportBlocked(1,false));
assert(sensors.active()&&sensors.isGpsPowered()&&Serial1.installed);
#else
assert(false&&"dedicated scenario requires a separate enable pin");
#endif
}
int main(int argc,char** argv) {
assert(argc==2);
if(!std::strcmp(argv[1],"uart")) uart(false);
else if(!std::strcmp(argv[1],"shared_uart")) uart(true);
else if(!std::strcmp(argv[1],"held_uart")) uart(false);
else if(!std::strcmp(argv[1],"hourly")) hourly();
else if(!std::strcmp(argv[1],"repeat")) repeatedDiscovery();
else if(!std::strcmp(argv[1],"prefs_before")) preferenceBeforeDiscovery();
else if(!std::strcmp(argv[1],"powered_status")) actualPoweredStatus();
else if(!std::strcmp(argv[1],"absent")) absent();
else if(!std::strcmp(argv[1],"storage")) storage();
else if(!std::strcmp(argv[1],"fixed_storage")) fixedStorage();
else if(!std::strcmp(argv[1],"storage_ids")) storageIds();
else if(!std::strcmp(argv[1],"i2c")) i2c(false);
else if(!std::strcmp(argv[1],"shared_i2c")) i2c(true);
else if(!std::strcmp(argv[1],"held_i2c")) i2c(false);
else if(!std::strcmp(argv[1],"wake_failure")) i2c(false,1);
else if(!std::strcmp(argv[1],"wake_output_failure")) i2c(false,2);
else if(!std::strcmp(argv[1],"wake_rate_failure")) i2c(false,3);
else if(!std::strcmp(argv[1],"wake_bus_failure")) i2c(false,4);
else if(!std::strcmp(argv[1],"initial_config_failure")) initialConfigFailure();
else if(!std::strcmp(argv[1],"dedicated")) dedicated();
else assert(false&&"unknown scenario");
}
+77
View File
@@ -0,0 +1,77 @@
#include <helpers/sensors/EnvironmentSensorManager.h>
#include <helpers/sensors/I2CAddressClaimPolicy.h>
#include <cassert>
#include <cstring>
#include <initializer_list>
#define WB_IO2 21
#define TELEM_WIRE (&Wire)
#define MESH_DEBUG_PRINTLN(...) ((void)0)
void LocationProvider::sendSentence(const char*) {}
namespace mesh { namespace ota {
struct OtaStoreQspiNrf52 {
inline static unsigned probes=0;
inline static uint32_t raw_id=0;
static uint8_t autoDetect() { ++probes; assert(digitalRead(WB_IO2)==HIGH); return 0; }
static void autoProbeIds(uint32_t& rak,uint32_t& w25) { rak=raw_id; w25=0; }
};
}}
static bool ensureI2cBusReleased(TwoWire*) { return true; }
struct SensorDef {
uint32_t address;
const char* name;
uint8_t (*init)(TwoWire*,uint8_t);
void (*query)(uint8_t,uint8_t,CayenneLPP&);
bool (*query_voltage)(uint8_t,float&);
};
static const SensorDef SENSOR_TABLE[1] = {};
static const size_t SENSOR_TABLE_SIZE=0;
bool EnvironmentSensorManager::i2c_probe(TwoWire&,uint8_t) { return false; }
@METHODS@
struct TestEnvironment : EnvironmentSensorManager {
using EnvironmentSensorManager::_wisblock_power;
};
int main(int argc,char** argv) {
assert(argc==2&&!std::strcmp(argv[1],"no_gps"));
for(bool prefs_first:{false,true}) {
for(bool shared:{false,true}) {
for(uint32_t id:{0U,0xffffffU,0x123456U}) {
now_ms=100; hardware_events.clear();
std::memset(pin_values,0,sizeof(pin_values)); pin_values[WB_IO2]=HIGH;
mesh::ota::OtaStoreQspiNrf52::raw_id=id;
mesh::ota::OtaStoreQspiNrf52::probes=0;
TestEnvironment sensors;
if(prefs_first) {
sensors.configureSharedPeripheralMode(shared);
assert(sensors._wisblock_power.discoveryActive());
assert(digitalRead(WB_IO2)==HIGH);
}
assert(sensors.begin());
if(!prefs_first) {
// Even a no-GPS image must wait for validated mode after discovery.
assert(digitalRead(WB_IO2)==HIGH);
sensors.configureSharedPeripheralMode(shared);
}
assert(sensors._wisblock_power.isConfigured()&&!sensors._wisblock_power.discoveryActive());
const bool storage=id!=0&&id!=0xffffffU;
const bool held=shared||storage||mesh::gps::wisBlockCompileRailHolds()!=0;
assert(digitalRead(WB_IO2)==int(held));
assert(bool(sensors._wisblock_power.holds()&mesh::gps::ExternalStorage)==storage);
assert(mesh::ota::OtaStoreQspiNrf52::probes>0);
assert(!sensors.isGPSDetected()&&!sensors.isGpsPowered());
assert(!sensors.setSettingValue("gps","1"));
assert(!sensors.gpsUsesSerialUart(1)&&!sensors.gpsSerialTransportCanYield(1));
assert(!sensors.setGpsSerialTransportBlocked(1,true));
if(held) {
for(const auto& event:hardware_events)
assert(!(event.name=="write"&&event.a==WB_IO2&&event.b==LOW));
}
}
}
}
}
+134
View File
@@ -0,0 +1,134 @@
#include <helpers/sensors/WisBlockSharedPowerPolicy.h>
#include <cassert>
#include <cstdint>
#include <type_traits>
#include <initializer_list>
using mesh::gps::WisBlockPeripheralMode;
using mesh::gps::WisBlockSharedPowerPolicy;
using mesh::gps::RadioPower;
using mesh::gps::EthernetPower;
using mesh::gps::ExternalStorage;
static_assert(sizeof(WisBlockSharedPowerPolicy) == 2, "No allocation or added per-board RAM");
static_assert(std::is_trivially_copyable<WisBlockSharedPowerPolicy>::value,
"The rail policy must not acquire an owning allocation");
static_assert(mesh::gps::wisBlockCompileRailHolds() == EXPECTED_COMPILE_HOLDS,
"Compile-time radio/PoE dependencies are unconditional holds");
static void expectState(const WisBlockSharedPowerPolicy& policy, bool configured,
bool discovery, bool gps, WisBlockPeripheralMode mode,
uint8_t holds) {
assert(policy.isConfigured() == configured);
assert(policy.discoveryActive() == discovery);
assert(policy.gpsActive() == gps);
assert(policy.mode() == mode);
assert(policy.holds() == holds);
const bool independently_controllable = configured && !discovery
&& mode == WisBlockPeripheralMode::GpsOnly && holds == 0;
assert(policy.gpsPowerIndependentlyControllable() == independently_controllable);
assert(policy.canPowerDown() == (independently_controllable && !gps));
assert(policy.railEnabled() == !(independently_controllable && !gps));
}
static void bootAndDiscovery() {
WisBlockSharedPowerPolicy policy;
expectState(policy, false, true, false, WisBlockPeripheralMode::GpsOnly,
EXPECTED_COMPILE_HOLDS);
// Even an early discovery completion cannot cut power before saved mode is known.
policy.setDiscoveryActive(false);
expectState(policy, false, false, false, WisBlockPeripheralMode::GpsOnly,
EXPECTED_COMPILE_HOLDS);
policy.setDiscoveryActive(true);
policy.setGpsActive(true);
assert(policy.configure(WisBlockPeripheralMode::GpsOnly));
expectState(policy, true, true, true, WisBlockPeripheralMode::GpsOnly,
EXPECTED_COMPILE_HOLDS);
policy.setGpsActive(false);
assert(policy.railEnabled()); // Saved disabled GPS cannot defeat discovery protection.
policy.setDiscoveryActive(false);
expectState(policy, true, false, false, WisBlockPeripheralMode::GpsOnly,
EXPECTED_COMPILE_HOLDS);
// Configuration is boot-only: changing a saved mode must not cause a live rail transition.
assert(!policy.configure(WisBlockPeripheralMode::Shared, ExternalStorage));
expectState(policy, true, false, false, WisBlockPeripheralMode::GpsOnly,
EXPECTED_COMPILE_HOLDS);
policy.setGpsActive(true);
expectState(policy, true, false, true, WisBlockPeripheralMode::GpsOnly,
EXPECTED_COMPILE_HOLDS);
policy.setGpsActive(false);
policy.setDiscoveryActive(true); // A subsequent safe probe reacquires its hold.
assert(policy.railEnabled() && !policy.gpsPowerIndependentlyControllable());
policy.setDiscoveryActive(false);
expectState(policy, true, false, false, WisBlockPeripheralMode::GpsOnly,
EXPECTED_COMPILE_HOLDS);
}
static void everyOwnershipCombination() {
// External NOR is a physical dependency, independent of user/service enablement.
// All valid owner unions must survive GPS settings and repeated probes.
for (uint8_t initial = 0; initial < 8; ++initial) {
for (uint8_t detected = 0; detected < 8; ++detected) {
for (uint8_t mode_value = 0; mode_value < 2; ++mode_value) {
const auto mode = static_cast<WisBlockPeripheralMode>(mode_value);
WisBlockSharedPowerPolicy policy(initial);
assert(policy.configure(mode, detected));
const uint8_t holds = static_cast<uint8_t>(initial | detected | EXPECTED_COMPILE_HOLDS);
for (unsigned repeat = 0; repeat < 2; ++repeat) {
for (bool discovery : {false, true}) {
for (bool gps : {false, true}) {
policy.setDiscoveryActive(discovery);
policy.setGpsActive(gps);
expectState(policy, true, discovery, gps, mode, holds);
assert(!policy.configure(WisBlockPeripheralMode::GpsOnly, 0));
expectState(policy, true, discovery, gps, mode, holds);
}
}
}
}
}
}
WisBlockSharedPowerPolicy storage;
assert(storage.configure(WisBlockPeripheralMode::GpsOnly, ExternalStorage));
storage.setDiscoveryActive(false);
assert(storage.railEnabled() && !storage.canPowerDown());
assert(!storage.gpsPowerIndependentlyControllable());
WisBlockSharedPowerPolicy shared;
assert(shared.configure(WisBlockPeripheralMode::SharedPeripherals));
shared.setDiscoveryActive(false);
assert(shared.railEnabled() && !shared.gpsPowerIndependentlyControllable());
}
static void rejectedConfigurationRetainsOwners() {
WisBlockSharedPowerPolicy policy(RadioPower);
policy.setGpsActive(true);
assert(!policy.configure(static_cast<WisBlockPeripheralMode>(2), ExternalStorage));
expectState(policy, false, true, true, WisBlockPeripheralMode::GpsOnly,
RadioPower | EXPECTED_COMPILE_HOLDS);
assert(!policy.configure(static_cast<WisBlockPeripheralMode>(255), 0));
assert(!policy.configure(WisBlockPeripheralMode::GpsOnly, 0x80));
expectState(policy, false, true, true, WisBlockPeripheralMode::GpsOnly,
RadioPower | EXPECTED_COMPILE_HOLDS);
assert(policy.configure(WisBlockPeripheralMode::GpsOnly, ExternalStorage));
policy.setGpsActive(false);
policy.setDiscoveryActive(false);
expectState(policy, true, false, false, WisBlockPeripheralMode::GpsOnly,
RadioPower | ExternalStorage | EXPECTED_COMPILE_HOLDS);
assert(!policy.configure(WisBlockPeripheralMode::Shared, EthernetPower));
expectState(policy, true, false, false, WisBlockPeripheralMode::GpsOnly,
RadioPower | ExternalStorage | EXPECTED_COMPILE_HOLDS);
// A future/unknown constructor owner stays fail-closed rather than being masked out.
for (uint8_t unknown : {uint8_t(0x08), uint8_t(0x80), uint8_t(0xff)}) {
WisBlockSharedPowerPolicy held(unknown);
assert(held.configure(WisBlockPeripheralMode::GpsOnly));
held.setDiscoveryActive(false);
expectState(held, true, false, false, WisBlockPeripheralMode::GpsOnly,
static_cast<uint8_t>(unknown | EXPECTED_COMPILE_HOLDS));
}
}
int main() {
bootAndDiscovery();
everyOwnershipCombination();
rejectedConfigurationRetainsOwners();
}
+7 -1
View File
@@ -429,7 +429,13 @@ for env_name in (
rak_usb = "RAK_3401_companion_radio_usb"
require(rak_usb, "build_flags", "RAK_BOARD")
require(rak_usb, "build_flags", "FORCE_GPS_ALIVE")
require(rak_usb, "build_flags", "RAK_3401")
require(rak_usb, "build_flags", "PIN_GPS_EN=-1")
reject(rak_usb, "build_flags", "FORCE_GPS_ALIVE")
for env_name in ("RAK_4631_companion_radio_usb", "RAK_4631_repeater"):
require(env_name, "build_flags", "RAK_4631")
require(env_name, "build_flags", "PIN_GPS_EN=-1")
reject(env_name, "build_flags", "FORCE_GPS_ALIVE")
'
# Every qualified ESP32 Companion layout publishes one Full image even when
+2 -1
View File
@@ -185,11 +185,12 @@ class CompanionPrefsValuesTests(unittest.TestCase):
r'^\s*(?:float|char|u?int(?:8|16|32)_t)\s+([A-Za-z_][A-Za-z_0-9]*)',
prefix, re.M)
self.assertEqual(declarations[0], 'airtime_factor')
self.assertEqual(declarations[-1], 'lost_reply')
self.assertEqual(declarations[-2:], ['lost_reply', 'peripherals_shared'])
optional = {
'fan_mode': 'TBEAM_1W', 'fan_lo': 'TBEAM_1W', 'fan_hi': 'TBEAM_1W',
'wifi_ssid': 'defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE)',
'wifi_pwd': 'defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE)',
'peripherals_shared': 'RAK_4631',
}
def fields(template):
@@ -122,10 +122,13 @@ class CompanionSettingsPersistenceContractTests(unittest.TestCase):
"bool copyPersistedValuesFrom(const CompanionNodePrefs& source)",
)
self.assertIn("reinterpret_cast<uintptr_t>(&airtime_factor)", copy)
# The appended lost-reply byte is the last persisted value. Both
# snapshots must stop there before either runtime adapter begins.
self.assertIn("reinterpret_cast<uintptr_t>(&lost_reply)", copy)
self.assertIn("reinterpret_cast<uintptr_t>(&values.lost_reply)", copy)
# RAK4631 appends its peripheral-mode byte; every other target stops
# at lost-reply. Neither endpoint may include a runtime adapter.
self.assertIn("const auto& last_value = peripherals_shared;", copy)
self.assertIn("const auto& last_value = lost_reply;", copy)
self.assertIn("reinterpret_cast<uintptr_t>(&last_value)", copy)
self.assertIn("&values.peripherals_shared", copy)
self.assertIn("&values.lost_reply", copy)
self.assertIn("+ sizeof(lost_reply)", copy)
self.assertIn("end - begin == size - sizeof(values.lost_reply)", copy)
self.assertIn("reinterpret_cast<uintptr_t>(&values.radio)", copy)
+2 -1
View File
@@ -399,7 +399,8 @@ class CooperativeGpsDiscoveryTests(unittest.TestCase):
with self.subTest(defines=defines):
result = subprocess.run([self.compiler, "-E", "-P", "-x", "c++",
"-DENV_INCLUDE_GPS=1", *["-D" + value for value in defines],
"-I" + str(work), str(cpp)], text=True, capture_output=True, timeout=10)
"-I" + str(work), "-I" + str(ROOT / "src/helpers/sensors"),
str(cpp)], text=True, capture_output=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual("gps_discovery_pending" in result.stdout, expected)
+10
View File
@@ -41,7 +41,11 @@ struct LocationProvider {
struct Sensors {
LocationProvider* provider;
const char* user = "0";
bool physical_override = false, physical_power = false;
LocationProvider* getLocationProvider() { return provider; }
bool isGpsPowered() const {
return physical_override ? physical_power : (provider && provider->powered);
}
const char* getSettingByKey(const char*) { return user; }
};
struct Prefs { bool powersaving_enabled = true; };
@@ -58,6 +62,12 @@ int main() {
gps.powered = true;
status(&sensors, &prefs, reply);
assert(!strcmp(reply, "off, powered"));
// A provider without its own enable pin can report enabled despite a cut
// shared rail. The manager's physical policy must win over that stale view.
sensors.physical_override = true; sensors.physical_power = false;
status(&sensors, &prefs, reply);
assert(!strcmp(reply, "off, unpowered"));
sensors.physical_override = false;
sensors.user = "1"; gps.powered = false;
status(&sensors, &prefs, reply);
assert(!strcmp(reply, "on, unpowered (powersaving, wake in 0h 30m), last sync: none"));
+32 -15
View File
@@ -259,18 +259,35 @@ class Nrf52VariantContractsTest(unittest.TestCase):
self.assertIn("strap INA3221 A0 to SCL for 0x43", recipe)
self.assertIn("-DTELEM_INA3221_ADDRESS=0x43", recipe)
def test_rak4631_shared_sensor_rail_stays_enabled(self) -> None:
def test_rak4631_shared_sensor_rail_is_owned_by_boot_policy(self) -> None:
recipe = (ROOT / "variants/rak4631/platformio.ini").read_text()
rak3401_recipe = (ROOT / "variants/rak3401/platformio.ini").read_text()
board = (ROOT / "variants/rak4631/RAK4631Board.cpp").read_text()
sensors = (
ROOT / "src/helpers/sensors/EnvironmentSensorManager.cpp"
sensor_header = (
ROOT / "src/helpers/sensors/EnvironmentSensorManager.h"
).read_text()
policy = (
ROOT / "src/helpers/sensors/WisBlockSharedPowerPolicy.h"
).read_text()
prefs = (ROOT / "src/helpers/PeripheralModePrefs.h").read_text()
self.assertRegex(recipe, r"-D\s+FORCE_GPS_ALIVE\b")
for current_recipe in (recipe, rak3401_recipe):
self.assertNotRegex(current_recipe, r"-D\s+FORCE_GPS_ALIVE\b")
self.assertIn("peripherals.mode defaults to gps-only", recipe)
self.assertIn("pinMode(WB_IO2, OUTPUT)", board)
self.assertIn("digitalWrite(WB_IO2, HIGH)", board)
self.assertIn("setRakGpsControl(ioPin, shared_power_rail, true)", sensors)
self.assertIn("setRakGpsControl(ioPin, shared_power_rail, false)", sensors)
self.assertIn("WisBlockSharedPowerPolicy _wisblock_power", sensor_header)
self.assertIn("_state(DiscoveryActive)", policy)
self.assertIn("GpsOnly = 0", policy)
self.assertIn("Shared = 1", policy)
self.assertIn("#if defined(RAK_3401)", policy)
self.assertIn("| RadioPower", policy)
self.assertIn("defined(ETHERNET_ENABLED)", policy)
self.assertIn("| EthernetPower", policy)
self.assertIn("ExternalStorage =", policy)
self.assertIn("get peripherals.mode", prefs)
self.assertIn("set peripherals.mode", prefs)
self.assertIn("saved; reboot to apply", prefs)
def test_rak4631_ethernet_does_not_suppress_shared_rail_gps(self) -> None:
recipe = (ROOT / "variants/rak4631/platformio.ini").read_text()
@@ -362,17 +379,17 @@ class Nrf52VariantContractsTest(unittest.TestCase):
self.assertIn(
"return uart == 1 && gps_detected && gps_serial_transport", sensors
)
self.assertIn(
"#if defined(RAK_WISBLOCK_GPS) && defined(FORCE_GPS_ALIVE)",
can_yield,
shared_rail_guard = (
"#if defined(RAK_WISBLOCK_GPS) "
"&& !(defined(PIN_GPS_EN) && PIN_GPS_EN >= 0)"
)
forced_rak = can_yield[
can_yield.index(
"#if defined(RAK_WISBLOCK_GPS) && defined(FORCE_GPS_ALIVE)"
):
can_yield.index("#elif defined(RAK_WISBLOCK_GPS)")
self.assertIn(shared_rail_guard, can_yield)
shared_rak = can_yield[
can_yield.index(shared_rail_guard):
can_yield.index("#endif", can_yield.index(shared_rail_guard))
]
self.assertIn("return false;", forced_rak)
self.assertIn("return false;", shared_rak)
self.assertNotIn("FORCE_GPS_ALIVE", can_yield)
refusal = block.index(
"if (blocked && !gpsSerialTransportCanYield(uart)) return false;"
)
+302
View File
@@ -0,0 +1,302 @@
"""Execute RAK4631 append-only policy images, CLI rollback and boot ordering."""
from pathlib import Path
import os
import re
import subprocess
import tempfile
import unittest
from test_common_usb_debug import HARNESS as COMMON_HARNESS
from test_companion_preferences_transaction import HARNESS as COMPANION_HARNESS
from test_radio_receive_contract import method
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
def compile_run(code, macros=(), *, serializers=False):
with tempfile.TemporaryDirectory(prefix="meshcore-peripheral-mode-") as directory:
work = Path(directory)
(work / "helpers").mkdir()
fs = (ROOT / "test/fixtures/radio_profiles/mocks/helpers/IdentityStore.h").read_text()
fs = fs.replace(" size_t available() const", " bool seek(size_t p) { if (p > size()) return false; cursor_ = p; return true; }\n size_t available() const")
(work / "helpers/IdentityStore.h").write_text(fs)
(work / "Utils.h").write_text("""#pragma once
#include <Arduino.h>
#include <cassert>
namespace mesh { struct Utils {
static void printHex(Stream&,const uint8_t*,size_t){assert(false);}
static void fromHex(uint8_t*,size_t,const char*){assert(false);}
}; }
""")
(work / "platform_shim.h").write_text("""#include <cstdlib>
#include <cstdio>
inline char* utoa(unsigned int v,char* p,int b){if(b!=10)abort();sprintf(p,"%u",v);return p;}
""")
transaction = (ROOT / "src/helpers/ContactFileTransaction.h").read_text()
(work / "ContactFileTransaction.h").write_text(transaction.replace(
'#include "IdentityStore.h"', '#include <helpers/IdentityStore.h>'))
cpp, binary = work / "test.cpp", work / "test"
cpp.write_text(code)
flags = [] if os.name == "nt" else ["-fsanitize=address,undefined",
"-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
command = ["g++", "-std=c++17", *flags, *["-D" + x + "=1" for x in macros],
"-include", str(work / "platform_shim.h"), "-I" + str(work),
"-I" + str(ROOT / "test/fixtures/radio_profiles/mocks"),
"-I" + str(ROOT / "test/mocks"), "-I" + str(ROOT / "src"),
"-I" + str(ROOT / "src/helpers"), "-I" + str(ROOT), str(cpp)]
if serializers:
command += [str(ROOT / "src/helpers/" / name) for name in (
"ConfigSerializer.cpp", "DynamicConfigSerializer.cpp", "CommonRadioPrefs.cpp",
"TxtDataHelpers.cpp")]
build = subprocess.run(command + ["-o", str(binary)], capture_output=True, text=True, timeout=90)
if build.returncode:
raise AssertionError(build.stdout + build.stderr)
result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=30)
if result.returncode:
raise AssertionError(result.stdout + result.stderr)
def common_code():
source = (ROOT / "src/helpers/CommonCLI.cpp").read_text()
header = (ROOT / "src/helpers/CommonCLI.h").read_text()
fields = header.split("class NodePrefs :", 1)[1].split("private:", 1)[0]
fields = "\n".join(re.findall(r"^\s*(?:float|double|char|u?int(?:8|16|32)_t)\s+[^;]+;", fields, re.M))
constants = "\n".join(re.findall(r"^\s*#define\s+\w+\s+(?:0x[\dA-Fa-f]+|\d+)\s*$", header, re.M))
constants += "\n" + "\n".join(re.findall(r"^static const size_t LEGACY_\w+\s*=[^;]+;", source, re.M))
constants += "\n" + re.search(r"^static constexpr uint8_t DEFAULT_ADVERT_LOC_POLICY\s*=[^;]+;", header, re.M).group()
helpers = "\n".join(extract_braced(source, signature) for signature in (
"static bool bwMatches(", "static bool isValidLoRaBandwidth(",
"static float defaultLoRaBandwidth(", "static void markDirectRetryPrefsValid(",
"static void applyFloodRetryPreset(", "static void applyDirectRetryPreset(",
"static void setDefaultDirectRetryPrefs(", "static bool directRetryPrefsValid("))
methods = "\n".join(extract_braced(source, signature) for signature in (
"static const char* commonPrefsSaveResultName(", "bool CommonCLI::recoverCommonPrefsFiles(",
"bool CommonCLI::saveCommonPrefsImageAtomically(", "void CommonCLI::savePrefs(FILESYSTEM*",
"void CommonCLI::savePrefs(PrefsSaveRouting::Scope scope)", "bool CommonCLI::trySavePrefs(",
"void CommonCLI::loadPrefsInt("))
code = COMMON_HARNESS.split("int main(")[0]
for name, value in {
"FIELDS": fields, "CONSTANTS": constants, "HELPERS": helpers,
"LEGACY": extract_braced(header, "struct LegacyObserverTail") + ";",
"SERIALIZER": "template<typename Writer>\n" + extract_braced(source, "static bool writeCommonPrefsImage("),
"STORE": extract_braced(source, "class CommonPrefsFileStore"), "METHODS": methods,
"DYNAMIC": extract_braced(header, "class BridgePrefs :"),
"SET_DEBUG": "", "SET_MASTER": "", "GET_DEBUG": "", "GET_MASTER": "",
}.items():
code = code.replace("@" + name + "@", value)
return "#include <helpers/PeripheralModePrefs.h>\n#include <helpers/bridges/RS232PowerSettings.h>\n" + code
class PeripheralModePrefsTests(unittest.TestCase):
def test_actual_cli_wrappers_use_saved_intent_without_hot_gpio(self):
common = (ROOT / "src/helpers/CommonCLI.cpp").read_text()
companion = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
snippets = []
for source, signature in (
(common, "void CommonCLI::handleSetCmd("),
(common, "void CommonCLI::handleGetCmd("),
(companion, "bool MyMesh::handleLocalControlCommand(")):
snippet = method(source, signature).split("#if defined(RAK_4631)", 1)[1].split("#else", 1)[0]
self.assertIn("mesh::peripherals::handleCommand", snippet)
snippets.append(snippet)
code = r'''
#include <helpers/PeripheralModePrefs.h>
#include <cassert>
struct Prefs{uint8_t peripherals_shared=0;};
struct Sensors{bool supported=true;unsigned applications=0;
bool supportsSharedPeripheralMode()const{return supported;}
void configureSharedPeripheralMode(bool){++applications;}
};
struct Common{
Prefs prefs,durable;Prefs* _prefs=&prefs;Sensors sensor;Sensors* _sensors=&sensor;
bool ok=true;unsigned saves=0;
bool trySavePrefs(){++saves;if(ok)durable=prefs;return ok;}
void set(const char* command,char* reply){@SET@}
void get(const char* command,char* reply){@GET@}
};
struct Companion{
Prefs _prefs,durable;Sensors sensors;bool ok=true;unsigned saves=0;
bool savePrefs(){++saves;if(ok)durable=_prefs;return ok;}
bool command(const char* command,char* reply,size_t reply_size){@COMPANION@ return false;}
};
int main(){Common common;Companion companion;char reply[160];
common.set("set peripherals.mode shared",reply);
assert(common.prefs.peripherals_shared==1&&common.durable.peripherals_shared==1&&strstr(reply,"reboot"));
assert(companion.command("set peripherals.mode shared",reply,sizeof(reply)));
assert(companion._prefs.peripherals_shared==1&&companion.durable.peripherals_shared==1&&strstr(reply,"reboot"));
assert(common.sensor.applications==0&&companion.sensors.applications==0);
common.ok=companion.ok=false;
common.set("set peripherals.mode gps-only",reply);assert(strstr(reply,"could not be saved"));
assert(companion.command("set peripherals.mode gps-only",reply,sizeof(reply))&&strstr(reply,"could not be saved"));
assert(common.prefs.peripherals_shared==1&&companion._prefs.peripherals_shared==1);
common.get("get peripherals.mode",reply);assert(strstr(reply,"shared"));
assert(companion.command("get peripherals.mode",reply,sizeof(reply))&&strstr(reply,"shared"));
unsigned cs=common.saves,ps=companion.saves;
common.sensor.supported=companion.sensors.supported=false;
common.set("set peripherals.mode gps-only",reply);assert(strstr(reply,"unsupported"));
assert(companion.command("set peripherals.mode gps-only",reply,sizeof(reply))&&strstr(reply,"unsupported"));
assert(common.saves==cs&&companion.saves==ps&&common.sensor.applications==0&&companion.sensors.applications==0);
}
'''
for key, snippet in zip(("SET", "GET", "COMPANION"), snippets):
code = code.replace("@" + key + "@", snippet)
compile_run(code)
def test_codec_and_strict_cli_transactions(self):
compile_run(r'''
#include <helpers/PeripheralModePrefs.h>
#include <cassert>
#include <initializer_list>
int main(){
uint8_t mode=0, durable=0;unsigned saves=0;bool ok=true;char reply[160];
auto save=[&](){++saves;if(ok)durable=mode;return ok;};
assert(mesh::peripherals::handleCommand(mode,"get peripherals.mode",reply,sizeof(reply),true,save));
assert(strstr(reply,"gps-only")&&saves==0);
for(const char* text:{"shared","gps-only"}){
char cmd[80];snprintf(cmd,sizeof(cmd),"set peripherals.mode %s",text);
assert(mesh::peripherals::handleCommand(mode,cmd,reply,sizeof(reply),true,save));
assert(mode==(strcmp(text,"shared")==0)&&durable==mode&&strstr(reply,"reboot"));
}
for(const char* text:{"", "on", "1", "SHARED", "shared junk", "shared ", "gps-only\n"}){
unsigned before=saves;char cmd[80];snprintf(cmd,sizeof(cmd),"set peripherals.mode %s",text);
assert(mesh::peripherals::handleCommand(mode,cmd,reply,sizeof(reply),true,save));
assert(strstr(reply,"Error")&&saves==before&&mode==durable);
}
ok=false;assert(mesh::peripherals::handleCommand(mode,"set peripherals.mode shared",reply,sizeof(reply),true,save));
assert(mode==0&&durable==0&&strstr(reply,"could not be saved"));
unsigned before=saves;
assert(mesh::peripherals::handleCommand(mode,"set peripherals.mode shared",reply,sizeof(reply),false,save));
assert(strstr(reply,"unsupported")&&saves==before&&mode==0);
assert(!mesh::peripherals::handleCommand(mode,"set peripherals.modeX shared",reply,sizeof(reply),true,save));
for(uint8_t value:{0,1}){
uint8_t footer[8];mesh::peripherals::encodeFooter(footer,value);uint8_t decoded=9;
assert(mesh::peripherals::decodeFooter(footer,sizeof(footer),decoded)&&decoded==value);
for(unsigned i=0;i<8;++i){footer[i]^=1;decoded=9;
assert(!mesh::peripherals::decodeFooter(footer,sizeof(footer),decoded)&&decoded==9);footer[i]^=1;}
for(unsigned n=0;n<8;++n)assert(!mesh::peripherals::decodeFooter(footer,n,decoded));
}
}
''')
def test_actual_common_images_and_footer_fail_closed(self):
code = common_code() + r'''
int main(){
CommonCLI writer;writer.prefs.peripherals_shared=1;strcpy(writer.prefs.password,"keep secret");
strcpy(writer.prefs.node_name,"keep node");writer.savePrefs(&writer.fs,PrefsSaveRouting::Scope::Common);
assert(writer._common_save_succeeded);const auto disk=writer.fs.files["/com_prefs"];
const size_t legacy=mesh::peripherals::CommonLegacySize+
#ifdef WITH_RS232_BRIDGE
mesh::peripherals::LegacyRs232TailSize;
#else
0;
#endif
assert(disk.size()==legacy+8);Capture capture;
assert(writeCommonPrefsImage(capture,&writer.prefs)&&capture.bytes==disk);
for(uint8_t mode:{0,1}){
writer.prefs.peripherals_shared=mode;writer.savePrefs(&writer.fs,PrefsSaveRouting::Scope::Common);
CommonCLI reader;reader.fs.files=writer.fs.files;reader.prefs.peripherals_shared=9;
reader.loadPrefsInt(&reader.fs,"/com_prefs");assert(reader.prefs.peripherals_shared==mode);
assert(!strcmp(reader.prefs.password,"keep secret")&&!strcmp(reader.prefs.node_name,"keep node"));
}
for(size_t keep=legacy;keep<disk.size();++keep){
CommonCLI reader;reader.fs.files["/com_prefs"]={disk.begin(),disk.begin()+keep};
reader.loadPrefsInt(&reader.fs,"/com_prefs");
assert(reader.prefs.peripherals_shared==(keep==legacy?0:1));
}
for(size_t byte=legacy;byte<disk.size();++byte){
CommonCLI reader;reader.fs.files["/com_prefs"]=disk;reader.fs.files["/com_prefs"][byte]^=1;
reader.loadPrefsInt(&reader.fs,"/com_prefs");assert(reader.prefs.peripherals_shared==1);
}
// The optional UART tail belongs to the source profile, not the reader.
// Both no-UART -> UART and UART -> no-UART upgrades retain the checked mode.
for(bool uart:{false,true}){
std::vector<uint8_t> foreign(disk.begin(),disk.begin()+mesh::peripherals::CommonLegacySize);
if(uart){uint8_t tail[mesh::bridge::rs232power::PREFS_SIZE];
mesh::bridge::rs232power::encodePrefs(tail,1,1,300);foreign.insert(foreign.end(),tail,tail+sizeof(tail));}
CommonCLI old;old.fs.files["/com_prefs"]=foreign;old.loadPrefsInt(&old.fs,"/com_prefs");
assert(old.prefs.peripherals_shared==0);
uint8_t footer[8];mesh::peripherals::encodeFooter(footer,1);foreign.insert(foreign.end(),footer,footer+8);
CommonCLI migrated;migrated.fs.files["/com_prefs"]=foreign;migrated.loadPrefsInt(&migrated.fs,"/com_prefs");
assert(migrated.prefs.peripherals_shared==1);
}
CommonCLI unreadable;unreadable.fs.files["/com_prefs"]=disk;unreadable.fs.fail_read_open=true;
unreadable.loadPrefsInt(&unreadable.fs,"/com_prefs");assert(unreadable.prefs.peripherals_shared==1);
CommonCLI failed;failed.fs.files["/com_prefs"]=disk;failed.fs.fail_write_after=int(legacy+4);
failed.prefs.peripherals_shared=0;failed.savePrefs(&failed.fs,PrefsSaveRouting::Scope::Common);
assert(!failed._common_save_succeeded&&failed.fs.files["/com_prefs"]==disk);
}
'''
for flags in (("NRF52_PLATFORM", "RAK_4631", "ENABLE_OTA"),
("NRF52_PLATFORM", "RAK_4631", "ENABLE_OTA", "WITH_MQTT_BRIDGE"),
("NRF52_PLATFORM", "RAK_4631", "ENABLE_OTA", "WITH_RS232_BRIDGE")):
with self.subTest(flags=flags):
# Only the profile's compile-time UART constant matters here.
compile_run("#define WITH_RS232_BRIDGE_UART 2\n" + code, flags)
def test_actual_companion_images_and_adapter_snapshot(self):
source = (ROOT / "examples/companion_radio/DataStore.cpp").read_text()
methods = "\n".join(method(source, signature) for signature in (
"bool DataStore::loadPrefs(", "bool DataStore::loadPrefsInt(", "bool DataStore::savePrefs("))
code = "#include <helpers/PeripheralModePrefs.h>\n" + COMPANION_HARNESS.split("int main(")[0].replace("@METHODS@", methods)
code += r'''
int main(){
DataStore store;CompanionNodePrefs prefs;prefs.peripherals_shared=1;
strcpy(prefs.node_name,"retained name");prefs.ble_pin=654321;prefs.gps_interval=42;
assert(prefs.getCustom()->setByKey("snapshot_tag","original"));
assert(store.savePrefs(prefs,47.2,-122.1));const auto disk=store.fs.files["/new_prefs"];
assert(disk.size()==244);double lat=0,lon=0;
CompanionNodePrefs loaded;assert(loaded.getCustom()->setByKey("snapshot_tag","loaded"));
assert(store.loadPrefsInt("/new_prefs",loaded,lat,lon));
assert(loaded.peripherals_shared==1&&loaded.ble_pin==654321&&loaded.gps_interval==42);
assert(!strcmp(loaded.node_name,"retained name")&&lat==47.2&&lon==-122.1);
char tag[32];assert(loaded.getCustom()->getByKey("snapshot_tag",tag,sizeof(tag))&&!strcmp(tag,"loaded"));
loaded.getRadioPrefs()->setFreq(919.5f);assert(loaded.freq==919.5f&&prefs.freq!=919.5f);
for(size_t keep=236;keep<244;++keep){
DataStore damaged;damaged.fs.files["/new_prefs"]={disk.begin(),disk.begin()+keep};
CompanionNodePrefs candidate;candidate.peripherals_shared=1;candidate.ble_pin=123456;
const bool ok=damaged.loadPrefsInt("/new_prefs",candidate,lat,lon);
assert(ok==(keep==236));if(ok)assert(candidate.peripherals_shared==0);
else assert(candidate.peripherals_shared==1&&candidate.ble_pin==123456);
}
for(size_t byte=236;byte<244;++byte){
DataStore damaged;damaged.fs.files["/new_prefs"]=disk;damaged.fs.files["/new_prefs"][byte]^=1;
CompanionNodePrefs candidate;candidate.peripherals_shared=1;candidate.ble_pin=123456;
assert(!damaged.loadPrefsInt("/new_prefs",candidate,lat,lon));
assert(candidate.peripherals_shared==1&&candidate.ble_pin==123456);
// A separately chosen, fully validated image remains usable. This does
// not change nRF52's existing primary-file authority/fallback policy.
damaged.fs.files["/known_good"]=disk;
assert(damaged.loadPrefsInt("/known_good",candidate,lat,lon)&&candidate.peripherals_shared==1);
}
DataStore interrupted;interrupted.fs.files["/new_prefs"]=disk;
interrupted.fs.files["/new_prefs.tmp"]={disk.begin(),disk.begin()+240};
CompanionNodePrefs rebooted;assert(interrupted.loadPrefs(rebooted,lat,lon)&&rebooted.peripherals_shared==1);
DataStore fail;fail.fs.files["/new_prefs"]=disk;fail.fs.fail_write_after=240;
prefs.peripherals_shared=0;assert(!fail.savePrefs(prefs,1,2)&&fail.fs.files["/new_prefs"]==disk);
DataStore fresh;CompanionNodePrefs defaults;
assert(fresh.loadPrefs(defaults,lat,lon)&&defaults.peripherals_shared==0);
}
'''
compile_run(code, ("NRF52_PLATFORM", "RAK_4631"), serializers=True)
def test_production_wiring_is_boot_only_and_before_gps_intent(self):
common = (ROOT / "src/helpers/CommonCLI.cpp").read_text()
companion = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
load = method(common, "void CommonCLI::loadPrefs(FILESYSTEM*")
begin = method(companion, "void MyMesh::begin(")
self.assertLess(load.index("loadPrefsInt("), load.index("configureSharedPeripheralMode("))
self.assertLess(begin.index("_store->loadPrefs("), begin.index("configureSharedPeripheralMode("))
self.assertIn("if (!prefs_ready) _prefs.peripherals_shared = 1;", begin)
self.assertNotIn("configureSharedPeripheralMode", method(companion, "bool MyMesh::handleLocalControlCommand("))
self.assertNotIn("configureSharedPeripheralMode", method(common, "void CommonCLI::handleSetCmd("))
for path in ("examples/simple_repeater/MyMesh.cpp", "examples/simple_room_server/MyMesh.cpp",
"examples/simple_sensor/SensorMesh.cpp"):
source = (ROOT / path).read_text()
self.assertLess(source.index("_cli.loadPrefs(_fs)"), source.index("applyGpsPrefs()"))
main = (ROOT / "examples/companion_radio/main.cpp").read_text()
self.assertLess(main.index("the_mesh.begin("), main.index("the_mesh.applyGpsPrefs()"))
if __name__ == "__main__":
unittest.main()
+204
View File
@@ -0,0 +1,204 @@
#!/usr/bin/env python3
"""Execute WisBlock GPS/shared-rail ownership without touching hardware.
The production allocation-free policy is compiled unchanged. GPIO/UART/GNSS
integration regressions live alongside these portable boot/hold cases.
"""
from pathlib import Path
import os
import subprocess
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]
FIXTURE = ROOT / "test/fixtures/wisblock_shared_power_policy.cpp"
ENVIRONMENT = ROOT / "src/helpers/sensors/EnvironmentSensorManager.cpp"
def lifecycle_harness(gps=True, negative=None):
source = ENVIRONMENT.read_text(encoding="utf-8")
mutations = {
"shared_provider_pin": (
"_location->setPinEn(shared_power_rail ? -1 : gps_control_pin);",
"_location->setPinEn(gps_control_pin);"),
"forced_user_on": (
"_location->setPinEn(shared_power_rail ? -1 : gps_control_pin);",
"_location->setPinEn(shared_power_rail ? -1 : gps_control_pin);\n"
" setGpsTelemetryUserEnabled(true);"),
"no_wake_settle": ("if (!was_powered) delay(500);", "// Negative control: use UART before cold receiver settles."),
"no_pad_release": ("pinMode(PIN_GPS_TX, INPUT);\n pinMode(PIN_GPS_RX, INPUT);",
"// Negative control: retain driven serial pads after receiver power-off."),
}
if negative in mutations:
before, after = mutations[negative]
assert source.count(before) == 1, "negative-control source anchor changed"
source = source.replace(before, after, 1)
elif negative == "power_before_uart_end":
before = extract_braced(source, "void EnvironmentSensorManager::stop_gps()")
after = before.replace("if (serialGPSFlag) stopRakGpsUart();", "// Negative control: release UART too late.")
assert after != before, "negative-control UART stop anchor changed"
after = after.replace("applyWisBlockRailPower();", "applyWisBlockRailPower();\n if (serialGPSFlag) stopRakGpsUart();", 1)
source = source.replace(before, after, 1)
elif negative is not None:
raise ValueError("unknown lifecycle negative control")
fixture_name = "rak_gps_power.cpp" if gps else "rak_peripheral_power_no_gps.cpp"
fixture = (ROOT / "test/fixtures" / fixture_name).read_text(encoding="utf-8")
helpers = "\n".join(extract_braced(source, signature) for signature in (
"static uint8_t rakGpsControlActiveLevel(",
"static void setRakGpsControl(",
"static void stopRakGpsUart(",
"static bool serialHasValidGpsSentence(",
"static bool configureRakUbloxReceiver(",
)) if gps else ""
provider = extract_braced(source, "class RAK12500LocationProvider") + ";\n" if gps else ""
signatures = (
"bool EnvironmentSensorManager::begin()",
"bool EnvironmentSensorManager::querySensors(",
"uint8_t EnvironmentSensorManager::getVoltageSensorChannels(",
"uint8_t EnvironmentSensorManager::queryVoltageSensors(",
"int EnvironmentSensorManager::getNumSettings()",
"const char* EnvironmentSensorManager::getSettingName(",
"const char* EnvironmentSensorManager::getSettingValue(",
"bool EnvironmentSensorManager::setSettingValue(",
"void EnvironmentSensorManager::setPowerSavingEnabled(",
"bool EnvironmentSensorManager::supportsSharedPeripheralMode()",
"void EnvironmentSensorManager::applyWisBlockRailPower()",
"uint8_t EnvironmentSensorManager::detectWisBlockStorageHolds()",
"void EnvironmentSensorManager::configureSharedPeripheralMode(",
"bool EnvironmentSensorManager::isGpsPowered()",
"void EnvironmentSensorManager::rakGPSInit()",
"bool EnvironmentSensorManager::gpsIsAwake(",
"void EnvironmentSensorManager::armGpsPowerSavingCycle()",
"void EnvironmentSensorManager::start_gps()",
"void EnvironmentSensorManager::stop_gps()",
"bool EnvironmentSensorManager::gpsUsesSerialUart(",
"bool EnvironmentSensorManager::gpsSerialTransportMayConflict(",
"bool EnvironmentSensorManager::gpsSerialTransportCanYield(",
"bool EnvironmentSensorManager::setGpsSerialTransportBlocked(",
"void EnvironmentSensorManager::loop()",
)
if not gps:
signatures = tuple(signature for signature in signatures if signature not in (
"void EnvironmentSensorManager::rakGPSInit()",
"bool EnvironmentSensorManager::gpsIsAwake(",
"void EnvironmentSensorManager::armGpsPowerSavingCycle()",
"void EnvironmentSensorManager::start_gps()",
"void EnvironmentSensorManager::stop_gps()",
"void EnvironmentSensorManager::loop()",
))
methods = "\n".join(extract_braced(source, signature) for signature in signatures)
return fixture.replace("@HELPERS@", helpers).replace("@PROVIDER@", provider).replace("@METHODS@", methods)
def compile_lifecycle(testcase, cases, defines=(), gps=True, negative=None, auto_store=True):
with tempfile.TemporaryDirectory(prefix="rak-gps-lifecycle-") as folder:
work = Path(folder)
arduino = ARDUINO.replace("#include <deque>", "#include <deque>\n#include <vector>\n#include <string>\n#include <cassert>\n#define INPUT 0")
arduino = arduino.replace("inline void pinMode(int,int){}\ninline void digitalWrite(int,int){}\ninline int digitalRead(int){return HIGH;}", r'''
struct HardwareEvent { std::string name; int a; int b; uint32_t time; };
inline std::vector<HardwareEvent> hardware_events;
inline int pin_values[64]={};
inline void event(const char* name,int a=0,int b=0){hardware_events.push_back({name,a,b,now_ms});}
inline void pinMode(int pin,int mode){event("pinMode",pin,mode);}
inline void digitalWrite(int pin,int value){assert(pin>=0&&pin<64);pin_values[pin]=value;event("write",pin,value);}
inline int digitalRead(int pin){assert(pin>=0&&pin<64);return pin_values[pin];}
''')
for name, content in {"Arduino.h": arduino, "Mesh.h": MESH, "CayenneLPP.h": CAYENNE,
"Wire.h": "#pragma once\nclass TwoWire {};\ninline TwoWire Wire;\n"}.items():
(work / name).write_text(content, encoding="utf-8")
cpp, binary = work / "lifecycle.cpp", work / "lifecycle"
cpp.write_text(lifecycle_harness(gps=gps, negative=negative), encoding="utf-8")
compiled = subprocess.run([
os.environ.get("CXX", "g++"), "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-Wno-unused-parameter", "-fsanitize=address,undefined", "-fno-sanitize-recover=all",
*(["-Wno-unused-variable"] if not gps else []),
"-fno-pie", "-no-pie", f"-DENV_INCLUDE_GPS={int(gps)}", "-DNRF52_PLATFORM",
"-DRAK_BOARD", *(["-DRAK_WISBLOCK_GPS"] if gps else []),
*(["-DOTA_RAK_AUTO_STORE"] if auto_store else []),
"-DWITH_RS232_BRIDGE_GPS_CONFLICT_UART=1", *defines,
"-I" + str(work), "-I" + str(ROOT / "src"), str(cpp),
str(ROOT / "src/helpers/SensorManager.cpp"), "-o", str(binary),
], text=True, capture_output=True, timeout=60)
testcase.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr)
for case in cases:
with testcase.subTest(scenario=case, defines=defines, negative=negative):
result = subprocess.run([str(binary), case], text=True, capture_output=True, timeout=20)
if negative:
testcase.assertNotEqual(result.returncode, 0, "unsafe production variant escaped the regression")
testcase.assertIn("Assertion", result.stderr)
else:
testcase.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def compile_policy(testcase, flags=()):
with tempfile.TemporaryDirectory(prefix="wisblock-gps-power-") as folder:
binary = Path(folder) / "policy"
compiled = subprocess.run([
os.environ.get("CXX", "g++"), "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
*flags, "-I" + str(ROOT / "src"), str(FIXTURE), "-o", str(binary),
], text=True, capture_output=True, timeout=60)
testcase.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr)
executed = subprocess.run([str(binary)], text=True, capture_output=True, timeout=20)
testcase.assertEqual(executed.returncode, 0, executed.stdout + executed.stderr)
class RakGpsPowerTests(unittest.TestCase):
def test_actual_policy_boot_configuration_activity_and_fail_closed_holds(self):
compile_policy(self, ("-DEXPECTED_COMPILE_HOLDS=0",))
def test_actual_compile_time_radio_and_poe_dependencies_are_not_service_toggles(self):
variants = [
(("-DRAK_4631",), 0),
(("-DRAK_3401",), 1),
(("-DETHERNET_ENABLED=1",), 2),
(("-DETHERNET_ENABLED=0",), 2),
(("-DETHERNET_USE_RAK13800=1",), 2),
(("-DETHERNET_USE_RAK13800=0",), 2),
(("-DRAK4631_COMBINED_ETHERNET=1",), 2),
(("-DRAK4631_COMBINED_ETHERNET=0",), 2),
(("-DRAK_3401", "-DETHERNET_ENABLED=0"), 3),
(("-DETHERNET_ENABLED=0", "-DETHERNET_USE_RAK13800=0",
"-DRAK4631_COMBINED_ETHERNET=0"), 2),
]
for flags, expected in variants:
with self.subTest(flags=flags):
compile_policy(self, (*flags, f"-DEXPECTED_COMPILE_HOLDS={expected}"))
def test_actual_rak4631_uart_boot_off_wake_schedule_and_bridge_ownership(self):
compile_lifecycle(self, ("uart", "shared_uart", "repeat", "prefs_before", "powered_status", "hourly", "absent", "storage",
"storage_ids", "i2c", "shared_i2c", "wake_failure", "initial_config_failure",
"wake_output_failure", "wake_rate_failure", "wake_bus_failure"),
("-DRAK_4631",))
def test_actual_radio_and_poe_holds_preserve_the_supply_when_gps_is_off(self):
for defines in (("-DRAK_3401",), ("-DRAK_4631", "-DETHERNET_ENABLED=0")):
with self.subTest(defines=defines):
compile_lifecycle(self, ("held_uart", "held_i2c", "absent"), defines)
def test_actual_dedicated_gps_control_isolated_from_shared_slot_power(self):
for active in (0, 1):
with self.subTest(active_level=active):
compile_lifecycle(self, ("dedicated",),
("-DRAK_4631", "-DPIN_GPS_EN=14", f"-DPIN_GPS_EN_ACTIVE={active}"))
def test_actual_boot_policy_protects_storage_and_poe_even_when_gps_is_not_compiled(self):
for defines in (("-DRAK_4631",), ("-DRAK_3401",), ("-DRAK_4631", "-DETHERNET_ENABLED=0")):
with self.subTest(defines=defines):
compile_lifecycle(self, ("no_gps",), defines, gps=False)
def test_independent_negative_controls_reject_forced_gps_and_unsafe_rail_ownership(self):
for negative in ("shared_provider_pin", "forced_user_on", "no_wake_settle", "power_before_uart_end", "no_pad_release"):
with self.subTest(negative=negative):
compile_lifecycle(self, ("uart",), ("-DRAK_4631",), negative=negative)
def test_fixed_shared_spi_or_sd_storage_is_a_rail_owner_without_auto_probe(self):
for store in ("OTA_QSPI_SHARED_WISBLOCK_SPI", "OTA_SD_STORE"):
with self.subTest(store=store):
compile_lifecycle(self, ("fixed_storage",), ("-DRAK_4631", "-D" + store), auto_store=False)
if __name__ == "__main__":
unittest.main()