From 12b88502779a9d2cad010b78b12f600a8801519c Mon Sep 17 00:00:00 2001 From: honjow Date: Thu, 6 Nov 2025 23:23:59 +0800 Subject: [PATCH 01/12] Add touchpad support to OeangePi NEO --- src/hhd/device/orange_pi/__init__.py | 8 +++ src/hhd/device/orange_pi/base.py | 64 ++++++++++++++++++++---- src/hhd/device/orange_pi/const.py | 17 ++++++- src/hhd/device/orange_pi/controllers.yml | 2 + 4 files changed, 81 insertions(+), 10 deletions(-) diff --git a/src/hhd/device/orange_pi/__init__.py b/src/hhd/device/orange_pi/__init__.py index a86acc07..9cda6ec6 100644 --- a/src/hhd/device/orange_pi/__init__.py +++ b/src/hhd/device/orange_pi/__init__.py @@ -11,6 +11,7 @@ get_outputs_config, load_relative_yaml, ) +from hhd.plugins.inputs import get_touchpad_config from hhd.plugins.settings import HHDSettings from .const import CONFS, DEFAULT_MAPPINGS, get_default_config @@ -52,6 +53,13 @@ def settings(self) -> HHDSettings: ) ) + if self.dconf.get("touchpad", False): + base["controllers"]["handheld"]["children"][ + "touchpad" + ] = get_touchpad_config() + else: + del base["controllers"]["handheld"]["children"]["touchpad"] + base["controllers"]["handheld"]["children"]["imu_axis"] = get_gyro_config( self.dconf.get("mapping", DEFAULT_MAPPINGS) ) diff --git a/src/hhd/device/orange_pi/base.py b/src/hhd/device/orange_pi/base.py index 0bb15752..7a23c070 100644 --- a/src/hhd/device/orange_pi/base.py +++ b/src/hhd/device/orange_pi/base.py @@ -1,5 +1,6 @@ import logging import os +import re import select import time from threading import Event as TEvent @@ -7,6 +8,7 @@ import evdev from hhd.controller import Multiplexer, DEBUG_MODE +from hhd.controller.base import TouchpadAction from hhd.controller.lib.hide import unhide_all from hhd.controller.physical.evdev import B as EC from hhd.controller.physical.evdev import GenericGamepadEvdev @@ -14,7 +16,13 @@ from hhd.controller.physical.rgb import LedDevice from hhd.plugins import Config, Context, Emitter, get_gyro_state, get_outputs -from .const import AT_BTN_MAPPINGS, GAMEPAD_BTN_MAPPINGS, DEFAULT_MAPPINGS +from .const import ( + AT_BTN_MAPPINGS, + GAMEPAD_BTN_MAPPINGS, + DEFAULT_MAPPINGS, + OPI_TOUCHPAD_AXIS_MAP, + OPI_TOUCHPAD_BUTTON_MAP, +) ERROR_DELAY = 1 SELECT_TIMEOUT = 1 @@ -27,6 +35,9 @@ KBD_VID = 0x0001 KBD_PID = 0x0001 +TOUCHPAD_VID = 0x0911 +TOUCHPAD_PID = 0x5288 + BACK_BUTTON_DELAY = 0.1 @@ -90,11 +101,12 @@ def controller_loop( conf: Config, should_exit: TEvent, updated: TEvent, dconf: dict, emit: Emitter ): debug = DEBUG_MODE + has_touchpad = dconf.get("touchpad", False) # Output d_producers, d_outs, d_params = get_outputs( conf["controller_mode"], - None, + conf["touchpad"] if has_touchpad else None, conf["imu"].to(bool), emit=emit, ) @@ -136,13 +148,15 @@ def controller_loop( btn_map=dconf.get("gamepad_mapping", GAMEPAD_BTN_MAPPINGS), ) - multiplexer = Multiplexer( - trigger="analog_to_discrete", - dpad="analog_to_discrete", - share_to_qam=True, - nintendo_mode=conf["nintendo_mode"].to(bool), - emit=emit, - params=d_params, + d_touch = GenericGamepadEvdev( + vid=[TOUCHPAD_VID], + pid=[TOUCHPAD_PID], + name=[re.compile(".+Touchpad")], + capabilities={EC("EV_KEY"): [EC("BTN_MOUSE")]}, + btn_map=OPI_TOUCHPAD_BUTTON_MAP, + axis_map=OPI_TOUCHPAD_AXIS_MAP, + aspect_ratio=1, + required=False, ) # d_volume_btn = UInputDevice( @@ -162,6 +176,36 @@ def controller_loop( if d_rgb.supported: logger.info(f"RGB Support activated through kernel driver.") + + if has_touchpad: + touch_actions = ( + conf["touchpad.controller"] + if conf["touchpad.mode"].to(TouchpadAction) == "controller" + else conf["touchpad.emulation"] + ) + + multiplexer = Multiplexer( + trigger="analog_to_discrete", + dpad="analog_to_discrete", + share_to_qam=True, + touchpad_short=touch_actions["short"].to(TouchpadAction), + touchpad_hold=touch_actions["hold"].to(TouchpadAction), + nintendo_mode=conf["nintendo_mode"].to(bool), + emit=emit, + params=d_params, + qam_multi_tap=False, + ) + else: + multiplexer = Multiplexer( + trigger="analog_to_discrete", + dpad="analog_to_discrete", + share_to_qam=True, + nintendo_mode=conf["nintendo_mode"].to(bool), + emit=emit, + params=d_params, + qam_multi_tap=False, + ) + REPORT_FREQ_MIN = 25 REPORT_FREQ_MAX = 400 @@ -191,6 +235,8 @@ def prepare(m): start_imu = d_timer.open() if start_imu: prepare(d_imu) + if has_touchpad and d_params["uses_touch"]: + prepare(d_touch) prepare(d_kbd_1) prepare(d_kbd_2) for d in d_producers: diff --git a/src/hhd/device/orange_pi/const.py b/src/hhd/device/orange_pi/const.py index a85c2f75..1d446a34 100644 --- a/src/hhd/device/orange_pi/const.py +++ b/src/hhd/device/orange_pi/const.py @@ -2,6 +2,21 @@ from hhd.controller.physical.evdev import B, to_map from hhd.plugins import gen_gyro_state +OPI_TOUCHPAD_BUTTON_MAP: dict[int, Button] = to_map( + { + "touchpad_touch": [B("BTN_TOOL_FINGER")], # also BTN_TOUCH + "touchpad_right": [B("BTN_TOOL_DOUBLETAP"), B("BTN_RIGHT")], + "touchpad_left": [B("BTN_MOUSE")], + } +) + +OPI_TOUCHPAD_AXIS_MAP: dict[int, Axis] = to_map( + { + "touchpad_x": [B("ABS_X")], # also ABS_MT_POSITION_X + "touchpad_y": [B("ABS_Y")], # also ABS_MT_POSITION_Y + } +) + DEFAULT_MAPPINGS: dict[str, tuple[Axis, str | None, float, float | None]] = { "accel_x": ("accel_x", "accel", 1, None), "accel_y": ("accel_z", "accel", 1, None), @@ -31,7 +46,7 @@ CONFS = { # New hardware new firmware, the unit below was dissassembled # "G1621-02": {"name": "OrangePi G1621-02/G1621-02", "hrtimer": True}, - "NEO-01": {"name": "OrangePi NEO-01/NEO-01", "hrtimer": True}, + "NEO-01": {"name": "OrangePi NEO-01/NEO-01", "hrtimer": True, "touchpad": True}, } diff --git a/src/hhd/device/orange_pi/controllers.yml b/src/hhd/device/orange_pi/controllers.yml index 042af662..7d3cf5a7 100644 --- a/src/hhd/device/orange_pi/controllers.yml +++ b/src/hhd/device/orange_pi/controllers.yml @@ -33,6 +33,8 @@ children: imu_axis: + touchpad: + nintendo_mode: type: bool title: Nintendo Mode (A-B Swap) From 998ddd927d9fff8c6b9a3cda7139519dcb2f0c5a Mon Sep 17 00:00:00 2001 From: Antheas Kapenekakis Date: Thu, 6 Nov 2025 23:23:59 +0800 Subject: [PATCH 02/12] fix: remove unecessary touchpad option --- src/hhd/device/orange_pi/__init__.py | 2 +- src/hhd/plugins/inputs.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/hhd/device/orange_pi/__init__.py b/src/hhd/device/orange_pi/__init__.py index 9cda6ec6..f2bd2c4b 100644 --- a/src/hhd/device/orange_pi/__init__.py +++ b/src/hhd/device/orange_pi/__init__.py @@ -56,7 +56,7 @@ def settings(self) -> HHDSettings: if self.dconf.get("touchpad", False): base["controllers"]["handheld"]["children"][ "touchpad" - ] = get_touchpad_config() + ] = get_touchpad_config(dual_touchpad=True) else: del base["controllers"]["handheld"]["children"]["touchpad"] diff --git a/src/hhd/plugins/inputs.py b/src/hhd/plugins/inputs.py index 81ff9344..c77d88b3 100644 --- a/src/hhd/plugins/inputs.py +++ b/src/hhd/plugins/inputs.py @@ -22,9 +22,11 @@ def get_vendor(): return "Uknown" -def get_touchpad_config(): - return load_relative_yaml("touchpad.yml") - +def get_touchpad_config(dual_touchpad: bool = False): + conf = load_relative_yaml("touchpad.yml") + if dual_touchpad: + del conf["modes"]['controller']["children"]["correction"] + return conf def get_gyro_config( mapping: dict[str, tuple[Axis, str | None, float, float | None]] | None From 76e8947143935f82b9b976bcd78e5dc7088925ef Mon Sep 17 00:00:00 2001 From: Antheas Kapenekakis Date: Thu, 6 Nov 2025 23:24:30 +0800 Subject: [PATCH 03/12] add left touchpad to orangepi neo --- src/hhd/controller/const.py | 5 +++-- src/hhd/device/orange_pi/base.py | 18 ++++++++++++++++-- src/hhd/device/orange_pi/const.py | 15 +++++++++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/hhd/controller/const.py b/src/hhd/controller/const.py index 48110306..2df462c0 100644 --- a/src/hhd/controller/const.py +++ b/src/hhd/controller/const.py @@ -53,8 +53,6 @@ "right_gyro_y", "right_gyro_z", "right_imu_ts", - "right_touchpad_x", - "right_touchpad_y", ] RelAxis = Literal["mouse_x", "mouse_y", "mouse_wheel", "mouse_wheel_hires"] @@ -96,6 +94,9 @@ "touchpad_touch", "touchpad_left", "touchpad_right", + "left_touchpad_touch", + "left_touchpad_left", + "left_touchpad_right", # Kbd "keyboard", ] diff --git a/src/hhd/device/orange_pi/base.py b/src/hhd/device/orange_pi/base.py index 7a23c070..76d4ed60 100644 --- a/src/hhd/device/orange_pi/base.py +++ b/src/hhd/device/orange_pi/base.py @@ -22,6 +22,8 @@ DEFAULT_MAPPINGS, OPI_TOUCHPAD_AXIS_MAP, OPI_TOUCHPAD_BUTTON_MAP, + LEFT_TOUCHPAD_AXIS_MAP, + LEFT_TOUCHPAD_BUTTON_MAP, ) ERROR_DELAY = 1 @@ -151,12 +153,23 @@ def controller_loop( d_touch = GenericGamepadEvdev( vid=[TOUCHPAD_VID], pid=[TOUCHPAD_PID], - name=[re.compile(".+Touchpad")], + name=[re.compile("OPI0001.+Touchpad")], capabilities={EC("EV_KEY"): [EC("BTN_MOUSE")]}, btn_map=OPI_TOUCHPAD_BUTTON_MAP, axis_map=OPI_TOUCHPAD_AXIS_MAP, aspect_ratio=1, - required=False, + required=True, + ) + + d_touch_left = GenericGamepadEvdev( + vid=[TOUCHPAD_VID], + pid=[TOUCHPAD_PID], + name=[re.compile("OPI0002.+Touchpad")], + capabilities={EC("EV_KEY"): [EC("BTN_MOUSE")]}, + btn_map=LEFT_TOUCHPAD_BUTTON_MAP, + axis_map=LEFT_TOUCHPAD_AXIS_MAP, + aspect_ratio=1, + required=True, ) # d_volume_btn = UInputDevice( @@ -237,6 +250,7 @@ def prepare(m): prepare(d_imu) if has_touchpad and d_params["uses_touch"]: prepare(d_touch) + prepare(d_touch_left) prepare(d_kbd_1) prepare(d_kbd_2) for d in d_producers: diff --git a/src/hhd/device/orange_pi/const.py b/src/hhd/device/orange_pi/const.py index 1d446a34..553e8fd6 100644 --- a/src/hhd/device/orange_pi/const.py +++ b/src/hhd/device/orange_pi/const.py @@ -17,6 +17,21 @@ } ) +LEFT_TOUCHPAD_BUTTON_MAP: dict[int, Button] = to_map( + { + "left_touchpad_touch": [B("BTN_TOOL_FINGER")], # also BTN_TOUCH + "left_touchpad_right": [B("BTN_TOOL_DOUBLETAP"), B("BTN_RIGHT")], + "left_touchpad_left": [B("BTN_MOUSE")], + } +) + +LEFT_TOUCHPAD_AXIS_MAP: dict[int, Axis] = to_map( + { + "left_touchpad_x": [B("ABS_X")], # also ABS_MT_POSITION_X + "left_touchpad_y": [B("ABS_Y")], # also ABS_MT_POSITION_Y + } +) + DEFAULT_MAPPINGS: dict[str, tuple[Axis, str | None, float, float | None]] = { "accel_x": ("accel_x", "accel", 1, None), "accel_y": ("accel_z", "accel", 1, None), From f974de88b6480fe09d0b3b9fe2a60e7c1ce565ca Mon Sep 17 00:00:00 2001 From: Antheas Kapenekakis Date: Thu, 6 Nov 2025 23:24:30 +0800 Subject: [PATCH 04/12] add left touchpad emulation --- src/hhd/controller/virtual/uinput/const.py | 11 +++++++++++ src/hhd/plugins/outputs.py | 16 ++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/hhd/controller/virtual/uinput/const.py b/src/hhd/controller/virtual/uinput/const.py index cf5aa904..72a1267c 100644 --- a/src/hhd/controller/virtual/uinput/const.py +++ b/src/hhd/controller/virtual/uinput/const.py @@ -537,3 +537,14 @@ class AX(NamedTuple): "touchpad_right": B("BTN_RIGHT"), "touchpad_left": B("BTN_LEFT"), } + +LEFT_TOUCHPAD_AXIS_MAP: dict[Axis, AX] = { + "left_touchpad_x": AX(B("ABS_X"), 1023, bounds=(0, 2048)), + "left_touchpad_y": AX(B("ABS_Y"), 1023, bounds=(0, 2048)), +} + +LEFT_TOUCHPAD_BUTTON_MAP: dict[Button, int] = { + "left_touchpad_touch": B("BTN_TOUCH"), + "left_touchpad_right": B("BTN_RIGHT"), + "left_touchpad_left": B("BTN_LEFT"), +} diff --git a/src/hhd/plugins/outputs.py b/src/hhd/plugins/outputs.py index d3a33ac6..9fac2f0b 100644 --- a/src/hhd/plugins/outputs.py +++ b/src/hhd/plugins/outputs.py @@ -18,6 +18,8 @@ TOUCHPAD_BUTTON_MAP, TOUCHPAD_CAPABILITIES, XBOX_ELITE_BUTTON_MAP, + LEFT_TOUCHPAD_BUTTON_MAP, + LEFT_TOUCHPAD_AXIS_MAP, UInputDevice, ) from .plugin import is_steam_gamepad_running, open_steam_kbd @@ -43,6 +45,7 @@ def get_outputs( touchpad_enable: Literal["disabled", "gamemode", "always"] | None = None, rgb_init_times: int | None = None, extra_buttons: Literal["none", "dual", "quad"] = "dual", + dual_touchpad: bool = False, ) -> tuple[Sequence[Producer], Sequence[Consumer], Mapping[str, Any]]: producers = [] consumers = [] @@ -260,6 +263,19 @@ def get_outputs( ) producers.append(d) consumers.append(d) + if dual_touchpad: + d = UInputDevice( + name="Handheld Daemon Left Touchpad", + phys="phys-hhd-left", + capabilities=TOUCHPAD_CAPABILITIES, + pid=HHD_PID_TOUCHPAD, + btn_map=LEFT_TOUCHPAD_BUTTON_MAP, + axis_map=LEFT_TOUCHPAD_AXIS_MAP, + output_timestamps=True, + ignore_cmds=True, + ) + producers.append(d) + consumers.append(d) uses_touch = True return ( From 4f0d5eedf7f435e4284f29590d28028e144892e7 Mon Sep 17 00:00:00 2001 From: Antheas Kapenekakis Date: Thu, 6 Nov 2025 23:24:30 +0800 Subject: [PATCH 05/12] add second touchpad support to dualsense --- .../controller/virtual/dualsense/__init__.py | 30 +++++++++++++++++-- src/hhd/controller/virtual/dualsense/const.py | 3 +- src/hhd/plugins/outputs.py | 5 +++- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/hhd/controller/virtual/dualsense/__init__.py b/src/hhd/controller/virtual/dualsense/__init__.py index ab7756e8..b75fb740 100644 --- a/src/hhd/controller/virtual/dualsense/__init__.py +++ b/src/hhd/controller/virtual/dualsense/__init__.py @@ -52,6 +52,10 @@ DS5_EDGE_MIN_TIMESTAMP_INTERVAL = 1500 MAX_IMU_SYNC_DELAY = 2 +LEFT_TOUCH_CORRECTION = correct_touchpad( + DS5_EDGE_TOUCH_WIDTH, DS5_EDGE_TOUCH_HEIGHT, 1, "left" +) + logger = logging.getLogger(__name__) _cache = ControllerCache() @@ -297,7 +301,7 @@ def produce(self, fds: Sequence[int]) -> Sequence[Event]: "red": red, "blue": blue, "green": green, - "red2": 0, # disable for OXP + "red2": 0, # disable for OXP "blue2": 0, "green2": 0, "oxp": None, @@ -433,6 +437,28 @@ def consume(self, events: Sequence[Event]): (y & 0x0F) << 4 ) new_rep[self.ofs + 35] = y >> 4 + case "left_touchpad_x": + tc = LEFT_TOUCH_CORRECTION + x = int( + min(max(ev["value"], tc.x_clamp[0]), tc.x_clamp[1]) + * tc.x_mult + + tc.x_ofs + ) + new_rep[self.ofs + 36] = x & 0xFF + new_rep[self.ofs + 37] = (new_rep[self.ofs + 34] & 0xF0) | ( + x >> 8 + ) + case "left_touchpad_y": + tc = LEFT_TOUCH_CORRECTION + y = int( + min(max(ev["value"], tc.y_clamp[0]), tc.y_clamp[1]) + * tc.y_mult + + tc.y_ofs + ) + new_rep[self.ofs + 37] = (new_rep[self.ofs + 34] & 0x0F) | ( + (y & 0x0F) << 4 + ) + new_rep[self.ofs + 38] = y >> 4 case "gyro_ts" | "accel_ts" | "imu_ts": send = True self.last_imu = time.perf_counter() @@ -488,7 +514,7 @@ def consume(self, events: Sequence[Event]): ) set_button( new_rep, - self.btn_map["touchpad_touch2"], + self.btn_map["left_touchpad_touch"], ev["value"], ) diff --git a/src/hhd/controller/virtual/dualsense/const.py b/src/hhd/controller/virtual/dualsense/const.py index ad90c9af..05b97380 100644 --- a/src/hhd/controller/virtual/dualsense/const.py +++ b/src/hhd/controller/virtual/dualsense/const.py @@ -169,7 +169,8 @@ def prefill_ds5_report(bluetooth: bool): "extra_l3": BM(((ofs + 9) << 3) + 4), "share": BM(((ofs + 9) << 3) + 5), "touchpad_touch": BM(((ofs + 32) << 3), flipped=True), - "touchpad_touch2": BM(((ofs + 36) << 3), flipped=True), + # "touchpad_touch2": BM(((ofs + 36) << 3), flipped=True), + "left_touchpad_touch": BM(((ofs + 36) << 3), flipped=True), "touchpad_left": BM(((ofs + 9) << 3) + 6), "mode": BM(((ofs + 9) << 3) + 7), } diff --git a/src/hhd/plugins/outputs.py b/src/hhd/plugins/outputs.py index 9fac2f0b..189305a0 100644 --- a/src/hhd/plugins/outputs.py +++ b/src/hhd/plugins/outputs.py @@ -55,7 +55,10 @@ def get_outputs( desktop_disable = False if touch_conf is not None: touchpad = touch_conf["mode"].to(str) - correction = touch_conf["controller.correction"].to(TouchpadCorrectionType) + if dual_touchpad: + correction = "right" + else: + correction = touch_conf["controller.correction"].to(TouchpadCorrectionType) if touchpad in ("emulation", "controller"): desktop_disable = touch_conf[touchpad]["desktop_disable"].to(bool) elif touchpad_enable: From 2ebce8f1abe9ca53159ed2b3b44bb17ee2af613b Mon Sep 17 00:00:00 2001 From: honjow Date: Thu, 6 Nov 2025 23:24:30 +0800 Subject: [PATCH 06/12] Fix touchpad name --- src/hhd/device/orange_pi/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hhd/device/orange_pi/base.py b/src/hhd/device/orange_pi/base.py index 76d4ed60..d2c7b87c 100644 --- a/src/hhd/device/orange_pi/base.py +++ b/src/hhd/device/orange_pi/base.py @@ -153,7 +153,7 @@ def controller_loop( d_touch = GenericGamepadEvdev( vid=[TOUCHPAD_VID], pid=[TOUCHPAD_PID], - name=[re.compile("OPI0001.+Touchpad")], + name=[re.compile("OPI0002.+Touchpad")], capabilities={EC("EV_KEY"): [EC("BTN_MOUSE")]}, btn_map=OPI_TOUCHPAD_BUTTON_MAP, axis_map=OPI_TOUCHPAD_AXIS_MAP, @@ -164,7 +164,7 @@ def controller_loop( d_touch_left = GenericGamepadEvdev( vid=[TOUCHPAD_VID], pid=[TOUCHPAD_PID], - name=[re.compile("OPI0002.+Touchpad")], + name=[re.compile("OPI0001.+Touchpad")], capabilities={EC("EV_KEY"): [EC("BTN_MOUSE")]}, btn_map=LEFT_TOUCHPAD_BUTTON_MAP, axis_map=LEFT_TOUCHPAD_AXIS_MAP, From 3156cbe8765d7ec435162e534fcb6e3d045d5a4e Mon Sep 17 00:00:00 2001 From: honjow Date: Thu, 6 Nov 2025 23:24:30 +0800 Subject: [PATCH 07/12] fix get outputs --- src/hhd/device/orange_pi/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/hhd/device/orange_pi/base.py b/src/hhd/device/orange_pi/base.py index d2c7b87c..a5deac56 100644 --- a/src/hhd/device/orange_pi/base.py +++ b/src/hhd/device/orange_pi/base.py @@ -111,6 +111,7 @@ def controller_loop( conf["touchpad"] if has_touchpad else None, conf["imu"].to(bool), emit=emit, + dual_touchpad=True ) motion = d_params.get("uses_motion", True) and conf.get("imu", True) From 843dfa64630d64d1ed7ed9218c1e94b458f8d038 Mon Sep 17 00:00:00 2001 From: Antheas Kapenekakis Date: Thu, 6 Nov 2025 23:24:30 +0800 Subject: [PATCH 08/12] fix left touchpad points --- src/hhd/controller/virtual/dualsense/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/hhd/controller/virtual/dualsense/__init__.py b/src/hhd/controller/virtual/dualsense/__init__.py index b75fb740..1eb8291d 100644 --- a/src/hhd/controller/virtual/dualsense/__init__.py +++ b/src/hhd/controller/virtual/dualsense/__init__.py @@ -444,8 +444,8 @@ def consume(self, events: Sequence[Event]): * tc.x_mult + tc.x_ofs ) - new_rep[self.ofs + 36] = x & 0xFF - new_rep[self.ofs + 37] = (new_rep[self.ofs + 34] & 0xF0) | ( + new_rep[self.ofs + 37] = x & 0xFF + new_rep[self.ofs + 38] = (new_rep[self.ofs + 34] & 0xF0) | ( x >> 8 ) case "left_touchpad_y": @@ -455,10 +455,10 @@ def consume(self, events: Sequence[Event]): * tc.y_mult + tc.y_ofs ) - new_rep[self.ofs + 37] = (new_rep[self.ofs + 34] & 0x0F) | ( + new_rep[self.ofs + 38] = (new_rep[self.ofs + 34] & 0x0F) | ( (y & 0x0F) << 4 ) - new_rep[self.ofs + 38] = y >> 4 + new_rep[self.ofs + 39] = y >> 4 case "gyro_ts" | "accel_ts" | "imu_ts": send = True self.last_imu = time.perf_counter() From e4478e7e887bee58e0909e6b0a8ceca300c32aae Mon Sep 17 00:00:00 2001 From: honjow Date: Thu, 6 Nov 2025 23:24:30 +0800 Subject: [PATCH 09/12] dualsense: add dual touchpad intelligent mapping - Map dual touchpads to DS5 TP1/TP2 with first-come-first-served logic - Support persistent mode (real DS5 behavior, default) and practical mode --- .../controller/virtual/dualsense/__init__.py | 148 +++++++++++++++--- 1 file changed, 126 insertions(+), 22 deletions(-) diff --git a/src/hhd/controller/virtual/dualsense/__init__.py b/src/hhd/controller/virtual/dualsense/__init__.py index 1eb8291d..07bb1d55 100644 --- a/src/hhd/controller/virtual/dualsense/__init__.py +++ b/src/hhd/controller/virtual/dualsense/__init__.py @@ -162,6 +162,18 @@ def open(self) -> Sequence[int]: self.state: dict = defaultdict(lambda: 0) self.rumble = False self.touchpad_touch = False + self.left_touchpad_touch = False + self.right_touchpad_x = 0 + self.right_touchpad_y = 0 + self.left_touchpad_x = 0 + self.left_touchpad_y = 0 + self.tp1_owner = None # Track which touchpad owns TP1: 'right' or 'left' + + # Touchpad mapping mode: + # - True: Real DS5 behavior (persistent mapping, TP2-only becomes invisible) + # - False: Practical behavior (auto-transfer to TP1, always visible) + self.touchpad_persistent_mapping = True + curr = time.perf_counter() self.touchpad_down = curr self.last_imu = curr @@ -376,7 +388,8 @@ def consume(self, events: Sequence[Event]): code = ev["code"] match ev["type"]: case "axis": - if not self.enable_touchpad and code.startswith("touchpad"): + # Filter all touchpad events when touchpad is disabled + if not self.enable_touchpad and code.startswith(("touchpad", "left_touchpad")): continue if self.left_motion: # Only left keep imu events for left motion @@ -417,48 +430,36 @@ def consume(self, events: Sequence[Event]): ) case "touchpad_x": tc = self.touch_correction - x = int( + self.right_touchpad_x = int( min(max(ev["value"], tc.x_clamp[0]), tc.x_clamp[1]) * tc.x_mult + tc.x_ofs ) - new_rep[self.ofs + 33] = x & 0xFF - new_rep[self.ofs + 34] = (new_rep[self.ofs + 34] & 0xF0) | ( - x >> 8 - ) + # Coordinate will be written by smart mapping logic at the end case "touchpad_y": tc = self.touch_correction - y = int( + self.right_touchpad_y = int( min(max(ev["value"], tc.y_clamp[0]), tc.y_clamp[1]) * tc.y_mult + tc.y_ofs ) - new_rep[self.ofs + 34] = (new_rep[self.ofs + 34] & 0x0F) | ( - (y & 0x0F) << 4 - ) - new_rep[self.ofs + 35] = y >> 4 + # Coordinate will be written by smart mapping logic at the end case "left_touchpad_x": tc = LEFT_TOUCH_CORRECTION - x = int( + self.left_touchpad_x = int( min(max(ev["value"], tc.x_clamp[0]), tc.x_clamp[1]) * tc.x_mult + tc.x_ofs ) - new_rep[self.ofs + 37] = x & 0xFF - new_rep[self.ofs + 38] = (new_rep[self.ofs + 34] & 0xF0) | ( - x >> 8 - ) + # Coordinate will be written by smart mapping logic at the end case "left_touchpad_y": tc = LEFT_TOUCH_CORRECTION - y = int( + self.left_touchpad_y = int( min(max(ev["value"], tc.y_clamp[0]), tc.y_clamp[1]) * tc.y_mult + tc.y_ofs ) - new_rep[self.ofs + 38] = (new_rep[self.ofs + 34] & 0x0F) | ( - (y & 0x0F) << 4 - ) - new_rep[self.ofs + 39] = y >> 4 + # Coordinate will be written by smart mapping logic at the end case "gyro_ts" | "accel_ts" | "imu_ts": send = True self.last_imu = time.perf_counter() @@ -470,7 +471,8 @@ def consume(self, events: Sequence[Event]): if self.left_motion: # skip buttons for left motion continue - if not self.enable_touchpad and code.startswith("touchpad"): + # Filter all touchpad button events when touchpad is disabled + if not self.enable_touchpad and code.startswith(("touchpad", "left_touchpad")): continue if (self.paddles_to_clicks == "top" and code == "extra_l1") or ( self.paddles_to_clicks == "bottom" and code == "extra_l2" @@ -498,7 +500,15 @@ def consume(self, events: Sequence[Event]): # Fix touchpad click requiring touch if code == "touchpad_touch": + # Track TP1 owner for first-come-first-served mapping + if ev["value"] and not self.touchpad_touch and self.tp1_owner is None: + self.tp1_owner = "right" self.touchpad_touch = ev["value"] + if code == "left_touchpad_touch": + # Track TP1 owner for first-come-first-served mapping + if ev["value"] and not self.left_touchpad_touch and self.tp1_owner is None: + self.tp1_owner = "left" + self.left_touchpad_touch = ev["value"] if code == "touchpad_left": set_button( new_rep, @@ -539,6 +549,100 @@ def consume(self, events: Sequence[Event]): max(ev["value"] // 10, 0) ) + # Smart touchpad mapping: ensure TP2 only activates when TP1 is also active + # Uses first-come-first-served principle: first touched pad owns TP1 + def write_tp(offset, x, y): + """Helper function to write touchpoint coordinates to report""" + new_rep[offset + 1] = x & 0xFF + new_rep[offset + 2] = (new_rep[offset + 2] & 0xF0) | (x >> 8) + new_rep[offset + 2] = (new_rep[offset + 2] & 0x0F) | ((y & 0x0F) << 4) + new_rep[offset + 3] = y >> 4 + + if self.touchpad_touch or self.left_touchpad_touch: + # Check if both or only one touchpad is touching + both_touching = self.touchpad_touch and self.left_touchpad_touch + + if both_touching: + # Both touching: use tp1_owner to decide mapping + if self.tp1_owner == "right": + # Right owns TP1: TP1 = right, TP2 = left + tp1_x, tp1_y = self.right_touchpad_x, self.right_touchpad_y + tp2_x, tp2_y = self.left_touchpad_x, self.left_touchpad_y + tp1_is_left = False + else: # "left" + # Left owns TP1: TP1 = left, TP2 = right + tp1_x, tp1_y = self.left_touchpad_x, self.left_touchpad_y + tp2_x, tp2_y = self.right_touchpad_x, self.right_touchpad_y + tp1_is_left = True + + # Write both TP1 and TP2 + write_tp(self.ofs + 32, tp1_x, tp1_y) + write_tp(self.ofs + 36, tp2_x, tp2_y) + + # Set TP1 touch status if it's the left touchpad + if tp1_is_left: + new_rep[self.ofs + 32] = new_rep[self.ofs + 32] & 0x7F + else: + # Only one touching: behavior depends on mapping mode + if self.touchpad_persistent_mapping: + # Real DS5 mode: Keep persistent mapping + # Whichever touchpad is still touching keeps its assigned slot + if self.touchpad_touch: + # Right touchpad is touching + if self.tp1_owner == "right": + # Right owns TP1: write to TP1 + write_tp(self.ofs + 32, self.right_touchpad_x, self.right_touchpad_y) + # Touch status already set by set_button (touchpad_touch -> TP1) + # Clear TP2 (left was released) + new_rep[self.ofs + 36] = new_rep[self.ofs + 36] | 0x80 + else: + # Right owns TP2: write to TP2 (will be invisible in Steam) + write_tp(self.ofs + 36, self.right_touchpad_x, self.right_touchpad_y) + # Need to manually set TP2 touch status (touchpad_touch -> TP1, not TP2) + new_rep[self.ofs + 36] = new_rep[self.ofs + 36] & 0x7F + # Clear TP1 (left was released) + new_rep[self.ofs + 32] = new_rep[self.ofs + 32] | 0x80 + else: + # Left touchpad is touching + if self.tp1_owner == "left": + # Left owns TP1: write to TP1 + write_tp(self.ofs + 32, self.left_touchpad_x, self.left_touchpad_y) + new_rep[self.ofs + 32] = new_rep[self.ofs + 32] & 0x7F + # Clear TP2 (right was released) + new_rep[self.ofs + 36] = new_rep[self.ofs + 36] | 0x80 + else: + # Left owns TP2: write to TP2 (will be invisible in Steam) + write_tp(self.ofs + 36, self.left_touchpad_x, self.left_touchpad_y) + # Touch status already set by set_button (left_touchpad_touch -> TP2) + # Clear TP1 (right was released) + new_rep[self.ofs + 32] = new_rep[self.ofs + 32] | 0x80 + else: + # Practical mode: Auto-transfer to TP1 for visibility + if self.touchpad_touch: + tp1_x, tp1_y = self.right_touchpad_x, self.right_touchpad_y + tp1_is_left = False + # Transfer ownership only if it changed + if self.tp1_owner != "right": + self.tp1_owner = "right" + else: + tp1_x, tp1_y = self.left_touchpad_x, self.left_touchpad_y + tp1_is_left = True + # Transfer ownership only if it changed + if self.tp1_owner != "left": + self.tp1_owner = "left" + + # Write to TP1 + write_tp(self.ofs + 32, tp1_x, tp1_y) + + # Set TP1 touch status if it's the left touchpad + if tp1_is_left: + new_rep[self.ofs + 32] = new_rep[self.ofs + 32] & 0x7F + else: + # Both touchpads are not touching: reset and clear + self.tp1_owner = None + new_rep[self.ofs + 32] = new_rep[self.ofs + 32] | 0x80 # Clear TP1: bit7=1 means not touching + new_rep[self.ofs + 36] = new_rep[self.ofs + 36] | 0x80 # Clear TP2: bit7=1 means not touching + # Cache # Caching can cause issues since receivers expect reports # at least a couple of times per second From 04f1d99a44050236c59278f0528174e2c989a8d8 Mon Sep 17 00:00:00 2001 From: honjow Date: Thu, 6 Nov 2025 23:24:30 +0800 Subject: [PATCH 10/12] multiplexer: add left touchpad short/long press support --- src/hhd/controller/base.py | 96 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/src/hhd/controller/base.py b/src/hhd/controller/base.py index 5fb19bdb..3949c622 100644 --- a/src/hhd/controller/base.py +++ b/src/hhd/controller/base.py @@ -629,6 +629,9 @@ def __init__( self.touchpad_x = 0 self.touchpad_y = 0 self.touchpad_down = None + self.left_touchpad_x = 0 + self.left_touchpad_y = 0 + self.left_touchpad_down = None self.queue: list[tuple[Event | Literal["reboot"], float]] = [] self.reboot_pressed = None self.select_is_held = False @@ -693,6 +696,7 @@ def process(self, events: Sequence[Event]) -> Sequence[Event]: out: list[Event] = [] status_events = set() touched = False + left_touched = False send_steam_qam = False send_steam_expand = False @@ -797,6 +801,45 @@ def process(self, events: Sequence[Event]) -> Sequence[Event]: ): self.touchpad_down[3] = False + # Left touchpad hold detection + if ( + self.touchpad_hold != "disabled" + and self.left_touchpad_down + and self.left_touchpad_down[3] + and curr - self.left_touchpad_down[0] > 0.8 + ): + action = ( + "touchpad_left" + if self.touchpad_hold == "left_click" + else "touchpad_right" + ) + self.queue.append( + ( + { + "type": "button", + "code": action, + "value": True, + }, + curr, + ) + ) + self.queue.append( + ( + { + "type": "button", + "code": action, + "value": False, + }, + curr + self.QAM_DELAY, + ) + ) + self.left_touchpad_down = None + elif self.left_touchpad_down and ( + abs(self.left_touchpad_down[1] - self.left_touchpad_x) > 0.13 + or abs(self.left_touchpad_down[2] - self.left_touchpad_y) > 0.13 + ): + self.left_touchpad_down[3] = False + for ev in events: match ev["type"]: case "axis": @@ -931,6 +974,10 @@ def process(self, events: Sequence[Event]) -> Sequence[Event]: self.touchpad_x = ev["value"] if ev["code"] == "touchpad_y": self.touchpad_y = ev["value"] + if ev["code"] == "left_touchpad_x": + self.left_touchpad_x = ev["value"] + if ev["code"] == "left_touchpad_y": + self.left_touchpad_y = ev["value"] case "button": if self.trigger == "discrete_to_analog" and ev["code"] in ( "lt", @@ -1310,6 +1357,47 @@ def process(self, events: Sequence[Event]) -> Sequence[Event]: self.touchpad_down = None # append A after QAM_DELAY s + # Left touchpad short press detection + if ev["code"] == "left_touchpad_touch": + if ( + self.touchpad_short != "disabled" + and not ev["value"] + and self.left_touchpad_down + and curr - self.left_touchpad_down[0] < 0.2 + and abs(self.left_touchpad_down[1] - self.left_touchpad_x) < 0.04 + and abs(self.left_touchpad_down[2] - self.left_touchpad_y) < 0.04 + ): + action = ( + "touchpad_left" + if self.touchpad_short == "left_click" + else "touchpad_right" + ) + self.queue.append( + ( + { + "type": "button", + "code": action, + "value": True, + }, + curr, + ) + ) + self.queue.append( + ( + { + "type": "button", + "code": action, + "value": False, + }, + curr + self.QAM_DELAY, + ) + ) + + if ev["value"]: + left_touched = True + else: + self.left_touchpad_down = None + if self.r3_to_share and ev["code"] == "extra_r3": ev["code"] = "share" @@ -1398,6 +1486,14 @@ def process(self, events: Sequence[Event]) -> Sequence[Event]: bool(True), ] + if left_touched: + self.left_touchpad_down = [ + curr, + self.left_touchpad_x, + self.left_touchpad_y, + bool(True), + ] + for s in status_events: match s: case "battery": From 6f8d0775ba3cf4274b0f0bf23eaddd93ee949ec3 Mon Sep 17 00:00:00 2001 From: honjow Date: Thu, 6 Nov 2025 23:24:30 +0800 Subject: [PATCH 11/12] dualsense: fix dual touchpad contact bits in both-touching scenario --- src/hhd/controller/virtual/dualsense/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/hhd/controller/virtual/dualsense/__init__.py b/src/hhd/controller/virtual/dualsense/__init__.py index 07bb1d55..c2f5cf60 100644 --- a/src/hhd/controller/virtual/dualsense/__init__.py +++ b/src/hhd/controller/virtual/dualsense/__init__.py @@ -568,20 +568,20 @@ def write_tp(offset, x, y): # Right owns TP1: TP1 = right, TP2 = left tp1_x, tp1_y = self.right_touchpad_x, self.right_touchpad_y tp2_x, tp2_y = self.left_touchpad_x, self.left_touchpad_y - tp1_is_left = False else: # "left" # Left owns TP1: TP1 = left, TP2 = right tp1_x, tp1_y = self.left_touchpad_x, self.left_touchpad_y tp2_x, tp2_y = self.right_touchpad_x, self.right_touchpad_y - tp1_is_left = True # Write both TP1 and TP2 write_tp(self.ofs + 32, tp1_x, tp1_y) write_tp(self.ofs + 36, tp2_x, tp2_y) - # Set TP1 touch status if it's the left touchpad - if tp1_is_left: - new_rep[self.ofs + 32] = new_rep[self.ofs + 32] & 0x7F + # Manually set both contact bits to "touching" (override default mapping) + # This is necessary because the default mapping (touchpad_touch->TP1, left_touchpad_touch->TP2) + # may not match the actual coordinate assignment when tp1_owner is "left" + new_rep[self.ofs + 32] = new_rep[self.ofs + 32] & 0x7F # TP1 is touching + new_rep[self.ofs + 36] = new_rep[self.ofs + 36] & 0x7F # TP2 is touching else: # Only one touching: behavior depends on mapping mode if self.touchpad_persistent_mapping: From 06cb2f2e877b0c52e297d5aeb164ba746f9e2bda Mon Sep 17 00:00:00 2001 From: honjow Date: Thu, 6 Nov 2025 23:24:30 +0800 Subject: [PATCH 12/12] dualsense: clear TP2 in practical mode single-touch scenario --- src/hhd/controller/virtual/dualsense/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/hhd/controller/virtual/dualsense/__init__.py b/src/hhd/controller/virtual/dualsense/__init__.py index c2f5cf60..d5552b30 100644 --- a/src/hhd/controller/virtual/dualsense/__init__.py +++ b/src/hhd/controller/virtual/dualsense/__init__.py @@ -635,8 +635,12 @@ def write_tp(offset, x, y): write_tp(self.ofs + 32, tp1_x, tp1_y) # Set TP1 touch status if it's the left touchpad + # (right touchpad's status is already set by set_button) if tp1_is_left: new_rep[self.ofs + 32] = new_rep[self.ofs + 32] & 0x7F + + # Always clear TP2 in single-touch practical mode + new_rep[self.ofs + 36] = new_rep[self.ofs + 36] | 0x80 else: # Both touchpads are not touching: reset and clear self.tp1_owner = None