From c86b6036cc1c3ddf777f789ebc4d41d447edb46d Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Fri, 18 Apr 2025 17:21:22 +0800 Subject: [PATCH 1/2] --versions: Get stylus information through USI Only works on our touchscreen because report IDs are hardcoded instead of derived from report descriptor. ``` > sudo framework_tool --versions [...] Stylus Serial Number: 28C1A00-12E71DAE Vendor ID: 32AC (Framework Computer) Product ID: 002B (Framework Stylus) Firmware Version: FF.FF [...] ``` Signed-off-by: Daniel Schaefer --- EXAMPLES.md | 13 +++++ framework_lib/src/touchscreen.rs | 79 +++++++++++++++++++++++++--- framework_lib/src/touchscreen_win.rs | 37 +++++++++++-- 3 files changed, 118 insertions(+), 11 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index 2cccc7a4..75cf17c9 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -33,6 +33,19 @@ Touchscreen MPP Protocol: true ``` +### Stylus (Framework 12) + +``` +> sudo framework_tool --versions +[...] +Stylus + Serial Number: 28C1A00-12E71DAE + Vendor ID: 32AC (Framework Computer) + Product ID: 002B (Framework Stylus) + Firmware Version: FF.FF +[...] +``` + ### Touchpad (Framework 12, Framework 13, Framework 16) ``` diff --git a/framework_lib/src/touchscreen.rs b/framework_lib/src/touchscreen.rs index c155c173..a3246226 100644 --- a/framework_lib/src/touchscreen.rs +++ b/framework_lib/src/touchscreen.rs @@ -5,16 +5,24 @@ use crate::touchscreen_win; pub const ILI_VID: u16 = 0x222A; pub const ILI_PID: u16 = 0x5539; +const VENDOR_USAGE_PAGE: u16 = 0xFF00; pub const USI_BITMAP: u8 = 1 << 1; pub const MPP_BITMAP: u8 = 1 << 2; +const REPORT_ID_FIRMWARE: u8 = 0x27; +const REPORT_ID_USI_VER: u8 = 0x28; + struct HidapiTouchScreen { device: HidDevice, } impl TouchScreen for HidapiTouchScreen { - fn open_device() -> Option { - debug!("Looking for touchscreen HID device"); + fn open_device(target_up: u16, skip: u8) -> Option { + debug!( + "Looking for touchscreen HID device {:X} {}", + target_up, skip + ); + let mut skip = skip; match HidApi::new() { Ok(api) => { for dev_info in api.device_list() { @@ -29,7 +37,7 @@ impl TouchScreen for HidapiTouchScreen { " Found {:04X}:{:04X} (Usage Page {:04X})", vid, pid, usage_page ); - if usage_page != 0xFF00 { + if usage_page != target_up { debug!(" Skipping usage page. Expected {:04X}", 0xFF00); continue; } @@ -40,6 +48,10 @@ impl TouchScreen for HidapiTouchScreen { debug!(" Found matching touchscreen HID device"); debug!(" Path: {:?}", dev_info.path()); debug!(" IC Type: {:04X}", pid); + if skip > 0 { + skip -= 1; + continue; + } // Unwrapping because if we can enumerate it, we should be able to open it let device = dev_info.open_device(&api).unwrap(); @@ -97,16 +109,57 @@ impl TouchScreen for HidapiTouchScreen { debug!(" Read buf: {:X?}", buf); Some(buf[msg_len..msg_len + read_len].to_vec()) } + + fn get_stylus_fw(&self) -> Option<()> { + let mut msg = [0u8; 0x40]; + msg[0] = REPORT_ID_USI_VER; + self.device.get_feature_report(&mut msg).ok()?; + let usi_major = msg[2]; + let usi_minor = msg[3]; + debug!("USI version (Major.Minor): {}.{}", usi_major, usi_minor); + + if usi_major != 2 || usi_minor != 0 { + // Probably not USI mode + return None; + } + + let mut msg = [0u8; 0x40]; + msg[0] = REPORT_ID_FIRMWARE; + self.device.get_feature_report(&mut msg).ok()?; + let sn_low = u32::from_le_bytes([msg[2], msg[3], msg[4], msg[5]]); + let sn_high = u32::from_le_bytes([msg[6], msg[7], msg[8], msg[9]]); + let vid = u16::from_le_bytes([msg[14], msg[15]]); + let vendor = if vid == 0x32AC { + " (Framework Computer)" + } else { + "" + }; + let pid = u16::from_le_bytes([msg[16], msg[17]]); + let product = if pid == 0x002B { + " (Framework Stylus)" + } else { + "" + }; + println!("Stylus"); + println!(" Serial Number: {:X}-{:X}", sn_high, sn_low); + debug!(" Redundant SN {:X?}", &msg[10..14]); + println!(" Vendor ID: {:04X}{}", vid, vendor); + println!(" Product ID: {:04X}{}", pid, product); + println!(" Firmware Version: {:02X}.{:02X}", &msg[18], msg[19]); + + Some(()) + } } pub trait TouchScreen { - fn open_device() -> Option + fn open_device(usage_page: u16, skip: u8) -> Option where Self: std::marker::Sized; fn send_message(&self, message_id: u8, read_len: usize, data: Vec) -> Option>; fn check_fw_version(&self) -> Option<()> { println!("Touchscreen"); + let res = self.send_message(0x42, 3, vec![0])?; let ver = res .iter() @@ -135,22 +188,32 @@ pub trait TouchScreen { self.send_message(0x38, 0, vec![!enable as u8, 0x00])?; Some(()) } + + fn get_stylus_fw(&self) -> Option<()>; } pub fn print_fw_ver() -> Option<()> { + for skip in 0..5 { + if let Some(device) = HidapiTouchScreen::open_device(0x000D, skip) { + if device.get_stylus_fw().is_some() { + break; + } + } + } + #[cfg(target_os = "windows")] - let device = touchscreen_win::NativeWinTouchScreen::open_device()?; + let device = touchscreen_win::NativeWinTouchScreen::open_device(VENDOR_USAGE_PAGE, 0)?; #[cfg(not(target_os = "windows"))] - let device = HidapiTouchScreen::open_device()?; + let device = HidapiTouchScreen::open_device(VENDOR_USAGE_PAGE, 0)?; device.check_fw_version() } pub fn enable_touch(enable: bool) -> Option<()> { #[cfg(target_os = "windows")] - let device = touchscreen_win::NativeWinTouchScreen::open_device()?; + let device = touchscreen_win::NativeWinTouchScreen::open_device(VENDOR_USAGE_PAGE, 0)?; #[cfg(not(target_os = "windows"))] - let device = HidapiTouchScreen::open_device()?; + let device = HidapiTouchScreen::open_device(VENDOR_USAGE_PAGE, 0)?; device.enable_touch(enable) } diff --git a/framework_lib/src/touchscreen_win.rs b/framework_lib/src/touchscreen_win.rs index 6e73bacd..bcc6be8d 100644 --- a/framework_lib/src/touchscreen_win.rs +++ b/framework_lib/src/touchscreen_win.rs @@ -16,13 +16,20 @@ use windows::{ }, }; +const REPORT_ID_FIRMWARE: u8 = 0x27; +const REPORT_ID_USI_VER: u8 = 0x28; + pub struct NativeWinTouchScreen { handle: HANDLE, } impl TouchScreen for NativeWinTouchScreen { - fn open_device() -> Option { - debug!("Looking for touchscreen HID device"); + fn open_device(target_up: u16, skip: u8) -> Option { + debug!( + "Looking for touchscreen HID device {:X} {}", + target_up, skip + ); + let mut skip = skip; match HidApi::new() { Ok(api) => { for dev_info in api.device_list() { @@ -37,7 +44,7 @@ impl TouchScreen for NativeWinTouchScreen { " Found {:04X}:{:04X} (Usage Page {:04X})", vid, pid, usage_page ); - if usage_page != 0xFF00 { + if usage_page != target_up { debug!(" Skipping usage page. Expected {:04X}", 0xFF00); continue; } @@ -48,6 +55,10 @@ impl TouchScreen for NativeWinTouchScreen { debug!(" Found matching touchscreen HID device"); debug!(" Path: {:?}", dev_info.path()); debug!(" IC Type: {:04X}", pid); + if skip > 0 { + skip -= 1; + continue; + } // TODO: Enumerate with windows // Should enumerate and find the right one @@ -135,4 +146,24 @@ impl TouchScreen for NativeWinTouchScreen { Some(buf[msg_len..msg_len + read_len].to_vec()) } + + fn get_stylus_fw(&self) -> Option<()> { + let mut msg = [0u8; 0x40]; + msg[0] = REPORT_ID_FIRMWARE; + unsafe { + let success = HidD_GetFeature(self.handle, msg.as_mut_ptr() as _, msg.len() as u32); + debug!(" Success: {}", success); + } + println!("Stylus firmware: {:X?}", msg); + + let mut msg = [0u8; 0x40]; + msg[0] = REPORT_ID_USI_VER; + unsafe { + let success = HidD_GetFeature(self.handle, msg.as_mut_ptr() as _, msg.len() as u32); + debug!(" Success: {}", success); + } + println!("USI Version: {:X?}", msg); + + None + } } From 7205d8f366ae20e36f544c63c6a5c7734a9b4b6e Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Sun, 20 Apr 2025 17:28:37 +0800 Subject: [PATCH 2/2] Read stylus battery level Tested on Linux with USI stylus. Hardcoded to our firmware with report ID. Guaranteed won't work on other systems. I assume it will work on Windows with USI. It won't work with MPP styluses. Signed-off-by: Daniel Schaefer --- EXAMPLES.md | 8 ++++- framework_lib/src/commandline/clap_std.rs | 6 ++++ framework_lib/src/commandline/mod.rs | 18 ++++++++++ framework_lib/src/commandline/uefi.rs | 1 + framework_lib/src/touchscreen.rs | 41 +++++++++++++++++++++++ framework_lib/src/touchscreen_win.rs | 4 +++ 6 files changed, 77 insertions(+), 1 deletion(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index 75cf17c9..e5190c26 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -139,7 +139,6 @@ ALS: 76 Lux Fan Speed: 0 RPM ``` - ## Check expansion bay (Framework 16) ``` @@ -258,3 +257,10 @@ Battery Status > sudo framework_tool --charge-rate-limit 80 0.8 > sudo framework_tool --charge-current-limit 80 2000 ``` + +## Stylus (Framework 12) + +``` +> sudo framework_tool --stylus-battery +Stylus Battery Strength: 77% +``` diff --git a/framework_lib/src/commandline/clap_std.rs b/framework_lib/src/commandline/clap_std.rs index 591b9b4b..11ef2b72 100644 --- a/framework_lib/src/commandline/clap_std.rs +++ b/framework_lib/src/commandline/clap_std.rs @@ -198,6 +198,11 @@ struct ClapCli { #[arg(long)] touchscreen_enable: Option, + /// Check stylus battery level (USI 2.0 stylus only) + #[clap(value_enum)] + #[arg(long)] + stylus_battery: bool, + /// Get EC console, choose whether recent or to follow the output #[clap(value_enum)] #[arg(long)] @@ -390,6 +395,7 @@ pub fn parse(args: &[String]) -> Cli { rgbkbd: args.rgbkbd, tablet_mode: args.tablet_mode, touchscreen_enable: args.touchscreen_enable, + stylus_battery: args.stylus_battery, console: args.console, reboot_ec: args.reboot_ec, hash: args.hash.map(|x| x.into_os_string().into_string().unwrap()), diff --git a/framework_lib/src/commandline/mod.rs b/framework_lib/src/commandline/mod.rs index f16b5489..fb9cb491 100644 --- a/framework_lib/src/commandline/mod.rs +++ b/framework_lib/src/commandline/mod.rs @@ -184,6 +184,7 @@ pub struct Cli { pub rgbkbd: Vec, pub tablet_mode: Option, pub touchscreen_enable: Option, + pub stylus_battery: bool, pub console: Option, pub reboot_ec: Option, pub hash: Option, @@ -336,6 +337,18 @@ fn active_mode(mode: &FwMode, reference: FwMode) -> &'static str { } } +#[cfg(feature = "hidapi")] +fn print_stylus_battery_level() { + loop { + if let Some(level) = touchscreen::get_battery_level() { + println!("Stylus Battery Strength: {}%", level); + return; + } else { + debug!("Stylus Battery Strength: Unknown"); + } + } +} + fn print_versions(ec: &CrosEc) { println!("UEFI BIOS"); if let Some(smbios) = get_smbios() { @@ -816,6 +829,11 @@ pub fn run_with_args(args: &Cli, _allupdate: bool) -> i32 { if touchscreen::enable_touch(*_enable).is_none() { error!("Failed to enable/disable touch"); } + } else if args.stylus_battery { + #[cfg(feature = "hidapi")] + print_stylus_battery_level(); + #[cfg(not(feature = "hidapi"))] + error!("Not build with hidapi feature"); } else if let Some(console_arg) = &args.console { match console_arg { ConsoleArg::Follow => { diff --git a/framework_lib/src/commandline/uefi.rs b/framework_lib/src/commandline/uefi.rs index 4b32d765..bd362142 100644 --- a/framework_lib/src/commandline/uefi.rs +++ b/framework_lib/src/commandline/uefi.rs @@ -97,6 +97,7 @@ pub fn parse(args: &[String]) -> Cli { rgbkbd: vec![], tablet_mode: None, touchscreen_enable: None, + stylus_battery: false, console: None, reboot_ec: None, hash: None, diff --git a/framework_lib/src/touchscreen.rs b/framework_lib/src/touchscreen.rs index a3246226..b06965a7 100644 --- a/framework_lib/src/touchscreen.rs +++ b/framework_lib/src/touchscreen.rs @@ -110,6 +110,35 @@ impl TouchScreen for HidapiTouchScreen { Some(buf[msg_len..msg_len + read_len].to_vec()) } + fn get_battery_status(&self) -> Option { + let mut msg = [0u8; 0x40]; + msg[0] = 0x0D; + self.device.read(&mut msg).ok()?; + // println!(" Tip Switch {}%", msg[12]); + // println!(" Barrell Switch: {}%", msg[12]); + // println!(" Eraser: {}%", msg[12]); + // println!(" Invert: {}%", msg[12]); + // println!(" In Range: {}%", msg[12]); + // println!(" 2nd Barrel Switch:{}%", msg[12]); + // println!(" X {}%", msg[12]); + // println!(" Y {}%", msg[12]); + // println!(" Tip Pressure: {}%", msg[12]); + // println!(" X Tilt: {}%", msg[12]); + // println!(" Y Tilt: {}%", msg[12]); + debug!(" Battery Strength: {}%", msg[12]); + debug!( + " Barrel Pressure: {}", + u16::from_le_bytes([msg[13], msg[14]]) + ); + debug!(" Transducer Index: {}", msg[15]); + + if msg[12] == 0 { + None + } else { + Some(msg[12]) + } + } + fn get_stylus_fw(&self) -> Option<()> { let mut msg = [0u8; 0x40]; msg[0] = REPORT_ID_USI_VER; @@ -190,6 +219,18 @@ pub trait TouchScreen { } fn get_stylus_fw(&self) -> Option<()>; + fn get_battery_status(&self) -> Option; +} + +pub fn get_battery_level() -> Option { + for skip in 0..5 { + if let Some(device) = HidapiTouchScreen::open_device(0x000D, skip) { + if let Some(level) = device.get_battery_status() { + return Some(level); + } + } + } + None } pub fn print_fw_ver() -> Option<()> { diff --git a/framework_lib/src/touchscreen_win.rs b/framework_lib/src/touchscreen_win.rs index bcc6be8d..f5fd42a0 100644 --- a/framework_lib/src/touchscreen_win.rs +++ b/framework_lib/src/touchscreen_win.rs @@ -166,4 +166,8 @@ impl TouchScreen for NativeWinTouchScreen { None } + fn get_battery_status(&self) -> Option { + error!("Get stylus battery status not supported on Windows"); + None + } }