First Commit
This commit is contained in:
200
Deploy_Tienkung/udp_loopback/udp_keyboard_sender.py
Normal file
200
Deploy_Tienkung/udp_loopback/udp_keyboard_sender.py
Normal file
@@ -0,0 +1,200 @@
|
||||
"""Keyboard sender that encodes key events and emits them over localhost UDP."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import select
|
||||
import signal
|
||||
import socket
|
||||
import sys
|
||||
import termios
|
||||
import threading
|
||||
import tty
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
|
||||
import yaml
|
||||
|
||||
try:
|
||||
from .protocol import InputEnvelope
|
||||
except ImportError: # pragma: no cover - direct script execution fallback
|
||||
from protocol import InputEnvelope
|
||||
|
||||
|
||||
class UDPKeyboardSender:
|
||||
"""Standalone keyboard sender for localhost UDP testing."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.config: Dict[str, object] = {}
|
||||
self.seq_id = 0
|
||||
self.running = False
|
||||
self.socket: Optional[socket.socket] = None
|
||||
self.input_thread: Optional[threading.Thread] = None
|
||||
self.original_terminal_settings = None
|
||||
|
||||
self._load_config()
|
||||
self._init_socket()
|
||||
self._print_help()
|
||||
|
||||
def _load_config(self) -> None:
|
||||
config_path = Path(__file__).resolve().parent / "config" / "udp_loopback.yaml"
|
||||
with config_path.open("r", encoding="utf-8") as file:
|
||||
self.config = yaml.safe_load(file) or {}
|
||||
|
||||
sender_cfg = self.config.get("sender", {})
|
||||
self.target_host = sender_cfg.get("target_host", "127.0.0.1")
|
||||
self.target_port = int(sender_cfg.get("target_port", 31000))
|
||||
self.source_tag = sender_cfg.get("source_tag", "local_keys")
|
||||
|
||||
def _init_socket(self) -> None:
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
|
||||
def _print_help(self) -> None:
|
||||
print("UDP keyboard sender ready")
|
||||
print(f"Target: {self.target_host}:{self.target_port}")
|
||||
print("Keys:")
|
||||
print(" z -> pose_home")
|
||||
print(" c -> pose_hold")
|
||||
print(" m -> mode_stride")
|
||||
print(" p -> mode_dash")
|
||||
print(" n -> mode_xrun")
|
||||
print(" w/s -> surge +/-")
|
||||
print(" a/d -> sway +/-")
|
||||
print(" q/e -> spin +/-")
|
||||
print(" Left/Right -> lift +/-")
|
||||
print(" Up/Down -> surge +/-")
|
||||
print(" r -> trim_reset")
|
||||
print(" x -> session_quit")
|
||||
|
||||
def start(self) -> None:
|
||||
self.running = True
|
||||
self.input_thread = threading.Thread(target=self._input_loop, daemon=True)
|
||||
self.input_thread.start()
|
||||
print("UDP keyboard sender thread started")
|
||||
|
||||
def stop(self) -> None:
|
||||
self.running = False
|
||||
if self.input_thread and self.input_thread.is_alive():
|
||||
self.input_thread.join(timeout=1.0)
|
||||
if self.original_terminal_settings is not None:
|
||||
try:
|
||||
termios.tcsetattr(
|
||||
sys.stdin, termios.TCSADRAIN, self.original_terminal_settings
|
||||
)
|
||||
except termios.error:
|
||||
pass
|
||||
self.original_terminal_settings = None
|
||||
if self.socket is not None:
|
||||
self.socket.close()
|
||||
self.socket = None
|
||||
print("UDP keyboard sender stopped")
|
||||
|
||||
def _input_loop(self) -> None:
|
||||
self.original_terminal_settings = termios.tcgetattr(sys.stdin)
|
||||
try:
|
||||
tty.setraw(sys.stdin.fileno())
|
||||
while self.running:
|
||||
if select.select([sys.stdin], [], [], 0.1)[0]:
|
||||
key = sys.stdin.read(1)
|
||||
self._process_key(key)
|
||||
except KeyboardInterrupt:
|
||||
self._handle_ctrl_c()
|
||||
finally:
|
||||
if self.original_terminal_settings is not None:
|
||||
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.original_terminal_settings)
|
||||
self.original_terminal_settings = None
|
||||
|
||||
def _process_key(self, key: str) -> None:
|
||||
event_map = {
|
||||
"w": ("surge_up", "w"),
|
||||
"s": ("surge_down", "s"),
|
||||
"a": ("sway_left", "a"),
|
||||
"d": ("sway_right", "d"),
|
||||
"q": ("spin_left", "q"),
|
||||
"e": ("spin_right", "e"),
|
||||
"z": ("pose_home", "z"),
|
||||
"c": ("pose_hold", "c"),
|
||||
"m": ("mode_stride", "m"),
|
||||
"p": ("mode_dash", "p"),
|
||||
"n": ("mode_xrun", "n"),
|
||||
"r": ("trim_reset", "r"),
|
||||
"x": ("session_quit", "x"),
|
||||
}
|
||||
|
||||
if key == "\x03":
|
||||
self._handle_ctrl_c()
|
||||
return
|
||||
|
||||
if key == "\x1b":
|
||||
self._handle_arrow_key()
|
||||
return
|
||||
|
||||
if key in event_map:
|
||||
event_code, key_name = event_map[key]
|
||||
self._send_event(event_code, key_name)
|
||||
|
||||
def _handle_arrow_key(self) -> None:
|
||||
if not select.select([sys.stdin], [], [], 0.1)[0]:
|
||||
return
|
||||
key2 = sys.stdin.read(1)
|
||||
if key2 != "[":
|
||||
return
|
||||
if not select.select([sys.stdin], [], [], 0.1)[0]:
|
||||
return
|
||||
key3 = sys.stdin.read(1)
|
||||
arrow_map = {
|
||||
"A": ("surge_up", "arrow_up"),
|
||||
"B": ("surge_down", "arrow_down"),
|
||||
"C": ("lift_down", "arrow_right"),
|
||||
"D": ("lift_up", "arrow_left"),
|
||||
}
|
||||
if key3 in arrow_map:
|
||||
event_code, key_name = arrow_map[key3]
|
||||
self._send_event(event_code, key_name)
|
||||
|
||||
def _send_event(self, event_code: str, key_name: str) -> None:
|
||||
if self.socket is None:
|
||||
return
|
||||
envelope = InputEnvelope(
|
||||
seq_id=self.seq_id,
|
||||
event_code=event_code,
|
||||
key_name=key_name,
|
||||
source_tag=self.source_tag,
|
||||
)
|
||||
self.seq_id += 1
|
||||
self.socket.sendto(envelope.encode(), (self.target_host, self.target_port))
|
||||
print(
|
||||
f"sent seq={envelope.seq_id} event={envelope.event_code} "
|
||||
f"key={envelope.key_name}"
|
||||
)
|
||||
if event_code == "session_quit":
|
||||
self.running = False
|
||||
|
||||
def _handle_ctrl_c(self) -> None:
|
||||
self.running = False
|
||||
if self.original_terminal_settings is not None:
|
||||
try:
|
||||
termios.tcsetattr(
|
||||
sys.stdin, termios.TCSADRAIN, self.original_terminal_settings
|
||||
)
|
||||
except termios.error:
|
||||
pass
|
||||
self.original_terminal_settings = None
|
||||
os.kill(os.getpid(), signal.SIGINT)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
sender = UDPKeyboardSender()
|
||||
sender.start()
|
||||
try:
|
||||
while sender.running:
|
||||
if sender.input_thread is not None:
|
||||
sender.input_thread.join(timeout=0.2)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
sender.stop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user