/**
 * @file Power.cpp
 * @brief This file contains the implementation of the Power class, which is
 * responsible for managing power-related functionality of the device. It
 * includes battery level sensing, power management unit (PMU) control, and
 * power state machine management. The Power class is used by the main device
 * class to manage power-related functionality.
 *
 * The file also includes implementations of various battery level sensors, such
 * as the AnalogBatteryLevel class, which assumes the battery voltage is
 * attached via a voltage-divider to an analog input.
 *
 * This file is part of the Meshtastic project.
 * For more information, see: https://meshtastic.org/
 */
#include "Power.h"
#include "BluetoothCommon.h"
#include "MessageStore.h"
#include "NodeDB.h"
#include "PowerFSM.h"
#include "Throttle.h"
#include "buzz/buzz.h"
#include "configuration.h"
#include "main.h"
#include "meshUtils.h"
#include "power/PowerHAL.h"
#include "power/SGM41562.h"
#include "sleep.h"
#ifdef ARCH_ESP32
// #include <driver/adc.h>
#include <esp_adc/adc_cali.h>
#include <esp_adc/adc_cali_scheme.h>
#include <esp_adc/adc_oneshot.h>
#include <esp_err.h>
#endif

#if defined(ARCH_PORTDUINO)
#include "api/WiFiServerAPI.h"
#include "input/LinuxInputImpl.h"
#include "input/LinuxJoystick.h"
#endif

// Working USB detection for powered/charging states on the RAK platform
#ifdef NRF_APM
#include "nrfx_power.h"
#endif

#if defined(ARCH_NRF52)
#include "Nrf52SaadcLock.h"
#include "concurrency/LockGuard.h"
#endif

#if defined(ARCH_STM32) && defined(BATTERY_PIN)
#include "stm32yyxx_ll_adc.h"

/* Analog read resolution */
#if defined(LL_ADC_RESOLUTION_12B)
#define LL_ADC_RESOLUTION LL_ADC_RESOLUTION_12B
#define BATTERY_SENSE_RESOLUTION_BITS 12
#elif defined(LL_ADC_DS_DATA_WIDTH_12_BIT)
#define LL_ADC_RESOLUTION LL_ADC_DS_DATA_WIDTH_12_BIT
#define BATTERY_SENSE_RESOLUTION_BITS 12
#else
// The ST HAL headers that define these are outside cppcheck's include path (check_skip_packages), so static
// analysis always lands here even though real builds resolve one of the branches above.
// cppcheck-suppress preprocessorErrorDirective
#error "ADC resolution could not be defined!"
#endif
#define ADC_RANGE (1 << BATTERY_SENSE_RESOLUTION_BITS)
#endif

#if defined(DEBUG_HEAP_MQTT) && !MESHTASTIC_EXCLUDE_MQTT
#include "mqtt/MQTT.h"
#include "target_specific.h"
#if HAS_WIFI
#include <WiFi.h>
#endif

#if HAS_ETHERNET && defined(ARCH_ESP32)
#include <ETH.h>
#endif // HAS_ETHERNET

#endif

#ifndef DELAY_FOREVER
#define DELAY_FOREVER portMAX_DELAY
#endif

// How often the free-heap line is written to the debug log. The Power thread polls every
// 20s once it is initialized, so that is the effective granularity. Set to 0 to disable.
#ifndef HEAP_LOG_INTERVAL_MS
#define HEAP_LOG_INTERVAL_MS (5 * 60 * 1000)
#endif

#if defined(BATTERY_PIN) && defined(ARCH_ESP32)

#ifndef BAT_MEASURE_ADC_UNIT // ADC1 is default
static const adc_channel_t adc_channel = ADC_CHANNEL;
static const adc_unit_t unit = ADC_UNIT_1;
#else  // ADC2
static const adc_channel_t adc_channel = ADC_CHANNEL;
static const adc_unit_t unit = ADC_UNIT_2;
#endif // BAT_MEASURE_ADC_UNIT

static adc_oneshot_unit_handle_t adc_handle = nullptr;
static adc_cali_handle_t adc_cali_handle = nullptr;
static bool adc_calibrated = false;
#ifndef ADC_ATTENUATION
static const adc_atten_t atten = ADC_ATTEN_DB_12;
#else
static const adc_atten_t atten = ADC_ATTENUATION;
#endif
#ifdef ADC_BITWIDTH
static const adc_bitwidth_t adc_width = ADC_BITWIDTH;
#else
static const adc_bitwidth_t adc_width = ADC_BITWIDTH_DEFAULT;
#endif

static int adcBitWidthToBits(adc_bitwidth_t width)
{
    switch (width) {
    case ADC_BITWIDTH_9:
        return 9;
    case ADC_BITWIDTH_10:
        return 10;
    case ADC_BITWIDTH_11:
        return 11;
    case ADC_BITWIDTH_12:
        return 12;
#ifdef ADC_BITWIDTH_13
    case ADC_BITWIDTH_13:
        return 13;
#endif
    default:
        return 12;
    }
}

static bool initAdcCalibration()
{
#if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
    adc_cali_curve_fitting_config_t cali_config = {
        .unit_id = unit,
        .atten = atten,
        .bitwidth = adc_width,
    };
    esp_err_t ret = adc_cali_create_scheme_curve_fitting(&cali_config, &adc_cali_handle);
    if (ret == ESP_OK) {
        LOG_INFO("ADC calibration: curve fitting enabled");
        return true;
    }
    if (ret != ESP_ERR_NOT_SUPPORTED) {
        LOG_WARN("ADC calibration: curve fitting failed: %s", esp_err_to_name(ret));
    }
#endif

#if ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED
    adc_cali_line_fitting_config_t cali_config = {
        .unit_id = unit,
        .atten = atten,
        .bitwidth = adc_width,
        .default_vref = DEFAULT_VREF,
    };
    esp_err_t ret = adc_cali_create_scheme_line_fitting(&cali_config, &adc_cali_handle);
    if (ret == ESP_OK) {
        LOG_INFO("ADC calibration: line fitting enabled");
        return true;
    }
    if (ret != ESP_ERR_NOT_SUPPORTED) {
        LOG_WARN("ADC calibration: line fitting failed: %s", esp_err_to_name(ret));
    }
#endif

    LOG_INFO("ADC calibration unsupported; use approx scaling");
    return false;
}

#endif // BATTERY_PIN && ARCH_ESP32

#ifdef EXT_PWR_DETECT
#ifndef EXT_PWR_DETECT_MODE
#define EXT_PWR_DETECT_MODE INPUT
// If using internal pull resistors, we can infer EXT_PWR_DETECT_VALUE
#elif EXT_PWR_DETECT_MODE == INPUT_PULLUP
#define EXT_PWR_DETECT_VALUE LOW
#elif EXT_PWR_DETECT_MODE == INPUT_PULLDOWN
#define EXT_PWR_DETECT_VALUE HIGH
#endif
#ifndef EXT_PWR_DETECT_VALUE
#define EXT_PWR_DETECT_VALUE HIGH
#endif
#endif

#ifdef EXT_CHRG_DETECT
#ifndef EXT_CHRG_DETECT_MODE
#define EXT_CHRG_DETECT_MODE INPUT
// If using internal pull resistors, we can infer EXT_CHRG_DETECT_VALUE
#elif EXT_CHRG_DETECT_MODE == INPUT_PULLUP
#define EXT_CHRG_DETECT_VALUE LOW
#elif EXT_CHRG_DETECT_MODE == INPUT_PULLDOWN
#define EXT_CHRG_DETECT_VALUE HIGH
#endif
#ifndef EXT_CHRG_DETECT_VALUE
#define EXT_CHRG_DETECT_VALUE HIGH
#endif
#endif

#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
#if __has_include(<Adafruit_INA219.h>)
INA219Sensor ina219Sensor;
#else
NullSensor ina219Sensor;
#endif

#if __has_include(<INA226.h>)
INA226Sensor ina226Sensor;
#else
NullSensor ina226Sensor;
#endif

#if __has_include(<Adafruit_INA260.h>)
INA260Sensor ina260Sensor;
#else
NullSensor ina260Sensor;
#endif

#if __has_include(<INA3221.h>)
INA3221Sensor ina3221Sensor;
#else
NullSensor ina3221Sensor;
#endif

#endif

#if !MESHTASTIC_EXCLUDE_I2C
#include "modules/Telemetry/Sensor/MAX17048Sensor.h"
#include <utility>
extern std::pair<uint8_t, TwoWire *> nodeTelemetrySensorsMap[_meshtastic_TelemetrySensorType_MAX + 1];
#if HAS_TELEMETRY && (!MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_POWER_TELEMETRY)
#if __has_include(<Adafruit_MAX1704X.h>)
MAX17048Sensor max17048Sensor;
#else
NullSensor max17048Sensor;
#endif
#endif
#endif

#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && HAS_RAKPROT
RAK9154Sensor rak9154Sensor;
#endif

#ifdef HAS_PPM
// note: XPOWERS_CHIP_XXX must be defined in variant.h
#include <XPowersLib.h>
XPowersPPM *PPM = NULL;
#endif

#ifdef HAS_BQ27220
#include "bq27220.h"
#endif

#ifdef HAS_PMU
XPowersLibInterface *PMU = NULL;
#else

// Copy of the base class defined in axp20x.h.
// I'd rather not include axp20x.h as it brings Wire dependency.
class HasBatteryLevel
{
  public:
    /**
     * Battery state of charge, from 0 to 100 or -1 for unknown
     */
    virtual int getBatteryPercent() { return -1; }

    /**
     * The raw voltage of the battery or NAN if unknown
     */
    virtual uint16_t getBattVoltage() { return 0; }

    /**
     * return true if there is a battery installed in this unit
     */
    virtual bool isBatteryConnect() { return false; }

    virtual bool isVbusIn() { return false; }
    virtual bool isCharging() { return false; }
};
#endif

bool pmu_irq = false;

Power *power;

using namespace meshtastic;

// NRF52 has AREF_VOLTAGE defined in architecture.h but
// make sure it's included. If something is wrong with NRF52
// definition - compilation will fail on missing definition
#if !defined(AREF_VOLTAGE) && !defined(ARCH_NRF52)
#define AREF_VOLTAGE 3.3
#endif

/**
 * If this board has a battery level sensor, set this to a valid implementation
 */
static HasBatteryLevel *batteryLevel; // Default to NULL for no battery level sensor

#ifdef BATTERY_PIN

void battery_adcEnable()
{
#ifdef ADC_CTRL // enable adc voltage divider when we need to read
#ifdef ADC_USE_PULLUP
    pinMode(ADC_CTRL, INPUT_PULLUP);
#else
#ifdef HELTEC_V3
    pinMode(ADC_CTRL, INPUT);
    uint8_t adc_ctl_enable_value = !(digitalRead(ADC_CTRL));
    pinMode(ADC_CTRL, OUTPUT);
    digitalWrite(ADC_CTRL, adc_ctl_enable_value);
#else
    pinMode(ADC_CTRL, OUTPUT);
    digitalWrite(ADC_CTRL, ADC_CTRL_ENABLED);
#endif
#endif
    delay(10);
#endif
}

static void battery_adcDisable()
{
#ifdef ADC_CTRL // disable adc voltage divider when we need to read
#ifdef ADC_USE_PULLUP
    pinMode(ADC_CTRL, INPUT_PULLDOWN);
#else
#ifdef HELTEC_V3
    pinMode(ADC_CTRL, ANALOG);
#else
    digitalWrite(ADC_CTRL, !ADC_CTRL_ENABLED);
#endif
#endif
#endif
}

#endif

/**
 * A simple battery level sensor that assumes the battery voltage is attached
 * via a voltage-divider to an analog input
 */
class AnalogBatteryLevel : public HasBatteryLevel
{
  public:
    /**
     * Battery state of charge, from 0 to 100 or -1 for unknown
     */
    virtual int getBatteryPercent() override
    {
#if defined(HAS_RAKPROT) && !defined(HAS_PMU)
        if (hasRAK()) {
            return rak9154Sensor.getBusBatteryPercent();
        }
#endif

        float v = getBattVoltage();

        if (v < noBatVolt)
            return -1; // If voltage is super low assume no battery installed

#ifdef NO_BATTERY_LEVEL_ON_CHARGE
        // This does not work on a RAK4631 with battery connected
        if (v > chargingVolt)
            return 0; // While charging we can't report % full on the battery
#endif
        /**
         * @brief   Battery voltage lookup table interpolation to obtain a more
         * precise percentage rather than the old proportional one.
         * @author  Gabriele Russo
         * @date    06/02/2024
         */
        float battery_SOC = 0.0;
        uint16_t voltage = v / NUM_CELLS; // single cell voltage (average)
        for (int i = 0; i < NUM_OCV_POINTS; i++) {
            if (OCV[i] <= voltage) {
                if (i == 0) {
                    battery_SOC = 100.0; // 100% full
                } else {
                    // interpolate between OCV[i] and OCV[i-1]
                    battery_SOC = (float)100.0 / (NUM_OCV_POINTS - 1.0) *
                                  (NUM_OCV_POINTS - 1.0 - i + ((float)voltage - OCV[i]) / (OCV[i - 1] - OCV[i]));
                }
                break;
            }
        }
#if defined(BATTERY_CHARGING_INV)
        // bit of trickery to show 99% up until the charge finishes
        if (!digitalRead(BATTERY_CHARGING_INV) && battery_SOC > 99)
            battery_SOC = 99;
#endif
        return clamp((int)(battery_SOC), 0, 100);
    }

    /**
     * The raw voltage of the batteryin millivolts or NAN if unknown
     */
    virtual uint16_t getBattVoltage() override
    {

#if HAS_TELEMETRY && defined(HAS_RAKPROT) && !defined(HAS_PMU) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
        if (hasRAK()) {
            return getRAKVoltage();
        }
#endif

#if HAS_TELEMETRY && !defined(HAS_PMU) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
        if (hasINA()) {
            return getINAVoltage();
        }
#endif

#ifndef ADC_MULTIPLIER
#define ADC_MULTIPLIER 2.0
#endif

#ifndef BATTERY_SENSE_SAMPLES
#define BATTERY_SENSE_SAMPLES                                                                                                    \
    15 // Set the number of samples, it has an effect of increasing sensitivity in
       // complex electromagnetic environment.
#endif

#ifdef BATTERY_PIN
        // Override variant or default ADC_MULTIPLIER if we have the override pref
        float operativeAdcMultiplier =
            config.power.adc_multiplier_override > 0 ? config.power.adc_multiplier_override : ADC_MULTIPLIER;
        // Do not call analogRead() often.
        const uint32_t min_read_interval = 5000;
        if (!initial_read_done || !Throttle::isWithinTimespanMs(last_read_time_ms, min_read_interval)) {
            last_read_time_ms = millis();

            uint32_t raw = 0;
            float scaled = 0;

            battery_adcEnable();
#ifdef ARCH_STM32
            // STM32 ADC with VREFINT runtime calibration
            Vref = __LL_ADC_CALC_VREFANALOG_VOLTAGE(analogRead(AVREF), LL_ADC_RESOLUTION);
            raw = analogRead(BATTERY_PIN);
            scaled = __LL_ADC_CALC_DATA_TO_VOLTAGE(Vref, raw, LL_ADC_RESOLUTION);
            scaled *= operativeAdcMultiplier;
#elif defined(ARCH_ESP32) // ADC block for espressif platforms
            raw = espAdcRead();
            int voltage_mv = 0;
            if (adc_calibrated && adc_cali_handle) {
                if (adc_cali_raw_to_voltage(adc_cali_handle, raw, &voltage_mv) != ESP_OK) {
                    LOG_WARN("ADC calibration read failed; using raw value");
                    voltage_mv = 0;
                }
            }
            if (voltage_mv == 0) {
                // Fallback approximate conversion without calibration
                const int bits = adcBitWidthToBits(adc_width);
                const float max_code = powf(2.0f, bits) - 1.0f;
                voltage_mv = (int)((raw / max_code) * DEFAULT_VREF);
            }
            scaled = voltage_mv * operativeAdcMultiplier;
#else                     // block for all other platforms
#ifdef ARCH_NRF52
            concurrency::LockGuard saadcGuard(concurrency::nrf52SaadcLock);
#endif
            for (uint32_t i = 0; i < BATTERY_SENSE_SAMPLES; i++) {
                raw += analogRead(BATTERY_PIN);
            }
            raw = raw / BATTERY_SENSE_SAMPLES;
            scaled = operativeAdcMultiplier * ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * raw;
#endif
            battery_adcDisable();

            if (!initial_read_done) {
                // Flush the smoothing filter with an ADC reading, if the reading is
                // plausibly correct
                if (scaled > last_read_value)
                    last_read_value = scaled;
                initial_read_done = true;
            } else {
                // Already initialized - filter this reading
                last_read_value += (scaled - last_read_value) * 0.5; // Virtual LPF
            }

            // LOG_DEBUG("battery gpio %d raw val=%u scaled=%u filtered=%u",
            // BATTERY_PIN, raw, (uint32_t)(scaled), (uint32_t) (last_read_value));
        }
        return last_read_value;
#endif // BATTERY_PIN
        return 0;
    }

#if defined(ARCH_ESP32) && !defined(HAS_PMU) && defined(BATTERY_PIN)
    /**
     * ESP32 specific function for getting calibrated ADC reads
     */
    uint32_t espAdcRead()
    {

        uint32_t raw = 0;
        uint8_t raw_c = 0; // raw reading counter

        if (!adc_handle) {
            LOG_ERROR("ADC oneshot handle not initialized");
            return 0;
        }

        for (int i = 0; i < BATTERY_SENSE_SAMPLES; i++) {
            int val = 0;
            esp_err_t err = adc_oneshot_read(adc_handle, adc_channel, &val);
            if (err == ESP_OK) {
                raw += val;
                raw_c++;
            } else {
                LOG_DEBUG("ADC read failed: %s", esp_err_to_name(err));
            }
        }

        return (raw / (raw_c < 1 ? 1 : raw_c));
    }
#endif

    /**
     * return true if there is a battery installed in this unit
     */
    // if we have a integrated device with a battery, we can assume that the
    // battery is always connected
#ifdef BATTERY_IMMUTABLE
    virtual bool isBatteryConnect() override { return true; }
#elif defined(ADC_V)
    virtual bool isBatteryConnect() override
    {
        int lastReading = digitalRead(ADC_V);
        // 判断值是否变化
        for (int i = 2; i < 500; i++) {
            int reading = digitalRead(ADC_V);
            if (reading != lastReading) {
                return false; // 有变化，USB供电, 没接电池
            }
        }

        return true;
    }
#else
    virtual bool isBatteryConnect() override { return getBatteryPercent() != -1; }
#endif

    // Detect if an external power source is connected if we don’t have a PMIC;
    // Firstly prefer EXT_PWR_DETECT GPIO if available,
    // secondly try an nRF52-specific routine on some variants,
    // lastly provide a fallback to indicate external power when fully charged.
    virtual bool isVbusIn() override
    {
#ifdef HAS_SGM41562
        if (sgm41562 && sgm41562->refresh())
            return sgm41562->isInputPowerGood();
#endif
#ifdef EXT_PWR_DETECT
        if (digitalRead(EXT_PWR_DETECT) == EXT_PWR_DETECT_VALUE)
            return true;
#ifdef EXT_CHRG_DETECT
        // EXT_PWR_DETECT alone may not catch active charging (e.g. a charge-complete
        // pin that only asserts once the battery is full) - CHRG being active implies
        // power is present regardless.
        return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE;
#else
        return false;
#endif

// technically speaking this should work for all(?) NRF52 boards
// but needs testing across multiple devices. NRF52 USB would not even work if
// VBUS was not properly connected and detected by the CPU
#elif defined(MUZI_BASE) || defined(PROMICRO_DIY_TCXO) || defined(ELECROW_ThinkNode_M8)
        return powerHAL_isVBUSConnected();
#endif
        return getBattVoltage() > chargingVolt;
    }

    /// Assume charging if we have a battery and external power is connected.
    /// we can't be smart enough to say 'full'?
    virtual bool isCharging() override
    {
#ifdef HAS_SGM41562
        if (sgm41562 && sgm41562->refresh())
            return sgm41562->isCharging();
#endif
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT) && !defined(HAS_PMU)
        if (hasRAK()) {
            return (rak9154Sensor.isCharging()) ? OptTrue : OptFalse;
        }
#endif
#if defined(ELECROW_ThinkNode_M6)
        return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE || isVbusIn();
#elif defined(EXT_CHRG_DETECT)
        return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE;
#elif defined(BATTERY_CHARGING_INV)
        return !digitalRead(BATTERY_CHARGING_INV);
#else
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(DISABLE_INA_CHARGING_DETECTION)
        if (hasINA()) {
            // get current flow from INA sensor - negative value means power flowing
            // into the battery default assuming  BATTERY+  <--> INA_VIN+ <--> SHUNT
            // RESISTOR <--> INA_VIN- <--> LOAD
            LOG_TRACE("Using INA on I2C addr 0x%x for charging detection", config.power.device_battery_ina_address);
#if defined(INA_CHARGING_DETECTION_INVERT)
            return getINACurrent() > 0;
#else
            return getINACurrent() < 0;
#endif
        }
        return isBatteryConnect() && isVbusIn();
#endif
#endif
        // by default, we check the battery voltage only
        return isVbusIn();
    }

  private:
    /// If we see a battery voltage higher than physics allows - assume charger is
    /// pumping in power

    /// For heltecs with no battery connected, the measured voltage is 2204, so
    // need to be higher than that, in this case is 2500mV (3000-500)
    const uint16_t OCV[NUM_OCV_POINTS] = {OCV_ARRAY};
    const float chargingVolt = (OCV[0] + 10) * NUM_CELLS;
    const float noBatVolt = (OCV[NUM_OCV_POINTS - 1] - 500) * NUM_CELLS;
    // Start value from minimum voltage for the filter to not start from 0
    // that could trigger some events.
    // This value is over-written by the first ADC reading, it the voltage seems
    // reasonable.
    bool initial_read_done = false;
    float last_read_value = (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS);
    uint32_t last_read_time_ms = 0;
#ifdef ARCH_STM32
    // 3300mV placeholder for STM32 errata where VREFINT factory calibration may be missing
    // (e.g. STM32U0, see DS14756 Rev 3 §2.4.1 "VREFINT offset")
    uint32_t Vref = 3300;
#endif

#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT)

    uint16_t getRAKVoltage() { return rak9154Sensor.getBusVoltageMv(); }

    bool hasRAK()
    {
        if (!rak9154Sensor.isInitialized())
            return rak9154Sensor.runOnce() > 0;
        return rak9154Sensor.isRunning();
    }
#endif

#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
    uint16_t getINAVoltage()
    {
        if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA219].first == config.power.device_battery_ina_address) {
            return ina219Sensor.getBusVoltageMv();
        } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA226].first ==
                   config.power.device_battery_ina_address) {
            return ina226Sensor.getBusVoltageMv();
        } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA260].first ==
                   config.power.device_battery_ina_address) {
            return ina260Sensor.getBusVoltageMv();
        } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA3221].first ==
                   config.power.device_battery_ina_address) {
            return ina3221Sensor.getBusVoltageMv();
        }
        return 0;
    }

    int16_t getINACurrent()
    {
        if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA219].first == config.power.device_battery_ina_address) {
            return ina219Sensor.getCurrentMa();
        } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA226].first ==
                   config.power.device_battery_ina_address) {
            return ina226Sensor.getCurrentMa();
        } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA3221].first ==
                   config.power.device_battery_ina_address) {
            return ina3221Sensor.getCurrentMa();
        }
        return 0;
    }

    bool hasINA()
    {
        if (!config.power.device_battery_ina_address) {
            return false;
        }
        if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA219].first == config.power.device_battery_ina_address) {
            if (!ina219Sensor.isInitialized())
                return ina219Sensor.runOnce() > 0;
            return ina219Sensor.isRunning();
        } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA226].first ==
                   config.power.device_battery_ina_address) {
            if (!ina226Sensor.isInitialized())
                return ina226Sensor.runOnce() > 0;
            return ina226Sensor.isRunning();
        } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA260].first ==
                   config.power.device_battery_ina_address) {
            if (!ina260Sensor.isInitialized())
                return ina260Sensor.runOnce() > 0;
            return ina260Sensor.isRunning();
        } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA3221].first ==
                   config.power.device_battery_ina_address) {
            if (!ina3221Sensor.isInitialized())
                return ina3221Sensor.runOnce() > 0;
            return ina3221Sensor.isRunning();
        }
        return false;
    }
#endif
};

static AnalogBatteryLevel analogLevel;

Power::Power() : OSThread("Power")
{
    statusHandler = {};
    low_voltage_counter = 0;
#ifdef DEBUG_HEAP
    lastheap = memGet.getFreeHeap();
#endif
}

bool Power::analogInit()
{
#ifdef EXT_PWR_DETECT
    pinMode(EXT_PWR_DETECT, EXT_PWR_DETECT_MODE);
#endif
#ifdef EXT_CHRG_DETECT
    pinMode(EXT_CHRG_DETECT, EXT_CHRG_DETECT_MODE);
#endif

#ifdef BATTERY_PIN
    LOG_DEBUG("Use analog input %d for battery level", BATTERY_PIN);

    // disable any internal pullups
    pinMode(BATTERY_PIN, INPUT);

#ifndef BATTERY_SENSE_RESOLUTION_BITS
#define BATTERY_SENSE_RESOLUTION_BITS 10
#endif

#ifdef ARCH_STM32
    analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS);
#elif defined(ARCH_ESP32) // ESP32 needs special analog stuff
    adc_oneshot_unit_init_cfg_t init_config = {
        .unit_id = unit,
    };

    if (!adc_handle) {
        esp_err_t err = adc_oneshot_new_unit(&init_config, &adc_handle);
        if (err != ESP_OK) {
            LOG_ERROR("ADC oneshot init failed: %s", esp_err_to_name(err));
            return false;
        }
    }

    adc_oneshot_chan_cfg_t chan_cfg = {
        .atten = atten,
        .bitwidth = adc_width,
    };

    esp_err_t err = adc_oneshot_config_channel(adc_handle, adc_channel, &chan_cfg);
    if (err != ESP_OK) {
        LOG_ERROR("ADC channel config failed: %s", esp_err_to_name(err));
        return false;
    }

    adc_calibrated = initAdcCalibration();
#endif                    // ARCH_ESP32

    // NRF52 ADC init moved to powerHAL_init in nrf52 platform

#if !defined(ARCH_ESP32) && !defined(ARCH_STM32)
    analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS);
#endif

    batteryLevel = &analogLevel;
    return true;
#else
    return false;
#endif
}

/**
 * Initializes the Power class.
 *
 * @return true if the setup was successful, false otherwise.
 */
bool Power::setup()
{
#ifdef HAS_SGM41562
    // Initialize the charger early so AnalogBatteryLevel can read charging
    // state from it. The charger does not provide battery voltage / percent -
    // those still come from the platform ADC via analogInit() below.
    initSGM41562(SGM41562_WIRE);
#endif
    bool found = false;
    if (axpChipInit()) {
        found = true;
    } else if (cw2015Init()) {
        found = true;
    } else if (max17048Init()) {
        found = true;
    } else if (lipoChargerInit()) {
        found = true;
    } else if (serialBatteryInit()) {
        found = true;
    } else if (meshSolarInit()) {
        found = true;
    } else if (analogInit()) {
        found = true;
    } else {
#ifdef NRF_APM
        found = true;
#endif
    }
    attachPowerInterrupts();
    enabled = found;
    low_voltage_counter = 0;

#ifdef ARCH_ESP32
    // Register callbacks for before and after lightsleep
    // Used to detach and reattach interrupts
    lsObserver.observe(&notifyLightSleep);
    lsEndObserver.observe(&notifyLightSleepEnd);
#endif

    return found;
}

void Power::powerCommandsCheck()
{
    // 0 means "not scheduled" for both, and reads as long expired - test it first.
    if (rebootAtMsec && Throttle::deadlinePassed(rebootAtMsec)) {
        LOG_INFO("Rebooting");
        reboot();
    }

    if (shutdownAtMsec && Throttle::deadlinePassed(shutdownAtMsec)) {
        shutdownAtMsec = 0;
        shutdown();
    }
}

void Power::reboot()
{
    notifyReboot.notifyObservers(NULL);
#if defined(ARCH_ESP32)
    ESP.restart();
#elif defined(ARCH_NRF52)
    NVIC_SystemReset();
#elif defined(ARCH_RP2040)
    rp2040.reboot();
#elif defined(ARCH_PORTDUINO_WASM)
    // Browser/headless WASM node: no in-process restart. notifyReboot above
    // already let modules persist; hand off to the host (reboot() ->
    // location.reload() in a tab, or Module.onReboot() headless). Deliberately
    // skip the ARCH_PORTDUINO SPI/Wire/Serial teardown below - it would kill the
    // radio with no actual restart to follow, leaving a wedged node. Must come
    // before the ARCH_PORTDUINO arm: the wasm build defines both macros.
    ::reboot();
#elif defined(ARCH_PORTDUINO)
    deInitApiServer();
#ifdef __linux__
    if (aLinuxInputImpl)
        aLinuxInputImpl->deInit();
    if (aLinuxJoystick)
        aLinuxJoystick->deInit();
#endif
    SPI.end();
    Wire.end();
    Serial1.end();
    if (screen) {
        screen = nullptr;
    }
    LOG_DEBUG("final reboot");
    ::reboot();
#elif defined(ARCH_STM32)
    HAL_NVIC_SystemReset();
#else
    // 0 disarms; UINT32_MAX would read as long expired and reboot-loop.
    rebootAtMsec = 0;
    LOG_WARN("FIXME implement reboot for this platform. Note that some settings "
             "require a restart to be applied");
#endif
}

void Power::shutdown()
{

#if HAS_SCREEN
    if (screen) {
#ifdef T_DECK_PRO
        screen->showSimpleBanner("Device is powered off.\nConnect USB to start!",
                                 0); // T-Deck Pro has no power button
#elif defined(USE_EINK)
        screen->showSimpleBanner("Shutting Down...",
                                 2250); // dismiss after 3 seconds to avoid the
                                        // banner on the sleep screen
#else
        screen->showSimpleBanner("Shutting Down...", 0); // stays on screen
#endif
    }
#endif
#if !defined(ARCH_STM32WL)
    playShutdownMelody();
#endif
    nodeDB->saveToDisk();
#if HAS_SCREEN
    messageStore.saveToFlash();
#endif
#if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)
#ifdef PIN_LED1
    ledOff(PIN_LED1);
#endif
#ifdef PIN_LED2
    ledOff(PIN_LED2);
#endif
#ifdef PIN_LED3
    ledOff(PIN_LED3);
#endif
#ifdef LED_NOTIFICATION
    ledOff(LED_NOTIFICATION);
#endif
    doDeepSleep(DELAY_FOREVER, true, true);
#elif defined(ARCH_PORTDUINO)
    exit(EXIT_SUCCESS);
#else
    LOG_WARN("FIXME implement shutdown for this platform");
#endif
}

/// Reads power status to powerStatus singleton.
//
// TODO(girts): move this and other axp stuff to power.h/power.cpp.
void Power::readPowerStatus()
{
    int32_t batteryVoltageMv = -1; // Assume unknown
    int8_t batteryChargePercent = -1;
    OptionalBool usbPowered = OptUnknown;
    OptionalBool hasBattery = OptUnknown; // These must be static because NRF_APM
                                          // code doesn't run every time
    OptionalBool isChargingNow = OptUnknown;

    if (batteryLevel) {
        hasBattery = batteryLevel->isBatteryConnect() ? OptTrue : OptFalse;
#ifndef NRF_APM
        usbPowered = batteryLevel->isVbusIn() ? OptTrue : OptFalse;
        isChargingNow = batteryLevel->isCharging() ? OptTrue : OptFalse;
#endif
        if (hasBattery) {
            batteryVoltageMv = batteryLevel->getBattVoltage();
            // If the AXP192 returns a valid battery percentage, use it
            if (batteryLevel->getBatteryPercent() >= 0) {
                batteryChargePercent = batteryLevel->getBatteryPercent();
            } else {
                // If the AXP192 returns a percentage less than 0, the feature is either
                // not supported or there is an error In that case, we compute an
                // estimate of the charge percent based on open circuit voltage table
                // defined in power.h
                batteryChargePercent = clamp((int)(((batteryVoltageMv - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS)) * 1e2) /
                                                   ((OCV[0] * NUM_CELLS) - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS))),
                                             0, 100);
            }
        }
    }

// FIXME: IMO we shouldn't be littering our code with all these ifdefs.  Way
// better instead to make a Nrf52IsUsbPowered subclass (which shares a
// superclass with the BatteryLevel stuff) that just provides a few methods. But
// in the interest of fixing this bug I'm going to follow current practice.
#ifdef NRF_APM // Section of code detects USB power on the RAK4631 and updates
               // the power states.  Takes 20 seconds or so to detect changes.

    nrfx_power_usb_state_t nrf_usb_state = nrfx_power_usbstatus_get();
    // LOG_DEBUG("NRF Power %d", nrf_usb_state);

    // If changed to DISCONNECTED
    if (nrf_usb_state == NRFX_POWER_USB_STATE_DISCONNECTED)
        isChargingNow = usbPowered = OptFalse;
    // If changed to CONNECTED / READY
    else
        isChargingNow = usbPowered = OptTrue;

#endif

    // Notify any status instances that are observing us
    const PowerStatus powerStatus2 = PowerStatus(hasBattery, usbPowered, isChargingNow, batteryVoltageMv, batteryChargePercent);

    // Log battery-presence transitions once; skip OptUnknown so we don't lie before the first probe.
    static OptionalBool prevHasBattery = OptUnknown;
    if (hasBattery != OptUnknown && hasBattery != prevHasBattery) {
        LOG_INFO("Power: battery hardware %s", hasBattery == OptTrue ? "detected" : "absent (USB-only)");
        prevHasBattery = hasBattery;
    }

    // Periodic telemetry only emits when a battery is actually present (otherwise values are constant -1/0).
    if (hasBattery == OptTrue && !Throttle::isWithinTimespanMs(lastLogTime, 50 * 1000)) {
        LOG_DEBUG("Battery: usbPower=%d, isCharging=%d, batMv=%d, batPct=%d", powerStatus2.getHasUSB(),
                  powerStatus2.getIsCharging(), powerStatus2.getBatteryVoltageMv(), powerStatus2.getBatteryChargePercent());
        lastLogTime = millis();
    }
    newStatus.notifyObservers(&powerStatus2);

    // Mirror battery level to the BLE Battery Service (0x2A19); the platform layer clamps and dedupes.
    if (hasBattery == OptTrue)
        updateBatteryLevel(powerStatus2.getBatteryChargePercent());
#ifdef DEBUG_HEAP
    if (lastheap != memGet.getFreeHeap()) {
        // Use stack-allocated buffer to avoid heap allocations in monitoring code
        char threadlist[256] = "Threads running:";
        int threadlistLen = strlen(threadlist);
        int running = 0;
        for (int i = 0; i < MAX_THREADS; i++) {
            auto thread = concurrency::mainController.get(i);
            if ((thread != nullptr) && (thread->enabled)) {
                // Use snprintf to safely append to stack buffer without heap allocation
                int remaining = sizeof(threadlist) - threadlistLen - 1;
                if (remaining > 0) {
                    int written = snprintf(threadlist + threadlistLen, remaining, " %s", thread->ThreadName.c_str());
                    if (written > 0 && written < remaining) {
                        threadlistLen += written;
                    }
                }
                running++;
            }
        }
        LOG_HEAP(threadlist);
        LOG_HEAP("Heap status: %d/%d bytes free (%d), running %d/%d threads", memGet.getFreeHeap(), memGet.getHeapSize(),
                 memGet.getFreeHeap() - lastheap, running, concurrency::mainController.size(false));
        lastheap = memGet.getFreeHeap();
    }
#ifdef DEBUG_HEAP_MQTT
    if (mqtt) {
        // send MQTT-Packet with Heap-Size
        uint8_t dmac[6];
        getMacAddr(dmac); // Get our hardware ID
        char mac[18];
        sprintf(mac, "!%02x%02x%02x%02x", dmac[2], dmac[3], dmac[4], dmac[5]);

        auto newHeap = memGet.getFreeHeap();
        // Use stack-allocated buffers to avoid heap allocations in monitoring code
        char heapTopic[128];
        snprintf(heapTopic, sizeof(heapTopic), "%s/2/heap/%s", (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh"), mac);
        char heapString[16];
        snprintf(heapString, sizeof(heapString), "%u", newHeap);
        mqtt->pubSub.publish(heapTopic, heapString, false);

        auto wifiRSSI = WiFi.RSSI();
        char wifiTopic[128];
        snprintf(wifiTopic, sizeof(wifiTopic), "%s/2/wifi/%s", (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh"), mac);
        char wifiString[16];
        snprintf(wifiString, sizeof(wifiString), "%d", wifiRSSI);
        mqtt->pubSub.publish(wifiTopic, wifiString, false);
    }
#endif

#endif

    // If we have a battery at all and it is less than 0%, force deep sleep if we
    // have more than 10 low readings in a row. NOTE: min LiIon/LiPo voltage
    // is 2.0 to 2.5V, current OCV min is set to 3100 that is large enough.
    //

    if (batteryLevel && powerStatus2.getHasBattery() && !powerStatus2.getHasUSB()) {
        if (batteryLevel->getBattVoltage() < OCV[NUM_OCV_POINTS - 1]) {
            low_voltage_counter++;
            LOG_DEBUG("Low voltage counter: %d/10", low_voltage_counter);
            if (low_voltage_counter > 10) {
                LOG_INFO("Low voltage detected, trigger deep sleep");
                powerFSM.trigger(EVENT_LOW_BATTERY);
            }
        } else {
            low_voltage_counter = 0;
        }
    }
}

/**
 * Emit a free-heap line to the debug log every HEAP_LOG_INTERVAL_MS, so a slow leak shows up
 * as a trend in a field log instead of only as an out-of-memory reboot. Unlike the DEBUG_HEAP
 * instrumentation above this is always on, and costs one line per interval.
 */
void Power::logHeapUsage()
{
#if HEAP_LOG_INTERVAL_MS > 0
    if (Throttle::isWithinTimespanMs(lastHeapLogTime, HEAP_LOG_INTERVAL_MS))
        return;

    const uint32_t heapTotal = memGet.getHeapSize();
    // Platforms without heap accounting report UINT32_MAX (or 0) - nothing worth logging
    if (heapTotal == 0 || heapTotal == UINT32_MAX)
        return;

    const uint32_t heapFree = memGet.getFreeHeap();
    // The first line has no earlier sample to difference against
    const int32_t delta = lastHeapLogTime ? (int32_t)(heapFree - lastHeapLogFree) : 0;

    const uint32_t psramTotal = memGet.getPsramSize();
    if (psramTotal)
        LOG_INFO("Heap: %u/%u bytes free (%d since last), PSRAM: %u/%u bytes free", heapFree, heapTotal, delta,
                 memGet.getFreePsram(), psramTotal);
    else
        LOG_INFO("Heap: %u/%u bytes free (%d since last)", heapFree, heapTotal, delta);

    lastHeapLogFree = heapFree;
    lastHeapLogTime = millis();
#endif
}

int32_t Power::runOnce()
{
    readPowerStatus();
    logHeapUsage();
    lipoChargerRetry();

#ifdef HAS_PMU
    // WE no longer use the IRQ line to wake the CPU (due to false wakes from
    // sleep), but we do poll the IRQ status by reading the registers over I2C
    if (PMU) {

        PMU->getIrqStatus();

        if (PMU->isVbusRemoveIrq()) {
            LOG_INFO("USB unplugged");
            powerFSM.trigger(EVENT_POWER_DISCONNECTED);
        }

        if (PMU->isVbusInsertIrq()) {
            LOG_INFO("USB plugged In");
            powerFSM.trigger(EVENT_POWER_CONNECTED);
        }

#ifdef PMU_POWER_BUTTON_IS_CANCEL
        // cancel action also turns the screen on and off.
        if (PMU->isPekeyShortPressIrq()) {
            LOG_INFO("Input: Corona Button Click");
            InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_CANCEL, .kbchar = 0, .touchX = 0, .touchY = 0};
            inputBroker->injectInputEvent(&event);
        }
#endif
        /*
        Other things we could check if we cared...

        if (PMU->isBatChagerStartIrq()) {
            LOG_DEBUG("Battery start charging");
        }
        if (PMU->isBatChagerDoneIrq()) {
            LOG_DEBUG("Battery fully charged");
        }
        if (PMU->isBatInsertIrq()) {
            LOG_DEBUG("Battery inserted");
        }
        if (PMU->isBatRemoveIrq()) {
            LOG_DEBUG("Battery removed");
        }
        */

        PMU->clearIrqStatus();
    }
#endif
    // Only read once every 20 seconds once the power status for the app has been
    // initialized
    return (statusHandler && statusHandler->isInitialized()) ? (1000 * 20) : RUN_SAME;
}

#ifdef ARCH_ESP32

// Detach our class' interrupts before lightsleep
// Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press
int Power::beforeLightSleep(void *unused)
{
    LOG_WARN("Detaching power interrupts for sleep");
    detachPowerInterrupts();
    return 0; // Indicates success
}

// Reconfigure our interrupts
// Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep
int Power::afterLightSleep(esp_sleep_wakeup_cause_t cause)
{
    attachPowerInterrupts();
    return 0; // Indicates success
}

#endif

/*
 * Attach (or re-attach) hardware interrupts for power management
 * Public method. Used outside class when waking from MCU sleep
 */
void Power::attachPowerInterrupts()
{
#ifdef EXT_PWR_DETECT
    attachInterrupt(
        EXT_PWR_DETECT,
        []() {
            power->setIntervalFromNow(0);
            runASAP = true;
        },
        CHANGE);
#endif
#ifdef BATTERY_CHARGING_INV
    attachInterrupt(
        BATTERY_CHARGING_INV,
        []() {
            power->setIntervalFromNow(0);
            runASAP = true;
        },
        CHANGE);
#endif
#ifdef EXT_CHRG_DETECT
    attachInterrupt(
        EXT_CHRG_DETECT,
        []() {
            power->setIntervalFromNow(0);
            runASAP = true;
            BaseType_t higherWake = 0;
        },
        CHANGE);
#endif
#ifdef PMU_IRQ
    if (PMU) {
        attachInterrupt(
            PMU_IRQ,
            []() {
                pmu_irq = true;
                power->setIntervalFromNow(0);
                runASAP = true;
            },
            FALLING);
    }
#endif
}

/*
 * Detach the "normal" button interrupts.
 * Public method. Used before attaching a "wake-on-button" interrupt for MCU sleep
 */
void Power::detachPowerInterrupts()
{
#ifdef EXT_PWR_DETECT
    detachInterrupt(EXT_PWR_DETECT);
#endif
#ifdef BATTERY_CHARGING_INV
    detachInterrupt(BATTERY_CHARGING_INV);
#endif
#ifdef EXT_CHRG_DETECT
    detachInterrupt(EXT_CHRG_DETECT);
#endif
#ifdef PMU_IRQ
    if (PMU) {
        detachInterrupt(PMU_IRQ);
    }
#endif
}

/**
 * Init the power manager chip
 *
 * axp192 power
    DCDC1 0.7-3.5V @ 1200mA max -> OLED // If you turn this off you'll lose
 comms to the axp192 because the OLED and the axp192 share the same i2c bus,
 instead use ssd1306 sleep mode DCDC2 -> unused DCDC3 0.7-3.5V @ 700mA max ->
 ESP32 (keep this on!) LDO1 30mA -> charges GPS backup battery // charges the
 tiny J13 battery by the GPS to power the GPS ram (for a couple of days), can
 not be turned off LDO2 200mA -> LORA LDO3 200mA -> GPS
 *
 */
bool Power::axpChipInit()
{

#ifdef HAS_PMU

    TwoWire *w = NULL;

    // Use macro to distinguish which wire is used by PMU
#ifdef PMU_USE_WIRE1
    w = &Wire1;
#else
    w = &Wire;
#endif

    /**
     * It is not necessary to specify the wire pin,
     * just input the wire, because the wire has been initialized in main.cpp
     */
    if (!PMU) {
        PMU = new XPowersAXP2101(*w);
        if (!PMU->init()) {
            LOG_WARN("No AXP2101 power management");
            delete PMU;
            PMU = NULL;
        } else {
            LOG_INFO("AXP2101 PMU init succeeded");
        }
    }

    if (!PMU) {
        PMU = new XPowersAXP192(*w);
        if (!PMU->init()) {
            LOG_WARN("No AXP192 power management");
            delete PMU;
            PMU = NULL;
        } else {
            LOG_INFO("AXP192 PMU init succeeded");
        }
    }

    if (!PMU) {
        /*
         * In XPowersLib, if the XPowersAXPxxx object is released, Wire.end() will
         * be called at the same time. In order not to affect other devices, if the
         * initialization of the PMU fails, Wire needs to be re-initialized once, if
         * there are multiple devices sharing the bus.
         * * */
#ifndef PMU_USE_WIRE1
        w->begin(I2C_SDA, I2C_SCL);
#endif
        return false;
    }

    batteryLevel = PMU;

    if (PMU->getChipModel() == XPOWERS_AXP192) {

        // lora radio power channel
        PMU->setPowerChannelVoltage(XPOWERS_LDO2, 3300);
        PMU->enablePowerOutput(XPOWERS_LDO2);

        // oled module power channel,
        // disable it will cause abnormal communication between boot and AXP power
        // supply, do not turn it off
        PMU->setPowerChannelVoltage(XPOWERS_DCDC1, 3300);
        // enable oled power
        PMU->enablePowerOutput(XPOWERS_DCDC1);

        // gnss module power channel -  now turned on in setGpsPower
        PMU->setPowerChannelVoltage(XPOWERS_LDO3, 3300);
        // PMU->enablePowerOutput(XPOWERS_LDO3);

        // protected oled power source
        PMU->setProtectedChannel(XPOWERS_DCDC1);
        // protected esp32 power source
        PMU->setProtectedChannel(XPOWERS_DCDC3);

        // disable not use channel
        PMU->disablePowerOutput(XPOWERS_DCDC2);

        // disable all axp chip interrupt
        PMU->disableIRQ(XPOWERS_AXP192_ALL_IRQ);

        // Set constant current charging current
        PMU->setChargerConstantCurr(XPOWERS_AXP192_CHG_CUR_450MA);

        // Set up the charging voltage
        PMU->setChargeTargetVoltage(XPOWERS_AXP192_CHG_VOL_4V2);
    } else if (PMU->getChipModel() == XPOWERS_AXP2101) {

        /*The alternative version of T-Beam 1.1 differs from T-Beam V1.1 in that it
         * uses an AXP2101 power chip*/
        if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) {
            // Unuse power channel
            PMU->disablePowerOutput(XPOWERS_DCDC2);
            PMU->disablePowerOutput(XPOWERS_DCDC3);
            PMU->disablePowerOutput(XPOWERS_DCDC4);
            PMU->disablePowerOutput(XPOWERS_DCDC5);
            PMU->disablePowerOutput(XPOWERS_ALDO1);
            PMU->disablePowerOutput(XPOWERS_ALDO4);
            PMU->disablePowerOutput(XPOWERS_BLDO1);
            PMU->disablePowerOutput(XPOWERS_BLDO2);
            PMU->disablePowerOutput(XPOWERS_DLDO1);
            PMU->disablePowerOutput(XPOWERS_DLDO2);

            // GNSS RTC PowerVDD 3300mV
            PMU->setPowerChannelVoltage(XPOWERS_VBACKUP, 3300);
            PMU->enablePowerOutput(XPOWERS_VBACKUP);

            // ESP32 VDD 3300mV
            //  ! No need to set, automatically open , Don't close it
            //  PMU->setPowerChannelVoltage(XPOWERS_DCDC1, 3300);
            //  PMU->setProtectedChannel(XPOWERS_DCDC1);

            // LoRa VDD 3300mV
            PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300);
            PMU->enablePowerOutput(XPOWERS_ALDO2);

            // GNSS VDD 3300mV
            PMU->setPowerChannelVoltage(XPOWERS_ALDO3, 3300);
            PMU->enablePowerOutput(XPOWERS_ALDO3);
        } else if (HW_VENDOR == meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE ||
                   HW_VENDOR == meshtastic_HardwareModel_T_WATCH_S3) {
            // t-beam s3 core
            /**
             * gnss module power channel
             * The default ALDO4 is off, you need to turn on the GNSS power first,
             * otherwise it will be invalid during initialization
             */
            PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 3300);
            PMU->enablePowerOutput(XPOWERS_ALDO4);

            // lora radio power channel
            PMU->setPowerChannelVoltage(XPOWERS_ALDO3, 3300);
            PMU->enablePowerOutput(XPOWERS_ALDO3);

            // m.2 interface
            PMU->setPowerChannelVoltage(XPOWERS_DCDC3, 3300);
            PMU->enablePowerOutput(XPOWERS_DCDC3);

            /**
             * ALDO2 cannot be turned off.
             * It is a necessary condition for sensor communication.
             * It must be turned on to properly access the sensor and screen
             * It is also responsible for the power supply of PCF8563
             */
            PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300);
            PMU->enablePowerOutput(XPOWERS_ALDO2);

            // 6-axis , magnetometer ,bme280 , oled screen power channel
            PMU->setPowerChannelVoltage(XPOWERS_ALDO1, 3300);
            PMU->enablePowerOutput(XPOWERS_ALDO1);

            // sdcard (T-Beam S3) / gnns (T-Watch S3 Plus) power channel
            PMU->setPowerChannelVoltage(XPOWERS_BLDO1, 3300);
#ifndef T_WATCH_S3
            PMU->enablePowerOutput(XPOWERS_BLDO1);
#else
            // DRV2605 power channel
            PMU->setPowerChannelVoltage(XPOWERS_BLDO2, 3300);
            PMU->enablePowerOutput(XPOWERS_BLDO2);
#endif

            // PMU->setPowerChannelVoltage(XPOWERS_DCDC4, 3300);
            // PMU->enablePowerOutput(XPOWERS_DCDC4);

            // not use channel
            PMU->disablePowerOutput(XPOWERS_DCDC2); // not elicited
            PMU->disablePowerOutput(XPOWERS_DCDC5); // not elicited
            PMU->disablePowerOutput(XPOWERS_DLDO1); // Invalid power channel, it does not exist
            PMU->disablePowerOutput(XPOWERS_DLDO2); // Invalid power channel, it does not exist
            PMU->disablePowerOutput(XPOWERS_VBACKUP);
        } else if (HW_VENDOR == meshtastic_HardwareModel_TBEAM_BPF) {
            // T-Beam BPF rail map (per schematic LilyGo_TBeam_BPF r2025-05-08):
            //   DCDC1  -> ESP32 + OLED 3V3 (always on, protected)
            //   ALDO2  -> MicroSD 3V3    (OFF at reset, must enable)
            //   ALDO4  -> L76K GNSS 3V3  (OFF at reset, must enable)
            //   ALDO1/3, BLDO1/2, DLDO1 -> user headers / unused at boot, leave at reset defaults.
            // LoRa power is outside the PMU (external P-MOSFET switched by RF95_POWER_EN / IO16).
            PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 3300);
            PMU->enablePowerOutput(XPOWERS_ALDO4);

            PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300);
            PMU->enablePowerOutput(XPOWERS_ALDO2);

            // Make sure nothing's driving into an unused rail
            PMU->disablePowerOutput(XPOWERS_DCDC5);
            PMU->disablePowerOutput(XPOWERS_DLDO1);
        }

        // disable all axp chip interrupt
        PMU->disableIRQ(XPOWERS_AXP2101_ALL_IRQ);

        // Set the constant current charging current of AXP2101, temporarily use
        // 500mA by default
        PMU->setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_500MA);

        // Set up the charging voltage
        PMU->setChargeTargetVoltage(XPOWERS_AXP2101_CHG_VOL_4V2);
    }

    PMU->clearIrqStatus();

    // TBeam1.1 /T-Beam S3-Core has no external TS detection,
    // it needs to be disabled, otherwise it will cause abnormal charging
    PMU->disableTSPinMeasure();

    // PMU->enableSystemVoltageMeasure();
    PMU->enableVbusVoltageMeasure();
    PMU->enableBattVoltageMeasure();

    if (PMU->isChannelAvailable(XPOWERS_DCDC1)) {
        LOG_DEBUG("DC1  : %s   Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_DCDC1) ? "+" : "-",
                  PMU->getPowerChannelVoltage(XPOWERS_DCDC1));
    }
    if (PMU->isChannelAvailable(XPOWERS_DCDC2)) {
        LOG_DEBUG("DC2  : %s   Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_DCDC2) ? "+" : "-",
                  PMU->getPowerChannelVoltage(XPOWERS_DCDC2));
    }
    if (PMU->isChannelAvailable(XPOWERS_DCDC3)) {
        LOG_DEBUG("DC3  : %s   Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_DCDC3) ? "+" : "-",
                  PMU->getPowerChannelVoltage(XPOWERS_DCDC3));
    }
    if (PMU->isChannelAvailable(XPOWERS_DCDC4)) {
        LOG_DEBUG("DC4  : %s   Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_DCDC4) ? "+" : "-",
                  PMU->getPowerChannelVoltage(XPOWERS_DCDC4));
    }
    if (PMU->isChannelAvailable(XPOWERS_LDO2)) {
        LOG_DEBUG("LDO2 : %s   Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_LDO2) ? "+" : "-",
                  PMU->getPowerChannelVoltage(XPOWERS_LDO2));
    }
    if (PMU->isChannelAvailable(XPOWERS_LDO3)) {
        LOG_DEBUG("LDO3 : %s   Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_LDO3) ? "+" : "-",
                  PMU->getPowerChannelVoltage(XPOWERS_LDO3));
    }
    if (PMU->isChannelAvailable(XPOWERS_ALDO1)) {
        LOG_DEBUG("ALDO1: %s   Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_ALDO1) ? "+" : "-",
                  PMU->getPowerChannelVoltage(XPOWERS_ALDO1));
    }
    if (PMU->isChannelAvailable(XPOWERS_ALDO2)) {
        LOG_DEBUG("ALDO2: %s   Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_ALDO2) ? "+" : "-",
                  PMU->getPowerChannelVoltage(XPOWERS_ALDO2));
    }
    if (PMU->isChannelAvailable(XPOWERS_ALDO3)) {
        LOG_DEBUG("ALDO3: %s   Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_ALDO3) ? "+" : "-",
                  PMU->getPowerChannelVoltage(XPOWERS_ALDO3));
    }
    if (PMU->isChannelAvailable(XPOWERS_ALDO4)) {
        LOG_DEBUG("ALDO4: %s   Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_ALDO4) ? "+" : "-",
                  PMU->getPowerChannelVoltage(XPOWERS_ALDO4));
    }
    if (PMU->isChannelAvailable(XPOWERS_BLDO1)) {
        LOG_DEBUG("BLDO1: %s   Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_BLDO1) ? "+" : "-",
                  PMU->getPowerChannelVoltage(XPOWERS_BLDO1));
    }
    if (PMU->isChannelAvailable(XPOWERS_BLDO2)) {
        LOG_DEBUG("BLDO2: %s   Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_BLDO2) ? "+" : "-",
                  PMU->getPowerChannelVoltage(XPOWERS_BLDO2));
    }

// We can safely ignore this approach for most (or all) boards because MCU
// turned off earlier than battery discharged to 2.6V.
//
// Unfortunately for now we can't use this killswitch for RAK4630-based boards
// because they have a bug with battery voltage measurement. Probably it
// sometimes drops to low values.
#ifndef RAK4630
    // Set PMU shutdown voltage at 2.6V to maximize battery utilization
    PMU->setSysPowerDownVoltage(2600);
#endif

#ifdef PMU_IRQ
    uint64_t pmuIrqMask = 0;

    if (PMU->getChipModel() == XPOWERS_AXP192) {
        pmuIrqMask = XPOWERS_AXP192_VBUS_INSERT_IRQ | XPOWERS_AXP192_VBUS_REMOVE_IRQ | XPOWERS_AXP192_PKEY_SHORT_IRQ;
    } else if (PMU->getChipModel() == XPOWERS_AXP2101) {
        pmuIrqMask = XPOWERS_AXP2101_VBUS_INSERT_IRQ | XPOWERS_AXP2101_VBUS_REMOVE_IRQ | XPOWERS_AXP2101_PKEY_SHORT_IRQ;
    }

    pinMode(PMU_IRQ, INPUT);

    // We wake on IRQ, so only enable the IRQs that we care about.
    // we want USB plug and unplug to update the screen and LED status,
    // and short press on the power button to trigger the "cancel" action in the UI (which also turns the screen on and off).
    PMU->enableIRQ(pmuIrqMask);

    PMU->clearIrqStatus();
#endif /*PMU_IRQ*/

    readPowerStatus();

    pmu_found = true;

    return pmu_found;

#else
    return false;
#endif
}

#if !MESHTASTIC_EXCLUDE_I2C && __has_include(<Adafruit_MAX1704X.h>)

/**
 * Wrapper class for an I2C MAX17048 Lipo battery sensor.
 */
class MAX17048BatteryLevel : public HasBatteryLevel
{
  private:
    MAX17048Singleton *max17048 = nullptr;

  public:
    /**
     * Init the I2C MAX17048 Lipo battery level sensor
     */
    bool runOnce()
    {
        if (max17048 == nullptr) {
            max17048 = MAX17048Singleton::GetInstance();
        }

        // try to start if the sensor has been detected
        if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX17048].first != 0) {
            return max17048->runOnce(nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX17048].second);
        }
        return false;
    }

    /**
     * Battery state of charge, from 0 to 100 or -1 for unknown
     */
    virtual int getBatteryPercent() override { return max17048->getBusBatteryPercent(); }

    /**
     * The raw voltage of the battery in millivolts, or NAN if unknown
     */
    virtual uint16_t getBattVoltage() override { return max17048->getBusVoltageMv(); }

    /**
     * return true if there is a battery installed in this unit
     */
    virtual bool isBatteryConnect() override { return max17048->isBatteryConnected(); }

    /**
     * return true if there is an external power source detected
     */
    virtual bool isVbusIn() override { return max17048->isExternallyPowered(); }

    /**
     * return true if the battery is currently charging
     */
    virtual bool isCharging() override { return max17048->isBatteryCharging(); }
};

MAX17048BatteryLevel max17048Level;

/**
 * Init the Lipo battery level sensor
 */
bool Power::max17048Init()
{
    bool result = max17048Level.runOnce();
    LOG_DEBUG("Power::max17048Init lipo sensor is %s", result ? "ready" : "not ready yet");
    if (!result)
        return false;
    batteryLevel = &max17048Level;
    return true;
}

#else
/**
 * The Lipo battery level sensor is unavailable - default to AnalogBatteryLevel
 */
bool Power::max17048Init()
{
    return false;
}
#endif

#if !MESHTASTIC_EXCLUDE_I2C && HAS_CW2015

class CW2015BatteryLevel : public AnalogBatteryLevel
{
  public:
    /**
     * Battery state of charge, from 0 to 100 or -1 for unknown
     */
    virtual int getBatteryPercent() override
    {
        int data = -1;
        Wire.beginTransmission(CW2015_ADDR);
        Wire.write(0x04);
        if (Wire.endTransmission() == 0) {
            if (Wire.requestFrom(CW2015_ADDR, (uint8_t)1)) {
                data = Wire.read();
            }
        }
        return data;
    }

    /**
     * The raw voltage of the battery in millivolts, or NAN if unknown
     */
    virtual uint16_t getBattVoltage() override
    {
        uint16_t mv = 0;
        Wire.beginTransmission(CW2015_ADDR);
        Wire.write(0x02);
        if (Wire.endTransmission() == 0) {
            if (Wire.requestFrom(CW2015_ADDR, (uint8_t)2)) {
                mv = Wire.read();
                mv <<= 8;
                mv |= Wire.read();
                // Voltage is read in  305uV units, convert to mV
                mv = mv * 305 / 1000;
            }
        }
        return mv;
    }
};

CW2015BatteryLevel cw2015Level;

/**
 * Init the CW2015 battery level sensor
 */
bool Power::cw2015Init()
{

    Wire.beginTransmission(CW2015_ADDR);
    uint8_t getInfo[] = {0x0a, 0x00};
    Wire.write(getInfo, 2);
    Wire.endTransmission();
    delay(10);
    Wire.beginTransmission(CW2015_ADDR);
    Wire.write(0x00);
    bool result = false;
    if (Wire.endTransmission() == 0) {
        if (Wire.requestFrom(CW2015_ADDR, (uint8_t)1)) {
            uint8_t data = Wire.read();
            LOG_DEBUG("CW2015 init read data: 0x%x", data);
            if (data == 0x73) {
                result = true;
                batteryLevel = &cw2015Level;
            }
        }
    }
    return result;
}

#else
/**
 * The CW2015 battery level sensor is unavailable - default to AnalogBatteryLevel
 */
bool Power::cw2015Init()
{
    return false;
}
#endif

#if defined(HAS_PPM) && HAS_PPM

// The gauge is soldered on, so a failed init means wedged rather than absent - retry from
// the power thread before writing it off.
#define BQ27220_INIT_ATTEMPTS 3
#define BQ27220_RETRY_INTERVAL_MS (60 * 1000)

/**
 * Adapter class for BQ25896/BQ27220 Lipo battery charger.
 *
 * The gauge only adds time-to-full/empty, so its failure must not take the charger down.
 */
class LipoCharger : public HasBatteryLevel
{
  private:
    BQ27220 *bq = nullptr;
    uint8_t gaugeAttemptsLeft = BQ27220_INIT_ATTEMPTS;
    uint32_t lastGaugeAttemptMs = 0;

    // An aborted transfer leaves the i2c_master driver holding a stale transaction, which
    // the next transfer trips over. Deleting the bus frees it along with the interrupt.
    void recoverI2CBus()
    {
#ifdef ARCH_ESP32
        Wire.end();
        Wire.begin(I2C_SDA, I2C_SCL);
#endif
    }

  public:
    /**
     * Init the I2C BQ25896 Lipo battery charger
     */
    bool runOnce()
    {
        if (PPM == nullptr) {
            PPM = new XPowersPPM;
            bool result = PPM->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR);
            if (result) {
                LOG_INFO("PPM BQ25896 init succeeded");
                // Set the minimum operating voltage. Below this voltage, the PPM will
                // protect PPM->setSysPowerDownVoltage(3100);

                // Set input current limit, default is 500mA
                // PPM->setInputCurrentLimit(800);

                // Disable current limit pin
                // PPM->disableCurrentLimitPin();

                // Set the charging target voltage, Range:3840 ~ 4608mV ,step:16 mV
                PPM->setChargeTargetVoltage(4288);

                // Set the precharge current , Range: 64mA ~ 1024mA ,step:64mA
                // PPM->setPrechargeCurr(64);

                // The premise is that limit pin is disabled, or it will
                // only follow the maximum charging current set by limit pin.
                // Set the charging current , Range:0~5056mA ,step:64mA
                PPM->setChargerConstantCurr(1024);

                // To obtain voltage data, the ADC must be enabled first
                PPM->enableMeasure();

                // Turn on charging function
                // If there is no battery connected, do not turn on the charging
                // function
                PPM->enableCharge();
            } else {
                LOG_WARN("PPM BQ25896 init failed");
                delete PPM;
                PPM = nullptr;
                return false;
            }
        }
        gaugeRunOnce();
        // Ready on the charger alone, so Power stays enabled and can retry the gauge later.
        return true;
    }

    /// Bring up the BQ27220 fuel gauge, unless it is already up or out of attempts
    void gaugeRunOnce()
    {
        if (bq != nullptr || gaugeAttemptsLeft == 0)
            return;
        if (gaugeAttemptsLeft < BQ27220_INIT_ATTEMPTS &&
            Throttle::isWithinTimespanMs(lastGaugeAttemptMs, BQ27220_RETRY_INTERVAL_MS))
            return;

        lastGaugeAttemptMs = millis();
        gaugeAttemptsLeft--;

        // Cheap probe first: a silent gauge costs one transaction instead of the
        // multi-second unseal/reset/provision sequence inside init().
        Wire.beginTransmission(BQ27220_I2C_ADDRESS);
        if (Wire.endTransmission() != 0) {
            LOG_WARN("BQ27220 not responding at 0x%x", BQ27220_I2C_ADDRESS);
            return;
        }

        bq = new BQ27220;
        bq->setDefaultCapacity(BQ27220_DESIGN_CAPACITY);

        if (bq->init()) {
            LOG_DEBUG("BQ27220 design capacity: %d", bq->getDesignCapacity());
            LOG_DEBUG("BQ27220 fullCharge capacity: %d", bq->getFullChargeCapacity());
            LOG_DEBUG("BQ27220 remaining capacity: %d", bq->getRemainingCapacity());
            return;
        }

        delete bq;
        bq = nullptr;
        // init() bails out mid-sequence, so hand the next bus user a sane driver state.
        recoverI2CBus();
        LOG_WARN("BQ27220 init failed (%d retries left), use BQ25896 for battery state", (int)gaugeAttemptsLeft);
    }

    /**
     * Battery state of charge, from 0 to 100 or -1 for unknown
     */
    virtual int getBatteryPercent() override
    {
        return -1;
        // return bq->getChargePercent(); // don't use BQ27220 for battery percent,
        // it is not calibrated
    }

    /**
     * The raw voltage of the battery in millivolts, or NAN if unknown
     */
    virtual uint16_t getBattVoltage() override { return bq ? bq->getVoltage() : PPM->getBattVoltage(); }

    /**
     * return true if there is a battery installed in this unit
     */
    virtual bool isBatteryConnect() override { return PPM->getBattVoltage() > 0; }

    /**
     * return true if there is an external power source detected
     */
    virtual bool isVbusIn() override { return PPM->isVbusIn(); }

    /**
     * return true if the battery is currently charging
     */
    virtual bool isCharging() override
    {
        bool isCharging = PPM->isCharging();
        if (bq) {
            if (isCharging) {
                LOG_TRACE("BQ27220 time to full charge: %d min", bq->getTimeToFull());
            } else {
                if (!PPM->isVbusIn()) {
                    LOG_TRACE("BQ27220 time to empty: %d min (%d mAh)", bq->getTimeToEmpty(), bq->getRemainingCapacity());
                }
            }
        }
        return isCharging;
    }
};

LipoCharger lipoCharger;

/**
 * Init the Lipo battery charger
 */
bool Power::lipoChargerInit()
{
    bool result = lipoCharger.runOnce();
    LOG_DEBUG("Power::lipoChargerInit lipo sensor is %s", result ? "ready" : "not ready yet");
    if (!result)
        return false;
    batteryLevel = &lipoCharger;
    return true;
}

/// Retry a fuel gauge that did not come up during setup
void Power::lipoChargerRetry()
{
    lipoCharger.gaugeRunOnce();
}

#else
/**
 * The Lipo battery level sensor is unavailable - default to AnalogBatteryLevel
 */
bool Power::lipoChargerInit()
{
    return false;
}

void Power::lipoChargerRetry() {}
#endif

#ifdef HELTEC_MESH_SOLAR
#include "meshSolarApp.h"

/**
 * meshSolar class for an SMBUS battery sensor.
 */
class meshSolarBatteryLevel : public HasBatteryLevel
{

  public:
    /**
     * Init the I2C meshSolar battery level sensor
     */
    bool runOnce()
    {
        meshSolarStart();
        return true;
    }

    /**
     * Battery state of charge, from 0 to 100 or -1 for unknown
     */
    virtual int getBatteryPercent() override { return meshSolarGetBatteryPercent(); }

    /**
     * The raw voltage of the battery in millivolts, or NAN if unknown
     */
    virtual uint16_t getBattVoltage() override { return meshSolarGetBattVoltage(); }

    /**
     * return true if there is a battery installed in this unit
     */
    virtual bool isBatteryConnect() override { return meshSolarIsBatteryConnect(); }

    /**
     * return true if there is an external power source detected
     */
    virtual bool isVbusIn() override { return meshSolarIsVbusIn(); }

    /**
     * return true if the battery is currently charging
     */
    virtual bool isCharging() override { return meshSolarIsCharging(); }
};

meshSolarBatteryLevel meshSolarLevel;

/**
 * Init the meshSolar battery level sensor
 */
bool Power::meshSolarInit()
{
    bool result = meshSolarLevel.runOnce();
    LOG_DEBUG("Power::meshSolarInit sensor is %s", result ? "ready" : "not ready yet");
    if (!result)
        return false;
    batteryLevel = &meshSolarLevel;
    return true;
}

#else
/**
 * The meshSolar battery level sensor is unavailable - default to
 * AnalogBatteryLevel
 */
bool Power::meshSolarInit()
{
    return false;
}
#endif

#ifdef HAS_SERIAL_BATTERY_LEVEL
#include <SoftwareSerial.h>

/**
 * SerialBatteryLevel class for pulling battery information from a secondary MCU over serial.
 */
class SerialBatteryLevel : public HasBatteryLevel
{

  public:
    /**
     * Init the I2C meshSolar battery level sensor
     */
    bool runOnce()
    {
        BatterySerial.begin(4800);

        return true;
    }

    /**
     * Battery state of charge, from 0 to 100 or -1 for unknown
     */
    virtual int getBatteryPercent() override { return v_percent; }

    /**
     * The raw voltage of the battery in millivolts, or NAN if unknown
     */
    virtual uint16_t getBattVoltage() override { return voltage * 1000; }

    /**
     * return true if there is a battery installed in this unit
     */
    virtual bool isBatteryConnect() override
    {
        // definitely need to gobble up more bytes at once
        if (BatterySerial.available() > 5) {
            // LOG_WARN("SerialBatteryLevel: %u bytes available", BatterySerial.available());
            while (BatterySerial.available() > 11) {
                BatterySerial.read(); // flush old data
            }
            // LOG_WARN("SerialBatteryLevel: %u bytes now available", BatterySerial.available());
            int tries = 0;
            while (BatterySerial.read() != 0xFE) {
                tries++; // wait for start byte
                if (tries > 10) {
                    LOG_WARN("SerialBatteryLevel: no start byte found");
                    return 1;
                }
            }

            Data[1] = BatterySerial.read();
            Data[2] = BatterySerial.read();
            Data[3] = BatterySerial.read();
            Data[4] = BatterySerial.read();
            Data[5] = BatterySerial.read();
            if (Data[5] != 0xFD) {
                LOG_WARN("SerialBatteryLevel: invalid end byte %02x", Data[5]);
                return true;
            }
            v_percent = Data[1];
            voltage = Data[2] + (((float)Data[3]) / 100) + (((float)Data[4]) / 10000);
            voltage *= 2;
            // LOG_WARN("SerialBatteryLevel: received data %u, %f, %02x", v_percent, voltage, Data[5]);
            return true;
        }
        // This function runs first, so use it to grab the latest data from the secondary MCU
        return true;
    }

    /**
     * return true if there is an external power source detected
     */
    virtual bool isVbusIn() override
    {
#if defined(EXT_CHRG_DETECT)

        return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE;

#endif
        return false;
    }

    virtual bool isCharging() override
    {
#ifdef EXT_CHRG_DETECT
        return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE;

#endif
        // by default, we check the battery voltage only
        return isVbusIn();
    }

  private:
    SoftwareSerial BatterySerial = SoftwareSerial(SERIAL_BATTERY_RX, SERIAL_BATTERY_TX);
    uint8_t Data[6] = {0};
    int v_percent = 0;
    float voltage = 0.0;
};

SerialBatteryLevel serialBatteryLevel;

/**
 * Init the serial battery level sensor
 */
bool Power::serialBatteryInit()
{
#ifdef EXT_PWR_DETECT
    pinMode(EXT_PWR_DETECT, EXT_PWR_DETECT_MODE);
#endif
#ifdef EXT_CHRG_DETECT
    pinMode(EXT_CHRG_DETECT, EXT_CHRG_DETECT_MODE);
#endif

    bool result = serialBatteryLevel.runOnce();
    LOG_DEBUG("Power::serialBatteryInit sensor is %s", result ? "ready" : "not ready yet");
    if (!result)
        return false;
    batteryLevel = &serialBatteryLevel;
    return true;
}

#else
/**
 * If this device has no serial battery level sensor, don't try to use it.
 */
bool Power::serialBatteryInit()
{
    return false;
}
#endif
