Make example work and redo some library code

This commit is contained in:
jojomawswan 2025-12-01 12:45:13 -07:00
commit 1cb7075180
4 changed files with 172 additions and 41 deletions

View file

@ -127,16 +127,16 @@ Hot-plugging: controllers and UARTs can be plugged/unplugged while running; the
### Using the lightweight UART helper (no SDL needed) ### Using the lightweight UART helper (no SDL needed)
For simple scripts or tests you can skip SDL and drive the Pico directly with `switch_pico_uart.py`: For simple scripts or tests you can skip SDL and drive the Pico directly with `switch_pico_uart.py`:
```python ```python
from switch_pico_uart import SwitchUARTClient, SwitchButton, SwitchHat from switch_pico_uart import SwitchUARTClient, SwitchButton, SwitchDpad
with SwitchUARTClient("/dev/cu.usbserial-0001") as client: with SwitchUARTClient("/dev/cu.usbserial-0001") as client:
client.press(SwitchButton.A) client.press(SwitchButton.A)
client.release(SwitchButton.A) client.release(SwitchButton.A)
client.move_left_stick(0.0, -1.0) # push up client.move_left_stick(0.0, -1.0) # push up
client.set_hat(SwitchHat.TOP_RIGHT) client.set_hat(SwitchDpad.UP_RIGHT)
print(client.poll_rumble()) # returns (left, right) amplitudes 0.0-1.0 or None print(client.poll_rumble()) # returns (left, right) amplitudes 0.0-1.0 or None
``` ```
- `SwitchButton` is an `IntFlag` (bitwise friendly) and `SwitchHat` is an `IntEnum` for the DPAD/hat values. - `SwitchButton` is an `IntFlag` (bitwise friendly) and `SwitchDpad` is an `IntEnum` for the DPAD/hat values (alias `SwitchHat` remains for older scripts).
- The helper only depends on `pyserial`; SDL is not required. - The helper only depends on `pyserial`; SDL is not required.
### macOS tips ### macOS tips

View file

@ -39,10 +39,10 @@ from switch_pico_uart import (
UART_BAUD, UART_BAUD,
PicoUART, PicoUART,
SwitchButton, SwitchButton,
SwitchHat, SwitchDpad,
SwitchReport, SwitchReport,
axis_to_stick, axis_to_stick,
dpad_to_hat, str_to_dpad,
decode_rumble, decode_rumble,
trigger_to_button, trigger_to_button,
) )
@ -707,7 +707,7 @@ def poll_controller_buttons(ctx: ControllerContext, button_map: Dict[int, Switch
dpad_changed = True dpad_changed = True
if dpad_changed: if dpad_changed:
ctx.report.hat = dpad_to_hat(ctx.dpad) ctx.report.hat = str_to_dpad(ctx.dpad)
@dataclass @dataclass
@ -1122,7 +1122,7 @@ def handle_button_event(
ctx.button_state[button] = pressed ctx.button_state[button] = pressed
elif button in DPAD_BUTTONS: elif button in DPAD_BUTTONS:
ctx.dpad[DPAD_BUTTONS[button]] = pressed ctx.dpad[DPAD_BUTTONS[button]] = pressed
ctx.report.hat = dpad_to_hat(ctx.dpad) ctx.report.hat = str_to_dpad(ctx.dpad)
def handle_device_added( def handle_device_added(

58
example_switch_macro.py Normal file
View file

@ -0,0 +1,58 @@
# example_switch_macro.py
import time
from switch_pico_uart import SwitchUARTClient, SwitchButton, SwitchDpad
PORT = "COM5" # change to your serial port, e.g. /dev/cu.usbserial-0001
SEND_INTERVAL = 1 / 500 # optional: match controller_uart_bridge default
# Convenience list of every SwitchButton (plus DPAD directions).
ALL_BUTTONS = [
SwitchButton.A,
SwitchButton.B,
SwitchButton.X,
SwitchButton.Y,
SwitchButton.L,
SwitchButton.R,
SwitchButton.ZL,
SwitchButton.ZR,
SwitchButton.PLUS,
SwitchButton.MINUS,
SwitchButton.CAPTURE,
SwitchButton.LCLICK,
SwitchButton.RCLICK,
SwitchDpad.DOWN,
SwitchDpad.UP,
SwitchDpad.LEFT,
SwitchDpad.RIGHT,
]
def main() -> None:
# auto_send keeps the current state flowing in the background, so we don't
# need to manually pump frames to the Pico.
with SwitchUARTClient(PORT, send_interval=SEND_INTERVAL, auto_send=True) as client:
client.neutral()
# Press every button/DPAD direction one-by-one, holding each briefly.
for button in ALL_BUTTONS:
client.press_for(0.10, button)
time.sleep(0.05) # short gap between presses
# Push left stick up briefly.
client.move_left_stick_for(0.0, -1.0, 0.2)
# Hold dpad right for one second using hat-friendly press/release.
client.press_for(0.5, SwitchDpad.RIGHT)
# Listen for rumble frames for a few seconds while the background sender runs.
end = time.monotonic() + 3
while time.monotonic() < end:
rumble = client.poll_rumble()
if rumble:
left, right = rumble
print(f"Rumble: L={left:.2f} R={right:.2f}")
time.sleep(0.01)
if __name__ == "__main__":
main()

View file

@ -14,6 +14,7 @@ from __future__ import annotations
import struct import struct
import time import time
import threading
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import IntEnum, IntFlag from enum import IntEnum, IntFlag
from typing import Iterable, Mapping, Optional, Tuple, Union from typing import Iterable, Mapping, Optional, Tuple, Union
@ -44,15 +45,15 @@ class SwitchButton(IntFlag):
CAPTURE = 1 << 13 CAPTURE = 1 << 13
class SwitchHat(IntEnum): class SwitchDpad(IntEnum):
TOP = 0x00 UP = 0x00
TOP_RIGHT = 0x01 UP_RIGHT = 0x01
RIGHT = 0x02 RIGHT = 0x02
BOTTOM_RIGHT = 0x03 DOWN_RIGHT = 0x03
BOTTOM = 0x04 DOWN = 0x04
BOTTOM_LEFT = 0x05 DOWN_LEFT = 0x05
LEFT = 0x06 LEFT = 0x06
TOP_LEFT = 0x07 UP_LEFT = 0x07
CENTER = 0x08 CENTER = 0x08
@ -88,7 +89,7 @@ def trigger_to_button(value: int, threshold: int) -> bool:
return value >= threshold return value >= threshold
def dpad_to_hat(flags: Mapping[str, bool]) -> SwitchHat: 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 value."""
up = flags.get("up", False) up = flags.get("up", False)
down = flags.get("down", False) down = flags.get("down", False)
@ -96,28 +97,28 @@ def dpad_to_hat(flags: Mapping[str, bool]) -> SwitchHat:
right = flags.get("right", False) right = flags.get("right", False)
if up and right: if up and right:
return SwitchHat.TOP_RIGHT return SwitchDpad.UP_RIGHT
if up and left: if up and left:
return SwitchHat.TOP_LEFT return SwitchDpad.UP_LEFT
if down and right: if down and right:
return SwitchHat.BOTTOM_RIGHT return SwitchDpad.DOWN_RIGHT
if down and left: if down and left:
return SwitchHat.BOTTOM_LEFT return SwitchDpad.DOWN_LEFT
if up: if up:
return SwitchHat.TOP return SwitchDpad.UP
if down: if down:
return SwitchHat.BOTTOM return SwitchDpad.DOWN
if right: if right:
return SwitchHat.RIGHT return SwitchDpad.RIGHT
if left: if left:
return SwitchHat.LEFT return SwitchDpad.LEFT
return SwitchHat.CENTER return SwitchDpad.CENTER
@dataclass @dataclass
class SwitchReport: class SwitchReport:
buttons: int = 0 buttons: int = 0
hat: SwitchHat = SwitchHat.CENTER hat: SwitchDpad = SwitchDpad.CENTER
lx: int = 128 lx: int = 128
ly: int = 128 ly: int = 128
rx: int = 128 rx: int = 128
@ -215,24 +216,31 @@ class SwitchControllerState:
report: SwitchReport = field(default_factory=SwitchReport) report: SwitchReport = field(default_factory=SwitchReport)
def press(self, *buttons: Union[SwitchButton, int]) -> None: def press(self, *buttons_or_hat: Union[SwitchButton, SwitchDpad, int]) -> None:
"""Set one or more buttons as pressed.""" """Press one or more buttons, or set the hat if a SwitchDpad is provided."""
for button in buttons: for item in buttons_or_hat:
self.report.buttons |= int(button) if isinstance(item, SwitchDpad):
# If multiple hats are provided, the last one wins.
self.report.hat = SwitchDpad(int(item) & 0xFF)
else:
self.report.buttons |= int(item)
def release(self, *buttons: Union[SwitchButton, int]) -> None: def release(self, *buttons_or_hat: Union[SwitchButton, SwitchDpad, int]) -> None:
"""Release one or more buttons.""" """Release one or more buttons, or center the hat if a SwitchDpad is provided."""
for button in buttons: for item in buttons_or_hat:
self.report.buttons &= ~int(button) if isinstance(item, SwitchDpad):
self.report.hat = SwitchDpad.CENTER
else:
self.report.buttons &= ~int(item)
def set_buttons(self, buttons: Iterable[Union[SwitchButton, int]]) -> None: def set_buttons(self, buttons: Iterable[Union[SwitchButton, int]]) -> None:
"""Replace the current button bitmask with the provided buttons.""" """Replace the current button bitmask with the provided buttons."""
self.report.buttons = 0 self.report.buttons = 0
self.press(*buttons) self.press(*buttons)
def set_hat(self, hat: Union[SwitchHat, int]) -> None: def set_hat(self, hat: Union[SwitchDpad, int]) -> None:
"""Set the DPAD/hat value directly.""" """Set the DPAD/hat value directly."""
self.report.hat = int(hat) & 0xFF self.report.hat = SwitchDpad(int(hat) & 0xFF)
def move_left_stick(self, x: Union[int, float], y: Union[int, float]) -> None: def move_left_stick(self, x: Union[int, float], y: Union[int, float]) -> None:
"""Move the left stick using normalized floats (-1..1) or raw bytes (0-255).""" """Move the left stick using normalized floats (-1..1) or raw bytes (0-255)."""
@ -247,7 +255,7 @@ class SwitchControllerState:
def neutral(self) -> None: def neutral(self) -> None:
"""Clear all input back to the neutral controller state.""" """Clear all input back to the neutral controller state."""
self.report.buttons = 0 self.report.buttons = 0
self.report.hat = SwitchHat.CENTER self.report.hat = SwitchDpad.CENTER
self.report.lx = 128 self.report.lx = 128
self.report.ly = 128 self.report.ly = 128
self.report.rx = 128 self.report.rx = 128
@ -266,11 +274,30 @@ class SwitchUARTClient:
client.move_left_stick(0.0, -1.0) # push up client.move_left_stick(0.0, -1.0) # push up
""" """
def __init__(self, port: str, baud: int = UART_BAUD, send_interval: float = 0.0) -> None: def __init__(
self,
port: str,
baud: int = UART_BAUD,
send_interval: float = 1.0 / 500.0,
auto_send: bool = True,
) -> None:
"""
Args:
port: Serial port path (e.g., 'COM5' or '/dev/cu.usbserial-0001').
baud: UART baud rate.
send_interval: Minimum interval between sends in seconds (defaults to 500 Hz).
auto_send: If True, keep sending the current state in a background thread so the
Pico continuously sees the latest input (mirrors controller_uart_bridge).
"""
self.uart = PicoUART(port, baud) self.uart = PicoUART(port, baud)
self.state = SwitchControllerState() self.state = SwitchControllerState()
self.send_interval = max(0.0, send_interval) self.send_interval = max(0.0, send_interval)
self._last_send = 0.0 self._last_send = 0.0
self._auto_send = auto_send
self._stop_event = threading.Event()
self._auto_thread: Optional[threading.Thread] = None
if self._auto_send:
self._start_auto_send_thread()
def send(self) -> None: def send(self) -> None:
"""Send the current state to the Pico, throttled by send_interval if set.""" """Send the current state to the Pico, throttled by send_interval if set."""
@ -280,19 +307,35 @@ class SwitchUARTClient:
self.uart.send_report(self.state.report) self.uart.send_report(self.state.report)
self._last_send = now self._last_send = now
def press(self, *buttons: int) -> None: def _start_auto_send_thread(self) -> None:
"""Continuously send the current state so the Pico stays active."""
if self._auto_thread is not None:
return
sleep_time = self.send_interval if self.send_interval > 0 else 0.002
def loop() -> None:
while not self._stop_event.is_set():
self.send()
self._stop_event.wait(sleep_time)
self._auto_thread = threading.Thread(target=loop, daemon=True)
self._auto_thread.start()
def press(self, *buttons: SwitchButton | SwitchDpad | int) -> None:
"""Press buttons or set hat using SwitchButton/SwitchDpad (ints also allowed)."""
self.state.press(*buttons) self.state.press(*buttons)
self.send() self.send()
def release(self, *buttons: int) -> None: def release(self, *buttons: SwitchButton | SwitchDpad | int) -> None:
"""Release buttons or center hat when given a SwitchDpad."""
self.state.release(*buttons) self.state.release(*buttons)
self.send() self.send()
def set_buttons(self, buttons: Iterable[int]) -> None: def set_buttons(self, buttons: Iterable[SwitchButton | int]) -> None:
self.state.set_buttons(buttons) self.state.set_buttons(buttons)
self.send() self.send()
def set_hat(self, hat: int) -> None: def set_hat(self, hat: SwitchDpad | int) -> None:
self.state.set_hat(hat) self.state.set_hat(hat)
self.send() self.send()
@ -304,6 +347,32 @@ class SwitchUARTClient:
self.state.move_right_stick(x, y) self.state.move_right_stick(x, y)
self.send() self.send()
def press_for(self, duration: float, *buttons: SwitchButton | SwitchDpad | int) -> None:
"""Press buttons/hat for a duration, then release."""
self.press(*buttons)
time.sleep(max(0.0, duration))
self.release(*buttons)
def move_left_stick_for(
self, x: Union[int, float], y: Union[int, float], duration: float, neutral_after: bool = True
) -> None:
"""Move left stick for a duration, optionally returning it to neutral afterward."""
self.move_left_stick(x, y)
time.sleep(max(0.0, duration))
if neutral_after:
self.state.move_left_stick(128, 128)
self.send()
def move_right_stick_for(
self, x: Union[int, float], y: Union[int, float], duration: float, neutral_after: bool = True
) -> None:
"""Move right stick for a duration, optionally returning it to neutral afterward."""
self.move_right_stick(x, y)
time.sleep(max(0.0, duration))
if neutral_after:
self.state.move_right_stick(128, 128)
self.send()
def neutral(self) -> None: def neutral(self) -> None:
self.state.neutral() self.state.neutral()
self.send() self.send()
@ -319,6 +388,10 @@ class SwitchUARTClient:
return None return None
def close(self) -> None: def close(self) -> None:
if self._auto_thread:
self._stop_event.set()
self._auto_thread.join(timeout=0.5)
self._auto_thread = None
self.uart.close() self.uart.close()
def __enter__(self) -> "SwitchUARTClient": def __enter__(self) -> "SwitchUARTClient":