diff --git a/EXAMPLES.md b/EXAMPLES.md index 67aac99..059228a 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -419,6 +419,34 @@ framework_tool --inputdeck-mode auto framework_tool --inputdeck-mode resets ``` +## Haptic touchpad (Laptop 13 Pro) + +Just like our clickpads, the Laptop 13 Pro haptic touchpad supports tap to +click, but for tactile clicking, instead of a physical button it uses piezo +crystals for sensing your click and responding with a haptic click sensation. + +To configure that feeling, it exposes two configuration knobs: + +- Sensitivity: How hard you have to press to trigger a click +- Intensity: How strong the feedback vibration is + +``` +# Disable haptic feedback +> framework_tool --haptic-intensity 0 + +# Set haptic feedback intensity back to default +# Only 0/off, 25, 50, 75, 100 are accepted +> framework_tool --haptic-intensity 75 +``` + +``` +# Set click force / sensitivity (low / medium / high) +> framework_tool --click-force high + +# Set back to default +> framework_tool --click-force medium +``` + ## Checking board ID Most inputdeck checking is implemented by Board ID. To read those directly for diff --git a/framework_lib/src/commandline/clap_std.rs b/framework_lib/src/commandline/clap_std.rs index 6c96757..34790f6 100644 --- a/framework_lib/src/commandline/clap_std.rs +++ b/framework_lib/src/commandline/clap_std.rs @@ -3,6 +3,7 @@ //! as well as on the UEFI shell tool. use std::io; +use clap::builder::TypedValueParser; use clap::error::ErrorKind; use clap::Parser; use clap::{command, Arg, Args, FromArgMatches}; @@ -12,8 +13,8 @@ use clap_num::maybe_hex; use crate::chromium_ec::commands::SetGpuSerialMagic; use crate::chromium_ec::CrosEcDriverType; use crate::commandline::{ - Cli, ConsoleArg, FpBrightnessArg, HardwareDeviceType, InputDeckModeArg, LogLevel, RebootEcArg, - TabletModeArg, + Cli, ClickForceArg, ConsoleArg, FpBrightnessArg, HardwareDeviceType, InputDeckModeArg, + LogLevel, RebootEcArg, TabletModeArg, }; /// Swiss army knife for Framework laptops @@ -236,6 +237,23 @@ struct ClapCli { #[arg(long)] touchscreen_enable: Option, + /// Set touchpad haptic feedback intensity + #[arg( + long, + value_name = "INTENSITY", + value_parser = clap::builder::PossibleValuesParser::new(["0", "25", "50", "75", "100"]) + .try_map(|s| { + s.parse::() + .map_err(|_| format!("invalid haptic intensity value: {s}")) + }), + )] + haptic_intensity: Option, + + /// Set touchpad click force / sensitivity + #[clap(value_enum)] + #[arg(long)] + click_force: Option, + /// Check stylus battery level (USI 2.0 stylus only) #[clap(value_enum)] #[arg(long)] @@ -534,6 +552,8 @@ pub fn parse(args: &[String]) -> Cli { ps2_enable: args.ps2_enable, tablet_mode: args.tablet_mode, touchscreen_enable: args.touchscreen_enable, + haptic_intensity: args.haptic_intensity, + click_force: args.click_force, stylus_battery: args.stylus_battery, console: args.console, reboot_ec: args.reboot_ec, diff --git a/framework_lib/src/commandline/mod.rs b/framework_lib/src/commandline/mod.rs index ade39e1..3f9618c 100644 --- a/framework_lib/src/commandline/mod.rs +++ b/framework_lib/src/commandline/mod.rs @@ -125,6 +125,24 @@ impl From for FpLedBrightnessLevel { } } +#[cfg_attr(not(feature = "uefi"), derive(clap::ValueEnum))] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum ClickForceArg { + Low, + Medium, + High, +} +#[cfg(feature = "hidapi")] +impl From for crate::touchpad::ClickForce { + fn from(w: ClickForceArg) -> crate::touchpad::ClickForce { + match w { + ClickForceArg::Low => crate::touchpad::ClickForce::Low, + ClickForceArg::Medium => crate::touchpad::ClickForce::Medium, + ClickForceArg::High => crate::touchpad::ClickForce::High, + } + } +} + #[cfg_attr(not(feature = "uefi"), derive(clap::ValueEnum))] #[derive(Clone, Copy, Debug, PartialEq)] pub enum InputDeckModeArg { @@ -215,6 +233,8 @@ pub struct Cli { pub ps2_enable: Option, pub tablet_mode: Option, pub touchscreen_enable: Option, + pub haptic_intensity: Option, + pub click_force: Option, pub stylus_battery: bool, pub console: Option, pub reboot_ec: Option, @@ -1487,6 +1507,20 @@ 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 let Some(_intensity) = &args.haptic_intensity { + #[cfg(feature = "hidapi")] + if let Err(e) = crate::touchpad::set_haptic_intensity(*_intensity) { + error!("Failed to set haptic intensity: {}", e); + } + #[cfg(not(feature = "hidapi"))] + error!("Not built with hidapi feature"); + } else if let Some(_force) = &args.click_force { + #[cfg(feature = "hidapi")] + if let Err(e) = crate::touchpad::set_click_force((*_force).into()) { + error!("Failed to set click force: {}", e); + } + #[cfg(not(feature = "hidapi"))] + error!("Not built with hidapi feature"); } else if args.stylus_battery { #[cfg(feature = "hidapi")] print_stylus_battery_level(); diff --git a/framework_lib/src/commandline/uefi.rs b/framework_lib/src/commandline/uefi.rs index e1d77d8..20d221b 100644 --- a/framework_lib/src/commandline/uefi.rs +++ b/framework_lib/src/commandline/uefi.rs @@ -81,6 +81,8 @@ pub fn parse(args: &[String]) -> Cli { ps2_enable: None, tablet_mode: None, touchscreen_enable: None, + haptic_intensity: None, + click_force: None, stylus_battery: false, console: None, reboot_ec: None, diff --git a/framework_lib/src/touchpad.rs b/framework_lib/src/touchpad.rs index 3aa14b0..2294b79 100644 --- a/framework_lib/src/touchpad.rs +++ b/framework_lib/src/touchpad.rs @@ -5,6 +5,84 @@ pub const PIX_VID: u16 = 0x093A; pub const P274_REPORT_ID: u8 = 0x43; pub const P239_REPORT_ID: u8 = 0x42; +// Standard HID Precision Touchpad (PTP) interface — every PTP-compliant touchpad +// reports on this usage. Only haptic touchpads expose the feature reports below. +const TOUCHPAD_USAGE_PAGE: u16 = 0x000D; // Digitizers +const TOUCHPAD_USAGE: u16 = 0x0005; // Touch Pad + +// Haptic feedback intensity (HID Haptic page 0x0E, Usage 0x23 Intensity). +// Descriptor says logical range 0..100, but the Boreas haptic firmware +// only implements five steps: 0%, 25%, 50%, 75%, 100%. +const HAPTIC_INTENSITY_REPORT_ID: u8 = 0x09; +pub const HAPTIC_INTENSITY_LEVELS: [u8; 5] = [0, 25, 50, 75, 100]; + +// Button press threshold / click force (HID Digitizer page 0x0D, Usage 0xB0). +// 2-bit field, firmware accepts 1=Low, 2=Medium, 3=High. +const CLICK_FORCE_REPORT_ID: u8 = 0x08; + +#[derive(Clone, Copy, Debug, PartialEq)] +#[repr(u8)] +pub enum ClickForce { + Low = 1, + Medium = 2, + High = 3, +} + +/// Open the PTP HID interface of the touchpad. Note: every modern touchpad +/// exposes this interface; only haptic touchpads respond to the feature +/// reports used by `set_haptic_intensity` / `set_click_force`. +fn open_haptic_touchpad() -> Option { + let api = HidApi::new().ok()?; + for dev_info in api.device_list() { + if dev_info.usage_page() != TOUCHPAD_USAGE_PAGE || dev_info.usage() != TOUCHPAD_USAGE { + continue; + } + debug!( + " Touchpad candidate {:04X}:{:04X} (Usage Page {:04X}, Usage {:04X})", + dev_info.vendor_id(), + dev_info.product_id(), + dev_info.usage_page(), + dev_info.usage() + ); + if let Ok(device) = dev_info.open_device(&api) { + return Some(device); + } + } + None +} + +// The firmware accepts SET_FEATURE for these reports but doesn't reply +// to GET_FEATURE, so both controls are write-only. + +fn hid_err(message: impl Into) -> HidError { + HidError::HidApiError { + message: message.into(), + } +} + +pub fn set_haptic_intensity(value: u8) -> Result<(), HidError> { + if !HAPTIC_INTENSITY_LEVELS.contains(&value) { + return Err(hid_err(format!( + "Haptic intensity must be one of: {:?}", + HAPTIC_INTENSITY_LEVELS + ))); + } + let device = + open_haptic_touchpad().ok_or_else(|| hid_err("Could not find a haptic touchpad"))?; + let buf = [HAPTIC_INTENSITY_REPORT_ID, value]; + debug!(" send_feature_report (haptic intensity) {:X?}", buf); + device.send_feature_report(&buf) +} + +pub fn set_click_force(force: ClickForce) -> Result<(), HidError> { + let device = + open_haptic_touchpad().ok_or_else(|| hid_err("Could not find a haptic touchpad"))?; + // Field is 2 bits at the bottom of the report payload + let buf = [CLICK_FORCE_REPORT_ID, force as u8]; + debug!(" send_feature_report (click force) {:X?}", buf); + device.send_feature_report(&buf) +} + fn read_byte(device: &HidDevice, report_id: u8, addr: u8) -> Result { device.send_feature_report(&[report_id, addr, 0x10, 0])?; diff --git a/framework_tool/completions/bash/framework_tool b/framework_tool/completions/bash/framework_tool index 1f32cc9..dd173fa 100755 --- a/framework_tool/completions/bash/framework_tool +++ b/framework_tool/completions/bash/framework_tool @@ -23,7 +23,7 @@ _framework_tool() { case "${cmd}" in framework_tool) - opts="-v -q -t -f -h --flash-gpu-descriptor --verbose --quiet --versions --version --features --esrt --device --compare-version --power --thermal --sensors --fansetduty --fansetrpm --autofanctrl --pdports --pdports-chromebook --info --meinfo --pd-info --pd-reset --pd-disable --pd-enable --dp-hdmi-info --dp-hdmi-update --audio-card-info --privacy --pd-bin --ec-bin --capsule --dump --h2o-capsule --dump-ec-flash --flash-full-ec --flash-ec --flash-ro-ec --flash-rw-ec --intrusion --inputdeck --inputdeck-mode --expansion-bay --charge-limit --charge-current-limit --charge-rate-limit --get-gpio --fp-led-level --fp-brightness --kblight --remap-key --rgbkbd --ps2-enable --tablet-mode --touchscreen-enable --stylus-battery --console --reboot-ec --ec-hib-delay --uptimeinfo --s0ix-counter --hash --driver --pd-addrs --pd-ports --test --test-retimer --boardid --force --dry-run --flash-gpu-descriptor-file --dump-gpu-descriptor-file --nvidia --host-command --generate-completions --help" + opts="-v -q -t -f -h --flash-gpu-descriptor --verbose --quiet --versions --version --features --esrt --device --compare-version --power --thermal --sensors --fansetduty --fansetrpm --autofanctrl --pdports --pdports-chromebook --info --meinfo --pd-info --pd-reset --pd-disable --pd-enable --dp-hdmi-info --dp-hdmi-update --audio-card-info --privacy --pd-bin --ec-bin --capsule --dump --h2o-capsule --dump-ec-flash --flash-full-ec --flash-ec --flash-ro-ec --flash-rw-ec --intrusion --inputdeck --inputdeck-mode --expansion-bay --charge-limit --charge-current-limit --charge-rate-limit --get-gpio --fp-led-level --fp-brightness --kblight --remap-key --rgbkbd --ps2-enable --tablet-mode --touchscreen-enable --haptic-intensity --click-force --stylus-battery --console --reboot-ec --ec-hib-delay --uptimeinfo --s0ix-counter --hash --driver --pd-addrs --pd-ports --test --test-retimer --boardid --force --dry-run --flash-gpu-descriptor-file --dump-gpu-descriptor-file --nvidia --host-command --generate-completions --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -165,6 +165,14 @@ _framework_tool() { COMPREPLY=($(compgen -W "true false" -- "${cur}")) return 0 ;; + --haptic-intensity) + COMPREPLY=($(compgen -W "0 25 50 75 100" -- "${cur}")) + return 0 + ;; + --click-force) + COMPREPLY=($(compgen -W "low medium high" -- "${cur}")) + return 0 + ;; --console) COMPREPLY=($(compgen -W "recent follow" -- "${cur}")) return 0 diff --git a/framework_tool/completions/fish/framework_tool.fish b/framework_tool/completions/fish/framework_tool.fish index 7dec88d..cfd57e7 100644 --- a/framework_tool/completions/fish/framework_tool.fish +++ b/framework_tool/completions/fish/framework_tool.fish @@ -50,6 +50,14 @@ tablet\t'' laptop\t''" complete -c framework_tool -l touchscreen-enable -d 'Enable/disable touchscreen' -r -f -a "true\t'' false\t''" +complete -c framework_tool -l haptic-intensity -d 'Set touchpad haptic feedback intensity' -r -f -a "0\t'' +25\t'' +50\t'' +75\t'' +100\t''" +complete -c framework_tool -l click-force -d 'Set touchpad click force / sensitivity' -r -f -a "low\t'' +medium\t'' +high\t''" complete -c framework_tool -l console -d 'Get EC console, choose whether recent or to follow the output' -r -f -a "recent\t'' follow\t''" complete -c framework_tool -l reboot-ec -d 'Control EC RO/RW jump' -r -f -a "reboot\t'' diff --git a/framework_tool/completions/zsh/_framework_tool b/framework_tool/completions/zsh/_framework_tool index 511d8e9..9135d11 100644 --- a/framework_tool/completions/zsh/_framework_tool +++ b/framework_tool/completions/zsh/_framework_tool @@ -49,6 +49,8 @@ _framework_tool() { '--ps2-enable=[Control PS2 touchpad emulation (DEBUG COMMAND, if touchpad not working, reboot system)]:PS2_ENABLE:(true false)' \ '--tablet-mode=[Set tablet mode override]:TABLET_MODE:(auto tablet laptop)' \ '--touchscreen-enable=[Enable/disable touchscreen]:TOUCHSCREEN_ENABLE:(true false)' \ +'--haptic-intensity=[Set touchpad haptic feedback intensity]:INTENSITY:(0 25 50 75 100)' \ +'--click-force=[Set touchpad click force / sensitivity]:CLICK_FORCE:(low medium high)' \ '--console=[Get EC console, choose whether recent or to follow the output]:CONSOLE:(recent follow)' \ '--reboot-ec=[Control EC RO/RW jump]:REBOOT_EC:(reboot jump-ro jump-rw cancel-jump disable-jump)' \ '--ec-hib-delay=[Get or set EC hibernate delay (S5 to G3)]::EC_HIB_DELAY:_default' \