feat: add remote controller server

This commit is contained in:
2025-10-22 22:17:30 +02:00
parent 784d594a3b
commit bf1935fd7e
3 changed files with 114 additions and 1 deletions

View File

@@ -4,10 +4,10 @@ from typing import Optional
import pygame import pygame
from src.camera import Camera from src.camera import Camera
from src.remote_controller import RemoteController
from src.utils import get_segments_intersection, segments_intersect from src.utils import get_segments_intersection, segments_intersect
from src.vec import Vec from src.vec import Vec
sign = lambda x: 0 if x == 0 else (-1 if x < 0 else 1) sign = lambda x: 0 if x == 0 else (-1 if x < 0 else 1)
@@ -38,6 +38,9 @@ class Car:
self.rays: list[float] = [0] * self.N_RAYS self.rays: list[float] = [0] * self.N_RAYS
self.rays_end: list[Vec] = [Vec() for _ in range(self.N_RAYS)] self.rays_end: list[Vec] = [Vec() for _ in range(self.N_RAYS)]
self.controller: RemoteController = RemoteController(self)
self.controller.start_server()
def update(self, dt: float): def update(self, dt: float):
if self.forward: if self.forward:
self.speed += self.ACCELERATION * dt self.speed += self.ACCELERATION * dt

View File

@@ -37,6 +37,7 @@ class Game:
while self.running: while self.running:
dt: float = self.clock.get_time() / 1000 dt: float = self.clock.get_time() / 1000
self.process_pygame_events() self.process_pygame_events()
self.car.controller.process_commands()
self.car.update(dt) self.car.update(dt)
self.car.check_collisions(self.track.get_collision_polygons()) self.car.check_collisions(self.track.get_collision_polygons())
self.update_camera() self.update_camera()
@@ -64,6 +65,7 @@ class Game:
def quit(self): def quit(self):
self.running = False self.running = False
self.car.controller.close()
def render(self): def render(self):
self.win.fill(self.BACKGROUND_COLOR) self.win.fill(self.BACKGROUND_COLOR)

108
src/remote_controller.py Normal file
View File

@@ -0,0 +1,108 @@
from __future__ import annotations
import queue
import socket
import struct
import threading
from typing import TYPE_CHECKING, Optional
from src.command import CarControl, Command, ControlCommand
if TYPE_CHECKING:
from src.car import Car
class RemoteController:
DEFAULT_PORT = 5000
DATA_CHUNK_SIZE = 4096
CONTROL_ATTRIBUTES: dict[CarControl, str] = {
CarControl.FORWARD: "forward",
CarControl.BACKWARD: "backward",
CarControl.LEFT: "left",
CarControl.RIGHT: "right",
}
def __init__(self, car: Car, port: int = DEFAULT_PORT) -> None:
self.car: Car = car
self.port: int = port
self.server: socket.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server_thread: threading.Thread = threading.Thread(
target=self.wait_for_connections, daemon=True
)
self.running: bool = False
self.queue: queue.Queue[Command] = queue.Queue()
self.client_thread: Optional[threading.Thread] = None
self.client: Optional[socket.socket] = None
def wait_for_connections(self):
self.server.bind(("", self.port))
self.server.listen(1)
print(f"Remote control server listening on port {self.port}")
while self.running:
conn, addr = self.server.accept()
print(f"Remote connection from {addr}")
self.on_client_connected(conn)
def start_server(self):
self.running = True
self.server_thread.start()
def close(self):
if self.client:
self.client.close()
self.server.close()
self.running = False
def on_client_connected(self, conn: socket.socket):
if self.client:
print("A client is already connected")
conn.close()
return
self.client = conn
self.client_thread = threading.Thread(target=self.client_loop)
self.client_thread.start()
def client_loop(self):
buffer: bytes = b""
while self.running and self.client:
chunk: bytes = self.client.recv(self.DATA_CHUNK_SIZE)
if not chunk:
print("Client disconnected")
break
buffer += chunk
while True:
if len(buffer) < 4:
break
msg_len: int = struct.unpack(">I", buffer[:4])[0]
msg_end: int = 4 + msg_len
if len(buffer) < msg_end:
break
message: bytes = buffer[4:msg_end]
buffer = buffer[msg_end:]
self.on_message(message)
if self.client:
self.client.close()
self.client = None
self.client_thread = None
def on_message(self, message: bytes):
command: Command = Command.unpack(message)
self.queue.put(command)
def process_commands(self):
while not self.queue.empty():
command: Command = self.queue.get()
self.process_command(command)
def process_command(self, command: Command):
match command:
case ControlCommand(control, active):
self.set_control(control, active)
def set_control(self, control: CarControl, active: bool):
setattr(self.car, self.CONTROL_ATTRIBUTES[control], active)