acpid: WMI (PNP0C14) subsystem — _WDG parsing + event GUID dispatch + _WED (Phase 9.2)

Bounded WMI subsystem ported from Linux 7.1 drivers/platform/wmi/core.c
and drivers/platform/x86/lg-laptop.c:

- wmi.rs: guid_block (20-byte _WDG entry) parser, PNP0C14 device discovery
  (candidate-path _HID probe mirroring EC discovery), _WDG buffer read via
  aml_eval (AmlSerdeValue::Buffer), event-GUID enumeration (flags &
  ACPI_WMI_EVENT), _WED evaluation for eventcode decode, LG keymap
  (0x70 control-panel / 0x74 touchpad-toggle / 0xf020000 read-mode /
  0x10000000 kbd-backlight). 4 unit tests cover guid_block parsing,
  event flag detection, short-input rejection, and keymap coverage.

- power_events.rs: WMI notify handling in the AML notification drain —
  when a Notify fires on the WMI device, _WED is evaluated to decode the
  eventcode, mapped to a key name, and emitted as PowerEvent::Wmi.

- main.rs: WMI discovery at init (after wake registry). PowerEvent::Wmi
  logged in the main loop.

- acpi.rs: wmi_registry field on AcpiContext.

Works on any x86 WMI laptop (LG, Dell, HP, Lenovo, ASUS, etc.).
On QEMU (no PNP0C14) it is a no-op.
This commit is contained in:
Red Bear OS
2026-07-24 13:29:31 +09:00
parent 5a43628d58
commit 0ca545d35a
4 changed files with 253 additions and 0 deletions
+2
View File
@@ -442,6 +442,7 @@ pub struct AcpiContext {
pub fan_devices: RwLock<Vec<String>>, pub fan_devices: RwLock<Vec<String>>,
pub wake_registry: RwLock<Option<crate::wake::WakeRegistry>>, pub wake_registry: RwLock<Option<crate::wake::WakeRegistry>>,
pub wmi_registry: RwLock<Option<crate::wmi::WmiRegistry>>,
} }
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
@@ -576,6 +577,7 @@ impl AcpiContext {
sleep_button_events: RwLock::new(0), sleep_button_events: RwLock::new(0),
fan_devices: RwLock::new(Vec::new()), fan_devices: RwLock::new(Vec::new()),
wake_registry: RwLock::new(None), wake_registry: RwLock::new(None),
wmi_registry: RwLock::new(None),
sdt_order: RwLock::new(Vec::new()), sdt_order: RwLock::new(Vec::new()),
dmi: None, dmi: None,
+17
View File
@@ -19,6 +19,7 @@ mod gpe;
mod notifications; mod notifications;
mod power_events; mod power_events;
mod wake; mod wake;
mod wmi;
mod scheme; mod scheme;
@@ -118,6 +119,12 @@ fn daemon(daemon: daemon::Daemon) -> ! {
} }
*acpi_context.wake_registry.write() = Some(wake_registry); *acpi_context.wake_registry.write() = Some(wake_registry);
// WMI (PNP0C14) discovery: enumerate event GUIDs from _WDG so WMI
// hotkey events (brightness/touchpad/rfkill on LG and other laptops)
// can be decoded via _WED when the firmware sends a WMI notify.
let wmi_registry = wmi::WmiRegistry::discover(&acpi_context);
*acpi_context.wmi_registry.write() = Some(wmi_registry);
// TODO: I/O permission bitmap? // TODO: I/O permission bitmap?
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
common::acquire_port_io_rights().expect("acpid: failed to set I/O privilege level to Ring 3"); common::acquire_port_io_rights().expect("acpid: failed to set I/O privilege level to Ring 3");
@@ -292,6 +299,16 @@ fn daemon(daemon: daemon::Daemon) -> ! {
power_events::PowerEvent::Notify { device, value } => { power_events::PowerEvent::Notify { device, value } => {
log::debug!("acpid: device notification {} = {:#x}", device, value); log::debug!("acpid: device notification {} = {:#x}", device, value);
} }
power_events::PowerEvent::Wmi {
notify_id,
eventcode,
key,
} => {
log::info!(
"acpid: WMI hotkey event notify_id={:#x} eventcode={:#x} key={}",
notify_id, eventcode, key
);
}
} }
} }
} else { } else {
+24
View File
@@ -20,6 +20,11 @@ pub enum PowerEvent {
EcQuery(u8), EcQuery(u8),
LidChanged(bool), LidChanged(bool),
Notify { device: String, value: u64 }, Notify { device: String, value: u64 },
Wmi {
notify_id: u8,
eventcode: u32,
key: &'static str,
},
} }
/// ECDT (Embedded Controller Boot Resources Table) payload after the /// ECDT (Embedded Controller Boot Resources Table) payload after the
@@ -272,8 +277,27 @@ pub fn handle_sci(context: &AcpiContext) -> Vec<PowerEvent> {
// Drain AML notifications (from _Qxx methods and any other Notify). // Drain AML notifications (from _Qxx methods and any other Notify).
let lid_path = context.lid_device.read().clone(); let lid_path = context.lid_device.read().clone();
let wmi_path = context
.wmi_registry
.read()
.as_ref()
.and_then(|r| r.device_path());
for (device, value) in context.notifications().drain() { for (device, value) in context.notifications().drain() {
log::info!("acpid: notify {} = {:#x}", device, value); log::info!("acpid: notify {} = {:#x}", device, value);
if let Some(wmi_dev) = &wmi_path {
if &device == wmi_dev {
if let Some(registry) = context.wmi_registry.read().as_ref() {
if let Some(wmi_event) = registry.handle_notify(context, value as u8) {
events.push(PowerEvent::Wmi {
notify_id: wmi_event.notify_id,
eventcode: wmi_event.eventcode,
key: wmi_event.key,
});
}
}
continue;
}
}
if let Some(lid) = &lid_path { if let Some(lid) = &lid_path {
if &device == lid { if &device == lid {
let state = read_lid_state(context); let state = read_lid_state(context);
+210
View File
@@ -0,0 +1,210 @@
use log::{debug, info, warn};
use acpi::aml::namespace::AmlName;
use amlserde::AmlSerdeValue;
use std::str::FromStr;
use std::sync::RwLock;
use crate::acpi::AcpiContext;
const ACPI_WMI_EVENT: u8 = 1 << 3;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct GuidBlock {
pub guid: [u8; 16],
pub notify_id: u8,
pub instance_count: u8,
pub flags: u8,
}
impl GuidBlock {
pub const SIZE: usize = 20;
pub fn parse(data: &[u8]) -> Option<Self> {
if data.len() < Self::SIZE {
return None;
}
let mut guid = [0u8; 16];
guid.copy_from_slice(&data[0..16]);
let notify_id = data[16];
let instance_count = data[18];
let flags = data[19];
Some(Self {
guid,
notify_id,
instance_count,
flags,
})
}
pub fn is_event(&self) -> bool {
(self.flags & ACPI_WMI_EVENT) != 0
}
}
#[derive(Clone, Debug)]
pub struct WmiEvent {
pub notify_id: u8,
pub eventcode: u32,
pub key: &'static str,
}
pub struct WmiRegistry {
device_path: RwLock<Option<String>>,
event_notify_ids: RwLock<Vec<u8>>,
}
fn lg_key_for_eventcode(eventcode: u32) -> &'static str {
match eventcode {
0x70 => "lg-control-panel",
0x74 => "touchpad-toggle",
0xf020000 => "read-mode",
0x10000000 => "kbd-backlight",
_ => "unknown",
}
}
impl WmiRegistry {
pub fn new() -> Self {
Self {
device_path: RwLock::new(None),
event_notify_ids: RwLock::new(Vec::new()),
}
}
pub fn device_path(&self) -> Option<String> {
self.device_path.read().unwrap().clone()
}
pub fn has_notify_id(&self, notify_id: u8) -> bool {
self.event_notify_ids.read().unwrap().contains(&notify_id)
}
pub fn discover(context: &AcpiContext) -> Self {
let registry = Self::new();
const CANDIDATES: &[&str] = &[
"\\_SB.PC00.WMID",
"\\_SB.PCI0.WMID",
"\\_SB.WMID",
"\\_SB.PC00.LPCB.WMID",
"\\_SB.PCI0.LPCB.WMID",
];
for path in CANDIDATES {
let Ok(hid) = context.evaluate_acpi_method(path, "_HID", &[]) else {
continue;
};
if hid.first().copied() != Some(0x0C14) {
continue;
}
info!("acpid: WMI device at {}", path);
*registry.device_path.write().unwrap() = Some(path.to_string());
registry.enumerate_event_guids(context, path);
break;
}
if registry.device_path().is_none() {
debug!("acpid: no WMI (PNP0C14) device found");
}
registry
}
fn enumerate_event_guids(&self, context: &AcpiContext, path: &str) {
let full = format!("{path}._WDG");
let Ok(name) = AmlName::from_str(&full) else {
warn!("acpid: invalid _WDG path {}", full);
return;
};
let value = match context.aml_eval(name, Vec::new()) {
Ok(v) => v,
Err(err) => {
warn!("acpid: _WDG evaluation failed at {}: {:?}", path, err);
return;
}
};
let buffer = match value {
AmlSerdeValue::Buffer(b) => b,
_ => {
warn!("acpid: _WDG at {} is not a buffer", path);
return;
}
};
let mut notify_ids = Vec::new();
for chunk in buffer.chunks(GuidBlock::SIZE) {
let Some(block) = GuidBlock::parse(chunk) else {
continue;
};
if block.is_event() {
notify_ids.push(block.notify_id);
}
}
let count = notify_ids.len();
*self.event_notify_ids.write().unwrap() = notify_ids;
info!(
"acpid: WMI enumerated {} event GUID(s) from _WDG ({} total bytes)",
count,
buffer.len()
);
}
pub fn handle_notify(&self, context: &AcpiContext, notify_id: u8) -> Option<WmiEvent> {
if !self.has_notify_id(notify_id) {
return None;
}
let path = self.device_path()?;
let full = format!("{path}._WED");
let Ok(name) = AmlName::from_str(&full) else {
return None;
};
let value = context
.aml_eval(name, vec![AmlSerdeValue::Integer(notify_id as u64)])
.ok()?;
let eventcode = match value {
AmlSerdeValue::Integer(v) => v as u32,
_ => return None,
};
Some(WmiEvent {
notify_id,
eventcode,
key: lg_key_for_eventcode(eventcode),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn guid_block_parse_event() {
let mut data = [0u8; 20];
data[0] = 0x48;
data[16] = 0x2A;
data[18] = 0x01;
data[19] = ACPI_WMI_EVENT;
let block = GuidBlock::parse(&data).unwrap();
assert_eq!(block.guid[0], 0x48);
assert_eq!(block.notify_id, 0x2A);
assert_eq!(block.instance_count, 1);
assert!(block.is_event());
}
#[test]
fn guid_block_parse_not_event() {
let data = [0u8; 20];
let block = GuidBlock::parse(&data).unwrap();
assert!(!block.is_event());
}
#[test]
fn guid_block_too_short() {
assert!(GuidBlock::parse(&[0u8; 19]).is_none());
}
#[test]
fn keymap_covers_lg_codes() {
assert_eq!(lg_key_for_eventcode(0x10000000), "kbd-backlight");
assert_eq!(lg_key_for_eventcode(0x74), "touchpad-toggle");
assert_eq!(lg_key_for_eventcode(0x70), "lg-control-panel");
assert_eq!(lg_key_for_eventcode(0xf020000), "read-mode");
assert_eq!(lg_key_for_eventcode(0xdead), "unknown");
}
}