Files
radar_system/locator_test_client.py
T
2026-10-05 16:46:28 +03:00

140 lines
4.2 KiB
Python

import json
import random
import socket
import struct
import threading
from contextlib import suppress
from typing import Any
HOST = "127.0.0.1"
PORT = 8888
CLIENT_DEVICE_ID = 0
MIN_TEST_VLC = 5.0
MAX_TEST_VLC = 6.0
SEND_INTERVAL_SECONDS = 1.0
RECV_TIMEOUT_SECONDS = 1.0
CONNECT_TIMEOUT_SECONDS = 5.0
MAX_PAYLOAD_BYTES = 64 * 1024
HEADER_STRUCT = struct.Struct("<II")
def encode_packet(payload: dict[str, Any], device_id: int) -> bytes:
"""Encode a JSON payload using the protocol binary header."""
payload_bytes = json.dumps(
payload,
ensure_ascii=True,
separators=(",", ":"),
).encode("utf-8")
return HEADER_STRUCT.pack(device_id, len(payload_bytes)) + payload_bytes
def recv_exactly(sock: socket.socket, size: int, stop_event: threading.Event | None = None) -> bytes:
"""Receive an exact number of bytes from the socket."""
chunks = bytearray()
while len(chunks) < size:
if stop_event is not None and stop_event.is_set():
raise ConnectionError("Client stopped.")
try:
chunk = sock.recv(size - len(chunks))
except socket.timeout:
continue # Preserve a partial header/payload across timeout polls.
if not chunk:
raise ConnectionError("Connection closed by peer.")
chunks.extend(chunk)
return bytes(chunks)
def read_packet(sock: socket.socket, stop_event: threading.Event | None = None) -> tuple[int, Any]:
"""Read and decode a single packet from the server."""
header_bytes = recv_exactly(sock, HEADER_STRUCT.size, stop_event)
device_id, payload_length = HEADER_STRUCT.unpack(header_bytes)
if payload_length > MAX_PAYLOAD_BYTES:
raise ValueError(
"Payload length %d exceeds the %d byte limit."
% (payload_length, MAX_PAYLOAD_BYTES)
)
payload_bytes = recv_exactly(sock, payload_length, stop_event)
payload = json.loads(payload_bytes.decode("utf-8"))
return device_id, payload
def send_vlc_message(sock: socket.socket, device_id: int, vlc: float) -> None:
"""Send one test client payload to the server."""
payload = {"vlc": vlc}
sock.sendall(encode_packet(payload, device_id=device_id))
print(f'>>> sent {json.dumps(payload, ensure_ascii=False)} device_id={device_id}')
def receive_loop(sock: socket.socket, stop_event: threading.Event) -> None:
"""Print all packets received from the locator server."""
while not stop_event.is_set():
try:
device_id, payload = read_packet(sock, stop_event)
except (OSError, ValueError) as error:
if not stop_event.is_set():
print("Receiver stopped:", error)
stop_event.set()
return
print(f"<<< received device_id={device_id}")
print(json.dumps(payload, ensure_ascii=False, indent=2))
def send_loop(sock: socket.socket, stop_event: threading.Event) -> None:
"""Send test vlc packets until the client is stopped."""
send_vlc_message(
sock,
CLIENT_DEVICE_ID,
round(random.uniform(MIN_TEST_VLC, MAX_TEST_VLC), 2),
)
if SEND_INTERVAL_SECONDS <= 0:
stop_event.wait()
return
while not stop_event.wait(SEND_INTERVAL_SECONDS):
send_vlc_message(
sock,
CLIENT_DEVICE_ID,
round(random.uniform(MIN_TEST_VLC, MAX_TEST_VLC), 2),
)
def main() -> None:
"""Run the client until interrupted or disconnected."""
stop_event = threading.Event()
with socket.create_connection(
(HOST, PORT),
timeout=CONNECT_TIMEOUT_SECONDS,
) as sock:
sock.settimeout(RECV_TIMEOUT_SECONDS)
print(f"Connected to {HOST}:{PORT}")
receiver = threading.Thread(
target=receive_loop,
args=(sock, stop_event),
daemon=True,
)
receiver.start()
try:
send_loop(sock, stop_event)
except KeyboardInterrupt:
print("Stopping client.")
except OSError as error:
print("Sender stopped:", error)
finally:
stop_event.set()
with suppress(OSError):
sock.shutdown(socket.SHUT_RDWR)
receiver.join(RECV_TIMEOUT_SECONDS + 1.0)
if __name__ == "__main__":
main()