Files
ratdeck/src/hal/Power.cpp
T

229 lines
6.5 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "Power.h"
#include "hal/Display.h"
#include "hal/Keyboard.h"
#include "storage/SDStore.h"
// Forward declarations — display & keyboard instances provided externally
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 VoltPoint { float v; int pct; };
static constexpr VoltPoint LIPO_CURVE[] = {
{3.90f, 100},
{3.80f, 90},
{3.72f, 80},
{3.65f, 70},
{3.59f, 60},
{3.53f, 50},
{3.48f, 40},
{3.44f, 30},
{3.40f, 20},
{3.36f, 15},
{3.30f, 10},
{3.15f, 5},
{3.00f, 0},
};
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);
digitalWrite(BOARD_POWER_PIN, HIGH);
delay(10); // Allow peripherals to stabilize
}
void Power::begin() {
_lastActivity = millis();
_state = ACTIVE;
// Configure battery ADC
pinMode(BAT_ADC_PIN, INPUT);
analogReadResolution(12);
Serial.println("[POWER] Power manager initialized");
}
float Power::batteryVoltage() const {
// T-Deck Plus: voltage divider on GPIO 4
int raw = analogRead(BAT_ADC_PIN);
// Voltage divider: 2x ratio, 3.3V reference, 12-bit ADC
return (raw / 4095.0f) * 3.3f * 2.0f;
}
int Power::batteryPercent() const {
float v = batteryVoltage();
// Charging: voltage exceeds the discharge curve's top (3.9V = full).
// A discharging cell never gets there, so treat this as full.
if (isCharging())
return 100;
// 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
v += (3.9f - _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() >= _chargeThreshold;
}
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;
if (pct >= 100) return 255;
// Map 1-100 to ~6-255 (minimum visible PWM ~6)
return (uint8_t)(6 + (uint16_t)(pct - 1) * 249 / 99);
}
void Power::activity() {
_lastActivity = millis();
if (_state == SCREEN_OFF) {
_justWokeFromOff = true;
}
if (_state != ACTIVE) {
setState(ACTIVE);
}
}
void Power::forceScreenOff() {
if (_justWokeFromOff) {
_justWokeFromOff = false;
return;
}
setState(SCREEN_OFF);
}
void Power::weakActivity() {
_lastActivity = millis();
// Trackball wakes from DIM but not from SCREEN_OFF
if (_state == DIMMED) {
setState(ACTIVE);
}
}
void Power::setBrightness(uint8_t percent) {
_brightnessPct = constrain(percent, 1, 100);
if (_state == ACTIVE) {
display.setBrightness(percentToPWM(_brightnessPct));
}
}
void Power::setKbBrightness(uint8_t percent, bool apply) {
percent = constrain(percent, 0, 100);
keyboard.setBacklightBrightness(percent);
if (percent == 0) {
keyboard.backlightOff();
} else if (apply) { // Show the new brightness
keyboard.backlightOn();
}
}
void Power::loop() {
unsigned long elapsed = millis() - _lastActivity;
switch (_state) {
case ACTIVE:
if (_offTimeout > 0 && elapsed >= _offTimeout) {
setState(SCREEN_OFF);
} else if (_dimTimeout > 0 && elapsed >= _dimTimeout) {
setState(DIMMED);
}
break;
case DIMMED:
if (_offTimeout > 0 && elapsed >= _offTimeout) {
setState(SCREEN_OFF);
}
break;
case SCREEN_OFF:
break;
}
_justWokeFromOff = false;
}
void Power::setState(State newState) {
if (newState == _state) return;
const char* names[] = {"ACTIVE", "DIMMED", "SCREEN_OFF"};
Serial.printf("[POWER] %s -> %s\n", names[_state], names[newState]);
State oldState = _state;
_state = newState;
switch (_state) {
case ACTIVE:
if (oldState == SCREEN_OFF) {
// Pre-load correct brightness into LovyanGFX state before wakeup.
// wakeup() sends SLPOUT then restores LGFX's internal _brightness
// to the LEDC — with this ordering, it restores the correct value
// instead of a stale 0, eliminating the rapid 0→0→correct triple-
// write that can cause missed LEDC duty updates on ESP32-S3.
display.setBrightness(percentToPWM(_brightnessPct));
display.wakeup();
} else {
display.setBrightness(percentToPWM(_brightnessPct));
}
// On wake, relight only what screen-off forced dark (or per auto-on) —
// never force-enable for users who keep the kb light off.
if (_kbAutoOn || (oldState == SCREEN_OFF && _kbLitBeforeOff)) {
keyboard.backlightOn();
}
break;
case DIMMED:
display.setBrightness(DIM_PWM);
if (_kbAutoOff) {
keyboard.backlightOff();
}
break;
case SCREEN_OFF:
// LovyanGFX sleep() sets brightness to 0 internally — no
// need to call setBrightness(0) beforehand.
display.sleep();
// Kb backlight always follows screen-off — the timeout exists to save battery.
_kbLitBeforeOff = keyboard.backlightIsLit();
keyboard.backlightOff();
break;
}
}