diff --git a/src/config/UserConfig.cpp b/src/config/UserConfig.cpp index 586719a..09601a8 100644 --- a/src/config/UserConfig.cpp +++ b/src/config/UserConfig.cpp @@ -107,7 +107,10 @@ bool UserConfig::parseJson(const String& json) { _settings.bleEnabled = false; // battery settings - _settings.batteryDisplay = doc["batt_display"] | 0; + _settings.batteryDisplay = doc["batt_display"] | 0; + _settings.batteryModel = doc["batt_model"] | 0; + _settings.chargeThresholdV = doc["charge_thresh_v"] | 4.1f; + _settings.fullBatteryV = doc["full_battery_v"] | 3.8f; _settings.gpsTimeEnabled = doc["gps_time"] | true; _settings.gpsLocationEnabled = doc["gps_location"] | false; @@ -186,7 +189,9 @@ String UserConfig::serializeToJson() { // battery settings doc["batt_display"] = _settings.batteryDisplay; - + doc["batt_model"] = _settings.batteryModel; + doc["charge_thresh_v"] = _settings.chargeThresholdV; + doc["full_battery_v"] = _settings.fullBatteryV; doc["gps_time"] = _settings.gpsTimeEnabled; doc["gps_location"] = _settings.gpsLocationEnabled; diff --git a/src/config/UserConfig.h b/src/config/UserConfig.h index 881d272..ec5a2d8 100644 --- a/src/config/UserConfig.h +++ b/src/config/UserConfig.h @@ -60,6 +60,9 @@ struct UserSettings { // Battery uint8_t batteryDisplay = 0; // 0 = percent, 1 = bar + uint8_t batteryModel = 0; // 0=lipo, 1=linear + float chargeThresholdV = 4.1f; // Battery voltage threshold for indication "is charging" + float fullBatteryV = 3.8f; // Battery voltage if 100% charged // Keyboard uint8_t keyboardBrightness = 100; // Percentage 0-100 (0 = off) diff --git a/src/hal/Power.cpp b/src/hal/Power.cpp index f546de2..188d724 100644 --- a/src/hal/Power.cpp +++ b/src/hal/Power.cpp @@ -6,6 +6,40 @@ extern Display display; extern Keyboard keyboard; +// Battery type lookup table for getting voltage vs percent +// interpolated chart from https://www.researchgate.net/figure/Li-ion-battery-discharge-voltage-curve_fig5_363575973 +// the manual chart reading entries are read from chart, while the others are interpolated +namespace { +struct VoltPct { float v; int pct; }; +static const VoltPct LIPO_CURVE[] = { + {4.20f, 100}, // manual chart reading + {4.00f, 95}, + {3.80f, 90}, // manual chart reading + {3.78f, 85}, + {3.75f, 80}, // manual chart reading + {3.74f, 75}, + {3.73f, 70}, + {3.71f, 65}, + {3.70f, 60}, // manual chart reading + {3.69f, 55}, + {3.68f, 50}, + {3.66f, 45}, + {3.65f, 40}, // manual chart reading + {3.64f, 35}, + {3.63f, 30}, + {3.61f, 25}, + {3.60f, 20}, // manual chart reading + {3.55f, 15}, + {3.50f, 10}, // manual chart reading + {3.25f, 5}, // manual chart reading + {3.00f, 0}, // manual chart reading +}; +constexpr int LIPO_CURVE_N = sizeof(LIPO_CURVE) / sizeof(LIPO_CURVE[0]); +} + + + + void Power::enablePeripherals() { // CRITICAL: GPIO 10 must be HIGH to enable all T-Deck Plus peripherals pinMode(BOARD_POWER_PIN, OUTPUT); @@ -33,16 +67,46 @@ float Power::batteryVoltage() const { int Power::batteryPercent() const { float v = batteryVoltage(); - // LiPo voltage curve approximation - if (v >= 4.2f) return 100; - if (v <= 3.0f) return 0; - return (int)((v - 3.0f) / 1.2f * 100.0f); + + // Compensate for load-induced voltage drop when running on battery. + // Calculates an offset and adds it to real voltage for to be able using default LiPo lookup table + if (!isCharging()) + v += (4.2f - _fullBatteryV); + + // Clamp to valid curve range. + v = constrain(v, 3.0f, 4.2f); + + if (_batteryModel == 1) { + // Linear: distribution across 3.0–4.2V. + return (int)((v - 3.0f) / 1.2f * 100.0f); + } + + // LiPo: interpolate between nearest table entries. + for (int i = 0; i < LIPO_CURVE_N - 1; i++) { + if (v >= LIPO_CURVE[i + 1].v) { + float t = (v - LIPO_CURVE[i + 1].v) / (LIPO_CURVE[i].v - LIPO_CURVE[i + 1].v); + return (int)(LIPO_CURVE[i + 1].pct + t * (LIPO_CURVE[i].pct - LIPO_CURVE[i + 1].pct)); + } + } + return 0; +} + +void Power::setBatteryModel(uint8_t model) { + _batteryModel = model; } bool Power::isCharging() const { return batteryVoltage() >= 4.0f; } +void Power::setChargeThreshold(float v) { + _chargeThreshold = v; +} + +void Power::setFullBatteryVoltage(float v) { + _fullBatteryV = v; +} + uint8_t Power::percentToPWM(uint8_t pct) const { if (pct == 0) return 0; diff --git a/src/hal/Power.h b/src/hal/Power.h index 8c2e2a6..1bf326e 100644 --- a/src/hal/Power.h +++ b/src/hal/Power.h @@ -23,7 +23,10 @@ public: float batteryVoltage() const; int batteryPercent() const; bool isCharging() const; - + void setBatteryModel(uint8_t model); // 0=lipo, 1=linear + void setChargeThreshold(float v); + void setFullBatteryVoltage(float v); + // Display backlight — accepts percentage 1-100 void setBrightness(uint8_t percent); void setDimTimeout(uint16_t seconds) { @@ -63,4 +66,9 @@ private: bool _kbAutoOff = false; bool _kbLitBeforeOff = false; bool _justWokeFromOff = false; + + // Battery + uint8_t _batteryModel = 0; + float _chargeThreshold = 4.1f; + float _fullBatteryV = 3.8f; }; diff --git a/src/ui/screens/LvSettingsScreen.cpp b/src/ui/screens/LvSettingsScreen.cpp index c316c94..f546f45 100644 --- a/src/ui/screens/LvSettingsScreen.cpp +++ b/src/ui/screens/LvSettingsScreen.cpp @@ -583,37 +583,60 @@ void LvSettingsScreen::buildItems() { }}); // Battery -int battStart = idx; -_items.push_back({"Battery Display", SettingType::ENUM_CHOICE, - [&s]() { return (int)s.batteryDisplay; }, - [&s](int v) { s.batteryDisplay = (uint8_t)v; }, - nullptr, 0, 1, 1, {"Percent", "Bar"}}); -idx++; -_items.push_back({"Voltage", SettingType::READONLY, nullptr, nullptr, - [this](int) -> String { - if (!_power) return String("--"); - char buf[12]; - snprintf(buf, sizeof(buf), "%.2fV", _power->batteryVoltage()); - return String(buf); - }}); -idx++; -_items.push_back({"Charge", SettingType::READONLY, nullptr, nullptr, - [this](int) -> String { - if (!_power) return String("--"); - return String(_power->batteryPercent()) + "%"; - }}); -idx++; -_categories.push_back({"Battery", battStart, idx - battStart, - [this, &s]() -> String { - String summary = s.batteryDisplay == 0 ? String("Percent") : String("Bar"); - if (_power) { - summary += " / "; - char buf[8]; + int battStart = idx; + _items.push_back({"Battery Display", SettingType::ENUM_CHOICE, + [&s]() { return (int)s.batteryDisplay; }, + [&s](int v) { s.batteryDisplay = (uint8_t)v; }, + nullptr, 0, 1, 1, {"Percent", "Bar"}}); + idx++; + + _items.push_back({"Discharge Curve", SettingType::ENUM_CHOICE, + [&s]() { return (int)s.batteryModel; }, + [&s](int v) { s.batteryModel = (uint8_t)v; }, + nullptr, 0, 1, 1, {"LiPo / Li-Ion", "Linear"}}); + idx++; + + _items.push_back({"Charge Above", SettingType::INTEGER, + [&s]() { return (int)roundf(s.chargeThresholdV * 100); }, + [&s](int v) { s.chargeThresholdV = v / 100.0f; }, + [](int v) -> String { + char buf[8]; snprintf(buf, sizeof(buf), "%.2fV", v / 100.0f); return String(buf); + }, 380, 430, 1}); + idx++; + _items.push_back({"Full Battery", SettingType::INTEGER, + [&s]() { return (int)roundf(s.fullBatteryV * 100); }, + [&s](int v) { s.fullBatteryV = v / 100.0f; }, + [](int v) -> String { + char buf[8]; snprintf(buf, sizeof(buf), "%.2fV", v / 100.0f); return String(buf); + }, 350, 420, 1}); + idx++; + + + _items.push_back({"Voltage", SettingType::ACTION, nullptr, nullptr, + [this](int) -> String { + if (!_power) return String("--"); + char buf[12]; snprintf(buf, sizeof(buf), "%.2fV", _power->batteryVoltage()); - summary += buf; - } - return summary; - }}); + return String(buf); + }}); + idx++; + _items.push_back({"Charge", SettingType::ACTION, nullptr, nullptr, + [this](int) -> String { + if (!_power) return String("--"); + return String(_power->batteryPercent()) + "%"; + }}); + idx++; + _categories.push_back({"Battery", battStart, idx - battStart, + [this, &s]() -> String { + String summary = s.batteryDisplay == 0 ? String("Percent") : String("Bar"); + if (_power) { + summary += " / "; + char buf[8]; + snprintf(buf, sizeof(buf), "%.2fV", _power->batteryVoltage()); + summary += buf; + } + return summary; + }}); // LoRa link int radioStart = idx; @@ -2135,6 +2158,12 @@ void LvSettingsScreen::applyAndSave() { if (_scrollContainer) lv_obj_set_style_bg_color(_scrollContainer, lv_color_hex(Theme::BG), 0); } if (_power) { + + // Battery + _power->setBatteryModel(s.batteryModel); + _power->setChargeThreshold(s.chargeThresholdV); + _power->setFullBatteryVoltage(s.fullBatteryV); + _power->setBrightness(s.brightness); _power->setDimTimeout(s.screenDimTimeout); _power->setOffTimeout(s.screenOffTimeout);