Skip to content

Commit 3a6c1f4

Browse files
committed
Add haptic settings for Laptop 13 Pro touchpad
See examples for details: > framework_tool --haptic-intensity 75 > framework_tool --click-force high Signed-off-by: Daniel Schaefer <dhs@frame.work>
1 parent 5ac7519 commit 3a6c1f4

8 files changed

Lines changed: 183 additions & 3 deletions

File tree

EXAMPLES.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,34 @@ framework_tool --inputdeck-mode auto
419419
framework_tool --inputdeck-mode resets
420420
```
421421

422+
## Haptic touchpad (Laptop 13 Pro)
423+
424+
Just like our clickpads, the Laptop 13 Pro haptic touchpad supports tap to
425+
click, but for tactile clicking, instead of a physical button it uses piezo
426+
crystals for sensing your click and responding with a haptic click sensation.
427+
428+
To configure that feeling, it exposes two configuration knobs:
429+
430+
- Sensitivity: How hard you have to press to trigger a click
431+
- Intensity: How strong the feedback vibration is
432+
433+
```
434+
# Disable haptic feedback
435+
> framework_tool --haptic-intensity 0
436+
437+
# Set haptic feedback intensity back to default
438+
# Only 0/off, 25, 50, 75, 100 are accepted
439+
> framework_tool --haptic-intensity 75
440+
```
441+
442+
```
443+
# Set click force / sensitivity (low / medium / high)
444+
> framework_tool --click-force high
445+
446+
# Set back to default
447+
> framework_tool --click-force medium
448+
```
449+
422450
## Checking board ID
423451

424452
Most inputdeck checking is implemented by Board ID. To read those directly for

framework_lib/src/commandline/clap_std.rs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
//! as well as on the UEFI shell tool.
44
use std::io;
55

6+
use clap::builder::TypedValueParser;
67
use clap::error::ErrorKind;
78
use clap::Parser;
89
use clap::{command, Arg, Args, FromArgMatches};
@@ -12,8 +13,8 @@ use clap_num::maybe_hex;
1213
use crate::chromium_ec::commands::SetGpuSerialMagic;
1314
use crate::chromium_ec::CrosEcDriverType;
1415
use crate::commandline::{
15-
Cli, ConsoleArg, FpBrightnessArg, HardwareDeviceType, InputDeckModeArg, LogLevel, RebootEcArg,
16-
TabletModeArg,
16+
Cli, ClickForceArg, ConsoleArg, FpBrightnessArg, HardwareDeviceType, InputDeckModeArg,
17+
LogLevel, RebootEcArg, TabletModeArg,
1718
};
1819

1920
/// Swiss army knife for Framework laptops
@@ -236,6 +237,23 @@ struct ClapCli {
236237
#[arg(long)]
237238
touchscreen_enable: Option<bool>,
238239

240+
/// Set touchpad haptic feedback intensity
241+
#[arg(
242+
long,
243+
value_name = "INTENSITY",
244+
value_parser = clap::builder::PossibleValuesParser::new(["0", "25", "50", "75", "100"])
245+
.try_map(|s| {
246+
s.parse::<u8>()
247+
.map_err(|_| format!("invalid haptic intensity value: {s}"))
248+
}),
249+
)]
250+
haptic_intensity: Option<u8>,
251+
252+
/// Set touchpad click force / sensitivity
253+
#[clap(value_enum)]
254+
#[arg(long)]
255+
click_force: Option<ClickForceArg>,
256+
239257
/// Check stylus battery level (USI 2.0 stylus only)
240258
#[clap(value_enum)]
241259
#[arg(long)]
@@ -534,6 +552,8 @@ pub fn parse(args: &[String]) -> Cli {
534552
ps2_enable: args.ps2_enable,
535553
tablet_mode: args.tablet_mode,
536554
touchscreen_enable: args.touchscreen_enable,
555+
haptic_intensity: args.haptic_intensity,
556+
click_force: args.click_force,
537557
stylus_battery: args.stylus_battery,
538558
console: args.console,
539559
reboot_ec: args.reboot_ec,

framework_lib/src/commandline/mod.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,24 @@ impl From<FpBrightnessArg> for FpLedBrightnessLevel {
125125
}
126126
}
127127

128+
#[cfg_attr(not(feature = "uefi"), derive(clap::ValueEnum))]
129+
#[derive(Clone, Copy, Debug, PartialEq)]
130+
pub enum ClickForceArg {
131+
Low,
132+
Medium,
133+
High,
134+
}
135+
#[cfg(feature = "hidapi")]
136+
impl From<ClickForceArg> for crate::touchpad::ClickForce {
137+
fn from(w: ClickForceArg) -> crate::touchpad::ClickForce {
138+
match w {
139+
ClickForceArg::Low => crate::touchpad::ClickForce::Low,
140+
ClickForceArg::Medium => crate::touchpad::ClickForce::Medium,
141+
ClickForceArg::High => crate::touchpad::ClickForce::High,
142+
}
143+
}
144+
}
145+
128146
#[cfg_attr(not(feature = "uefi"), derive(clap::ValueEnum))]
129147
#[derive(Clone, Copy, Debug, PartialEq)]
130148
pub enum InputDeckModeArg {
@@ -215,6 +233,8 @@ pub struct Cli {
215233
pub ps2_enable: Option<bool>,
216234
pub tablet_mode: Option<TabletModeArg>,
217235
pub touchscreen_enable: Option<bool>,
236+
pub haptic_intensity: Option<u8>,
237+
pub click_force: Option<ClickForceArg>,
218238
pub stylus_battery: bool,
219239
pub console: Option<ConsoleArg>,
220240
pub reboot_ec: Option<RebootEcArg>,
@@ -1487,6 +1507,20 @@ pub fn run_with_args(args: &Cli, _allupdate: bool) -> i32 {
14871507
if touchscreen::enable_touch(*_enable).is_none() {
14881508
error!("Failed to enable/disable touch");
14891509
}
1510+
} else if let Some(_intensity) = &args.haptic_intensity {
1511+
#[cfg(feature = "hidapi")]
1512+
if let Err(e) = crate::touchpad::set_haptic_intensity(*_intensity) {
1513+
error!("Failed to set haptic intensity: {}", e);
1514+
}
1515+
#[cfg(not(feature = "hidapi"))]
1516+
error!("Not built with hidapi feature");
1517+
} else if let Some(_force) = &args.click_force {
1518+
#[cfg(feature = "hidapi")]
1519+
if let Err(e) = crate::touchpad::set_click_force((*_force).into()) {
1520+
error!("Failed to set click force: {}", e);
1521+
}
1522+
#[cfg(not(feature = "hidapi"))]
1523+
error!("Not built with hidapi feature");
14901524
} else if args.stylus_battery {
14911525
#[cfg(feature = "hidapi")]
14921526
print_stylus_battery_level();

framework_lib/src/commandline/uefi.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ pub fn parse(args: &[String]) -> Cli {
8181
ps2_enable: None,
8282
tablet_mode: None,
8383
touchscreen_enable: None,
84+
haptic_intensity: None,
85+
click_force: None,
8486
stylus_battery: false,
8587
console: None,
8688
reboot_ec: None,

framework_lib/src/touchpad.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,84 @@ pub const PIX_VID: u16 = 0x093A;
55
pub const P274_REPORT_ID: u8 = 0x43;
66
pub const P239_REPORT_ID: u8 = 0x42;
77

8+
// Standard HID Precision Touchpad (PTP) interface — every PTP-compliant touchpad
9+
// reports on this usage. Only haptic touchpads expose the feature reports below.
10+
const TOUCHPAD_USAGE_PAGE: u16 = 0x000D; // Digitizers
11+
const TOUCHPAD_USAGE: u16 = 0x0005; // Touch Pad
12+
13+
// Haptic feedback intensity (HID Haptic page 0x0E, Usage 0x23 Intensity).
14+
// Descriptor says logical range 0..100, but the Boreas haptic firmware
15+
// only implements five steps: 0%, 25%, 50%, 75%, 100%.
16+
const HAPTIC_INTENSITY_REPORT_ID: u8 = 0x09;
17+
pub const HAPTIC_INTENSITY_LEVELS: [u8; 5] = [0, 25, 50, 75, 100];
18+
19+
// Button press threshold / click force (HID Digitizer page 0x0D, Usage 0xB0).
20+
// 2-bit field, firmware accepts 1=Low, 2=Medium, 3=High.
21+
const CLICK_FORCE_REPORT_ID: u8 = 0x08;
22+
23+
#[derive(Clone, Copy, Debug, PartialEq)]
24+
#[repr(u8)]
25+
pub enum ClickForce {
26+
Low = 1,
27+
Medium = 2,
28+
High = 3,
29+
}
30+
31+
/// Open the PTP HID interface of the touchpad. Note: every modern touchpad
32+
/// exposes this interface; only haptic touchpads respond to the feature
33+
/// reports used by `set_haptic_intensity` / `set_click_force`.
34+
fn open_haptic_touchpad() -> Option<HidDevice> {
35+
let api = HidApi::new().ok()?;
36+
for dev_info in api.device_list() {
37+
if dev_info.usage_page() != TOUCHPAD_USAGE_PAGE || dev_info.usage() != TOUCHPAD_USAGE {
38+
continue;
39+
}
40+
debug!(
41+
" Touchpad candidate {:04X}:{:04X} (Usage Page {:04X}, Usage {:04X})",
42+
dev_info.vendor_id(),
43+
dev_info.product_id(),
44+
dev_info.usage_page(),
45+
dev_info.usage()
46+
);
47+
if let Ok(device) = dev_info.open_device(&api) {
48+
return Some(device);
49+
}
50+
}
51+
None
52+
}
53+
54+
// The firmware accepts SET_FEATURE for these reports but doesn't reply
55+
// to GET_FEATURE, so both controls are write-only.
56+
57+
fn hid_err(message: impl Into<String>) -> HidError {
58+
HidError::HidApiError {
59+
message: message.into(),
60+
}
61+
}
62+
63+
pub fn set_haptic_intensity(value: u8) -> Result<(), HidError> {
64+
if !HAPTIC_INTENSITY_LEVELS.contains(&value) {
65+
return Err(hid_err(format!(
66+
"Haptic intensity must be one of: {:?}",
67+
HAPTIC_INTENSITY_LEVELS
68+
)));
69+
}
70+
let device =
71+
open_haptic_touchpad().ok_or_else(|| hid_err("Could not find a haptic touchpad"))?;
72+
let buf = [HAPTIC_INTENSITY_REPORT_ID, value];
73+
debug!(" send_feature_report (haptic intensity) {:X?}", buf);
74+
device.send_feature_report(&buf)
75+
}
76+
77+
pub fn set_click_force(force: ClickForce) -> Result<(), HidError> {
78+
let device =
79+
open_haptic_touchpad().ok_or_else(|| hid_err("Could not find a haptic touchpad"))?;
80+
// Field is 2 bits at the bottom of the report payload
81+
let buf = [CLICK_FORCE_REPORT_ID, force as u8];
82+
debug!(" send_feature_report (click force) {:X?}", buf);
83+
device.send_feature_report(&buf)
84+
}
85+
886
fn read_byte(device: &HidDevice, report_id: u8, addr: u8) -> Result<u8, HidError> {
987
device.send_feature_report(&[report_id, addr, 0x10, 0])?;
1088

framework_tool/completions/bash/framework_tool

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ _framework_tool() {
2323

2424
case "${cmd}" in
2525
framework_tool)
26-
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"
26+
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"
2727
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
2828
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
2929
return 0
@@ -165,6 +165,14 @@ _framework_tool() {
165165
COMPREPLY=($(compgen -W "true false" -- "${cur}"))
166166
return 0
167167
;;
168+
--haptic-intensity)
169+
COMPREPLY=($(compgen -W "0 25 50 75 100" -- "${cur}"))
170+
return 0
171+
;;
172+
--click-force)
173+
COMPREPLY=($(compgen -W "low medium high" -- "${cur}"))
174+
return 0
175+
;;
168176
--console)
169177
COMPREPLY=($(compgen -W "recent follow" -- "${cur}"))
170178
return 0

framework_tool/completions/fish/framework_tool.fish

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ tablet\t''
5050
laptop\t''"
5151
complete -c framework_tool -l touchscreen-enable -d 'Enable/disable touchscreen' -r -f -a "true\t''
5252
false\t''"
53+
complete -c framework_tool -l haptic-intensity -d 'Set touchpad haptic feedback intensity' -r -f -a "0\t''
54+
25\t''
55+
50\t''
56+
75\t''
57+
100\t''"
58+
complete -c framework_tool -l click-force -d 'Set touchpad click force / sensitivity' -r -f -a "low\t''
59+
medium\t''
60+
high\t''"
5361
complete -c framework_tool -l console -d 'Get EC console, choose whether recent or to follow the output' -r -f -a "recent\t''
5462
follow\t''"
5563
complete -c framework_tool -l reboot-ec -d 'Control EC RO/RW jump' -r -f -a "reboot\t''

framework_tool/completions/zsh/_framework_tool

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ _framework_tool() {
4949
'--ps2-enable=[Control PS2 touchpad emulation (DEBUG COMMAND, if touchpad not working, reboot system)]:PS2_ENABLE:(true false)' \
5050
'--tablet-mode=[Set tablet mode override]:TABLET_MODE:(auto tablet laptop)' \
5151
'--touchscreen-enable=[Enable/disable touchscreen]:TOUCHSCREEN_ENABLE:(true false)' \
52+
'--haptic-intensity=[Set touchpad haptic feedback intensity]:INTENSITY:(0 25 50 75 100)' \
53+
'--click-force=[Set touchpad click force / sensitivity]:CLICK_FORCE:(low medium high)' \
5254
'--console=[Get EC console, choose whether recent or to follow the output]:CONSOLE:(recent follow)' \
5355
'--reboot-ec=[Control EC RO/RW jump]:REBOOT_EC:(reboot jump-ro jump-rw cancel-jump disable-jump)' \
5456
'--ec-hib-delay=[Get or set EC hibernate delay (S5 to G3)]::EC_HIB_DELAY:_default' \

0 commit comments

Comments
 (0)