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)
For simple scripts or tests you can skip SDL and drive the Pico directly with `switch_pico_uart.py`:
```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:
client.press(SwitchButton.A)
client.release(SwitchButton.A)
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
```
- `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.
### macOS tips

View file

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

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 time
import threading
from dataclasses import dataclass, field
from enum import IntEnum, IntFlag
from typing import Iterable, Mapping, Optional, Tuple, Union
@ -44,15 +45,15 @@ class SwitchButton(IntFlag):
CAPTURE = 1 << 13
class SwitchHat(IntEnum):
TOP = 0x00
TOP_RIGHT = 0x01
class SwitchDpad(IntEnum):
UP = 0x00
UP_RIGHT = 0x01
RIGHT = 0x02
BOTTOM_RIGHT = 0x03
BOTTOM = 0x04
BOTTOM_LEFT = 0x05
DOWN_RIGHT = 0x03
DOWN = 0x04
DOWN_LEFT = 0x05
LEFT = 0x06
TOP_LEFT = 0x07
UP_LEFT = 0x07
CENTER = 0x08
@ -88,7 +89,7 @@ def trigger_to_button(value: int, threshold: int) -> bool:
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."""
up = flags.get("up", False)
down = flags.get("down", False)
@ -96,28 +97,28 @@ def dpad_to_hat(flags: Mapping[str, bool]) -> SwitchHat:
right = flags.get("right", False)
if up and right:
return SwitchHat.TOP_RIGHT
return SwitchDpad.UP_RIGHT
if up and left:
return SwitchHat.TOP_LEFT
return SwitchDpad.UP_LEFT
if down and right:
return SwitchHat.BOTTOM_RIGHT
return SwitchDpad.DOWN_RIGHT
if down and left:
return SwitchHat.BOTTOM_LEFT
return SwitchDpad.DOWN_LEFT
if up:
return SwitchHat.TOP
return SwitchDpad.UP
if down:
return SwitchHat.BOTTOM
return SwitchDpad.DOWN
if right:
return SwitchHat.RIGHT
return SwitchDpad.RIGHT
if left:
return SwitchHat.LEFT
return SwitchHat.CENTER
return SwitchDpad.LEFT
return SwitchDpad.CENTER
@dataclass
class SwitchReport:
buttons: int = 0
hat: SwitchHat = SwitchHat.CENTER
hat: SwitchDpad = SwitchDpad.CENTER
lx: int = 128
ly: int = 128
rx: int = 128
@ -215,24 +216,31 @@ class SwitchControllerState:
report: SwitchReport = field(default_factory=SwitchReport)
def press(self, *buttons: Union[SwitchButton, int]) -> None:
"""Set one or more buttons as pressed."""
for button in buttons:
self.report.buttons |= int(button)
def press(self, *buttons_or_hat: Union[SwitchButton, SwitchDpad, int]) -> None:
"""Press one or more buttons, or set the hat if a SwitchDpad is provided."""
for item in buttons_or_hat:
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:
"""Release one or more buttons."""
for button in buttons:
self.report.buttons &= ~int(button)
def release(self, *buttons_or_hat: Union[SwitchButton, SwitchDpad, int]) -> None:
"""Release one or more buttons, or center the hat if a SwitchDpad is provided."""
for item in buttons_or_hat:
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:
"""Replace the current button bitmask with the provided buttons."""
self.report.buttons = 0
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."""
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:
"""Move the left stick using normalized floats (-1..1) or raw bytes (0-255)."""
@ -247,7 +255,7 @@ class SwitchControllerState:
def neutral(self) -> None:
"""Clear all input back to the neutral controller state."""
self.report.buttons = 0
self.report.hat = SwitchHat.CENTER
self.report.hat = SwitchDpad.CENTER
self.report.lx = 128
self.report.ly = 128
self.report.rx = 128
@ -266,11 +274,30 @@ class SwitchUARTClient:
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.state = SwitchControllerState()
self.send_interval = max(0.0, send_interval)
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:
"""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._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.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.send()
def set_buttons(self, buttons: Iterable[int]) -> None:
def set_buttons(self, buttons: Iterable[SwitchButton | int]) -> None:
self.state.set_buttons(buttons)
self.send()
def set_hat(self, hat: int) -> None:
def set_hat(self, hat: SwitchDpad | int) -> None:
self.state.set_hat(hat)
self.send()
@ -304,6 +347,32 @@ class SwitchUARTClient:
self.state.move_right_stick(x, y)
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:
self.state.neutral()
self.send()
@ -319,6 +388,10 @@ class SwitchUARTClient:
return 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()
def __enter__(self) -> "SwitchUARTClient":