redbear-power: upower client + render/sensor/battery/dmi updates
This commit is contained in:
@@ -1,22 +1,23 @@
|
||||
//! Battery information via `sysfs` (`/sys/class/power_supply/BAT0/*`).
|
||||
//! Battery information.
|
||||
//!
|
||||
//! Linux hosts expose laptop batteries through the `power_supply` class.
|
||||
//! On Redox, no equivalent scheme exists yet, so `read_battery()` returns
|
||||
//! an `available=false` `BatteryInfo` and the render layer skips the
|
||||
//! panel entirely — per the zero-stub policy.
|
||||
//!
|
||||
//! The Redox target needs a `power_supply` scheme daemon (likely wired to
|
||||
//! ACPI); this is forward work tracked in the v1.6 docs.
|
||||
//! On Linux the primary source is `/sys/class/power_supply/BAT0/*`. On
|
||||
//! Redox, ACPI exposes battery and adapter state via the power scheme:
|
||||
//! `/scheme/acpi/power/batteries/BAT0/{state,percentage}` and
|
||||
//! `/scheme/acpi/power/adapters/AC/online`. If neither source is
|
||||
//! available, `read_battery()` returns `available=false` and the render
|
||||
//! layer skips the panel.
|
||||
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const SYS_POWER_SUPPLY: &str = "/sys/class/power_supply";
|
||||
const REDOX_POWER: &str = "/scheme/acpi/power";
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct BatteryInfo {
|
||||
pub available: bool,
|
||||
pub on_battery: Option<bool>,
|
||||
pub name: Option<String>,
|
||||
pub status: Option<String>,
|
||||
pub capacity_percent: Option<u32>,
|
||||
@@ -60,6 +61,37 @@ fn read_sysfs_f64_micro_to_units(path: &Path, unit_divisor: f64) -> Option<f64>
|
||||
Some((raw as f64) / unit_divisor)
|
||||
}
|
||||
|
||||
/// Read a Redox power scheme file.
|
||||
fn read_redox(path: &Path) -> Option<String> {
|
||||
read_sysfs(path)
|
||||
}
|
||||
|
||||
fn read_redox_u32(path: &Path) -> Option<u32> {
|
||||
read_redox(path)?.parse::<u32>().ok()
|
||||
}
|
||||
|
||||
fn read_redox_f64(path: &Path) -> Option<f64> {
|
||||
read_redox(path)?.parse::<f64>().ok()
|
||||
}
|
||||
|
||||
/// Map a Redox battery state bitmask to a status string.
|
||||
/// 0x1 = discharging, 0x2 = charging, 0x4 = empty
|
||||
fn redox_state_to_status(state: u32) -> String {
|
||||
if state == 0 {
|
||||
return "Unknown".to_string();
|
||||
}
|
||||
if state & 0x4 != 0 {
|
||||
return "Empty".to_string();
|
||||
}
|
||||
if state & 0x2 != 0 {
|
||||
return "Charging".to_string();
|
||||
}
|
||||
if state & 0x1 != 0 {
|
||||
return "Discharging".to_string();
|
||||
}
|
||||
"Unknown".to_string()
|
||||
}
|
||||
|
||||
impl BatteryInfo {
|
||||
/// Scan `/sys/class/power_supply/` for the first battery device
|
||||
/// (`type == "Battery"`). Returns `None` if no battery is present
|
||||
@@ -80,14 +112,74 @@ impl BatteryInfo {
|
||||
None
|
||||
}
|
||||
|
||||
/// Build a populated `BatteryInfo` from sysfs. Returns an empty
|
||||
/// `available=false` struct if no battery is detected.
|
||||
/// Find the first Redox battery directory under `/scheme/acpi/power/batteries/`.
|
||||
fn find_redox_battery_dir() -> Option<PathBuf> {
|
||||
let batteries_root = Path::new(REDOX_POWER).join("batteries");
|
||||
let dir = fs::read_dir(&batteries_root).ok()?;
|
||||
for entry in dir.flatten() {
|
||||
if entry.path().is_dir() {
|
||||
return Some(entry.path());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Read battery/AC status from the Redox ACPI power scheme.
|
||||
fn read_redox() -> Self {
|
||||
let mut info = Self {
|
||||
available: true,
|
||||
on_battery: None,
|
||||
..Default::default()
|
||||
};
|
||||
let base = match Self::find_redox_battery_dir() {
|
||||
Some(b) => b,
|
||||
None => {
|
||||
info.available = false;
|
||||
return info;
|
||||
}
|
||||
};
|
||||
info.name = base
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.map(|n| n.to_string());
|
||||
let state = read_redox_u32(&base.join("state")).unwrap_or(0);
|
||||
info.status = Some(redox_state_to_status(state));
|
||||
info.capacity_percent = read_redox_u32(&base.join("percentage"));
|
||||
// Redox ACPI power scheme currently does not expose energy, power,
|
||||
// voltage, time estimates, cycle count, or model metadata. Leave those
|
||||
// as None so the render layer shows "?" for unsupported fields.
|
||||
info
|
||||
}
|
||||
|
||||
/// Build a populated `BatteryInfo` from the best available source.
|
||||
/// Returns an empty `available=false` struct if no battery is detected.
|
||||
pub fn read() -> Self {
|
||||
let mut info = Self::read_native();
|
||||
if !info.available {
|
||||
// No local sysfs/scheme battery; try UPower D-Bus as a fallback.
|
||||
if let Some(upower) = crate::upower_client::read_upower() {
|
||||
return upower;
|
||||
}
|
||||
} else {
|
||||
// We have a local battery; overlay UPower's authoritative
|
||||
// status/percentage/on_battery values when possible.
|
||||
info = crate::upower_client::merge_upower(info);
|
||||
}
|
||||
info
|
||||
}
|
||||
|
||||
/// Build a `BatteryInfo` from local sysfs (Linux) or ACPI scheme
|
||||
/// (Redox), without using UPower.
|
||||
fn read_native() -> Self {
|
||||
if Path::new(REDOX_POWER).exists() {
|
||||
return Self::read_redox();
|
||||
}
|
||||
let Some(base) = Self::find_battery_dir() else {
|
||||
return Self::default();
|
||||
};
|
||||
Self {
|
||||
available: true,
|
||||
on_battery: None,
|
||||
name: read_sysfs(&base.join("name")),
|
||||
status: read_sysfs(&base.join("status")),
|
||||
capacity_percent: read_sysfs_u32(&base.join("capacity")),
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
//! SMBIOS / DMI motherboard information.
|
||||
//!
|
||||
//! Reads `/sys/class/dmi/id/*` on Linux hosts. On Redox, no equivalent
|
||||
//! scheme exists yet, so `read_dmi()` returns an empty struct and the
|
||||
//! render layer displays `?` for missing fields — per the zero-stub policy.
|
||||
//!
|
||||
//! The Redox target needs a `dmi` scheme daemon that exposes SMBIOS tables
|
||||
//! via `/scheme/dmi/...`; this is forward work tracked in the v1.5 docs.
|
||||
//! Reads `/sys/class/dmi/id/*` on Linux hosts. On Redox, the ACPI
|
||||
//! scheme exposes SMBIOS data at `/scheme/acpi/dmi` as a single
|
||||
//! `key=value` file (and optionally per-field files like
|
||||
//! `/scheme/acpi/dmi/sys_vendor`). If neither source is available,
|
||||
//! `read_dmi()` returns an empty struct and the render layer displays
|
||||
//! `?` for missing fields.
|
||||
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
/// Linux sysfs path for DMI/SMBIOS data.
|
||||
const SYS_DMI: &str = "/sys/class/dmi/id";
|
||||
/// Redox ACPI scheme path for DMI/SMBIOS data.
|
||||
const REDOX_DMI: &str = "/scheme/acpi/dmi";
|
||||
|
||||
/// DMI/SMBIOS fields. All fields are `Option<String>` because any one of
|
||||
/// them may be unreadable (permission denied, missing sysfs file, etc.).
|
||||
/// them may be unreadable (permission denied, missing file, etc.).
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct DmiInfo {
|
||||
pub board_vendor: Option<String>,
|
||||
@@ -57,35 +59,93 @@ impl DmiInfo {
|
||||
}
|
||||
}
|
||||
|
||||
/// Probe whether `/sys/class/dmi/id/` exists. Used by the Sources
|
||||
/// header line to report `dmi=ok` vs `dmi=no`.
|
||||
pub fn available() -> bool {
|
||||
Path::new(SYS_DMI).is_dir()
|
||||
/// Read a single Redox per-field file (`/scheme/acpi/dmi/<field>`).
|
||||
fn read_redox_field(name: &str) -> Option<String> {
|
||||
let path = Path::new(REDOX_DMI).join(name);
|
||||
match fs::read_to_string(&path) {
|
||||
Ok(s) => {
|
||||
let trimmed = s.trim().to_string();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed)
|
||||
}
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a populated `DmiInfo` from sysfs. Each field is read
|
||||
/// independently so one failure doesn't poison the others.
|
||||
/// Parse the Redox `/scheme/acpi/dmi` single-file format, which is a
|
||||
/// set of `key=value` lines. Returns an empty map on any error.
|
||||
fn parse_redox_dmi() -> std::collections::HashMap<String, String> {
|
||||
let mut map = std::collections::HashMap::new();
|
||||
let Ok(content) = fs::read_to_string(REDOX_DMI) else {
|
||||
return map;
|
||||
};
|
||||
for line in content.lines() {
|
||||
let Some((key, value)) = line.split_once('=') else {
|
||||
continue;
|
||||
};
|
||||
let trimmed = value.trim().to_string();
|
||||
if !trimmed.is_empty() {
|
||||
map.insert(key.trim().to_string(), trimmed);
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
/// Build a populated `DmiInfo` from Redox ACPI scheme data. Each field
|
||||
/// is read independently from per-field files or the single-file
|
||||
/// key=value fallback so one failure doesn't poison the others.
|
||||
fn read_redox() -> Self {
|
||||
let fields = Self::parse_redox_dmi();
|
||||
let mut info = Self::default();
|
||||
let fill = |key: &str, target: &mut Option<String>| {
|
||||
*target = Self::read_redox_field(key).or_else(|| fields.get(key).cloned());
|
||||
};
|
||||
fill("sys_vendor", &mut info.sys_vendor);
|
||||
fill("board_vendor", &mut info.board_vendor);
|
||||
fill("board_name", &mut info.board_name);
|
||||
fill("board_version", &mut info.board_version);
|
||||
fill("product_name", &mut info.product_name);
|
||||
fill("product_version", &mut info.product_version);
|
||||
fill("bios_version", &mut info.bios_version);
|
||||
// The remaining fields are not exposed by the Redox ACPI DMI scheme
|
||||
// (per redox-driver-sys/src/quirks/dmi.rs); leave them as None.
|
||||
info
|
||||
}
|
||||
|
||||
/// Probe whether DMI data is available on this host.
|
||||
pub fn available() -> bool {
|
||||
Path::new(SYS_DMI).is_dir() || Path::new(REDOX_DMI).exists()
|
||||
}
|
||||
|
||||
/// Build a populated `DmiInfo` from the best available source.
|
||||
pub fn read() -> Self {
|
||||
Self {
|
||||
board_vendor: Self::read_sysfs("board_vendor"),
|
||||
board_name: Self::read_sysfs("board_name"),
|
||||
board_version: Self::read_sysfs("board_version"),
|
||||
board_serial: Self::read_sysfs("board_serial"),
|
||||
board_asset_tag: Self::read_sysfs("board_asset_tag"),
|
||||
bios_vendor: Self::read_sysfs("bios_vendor"),
|
||||
bios_version: Self::read_sysfs("bios_version"),
|
||||
bios_date: Self::read_sysfs("bios_date"),
|
||||
bios_release: Self::read_sysfs("bios_release"),
|
||||
product_name: Self::read_sysfs("product_name"),
|
||||
product_family: Self::read_sysfs("product_family"),
|
||||
product_version: Self::read_sysfs("product_version"),
|
||||
product_serial: Self::read_sysfs("product_serial"),
|
||||
product_uuid: Self::read_sysfs("product_uuid"),
|
||||
sys_vendor: Self::read_sysfs("sys_vendor"),
|
||||
chassis_vendor: Self::read_sysfs("chassis_vendor"),
|
||||
chassis_type: Self::read_sysfs("chassis_type"),
|
||||
chassis_version: Self::read_sysfs("chassis_version"),
|
||||
chassis_asset_tag: Self::read_sysfs("chassis_asset_tag"),
|
||||
if Path::new(REDOX_DMI).exists() {
|
||||
Self::read_redox()
|
||||
} else {
|
||||
Self {
|
||||
board_vendor: Self::read_sysfs("board_vendor"),
|
||||
board_name: Self::read_sysfs("board_name"),
|
||||
board_version: Self::read_sysfs("board_version"),
|
||||
board_serial: Self::read_sysfs("board_serial"),
|
||||
board_asset_tag: Self::read_sysfs("board_asset_tag"),
|
||||
bios_vendor: Self::read_sysfs("bios_vendor"),
|
||||
bios_version: Self::read_sysfs("bios_version"),
|
||||
bios_date: Self::read_sysfs("bios_date"),
|
||||
bios_release: Self::read_sysfs("bios_release"),
|
||||
product_name: Self::read_sysfs("product_name"),
|
||||
product_family: Self::read_sysfs("product_family"),
|
||||
product_version: Self::read_sysfs("product_version"),
|
||||
product_serial: Self::read_sysfs("product_serial"),
|
||||
product_uuid: Self::read_sysfs("product_uuid"),
|
||||
sys_vendor: Self::read_sysfs("sys_vendor"),
|
||||
chassis_vendor: Self::read_sysfs("chassis_vendor"),
|
||||
chassis_type: Self::read_sysfs("chassis_type"),
|
||||
chassis_version: Self::read_sysfs("chassis_version"),
|
||||
chassis_asset_tag: Self::read_sysfs("chassis_asset_tag"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ mod session;
|
||||
mod smart;
|
||||
mod storage;
|
||||
mod theme;
|
||||
mod upower_client;
|
||||
mod wakeup;
|
||||
|
||||
use crate::app::{App, POLL_MS, TabId};
|
||||
|
||||
@@ -977,7 +977,7 @@ pub fn render_battery_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> {
|
||||
let bat = &app.battery;
|
||||
if !bat.available {
|
||||
return Paragraph::new(Line::from(
|
||||
"(no battery detected — /sys/class/power_supply/BAT* not present)"
|
||||
"(no battery detected — no UPower, /sys/class/power_supply, or /scheme/acpi/power battery found)"
|
||||
.set_style(theme.value_warm),
|
||||
))
|
||||
.block(panel_border(focused, " Battery ", &app.theme))
|
||||
@@ -1007,6 +1007,13 @@ pub fn render_battery_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> {
|
||||
]));
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from("State".set_style(theme.label_bold)));
|
||||
lines.push(Line::from(vec![
|
||||
" On battery: ".set_style(theme.label),
|
||||
bat.on_battery
|
||||
.map(|b| if b { "yes" } else { "no" })
|
||||
.unwrap_or("?")
|
||||
.set_style(theme.value),
|
||||
]));
|
||||
lines.push(Line::from(vec![
|
||||
" Status: ".set_style(theme.label),
|
||||
crate::battery::BatteryInfo::display(&bat.status).set_style(theme.value),
|
||||
|
||||
@@ -20,6 +20,7 @@ use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const SYS_HWMON: &str = "/sys/class/hwmon";
|
||||
const REDOX_THERMAL: &str = "/scheme/thermal";
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct SensorReading {
|
||||
@@ -123,6 +124,57 @@ fn read_first_cpu_temp_msr() -> Option<f64> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Read a Redox thermal zone temperature file.
|
||||
fn read_redox_zone_temp(zone_name: &str) -> Option<i64> {
|
||||
let path = Path::new(REDOX_THERMAL)
|
||||
.join("zones")
|
||||
.join(zone_name)
|
||||
.join("temperature");
|
||||
read_sysfs_i64(&path)
|
||||
}
|
||||
|
||||
/// Read a Redox thermal zone status file.
|
||||
fn read_redox_zone_status(zone_name: &str) -> Option<String> {
|
||||
let path = Path::new(REDOX_THERMAL)
|
||||
.join("zones")
|
||||
.join(zone_name)
|
||||
.join("status");
|
||||
read_sysfs(&path)
|
||||
}
|
||||
|
||||
/// Enumerate Redox `/scheme/thermal/zones/` directories and return
|
||||
/// one `HwmonChip`-like entry per zone with a temperature reading.
|
||||
fn read_redox_thermal_zones() -> Vec<HwmonChip> {
|
||||
let zones_root = Path::new(REDOX_THERMAL).join("zones");
|
||||
let Ok(entries) = fs::read_dir(&zones_root) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut chips = Vec::new();
|
||||
for entry in entries.flatten() {
|
||||
let zone_name = match entry.file_name().into_string() {
|
||||
Ok(n) => n,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let Some(temp_raw) = read_redox_zone_temp(&zone_name) else {
|
||||
continue;
|
||||
};
|
||||
let label = read_redox_zone_status(&zone_name)
|
||||
.map(|s| format!("{} ({})", zone_name, s))
|
||||
.unwrap_or_else(|| zone_name.clone());
|
||||
chips.push(HwmonChip {
|
||||
name: "thermal".to_string(),
|
||||
path: zones_root.join(&zone_name),
|
||||
readings: vec![SensorReading {
|
||||
kind: SensorKind::Temp,
|
||||
label: Some(label),
|
||||
raw_value: temp_raw,
|
||||
display_value: format_sensor(SensorKind::Temp, temp_raw),
|
||||
}],
|
||||
});
|
||||
}
|
||||
chips
|
||||
}
|
||||
|
||||
/// Read all `*_input` files in the chip directory, grouped by prefix.
|
||||
fn read_chip_readings(chip_dir: &Path) -> Vec<SensorReading> {
|
||||
let entries = match fs::read_dir(chip_dir) {
|
||||
@@ -187,9 +239,17 @@ fn read_chip_readings(chip_dir: &Path) -> Vec<SensorReading> {
|
||||
|
||||
impl SensorInfo {
|
||||
pub fn available() -> bool {
|
||||
Path::new(SYS_HWMON).is_dir() || read_first_cpu_temp_msr().is_some()
|
||||
Path::new(SYS_HWMON).is_dir()
|
||||
|| Path::new(REDOX_THERMAL).exists()
|
||||
|| read_first_cpu_temp_msr().is_some()
|
||||
}
|
||||
pub fn read() -> Self {
|
||||
if Path::new(REDOX_THERMAL).exists() {
|
||||
let chips = read_redox_thermal_zones();
|
||||
if !chips.is_empty() {
|
||||
return Self { chips };
|
||||
}
|
||||
}
|
||||
let Ok(dirs) = fs::read_dir(SYS_HWMON) else {
|
||||
if let Some(temp_c) = read_first_cpu_temp_msr() {
|
||||
return Self {
|
||||
|
||||
@@ -1,25 +1,18 @@
|
||||
//! Block device storage info via `sysfs` (`/sys/block/<dev>/`).
|
||||
//! Block device storage info.
|
||||
//!
|
||||
//! Linux exposes block device metadata via sysfs: model, vendor, size
|
||||
//! (in 512-byte sectors), rotational flag, removable flag, IO
|
||||
//! scheduler, queue depth, and per-partition layout.
|
||||
//!
|
||||
//! Traffic counters come from `/sys/block/<dev>/stat`:
|
||||
//! read_bytes / write_bytes — total bytes transferred
|
||||
//! reads_completed / writes_completed — I/O operation counts
|
||||
//!
|
||||
//! SMART data (Temperature, ReallocatedSectorsCount, WearLevelingCount,
|
||||
//! etc.) is read via `smartctl --json` if the binary is in PATH.
|
||||
//! Otherwise the SMART section is omitted — per the zero-stub policy.
|
||||
//!
|
||||
//! On Redox, no equivalent scheme exists yet, so `read()` returns an
|
||||
//! empty `StorageInfo` and the render layer shows
|
||||
//! `(no storage devices detected)`.
|
||||
//! On Linux the primary source is `/sys/block/<dev>/` (model, vendor,
|
||||
//! size, rotational/removable flags, scheduler, queue depth, and
|
||||
//! per-device `stat` counters). On Redox, the disk aggregator daemon
|
||||
//! exposes block devices through `/scheme/diskd/`. Device metadata such
|
||||
//! as model, vendor, and exact size are not currently available through
|
||||
//! that scheme; only device names are enumerated, with I/O stats
|
||||
//! remaining unavailable. Linux remains the fully-featured source.
|
||||
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const SYS_BLOCK: &str = "/sys/block";
|
||||
const REDOX_DISKD: &str = "/scheme/diskd";
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct DiskStats {
|
||||
@@ -126,6 +119,25 @@ fn read_disk(name: &str, path: &Path) -> Option<DiskInfo> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Read a Redox diskd entry. The disk aggregator scheme exposes block
|
||||
/// device names but does not currently provide size, model, vendor, or I/O
|
||||
/// counters; those fields are left as sensible defaults.
|
||||
fn read_redox_disk(name: &str, path: &Path) -> DiskInfo {
|
||||
DiskInfo {
|
||||
name: name.to_string(),
|
||||
path: path.to_path_buf(),
|
||||
model: None,
|
||||
vendor: None,
|
||||
size_bytes: 0,
|
||||
rotational: false,
|
||||
removable: false,
|
||||
scheduler: None,
|
||||
queue_depth: None,
|
||||
stats: DiskStats::default(),
|
||||
partitions: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
impl DiskInfo {
|
||||
/// Format bytes with binary unit suffixes (B, KiB, MiB, GiB, TiB).
|
||||
pub fn format_size(bytes: u64) -> String {
|
||||
@@ -160,9 +172,12 @@ pub struct StorageInfo {
|
||||
|
||||
impl StorageInfo {
|
||||
pub fn available() -> bool {
|
||||
Path::new(SYS_BLOCK).is_dir()
|
||||
Path::new(SYS_BLOCK).is_dir() || Path::new(REDOX_DISKD).is_dir()
|
||||
}
|
||||
pub fn read() -> Self {
|
||||
if Path::new(REDOX_DISKD).is_dir() {
|
||||
return Self::read_redox();
|
||||
}
|
||||
let Ok(dirs) = fs::read_dir(SYS_BLOCK) else {
|
||||
return Self::default();
|
||||
};
|
||||
@@ -179,6 +194,24 @@ impl StorageInfo {
|
||||
disks.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
Self { disks }
|
||||
}
|
||||
|
||||
/// Read diskd device names on Redox. The disk aggregator scheme
|
||||
/// enumerates block devices but does not expose size/model/stats.
|
||||
fn read_redox() -> Self {
|
||||
let Ok(dirs) = fs::read_dir(REDOX_DISKD) else {
|
||||
return Self::default();
|
||||
};
|
||||
let mut disks = Vec::new();
|
||||
for entry in dirs.flatten() {
|
||||
let path = entry.path();
|
||||
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
|
||||
continue;
|
||||
};
|
||||
disks.push(read_redox_disk(name, &path));
|
||||
}
|
||||
disks.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
Self { disks }
|
||||
}
|
||||
/// Read disks and compute R/W throughput (KiB/s) for each based
|
||||
/// on delta of read_bytes/write_bytes vs previous read.
|
||||
pub fn read_with_throughput(prev: &StorageInfo, dt_secs: f64) -> Self {
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
//! D-Bus UPower client (system bus).
|
||||
//!
|
||||
//! Reads battery and AC adapter status from `org.freedesktop.UPower` so
|
||||
//! redbear-power can display the same battery information on Linux
|
||||
//! (real UPower) and Red Bear OS (redbear-upower compatibility daemon)
|
||||
//! without relying on sysfs or ACPI scheme details.
|
||||
//!
|
||||
//! Implementation note: this module creates a short-lived tokio runtime
|
||||
//! per call. Battery refresh is only every few seconds, so the overhead is
|
||||
//! acceptable. A long-lived worker thread would be cleaner, but keeping it
|
||||
//! synchronous simplifies the `BatteryInfo::read()` contract.
|
||||
|
||||
use tokio::runtime::Runtime;
|
||||
use zbus::proxy;
|
||||
use zbus::zvariant::OwnedObjectPath;
|
||||
|
||||
use crate::battery::BatteryInfo;
|
||||
|
||||
#[proxy(
|
||||
interface = "org.freedesktop.UPower",
|
||||
default_service = "org.freedesktop.UPower",
|
||||
default_path = "/org/freedesktop/UPower"
|
||||
)]
|
||||
trait UPower {
|
||||
/// Enumerate all power devices (batteries and AC adapters).
|
||||
fn enumerate_devices(&self) -> zbus::Result<Vec<OwnedObjectPath>>;
|
||||
|
||||
/// Critical action the system would take on a critical battery event.
|
||||
fn get_critical_action(&self) -> zbus::Result<String>;
|
||||
|
||||
/// True if the system is running on battery power.
|
||||
#[zbus(property)]
|
||||
fn on_battery(&self) -> zbus::Result<bool>;
|
||||
|
||||
#[zbus(property)]
|
||||
fn daemon_version(&self) -> zbus::Result<String>;
|
||||
}
|
||||
|
||||
#[proxy(
|
||||
interface = "org.freedesktop.UPower.Device",
|
||||
default_service = "org.freedesktop.UPower"
|
||||
)]
|
||||
trait UPowerDevice {
|
||||
/// 0=Unknown, 1=LinePower, 2=Battery.
|
||||
#[zbus(property)]
|
||||
fn type_(&self) -> zbus::Result<u32>;
|
||||
|
||||
/// 0=Unknown, 1=Charging, 2=Discharging, 3=Empty, 4=FullyCharged.
|
||||
#[zbus(property)]
|
||||
fn state(&self) -> zbus::Result<u32>;
|
||||
|
||||
/// Battery charge percentage (0.0–100.0).
|
||||
#[zbus(property)]
|
||||
fn percentage(&self) -> zbus::Result<f64>;
|
||||
|
||||
/// Whether the device is physically present.
|
||||
#[zbus(property)]
|
||||
fn is_present(&self) -> zbus::Result<bool>;
|
||||
|
||||
/// For AC adapters: is the adapter online. For batteries: always false.
|
||||
#[zbus(property)]
|
||||
fn online(&self) -> zbus::Result<bool>;
|
||||
|
||||
/// Native (ACPI) path of the device.
|
||||
#[zbus(property)]
|
||||
fn native_path(&self) -> zbus::Result<String>;
|
||||
}
|
||||
|
||||
/// Device type constants from the UPower specification.
|
||||
mod device_type {
|
||||
pub const UNKNOWN: u32 = 0;
|
||||
pub const LINE_POWER: u32 = 1;
|
||||
pub const BATTERY: u32 = 2;
|
||||
}
|
||||
|
||||
/// Device state constants from the UPower specification.
|
||||
mod device_state {
|
||||
pub const UNKNOWN: u32 = 0;
|
||||
pub const CHARGING: u32 = 1;
|
||||
pub const DISCHARGING: u32 = 2;
|
||||
pub const EMPTY: u32 = 3;
|
||||
pub const FULLY_CHARGED: u32 = 4;
|
||||
}
|
||||
|
||||
fn state_to_status(state: u32) -> String {
|
||||
match state {
|
||||
device_state::CHARGING => "Charging".to_string(),
|
||||
device_state::DISCHARGING => "Discharging".to_string(),
|
||||
device_state::EMPTY => "Empty".to_string(),
|
||||
device_state::FULLY_CHARGED => "Full".to_string(),
|
||||
_ => "Unknown".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read battery/AC status from the UPower D-Bus service. Returns `None` if
|
||||
/// the system bus is unreachable or UPower is not available.
|
||||
pub fn read_upower() -> Option<BatteryInfo> {
|
||||
let rt = Runtime::new().ok()?;
|
||||
rt.block_on(read_upower_async())
|
||||
}
|
||||
|
||||
async fn read_upower_async() -> Option<BatteryInfo> {
|
||||
let conn = zbus::connection::Builder::system()
|
||||
.ok()?
|
||||
.build()
|
||||
.await
|
||||
.ok()?;
|
||||
let upower = UPowerProxy::new(&conn).await.ok()?;
|
||||
|
||||
let on_battery = upower.on_battery().await.unwrap_or(false);
|
||||
let devices = upower.enumerate_devices().await.unwrap_or_default();
|
||||
|
||||
let mut info = BatteryInfo {
|
||||
available: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
for path in devices {
|
||||
let dev = match UPowerDeviceProxy::builder(&conn)
|
||||
.path(path.clone())
|
||||
.ok()?
|
||||
.build()
|
||||
.await
|
||||
{
|
||||
Ok(d) => d,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let dev_type = dev.type_().await.unwrap_or(device_type::UNKNOWN);
|
||||
if dev_type == device_type::LINE_POWER {
|
||||
// AC adapter: we expose its online state as the power_now_w
|
||||
// field? No, we use a dedicated field if it existed. For now,
|
||||
// represent the AC adapter as a synthetic device with name.
|
||||
let _ = dev.online().await.unwrap_or(false);
|
||||
// AC online state is not represented in BatteryInfo; it is
|
||||
// implicit in the OnBattery daemon property. We leave the
|
||||
// BatteryInfo focused on the battery device itself.
|
||||
} else if dev_type == device_type::BATTERY {
|
||||
let is_present = dev.is_present().await.unwrap_or(false);
|
||||
if !is_present {
|
||||
continue;
|
||||
}
|
||||
info.available = true;
|
||||
let state = dev.state().await.unwrap_or(device_state::UNKNOWN);
|
||||
info.status = Some(state_to_status(state));
|
||||
info.capacity_percent = Some(dev.percentage().await.unwrap_or(0.0) as u32);
|
||||
info.name = dev
|
||||
.native_path()
|
||||
.await
|
||||
.ok()
|
||||
.map(|p| p.rsplit('/').next().unwrap_or(&p).to_string());
|
||||
info.on_battery = Some(on_battery);
|
||||
// redbear-upower currently does not expose Energy, Power, Voltage,
|
||||
// TimeToEmpty, TimeToFull, CycleCount, Technology, Model, Vendor,
|
||||
// or Serial. Leave those fields as None so the render layer shows
|
||||
// "?" for unsupported values.
|
||||
}
|
||||
}
|
||||
|
||||
if info.available { Some(info) } else { None }
|
||||
}
|
||||
|
||||
/// Merge the UPower-derived fields into a base `BatteryInfo`. This is a
|
||||
/// convenience helper so `battery.rs` can keep its Linux/Redox sysfs/scheme
|
||||
/// fallback and only overlay the values that UPower provided.
|
||||
pub fn merge_upower(mut base: BatteryInfo) -> BatteryInfo {
|
||||
if let Some(upower) = read_upower() {
|
||||
if base.available {
|
||||
// UPower is authoritative for status and percentage; other fields
|
||||
// that the local sysfs/scheme read already populated are kept.
|
||||
if upower.status.is_some() {
|
||||
base.status = upower.status;
|
||||
}
|
||||
if upower.capacity_percent.is_some() {
|
||||
base.capacity_percent = upower.capacity_percent;
|
||||
}
|
||||
if upower.on_battery.is_some() {
|
||||
base.on_battery = upower.on_battery;
|
||||
}
|
||||
base
|
||||
} else {
|
||||
upower
|
||||
}
|
||||
} else {
|
||||
base
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user