#!/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()