From 3bda527d05e9b6d70f04ee8176d765511a51252f Mon Sep 17 00:00:00 2001 From: Nelson Date: Fri, 28 Aug 2026 14:36:32 -0700 Subject: [PATCH] fix(battery): read voltage from configured AXP2101 (Waveshare ESP32-S3-PhotoPainter) The normal battery-reading path (readBatteryVoltageUncached) checked the BQ27220 gauge and then the analog battery-sense pin, but never the AXP2101. On the Waveshare ESP32-S3-PhotoPainter -- whose official esp32-s3-wspp preset configures an AXP2101 (sensor type 3) and no analog battery-sense pin -- it returned -1.0 and updatemsdata() encoded the voltage as zero in the BLE MSD advertisement. Add an AXP2101 arm to the same battery path that: - selects the PMIC from the parsed SensorData (no board or PhotoPainter flag), respecting the configured bus and sensor-address defaulting (0 / 0xFF -> 0x34, any other 7-bit address respected verbatim), - reuses initOrRestoreWireForBus() so bus switching stays centralised, - confirms battery presence via power-status reg 0x00 bit 3, - enables only ADC channel bit 0 in reg 0x30, preserving every other bit, and allows a short settling delay ONLY when the channel just flipped 0->1, - decodes VBAT from regs 0x34/0x35 as (hi & 0x3F) << 8 | lo at 1 mV per count, and - fails soft with -1.0f + od_log_warn on any I2C failure so the known post-panel-shutdown bus loss stays a separate observation. The existing BQ27220 precedence, analog-sense fallback, and 30-second top-level cache are preserved. No heap. No changes to the vendored protocol header, PMIC rail policy, ALDO3/ALDO4, audio, panel shutdown, Wire.end, GPIO47/48, ws_pp_init, PhotoPainter detection flags, or the esp32-s3-wspp preset. Flagged but deliberately not fixed here: readAXP2101Data() in display_service.cpp decodes VBAT as a 12-bit ((H<<4)|(L&0x0F)) * 0.5 mV field, writes 0xFF to reg 0x30 (enabling every ADC channel and stomping unrelated bits), and reads battery presence at bit 5 and VBUS at bit 3, which is the reverse of the datasheet. The function has no callers, so none of that is reachable, and CLAUDE.md asks that pre-existing problems be flagged rather than fixed as a side effect of unrelated work. Happy to follow up separately, either against the shared helpers added here or by removing the function. Host test tools/test_sensor_axp2101.cpp covers every case listed in the host-test plan: VBAT decoding with unused bits clear and set, targeted ADC-enable that preserves every other bit, battery-present set/clear, address defaulting for 0 and 0xFF, configured-address preservation, no configured AXP2101, and short/failed I2C reads. Standalone build; not part of the PlatformIO build. Registered in tools/README.md next to the other host harnesses. Verified: host test 49/49 checks pass under -fsanitize=undefined,address; all 12 environments in .github/firmware-targets.json compile clean; and a four-state before/after hardware cycle on a PhotoPainter (positive) and a XIAO ESP32-C3 (negative) shows the PhotoPainter reading ~4.14 V into the BLE advertisement where mainline encoded zero, with the C3 serial logs byte-identical between unmodified main and this commit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/display_service.cpp | 5 + src/sensor_axp2101.cpp | 108 ++++++++++++++ src/sensor_axp2101.h | 108 ++++++++++++++ tools/README.md | 4 + tools/test_sensor_axp2101.cpp | 269 ++++++++++++++++++++++++++++++++++ 5 files changed, 494 insertions(+) create mode 100644 src/sensor_axp2101.cpp create mode 100644 src/sensor_axp2101.h create mode 100644 tools/test_sensor_axp2101.cpp diff --git a/src/display_service.cpp b/src/display_service.cpp index 0ed04bd..fc0b711 100644 --- a/src/display_service.cpp +++ b/src/display_service.cpp @@ -9,6 +9,7 @@ #include "buzzer_control.h" #include "sensor_sht40.h" #include "sensor_bq27220.h" +#include "sensor_axp2101.h" #include "communication.h" #include "encryption.h" #include "boot_screen.h" @@ -1767,6 +1768,10 @@ static float readBatteryVoltageUncached() { return gaugeV; } } + const float pmicV = axp2101BatteryVoltageVolts(globalConfig.sensors, globalConfig.sensor_count); + if (pmicV >= 0.0f) { + return pmicV; + } if (globalConfig.power_option.battery_sense_pin == 0xFF) return -1.0; uint8_t sensePin = globalConfig.power_option.battery_sense_pin; uint8_t enablePin = globalConfig.power_option.battery_sense_enable_pin; diff --git a/src/sensor_axp2101.cpp b/src/sensor_axp2101.cpp new file mode 100644 index 0000000..da93ab2 --- /dev/null +++ b/src/sensor_axp2101.cpp @@ -0,0 +1,108 @@ +#include "sensor_axp2101.h" + +#include "structs.h" +#include "display_service.h" +#include "od_log.h" + +#include +#include + +static const SensorData* axp2101_config(const SensorData* sensors, uint8_t count) { + if (sensors == nullptr) { + return nullptr; + } + for (uint8_t i = 0; i < count; i++) { + if (sensors[i].sensor_type == OD_SENSOR_TYPE_AXP2101) { + return &sensors[i]; + } + } + return nullptr; +} + +static uint8_t axp2101_bus_id(const SensorData* s) { + uint8_t bid = s->bus_id; + if (bid == 0xFF) { + bid = 0; + } + return bid; +} + +// Register block read: write `reg`, then requestFrom `len` bytes into the +// caller-supplied `out`. Returns false on any transaction failure or short read. +// Allocates nothing. Mirrors bq27220_read_block() in sensor_bq27220.cpp. +static bool axp2101_read_block(uint8_t addr, uint8_t reg, uint8_t* out, uint8_t len) { + Wire.beginTransmission(addr); + Wire.write(reg); + if (Wire.endTransmission(false) != 0) { + return false; + } + if (Wire.requestFrom(addr, (size_t)len, true) != len) { + return false; + } + for (uint8_t i = 0; i < len; i++) { + out[i] = Wire.read(); + } + return true; +} + +static bool axp2101_write_reg(uint8_t addr, uint8_t reg, uint8_t value) { + Wire.beginTransmission(addr); + Wire.write(reg); + Wire.write(value); + return Wire.endTransmission() == 0; +} + +float axp2101BatteryVoltageVolts(const SensorData* sensors, uint8_t sensor_count) { + const SensorData* s = axp2101_config(sensors, sensor_count); + if (s == nullptr) { + return -1.0f; + } + const uint8_t bus = axp2101_bus_id(s); + if (!initOrRestoreWireForBus(bus)) { + od_log_warn("AXP2101: bus %u init failed", bus); + return -1.0f; + } + const uint8_t addr = axp2101_resolve_addr(s->i2c_addr_7bit); + od_log_debug("AXP2101: addr=0x%02X bus=%u", addr, bus); + + uint8_t status = 0; + if (!axp2101_read_block(addr, AXP2101_REG_POWER_STATUS_ADDR, &status, 1)) { + od_log_warn("AXP2101: power-status read failed @0x%02X", addr); + return -1.0f; + } + const bool batt_present = axp2101_batt_present(status); + const bool vbus_present = axp2101_vbus_present(status); + od_log_debug("AXP2101: power_status=0x%02X batt=%d vbus=%d", + status, (int)batt_present, (int)vbus_present); + if (!batt_present) { + od_log_debug("AXP2101: battery not present, returning -1"); + return -1.0f; + } + + uint8_t adc_ctrl = 0; + if (!axp2101_read_block(addr, AXP2101_REG_ADC_CHANNEL_CTRL_ADDR, &adc_ctrl, 1)) { + od_log_warn("AXP2101: ADC ctrl read failed @0x%02X", addr); + return -1.0f; + } + bool channel_changed = false; + const uint8_t adc_ctrl_new = axp2101_adc_enable_bit0(adc_ctrl, &channel_changed); + od_log_debug("AXP2101: adc_ctrl=0x%02X -> 0x%02X (changed=%d)", + adc_ctrl, adc_ctrl_new, (int)channel_changed); + if (channel_changed) { + if (!axp2101_write_reg(addr, AXP2101_REG_ADC_CHANNEL_CTRL_ADDR, adc_ctrl_new)) { + od_log_warn("AXP2101: ADC ctrl enable failed @0x%02X", addr); + return -1.0f; + } + delay(AXP2101_ADC_SETTLING_MS); + } + + uint8_t vbat_raw[2] = {0, 0}; + if (!axp2101_read_block(addr, AXP2101_REG_VBAT_H_ADDR, vbat_raw, 2)) { + od_log_warn("AXP2101: VBAT read failed @0x%02X", addr); + return -1.0f; + } + const uint16_t mv = axp2101_decode_vbat_mv(vbat_raw[0], vbat_raw[1]); + od_log_debug("AXP2101: VBAT raw=[0x%02X,0x%02X] -> %u mV (%.3fV)", + vbat_raw[0], vbat_raw[1], (unsigned)mv, (float)mv / 1000.0f); + return (float)mv / 1000.0f; +} diff --git a/src/sensor_axp2101.h b/src/sensor_axp2101.h new file mode 100644 index 0000000..ab86e59 --- /dev/null +++ b/src/sensor_axp2101.h @@ -0,0 +1,108 @@ +// AXP2101 battery-voltage reader for the normal OpenDisplay battery path. +// +// Sits alongside sensor_bq27220 in shape: the sensor-specific configuration +// lookup, address defaulting, bus restoration, and voltage getter all live +// here. display_service.cpp's readBatteryVoltageUncached() consults this file +// when the configured sensor set contains an AXP2101 (sensor type 3), which is +// the case for the Waveshare ESP32-S3-PhotoPainter esp32-s3-wspp preset. +// +// The register decoding and bit manipulation are kept as pure `static inline` +// helpers here so tools/test_sensor_axp2101.cpp can exercise them without a +// PlatformIO build. See tools/README.md. +#ifndef SENSOR_AXP2101_H +#define SENSOR_AXP2101_H + +#include +#include +#include + +// AXP2101 register layout used by the battery-voltage path. display_service.cpp +// carries its own AXP2101_* constants for the PMIC init and shutdown block, and +// four values appear in both places: the 0x34 slave address, 0x00 power status, +// 0x30 ADC channel control, and 0x34 VBAT high. The duplication is deliberate -- +// those belong to that block, and the pure helpers below must compile as a +// standalone unit for the host test. +#define AXP2101_DEFAULT_ADDR_7BIT 0x34u +#define AXP2101_REG_POWER_STATUS_ADDR 0x00u +#define AXP2101_REG_ADC_CHANNEL_CTRL_ADDR 0x30u +#define AXP2101_REG_VBAT_H_ADDR 0x34u // VBAT high byte; read as 2-byte block with 0x35 + +// Datasheet reg 0x00: bit 3 = battery presence, bit 5 = VBUS presence. +// The uncalled readAXP2101Data() diagnostic in display_service.cpp reads these +// the other way round (0x20 for battery, 0x08 for VBUS). That is a pre-existing +// defect in unreachable code, flagged in the pull request and left unchanged. +#define AXP2101_POWER_STATUS_BATT_PRESENT_BIT (1u << 3) +#define AXP2101_POWER_STATUS_VBUS_PRESENT_BIT (1u << 5) + +// Datasheet reg 0x30: bit 0 enables the VBAT-voltage ADC channel. Every other +// bit is a different channel or an unrelated ADC control and must be preserved. +#define AXP2101_ADC_ENABLE_VBAT_BIT (1u << 0) + +// Post-enable settling window before the first VBAT read is meaningful. Only +// applied when the caller has just flipped bit 0 from 0 to 1. +#define AXP2101_ADC_SETTLING_MS 2u + +// Address defaulting: `0` and `0xFF` (SensorData.i2c_addr_7bit sentinel values, +// see include/opendisplay_structs.h) both mean "use the AXP2101 default 0x34". +// Any other value is respected as the configured 7-bit address. +static inline uint8_t axp2101_resolve_addr(uint8_t configured) { + if (configured == 0u || configured == 0xFFu) { + return AXP2101_DEFAULT_ADDR_7BIT; + } + return configured; +} + +// Battery-present interpretation of the power-status register. +static inline bool axp2101_batt_present(uint8_t status_reg) { + return (status_reg & AXP2101_POWER_STATUS_BATT_PRESENT_BIT) != 0u; +} + +// VBUS-present interpretation of the power-status register. Reported in the +// battery reader's debug line; it does not affect the voltage returned. +static inline bool axp2101_vbus_present(uint8_t status_reg) { + return (status_reg & AXP2101_POWER_STATUS_VBUS_PRESENT_BIT) != 0u; +} + +// Returns the value to write back to reg 0x30 so bit 0 is set, with every other +// bit preserved. `out_channel_changed` receives `true` iff the caller has just +// flipped the channel from disabled to enabled -- the only case where the +// caller must allow AXP2101_ADC_SETTLING_MS before the first VBAT sample. +static inline uint8_t axp2101_adc_enable_bit0(uint8_t current, bool* out_channel_changed) { + const bool was_off = (current & AXP2101_ADC_ENABLE_VBAT_BIT) == 0u; + if (out_channel_changed != NULL) { + *out_channel_changed = was_off; + } + return (uint8_t)(current | AXP2101_ADC_ENABLE_VBAT_BIT); +} + +// VBAT decoding. Reg 0x34 holds the six valid high bits (bits 7:6 are unused +// and must be masked), reg 0x35 holds the full low byte. Result is millivolts +// at 1 mV per count. +static inline uint16_t axp2101_decode_vbat_mv(uint8_t hi, uint8_t lo) { + const uint16_t high_bits = (uint16_t)(hi & 0x3Fu); + return (uint16_t)((high_bits << 8) | (uint16_t)lo); +} + +struct SensorData; + +#ifdef __cplusplus +extern "C" { +#endif + +// Reads the current battery voltage in volts through the OpenDisplay bus +// abstraction. The caller passes the parsed sensor set rather than this module +// reaching for a global (see the no-extern rule in CLAUDE.md); the AXP2101 entry +// is located here so the lookup stays with the sensor-specific code, matching +// sensor_bq27220. +// +// Returns `-1.0f` on any failure -- no AXP2101 in `sensors`, bus init failed, +// PMIC not answering, short read, or battery absent per reg 0x00 -- so the +// caller can fall through to its next battery source. Never allocates. Emits an +// od_log_warn on transaction failure. +float axp2101BatteryVoltageVolts(const struct SensorData* sensors, uint8_t sensor_count); + +#ifdef __cplusplus +} +#endif + +#endif // SENSOR_AXP2101_H diff --git a/tools/README.md b/tools/README.md index 96ba103..abd4409 100644 --- a/tools/README.md +++ b/tools/README.md @@ -13,6 +13,10 @@ firmware's config layout. boards that flash via USB mass storage. - `test_zlib_stream.c` — standalone test harness for the firmware's streaming zlib/uzlib decoder (`lib/uzlib`). Not part of the PlatformIO build. +- `test_sensor_axp2101.cpp` — standalone host test for the AXP2101 battery- + voltage helpers in `src/sensor_axp2101.h` (VBAT decoding, targeted ADC-enable, + address defaulting, and I2C-failure fall-through). Not part of the PlatformIO + build. See the header comment for the exact build line. - `ble_crypto.py` / `config_packet.py` — shared helpers. `ble_crypto.py` is inlined into `od-device-cli.py` and kept here for reference. `config_packet.py` is imported by `provision_firmware.py`. diff --git a/tools/test_sensor_axp2101.cpp b/tools/test_sensor_axp2101.cpp new file mode 100644 index 0000000..50207ab --- /dev/null +++ b/tools/test_sensor_axp2101.cpp @@ -0,0 +1,269 @@ +// Host test for src/sensor_axp2101.h -- the AXP2101 battery-voltage helpers. +// +// Build and run from the repo root: +// +// g++ -std=c++17 -Wall -Wextra -Werror -O1 -fsanitize=undefined,address -I include -I src tools/test_sensor_axp2101.cpp -o /tmp/test_sensor_axp2101 +// /tmp/test_sensor_axp2101 +// +// Like tools/test_link_owner.cpp, this is as much a written-down statement of +// the intended semantics as it is a test. It covers: +// +// - VBAT decoding with upper unused bits clear and set +// - ADC bit-0 enable while preserving every other bit +// - Battery-present status set and clear +// - Default address selection for zero and 0xFF +// - Configured address preservation +// - No configured AXP2101 (walked via a SensorData-shaped array) +// - Short or failed I2C reads (walked via a fake bus driving the same helpers) +// +// The pure helpers under test have no Arduino/Wire dependency, so no hostshim +// is required. The fake-bus scenarios call the SAME helpers the production +// reader calls, but they re-implement its call sequence rather than driving +// axp2101BatteryVoltageVolts() itself. That ordering and the Wire I/O are what +// on-device testing covers, not this file. + +#include "sensor_axp2101.h" +#include "opendisplay_structs.h" + +#include +#include +#include +#include + +// --- tiny harness ---------------------------------------------------------- +static int g_checks = 0; +static int g_failures = 0; + +static void check(bool cond, const char* what) { + g_checks++; + if (!cond) { + g_failures++; + std::printf("FAIL: %s\n", what); + } +} + +extern "C" void __ubsan_on_report(void) { + std::printf("FAIL: UBSan report\n"); + std::exit(1); +} + +// Fake bus for the "short or failed I2C reads" case. Records writes so the test +// can verify targeted ADC-enable semantics, and lets each register return either +// a canned byte or an error. +struct FakeBus { + // Canned responses for the three registers the reader touches. If .ok is + // false, the read fails (mirrors an I2C NAK or a short read). + struct Reply { + bool ok; + uint8_t value; + }; + Reply power_status{true, 0x08}; // battery present by default + Reply adc_ctrl{true, 0x00}; // channel currently disabled + Reply vbat_hi{true, 0x0F}; // 0x0FB8 -> 4024 mV + Reply vbat_lo{true, 0xB8}; + + // Recorded writes. + bool adc_write_seen = false; + uint8_t adc_write_value = 0xFF; +}; + +// Executes the same production sequence readBatteryVoltageUncached's AXP2101 +// arm does, but against FakeBus. Returns millivolts on success, -1 on any +// failure -- mirroring axp2101BatteryVoltageVolts()'s -1.0f return. +static int fake_read_vbat_mv(FakeBus& bus, uint8_t addr) { + (void)addr; + if (!bus.power_status.ok) return -1; + if (!axp2101_batt_present(bus.power_status.value)) return -1; + if (!bus.adc_ctrl.ok) return -1; + bool changed = false; + const uint8_t adc_new = axp2101_adc_enable_bit0(bus.adc_ctrl.value, &changed); + if (changed) { + bus.adc_write_seen = true; + bus.adc_write_value = adc_new; + // (Production delays AXP2101_ADC_SETTLING_MS here; no-op in the test.) + } + if (!bus.vbat_hi.ok || !bus.vbat_lo.ok) return -1; + return (int)axp2101_decode_vbat_mv(bus.vbat_hi.value, bus.vbat_lo.value); +} + +int main(void) { + // ---- VBAT decoding with upper unused bits clear ------------------------- + { + // 0x0F B8 -- the ESPHome-observed sample -- must decode to 4024 mV. + check(axp2101_decode_vbat_mv(0x0F, 0xB8) == 4024, "vbat decode 0x0FB8 == 4024 mV"); + check(axp2101_decode_vbat_mv(0x00, 0x00) == 0, "vbat decode 0x0000 == 0 mV"); + check(axp2101_decode_vbat_mv(0x00, 0xFF) == 255, "vbat decode 0x00FF == 255 mV"); + check(axp2101_decode_vbat_mv(0x01, 0x00) == 256, "vbat decode 0x0100 == 256 mV"); + check(axp2101_decode_vbat_mv(0x3F, 0xFF) == 16383, "vbat decode 0x3FFF == 16383 mV"); + } + + // ---- VBAT decoding with upper unused bits SET --------------------------- + { + // Bits 7:6 of reg 0x34 are unused; the decoder must mask them off so + // spurious garbage in that field cannot inflate the reading. + check(axp2101_decode_vbat_mv(0xC0, 0x00) == 0, + "vbat unused bits 0xC0 masked -> 0 mV"); + check(axp2101_decode_vbat_mv(0xCF, 0xB8) == 4024, + "vbat 0xCFB8 (unused bits set) decodes same as 0x0FB8"); + check(axp2101_decode_vbat_mv(0xFF, 0xFF) == 16383, + "vbat 0xFFFF decodes as 0x3FFF -> 16383 mV"); + } + + // ---- ADC bit-0 enable while preserving every other bit ------------------ + { + // From all-zero: bit 0 flips 0->1, no other bit changes. + bool changed = false; + uint8_t next = axp2101_adc_enable_bit0(0x00, &changed); + check(next == 0x01, "adc enable from 0x00 -> 0x01"); + check(changed, "adc enable from 0x00 flags channel change"); + + // From every other bit set: bit 0 flips 0->1, all other bits preserved. + changed = false; + next = axp2101_adc_enable_bit0(0xFE, &changed); + check(next == 0xFF, "adc enable from 0xFE preserves 7..1, sets bit 0 -> 0xFF"); + check(changed, "adc enable from 0xFE flags channel change"); + + // Already enabled (0x01): value unchanged, no channel-change flag. + changed = true; + next = axp2101_adc_enable_bit0(0x01, &changed); + check(next == 0x01, "adc enable from 0x01 stays 0x01"); + check(!changed, "adc enable from 0x01 does not flag channel change"); + + // Already enabled with siblings set: value unchanged, no flag. + changed = true; + next = axp2101_adc_enable_bit0(0xA5, &changed); + check(next == 0xA5, "adc enable from 0xA5 stays 0xA5 (bit 0 already set)"); + check(!changed, "adc enable from 0xA5 does not flag channel change"); + + // NULL out-pointer must be tolerated. + next = axp2101_adc_enable_bit0(0x00, nullptr); + check(next == 0x01, "adc enable with null out-pointer still returns 0x01"); + } + + // ---- Battery-present status set and clear ------------------------------- + { + check(!axp2101_batt_present(0x00), "batt present false when reg 0x00 is zero"); + check(axp2101_batt_present(0x08), "batt present true when bit 3 set"); + check(!axp2101_batt_present(0xF7), "batt present false when only bit 3 clear"); + check(axp2101_batt_present(0xFF), "batt present true when every bit set"); + // Bit 3 is battery, bit 5 is VBUS -- they are DISTINCT and must not + // shadow each other (this is exactly what mainline had swapped). + check(!axp2101_batt_present(0x20), "0x20 alone (VBUS bit) does NOT imply battery present"); + check(axp2101_vbus_present(0x20), "0x20 alone is VBUS present"); + check(!axp2101_vbus_present(0x08), "0x08 alone (batt bit) does NOT imply VBUS present"); + } + + // ---- Default address selection for zero and 0xFF ------------------------ + { + check(axp2101_resolve_addr(0x00) == 0x34, "configured 0x00 -> default 0x34"); + check(axp2101_resolve_addr(0xFF) == 0x34, "configured 0xFF -> default 0x34"); + } + + // ---- Configured address preservation ------------------------------------ + { + // A valid 7-bit address must be respected verbatim -- no defaulting. + check(axp2101_resolve_addr(0x34) == 0x34, "configured 0x34 respected"); + check(axp2101_resolve_addr(0x35) == 0x35, "configured 0x35 respected"); + check(axp2101_resolve_addr(0x01) == 0x01, "configured 0x01 respected (edge)"); + check(axp2101_resolve_addr(0x7F) == 0x7F, "configured 0x7F respected (top of 7-bit)"); + } + + // ---- No configured AXP2101 --------------------------------------------- + // The reader must not consult AXP2101 when the parsed sensor set has no + // entry of type OD_SENSOR_TYPE_AXP2101. This mirrors the walk that + // axp2101_config() performs in sensor_axp2101.cpp against globalConfig. + { + auto find_axp = [](const SensorData* sensors, uint8_t count) -> const SensorData* { + for (uint8_t i = 0; i < count; i++) { + if (sensors[i].sensor_type == OD_SENSOR_TYPE_AXP2101) return &sensors[i]; + } + return nullptr; + }; + + SensorData empty{}; + check(find_axp(&empty, 0) == nullptr, "no sensors -> no AXP2101 found"); + + SensorData sht{}; + sht.sensor_type = OD_SENSOR_TYPE_SHT40; + check(find_axp(&sht, 1) == nullptr, "SHT40 only -> no AXP2101 found"); + + SensorData bq{}; + bq.sensor_type = OD_SENSOR_TYPE_BQ27220; + check(find_axp(&bq, 1) == nullptr, "BQ27220 only -> no AXP2101 found"); + + SensorData mixed[3]{}; + mixed[0].sensor_type = OD_SENSOR_TYPE_SHT40; + mixed[1].sensor_type = OD_SENSOR_TYPE_AXP2101; + mixed[1].bus_id = 0; + mixed[1].i2c_addr_7bit = 0xFF; + mixed[2].sensor_type = OD_SENSOR_TYPE_BQ27220; + const SensorData* hit = find_axp(mixed, 3); + check(hit == &mixed[1], "mixed sensor set finds the AXP2101 entry"); + check(axp2101_resolve_addr(hit->i2c_addr_7bit) == 0x34, + "PhotoPainter-style entry (addr 0xFF) resolves to default 0x34"); + } + + // ---- Short or failed I2C reads ----------------------------------------- + // Every failure mode returns -1 (matching axp2101BatteryVoltageVolts's + // -1.0f) and does NOT touch the ADC-enable write when a preceding step + // has already failed. Success case verifies the ADC-enable is issued + // exactly once and only when the channel actually flipped. + { + // Success baseline: pre-shutdown ESPHome sample. + FakeBus b; + int mv = fake_read_vbat_mv(b, 0x34); + check(mv == 4024, "success path decodes 4024 mV"); + check(b.adc_write_seen, "success path issued ADC-enable write"); + check(b.adc_write_value == 0x01, "success path wrote only bit 0 to reg 0x30"); + } + { + // Power-status read failure -> -1, no ADC-enable write. + FakeBus b; + b.power_status.ok = false; + int mv = fake_read_vbat_mv(b, 0x34); + check(mv == -1, "power-status read failure returns -1"); + check(!b.adc_write_seen, "power-status failure aborts before ADC-enable write"); + } + { + // Battery-present clear -> -1, no ADC-enable write. + FakeBus b; + b.power_status.value = 0x00; + int mv = fake_read_vbat_mv(b, 0x34); + check(mv == -1, "battery-absent returns -1"); + check(!b.adc_write_seen, "battery-absent aborts before ADC-enable write"); + } + { + // ADC ctrl read failure -> -1, no ADC-enable write. + FakeBus b; + b.adc_ctrl.ok = false; + int mv = fake_read_vbat_mv(b, 0x34); + check(mv == -1, "adc-ctrl read failure returns -1"); + check(!b.adc_write_seen, "adc-ctrl read failure aborts before ADC-enable write"); + } + { + // ADC channel already enabled: no write issued (targeted enable is a no-op). + FakeBus b; + b.adc_ctrl.value = 0x81; // bit 0 already set, other bits present + int mv = fake_read_vbat_mv(b, 0x34); + check(mv == 4024, "already-enabled channel still decodes correctly"); + check(!b.adc_write_seen, "already-enabled channel does NOT re-write reg 0x30"); + } + { + // VBAT high-byte short read -> -1. + FakeBus b; + b.vbat_hi.ok = false; + int mv = fake_read_vbat_mv(b, 0x34); + check(mv == -1, "VBAT high-byte read failure returns -1"); + check(b.adc_write_seen, "VBAT read failure still records the earlier ADC-enable"); + } + { + // VBAT low-byte short read -> -1. + FakeBus b; + b.vbat_lo.ok = false; + int mv = fake_read_vbat_mv(b, 0x34); + check(mv == -1, "VBAT low-byte read failure returns -1"); + } + + std::printf("\n%d checks, %d failures\n", g_checks, g_failures); + return g_failures == 0 ? 0 : 1; +}