Fix WisBlock GPS power policy and preserve shared peripherals
This commit is contained in:
1 parent
af08c02a00
commit
a2f51253d8
26 files changed
+1830
-107
No files matched your search
Vendored
+444
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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;"
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user