mirror of
https://github.com/ratspeak/ratdeck.git
synced 2026-10-07 10:17:16 +00:00
229 lines
6.5 KiB
C++
229 lines
6.5 KiB
C++
#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;
|
||
}
|
||
}
|