diff --git a/daemon/src/battery/mod.rs b/daemon/src/battery/mod.rs new file mode 100644 index 0000000..0f0c570 --- /dev/null +++ b/daemon/src/battery/mod.rs @@ -0,0 +1,63 @@ +use std::path::Path; + +use rayhunter::Device; +use serde::Serialize; + +use crate::error::RayhunterError; + +pub mod orbic; +pub mod tmobile; +pub mod wingtech; + +#[derive(Clone, Copy, PartialEq, Debug, Serialize)] +pub enum BatteryLevel { + VeryLow, + Low, + Medium, + High, + Full, +} + +#[derive(Clone, Copy, PartialEq, Debug, Serialize)] +pub struct BatteryState { + level: BatteryLevel, + is_plugged_in: bool, +} + +async fn is_plugged_in_from_file(path: &Path) -> Result { + match tokio::fs::read_to_string(path) + .await + .map_err(RayhunterError::TokioError)? + .chars() + .next() + { + Some('0') => Ok(false), + Some('1') => Ok(true), + _ => Err(RayhunterError::BatteryPluggedInStatusParseError), + } +} + +async fn get_level_from_percentage_file(path: &Path) -> Result { + match tokio::fs::read_to_string(path) + .await + .map_err(RayhunterError::TokioError)? + .trim_end() + .parse() + { + Ok(0..=10) => Ok(BatteryLevel::VeryLow), + Ok(11..=25) => Ok(BatteryLevel::Low), + Ok(26..=50) => Ok(BatteryLevel::Medium), + Ok(51..=75) => Ok(BatteryLevel::High), + Ok(76..=100) => Ok(BatteryLevel::Full), + _ => Err(RayhunterError::BatteryLevelParseError), + } +} + +pub async fn get_battery_status(device: &Device) -> Result { + Ok(match device { + Device::Orbic => orbic::get_battery_state().await?, + Device::Wingtech => wingtech::get_battery_state().await?, + Device::Tmobile => tmobile::get_battery_state().await?, + _ => return Err(RayhunterError::FunctionNotSupportedForDeviceError), + }) +} diff --git a/daemon/src/battery/orbic.rs b/daemon/src/battery/orbic.rs new file mode 100644 index 0000000..948b46e --- /dev/null +++ b/daemon/src/battery/orbic.rs @@ -0,0 +1,28 @@ +use std::path::Path; + +use crate::{ + battery::{BatteryLevel, BatteryState, is_plugged_in_from_file}, + error::RayhunterError, +}; + +const BATTERY_LEVEL_FILE: &str = "/sys/kernel/chg_info/level"; +const PLUGGED_IN_STATE_FILE: &str = "/sys/kernel/chg_info/chg_en"; + +pub async fn get_battery_state() -> Result { + Ok(BatteryState { + level: match tokio::fs::read_to_string(&BATTERY_LEVEL_FILE) + .await + .map_err(RayhunterError::TokioError)? + .chars() + .next() + { + Some('1') => Ok(BatteryLevel::VeryLow), + Some('2') => Ok(BatteryLevel::Low), + Some('3') => Ok(BatteryLevel::Medium), + Some('4') => Ok(BatteryLevel::High), + Some('5') => Ok(BatteryLevel::Full), + _ => Err(RayhunterError::BatteryLevelParseError), + }?, + is_plugged_in: is_plugged_in_from_file(Path::new(PLUGGED_IN_STATE_FILE)).await?, + }) +} diff --git a/daemon/src/battery/tmobile.rs b/daemon/src/battery/tmobile.rs new file mode 100644 index 0000000..ccc93ae --- /dev/null +++ b/daemon/src/battery/tmobile.rs @@ -0,0 +1,16 @@ +use std::path::Path; + +use crate::{ + battery::{BatteryState, get_level_from_percentage_file, is_plugged_in_from_file}, + error::RayhunterError, +}; + +const BATTERY_LEVEL_FILE: &str = "/sys/class/power_supply/bms/capacity"; +const PLUGGED_IN_STATE_FILE: &str = "/sys/devices/78d9000.usb/power_supply/usb/online"; + +pub async fn get_battery_state() -> Result { + Ok(BatteryState { + level: get_level_from_percentage_file(Path::new(BATTERY_LEVEL_FILE)).await?, + is_plugged_in: is_plugged_in_from_file(Path::new(PLUGGED_IN_STATE_FILE)).await?, + }) +} diff --git a/daemon/src/battery/wingtech.rs b/daemon/src/battery/wingtech.rs new file mode 100644 index 0000000..9c82a58 --- /dev/null +++ b/daemon/src/battery/wingtech.rs @@ -0,0 +1,17 @@ +use std::path::Path; + +use crate::{ + battery::{BatteryState, get_level_from_percentage_file, is_plugged_in_from_file}, + error::RayhunterError, +}; + +const BATTERY_LEVEL_FILE: &str = + "/sys/devices/78b7000.i2c/i2c-3/3-0063/power_supply/cw2017-bat/capacity"; +const PLUGGED_IN_STATE_FILE: &str = "/sys/devices/8a00000.ssusb/power_supply/usb/online"; + +pub async fn get_battery_state() -> Result { + Ok(BatteryState { + level: get_level_from_percentage_file(Path::new(BATTERY_LEVEL_FILE)).await?, + is_plugged_in: is_plugged_in_from_file(Path::new(PLUGGED_IN_STATE_FILE)).await?, + }) +} diff --git a/daemon/src/error.rs b/daemon/src/error.rs index 9016a20..0c9adb6 100644 --- a/daemon/src/error.rs +++ b/daemon/src/error.rs @@ -15,4 +15,10 @@ pub enum RayhunterError { QmdlStoreError(#[from] RecordingStoreError), #[error("No QMDL store found at path {0}, but can't create a new one due to debug mode")] NoStoreDebugMode(String), + #[error("Error parsing file to determine battery level")] + BatteryLevelParseError, + #[error("Error parsing file to determine whether device is plugged in")] + BatteryPluggedInStatusParseError, + #[error("The requested functionality is not supported for this device")] + FunctionNotSupportedForDeviceError, } diff --git a/daemon/src/main.rs b/daemon/src/main.rs index 7019812..3a9e8ed 100644 --- a/daemon/src/main.rs +++ b/daemon/src/main.rs @@ -1,4 +1,5 @@ mod analysis; +mod battery; mod config; mod diag; mod display; diff --git a/daemon/src/stats.rs b/daemon/src/stats.rs index 0b5e90d..f77e3e1 100644 --- a/daemon/src/stats.rs +++ b/daemon/src/stats.rs @@ -1,7 +1,9 @@ use std::sync::Arc; -use crate::qmdl_store::ManifestEntry; +use crate::battery::get_battery_status; +use crate::error::RayhunterError; use crate::server::ServerState; +use crate::{battery::BatteryState, qmdl_store::ManifestEntry}; use axum::Json; use axum::extract::State; @@ -16,6 +18,8 @@ pub struct SystemStats { pub disk_stats: DiskStats, pub memory_stats: MemoryStats, pub runtime_metadata: RuntimeMetadata, + #[serde(skip_serializing_if = "Option::is_none")] + pub battery_status: Option, } impl SystemStats { @@ -24,6 +28,11 @@ impl SystemStats { disk_stats: DiskStats::new(qmdl_path, device).await?, memory_stats: MemoryStats::new(device).await?, runtime_metadata: RuntimeMetadata::new(), + battery_status: match get_battery_status(device).await { + Ok(status) => Some(status), + Err(RayhunterError::FunctionNotSupportedForDeviceError) => None, + Err(err) => return Err(err.to_string()), + }, }) } } diff --git a/daemon/web/src/lib/components/SystemStatsTable.svelte b/daemon/web/src/lib/components/SystemStatsTable.svelte index 2542d09..c99004b 100644 --- a/daemon/web/src/lib/components/SystemStatsTable.svelte +++ b/daemon/web/src/lib/components/SystemStatsTable.svelte @@ -1,5 +1,5 @@
+ + Battery + + + {title_text} + + + + + + + {#if stats.battery_status && stats.battery_status.is_plugged_in} + + + {/if} + {#if !stats.battery_status} + + ? + {/if} + + +
diff --git a/daemon/web/src/lib/systemStats.ts b/daemon/web/src/lib/systemStats.ts index 081517b..bfdd333 100644 --- a/daemon/web/src/lib/systemStats.ts +++ b/daemon/web/src/lib/systemStats.ts @@ -2,6 +2,7 @@ export interface SystemStats { disk_stats: DiskStats; memory_stats: MemoryStats; runtime_metadata: RuntimeMetadata; + battery_status?: BatteryStatus; } export interface RuntimeMetadata { @@ -24,3 +25,16 @@ export interface MemoryStats { used: string; free: string; } + +export interface BatteryStatus { + level: BatteryLevel; + is_plugged_in: boolean; +} + +export enum BatteryLevel { + VeryLow = 'VeryLow', + Low = 'Low', + Medium = 'Medium', + High = 'High', + Full = 'Full', +}