Add explicit SolarXiao Advanced carrier selection and capability reporting
This commit is contained in:
1 parent
204fc5265b
commit
e6b08cafab
11 files changed
+700
-12
No files matched your search
@@ -4,8 +4,9 @@
|
||||
|
||||
# CLI Commands
|
||||
|
||||
For SolarXiao carrier battery readings and charger controls, see
|
||||
[SolarXiao MPPT and battery controls](solarxiao_power.md) (`get mppt`, `set mppt`).
|
||||
For SolarXiao Advanced/MPPT carrier selection, live battery/MCU readings and
|
||||
charger controls, see [SolarXiao carrier profiles](solarxiao_power.md)
|
||||
(`get solar`, `get solar readings`, `set solar.board`, `get mppt`, `set mppt`).
|
||||
|
||||
See [two LoRa profiles](radio_profiles.md) for `radio2`, `tempradio2`, scheduling,
|
||||
RX-only mode, cross-transmission policy, and optional radio preambles.
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ Choose firmware with the [firmware picker](firmware_picker.md). For help with a
|
||||
|
||||
## Documentation
|
||||
|
||||
- [SolarXiao MPPT and battery controls](solarxiao_power.md)
|
||||
- [SolarXiao Advanced/MPPT carrier profiles and battery controls](solarxiao_power.md)
|
||||
|
||||
- [Feature switches by role](role_feature_switches.md) and [Full Companion features](full_companion_features.md)
|
||||
- [Firmware update guide](ota_user_guide.md), [easy LoRa OTA](ota_easy.md), and [scripted LoRa OTA](lora_ota_automation.md)
|
||||
|
||||
+70
-3
@@ -1,10 +1,74 @@
|
||||
# SolarXiao MPPT and battery controls
|
||||
# SolarXiao carrier profiles, MPPT and battery controls
|
||||
|
||||
SolarXiao Repeater firmware includes a native INA260 battery monitor and LTC4162
|
||||
charger interface in Full and Reduced builds. Use USB CLI or the authenticated
|
||||
SolarXiao Repeater firmware supports the older BQ24650 Advanced carrier and the
|
||||
LTC4162/INA260 MPPT carrier in Full and Reduced builds. Use USB CLI or the authenticated
|
||||
administrator CLI over LoRa. Ship mode and
|
||||
saved-policy reset require local USB. Commands do not change RF settings.
|
||||
|
||||
## Selecting the carrier
|
||||
|
||||
Carrier selection is configured by the operator; missing I2C responses never
|
||||
identify an Advanced board. `mppt` is the default for existing installations.
|
||||
The firmware environment, hardware/OTA identity and flash layout are unchanged.
|
||||
|
||||
```
|
||||
get solar.board
|
||||
set solar.board advanced
|
||||
get solar
|
||||
get solar capabilities
|
||||
get solar readings
|
||||
get solar history
|
||||
```
|
||||
|
||||
For the older SolarXiao 2W Advanced, choose `advanced`. This reports the BQ24650
|
||||
hardware capabilities and the live XIAO battery ADC and processor temperature.
|
||||
The existing self telemetry stays on channel 1, with voltage and temperature as
|
||||
different LPP types. It adds no invented charger flags, current, watts, battery
|
||||
NTC temperature or panel measurements. `get solar battery` aliases the self
|
||||
readings; the MCU temperature is not a battery or charger temperature.
|
||||
If a node already reports battery voltage and MCU temperature on channel 1,
|
||||
this profile adds no further physical measurements. It adds explicit carrier
|
||||
selection and capability reporting; the voltage/temperature history commands
|
||||
also already exist in builds that include telemetry history.
|
||||
|
||||
The [2025 Advanced schematic](https://github.com/wehooper4/Meshtastic-Hardware/blob/main/XaioSeries/SolarXaio/Advanced/Standard/Schematic_SolarXaio_ADV_2025-10-23.pdf)
|
||||
has no INA monitor or digital charger interface. Charger status pins drive LEDs,
|
||||
NTC goes only to the charger, and current/voltage/MPP setpoints use hardware
|
||||
resistors, switches and a solder jumper. An OTA update cannot measure those
|
||||
signals or change those setpoints without additional hardware connections.
|
||||
|
||||
Advanced mode suppresses native LTC4162/INA260 polling, register reads/writes,
|
||||
replay and telemetry/history channels 2..13. `get mppt`, `set mppt ...` and
|
||||
`mppt ...` explain that the Advanced carrier has no digital charger interface.
|
||||
The generic sensor scanner still reserves native addresses 0x40 and 0x68, so it
|
||||
cannot bypass that policy. Optional external sensor channels still start at 14.
|
||||
Historical samples retained from a previous carrier selection describe the old
|
||||
samples, not current sensor availability.
|
||||
|
||||
Where telemetry history is included, `get solar history` points to the existing
|
||||
`get telemetry.volt [1-7]` and `get telemetry.temp [1-7]` commands: 30-minute
|
||||
samples, seven days in RAM. History starts filling after boot and resets at
|
||||
reboot. Voltage trends can help assess a remote installation, but are not a
|
||||
measurement of solar power, charge current or charge state. Radio/status and
|
||||
history-transfer commands remain the existing role commands.
|
||||
|
||||
The selection is stored separately in an eight-byte, versioned checked record
|
||||
at `/solar_board`, using atomic publication and full public readback. A corrupt,
|
||||
unreadable or unconfirmed selection blocks native access and replay. Self ADC
|
||||
and MCU readings remain available. An explicit successful `set solar.board`
|
||||
recovers selection. With an unavailable filesystem, the selection cannot be
|
||||
saved and native access remains blocked.
|
||||
|
||||
For the LTC4162 carrier, select `set solar.board mppt`. This re-enables native
|
||||
access and reloads any retained `/solar_power` charger overrides. Changing the
|
||||
carrier does not erase those overrides, change RF/PA settings, modify battery
|
||||
protection, or write charger defaults. Advanced mode is not chip autodetection.
|
||||
A remotely deployed node must first receive compatible firmware before these
|
||||
commands can be used. Deployment still requires its installed target and
|
||||
bootloader compatibility to be checked when it is reachable.
|
||||
|
||||
## MPPT carrier commands
|
||||
|
||||
Start with:
|
||||
|
||||
```
|
||||
@@ -127,6 +191,9 @@ INA260 uses its integrated shunt and does not use these sense-resistor values.
|
||||
|
||||
## Telemetry channels
|
||||
|
||||
The following carrier channels apply to MPPT mode. Advanced mode reports the
|
||||
existing self voltage/MCU temperature on channel 1 and omits native channels.
|
||||
|
||||
Carrier telemetry requires the environmental telemetry permission. Channel 1
|
||||
remains the XIAO internal ADC; it is separate from the 4S carrier battery and its
|
||||
existing startup/low-voltage policy is unchanged.
|
||||
|
||||
@@ -215,7 +215,7 @@ void setup() {
|
||||
#endif
|
||||
mesh::leds::beginSettings(board, regulator_prefs_fs);
|
||||
#if defined(MESH_SOLARXIAO_POWER) && MESH_SOLARXIAO_POWER
|
||||
mesh::solar::beginSettings(regulator_prefs_fs);
|
||||
mesh::solar::beginSettings(board, regulator_prefs_fs, MESH_ENABLE_TELEMETRY_HISTORY);
|
||||
#endif
|
||||
if (!mesh::power::beginRegulatorSettings(board, radio_driver, regulator_prefs_fs)) {
|
||||
MESH_DEBUG_PRINTLN("Regulator selection could not be applied");
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
#include "SolarXiaoCarrier.h"
|
||||
#if defined(MESH_SOLARXIAO_POWER) && MESH_SOLARXIAO_POWER
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh { namespace solar {
|
||||
namespace {
|
||||
// Complete canonical records detect bad version, length, value or complement.
|
||||
// The filesystem adapter also verifies atomic publication and public readback.
|
||||
const uint8_t images[2][CarrierSelection::IMAGE_SIZE] = {
|
||||
{'S','X','B',1,0,0xff,0xa5,0x5a},
|
||||
{'S','X','B',1,1,0xfe,0xa5,0x5a}
|
||||
};
|
||||
}
|
||||
|
||||
void CarrierSelection::begin() {
|
||||
type=Mppt; saved=false; healthy=store.available();
|
||||
if (!healthy || !store.exists()) return;
|
||||
uint8_t image[IMAGE_SIZE];
|
||||
if (!store.load(image,sizeof(image)) || image[4]>1
|
||||
|| memcmp(image,images[image[4]],sizeof(image))) {
|
||||
healthy=false;
|
||||
return;
|
||||
}
|
||||
type=static_cast<Type>(image[4]); saved=true;
|
||||
}
|
||||
|
||||
bool CarrierSelection::select(Type requested) {
|
||||
if (!store.available() || !store.save(images[requested],IMAGE_SIZE)) {
|
||||
// A failed publication/readback may leave either record live on disk.
|
||||
// Block native I2C/replay until an explicit selection succeeds or reloads.
|
||||
healthy=false;
|
||||
return false;
|
||||
}
|
||||
type=requested; saved=true; healthy=true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CarrierSelection::handle(const char* command,char* reply,size_t capacity,
|
||||
float battery_v,float mcu_c,bool history_available) {
|
||||
if (!command || !reply || !capacity) return false;
|
||||
if (!strcmp(command,"get solar.board")) {
|
||||
if (!healthy) snprintf(reply,capacity,"Err - carrier selection unconfirmed; set solar.board advanced|mppt");
|
||||
else snprintf(reply,capacity,"> %s source=%s (configured, not autodetected)",
|
||||
type==Advanced?"advanced":"mppt",saved?"saved":"default");
|
||||
return true;
|
||||
}
|
||||
if (!strncmp(command,"set solar.board",15) && (!command[15] || command[15]==' ')) {
|
||||
const char* value=command+15;
|
||||
while (*value==' ') ++value;
|
||||
if (strcmp(value,"advanced") && strcmp(value,"mppt")) {
|
||||
snprintf(reply,capacity,"Err - use set solar.board advanced|mppt");
|
||||
} else if (!select(!strcmp(value,"advanced")?Advanced:Mppt)) {
|
||||
snprintf(reply,capacity,"Err - carrier save unconfirmed; native access blocked; repeat selection");
|
||||
} else if (type==Advanced) {
|
||||
snprintf(reply,capacity,"OK - Advanced saved; native LTC/INA access disabled; self ADC/MCU telemetry retained");
|
||||
} else {
|
||||
snprintf(reply,capacity,"OK - MPPT saved; native access and retained charger overrides re-enabled");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (strncmp(command,"get solar",9) || (command[9] && command[9]!=' ')) return false;
|
||||
const char* key=command+9; if (*key==' ') ++key;
|
||||
if (!strcmp(key,"readings") || !strcmp(key,"battery")) {
|
||||
char battery[24],temperature[24];
|
||||
if (isfinite(battery_v) && battery_v>0) snprintf(battery,sizeof(battery),"%.3fV",double(battery_v));
|
||||
else snprintf(battery,sizeof(battery),"unavailable");
|
||||
if (isfinite(mcu_c)) snprintf(temperature,sizeof(temperature),"%.2fC",double(mcu_c));
|
||||
else snprintf(temperature,sizeof(temperature),"unavailable");
|
||||
snprintf(reply,capacity,"> XIAO battery=%s MCU=%s (processor); self channel=1",battery,temperature);
|
||||
} else if (!strcmp(key,"history")) {
|
||||
snprintf(reply,capacity,history_available
|
||||
? "> get telemetry.volt [1-7]; get telemetry.temp [1-7]; 30min samples, 7d RAM history; voltage trend does not measure charge current"
|
||||
: "Err - telemetry history is not included in this build");
|
||||
} else if (!healthy) {
|
||||
snprintf(reply,capacity,"Err - carrier selection unconfirmed; self readings available; set solar.board advanced|mppt");
|
||||
} else if (!*key || !strcmp(key,"info")) {
|
||||
snprintf(reply,capacity,"> carrier=%s source=%s charger=%s; %s",
|
||||
type==Advanced?"advanced":"mppt",saved?"configured":"default",
|
||||
type==Advanced?"BQ24650":"LTC4162",
|
||||
type==Advanced?"ADC/MCU only; charger settings hardware-only":"use get mppt for live chip identity/readings");
|
||||
} else if (!strcmp(key,"capabilities")) {
|
||||
snprintf(reply,capacity,type==Advanced
|
||||
? "> Advanced: battery ADC + MCU temp; solar V/I, battery I/W, NTC and charger flags unwired; settings hardware-only"
|
||||
: "> MPPT: self ADC/MCU + INA260 V/A/W + LTC4162 readings/settings when reachable; use get mppt fields 0");
|
||||
} else {
|
||||
snprintf(reply,capacity,"Err - use get solar info|readings|capabilities|history; get/set solar.board");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}} // namespace mesh::solar
|
||||
#endif
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include "SolarXiaoPowerCore.h"
|
||||
|
||||
namespace mesh { namespace solar {
|
||||
|
||||
// Carrier identity is selected by the operator. A missing/unpowered I2C
|
||||
// peripheral cannot distinguish the Advanced board from the MPPT board.
|
||||
class CarrierSelection {
|
||||
public:
|
||||
enum Type : uint8_t { Mppt = 0, Advanced = 1 };
|
||||
static constexpr size_t IMAGE_SIZE = 8;
|
||||
|
||||
private:
|
||||
Store& store;
|
||||
Type type = Mppt;
|
||||
bool healthy = false;
|
||||
bool saved = false;
|
||||
bool select(Type requested);
|
||||
|
||||
public:
|
||||
explicit CarrierSelection(Store& persistence) : store(persistence) {}
|
||||
void begin();
|
||||
bool nativeAllowed() const { return healthy && type == Mppt; }
|
||||
bool isAdvanced() const { return healthy && type == Advanced; }
|
||||
bool settingsHealthy() const { return healthy; }
|
||||
bool handle(const char* command, char* reply, size_t capacity,
|
||||
float xiao_battery_v, float mcu_c, bool history_available);
|
||||
};
|
||||
|
||||
}} // namespace mesh::solar
|
||||
@@ -1,7 +1,9 @@
|
||||
#include "SolarXiaoPower.h"
|
||||
#if defined(MESH_SOLARXIAO_POWER) && MESH_SOLARXIAO_POWER
|
||||
#include "SolarXiaoPowerCore.h"
|
||||
#include "SolarXiaoCarrier.h"
|
||||
#include <helpers/CheckedSettingsFile.h>
|
||||
#include <helpers/FilePresence.h>
|
||||
#include <Wire.h>
|
||||
#include <math.h>
|
||||
|
||||
@@ -55,21 +57,68 @@ public:
|
||||
}
|
||||
};
|
||||
constexpr const char* CarrierStore::path;
|
||||
class SelectionStore : public Store {
|
||||
public:
|
||||
FILESYSTEM* fs=nullptr;
|
||||
static constexpr const char* path="/solar_board";
|
||||
bool available() const override { return fs!=nullptr; }
|
||||
bool exists() const override {
|
||||
bool present=false;
|
||||
// Unknown metadata must force a complete checked read, never a default.
|
||||
return fs && (!mesh::filePresence(fs,path,present) || present);
|
||||
}
|
||||
bool load(uint8_t* data,size_t size) override {
|
||||
return storage::readFixedSettingsFile(fs,path,data,size);
|
||||
}
|
||||
bool save(const uint8_t* data,size_t size) override {
|
||||
return storage::writeCheckedSettingsFile(fs,path,data,size)
|
||||
== AtomicFileWriter::Outcome::Committed;
|
||||
}
|
||||
};
|
||||
constexpr const char* SelectionStore::path;
|
||||
MainBoard* main_board=nullptr;
|
||||
bool history_available=false;
|
||||
CarrierBus transport;
|
||||
CarrierStore persistence;
|
||||
SelectionStore selection_store;
|
||||
CarrierSelection selection(selection_store);
|
||||
Controller controller(transport,persistence);
|
||||
}
|
||||
|
||||
void beginSettings(FILESYSTEM* fs) {
|
||||
void beginSettings(MainBoard& board,FILESYSTEM* fs,bool telemetry_history_available) {
|
||||
const volatile char* marker=meshcore_capability_solarxiao_power;
|
||||
(void)*marker;
|
||||
main_board=&board;
|
||||
history_available=telemetry_history_available;
|
||||
selection_store.fs=fs;
|
||||
selection.begin();
|
||||
persistence.fs=fs; controller.begin();
|
||||
}
|
||||
void service() { controller.service(millis()); }
|
||||
void service() { if (selection.nativeAllowed()) controller.service(millis()); }
|
||||
bool handleCommand(const char* command,char* reply,size_t capacity,bool local) {
|
||||
float battery=NAN,temperature=NAN;
|
||||
if (main_board && command && (!strcmp(command,"get solar readings") || !strcmp(command,"get solar battery"))) {
|
||||
battery=main_board->getBattMilliVolts()/1000.0f;
|
||||
temperature=main_board->getMCUTemperature();
|
||||
}
|
||||
const bool native_before=selection.nativeAllowed();
|
||||
if (selection.handle(command,reply,capacity,battery,temperature,history_available)) {
|
||||
if (!native_before && selection.nativeAllowed()) controller.reload();
|
||||
return true;
|
||||
}
|
||||
if (!command || !reply || !capacity) return false;
|
||||
const bool mppt=(!strncmp(command,"get mppt",8) && (!command[8] || command[8]==' '))
|
||||
|| !strncmp(command,"set mppt ",9) || !strncmp(command,"mppt ",5);
|
||||
if (mppt && !selection.nativeAllowed()) {
|
||||
snprintf(reply,capacity,selection.isAdvanced()
|
||||
? "Err - BQ24650 Advanced has no digital charger/INA interface; use get solar readings"
|
||||
: "Err - carrier selection unconfirmed; set solar.board advanced|mppt");
|
||||
return true;
|
||||
}
|
||||
return controller.handle(command,reply,capacity,local);
|
||||
}
|
||||
void addTelemetry(CayenneLPP& lpp) {
|
||||
if (!selection.nativeAllowed()) return;
|
||||
Snapshot s; controller.snapshot(s);
|
||||
if (s.monitor) {
|
||||
lpp.addVoltage(2,s.battery_v); lpp.addCurrent(2,s.battery_a); lpp.addPower(2,s.battery_w);
|
||||
@@ -89,12 +138,13 @@ void addTelemetry(CayenneLPP& lpp) {
|
||||
for (unsigned i=0;i<3;++i) lpp.addGenericSensor(11+i,s.alerts[i]);
|
||||
}
|
||||
uint8_t voltageChannels(uint8_t* channels,uint8_t capacity) {
|
||||
if (!selection.nativeAllowed()) return 0;
|
||||
const uint8_t count=capacity<4 ? capacity : 4;
|
||||
for (uint8_t i=0;channels && i<count;++i) channels[i]=2+i;
|
||||
return count;
|
||||
}
|
||||
uint8_t queryVoltages(SensorManager::VoltageSensorReading* readings,uint8_t capacity) {
|
||||
if (!readings || !capacity) return 0;
|
||||
if (!selection.nativeAllowed() || !readings || !capacity) return 0;
|
||||
Snapshot s; controller.snapshot(s);
|
||||
const float volts[]={s.battery_v,s.vin_v,s.vout_v,s.charger_vbat_v};
|
||||
const uint8_t count=capacity<4 ? capacity : 4;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
namespace mesh { namespace solar {
|
||||
// Stable carrier channels 2..13 precede optional environmental sensors.
|
||||
constexpr uint8_t FIRST_OPTIONAL_CHANNEL = 14;
|
||||
void beginSettings(FILESYSTEM* fs);
|
||||
void beginSettings(MainBoard& board, FILESYSTEM* fs, bool telemetry_history_available);
|
||||
void service();
|
||||
bool handleCommand(const char* command, char* reply, size_t capacity, bool local);
|
||||
void addTelemetry(CayenneLPP& telemetry);
|
||||
|
||||
@@ -303,6 +303,11 @@ bool Controller::identify(uint16_t& identity) {
|
||||
if (cells) last_cells=cells;
|
||||
return true;
|
||||
}
|
||||
void Controller::reload() {
|
||||
started=false;
|
||||
begin();
|
||||
next_service=0;
|
||||
}
|
||||
bool Controller::verifyWritableIdentity(char* reply,size_t capacity) {
|
||||
uint16_t identity;
|
||||
if (!identify(identity)) {snprintf(reply,capacity,"Err - charger unavailable; power SOLAR/VIN"); return false;}
|
||||
@@ -523,7 +528,7 @@ bool Controller::action(const char* args,bool local,char* reply,size_t capacity)
|
||||
return true;
|
||||
}
|
||||
if (!strcmp(args,"reload")) {
|
||||
started=false; begin(); next_service=0;
|
||||
reload();
|
||||
snprintf(reply,capacity,storage_healthy?"OK - saved settings loaded; apply on service":"Err - saved settings invalid/unavailable"); return true;
|
||||
}
|
||||
if (!strcmp(args,"forget")) {
|
||||
|
||||
@@ -70,6 +70,7 @@ public:
|
||||
settings.defaults();
|
||||
}
|
||||
void begin();
|
||||
void reload();
|
||||
void service(uint32_t now);
|
||||
bool snapshot(Snapshot& result);
|
||||
bool readMonitor(float& volts, float& amps, float& watts);
|
||||
|
||||
@@ -0,0 +1,440 @@
|
||||
"""Exercise the real carrier selector and adapter with persistence/bus faults."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_regulator_settings import FILESYSTEM
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SENSORS = ROOT / "src/helpers/sensors"
|
||||
|
||||
# Compile the production NRF52 FilePresence boundary, including the distinction
|
||||
# between an absent record and failed filesystem metadata. Other file operations
|
||||
# keep using the shared mock so metadata and full-read faults are independent.
|
||||
FILESYSTEM_WITH_METADATA = FILESYSTEM.replace(
|
||||
"struct MockFS;", "struct MockFS;\nstruct lfs_info {};\nconstexpr int LFS_ERR_NOENT=-2;\n"
|
||||
"int lfs_stat(MockFS*,const char*,lfs_info*);"
|
||||
).replace(
|
||||
"unsigned writes=0,renames=0;",
|
||||
"unsigned writes=0,renames=0;bool fail_stat=false;\n"
|
||||
" void _lockFS() {}\n void _unlockFS() {}\n MockFS* _getFS() {return this;}"
|
||||
) + r'''
|
||||
inline int lfs_stat(MockFS* fs,const char* path,lfs_info*) {
|
||||
return fs->fail_stat ? -5 : fs->files.count(path) ? 0 : LFS_ERR_NOENT;
|
||||
}
|
||||
'''
|
||||
|
||||
SELECTOR_HARNESS = r'''
|
||||
#include "SolarXiaoCarrier.h"
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
using namespace mesh::solar;
|
||||
struct MemoryStore : Store {
|
||||
std::vector<uint8_t> bytes;
|
||||
bool present=false, available_now=true, fail_load=false;
|
||||
bool fail_save=false, publish_before_failure=false;
|
||||
unsigned loads=0,saves=0;
|
||||
bool available() const override { return available_now; }
|
||||
bool exists() const override { return present; }
|
||||
bool load(uint8_t* out,size_t n) override {
|
||||
++loads;
|
||||
if(fail_load || bytes.size()!=n)return false;
|
||||
memcpy(out,bytes.data(),n);return true;
|
||||
}
|
||||
bool save(const uint8_t* data,size_t n) override {
|
||||
++saves;
|
||||
if(!fail_save || publish_before_failure) {bytes.assign(data,data+n);present=true;}
|
||||
return !fail_save;
|
||||
}
|
||||
};
|
||||
std::string cli(CarrierSelection& c,const char* cmd,float mv=3.876f,
|
||||
float temp=27.25f,bool history=true) {
|
||||
char reply[157]={};
|
||||
assert(c.handle(cmd,reply,sizeof(reply),mv,temp,history));return reply;
|
||||
}
|
||||
void has(const std::string& text,const char* wanted) {
|
||||
assert(text.find(wanted)!=std::string::npos);
|
||||
}
|
||||
void blocked(CarrierSelection& c) {
|
||||
assert(!c.nativeAllowed() && !c.settingsHealthy() && !c.isAdvanced());
|
||||
has(cli(c,"get solar.board"),"unconfirmed");
|
||||
}
|
||||
int main(int argc,char** argv) {
|
||||
assert(argc==2);const std::string scenario=argv[1];
|
||||
if(scenario=="persistence") {
|
||||
MemoryStore store;CarrierSelection c(store);
|
||||
assert(!c.nativeAllowed());c.begin();
|
||||
assert(c.settingsHealthy() && c.nativeAllowed() && !c.isAdvanced());
|
||||
assert(!store.loads && !store.saves && !store.present);
|
||||
has(cli(c,"get solar.board"),"source=default");
|
||||
has(cli(c,"get solar.board"),"not autodetected");
|
||||
has(cli(c,"get solar capabilities"),"INA260");assert(!store.saves);
|
||||
has(cli(c,"set solar.board advanced"),"OK");
|
||||
assert(c.isAdvanced() && !c.nativeAllowed() && store.saves==1);
|
||||
assert(store.bytes.size()==CarrierSelection::IMAGE_SIZE);
|
||||
CarrierSelection restarted(store);restarted.begin();
|
||||
assert(restarted.isAdvanced() && !restarted.nativeAllowed());
|
||||
has(cli(restarted,"get solar.board"),"source=saved");
|
||||
has(cli(restarted,"get solar info"),"BQ24650");
|
||||
has(cli(restarted,"get solar capabilities"),"unwired");
|
||||
assert(store.saves==1);
|
||||
has(cli(restarted,"set solar.board mppt"),"retained charger overrides");
|
||||
CarrierSelection mppt(store);mppt.begin();
|
||||
assert(mppt.nativeAllowed() && !mppt.isAdvanced());
|
||||
has(cli(mppt,"get solar.board"),"mppt source=saved");
|
||||
} else if(scenario=="invalid-records") {
|
||||
MemoryStore reference;CarrierSelection baseline(reference);baseline.begin();
|
||||
cli(baseline,"set solar.board advanced");const auto valid=reference.bytes;
|
||||
for(size_t byte=0;byte<valid.size();++byte) {
|
||||
MemoryStore store;store.present=true;store.bytes=valid;store.bytes[byte]^=0x40;
|
||||
CarrierSelection c(store);c.begin();blocked(c);assert(!store.saves);
|
||||
}
|
||||
for(size_t size=0;size<=10;++size) {
|
||||
if(size==valid.size())continue;
|
||||
MemoryStore store;store.present=true;store.bytes=valid;store.bytes.resize(size);
|
||||
CarrierSelection c(store);c.begin();blocked(c);assert(!store.saves);
|
||||
}
|
||||
MemoryStore store;store.present=true;store.bytes=valid;store.fail_load=true;
|
||||
CarrierSelection c(store);c.begin();blocked(c);
|
||||
store.fail_load=false;cli(c,"set solar.board advanced");assert(c.isAdvanced());
|
||||
store.available_now=false;c.begin();blocked(c);
|
||||
const unsigned saves=store.saves;has(cli(c,"set solar.board mppt"),"unconfirmed");
|
||||
assert(store.saves==saves);blocked(c);
|
||||
store.available_now=true;c.begin();assert(c.isAdvanced());
|
||||
} else if(scenario=="save-failures") {
|
||||
for(bool published:{false,true}) {
|
||||
MemoryStore store;CarrierSelection c(store);c.begin();
|
||||
store.fail_save=true;store.publish_before_failure=published;
|
||||
has(cli(c,"set solar.board advanced"),"native access blocked");blocked(c);
|
||||
assert(store.present==published);
|
||||
store.fail_save=false;
|
||||
has(cli(c,"set solar.board advanced"),"OK");assert(c.isAdvanced());
|
||||
store.fail_save=true;store.publish_before_failure=published;
|
||||
has(cli(c,"set solar.board mppt"),"unconfirmed");blocked(c);
|
||||
assert(store.bytes[4]==(published?0:1));
|
||||
store.fail_save=false;
|
||||
cli(c,"set solar.board mppt");assert(c.nativeAllowed());
|
||||
}
|
||||
} else if(scenario=="readings-history") {
|
||||
MemoryStore store;CarrierSelection c(store);c.begin();cli(c,"set solar.board advanced");
|
||||
has(cli(c,"get solar readings"),"battery=3.876V MCU=27.25C");
|
||||
has(cli(c,"get solar battery"),"processor");
|
||||
has(cli(c,"get solar readings"),"self channel=1");
|
||||
for(float value:{NAN,INFINITY,-INFINITY}) {
|
||||
has(cli(c,"get solar readings",value,value),"battery=unavailable MCU=unavailable");
|
||||
}
|
||||
for(float value:{0.0f,-1.0f})
|
||||
has(cli(c,"get solar battery",value,-15.5f),"battery=unavailable MCU=-15.50C");
|
||||
has(cli(c,"get solar history"),"get telemetry.volt [1-7]");
|
||||
has(cli(c,"get solar history"),"30min samples, 7d RAM history");
|
||||
has(cli(c,"get solar history",3.8f,25,false),"not included");
|
||||
store.fail_load=true;c.begin();blocked(c);
|
||||
has(cli(c,"get solar readings"),"3.876V");
|
||||
has(cli(c,"get solar history",3.8f,25,false),"not included");
|
||||
} else if(scenario=="parser-bounds") {
|
||||
MemoryStore store;CarrierSelection c(store);c.begin();char reply[157]={};
|
||||
for(const char* unrelated:{"", "get solarpanel", "get solar.boardextra",
|
||||
"set solar.boardextra advanced", "get mppt", "get SOLAR", "solar"}) {
|
||||
assert(!c.handle(unrelated,reply,sizeof(reply),3.8f,25,true));
|
||||
}
|
||||
for(const char* invalid:{"set solar.board", "set solar.board auto",
|
||||
"set solar.board ADVANCED", "set solar.board advanced extra",
|
||||
"set solar.board advanced ", "get solar unknown"}) {
|
||||
has(cli(c,invalid),"Err");assert(!store.saves && c.nativeAllowed());
|
||||
}
|
||||
assert(!c.handle(nullptr,reply,sizeof(reply),3.8f,25,true));
|
||||
assert(!c.handle("get solar",nullptr,sizeof(reply),3.8f,25,true));
|
||||
assert(!c.handle("get solar",reply,0,3.8f,25,true));
|
||||
for(const char* cmd:{"get solar", "get solar info", "get solar.board",
|
||||
"get solar capabilities", "get solar readings", "get solar battery",
|
||||
"get solar history", "set solar.board advanced", "set solar.board mppt"}) {
|
||||
for(size_t capacity=1;capacity<=157;++capacity) {
|
||||
std::vector<char> guarded(capacity+16,'~');
|
||||
char* out=guarded.data()+8;
|
||||
assert(c.handle(cmd,out,capacity,NAN,NAN,true));
|
||||
for(size_t i=0;i<8;++i)assert(guarded[i]=='~' && guarded[8+capacity+i]=='~');
|
||||
assert(memchr(out,0,capacity)!=nullptr);
|
||||
}
|
||||
}
|
||||
} else assert(false);
|
||||
}
|
||||
'''
|
||||
|
||||
SENSOR_MANAGER = r'''
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <cmath>
|
||||
struct CayenneLPP {
|
||||
std::vector<uint8_t> channels;
|
||||
void addVoltage(uint8_t c,float) {channels.push_back(c);}
|
||||
void addCurrent(uint8_t c,float) {channels.push_back(c);}
|
||||
void addPower(uint8_t c,float) {channels.push_back(c);}
|
||||
void addTemperature(uint8_t c,float) {channels.push_back(c);}
|
||||
void addGenericSensor(uint8_t c,float) {channels.push_back(c);}
|
||||
};
|
||||
namespace mesh {
|
||||
struct MainBoard {
|
||||
uint16_t battery_mv=3850;float mcu_c=26.75f;
|
||||
unsigned battery_reads=0,temperature_reads=0;
|
||||
uint16_t getBattMilliVolts() {++battery_reads;return battery_mv;}
|
||||
float getMCUTemperature() {++temperature_reads;return mcu_c;}
|
||||
};
|
||||
struct SensorManager {
|
||||
struct VoltageSensorReading {uint8_t channel;float voltage;bool valid;};
|
||||
};
|
||||
}
|
||||
'''
|
||||
|
||||
WIRE = r'''
|
||||
#pragma once
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
struct FakeWire {
|
||||
uint16_t charger[256]={},ina[256]={};
|
||||
unsigned calls=0,reads=0,writes=0;
|
||||
uint8_t address=0,reg=0;size_t cursor=0;
|
||||
std::vector<uint8_t> tx,rx;
|
||||
FakeWire() {
|
||||
charger[0x43]=0x46b4;charger[0x14]=2;charger[0x29]=1;
|
||||
charger[0x4a]=1;charger[0x39]=7;charger[0x34]=256;
|
||||
charger[0x3a]=17152;charger[0x3b]=8000;charger[0x3c]=7985;
|
||||
charger[0x3d]=uint16_t(-100);charger[0x3e]=200;
|
||||
charger[0x3f]=14000;charger[0x40]=10914;
|
||||
ina[0xfe]=0x5449;ina[0xff]=0x2270;
|
||||
ina[2]=10560;ina[1]=uint16_t(-8);ina[3]=13;
|
||||
}
|
||||
void beginTransmission(uint8_t a) {assert(a==0x68 || a==0x40);address=a;++calls;tx.clear();}
|
||||
void write(uint8_t b) {tx.push_back(b);}
|
||||
int endTransmission(bool stop=true) {
|
||||
assert(!tx.empty());reg=tx[0];
|
||||
if(stop && tx.size()==3) {
|
||||
assert(address==0x68);++writes;charger[reg]=tx[1]|(uint16_t(tx[2])<<8);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
int requestFrom(uint8_t a,uint8_t n) {
|
||||
assert(a==address && n==2);++reads;cursor=0;
|
||||
const uint16_t value=(a==0x68?charger:ina)[reg];
|
||||
rx={uint8_t(a==0x68?value:value>>8),uint8_t(a==0x68?value>>8:value)};
|
||||
return 2;
|
||||
}
|
||||
int available() {return int(rx.size()-cursor);}
|
||||
int read() {return cursor<rx.size()?rx[cursor++]:-1;}
|
||||
};
|
||||
extern FakeWire Wire;
|
||||
'''
|
||||
|
||||
ADAPTER_HARNESS = r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <helpers/IdentityStore.h>
|
||||
#include <helpers/SensorManager.h>
|
||||
#include <Wire.h>
|
||||
#define ATOMIC_FILE_WRITER_IMPLEMENTATION
|
||||
static uint32_t now_ms=100;
|
||||
uint32_t millis() {return now_ms;}
|
||||
int digitalRead(int pin) {assert(pin==30 || pin==31);return 1;}
|
||||
FakeWire Wire;
|
||||
#include "@ADAPTER@"
|
||||
using namespace mesh;
|
||||
using namespace mesh::solar;
|
||||
std::string cli(const char* command) {
|
||||
char reply[157]={};assert(handleCommand(command,reply,sizeof(reply),false));return reply;
|
||||
}
|
||||
void has(const std::string& text,const char* wanted) {
|
||||
assert(text.find(wanted)!=std::string::npos);
|
||||
}
|
||||
void nativeSilent() {
|
||||
const auto calls=Wire.calls,reads=Wire.reads,writes=Wire.writes;
|
||||
for(const char* command:{"get mppt", "get mppt info", "get mppt battery",
|
||||
"get mppt raw 0x43", "get mppt fields 0", "set mppt mppt.enabled off",
|
||||
"mppt reload", "mppt reset", "mppt bsr run", "mppt ship arm",
|
||||
"mppt alerts clear limits 65535"}) has(cli(command),"Err");
|
||||
service();now_ms+=10000;service();
|
||||
CayenneLPP lpp;addTelemetry(lpp);assert(lpp.channels.empty());
|
||||
uint8_t channels[]={99,99,99,99,99};
|
||||
SensorManager::VoltageSensorReading values[5];
|
||||
for(auto& value:values)value={99,99,true};
|
||||
assert(!voltageChannels(channels,5));assert(!queryVoltages(values,5));
|
||||
assert(!voltageChannels(nullptr,0));assert(!queryVoltages(nullptr,0));
|
||||
for(auto c:channels)assert(c==99);
|
||||
for(auto value:values)assert(value.channel==99 && value.voltage==99 && value.valid);
|
||||
assert(calls==Wire.calls && reads==Wire.reads && writes==Wire.writes);
|
||||
}
|
||||
int main(int argc,char** argv) {
|
||||
assert(argc==2);const std::string scenario=argv[1];MockFS fs;MainBoard board;
|
||||
if(scenario=="advanced-runtime") {
|
||||
beginSettings(board,&fs,MESH_ENABLE_TELEMETRY_HISTORY);assert(!Wire.calls);
|
||||
has(cli("set solar.board advanced"),"OK");nativeSilent();
|
||||
assert(fs.exists("/solar_board") && !fs.exists("/solar_power"));
|
||||
const auto calls=Wire.calls;
|
||||
has(cli("get solar readings"),"battery=3.850V MCU=26.75C");
|
||||
assert(board.battery_reads==1 && board.temperature_reads==1);
|
||||
board.battery_mv=3991;board.mcu_c=-12.5f;
|
||||
has(cli("get solar battery"),"battery=3.991V MCU=-12.50C");
|
||||
assert(board.battery_reads==2 && board.temperature_reads==2);
|
||||
has(cli("get solar info"),"BQ24650");has(cli("get solar capabilities"),"unwired");
|
||||
has(cli("get solar history"),MESH_ENABLE_TELEMETRY_HISTORY?"30min":"not included");
|
||||
assert(board.battery_reads==2 && board.temperature_reads==2 && calls==Wire.calls);
|
||||
beginSettings(board,&fs,MESH_ENABLE_TELEMETRY_HISTORY);has(cli("get solar.board"),"advanced source=saved");nativeSilent();
|
||||
board.battery_mv=0;board.mcu_c=NAN;
|
||||
has(cli("get solar readings"),"battery=unavailable MCU=unavailable");
|
||||
} else if(scenario=="invalid-runtime") {
|
||||
fs.files["/solar_board"]={'b','a','d'};beginSettings(board,&fs,MESH_ENABLE_TELEMETRY_HISTORY);assert(!Wire.calls);
|
||||
nativeSilent();has(cli("get solar.board"),"unconfirmed");
|
||||
has(cli("get solar readings"),"3.850V");assert(board.battery_reads==1);
|
||||
has(cli("set solar.board advanced"),"OK");nativeSilent();
|
||||
beginSettings(board,nullptr,MESH_ENABLE_TELEMETRY_HISTORY);nativeSilent();has(cli("get solar.board"),"unconfirmed");
|
||||
has(cli("set solar.board mppt"),"unconfirmed");nativeSilent();
|
||||
has(cli("get solar battery"),"3.850V");
|
||||
} else if(scenario=="reenable-reload") {
|
||||
beginSettings(board,&fs,MESH_ENABLE_TELEMETRY_HISTORY);has(cli("set solar.board advanced"),"OK");nativeSilent();
|
||||
Settings overrides;overrides.defaults();overrides.masks[0x14]=2;overrides.values[0x14]=0;
|
||||
uint8_t image[Settings::IMAGE_SIZE];overrides.encode(image);
|
||||
fs.files["/solar_power"]=std::vector<uint8_t>(image,image+sizeof(image));
|
||||
const auto calls=Wire.calls;
|
||||
has(cli("set solar.board mppt"),"retained charger overrides");
|
||||
assert(Wire.calls==calls && !Wire.writes);
|
||||
service();assert(Wire.calls>calls && Wire.writes==1 && !(Wire.charger[0x14]&2));
|
||||
has(cli("get mppt info"),"FST");
|
||||
CayenneLPP lpp;addTelemetry(lpp);assert(!lpp.channels.empty());
|
||||
assert(std::find(lpp.channels.begin(),lpp.channels.end(),2)!=lpp.channels.end());
|
||||
assert(std::find(lpp.channels.begin(),lpp.channels.end(),13)!=lpp.channels.end());
|
||||
uint8_t channels[5]={99,99,99,99,99};
|
||||
SensorManager::VoltageSensorReading values[5];values[4].channel=99;
|
||||
assert(voltageChannels(channels,5)==4 && queryVoltages(values,5)==4);
|
||||
for(unsigned i=0;i<4;++i)assert(channels[i]==i+2 && values[i].channel==i+2);
|
||||
assert(values[0].valid && fabs(values[0].voltage-13.2f)<0.001f);
|
||||
assert(channels[4]==99 && values[4].channel==99);
|
||||
has(cli("set solar.board advanced"),"OK");nativeSilent();
|
||||
assert(fs.files["/solar_power"]==std::vector<uint8_t>(image,image+sizeof(image)));
|
||||
} else if(scenario=="selection-store-faults") {
|
||||
beginSettings(board,&fs,MESH_ENABLE_TELEMETRY_HISTORY);has(cli("set solar.board advanced"),"OK");
|
||||
const auto advanced=fs.files["/solar_board"];
|
||||
fs.fail_write=true;has(cli("set solar.board mppt"),"unconfirmed");nativeSilent();
|
||||
assert(fs.files["/solar_board"]==advanced);fs.fail_write=false;
|
||||
cli("set solar.board advanced");
|
||||
fs.fail_rename_from="/solar_board.tmp";
|
||||
has(cli("set solar.board mppt"),"unconfirmed");nativeSilent();
|
||||
assert(fs.files["/solar_board"]==advanced);fs.fail_rename_from.clear();
|
||||
cli("set solar.board advanced");fs.corrupt_write=true;
|
||||
has(cli("set solar.board mppt"),"unconfirmed");nativeSilent();
|
||||
assert(fs.files["/solar_board"]==advanced);fs.corrupt_write=false;
|
||||
cli("set solar.board advanced");fs.fail_read_path="/solar_board";
|
||||
has(cli("set solar.board mppt"),"unconfirmed");nativeSilent();
|
||||
assert(fs.files["/solar_board"]!=advanced);fs.fail_read_path.clear();
|
||||
has(cli("set solar.board advanced"),"OK");nativeSilent();
|
||||
beginSettings(board,&fs,MESH_ENABLE_TELEMETRY_HISTORY);assert(selection.isAdvanced());nativeSilent();
|
||||
} else if(scenario=="metadata-faults") {
|
||||
fs.fail_stat=true;
|
||||
beginSettings(board,&fs,MESH_ENABLE_TELEMETRY_HISTORY);
|
||||
has(cli("get solar.board"),"unconfirmed");nativeSilent();
|
||||
has(cli("set solar.board advanced"),"OK");nativeSilent();
|
||||
beginSettings(board,&fs,MESH_ENABLE_TELEMETRY_HISTORY);
|
||||
has(cli("get solar.board"),"advanced source=saved");nativeSilent();
|
||||
fs.fail_read_path="/solar_board";
|
||||
beginSettings(board,&fs,MESH_ENABLE_TELEMETRY_HISTORY);
|
||||
has(cli("get solar.board"),"unconfirmed");nativeSilent();
|
||||
fs.fail_read_path.clear();
|
||||
has(cli("set solar.board advanced"),"OK");nativeSilent();
|
||||
fs.files["/solar_board"][0]^=1;
|
||||
beginSettings(board,&fs,MESH_ENABLE_TELEMETRY_HISTORY);
|
||||
has(cli("get solar.board"),"unconfirmed");nativeSilent();
|
||||
} else assert(false);
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class SolarXiaoCarrierTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
|
||||
if not cls.compiler:
|
||||
raise unittest.SkipTest("host C++11 compiler unavailable")
|
||||
cls.temp = tempfile.TemporaryDirectory(prefix="solarxiao-carrier-test-")
|
||||
cls.addClassCleanup(cls.temp.cleanup)
|
||||
directory = Path(cls.temp.name)
|
||||
(directory / "helpers").mkdir()
|
||||
(directory / "helpers/IdentityStore.h").write_text(FILESYSTEM_WITH_METADATA, encoding="ascii")
|
||||
(directory / "helpers/SensorManager.h").write_text(SENSOR_MANAGER, encoding="ascii")
|
||||
(directory / "Wire.h").write_text(WIRE, encoding="ascii")
|
||||
cls.binaries = {}
|
||||
for name, harness, flags, extra in (
|
||||
("selector", SELECTOR_HARNESS, [], []),
|
||||
("adapter-history", ADAPTER_HARNESS, ["-DNRF52_PLATFORM", "-DMESH_ENABLE_TELEMETRY_HISTORY=1",
|
||||
"-DHIGH=1", "-DPIN_BOARD_SDA=30", "-DPIN_BOARD_SCL=31"],
|
||||
[str(SENSORS / "SolarXiaoPowerCore.cpp")]),
|
||||
("adapter-no-history", ADAPTER_HARNESS, ["-DNRF52_PLATFORM", "-DMESH_ENABLE_TELEMETRY_HISTORY=0",
|
||||
"-DHIGH=1", "-DPIN_BOARD_SDA=30", "-DPIN_BOARD_SCL=31"],
|
||||
[str(SENSORS / "SolarXiaoPowerCore.cpp")]),
|
||||
):
|
||||
cpp = directory / (name + ".cpp")
|
||||
cpp.write_text(harness.replace("@ADAPTER@", str(SENSORS / "SolarXiaoPower.cpp")), encoding="ascii")
|
||||
binary = directory / name
|
||||
built = subprocess.run([cls.compiler, "-std=c++11", "-Wall", "-Wextra", "-Werror",
|
||||
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
|
||||
"-DMESH_SOLARXIAO_POWER=1", *flags, "-I" + str(directory),
|
||||
"-I" + str(ROOT / "src"), "-I" + str(SENSORS), str(cpp),
|
||||
str(SENSORS / "SolarXiaoCarrier.cpp"), *extra, "-o", str(binary)],
|
||||
text=True, capture_output=True, timeout=45)
|
||||
if built.returncode:
|
||||
raise AssertionError(built.stdout + built.stderr)
|
||||
cls.binaries[name] = binary
|
||||
|
||||
def run_scenario(self, binary, scenario):
|
||||
result = subprocess.run([str(self.binaries[binary]), scenario], capture_output=True,
|
||||
text=True, timeout=10)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
def test_explicit_selection_and_restart_persistence(self):
|
||||
self.run_scenario("selector", "persistence")
|
||||
|
||||
def test_corrupt_missing_and_unreadable_records_fail_closed(self):
|
||||
self.run_scenario("selector", "invalid-records")
|
||||
|
||||
def test_ambiguous_publication_requires_explicit_recovery(self):
|
||||
self.run_scenario("selector", "save-failures")
|
||||
|
||||
def test_unavailable_measurements_and_history_build_capability(self):
|
||||
self.run_scenario("selector", "readings-history")
|
||||
|
||||
def test_command_boundaries_and_every_reply_capacity(self):
|
||||
self.run_scenario("selector", "parser-bounds")
|
||||
|
||||
def test_actual_advanced_adapter_has_no_native_io_and_live_self_readings(self):
|
||||
for binary in ("adapter-history", "adapter-no-history"):
|
||||
with self.subTest(binary=binary):
|
||||
self.run_scenario(binary, "advanced-runtime")
|
||||
|
||||
def test_actual_adapter_blocks_native_io_with_unconfirmed_selection(self):
|
||||
self.run_scenario("adapter-history", "invalid-runtime")
|
||||
|
||||
def test_mppt_reenable_reloads_retained_policy_and_restores_telemetry(self):
|
||||
self.run_scenario("adapter-history", "reenable-reload")
|
||||
|
||||
def test_actual_selection_store_write_rename_checksum_and_public_read_faults(self):
|
||||
self.run_scenario("adapter-history", "selection-store-faults")
|
||||
|
||||
def test_history_capability_is_passed_from_the_role_translation_unit(self):
|
||||
adapter = (SENSORS / "SolarXiaoPower.cpp").read_text(encoding="ascii")
|
||||
self.assertNotRegex(adapter, r"(?m)^\s*#\s*(?:if|ifdef|ifndef)\b[^\n]*MESH_ENABLE_TELEMETRY_HISTORY")
|
||||
role = (ROOT / "examples/simple_repeater/main.cpp").read_text(encoding="ascii")
|
||||
self.assertRegex(role, r"mesh::solar::beginSettings\(board,\s*regulator_prefs_fs,\s*MESH_ENABLE_TELEMETRY_HISTORY")
|
||||
|
||||
def test_metadata_errors_require_a_valid_full_record_or_block_native_access(self):
|
||||
self.run_scenario("adapter-history", "metadata-faults")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user