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)
|
### 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
|
||||||
|
|
|
||||||
|
|
@ -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
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 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":
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue