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": 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/controller/virtual/dualsense/__init__.py b/src/hhd/controller/virtual/dualsense/__init__.py index ab7756e8..d5552b30 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() @@ -158,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 @@ -297,7 +313,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, @@ -372,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 @@ -413,26 +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 + # Coordinate will be written by smart mapping logic at the end + case "left_touchpad_x": + tc = LEFT_TOUCH_CORRECTION + self.left_touchpad_x = int( + min(max(ev["value"], tc.x_clamp[0]), tc.x_clamp[1]) + * tc.x_mult + + tc.x_ofs + ) + # Coordinate will be written by smart mapping logic at the end + case "left_touchpad_y": + tc = LEFT_TOUCH_CORRECTION + 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 + 35] = 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() @@ -444,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" @@ -472,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, @@ -488,7 +524,7 @@ def consume(self, events: Sequence[Event]): ) set_button( new_rep, - self.btn_map["touchpad_touch2"], + self.btn_map["left_touchpad_touch"], ev["value"], ) @@ -513,6 +549,104 @@ 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 + 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 + + # Write both TP1 and TP2 + write_tp(self.ofs + 32, tp1_x, tp1_y) + write_tp(self.ofs + 36, tp2_x, tp2_y) + + # 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: + # 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 + # (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 + 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 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/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/device/orange_pi/__init__.py b/src/hhd/device/orange_pi/__init__.py index a86acc07..f2bd2c4b 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(dual_touchpad=True) + 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..a5deac56 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,15 @@ 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, + LEFT_TOUCHPAD_AXIS_MAP, + LEFT_TOUCHPAD_BUTTON_MAP, +) ERROR_DELAY = 1 SELECT_TIMEOUT = 1 @@ -27,6 +37,9 @@ KBD_VID = 0x0001 KBD_PID = 0x0001 +TOUCHPAD_VID = 0x0911 +TOUCHPAD_PID = 0x5288 + BACK_BUTTON_DELAY = 0.1 @@ -90,13 +103,15 @@ 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, + dual_touchpad=True ) motion = d_params.get("uses_motion", True) and conf.get("imu", True) @@ -136,13 +151,26 @@ 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("OPI0002.+Touchpad")], + capabilities={EC("EV_KEY"): [EC("BTN_MOUSE")]}, + btn_map=OPI_TOUCHPAD_BUTTON_MAP, + axis_map=OPI_TOUCHPAD_AXIS_MAP, + aspect_ratio=1, + required=True, + ) + + d_touch_left = GenericGamepadEvdev( + vid=[TOUCHPAD_VID], + pid=[TOUCHPAD_PID], + name=[re.compile("OPI0001.+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( @@ -162,6 +190,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 +249,9 @@ 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_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 a85c2f75..553e8fd6 100644 --- a/src/hhd/device/orange_pi/const.py +++ b/src/hhd/device/orange_pi/const.py @@ -2,6 +2,36 @@ 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 + } +) + +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), @@ -31,7 +61,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) 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 diff --git a/src/hhd/plugins/outputs.py b/src/hhd/plugins/outputs.py index d3a33ac6..189305a0 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 = [] @@ -52,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: @@ -260,6 +266,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 (