Make example work and redo some library code
This commit is contained in:
parent
abdda5a7c4
commit
1cb7075180
4 changed files with 172 additions and 41 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
58
example_switch_macro.py
Normal 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()
|
||||
|
|
@ -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":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue