Detect SolarXiao MPPT automatically and support Base self telemetry
This commit is contained in:
1 parent
0f7b898bcb
commit
804219e9fe
7 files changed
+318
-56
No files matched your search
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user