mirror of
https://github.com/Rulez777/FramePicoTracker.git
synced 2026-10-18 05:01:14 +02:00
124 lines
4.1 KiB
Python
124 lines
4.1 KiB
Python
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
# Copyright (C) 2026 L8OLOOE and contributors. See CREDITS.md and NOTICE.md.
|
|
"""TCP Receiver for Pico Motion Tracker Wire Protocol v1.
|
|
|
|
Listens on TCP 127.0.0.1:39500 for incoming connections forwarded via
|
|
`adb reverse tcp:39500 tcp:39500`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import socket
|
|
from typing import Callable, Optional
|
|
|
|
from protocol import (
|
|
HEADER_SIZE,
|
|
TYPE_FRAME,
|
|
TYPE_HELLO,
|
|
FramePayload,
|
|
HelloPayload,
|
|
unpack_frame,
|
|
unpack_header,
|
|
unpack_hello,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
DEFAULT_TCP_HOST = "127.0.0.1"
|
|
DEFAULT_TCP_PORT = 39500
|
|
|
|
|
|
def _recv_exact(sock: socket.socket, num_bytes: int) -> Optional[bytes]:
|
|
"""Read exactly num_bytes from socket, or return None if disconnected."""
|
|
buf = bytearray()
|
|
while len(buf) < num_bytes:
|
|
chunk = sock.recv(num_bytes - len(buf))
|
|
if not chunk:
|
|
return None
|
|
buf.extend(chunk)
|
|
return bytes(buf)
|
|
|
|
|
|
class StreamReceiver:
|
|
"""TCP Server receiving Wire Protocol v1 messages from Pico headset."""
|
|
|
|
def __init__(
|
|
self,
|
|
host: str = DEFAULT_TCP_HOST,
|
|
port: int = DEFAULT_TCP_PORT,
|
|
on_hello: Optional[Callable[[HelloPayload], None]] = None,
|
|
on_frame: Optional[Callable[[FramePayload], None]] = None,
|
|
) -> None:
|
|
self.host = host
|
|
self.port = port
|
|
self.on_hello = on_hello
|
|
self.on_frame = on_frame
|
|
self._running = False
|
|
self._server_sock: Optional[socket.socket] = None
|
|
|
|
def start(self) -> None:
|
|
"""Run blocking server loop."""
|
|
self._running = True
|
|
self._server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
self._server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
self._server_sock.bind((self.host, self.port))
|
|
self._server_sock.listen(1)
|
|
logger.info("TCP Receiver listening on %s:%d", self.host, self.port)
|
|
|
|
while self._running:
|
|
try:
|
|
logger.info("Waiting for connection from Pico app (via ADB reverse)...")
|
|
conn, addr = self._server_sock.accept()
|
|
conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
|
logger.info("Connected to %s", addr)
|
|
self._handle_client(conn)
|
|
except OSError as e:
|
|
if not self._running:
|
|
break
|
|
logger.warning("Socket error in accept: %s", e)
|
|
|
|
def stop(self) -> None:
|
|
"""Stop server."""
|
|
self._running = False
|
|
if self._server_sock:
|
|
try:
|
|
self._server_sock.close()
|
|
except Exception:
|
|
pass
|
|
|
|
def _handle_client(self, conn: socket.socket) -> None:
|
|
with conn:
|
|
while self._running:
|
|
# 1. Read header
|
|
hdr_bytes = _recv_exact(conn, HEADER_SIZE)
|
|
if hdr_bytes is None:
|
|
logger.info("Client disconnected")
|
|
break
|
|
|
|
try:
|
|
hdr = unpack_header(hdr_bytes)
|
|
except ValueError as e:
|
|
logger.error("Protocol error unpacking header: %s", e)
|
|
break
|
|
|
|
# 2. Read payload
|
|
payload_bytes = _recv_exact(conn, hdr.length)
|
|
if payload_bytes is None:
|
|
logger.warning("Client disconnected before payload finished")
|
|
break
|
|
|
|
# 3. Parse and dispatch
|
|
try:
|
|
if hdr.msg_type == TYPE_HELLO:
|
|
hello = unpack_hello(payload_bytes)
|
|
logger.info("Received Hello: %s", hello)
|
|
if self.on_hello:
|
|
self.on_hello(hello)
|
|
elif hdr.msg_type == TYPE_FRAME:
|
|
frame = unpack_frame(payload_bytes)
|
|
if self.on_frame:
|
|
self.on_frame(frame)
|
|
except Exception as e:
|
|
logger.error("Error processing message type %d: %s", hdr.msg_type, e)
|