From fb430eaea910bc578f8840365e6f780e9ccc264f Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Thu, 9 Apr 2026 22:55:59 +0200 Subject: [PATCH 1/6] implement CpuInformationsDevice to parse and render CPU information --- drivers/std/src/devices/cpu.rs | 177 +++++++++++++++++++++++++++++++++ drivers/std/src/devices/mod.rs | 2 + 2 files changed, 179 insertions(+) create mode 100644 drivers/std/src/devices/cpu.rs diff --git a/drivers/std/src/devices/cpu.rs b/drivers/std/src/devices/cpu.rs new file mode 100644 index 00000000..462337f5 --- /dev/null +++ b/drivers/std/src/devices/cpu.rs @@ -0,0 +1,177 @@ +use file_system::{ + DirectBaseOperations, DirectCharacterDevice, Error, MountOperations, Result, Size, +}; +use std::collections::BTreeMap; + +const EMBEDDED_FIELDS: [&str; 6] = [ + "processor", + "vendor_id", + "model name", + "cpu MHz", + "cpu cores", + "architecture", +]; + +pub struct CpuInformationsDevice; + +fn parse_proc_cpuinfo(content: &str) -> Vec> { + let mut entries = Vec::new(); + let mut current = BTreeMap::new(); + + for line in content.lines() { + let line = line.trim(); + + if line.is_empty() { + if !current.is_empty() { + entries.push(current); + current = BTreeMap::new(); + } + continue; + } + + if let Some((key, value)) = line.split_once(':') { + current.insert(key.trim().to_string(), value.trim().to_string()); + } + } + + if !current.is_empty() { + entries.push(current); + } + + entries +} + +fn format_entry( + entry: &BTreeMap, + index: usize, + architecture: &str, + fallback_cores: &str, +) -> String { + let mut block = String::new(); + + for key in EMBEDDED_FIELDS { + let value = match key { + "processor" => entry.get(key).cloned().unwrap_or_else(|| index.to_string()), + "architecture" => architecture.to_string(), + "cpu cores" => entry + .get(key) + .cloned() + .unwrap_or_else(|| fallback_cores.to_string()), + _ => entry + .get(key) + .cloned() + .unwrap_or_else(|| "unknown".to_string()), + }; + + block.push_str(key); + block.push_str("\t: "); + block.push_str(&value); + block.push('\n'); + } + + block.push('\n'); + block +} + +fn build_cpuinfo_text_from_proc( + content: &str, + architecture: &str, + available_cores: Option, +) -> String { + let parsed = parse_proc_cpuinfo(content); + let fallback_cores = available_cores + .map(|value| value.to_string()) + .unwrap_or_else(|| "unknown".to_string()); + + if parsed.is_empty() { + return format_entry(&BTreeMap::new(), 0, architecture, &fallback_cores); + } + + let mut output = String::new(); + for (index, entry) in parsed.iter().enumerate() { + output.push_str(&format_entry(entry, index, architecture, &fallback_cores)); + } + + output +} + +fn render_cpuinfo_text() -> String { + let architecture = std::env::consts::ARCH; + let cores = std::thread::available_parallelism().ok().map(usize::from); + + if cfg!(target_os = "linux") { + if let Ok(content) = std::fs::read_to_string("/proc/cpuinfo") { + return build_cpuinfo_text_from_proc(&content, architecture, cores); + } + } + + build_cpuinfo_text_from_proc("", architecture, cores) +} + +impl DirectBaseOperations for CpuInformationsDevice { + fn read(&self, buffer: &mut [u8], absolute_position: Size) -> Result { + let content = render_cpuinfo_text(); + let bytes = content.as_bytes(); + + let start = usize::try_from(absolute_position).unwrap_or(usize::MAX); + if start >= bytes.len() { + return Ok(0); + } + + let length = buffer.len().min(bytes.len() - start); + buffer[..length].copy_from_slice(&bytes[start..start + length]); + + Ok(length) + } + + fn write(&self, _: &[u8], _: Size) -> Result { + Err(Error::UnsupportedOperation) + } +} + +impl MountOperations for CpuInformationsDevice {} + +impl DirectCharacterDevice for CpuInformationsDevice {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parser_extracts_embedded_subset_fields() { + let sample = "processor\t: 0\nvendor_id\t: ARM\nmodel name\t: Cortex-A53\ncpu MHz\t\t: 1200.0\ncpu cores\t: 4\n\n"; + let output = build_cpuinfo_text_from_proc(sample, "aarch64", Some(4)); + assert!(output.contains("processor\t: 0")); + assert!(output.contains("vendor_id\t: ARM")); + assert!(output.contains("model name\t: Cortex-A53")); + assert!(output.contains("cpu MHz\t: 1200.0")); + assert!(output.contains("cpu cores\t: 4")); + assert!(output.contains("architecture\t: aarch64")); + } + + #[test] + fn parser_falls_back_to_unknown_for_missing_values() { + let output = build_cpuinfo_text_from_proc("", "aarch64", None); + assert!(output.contains("vendor_id\t: unknown")); + assert!(output.contains("cpu MHz\t: unknown")); + assert!(output.contains("cpu cores\t: unknown")); + } + + #[test] + fn read_uses_absolute_offset_and_handles_eof() { + let device = CpuInformationsDevice; + let mut buffer = [0u8; 32]; + let first = device.read(&mut buffer, 0).unwrap(); + assert!(first > 0); + + let eof = device.read(&mut buffer, Size::MAX).unwrap(); + assert_eq!(eof, 0); + } + + #[test] + fn write_is_not_supported() { + let device = CpuInformationsDevice; + let result = device.write(b"ignored", 0); + assert!(matches!(result, Err(Error::UnsupportedOperation))); + } +} diff --git a/drivers/std/src/devices/mod.rs b/drivers/std/src/devices/mod.rs index db5e2a55..9a0a123b 100644 --- a/drivers/std/src/devices/mod.rs +++ b/drivers/std/src/devices/mod.rs @@ -1,3 +1,5 @@ +mod cpu; mod time; +pub use cpu::*; pub use time::*; From ddef5232dd9e7b4db21f25ac00d3fe2e5f33bc60 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Thu, 9 Apr 2026 22:56:05 +0200 Subject: [PATCH 2/6] implement CpuInformationsDevice to retrieve and format CPU information --- drivers/wasm/src/devices/cpu.rs | 110 ++++++++++++++++++++++++++++++++ drivers/wasm/src/devices/mod.rs | 2 + 2 files changed, 112 insertions(+) create mode 100644 drivers/wasm/src/devices/cpu.rs diff --git a/drivers/wasm/src/devices/cpu.rs b/drivers/wasm/src/devices/cpu.rs new file mode 100644 index 00000000..c8379a5d --- /dev/null +++ b/drivers/wasm/src/devices/cpu.rs @@ -0,0 +1,110 @@ +use alloc::string::{String, ToString}; +use file_system::{ + DirectBaseOperations, DirectCharacterDevice, Error, MountOperations, Result, Size, +}; + +pub struct CpuInformationsDevice; + +fn get_hardware_concurrency() -> Option { + let window = web_sys::window()?; + let navigator = window.navigator(); + let value = js_sys::Reflect::get( + &navigator, + &wasm_bindgen::JsValue::from_str("hardwareConcurrency"), + ) + .ok()?; + let value = value.as_f64()?; + + if !value.is_finite() || value <= 0.0 { + return None; + } + + Some(value as u32) +} + +fn build_cpuinfo_text(architecture: &str, hardware_concurrency: Option) -> String { + let cores = hardware_concurrency + .map(|value| value.to_string()) + .unwrap_or_else(|| "unknown".to_string()); + + let mut content = String::new(); + content.push_str("processor\t: 0\n"); + content.push_str("vendor_id\t: unknown\n"); + content.push_str("model name\t: unknown\n"); + content.push_str("cpu MHz\t: unknown\n"); + content.push_str("cpu cores\t: "); + content.push_str(&cores); + content.push('\n'); + content.push_str("architecture\t: "); + content.push_str(architecture); + content.push_str("\n\n"); + content +} + +fn render_cpuinfo_text() -> String { + build_cpuinfo_text("wasm32", get_hardware_concurrency()) +} + +impl DirectBaseOperations for CpuInformationsDevice { + fn read(&self, buffer: &mut [u8], absolute_position: Size) -> Result { + let content = render_cpuinfo_text(); + let bytes = content.as_bytes(); + + let start = usize::try_from(absolute_position).unwrap_or(usize::MAX); + if start >= bytes.len() { + return Ok(0); + } + + let length = buffer.len().min(bytes.len() - start); + buffer[..length].copy_from_slice(&bytes[start..start + length]); + + Ok(length) + } + + fn write(&self, _: &[u8], _: Size) -> Result { + Err(Error::UnsupportedOperation) + } +} + +impl MountOperations for CpuInformationsDevice {} + +impl DirectCharacterDevice for CpuInformationsDevice {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn formatter_uses_unknown_for_missing_values() { + let output = build_cpuinfo_text("wasm32", None); + assert!(output.contains("vendor_id\t: unknown")); + assert!(output.contains("model name\t: unknown")); + assert!(output.contains("cpu MHz\t: unknown")); + assert!(output.contains("cpu cores\t: unknown")); + assert!(output.contains("architecture\t: wasm32")); + } + + #[test] + fn formatter_includes_detected_cores() { + let output = build_cpuinfo_text("wasm32", Some(8)); + assert!(output.contains("cpu cores\t: 8")); + } + + #[test] + fn read_uses_absolute_offset_and_handles_eof() { + let device = CpuInformationsDevice; + let mut buffer = [0u8; 24]; + let first = device.read(&mut buffer, 0).unwrap(); + assert!(first > 0); + + let eof = device.read(&mut buffer, Size::MAX).unwrap(); + assert_eq!(eof, 0); + } + + #[test] + fn write_is_not_supported() { + let device = CpuInformationsDevice; + let result = device.write(b"ignored", 0); + assert!(matches!(result, Err(Error::UnsupportedOperation))); + } +} diff --git a/drivers/wasm/src/devices/mod.rs b/drivers/wasm/src/devices/mod.rs index 1e462e8d..3484ecdb 100644 --- a/drivers/wasm/src/devices/mod.rs +++ b/drivers/wasm/src/devices/mod.rs @@ -1,8 +1,10 @@ +mod cpu; mod drive; pub mod graphics; mod http_client; mod time; +pub use cpu::*; pub use drive::*; pub use http_client::*; pub use time::*; From d94dd74a158e09a1eb84910606917a1709504fef Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Thu, 9 Apr 2026 22:56:27 +0200 Subject: [PATCH 3/6] implement Cpu device directory and add CpuInformationsDevice to the virtual file system --- examples/native/src/main.rs | 8 ++++++++ examples/wasm/src/main.rs | 8 ++++++++ modules/testing/src/lib.rs | 9 +++++++++ 3 files changed, 25 insertions(+) diff --git a/examples/native/src/main.rs b/examples/native/src/main.rs index c0e6172b..b443c385 100644 --- a/examples/native/src/main.rs +++ b/examples/native/src/main.rs @@ -118,6 +118,9 @@ async fn main() { // - - Create the default system hierarchy let _ = virtual_file_system::create_default_hierarchy(virtual_file_system, task).await; + let _ = virtual_file_system + .create_directory(task, "/devices/cpu") + .await; log::information!("Default hierarchy created."); @@ -145,6 +148,11 @@ async fn main() { CharacterDevice, drivers_std::devices::TimeDevice ), + ( + &"/devices/cpu/informations", + CharacterDevice, + drivers_std::devices::CpuInformationsDevice + ), ( &"/devices/random", CharacterDevice, diff --git a/examples/wasm/src/main.rs b/examples/wasm/src/main.rs index 9edd1580..ca3dbb2d 100644 --- a/examples/wasm/src/main.rs +++ b/examples/wasm/src/main.rs @@ -101,6 +101,9 @@ async fn main() { // - - Create the default system hierarchy let _ = virtual_file_system::create_default_hierarchy(virtual_file_system, task).await; + let _ = virtual_file_system + .create_directory(task, "/devices/cpu") + .await; // - - Mount the devices virtual_file_system::clean_devices(virtual_file_system, task) @@ -131,6 +134,11 @@ async fn main() { CharacterDevice, drivers_wasm::devices::TimeDevice ), + ( + &"/devices/cpu/informations", + CharacterDevice, + drivers_wasm::devices::CpuInformationsDevice + ), ( &"/devices/random", CharacterDevice, diff --git a/modules/testing/src/lib.rs b/modules/testing/src/lib.rs index 0dd410b9..38469b1c 100644 --- a/modules/testing/src/lib.rs +++ b/modules/testing/src/lib.rs @@ -80,6 +80,10 @@ pub async fn initialize(graphics_enabled: bool, network_enabled: bool) -> Standa .await .unwrap(); + let _ = virtual_file_system + .create_directory(task, "/devices/cpu") + .await; + virtual_file_system .mount_static( task, @@ -152,6 +156,11 @@ pub async fn initialize(graphics_enabled: bool, network_enabled: bool) -> Standa CharacterDevice, drivers_std::devices::TimeDevice ), + ( + &"/devices/cpu/informations", + CharacterDevice, + drivers_std::devices::CpuInformationsDevice + ), (&"/devices/null", CharacterDevice, drivers_core::NullDevice), ( &"/devices/hasher", From b4697511318a5a76726fb764cf2897a66eb737e9 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Thu, 9 Apr 2026 22:56:33 +0200 Subject: [PATCH 4/6] implement CPU summary retrieval in AboutTab and update UI to display CPU information --- executables/settings/src/tabs/about.rs | 51 +++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/executables/settings/src/tabs/about.rs b/executables/settings/src/tabs/about.rs index a0c8029f..85e81a23 100644 --- a/executables/settings/src/tabs/about.rs +++ b/executables/settings/src/tabs/about.rs @@ -1,6 +1,6 @@ use crate::error::Result; -use alloc::{ffi::CString, format}; -use core::{ffi::CStr, ptr::null_mut}; +use alloc::{ffi::CString, format, string::ToString, vec::Vec}; +use core::{ffi::CStr, ptr::null_mut, str}; use xila::{ about, graphics::{ @@ -10,6 +10,8 @@ use xila::{ internationalization::{self, translate}, memory, shared::{BYTES_SUFFIX, Unit}, + task, + virtual_file_system::{self, File}, }; pub struct AboutTab { @@ -81,9 +83,54 @@ impl AboutTab { .map_err(|_| crate::error::Error::FailedToCreateUiElement)?; self.create_list_item(translate!(c"Memory:"), &memory)?; + let cpu_summary = CString::new(Self::get_cpu_summary().await) + .map_err(|_| crate::error::Error::FailedToCreateUiElement)?; + self.create_list_item(translate!(c"CPU:"), &cpu_summary)?; + Ok(self.container) } + async fn get_cpu_summary() -> alloc::string::String { + let virtual_file_system = virtual_file_system::get_instance(); + let task = task::get_instance().get_current_task_identifier().await; + + let mut buffer = Vec::new(); + + if File::read_from_path( + virtual_file_system, + task, + "/devices/cpu/informations", + &mut buffer, + ) + .await + .is_err() + { + return "unknown (unknown cores, unknown)".to_string(); + } + + let content = str::from_utf8(&buffer).unwrap_or_default(); + + let model_name = Self::get_cpu_info_value(content, "model name").unwrap_or("unknown"); + let cpu_cores = Self::get_cpu_info_value(content, "cpu cores").unwrap_or("unknown"); + let architecture = Self::get_cpu_info_value(content, "architecture").unwrap_or("unknown"); + + format!("{} ({} cores, {})", model_name, cpu_cores, architecture) + } + + fn get_cpu_info_value<'a>(content: &'a str, key: &str) -> Option<&'a str> { + for line in content.lines() { + let Some((line_key, line_value)) = line.split_once(':') else { + continue; + }; + + if line_key.trim() == key { + return Some(line_value.trim()); + } + } + + None + } + fn create_list_item(&mut self, name: &CStr, value: &CStr) -> Result<()> { unsafe { lvgl::lv_list_add_text(self.list, name.as_ptr()); From 6799f4c4b6a273e4baf49ea1ff6bc4d1f04469dd Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Thu, 9 Apr 2026 23:00:43 +0200 Subject: [PATCH 5/6] add CPU label to English and French locale files --- executables/settings/locales/en.json | 1 + executables/settings/locales/fr.json | 1 + 2 files changed, 2 insertions(+) diff --git a/executables/settings/locales/en.json b/executables/settings/locales/en.json index b4b25d44..bbfafcc5 100644 --- a/executables/settings/locales/en.json +++ b/executables/settings/locales/en.json @@ -3,6 +3,7 @@ "Address": "Address", "Apply": "Apply", "Authentication failed: {}": "Authentication failed: {}", + "CPU:": "CPU:", "Cancel": "Cancel", "Change Password": "Change Password", "Configuration Mode": "Configuration Mode", diff --git a/executables/settings/locales/fr.json b/executables/settings/locales/fr.json index a65d399d..c52801bc 100644 --- a/executables/settings/locales/fr.json +++ b/executables/settings/locales/fr.json @@ -3,6 +3,7 @@ "Address": "Adresse", "Apply": "Appliquer", "Authentication failed: {}": "Échec de l'authentification: {}", + "CPU:": "Processeur :", "Cancel": "Annuler", "Change Password": "Changer le mot de passe", "Configuration Mode": "Mode de configuration", From f7cba4015152b58b981fa0334afd27aecca0d0a3 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Thu, 9 Apr 2026 23:04:20 +0200 Subject: [PATCH 6/6] refactor: use string reference for CPU device directory creation --- examples/native/src/main.rs | 2 +- examples/wasm/src/main.rs | 2 +- modules/testing/src/lib.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/native/src/main.rs b/examples/native/src/main.rs index b443c385..5ff1d628 100644 --- a/examples/native/src/main.rs +++ b/examples/native/src/main.rs @@ -119,7 +119,7 @@ async fn main() { // - - Create the default system hierarchy let _ = virtual_file_system::create_default_hierarchy(virtual_file_system, task).await; let _ = virtual_file_system - .create_directory(task, "/devices/cpu") + .create_directory(task, &"/devices/cpu") .await; log::information!("Default hierarchy created."); diff --git a/examples/wasm/src/main.rs b/examples/wasm/src/main.rs index ca3dbb2d..46e3062b 100644 --- a/examples/wasm/src/main.rs +++ b/examples/wasm/src/main.rs @@ -102,7 +102,7 @@ async fn main() { // - - Create the default system hierarchy let _ = virtual_file_system::create_default_hierarchy(virtual_file_system, task).await; let _ = virtual_file_system - .create_directory(task, "/devices/cpu") + .create_directory(task, &"/devices/cpu") .await; // - - Mount the devices diff --git a/modules/testing/src/lib.rs b/modules/testing/src/lib.rs index 38469b1c..6e53dbde 100644 --- a/modules/testing/src/lib.rs +++ b/modules/testing/src/lib.rs @@ -81,7 +81,7 @@ pub async fn initialize(graphics_enabled: bool, network_enabled: bool) -> Standa .unwrap(); let _ = virtual_file_system - .create_directory(task, "/devices/cpu") + .create_directory(task, &"/devices/cpu") .await; virtual_file_system