diff --git a/README.md b/README.md index e131173..bbb7439 100644 --- a/README.md +++ b/README.md @@ -63,17 +63,31 @@ Flags: ## Python bridge (recommended) Works on macOS, Windows, Linux. Uses SDL2 + pyserial. -### Install dependencies with uv (or pip) +### Install dependencies (pyproject-enabled) +The repository now includes a `pyproject.toml`, so you can install the bridge and helper scripts as an editable package: + ```sh # from repo root uv venv .venv -uv pip install pyserial pysdl2 +source .venv/bin/activate # or .venv\Scripts\activate on Windows +uv pip install -e . ``` + +Prefer stock pip? + +```sh +python -m venv .venv +source .venv/bin/activate # or .venv\Scripts\activate on Windows +pip install -e . +``` + - SDL2 runtime: install via your OS package manager (macOS: `brew install sdl2`; Windows: place `SDL2.dll` on PATH or next to the script; Linux: `sudo apt install libsdl2-2.0-0` or equivalent). ### Run ```sh source .venv/bin/activate # or .venv\Scripts\activate on Windows +controller-uart-bridge --interactive +# or, equivalently python controller_uart_bridge.py --interactive ``` Options: @@ -88,6 +102,8 @@ Options: - `--deadzone 0.08` to change stick deadzone (0.0-1.0). - `--zero-sticks` to sample the current stick positions on connect and treat them as neutral (cancel drift). - `--zero-hotkey z` to choose the terminal hotkey that re-zeroes all connected controllers on demand (press `z` by default; pass an empty string to disable). +- `--update-controller-db` to download the latest SDL GameController database before launching (defaults to the copy in `controller_db/` if present). +- `--controller-db-url URL` to override the source URL when updating the controller database (defaults to the official mdqinc repo). - `--trigger-threshold 0.35` to change analog trigger press threshold (0.0-1.0). - `--swap-abxy` to flip AB/XY globally. - `--swap-abxy-index N` (repeatable) to flip AB/XY for controllers first seen at index N (auto-converts to a stable GUID). @@ -101,6 +117,11 @@ Options: - Hotkeys work only when the bridge is started from a TTY/console that currently has focus. Pass an empty string to either flag to disable that shortcut (useful when running unattended). - If you launch the bridge with `--swap-abxy` (global swap), the per-controller toggle hotkey will show that the layout is enforced globally and will not override it. +### Updating SDL controller mappings +- The bridge ships with a pinned `controller_db/gamecontrollerdb.txt`. Run `controller-uart-bridge --update-controller-db ...` to download the latest database from the official upstream (`mdqinc/SDL_GameControllerDB`). +- The download only touches `controller_db/gamecontrollerdb.txt`; add `--controller-db-url https://.../custom.txt` if you maintain your own fork. +- If the file is missing, the bridge will automatically attempt a download on startup. + Hot-plugging: controllers and UARTs can be plugged/unplugged while running; the bridge will auto reconnect when possible. ### Using the lightweight UART helper (no SDL needed) diff --git a/controller_uart_bridge.py b/controller_uart_bridge.py index 881ed2d..2e07a58 100644 --- a/controller_uart_bridge.py +++ b/controller_uart_bridge.py @@ -19,6 +19,7 @@ import argparse import os import sys import time +import urllib.request from dataclasses import dataclass, field from ctypes import create_string_buffer from pathlib import Path @@ -50,6 +51,9 @@ 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 +CONTROLLER_DB_URL_DEFAULT = ( + "https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/refs/heads/master/gamecontrollerdb.txt" +) def parse_mapping(value: str) -> Tuple[int, str]: @@ -66,6 +70,28 @@ def parse_mapping(value: str) -> Tuple[int, str]: return idx, port.strip() +def download_controller_db(console: Console, destination: Path, url: str) -> bool: + """Download the latest SDL controller DB to the local controller_db directory.""" + console.print(f"[cyan]Fetching SDL controller database from {url}...[/cyan]") + try: + with urllib.request.urlopen(url, timeout=20) as response: + status = getattr(response, "status", 200) + if status != 200: + raise RuntimeError(f"HTTP {status}") + data = response.read() + except Exception as exc: + console.print(f"[red]Failed to download controller database: {exc}[/red]") + return False + destination.parent.mkdir(parents=True, exist_ok=True) + try: + destination.write_bytes(data) + except Exception as exc: + console.print(f"[red]Failed to write controller database to {destination}: {exc}[/red]") + return False + console.print(f"[green]Updated controller database ({len(data)} bytes) at {destination}[/green]") + return True + + def parse_hotkey(value: str) -> str: """Validate a single-character hotkey (empty string disables).""" if value is None: @@ -578,6 +604,16 @@ def build_arg_parser() -> argparse.ArgumentParser: metavar="KEY", help="Press this key in the terminal to re-zero sticks at runtime (default: 'z', empty string disables).", ) + parser.add_argument( + "--update-controller-db", + action="store_true", + help="Download the latest SDL GameController database before loading mappings.", + ) + parser.add_argument( + "--controller-db-url", + default=CONTROLLER_DB_URL_DEFAULT, + help="Override the URL used to download the SDL GameController database.", + ) parser.add_argument( "--swap-hotkey", type=parse_hotkey, @@ -704,6 +740,8 @@ class PairingState: def load_button_maps(console: Console, args: argparse.Namespace) -> Tuple[Dict[int, SwitchButton], Dict[int, SwitchButton], set[int]]: """Load SDL controller mappings and return button map variants.""" default_mapping = Path(__file__).parent / "controller_db" / "gamecontrollerdb.txt" + if args.update_controller_db or not default_mapping.exists(): + download_controller_db(console, default_mapping, args.controller_db_url) mappings_to_load: List[str] = [] if default_mapping.exists(): mappings_to_load.append(str(default_mapping)) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..85f8a54 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,27 @@ +[build-system] +requires = ["setuptools>=65"] +build-backend = "setuptools.build_meta" + +[project] +name = "switch-pico-bridge" +version = "0.1.0" +description = "SDL2-to-UART host bridge and helpers for the switch-pico firmware." +readme = "README.md" +requires-python = ">=3.9" +authors = [{ name = "Switch Pico Maintainers" }] +dependencies = [ + "pyserial", + "pysdl2", + "rich", +] + +[project.scripts] +controller-uart-bridge = "controller_uart_bridge:main" +host-uart-logger = "host_uart_logger:main" + +[tool.setuptools] +py-modules = [ + "controller_uart_bridge", + "host_uart_logger", + "switch_pico_uart", +]