From 15538c4f5aed86081916f4429e859a6fa1091a4e Mon Sep 17 00:00:00 2001 From: David Heiemeier Hansson Date: Thu, 26 Mar 2026 11:08:44 +0100 Subject: [PATCH] New solution to haptic touchpad --- bin/omarchy-haptic-touchpad | 95 +++++++++++++++++++ .../hardware/dell/fix-xps-haptic-touchpad.sh | 28 +++--- 2 files changed, 111 insertions(+), 12 deletions(-) create mode 100755 bin/omarchy-haptic-touchpad diff --git a/bin/omarchy-haptic-touchpad b/bin/omarchy-haptic-touchpad new file mode 100755 index 00000000..f800f0d0 --- /dev/null +++ b/bin/omarchy-haptic-touchpad @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 + +"""Haptic feedback daemon for Synaptics touchpads with Manual Trigger. + +Monitors touchpad button press events and sends haptic pulses via HID +feature reports. Required because the kernel's HID haptic subsystem only +supports Auto Trigger with waveform enumeration, not the simpler Manual +Trigger protocol used by these Synaptics touchpads. +""" + +import fcntl, glob, os, struct, sys + +VENDOR = "06CB" +PRODUCT = "D01A" +REPORT_ID = 0x37 +INTENSITY = 40 # 0-100 + +# input_event: struct timeval (16 bytes on 64-bit) + type(H) + code(H) + value(i) +EVENT_FORMAT = "llHHi" +EVENT_SIZE = struct.calcsize(EVENT_FORMAT) +EV_KEY = 0x01 +BTN_LEFT = 272 +BTN_RIGHT = 273 +BTN_MIDDLE = 274 + +# ioctl: HIDIOCSFEATURE(len) = _IOC(_IOC_WRITE|_IOC_READ, 'H', 0x06, len) +def HIDIOCSFEATURE(length): + return 0xC0000000 | (length << 16) | (ord("H") << 8) | 0x06 + + +def find_hidraw(): + for path in sorted(glob.glob("/sys/class/hidraw/hidraw*")): + uevent = os.path.join(path, "device", "uevent") + try: + with open(uevent) as f: + content = f.read().upper() + if f"0000{VENDOR}" in content and f"0000{PRODUCT}" in content: + return os.path.join("/dev", os.path.basename(path)) + except OSError: + continue + return None + + +def find_touchpad_event(): + for path in sorted(glob.glob("/sys/class/input/event*/device/name")): + try: + with open(path) as f: + name = f.read().strip().upper() + if VENDOR in name and PRODUCT in name and "TOUCHPAD" in name: + event = path.split("/")[-3] + return os.path.join("/dev/input", event) + except OSError: + continue + return None + + +def main(): + hidraw = find_hidraw() + if not hidraw: + print("No Synaptics haptic touchpad hidraw device found", file=sys.stderr) + sys.exit(1) + + event = find_touchpad_event() + if not event: + print("No Synaptics haptic touchpad input device found", file=sys.stderr) + sys.exit(1) + + print(f"Haptic touchpad: hidraw={hidraw} input={event} intensity={INTENSITY}", flush=True) + + haptic_report = struct.pack("BB", REPORT_ID, INTENSITY) + ioctl_req = HIDIOCSFEATURE(len(haptic_report)) + + hidraw_fd = os.open(hidraw, os.O_RDWR) + event_fd = os.open(event, os.O_RDONLY) + + try: + while True: + data = os.read(event_fd, EVENT_SIZE) + if len(data) < EVENT_SIZE: + continue + _, _, ev_type, code, value = struct.unpack(EVENT_FORMAT, data) + if ev_type == EV_KEY and code in (BTN_LEFT, BTN_RIGHT, BTN_MIDDLE) and value == 1: + try: + fcntl.ioctl(hidraw_fd, ioctl_req, haptic_report) + except OSError: + pass + except KeyboardInterrupt: + pass + finally: + os.close(event_fd) + os.close(hidraw_fd) + + +if __name__ == "__main__": + main() diff --git a/install/config/hardware/dell/fix-xps-haptic-touchpad.sh b/install/config/hardware/dell/fix-xps-haptic-touchpad.sh index 92313492..427f3783 100644 --- a/install/config/hardware/dell/fix-xps-haptic-touchpad.sh +++ b/install/config/hardware/dell/fix-xps-haptic-touchpad.sh @@ -1,31 +1,35 @@ -# Fix Dell XPS haptic touchpad losing haptic feedback after suspend/resume. -# The I2C controller's runtime power management aggressively suspends the touchpad, -# and on resume the haptic engine sometimes fails to reinitialize. -# This udev rule keeps the I2C controller always on to prevent that. -# Applies to any Dell XPS with the Synaptics haptic touchpad (06CB:D01A). +# Fix Dell XPS haptic touchpad. +# The Synaptics haptic touchpad (06CB:D01A) uses the HID Manual Trigger +# protocol, but the kernel's HID haptic subsystem only supports Auto Trigger. +# This sets up a lightweight daemon that monitors touchpad button events and +# sends haptic pulses via HID feature reports on the hidraw device. +# Also disables I2C runtime PM to prevent the haptic engine losing state +# across suspend/resume. if omarchy-hw-match "XPS" \ && ls /sys/bus/i2c/devices/i2c-VEN_06CB:00 2>/dev/null; then - # Disable runtime PM for I2C controller so haptic state isn't lost + # Keep I2C controller power on to prevent haptic engine losing state sudo tee /etc/udev/rules.d/99-dell-xps-haptic-touchpad.rules << 'EOF' ACTION=="add", SUBSYSTEM=="pci", KERNEL=="0000:00:19.0", ATTR{power/control}="on" ACTION=="add", SUBSYSTEM=="platform", KERNEL=="i2c_designware.0", ATTR{power/control}="on" EOF sudo udevadm control --reload-rules - # Rebind the I2C HID touchpad on boot and resume to reinitialize haptic engine - sudo tee /etc/systemd/system/dell-xps-haptic-touchpad.service << 'SVC' + # Haptic feedback daemon as a systemd service + sudo tee /etc/systemd/system/dell-xps-haptic-touchpad.service << SVC [Unit] -Description=Rebind Dell XPS haptic touchpad +Description=Dell XPS haptic touchpad feedback After=systemd-udev-settle.service [Service] -Type=oneshot -ExecStart=/bin/bash -c 'if [[ -d /sys/bus/i2c/devices/i2c-VEN_06CB:00 ]]; then echo "i2c-VEN_06CB:00" | tee /sys/bus/i2c/drivers/i2c_hid_acpi/unbind > /dev/null 2>&1; sleep 1; echo "i2c-VEN_06CB:00" | tee /sys/bus/i2c/drivers/i2c_hid_acpi/bind > /dev/null 2>&1; fi' +Type=simple +ExecStart=$OMARCHY_PATH/bin/omarchy-haptic-touchpad +Restart=on-failure +RestartSec=2 [Install] -WantedBy=multi-user.target suspend.target hibernate.target +WantedBy=multi-user.target SVC sudo systemctl daemon-reload sudo systemctl enable dell-xps-haptic-touchpad.service