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

+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);