Skip to content

Commit

Permalink
feat: user defined battery curve (#110)
Browse files Browse the repository at this point in the history
* chore: battery curve

* feat: user defined battery curve
  • Loading branch information
fengyc authored Apr 16, 2024
1 parent dd0a73d commit 23eb147
Show file tree
Hide file tree
Showing 8 changed files with 301 additions and 232 deletions.
5 changes: 4 additions & 1 deletion doc/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,7 @@ Json format configuration file of pisugar-server:
default null
soft_poweroff_shell Shell script of soft poweroff, default null

auto_rtc_sync Automatically sync rtc time (Every 10s)
auto_rtc_sync Automatically sync rtc time (Every 10s)

battery_curve Customized battery curve, optional, e.g.:
[[3.2, 5], [3.3, 20], [3.5, 60], [3.7, 80], [3.8, 90], [4.0, 100]]
201 changes: 201 additions & 0 deletions pisugar-core/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
use std::{
fs::{File, OpenOptions},
io::{self, Read, Write},
path::Path,
};

use chrono::{DateTime, Local};
use serde::{Deserialize, Serialize};

/// Battery voltage threshold, (low, percentage at low)
pub type BatteryThreshold = (f32, f32);

fn default_i2c_bus() -> u8 {
1
}

/// Default auth session timeout, 1h
fn default_session_timeout() -> u32 {
60 * 60
}

/// PiSugar configuration
#[derive(Clone, Serialize, Deserialize)]
pub struct PiSugarConfig {
/// Http digest auth
#[serde(default)]
pub auth_user: Option<String>,

#[serde(default)]
pub auth_password: Option<String>,

/// Auth session timeout in seconds
#[serde(default = "default_session_timeout")]
pub session_timeout: u32,

/// I2C bus, default 1 (/dev/i2c-1)
#[serde(default = "default_i2c_bus")]
pub i2c_bus: u8,

/// I2C addr, default 0x57 (87), available in PiSugar3
#[serde(default)]
pub i2c_addr: Option<u16>,

/// Alarm time
#[serde(default)]
pub auto_wake_time: Option<DateTime<Local>>,

/// Alarm weekday repeat
#[serde(default)]
pub auto_wake_repeat: u8,

/// Single tap enable
#[serde(default)]
pub single_tap_enable: bool,

/// Single tap shell script
#[serde(default)]
pub single_tap_shell: String,

/// Double tap enable
#[serde(default)]
pub double_tap_enable: bool,

/// Double tap shell script
#[serde(default)]
pub double_tap_shell: String,

/// Long tap enable
#[serde(default)]
pub long_tap_enable: bool,

/// Long tap shell script
#[serde(default)]
pub long_tap_shell: String,

/// Auto shutdown when battery level is low
#[serde(default)]
pub auto_shutdown_level: Option<f64>,

/// Auto shutdown delay, seconds
#[serde(default)]
pub auto_shutdown_delay: Option<f64>,

/// Charging range
#[serde(default)]
pub auto_charging_range: Option<(f32, f32)>,

/// Keep charging duration
#[serde(default)]
pub full_charge_duration: Option<u64>,

/// UPS automatically power on when power recovered
#[serde(default)]
pub auto_power_on: Option<bool>,

/// Soft poweroff, PiSugar 3 only
#[serde(default)]
pub soft_poweroff: Option<bool>,

/// Soft poweroff shell script
#[serde(default)]
pub soft_poweroff_shell: Option<String>,

/// Auto rtc sync
#[serde(default)]
pub auto_rtc_sync: Option<bool>,

/// RTC ppm adjust comm (every second)
#[serde(default)]
pub adj_comm: Option<u8>,

/// RTC ppm adjust diff (in 31s)
#[serde(default)]
pub adj_diff: Option<u8>,

/// RTC adjust ppm
#[serde(default)]
pub rtc_adj_ppm: Option<f64>,

/// Anti mistouch
#[serde(default)]
pub anti_mistouch: Option<bool>,

/// Battery hardware protect
#[serde(default)]
pub bat_protect: Option<bool>,

/// User defined battery curve
#[serde(default)]
pub battery_curve: Option<Vec<BatteryThreshold>>,
}

impl PiSugarConfig {
fn _validate_battery_curve(cfg: &PiSugarConfig) -> bool {
let mut curve = cfg.battery_curve.clone().unwrap_or_default();
curve.sort_by(|x, y| x.0.total_cmp(&y.0));
for i in 1..curve.len() {
if curve[i].0 == curve[i - 1].0 || curve[i].1 <= curve[i - 1].1 {
log::error!("Invalid customized battery curve {:?} {:?}", curve[i - 1], curve[i]);
return false;
}
}
true
}

pub fn load(&mut self, path: &Path) -> io::Result<()> {
let mut f = File::open(path)?;
let mut buff = String::new();
let _ = f.read_to_string(&mut buff)?;
let config = serde_json::from_str(&buff)?;
if !PiSugarConfig::_validate_battery_curve(&config) {
return Err(io::ErrorKind::InvalidData.into());
}
*self = config;
Ok(())
}

pub fn save_to(&self, path: &Path) -> io::Result<()> {
let mut options = OpenOptions::new();
options.write(true).create(true);
let mut f = options.open(path)?;
let s = serde_json::to_string_pretty(self)?;
log::info!("Dump config:\n{}", s);
f.set_len(0)?;
f.write_all(s.as_bytes())
}
}

impl Default for PiSugarConfig {
fn default() -> Self {
Self {
auth_user: Default::default(),
auth_password: Default::default(),
session_timeout: default_session_timeout(),
i2c_bus: default_i2c_bus(),
i2c_addr: Default::default(),
auto_wake_time: Default::default(),
auto_wake_repeat: Default::default(),
single_tap_enable: Default::default(),
single_tap_shell: Default::default(),
double_tap_enable: Default::default(),
double_tap_shell: Default::default(),
long_tap_enable: Default::default(),
long_tap_shell: Default::default(),
auto_shutdown_level: Default::default(),
auto_shutdown_delay: Default::default(),
auto_charging_range: Default::default(),
full_charge_duration: Default::default(),
auto_power_on: Default::default(),
soft_poweroff: Default::default(),
soft_poweroff_shell: Default::default(),
auto_rtc_sync: Default::default(),
adj_comm: Default::default(),
adj_diff: Default::default(),
rtc_adj_ppm: Default::default(),
anti_mistouch: Default::default(),
bat_protect: Default::default(),
battery_curve: Default::default(),
}
}
}
30 changes: 21 additions & 9 deletions pisugar-core/src/ip5209.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ use std::time::Instant;

use rppal::i2c::I2c;

use crate::battery::{Battery, BatteryEvent};
use crate::{convert_battery_voltage_to_level, gpio_detect_tap, BatteryThreshold, Error, Model, PiSugarConfig, Result};
use crate::config::BatteryThreshold;
use crate::{
battery::{Battery, BatteryEvent},
I2C_ADDR_BAT,
};
use crate::{convert_battery_voltage_to_level, gpio_detect_tap, Error, Model, PiSugarConfig, Result};

/// Battery threshold curve
pub const BATTERY_CURVE: [BatteryThreshold; 10] = [
Expand Down Expand Up @@ -54,9 +58,9 @@ impl IP5209 {
}

/// Parse level(%)
pub fn parse_voltage_level(voltage: f32) -> f32 {
pub fn parse_voltage_level(voltage: f32, curve: &[BatteryThreshold]) -> f32 {
if voltage > 0.0 {
convert_battery_voltage_to_level(voltage, &BATTERY_CURVE)
convert_battery_voltage_to_level(voltage, curve)
} else {
100.0
}
Expand Down Expand Up @@ -245,18 +249,20 @@ pub struct IP5209Battery {
levels: VecDeque<f32>,
intensities: VecDeque<(Instant, f32)>,
tap_history: String,
cfg: PiSugarConfig,
}

impl IP5209Battery {
pub fn new(i2c_bus: u8, i2c_addr: u16, model: Model) -> Result<Self> {
let ip5209 = IP5209::new(i2c_bus, i2c_addr)?;
pub fn new(cfg: PiSugarConfig, model: Model) -> Result<Self> {
let ip5209 = IP5209::new(cfg.i2c_bus, cfg.i2c_addr.unwrap_or(I2C_ADDR_BAT))?;
Ok(Self {
ip5209,
model,
voltages: VecDeque::with_capacity(30),
intensities: VecDeque::with_capacity(30),
levels: VecDeque::with_capacity(30),
tap_history: String::with_capacity(30),
cfg,
})
}
}
Expand Down Expand Up @@ -317,7 +323,13 @@ impl Battery for IP5209Battery {
}

fn level(&self) -> Result<f32> {
self.voltage_avg().map(IP5209::parse_voltage_level)
let curve = self
.cfg
.battery_curve
.as_ref()
.map(|x| &x[..])
.unwrap_or(BATTERY_CURVE.as_ref());
self.voltage_avg().map(|x| IP5209::parse_voltage_level(x, curve))
}

fn intensity(&self) -> Result<f32> {
Expand Down Expand Up @@ -397,11 +409,11 @@ impl Battery for IP5209Battery {
}
self.voltages.push_back((now, voltage));

let level = IP5209::parse_voltage_level(voltage);
let level = self.level()?;
if self.levels.len() >= self.levels.capacity() {
self.levels.pop_front();
}
self.levels.push_back(level as f32);
self.levels.push_back(level);

let intensity = self.intensity()?;
if self.intensities.len() >= self.intensities.capacity() {
Expand Down
27 changes: 19 additions & 8 deletions pisugar-core/src/ip5312.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ use std::time::Instant;

use rppal::i2c::I2c;

use crate::battery::{Battery, BatteryEvent};
use crate::Error;
use crate::{
battery::{Battery, BatteryEvent},
config::BatteryThreshold,
};
use crate::{convert_battery_voltage_to_level, I2cError, Model, PiSugarConfig};
use crate::{gpio_detect_tap, Result};
use crate::{BatteryThreshold, Error};

/// Battery threshold curve
pub const BATTERY_CURVE: [BatteryThreshold; 10] = [
Expand Down Expand Up @@ -53,9 +56,9 @@ impl IP5312 {
}

/// Parse level(%)
pub fn parse_voltage_level(voltage: f32) -> f32 {
pub fn parse_voltage_level(voltage: f32, curve: &[BatteryThreshold]) -> f32 {
if voltage > 0.0 {
convert_battery_voltage_to_level(voltage, &BATTERY_CURVE)
convert_battery_voltage_to_level(voltage, curve)
} else {
100.0
}
Expand Down Expand Up @@ -242,18 +245,20 @@ pub struct IP5312Battery {
intensities: VecDeque<(Instant, f32)>,
levels: VecDeque<f32>,
tap_history: String,
cfg: PiSugarConfig,
}

impl IP5312Battery {
pub fn new(i2c_bus: u8, i2c_addr: u16, model: Model) -> Result<Self> {
let ip5312 = IP5312::new(i2c_bus, i2c_addr)?;
pub fn new(cfg: PiSugarConfig, model: Model) -> Result<Self> {
let ip5312 = IP5312::new(cfg.i2c_bus, cfg.i2c_addr.unwrap_or(model.default_battery_i2c_addr()))?;
Ok(Self {
ip5312,
model,
voltages: VecDeque::with_capacity(30),
intensities: VecDeque::with_capacity(30),
levels: VecDeque::with_capacity(30),
tap_history: String::with_capacity(30),
cfg,
})
}
}
Expand Down Expand Up @@ -315,7 +320,13 @@ impl Battery for IP5312Battery {
}

fn level(&self) -> Result<f32> {
self.voltage_avg().map(IP5312::parse_voltage_level)
let curve = self
.cfg
.battery_curve
.as_ref()
.map(|x| &x[..])
.unwrap_or(BATTERY_CURVE.as_ref());
self.voltage_avg().map(|x| IP5312::parse_voltage_level(x, curve))
}

fn intensity(&self) -> Result<f32> {
Expand Down Expand Up @@ -395,7 +406,7 @@ impl Battery for IP5312Battery {
self.voltages.push_back((now, voltage));
}

let level = IP5312::parse_voltage_level(voltage);
let level = self.level()?;
self.levels.pop_front();
while self.levels.len() < self.levels.capacity() {
self.levels.push_back(level);
Expand Down
Loading

0 comments on commit 23eb147

Please sign in to comment.