Detect SolarXiao MPPT automatically and support Base self telemetry

This commit is contained in:
mikecarper committed 2026-10-21 16:32:03 -07:00
1 parent 0f7b898bcb
commit 804219e9fe
7 files changed
+318 -56

No files matched your search

+3 -3
View File
@@ -4,9 +4,9 @@
# CLI Commands
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`).
For SolarXiao Base/Advanced/MPPT automatic detection, manual overrides, live
battery/MCU readings and charger controls, see [SolarXiao carrier profiles](solarxiao_power.md)
(`get solar`, `get solar readings`, `set solar.board auto`, `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
View File
@@ -13,7 +13,7 @@ Choose firmware with the [firmware picker](firmware_picker.md). For help with a
## Documentation
- [SolarXiao Advanced/MPPT carrier profiles and battery controls](solarxiao_power.md)
- [SolarXiao Base/Advanced/MPPT detection 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)
+62 -17
View File
@@ -1,45 +1,81 @@
# SolarXiao carrier profiles, MPPT and battery controls
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
SolarXiao Repeater firmware supports the CN3791 Base, older BQ24650 Advanced and
LTC4162/INA260 MPPT carriers 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
## Automatic detection and manual overrides
Carrier selection is configured by the operator; missing I2C responses never
identify an Advanced board. `mppt` is the default for existing installations.
The default is `auto` when no carrier selection has been saved. Existing saved
`mppt` or `advanced` selections remain manual overrides after an update.
Use `set solar.board auto` to enable detection on such an installation.
The firmware environment, hardware/OTA identity and flash layout are unchanged.
```
get solar.board
set solar.board advanced
set solar.board auto
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.
Auto mode checks the INA260 manufacturer's ID (`0xfe = 0x5449`) and device ID
(`0xff & 0xfff0 = 0x2270`) at carrier address `0x40`. An address ACK alone is not
enough: another device such as INA219 can share that address. Detection only
reads identity registers; it does not configure/reset either chip. The first
probe occurs in the loop after initialization, then retries every five seconds
until a valid identity is found. A positive result identifies MPPT for these
carrier designs and is retained until reboot or a new board selection.
Temporary communication loss does not relabel a previously detected MPPT.
Without a verified INA260, auto reports `self-only` and keeps retrying. It does
not claim to distinguish Base from Advanced: neither has a digital identity,
and missing responses can also mean an MPPT power/bus fault. Charger access,
saved-override replay and native carrier telemetry stay blocked until a valid
INA260 identity is found. XIAO self readings remain available throughout.
The MPPT carrier's INA260 was previously read with USB power only; detection
does not require the LTC4162 charger to respond at SOLAR/VIN.
Manual settings are available when the physical carrier is known:
| Setting | Behavior |
| --- | --- |
| `set solar.board auto` | Verify INA260 before enabling MPPT; otherwise self-only, retrying. |
| `set solar.board base` | Base/CN3791 capabilities, self readings, no native I2C access. |
| `set solar.board advanced` | Advanced/BQ24650 capabilities, self readings, no native I2C access. |
| `set solar.board mppt` | Enable native access without requiring INA260 detection; existing charger write identity checks still apply. |
For Base or Advanced, an explicit selection reports that design's 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
this profile adds no further physical measurements. It adds carrier detection,
manual overrides and capability reporting; the voltage/temperature history commands
also already exist in builds that include telemetry history.
The [2025 Base Standard schematic](https://github.com/wehooper4/Meshtastic-Hardware/blob/main/XaioSeries/SolarXaio/Base/Standard/Schematic_SolarXaio_Base_2025-10-23.pdf)
uses a CN3791 analog charger with no digital interface or INA monitor. Battery
voltage reaches the XIAO BAT input. Charge status drives an LED, and the other
charger signals are not connected to XIAO measurement/control inputs. Base
therefore needs no extra telemetry driver: only self battery voltage and MCU
temperature are available, just like Advanced. Base Lite publishes a README
but no schematic, so its exact connections are not independently qualified here.
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,
Manual Base/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.
`mppt ...` explain that the selected carrier has no digital charger interface.
Pending auto mode only probes INA260 identity; it does not access the charger.
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
@@ -59,15 +95,24 @@ 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
When auto verifies MPPT or a manual MPPT selection re-enables native access,
the firmware 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.
protection, or write charger defaults. The INA260 identity verifies the monitor;
LTC4162 chemistry and cell-count checks still independently guard charger writes.
Radio target selection remains separate. Choose the existing
`solarxiao_30S_repeater` or `solarxiao_33S_repeater` firmware target to match the
fitted E22 radio module. The
published Base Standard schematic specifies M30S; the pictured 2W Advanced
uses M33S. Charger detection cannot identify the PA variant or select its TX
power limits.
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.
See the [Advanced profile build/host validation](test-results/solarxiao_advanced_2026-10-21.md).
See the [earlier manual Advanced profile validation](test-results/solarxiao_advanced_2026-10-21.md).
This adds no measurements beyond existing self voltage/MCU temperature and has
not been installed on the remote Advanced board.
+53 -19
View File
@@ -8,17 +8,20 @@ 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] = {
const uint8_t images[4][CarrierSelection::IMAGE_SIZE] = {
{'S','X','B',1,0,0xff,0xa5,0x5a},
{'S','X','B',1,1,0xfe,0xa5,0x5a}
{'S','X','B',1,1,0xfe,0xa5,0x5a},
{'S','X','B',1,2,0xfd,0xa5,0x5a},
{'S','X','B',1,3,0xfc,0xa5,0x5a}
};
}
void CarrierSelection::begin() {
type=Mppt; saved=false; healthy=store.available();
type=Auto; saved=false; healthy=store.available();
verified_mppt=false; probed=false; next_probe=0;
if (!healthy || !store.exists()) return;
uint8_t image[IMAGE_SIZE];
if (!store.load(image,sizeof(image)) || image[4]>1
if (!store.load(image,sizeof(image)) || image[4]>Base
|| memcmp(image,images[image[4]],sizeof(image))) {
healthy=false;
return;
@@ -26,6 +29,17 @@ void CarrierSelection::begin() {
type=static_cast<Type>(image[4]); saved=true;
}
void CarrierSelection::service(Bus& bus,uint32_t now) {
if (!isAuto() || verified_mppt || (probed && static_cast<int32_t>(now-next_probe)<0)) return;
probed=true; next_probe=now+5000;
uint16_t manufacturer,device;
// These read-only identity registers do not configure the INA260 or charger.
// A simple address ACK could be a different device (including an INA219).
verified_mppt=bus.read(0x40,0xfe,manufacturer) && manufacturer==0x5449
&& bus.read(0x40,0xff,device) && (device&0xfff0)==0x2270;
// Latch a positive identity for this boot; transient loss must not relabel it.
}
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.
@@ -34,6 +48,7 @@ bool CarrierSelection::select(Type requested) {
return false;
}
type=requested; saved=true; healthy=true;
verified_mppt=false; probed=false; next_probe=0;
return true;
}
@@ -41,20 +56,31 @@ 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");
if (!healthy) snprintf(reply,capacity,"Err - carrier selection unconfirmed; set solar.board auto|advanced|base|mppt");
else if (type==Auto) snprintf(reply,capacity,"> auto source=%s result=%s",saved?"saved":"default",
verified_mppt?"mppt (INA260 ID verified)":"self-only (MPPT unconfirmed; Base/Advanced indistinguishable)");
else snprintf(reply,capacity,"> %s source=%s (manual override)",
type==Advanced?"advanced":type==Base?"base":"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)) {
Type requested=Auto;
bool valid=true;
if (!strcmp(value,"auto")) requested=Auto;
else if (!strcmp(value,"advanced")) requested=Advanced;
else if (!strcmp(value,"base")) requested=Base;
else if (!strcmp(value,"mppt")) requested=Mppt;
else valid=false;
if (!valid) {
snprintf(reply,capacity,"Err - use set solar.board auto|advanced|base|mppt");
} else if (!select(requested)) {
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 if (type==Auto) {
snprintf(reply,capacity,"OK - auto saved; verifying INA260; self ADC/MCU retained; verified MPPT reloads retained charger overrides");
} else if (type==Advanced || type==Base) {
snprintf(reply,capacity,"OK - %s saved; native LTC/INA access disabled; self ADC/MCU telemetry retained",type==Base?"Base":"Advanced");
} else {
snprintf(reply,capacity,"OK - MPPT saved; native access and retained charger overrides re-enabled");
}
@@ -74,16 +100,24 @@ bool CarrierSelection::handle(const char* command,char* reply,size_t capacity,
? "> 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");
snprintf(reply,capacity,"Err - carrier selection unconfirmed; self readings available; set solar.board auto|advanced|base|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");
if (type==Auto) snprintf(reply,capacity,"> mode=auto source=%s %s",saved?"saved":"default",
verified_mppt?"carrier=mppt (INA260 verified); use get mppt for LTC4162 identity/readings"
:"result=self-only; INA260 not verified; Base/Advanced cannot be distinguished");
else snprintf(reply,capacity,"> carrier=%s source=%s charger=%s; %s",
type==Advanced?"advanced":type==Base?"base":"mppt",saved?"configured":"default",
type==Advanced?"BQ24650":type==Base?"CN3791":"LTC4162",
type==Mppt?"use get mppt for live chip identity/readings":"ADC/MCU only; charger settings hardware-only");
} else if (!strcmp(key,"capabilities")) {
snprintf(reply,capacity,type==Advanced
const char* capabilities=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");
: type==Base
? "> Base: battery ADC + MCU temp; CN3791 analog charger has no digital readings/settings; other power signals unwired"
: nativeAllowed()
? "> MPPT: self ADC/MCU + INA260 V/A/W + LTC4162 readings/settings when reachable; use get mppt fields 0"
: "> Auto: self ADC/MCU only until INA260 ID verifies MPPT; Base/Advanced have no digital identity; retrying detection";
snprintf(reply,capacity,"%s",capabilities);
} else {
snprintf(reply,capacity,"Err - use get solar info|readings|capabilities|history; get/set solar.board");
}
+12 -5
View File
@@ -4,25 +4,32 @@
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.
// Only a verified INA260 identifies the MPPT carrier automatically. Missing
// responses leave self-only telemetry; Base and Advanced have no digital ID.
class CarrierSelection {
public:
enum Type : uint8_t { Mppt = 0, Advanced = 1 };
// Preserve the original manual record values for installed selections.
enum Type : uint8_t { Mppt = 0, Advanced = 1, Auto = 2, Base = 3 };
static constexpr size_t IMAGE_SIZE = 8;
private:
Store& store;
Type type = Mppt;
Type type = Auto;
bool healthy = false;
bool saved = false;
bool verified_mppt = false;
bool probed = false;
uint32_t next_probe = 0;
bool select(Type requested);
public:
explicit CarrierSelection(Store& persistence) : store(persistence) {}
void begin();
bool nativeAllowed() const { return healthy && type == Mppt; }
void service(Bus& bus, uint32_t now);
bool nativeAllowed() const { return healthy && (type == Mppt || (type == Auto && verified_mppt)); }
bool isAdvanced() const { return healthy && type == Advanced; }
bool isBase() const { return healthy && type == Base; }
bool isAuto() const { return healthy && type == Auto; }
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);
+13 -3
View File
@@ -94,7 +94,12 @@ void beginSettings(MainBoard& board,FILESYSTEM* fs,bool telemetry_history_availa
selection.begin();
persistence.fs=fs; controller.begin();
}
void service() { if (selection.nativeAllowed()) controller.service(millis()); }
void service() {
const bool native_before=selection.nativeAllowed();
selection.service(transport,millis());
if (!native_before && selection.nativeAllowed()) controller.reload();
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"))) {
@@ -110,9 +115,14 @@ bool handleCommand(const char* command,char* reply,size_t capacity,bool local) {
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()
const char* error=selection.isAdvanced()
? "Err - BQ24650 Advanced has no digital charger/INA interface; use get solar readings"
: "Err - carrier selection unconfirmed; set solar.board advanced|mppt");
: selection.isBase()
? "Err - CN3791 Base has no digital charger/INA interface; use get solar readings"
: selection.isAuto()
? "Err - auto has not verified INA260; self-only; use get solar.board or set solar.board mppt for manual override"
: "Err - carrier selection unconfirmed; set solar.board auto|advanced|base|mppt";
snprintf(reply,capacity,"%s",error);
return true;
}
return controller.handle(command,reply,capacity,local);
+174 -8
View File
@@ -53,6 +53,19 @@ struct MemoryStore : Store {
return !fail_save;
}
};
struct IdentityBus : Bus {
uint16_t manufacturer=0x5449,device=0x2270;
bool online=true;int fail_reg=-1;
unsigned reads=0,writes=0;
std::vector<uint8_t> registers;
bool read(uint8_t address,uint8_t reg,uint16_t& value) override {
assert(address==0x40 && (reg==0xfe || reg==0xff));
++reads;registers.push_back(reg);
if(!online || reg==fail_reg)return false;
value=reg==0xfe?manufacturer:device;return true;
}
bool write(uint8_t,uint8_t,uint16_t) override {++writes;assert(false);return false;}
};
std::string cli(CarrierSelection& c,const char* cmd,float mv=3.876f,
float temp=27.25f,bool history=true) {
char reply[157]={};
@@ -70,11 +83,10 @@ int main(int argc,char** argv) {
if(scenario=="persistence") {
MemoryStore store;CarrierSelection c(store);
assert(!c.nativeAllowed());c.begin();
assert(c.settingsHealthy() && c.nativeAllowed() && !c.isAdvanced());
assert(c.settingsHealthy() && c.isAuto() && !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,"get solar.board"),"auto");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);
@@ -88,6 +100,14 @@ int main(int argc,char** argv) {
CarrierSelection mppt(store);mppt.begin();
assert(mppt.nativeAllowed() && !mppt.isAdvanced());
has(cli(mppt,"get solar.board"),"mppt source=saved");
cli(mppt,"set solar.board base");assert(mppt.isBase() && !mppt.nativeAllowed());
CarrierSelection base(store);base.begin();assert(base.isBase());
has(cli(base,"get solar info"),"CN3791");
has(cli(base,"get solar capabilities"),"Base");
cli(base,"set solar.board auto");assert(base.isAuto() && !base.nativeAllowed());
CarrierSelection automatic(store);automatic.begin();
assert(automatic.isAuto() && !automatic.nativeAllowed());
has(cli(automatic,"get solar.board"),"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;
@@ -143,17 +163,18 @@ int main(int argc,char** argv) {
"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",
for(const char* invalid:{"set solar.board", "set solar.board unknown",
"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());
has(cli(c,invalid),"Err");assert(!store.saves && c.isAuto() && !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"}) {
"get solar history", "set solar.board advanced", "set solar.board mppt",
"set solar.board auto", "set solar.board base"}) {
for(size_t capacity=1;capacity<=157;++capacity) {
std::vector<char> guarded(capacity+16,'~');
char* out=guarded.data()+8;
@@ -162,6 +183,61 @@ int main(int argc,char** argv) {
assert(memchr(out,0,capacity)!=nullptr);
}
}
} else if(scenario=="auto-positive") {
MemoryStore store;CarrierSelection c(store);IdentityBus bus;c.begin();
assert(c.isAuto() && !c.nativeAllowed() && !bus.reads);
c.service(bus,0);assert(c.nativeAllowed() && bus.reads==2 && !bus.writes);
assert(bus.registers==std::vector<uint8_t>({0xfe,0xff}));
assert(!store.saves);has(cli(c,"get solar.board"),"auto");
bus.online=false;const unsigned reads=bus.reads;
c.service(bus,5000);c.service(bus,100000);assert(c.nativeAllowed() && bus.reads==reads);
cli(c,"set solar.board advanced");assert(!c.nativeAllowed());
c.service(bus,100001);assert(bus.reads==reads);
cli(c,"set solar.board auto");assert(c.isAuto() && !c.nativeAllowed());
bus.online=true;bus.device=0x227f;c.service(bus,100002);
assert(c.nativeAllowed() && bus.reads==reads+2);
c.begin();assert(c.isAuto() && !c.nativeAllowed());
has(cli(c,"get solar.board"),"source=saved");
c.service(bus,0xf0000000);assert(c.nativeAllowed());
} else if(scenario=="auto-negative-retry") {
for(unsigned fault=0;fault<5;++fault) {
MemoryStore store;CarrierSelection c(store);IdentityBus bus;c.begin();
if(fault==0)bus.online=false;
if(fault==1)bus.manufacturer=0x5448;
if(fault==2)bus.device=0x2260;
if(fault==3){bus.manufacturer=0;bus.device=0;} // ACK alone is not identity.
if(fault==4)bus.fail_reg=0xff;
c.service(bus,123);assert(!c.nativeAllowed() && !c.isAdvanced() && !c.isBase());
const unsigned reads=bus.reads;assert(reads>=1 && reads<=2 && !bus.writes);
bus.online=true;bus.manufacturer=0x5449;bus.device=0x2270;bus.fail_reg=-1;
c.service(bus,124);c.service(bus,5122);assert(bus.reads==reads && !c.nativeAllowed());
c.service(bus,5123);assert(c.nativeAllowed() && bus.reads==reads+2 && !bus.writes);
assert(!store.saves);
}
} else if(scenario=="auto-wrap-manual-silence") {
MemoryStore store;CarrierSelection c(store);IdentityBus bus;c.begin();bus.online=false;
const uint32_t start=UINT32_MAX-1000;c.service(bus,start);
assert(bus.reads>0 && !c.nativeAllowed());const unsigned reads=bus.reads;
bus.online=true;c.service(bus,UINT32_MAX);c.service(bus,0);c.service(bus,3998);
assert(bus.reads==reads && !c.nativeAllowed());
c.service(bus,3999);assert(c.nativeAllowed() && bus.reads==reads+2);
for(const char* mode:{"set solar.board advanced","set solar.board base","set solar.board mppt"}) {
cli(c,mode);const unsigned before=bus.reads;
c.service(bus,10000);c.service(bus,0xf0000000);assert(bus.reads==before);
}
cli(c,"set solar.board auto");c.begin();assert(!c.nativeAllowed());
store.fail_save=true;cli(c,"set solar.board mppt");blocked(c);
const unsigned before=bus.reads;c.service(bus,1);c.service(bus,99999);
assert(bus.reads==before && !bus.writes);
} else if(scenario=="legacy-manual-records") {
for(uint8_t mode:{0,1}) {
MemoryStore store;store.present=true;
store.bytes={'S','X','B',1,mode,uint8_t(0xff^mode),0xa5,0x5a};
CarrierSelection c(store);IdentityBus bus;c.begin();
assert(c.settingsHealthy() && c.nativeAllowed()==(mode==0));
assert(c.isAdvanced()==(mode==1) && !c.isAuto() && !c.isBase());
c.service(bus,0);c.service(bus,99999);assert(!bus.reads && !bus.writes && !store.saves);
}
} else assert(false);
}
'''
@@ -200,6 +276,8 @@ WIRE = r'''
struct FakeWire {
uint16_t charger[256]={},ina[256]={};
unsigned calls=0,reads=0,writes=0;
bool online=true;
struct Read {uint8_t address,reg;};std::vector<Read> log;
uint8_t address=0,reg=0;size_t cursor=0;
std::vector<uint8_t> tx,rx;
FakeWire() {
@@ -215,6 +293,7 @@ struct FakeWire {
void write(uint8_t b) {tx.push_back(b);}
int endTransmission(bool stop=true) {
assert(!tx.empty());reg=tx[0];
if(!online)return 1;
if(stop && tx.size()==3) {
assert(address==0x68);++writes;charger[reg]=tx[1]|(uint16_t(tx[2])<<8);
}
@@ -222,6 +301,7 @@ struct FakeWire {
}
int requestFrom(uint8_t a,uint8_t n) {
assert(a==address && n==2);++reads;cursor=0;
log.push_back({a,reg});
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;
@@ -255,13 +335,13 @@ std::string cli(const char* command) {
void has(const std::string& text,const char* wanted) {
assert(text.find(wanted)!=std::string::npos);
}
void nativeSilent() {
void nativeSilent(bool periodic=true) {
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();
if(periodic){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];
@@ -351,6 +431,68 @@ int main(int argc,char** argv) {
fs.files["/solar_board"][0]^=1;
beginSettings(board,&fs,MESH_ENABLE_TELEMETRY_HISTORY);
has(cli("get solar.board"),"unconfirmed");nativeSilent();
} else if(scenario=="base-runtime") {
beginSettings(board,&fs,MESH_ENABLE_TELEMETRY_HISTORY);assert(!Wire.calls);
has(cli("set solar.board base"),"OK");nativeSilent();
has(cli("get solar info"),"CN3791");has(cli("get solar capabilities"),"Base");
has(cli("get solar readings"),"battery=3.850V MCU=26.75C");
beginSettings(board,&fs,MESH_ENABLE_TELEMETRY_HISTORY);
assert(selection.isBase());nativeSilent();
} else if(scenario=="auto-pending-replay") {
beginSettings(board,&fs,MESH_ENABLE_TELEMETRY_HISTORY);
assert(!Wire.calls && selection.isAuto() && !selection.nativeAllowed());
has(cli("get solar.board"),"auto");nativeSilent(false);
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));
Wire.ina[0xfe]=0;now_ms=100;service();
assert(!selection.nativeAllowed() && !Wire.writes && Wire.calls>0);
for(auto item:Wire.log)assert(item.address==0x40 && (item.reg==0xfe || item.reg==0xff));
const auto calls=Wire.calls;Wire.ina[0xfe]=0x5449;
now_ms=5099;service();assert(Wire.calls==calls && !Wire.writes);nativeSilent(false);
now_ms=5100;service();assert(selection.nativeAllowed());
assert(Wire.writes==1 && !(Wire.charger[0x14]&2));
CayenneLPP lpp;addTelemetry(lpp);assert(!lpp.channels.empty());
overrides.values[0x14]=2;overrides.encode(image);
fs.files["/solar_power"]=std::vector<uint8_t>(image,image+sizeof(image));
now_ms=10100;service();assert(Wire.writes==1 && !(Wire.charger[0x14]&2));
Wire.online=false;now_ms=15100;service();assert(selection.nativeAllowed());
Wire.online=true;
has(cli("set solar.board auto"),"OK");assert(!selection.nativeAllowed());nativeSilent(false);
now_ms=15101;service();assert(selection.nativeAllowed() && Wire.writes==2 && (Wire.charger[0x14]&2));
beginSettings(board,&fs,MESH_ENABLE_TELEMETRY_HISTORY);
assert(selection.isAuto() && !selection.nativeAllowed());nativeSilent(false);
} else if(scenario=="auto-no-ack") {
Wire.online=false;beginSettings(board,&fs,MESH_ENABLE_TELEMETRY_HISTORY);assert(!Wire.calls);
now_ms=123;service();assert(!selection.nativeAllowed() && !Wire.writes);nativeSilent(false);
const auto calls=Wire.calls;Wire.online=true;now_ms=5122;service();
assert(Wire.calls==calls && !selection.nativeAllowed());
now_ms=5123;service();assert(selection.nativeAllowed() && !Wire.writes);
} else if(scenario=="late-auto-replay") {
beginSettings(board,&fs,MESH_ENABLE_TELEMETRY_HISTORY);
cli("set solar.board mppt");now_ms=100;service();
assert(selection.nativeAllowed() && Wire.calls>0 && !Wire.writes);
cli("set solar.board advanced");nativeSilent(false);
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));
cli("set solar.board auto");assert(!selection.nativeAllowed());
const uint32_t late=UINT32_MAX-1000;
now_ms=late;service();
assert(selection.nativeAllowed() && Wire.writes==1 && !(Wire.charger[0x14]&2));
auto calls=Wire.calls;
// The first replay must be immediate despite the previous low-timestamp
// service. Subsequent cycles must honor five seconds across millis wrap.
now_ms=late;service();now_ms=UINT32_MAX;service();now_ms=0;service();
now_ms=3998;service();assert(Wire.calls==calls && Wire.writes==1);
Wire.charger[0x14]|=2; // Simulate a charger reset needing policy replay.
service();assert(Wire.calls==calls && Wire.writes==1);
now_ms=3999;service();
assert(Wire.calls>calls && Wire.writes==2 && !(Wire.charger[0x14]&2));
calls=Wire.calls;now_ms=4000;service();now_ms=8998;service();
assert(Wire.calls==calls && Wire.writes==2);
now_ms=8999;service();
assert(Wire.calls>calls && Wire.writes==2 && !(Wire.charger[0x14]&2));
} else assert(false);
}
'''
@@ -435,6 +577,30 @@ class SolarXiaoCarrierTests(unittest.TestCase):
def test_metadata_errors_require_a_valid_full_record_or_block_native_access(self):
self.run_scenario("adapter-history", "metadata-faults")
def test_auto_requires_exact_read_only_identity_and_latches_until_reselection(self):
self.run_scenario("selector", "auto-positive")
def test_auto_retries_missing_wrong_and_ack_only_devices_without_charger_io(self):
self.run_scenario("selector", "auto-negative-retry")
def test_auto_timer_wrap_and_all_manual_or_unhealthy_modes_are_silent(self):
self.run_scenario("selector", "auto-wrap-manual-silence")
def test_original_manual_record_bytes_keep_their_meanings(self):
self.run_scenario("selector", "legacy-manual-records")
def test_actual_base_adapter_has_no_native_io_or_invented_measurements(self):
self.run_scenario("adapter-history", "base-runtime")
def test_actual_auto_adapter_reloads_overrides_only_after_positive_detection(self):
self.run_scenario("adapter-history", "auto-pending-replay")
def test_actual_auto_adapter_recovers_from_an_initial_missing_device(self):
self.run_scenario("adapter-history", "auto-no-ack")
def test_actual_late_auto_detection_replays_immediately_and_schedules_across_wrap(self):
self.run_scenario("adapter-history", "late-auto-replay")
if __name__ == "__main__":
unittest.main()