From b0826149ec12f0c8d458d71eb2d6fbf09c059f2f Mon Sep 17 00:00:00 2001 From: Felipe Balbi Date: Tue, 14 Apr 2026 13:28:32 -0700 Subject: [PATCH 1/2] Completely remove acpi feature --- ec/test-cli/Cargo.toml | 5 ----- ec/test-cli/src/cli.rs | 37 ++++++++++++++++++++++++---------- ec/test-cli/src/main.rs | 44 ++++++++++++++++++----------------------- ec/test-lib/Cargo.toml | 7 +++---- ec/test-lib/src/lib.rs | 16 +-------------- ec/test-tui/Cargo.toml | 3 --- ec/test-tui/src/main.rs | 2 -- 7 files changed, 50 insertions(+), 64 deletions(-) diff --git a/ec/test-cli/Cargo.toml b/ec/test-cli/Cargo.toml index 5c963d6..b880b29 100644 --- a/ec/test-cli/Cargo.toml +++ b/ec/test-cli/Cargo.toml @@ -14,10 +14,5 @@ ec-test-lib = { path = "../test-lib" } time-alarm-service-messages.workspace = true battery-service-messages.workspace = true -[features] -mock = [] -acpi = ["ec-test-lib/acpi"] -serial = [] - [lints] workspace = true diff --git a/ec/test-cli/src/cli.rs b/ec/test-cli/src/cli.rs index 78be73d..5d33eba 100644 --- a/ec/test-cli/src/cli.rs +++ b/ec/test-cli/src/cli.rs @@ -3,27 +3,44 @@ use clap::{Parser, Subcommand, ValueEnum}; #[derive(Parser)] #[command(name = "ec-test-cli", about = "CLI tool for EC feature testing")] pub struct Cli { - #[cfg(feature = "serial")] - #[arg(long)] - pub port: String, + /// Data source to use. + #[arg(long, value_enum)] + pub source: SourceKind, - #[cfg(feature = "serial")] - #[arg(long, value_enum, default_value = "none")] + /// Serial port path (required when --source serial). + #[arg(long, required_if_eq("source", "serial"))] + pub port: Option, + + /// Serial flow-control mode. + #[arg(long, value_enum, default_value_t = FlowControl::None)] pub flow_control: FlowControl, - #[cfg(feature = "serial")] - #[arg(long, default_value = "115200")] + /// Serial baud rate. + #[arg(long, default_value_t = 115_200)] pub baud: u32, #[command(subcommand)] pub command: Command, } -#[cfg(feature = "serial")] -#[derive(Clone, PartialEq, ValueEnum)] +/// Available data sources. +#[derive(Clone, Copy, ValueEnum)] +pub enum SourceKind { + /// Deterministic in-process mock — no hardware required. + Mock, + /// Real hardware via serial transport. + Serial, + /// Real hardware via ACPI (Windows only). + Acpi, +} + +#[derive(Clone, Copy, Default, ValueEnum)] pub enum FlowControl { - Hw, + #[default] + #[value(name = "none")] None, + #[value(name = "hw")] + Hw, } #[derive(Subcommand)] diff --git a/ec/test-cli/src/main.rs b/ec/test-cli/src/main.rs index 0969ce7..23bfdfc 100644 --- a/ec/test-cli/src/main.rs +++ b/ec/test-cli/src/main.rs @@ -1,38 +1,32 @@ -const _: () = { - let count = cfg!(feature = "mock") as u8 + cfg!(feature = "acpi") as u8 + cfg!(feature = "serial") as u8; - assert!( - count == 1, - "Exactly one of the following features must be enabled: `mock`, `acpi`, or `serial`." - ); -}; - mod cli; mod commands; mod debug; use clap::Parser; -use cli::{Cli, Command}; +use cli::{Cli, Command, SourceKind}; +use ec_test_lib::Source; + +fn dispatch(source: S, command: Command) -> Result<(), Box> { + match command { + Command::Thermal(cmd) => commands::thermal::run(source, cmd).map_err(Into::into), + Command::Battery(cmd) => commands::battery::run(source, cmd).map_err(Into::into), + Command::Rtc(cmd) => commands::rtc::run(source, cmd).map_err(Into::into), + } +} fn main() -> Result<(), Box> { let cli = Cli::parse(); - #[cfg(feature = "mock")] - let source = ec_test_lib::mock::Mock::default(); + match cli.source { + SourceKind::Mock => dispatch(ec_test_lib::mock::Mock::default(), cli.command), - #[cfg(feature = "acpi")] - let source = ec_test_lib::acpi::Acpi::default(); + SourceKind::Serial => { + let port = cli.port.expect("--port is required for --source serial"); + let hw_flow = matches!(cli.flow_control, cli::FlowControl::Hw); + let source = ec_test_lib::serial::Serial::new(&port, cli.baud, hw_flow)?; + dispatch(source, cli.command) + } - #[cfg(feature = "serial")] - let source = { - let flow_control = cli.flow_control == cli::FlowControl::Hw; - ec_test_lib::serial::Serial::new(&cli.port, cli.baud, flow_control)? - }; - - match cli.command { - Command::Thermal(cmd) => commands::thermal::run(source, cmd)?, - Command::Battery(cmd) => commands::battery::run(source, cmd)?, - Command::Rtc(cmd) => commands::rtc::run(source, cmd)?, + SourceKind::Acpi => dispatch(ec_test_lib::acpi::Acpi::default(), cli.command), } - - Ok(()) } diff --git a/ec/test-lib/Cargo.toml b/ec/test-lib/Cargo.toml index 89caab0..7e7f0ef 100644 --- a/ec/test-lib/Cargo.toml +++ b/ec/test-lib/Cargo.toml @@ -15,7 +15,7 @@ battery-service-messages.workspace = true embedded-mcu-hal = { workspace = true } # ACPI feature specific -num_enum = { version = "0.7.5", default-features = false, optional = true } +num_enum = { version = "0.7.5", default-features = false } windows = { version = "0.58", features = [ "Win32_Devices_DeviceAndDriverInstallation", "Win32_Devices_Properties", @@ -24,8 +24,8 @@ windows = { version = "0.58", features = [ "Win32_Foundation", "Win32_System_SystemServices", "Win32_Security", -], optional = true } -scopeguard = { version = "1.2", optional = true } +] } +scopeguard = { version = "1.2" } # Serial feature specific serialport = { version = "4.8.1" } @@ -33,7 +33,6 @@ embedded-services = { git = "https://github.com/OpenDevicePartnership/embedded-s thermal-service-messages = { git = "https://github.com/OpenDevicePartnership/embedded-services", branch = "v0.2.0" } [features] -acpi = ["dep:num_enum", "dep:windows", "dep:scopeguard"] [lints] workspace = true diff --git a/ec/test-lib/src/lib.rs b/ec/test-lib/src/lib.rs index 8356cd0..fcc2ca9 100644 --- a/ec/test-lib/src/lib.rs +++ b/ec/test-lib/src/lib.rs @@ -1,14 +1,5 @@ // Multiple source features may be enabled simultaneously; the binary selects one at runtime. -#[cfg(all( - feature = "acpi", - not(all(target_arch = "aarch64", target_os = "windows", target_env = "msvc")) -))] -compile_error!( - "The `acpi` feature requires targeting `aarch64-pc-windows-msvc`.\n\ - If on WSL, try: cargo build-win --release --features acpi" -); - use battery_service_messages::{BixFixedStrings, BstReturn}; use time_alarm_service_messages::{ AcpiTimerId, AcpiTimestamp, AlarmExpiredWakePolicy, AlarmTimerSeconds, TimeAlarmDeviceCapabilities, TimerStatus, @@ -16,12 +7,7 @@ use time_alarm_service_messages::{ pub(crate) mod common; -#[cfg(all( - feature = "acpi", - target_arch = "aarch64", - target_os = "windows", - target_env = "msvc" -))] +#[cfg(target_os = "windows")] pub mod acpi; pub mod mock; diff --git a/ec/test-tui/Cargo.toml b/ec/test-tui/Cargo.toml index 06306ad..74b1153 100644 --- a/ec/test-tui/Cargo.toml +++ b/ec/test-tui/Cargo.toml @@ -27,8 +27,5 @@ critical-section.workspace = true # EC transport library ec-test-lib = { path = "../test-lib" } -[features] -acpi = ["ec-test-lib/acpi"] - [lints] workspace = true diff --git a/ec/test-tui/src/main.rs b/ec/test-tui/src/main.rs index c41f6ce..68cc4f1 100644 --- a/ec/test-tui/src/main.rs +++ b/ec/test-tui/src/main.rs @@ -44,7 +44,6 @@ enum SourceKind { /// Real hardware via serial transport. Serial, /// Real hardware via ACPI (aarch64-pc-windows-msvc only). - #[cfg(feature = "acpi")] Acpi, } @@ -77,7 +76,6 @@ fn main() -> color_eyre::Result<()> { app::App::new(source, period).run(terminal) } - #[cfg(feature = "acpi")] SourceKind::Acpi => { let period = Duration::from_secs(cli.sample_period.unwrap_or(60)); app::App::new(ec_test_lib::acpi::Acpi::default(), period).run(terminal) From 7c904547463a6c635f6df50664beb7dd9513156e Mon Sep 17 00:00:00 2001 From: Felipe Balbi Date: Tue, 14 Apr 2026 14:54:31 -0700 Subject: [PATCH 2/2] Add linux support --- ec/Cargo.lock | 1 + ec/test-cli/src/main.rs | 2 +- ec/test-lib/Cargo.toml | 15 +- ec/test-lib/src/lib.rs | 3 +- ec/test-lib/src/os/linux.rs | 458 +++++++++++++++++++++ ec/test-lib/src/os/mod.rs | 5 + ec/test-lib/src/{acpi.rs => os/windows.rs} | 36 +- ec/test-tui/src/main.rs | 2 +- 8 files changed, 494 insertions(+), 28 deletions(-) create mode 100644 ec/test-lib/src/os/linux.rs create mode 100644 ec/test-lib/src/os/mod.rs rename ec/test-lib/src/{acpi.rs => os/windows.rs} (95%) diff --git a/ec/Cargo.lock b/ec/Cargo.lock index ddb91f1..f96b3a9 100644 --- a/ec/Cargo.lock +++ b/ec/Cargo.lock @@ -445,6 +445,7 @@ dependencies = [ "battery-service-messages", "embedded-mcu-hal", "embedded-services", + "libc", "num_enum", "scopeguard", "serialport", diff --git a/ec/test-cli/src/main.rs b/ec/test-cli/src/main.rs index 23bfdfc..adeee67 100644 --- a/ec/test-cli/src/main.rs +++ b/ec/test-cli/src/main.rs @@ -27,6 +27,6 @@ fn main() -> Result<(), Box> { dispatch(source, cli.command) } - SourceKind::Acpi => dispatch(ec_test_lib::acpi::Acpi::default(), cli.command), + SourceKind::Acpi => dispatch(ec_test_lib::os::OsSource::default(), cli.command), } } diff --git a/ec/test-lib/Cargo.toml b/ec/test-lib/Cargo.toml index 7e7f0ef..18468da 100644 --- a/ec/test-lib/Cargo.toml +++ b/ec/test-lib/Cargo.toml @@ -14,8 +14,14 @@ battery-service-messages.workspace = true embedded-mcu-hal = { workspace = true } -# ACPI feature specific +# Serial transport specific +serialport = { version = "4.8.1" } +embedded-services = { git = "https://github.com/OpenDevicePartnership/embedded-services", branch = "v0.2.0" } +thermal-service-messages = { git = "https://github.com/OpenDevicePartnership/embedded-services", branch = "v0.2.0" } + +[target.'cfg(target_os = "windows")'.dependencies] num_enum = { version = "0.7.5", default-features = false } +scopeguard = { version = "1.2" } windows = { version = "0.58", features = [ "Win32_Devices_DeviceAndDriverInstallation", "Win32_Devices_Properties", @@ -25,12 +31,9 @@ windows = { version = "0.58", features = [ "Win32_System_SystemServices", "Win32_Security", ] } -scopeguard = { version = "1.2" } -# Serial feature specific -serialport = { version = "4.8.1" } -embedded-services = { git = "https://github.com/OpenDevicePartnership/embedded-services", branch = "v0.2.0" } -thermal-service-messages = { git = "https://github.com/OpenDevicePartnership/embedded-services", branch = "v0.2.0" } +[target.'cfg(target_os = "linux")'.dependencies] +libc = { version = "0.2" } [features] diff --git a/ec/test-lib/src/lib.rs b/ec/test-lib/src/lib.rs index fcc2ca9..efd9602 100644 --- a/ec/test-lib/src/lib.rs +++ b/ec/test-lib/src/lib.rs @@ -7,8 +7,7 @@ use time_alarm_service_messages::{ pub(crate) mod common; -#[cfg(target_os = "windows")] -pub mod acpi; +pub mod os; pub mod mock; pub mod serial; diff --git a/ec/test-lib/src/os/linux.rs b/ec/test-lib/src/os/linux.rs new file mode 100644 index 0000000..302e587 --- /dev/null +++ b/ec/test-lib/src/os/linux.rs @@ -0,0 +1,458 @@ +use crate::{BatterySource, ErrorType, RtcSource, ThermalSource, Threshold}; +use battery_service_messages::{ + BatteryState, BixFixedStrings, BstReturn, bat_swap_try_from_u32, bat_tech_try_from_u32, power_unit_try_from_u32, +}; +use std::{fs, io, path::Path, path::PathBuf}; +use time_alarm_service_messages::{ + AcpiTimerId, AcpiTimestamp, AlarmExpiredWakePolicy, AlarmTimerSeconds, TimeAlarmDeviceCapabilities, TimerStatus, +}; + +/// Errors produced by the Linux OS data source. +#[derive(Debug)] +pub enum Error { + Io(io::Error), + Parse, + Unavailable(&'static str), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Io(e) => write!(f, "I/O error: {e}"), + Self::Parse => write!(f, "Parse error"), + Self::Unavailable(what) => write!(f, "Unavailable on Linux: {what}"), + } + } +} + +impl std::error::Error for Error {} + +impl crate::Error for Error { + fn kind(&self) -> crate::ErrorKind { + match self { + Self::Io(_) => crate::ErrorKind::Io, + Self::Parse => crate::ErrorKind::InvalidData, + Self::Unavailable(_) => crate::ErrorKind::Other, + } + } +} + +impl From for Error { + fn from(e: io::Error) -> Self { + Self::Io(e) + } +} + +fn parse_error(_e: E) -> Error { + Error::Parse +} + +// --- sysfs helpers --- + +fn read_u64(path: &Path) -> Result { + fs::read_to_string(path) + .map_err(Error::Io)? + .trim() + .parse::() + .map_err(parse_error) +} + +fn read_string(path: &Path) -> Result { + Ok(fs::read_to_string(path).map_err(Error::Io)?.trim().to_string()) +} + +/// Truncate or zero-pad a string into a fixed-size byte array. +fn to_fixed_bytes(s: &str) -> [u8; N] { + let mut arr = [0u8; N]; + let bytes = s.as_bytes(); + let len = bytes.len().min(N); + arr[..len].copy_from_slice(&bytes[..len]); + arr +} + +// --- device discovery --- + +/// Find the sysfs path for the first battery power supply. +fn battery_path() -> Result { + for entry in fs::read_dir("/sys/class/power_supply/").map_err(Error::Io)? { + let path = entry.map_err(Error::Io)?.path(); + if fs::read_to_string(path.join("type")) + .map(|t| t.trim() == "Battery") + .unwrap_or(false) + { + return Ok(path); + } + } + Err(Error::Io(io::Error::new( + io::ErrorKind::NotFound, + "no battery found in /sys/class/power_supply/", + ))) +} + +/// Find the sysfs path for the first hwmon device that exposes fan inputs. +fn hwmon_path() -> Result { + for entry in fs::read_dir("/sys/class/hwmon/").map_err(Error::Io)? { + let path = entry.map_err(Error::Io)?.path(); + if path.join("fan1_input").exists() { + return Ok(path); + } + } + Err(Error::Io(io::Error::new( + io::ErrorKind::NotFound, + "no hwmon device with fan1_input found", + ))) +} + +/// Find the sysfs path for the first ACPI thermal zone, falling back to any thermal zone. +fn thermal_zone_path() -> Result { + let mut fallback = None; + for entry in fs::read_dir("/sys/class/thermal/").map_err(Error::Io)? { + let path = entry.map_err(Error::Io)?.path(); + if !path.join("temp").exists() { + continue; + } + if fallback.is_none() { + fallback = Some(path.clone()); + } + if fs::read_to_string(path.join("type")) + .map(|t| t.trim() == "acpitz") + .unwrap_or(false) + { + return Ok(path); + } + } + fallback.ok_or_else(|| Error::Io(io::Error::new(io::ErrorKind::NotFound, "no thermal zone found"))) +} + +/// Read the temperature of the first trip point with the given type (e.g. "active0", "passive", +/// "critical"), returning degrees Celsius. +fn thermal_trip_celsius(zone: &Path, trip_type: &str) -> Result { + for i in 0u32.. { + let type_path = zone.join(format!("trip_point_{i}_type")); + if !type_path.exists() { + break; + } + if fs::read_to_string(&type_path) + .map(|t| t.trim() == trip_type) + .unwrap_or(false) + { + let millideg = read_u64(&zone.join(format!("trip_point_{i}_temp")))? as i64; + return Ok(millideg as f64 / 1000.0); + } + } + Err(Error::Unavailable("thermal trip point not found")) +} + +// --- RTC ioctl --- + +/// Mirrors the kernel's `struct rtc_time` (9 × i32, packed in declaration order). +#[repr(C)] +struct RtcTime { + tm_sec: i32, + tm_min: i32, + tm_hour: i32, + tm_mday: i32, + tm_mon: i32, // 0-based + tm_year: i32, // years since 1900 + tm_wday: i32, + tm_yday: i32, + tm_isdst: i32, +} + +/// Mirrors the kernel's `struct rtc_wkalrm` (2 bytes + 2 padding + rtc_time = 40 bytes). +#[repr(C)] +struct RtcWkalrm { + enabled: u8, + pending: u8, + _pad: [u8; 2], + time: RtcTime, +} + +// _IOR('p', 0x09, struct rtc_time) sizeof(rtc_time) = 36 +const RTC_RD_TIME: libc::c_ulong = 0x8024_7009; +// _IOR('p', 0x10, struct rtc_wkalrm) sizeof(rtc_wkalrm) = 40 +const RTC_WKALM_RD: libc::c_ulong = 0x8028_7010; + +struct OwnedFd(libc::c_int); + +impl Drop for OwnedFd { + fn drop(&mut self) { + unsafe { libc::close(self.0) }; + } +} + +fn open_rtc() -> Result { + let path = c"/dev/rtc0"; + let fd = unsafe { libc::open(path.as_ptr(), libc::O_RDONLY) }; + if fd < 0 { + Err(Error::Io(io::Error::last_os_error())) + } else { + Ok(OwnedFd(fd)) + } +} + +fn rtc_read_time() -> Result { + let fd = open_rtc()?; + let mut rt = RtcTime { + tm_sec: 0, + tm_min: 0, + tm_hour: 0, + tm_mday: 0, + tm_mon: 0, + tm_year: 0, + tm_wday: 0, + tm_yday: 0, + tm_isdst: 0, + }; + let ret = unsafe { libc::ioctl(fd.0, RTC_RD_TIME, &mut rt) }; + if ret < 0 { + Err(Error::Io(io::Error::last_os_error())) + } else { + Ok(rt) + } +} + +fn rtc_read_wkalrm() -> Result { + let fd = open_rtc()?; + let mut wkalrm = RtcWkalrm { + enabled: 0, + pending: 0, + _pad: [0; 2], + time: RtcTime { + tm_sec: 0, + tm_min: 0, + tm_hour: 0, + tm_mday: 0, + tm_mon: 0, + tm_year: 0, + tm_wday: 0, + tm_yday: 0, + tm_isdst: 0, + }, + }; + let ret = unsafe { libc::ioctl(fd.0, RTC_WKALM_RD, &mut wkalrm) }; + if ret < 0 { + Err(Error::Io(io::Error::last_os_error())) + } else { + Ok(wkalrm) + } +} + +// --- public source type --- + +#[derive(Default, Copy, Clone)] +pub struct OsSource {} + +impl OsSource { + pub fn new() -> Self { + Default::default() + } +} + +impl ErrorType for OsSource { + type Error = Error; +} + +impl ThermalSource for OsSource { + fn get_temperature(&self) -> Result { + let zone = thermal_zone_path()?; + let millideg = read_u64(&zone.join("temp"))? as i64; + Ok(millideg as f64 / 1000.0) + } + + fn get_rpm(&self) -> Result { + Ok(read_u64(&hwmon_path()?.join("fan1_input"))? as f64) + } + + fn get_min_rpm(&self) -> Result { + Ok(read_u64(&hwmon_path()?.join("fan1_min"))? as f64) + } + + fn get_max_rpm(&self) -> Result { + Ok(read_u64(&hwmon_path()?.join("fan1_max"))? as f64) + } + + fn get_threshold(&self, threshold: Threshold) -> Result { + let zone = thermal_zone_path()?; + match threshold { + Threshold::On => thermal_trip_celsius(&zone, "active0"), + Threshold::Ramping => thermal_trip_celsius(&zone, "passive"), + Threshold::Max => thermal_trip_celsius(&zone, "critical"), + } + } + + fn set_rpm(&self, rpm: f64) -> Result<(), Self::Error> { + let target = hwmon_path()?.join("fan1_target"); + if target.exists() { + fs::write(&target, format!("{}\n", rpm as u64)).map_err(Error::Io) + } else { + Err(Error::Unavailable("fan1_target not writable on this system")) + } + } +} + +impl BatterySource for OsSource { + fn get_bst(&self) -> Result { + let bat = battery_path()?; + + let status = read_string(&bat.join("status"))?; + let battery_state = match status.as_str() { + "Charging" => BatteryState::CHARGING, + "Discharging" => BatteryState::DISCHARGING, + _ => BatteryState::empty(), + }; + + // Present rate: µW → mW, or µA → mA + let battery_present_rate = if bat.join("power_now").exists() { + (read_u64(&bat.join("power_now"))? / 1000) as u32 + } else { + (read_u64(&bat.join("current_now"))? / 1000) as u32 + }; + + // Remaining capacity: µWh → mWh, or µAh → mAh + let battery_remaining_capacity = if bat.join("energy_now").exists() { + (read_u64(&bat.join("energy_now"))? / 1000) as u32 + } else { + (read_u64(&bat.join("charge_now"))? / 1000) as u32 + }; + + // Present voltage: µV → mV + let battery_present_voltage = (read_u64(&bat.join("voltage_now"))? / 1000) as u32; + + Ok(BstReturn { + battery_state, + battery_present_rate, + battery_remaining_capacity, + battery_present_voltage, + }) + } + + fn get_bix(&self) -> Result { + let bat = battery_path()?; + + // power_unit: 0 = mW, 1 = mA — infer from which capacity attributes are present + let energy_based = bat.join("energy_full").exists(); + let power_unit = power_unit_try_from_u32(if energy_based { 0 } else { 1 }).map_err(|_| Error::Parse)?; + + let design_capacity = if energy_based { + (read_u64(&bat.join("energy_full_design"))? / 1000) as u32 + } else { + (read_u64(&bat.join("charge_full_design"))? / 1000) as u32 + }; + + let last_full_charge_capacity = if energy_based { + (read_u64(&bat.join("energy_full"))? / 1000) as u32 + } else { + (read_u64(&bat.join("charge_full"))? / 1000) as u32 + }; + + let design_voltage = (read_u64(&bat.join("voltage_min_design"))? / 1000) as u32; + + let cycle_count = read_u64(&bat.join("cycle_count")).unwrap_or(0) as u32; + + // Li-ion and Li-poly are rechargeable (Secondary); everything else is Primary. + let technology_str = read_string(&bat.join("technology")).unwrap_or_default(); + let battery_technology = bat_tech_try_from_u32(match technology_str.as_str() { + "Li-ion" | "Li-poly" | "LiP" => 1, + _ => 0, + }) + .map_err(|_| Error::Parse)?; + + let model_number = to_fixed_bytes(&read_string(&bat.join("model_name")).unwrap_or_default()); + let serial_number = to_fixed_bytes(&read_string(&bat.join("serial_number")).unwrap_or_default()); + let battery_type = to_fixed_bytes(&technology_str); + let oem_info = to_fixed_bytes(&read_string(&bat.join("manufacturer")).unwrap_or_default()); + + Ok(BixFixedStrings { + revision: 0, + power_unit, + design_capacity, + last_full_charge_capacity, + battery_technology, + design_voltage, + // design_cap_of_warning/low are in sysfs only as percentages; not available as + // absolute mWh/mAh values without also knowing full charge capacity. + design_cap_of_warning: 0, + design_cap_of_low: 0, + cycle_count, + // The fields below have no Linux sysfs equivalent. + measurement_accuracy: 0, + max_sampling_time: 0, + min_sampling_time: 0, + max_averaging_interval: 0, + min_averaging_interval: 0, + battery_capacity_granularity_1: 0, + battery_capacity_granularity_2: 0, + model_number, + serial_number, + battery_type, + oem_info, + battery_swapping_capability: bat_swap_try_from_u32(0).map_err(|_| Error::Parse)?, + }) + } + + fn set_btp(&self, trippoint: u32) -> Result<(), Self::Error> { + let bat = battery_path()?; + // sysfs alarm is in µWh/µAh; convert from mWh/mAh by multiplying by 1000. + let alarm_value = trippoint as u64 * 1000; + fs::write(bat.join("alarm"), format!("{alarm_value}\n")).map_err(Error::Io) + } +} + +impl RtcSource for OsSource { + fn get_capabilities(&self) -> Result { + // No Linux sysfs equivalent for the ACPI _GCP capabilities bitfield. + Ok(TimeAlarmDeviceCapabilities(0)) + } + + fn get_real_time(&self) -> Result { + let rt = rtc_read_time()?; + + // Build a 16-byte RawAcpiTimestamp buffer (matches the packed struct in acpi_timestamp.rs): + // [0..2] year (u16 LE) + // [2] month (1-based) + // [3] day + // [4] hour + // [5] minute + // [6] second + // [7] valid_or_padding — 1 = time is valid (_GRT semantics) + // [8..10] milliseconds (u16 LE) + // [10..12] time_zone (i16 LE) — 2047 = unspecified + // [12] daylight (u8) — 0 = NotObserved + // [13..16] _padding + let mut buf = [0u8; 16]; + buf[0..2].copy_from_slice(&((rt.tm_year + 1900) as u16).to_le_bytes()); + buf[2] = (rt.tm_mon + 1) as u8; // tm_mon is 0-based; UEFI is 1-based + buf[3] = rt.tm_mday as u8; + buf[4] = rt.tm_hour as u8; + buf[5] = rt.tm_min as u8; + buf[6] = rt.tm_sec as u8; + buf[7] = 1; // valid_or_padding = 1 (time is valid) + // buf[8..10] milliseconds = 0 (already zeroed) + buf[10..12].copy_from_slice(&2047i16.to_le_bytes()); // EFI_UNSPECIFIED_TIMEZONE + // buf[12] daylight = 0 (NotObserved, already zeroed) + // buf[13..16] _padding (already zeroed) + + AcpiTimestamp::try_from_bytes(&buf).map_err(|_| Error::Parse) + } + + fn get_wake_status(&self, _timer_id: AcpiTimerId) -> Result { + let wkalrm = rtc_read_wkalrm()?; + // Linux has a single RTC alarm shared by both AC and DC timer IDs. + // Report bit 0 (AC) and bit 1 (DC) together to reflect that status. + let bits = if wkalrm.enabled != 0 { 0x03u32 } else { 0x00u32 }; + Ok(TimerStatus(bits)) + } + + fn get_expired_timer_wake_policy(&self, _timer_id: AcpiTimerId) -> Result { + Err(Error::Unavailable( + "_TIP (expired timer wake policy) has no Linux equivalent", + )) + } + + fn get_timer_value(&self, _timer_id: AcpiTimerId) -> Result { + Err(Error::Unavailable( + "_TIV (timer interrupt value) has no Linux equivalent", + )) + } +} diff --git a/ec/test-lib/src/os/mod.rs b/ec/test-lib/src/os/mod.rs new file mode 100644 index 0000000..55ae607 --- /dev/null +++ b/ec/test-lib/src/os/mod.rs @@ -0,0 +1,5 @@ +#[cfg_attr(target_os = "windows", path = "windows.rs")] +#[cfg_attr(target_os = "linux", path = "linux.rs")] +mod imp; + +pub use imp::*; diff --git a/ec/test-lib/src/acpi.rs b/ec/test-lib/src/os/windows.rs similarity index 95% rename from ec/test-lib/src/acpi.rs rename to ec/test-lib/src/os/windows.rs index f240bbb..b34d2bd 100644 --- a/ec/test-lib/src/acpi.rs +++ b/ec/test-lib/src/os/windows.rs @@ -346,9 +346,9 @@ impl TryFrom> for AcpiEvalOutputBufferV1 { } #[derive(Default, Copy, Clone)] -pub struct Acpi {} +pub struct OsSource {} -impl Acpi { +impl OsSource { pub fn new() -> Self { Default::default() } @@ -426,7 +426,7 @@ impl Acpi { /// Evaluates the provided method with the provided arguments and returns its single u32 result. /// Errors if the result is not a single u32. fn evaluate_u32(name: &str, args: Option<&[AcpiMethodArgument]>) -> Result { - let output = Acpi::evaluate(name, args)?; + let output = OsSource::evaluate(name, args)?; if output.count != 1 { Err(Error::UnexpectedResponse) @@ -440,7 +440,7 @@ impl Acpi { fn acpi_get_var(guid: uuid::Uuid) -> Result { let args = [AcpiMethodArgument::Int(1), AcpiMethodArgument::Guid(guid.to_bytes_le())]; - let output = Acpi::evaluate("\\_SB.ECT0.TGVR", Some(&args))?; + let output = OsSource::evaluate("\\_SB.ECT0.TGVR", Some(&args))?; if output.count != 2 { Err(Error::UnexpectedResponse) @@ -459,7 +459,7 @@ fn acpi_set_var(guid: uuid::Uuid, value: f64) -> Result<(), Error> { AcpiMethodArgument::Guid(guid.to_bytes_le()), AcpiMethodArgument::Int(value), ]; - let output = Acpi::evaluate("\\_SB.ECT0.TSVR", Some(&args))?; + let output = OsSource::evaluate("\\_SB.ECT0.TSVR", Some(&args))?; if output.count != 1 { Err(Error::UnexpectedResponse) @@ -470,13 +470,13 @@ fn acpi_set_var(guid: uuid::Uuid, value: f64) -> Result<(), Error> { } } -impl ErrorType for Acpi { +impl ErrorType for OsSource { type Error = Error; } -impl ThermalSource for Acpi { +impl ThermalSource for OsSource { fn get_temperature(&self) -> Result { - let output = Acpi::evaluate("\\_SB.ECT0.RTMP", None)?; + let output = OsSource::evaluate("\\_SB.ECT0.RTMP", None)?; if output.count != 1 { Err(Error::UnexpectedResponse) } else { @@ -509,9 +509,9 @@ impl ThermalSource for Acpi { } } -impl BatterySource for Acpi { +impl BatterySource for OsSource { fn get_bst(&self) -> Result { - let data = Acpi::evaluate("\\_SB.ECT0.TBST", None)?; + let data = OsSource::evaluate("\\_SB.ECT0.TBST", None)?; // We are expecting 4 32-bit values if data.count != 4 { @@ -527,7 +527,7 @@ impl BatterySource for Acpi { } fn get_bix(&self) -> Result { - let data = Acpi::evaluate("\\_SB.ECT0.TBIX", None)?; + let data = OsSource::evaluate("\\_SB.ECT0.TBIX", None)?; // We are expecting 21 arguments if data.count != 21 { Err(Error::UnexpectedResponse) @@ -577,21 +577,21 @@ impl BatterySource for Acpi { fn set_btp(&self, trippoint: u32) -> Result<(), Self::Error> { // No return value is expected according to ACPI spec - let _ = Acpi::evaluate("\\_SB.ECT0.TBTP", Some(&[AcpiMethodArgument::Int(trippoint)]))?; + let _ = OsSource::evaluate("\\_SB.ECT0.TBTP", Some(&[AcpiMethodArgument::Int(trippoint)]))?; Ok(()) } } -impl RtcSource for Acpi { +impl RtcSource for OsSource { fn get_capabilities(&self) -> Result { - Ok(TimeAlarmDeviceCapabilities(Acpi::evaluate_u32( + Ok(TimeAlarmDeviceCapabilities(OsSource::evaluate_u32( "\\_SB.ECT0._GCP", None, )?)) } fn get_real_time(&self) -> Result { - let result = Acpi::evaluate("\\_SB.ECT0._GRT", None)?; + let result = OsSource::evaluate("\\_SB.ECT0._GRT", None)?; if result.count != 1 { return Err(Error::UnexpectedResponse); } @@ -605,21 +605,21 @@ impl RtcSource for Acpi { } fn get_wake_status(&self, timer_id: AcpiTimerId) -> Result { - Ok(TimerStatus(Acpi::evaluate_u32( + Ok(TimerStatus(OsSource::evaluate_u32( "\\_SB.ECT0._GWS", Some(&[AcpiMethodArgument::Int(timer_id.into())]), )?)) } fn get_expired_timer_wake_policy(&self, timer_id: AcpiTimerId) -> Result { - Ok(AlarmExpiredWakePolicy(Acpi::evaluate_u32( + Ok(AlarmExpiredWakePolicy(OsSource::evaluate_u32( "\\_SB.ECT0._TIP", Some(&[AcpiMethodArgument::Int(timer_id.into())]), )?)) } fn get_timer_value(&self, timer_id: AcpiTimerId) -> Result { - Ok(AlarmTimerSeconds(Acpi::evaluate_u32( + Ok(AlarmTimerSeconds(OsSource::evaluate_u32( "\\_SB.ECT0._TIV", Some(&[AcpiMethodArgument::Int(timer_id.into())]), )?)) diff --git a/ec/test-tui/src/main.rs b/ec/test-tui/src/main.rs index 68cc4f1..0f599bd 100644 --- a/ec/test-tui/src/main.rs +++ b/ec/test-tui/src/main.rs @@ -78,7 +78,7 @@ fn main() -> color_eyre::Result<()> { SourceKind::Acpi => { let period = Duration::from_secs(cli.sample_period.unwrap_or(60)); - app::App::new(ec_test_lib::acpi::Acpi::default(), period).run(terminal) + app::App::new(ec_test_lib::os::OsSource::default(), period).run(terminal) } } }