#include "Power.h" #include #include #include #include #include #include static const auto* TAG = "ES3C28P_Power"; // HW: BAT+ -> R14 200K + R15 200K -> GND => BAT_ADC middle, 2x divider, to GPIO9 ADC1_CH8 // Charger TP4054 CHRG pin only drives LED, not connected to ESP32 GPIO. // So we infer charging by voltage behavior: rising trend and high voltage. class Es3c28pPower final : public tt::hal::power::PowerDevice { public: Es3c28pPower() { adc_oneshot_unit_init_cfg_t init_cfg = { .unit_id = ADC_UNIT_1, .clk_src = ADC_RTC_CLK_SRC_DEFAULT, .ulp_mode = ADC_ULP_MODE_DISABLE, }; if (adc_oneshot_new_unit(&init_cfg, &adc_handle) != ESP_OK) { ESP_LOGE(TAG, "ADC unit init failed"); return; } adc_oneshot_chan_cfg_t chan_cfg = { .atten = ADC_ATTEN_DB_12, .bitwidth = ADC_BITWIDTH_12, }; if (adc_oneshot_config_channel(adc_handle, ADC_CHANNEL_8, &chan_cfg) != ESP_OK) { ESP_LOGE(TAG, "ADC channel config failed"); adc_oneshot_del_unit(adc_handle); adc_handle = nullptr; return; } adc_cali_curve_fitting_config_t cali_cfg = { .unit_id = ADC_UNIT_1, .atten = ADC_ATTEN_DB_12, .bitwidth = ADC_BITWIDTH_12, }; if (adc_cali_create_scheme_curve_fitting(&cali_cfg, &cali_handle) != ESP_OK) { ESP_LOGW(TAG, "ADC cali scheme failed"); cali_handle = nullptr; } ESP_LOGI(TAG, "Battery ADC GPIO9 ADC1_CH8, 2x divider, cali %s", cali_handle ? "OK" : "none"); } ~Es3c28pPower() override { if (cali_handle) adc_cali_delete_scheme_curve_fitting(cali_handle); if (adc_handle) adc_oneshot_del_unit(adc_handle); } std::string getName() const override { return "ES3C28P Power"; } std::string getDescription() const override { return "Battery GPIO9 ADC1_CH8 + TP4054 inferred charging"; } bool supportsMetric(MetricType type) const override { switch (type) { case MetricType::BatteryVoltage: case MetricType::ChargeLevel: case MetricType::IsCharging: return adc_handle != nullptr; default: return false; } } bool getMetric(MetricType type, MetricData& data) override { if (!adc_handle) return false; int raw = 0; int mv = 0; if (adc_oneshot_read(adc_handle, ADC_CHANNEL_8, &raw) != ESP_OK) return false; if (cali_handle) { if (adc_cali_raw_to_voltage(cali_handle, raw, &mv) != ESP_OK) return false; } else { mv = (raw * 3300) / 4095; } int vbat_mv = mv * 2; // 200K/200K divider // Track voltage trend for charging detection const int now_ms = xTaskGetTickCount() * portTICK_PERIOD_MS; if (last_read_ms == 0) { last_vbat_mv = vbat_mv; last_read_ms = now_ms; } // Update trend every 2 seconds to avoid noise if (now_ms - last_read_ms > 2000) { // If voltage increased by at least 10mV in 2s, likely charging if (vbat_mv > last_vbat_mv + 10) { rising_count++; falling_count = 0; } else if (vbat_mv < last_vbat_mv - 10) { falling_count++; rising_count = 0; if (falling_count > 3) is_charging_trend = false; } last_vbat_mv = vbat_mv; last_read_ms = now_ms; } switch (type) { case MetricType::BatteryVoltage: data.valueAsUint32 = (uint32_t)vbat_mv; return true; case MetricType::ChargeLevel: { uint8_t pct; if (vbat_mv <= 2500) pct = 0; else if (vbat_mv >= 4200) pct = 100; else pct = (uint8_t)((vbat_mv - 2500) / 17); data.valueAsUint8 = pct; return true; } case MetricType::IsCharging: { // Cases: // 1. No battery / wall powered: vbat <500mV or >5000mV (VBUS via MOSFET) => charging true if (vbat_mv < 500 || vbat_mv > 5000) { data.valueAsBool = true; is_charging_trend = true; return true; } // 2. High voltage near full with charger attached: >=4150mV => charging (charger holds at 4.2V) if (vbat_mv >= 4150) { data.valueAsBool = true; is_charging_trend = true; return true; } // 3. Rising trend indicates charging if (rising_count >= 2) { is_charging_trend = true; } // 4. If previously charging and voltage still >=4000, keep charging until drop if (is_charging_trend && vbat_mv >= 4000) { data.valueAsBool = true; return true; } data.valueAsBool = is_charging_trend; return true; } default: return false; } } private: adc_oneshot_unit_handle_t adc_handle = nullptr; adc_cali_handle_t cali_handle = nullptr; int last_vbat_mv = 0; int last_read_ms = 0; int rising_count = 0; int falling_count = 0; bool is_charging_trend = false; }; std::shared_ptr createPower() { return std::make_shared(); }