base: Add ACPI fan device discovery and status exposure (P48)
Add fan.rs module to acpid that discovers FAN* devices under \_TZ, evaluates _FST for current speed level and RPM, and exposes them via /scheme/acpi/fan/<name>/status. Update thermald to read and log fan status alongside temperature sensors.
This commit is contained in:
@@ -0,0 +1,324 @@
|
||||
diff --git a/drivers/acpid/src/acpi.rs b/drivers/acpid/src/acpi.rs
|
||||
index 5c881334..ea480bb7 100644
|
||||
--- a/drivers/acpid/src/acpi.rs
|
||||
+++ b/drivers/acpid/src/acpi.rs
|
||||
@@ -452,0 +453 @@ pub struct AcpiContext {
|
||||
+ pub fan_state: crate::fan::FanState,
|
||||
@@ -533,0 +535 @@ impl AcpiContext {
|
||||
+ fan_state: crate::fan::FanState::new(),
|
||||
@@ -564 +566 @@ impl AcpiContext {
|
||||
- // Discover thermal zones if AML is ready.
|
||||
+ // Discover thermal zones and fan devices if AML is ready.
|
||||
@@ -565,0 +568 @@ impl AcpiContext {
|
||||
+ this.fan_state.refresh(&this);
|
||||
@@ -663,0 +667,24 @@ impl AcpiContext {
|
||||
+ /// Discover fan device names by scanning the AML namespace under `\_TZ`.
|
||||
+ pub fn fan_device_names(&self) -> Result<Vec<String>, AmlEvalError> {
|
||||
+ let mut symbols = self.aml_symbols.write();
|
||||
+ let interpreter = symbols.aml_context_mut()?;
|
||||
+ let mut ns = interpreter.namespace.lock();
|
||||
+
|
||||
+ let mut names = Vec::new();
|
||||
+ let _ = ns.traverse(|level_aml_name, _level| {
|
||||
+ let name_str = aml_to_symbol(level_aml_name);
|
||||
+ if name_str.starts_with("\\_TZ_.FAN") || name_str.starts_with("_TZ_.FAN") {
|
||||
+ let after_prefix = if name_str.starts_with("\\_TZ_.") {
|
||||
+ &name_str[7..]
|
||||
+ } else {
|
||||
+ &name_str[6..]
|
||||
+ };
|
||||
+ if !after_prefix.contains('.') {
|
||||
+ names.push(after_prefix.to_string());
|
||||
+ }
|
||||
+ }
|
||||
+ Ok(true)
|
||||
+ });
|
||||
+ Ok(names)
|
||||
+ }
|
||||
+
|
||||
diff --git a/drivers/acpid/src/fan.rs b/drivers/acpid/src/fan.rs
|
||||
new file mode 100644
|
||||
index 00000000..8b4fd533
|
||||
--- /dev/null
|
||||
+++ b/drivers/acpid/src/fan.rs
|
||||
@@ -0,0 +1,177 @@
|
||||
+use acpi::aml::namespace::AmlName;
|
||||
+use acpi::aml::AmlError;
|
||||
+use std::str::FromStr;
|
||||
+use std::sync::{Arc, RwLock};
|
||||
+
|
||||
+use crate::acpi::{AcpiContext, AmlEvalError};
|
||||
+use amlserde::AmlSerdeValue;
|
||||
+
|
||||
+/// A discovered ACPI fan device.
|
||||
+#[derive(Clone, Debug)]
|
||||
+pub struct FanDevice {
|
||||
+ pub name: String,
|
||||
+ /// Current speed level from _FST (0 = off, higher = faster).
|
||||
+ pub current_level: Option<u64>,
|
||||
+ /// Current speed in RPM from _FST (0xFFFFFFFF = unknown).
|
||||
+ pub current_rpm: Option<u64>,
|
||||
+}
|
||||
+
|
||||
+impl FanDevice {
|
||||
+ fn from_device_eval(
|
||||
+ ctx: &AcpiContext,
|
||||
+ device_name: &str,
|
||||
+ ) -> Result<Self, FanError> {
|
||||
+ let aml_prefix = format!("\\_TZ_.{device_name}");
|
||||
+
|
||||
+ let mut fan = FanDevice {
|
||||
+ name: device_name.to_owned(),
|
||||
+ current_level: None,
|
||||
+ current_rpm: None,
|
||||
+ };
|
||||
+
|
||||
+ // Evaluate _FST (fan status). ACPI spec: returns a package:
|
||||
+ // { Revision (Integer), CurrentSpeedLevel (Integer), CurrentSpeedRPM (Integer) }
|
||||
+ if let Ok(fst_name) = AmlName::from_str(&format!("{aml_prefix}._FST")) {
|
||||
+ match ctx.aml_eval(fst_name, Vec::new()) {
|
||||
+ Ok(value) => {
|
||||
+ if let AmlSerdeValue::Package(elements) = value {
|
||||
+ if elements.len() >= 2 {
|
||||
+ fan.current_level = extract_u64(&elements[1]);
|
||||
+ }
|
||||
+ if elements.len() >= 3 {
|
||||
+ fan.current_rpm = extract_u64(&elements[2]);
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ Err(e) => {
|
||||
+ log::debug!("Fan device {device_name}: _FST eval failed: {e:?}");
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ Ok(fan)
|
||||
+ }
|
||||
+
|
||||
+ /// Produce a text summary suitable for scheme read().
|
||||
+ pub fn to_text(&self) -> String {
|
||||
+ let mut s = String::new();
|
||||
+ s.push_str(&format!("name={}\n", self.name));
|
||||
+ s.push_str(&format!(
|
||||
+ "current_level={}\n",
|
||||
+ format_option_u64(self.current_level)
|
||||
+ ));
|
||||
+ s.push_str(&format!(
|
||||
+ "current_rpm={}\n",
|
||||
+ format_option_u64(self.current_rpm)
|
||||
+ ));
|
||||
+ s
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+fn format_option_u64(value: Option<u64>) -> String {
|
||||
+ match value {
|
||||
+ Some(v) => {
|
||||
+ if v == 0xFFFFFFFF {
|
||||
+ "unknown".to_string()
|
||||
+ } else {
|
||||
+ format!("{v}")
|
||||
+ }
|
||||
+ }
|
||||
+ None => "na".to_string(),
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+fn extract_u64(value: &AmlSerdeValue) -> Option<u64> {
|
||||
+ match value {
|
||||
+ AmlSerdeValue::Integer(i) => Some(*i as u64),
|
||||
+ _ => None,
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+#[derive(Debug)]
|
||||
+pub enum FanError {
|
||||
+ AmlError(AmlError),
|
||||
+ EvalError(AmlEvalError),
|
||||
+ NotFound,
|
||||
+}
|
||||
+
|
||||
+impl From<AmlError> for FanError {
|
||||
+ fn from(value: AmlError) -> Self {
|
||||
+ FanError::AmlError(value)
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+impl From<AmlEvalError> for FanError {
|
||||
+ fn from(value: AmlEvalError) -> Self {
|
||||
+ FanError::EvalError(value)
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+/// Discovers all ACPI fan devices under the `\_TZ` namespace.
|
||||
+///
|
||||
+/// Walks the AML namespace looking for objects directly under `_TZ` whose
|
||||
+/// names start with `FAN` (e.g., `FAN0`, `FAN1`). For each, evaluates
|
||||
+/// fan status methods and returns a populated [`FanDevice`].
|
||||
+pub fn discover_fans(ctx: &AcpiContext) -> Vec<FanDevice> {
|
||||
+ let mut fans = Vec::new();
|
||||
+
|
||||
+ let fan_names = match ctx.fan_device_names() {
|
||||
+ Ok(names) => names,
|
||||
+ Err(e) => {
|
||||
+ log::debug!("Fan device discovery failed: {e:?}");
|
||||
+ return fans;
|
||||
+ }
|
||||
+ };
|
||||
+
|
||||
+ for child_name in fan_names {
|
||||
+ match FanDevice::from_device_eval(ctx, &child_name) {
|
||||
+ Ok(fan) => {
|
||||
+ log::info!(
|
||||
+ "Fan device discovered: {} = level={:?}, rpm={:?}",
|
||||
+ fan.name,
|
||||
+ fan.current_level,
|
||||
+ fan.current_rpm,
|
||||
+ );
|
||||
+ fans.push(fan);
|
||||
+ }
|
||||
+ Err(e) => {
|
||||
+ log::warn!("Fan device {child_name}: discovery failed: {e:?}");
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ fans
|
||||
+}
|
||||
+
|
||||
+/// Cached fan device state, refreshed on demand.
|
||||
+pub struct FanState {
|
||||
+ fans: RwLock<Vec<FanDevice>>,
|
||||
+}
|
||||
+
|
||||
+impl FanState {
|
||||
+ pub fn new() -> Self {
|
||||
+ Self {
|
||||
+ fans: RwLock::new(Vec::new()),
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ pub fn refresh(&self, ctx: &AcpiContext) {
|
||||
+ let discovered = discover_fans(ctx);
|
||||
+ if let Ok(mut fans) = self.fans.write() {
|
||||
+ *fans = discovered;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ pub fn fans(&self) -> Vec<FanDevice> {
|
||||
+ self.fans.read().map(|g| g.clone()).unwrap_or_default()
|
||||
+ }
|
||||
+
|
||||
+ pub fn fan_by_name(&self, name: &str) -> Option<FanDevice> {
|
||||
+ self.fans
|
||||
+ .read()
|
||||
+ .ok()?
|
||||
+ .iter()
|
||||
+ .find(|f| f.name == name)
|
||||
+ .cloned()
|
||||
+ }
|
||||
+}
|
||||
diff --git a/drivers/acpid/src/main.rs b/drivers/acpid/src/main.rs
|
||||
index 91336ba7..c7b8ff3e 100644
|
||||
--- a/drivers/acpid/src/main.rs
|
||||
+++ b/drivers/acpid/src/main.rs
|
||||
@@ -18,0 +19 @@ mod thermal;
|
||||
+mod fan;
|
||||
diff --git a/drivers/acpid/src/scheme.rs b/drivers/acpid/src/scheme.rs
|
||||
index b92327be..905b42ff 100644
|
||||
--- a/drivers/acpid/src/scheme.rs
|
||||
+++ b/drivers/acpid/src/scheme.rs
|
||||
@@ -49,0 +50,2 @@ enum HandleKind<'a> {
|
||||
+ Fan,
|
||||
+ FanDevice(String),
|
||||
@@ -64,0 +67,2 @@ impl HandleKind<'_> {
|
||||
+ Self::Fan => true,
|
||||
+ Self::FanDevice(_) => false,
|
||||
@@ -80,0 +85,2 @@ impl HandleKind<'_> {
|
||||
+ Self::Fan => 0,
|
||||
+ Self::FanDevice(ref text) => text.len(),
|
||||
@@ -243,0 +250,8 @@ impl SchemeSync for AcpiScheme<'_, '_> {
|
||||
+ ["fan"] => HandleKind::Fan,
|
||||
+ ["fan", device] => {
|
||||
+ if let Some(fan) = self.ctx.fan_state.fan_by_name(device) {
|
||||
+ HandleKind::FanDevice(fan.to_text())
|
||||
+ } else {
|
||||
+ return Err(Error::new(ENOENT));
|
||||
+ }
|
||||
+ }
|
||||
@@ -333,0 +348 @@ impl SchemeSync for AcpiScheme<'_, '_> {
|
||||
+ HandleKind::FanDevice(ref text) => text.as_bytes(),
|
||||
@@ -361,0 +377 @@ impl SchemeSync for AcpiScheme<'_, '_> {
|
||||
+ (DirentKind::Directory, "fan"),
|
||||
@@ -437,0 +454,17 @@ impl SchemeSync for AcpiScheme<'_, '_> {
|
||||
+ HandleKind::Fan => {
|
||||
+ for (idx, fan) in self
|
||||
+ .ctx
|
||||
+ .fan_state
|
||||
+ .fans()
|
||||
+ .iter()
|
||||
+ .enumerate()
|
||||
+ .skip(opaque_offset as usize)
|
||||
+ {
|
||||
+ buf.entry(DirEntry {
|
||||
+ inode: 0,
|
||||
+ next_opaque_id: idx as u64 + 1,
|
||||
+ name: &fan.name,
|
||||
+ kind: DirentKind::Regular,
|
||||
+ })?;
|
||||
+ }
|
||||
+ }
|
||||
diff --git a/drivers/thermald/src/main.rs b/drivers/thermald/src/main.rs
|
||||
index 10c4b531..b8d271b5 100644
|
||||
--- a/drivers/thermald/src/main.rs
|
||||
+++ b/drivers/thermald/src/main.rs
|
||||
@@ -45,0 +46,31 @@ fn read_coretemp_cpus() -> Vec<(String, f32)> {
|
||||
+fn read_acpi_fans() -> Vec<(String, Option<u64>, Option<u64>)> {
|
||||
+ let mut fans = Vec::new();
|
||||
+ if let Ok(entries) = fs::read_dir("/scheme/acpi/fan") {
|
||||
+ for entry in entries.flatten() {
|
||||
+ let name = entry.file_name().into_string().unwrap_or_default();
|
||||
+ if name.starts_with('.') {
|
||||
+ continue;
|
||||
+ }
|
||||
+ let path = format!("/scheme/acpi/fan/{}/status", name);
|
||||
+ let mut level = None;
|
||||
+ let mut rpm = None;
|
||||
+ if let Ok(data) = fs::read_to_string(&path) {
|
||||
+ for line in data.lines() {
|
||||
+ if let Some(val) = line.strip_prefix("current_level=") {
|
||||
+ if val != "na" && val != "unknown" {
|
||||
+ level = val.parse::<u64>().ok();
|
||||
+ }
|
||||
+ }
|
||||
+ if let Some(val) = line.strip_prefix("current_rpm=") {
|
||||
+ if val != "na" && val != "unknown" {
|
||||
+ rpm = val.parse::<u64>().ok();
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ fans.push((name, level, rpm));
|
||||
+ }
|
||||
+ }
|
||||
+ fans
|
||||
+}
|
||||
+
|
||||
@@ -58,0 +90 @@ fn main() -> Result<()> {
|
||||
+ let fans = read_acpi_fans();
|
||||
@@ -90,0 +123,14 @@ fn main() -> Result<()> {
|
||||
+ for (name, level, rpm) in &fans {
|
||||
+ match (level, rpm) {
|
||||
+ (Some(l), Some(r)) => {
|
||||
+ log::info!("thermald: fan {} = level {}, {} RPM", name, l, r);
|
||||
+ }
|
||||
+ (Some(l), None) => {
|
||||
+ log::info!("thermald: fan {} = level {}", name, l);
|
||||
+ }
|
||||
+ _ => {
|
||||
+ log::debug!("thermald: fan {} = status unavailable", name);
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
@@ -95,6 +95,8 @@ patches = [
|
||||
"P46-storage-audio-msix.patch",
|
||||
# P47: Update thermald to read from P44 thermal zones and coretempd
|
||||
"P47-thermald-backend.patch",
|
||||
# P48: Add ACPI fan device discovery and status exposure
|
||||
"P48-acpid-fan-support.patch",
|
||||
]
|
||||
|
||||
[package]
|
||||
|
||||
Reference in New Issue
Block a user