diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 143fa14..bb417bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,7 @@ jobs: pkg install -y rust hidapi pkgconf run: | cargo build -p framework_lib + cargo build -p framework_dotnet_ffi cargo build -p framework_tool build: @@ -43,6 +44,17 @@ jobs: - name: Build library (Linux) run: cargo build -p framework_lib + - name: Build .NET FFI (Linux Release) + run: cargo build -p framework_dotnet_ffi --release + + - name: Upload Linux .NET FFI + uses: actions/upload-artifact@v6 + with: + name: framework_dotnet_ffi_linux + path: | + target/release/libframework_dotnet_ffi.so + framework_dotnet_ffi/csharp/NativeMethods.g.cs + - name: Build Linux tool run: cargo build -p framework_tool @@ -140,6 +152,19 @@ jobs: - name: Build library (Windows) run: cargo build -p framework_lib + - name: Build .NET FFI (Windows Release) + run: cargo build -p framework_dotnet_ffi --release + + - name: Upload Windows .NET FFI + uses: actions/upload-artifact@v6 + with: + name: framework_dotnet_ffi_windows + path: | + target/release/framework_dotnet_ffi.dll + target/release/framework_dotnet_ffi.dll.lib + target/release/framework_dotnet_ffi.pdb + framework_dotnet_ffi/csharp/NativeMethods.g.cs + - name: Build Windows tool run: | cargo build -p framework_tool diff --git a/.gitignore b/.gitignore index 4a4e507..9fa196a 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,5 @@ dump.bmp OVMF.fd OVMF_CODE.fd OVMF_VARS.fd + +*.g.cs diff --git a/Cargo.lock b/Cargo.lock index 8edd60b..1846122 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -248,6 +248,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "csbindgen" +version = "1.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710604f525e7b68070458083252602c2bf2afe255dfe019e83bd57bdfa50bdb4" +dependencies = [ + "regex", + "syn", +] + [[package]] name = "darling" version = "0.20.11" @@ -378,6 +388,14 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "framework_dotnet_ffi" +version = "0.6.2" +dependencies = [ + "csbindgen", + "framework_lib", +] + [[package]] name = "framework_lib" version = "0.6.2" diff --git a/Cargo.toml b/Cargo.toml index 66801a4..4ca8d27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,8 @@ resolver = "2" members = [ + # Native FFI surface for .NET interop + "framework_dotnet_ffi", # Linux and Windows tool to inspect and customize the system "framework_tool", # UEFI tool to inspect and customize the system diff --git a/README.md b/README.md index 0fdff78..5094f99 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,10 @@ Rust libraries and tools to interact with the system. The tool works on Linux, Windows and the UEFI shell. Most features are supported on every "OS". +The workspace also contains an optional .NET interop crate, `framework_dotnet_ffi`, +which exposes a native FFI surface over `framework_lib` and generates C# P/Invoke +bindings via `csbindgen`. + You can find lots of examples in [EXAMPLES.md](./EXAMPLES.md). ## Installation @@ -260,12 +264,59 @@ cargo build -p framework_lib cargo build -p framework_tool ls -l target/debug/framework_tool +# Building the optional .NET interop crate +cargo build -p framework_dotnet_ffi + # Build the UEFI application # Can't be built with cargo! That's why we need to exclude it in the other commands. make -C framework_uefi ls -l framework_uefi/build/x86_64-unknown-uefi/boot.efi ``` +### Optional .NET Interop + +The `framework_dotnet_ffi` crate is an optional workspace member and is not part of +the default `cargo build` / `cargo check` set. + +Build it explicitly when you want to consume Framework functionality from .NET: + +```sh +cargo build -p framework_dotnet_ffi +``` + +Building the crate also regenerates the low-level C# bindings at +`framework_dotnet_ffi/csharp/NativeMethods.g.cs` using `csbindgen`. + +- Native library name: `framework_dotnet_ffi` +- Generated C# namespace: `Framework.System.Interop` +- Generated C# class: `NativeMethods` + +The generated surface is intentionally low-level and mirrors the Rust ABI closely: + +- APIs return by-value `Framework...Result` records with a shared `FrameworkStatus status` + field instead of using out parameters. +- Strings and byte sequences are exposed as `FrameworkByteBuffer`. +- Thermal and power data use explicit enums and nested records so the generated C# stays + predictable. + +Common buffer-returning fields include: + +- `FrameworkProductNameResult.product_name` +- `FrameworkEcBuildInfoResult.build_info` +- `FrameworkStatusDeviceErrorMessageResult.message` +- `FrameworkStatusDescriptionResult.description` +- `FrameworkEcFlashVersions.ro_version` +- `FrameworkEcFlashVersions.rw_version` +- `FrameworkPowerSnapshot.battery_0.manufacturer` +- `FrameworkPowerSnapshot.battery_0.model_number` +- `FrameworkPowerSnapshot.battery_0.serial_number` +- `FrameworkPowerSnapshot.battery_0.battery_type` + +Every `FrameworkByteBuffer` returned by the library must be released with +`framework_byte_buffer_free` after the managed side has copied its contents. This also +applies to nested buffers such as battery text fields inside `FrameworkPowerSnapshot` +and flash version strings inside `FrameworkEcFlashVersions`. + ## Install local package ``` diff --git a/framework_dotnet_ffi/Cargo.toml b/framework_dotnet_ffi/Cargo.toml new file mode 100644 index 0000000..0380d85 --- /dev/null +++ b/framework_dotnet_ffi/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "framework_dotnet_ffi" +version = "0.6.2" +description = "Native FFI surface for Framework System .NET interop" +homepage = "https://github.com/FrameworkComputer/framework-system" +repository = "https://github.com/FrameworkComputer/framework-system" +license = "BSD-3-Clause" +edition = "2021" +rust-version = "1.81" +build = "build.rs" + +[lib] +crate-type = ["cdylib", "rlib"] + +[build-dependencies] +csbindgen = "1.9.7" + +[dependencies] +framework_lib = { path = "../framework_lib" } diff --git a/framework_dotnet_ffi/build.rs b/framework_dotnet_ffi/build.rs new file mode 100644 index 0000000..b49c258 --- /dev/null +++ b/framework_dotnet_ffi/build.rs @@ -0,0 +1,19 @@ +use std::error::Error; +use std::fs; +use std::path::Path; + +fn main() -> Result<(), Box> { + let output_dir = Path::new("csharp"); + fs::create_dir_all(output_dir)?; + println!("cargo:rerun-if-changed=src/lib.rs"); + + csbindgen::Builder::default() + .input_extern_file("src/lib.rs") + .csharp_dll_name("framework_dotnet_ffi") + .csharp_namespace("Framework.System.Interop") + .csharp_class_name("NativeMethods") + .csharp_class_accessibility("internal") + .generate_csharp_file(output_dir.join("NativeMethods.g.cs"))?; + + Ok(()) +} diff --git a/framework_dotnet_ffi/src/lib.rs b/framework_dotnet_ffi/src/lib.rs new file mode 100644 index 0000000..726258c --- /dev/null +++ b/framework_dotnet_ffi/src/lib.rs @@ -0,0 +1,1536 @@ +use std::collections::VecDeque; +use std::convert::TryFrom; +use std::ptr; +use std::sync::atomic::{AtomicI32, Ordering}; +use std::sync::{Mutex, OnceLock}; + +use framework_lib::chromium_ec::command::EcRequestRaw; +use framework_lib::chromium_ec::commands::{EcFeatureCode, EcRequestGetFeatures}; +use framework_lib::chromium_ec::{ + CrosEc, CrosEcDriverType, EcCurrentImage, EcError, EcResponseStatus, +}; +use framework_lib::power::{self, ThermalSensorStatus, FAN_SLOT_COUNT, THERMAL_SENSOR_COUNT}; +use framework_lib::smbios; +use framework_lib::smbios::{Platform, PlatformFamily}; + +const STORED_DEVICE_ERROR_LIMIT: usize = 64; +#[cfg(target_os = "linux")] +const CROS_EC_DEV_PATH: &str = "/dev/cros_ec"; + +static NEXT_DEVICE_ERROR_ID: AtomicI32 = AtomicI32::new(1); +static DEVICE_ERROR_MESSAGES: OnceLock>> = OnceLock::new(); + +pub struct FrameworkEcHandle { + ec: CrosEc, + driver: FrameworkEcDriver, +} + +#[repr(i32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FrameworkStatusCode { + Success = 0, + NullPointer = -1, + InvalidArgument = -2, + NoDriverAvailable = -3, + UnsupportedDriver = -4, + DeviceError = -5, + EcResponse = -6, + UnknownResponseCode = -7, + DataUnavailable = -8, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FrameworkStatusNoPayload { + pub reserved: i32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FrameworkStatusInvalidFanIndexRecord { + pub fan_index: i32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FrameworkStatusEcResponseRecord { + pub response: FrameworkEcResponseDetail, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FrameworkStatusUnknownEcResponseCodeRecord { + pub response_code: i32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FrameworkStatusDeviceErrorRecord { + pub message_token: i32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union FrameworkStatusPayload { + pub none: FrameworkStatusNoPayload, + pub invalid_fan_index: FrameworkStatusInvalidFanIndexRecord, + pub ec_response: FrameworkStatusEcResponseRecord, + pub unknown_ec_response_code: FrameworkStatusUnknownEcResponseCodeRecord, + pub device_error: FrameworkStatusDeviceErrorRecord, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FrameworkStatus { + pub code: FrameworkStatusCode, + pub payload: FrameworkStatusPayload, +} + +impl FrameworkStatus { + fn success() -> Self { + Self::no_payload(FrameworkStatusCode::Success) + } + + fn with(code: FrameworkStatusCode, detail: i32) -> Self { + match code { + FrameworkStatusCode::Success => Self::success(), + FrameworkStatusCode::NullPointer => Self::no_payload(FrameworkStatusCode::NullPointer), + FrameworkStatusCode::InvalidArgument => Self::invalid_fan_index(detail), + FrameworkStatusCode::NoDriverAvailable => { + Self::no_payload(FrameworkStatusCode::NoDriverAvailable) + } + FrameworkStatusCode::UnsupportedDriver => { + Self::no_payload(FrameworkStatusCode::UnsupportedDriver) + } + FrameworkStatusCode::DeviceError => Self::device_error(detail), + FrameworkStatusCode::EcResponse => { + Self::ec_response(ec_response_detail_from_raw(detail)) + } + FrameworkStatusCode::UnknownResponseCode => Self::unknown_response_code(detail), + FrameworkStatusCode::DataUnavailable => { + Self::no_payload(FrameworkStatusCode::DataUnavailable) + } + } + } + + fn no_payload(code: FrameworkStatusCode) -> Self { + Self { + code, + payload: FrameworkStatusPayload { + none: FrameworkStatusNoPayload { reserved: 0 }, + }, + } + } + + fn invalid_fan_index(fan_index: i32) -> Self { + Self { + code: FrameworkStatusCode::InvalidArgument, + payload: FrameworkStatusPayload { + invalid_fan_index: FrameworkStatusInvalidFanIndexRecord { fan_index }, + }, + } + } + + fn ec_response(response: FrameworkEcResponseDetail) -> Self { + Self { + code: FrameworkStatusCode::EcResponse, + payload: FrameworkStatusPayload { + ec_response: FrameworkStatusEcResponseRecord { response }, + }, + } + } + + fn unknown_response_code(response_code: i32) -> Self { + Self { + code: FrameworkStatusCode::UnknownResponseCode, + payload: FrameworkStatusPayload { + unknown_ec_response_code: FrameworkStatusUnknownEcResponseCodeRecord { + response_code, + }, + }, + } + } + + fn device_error(message_token: i32) -> Self { + Self { + code: FrameworkStatusCode::DeviceError, + payload: FrameworkStatusPayload { + device_error: FrameworkStatusDeviceErrorRecord { message_token }, + }, + } + } + + fn invalid_fan_index_value(&self) -> Option { + if self.code != FrameworkStatusCode::InvalidArgument { + return None; + } + + Some(unsafe { self.payload.invalid_fan_index.fan_index }) + } + + fn ec_response_detail(&self) -> Option { + if self.code != FrameworkStatusCode::EcResponse { + return None; + } + + Some(unsafe { self.payload.ec_response.response }) + } + + fn unknown_response_code_value(&self) -> Option { + if self.code != FrameworkStatusCode::UnknownResponseCode { + return None; + } + + Some(unsafe { self.payload.unknown_ec_response_code.response_code }) + } + + fn device_error_message_token(&self) -> Option { + if self.code != FrameworkStatusCode::DeviceError { + return None; + } + + Some(unsafe { self.payload.device_error.message_token }) + } +} + +#[repr(i32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FrameworkEcResponseDetail { + Unknown = -1, + Success = 0, + InvalidCommand = 1, + Error = 2, + InvalidParameter = 3, + AccessDenied = 4, + InvalidResponse = 5, + InvalidVersion = 6, + InvalidChecksum = 7, + InProgress = 8, + Unavailable = 9, + Timeout = 10, + Overflow = 11, + InvalidHeader = 12, + RequestTruncated = 13, + ResponseTooBig = 14, + BusError = 15, + Busy = 16, +} + +impl From for FrameworkEcResponseDetail { + fn from(value: EcResponseStatus) -> Self { + match value { + EcResponseStatus::Success => FrameworkEcResponseDetail::Success, + EcResponseStatus::InvalidCommand => FrameworkEcResponseDetail::InvalidCommand, + EcResponseStatus::Error => FrameworkEcResponseDetail::Error, + EcResponseStatus::InvalidParameter => FrameworkEcResponseDetail::InvalidParameter, + EcResponseStatus::AccessDenied => FrameworkEcResponseDetail::AccessDenied, + EcResponseStatus::InvalidResponse => FrameworkEcResponseDetail::InvalidResponse, + EcResponseStatus::InvalidVersion => FrameworkEcResponseDetail::InvalidVersion, + EcResponseStatus::InvalidChecksum => FrameworkEcResponseDetail::InvalidChecksum, + EcResponseStatus::InProgress => FrameworkEcResponseDetail::InProgress, + EcResponseStatus::Unavailable => FrameworkEcResponseDetail::Unavailable, + EcResponseStatus::Timeout => FrameworkEcResponseDetail::Timeout, + EcResponseStatus::Overflow => FrameworkEcResponseDetail::Overflow, + EcResponseStatus::InvalidHeader => FrameworkEcResponseDetail::InvalidHeader, + EcResponseStatus::RequestTruncated => FrameworkEcResponseDetail::RequestTruncated, + EcResponseStatus::ResponseTooBig => FrameworkEcResponseDetail::ResponseTooBig, + EcResponseStatus::BusError => FrameworkEcResponseDetail::BusError, + EcResponseStatus::Busy => FrameworkEcResponseDetail::Busy, + } + } +} + +#[repr(i32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FrameworkEcDriver { + Unknown = -1, + Portio = 0, + CrosEc = 1, + Windows = 2, +} + +impl From for FrameworkEcDriver { + fn from(value: CrosEcDriverType) -> Self { + match value { + CrosEcDriverType::Portio => FrameworkEcDriver::Portio, + CrosEcDriverType::CrosEc => FrameworkEcDriver::CrosEc, + CrosEcDriverType::Windows => FrameworkEcDriver::Windows, + } + } +} + +impl TryFrom for CrosEcDriverType { + type Error = (); + + fn try_from(value: FrameworkEcDriver) -> Result { + match value { + FrameworkEcDriver::Unknown => Err(()), + FrameworkEcDriver::Portio => Ok(CrosEcDriverType::Portio), + FrameworkEcDriver::CrosEc => Ok(CrosEcDriverType::CrosEc), + FrameworkEcDriver::Windows => Ok(CrosEcDriverType::Windows), + } + } +} + +#[repr(i32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FrameworkPlatform { + Framework12IntelGen13 = 0, + IntelGen11 = 1, + IntelGen12 = 2, + IntelGen13 = 3, + IntelCoreUltra1 = 4, + Framework13Amd7080 = 5, + Framework13AmdAi300 = 6, + Framework16Amd7080 = 7, + Framework16AmdAi300 = 8, + FrameworkDesktopAmdAiMax300 = 9, + GenericFramework = 10, + UnknownSystem = 11, + IntelCoreUltra3 = 12, +} + +impl From for FrameworkPlatform { + fn from(value: Platform) -> Self { + match value { + Platform::Framework12IntelGen13 => FrameworkPlatform::Framework12IntelGen13, + Platform::IntelGen11 => FrameworkPlatform::IntelGen11, + Platform::IntelGen12 => FrameworkPlatform::IntelGen12, + Platform::IntelGen13 => FrameworkPlatform::IntelGen13, + Platform::IntelCoreUltra1 => FrameworkPlatform::IntelCoreUltra1, + Platform::IntelCoreUltra3 => FrameworkPlatform::IntelCoreUltra3, + Platform::Framework13Amd7080 => FrameworkPlatform::Framework13Amd7080, + Platform::Framework13AmdAi300 => FrameworkPlatform::Framework13AmdAi300, + Platform::Framework16Amd7080 => FrameworkPlatform::Framework16Amd7080, + Platform::Framework16AmdAi300 => FrameworkPlatform::Framework16AmdAi300, + Platform::FrameworkDesktopAmdAiMax300 => FrameworkPlatform::FrameworkDesktopAmdAiMax300, + Platform::GenericFramework(..) => FrameworkPlatform::GenericFramework, + Platform::UnknownSystem => FrameworkPlatform::UnknownSystem, + } + } +} + +#[repr(i32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FrameworkPlatformFamily { + Unknown = -1, + Framework12 = 0, + Framework13 = 1, + Framework16 = 2, + FrameworkDesktop = 3, +} + +impl From for FrameworkPlatformFamily { + fn from(value: PlatformFamily) -> Self { + match value { + PlatformFamily::Framework12 => FrameworkPlatformFamily::Framework12, + PlatformFamily::Framework13 => FrameworkPlatformFamily::Framework13, + PlatformFamily::Framework16 => FrameworkPlatformFamily::Framework16, + PlatformFamily::FrameworkDesktop => FrameworkPlatformFamily::FrameworkDesktop, + } + } +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FrameworkPlatformResult { + pub status: FrameworkStatus, + pub platform: FrameworkPlatform, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FrameworkPlatformFamilyResult { + pub status: FrameworkStatus, + pub family: FrameworkPlatformFamily, +} + +#[repr(i32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FrameworkTemperatureState { + Ok = 0, + NotPresent = 1, + Error = 2, + NotPowered = 3, + NotCalibrated = 4, +} + +impl From for FrameworkTemperatureState { + fn from(value: ThermalSensorStatus) -> Self { + match value { + ThermalSensorStatus::Ok => FrameworkTemperatureState::Ok, + ThermalSensorStatus::NotPresent => FrameworkTemperatureState::NotPresent, + ThermalSensorStatus::Error => FrameworkTemperatureState::Error, + ThermalSensorStatus::NotPowered => FrameworkTemperatureState::NotPowered, + ThermalSensorStatus::NotCalibrated => FrameworkTemperatureState::NotCalibrated, + } + } +} + +#[repr(i32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FrameworkEcCurrentImage { + Unknown = 0, + Ro = 1, + Rw = 2, +} + +impl From for FrameworkEcCurrentImage { + fn from(value: EcCurrentImage) -> Self { + match value { + EcCurrentImage::Unknown => FrameworkEcCurrentImage::Unknown, + EcCurrentImage::RO => FrameworkEcCurrentImage::Ro, + EcCurrentImage::RW => FrameworkEcCurrentImage::Rw, + } + } +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FrameworkTemperatureReading { + pub state: FrameworkTemperatureState, + pub celsius: i16, + pub reserved: u16, +} + +#[repr(i32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FrameworkFanState { + Ok = 0, + NotPresent = 1, + Stalled = 2, +} + +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FrameworkFanFeaturesState { + None = 0, + FanControl = 1, + ThermalReporting = 2, + All = 3, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FrameworkFanReading { + pub state: FrameworkFanState, + pub rpm: u16, + pub reserved: u16, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FrameworkThermalSnapshot { + pub fan_count: u8, + pub reserved: [u8; 3], + pub temperature_0: FrameworkTemperatureReading, + pub temperature_1: FrameworkTemperatureReading, + pub temperature_2: FrameworkTemperatureReading, + pub temperature_3: FrameworkTemperatureReading, + pub temperature_4: FrameworkTemperatureReading, + pub temperature_5: FrameworkTemperatureReading, + pub temperature_6: FrameworkTemperatureReading, + pub temperature_7: FrameworkTemperatureReading, + pub fan_0: FrameworkFanReading, + pub fan_1: FrameworkFanReading, + pub fan_2: FrameworkFanReading, + pub fan_3: FrameworkFanReading, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FrameworkFanCapabilities { + pub fan_count: u8, + pub features: FrameworkFanFeaturesState, + pub reserved: [u8; 2], +} + +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FrameworkPowerSourceState { + None = 0, + AcOnly = 1, + BatteryOnly = 2, + AcAndBattery = 3, +} + +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FrameworkBatteryState { + NotPresent = 0, + Idle = 1, + Charging = 2, + Discharging = 3, + ChargingAndDischarging = 4, + Critical = 5, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FrameworkBatterySnapshot { + pub battery_state: FrameworkBatteryState, + pub reserved: [u8; 3], + pub present_voltage: u32, + pub present_rate: u32, + pub remaining_capacity: u32, + pub design_capacity: u32, + pub design_voltage: u32, + pub last_full_charge_capacity: u32, + pub cycle_count: u32, + pub charge_percentage: u32, + pub manufacturer: FrameworkByteBuffer, + pub model_number: FrameworkByteBuffer, + pub serial_number: FrameworkByteBuffer, + pub battery_type: FrameworkByteBuffer, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FrameworkPowerSnapshot { + pub power_source_state: FrameworkPowerSourceState, + pub battery_count: u8, + pub reserved: [u8; 2], + pub battery_0: FrameworkBatterySnapshot, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FrameworkEcFlashVersions { + pub current_image: FrameworkEcCurrentImage, + pub ro_version: FrameworkByteBuffer, + pub rw_version: FrameworkByteBuffer, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FrameworkByteBuffer { + pub ptr: *mut u8, + pub length: i32, + pub capacity: i32, +} + +impl Default for FrameworkByteBuffer { + fn default() -> Self { + Self { + ptr: ptr::null_mut(), + length: 0, + capacity: 0, + } + } +} + +impl FrameworkByteBuffer { + fn from_vec(bytes: Vec) -> Self { + let length = i32::try_from(bytes.len()).expect("buffer length overflowed i32"); + let capacity = i32::try_from(bytes.capacity()).expect("buffer capacity overflowed i32"); + let mut bytes = std::mem::ManuallyDrop::new(bytes); + + Self { + ptr: bytes.as_mut_ptr(), + length, + capacity, + } + } + + unsafe fn destroy(self) { + if self.ptr.is_null() { + return; + } + + let length = usize::try_from(self.length).expect("negative buffer length"); + let capacity = usize::try_from(self.capacity).expect("negative buffer capacity"); + drop(Vec::from_raw_parts(self.ptr, length, capacity)); + } +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FrameworkEcHandleResult { + pub status: FrameworkStatus, + pub handle: *mut FrameworkEcHandle, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FrameworkProductNameResult { + pub status: FrameworkStatus, + pub product_name: FrameworkByteBuffer, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FrameworkEcBuildInfoResult { + pub status: FrameworkStatus, + pub build_info: FrameworkByteBuffer, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FrameworkEcFlashVersionsResult { + pub status: FrameworkStatus, + pub versions: FrameworkEcFlashVersions, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FrameworkEcPowerSnapshotResult { + pub status: FrameworkStatus, + pub snapshot: FrameworkPowerSnapshot, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FrameworkEcFanCapabilitiesResult { + pub status: FrameworkStatus, + pub capabilities: FrameworkFanCapabilities, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FrameworkEcThermalSnapshotResult { + pub status: FrameworkStatus, + pub snapshot: FrameworkThermalSnapshot, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FrameworkEcActiveDriverResult { + pub status: FrameworkStatus, + pub driver: FrameworkEcDriver, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FrameworkStatusDeviceErrorMessageResult { + pub status: FrameworkStatus, + pub message: FrameworkByteBuffer, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FrameworkStatusDescriptionResult { + pub status: FrameworkStatus, + pub description: FrameworkByteBuffer, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FrameworkEcSetFanRpmResult { + pub status: FrameworkStatus, + pub fan_index: i32, + pub rpm: u32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FrameworkEcSetFanDutyResult { + pub status: FrameworkStatus, + pub fan_index: i32, + pub percent: u32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FrameworkEcRestoreAutoFanControlResult { + pub status: FrameworkStatus, + pub fan_index: i32, +} + +fn device_error_messages() -> &'static Mutex> { + DEVICE_ERROR_MESSAGES.get_or_init(|| Mutex::new(VecDeque::new())) +} + +fn store_device_error_message(message: String) -> i32 { + let id = NEXT_DEVICE_ERROR_ID.fetch_add(1, Ordering::Relaxed); + let mut messages = device_error_messages() + .lock() + .expect("device error message lock poisoned"); + messages.push_back((id, message)); + while messages.len() > STORED_DEVICE_ERROR_LIMIT { + messages.pop_front(); + } + id +} + +fn get_device_error_message(detail: i32) -> Option { + if detail <= 0 { + return None; + } + + let messages = device_error_messages().lock().ok()?; + messages + .iter() + .find(|(id, _)| *id == detail) + .map(|(_, message)| message.clone()) +} + +fn ec_response_detail_from_raw(detail: i32) -> FrameworkEcResponseDetail { + match detail { + 0 => FrameworkEcResponseDetail::Success, + 1 => FrameworkEcResponseDetail::InvalidCommand, + 2 => FrameworkEcResponseDetail::Error, + 3 => FrameworkEcResponseDetail::InvalidParameter, + 4 => FrameworkEcResponseDetail::AccessDenied, + 5 => FrameworkEcResponseDetail::InvalidResponse, + 6 => FrameworkEcResponseDetail::InvalidVersion, + 7 => FrameworkEcResponseDetail::InvalidChecksum, + 8 => FrameworkEcResponseDetail::InProgress, + 9 => FrameworkEcResponseDetail::Unavailable, + 10 => FrameworkEcResponseDetail::Timeout, + 11 => FrameworkEcResponseDetail::Overflow, + 12 => FrameworkEcResponseDetail::InvalidHeader, + 13 => FrameworkEcResponseDetail::RequestTruncated, + 14 => FrameworkEcResponseDetail::ResponseTooBig, + 15 => FrameworkEcResponseDetail::BusError, + 16 => FrameworkEcResponseDetail::Busy, + _ => FrameworkEcResponseDetail::Unknown, + } +} +fn ec_response_detail_name(detail: FrameworkEcResponseDetail) -> &'static str { + match detail { + FrameworkEcResponseDetail::Unknown => "Unknown", + FrameworkEcResponseDetail::Success => "Success", + FrameworkEcResponseDetail::InvalidCommand => "InvalidCommand", + FrameworkEcResponseDetail::Error => "Error", + FrameworkEcResponseDetail::InvalidParameter => "InvalidParameter", + FrameworkEcResponseDetail::AccessDenied => "AccessDenied", + FrameworkEcResponseDetail::InvalidResponse => "InvalidResponse", + FrameworkEcResponseDetail::InvalidVersion => "InvalidVersion", + FrameworkEcResponseDetail::InvalidChecksum => "InvalidChecksum", + FrameworkEcResponseDetail::InProgress => "InProgress", + FrameworkEcResponseDetail::Unavailable => "Unavailable", + FrameworkEcResponseDetail::Timeout => "Timeout", + FrameworkEcResponseDetail::Overflow => "Overflow", + FrameworkEcResponseDetail::InvalidHeader => "InvalidHeader", + FrameworkEcResponseDetail::RequestTruncated => "RequestTruncated", + FrameworkEcResponseDetail::ResponseTooBig => "ResponseTooBig", + FrameworkEcResponseDetail::BusError => "BusError", + FrameworkEcResponseDetail::Busy => "Busy", + } +} + +fn default_ec_flash_versions() -> FrameworkEcFlashVersions { + FrameworkEcFlashVersions { + current_image: FrameworkEcCurrentImage::Unknown, + ro_version: FrameworkByteBuffer::default(), + rw_version: FrameworkByteBuffer::default(), + } +} + +fn default_battery_snapshot() -> FrameworkBatterySnapshot { + FrameworkBatterySnapshot { + battery_state: FrameworkBatteryState::NotPresent, + reserved: [0; 3], + present_voltage: 0, + present_rate: 0, + remaining_capacity: 0, + design_capacity: 0, + design_voltage: 0, + last_full_charge_capacity: 0, + cycle_count: 0, + charge_percentage: 0, + manufacturer: FrameworkByteBuffer::default(), + model_number: FrameworkByteBuffer::default(), + serial_number: FrameworkByteBuffer::default(), + battery_type: FrameworkByteBuffer::default(), + } +} + +fn default_power_snapshot() -> FrameworkPowerSnapshot { + FrameworkPowerSnapshot { + power_source_state: FrameworkPowerSourceState::None, + battery_count: 0, + reserved: [0; 2], + battery_0: default_battery_snapshot(), + } +} + +fn default_fan_capabilities() -> FrameworkFanCapabilities { + FrameworkFanCapabilities { + fan_count: 0, + features: FrameworkFanFeaturesState::None, + reserved: [0; 2], + } +} + +fn fan_features_state( + supports_fan_control: bool, + supports_thermal_reporting: bool, +) -> FrameworkFanFeaturesState { + match (supports_fan_control, supports_thermal_reporting) { + (false, false) => FrameworkFanFeaturesState::None, + (true, false) => FrameworkFanFeaturesState::FanControl, + (false, true) => FrameworkFanFeaturesState::ThermalReporting, + (true, true) => FrameworkFanFeaturesState::All, + } +} + +fn power_source_state(ac_present: bool, battery_present: bool) -> FrameworkPowerSourceState { + match (ac_present, battery_present) { + (false, false) => FrameworkPowerSourceState::None, + (true, false) => FrameworkPowerSourceState::AcOnly, + (false, true) => FrameworkPowerSourceState::BatteryOnly, + (true, true) => FrameworkPowerSourceState::AcAndBattery, + } +} + +fn battery_state(level_critical: bool, discharging: bool, charging: bool) -> FrameworkBatteryState { + if level_critical { + FrameworkBatteryState::Critical + } else { + match (discharging, charging) { + (false, false) => FrameworkBatteryState::Idle, + (false, true) => FrameworkBatteryState::Charging, + (true, false) => FrameworkBatteryState::Discharging, + (true, true) => FrameworkBatteryState::ChargingAndDischarging, + } + } +} + +fn default_temperature_reading() -> FrameworkTemperatureReading { + FrameworkTemperatureReading { + state: FrameworkTemperatureState::NotPresent, + celsius: 0, + reserved: 0, + } +} + +fn default_fan_reading() -> FrameworkFanReading { + FrameworkFanReading { + state: FrameworkFanState::NotPresent, + rpm: 0, + reserved: 0, + } +} + +fn fan_state(present: bool, stalled: bool) -> FrameworkFanState { + if !present { + FrameworkFanState::NotPresent + } else if stalled { + FrameworkFanState::Stalled + } else { + FrameworkFanState::Ok + } +} + +fn default_thermal_snapshot() -> FrameworkThermalSnapshot { + let temperature = default_temperature_reading(); + let fan = default_fan_reading(); + + FrameworkThermalSnapshot { + fan_count: 0, + reserved: [0; 3], + temperature_0: temperature, + temperature_1: temperature, + temperature_2: temperature, + temperature_3: temperature, + temperature_4: temperature, + temperature_5: temperature, + temperature_6: temperature, + temperature_7: temperature, + fan_0: fan, + fan_1: fan, + fan_2: fan, + fan_3: fan, + } +} + +fn ec_handle_result( + status: FrameworkStatus, + handle: *mut FrameworkEcHandle, +) -> FrameworkEcHandleResult { + FrameworkEcHandleResult { status, handle } +} + +fn product_name_result( + status: FrameworkStatus, + product_name: FrameworkByteBuffer, +) -> FrameworkProductNameResult { + FrameworkProductNameResult { + status, + product_name, + } +} + +fn build_info_result( + status: FrameworkStatus, + build_info: FrameworkByteBuffer, +) -> FrameworkEcBuildInfoResult { + FrameworkEcBuildInfoResult { status, build_info } +} + +fn flash_versions_result( + status: FrameworkStatus, + versions: FrameworkEcFlashVersions, +) -> FrameworkEcFlashVersionsResult { + FrameworkEcFlashVersionsResult { status, versions } +} + +fn power_snapshot_result( + status: FrameworkStatus, + snapshot: FrameworkPowerSnapshot, +) -> FrameworkEcPowerSnapshotResult { + FrameworkEcPowerSnapshotResult { status, snapshot } +} + +fn fan_capabilities_result( + status: FrameworkStatus, + capabilities: FrameworkFanCapabilities, +) -> FrameworkEcFanCapabilitiesResult { + FrameworkEcFanCapabilitiesResult { + status, + capabilities, + } +} + +fn thermal_snapshot_result( + status: FrameworkStatus, + snapshot: FrameworkThermalSnapshot, +) -> FrameworkEcThermalSnapshotResult { + FrameworkEcThermalSnapshotResult { status, snapshot } +} + +fn active_driver_result( + status: FrameworkStatus, + driver: FrameworkEcDriver, +) -> FrameworkEcActiveDriverResult { + FrameworkEcActiveDriverResult { status, driver } +} + +fn status_device_error_message_result( + status: FrameworkStatus, + message: FrameworkByteBuffer, +) -> FrameworkStatusDeviceErrorMessageResult { + FrameworkStatusDeviceErrorMessageResult { status, message } +} + +fn status_description_result( + status: FrameworkStatus, + description: FrameworkByteBuffer, +) -> FrameworkStatusDescriptionResult { + FrameworkStatusDescriptionResult { + status, + description, + } +} + +fn platform_result( + status: FrameworkStatus, + platform: FrameworkPlatform, +) -> FrameworkPlatformResult { + FrameworkPlatformResult { status, platform } +} + +fn platform_family_result( + status: FrameworkStatus, + family: FrameworkPlatformFamily, +) -> FrameworkPlatformFamilyResult { + FrameworkPlatformFamilyResult { status, family } +} + +fn set_fan_rpm_result( + status: FrameworkStatus, + fan_index: i32, + rpm: u32, +) -> FrameworkEcSetFanRpmResult { + FrameworkEcSetFanRpmResult { + status, + fan_index, + rpm, + } +} + +fn set_fan_duty_result( + status: FrameworkStatus, + fan_index: i32, + percent: u32, +) -> FrameworkEcSetFanDutyResult { + FrameworkEcSetFanDutyResult { + status, + fan_index, + percent, + } +} + +fn restore_auto_fan_control_result( + status: FrameworkStatus, + fan_index: i32, +) -> FrameworkEcRestoreAutoFanControlResult { + FrameworkEcRestoreAutoFanControlResult { status, fan_index } +} + +fn status_description(status: FrameworkStatus) -> String { + match status.code { + FrameworkStatusCode::Success => "Success".to_string(), + FrameworkStatusCode::NullPointer => "Null pointer".to_string(), + FrameworkStatusCode::InvalidArgument => { + format!( + "Invalid fan index: {}", + status.invalid_fan_index_value().unwrap_or_default() + ) + } + FrameworkStatusCode::NoDriverAvailable => "No EC driver available".to_string(), + FrameworkStatusCode::UnsupportedDriver => { + "Requested EC driver is not supported on this system".to_string() + } + FrameworkStatusCode::DeviceError => { + if let Some(message) = status + .device_error_message_token() + .and_then(get_device_error_message) + { + format!("Device error: {}", message) + } else { + "Device error".to_string() + } + } + FrameworkStatusCode::EcResponse => { + let detail = status + .ec_response_detail() + .unwrap_or(FrameworkEcResponseDetail::Unknown); + format!( + "EC response: {} ({})", + ec_response_detail_name(detail), + detail as i32 + ) + } + FrameworkStatusCode::UnknownResponseCode => { + format!( + "Unknown EC response code: {}", + status.unknown_response_code_value().unwrap_or_default() + ) + } + FrameworkStatusCode::DataUnavailable => "Data unavailable".to_string(), + } +} + +fn status_from_error(error: EcError) -> FrameworkStatus { + match error { + EcError::Response(response) => { + FrameworkStatus::with(FrameworkStatusCode::EcResponse, response as i32) + } + EcError::UnknownResponseCode(code) => FrameworkStatus::with( + FrameworkStatusCode::UnknownResponseCode, + i32::try_from(code).unwrap_or(i32::MAX), + ), + EcError::DeviceError(message) => { + let detail = store_device_error_message(message); + FrameworkStatus::with(FrameworkStatusCode::DeviceError, detail) + } + } +} + +fn default_ec_handle() -> Option { + #[cfg(windows)] + if let Some(ec) = CrosEc::with(CrosEcDriverType::Windows) { + return Some(FrameworkEcHandle { + ec, + driver: FrameworkEcDriver::Windows, + }); + } + + #[cfg(target_os = "linux")] + if std::path::Path::new(CROS_EC_DEV_PATH).exists() { + if let Some(ec) = CrosEc::with(CrosEcDriverType::CrosEc) { + return Some(FrameworkEcHandle { + ec, + driver: FrameworkEcDriver::CrosEc, + }); + } + } + + #[cfg(all(not(windows), target_arch = "x86_64"))] + if let Some(ec) = CrosEc::with(CrosEcDriverType::Portio) { + return Some(FrameworkEcHandle { + ec, + driver: FrameworkEcDriver::Portio, + }); + } + + None +} + +fn read_feature_flags(ec: &CrosEc) -> Result<[u32; 2], FrameworkStatus> { + EcRequestGetFeatures {} + .send_command(ec) + .map(|response| response.flags) + .map_err(status_from_error) +} + +fn feature_enabled(ec: &CrosEc, feature: EcFeatureCode) -> Result { + let flags = read_feature_flags(ec)?; + let index = feature as usize; + let word = index / 32; + let bit = index % 32; + Ok((flags[word] & (1 << bit)) != 0) +} + +fn require_handle<'a>( + handle: *const FrameworkEcHandle, +) -> Result<&'a FrameworkEcHandle, FrameworkStatus> { + if handle.is_null() { + return Err(FrameworkStatus::with(FrameworkStatusCode::NullPointer, 0)); + } + + // SAFETY: the caller guarantees the handle pointer came from framework_ec_open_*. + Ok(unsafe { &*handle }) +} + +fn parse_optional_fan_index(fan_index: i32) -> Result, FrameworkStatus> { + if fan_index == -1 { + return Ok(None); + } + + let fan_index = u32::try_from(fan_index) + .map_err(|_| FrameworkStatus::with(FrameworkStatusCode::InvalidArgument, fan_index))?; + Ok(Some(fan_index)) +} + +fn parse_optional_fan_index_u8(fan_index: i32) -> Result, FrameworkStatus> { + if fan_index == -1 { + return Ok(None); + } + + let fan_index = u8::try_from(fan_index) + .map_err(|_| FrameworkStatus::with(FrameworkStatusCode::InvalidArgument, fan_index))?; + Ok(Some(fan_index)) +} + +#[no_mangle] +pub extern "C" fn framework_ec_driver_is_supported(driver: FrameworkEcDriver) -> bool { + let Ok(driver) = CrosEcDriverType::try_from(driver) else { + return false; + }; + + CrosEc::with(driver).is_some() +} + +#[no_mangle] +/// The returned `message` buffer must be released with +/// `framework_byte_buffer_free`. +pub extern "C" fn framework_status_get_device_error_message( + status: FrameworkStatus, +) -> FrameworkStatusDeviceErrorMessageResult { + let Some(message) = status + .device_error_message_token() + .and_then(get_device_error_message) + else { + return status_device_error_message_result( + FrameworkStatus::with(FrameworkStatusCode::DataUnavailable, 0), + FrameworkByteBuffer::default(), + ); + }; + + status_device_error_message_result( + FrameworkStatus::success(), + FrameworkByteBuffer::from_vec(message.into_bytes()), + ) +} + +#[no_mangle] +/// The returned `description` buffer must be released with +/// `framework_byte_buffer_free`. +pub extern "C" fn framework_status_get_description( + status: FrameworkStatus, +) -> FrameworkStatusDescriptionResult { + status_description_result( + FrameworkStatus::success(), + FrameworkByteBuffer::from_vec(status_description(status).into_bytes()), + ) +} + +#[no_mangle] +pub extern "C" fn framework_ec_open_default() -> FrameworkEcHandleResult { + let Some(handle) = default_ec_handle() else { + return ec_handle_result( + FrameworkStatus::with(FrameworkStatusCode::NoDriverAvailable, 0), + ptr::null_mut(), + ); + }; + + if let Err(error) = handle.ec.check_mem_magic() { + return ec_handle_result(status_from_error(error), ptr::null_mut()); + } + + ec_handle_result(FrameworkStatus::success(), Box::into_raw(Box::new(handle))) +} + +#[no_mangle] +pub extern "C" fn framework_ec_open_with_driver( + driver: FrameworkEcDriver, +) -> FrameworkEcHandleResult { + let Ok(driver_type) = CrosEcDriverType::try_from(driver) else { + return ec_handle_result( + FrameworkStatus::with(FrameworkStatusCode::UnsupportedDriver, 0), + ptr::null_mut(), + ); + }; + + let Some(ec) = CrosEc::with(driver_type) else { + return ec_handle_result( + FrameworkStatus::with(FrameworkStatusCode::UnsupportedDriver, 0), + ptr::null_mut(), + ); + }; + + if let Err(error) = ec.check_mem_magic() { + return ec_handle_result(status_from_error(error), ptr::null_mut()); + } + + ec_handle_result( + FrameworkStatus::success(), + Box::into_raw(Box::new(FrameworkEcHandle { ec, driver })), + ) +} + +#[no_mangle] +/// # Safety +/// `handle` must either be null or be a pointer previously returned by one of +/// the `framework_ec_open_*` functions that has not already been freed. +pub unsafe extern "C" fn framework_ec_close(handle: *mut FrameworkEcHandle) { + if !handle.is_null() { + drop(Box::from_raw(handle)); + } +} + +#[no_mangle] +/// # Safety +/// `handle` must be a valid pointer returned by this library. +pub unsafe extern "C" fn framework_ec_get_active_driver( + handle: *const FrameworkEcHandle, +) -> FrameworkEcActiveDriverResult { + let handle = match require_handle(handle) { + Ok(handle) => handle, + Err(status) => return active_driver_result(status, FrameworkEcDriver::Unknown), + }; + + active_driver_result(FrameworkStatus::success(), handle.driver) +} + +#[no_mangle] +pub extern "C" fn framework_get_platform() -> FrameworkPlatformResult { + match smbios::get_platform() { + Some(platform) => platform_result(FrameworkStatus::success(), platform.into()), + None => platform_result( + FrameworkStatus::with(FrameworkStatusCode::DataUnavailable, 0), + FrameworkPlatform::UnknownSystem, + ), + } +} + +#[no_mangle] +pub extern "C" fn framework_get_platform_family() -> FrameworkPlatformFamilyResult { + match smbios::get_family() { + Some(family) => platform_family_result(FrameworkStatus::success(), family.into()), + None => platform_family_result( + FrameworkStatus::with(FrameworkStatusCode::DataUnavailable, 0), + FrameworkPlatformFamily::Unknown, + ), + } +} + +#[no_mangle] +pub extern "C" fn framework_get_product_name() -> FrameworkProductNameResult { + let Some(product_name) = smbios::get_product_name() else { + return product_name_result( + FrameworkStatus::with(FrameworkStatusCode::DataUnavailable, 0), + FrameworkByteBuffer::default(), + ); + }; + + product_name_result( + FrameworkStatus::success(), + FrameworkByteBuffer::from_vec(product_name.into_bytes()), + ) +} + +#[no_mangle] +/// # Safety +/// `handle` must be a valid pointer returned by this library. The returned +/// `build_info` buffer must be released with `framework_byte_buffer_free`. +pub unsafe extern "C" fn framework_ec_get_build_info( + handle: *const FrameworkEcHandle, +) -> FrameworkEcBuildInfoResult { + let handle = match require_handle(handle) { + Ok(handle) => handle, + Err(status) => return build_info_result(status, FrameworkByteBuffer::default()), + }; + + match handle.ec.version_info() { + Ok(build_info) => build_info_result( + FrameworkStatus::success(), + FrameworkByteBuffer::from_vec(build_info.into_bytes()), + ), + Err(error) => build_info_result(status_from_error(error), FrameworkByteBuffer::default()), + } +} + +#[no_mangle] +/// # Safety +/// `handle` must be a valid pointer returned by this library. +pub unsafe extern "C" fn framework_ec_get_flash_versions( + handle: *const FrameworkEcHandle, +) -> FrameworkEcFlashVersionsResult { + let handle = match require_handle(handle) { + Ok(handle) => handle, + Err(status) => return flash_versions_result(status, default_ec_flash_versions()), + }; + + let Some((ro_version, rw_version, current_image)) = handle.ec.flash_version() else { + return flash_versions_result( + FrameworkStatus::with(FrameworkStatusCode::DataUnavailable, 0), + default_ec_flash_versions(), + ); + }; + + flash_versions_result( + FrameworkStatus::success(), + FrameworkEcFlashVersions { + current_image: current_image.into(), + ro_version: FrameworkByteBuffer::from_vec(ro_version.into_bytes()), + rw_version: FrameworkByteBuffer::from_vec(rw_version.into_bytes()), + }, + ) +} + +#[no_mangle] +/// # Safety +/// `buffer` must either be the default zeroed buffer or a buffer previously +/// returned by this library that has not already been freed. +pub unsafe extern "C" fn framework_byte_buffer_free(buffer: FrameworkByteBuffer) { + buffer.destroy(); +} + +#[no_mangle] +/// # Safety +/// `handle` must be a valid pointer returned by this library. +pub unsafe extern "C" fn framework_ec_get_power_snapshot( + handle: *const FrameworkEcHandle, +) -> FrameworkEcPowerSnapshotResult { + let handle = match require_handle(handle) { + Ok(handle) => handle, + Err(status) => return power_snapshot_result(status, default_power_snapshot()), + }; + + let Some(power_info) = power::power_info(&handle.ec) else { + return power_snapshot_result( + FrameworkStatus::with(FrameworkStatusCode::DataUnavailable, 0), + default_power_snapshot(), + ); + }; + + let mut snapshot = FrameworkPowerSnapshot { + power_source_state: power_source_state(power_info.ac_present, power_info.battery.is_some()), + ..default_power_snapshot() + }; + + if let Some(battery) = power_info.battery { + snapshot.battery_count = battery.battery_count; + snapshot.battery_0 = FrameworkBatterySnapshot { + battery_state: battery_state( + battery.level_critical, + battery.discharging, + battery.charging, + ), + reserved: [0; 3], + present_voltage: battery.present_voltage, + present_rate: battery.present_rate, + remaining_capacity: battery.remaining_capacity, + design_capacity: battery.design_capacity, + design_voltage: battery.design_voltage, + last_full_charge_capacity: battery.last_full_charge_capacity, + cycle_count: battery.cycle_count, + charge_percentage: battery.charge_percentage, + manufacturer: FrameworkByteBuffer::from_vec(battery.manufacturer.into_bytes()), + model_number: FrameworkByteBuffer::from_vec(battery.model_number.into_bytes()), + serial_number: FrameworkByteBuffer::from_vec(battery.serial_number.into_bytes()), + battery_type: FrameworkByteBuffer::from_vec(battery.battery_type.into_bytes()), + }; + } + + power_snapshot_result(FrameworkStatus::success(), snapshot) +} + +#[no_mangle] +/// # Safety +/// `handle` must be a valid pointer returned by this library. +pub unsafe extern "C" fn framework_ec_get_fan_capabilities( + handle: *const FrameworkEcHandle, +) -> FrameworkEcFanCapabilitiesResult { + let handle = match require_handle(handle) { + Ok(handle) => handle, + Err(status) => return fan_capabilities_result(status, default_fan_capabilities()), + }; + + let fan_control = match feature_enabled(&handle.ec, EcFeatureCode::PwmFan) { + Ok(supported) => supported, + Err(status) => return fan_capabilities_result(status, default_fan_capabilities()), + }; + let thermal = match feature_enabled(&handle.ec, EcFeatureCode::Thermal) { + Ok(supported) => supported, + Err(status) => return fan_capabilities_result(status, default_fan_capabilities()), + }; + + let fan_count = power::thermal_snapshot(&handle.ec) + .map(|snapshot| snapshot.fan_count) + .unwrap_or(0); + + fan_capabilities_result( + FrameworkStatus::success(), + FrameworkFanCapabilities { + fan_count, + features: fan_features_state(fan_control, thermal), + reserved: [0; 2], + }, + ) +} + +#[no_mangle] +/// # Safety +/// `handle` must be a valid pointer returned by this library. +pub unsafe extern "C" fn framework_ec_get_thermal_snapshot( + handle: *const FrameworkEcHandle, +) -> FrameworkEcThermalSnapshotResult { + let handle = match require_handle(handle) { + Ok(handle) => handle, + Err(status) => return thermal_snapshot_result(status, default_thermal_snapshot()), + }; + + let Some(snapshot) = power::thermal_snapshot(&handle.ec) else { + return thermal_snapshot_result( + FrameworkStatus::with(FrameworkStatusCode::DataUnavailable, 0), + default_thermal_snapshot(), + ); + }; + + let mut temperatures = [default_temperature_reading(); THERMAL_SENSOR_COUNT]; + for (index, reading) in snapshot.temperatures.iter().enumerate() { + temperatures[index] = FrameworkTemperatureReading { + state: reading.status.into(), + celsius: reading.celsius, + reserved: 0, + }; + } + + let mut fan_present = [0u8; FAN_SLOT_COUNT]; + let mut fan_stalled = [0u8; FAN_SLOT_COUNT]; + for index in 0..FAN_SLOT_COUNT { + fan_present[index] = u8::from(snapshot.fan_present[index]); + fan_stalled[index] = u8::from(snapshot.fan_stalled[index]); + } + + thermal_snapshot_result( + FrameworkStatus::success(), + FrameworkThermalSnapshot { + fan_count: snapshot.fan_count, + reserved: [0; 3], + temperature_0: temperatures[0], + temperature_1: temperatures[1], + temperature_2: temperatures[2], + temperature_3: temperatures[3], + temperature_4: temperatures[4], + temperature_5: temperatures[5], + temperature_6: temperatures[6], + temperature_7: temperatures[7], + fan_0: FrameworkFanReading { + state: fan_state(snapshot.fan_present[0], snapshot.fan_stalled[0]), + rpm: snapshot.fan_rpms[0], + reserved: 0, + }, + fan_1: FrameworkFanReading { + state: fan_state(snapshot.fan_present[1], snapshot.fan_stalled[1]), + rpm: snapshot.fan_rpms[1], + reserved: 0, + }, + fan_2: FrameworkFanReading { + state: fan_state(snapshot.fan_present[2], snapshot.fan_stalled[2]), + rpm: snapshot.fan_rpms[2], + reserved: 0, + }, + fan_3: FrameworkFanReading { + state: fan_state(snapshot.fan_present[3], snapshot.fan_stalled[3]), + rpm: snapshot.fan_rpms[3], + reserved: 0, + }, + }, + ) +} + +#[no_mangle] +/// # Safety +/// `handle` must be a valid pointer returned by this library. +pub unsafe extern "C" fn framework_ec_set_fan_rpm( + handle: *const FrameworkEcHandle, + fan_index: i32, + rpm: u32, +) -> FrameworkEcSetFanRpmResult { + let requested_fan_index = fan_index; + + let handle = match require_handle(handle) { + Ok(handle) => handle, + Err(status) => return set_fan_rpm_result(status, fan_index, rpm), + }; + let fan_index = match parse_optional_fan_index(fan_index) { + Ok(fan_index) => fan_index, + Err(status) => return set_fan_rpm_result(status, requested_fan_index, rpm), + }; + + let status = match handle.ec.fan_set_rpm(fan_index, rpm) { + Ok(()) => FrameworkStatus::success(), + Err(error) => status_from_error(error), + }; + + set_fan_rpm_result(status, requested_fan_index, rpm) +} + +#[no_mangle] +/// # Safety +/// `handle` must be a valid pointer returned by this library. +pub unsafe extern "C" fn framework_ec_set_fan_duty( + handle: *const FrameworkEcHandle, + fan_index: i32, + percent: u32, +) -> FrameworkEcSetFanDutyResult { + let requested_fan_index = fan_index; + + let handle = match require_handle(handle) { + Ok(handle) => handle, + Err(status) => return set_fan_duty_result(status, fan_index, percent), + }; + let fan_index = match parse_optional_fan_index(fan_index) { + Ok(fan_index) => fan_index, + Err(status) => return set_fan_duty_result(status, requested_fan_index, percent), + }; + + let status = match handle.ec.fan_set_duty(fan_index, percent) { + Ok(()) => FrameworkStatus::success(), + Err(error) => status_from_error(error), + }; + + set_fan_duty_result(status, requested_fan_index, percent) +} + +#[no_mangle] +/// # Safety +/// `handle` must be a valid pointer returned by this library. +pub unsafe extern "C" fn framework_ec_restore_auto_fan_control( + handle: *const FrameworkEcHandle, + fan_index: i32, +) -> FrameworkEcRestoreAutoFanControlResult { + let requested_fan_index = fan_index; + + let handle = match require_handle(handle) { + Ok(handle) => handle, + Err(status) => return restore_auto_fan_control_result(status, fan_index), + }; + let fan_index = match parse_optional_fan_index_u8(fan_index) { + Ok(fan_index) => fan_index, + Err(status) => return restore_auto_fan_control_result(status, requested_fan_index), + }; + + let status = match handle.ec.autofanctrl(fan_index) { + Ok(()) => FrameworkStatus::success(), + Err(error) => status_from_error(error), + }; + + restore_auto_fan_control_result(status, requested_fan_index) +} diff --git a/framework_lib/src/power.rs b/framework_lib/src/power.rs index 4b07208..c3d252d 100644 --- a/framework_lib/src/power.rs +++ b/framework_lib/src/power.rs @@ -72,6 +72,8 @@ const EC_BATT_FLAG_CHARGING: u8 = 0x08; const EC_BATT_FLAG_LEVEL_CRITICAL: u8 = 0x10; const EC_FAN_SPEED_ENTRIES: usize = 4; +pub const THERMAL_SENSOR_COUNT: usize = 8; +pub const FAN_SLOT_COUNT: usize = EC_FAN_SPEED_ENTRIES; /// Used on old EC firmware (before 2023) const EC_FAN_SPEED_STALLED_DEPRECATED: u16 = 0xFFFE; const EC_FAN_SPEED_NOT_PRESENT: u16 = 0xFFFF; @@ -105,6 +107,57 @@ impl fmt::Display for TempSensor { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ThermalSensorStatus { + Ok, + NotPresent, + Error, + NotPowered, + NotCalibrated, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ThermalSensorReading { + pub status: ThermalSensorStatus, + pub celsius: i16, +} + +impl From for ThermalSensorReading { + fn from(value: TempSensor) -> Self { + match value { + TempSensor::Ok(temp) => Self { + status: ThermalSensorStatus::Ok, + celsius: i16::from(temp), + }, + TempSensor::NotPresent => Self { + status: ThermalSensorStatus::NotPresent, + celsius: 0, + }, + TempSensor::Error => Self { + status: ThermalSensorStatus::Error, + celsius: 0, + }, + TempSensor::NotPowered => Self { + status: ThermalSensorStatus::NotPowered, + celsius: 0, + }, + TempSensor::NotCalibrated => Self { + status: ThermalSensorStatus::NotCalibrated, + celsius: 0, + }, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ThermalSnapshot { + pub temperatures: [ThermalSensorReading; THERMAL_SENSOR_COUNT], + pub fan_rpms: [u16; FAN_SLOT_COUNT], + pub fan_present: [bool; FAN_SLOT_COUNT], + pub fan_stalled: [bool; FAN_SLOT_COUNT], + pub fan_count: u8, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct BatteryInformation { pub present_voltage: u32, @@ -455,6 +508,49 @@ pub fn print_thermal(ec: &CrosEc) { } } +pub fn thermal_snapshot(ec: &CrosEc) -> Option { + let temps = ec.read_memory(EC_MEMMAP_TEMP_SENSOR, 0x0F)?; + let fans = ec.read_memory(EC_MEMMAP_FAN, 0x08)?; + + let mut temperatures = [ThermalSensorReading { + status: ThermalSensorStatus::NotPresent, + celsius: 0, + }; THERMAL_SENSOR_COUNT]; + for (index, byte) in temps.iter().take(THERMAL_SENSOR_COUNT).enumerate() { + temperatures[index] = TempSensor::from(*byte).into(); + } + + let mut fan_rpms = [0u16; FAN_SLOT_COUNT]; + let mut fan_present = [false; FAN_SLOT_COUNT]; + let mut fan_stalled = [false; FAN_SLOT_COUNT]; + let mut fan_count = 0u8; + + for i in 0..FAN_SLOT_COUNT { + let fan = u16::from_le_bytes([fans[i * 2], fans[1 + i * 2]]); + match fan { + EC_FAN_SPEED_NOT_PRESENT => {} + EC_FAN_SPEED_STALLED_DEPRECATED => { + fan_present[i] = true; + fan_stalled[i] = true; + fan_count += 1; + } + rpm => { + fan_rpms[i] = rpm; + fan_present[i] = true; + fan_count += 1; + } + } + } + + Some(ThermalSnapshot { + temperatures, + fan_rpms, + fan_present, + fan_stalled, + fan_count, + }) +} + pub fn get_fan_num(ec: &CrosEc) -> EcResult { let fans = ec.read_memory(EC_MEMMAP_FAN, 0x08).unwrap(); diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 0ac5599..f34094e 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -43,11 +43,11 @@ parts: cd "${CRAFT_PART_SRC}" cargo build --release -p framework_tool --features nvidia install -Dm755 target/release/framework_tool "${CRAFT_PART_INSTALL}/bin/framework_tool" - install -Dm644 "${CRAFT_PART_SRC}/completions/bash/framework_tool" \ + install -Dm644 "${CRAFT_PART_SRC}/framework_tool/completions/bash/framework_tool" \ "${CRAFT_PART_INSTALL}/share/bash-completion/completions/framework_tool" - install -Dm644 "${CRAFT_PART_SRC}/completions/zsh/_framework_tool" \ + install -Dm644 "${CRAFT_PART_SRC}/framework_tool/completions/zsh/_framework_tool" \ "${CRAFT_PART_INSTALL}/share/zsh/site-functions/_framework_tool" - install -Dm644 "${CRAFT_PART_SRC}/completions/fish/framework_tool.fish" \ + install -Dm644 "${CRAFT_PART_SRC}/framework_tool/completions/fish/framework_tool.fish" \ "${CRAFT_PART_INSTALL}/share/fish/vendor_completions.d/framework_tool.fish" apps: