vita_gamepad/vitapad/receiver.py

124 lines
4.0 KiB
Python

from __future__ import annotations
import socket
import threading
import time
from collections.abc import Callable
from vitapad.backends import GamepadBackend
from vitapad.protocol import (
DISCOVERY_MAGIC,
InputState,
decode_packet,
sequence_is_newer,
)
class Receiver:
def __init__(
self,
backend: GamepadBackend,
bind: str = "0.0.0.0",
port: int = 5000,
discovery_port: int = 5001,
allow: str | None = None,
timeout_ms: int = 300,
log: Callable[[str], None] = print,
on_input: Callable[[InputState], None] | None = None,
) -> None:
self.backend = backend
self.bind = bind
self.port = port
self.discovery_port = discovery_port
self.allow = allow
self.timeout = timeout_ms / 1000.0
self.log = log
self.on_input = on_input
self._stop = threading.Event()
self._last_packet = 0.0
self._connected_address: str | None = None
self._last_sequence: int | None = None
self._neutral_sent = True
def _broadcast_discovery(self) -> None:
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as beacon:
beacon.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
while not self._stop.is_set():
try:
beacon.sendto(
DISCOVERY_MAGIC, ("255.255.255.255", self.discovery_port)
)
except OSError:
# Interfaces can disappear during sleep/network changes.
pass
self._stop.wait(1.0)
def run(self) -> None:
discovery = threading.Thread(
target=self._broadcast_discovery, name="discovery", daemon=True
)
discovery.start()
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as listener:
listener.bind((self.bind, self.port))
listener.settimeout(0.05)
self.log(
f"正在监听 {self.bind}:{self.port},后端: {self.backend.name}\n"
"等待 PSVita..."
)
try:
while not self._stop.is_set():
self._receive_once(listener)
self._apply_failsafe()
finally:
self._stop.set()
discovery.join(timeout=1.2)
if self.on_input is not None:
self.on_input(InputState.neutral())
self.backend.close()
def _receive_once(self, listener: socket.socket) -> None:
try:
data, address = listener.recvfrom(256)
except socket.timeout:
return
if self.allow and address[0] != self.allow:
return
try:
state = decode_packet(data)
except ValueError:
return
if self._connected_address != address[0]:
self._connected_address = address[0]
self._last_sequence = None
self.log(f"已连接 PSVita: {address[0]}")
if self._last_sequence is not None and not sequence_is_newer(
state.sequence, self._last_sequence
):
return
self._last_sequence = state.sequence
self._last_packet = time.monotonic()
self._neutral_sent = False
self.backend.update(state)
if self.on_input is not None:
self.on_input(state)
def _apply_failsafe(self) -> None:
if (
not self._neutral_sent
and time.monotonic() - self._last_packet >= self.timeout
):
self.backend.update(InputState.neutral())
if self.on_input is not None:
self.on_input(InputState.neutral())
self._neutral_sent = True
self.log("连接超时,已释放全部按键;等待重连...")
def stop(self) -> None:
self._stop.set()
@property
def connected_address(self) -> str | None:
if self._neutral_sent:
return None
return self._connected_address