feat: add remote controller server
This commit is contained in:
@@ -4,10 +4,10 @@ from typing import Optional
|
||||
import pygame
|
||||
|
||||
from src.camera import Camera
|
||||
from src.remote_controller import RemoteController
|
||||
from src.utils import get_segments_intersection, segments_intersect
|
||||
from src.vec import Vec
|
||||
|
||||
|
||||
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_end: list[Vec] = [Vec() for _ in range(self.N_RAYS)]
|
||||
|
||||
self.controller: RemoteController = RemoteController(self)
|
||||
self.controller.start_server()
|
||||
|
||||
def update(self, dt: float):
|
||||
if self.forward:
|
||||
self.speed += self.ACCELERATION * dt
|
||||
|
||||
@@ -37,6 +37,7 @@ class Game:
|
||||
while self.running:
|
||||
dt: float = self.clock.get_time() / 1000
|
||||
self.process_pygame_events()
|
||||
self.car.controller.process_commands()
|
||||
self.car.update(dt)
|
||||
self.car.check_collisions(self.track.get_collision_polygons())
|
||||
self.update_camera()
|
||||
@@ -64,6 +65,7 @@ class Game:
|
||||
|
||||
def quit(self):
|
||||
self.running = False
|
||||
self.car.controller.close()
|
||||
|
||||
def render(self):
|
||||
self.win.fill(self.BACKGROUND_COLOR)
|
||||
|
||||
108
src/remote_controller.py
Normal file
108
src/remote_controller.py
Normal 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)
|
||||
Reference in New Issue
Block a user