Skip to content

Commit aee7110

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 586a6ac commit aee7110

8 files changed

Lines changed: 166 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: 19 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,20 @@ 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+
.map(|s| s.parse::<u8>().unwrap()),
246+
)]
247+
haptic_intensity: Option<u8>,
248+
249+
/// Set touchpad click force / sensitivity
250+
#[clap(value_enum)]
251+
#[arg(long)]
252+
click_force: Option<ClickForceArg>,
253+
239254
/// Check stylus battery level (USI 2.0 stylus only)
240255
#[clap(value_enum)]
241256
#[arg(long)]
@@ -534,6 +549,8 @@ pub fn parse(args: &[String]) -> Cli {
534549
ps2_enable: args.ps2_enable,
535550
tablet_mode: args.tablet_mode,
536551
touchscreen_enable: args.touchscreen_enable,
552+
haptic_intensity: args.haptic_intensity,
553+
click_force: args.click_force,
537554
stylus_battery: args.stylus_battery,
538555
console: args.console,
539556
reboot_ec: args.reboot_ec,

framework_lib/src/commandline/mod.rs

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

128+
#[cfg_attr(not(feature = "uefi"), derive(clap::ValueEnum))]
129+
#[derive(Clone, Copy, Debug, PartialEq)]
130+
#[repr(u8)]
131+
pub enum ClickForceArg {
132+
Low = 1,
133+
Medium = 2,
134+
High = 3,
135+
}
136+
128137
#[cfg_attr(not(feature = "uefi"), derive(clap::ValueEnum))]
129138
#[derive(Clone, Copy, Debug, PartialEq)]
130139
pub enum InputDeckModeArg {
@@ -215,6 +224,8 @@ pub struct Cli {
215224
pub ps2_enable: Option<bool>,
216225
pub tablet_mode: Option<TabletModeArg>,
217226
pub touchscreen_enable: Option<bool>,
227+
pub haptic_intensity: Option<u8>,
228+
pub click_force: Option<ClickForceArg>,
218229
pub stylus_battery: bool,
219230
pub console: Option<ConsoleArg>,
220231
pub reboot_ec: Option<RebootEcArg>,
@@ -1487,6 +1498,20 @@ pub fn run_with_args(args: &Cli, _allupdate: bool) -> i32 {
14871498
if touchscreen::enable_touch(*_enable).is_none() {
14881499
error!("Failed to enable/disable touch");
14891500
}
1501+
} else if let Some(_intensity) = &args.haptic_intensity {
1502+
#[cfg(feature = "hidapi")]
1503+
if let Err(e) = crate::touchpad::set_haptic_intensity(*_intensity) {
1504+
error!("Failed to set haptic intensity: {}", e);
1505+
}
1506+
#[cfg(not(feature = "hidapi"))]
1507+
error!("Not built with hidapi feature");
1508+
} else if let Some(_force) = &args.click_force {
1509+
#[cfg(feature = "hidapi")]
1510+
if let Err(e) = crate::touchpad::set_click_force(*_force as u8) {
1511+
error!("Failed to set click force: {}", e);
1512+
}
1513+
#[cfg(not(feature = "hidapi"))]
1514+
error!("Not built with hidapi feature");
14901515
} else if args.stylus_battery {
14911516
#[cfg(feature = "hidapi")]
14921517
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: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,79 @@ 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+
/// Open the PTP HID interface of the touchpad. Note: every modern touchpad
24+
/// exposes this interface; only haptic touchpads respond to the feature
25+
/// reports used by `set_haptic_intensity` / `set_click_force`.
26+
fn open_haptic_touchpad() -> Option<HidDevice> {
27+
let api = HidApi::new().ok()?;
28+
for dev_info in api.device_list() {
29+
if dev_info.usage_page() != TOUCHPAD_USAGE_PAGE || dev_info.usage() != TOUCHPAD_USAGE {
30+
continue;
31+
}
32+
debug!(
33+
" Touchpad candidate {:04X}:{:04X} (Usage Page {:04X}, Usage {:04X})",
34+
dev_info.vendor_id(),
35+
dev_info.product_id(),
36+
dev_info.usage_page(),
37+
dev_info.usage()
38+
);
39+
if let Ok(device) = dev_info.open_device(&api) {
40+
return Some(device);
41+
}
42+
}
43+
None
44+
}
45+
46+
// The firmware accepts SET_FEATURE for these reports but doesn't reply
47+
// to GET_FEATURE, so both controls are write-only.
48+
49+
pub fn set_haptic_intensity(value: u8) -> Result<(), HidError> {
50+
if !HAPTIC_INTENSITY_LEVELS.contains(&value) {
51+
error!(
52+
"Haptic intensity must be one of: {:?}",
53+
HAPTIC_INTENSITY_LEVELS
54+
);
55+
return Ok(());
56+
}
57+
let Some(device) = open_haptic_touchpad() else {
58+
error!("Could not find a haptic touchpad");
59+
return Ok(());
60+
};
61+
let buf = [HAPTIC_INTENSITY_REPORT_ID, value];
62+
debug!(" send_feature_report (haptic intensity) {:X?}", buf);
63+
device.send_feature_report(&buf)
64+
}
65+
66+
pub fn set_click_force(level: u8) -> Result<(), HidError> {
67+
if !(1..=3).contains(&level) {
68+
error!("Click force level must be 1 (Low), 2 (Medium), or 3 (High)");
69+
return Ok(());
70+
}
71+
let Some(device) = open_haptic_touchpad() else {
72+
error!("Could not find a haptic touchpad");
73+
return Ok(());
74+
};
75+
// Field is 2 bits at the bottom of the report payload
76+
let buf = [CLICK_FORCE_REPORT_ID, level & 0x03];
77+
debug!(" send_feature_report (click force) {:X?}", buf);
78+
device.send_feature_report(&buf)
79+
}
80+
881
fn read_byte(device: &HidDevice, report_id: u8, addr: u8) -> Result<u8, HidError> {
982
device.send_feature_report(&[report_id, addr, 0x10, 0])?;
1083

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)