6ac41ee37a
daemon/src/lib.rs: Daemon::ready() previously called .unwrap() on the
init pipe write, causing a panic with BrokenPipe when init had already
closed its read end during the startup phase. Daemons like i2c-gpio-expanderd,
intel-gpiod, dw-acpi-i2cd, and i2c-hidd hit this in redbear-mini boots.
Now BrokenPipe is silently ignored — the daemon is operational regardless
of init's readiness tracking state.
drivers/usb/ucsid/src/main.rs and drivers/gpio/i2c-gpio-expanderd/src/main.rs:
read_i2c_control_response() returned an empty buffer (no I2C adapters
registered) and then tried ron::from_str('') which failed at 1:1 with
'Unexpected end of RON'. This produced false-positive warnings on every
boot where no I2C hardware is present. Now an empty/whitespace response
returns AdapterList(Vec::new()) gracefully.
840 lines
26 KiB
Rust
840 lines
26 KiB
Rust
use std::collections::BTreeMap;
|
|
use std::fs::{self, File, OpenOptions};
|
|
use std::io::{Read, Write};
|
|
use std::path::Path;
|
|
use std::process;
|
|
|
|
use acpi_resource::{
|
|
AddressResourceType, FixedMemory32Descriptor, I2cSerialBusDescriptor, Memory32RangeDescriptor,
|
|
ResourceDescriptor,
|
|
};
|
|
use anyhow::{bail, Context, Result};
|
|
use i2c_interface::{
|
|
I2cAdapterInfo, I2cControlRequest, I2cControlResponse, I2cTransferRequest,
|
|
I2cTransferSegment,
|
|
};
|
|
use libredox::flag::{O_CLOEXEC, O_RDWR};
|
|
use redox_scheme::scheme::SchemeSync;
|
|
use redox_scheme::{CallerCtx, OpenResult, Socket};
|
|
use scheme_utils::{Blocking, HandleMap};
|
|
use serde::{Deserialize, Serialize};
|
|
use syscall::schemev2::NewFdFlags;
|
|
use syscall::{Error as SysError, EACCES, EBADF, EINVAL, ENOENT};
|
|
|
|
const SUPPORTED_IDS: &[&str] = &["PNP0CA0", "AMDI0042"];
|
|
const GET_CAPABILITY: u8 = 0x01;
|
|
const GET_CONNECTOR_STATUS: u8 = 0x10;
|
|
const UCSI_RESPONSE_HEADER_LEN: usize = 4;
|
|
const UCSI_CAPABILITY_READ_LEN: usize = 20;
|
|
const UCSI_CONNECTOR_STATUS_READ_LEN: usize = 20;
|
|
const MAX_CONNECTOR_PROBE: u8 = 8;
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct AmlSymbol {
|
|
name: String,
|
|
value: AmlValue,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
enum AmlValue {
|
|
Integer(u64),
|
|
String(String),
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
struct UcsiCommand {
|
|
command: u8,
|
|
data_length: u8,
|
|
specific_data: [u8; 6],
|
|
}
|
|
|
|
impl UcsiCommand {
|
|
fn new(command: u8, data_length: u8, specific_data: [u8; 6]) -> Self {
|
|
Self {
|
|
command,
|
|
data_length,
|
|
specific_data,
|
|
}
|
|
}
|
|
|
|
fn as_bytes(self) -> [u8; 8] {
|
|
let mut bytes = [0_u8; 8];
|
|
bytes[0] = self.command;
|
|
bytes[1] = self.data_length;
|
|
bytes[2..].copy_from_slice(&self.specific_data);
|
|
bytes
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
struct UcsiResponseHeader {
|
|
_status: u16,
|
|
data_length: u16,
|
|
}
|
|
|
|
impl UcsiResponseHeader {
|
|
fn parse(bytes: &[u8]) -> Option<Self> {
|
|
let header = bytes.get(..UCSI_RESPONSE_HEADER_LEN)?;
|
|
Some(Self {
|
|
_status: u16::from_le_bytes([header[0], header[1]]),
|
|
data_length: u16::from_le_bytes([header[2], header[3]]),
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct DiscoveredUcsiDevice {
|
|
name: String,
|
|
hid: String,
|
|
transport: UcsiTransport,
|
|
dsm_probe: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
enum UcsiTransport {
|
|
I2c {
|
|
adapter: String,
|
|
address: u16,
|
|
ten_bit_address: bool,
|
|
},
|
|
Mmio {
|
|
base: usize,
|
|
len: usize,
|
|
},
|
|
Unknown,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
struct UcsiCapability {
|
|
connector_count: u8,
|
|
supports_usb_pd: bool,
|
|
supports_alt_modes: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
struct UcsiConnectorSummary {
|
|
device: String,
|
|
connector_number: u8,
|
|
connected: bool,
|
|
data_role: String,
|
|
power_direction: String,
|
|
input_critical: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
struct UcsiDeviceSummary {
|
|
name: String,
|
|
hid: String,
|
|
transport: UcsiTransport,
|
|
capability: Option<UcsiCapability>,
|
|
connectors: Vec<UcsiConnectorSummary>,
|
|
dsm_probe: bool,
|
|
issues: Vec<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
struct UcsiSummary {
|
|
schema_version: u32,
|
|
device_count: usize,
|
|
connector_count: usize,
|
|
input_critical_ports: usize,
|
|
devices: Vec<UcsiDeviceSummary>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
struct UcsiHealth {
|
|
healthy: bool,
|
|
scanned_devices: usize,
|
|
responsive_devices: usize,
|
|
issues: Vec<String>,
|
|
}
|
|
|
|
struct UcsiState {
|
|
summary: UcsiSummary,
|
|
connectors: Vec<UcsiConnectorSummary>,
|
|
health: UcsiHealth,
|
|
}
|
|
|
|
enum Handle {
|
|
SchemeRoot,
|
|
Summary { pending: Vec<u8> },
|
|
Connectors { pending: Vec<u8> },
|
|
Health { pending: Vec<u8> },
|
|
}
|
|
|
|
struct UcsiScheme {
|
|
handles: HandleMap<Handle>,
|
|
state: UcsiState,
|
|
}
|
|
|
|
impl UcsiScheme {
|
|
fn new(state: UcsiState) -> Self {
|
|
Self {
|
|
handles: HandleMap::new(),
|
|
state,
|
|
}
|
|
}
|
|
|
|
fn serialize_payload<T: Serialize>(value: &T) -> syscall::Result<Vec<u8>> {
|
|
ron::ser::to_string(value)
|
|
.map(|text| text.into_bytes())
|
|
.map_err(|err| {
|
|
log::error!("ucsid: failed to serialize scheme payload: {err}");
|
|
SysError::new(EINVAL)
|
|
})
|
|
}
|
|
|
|
fn set_pending(handle: &mut Handle, pending: Vec<u8>) -> syscall::Result<()> {
|
|
match handle {
|
|
Handle::Summary { pending: slot }
|
|
| Handle::Connectors { pending: slot }
|
|
| Handle::Health { pending: slot } => {
|
|
*slot = pending;
|
|
Ok(())
|
|
}
|
|
Handle::SchemeRoot => Err(SysError::new(EBADF)),
|
|
}
|
|
}
|
|
|
|
fn copy_pending(handle: &mut Handle, buf: &mut [u8], offset: u64) -> syscall::Result<usize> {
|
|
let pending = match handle {
|
|
Handle::Summary { pending }
|
|
| Handle::Connectors { pending }
|
|
| Handle::Health { pending } => pending,
|
|
Handle::SchemeRoot => return Err(SysError::new(EBADF)),
|
|
};
|
|
|
|
let offset = usize::try_from(offset).map_err(|_| SysError::new(EINVAL))?;
|
|
if offset >= pending.len() {
|
|
return Ok(0);
|
|
}
|
|
|
|
let copy_len = buf.len().min(pending.len() - offset);
|
|
buf[..copy_len].copy_from_slice(&pending[offset..offset + copy_len]);
|
|
Ok(copy_len)
|
|
}
|
|
}
|
|
|
|
impl SchemeSync for UcsiScheme {
|
|
fn scheme_root(&mut self) -> syscall::Result<usize> {
|
|
Ok(self.handles.insert(Handle::SchemeRoot))
|
|
}
|
|
|
|
fn openat(
|
|
&mut self,
|
|
dirfd: usize,
|
|
path: &str,
|
|
_flags: usize,
|
|
_fcntl_flags: u32,
|
|
_ctx: &CallerCtx,
|
|
) -> syscall::Result<OpenResult> {
|
|
if !matches!(self.handles.get(dirfd)?, Handle::SchemeRoot) {
|
|
return Err(SysError::new(EACCES));
|
|
}
|
|
|
|
let handle = match path.trim_matches('/') {
|
|
"summary" => Handle::Summary {
|
|
pending: Vec::new(),
|
|
},
|
|
"connectors" => Handle::Connectors {
|
|
pending: Vec::new(),
|
|
},
|
|
"health" => Handle::Health {
|
|
pending: Vec::new(),
|
|
},
|
|
"" => return Err(SysError::new(EINVAL)),
|
|
_ => return Err(SysError::new(ENOENT)),
|
|
};
|
|
|
|
let fd = self.handles.insert(handle);
|
|
Ok(OpenResult::ThisScheme {
|
|
number: fd,
|
|
flags: NewFdFlags::empty(),
|
|
})
|
|
}
|
|
|
|
fn read(
|
|
&mut self,
|
|
id: usize,
|
|
buf: &mut [u8],
|
|
offset: u64,
|
|
_fcntl_flags: u32,
|
|
_ctx: &CallerCtx,
|
|
) -> syscall::Result<usize> {
|
|
let payload = match self.handles.get(id)? {
|
|
Handle::Summary { pending } if pending.is_empty() => {
|
|
Some(Self::serialize_payload(&self.state.summary)?)
|
|
}
|
|
Handle::Connectors { pending } if pending.is_empty() => {
|
|
Some(Self::serialize_payload(&self.state.connectors)?)
|
|
}
|
|
Handle::Health { pending } if pending.is_empty() => {
|
|
log::info!(
|
|
"RB_UCSID_HEALTH healthy={} scanned_devices={} responsive_devices={} issues={}",
|
|
self.state.health.healthy,
|
|
self.state.health.scanned_devices,
|
|
self.state.health.responsive_devices,
|
|
self.state.health.issues.len(),
|
|
);
|
|
Some(Self::serialize_payload(&self.state.health)?)
|
|
}
|
|
_ => None,
|
|
};
|
|
|
|
let handle = self.handles.get_mut(id)?;
|
|
if let Some(payload) = payload {
|
|
Self::set_pending(handle, payload)?;
|
|
}
|
|
Self::copy_pending(handle, buf, offset)
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
common::setup_logging(
|
|
"usb",
|
|
"ucsi",
|
|
"ucsid",
|
|
common::output_level(),
|
|
common::file_level(),
|
|
);
|
|
|
|
daemon::SchemeDaemon::new(daemon_runner);
|
|
}
|
|
|
|
fn daemon_runner(daemon: daemon::SchemeDaemon) -> ! {
|
|
if let Err(err) = run_daemon(daemon) {
|
|
log::error!("ucsid: {err:#}");
|
|
process::exit(1);
|
|
}
|
|
|
|
process::exit(0);
|
|
}
|
|
|
|
fn run_daemon(daemon: daemon::SchemeDaemon) -> Result<()> {
|
|
log::info!("RB_UCSID_SCHEMA version=1");
|
|
|
|
let state = build_state().context("failed to build UCSI device snapshot")?;
|
|
let socket = Socket::create().context("failed to create ucsi scheme socket")?;
|
|
let mut scheme = UcsiScheme::new(state);
|
|
let handler = Blocking::new(&socket, 16);
|
|
|
|
daemon
|
|
.ready_sync_scheme(&socket, &mut scheme)
|
|
.context("failed to publish ucsi scheme root")?;
|
|
|
|
libredox::call::setrens(0, 0).context("failed to enter null namespace")?;
|
|
|
|
handler
|
|
.process_requests_blocking(scheme)
|
|
.context("failed to process ucsid requests")?;
|
|
}
|
|
|
|
fn build_state() -> Result<UcsiState> {
|
|
let adapters = list_i2c_adapters().unwrap_or_else(|err| {
|
|
log::warn!("ucsid: failed to query i2cd adapters: {err:#}");
|
|
Vec::new()
|
|
});
|
|
let devices = discover_ucsi_devices().context("failed to discover ACPI UCSI devices")?;
|
|
|
|
let mut summaries = Vec::new();
|
|
let mut connectors = Vec::new();
|
|
let mut issues = Vec::new();
|
|
let mut responsive_devices = 0usize;
|
|
|
|
for device in devices {
|
|
log::info!(
|
|
"RB_UCSID_DEVICE name={} hid={} transport={:?} dsm_probe={}",
|
|
device.name,
|
|
device.hid,
|
|
device.transport,
|
|
device.dsm_probe,
|
|
);
|
|
let summary = summarize_device(device, &adapters)
|
|
.context("failed to summarize discovered UCSI device")?;
|
|
if summary.capability.is_some() {
|
|
responsive_devices += 1;
|
|
}
|
|
issues.extend(summary.issues.iter().cloned());
|
|
connectors.extend(summary.connectors.iter().cloned());
|
|
summaries.push(summary);
|
|
}
|
|
|
|
let summary = UcsiSummary {
|
|
schema_version: 1,
|
|
device_count: summaries.len(),
|
|
connector_count: connectors.len(),
|
|
input_critical_ports: connectors.iter().filter(|connector| connector.input_critical).count(),
|
|
devices: summaries,
|
|
};
|
|
let health = UcsiHealth {
|
|
healthy: issues.is_empty(),
|
|
scanned_devices: summary.device_count,
|
|
responsive_devices,
|
|
issues,
|
|
};
|
|
|
|
log::info!(
|
|
"RB_UCSID_SUMMARY devices={} connectors={} input_critical_ports={} healthy={}",
|
|
summary.device_count,
|
|
summary.connector_count,
|
|
summary.input_critical_ports,
|
|
health.healthy,
|
|
);
|
|
|
|
Ok(UcsiState {
|
|
summary,
|
|
connectors,
|
|
health,
|
|
})
|
|
}
|
|
|
|
fn discover_ucsi_devices() -> Result<Vec<DiscoveredUcsiDevice>> {
|
|
let mut matched = BTreeMap::new();
|
|
|
|
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!("ucsid: ACPI symbols are not ready yet");
|
|
return Ok(Vec::new());
|
|
}
|
|
Err(err) => return Err(err).context("failed to read /scheme/acpi/symbols"),
|
|
};
|
|
|
|
for entry in entries {
|
|
let entry = entry.context("failed to read ACPI symbol directory 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 Some(id) = read_symbol_id(&entry.path())? else {
|
|
continue;
|
|
};
|
|
if !SUPPORTED_IDS.iter().any(|candidate| *candidate == id) {
|
|
continue;
|
|
}
|
|
|
|
let Some(device) = file_name
|
|
.strip_suffix("_HID")
|
|
.or_else(|| file_name.strip_suffix("_CID"))
|
|
.map(str::to_owned)
|
|
else {
|
|
continue;
|
|
};
|
|
matched.entry(device).or_insert(id);
|
|
}
|
|
|
|
let mut devices = Vec::new();
|
|
for (device, hid) in matched {
|
|
let transport = read_ucsi_transport(&device)
|
|
.with_context(|| format!("failed to decode transport resources for {device}"))?;
|
|
let dsm_probe = bounded_dsm_probe(&device).unwrap_or_else(|err| {
|
|
log::debug!("ucsid: bounded _DSM probe failed for {device}: {err:#}");
|
|
false
|
|
});
|
|
devices.push(DiscoveredUcsiDevice {
|
|
name: device,
|
|
hid,
|
|
transport,
|
|
dsm_probe,
|
|
});
|
|
}
|
|
|
|
Ok(devices)
|
|
}
|
|
|
|
fn summarize_device(device: DiscoveredUcsiDevice, adapters: &[I2cAdapterInfo]) -> Result<UcsiDeviceSummary> {
|
|
let mut issues = Vec::new();
|
|
let capability = match &device.transport {
|
|
UcsiTransport::I2c {
|
|
adapter,
|
|
address,
|
|
ten_bit_address,
|
|
} => match match_i2c_adapter(adapters, adapter) {
|
|
Some(adapter_info) => match execute_ucsi_i2c_command(
|
|
adapter_info,
|
|
adapter,
|
|
*address,
|
|
*ten_bit_address,
|
|
UcsiCommand::new(GET_CAPABILITY, 0, [0; 6]),
|
|
UCSI_CAPABILITY_READ_LEN,
|
|
) {
|
|
Ok(bytes) => parse_ucsi_payload(&bytes)
|
|
.and_then(|(_header, payload)| parse_capability(payload))
|
|
.or_else(|| {
|
|
issues.push(format!(
|
|
"{}: GET_CAPABILITY returned an unexpected payload",
|
|
device.name
|
|
));
|
|
None
|
|
}),
|
|
Err(err) => {
|
|
issues.push(format!("{}: GET_CAPABILITY failed: {err:#}", device.name));
|
|
None
|
|
}
|
|
},
|
|
None => {
|
|
issues.push(format!(
|
|
"{}: no i2cd adapter matched ACPI source {}",
|
|
device.name, adapter
|
|
));
|
|
None
|
|
}
|
|
},
|
|
UcsiTransport::Mmio { base, len } => {
|
|
issues.push(format!(
|
|
"{}: MMIO UCSI transport discovered at {base:#x}+{len:#x} but command execution is not implemented yet",
|
|
device.name,
|
|
));
|
|
None
|
|
}
|
|
UcsiTransport::Unknown => {
|
|
issues.push(format!(
|
|
"{}: no supported UCSI transport was decoded from ACPI resources",
|
|
device.name,
|
|
));
|
|
None
|
|
}
|
|
};
|
|
|
|
let connector_count = capability
|
|
.as_ref()
|
|
.map(|capability| capability.connector_count.min(MAX_CONNECTOR_PROBE))
|
|
.unwrap_or(0);
|
|
let mut connectors = Vec::new();
|
|
for connector in 1..=connector_count {
|
|
match query_connector_status(&device, adapters, connector) {
|
|
Ok(connector_summary) => connectors.push(connector_summary),
|
|
Err(err) => issues.push(format!(
|
|
"{}: GET_CONNECTOR_STATUS({connector}) failed: {err:#}",
|
|
device.name,
|
|
)),
|
|
}
|
|
}
|
|
|
|
Ok(UcsiDeviceSummary {
|
|
name: device.name,
|
|
hid: device.hid,
|
|
transport: device.transport,
|
|
capability,
|
|
connectors,
|
|
dsm_probe: device.dsm_probe,
|
|
issues,
|
|
})
|
|
}
|
|
|
|
fn read_ucsi_transport(device: &str) -> Result<UcsiTransport> {
|
|
let contents = fs::read_to_string(format!("/scheme/acpi/resources/{device}"))
|
|
.with_context(|| format!("failed to read /scheme/acpi/resources/{device}"))?;
|
|
let resources = ron::from_str::<Vec<ResourceDescriptor>>(&contents)
|
|
.with_context(|| format!("failed to decode RON resources for {device}"))?;
|
|
|
|
let mut i2c = None::<I2cSerialBusDescriptor>;
|
|
let mut mmio = None::<(usize, usize)>;
|
|
|
|
for resource in resources {
|
|
match resource {
|
|
ResourceDescriptor::I2cSerialBus(bus) if i2c.is_none() => i2c = Some(bus),
|
|
ResourceDescriptor::FixedMemory32(FixedMemory32Descriptor {
|
|
address,
|
|
address_length,
|
|
..
|
|
}) if mmio.is_none() => {
|
|
mmio = Some((address as usize, address_length as usize));
|
|
}
|
|
ResourceDescriptor::Memory32Range(Memory32RangeDescriptor {
|
|
minimum,
|
|
maximum,
|
|
address_length,
|
|
..
|
|
}) if mmio.is_none() && maximum >= minimum => {
|
|
let span = maximum.saturating_sub(minimum).saturating_add(1) as usize;
|
|
mmio = Some((minimum as usize, span.max(address_length as usize)));
|
|
}
|
|
ResourceDescriptor::Address32(descriptor)
|
|
if mmio.is_none()
|
|
&& matches!(descriptor.resource_type, AddressResourceType::MemoryRange) =>
|
|
{
|
|
mmio = Some((descriptor.minimum as usize, descriptor.address_length as usize));
|
|
}
|
|
ResourceDescriptor::Address64(descriptor)
|
|
if mmio.is_none()
|
|
&& matches!(descriptor.resource_type, AddressResourceType::MemoryRange) =>
|
|
{
|
|
let base = usize::try_from(descriptor.minimum)
|
|
.context("64-bit MMIO base does not fit in usize")?;
|
|
let len = usize::try_from(descriptor.address_length)
|
|
.context("64-bit MMIO length does not fit in usize")?;
|
|
mmio = Some((base, len));
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
if let Some(bus) = i2c {
|
|
let adapter = bus
|
|
.resource_source
|
|
.as_ref()
|
|
.map(|source| source.source.clone())
|
|
.filter(|source| !source.is_empty())
|
|
.unwrap_or_else(|| String::from("ACPI-I2C"));
|
|
return Ok(UcsiTransport::I2c {
|
|
adapter,
|
|
address: bus.slave_address,
|
|
ten_bit_address: bus.access_mode_10bit,
|
|
});
|
|
}
|
|
if let Some((base, len)) = mmio {
|
|
return Ok(UcsiTransport::Mmio { base, len });
|
|
}
|
|
Ok(UcsiTransport::Unknown)
|
|
}
|
|
|
|
fn bounded_dsm_probe(device: &str) -> Result<bool> {
|
|
let symbol_name = format!("{}.{}", normalize_device_path(device), "_DSM");
|
|
let symbol_path = format!("/scheme/acpi/symbols/{symbol_name}");
|
|
let fd = match libredox::Fd::open(&symbol_path, O_RDWR | O_CLOEXEC, 0) {
|
|
Ok(fd) => fd,
|
|
Err(err) => {
|
|
log::debug!("ucsid: {} has no callable _DSM: {err}", device);
|
|
return Ok(false);
|
|
}
|
|
};
|
|
|
|
let mut payload = ron::to_string(&Vec::<u8>::new())
|
|
.context("failed to serialize bounded _DSM probe arguments")?
|
|
.into_bytes();
|
|
payload.resize(payload.len() + 1024, 0);
|
|
match libredox::call::call_ro(fd.raw(), &mut payload, syscall::CallFlags::empty(), &[]) {
|
|
Ok(_) => Ok(true),
|
|
Err(err) => {
|
|
log::debug!("ucsid: bounded _DSM probe for {} failed: {err}", device);
|
|
Ok(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn parse_capability(payload: &[u8]) -> Option<UcsiCapability> {
|
|
let connector_count = *payload.first()?;
|
|
let flags = payload.get(1).copied().unwrap_or(0);
|
|
Some(UcsiCapability {
|
|
connector_count,
|
|
supports_usb_pd: flags & 0x01 != 0,
|
|
supports_alt_modes: flags & 0x02 != 0,
|
|
})
|
|
}
|
|
|
|
fn query_connector_status(
|
|
device: &DiscoveredUcsiDevice,
|
|
adapters: &[I2cAdapterInfo],
|
|
connector: u8,
|
|
) -> Result<UcsiConnectorSummary> {
|
|
match &device.transport {
|
|
UcsiTransport::I2c {
|
|
adapter,
|
|
address,
|
|
ten_bit_address,
|
|
} => {
|
|
let adapter_info = match_i2c_adapter(adapters, adapter).with_context(|| {
|
|
format!("no i2cd adapter matched ACPI source {} for {}", adapter, device.name)
|
|
})?;
|
|
let bytes = execute_ucsi_i2c_command(
|
|
adapter_info,
|
|
adapter,
|
|
*address,
|
|
*ten_bit_address,
|
|
UcsiCommand::new(GET_CONNECTOR_STATUS, 1, [connector, 0, 0, 0, 0, 0]),
|
|
UCSI_CONNECTOR_STATUS_READ_LEN,
|
|
)?;
|
|
let (_header, payload) = parse_ucsi_payload(&bytes)
|
|
.with_context(|| format!("{}: malformed connector-status response", device.name))?;
|
|
Ok(parse_connector_summary(&device.name, connector, payload))
|
|
}
|
|
UcsiTransport::Mmio { base, len } => bail!(
|
|
"MMIO connector-status transport is not implemented yet for {:#x}+{:#x}",
|
|
base,
|
|
len,
|
|
),
|
|
UcsiTransport::Unknown => bail!("unknown UCSI transport"),
|
|
}
|
|
}
|
|
|
|
fn parse_connector_summary(device_name: &str, connector: u8, payload: &[u8]) -> UcsiConnectorSummary {
|
|
let state = payload.first().copied().unwrap_or(0);
|
|
let connected = state & 0x01 != 0;
|
|
let power_direction = if state & 0x02 != 0 { "source" } else { "sink" };
|
|
let data_role = if state & 0x04 != 0 { "dfp" } else { "ufp" };
|
|
UcsiConnectorSummary {
|
|
device: device_name.to_string(),
|
|
connector_number: connector,
|
|
connected,
|
|
data_role: data_role.to_string(),
|
|
power_direction: power_direction.to_string(),
|
|
input_critical: classify_input_critical(device_name),
|
|
}
|
|
}
|
|
|
|
fn classify_input_critical(device_name: &str) -> bool {
|
|
let normalized = device_name.to_ascii_lowercase();
|
|
normalized.contains("kbd")
|
|
|| normalized.contains("key")
|
|
|| normalized.contains("touch")
|
|
|| normalized.contains("thc")
|
|
}
|
|
|
|
fn parse_ucsi_payload(bytes: &[u8]) -> Option<(UcsiResponseHeader, &[u8])> {
|
|
let header = UcsiResponseHeader::parse(bytes)?;
|
|
let body = bytes.get(UCSI_RESPONSE_HEADER_LEN..)?;
|
|
let body_len = usize::from(header.data_length).min(body.len());
|
|
Some((header, &body[..body_len]))
|
|
}
|
|
|
|
fn execute_ucsi_i2c_command(
|
|
adapter: &I2cAdapterInfo,
|
|
adapter_name: &str,
|
|
address: u16,
|
|
ten_bit_address: bool,
|
|
command: UcsiCommand,
|
|
read_len: usize,
|
|
) -> Result<Vec<u8>> {
|
|
let request = I2cTransferRequest {
|
|
adapter: adapter_name.to_string(),
|
|
segments: vec![
|
|
I2cTransferSegment {
|
|
address,
|
|
ten_bit_address,
|
|
op: i2c_interface::I2cTransferOp::Write(command.as_bytes().to_vec()),
|
|
},
|
|
I2cTransferSegment {
|
|
address,
|
|
ten_bit_address,
|
|
op: i2c_interface::I2cTransferOp::Read(read_len),
|
|
},
|
|
],
|
|
stop: true,
|
|
};
|
|
|
|
let mut file = OpenOptions::new()
|
|
.read(true)
|
|
.write(true)
|
|
.open("/scheme/i2c/transfer")
|
|
.context("failed to open /scheme/i2c/transfer")?;
|
|
let payload = ron::ser::to_string(&I2cControlRequest::Transfer {
|
|
adapter_id: adapter.id,
|
|
request,
|
|
})
|
|
.context("failed to encode UCSI I2C transfer request")?;
|
|
file.write_all(payload.as_bytes())
|
|
.context("failed to send UCSI I2C transfer request")?;
|
|
|
|
let response = read_i2c_control_response(&mut file)?;
|
|
match response {
|
|
I2cControlResponse::TransferResult(result) => {
|
|
if !result.ok {
|
|
let detail = result
|
|
.error
|
|
.clone()
|
|
.unwrap_or_else(|| String::from("unknown I2C transfer failure"));
|
|
bail!("UCSI I2C transfer failed: {detail}");
|
|
}
|
|
result
|
|
.read_data
|
|
.into_iter()
|
|
.next()
|
|
.context("UCSI I2C transfer returned no response payload")
|
|
}
|
|
I2cControlResponse::Error(message) => bail!("i2cd returned an error: {message}"),
|
|
other => bail!("unexpected i2cd transfer response: {other:?}"),
|
|
}
|
|
}
|
|
|
|
fn list_i2c_adapters() -> Result<Vec<I2cAdapterInfo>> {
|
|
let mut file = OpenOptions::new()
|
|
.read(true)
|
|
.write(true)
|
|
.open("/scheme/i2c/adapters")
|
|
.context("failed to open /scheme/i2c/adapters")?;
|
|
|
|
let payload = ron::ser::to_string(&I2cControlRequest::ListAdapters)
|
|
.context("failed to encode I2C list-adapters request")?;
|
|
file.write_all(payload.as_bytes())
|
|
.context("failed to request I2C adapter list")?;
|
|
|
|
let response = read_i2c_control_response(&mut file)?;
|
|
match response {
|
|
I2cControlResponse::AdapterList(adapters) => Ok(adapters),
|
|
I2cControlResponse::Error(message) => bail!("i2cd returned an error: {message}"),
|
|
other => bail!("unexpected i2cd list-adapters response: {other:?}"),
|
|
}
|
|
}
|
|
|
|
fn match_i2c_adapter<'a>(adapters: &'a [I2cAdapterInfo], wanted: &str) -> Option<&'a I2cAdapterInfo> {
|
|
adapters
|
|
.iter()
|
|
.find(|adapter| adapter.name == wanted)
|
|
.or_else(|| adapters.iter().find(|adapter| adapter.name.ends_with(wanted)))
|
|
.or_else(|| adapters.iter().find(|adapter| wanted.ends_with(&adapter.name)))
|
|
}
|
|
|
|
fn read_i2c_control_response(file: &mut File) -> Result<I2cControlResponse> {
|
|
let mut buffer = vec![0_u8; 4096];
|
|
let count = file
|
|
.read(&mut buffer)
|
|
.context("failed to read I2C control response")?;
|
|
buffer.truncate(count);
|
|
let text = std::str::from_utf8(&buffer).context("I2C control response was not UTF-8")?;
|
|
let trimmed = text.trim();
|
|
if trimmed.is_empty() {
|
|
return Ok(I2cControlResponse::AdapterList(Vec::new()));
|
|
}
|
|
ron::from_str(trimmed).context("failed to decode I2C control response")
|
|
}
|
|
|
|
fn read_symbol_id(path: &Path) -> Result<Option<String>> {
|
|
let contents = fs::read_to_string(path)
|
|
.with_context(|| format!("failed to read ACPI symbol {}", path.display()))?;
|
|
let symbol = match ron::from_str::<AmlSymbol>(&contents) {
|
|
Ok(symbol) => symbol,
|
|
Err(err) => {
|
|
log::debug!(
|
|
"ucsid: skipping {} because the symbol payload was not a scalar ID: {err}",
|
|
path.display(),
|
|
);
|
|
return Ok(None);
|
|
}
|
|
};
|
|
|
|
let id = match symbol.value {
|
|
AmlValue::Integer(integer) => eisa_id_from_integer(integer),
|
|
AmlValue::String(string) => string,
|
|
};
|
|
|
|
log::debug!("ucsid: {} -> {id}", symbol.name);
|
|
Ok(Some(id))
|
|
}
|
|
|
|
fn normalize_device_path(path: &str) -> String {
|
|
path.trim_start_matches('\\')
|
|
.trim_matches('/')
|
|
.replace('/', ".")
|
|
}
|
|
|
|
fn eisa_id_from_integer(integer: u64) -> String {
|
|
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;
|
|
|
|
format!(
|
|
"{vendor_1}{vendor_2}{vendor_3}{device_1:01X}{device_2:01X}{device_3:01X}{device_4:01X}"
|
|
)
|
|
}
|