Add battery charge limit functionality

This commit is contained in:
NGnius (Graham) 2023-03-26 10:49:17 -04:00
parent 32ae1a7eeb
commit f42efab0b0
33 changed files with 799 additions and 63 deletions

2
backend/Cargo.lock generated
View file

@ -1095,7 +1095,7 @@ dependencies = [
[[package]]
name = "powertools"
version = "1.3.0-alpha"
version = "1.3.0-beta1"
dependencies = [
"async-trait",
"libryzenadj",

View file

@ -1,6 +1,6 @@
[package]
name = "powertools"
version = "1.3.0-alpha"
version = "1.3.0-beta1"
edition = "2021"
authors = ["NGnius (Graham) <ngniusness@gmail.com>"]
description = "Backend (superuser) functionality for PowerTools"

View file

@ -26,6 +26,8 @@ pub struct BatteryLimits {
pub charge_current: Option<RangeLimit<u64>>,
pub charge_current_step: u64,
pub charge_modes: Vec<String>,
pub charge_limit: Option<RangeLimit<f64>>,
pub charge_limit_step: f64,
}
#[derive(Serialize, Deserialize)]

View file

@ -193,3 +193,83 @@ pub fn unset_charge_mode(
vec![true.into()]
}
}
/// Generate unplugged event receiver web method
pub fn on_unplugged(
sender: Sender<ApiMessage>,
) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType {
let sender = Mutex::new(sender); // Sender is not Sync; this is required for safety
move |_| {
sender.lock().unwrap().send(ApiMessage::OnUnplugged).expect("on_unplugged send failed");
vec![true.into()]
}
}
/// Generate plugged in event receiver web method
pub fn on_plugged(
sender: Sender<ApiMessage>,
) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType {
let sender = Mutex::new(sender); // Sender is not Sync; this is required for safety
move |_| {
sender.lock().unwrap().send(ApiMessage::OnPluggedIn).expect("on_plugged send failed");
vec![true.into()]
}
}
/// Generate set battery charge limit web method
pub fn set_charge_limit(
sender: Sender<ApiMessage>,
) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType {
let sender = Mutex::new(sender); // Sender is not Sync; this is required for safety
let setter = move |limit: f64|
sender.lock()
.unwrap()
.send(ApiMessage::Battery(BatteryMessage::SetChargeLimit(Some(limit))))
.expect("set_charge_limit send failed");
move |params_in: super::ApiParameterType| {
if let Some(&Primitive::F64(new_val)) = params_in.get(0) {
setter(new_val);
vec![new_val.into()]
} else {
vec!["set_charge_limit missing parameter".into()]
}
}
}
/// Generate unset battery charge limit web method
pub fn unset_charge_limit(
sender: Sender<ApiMessage>,
) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType {
let sender = Mutex::new(sender); // Sender is not Sync; this is required for safety
let unsetter = move ||
sender.lock()
.unwrap()
.send(ApiMessage::Battery(BatteryMessage::SetChargeLimit(None)))
.expect("unset_charge_limit send failed");
move |_: super::ApiParameterType| {
unsetter();
vec![true.into()]
}
}
/// Charge design web method
pub fn get_charge_limit(
sender: Sender<ApiMessage>,
) -> impl AsyncCallable {
let sender = Arc::new(Mutex::new(sender)); // Sender is not Sync; this is required for safety
let getter = move || {
let sender2 = sender.clone();
move || {
let (tx, rx) = mpsc::channel();
let callback = move |val: Option<f64>| tx.send(val).expect("get_charge_limit callback send failed");
sender2.lock().unwrap().send(ApiMessage::Battery(BatteryMessage::GetChargeLimit(Box::new(callback)))).expect("get_charge_limit send failed");
rx.recv().expect("get_charge_limit callback recv failed")
}
};
super::async_utils::AsyncIshGetter {
set_get: getter,
trans_getter: |result| {
super::utility::map_optional_result(Ok(result))
}
}
}

View file

@ -47,14 +47,14 @@ pub fn load_settings(
sender: Sender<ApiMessage>,
) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType {
let sender = Mutex::new(sender); // Sender is not Sync; this is required for safety
let setter = move |path: String, name: String|
let setter = move |path: i64, name: String|
sender.lock()
.unwrap()
.send(ApiMessage::LoadSettings(path, name)).expect("load_settings send failed");
move |params_in: super::ApiParameterType| {
if let Some(Primitive::String(path)) = params_in.get(0) {
if let Some(Primitive::F64(id)) = params_in.get(0) {
if let Some(Primitive::String(name)) = params_in.get(1) {
setter(path.to_owned(), name.to_owned());
setter(*id as i64, name.to_owned());
vec![true.into()]
} else {
vec!["load_settings missing name parameter".into()]

View file

@ -1,7 +1,7 @@
use std::sync::mpsc::{self, Receiver, Sender};
use std::fmt::Write;
use crate::settings::{Settings, TCpus, TGpu, TBattery, TGeneral, OnSet, OnResume, MinMax};
use crate::settings::{Settings, TCpus, TGpu, TBattery, TGeneral, OnSet, OnResume, MinMax, OnPowerEvent, PowerMode};
use crate::persist::SettingsJson;
use crate::utility::unwrap_maybe_fatal;
@ -13,8 +13,15 @@ pub enum ApiMessage {
Gpu(GpuMessage),
General(GeneralMessage),
OnResume,
#[allow(dead_code)]
OnPluggedIn,
#[allow(dead_code)]
OnUnplugged,
#[allow(dead_code)]
OnChargeChange(f64), // battery fill amount: 0 = empty, 1 = full
PowerVibeCheck,
WaitForEmptyQueue(Callback<()>),
LoadSettings(String, String), // (path, name)
LoadSettings(i64, String), // (path, name)
LoadMainSettings,
LoadSystemSettings,
GetLimits(Callback<super::SettingsLimits>),
@ -30,6 +37,8 @@ pub enum BatteryMessage {
ReadChargeNow(Callback<Option<f64>>),
ReadChargeDesign(Callback<Option<f64>>),
ReadCurrentNow(Callback<Option<f64>>),
SetChargeLimit(Option<f64>),
GetChargeLimit(Callback<Option<f64>>),
}
impl BatteryMessage {
@ -44,6 +53,8 @@ impl BatteryMessage {
Self::ReadChargeNow(cb) => cb(settings.read_charge_now()),
Self::ReadChargeDesign(cb) => cb(settings.read_charge_design()),
Self::ReadCurrentNow(cb) => cb(settings.read_current_now()),
Self::SetChargeLimit(limit) => settings.charge_limit(limit),
Self::GetChargeLimit(cb) => cb(settings.get_charge_limit()),
}
dirty
}
@ -287,11 +298,44 @@ impl ApiMessageHandler {
}
false
}
ApiMessage::OnPluggedIn => {
if let Err(e) = settings.on_power_event(PowerMode::PluggedIn) {
print_errors("on_power_event(PluggedIn)", e);
}
true
}
ApiMessage::OnUnplugged => {
if let Err(e) = settings.on_power_event(PowerMode::PluggedOut) {
print_errors("on_power_event(PluggedOut)", e);
}
true
}
ApiMessage::OnChargeChange(charge) => {
if let Err(e) = settings.on_power_event(PowerMode::BatteryCharge(charge)) {
print_errors(&format!("on_power_event(BatteryCharge={:#0.5})", charge), e);
}
true
}
ApiMessage::PowerVibeCheck => {
match settings.battery.check_power() {
Err(e) => print_errors("check_power()", e),
Ok(events) => {
for ev in events {
let name = format!("on_power_event([vibe]{:?})", ev);
if let Err(e) = settings.on_power_event(ev) {
print_errors(&name, e);
}
}
}
}
true
}
ApiMessage::WaitForEmptyQueue(callback) => {
self.on_empty.push(callback);
false
},
ApiMessage::LoadSettings(path, name) => {
ApiMessage::LoadSettings(id, name) => {
let path = format!("{}.json", id);
match settings.load_file(path.into(), name, false) {
Ok(success) => log::info!("Loaded settings file? {}", success),
Err(e) => log::warn!("Load file err: {}", e),

View file

@ -5,6 +5,7 @@ mod state;
mod consts;
use consts::*;
mod power_worker;
mod resume_worker;
//mod save_worker;
mod api_worker;
@ -70,6 +71,7 @@ fn main() -> Result<(), ()> {
//let (_save_handle, save_sender) = save_worker::spawn(loaded_settings.clone());
let _resume_handle = resume_worker::spawn(api_sender.clone());
let _power_handle = power_worker::spawn(api_sender.clone());
let instance = Instance::new(PORT)
.register("V_INFO", |_: Vec<Primitive>| {
@ -111,6 +113,18 @@ fn main() -> Result<(), ()> {
"BATTERY_unset_charge_mode",
api::battery::unset_charge_mode(api_sender.clone()),
)
.register(
"BATTERY_set_charge_limit",
api::battery::set_charge_limit(api_sender.clone()),
)
.register(
"BATTERY_unset_charge_limit",
api::battery::unset_charge_limit(api_sender.clone()),
)
.register_async(
"BATTERY_get_charge_limit",
api::battery::get_charge_limit(api_sender.clone()),
)
// cpu API functions
.register("CPU_count", api::cpu::max_cpus)
.register(
@ -232,10 +246,17 @@ fn main() -> Result<(), ()> {
api::general::get_provider(api_sender.clone())
)
.register("GENERAL_idk", api::general::gunter)
// general API functions
.register(
"GENERAL_apply_now",
api::general::force_apply(api_sender.clone())
)
.register(
"GENERAL_on_pluggedin",
api::battery::on_plugged(api_sender.clone())
)
.register(
"GENERAL_on_unplugged",
api::battery::on_unplugged(api_sender.clone())
);
if let Err(e) = loaded_settings.on_set() {

View file

@ -7,6 +7,15 @@ use serde::{Deserialize, Serialize};
pub struct BatteryJson {
pub charge_rate: Option<u64>,
pub charge_mode: Option<String>,
#[serde(default)]
pub events: Vec<BatteryEventJson>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct BatteryEventJson {
pub trigger: String,
pub charge_rate: Option<u64>,
pub charge_mode: Option<String>,
}
impl Default for BatteryJson {
@ -14,6 +23,7 @@ impl Default for BatteryJson {
Self {
charge_rate: None,
charge_mode: None,
events: Vec::new(),
}
}
}

View file

@ -5,7 +5,7 @@ mod error;
mod general;
mod gpu;
pub use battery::BatteryJson;
pub use battery::{BatteryJson, BatteryEventJson};
pub use cpu::CpuJson;
pub use driver::DriverJson;
pub use general::{MinMaxJson, SettingsJson};

View file

@ -0,0 +1,19 @@
use std::thread::{self, JoinHandle};
use std::time::Duration;
use std::sync::mpsc::Sender;
use crate::api::handler::ApiMessage;
//use crate::utility::unwrap_maybe_fatal;
const PERIOD: Duration = Duration::from_secs(5);
pub fn spawn(sender: Sender<ApiMessage>) -> JoinHandle<()> {
thread::spawn(move || {
log::info!("power_worker starting...");
loop {
sender.send(ApiMessage::PowerVibeCheck).expect("power_worker send failed");
thread::sleep(PERIOD);
}
//log::warn!("resume_worker completed!");
})
}

View file

@ -48,6 +48,8 @@ impl OnResume for General {
}
}
impl crate::settings::OnPowerEvent for General {}
impl TGeneral for General {
fn limits(&self) -> crate::api::GeneralLimits {
crate::api::GeneralLimits { }
@ -92,11 +94,23 @@ pub struct Settings {
impl OnSet for Settings {
fn on_set(&mut self) -> Result<(), Vec<SettingError>> {
self.battery.on_set()?;
self.cpus.on_set()?;
self.gpu.on_set()?;
self.general.on_set()?;
let mut errors = Vec::new();
log::debug!("Applying settings for on_resume");
self.general.on_set().unwrap_or_else(|mut e| errors.append(&mut e));
log::debug!("Resumed general");
self.battery.on_set().unwrap_or_else(|mut e| errors.append(&mut e));
log::debug!("Resumed battery");
self.cpus.on_set().unwrap_or_else(|mut e| errors.append(&mut e));
log::debug!("Resumed CPUs");
self.gpu.on_set().unwrap_or_else(|mut e| errors.append(&mut e));
log::debug!("Resumed GPU");
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
@ -226,14 +240,40 @@ impl Settings {
impl OnResume for Settings {
fn on_resume(&self) -> Result<(), Vec<SettingError>> {
let mut errors = Vec::new();
log::debug!("Applying settings for on_resume");
self.battery.on_resume()?;
self.general.on_resume().unwrap_or_else(|mut e| errors.append(&mut e));
log::debug!("Resumed general");
self.battery.on_resume().unwrap_or_else(|mut e| errors.append(&mut e));
log::debug!("Resumed battery");
self.cpus.on_resume()?;
self.cpus.on_resume().unwrap_or_else(|mut e| errors.append(&mut e));
log::debug!("Resumed CPUs");
self.gpu.on_resume()?;
self.gpu.on_resume().unwrap_or_else(|mut e| errors.append(&mut e));
log::debug!("Resumed GPU");
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
impl crate::settings::OnPowerEvent for Settings {
fn on_power_event(&mut self, new_mode: super::PowerMode) -> Result<(), Vec<SettingError>> {
let mut errors = Vec::new();
self.general.on_power_event(new_mode).unwrap_or_else(|mut e| errors.append(&mut e));
self.battery.on_power_event(new_mode).unwrap_or_else(|mut e| errors.append(&mut e));
self.cpus.on_power_event(new_mode).unwrap_or_else(|mut e| errors.append(&mut e));
self.gpu.on_power_event(new_mode).unwrap_or_else(|mut e| errors.append(&mut e));
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}

View file

@ -18,6 +18,7 @@ impl Into<BatteryJson> for Battery {
BatteryJson {
charge_rate: None,
charge_mode: None,
events: Vec::default(),
}
}
}
@ -65,12 +66,16 @@ impl OnResume for Battery {
}
}
impl crate::settings::OnPowerEvent for Battery {}
impl TBattery for Battery {
fn limits(&self) -> crate::api::BatteryLimits {
crate::api::BatteryLimits {
charge_current: None,
charge_current_step: 50,
charge_modes: vec![],
charge_limit: None,
charge_limit_step: 1.0,
}
}
@ -126,6 +131,10 @@ impl TBattery for Battery {
None
}
fn charge_limit(&mut self, _limit: Option<f64>) {}
fn get_charge_limit(&self) -> Option<f64> { None }
fn provider(&self) -> crate::persist::DriverJson {
crate::persist::DriverJson::Generic
}

View file

@ -138,7 +138,21 @@ impl<C: AsMut<Cpu> + AsRef<Cpu> + TCpu + FromGenericCpuInfo> Cpus<C> {
}
}
impl<C: AsMut<Cpu> + AsRef<Cpu> + TCpu + OnResume + OnSet> TCpus for Cpus<C> {
impl<C: AsMut<Cpu> + AsRef<Cpu> + TCpu + crate::settings::OnPowerEvent> crate::settings::OnPowerEvent for Cpus<C> {
fn on_power_event(&mut self, new_mode: crate::settings::PowerMode) -> Result<(), Vec<SettingError>> {
let mut errors = Vec::new();
for cpu in &mut self.cpus {
cpu.on_power_event(new_mode).unwrap_or_else(|mut e| errors.append(&mut e));
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
impl<C: AsMut<Cpu> + AsRef<Cpu> + TCpu + OnResume + OnSet + crate::settings::OnPowerEvent> TCpus for Cpus<C> {
fn limits(&self) -> crate::api::CpusLimits {
crate::api::CpusLimits {
cpus: self.cpus.iter().map(|x| x.as_ref().limits()).collect(),
@ -333,6 +347,8 @@ impl OnResume for Cpu {
}
}
impl crate::settings::OnPowerEvent for Cpu {}
impl TCpu for Cpu {
fn online(&mut self) -> &mut bool {
&mut self.online

View file

@ -80,6 +80,8 @@ impl OnResume for Gpu {
}
}
impl crate::settings::OnPowerEvent for Gpu {}
impl TGpu for Gpu {
fn limits(&self) -> crate::api::GpuLimits {
crate::api::GpuLimits {

View file

@ -37,6 +37,8 @@ impl OnSet for Cpus {
}
}
impl crate::settings::OnPowerEvent for Cpus {}
impl TCpus for Cpus {
fn limits(&self) -> crate::api::CpusLimits {
self.generic.limits()
@ -110,6 +112,8 @@ impl OnSet for Cpu {
}
}
impl crate::settings::OnPowerEvent for Cpu {}
impl TCpu for Cpu {
fn online(&mut self) -> &mut bool {
self.generic.online()

View file

@ -213,6 +213,8 @@ impl OnSet for Gpu {
}
}
impl crate::settings::OnPowerEvent for Gpu {}
impl TGpu for Gpu {
fn limits(&self) -> crate::api::GpuLimits {
self.generic.limits()

View file

@ -17,7 +17,7 @@ pub use general::{SettingVariant, Settings, General};
pub use min_max::{MinMax, min_max_from_json};
pub use error::SettingError;
pub use traits::{OnResume, OnSet, SettingsRange, TGeneral, TGpu, TCpus, TBattery, TCpu};
pub use traits::{OnResume, OnSet, TGeneral, TGpu, TCpus, TBattery, TCpu, OnPowerEvent, PowerMode};
#[cfg(test)]
mod tests {

View file

@ -1,9 +1,9 @@
use std::convert::Into;
use crate::api::RangeLimit;
use crate::settings::{OnResume, OnSet, SettingError};
use crate::settings::{OnResume, OnSet, SettingError, OnPowerEvent, PowerMode};
use crate::settings::TBattery;
use crate::persist::BatteryJson;
use crate::persist::{BatteryJson, BatteryEventJson};
use super::util::ChargeMode;
use super::oc_limits::{BatteryLimits, OverclockLimits};
@ -11,11 +11,155 @@ use super::oc_limits::{BatteryLimits, OverclockLimits};
pub struct Battery {
pub charge_rate: Option<u64>,
pub charge_mode: Option<ChargeMode>,
events: Vec<EventInstruction>,
limits: BatteryLimits,
state: crate::state::steam_deck::Battery,
driver_mode: crate::persist::DriverJson,
}
#[derive(Debug, Clone)]
enum EventTrigger {
PluggedIn,
PluggedOut,
BatteryAbove(f64),
BatteryBelow(f64),
Ignored,
}
#[derive(Debug, Clone)]
struct EventInstruction {
trigger: EventTrigger,
charge_rate: Option<u64>,
charge_mode: Option<ChargeMode>,
is_triggered: bool,
}
impl OnPowerEvent for EventInstruction {
fn on_power_event(&mut self, new_mode: PowerMode) -> Result<(), Vec<SettingError>> {
match (&self.trigger, new_mode) {
(EventTrigger::PluggedIn, PowerMode::PluggedIn) => {
log::info!("Steam Deck plugged in event handled");
self.set_all()
},
(EventTrigger::PluggedOut, PowerMode::PluggedOut) => {
log::info!("Steam Deck plugged out event handled");
self.set_all()
},
(EventTrigger::BatteryAbove(exp), PowerMode::BatteryCharge(act)) => {
if act > *exp {
if self.is_triggered {
Ok(())
} else {
self.is_triggered = true;
log::info!("Steam Deck battery above {} event handled", exp);
self.set_all()
}
} else {
self.is_triggered = false;
Ok(())
}
},
(EventTrigger::BatteryBelow(exp), PowerMode::BatteryCharge(act)) => {
if act < *exp {
if self.is_triggered {
Ok(())
} else {
self.is_triggered = true;
log::info!("Steam Deck battery below {} event handled", exp);
self.set_all()
}
} else {
self.is_triggered = false;
Ok(())
}
},
_ => Ok(())
}
}
}
impl EventInstruction {
#[inline]
fn trigger_to_str(mode: EventTrigger) -> String {
match mode {
EventTrigger::PluggedIn => "plug-in".to_owned(),
EventTrigger::PluggedOut => "plug-out".to_owned(),
EventTrigger::BatteryAbove(x) => format!(">{:#0.2}", x * 100.0),
EventTrigger::BatteryBelow(x) => format!("<{:#0.2}", x * 100.0),
EventTrigger::Ignored => "/shrug".to_owned(),
}
}
#[inline]
fn str_to_trigger(s: &str) -> Option<EventTrigger> {
match s {
"normal" => Some(EventTrigger::PluggedIn),
"idle" => Some(EventTrigger::PluggedOut),
s if s.starts_with('>') => s.trim_start_matches('>').parse::<f64>().ok().map(|x| EventTrigger::BatteryAbove(x)),
s if s.starts_with('<') => s.trim_start_matches('<').parse::<f64>().ok().map(|x| EventTrigger::BatteryBelow(x)),
_ => None,
}
}
fn from_json(other: BatteryEventJson, _version: u64) -> Self {
Self {
trigger: Self::str_to_trigger(&other.trigger).unwrap_or(EventTrigger::Ignored),
charge_rate: other.charge_rate,
charge_mode: other.charge_mode.map(|x| Battery::str_to_charge_mode(&x)).flatten(),
is_triggered: false,
}
}
fn set_charge_mode(&self) -> Result<(), SettingError> {
if let Some(charge_mode) = self.charge_mode {
super::util::set(super::util::Setting::ChargeMode, charge_mode as _).map_err(
|e| SettingError {
msg: format!("Failed to set charge mode: {}", e),
setting: crate::settings::SettingVariant::Battery,
},
).map(|_| ())
} else {
Ok(())
}
}
fn set_charge_rate(&self) -> Result<(), SettingError> {
if let Some(charge_rate) = self.charge_rate {
usdpl_back::api::files::write_single(BATTERY_CHARGE_RATE_PATH, charge_rate).map_err(
|e| SettingError {
msg: format!("Failed to write to `{}`: {}", BATTERY_CHARGE_RATE_PATH, e),
setting: crate::settings::SettingVariant::Battery,
},
).map(|_| ())
} else {
Ok(())
}
}
fn set_all(&self) -> Result<(), Vec<SettingError>> {
let mut errors = Vec::new();
self.set_charge_rate().unwrap_or_else(|e| errors.push(e));
self.set_charge_mode().unwrap_or_else(|e| errors.push(e));
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
impl Into<BatteryEventJson> for EventInstruction {
fn into(self) -> BatteryEventJson {
BatteryEventJson {
trigger: Self::trigger_to_str(self.trigger),
charge_rate: self.charge_rate,
charge_mode: self.charge_mode.map(|c| Battery::charge_mode_to_str(c)),
}
}
}
const BATTERY_VOLTAGE: f64 = 7.7;
const BATTERY_CHARGE_RATE_PATH: &str = "/sys/class/hwmon/hwmon5/maximum_battery_charge_rate"; // write-only
@ -23,6 +167,7 @@ const BATTERY_CURRENT_NOW_PATH: &str = "/sys/class/power_supply/BAT1/current_now
const BATTERY_CHARGE_NOW_PATH: &str = "/sys/class/power_supply/BAT1/charge_now"; // read-only
const BATTERY_CHARGE_FULL_PATH: &str = "/sys/class/power_supply/BAT1/charge_full"; // read-only
const BATTERY_CHARGE_DESIGN_PATH: &str = "/sys/class/power_supply/BAT1/charge_full_design"; // read-only
const USB_PD_IN_MVOLTAGE_PATH: &str = "/sys/class/hwmon/hwmon5/in0_input"; // read-only
impl Battery {
#[inline]
@ -34,6 +179,7 @@ impl Battery {
0 => Self {
charge_rate: other.charge_rate,
charge_mode: other.charge_mode.map(|x| Self::str_to_charge_mode(&x)).flatten(),
events: other.events.into_iter().map(|x| EventInstruction::from_json(x, version)).collect(),
limits: oc_limits,
state: crate::state::steam_deck::Battery::default(),
driver_mode: driver,
@ -41,6 +187,7 @@ impl Battery {
_ => Self {
charge_rate: other.charge_rate,
charge_mode: other.charge_mode.map(|x| Self::str_to_charge_mode(&x)).flatten(),
events: other.events.into_iter().map(|x| EventInstruction::from_json(x, version)).collect(),
limits: oc_limits,
state: crate::state::steam_deck::Battery::default(),
driver_mode: driver,
@ -62,11 +209,33 @@ impl Battery {
match s {
"normal" => Some(ChargeMode::Normal),
"idle" => Some(ChargeMode::Idle),
"discharge" | "disacharge" => Some(ChargeMode::Discharge),
"discharge" => Some(ChargeMode::Discharge),
_ => None,
}
}
fn set_charge_mode(&mut self) -> Result<(), SettingError> {
if let Some(charge_mode) = self.charge_mode {
self.state.charge_mode_set = true;
super::util::set(super::util::Setting::ChargeMode, charge_mode as _).map_err(
|e| SettingError {
msg: format!("Failed to set charge mode: {}", e),
setting: crate::settings::SettingVariant::Battery,
},
).map(|_| ())
} else if self.state.charge_mode_set {
self.state.charge_mode_set = false;
super::util::set(super::util::Setting::ChargeMode, ChargeMode::Normal as _).map_err(
|e| SettingError {
msg: format!("Failed to set charge mode: {}", e),
setting: crate::settings::SettingVariant::Battery,
},
).map(|_| ())
} else {
Ok(())
}
}
fn set_all(&mut self) -> Result<(), Vec<SettingError>> {
let mut errors = Vec::new();
if let Some(charge_rate) = self.charge_rate {
@ -86,23 +255,7 @@ impl Battery {
},
).unwrap_or_else(|e| errors.push(e));
}
if let Some(charge_mode) = self.charge_mode {
self.state.charge_mode_set = true;
super::util::set(super::util::Setting::ChargeMode, charge_mode as _).map_err(
|e| SettingError {
msg: format!("Failed to set charge mode: {}", e),
setting: crate::settings::SettingVariant::Battery,
},
).unwrap_or_else(|e| {errors.push(e); 0});
} else if self.state.charge_mode_set {
self.state.charge_mode_set = false;
super::util::set(super::util::Setting::ChargeMode, ChargeMode::Normal as _).map_err(
|e| SettingError {
msg: format!("Failed to set charge mode: {}", e),
setting: crate::settings::SettingVariant::Battery,
},
).unwrap_or_else(|e| {errors.push(e); 0});
}
self.set_charge_mode().unwrap_or_else(|e| errors.push(e));
if errors.is_empty() {
Ok(())
} else {
@ -161,6 +314,17 @@ impl Battery {
}
}
pub fn read_usb_voltage() -> Result<f64, SettingError> {
match usdpl_back::api::files::read_single::<_, u64, _>(USB_PD_IN_MVOLTAGE_PATH) {
Err(e) => Err(SettingError {
msg: format!("Failed to read from `{}`: {}", USB_PD_IN_MVOLTAGE_PATH, e),
setting: crate::settings::SettingVariant::Battery,
}),
// convert to V (from mV)
Ok(val) => Ok((val as f64)/1000.0),
}
}
pub fn system_default() -> Self {
let (oc_limits, is_default) = OverclockLimits::load_or_default();
let oc_limits = oc_limits.battery;
@ -168,11 +332,40 @@ impl Battery {
Self {
charge_rate: None,
charge_mode: None,
events: Vec::new(),
limits: oc_limits,
state: crate::state::steam_deck::Battery::default(),
driver_mode: driver,
}
}
fn find_limit_event(&self) -> Option<usize> {
for (i, event) in self.events.iter().enumerate() {
match event.trigger {
EventTrigger::BatteryAbove(_) => {
if event.charge_mode.is_some() {
return Some(i);
}
},
_ => {},
}
}
None
}
fn find_unlimit_event(&self) -> Option<usize> {
for (i, event) in self.events.iter().enumerate() {
match event.trigger {
EventTrigger::BatteryBelow(_) => {
if event.charge_mode.is_some() {
return Some(i);
}
},
_ => {},
}
}
None
}
}
impl Into<BatteryJson> for Battery {
@ -181,6 +374,7 @@ impl Into<BatteryJson> for Battery {
BatteryJson {
charge_rate: self.charge_rate,
charge_mode: self.charge_mode.map(Self::charge_mode_to_str),
events: self.events.into_iter().map(|x| x.into()).collect()
}
}
}
@ -198,6 +392,35 @@ impl OnResume for Battery {
}
}
impl OnPowerEvent for Battery {
fn on_power_event(&mut self, new_mode: PowerMode) -> Result<(), Vec<SettingError>> {
let mut errors = Vec::new();
match new_mode {
PowerMode::PluggedIn => {
// plug event resets battery settings
self.events.iter_mut().for_each(|ev| ev.is_triggered = false);
self.set_charge_mode()
.map_err(|e| vec![e])
},
PowerMode::PluggedOut => {
// plug event resets battery settings
self.events.iter_mut().for_each(|ev| ev.is_triggered = false);
self.set_charge_mode()
.map_err(|e| vec![e])
},
PowerMode::BatteryCharge(_) => Ok(())
}.unwrap_or_else(|mut e| errors.append(&mut e));
for ev in &mut self.events {
ev.on_power_event(new_mode).unwrap_or_else(|mut e| errors.append(&mut e));
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
impl TBattery for Battery {
fn limits(&self) -> crate::api::BatteryLimits {
crate::api::BatteryLimits {
@ -207,6 +430,11 @@ impl TBattery for Battery {
}),
charge_current_step: 50,
charge_modes: vec!["normal".to_owned(), "discharge".to_owned(), "idle".to_owned()],
charge_limit: Some(RangeLimit {
min: 10.0,
max: 90.0,
}),
charge_limit_step: 1.0,
}
}
@ -270,6 +498,107 @@ impl TBattery for Battery {
}
}
fn charge_limit(&mut self, limit: Option<f64>) {
// upper limit
let index = self.find_limit_event();
if let Some(index) = index {
if let Some(limit) = limit {
log::info!("Updating Steam Deck charge limit event instruction to >{}", limit);
self.events[index] = EventInstruction {
trigger: EventTrigger::BatteryAbove(limit/100.0),
charge_rate: None,
charge_mode: Some(ChargeMode::Idle),
is_triggered: false,
};
} else {
self.events.remove(index);
}
} else if let Some(limit) = limit {
log::info!("Creating Steam Deck charge limit event instruction of >{}", limit);
self.events.push(
EventInstruction {
trigger: EventTrigger::BatteryAbove(limit/100.0),
charge_rate: None,
charge_mode: Some(ChargeMode::Idle),
is_triggered: false,
}
);
}
// lower limit
let index = self.find_unlimit_event();
if let Some(index) = index {
if let Some(limit) = limit {
let limit = (limit - 10.0).clamp(0.0, 100.0);
log::info!("Updating Steam Deck charge limit event instruction to <{}", limit);
self.events[index] = EventInstruction {
trigger: EventTrigger::BatteryBelow(limit/100.0),
charge_rate: None,
charge_mode: Some(ChargeMode::Normal),
is_triggered: false,
};
} else {
self.events.remove(index);
}
} else if let Some(limit) = limit {
let limit = (limit - 10.0).clamp(0.0, 100.0);
log::info!("Creating Steam Deck charge limit event instruction of <{}", limit);
self.events.push(
EventInstruction {
trigger: EventTrigger::BatteryBelow(limit/100.0),
charge_rate: None,
charge_mode: Some(ChargeMode::Normal),
is_triggered: false,
}
);
}
}
fn get_charge_limit(&self) -> Option<f64> {
let index = self.find_limit_event();
if let Some(index) = index {
if let EventTrigger::BatteryAbove(limit) = self.events[index].trigger {
Some(limit * 100.0)
} else {
log::error!("Got index {} for battery charge limit which does not have expected event trigger: {:?}", index, &self.events);
None
}
} else {
None
}
}
fn check_power(&mut self) -> Result<Vec<PowerMode>, Vec<SettingError>> {
log::debug!("Steam Deck power vibe check");
let mut errors = Vec::new();
let mut events = Vec::new();
match (Self::read_charge_full(), Self::read_charge_now()) {
(Ok(full), Ok(now)) => events.push(PowerMode::BatteryCharge(now/full)),
(Err(e1), Err(e2)) => {
errors.push(e1);
errors.push(e2);
},
(Err(e), _) => errors.push(e),
(_, Err(e)) => errors.push(e),
}
match Self::read_usb_voltage() {
Ok(voltage) => {
if voltage > 0.0 && self.state.charger_state != crate::state::steam_deck::ChargeState::PluggedIn {
events.push(PowerMode::PluggedIn);
self.state.charger_state = crate::state::steam_deck::ChargeState::PluggedIn;
} else if voltage == 0.0 && self.state.charger_state != crate::state::steam_deck::ChargeState::Unplugged {
events.push(PowerMode::PluggedOut);
self.state.charger_state = crate::state::steam_deck::ChargeState::Unplugged;
}
},
Err(e) => errors.push(e),
}
if errors.is_empty() {
Ok(events)
} else {
Err(errors)
}
}
fn provider(&self) -> crate::persist::DriverJson {
self.driver_mode.clone()
}

View file

@ -158,6 +158,8 @@ impl Cpus {
}
}
impl crate::settings::OnPowerEvent for Cpus {}
impl TCpus for Cpus {
fn limits(&self) -> crate::api::CpusLimits {
crate::api::CpusLimits {

View file

@ -249,6 +249,8 @@ impl OnResume for Gpu {
}
}
impl crate::settings::OnPowerEvent for Gpu {}
impl TGpu for Gpu {
fn limits(&self) -> crate::api::GpuLimits {
crate::api::GpuLimits {

View file

@ -10,12 +10,37 @@ pub trait OnResume {
fn on_resume(&self) -> Result<(), Vec<SettingError>>;
}
pub trait SettingsRange {
fn max() -> Self;
fn min() -> Self;
#[repr(u8)]
#[derive(Clone, Copy, Debug)]
pub enum PowerMode {
PluggedIn,
PluggedOut, // unplugged
BatteryCharge(f64), // battery fill amount: 0 = empty, 1 = full
}
pub trait TGpu: OnResume + OnSet + Debug + Send {
pub trait OnPowerEvent {
fn on_plugged_in(&mut self) -> Result<(), Vec<SettingError>> {
Ok(())
}
fn on_plugged_out(&mut self) -> Result<(), Vec<SettingError>> {
Ok(())
}
fn on_charge_amount(&mut self, _amount: f64) -> Result<(), Vec<SettingError>> {
Ok(())
}
fn on_power_event(&mut self, new_mode: PowerMode) -> Result<(), Vec<SettingError>> {
match new_mode {
PowerMode::PluggedIn => self.on_plugged_in(),
PowerMode::PluggedOut => self.on_plugged_out(),
PowerMode::BatteryCharge(now) => self.on_charge_amount(now),
}
}
}
pub trait TGpu: OnSet + OnResume + OnPowerEvent + Debug + Send {
fn limits(&self) -> crate::api::GpuLimits;
fn json(&self) -> crate::persist::GpuJson;
@ -35,7 +60,7 @@ pub trait TGpu: OnResume + OnSet + Debug + Send {
}
}
pub trait TCpus: OnResume + OnSet + Debug + Send {
pub trait TCpus: OnSet + OnResume + OnPowerEvent + Debug + Send {
fn limits(&self) -> crate::api::CpusLimits;
fn json(&self) -> Vec<crate::persist::CpuJson>;
@ -63,7 +88,7 @@ pub trait TCpu: Debug + Send {
fn get_clock_limits(&self) -> Option<&MinMax<u64>>;
}
pub trait TGeneral: OnResume + OnSet + Debug + Send {
pub trait TGeneral: OnSet + OnResume + OnPowerEvent + Debug + Send {
fn limits(&self) -> crate::api::GeneralLimits;
fn get_persistent(&self) -> bool;
@ -81,7 +106,7 @@ pub trait TGeneral: OnResume + OnSet + Debug + Send {
fn provider(&self) -> crate::persist::DriverJson;
}
pub trait TBattery: OnResume + OnSet + Debug + Send {
pub trait TBattery: OnSet + OnResume + OnPowerEvent + Debug + Send {
fn limits(&self) -> crate::api::BatteryLimits;
fn json(&self) -> crate::persist::BatteryJson;
@ -102,6 +127,19 @@ pub trait TBattery: OnResume + OnSet + Debug + Send {
fn read_current_now(&self) -> Option<f64>;
fn charge_limit(&mut self, limit: Option<f64>);
fn get_charge_limit(&self) -> Option<f64>;
fn check_power(&mut self) -> Result<Vec<PowerMode>, Vec<SettingError>> {
log::warn!("Power event check using default trait implementation");
let mut events = Vec::new();
if let (Some(full), Some(now)) = (self.read_charge_full(), self.read_charge_now()) {
events.push(PowerMode::BatteryCharge(now/full));
}
Ok(events)
}
fn provider(&self) -> crate::persist::DriverJson {
crate::persist::DriverJson::AutoDetect
}

View file

@ -13,6 +13,7 @@ impl Into<BatteryJson> for Battery {
BatteryJson {
charge_rate: None,
charge_mode: None,
events: Vec::default(),
}
}
}
@ -29,12 +30,16 @@ impl OnResume for Battery {
}
}
impl crate::settings::OnPowerEvent for Battery {}
impl TBattery for Battery {
fn limits(&self) -> crate::api::BatteryLimits {
crate::api::BatteryLimits {
charge_current: None,
charge_current_step: 50,
charge_modes: vec![],
charge_limit: None,
charge_limit_step: 1.0,
}
}
@ -64,6 +69,10 @@ impl TBattery for Battery {
fn read_current_now(&self) -> Option<f64> { None }
fn charge_limit(&mut self, _limit: Option<f64>) {}
fn get_charge_limit(&self) -> Option<f64> { None }
fn provider(&self) -> crate::persist::DriverJson {
crate::persist::DriverJson::Unknown
}

View file

@ -68,6 +68,8 @@ impl OnResume for Cpus {
}
}
impl crate::settings::OnPowerEvent for Cpus {}
impl Cpus {
pub fn cpu_count() -> Option<usize> {
let mut data: String = usdpl_back::api::files::read_single(CPU_PRESENT_PATH)

View file

@ -49,6 +49,8 @@ impl OnResume for Gpu {
}
}
impl crate::settings::OnPowerEvent for Gpu {}
impl TGpu for Gpu {
fn limits(&self) -> crate::api::GpuLimits {
crate::api::GpuLimits {

View file

@ -2,13 +2,22 @@
pub struct Battery {
pub charge_rate_set: bool,
pub charge_mode_set: bool,
pub charger_state: ChargeState,
}
impl std::default::Default for Battery {
fn default() -> Self {
Self {
charge_rate_set: false,
charge_mode_set: false,
charge_rate_set: true,
charge_mode_set: true,
charger_state: ChargeState::Unknown,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChargeState {
PluggedIn,
Unplugged,
Unknown,
}

View file

@ -2,6 +2,6 @@ mod battery;
mod cpu;
mod gpu;
pub use battery::Battery;
pub use battery::{Battery, ChargeState};
pub use cpu::Cpu;
pub use gpu::Gpu;

View file

@ -1,6 +1,6 @@
{
"name": "PowerTools",
"version": "1.3.0-alpha",
"version": "1.3.0-beta1",
"description": "Power tweaks for power users",
"scripts": {
"build": "shx rm -rf dist && rollup -c",

View file

@ -63,6 +63,8 @@ export type BatteryLimits = {
charge_current: RangeLimit | null;
charge_current_step: number;
charge_modes: string[];
charge_limit: RangeLimit | null;
charge_limit_step: number;
};
export type CpuLimits = {
@ -139,6 +141,18 @@ export async function unsetBatteryChargeMode(): Promise<any[]> {
return await call_backend("BATTERY_unset_charge_mode", []);
}
export async function getBatteryChargeLimit(): Promise<number | null> {
return (await call_backend("BATTERY_get_charge_limit", []))[0];
}
export async function setBatteryChargeLimit(val: number): Promise<number> {
return (await call_backend("BATTERY_set_charge_limit", [val]))[0];
}
export async function unsetBatteryChargeLimit(): Promise<any[]> {
return await call_backend("BATTERY_unset_charge_limit", []);
}
// CPU
export async function setCpuSmt(status: boolean): Promise<boolean[]> {
@ -229,8 +243,8 @@ export async function getGeneralPersistent(): Promise<boolean> {
return (await call_backend("GENERAL_get_persistent", []))[0];
}
export async function loadGeneralSettings(path: string, name: string): Promise<boolean> {
return (await call_backend("GENERAL_load_settings", [path, name]))[0];
export async function loadGeneralSettings(id: number, name: string): Promise<boolean> {
return (await call_backend("GENERAL_load_settings", [id, name]))[0];
}
export async function loadGeneralDefaultSettings(): Promise<boolean> {
@ -280,3 +294,11 @@ export async function idk(): Promise<boolean> {
export async function forceApplySettings(): Promise<boolean> {
return (await call_backend("GENERAL_apply_now", []))[0];
}
export async function onPluggedIn(): Promise<boolean> {
return (await call_backend("GENERAL_on_pluggedin", []))[0];
}
export async function onUnplugged(): Promise<boolean> {
return (await call_backend("GENERAL_on_unplugged", []))[0];
}

View file

@ -19,6 +19,7 @@ import {
CHARGE_RATE_BATT,
CHARGE_MODE_BATT,
CURRENT_BATT,
CHARGE_LIMIT_BATT,
} from "../consts";
import { set_value, get_value} from "usdpl-front";
@ -53,6 +54,12 @@ export class Battery extends Component<backend.IdcProps> {
{get_value(CHARGE_FULL_BATT).toFixed(1)} Wh ({(100 * get_value(CHARGE_FULL_BATT) / get_value(CHARGE_DESIGN_BATT)).toFixed(1)}%)
</Field>
</PanelSectionRow>}
<PanelSectionRow>
<Field
label={tr("Current")}>
{get_value(CURRENT_BATT)} mA
</Field>
</PanelSectionRow>
{(get_value(LIMITS_INFO) as backend.SettingsLimits).battery.charge_current != null && <PanelSectionRow>
<ToggleField
checked={get_value(CHARGE_RATE_BATT) != null}
@ -60,7 +67,7 @@ export class Battery extends Component<backend.IdcProps> {
description={tr("Control battery charge rate when awake")}
onChange={(value: boolean) => {
if (value) {
set_value(CHARGE_RATE_BATT, 2500);
set_value(CHARGE_RATE_BATT, (get_value(LIMITS_INFO) as backend.SettingsLimits).battery.charge_current!.max);
reloadGUI("BATTChargeRateToggle");
} else {
set_value(CHARGE_RATE_BATT, null);
@ -128,12 +135,44 @@ export class Battery extends Component<backend.IdcProps> {
/>
</Field>}
</PanelSectionRow>}
<PanelSectionRow>
<Field
label={tr("Current")}>
{get_value(CURRENT_BATT)} mA
</Field>
</PanelSectionRow>
{(get_value(LIMITS_INFO) as backend.SettingsLimits).battery.charge_limit != null && <PanelSectionRow>
<ToggleField
checked={get_value(CHARGE_LIMIT_BATT) != null}
label={tr("Charge Limit")}
description={tr("Limit battery charge when awake")}
onChange={(value: boolean) => {
if (value) {
set_value(CHARGE_LIMIT_BATT, (get_value(LIMITS_INFO) as backend.SettingsLimits).battery.charge_limit!.max);
reloadGUI("BATTChargeLimitToggle");
} else {
set_value(CHARGE_LIMIT_BATT, null);
backend.resolve(backend.unsetBatteryChargeLimit(), (_: any[]) => {
reloadGUI("BATTUnsetChargeRate");
});
}
}}
/>
{ get_value(CHARGE_LIMIT_BATT) != null && <SliderField
label={tr("Maximum (%)")}
value={get_value(CHARGE_LIMIT_BATT)}
max={(get_value(LIMITS_INFO) as backend.SettingsLimits).battery.charge_limit!.max}
min={(get_value(LIMITS_INFO) as backend.SettingsLimits).battery.charge_limit!.min}
step={(get_value(LIMITS_INFO) as backend.SettingsLimits).battery.charge_limit_step}
showValue={true}
disabled={get_value(CHARGE_LIMIT_BATT) == null}
onChange={(val: number) => {
backend.log(backend.LogLevel.Debug, "Charge limit is now " + val.toString());
const rateNow = get_value(CHARGE_LIMIT_BATT);
if (val != rateNow) {
backend.resolve(backend.setBatteryChargeLimit(val),
(rate: number) => {
set_value(CHARGE_LIMIT_BATT, rate);
reloadGUI("BATTChargeLimit");
});
}
}}
/>}
</PanelSectionRow>}
</Fragment>);
}
}

View file

@ -6,6 +6,7 @@ export const LIMITS_INFO = "LIMITS_all";
export const CURRENT_BATT = "BATTERY_current_now";
export const CHARGE_RATE_BATT = "BATTERY_charge_rate";
export const CHARGE_MODE_BATT = "BATTERY_charge_mode";
export const CHARGE_LIMIT_BATT = "BATTERY_charge_limit";
export const CHARGE_NOW_BATT = "BATTERY_charge_now";
export const CHARGE_FULL_BATT = "BATTERY_charge_full";
export const CHARGE_DESIGN_BATT = "BATTERY_charge_design";

View file

@ -35,6 +35,7 @@ import {
CURRENT_BATT,
CHARGE_RATE_BATT,
CHARGE_MODE_BATT,
CHARGE_LIMIT_BATT,
CHARGE_NOW_BATT,
CHARGE_FULL_BATT,
CHARGE_DESIGN_BATT,
@ -108,6 +109,7 @@ const reload = function() {
backend.resolve(backend.getBatteryCurrent(), (rate: number) => { set_value(CURRENT_BATT, rate) });
backend.resolve_nullable(backend.getBatteryChargeRate(), (rate: number | null) => { set_value(CHARGE_RATE_BATT, rate) });
backend.resolve_nullable(backend.getBatteryChargeMode(), (mode: string | null) => { set_value(CHARGE_MODE_BATT, mode) });
backend.resolve_nullable(backend.getBatteryChargeLimit(), (limit: number | null) => { set_value(CHARGE_LIMIT_BATT, limit) });
backend.resolve(backend.getBatteryChargeNow(), (rate: number) => { set_value(CHARGE_NOW_BATT, rate) });
backend.resolve(backend.getBatteryChargeFull(), (rate: number) => { set_value(CHARGE_FULL_BATT, rate) });
backend.resolve(backend.getBatteryChargeDesign(), (rate: number) => { set_value(CHARGE_DESIGN_BATT, rate) });
@ -175,7 +177,7 @@ const reload = function() {
let gameInfo: any = appStore.GetAppOverviewByGameID(id);
// don't use gameInfo.appid, haha
backend.resolve(
backend.loadGeneralSettings(id.toString() + ".json", gameInfo.display_name),
backend.loadGeneralSettings(id, gameInfo.display_name),
(ok: boolean) => {backend.log(backend.LogLevel.Debug, "Loading settings ok? " + ok)}
);
});

View file

@ -83,7 +83,7 @@ msgstr "Maximum (mA)"
# (Battery charge mode override toggle)
#: components/battery.tsx:97,115
msgid "Charge Mode"
msgstr "Mode de Charge"
msgstr "Mode de charge"
# (Battery charge mode override toggle description)
#: components/battery.tsx:98
@ -100,6 +100,21 @@ msgstr "Mode"
msgid "Current"
msgstr "Courant"
#: components/battery.tsx:141
# (Battery charging maximum)
msgid "Charge Limit"
msgstr "Limite de charge"
#: components/battery.tsx:142
# (Battery charging maximum description)
msgid "Limit battery charge when awake"
msgstr "Limiter la charge de la batterie quand éveillé"
#: components/battery.tsx:156
# (Battery charging maximum slider)
msgid "Maximum (%)"
msgstr "Maximum (%)"
# -- components/cpus.tsx --
# (CPU section title)
#: components/cpus.tsx:64

View file

@ -99,6 +99,21 @@ msgstr ""
msgid "Current"
msgstr ""
#: components/battery.tsx:141
# (Battery charging maximum)
msgid "Charge Limit"
msgstr ""
#: components/battery.tsx:142
# (Battery charging maximum description)
msgid "Limit battery charge when awake"
msgstr ""
#: components/battery.tsx:156
# (Battery charging maximum slider)
msgid "Maximum (%)"
msgstr ""
# -- components/cpus.tsx --
#: components/cpus.tsx:64