This commit is contained in:
jojomawswan 2025-12-01 13:29:04 -07:00
commit aef37123fd
4 changed files with 81 additions and 72 deletions

View file

@ -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,

View file

@ -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,

View file

@ -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).

View file

@ -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)