Record SolarXiao charger control and LoRa acceptance [skip ci]

This commit is contained in:
mikecarper committed 2026-10-21 15:49:19 -07:00
1 parent 70b8db9388
commit 204fc5265b
5 files changed
+1324

No files matched your search

+3
View File
@@ -96,6 +96,9 @@ A failed/corrupt settings file blocks configuration saves and automatic replay.
These actions acknowledge the request, not completion of charging, resistance
measurement or entry into ship mode. Read the corresponding status afterward.
See the [bench acceptance report](test-results/solarxiao_power_2026-10-21.md)
for exact firmware identity, measured readouts and test limits.
## Conversion defaults and carrier revisions
The supplied [Standard schematic](https://github.com/wehooper4/Meshtastic-Hardware/blob/main/XaioSeries/SolarXaio/MPPT/Standard/Schematic_SolarXaio_MPPT_2025-10-23.pdf)
+2
View File
@@ -11,6 +11,8 @@ Completed validation reports, experiments, and test guides are collected here. O
## Radio and RF
- [SolarXiao native MPPT and battery controls, 2026-10-21](solarxiao_power_2026-10-21.md)
- [SolarXiao INA260 battery telemetry and carrier I2C fix, 2026-10-21](solarxiao_ina260_2026-10-21.md)
- [SolarXiao 33S HEAD bootloader, LoRa OTA and repeater acceptance, 2026-10-21](solarxiao_33s_head_bootloader_lora_ota_2026-10-21.md)
- [SX1262 channel-scanning hardware results](cad_scan_validation.md)
@@ -0,0 +1,104 @@
<!-- meshcore-hosted-doc-link:start -->
<p class="meshcore-hosted-doc-link"><a href="https://mikecarper.github.io/MeshCore/test-results/solarxiao_power_2026-10-21/">View this page on MeshCore Docs</a>.</p>
<!-- meshcore-hosted-doc-link:end -->
# SolarXiao MPPT and battery controls, 2026-10-21
The Mercerwood SolarXiao 33S repeater now has a native LTC4162-FST charger
interface and INA260 battery monitor in both Full and Reduced firmware.
All 75 named register fields were read on the board. Supported setting changes
passed live readback and persisted across a warm restart. The original charger
register values were restored and all test overrides/calibration flags cleared.
Three nonce-bound repeater forwarding tests passed at -6 dBm.
[Machine-readable acceptance record](solarxiao_power_2026-10-21.safe.json)
contains exact build identities, readouts, command replies, fault-test coverage,
RF samples, cleanup and limits. See the
[SolarXiao controls guide](../solarxiao_power.md) for commands, units, permissions,
persistence, calibration and telemetry channels.
## Qualified firmware and hardware
| Item | Result |
| --- | --- |
| Source | [`ea8176ddc6b2caa7e52f8dcba46178f69f6cbbea`](https://github.com/mikecarper/MeshCore/commit/ea8176ddc6b2caa7e52f8dcba46178f69f6cbbea) |
| Target / installed profile | `solarxiao_33S_repeater` / Full |
| Version | `v1.18.0.1-halo-keymind-cascade-dev-ea8176dd` |
| Application body hash | `5A46FDAABF34C62D` |
| Application size | 595,040 bytes |
| Application SHA-256 | `d6068baf837c0348ea841a04a2412bd4724a93022f2d8b750af15ca1fb4fbe76` |
| Full / Reduced qualification | PASS: build, runtime memory, DFU, UF2 and linked capabilities |
| Native power marker in both images | PASS |
| Native host test methods | 3 PASS, including eight core scenarios and real Wire/atomic-store boundaries; ASan/UBSan |
| XIAO startup/shutdown checks | 6 PASS, including GPIO safety negative controls |
| Powered USB readings | 75 named register fields; FST chemistry code 6, four cells |
| Settings / persistence | 37 commands including 10 rejected writes; readback, restart and exact register restoration PASS |
| LoRa carrier telemetry | PASS: three replies with all carrier channels 2..13 |
| Repeater forwarding | 3 PASS; exact nonce, four-byte route, one target hop |
| Bootloader | HEAD `b07255d4cdc601f1313e4cbe020caad3b5544821`, CRC `76EBABBF`, preserved |
Full has 150,608 internal bytes available against 76,960 required by the runtime
memory check; Reduced has 152,256 available. The installed public key, primary
radio, RX power saving and bridge settings remained unchanged. Repeating stayed
on and TX stayed -6 dBm. Each trace added one direct RX and TX with zero receive
errors. The cumulative receive-error counter was 2 after the trace run and 17
after all telemetry work; core error flags remained zero. The trace partner's settings were preserved and endpoints closed.
## Powered bench acceptance
The user supplied 13.2 V at SOLAR/VIN with BAT disconnected and USB connected.
Initial native readouts were approximately 13.2926 V input, 13.2785 V output,
27.76 C die and 27.73 C modeled NTC temperature. BAT was open: its changing
floating/capacitor voltage is not a battery measurement or a charger load test.
The charger reported `battery_short` while BAT was disconnected.
The test exercised force/fast telemetry, MPPT, input-current limiting, manual
input-voltage floor, suspend/resume, JEITA selection, manual charge current and
cell voltage, C/x threshold, CV/total timers and saved calibration metadata.
For example, 13000 mV input floor read back as 13078.1 mV, matching the hardware
step. Manual targets of 1000 mA and 3500 mV/cell read back correctly with JEITA
off. These are register-interface tests without a connected battery.
Saved policy reloaded after restart with nine register overrides and the
explicit calibration-source flag. Every modified register was restored to its
captured value, then `mppt reset` cleared saved overrides and calibration flags.
Final MPPT and JEITA were on; force/fast telemetry were off. Host fault tests
also simulated a charger reset, read/write faults, ignored writes, corrupt or
wrong-size storage, failed atomic publication, identity mismatch, masked-bit
preservation, thermal ordering and one-shot/W0C behavior.
## Supply state, radio checks and scope
The first install check found the charger unavailable. It restored the verified
previous firmware with identity/settings intact; the user then confirmed the
bench supply had been off and switched it on. The final firmware installed and
passed powered reading/control tests.
During a later interval when the charger was unavailable again, authenticated
LoRa CLI returned an explicit unavailable status while INA260 readings remained
available. Three telemetry replies carried channel-2 voltage/current/power;
charger channels were omitted. Temporary contacts and forwarding connections
were removed. One remote version read required a retry.
After the supply was switched on again, authenticated LoRa commands identified
FST/four cells and read charger/battery/status values. Three subsequent telemetry
replies contained every carrier channel 2..13: input 13.29 V, output 13.27..13.28 V,
die 25.4..25.5 C and modeled NTC 24.9 C. Two telemetry attempts missed replies before a successful retry. An earlier powered CLI session also needed retries and
missed its first telemetry response; all temporary contacts/forwards were cleaned
up. Final USB checks confirmed VIN 13.2926 V, MPPT/JEITA on, zero saved
overrides, zero calibration flags and the same firmware/bootloader identities.
These results establish successful readout, not a loss-free radio link.
No actual battery charging, loaded-current calibration, resistance measurement
or ship-mode entry was tested. The default 10 milliohm sense resistors and
10 kohm / 3490 K NTC model come from reference documentation; the supplied
Standard schematic uses a single-cell L42M, not this exact 12 V FSTM revision.
Actual carrier component values and the temperature model remain configurable.
The new application was installed by USB DFU. This run does not repeat a full
LoRa firmware transfer; the earlier successful HEAD bootloader/OTA transfer is
in the [separate OTA report](solarxiao_33s_head_bootloader_lora_ota_2026-10-21.md).
Reduced was build-checked, while live tests used Full. An exploratory Room
Server build exceeded its flash/UF2 limits; native power controls are therefore
scoped to the requested SolarXiao repeater targets. No Room Server or Companion
package is qualified by this report.
File diff suppressed because it is too large. Load diff
+71
View File
@@ -301,5 +301,76 @@ int main() {
self.assertEqual(run.returncode, 0, run.stdout + run.stderr)
class SolarXiaoAdapterTests(unittest.TestCase):
def test_real_transport_and_atomic_settings_boundary(self):
from test_regulator_settings import FILESYSTEM
source = (ROOT / "src/helpers/sensors/SolarXiaoPower.cpp").read_text()
classes = "\n".join(extract_braced(source, name) + ";" for name in
("class CarrierBus", "class CarrierStore"))
harness = r'''#include <cassert>
#include <vector>
#include <cstring>
#include "SolarXiaoPowerCore.h"
#define ATOMIC_FILE_WRITER_IMPLEMENTATION
#include <helpers/CheckedSettingsFile.h>
using namespace mesh;
using namespace mesh::solar;
#define HIGH 1
#define PIN_BOARD_SDA 30
#define PIN_BOARD_SCL 31
int lines=1;
int digitalRead(int p) {assert(p==30 || p==31);return lines;}
struct FakeWire {
std::vector<uint8_t> tx, rx;int reads=0, calls=0, ack=0, reported=-1;bool stop=true;
void beginTransmission(uint8_t a) {assert(a==0x68 || a==0x40);++calls;tx.clear();}
void write(uint8_t b) {tx.push_back(b);}
int endTransmission(bool s=true) {stop=s;return ack;}
int requestFrom(uint8_t,uint8_t n) {assert(n==2);reads=0;return reported<0?int(rx.size()):reported;}
int available() {return rx.size()-reads;}
int read() {return reads<int(rx.size())?rx[reads++]:-1;}
} Wire;
@CLASSES@
constexpr const char* CarrierStore::path;
int main() {
CarrierBus bus;uint16_t word=0;
Wire.rx={0x34,0x12};assert(bus.read(0x68,0x43,word));assert(word==0x1234 && !Wire.stop);
Wire.rx={0x12,0x34};assert(bus.read(0x40,2,word));assert(word==0x1234);
Wire.rx={0x12};assert(!bus.read(0x68,0x43,word));assert(!Wire.available());
Wire.reported=2;Wire.rx.clear();assert(!bus.read(0x68,0x43,word));
Wire.reported=-1;Wire.ack=2;assert(!bus.read(0x68,0x43,word));Wire.ack=0;
assert(bus.write(0x68,0x14,0x8052));assert(Wire.stop && Wire.tx==std::vector<uint8_t>({0x14,0x52,0x80}));
int calls=Wire.calls;assert(!bus.write(0x40,0,0) && !bus.read(0x42,0,word));assert(calls==Wire.calls);
lines=0;assert(!bus.read(0x68,0,word) && !bus.write(0x68,0x14,2));assert(calls==Wire.calls);
CarrierStore store;uint8_t image[Settings::IMAGE_SIZE],read[Settings::IMAGE_SIZE];Settings settings;settings.defaults();settings.encode(image);
assert(!store.available() && !store.exists() && !store.save(image,sizeof(image)));
MockFS fs;store.fs=&fs;assert(store.available() && !store.exists());
assert(store.save(image,sizeof(image)) && store.exists() && store.load(read,sizeof(read)));
assert(!memcmp(image,read,sizeof(image)));const auto baseline=fs.files[CarrierStore::path];
settings.calibration_user_mask=1;settings.rsnsb_mohm=20;settings.encode(image);
fs.fail_write=true;assert(!store.save(image,sizeof(image)));assert(fs.files[CarrierStore::path]==baseline);fs.fail_write=false;
fs.fail_rename_from="/solar_power.tmp";assert(!store.save(image,sizeof(image)));assert(fs.files[CarrierStore::path]==baseline);fs.fail_rename_from.clear();
assert(store.save(image,sizeof(image)));assert(store.load(read,sizeof(read)) && !memcmp(image,read,sizeof(image)));
fs.files[CarrierStore::path].push_back(0);assert(!store.load(read,sizeof(read)));
assert(!store.save(image,sizeof(image)-1));
}
'''.replace("@CLASSES@", classes)
with tempfile.TemporaryDirectory(prefix="solarxiao-adapter-test-") as temp:
directory = Path(temp)
(directory / "helpers").mkdir()
(directory / "helpers/IdentityStore.h").write_text(FILESYSTEM, encoding="ascii")
cpp = directory / "test.cpp"
binary = directory / "test"
cpp.write_text(harness, encoding="ascii")
build = subprocess.run([shutil.which("g++"), "-std=c++11", "-DNRF52_PLATFORM",
"-DMESH_SOLARXIAO_POWER=1", "-fsanitize=address,undefined", "-fno-pie", "-no-pie",
"-I" + str(directory), "-I" + str(ROOT / "src"),
"-I" + str(ROOT / "src/helpers/sensors"), str(cpp),
str(ROOT / "src/helpers/sensors/SolarXiaoPowerCore.cpp"), "-o", str(binary)],
capture_output=True, text=True, timeout=30)
self.assertEqual(build.returncode, 0, build.stdout + build.stderr)
run = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
self.assertEqual(run.returncode, 0, run.stdout + run.stderr)
if __name__ == "__main__":
unittest.main()