diff --git a/controller_db/gamecontrollerdb.txt b/controller_db/gamecontrollerdb.txt index a288a13..20472c2 100644 --- a/controller_db/gamecontrollerdb.txt +++ b/controller_db/gamecontrollerdb.txt @@ -992,6 +992,7 @@ xinput,XInput Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2, 030000006d04000019c2000005030000,Logitech F710,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, 030000006d0400001fc2000000000000,Logitech F710,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X, 030000006d04000018c2000000010000,Logitech RumblePad 2,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3~,start:b9,x:b0,y:b3,platform:Mac OS X, +030000006d04000019c2000000020000,Logitech Cordless RumblePad 2,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3~,start:b9,x:b0,y:b3,platform:Mac OS X, 03000000380700005032000000010000,Mad Catz PS3 Fightpad Pro,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, 03000000380700008433000000010000,Mad Catz PS3 Fightstick TE S Plus,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X, 03000000380700005082000000010000,Mad Catz PS4 Fightpad Pro,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Mac OS X, @@ -1153,6 +1154,7 @@ xinput,XInput Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2, 030000005e040000130b000017050000,Xbox Wireless Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, 030000005e040000130b000022050000,Xbox Wireless Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, 030000005e040000220b000017050000,Xbox Wireless Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, +030000005e040000220b000021050000,Xbox Wireless Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X, 03000000172700004431000029010000,XiaoMi Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b15,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a6,rightx:a2,righty:a5,start:b11,x:b3,y:b4,platform:Mac OS X, 03000000120c0000100e000000010000,Zeroplus P4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X, 03000000120c0000101e000000010000,Zeroplus P4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X, diff --git a/controller_uart_bridge.py b/controller_uart_bridge.py index 3b4c50e..4316399 100644 --- a/controller_uart_bridge.py +++ b/controller_uart_bridge.py @@ -26,9 +26,6 @@ from pathlib import Path from typing import Dict, List, Optional, Tuple from serial import SerialException -from serial.tools import list_ports -from serial.tools import list_ports_common - import sdl2 import sdl2.ext from rich.console import Console @@ -44,13 +41,14 @@ from switch_pico_uart import ( axis_to_stick, str_to_dpad, decode_rumble, + discover_serial_ports, trigger_to_button, ) RUMBLE_IDLE_TIMEOUT = 0.25 # seconds without packets before forcing rumble off RUMBLE_STUCK_TIMEOUT = 0.60 # continuous same-energy rumble will be stopped after this -RUMBLE_MIN_ACTIVE = 0.50 # below this, rumble is treated as off/noise -RUMBLE_SCALE = 0.8 +RUMBLE_MIN_ACTIVE = 0.40 # below this, rumble is treated as off/noise +RUMBLE_SCALE = 1.0 CONTROLLER_DB_URL_DEFAULT = ( "https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/refs/heads/master/gamecontrollerdb.txt" ) @@ -145,66 +143,6 @@ STICK_AXIS_LABELS = ( STICK_AXES = tuple(axis for axis, _ in STICK_AXIS_LABELS) -def is_usb_serial(path: str) -> bool: - """ - Heuristic for USB serial path prefixes (best-effort when VID/PID are missing). - - Accepts common USB-adapter patterns; rejects generic /dev/tty* unless they - clearly indicate USB. - """ - lower = path.lower() - usb_prefixes = ( - "/dev/ttyusb", # Linux USB serial - "/dev/ttyacm", # Linux CDC ACM - "/dev/cu.usb", # macOS cu/tty USB adapters - "/dev/tty.usb", - ) - if lower.startswith(usb_prefixes): - return True - # Default to False for unknown paths; caller can include_non_usb to override. - return False - - -def is_usb_serial_port(port: list_ports_common.ListPortInfo) -> bool: - """Heuristic: prefer ports with USB VID/PID; fall back to path hints.""" - if getattr(port, "vid", None) is not None or getattr(port, "pid", None) is not None: - return True - path = port.device or "" - manufacturer = (getattr(port, "manufacturer", "") or "").upper() - if "USB" in manufacturer: - return True - return is_usb_serial(path) - - -def discover_ports( - include_non_usb: bool = False, - ignore_descriptions: Optional[List[str]] = None, - include_descriptions: Optional[List[str]] = None, -) -> List[Dict[str, str]]: - """List serial ports, optionally filtering by description and USB-ness.""" - ignored = [d.lower() for d in ignore_descriptions or []] - includes = [d.lower() for d in include_descriptions or []] - results: List[Dict[str, str]] = [] - for port in list_ports.comports(): - path = port.device or "" - if not path: - continue - if not include_non_usb and not is_usb_serial_port(port): - continue - desc_lower = (port.description or "").lower() - if includes and not any(keep in desc_lower for keep in includes): - continue - if any(skip in desc_lower for skip in ignored): - continue - results.append( - { - "device": path, - "description": port.description or "Unknown", - } - ) - return results - - def interactive_pairing( console: Console, controller_info: Dict[int, str], ports: List[Dict[str, str]] ) -> List[Tuple[int, str]]: @@ -867,7 +805,7 @@ def prepare_pairing_state( if not controller_indices: parser.error("No controllers detected for interactive pairing.") # Interactive pairing shows the discovered ports and lets the user bind explicitly. - discovered = discover_ports( + discovered = discover_serial_ports( include_non_usb=include_non_usb, ignore_descriptions=ignore_port_desc, include_descriptions=include_port_desc, @@ -883,7 +821,7 @@ def prepare_pairing_state( console.print(f"[green]Prepared {len(available_ports)} specified UART port(s) for auto-pairing.[/green]") else: # Passive mode: grab whatever UARTs exist now, and keep looking later. - discovered = discover_ports( + discovered = discover_serial_ports( include_non_usb=include_non_usb, ignore_descriptions=ignore_port_desc, include_descriptions=include_port_desc, @@ -960,7 +898,7 @@ def discover_new_ports(pairing: PairingState, contexts: Dict[int, ControllerCont """Scan for new serial ports and add unused ones to the available pool.""" if not pairing.auto_discover_ports: return - discovered = discover_ports( + discovered = discover_serial_ports( include_non_usb=pairing.include_non_usb, ignore_descriptions=pairing.ignore_port_desc, include_descriptions=pairing.include_port_desc, diff --git a/example_switch_macro.py b/example_switch_macro.py index deadba7..703e2e9 100644 --- a/example_switch_macro.py +++ b/example_switch_macro.py @@ -1,8 +1,8 @@ # example_switch_macro.py import time -from switch_pico_uart import SwitchUARTClient, SwitchButton, SwitchDpad +from switch_pico_uart import SwitchUARTClient, SwitchButton, SwitchDpad, first_serial_port -PORT = "COM5" # change to your serial port, e.g. /dev/cu.usbserial-0001 +PORT = first_serial_port(include_descriptions=["USB to UART"]) or "COM5" # auto-pick first serial port, or fall back SEND_INTERVAL = 1 / 500 # optional: match controller_uart_bridge default # Convenience list of every SwitchButton (plus DPAD directions). diff --git a/switch_pico_uart.py b/switch_pico_uart.py index 00ccb98..15bfb5e 100644 --- a/switch_pico_uart.py +++ b/switch_pico_uart.py @@ -17,9 +17,10 @@ import time import threading from dataclasses import dataclass, field from enum import IntEnum, IntFlag -from typing import Iterable, Mapping, Optional, Tuple, Union +from typing import Iterable, Mapping, Optional, Tuple, Union, List, Dict import serial +from serial.tools import list_ports, list_ports_common UART_HEADER = 0xAA RUMBLE_HEADER = 0xBB @@ -57,6 +58,74 @@ class SwitchDpad(IntEnum): CENTER = 0x08 +def _is_usb_serial_path(path: str) -> bool: + """Heuristic for USB serial path prefixes.""" + lower = path.lower() + usb_prefixes = ( + "/dev/ttyusb", # Linux USB serial + "/dev/ttyacm", # Linux CDC ACM + "/dev/cu.usb", # macOS cu/tty USB adapters + "/dev/tty.usb", + ) + if lower.startswith(usb_prefixes): + return True + # Windows COM ports don't clearly indicate USB; treat as unknown here. + return False + + +def _is_usb_serial_port(port: list_ports_common.ListPortInfo) -> bool: + """Heuristic: prefer ports with USB VID/PID; fall back to path hints.""" + if getattr(port, "vid", None) is not None or getattr(port, "pid", None) is not None: + return True + path = port.device or "" + manufacturer = (getattr(port, "manufacturer", "") or "").upper() + if "USB" in manufacturer: + return True + return _is_usb_serial_path(path) + + +def discover_serial_ports( + include_non_usb: bool = False, + ignore_descriptions: Optional[List[str]] = None, + include_descriptions: Optional[List[str]] = None, +) -> List[Dict[str, str]]: + """ + List serial ports with simple filtering similar to controller_uart_bridge. + + Args: + include_non_usb: Include ports that don't look USB-based (e.g., onboard UARTs). + ignore_descriptions: Substrings (case-insensitive) to exclude by description. + include_descriptions: If provided, only include ports whose description contains one of these substrings. + """ + ignored = [d.lower() for d in (ignore_descriptions or [])] + includes = [d.lower() for d in (include_descriptions or [])] + results: List[Dict[str, str]] = [] + for port in list_ports.comports(): + path = port.device or "" + if not path: + continue + if not include_non_usb and not _is_usb_serial_port(port): + continue + desc_lower = (port.description or "").lower() + if includes and not any(keep in desc_lower for keep in includes): + continue + if any(skip in desc_lower for skip in ignored): + continue + results.append({"device": path, "description": port.description or "Unknown"}) + return results + + +def first_serial_port( + include_non_usb: bool = False, + ignore_descriptions: Optional[List[str]] = None, + include_descriptions: Optional[List[str]] = None, +) -> Optional[str]: + """Return the first discovered serial port path (or None if none are found).""" + ports = discover_serial_ports(include_non_usb, ignore_descriptions, include_descriptions) + if not ports: + return None + return ports[0]["device"] + def clamp_byte(value: Union[int, float]) -> int: """Clamp a numeric value to the 0-255 byte range.""" return max(0, min(255, int(value))) @@ -90,7 +159,7 @@ def trigger_to_button(value: int, threshold: int) -> bool: def str_to_dpad(flags: Mapping[str, bool]) -> SwitchDpad: - """Translate DPAD button flags into a Switch hat value.""" + """Translate DPAD button flags into a Switch hat/DPAD value.""" up = flags.get("up", False) down = flags.get("down", False) left = flags.get("left", False)