From 309e32913682a76d09ad181b628afee33bd2a98b Mon Sep 17 00:00:00 2001 From: TekuSP Date: Mon, 20 Apr 2026 19:15:04 +0200 Subject: [PATCH 01/11] framework_dotnet_ffi: Add native FFI surface for .NET interop - Introduce a new crate that exposes C-compatible functions for interacting with the EC and SMBIOS. - Integrate `csbindgen` to automatically generate C# bindings during the build process. - Implement helper types and snapshot functions in `framework_lib` to facilitate structured data retrieval. - Export functionality for platform identification, battery status, and thermal/fan control. --- .gitignore | 2 + Cargo.lock | 18 + Cargo.toml | 2 + framework_dotnet_ffi/Cargo.toml | 19 + framework_dotnet_ffi/build.rs | 18 + framework_dotnet_ffi/src/lib.rs | 804 ++++++++++++++++++++++++++++++++ framework_lib/src/power.rs | 96 ++++ 7 files changed, 959 insertions(+) create mode 100644 framework_dotnet_ffi/Cargo.toml create mode 100644 framework_dotnet_ffi/build.rs create mode 100644 framework_dotnet_ffi/src/lib.rs diff --git a/.gitignore b/.gitignore index 4a4e507d..9fa196a3 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 8edd60bd..18461227 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 66801a46..4ca8d273 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/framework_dotnet_ffi/Cargo.toml b/framework_dotnet_ffi/Cargo.toml new file mode 100644 index 00000000..0380d85f --- /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 00000000..63197ae3 --- /dev/null +++ b/framework_dotnet_ffi/build.rs @@ -0,0 +1,18 @@ +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)?; + + 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("public") + .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 00000000..51848f64 --- /dev/null +++ b/framework_dotnet_ffi/src/lib.rs @@ -0,0 +1,804 @@ +use std::convert::TryFrom; +use std::ptr; + +use framework_lib::chromium_ec::command::EcRequestRaw; +use framework_lib::chromium_ec::commands::{EcFeatureCode, EcRequestGetFeatures}; +use framework_lib::chromium_ec::{CrosEc, CrosEcDriverType, EcCurrentImage, EcError}; +use framework_lib::power::{ + self, ThermalSensorStatus, FAN_SLOT_COUNT, THERMAL_SENSOR_COUNT, +}; +use framework_lib::smbios; +use framework_lib::smbios::{Platform, PlatformFamily}; + +const BATTERY_TEXT_LEN: usize = 8; +#[cfg(target_os = "linux")] +const CROS_EC_DEV_PATH: &str = "/dev/cros_ec"; + +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 FrameworkStatus { + pub code: FrameworkStatusCode, + pub detail: i32, +} + +impl FrameworkStatus { + fn success() -> Self { + Self { + code: FrameworkStatusCode::Success, + detail: 0, + } + } + + fn with(code: FrameworkStatusCode, detail: i32) -> Self { + Self { code, detail } + } +} + +#[repr(i32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FrameworkEcDriver { + 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 From for CrosEcDriverType { + fn from(value: FrameworkEcDriver) -> Self { + match value { + FrameworkEcDriver::Portio => CrosEcDriverType::Portio, + FrameworkEcDriver::CrosEc => CrosEcDriverType::CrosEc, + FrameworkEcDriver::Windows => 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, +} + +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::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(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(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_rpm_0: u16, + pub fan_rpm_1: u16, + pub fan_rpm_2: u16, + pub fan_rpm_3: u16, + pub fan_present_0: u8, + pub fan_present_1: u8, + pub fan_present_2: u8, + pub fan_present_3: u8, + pub fan_stalled_0: u8, + pub fan_stalled_1: u8, + pub fan_stalled_2: u8, + pub fan_stalled_3: u8, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FrameworkFanCapabilities { + pub fan_count: u8, + pub supports_fan_control: u8, + pub supports_thermal_reporting: u8, + pub reserved: u8, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FrameworkPowerSnapshot { + pub ac_present: u8, + pub battery_present: u8, + pub discharging: u8, + pub charging: u8, + pub level_critical: u8, + pub battery_count: u8, + pub current_battery_index: u8, + pub reserved: u8, + 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: [u8; 8], + pub model_number: [u8; 8], + pub serial_number: [u8; 8], + pub battery_type: [u8; 8], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FrameworkEcFlashVersions { + pub current_image: FrameworkEcCurrentImage, + pub ro_version: [u8; 32], + pub rw_version: [u8; 32], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +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)); + } +} + +fn copy_text_bytes(text: &str) -> [u8; N] { + let mut buffer = [0u8; N]; + let bytes = text.as_bytes(); + let len = bytes.len().min(N); + buffer[..len].copy_from_slice(&bytes[..len]); + buffer +} + +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(_) => FrameworkStatus::with(FrameworkStatusCode::DeviceError, 0), + } +} + +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 { + CrosEc::with(driver.into()).is_some() +} + +#[no_mangle] +pub unsafe extern "C" fn framework_ec_open_default( + out_handle: *mut *mut FrameworkEcHandle, +) -> FrameworkStatus { + if out_handle.is_null() { + return FrameworkStatus::with(FrameworkStatusCode::NullPointer, 0); + } + + let Some(handle) = default_ec_handle() else { + *out_handle = ptr::null_mut(); + return FrameworkStatus::with(FrameworkStatusCode::NoDriverAvailable, 0); + }; + + if let Err(error) = handle.ec.check_mem_magic() { + *out_handle = ptr::null_mut(); + return status_from_error(error); + } + + *out_handle = Box::into_raw(Box::new(handle)); + FrameworkStatus::success() +} + +#[no_mangle] +pub unsafe extern "C" fn framework_ec_open_with_driver( + driver: FrameworkEcDriver, + out_handle: *mut *mut FrameworkEcHandle, +) -> FrameworkStatus { + if out_handle.is_null() { + return FrameworkStatus::with(FrameworkStatusCode::NullPointer, 0); + } + + let Some(ec) = CrosEc::with(driver.into()) else { + *out_handle = ptr::null_mut(); + return FrameworkStatus::with(FrameworkStatusCode::UnsupportedDriver, 0); + }; + + if let Err(error) = ec.check_mem_magic() { + *out_handle = ptr::null_mut(); + return status_from_error(error); + } + + *out_handle = Box::into_raw(Box::new(FrameworkEcHandle { ec, driver })); + FrameworkStatus::success() +} + +#[no_mangle] +pub unsafe extern "C" fn framework_ec_close(handle: *mut FrameworkEcHandle) { + if !handle.is_null() { + drop(Box::from_raw(handle)); + } +} + +#[no_mangle] +pub unsafe extern "C" fn framework_ec_get_active_driver( + handle: *const FrameworkEcHandle, + out_driver: *mut FrameworkEcDriver, +) -> FrameworkStatus { + if out_driver.is_null() { + return FrameworkStatus::with(FrameworkStatusCode::NullPointer, 0); + } + + let handle = match require_handle(handle) { + Ok(handle) => handle, + Err(status) => return status, + }; + + *out_driver = handle.driver; + FrameworkStatus::success() +} + +#[no_mangle] +pub unsafe extern "C" fn framework_get_platform( + out_platform: *mut FrameworkPlatform, +) -> FrameworkStatus { + if out_platform.is_null() { + return FrameworkStatus::with(FrameworkStatusCode::NullPointer, 0); + } + + *out_platform = smbios::get_platform() + .unwrap_or(Platform::UnknownSystem) + .into(); + FrameworkStatus::success() +} + +#[no_mangle] +pub unsafe extern "C" fn framework_get_platform_family( + out_family: *mut FrameworkPlatformFamily, +) -> FrameworkStatus { + if out_family.is_null() { + return FrameworkStatus::with(FrameworkStatusCode::NullPointer, 0); + } + + *out_family = smbios::get_family() + .map(FrameworkPlatformFamily::from) + .unwrap_or(FrameworkPlatformFamily::Unknown); + FrameworkStatus::success() +} + +#[no_mangle] +pub unsafe extern "C" fn framework_get_product_name( + out_buffer: *mut FrameworkByteBuffer, +) -> FrameworkStatus { + if out_buffer.is_null() { + return FrameworkStatus::with(FrameworkStatusCode::NullPointer, 0); + } + + let Some(product_name) = smbios::get_product_name() else { + *out_buffer = FrameworkByteBuffer::default(); + return FrameworkStatus::with(FrameworkStatusCode::DataUnavailable, 0); + }; + + *out_buffer = FrameworkByteBuffer::from_vec(product_name.into_bytes()); + FrameworkStatus::success() +} + +#[no_mangle] +pub unsafe extern "C" fn framework_ec_get_build_info( + handle: *const FrameworkEcHandle, + out_buffer: *mut FrameworkByteBuffer, +) -> FrameworkStatus { + if out_buffer.is_null() { + return FrameworkStatus::with(FrameworkStatusCode::NullPointer, 0); + } + + let handle = match require_handle(handle) { + Ok(handle) => handle, + Err(status) => return status, + }; + + match handle.ec.version_info() { + Ok(build_info) => { + *out_buffer = FrameworkByteBuffer::from_vec(build_info.into_bytes()); + FrameworkStatus::success() + } + Err(error) => { + *out_buffer = FrameworkByteBuffer::default(); + status_from_error(error) + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn framework_ec_get_flash_versions( + handle: *const FrameworkEcHandle, + out_versions: *mut FrameworkEcFlashVersions, +) -> FrameworkStatus { + if out_versions.is_null() { + return FrameworkStatus::with(FrameworkStatusCode::NullPointer, 0); + } + + let handle = match require_handle(handle) { + Ok(handle) => handle, + Err(status) => return status, + }; + + let Some((ro_version, rw_version, current_image)) = handle.ec.flash_version() else { + return FrameworkStatus::with(FrameworkStatusCode::DataUnavailable, 0); + }; + + *out_versions = FrameworkEcFlashVersions { + current_image: current_image.into(), + ro_version: copy_text_bytes(&ro_version), + rw_version: copy_text_bytes(&rw_version), + }; + + FrameworkStatus::success() +} + +#[no_mangle] +pub unsafe extern "C" fn framework_byte_buffer_free(buffer: FrameworkByteBuffer) { + buffer.destroy(); +} + +#[no_mangle] +pub unsafe extern "C" fn framework_ec_get_power_snapshot( + handle: *const FrameworkEcHandle, + out_snapshot: *mut FrameworkPowerSnapshot, +) -> FrameworkStatus { + if out_snapshot.is_null() { + return FrameworkStatus::with(FrameworkStatusCode::NullPointer, 0); + } + + let handle = match require_handle(handle) { + Ok(handle) => handle, + Err(status) => return status, + }; + + let Some(power_info) = power::power_info(&handle.ec) else { + return FrameworkStatus::with(FrameworkStatusCode::DataUnavailable, 0); + }; + + let mut snapshot = FrameworkPowerSnapshot { + ac_present: u8::from(power_info.ac_present), + battery_present: 0, + discharging: 0, + charging: 0, + level_critical: 0, + battery_count: 0, + current_battery_index: 0, + reserved: 0, + 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: [0; BATTERY_TEXT_LEN], + model_number: [0; BATTERY_TEXT_LEN], + serial_number: [0; BATTERY_TEXT_LEN], + battery_type: [0; BATTERY_TEXT_LEN], + }; + + if let Some(battery) = power_info.battery { + snapshot.battery_present = 1; + snapshot.discharging = u8::from(battery.discharging); + snapshot.charging = u8::from(battery.charging); + snapshot.level_critical = u8::from(battery.level_critical); + snapshot.battery_count = battery.battery_count; + snapshot.current_battery_index = battery.current_battery_index; + snapshot.present_voltage = battery.present_voltage; + snapshot.present_rate = battery.present_rate; + snapshot.remaining_capacity = battery.remaining_capacity; + snapshot.design_capacity = battery.design_capacity; + snapshot.design_voltage = battery.design_voltage; + snapshot.last_full_charge_capacity = battery.last_full_charge_capacity; + snapshot.cycle_count = battery.cycle_count; + snapshot.charge_percentage = battery.charge_percentage; + snapshot.manufacturer = copy_text_bytes(&battery.manufacturer); + snapshot.model_number = copy_text_bytes(&battery.model_number); + snapshot.serial_number = copy_text_bytes(&battery.serial_number); + snapshot.battery_type = copy_text_bytes(&battery.battery_type); + } + + *out_snapshot = snapshot; + FrameworkStatus::success() +} + +#[no_mangle] +pub unsafe extern "C" fn framework_ec_get_fan_capabilities( + handle: *const FrameworkEcHandle, + out_capabilities: *mut FrameworkFanCapabilities, +) -> FrameworkStatus { + if out_capabilities.is_null() { + return FrameworkStatus::with(FrameworkStatusCode::NullPointer, 0); + } + + let handle = match require_handle(handle) { + Ok(handle) => handle, + Err(status) => return status, + }; + + let fan_control = match feature_enabled(&handle.ec, EcFeatureCode::PwmFan) { + Ok(supported) => supported, + Err(status) => return status, + }; + let thermal = match feature_enabled(&handle.ec, EcFeatureCode::Thermal) { + Ok(supported) => supported, + Err(status) => return status, + }; + + let fan_count = power::thermal_snapshot(&handle.ec) + .map(|snapshot| snapshot.fan_count) + .unwrap_or(0); + + *out_capabilities = FrameworkFanCapabilities { + fan_count, + supports_fan_control: u8::from(fan_control), + supports_thermal_reporting: u8::from(thermal), + reserved: 0, + }; + + FrameworkStatus::success() +} + +#[no_mangle] +pub unsafe extern "C" fn framework_ec_get_thermal_snapshot( + handle: *const FrameworkEcHandle, + out_snapshot: *mut FrameworkThermalSnapshot, +) -> FrameworkStatus { + if out_snapshot.is_null() { + return FrameworkStatus::with(FrameworkStatusCode::NullPointer, 0); + } + + let handle = match require_handle(handle) { + Ok(handle) => handle, + Err(status) => return status, + }; + + let Some(snapshot) = power::thermal_snapshot(&handle.ec) else { + return FrameworkStatus::with(FrameworkStatusCode::DataUnavailable, 0); + }; + + let mut temperatures = [FrameworkTemperatureReading { + state: FrameworkTemperatureState::NotPresent, + celsius: 0, + reserved: 0, + }; 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]); + } + + *out_snapshot = 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_rpm_0: snapshot.fan_rpms[0], + fan_rpm_1: snapshot.fan_rpms[1], + fan_rpm_2: snapshot.fan_rpms[2], + fan_rpm_3: snapshot.fan_rpms[3], + fan_present_0: fan_present[0], + fan_present_1: fan_present[1], + fan_present_2: fan_present[2], + fan_present_3: fan_present[3], + fan_stalled_0: fan_stalled[0], + fan_stalled_1: fan_stalled[1], + fan_stalled_2: fan_stalled[2], + fan_stalled_3: fan_stalled[3], + }; + + FrameworkStatus::success() +} + +#[no_mangle] +pub unsafe extern "C" fn framework_ec_set_fan_rpm( + handle: *const FrameworkEcHandle, + fan_index: i32, + rpm: u32, +) -> FrameworkStatus { + let handle = match require_handle(handle) { + Ok(handle) => handle, + Err(status) => return status, + }; + let fan_index = match parse_optional_fan_index(fan_index) { + Ok(fan_index) => fan_index, + Err(status) => return status, + }; + + match handle.ec.fan_set_rpm(fan_index, rpm) { + Ok(()) => FrameworkStatus::success(), + Err(error) => status_from_error(error), + } +} + +#[no_mangle] +pub unsafe extern "C" fn framework_ec_set_fan_duty( + handle: *const FrameworkEcHandle, + fan_index: i32, + percent: u32, +) -> FrameworkStatus { + let handle = match require_handle(handle) { + Ok(handle) => handle, + Err(status) => return status, + }; + let fan_index = match parse_optional_fan_index(fan_index) { + Ok(fan_index) => fan_index, + Err(status) => return status, + }; + + match handle.ec.fan_set_duty(fan_index, percent) { + Ok(()) => FrameworkStatus::success(), + Err(error) => status_from_error(error), + } +} + +#[no_mangle] +pub unsafe extern "C" fn framework_ec_restore_auto_fan_control( + handle: *const FrameworkEcHandle, + fan_index: i32, +) -> FrameworkStatus { + let handle = match require_handle(handle) { + Ok(handle) => handle, + Err(status) => return status, + }; + let fan_index = match parse_optional_fan_index_u8(fan_index) { + Ok(fan_index) => fan_index, + Err(status) => return status, + }; + + match handle.ec.autofanctrl(fan_index) { + Ok(()) => FrameworkStatus::success(), + Err(error) => status_from_error(error), + } +} diff --git a/framework_lib/src/power.rs b/framework_lib/src/power.rs index 4b072082..c3d252dd 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(); From bef23de5e7d993b093fa61276fc3ef408c1b61e6 Mon Sep 17 00:00:00 2001 From: TekuSP Date: Mon, 20 Apr 2026 19:27:25 +0200 Subject: [PATCH 02/11] ci, readme: Integrate framework_dotnet_ffi into CI and documentation - Add build steps for the .NET interop crate to FreeBSD, Linux, and Windows CI jobs. - Configure CI to upload compiled native libraries and generated C# bindings as build artifacts. - Update README with build instructions and technical details for the optional .NET interop functionality. --- .github/workflows/ci.yml | 25 +++++++++++++++++++++++++ README.md | 25 +++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 143fa14d..bb417bcf 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/README.md b/README.md index 8ab5ad56..ab1baa87 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,33 @@ 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` + ## Install local package ``` From b37b868fc9fa56f60ab4bcd8b49d84202a9059b2 Mon Sep 17 00:00:00 2001 From: TekuSP Date: Mon, 20 Apr 2026 19:30:02 +0200 Subject: [PATCH 03/11] Formating fixes to work with Lint rules --- framework_dotnet_ffi/src/lib.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/framework_dotnet_ffi/src/lib.rs b/framework_dotnet_ffi/src/lib.rs index 51848f64..6382ba87 100644 --- a/framework_dotnet_ffi/src/lib.rs +++ b/framework_dotnet_ffi/src/lib.rs @@ -4,9 +4,7 @@ use std::ptr; use framework_lib::chromium_ec::command::EcRequestRaw; use framework_lib::chromium_ec::commands::{EcFeatureCode, EcRequestGetFeatures}; use framework_lib::chromium_ec::{CrosEc, CrosEcDriverType, EcCurrentImage, EcError}; -use framework_lib::power::{ - self, ThermalSensorStatus, FAN_SLOT_COUNT, THERMAL_SENSOR_COUNT, -}; +use framework_lib::power::{self, ThermalSensorStatus, FAN_SLOT_COUNT, THERMAL_SENSOR_COUNT}; use framework_lib::smbios; use framework_lib::smbios::{Platform, PlatformFamily}; @@ -110,9 +108,7 @@ impl From for FrameworkPlatform { Platform::Framework13AmdAi300 => FrameworkPlatform::Framework13AmdAi300, Platform::Framework16Amd7080 => FrameworkPlatform::Framework16Amd7080, Platform::Framework16AmdAi300 => FrameworkPlatform::Framework16AmdAi300, - Platform::FrameworkDesktopAmdAiMax300 => { - FrameworkPlatform::FrameworkDesktopAmdAiMax300 - } + Platform::FrameworkDesktopAmdAiMax300 => FrameworkPlatform::FrameworkDesktopAmdAiMax300, Platform::GenericFramework(..) => FrameworkPlatform::GenericFramework, Platform::UnknownSystem => FrameworkPlatform::UnknownSystem, } @@ -365,7 +361,9 @@ fn feature_enabled(ec: &CrosEc, feature: EcFeatureCode) -> Result(handle: *const FrameworkEcHandle) -> Result<&'a FrameworkEcHandle, FrameworkStatus> { +fn require_handle<'a>( + handle: *const FrameworkEcHandle, +) -> Result<&'a FrameworkEcHandle, FrameworkStatus> { if handle.is_null() { return Err(FrameworkStatus::with(FrameworkStatusCode::NullPointer, 0)); } From 4eb72fa75caf1f198c5c39d60f2682a07b4409e4 Mon Sep 17 00:00:00 2001 From: TekuSP Date: Mon, 20 Apr 2026 19:38:25 +0200 Subject: [PATCH 04/11] Clippy fixes so Rust is happy --- framework_lib/src/commandline/mod.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/framework_lib/src/commandline/mod.rs b/framework_lib/src/commandline/mod.rs index 7114c7a9..2257cf04 100644 --- a/framework_lib/src/commandline/mod.rs +++ b/framework_lib/src/commandline/mod.rs @@ -1250,21 +1250,21 @@ fn compare_version(device: Option, version: String, ec: &Cro if let Some(esrt) = esrt::get_esrt() { for entry in &esrt.entries { match GUID::from(entry.fw_class) { - esrt::TGL_RETIMER01_GUID | esrt::ADL_RETIMER01_GUID | esrt::RPL_RETIMER01_GUID => { - if device == Some(HardwareDeviceType::RTM01) { - println!("Comparing RTM01 version {:?}", entry.fw_version.to_string()); + esrt::TGL_RETIMER01_GUID | esrt::ADL_RETIMER01_GUID | esrt::RPL_RETIMER01_GUID + if device == Some(HardwareDeviceType::RTM01) => + { + println!("Comparing RTM01 version {:?}", entry.fw_version.to_string()); - if entry.fw_version.to_string().contains(&version) { - return 0; - } + if entry.fw_version.to_string().contains(&version) { + return 0; } } - esrt::TGL_RETIMER23_GUID | esrt::ADL_RETIMER23_GUID | esrt::RPL_RETIMER23_GUID => { - if device == Some(HardwareDeviceType::RTM23) { - println!("Comparing RTM23 version {:?}", entry.fw_version.to_string()); - if entry.fw_version.to_string().contains(&version) { - return 0; - } + esrt::TGL_RETIMER23_GUID | esrt::ADL_RETIMER23_GUID | esrt::RPL_RETIMER23_GUID + if device == Some(HardwareDeviceType::RTM23) => + { + println!("Comparing RTM23 version {:?}", entry.fw_version.to_string()); + if entry.fw_version.to_string().contains(&version) { + return 0; } } _ => {} From cdfd9b467a380e570803ac74a12eef06d0d76f6b Mon Sep 17 00:00:00 2001 From: TekuSP Date: Mon, 20 Apr 2026 20:59:34 +0200 Subject: [PATCH 05/11] framework_dotnet_ffi: Make generated C# class internal Signed-off-by: TekuSP --- framework_dotnet_ffi/build.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework_dotnet_ffi/build.rs b/framework_dotnet_ffi/build.rs index 63197ae3..fde48c9b 100644 --- a/framework_dotnet_ffi/build.rs +++ b/framework_dotnet_ffi/build.rs @@ -11,7 +11,7 @@ fn main() -> Result<(), Box> { .csharp_dll_name("framework_dotnet_ffi") .csharp_namespace("Framework.System.Interop") .csharp_class_name("NativeMethods") - .csharp_class_accessibility("public") + .csharp_class_accessibility("internal") .generate_csharp_file(output_dir.join("NativeMethods.g.cs"))?; Ok(()) From df6a62eb96eb17695dca7580cb3f0e0aeb8d356e Mon Sep 17 00:00:00 2001 From: TekuSP Date: Sun, 26 Apr 2026 22:52:58 +0200 Subject: [PATCH 06/11] framework_dotnet_ffi: Enhance FFI error reporting and return ergonomics Refactors the FFI boundary to provide more comprehensive and structured error reporting, and to consolidate function return values. - The `FrameworkStatus` struct now uses a union to provide specific error details (e.g., invalid fan index, detailed EC response codes, device error messages). - FFI functions now return dedicated `Result` structs that combine status and data, simplifying consumption from .NET. - Introduces functions to retrieve human-readable descriptions for status codes and device errors. - Updates `build.rs` to ensure `csbindgen` reruns when `src/lib.rs` changes, improving build reliability. --- framework_dotnet_ffi/build.rs | 1 + framework_dotnet_ffi/src/lib.rs | 1061 +++++++++++++++++++++++++------ 2 files changed, 854 insertions(+), 208 deletions(-) diff --git a/framework_dotnet_ffi/build.rs b/framework_dotnet_ffi/build.rs index fde48c9b..b49c258c 100644 --- a/framework_dotnet_ffi/build.rs +++ b/framework_dotnet_ffi/build.rs @@ -5,6 +5,7 @@ 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") diff --git a/framework_dotnet_ffi/src/lib.rs b/framework_dotnet_ffi/src/lib.rs index 6382ba87..78b2cbc3 100644 --- a/framework_dotnet_ffi/src/lib.rs +++ b/framework_dotnet_ffi/src/lib.rs @@ -1,17 +1,26 @@ +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}; +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 BATTERY_TEXT_LEN: usize = 8; +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, @@ -33,27 +42,209 @@ pub enum FrameworkStatusCode { #[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 detail: i32, + 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: FrameworkStatusCode::Success, - detail: 0, + code, + payload: FrameworkStatusPayload { + none: FrameworkStatusNoPayload { reserved: 0 }, + }, } } - fn with(code: FrameworkStatusCode, detail: i32) -> Self { - Self { code, detail } + 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, @@ -69,12 +260,15 @@ impl From for FrameworkEcDriver { } } -impl From for CrosEcDriverType { - fn from(value: FrameworkEcDriver) -> Self { +impl TryFrom for CrosEcDriverType { + type Error = (); + + fn try_from(value: FrameworkEcDriver) -> Result { match value { - FrameworkEcDriver::Portio => CrosEcDriverType::Portio, - FrameworkEcDriver::CrosEc => CrosEcDriverType::CrosEc, - FrameworkEcDriver::Windows => CrosEcDriverType::Windows, + FrameworkEcDriver::Unknown => Err(()), + FrameworkEcDriver::Portio => Ok(CrosEcDriverType::Portio), + FrameworkEcDriver::CrosEc => Ok(CrosEcDriverType::CrosEc), + FrameworkEcDriver::Windows => Ok(CrosEcDriverType::Windows), } } } @@ -136,6 +330,20 @@ impl From for FrameworkPlatformFamily { } } +#[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 { @@ -295,6 +503,99 @@ impl FrameworkByteBuffer { } } +#[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 copy_text_bytes(text: &str) -> [u8; N] { let mut buffer = [0u8; N]; let bytes = text.as_bytes(); @@ -303,6 +604,326 @@ fn copy_text_bytes(text: &str) -> [u8; N] { buffer } +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: [0; 32], + rw_version: [0; 32], + } +} + +fn default_power_snapshot() -> FrameworkPowerSnapshot { + FrameworkPowerSnapshot { + ac_present: 0, + battery_present: 0, + discharging: 0, + charging: 0, + level_critical: 0, + battery_count: 0, + current_battery_index: 0, + reserved: 0, + 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: [0; BATTERY_TEXT_LEN], + model_number: [0; BATTERY_TEXT_LEN], + serial_number: [0; BATTERY_TEXT_LEN], + battery_type: [0; BATTERY_TEXT_LEN], + } +} + +fn default_fan_capabilities() -> FrameworkFanCapabilities { + FrameworkFanCapabilities { + fan_count: 0, + supports_fan_control: 0, + supports_thermal_reporting: 0, + reserved: 0, + } +} + +fn default_temperature_reading() -> FrameworkTemperatureReading { + FrameworkTemperatureReading { + state: FrameworkTemperatureState::NotPresent, + celsius: 0, + reserved: 0, + } +} + +fn default_thermal_snapshot() -> FrameworkThermalSnapshot { + let reading = default_temperature_reading(); + + FrameworkThermalSnapshot { + fan_count: 0, + reserved: [0; 3], + temperature_0: reading, + temperature_1: reading, + temperature_2: reading, + temperature_3: reading, + temperature_4: reading, + temperature_5: reading, + temperature_6: reading, + temperature_7: reading, + fan_rpm_0: 0, + fan_rpm_1: 0, + fan_rpm_2: 0, + fan_rpm_3: 0, + fan_present_0: 0, + fan_present_1: 0, + fan_present_2: 0, + fan_present_3: 0, + fan_stalled_0: 0, + fan_stalled_1: 0, + fan_stalled_2: 0, + fan_stalled_3: 0, + } +} + +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) => { @@ -312,7 +933,10 @@ fn status_from_error(error: EcError) -> FrameworkStatus { FrameworkStatusCode::UnknownResponseCode, i32::try_from(code).unwrap_or(i32::MAX), ), - EcError::DeviceError(_) => FrameworkStatus::with(FrameworkStatusCode::DeviceError, 0), + EcError::DeviceError(message) => { + let detail = store_device_error_message(message); + FrameworkStatus::with(FrameworkStatusCode::DeviceError, detail) + } } } @@ -394,55 +1018,95 @@ fn parse_optional_fan_index_u8(fan_index: i32) -> Result, FrameworkSt #[no_mangle] pub extern "C" fn framework_ec_driver_is_supported(driver: FrameworkEcDriver) -> bool { - CrosEc::with(driver.into()).is_some() + let Ok(driver) = CrosEcDriverType::try_from(driver) else { + return false; + }; + + CrosEc::with(driver).is_some() } #[no_mangle] -pub unsafe extern "C" fn framework_ec_open_default( - out_handle: *mut *mut FrameworkEcHandle, -) -> FrameworkStatus { - if out_handle.is_null() { - return FrameworkStatus::with(FrameworkStatusCode::NullPointer, 0); - } +/// 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 { - *out_handle = ptr::null_mut(); - return FrameworkStatus::with(FrameworkStatusCode::NoDriverAvailable, 0); + return ec_handle_result( + FrameworkStatus::with(FrameworkStatusCode::NoDriverAvailable, 0), + ptr::null_mut(), + ); }; if let Err(error) = handle.ec.check_mem_magic() { - *out_handle = ptr::null_mut(); - return status_from_error(error); + return ec_handle_result(status_from_error(error), ptr::null_mut()); } - *out_handle = Box::into_raw(Box::new(handle)); - FrameworkStatus::success() + ec_handle_result(FrameworkStatus::success(), Box::into_raw(Box::new(handle))) } #[no_mangle] -pub unsafe extern "C" fn framework_ec_open_with_driver( +pub extern "C" fn framework_ec_open_with_driver( driver: FrameworkEcDriver, - out_handle: *mut *mut FrameworkEcHandle, -) -> FrameworkStatus { - if out_handle.is_null() { - return FrameworkStatus::with(FrameworkStatusCode::NullPointer, 0); - } +) -> 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.into()) else { - *out_handle = ptr::null_mut(); - return FrameworkStatus::with(FrameworkStatusCode::UnsupportedDriver, 0); + 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() { - *out_handle = ptr::null_mut(); - return status_from_error(error); + return ec_handle_result(status_from_error(error), ptr::null_mut()); } - *out_handle = Box::into_raw(Box::new(FrameworkEcHandle { ec, driver })); - FrameworkStatus::success() + 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)); @@ -450,165 +1114,134 @@ pub unsafe extern "C" fn framework_ec_close(handle: *mut FrameworkEcHandle) { } #[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, - out_driver: *mut FrameworkEcDriver, -) -> FrameworkStatus { - if out_driver.is_null() { - return FrameworkStatus::with(FrameworkStatusCode::NullPointer, 0); - } - +) -> FrameworkEcActiveDriverResult { let handle = match require_handle(handle) { Ok(handle) => handle, - Err(status) => return status, + Err(status) => return active_driver_result(status, FrameworkEcDriver::Unknown), }; - *out_driver = handle.driver; - FrameworkStatus::success() + active_driver_result(FrameworkStatus::success(), handle.driver) } #[no_mangle] -pub unsafe extern "C" fn framework_get_platform( - out_platform: *mut FrameworkPlatform, -) -> FrameworkStatus { - if out_platform.is_null() { - return FrameworkStatus::with(FrameworkStatusCode::NullPointer, 0); +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, + ), } - - *out_platform = smbios::get_platform() - .unwrap_or(Platform::UnknownSystem) - .into(); - FrameworkStatus::success() } #[no_mangle] -pub unsafe extern "C" fn framework_get_platform_family( - out_family: *mut FrameworkPlatformFamily, -) -> FrameworkStatus { - if out_family.is_null() { - return FrameworkStatus::with(FrameworkStatusCode::NullPointer, 0); +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, + ), } - - *out_family = smbios::get_family() - .map(FrameworkPlatformFamily::from) - .unwrap_or(FrameworkPlatformFamily::Unknown); - FrameworkStatus::success() } #[no_mangle] -pub unsafe extern "C" fn framework_get_product_name( - out_buffer: *mut FrameworkByteBuffer, -) -> FrameworkStatus { - if out_buffer.is_null() { - return FrameworkStatus::with(FrameworkStatusCode::NullPointer, 0); - } - +pub extern "C" fn framework_get_product_name() -> FrameworkProductNameResult { let Some(product_name) = smbios::get_product_name() else { - *out_buffer = FrameworkByteBuffer::default(); - return FrameworkStatus::with(FrameworkStatusCode::DataUnavailable, 0); + return product_name_result( + FrameworkStatus::with(FrameworkStatusCode::DataUnavailable, 0), + FrameworkByteBuffer::default(), + ); }; - *out_buffer = FrameworkByteBuffer::from_vec(product_name.into_bytes()); - FrameworkStatus::success() + 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, - out_buffer: *mut FrameworkByteBuffer, -) -> FrameworkStatus { - if out_buffer.is_null() { - return FrameworkStatus::with(FrameworkStatusCode::NullPointer, 0); - } - +) -> FrameworkEcBuildInfoResult { let handle = match require_handle(handle) { Ok(handle) => handle, - Err(status) => return status, + Err(status) => return build_info_result(status, FrameworkByteBuffer::default()), }; match handle.ec.version_info() { - Ok(build_info) => { - *out_buffer = FrameworkByteBuffer::from_vec(build_info.into_bytes()); - FrameworkStatus::success() - } - Err(error) => { - *out_buffer = FrameworkByteBuffer::default(); - status_from_error(error) - } + 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, - out_versions: *mut FrameworkEcFlashVersions, -) -> FrameworkStatus { - if out_versions.is_null() { - return FrameworkStatus::with(FrameworkStatusCode::NullPointer, 0); - } - +) -> FrameworkEcFlashVersionsResult { let handle = match require_handle(handle) { Ok(handle) => handle, - Err(status) => return status, + 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 FrameworkStatus::with(FrameworkStatusCode::DataUnavailable, 0); - }; - - *out_versions = FrameworkEcFlashVersions { - current_image: current_image.into(), - ro_version: copy_text_bytes(&ro_version), - rw_version: copy_text_bytes(&rw_version), + return flash_versions_result( + FrameworkStatus::with(FrameworkStatusCode::DataUnavailable, 0), + default_ec_flash_versions(), + ); }; - FrameworkStatus::success() + flash_versions_result( + FrameworkStatus::success(), + FrameworkEcFlashVersions { + current_image: current_image.into(), + ro_version: copy_text_bytes(&ro_version), + rw_version: copy_text_bytes(&rw_version), + }, + ) } #[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, - out_snapshot: *mut FrameworkPowerSnapshot, -) -> FrameworkStatus { - if out_snapshot.is_null() { - return FrameworkStatus::with(FrameworkStatusCode::NullPointer, 0); - } - +) -> FrameworkEcPowerSnapshotResult { let handle = match require_handle(handle) { Ok(handle) => handle, - Err(status) => return status, + Err(status) => return power_snapshot_result(status, default_power_snapshot()), }; let Some(power_info) = power::power_info(&handle.ec) else { - return FrameworkStatus::with(FrameworkStatusCode::DataUnavailable, 0); + return power_snapshot_result( + FrameworkStatus::with(FrameworkStatusCode::DataUnavailable, 0), + default_power_snapshot(), + ); }; let mut snapshot = FrameworkPowerSnapshot { ac_present: u8::from(power_info.ac_present), - battery_present: 0, - discharging: 0, - charging: 0, - level_critical: 0, - battery_count: 0, - current_battery_index: 0, - reserved: 0, - 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: [0; BATTERY_TEXT_LEN], - model_number: [0; BATTERY_TEXT_LEN], - serial_number: [0; BATTERY_TEXT_LEN], - battery_type: [0; BATTERY_TEXT_LEN], + ..default_power_snapshot() }; if let Some(battery) = power_info.battery { @@ -632,70 +1265,63 @@ pub unsafe extern "C" fn framework_ec_get_power_snapshot( snapshot.battery_type = copy_text_bytes(&battery.battery_type); } - *out_snapshot = snapshot; - FrameworkStatus::success() + 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, - out_capabilities: *mut FrameworkFanCapabilities, -) -> FrameworkStatus { - if out_capabilities.is_null() { - return FrameworkStatus::with(FrameworkStatusCode::NullPointer, 0); - } - +) -> FrameworkEcFanCapabilitiesResult { let handle = match require_handle(handle) { Ok(handle) => handle, - Err(status) => return status, + 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 status, + 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 status, + 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); - *out_capabilities = FrameworkFanCapabilities { - fan_count, - supports_fan_control: u8::from(fan_control), - supports_thermal_reporting: u8::from(thermal), - reserved: 0, - }; - - FrameworkStatus::success() + fan_capabilities_result( + FrameworkStatus::success(), + FrameworkFanCapabilities { + fan_count, + supports_fan_control: u8::from(fan_control), + supports_thermal_reporting: u8::from(thermal), + reserved: 0, + }, + ) } #[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, - out_snapshot: *mut FrameworkThermalSnapshot, -) -> FrameworkStatus { - if out_snapshot.is_null() { - return FrameworkStatus::with(FrameworkStatusCode::NullPointer, 0); - } - +) -> FrameworkEcThermalSnapshotResult { let handle = match require_handle(handle) { Ok(handle) => handle, - Err(status) => return status, + Err(status) => return thermal_snapshot_result(status, default_thermal_snapshot()), }; let Some(snapshot) = power::thermal_snapshot(&handle.ec) else { - return FrameworkStatus::with(FrameworkStatusCode::DataUnavailable, 0); + return thermal_snapshot_result( + FrameworkStatus::with(FrameworkStatusCode::DataUnavailable, 0), + default_thermal_snapshot(), + ); }; - let mut temperatures = [FrameworkTemperatureReading { - state: FrameworkTemperatureState::NotPresent, - celsius: 0, - reserved: 0, - }; THERMAL_SENSOR_COUNT]; + let mut temperatures = [default_temperature_reading(); THERMAL_SENSOR_COUNT]; for (index, reading) in snapshot.temperatures.iter().enumerate() { temperatures[index] = FrameworkTemperatureReading { state: reading.status.into(), @@ -711,92 +1337,111 @@ pub unsafe extern "C" fn framework_ec_get_thermal_snapshot( fan_stalled[index] = u8::from(snapshot.fan_stalled[index]); } - *out_snapshot = 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_rpm_0: snapshot.fan_rpms[0], - fan_rpm_1: snapshot.fan_rpms[1], - fan_rpm_2: snapshot.fan_rpms[2], - fan_rpm_3: snapshot.fan_rpms[3], - fan_present_0: fan_present[0], - fan_present_1: fan_present[1], - fan_present_2: fan_present[2], - fan_present_3: fan_present[3], - fan_stalled_0: fan_stalled[0], - fan_stalled_1: fan_stalled[1], - fan_stalled_2: fan_stalled[2], - fan_stalled_3: fan_stalled[3], - }; - - FrameworkStatus::success() + 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_rpm_0: snapshot.fan_rpms[0], + fan_rpm_1: snapshot.fan_rpms[1], + fan_rpm_2: snapshot.fan_rpms[2], + fan_rpm_3: snapshot.fan_rpms[3], + fan_present_0: fan_present[0], + fan_present_1: fan_present[1], + fan_present_2: fan_present[2], + fan_present_3: fan_present[3], + fan_stalled_0: fan_stalled[0], + fan_stalled_1: fan_stalled[1], + fan_stalled_2: fan_stalled[2], + fan_stalled_3: fan_stalled[3], + }, + ) } #[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, -) -> FrameworkStatus { +) -> FrameworkEcSetFanRpmResult { + let requested_fan_index = fan_index; + let handle = match require_handle(handle) { Ok(handle) => handle, - Err(status) => return status, + 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 status, + Err(status) => return set_fan_rpm_result(status, requested_fan_index, rpm), }; - match handle.ec.fan_set_rpm(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, -) -> FrameworkStatus { +) -> FrameworkEcSetFanDutyResult { + let requested_fan_index = fan_index; + let handle = match require_handle(handle) { Ok(handle) => handle, - Err(status) => return status, + 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 status, + Err(status) => return set_fan_duty_result(status, requested_fan_index, percent), }; - match handle.ec.fan_set_duty(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, -) -> FrameworkStatus { +) -> FrameworkEcRestoreAutoFanControlResult { + let requested_fan_index = fan_index; + let handle = match require_handle(handle) { Ok(handle) => handle, - Err(status) => return status, + 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 status, + Err(status) => return restore_auto_fan_control_result(status, requested_fan_index), }; - match handle.ec.autofanctrl(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) } From 3ab184dbc6af5e9db0f4b5ae5819cd751df02399 Mon Sep 17 00:00:00 2001 From: TekuSP Date: Sun, 26 Apr 2026 23:02:27 +0200 Subject: [PATCH 07/11] framework_dotnet_ffi: Add Intel Core Ultra 3 platform --- framework_dotnet_ffi/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/framework_dotnet_ffi/src/lib.rs b/framework_dotnet_ffi/src/lib.rs index 78b2cbc3..d886aff9 100644 --- a/framework_dotnet_ffi/src/lib.rs +++ b/framework_dotnet_ffi/src/lib.rs @@ -288,6 +288,7 @@ pub enum FrameworkPlatform { FrameworkDesktopAmdAiMax300 = 9, GenericFramework = 10, UnknownSystem = 11, + IntelCoreUltra3 = 12, } impl From for FrameworkPlatform { @@ -298,6 +299,7 @@ impl From for FrameworkPlatform { 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, From 510768abce17492cd26e523d976bd9d226e675ce Mon Sep 17 00:00:00 2001 From: TekuSP Date: Sun, 26 Apr 2026 23:12:53 +0200 Subject: [PATCH 08/11] Attempt to fix snap --- snap/snapcraft.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 0ac55997..f34094eb 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: From 654f4deab5326fce6af9aa14a4621478a80b0ffb Mon Sep 17 00:00:00 2001 From: TekuSP Date: Mon, 27 Apr 2026 02:00:00 +0200 Subject: [PATCH 09/11] framework_dotnet_ffi: Refactor fan data representation in thermal snapshot Consolidates fan RPM, presence, and stalled status into a `FrameworkFanReading` struct, leveraging a new `FrameworkFanState` enum. This improves data encapsulation and provides clearer, type-safe status information for each fan within the `FrameworkThermalSnapshot`. --- framework_dotnet_ffi/src/lib.rs | 117 ++++++++++++++++++++------------ 1 file changed, 72 insertions(+), 45 deletions(-) diff --git a/framework_dotnet_ffi/src/lib.rs b/framework_dotnet_ffi/src/lib.rs index d886aff9..c6ece3f3 100644 --- a/framework_dotnet_ffi/src/lib.rs +++ b/framework_dotnet_ffi/src/lib.rs @@ -394,6 +394,22 @@ pub struct FrameworkTemperatureReading { pub reserved: u16, } +#[repr(i32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FrameworkFanState { + Ok = 0, + NotPresent = 1, + Stalled = 2, +} + +#[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 { @@ -407,18 +423,10 @@ pub struct FrameworkThermalSnapshot { pub temperature_5: FrameworkTemperatureReading, pub temperature_6: FrameworkTemperatureReading, pub temperature_7: FrameworkTemperatureReading, - pub fan_rpm_0: u16, - pub fan_rpm_1: u16, - pub fan_rpm_2: u16, - pub fan_rpm_3: u16, - pub fan_present_0: u8, - pub fan_present_1: u8, - pub fan_present_2: u8, - pub fan_present_3: u8, - pub fan_stalled_0: u8, - pub fan_stalled_1: u8, - pub fan_stalled_2: u8, - pub fan_stalled_3: u8, + pub fan_0: FrameworkFanReading, + pub fan_1: FrameworkFanReading, + pub fan_2: FrameworkFanReading, + pub fan_3: FrameworkFanReading, } #[repr(C)] @@ -729,32 +737,43 @@ fn default_temperature_reading() -> FrameworkTemperatureReading { } } +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 reading = default_temperature_reading(); + let temperature = default_temperature_reading(); + let fan = default_fan_reading(); FrameworkThermalSnapshot { fan_count: 0, reserved: [0; 3], - temperature_0: reading, - temperature_1: reading, - temperature_2: reading, - temperature_3: reading, - temperature_4: reading, - temperature_5: reading, - temperature_6: reading, - temperature_7: reading, - fan_rpm_0: 0, - fan_rpm_1: 0, - fan_rpm_2: 0, - fan_rpm_3: 0, - fan_present_0: 0, - fan_present_1: 0, - fan_present_2: 0, - fan_present_3: 0, - fan_stalled_0: 0, - fan_stalled_1: 0, - fan_stalled_2: 0, - fan_stalled_3: 0, + 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, } } @@ -1352,18 +1371,26 @@ pub unsafe extern "C" fn framework_ec_get_thermal_snapshot( temperature_5: temperatures[5], temperature_6: temperatures[6], temperature_7: temperatures[7], - fan_rpm_0: snapshot.fan_rpms[0], - fan_rpm_1: snapshot.fan_rpms[1], - fan_rpm_2: snapshot.fan_rpms[2], - fan_rpm_3: snapshot.fan_rpms[3], - fan_present_0: fan_present[0], - fan_present_1: fan_present[1], - fan_present_2: fan_present[2], - fan_present_3: fan_present[3], - fan_stalled_0: fan_stalled[0], - fan_stalled_1: fan_stalled[1], - fan_stalled_2: fan_stalled[2], - fan_stalled_3: fan_stalled[3], + 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, + }, }, ) } From 903a3c44ea8cf3463c4556108b60386b4db23c70 Mon Sep 17 00:00:00 2001 From: TekuSP Date: Mon, 27 Apr 2026 02:32:34 +0200 Subject: [PATCH 10/11] framework_dotnet_ffi: Refactor into C# oriented objects Modernizes the FFI interfaces by introducing type-safe enums for various states and capabilities, and by enabling dynamic length strings for textual data. - Replaces boolean flags with dedicated enums (`FrameworkFanFeaturesState`, `FrameworkPowerSourceState`, `FrameworkBatteryState`) for clearer and more robust state representation in fan capabilities and power snapshots. - Adopts `FrameworkByteBuffer` for variable-length strings in battery details (manufacturer, model, etc.) and EC flash versions, eliminating fixed-size buffer limitations. - Restructures `FrameworkPowerSnapshot` to embed a new `FrameworkBatterySnapshot`, providing a more organized and extensible data model for power and battery information. --- framework_dotnet_ffi/src/lib.rs | 200 +++++++++++++++++++++----------- 1 file changed, 130 insertions(+), 70 deletions(-) diff --git a/framework_dotnet_ffi/src/lib.rs b/framework_dotnet_ffi/src/lib.rs index c6ece3f3..726258c6 100644 --- a/framework_dotnet_ffi/src/lib.rs +++ b/framework_dotnet_ffi/src/lib.rs @@ -13,7 +13,6 @@ use framework_lib::power::{self, ThermalSensorStatus, FAN_SLOT_COUNT, THERMAL_SE use framework_lib::smbios; use framework_lib::smbios::{Platform, PlatformFamily}; -const BATTERY_TEXT_LEN: usize = 8; const STORED_DEVICE_ERROR_LIMIT: usize = 64; #[cfg(target_os = "linux")] const CROS_EC_DEV_PATH: &str = "/dev/cros_ec"; @@ -402,6 +401,15 @@ pub enum FrameworkFanState { 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 { @@ -433,22 +441,35 @@ pub struct FrameworkThermalSnapshot { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct FrameworkFanCapabilities { pub fan_count: u8, - pub supports_fan_control: u8, - pub supports_thermal_reporting: u8, - pub reserved: 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 FrameworkPowerSnapshot { - pub ac_present: u8, - pub battery_present: u8, - pub discharging: u8, - pub charging: u8, - pub level_critical: u8, - pub battery_count: u8, - pub current_battery_index: u8, - pub reserved: u8, +pub struct FrameworkBatterySnapshot { + pub battery_state: FrameworkBatteryState, + pub reserved: [u8; 3], pub present_voltage: u32, pub present_rate: u32, pub remaining_capacity: u32, @@ -457,22 +478,31 @@ pub struct FrameworkPowerSnapshot { pub last_full_charge_capacity: u32, pub cycle_count: u32, pub charge_percentage: u32, - pub manufacturer: [u8; 8], - pub model_number: [u8; 8], - pub serial_number: [u8; 8], - pub battery_type: [u8; 8], + 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: [u8; 32], - pub rw_version: [u8; 32], + pub ro_version: FrameworkByteBuffer, + pub rw_version: FrameworkByteBuffer, } #[repr(C)] -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct FrameworkByteBuffer { pub ptr: *mut u8, pub length: i32, @@ -606,14 +636,6 @@ pub struct FrameworkEcRestoreAutoFanControlResult { pub fan_index: i32, } -fn copy_text_bytes(text: &str) -> [u8; N] { - let mut buffer = [0u8; N]; - let bytes = text.as_bytes(); - let len = bytes.len().min(N); - buffer[..len].copy_from_slice(&bytes[..len]); - buffer -} - fn device_error_messages() -> &'static Mutex> { DEVICE_ERROR_MESSAGES.get_or_init(|| Mutex::new(VecDeque::new())) } @@ -690,21 +712,15 @@ fn ec_response_detail_name(detail: FrameworkEcResponseDetail) -> &'static str { fn default_ec_flash_versions() -> FrameworkEcFlashVersions { FrameworkEcFlashVersions { current_image: FrameworkEcCurrentImage::Unknown, - ro_version: [0; 32], - rw_version: [0; 32], + ro_version: FrameworkByteBuffer::default(), + rw_version: FrameworkByteBuffer::default(), } } -fn default_power_snapshot() -> FrameworkPowerSnapshot { - FrameworkPowerSnapshot { - ac_present: 0, - battery_present: 0, - discharging: 0, - charging: 0, - level_critical: 0, - battery_count: 0, - current_battery_index: 0, - reserved: 0, +fn default_battery_snapshot() -> FrameworkBatterySnapshot { + FrameworkBatterySnapshot { + battery_state: FrameworkBatteryState::NotPresent, + reserved: [0; 3], present_voltage: 0, present_rate: 0, remaining_capacity: 0, @@ -713,19 +729,61 @@ fn default_power_snapshot() -> FrameworkPowerSnapshot { last_full_charge_capacity: 0, cycle_count: 0, charge_percentage: 0, - manufacturer: [0; BATTERY_TEXT_LEN], - model_number: [0; BATTERY_TEXT_LEN], - serial_number: [0; BATTERY_TEXT_LEN], - battery_type: [0; BATTERY_TEXT_LEN], + 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, - supports_fan_control: 0, - supports_thermal_reporting: 0, - reserved: 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, + } } } @@ -1228,8 +1286,8 @@ pub unsafe extern "C" fn framework_ec_get_flash_versions( FrameworkStatus::success(), FrameworkEcFlashVersions { current_image: current_image.into(), - ro_version: copy_text_bytes(&ro_version), - rw_version: copy_text_bytes(&rw_version), + ro_version: FrameworkByteBuffer::from_vec(ro_version.into_bytes()), + rw_version: FrameworkByteBuffer::from_vec(rw_version.into_bytes()), }, ) } @@ -1261,29 +1319,32 @@ pub unsafe extern "C" fn framework_ec_get_power_snapshot( }; let mut snapshot = FrameworkPowerSnapshot { - ac_present: u8::from(power_info.ac_present), + 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_present = 1; - snapshot.discharging = u8::from(battery.discharging); - snapshot.charging = u8::from(battery.charging); - snapshot.level_critical = u8::from(battery.level_critical); snapshot.battery_count = battery.battery_count; - snapshot.current_battery_index = battery.current_battery_index; - snapshot.present_voltage = battery.present_voltage; - snapshot.present_rate = battery.present_rate; - snapshot.remaining_capacity = battery.remaining_capacity; - snapshot.design_capacity = battery.design_capacity; - snapshot.design_voltage = battery.design_voltage; - snapshot.last_full_charge_capacity = battery.last_full_charge_capacity; - snapshot.cycle_count = battery.cycle_count; - snapshot.charge_percentage = battery.charge_percentage; - snapshot.manufacturer = copy_text_bytes(&battery.manufacturer); - snapshot.model_number = copy_text_bytes(&battery.model_number); - snapshot.serial_number = copy_text_bytes(&battery.serial_number); - snapshot.battery_type = copy_text_bytes(&battery.battery_type); + 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) @@ -1317,9 +1378,8 @@ pub unsafe extern "C" fn framework_ec_get_fan_capabilities( FrameworkStatus::success(), FrameworkFanCapabilities { fan_count, - supports_fan_control: u8::from(fan_control), - supports_thermal_reporting: u8::from(thermal), - reserved: 0, + features: fan_features_state(fan_control, thermal), + reserved: [0; 2], }, ) } From 757f5ef5b0be91abf275a9ffbd60df91f69fcace Mon Sep 17 00:00:00 2001 From: TekuSP Date: Mon, 27 Apr 2026 20:49:52 +0200 Subject: [PATCH 11/11] framework_dotnet_ffi: Update documentation --- README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.md b/README.md index 791e7d47..5094f99e 100644 --- a/README.md +++ b/README.md @@ -291,6 +291,32 @@ Building the crate also regenerates the low-level C# bindings at - 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 ```