Skip to content

Commit 732a4d4

Browse files
committed
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
1 parent ca80528 commit 732a4d4

File tree

1 file changed

+126
-22
lines changed

1 file changed

+126
-22
lines changed

src/hhd/controller/virtual/dualsense/__init__.py

Lines changed: 126 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,18 @@ def open(self) -> Sequence[int]:
162162
self.state: dict = defaultdict(lambda: 0)
163163
self.rumble = False
164164
self.touchpad_touch = False
165+
self.left_touchpad_touch = False
166+
self.right_touchpad_x = 0
167+
self.right_touchpad_y = 0
168+
self.left_touchpad_x = 0
169+
self.left_touchpad_y = 0
170+
self.tp1_owner = None # Track which touchpad owns TP1: 'right' or 'left'
171+
172+
# Touchpad mapping mode:
173+
# - True: Real DS5 behavior (persistent mapping, TP2-only becomes invisible)
174+
# - False: Practical behavior (auto-transfer to TP1, always visible)
175+
self.touchpad_persistent_mapping = True
176+
165177
curr = time.perf_counter()
166178
self.touchpad_down = curr
167179
self.last_imu = curr
@@ -376,7 +388,8 @@ def consume(self, events: Sequence[Event]):
376388
code = ev["code"]
377389
match ev["type"]:
378390
case "axis":
379-
if not self.enable_touchpad and code.startswith("touchpad"):
391+
# Filter all touchpad events when touchpad is disabled
392+
if not self.enable_touchpad and code.startswith(("touchpad", "left_touchpad")):
380393
continue
381394
if self.left_motion:
382395
# Only left keep imu events for left motion
@@ -417,48 +430,36 @@ def consume(self, events: Sequence[Event]):
417430
)
418431
case "touchpad_x":
419432
tc = self.touch_correction
420-
x = int(
433+
self.right_touchpad_x = int(
421434
min(max(ev["value"], tc.x_clamp[0]), tc.x_clamp[1])
422435
* tc.x_mult
423436
+ tc.x_ofs
424437
)
425-
new_rep[self.ofs + 33] = x & 0xFF
426-
new_rep[self.ofs + 34] = (new_rep[self.ofs + 34] & 0xF0) | (
427-
x >> 8
428-
)
438+
# Coordinate will be written by smart mapping logic at the end
429439
case "touchpad_y":
430440
tc = self.touch_correction
431-
y = int(
441+
self.right_touchpad_y = int(
432442
min(max(ev["value"], tc.y_clamp[0]), tc.y_clamp[1])
433443
* tc.y_mult
434444
+ tc.y_ofs
435445
)
436-
new_rep[self.ofs + 34] = (new_rep[self.ofs + 34] & 0x0F) | (
437-
(y & 0x0F) << 4
438-
)
439-
new_rep[self.ofs + 35] = y >> 4
446+
# Coordinate will be written by smart mapping logic at the end
440447
case "left_touchpad_x":
441448
tc = LEFT_TOUCH_CORRECTION
442-
x = int(
449+
self.left_touchpad_x = int(
443450
min(max(ev["value"], tc.x_clamp[0]), tc.x_clamp[1])
444451
* tc.x_mult
445452
+ tc.x_ofs
446453
)
447-
new_rep[self.ofs + 37] = x & 0xFF
448-
new_rep[self.ofs + 38] = (new_rep[self.ofs + 34] & 0xF0) | (
449-
x >> 8
450-
)
454+
# Coordinate will be written by smart mapping logic at the end
451455
case "left_touchpad_y":
452456
tc = LEFT_TOUCH_CORRECTION
453-
y = int(
457+
self.left_touchpad_y = int(
454458
min(max(ev["value"], tc.y_clamp[0]), tc.y_clamp[1])
455459
* tc.y_mult
456460
+ tc.y_ofs
457461
)
458-
new_rep[self.ofs + 38] = (new_rep[self.ofs + 34] & 0x0F) | (
459-
(y & 0x0F) << 4
460-
)
461-
new_rep[self.ofs + 39] = y >> 4
462+
# Coordinate will be written by smart mapping logic at the end
462463
case "gyro_ts" | "accel_ts" | "imu_ts":
463464
send = True
464465
self.last_imu = time.perf_counter()
@@ -470,7 +471,8 @@ def consume(self, events: Sequence[Event]):
470471
if self.left_motion:
471472
# skip buttons for left motion
472473
continue
473-
if not self.enable_touchpad and code.startswith("touchpad"):
474+
# Filter all touchpad button events when touchpad is disabled
475+
if not self.enable_touchpad and code.startswith(("touchpad", "left_touchpad")):
474476
continue
475477
if (self.paddles_to_clicks == "top" and code == "extra_l1") or (
476478
self.paddles_to_clicks == "bottom" and code == "extra_l2"
@@ -498,7 +500,15 @@ def consume(self, events: Sequence[Event]):
498500

499501
# Fix touchpad click requiring touch
500502
if code == "touchpad_touch":
503+
# Track TP1 owner for first-come-first-served mapping
504+
if ev["value"] and not self.touchpad_touch and self.tp1_owner is None:
505+
self.tp1_owner = "right"
501506
self.touchpad_touch = ev["value"]
507+
if code == "left_touchpad_touch":
508+
# Track TP1 owner for first-come-first-served mapping
509+
if ev["value"] and not self.left_touchpad_touch and self.tp1_owner is None:
510+
self.tp1_owner = "left"
511+
self.left_touchpad_touch = ev["value"]
502512
if code == "touchpad_left":
503513
set_button(
504514
new_rep,
@@ -539,6 +549,100 @@ def consume(self, events: Sequence[Event]):
539549
max(ev["value"] // 10, 0)
540550
)
541551

552+
# Smart touchpad mapping: ensure TP2 only activates when TP1 is also active
553+
# Uses first-come-first-served principle: first touched pad owns TP1
554+
def write_tp(offset, x, y):
555+
"""Helper function to write touchpoint coordinates to report"""
556+
new_rep[offset + 1] = x & 0xFF
557+
new_rep[offset + 2] = (new_rep[offset + 2] & 0xF0) | (x >> 8)
558+
new_rep[offset + 2] = (new_rep[offset + 2] & 0x0F) | ((y & 0x0F) << 4)
559+
new_rep[offset + 3] = y >> 4
560+
561+
if self.touchpad_touch or self.left_touchpad_touch:
562+
# Check if both or only one touchpad is touching
563+
both_touching = self.touchpad_touch and self.left_touchpad_touch
564+
565+
if both_touching:
566+
# Both touching: use tp1_owner to decide mapping
567+
if self.tp1_owner == "right":
568+
# Right owns TP1: TP1 = right, TP2 = left
569+
tp1_x, tp1_y = self.right_touchpad_x, self.right_touchpad_y
570+
tp2_x, tp2_y = self.left_touchpad_x, self.left_touchpad_y
571+
tp1_is_left = False
572+
else: # "left"
573+
# Left owns TP1: TP1 = left, TP2 = right
574+
tp1_x, tp1_y = self.left_touchpad_x, self.left_touchpad_y
575+
tp2_x, tp2_y = self.right_touchpad_x, self.right_touchpad_y
576+
tp1_is_left = True
577+
578+
# Write both TP1 and TP2
579+
write_tp(self.ofs + 32, tp1_x, tp1_y)
580+
write_tp(self.ofs + 36, tp2_x, tp2_y)
581+
582+
# Set TP1 touch status if it's the left touchpad
583+
if tp1_is_left:
584+
new_rep[self.ofs + 32] = new_rep[self.ofs + 32] & 0x7F
585+
else:
586+
# Only one touching: behavior depends on mapping mode
587+
if self.touchpad_persistent_mapping:
588+
# Real DS5 mode: Keep persistent mapping
589+
# Whichever touchpad is still touching keeps its assigned slot
590+
if self.touchpad_touch:
591+
# Right touchpad is touching
592+
if self.tp1_owner == "right":
593+
# Right owns TP1: write to TP1
594+
write_tp(self.ofs + 32, self.right_touchpad_x, self.right_touchpad_y)
595+
# Touch status already set by set_button (touchpad_touch -> TP1)
596+
# Clear TP2 (left was released)
597+
new_rep[self.ofs + 36] = new_rep[self.ofs + 36] | 0x80
598+
else:
599+
# Right owns TP2: write to TP2 (will be invisible in Steam)
600+
write_tp(self.ofs + 36, self.right_touchpad_x, self.right_touchpad_y)
601+
# Need to manually set TP2 touch status (touchpad_touch -> TP1, not TP2)
602+
new_rep[self.ofs + 36] = new_rep[self.ofs + 36] & 0x7F
603+
# Clear TP1 (left was released)
604+
new_rep[self.ofs + 32] = new_rep[self.ofs + 32] | 0x80
605+
else:
606+
# Left touchpad is touching
607+
if self.tp1_owner == "left":
608+
# Left owns TP1: write to TP1
609+
write_tp(self.ofs + 32, self.left_touchpad_x, self.left_touchpad_y)
610+
new_rep[self.ofs + 32] = new_rep[self.ofs + 32] & 0x7F
611+
# Clear TP2 (right was released)
612+
new_rep[self.ofs + 36] = new_rep[self.ofs + 36] | 0x80
613+
else:
614+
# Left owns TP2: write to TP2 (will be invisible in Steam)
615+
write_tp(self.ofs + 36, self.left_touchpad_x, self.left_touchpad_y)
616+
# Touch status already set by set_button (left_touchpad_touch -> TP2)
617+
# Clear TP1 (right was released)
618+
new_rep[self.ofs + 32] = new_rep[self.ofs + 32] | 0x80
619+
else:
620+
# Practical mode: Auto-transfer to TP1 for visibility
621+
if self.touchpad_touch:
622+
tp1_x, tp1_y = self.right_touchpad_x, self.right_touchpad_y
623+
tp1_is_left = False
624+
# Transfer ownership only if it changed
625+
if self.tp1_owner != "right":
626+
self.tp1_owner = "right"
627+
else:
628+
tp1_x, tp1_y = self.left_touchpad_x, self.left_touchpad_y
629+
tp1_is_left = True
630+
# Transfer ownership only if it changed
631+
if self.tp1_owner != "left":
632+
self.tp1_owner = "left"
633+
634+
# Write to TP1
635+
write_tp(self.ofs + 32, tp1_x, tp1_y)
636+
637+
# Set TP1 touch status if it's the left touchpad
638+
if tp1_is_left:
639+
new_rep[self.ofs + 32] = new_rep[self.ofs + 32] & 0x7F
640+
else:
641+
# Both touchpads are not touching: reset and clear
642+
self.tp1_owner = None
643+
new_rep[self.ofs + 32] = new_rep[self.ofs + 32] | 0x80 # Clear TP1: bit7=1 means not touching
644+
new_rep[self.ofs + 36] = new_rep[self.ofs + 36] | 0x80 # Clear TP2: bit7=1 means not touching
645+
542646
# Cache
543647
# Caching can cause issues since receivers expect reports
544648
# at least a couple of times per second

0 commit comments

Comments
 (0)