Add native SolarXiao MPPT and battery telemetry and controls

This commit is contained in:
mikecarper committed 2026-10-21 15:25:53 -07:00
1 parent e5c8d28d5e
commit 2dcc1caccb
15 files changed
+1357 -2

No files matched your search

+4
View File
@@ -1,4 +1,5 @@
#include <helpers/RegulatorSettings.h>
#include <helpers/sensors/SolarXiaoPower.h>
#include <helpers/LedSettings.h>
#include <Arduino.h>
#include "CarrierTools.h"
@@ -3026,6 +3027,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
if (mesh::power::handleBatteryChargeCommand(*_board, command, reply, 160)) return;
#endif
if (mesh::power::handleRegulatorCommand(command, reply, 157)) return;
#if defined(MESH_SOLARXIAO_POWER) && MESH_SOLARXIAO_POWER
if (mesh::solar::handleCommand(command, reply, 157, sender_timestamp == 0)) return;
#endif
if (mesh::leds::handleCommand(command, reply, 157)) return;
if (mesh::handleUsbLoggingWatchdogCommand(command, reply, 160)) return;
if (mesh::powerdiag::handleCommand(command, reply, 157, millis())) return;
@@ -1,4 +1,5 @@
#include "EnvironmentSensorManager.h"
#include "SolarXiaoPower.h"
#include "I2CAddressClaimPolicy.h"
#include "EnvironmentI2CConfig.h"
#include "NmeaSentenceProbe.h"
@@ -1018,6 +1019,11 @@ bool EnvironmentSensorManager::begin() {
continue;
}
const uint8_t address = static_cast<uint8_t>(configured_address);
#if defined(MESH_SOLARXIAO_POWER) && MESH_SOLARXIAO_POWER
// Native carrier drivers own these addresses in Full and Reduced builds.
// In particular, never allow an INA219 initializer to configure the INA260.
if (TELEM_WIRE == &Wire && (address == 0x40 || address == 0x68)) continue;
#endif
if (!probed[address]) {
probed[address] = true;
detected[address] = i2c_probe(*TELEM_WIRE, address);
@@ -1029,6 +1035,9 @@ bool EnvironmentSensorManager::begin() {
const SensorDef& def = SENSOR_TABLE[i];
if (!mesh::isValidI2cPeripheralAddress(def.address)) continue;
const uint8_t address = static_cast<uint8_t>(def.address);
#if defined(MESH_SOLARXIAO_POWER) && MESH_SOLARXIAO_POWER
if (TELEM_WIRE == &Wire && (address == 0x40 || address == 0x68)) continue;
#endif
#ifdef RAK_WISBLOCK_GPS
if (mesh::shouldSkipSensorAtClaimedGpsAddress(
i2cGPSFlag, TELEM_WIRE == &Wire,
@@ -1074,6 +1083,10 @@ bool EnvironmentSensorManager::begin() {
bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) {
next_available_channel = TELEM_CHANNEL_SELF + 1;
#if defined(MESH_SOLARXIAO_POWER) && MESH_SOLARXIAO_POWER
next_available_channel = mesh::solar::FIRST_OPTIONAL_CHANNEL;
if (requester_permissions & TELEM_PERM_ENVIRONMENT) mesh::solar::addTelemetry(telemetry);
#endif
#if ENV_INCLUDE_GPS
queryGpsTelemetry(requester_permissions, telemetry);
@@ -1096,10 +1109,15 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen
uint8_t EnvironmentSensorManager::getVoltageSensorChannels(
uint8_t channels[], uint8_t capacity) const {
uint8_t count = 0;
uint8_t first_channel = TELEM_CHANNEL_SELF + 1;
#if defined(MESH_SOLARXIAO_POWER) && MESH_SOLARXIAO_POWER
count = mesh::solar::voltageChannels(channels, capacity);
first_channel = mesh::solar::FIRST_OPTIONAL_CHANNEL;
#endif
for (int i = 0; i < _active_sensor_count; i++) {
if (_active_sensors[i].query_voltage == NULL) continue;
if (count < capacity && channels != NULL) {
channels[count] = (uint8_t)(TELEM_CHANNEL_SELF + 1 + i);
channels[count] = (uint8_t)(first_channel + i);
}
count++;
}
@@ -1112,11 +1130,16 @@ uint8_t EnvironmentSensorManager::queryVoltageSensors(
if (!_wisblock_power.railEnabled()) return 0;
#endif
uint8_t count = 0;
uint8_t first_channel = TELEM_CHANNEL_SELF + 1;
#if defined(MESH_SOLARXIAO_POWER) && MESH_SOLARXIAO_POWER
count = mesh::solar::queryVoltages(readings, capacity);
first_channel = mesh::solar::FIRST_OPTIONAL_CHANNEL;
#endif
for (int i = 0; i < _active_sensor_count && count < capacity; i++) {
const ActiveSensor& sensor = _active_sensors[i];
if (sensor.query_voltage == NULL) continue;
VoltageSensorReading& reading = readings[count++];
reading.channel = (uint8_t)(TELEM_CHANNEL_SELF + 1 + i);
reading.channel = (uint8_t)(first_channel + i);
reading.voltage = 0.0f;
reading.valid = sensor.query_voltage(sensor.sub_channel,
reading.voltage)
+108
View File
@@ -0,0 +1,108 @@
#include "SolarXiaoPower.h"
#if defined(MESH_SOLARXIAO_POWER) && MESH_SOLARXIAO_POWER
#include "SolarXiaoPowerCore.h"
#include <helpers/CheckedSettingsFile.h>
#include <Wire.h>
#include <math.h>
namespace mesh { namespace solar {
extern const char meshcore_capability_solarxiao_power[] = "MESHCORE_CAPABILITY:solarxiao_power:v1";
namespace {
class CarrierBus : public Bus {
bool ready(uint8_t address) {
return (address==0x68 || address==0x40)
&& digitalRead(PIN_BOARD_SDA)==HIGH && digitalRead(PIN_BOARD_SCL)==HIGH;
}
public:
bool read(uint8_t address, uint8_t reg, uint16_t& value) override {
if (!ready(address)) return false;
Wire.beginTransmission(address);
Wire.write(reg);
if (Wire.endTransmission(false)!=0) return false;
if (Wire.requestFrom(address,uint8_t(2))!=2) {
while (Wire.available()) Wire.read();
return false;
}
const uint8_t first=Wire.read(), second=Wire.read();
// LTC4162 SMBus words are little endian; INA260 registers are big endian.
value=address==0x68 ? uint16_t(first)|(uint16_t(second)<<8)
: (uint16_t(first)<<8)|second;
return true;
}
bool write(uint8_t address,uint8_t reg,uint16_t value) override {
if (address!=0x68 || !ready(address)) return false;
Wire.beginTransmission(address);
Wire.write(reg); Wire.write(uint8_t(value)); Wire.write(uint8_t(value>>8));
return Wire.endTransmission()==0;
}
};
class CarrierStore : public Store {
public:
FILESYSTEM* fs=nullptr;
static constexpr const char* path="/solar_power";
bool available() const override { return fs!=nullptr; }
bool exists() const override { return fs && fs->exists(path); }
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 {
if (!fs || size!=Settings::IMAGE_SIZE) return false;
AtomicFileWriter writer(fs,path);
if (!writer || writer.write(data,size)!=size || !writer.commit()) return false;
uint8_t checked[Settings::IMAGE_SIZE];
return load(checked,size) && !memcmp(checked,data,size);
}
};
constexpr const char* CarrierStore::path;
CarrierBus transport;
CarrierStore persistence;
Controller controller(transport,persistence);
}
void beginSettings(FILESYSTEM* fs) {
const volatile char* marker=meshcore_capability_solarxiao_power;
(void)*marker;
persistence.fs=fs; controller.begin();
}
void service() { controller.service(millis()); }
bool handleCommand(const char* command,char* reply,size_t capacity,bool local) {
return controller.handle(command,reply,capacity,local);
}
void addTelemetry(CayenneLPP& lpp) {
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);
}
if (!s.charger) return;
if (isfinite(s.vin_v)) {
lpp.addVoltage(3,s.vin_v); lpp.addCurrent(3,s.iin_a);
lpp.addPower(3,fmaxf(0,s.vin_v*s.iin_a));
}
if (isfinite(s.vout_v)) lpp.addVoltage(4,s.vout_v);
if (isfinite(s.charger_vbat_v)) lpp.addVoltage(5,s.charger_vbat_v);
if (isfinite(s.ibat_a)) lpp.addCurrent(5,s.ibat_a);
if (isfinite(s.die_c)) lpp.addTemperature(6,s.die_c);
if (isfinite(s.ntc_c)) lpp.addTemperature(7,s.ntc_c);
lpp.addGenericSensor(8,s.state); lpp.addGenericSensor(9,s.status);
lpp.addGenericSensor(10,s.system);
for (unsigned i=0;i<3;++i) lpp.addGenericSensor(11+i,s.alerts[i]);
}
uint8_t voltageChannels(uint8_t* channels,uint8_t capacity) {
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;
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;
for (uint8_t i=0;i<count;++i) {
readings[i].channel=2+i;
readings[i].valid=(i==0?s.monitor:s.charger) && isfinite(volts[i]);
readings[i].voltage=readings[i].valid ? volts[i] : 0;
}
return count;
}
}} // namespace mesh::solar
#endif
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#if defined(MESH_SOLARXIAO_POWER) && MESH_SOLARXIAO_POWER
#include <helpers/IdentityStore.h>
#include <helpers/SensorManager.h>
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 service();
bool handleCommand(const char* command, char* reply, size_t capacity, bool local);
void addTelemetry(CayenneLPP& telemetry);
uint8_t voltageChannels(uint8_t* channels, uint8_t capacity);
uint8_t queryVoltages(SensorManager::VoltageSensorReading* readings, uint8_t capacity);
}}
#endif
+630
View File
@@ -0,0 +1,630 @@
#include "SolarXiaoPowerCore.h"
#if defined(MESH_SOLARXIAO_POWER) && MESH_SOLARXIAO_POWER
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
namespace mesh { namespace solar {
namespace {
constexpr uint8_t CHARGER = 0x68;
constexpr uint8_t MONITOR = 0x40;
enum Kind : uint8_t { Number, Signed, Boolean, Inverted, DieC, InputMa,
InputMv, BatteryMa, InputCurrentMa, ChargeMa, VinMv, VoutMv, BatteryMv, BsrOhm, CellMv };
enum Access : uint8_t { Read, Write, Locked };
struct Field {
const char* name;
uint8_t reg;
uint16_t mask;
uint8_t shift;
Kind kind;
Access access;
const char* unit;
};
// Registers and bitfields from LTC4162-F Rev A. Only FST-compatible settings
// are writable; identity and fixed-profile fields never reach Bus::write().
const Field fields[] = {
{"alert.battery_low_mv",1,0xffff,0,BatteryMv,Write,"mV"},
{"alert.battery_high_mv",2,0xffff,0,BatteryMv,Write,"mV"},
{"alert.input_low_mv",3,0xffff,0,VinMv,Write,"mV"},
{"alert.input_high_mv",4,0xffff,0,VinMv,Write,"mV"},
{"alert.output_low_mv",5,0xffff,0,VoutMv,Write,"mV"},
{"alert.output_high_mv",6,0xffff,0,VoutMv,Write,"mV"},
{"alert.input_high_raw",7,0xffff,0,Signed,Write,"ADC"},
{"alert.input_high_ma",7,0xffff,0,InputCurrentMa,Write,"mA"},
{"alert.battery_low_ma",8,0xffff,0,BatteryMa,Write,"mA"},
{"alert.die_high_c",9,0xffff,0,DieC,Write,"C"},
{"alert.resistance_raw",10,0xffff,0,Number,Write,"ADC"},
{"alert.ntc_high_raw",11,0xffff,0,Signed,Write,"ADC"},
{"alert.ntc_low_raw",12,0xffff,0,Signed,Write,"ADC"},
{"alert.limits_mask",13,0xcfff,0,Number,Write,"mask"},
{"alert.states_mask",14,0x1b7f,0,Number,Write,"mask"},
{"alert.status_mask",15,0x003f,0,Number,Write,"mask"},
{"thermal.start_c",16,0xffff,0,DieC,Write,"C"},
{"thermal.end_c",17,0xffff,0,DieC,Write,"C"},
{"charge.enabled",0x14,0x20,5,Inverted,Write,""},
{"telemetry.fast",0x14,8,3,Boolean,Write,""},
{"telemetry.force",0x14,4,2,Boolean,Write,""},
{"mppt.enabled",0x14,2,1,Boolean,Write,""},
{"input.limit_ma",0x15,63,0,InputMa,Write,"mA"},
{"input.minimum_mv",0x16,255,0,InputMv,Write,"mV"},
{"charge.current_code",0x1a,31,0,Number,Read,"code"},
{"charge.current_ma",0x1a,31,0,ChargeMa,Write,"mA"},
{"charge.cell_mv",0x1b,31,0,CellMv,Write,"mV/cell"},
{"termination.current_ma",0x1c,0xffff,0,BatteryMa,Write,"mA"},
{"termination.cv_seconds",0x1d,0xffff,0,Number,Write,"s"},
{"termination.total_minutes",0x1e,0xffff,0,Number,Write,"min"},
{"jeita.t1_raw",0x1f,0xffff,0,Signed,Locked,"ADC"},
{"jeita.t2_raw",0x20,0xffff,0,Signed,Locked,"ADC"},
{"jeita.t3_raw",0x21,0xffff,0,Signed,Locked,"ADC"},
{"jeita.t4_raw",0x22,0xffff,0,Signed,Locked,"ADC"},
{"jeita.t5_raw",0x23,0xffff,0,Signed,Locked,"ADC"},
{"jeita.t6_raw",0x24,0xffff,0,Signed,Locked,"ADC"},
{"jeita.voltages_65",0x25,0x03ff,0,Number,Locked,"packed"},
{"jeita.voltages_432",0x26,0x7fff,0,Number,Locked,"packed"},
{"jeita.currents_65",0x27,0x03ff,0,Number,Locked,"packed"},
{"jeita.currents_432",0x28,0x7fff,0,Number,Locked,"packed"},
{"termination.enabled",0x29,4,2,Boolean,Write,""},
{"jeita.enabled",0x29,1,0,Boolean,Write,""},
{"absorb.delta_code",0x2a,31,0,Number,Locked,"code"},
{"absorb.seconds",0x2b,0xffff,0,Number,Locked,"s"},
{"recharge.mv",0x2e,0xffff,0,BatteryMv,Locked,"mV"},
{"timer.charge_minutes",0x30,0xffff,0,Number,Read,"min"},
{"timer.cv_seconds",0x31,0xffff,0,Number,Read,"s"},
{"timer.absorb_seconds",0x32,0xffff,0,Number,Read,"s"},
{"state",0x34,0x1b7f,0,Number,Read,"mask"},
{"status",0x35,63,0,Number,Read,"mask"},
{"alerts.limits",0x36,0xcfff,0,Number,Read,"mask"},
{"alerts.states",0x37,0x1b7f,0,Number,Read,"mask"},
{"alerts.status",0x38,63,0,Number,Read,"mask"},
{"system",0x39,0x1ff,0,Number,Read,"mask"},
{"battery.mv",0x3a,0xffff,0,BatteryMv,Read,"mV"},
{"input.mv",0x3b,0xffff,0,VinMv,Read,"mV"},
{"output.mv",0x3c,0xffff,0,VoutMv,Read,"mV"},
{"battery.ma",0x3d,0xffff,0,BatteryMa,Read,"mA"},
{"input.current_raw",0x3e,0xffff,0,Signed,Read,"ADC"},
{"input.ma",0x3e,0xffff,0,InputCurrentMa,Read,"mA"},
{"die.c",0x3f,0xffff,0,DieC,Read,"C"},
{"ntc.raw",0x40,0xffff,0,Signed,Read,"ADC"},
{"battery.resistance_ohm",0x41,0xffff,0,BsrOhm,Read,"ohm"},
{"jeita.region",0x42,7,0,Number,Read,"region"},
{"chemistry",0x43,0x0f00,8,Number,Read,"code"},
{"cells",0x43,15,0,Number,Read,"cells"},
{"dac.charge_code",0x44,31,0,Number,Read,"code"},
{"dac.charge_ma",0x44,31,0,ChargeMa,Read,"mA"},
{"dac.cell_mv",0x45,31,0,CellMv,Read,"mV/cell"},
{"dac.input_ma",0x46,63,0,InputMa,Read,"mA"},
{"battery.filtered_mv",0x47,0xffff,0,BatteryMv,Read,"mV"},
{"resistance.current_ma",0x48,0xffff,0,BatteryMa,Read,"mA"},
{"telemetry.valid",0x4a,1,0,Boolean,Read,""},
{"resistance.questionable",0x4a,2,1,Boolean,Read,""},
{"dac.input_mv",0x4b,255,0,InputMv,Read,"mV"},
};
constexpr size_t FIELD_COUNT = sizeof(fields) / sizeof(fields[0]);
const Field* findField(const char* key) {
for (const auto& field : fields) if (strcmp(key, field.name) == 0) return &field;
return nullptr;
}
uint32_t crc(const uint8_t* data, size_t size) {
uint32_t value = 0xffffffff;
while (size--) {
value ^= *data++;
for (unsigned bit=0; bit<8; ++bit)
value = (value >> 1) ^ (0xedb88320UL & (0U - (value & 1)));
}
return ~value;
}
void put16(uint8_t* p, uint16_t n) { p[0]=n; p[1]=n>>8; }
uint16_t get16(const uint8_t* p) { return uint16_t(p[0]) | uint16_t(p[1])<<8; }
void put32(uint8_t* p, uint32_t n) { for (unsigned i=0;i<4;++i) p[i]=n>>(8*i); }
uint32_t get32(const uint8_t* p) {
uint32_t n=0; for (unsigned i=0;i<4;++i) n|=uint32_t(p[i])<<(8*i); return n;
}
bool number(const char* text, double& value) {
if (!text || !*text || *text==' ' || *text=='\t') return false;
char* end=nullptr; value=strtod(text,&end);
return end!=text && *end==0 && isfinite(value);
}
bool integer(const char* text, unsigned max, unsigned& value) {
double parsed; if (!number(text,parsed) || parsed<0 || parsed>max || floor(parsed)!=parsed) return false;
value=unsigned(parsed); return true;
}
bool signedKind(Kind kind) {
return kind==Signed || kind==DieC || kind==BatteryMa || kind==InputCurrentMa || kind==VinMv
|| kind==VoutMv || kind==BatteryMv;
}
double decode(const Field& f, uint16_t word, const Settings& settings, uint8_t cells) {
const uint16_t code=(word&f.mask)>>f.shift;
const double n=signedKind(f.kind) ? static_cast<int16_t>(code) : code;
switch (f.kind) {
case DieC:return n*0.0215-264.4;
case InputMa:return (n+1)*500.0/settings.rsnsi_mohm;
case InputMv:return (n+1)*140.625;
case BatteryMa:return n*1.466/settings.rsnsb_mohm;
case InputCurrentMa:return n*1.466/settings.rsnsi_mohm;
case ChargeMa:return (n+1)*1000.0/settings.rsnsb_mohm;
case VinMv:return n*1.649;
case VoutMv:return n*1.653;
case BatteryMv:return cells ? n*cells*0.1924 : NAN;
case BsrOhm:return cells ? n*cells*settings.rsnsb_mohm/500000.0 : NAN;
case CellMv:return 3412.5+n*12.5;
case Inverted:return !code;
default:return n;
}
}
bool encode(const Field& f, const char* text, const Settings& settings,
uint8_t cells, uint16_t& bits) {
double value;
if (f.kind==Boolean || f.kind==Inverted) {
if (!strcmp(text,"on")) value=1;
else if (!strcmp(text,"off")) value=0;
else return false;
if (f.kind==Inverted) value=!value;
} else {
if (!number(text,value)) return false;
switch (f.kind) {
case DieC:
if (value < -40 || value > 125) return false;
value=round((value+264.4)/0.0215); break;
case InputMa:
if (value<=0 || value>3200) return false;
value=floor(value*settings.rsnsi_mohm/500.0)-1; break;
case InputMv:
if (value<=0 || value>35000) return false;
value=ceil(value/140.625)-1; break;
case BatteryMa:value=round(value*settings.rsnsb_mohm/1.466); break;
case InputCurrentMa:value=round(value*settings.rsnsi_mohm/1.466); break;
case ChargeMa:
if (value<=0 || value>3200) return false;
value=floor(value*settings.rsnsb_mohm/1000.0)-1; break;
case CellMv:
if (value<3412.5 || value>3600) return false;
value=floor((value-3412.5)/12.5); break;
case VinMv:value=round(value/1.649); break;
case VoutMv:value=round(value/1.653); break;
case BatteryMv:
if (!cells) return false;
value=round(value/(cells*0.1924)); break;
default: if (floor(value)!=value) return false; break;
}
}
const double minimum=signedKind(f.kind) ? -32768 : 0;
const double maximum=signedKind(f.kind) ? 32767 : (f.mask>>f.shift);
if (!isfinite(value) || value<minimum || value>maximum) return false;
const uint16_t code=static_cast<uint16_t>(static_cast<int32_t>(value));
if ((static_cast<uint32_t>(code)<<f.shift)&~static_cast<uint32_t>(f.mask)) return false;
bits=static_cast<uint16_t>(code<<f.shift); return true;
}
} // namespace
uint16_t Controller::writableMask(uint8_t reg) {
uint16_t mask=0;
for (const auto& field:fields)
if (field.reg==reg && field.access==Write) mask|=field.mask;
return mask;
}
bool Controller::readableRegister(uint8_t reg) {
if (reg==0x19) return true;
for (const auto& field:fields) if (field.reg==reg) return true;
return false;
}
const char* Controller::chemistryName(uint8_t value) {
static const char* const names[]={"LAD","L42","L41","L40","FAD","FFS","FST","unknown","SST","SAD"};
return value<10 ? names[value] : "unknown";
}
const char* Controller::stateName(uint16_t state) {
switch(state) {
case 4096:return "battery_detection_failed";
case 2048:return "detecting_battery";
case 512:return "absorb";
case 256:return "suspended";
case 64:return "charging";
case 32:return "temperature_pause";
case 16:return "timer_terminated";
case 8:return "current_terminated";
case 4:return "charge_time_fault";
case 2:return "battery_missing";
case 1:return "battery_short";
default:return "unknown";
}
}
void Settings::defaults() {
memset(masks,0,sizeof(masks)); memset(values,0,sizeof(values));
rsnsb_mohm=rsnsi_mohm=10;
ntc_bias_ohm=ntc_r25_ohm=10000; ntc_beta=3490; calibration_user_mask=0;
}
bool Settings::valid() const {
const float calibration[]={rsnsb_mohm,rsnsi_mohm,ntc_bias_ohm,ntc_r25_ohm,ntc_beta};
const float minimum[]={0.1f,0.1f,100,100,1000};
const float maximum[]={1000,1000,1000000,1000000,10000};
for (unsigned i=0;i<5;++i)
if (!isfinite(calibration[i]) || calibration[i]<minimum[i] || calibration[i]>maximum[i]) return false;
if (calibration_user_mask&~31) return false;
for (unsigned reg=0;reg<REG_COUNT;++reg) {
if (masks[reg]&~Controller::writableMask(reg)) return false;
if (values[reg]&~masks[reg]) return false;
// Full-word targets must not contain a partial mask from a damaged record.
if (Controller::writableMask(reg)==0xffff && masks[reg] && masks[reg]!=0xffff) return false;
}
for (unsigned reg=16;reg<=17;++reg)
if (masks[reg] && (values[reg]<10437 || values[reg]>18112)) return false;
if (masks[0x1c] && static_cast<int16_t>(values[0x1c])<0) return false;
if (masks[0x1b] && values[0x1b]>15) return false;
if (masks[16] && masks[17] && static_cast<int16_t>(values[16])>=static_cast<int16_t>(values[17])) return false;
return true;
}
void Settings::encode(uint8_t image[IMAGE_SIZE]) const {
memset(image,0,IMAGE_SIZE); memcpy(image,"SXPW",4); image[4]=1; image[5]=6;
image[6]=calibration_user_mask;
size_t offset=8;
for (unsigned i=0;i<REG_COUNT;++i) {
put16(image+offset,masks[i]); put16(image+offset+2,values[i]); offset+=4;
}
const float calibration[]={rsnsb_mohm,rsnsi_mohm,ntc_bias_ohm,ntc_r25_ohm,ntc_beta};
for (float value:calibration) {
static_assert(sizeof(float)==4,"Settings require IEEE binary32");
uint32_t word; memcpy(&word,&value,4); put32(image+offset,word); offset+=4;
}
put32(image+IMAGE_SIZE-4,crc(image,IMAGE_SIZE-4));
}
bool Settings::decode(const uint8_t image[IMAGE_SIZE]) {
if (memcmp(image,"SXPW",4) || image[4]!=1 || image[5]!=6 || image[7]
|| get32(image+IMAGE_SIZE-4)!=crc(image,IMAGE_SIZE-4)) return false;
Settings candidate; candidate.defaults(); candidate.calibration_user_mask=image[6];
size_t offset=8;
for (unsigned i=0;i<REG_COUNT;++i) {
candidate.masks[i]=get16(image+offset); candidate.values[i]=get16(image+offset+2); offset+=4;
}
float* calibration[]={&candidate.rsnsb_mohm,&candidate.rsnsi_mohm,&candidate.ntc_bias_ohm,
&candidate.ntc_r25_ohm,&candidate.ntc_beta};
for (auto value:calibration) {uint32_t word=get32(image+offset); memcpy(value,&word,4); offset+=4;}
if (!candidate.valid()) return false;
*this=candidate; return true;
}
void Controller::begin() {
if (started) return;
started=true; serviced=false; settings.defaults(); storage_healthy=store.available();
if (store.exists()) {
uint8_t image[Settings::IMAGE_SIZE];
storage_healthy=store.load(image,sizeof(image)) && settings.decode(image);
}
}
bool Controller::identify(uint16_t& identity) {
if (!bus.read(CHARGER,0x43,identity)) return false;
const uint8_t chemistry=(identity>>8)&15;
if (!strcmp(chemistryName(chemistry),"unknown")) return false;
const uint8_t cells=identity&15;
if (cells>9) return false;
if (cells) last_cells=cells;
return true;
}
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;}
if (((identity>>8)&15)!=6 || ((identity&15)!=0 && (identity&15)!=4)) {
snprintf(reply,capacity,"Err - writes require LTC4162-FST on this 4S carrier; detected %s cells=%u",
chemistryName((identity>>8)&15),identity&15); return false;
}
return true;
}
bool Controller::update(uint8_t reg,uint16_t mask,uint16_t bits) {
if (!mask || (mask&~writableMask(reg)) || (bits&~mask)) return false;
uint16_t old;
if (!bus.read(CHARGER,reg,old)) return false;
uint16_t desired=(old&~mask)|bits;
if (reg==0x14) desired&=~0x10; // Never replay the one-shot run_bsr bit.
if ((old&mask)==bits) return true;
if (!bus.write(CHARGER,reg,desired)) return false;
uint16_t checked;
return bus.read(CHARGER,reg,checked) && (checked&mask)==bits;
}
bool Controller::save(const Settings& candidate) {
if (!candidate.valid() || !storage_healthy || !store.available()) return false;
uint8_t image[Settings::IMAGE_SIZE]; candidate.encode(image);
if (!store.save(image,sizeof(image))) {storage_healthy=false; return false;}
settings=candidate; return true;
}
void Controller::service(uint32_t now) {
if (!started || (serviced && static_cast<int32_t>(now-next_service)<0)) return;
serviced=true;
next_service=now+5000;
uint16_t identity;
if (!identify(identity)) {apply_ok=false; return;}
apply_ok=true;
if (!storage_healthy) return;
if (((identity>>8)&15)!=6 || ((identity&15) && (identity&15)!=4)) {apply_ok=false; return;}
// Restore an explicit suspend before other settings; explicit resume is last.
if ((settings.masks[0x14]&0x20) && (settings.values[0x14]&0x20)
&& !update(0x14,0x20,0x20)) {apply_ok=false; return;}
uint16_t start,end;
if (settings.masks[16] || settings.masks[17]) {
if (!bus.read(CHARGER,16,start) || !bus.read(CHARGER,17,end)) {apply_ok=false; return;}
const int16_t target_start=settings.masks[16] ? settings.values[16] : start;
const int16_t target_end=settings.masks[17] ? settings.values[17] : end;
if (target_start>=target_end) {apply_ok=false; return;}
const bool end_first=target_start>=static_cast<int16_t>(end);
const uint8_t order[2]={uint8_t(end_first?17:16),uint8_t(end_first?16:17)};
for (uint8_t reg:order) if (settings.masks[reg]
&& !update(reg,settings.masks[reg],settings.values[reg])) {apply_ok=false; return;}
}
// JEITA must be disabled before applying manual targets. When enabled, the
// chip continuously replaces those targets from its fixed JEITA table.
uint16_t config;
if (!bus.read(CHARGER,0x29,config)) {apply_ok=false; return;}
if (settings.masks[0x29] && !update(0x29,settings.masks[0x29],settings.values[0x29])) {apply_ok=false; return;}
const bool jeita=settings.masks[0x29]&1 ? settings.values[0x29]&1 : config&1;
for (unsigned reg=1;reg<Settings::REG_COUNT;++reg) {
if (reg==16 || reg==17) continue;
if (reg==0x29 || (jeita && (reg==0x1a || reg==0x1b))) continue;
uint16_t mask=settings.masks[reg];
if (reg==0x14) mask&=~0x20;
if (mask && !update(reg,mask,settings.values[reg]&mask)) {apply_ok=false; return;}
}
if (settings.masks[0x14]&0x20)
apply_ok=update(0x14,0x20,settings.values[0x14]&0x20);
}
bool Controller::readMonitor(float& volts,float& amps,float& watts) {
uint16_t manufacturer,device,v,i,p;
if (!bus.read(MONITOR,0xfe,manufacturer) || !bus.read(MONITOR,0xff,device)
|| manufacturer!=0x5449 || (device&0xfff0)!=0x2270
|| !bus.read(MONITOR,2,v) || !bus.read(MONITOR,1,i) || !bus.read(MONITOR,3,p)) return false;
volts=v*0.00125f; amps=static_cast<int16_t>(i)*0.00125f; watts=p*0.01f;
return true;
}
bool Controller::snapshot(Snapshot& result) {
result=Snapshot{};
result.vin_v=result.vout_v=result.charger_vbat_v=result.ibat_a=result.iin_a=NAN;
result.die_c=result.ntc_c=result.bsr_ohm=NAN;
result.monitor=readMonitor(result.battery_v,result.battery_a,result.battery_w);
uint16_t id;
if (!identify(id)) return result.monitor;
result.identity=id; result.chemistry=(id>>8)&15; result.cells=id&15;
const uint8_t regs[]={0x14,0x29,0x34,0x35,0x39,0x4a,0x36,0x37,0x38,
0x3a,0x3b,0x3c,0x3d,0x3e,0x3f,0x40,0x41};
uint16_t* values[]={&result.config,&result.charger_config,&result.state,&result.status,
&result.system,&result.telemetry,&result.alerts[0],&result.alerts[1],&result.alerts[2],
&result.raw_vbat,&result.raw_vin,&result.raw_vout,&result.raw_ibat,&result.raw_iin,
&result.raw_die,&result.raw_ntc,&result.raw_bsr};
for (unsigned i=0;i<sizeof(regs);++i) if (!bus.read(CHARGER,regs[i],*values[i])) return result.monitor;
result.charger=true;
// Do not label a retained ADC bank as a live reading while its ADC is off.
result.adc_active=(result.telemetry&1) && ((result.config&4) || (result.system&4));
if (!result.adc_active || result.chemistry<4 || result.chemistry>6) return true;
result.vin_v=static_cast<int16_t>(result.raw_vin)*0.001649f;
result.vout_v=static_cast<int16_t>(result.raw_vout)*0.001653f;
if (result.cells) {
result.charger_vbat_v=static_cast<int16_t>(result.raw_vbat)*result.cells*0.0001924f;
if (!(result.telemetry&2))
result.bsr_ohm=result.raw_bsr*result.cells*settings.rsnsb_mohm/500000.0f;
}
result.ibat_a=static_cast<int16_t>(result.raw_ibat)*0.001466f/settings.rsnsb_mohm;
result.iin_a=static_cast<int16_t>(result.raw_iin)*0.001466f/settings.rsnsi_mohm;
result.die_c=static_cast<int16_t>(result.raw_die)*0.0215f-264.4f;
const int raw=static_cast<int16_t>(result.raw_ntc);
if (raw>0 && raw<21684) {
const float resistance=settings.ntc_bias_ohm*raw/(21829.0f-raw);
result.ntc_c=1.0f/(1.0f/298.15f+logf(resistance/settings.ntc_r25_ohm)/settings.ntc_beta)-273.15f;
}
return true;
}
bool Controller::setField(const char* key,const char* value,char* reply,size_t capacity) {
const char* calibration[]={"cal.rsnsb_mohm","cal.rsnsi_mohm","cal.ntc_bias_ohm","cal.ntc_r25_ohm","cal.ntc_beta"};
for (unsigned i=0;i<5;++i) if (!strcmp(key,calibration[i])) {
double parsed;
Settings candidate=settings;
float* targets[]={&candidate.rsnsb_mohm,&candidate.rsnsi_mohm,&candidate.ntc_bias_ohm,
&candidate.ntc_r25_ohm,&candidate.ntc_beta};
if (!number(value,parsed)) {snprintf(reply,capacity,"Err - finite numeric calibration required"); return true;}
*targets[i]=parsed; candidate.calibration_user_mask|=1<<i;
if (!candidate.valid()) snprintf(reply,capacity,"Err - calibration out of range");
else if (!save(candidate)) snprintf(reply,capacity,"Err - settings save unconfirmed; use mppt reload");
else snprintf(reply,capacity,"OK - %s=%.6g saved; register targets unchanged",key,parsed);
return true;
}
const Field* field=findField(key);
if (!field) {snprintf(reply,capacity,"Err - unknown setting; get mppt fields <page>"); return true;}
if (field->access!=Write) {
snprintf(reply,capacity,"Err - %s is %s",key,field->access==Locked ? "locked by FST charging profile" : "read-only (hardware/state)"); return true;
}
if (!storage_healthy || !store.available()) {snprintf(reply,capacity,"Err - settings storage unavailable; use mppt reload"); return true;}
if (!verifyWritableIdentity(reply,capacity)) return true;
uint16_t bits;
// A valid cell count is needed for per-cell-scaled alert thresholds. Do not
// substitute the last known count while the hardware reports unknown.
uint16_t identity;
if (!identify(identity) || !encode(*field,value,settings,identity&15,bits)) {
snprintf(reply,capacity,"Err - invalid value/range or cell count unknown; %s (%s)",key,field->unit); return true;
}
uint16_t other;
if (field->reg==0x16 && (!bus.read(CHARGER,0x14,other) || (other&2))) {
snprintf(reply,capacity,"Err - disable mppt.enabled before setting manual input.minimum_mv"); return true;
}
if ((field->reg==0x1a || field->reg==0x1b)
&& (!bus.read(CHARGER,0x29,other) || (other&1))) {
snprintf(reply,capacity,"Err - disable jeita.enabled before setting manual charge targets"); return true;
}
if (field->reg==16 || field->reg==17) {
if (!bus.read(CHARGER,field->reg==16?17:16,other)
|| (field->reg==16 ? static_cast<int16_t>(bits)>=static_cast<int16_t>(other)
: static_cast<int16_t>(bits)<=static_cast<int16_t>(other))) {
snprintf(reply,capacity,"Err - thermal.start_c must be below thermal.end_c"); return true;
}
}
if (field->reg==0x1c && static_cast<int16_t>(bits)<0) {
snprintf(reply,capacity,"Err - termination current must be nonnegative"); return true;
}
uint16_t before;
if (!bus.read(CHARGER,field->reg,before)) {snprintf(reply,capacity,"Err - charger read failed"); return true;}
Settings candidate=settings;
candidate.masks[field->reg]|=field->mask;
candidate.values[field->reg]=(candidate.values[field->reg]&~field->mask)|bits;
if (!candidate.valid()) {snprintf(reply,capacity,"Err - setting conflicts with saved policy; use mppt forget"); return true;}
if (!update(field->reg,field->mask,bits)) {
const bool restored=update(field->reg,field->mask,before&field->mask);
snprintf(reply,capacity,"Err - write/readback failed; rollback=%s",restored?"verified":"unconfirmed"); return true;
}
if (!save(candidate)) {
const bool restored=update(field->reg,field->mask,before&field->mask);
snprintf(reply,capacity,"Err - save unconfirmed; live rollback=%s; use mppt reload",restored?"verified":"unconfirmed"); return true;
}
apply_ok=true;
snprintf(reply,capacity,"OK - %s=%.6g %s saved, readback verified",key,
decode(*field,bits,settings,identity&15),field->unit);
return true;
}
bool Controller::getField(const char* key,char* reply,size_t capacity) {
const char* calibration[]={"cal.rsnsb_mohm","cal.rsnsi_mohm","cal.ntc_bias_ohm","cal.ntc_r25_ohm","cal.ntc_beta"};
const float values[]={settings.rsnsb_mohm,settings.rsnsi_mohm,settings.ntc_bias_ohm,settings.ntc_r25_ohm,settings.ntc_beta};
for (unsigned i=0;i<5;++i) if (!strcmp(key,calibration[i])) {
snprintf(reply,capacity,"> %s=%.6g source=%s",key,double(values[i]),settings.calibration_user_mask&(1<<i)?"user":"reference"); return true;
}
const Field* field=findField(key);
if (!field) return false;
uint16_t identity,word;
if (!identify(identity) || !bus.read(CHARGER,field->reg,word)) {
snprintf(reply,capacity,"Err - charger unavailable; power SOLAR/VIN"); return true;
}
if ((field->kind==CellMv || field->kind==BatteryMv || field->kind==BsrOhm)
&& (((identity>>8)&15)<4 || ((identity>>8)&15)>6)) {
snprintf(reply,capacity,"> %s=unsupported chemistry; raw=%04X",key,word); return true;
}
if (field->access==Read && ((field->reg>=0x3a && field->reg<=0x41) || field->reg==0x47 || field->reg==0x48)) {
uint16_t cfg,system,valid;
if (!bus.read(CHARGER,0x14,cfg) || !bus.read(CHARGER,0x39,system)
|| !bus.read(CHARGER,0x4a,valid) || !(valid&1) || (!(cfg&4) && !(system&4))) {
snprintf(reply,capacity,"> %s=unavailable ADC inactive; raw=%04X",key,word); return true;
}
}
const double value=decode(*field,word,settings,identity&15);
if (!isfinite(value)) snprintf(reply,capacity,"> %s=unknown cells; raw=%04X",key,word);
else if (field->kind==Boolean || field->kind==Inverted)
snprintf(reply,capacity,"> %s=%s raw=%04X",key,value?"on":"off",word);
else snprintf(reply,capacity,"> %s=%.7g %s raw=%04X%s",key,value,field->unit,word,
field->access==Locked?" locked":"");
return true;
}
bool Controller::action(const char* args,bool local,char* reply,size_t capacity) {
if (!strcmp(args,"reset")) {
if (!local) {snprintf(reply,capacity,"Err - reset requires local USB"); return true;}
Settings candidate; candidate.defaults();
storage_healthy=store.available();
if (!save(candidate)) snprintf(reply,capacity,"Err - default policy save unconfirmed");
else snprintf(reply,capacity,"OK - saved policy/calibration reset; live charger unchanged");
return true;
}
if (!strcmp(args,"reload")) {
started=false; begin(); next_service=0;
snprintf(reply,capacity,storage_healthy?"OK - saved settings loaded; apply on service":"Err - saved settings invalid/unavailable"); return true;
}
if (!strcmp(args,"forget")) {
Settings candidate=settings;
memset(candidate.masks,0,sizeof(candidate.masks)); memset(candidate.values,0,sizeof(candidate.values));
if (!save(candidate)) snprintf(reply,capacity,"Err - override removal save unconfirmed");
else snprintf(reply,capacity,"OK - saved register overrides removed; live charger unchanged");
return true;
}
if (!strcmp(args,"bsr run") || !strcmp(args,"ship arm") || !strcmp(args,"ship disarm")) {
const bool ship=!strncmp(args,"ship ",5);
if (ship && !local) {snprintf(reply,capacity,"Err - ship mode requires local USB"); return true;}
if (!verifyWritableIdentity(reply,capacity)) return true;
bool ok;
if (ship) ok=bus.write(CHARGER,0x19,!strcmp(args,"ship arm")?21325:0);
else {
uint16_t cfg; ok=bus.read(CHARGER,0x14,cfg) && bus.write(CHARGER,0x14,cfg|0x10);
}
snprintf(reply,capacity,ok?"OK - action requested; not saved":"Err - action write failed"); return true;
}
const char* groups[]={"limits","states","status"};
for (unsigned i=0;i<3;++i) {
char prefix[32]; snprintf(prefix,sizeof(prefix),"alerts clear %s ",groups[i]);
if (strncmp(args,prefix,strlen(prefix))) continue;
unsigned mask;
const uint16_t allowed[]={0xcfff,0x1b7f,0x3f};
if (!integer(args+strlen(prefix),65535,mask) || !mask || (mask&~allowed[i])) {
snprintf(reply,capacity,"Err - invalid alert mask"); return true;
}
if (!verifyWritableIdentity(reply,capacity)) return true;
// Alerts are W0C: ones preserve all unselected (including newly arrived) alerts.
const bool ok=bus.write(CHARGER,0x36+i,static_cast<uint16_t>(~mask));
snprintf(reply,capacity,ok?"OK - selected alerts cleared":"Err - alert clear failed"); return true;
}
return false;
}
bool Controller::handle(const char* command,char* reply,size_t capacity,bool local) {
if (!command || !reply || capacity<2) return false;
const bool get=!strncmp(command,"get mppt",8) && (!command[8] || command[8]==' ');
const bool set=!strncmp(command,"set mppt ",9);
const bool act=!strncmp(command,"mppt ",5);
if (!get && !set && !act) return false;
if (set) {
char key[48],value[48],tail;
if (sscanf(command+9,"%47s %47s %c",key,value,&tail)!=2)
snprintf(reply,capacity,"Err - use set mppt <field> <value>; get mppt fields <page>");
else setField(key,value,reply,capacity);
return true;
}
if (act) {
if (!action(command+5,local,reply,capacity)) snprintf(reply,capacity,"Err - mppt reload|forget|reset|bsr run|ship arm/disarm|alerts clear <group> <mask>");
return true;
}
const char* key=command+8; if (*key==' ') ++key;
if (!strncmp(key,"fields",6) && (!key[6] || key[6]==' ')) {
unsigned page=0;
if (key[6] && !integer(key+7,100,page)) {snprintf(reply,capacity,"Err - fields page must be an integer"); return true;}
const size_t start=page*3;
if (start>=FIELD_COUNT) {snprintf(reply,capacity,"> end; fields=%u",unsigned(FIELD_COUNT)); return true;}
size_t used=snprintf(reply,capacity,"> page=%u ",page);
for (size_t i=start;i<FIELD_COUNT && i<start+3 && used<capacity;++i)
used+=snprintf(reply+used,capacity-used,"%s[%s]%s",fields[i].name,
fields[i].access==Write?"rw":fields[i].access==Locked?"locked":"ro",i+1<start+3?" ":"");
return true;
}
if (!strncmp(key,"raw ",4)) {
unsigned reg;
if (!integer(key+4,255,reg) || !readableRegister(reg)) snprintf(reply,capacity,"Err - unknown/read-protected register");
else {uint16_t id,value; if (!identify(id) || !bus.read(CHARGER,reg,value)) snprintf(reply,capacity,"Err - charger read failed");
else snprintf(reply,capacity,"> addr=68 reg=%02X raw=%04X",reg,value);}
return true;
}
if (!strcmp(key,"saved")) {
unsigned count=0; for (uint16_t mask:settings.masks) if (mask) ++count;
snprintf(reply,capacity,"> storage=%s overrides=%u apply=%s rsnsb=%.3gmOhm rsnsi=%.3gmOhm cal_user=%02X",
storage_healthy?"ok":"unavailable",count,apply_ok?"verified":"pending",double(settings.rsnsb_mohm),double(settings.rsnsi_mohm),settings.calibration_user_mask); return true;
}
if (*key && getField(key,reply,capacity)) return true;
Snapshot s; snapshot(s);
if (!*key || !strcmp(key,"info")) {
if (!s.charger) snprintf(reply,capacity,"> LTC4162=unavailable INA260=%s; power SOLAR/VIN for charger access",s.monitor?"online":"unavailable");
else snprintf(reply,capacity,"> LTC4162-%s chem=%u cells=%u last=%u MPPT=%s JEITA=%s state=%s ADC=%s saved=%s",
chemistryName(s.chemistry),s.chemistry,s.cells,last_cells,s.config&2?"on":"off",
s.charger_config&1?"on":"off",stateName(s.state),s.adc_active?"on":"off",storage_healthy?"ok":"bad");
} else if (!strcmp(key,"battery")) {
if (!s.monitor) snprintf(reply,capacity,"Err - INA260 unavailable");
else snprintf(reply,capacity,"> INA260 %.4fV %+.5fA %.3fW; charger=%.4fV %+.5fA NTC=%.2fC model=%s",
double(s.battery_v),double(s.battery_a),double(s.battery_w),double(s.charger_vbat_v),double(s.ibat_a),double(s.ntc_c),
(settings.calibration_user_mask&28)==28?"user":"reference");
} else if (!strcmp(key,"power")) {
if (!s.charger) snprintf(reply,capacity,"Err - charger unavailable; power SOLAR/VIN");
else snprintf(reply,capacity,"> ADC=%s VIN=%.4fV IIN=%+.5fA VOUT=%.4fV die=%.2fC NTC=%.2fC BSR=%.5fohm",
s.adc_active?"on":"off",double(s.vin_v),double(s.iin_a),double(s.vout_v),double(s.die_c),double(s.ntc_c),double(s.bsr_ohm));
} else if (!strcmp(key,"alerts")) {
if (!s.charger) snprintf(reply,capacity,"Err - charger unavailable");
else snprintf(reply,capacity,"> limits=%04X states=%04X status=%04X state=%04X/%s charge_status=%04X system=%04X telemetry=%04X",
s.alerts[0],s.alerts[1],s.alerts[2],s.state,stateName(s.state),s.status,s.system,s.telemetry);
} else snprintf(reply,capacity,"Err - use get mppt info|battery|power|alerts|saved|fields <page>|raw <reg>|<field>");
return true;
}
}} // namespace mesh::solar
#endif
+87
View File
@@ -0,0 +1,87 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
// Portable charger logic. The board adapter owns Wire and filesystem access.
namespace mesh { namespace solar {
class Bus {
public:
virtual ~Bus() {}
virtual bool read(uint8_t address, uint8_t reg, uint16_t& value) = 0;
virtual bool write(uint8_t address, uint8_t reg, uint16_t value) = 0;
};
class Store {
public:
virtual ~Store() {}
virtual bool available() const = 0;
virtual bool exists() const = 0;
virtual bool load(uint8_t* data, size_t size) = 0;
virtual bool save(const uint8_t* data, size_t size) = 0;
};
struct Settings {
static constexpr size_t REG_COUNT = 0x30;
static constexpr size_t IMAGE_SIZE = 224;
uint16_t masks[REG_COUNT];
uint16_t values[REG_COUNT];
// Reference schematic values, configurable for other carrier revisions.
float rsnsb_mohm, rsnsi_mohm, ntc_bias_ohm, ntc_r25_ohm, ntc_beta;
uint8_t calibration_user_mask;
void defaults();
bool valid() const;
void encode(uint8_t image[IMAGE_SIZE]) const;
bool decode(const uint8_t image[IMAGE_SIZE]);
};
struct Snapshot {
bool charger, monitor, adc_active;
uint8_t chemistry, cells;
uint16_t identity, config, charger_config, state, status, system, telemetry;
uint16_t alerts[3];
uint16_t raw_vbat, raw_vin, raw_vout, raw_ibat, raw_iin, raw_die, raw_ntc, raw_bsr;
float battery_v, battery_a, battery_w;
float vin_v, vout_v, charger_vbat_v, ibat_a, iin_a, die_c, ntc_c, bsr_ohm;
};
class Controller {
Bus& bus;
Store& store;
Settings settings;
bool storage_healthy = true;
bool apply_ok = true;
uint8_t last_cells = 0;
uint32_t next_service = 0;
bool serviced = false;
bool started = false;
bool identify(uint16_t& identity);
bool verifyWritableIdentity(char* reply, size_t capacity);
bool update(uint8_t reg, uint16_t mask, uint16_t bits);
bool save(const Settings& candidate);
bool setField(const char* key, const char* value, char* reply, size_t capacity);
bool getField(const char* key, char* reply, size_t capacity);
bool action(const char* arguments, bool local, char* reply, size_t capacity);
public:
Controller(Bus& transport, Store& persistence) : bus(transport), store(persistence) {
settings.defaults();
}
void begin();
void service(uint32_t now);
bool snapshot(Snapshot& result);
bool readMonitor(float& volts, float& amps, float& watts);
bool handle(const char* command, char* reply, size_t capacity, bool local);
const Settings& configuration() const { return settings; }
bool settingsHealthy() const { return storage_healthy; }
bool settingsApplied() const { return apply_ok; }
uint8_t lastCellCount() const { return last_cells; }
static uint16_t writableMask(uint8_t reg);
static bool readableRegister(uint8_t reg);
static const char* chemistryName(uint8_t chemistry);
static const char* stateName(uint16_t state);
};
}} // namespace mesh::solar