diff --git a/drivers/input/i2c-hidd/src/main.rs b/drivers/input/i2c-hidd/src/main.rs new file mode 100644 index 00000000..88270e37 --- /dev/null +++ b/drivers/input/i2c-hidd/src/main.rs @@ -0,0 +1,114 @@ +use std::process; +use std::thread; +use std::time::Duration; + +use anyhow::{Context, Result}; + +mod acpi; +mod hid; +mod input; +mod quirks; + +use acpi::{ + hid_descriptor_address, prepare_acpi_device, read_decoded_resources, recover_acpi_device, + scan_acpi_i2c_hid_devices, +}; +use hid::{fetch_hid_descriptor, fetch_report_descriptor, stream_input_reports, I2cAdapterClient}; +use input::InputForwarder; +use quirks::match_probe_failure_quirk; + +fn main() { + daemon::Daemon::new(daemon); +} + +fn daemon(daemon: daemon::Daemon) -> ! { + common::setup_logging( + "input", + "i2c-hid", + "i2c-hidd", + common::output_level(), + common::file_level(), + ); + + if let Err(err) = run(daemon) { + log::error!("RB_I2C_HIDD_BLOCKER stage=startup error={err:#}"); + process::exit(1); + } + + process::exit(0); +} + +fn run(daemon: daemon::Daemon) -> Result<()> { + log::info!("RB_I2C_HIDD_SCHEMA version=1"); + + let devices = scan_acpi_i2c_hid_devices().context("failed to scan ACPI I2C HID devices")?; + if devices.is_empty() { + log::warn!("RB_I2C_HIDD_BLOCKER stage=scan error=no PNP0C50/ACPI0C50 devices found"); + } + + let mut workers = Vec::new(); + for device in devices { + log::info!("RB_I2C_HIDD_SNAPSHOT device={device}"); + workers.push(thread::spawn(move || { + if let Err(err) = bind_device(&device) { + log::error!("RB_I2C_HIDD_BLOCKER device={} error={:#}", device, err); + } + })); + } + + daemon.ready(); + + if workers.is_empty() { + loop { + thread::sleep(Duration::from_secs(5)); + } + } + + for worker in workers { + let _ = worker.join(); + } + Ok(()) +} + +pub fn bind_device(device_path: &str) -> Result<()> { + prepare_acpi_device(device_path) + .with_context(|| format!("failed to prepare ACPI device {device_path}"))?; + + let resources = read_decoded_resources(device_path) + .with_context(|| format!("failed to decode _CRS for {device_path}"))?; + log::info!( + "RB_I2C_HIDD_SNAPSHOT device={} adapter={} addr={:04x} irq={:?} gpio_int={} gpio_io={}", + device_path, + resources.i2c.adapter, + resources.i2c.address, + resources.irq, + resources.gpio_int.len(), + resources.gpio_io.len() + ); + + let hid_desc_addr = hid_descriptor_address(device_path) + .with_context(|| format!("failed to evaluate _DSM for {device_path}"))?; + let adapter = I2cAdapterClient::new(resources.i2c.clone()); + let hid_desc = fetch_hid_descriptor(&adapter, resources.i2c.address, hid_desc_addr) + .with_context(|| format!("failed to fetch HID descriptor for {device_path}"))?; + let report_desc = fetch_report_descriptor(&adapter, resources.i2c.address, &hid_desc) + .with_context(|| format!("failed to fetch report descriptor for {device_path}"))?; + let mut forwarder = InputForwarder::new().context("failed to connect to inputd producer")?; + + match stream_input_reports( + &adapter, + resources.i2c.address, + &hid_desc, + &report_desc, + &mut forwarder, + ) { + Ok(()) => Ok(()), + Err(err) => { + let quirk = + match_probe_failure_quirk().context("failed to evaluate DMI recovery quirks")?; + recover_acpi_device(device_path, &resources, quirk.as_ref()) + .with_context(|| format!("failed ACPI recovery for {device_path}"))?; + Err(err).with_context(|| format!("streaming input reports failed for {device_path}")) + } + } +} diff --git a/drivers/input/intel-thc-hidd/src/main.rs b/drivers/input/intel-thc-hidd/src/main.rs new file mode 100644 index 00000000..c5cda29e --- /dev/null +++ b/drivers/input/intel-thc-hidd/src/main.rs @@ -0,0 +1,282 @@ +use std::collections::BTreeSet; +use std::fs::{self, OpenOptions}; +use std::io::Read; +use std::process; +use std::thread; +use std::time::Duration; + +use acpi_resource::ResourceDescriptor; +use amlserde::{AmlSerde, AmlSerdeValue}; +use anyhow::{bail, Context, Result}; +use libredox::flag::{O_CLOEXEC, O_RDWR}; +use pcid_interface::PciFunctionHandle; + +mod quicki2c; +mod thc; + +use quicki2c::QuickI2cTransport; +use thc::{ThcController, SUPPORTED_PCI_IDS}; + +fn main() { + pcid_interface::pci_daemon(daemon); +} + +fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! { + common::setup_logging( + "input", + "intel-thc", + "intel-thc-hidd", + common::output_level(), + common::file_level(), + ); + + if let Err(err) = run(daemon, &mut pcid_handle) { + log::error!("RB_THC_HIDD_FATAL error={err:#}"); + process::exit(1); + } + + process::exit(0); +} + +fn run(daemon: daemon::Daemon, pcid_handle: &mut PciFunctionHandle) -> Result<()> { + log::info!("RB_THC_HIDD_SCHEMA version=1"); + + let pci_config = pcid_handle.config(); + let id = ( + pci_config.func.full_device_id.vendor_id, + pci_config.func.full_device_id.device_id, + ); + if !SUPPORTED_PCI_IDS.contains(&id) { + bail!("unsupported Intel THC PCI device {:04x}:{:04x}", id.0, id.1); + } + + pcid_handle.enable_device(); + let bar = unsafe { pcid_handle.map_bar(0) }; + let controller = ThcController::new(bar.ptr.as_ptr(), bar.bar_size) + .context("failed to create THC controller")?; + + let companion = resolve_acpi_companion(&pci_config.func.addr) + .context("failed to resolve ACPI companion for THC device")?; + let override_address = companion + .as_deref() + .map(companion_slave_address_override) + .transpose() + .context("failed to evaluate THC slave-address override")? + .flatten(); + let hid_devices = scan_bound_i2c_hid_devices(companion.as_deref()) + .context("failed to scan PNP0C50 devices for THC controller")?; + + let effective_address = override_address.unwrap_or(0x0015); + let transport = QuickI2cTransport::new(controller, effective_address); + transport.prime_controller(); + transport.emulate_transfer(&[]); + log::debug!("RB_THC_HIDD status={:#x}", transport.status()); + + match transport.register_with_i2cd(companion.as_deref(), override_address) { + Ok(()) => {} + Err(err) => { + log::warn!("RB_THC_HIDD registration error={err:#}"); + } + } + + log::info!( + "RB_THC_HIDD pci={} companion={:?} override={:?} hid_devices={}", + pci_config.func.name(), + companion, + override_address, + hid_devices.len() + ); + + daemon.ready(); + + loop { + thread::sleep(Duration::from_secs(5)); + } +} + +fn resolve_acpi_companion(addr: &pci_types::PciAddress) -> Result> { + let entries = match fs::read_dir("/scheme/acpi/symbols") { + Ok(entries) => entries, + Err(err) if err.kind() == std::io::ErrorKind::WouldBlock || err.raw_os_error() == Some(11) => { + log::debug!("intel-thc-hidd: ACPI symbols are not ready yet"); + return Ok(None); + } + // ESTALE (116): filesystem handle stale during initfs-to-rootfs transition + // ENOENT (2): /scheme/acpi not mounted yet (e.g., acpid not started) + Err(err) if err.raw_os_error() == Some(116) || err.kind() == std::io::ErrorKind::NotFound => { + log::info!("intel-thc-hidd: ACPI symbols unavailable ({}), skipping companion resolution", err); + return Ok(None); + } + Err(err) => return Err(err).context("failed to read /scheme/acpi/symbols"), + }; + let expected_adr = (u64::from(addr.device()) << 16) | u64::from(addr.function()); + + for entry in entries { + let entry = entry.context("failed to enumerate ACPI symbol entry")?; + let Some(file_name) = entry.file_name().to_str().map(str::to_owned) else { + continue; + }; + if !file_name.ends_with("._ADR") { + continue; + } + + let symbol = read_aml_symbol(&file_name)?; + if !matches!(symbol.value, AmlSerdeValue::Integer(value) if value == expected_adr) { + continue; + } + + let device = symbol + .name + .strip_suffix("._ADR") + .unwrap_or(&symbol.name) + .trim_start_matches('\\') + .replace('/', "."); + return Ok(Some(device)); + } + + Ok(None) +} + +fn companion_slave_address_override(path: &str) -> Result> { + let icrs = evaluate_integer_method(path, "ICRS").ok(); + let isub = evaluate_integer_method(path, "ISUB").ok(); + Ok(icrs + .or(isub) + .map(|value| u16::try_from(value)) + .transpose() + .context("THC ACPI override out of range")?) +} + +fn scan_bound_i2c_hid_devices(companion: Option<&str>) -> Result> { + let entries = match fs::read_dir("/scheme/acpi/symbols") { + Ok(entries) => entries, + Err(err) if err.kind() == std::io::ErrorKind::WouldBlock || err.raw_os_error() == Some(11) => { + log::debug!("intel-thc-hidd: ACPI symbols are not ready yet"); + return Ok(Vec::new()); + } + Err(err) if err.raw_os_error() == Some(116) || err.kind() == std::io::ErrorKind::NotFound => { + log::info!("intel-thc-hidd: ACPI symbols unavailable ({}), running with no HID devices", err); + return Ok(Vec::new()); + } + Err(err) => return Err(err).context("failed to read /scheme/acpi/symbols"), + }; + let mut devices = BTreeSet::new(); + + for entry in entries { + let entry = entry.context("failed to enumerate ACPI HID entry")?; + let Some(file_name) = entry.file_name().to_str().map(str::to_owned) else { + continue; + }; + if !file_name.ends_with("._HID") && !file_name.ends_with("._CID") { + continue; + } + + let symbol = read_aml_symbol(&file_name)?; + let is_hid = matches!( + decode_hardware_id(&symbol.value).as_deref(), + Some("PNP0C50" | "ACPI0C50") + ); + if !is_hid { + continue; + } + + let device = symbol + .name + .strip_suffix("._HID") + .or_else(|| symbol.name.strip_suffix("._CID")) + .unwrap_or(&symbol.name) + .trim_start_matches('\\') + .replace('/', "."); + if let Some(companion) = companion { + if !is_bound_to_companion(&device, companion)? { + continue; + } + } + devices.insert(device); + } + + Ok(devices.into_iter().collect()) +} + +fn is_bound_to_companion(device: &str, companion: &str) -> Result { + let resource_path = format!("/scheme/acpi/resources/{device}"); + let serialized = match fs::read_to_string(&resource_path) { + Ok(serialized) => serialized, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(false), + Err(err) => return Err(err).with_context(|| format!("failed to read {resource_path}")), + }; + + let resources: Vec = + ron::from_str(&serialized).with_context(|| format!("failed to decode {resource_path}"))?; + Ok(resources.into_iter().any(|resource| match resource { + ResourceDescriptor::I2cSerialBus(bus) => bus + .resource_source + .as_ref() + .map(|source| source.source == companion) + .unwrap_or(false), + _ => false, + })) +} + +fn evaluate_integer_method(path: &str, method: &str) -> Result { + let symbol_name = format!("{}.{}", normalize_device_path(path), method); + let symbol_path = format!("/scheme/acpi/symbols/{symbol_name}"); + let fd = libredox::Fd::open(&symbol_path, O_RDWR | O_CLOEXEC, 0) + .with_context(|| format!("failed to open {symbol_path}"))?; + + let mut payload = ron::to_string(&Vec::::new()) + .context("failed to serialize ACPI call arguments")? + .into_bytes(); + payload.resize(payload.len() + 2048, 0); + let used = libredox::call::call_ro(fd.raw(), &mut payload, syscall::CallFlags::empty(), &[]) + .with_context(|| format!("ACPI evaluation failed for {symbol_name}"))?; + let response = std::str::from_utf8(&payload[..used]) + .with_context(|| format!("invalid UTF-8 ACPI response for {symbol_name}"))?; + match ron::from_str::(response) + .with_context(|| format!("failed to decode ACPI response for {symbol_name}"))? + { + AmlSerdeValue::Integer(value) => Ok(value), + other => bail!("{}.{} returned non-integer value {other:?}", path, method), + } +} + +fn read_aml_symbol(file_name: &str) -> Result { + let path = format!("/scheme/acpi/symbols/{file_name}"); + let mut file = OpenOptions::new() + .read(true) + .open(&path) + .with_context(|| format!("failed to open {path}"))?; + let mut ron_text = String::new(); + file.read_to_string(&mut ron_text) + .with_context(|| format!("failed to read {path}"))?; + ron::from_str(&ron_text).with_context(|| format!("failed to decode {path}")) +} + +fn decode_hardware_id(value: &AmlSerdeValue) -> Option { + match value { + AmlSerdeValue::String(value) => Some(value.clone()), + AmlSerdeValue::Integer(integer) => { + let vendor = integer & 0xFFFF; + let device = (integer >> 16) & 0xFFFF; + let vendor_rev = ((vendor & 0xFF) << 8) | (vendor >> 8); + let vendor_1 = (((vendor_rev >> 10) & 0x1f) as u8 + 64) as char; + let vendor_2 = (((vendor_rev >> 5) & 0x1f) as u8 + 64) as char; + let vendor_3 = (((vendor_rev >> 0) & 0x1f) as u8 + 64) as char; + let device_1 = (device >> 4) & 0xF; + let device_2 = (device >> 0) & 0xF; + let device_3 = (device >> 12) & 0xF; + let device_4 = (device >> 8) & 0xF; + Some(format!( + "{}{}{}{:01X}{:01X}{:01X}{:01X}", + vendor_1, vendor_2, vendor_3, device_1, device_2, device_3, device_4 + )) + } + _ => None, + } +} + +fn normalize_device_path(path: &str) -> String { + path.trim_start_matches('\\') + .trim_matches('/') + .replace('/', ".") +}