feat: implement controller interface
This commit is contained in:
		
							
								
								
									
										23
									
								
								scripts/recorder.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								scripts/recorder.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | from PyQt6.QtWidgets import QApplication | ||||||
|  |  | ||||||
|  | from src.recorder import RecorderWindow | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main(): | ||||||
|  |     import sys | ||||||
|  |  | ||||||
|  |     def except_hook(cls, exception, traceback): | ||||||
|  |         sys.__excepthook__(cls, exception, traceback) | ||||||
|  |  | ||||||
|  |     sys.excepthook = except_hook | ||||||
|  |  | ||||||
|  |     app = QApplication(sys.argv) | ||||||
|  |     window = RecorderWindow("localhost", 5000) | ||||||
|  |     app.aboutToQuit.connect(window.shutdown) | ||||||
|  |     window.show() | ||||||
|  |  | ||||||
|  |     app.exec() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
							
								
								
									
										181
									
								
								src/recorder.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								src/recorder.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,181 @@ | |||||||
|  | import socket | ||||||
|  | import struct | ||||||
|  |  | ||||||
|  | from PyQt6 import uic | ||||||
|  | from PyQt6.QtCore import QObject, Qt, QThread, QTimer, pyqtSignal, pyqtSlot | ||||||
|  | from PyQt6.QtWidgets import QMainWindow | ||||||
|  |  | ||||||
|  | from src.command import CarControl, Command, ControlCommand | ||||||
|  | from src.recorder_ui import Ui_Recorder | ||||||
|  | from src.snapshot import Snapshot | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RecorderClient(QObject): | ||||||
|  |     DATA_CHUNK_SIZE = 4096 | ||||||
|  |     data_received: pyqtSignal = pyqtSignal(Snapshot) | ||||||
|  |  | ||||||
|  |     def __init__(self, host: str, port: int) -> None: | ||||||
|  |         super().__init__() | ||||||
|  |         self.host: str = host | ||||||
|  |         self.port: int = port | ||||||
|  |         self.socket: socket.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||||||
|  |         self.timer: QTimer = QTimer(self) | ||||||
|  |         self.timer.timeout.connect(self.poll_socket) | ||||||
|  |         self.connected: bool = False | ||||||
|  |  | ||||||
|  |     @pyqtSlot() | ||||||
|  |     def start(self): | ||||||
|  |         self.socket.connect((self.host, self.port)) | ||||||
|  |         self.socket.setblocking(False) | ||||||
|  |         self.connected = True | ||||||
|  |         self.timer.start(50) | ||||||
|  |         print(f"Connected to server") | ||||||
|  |  | ||||||
|  |     def poll_socket(self): | ||||||
|  |         buffer: bytes = b"" | ||||||
|  |         if not self.connected: | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         try: | ||||||
|  |             chunk: bytes = self.socket.recv(self.DATA_CHUNK_SIZE) | ||||||
|  |             if not chunk: | ||||||
|  |                 return | ||||||
|  |             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) | ||||||
|  |         except BlockingIOError: | ||||||
|  |             pass | ||||||
|  |         except Exception as e: | ||||||
|  |             print(f"Socket error: {e}") | ||||||
|  |             self.shutdown() | ||||||
|  |  | ||||||
|  |     def on_message(self, message: bytes): | ||||||
|  |         snapshot: Snapshot = Snapshot.unpack(message) | ||||||
|  |         self.data_received.emit(snapshot) | ||||||
|  |  | ||||||
|  |     @pyqtSlot(object) | ||||||
|  |     def send_command(self, command): | ||||||
|  |         if self.connected: | ||||||
|  |             try: | ||||||
|  |                 payload: bytes = command.pack() | ||||||
|  |                 self.socket.sendall(struct.pack(">I", len(payload)) + payload) | ||||||
|  |             except Exception as e: | ||||||
|  |                 print(f"An exception occured: {e}") | ||||||
|  |                 self.shutdown() | ||||||
|  |         else: | ||||||
|  |             print("Not connected") | ||||||
|  |  | ||||||
|  |     @pyqtSlot() | ||||||
|  |     def shutdown(self): | ||||||
|  |         print("Shutting down client") | ||||||
|  |         self.timer.stop() | ||||||
|  |         self.connected = False | ||||||
|  |         self.socket.close() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RecorderWindow(Ui_Recorder, QMainWindow): | ||||||
|  |     close_signal: pyqtSignal = pyqtSignal() | ||||||
|  |     send_signal: pyqtSignal = pyqtSignal(object) | ||||||
|  |  | ||||||
|  |     def __init__(self, host: str, port: int) -> None: | ||||||
|  |         super().__init__() | ||||||
|  |  | ||||||
|  |         self.host: str = host | ||||||
|  |         self.port: int = port | ||||||
|  |         self.client_thread: QThread = QThread() | ||||||
|  |         self.client: RecorderClient = RecorderClient(self.host, self.port) | ||||||
|  |         self.client.data_received.connect(self.on_snapshot_received) | ||||||
|  |         self.client.moveToThread(self.client_thread) | ||||||
|  |         self.client_thread.started.connect(self.client.start) | ||||||
|  |         self.close_signal.connect(self.client.shutdown) | ||||||
|  |         self.send_signal.connect(self.client.send_command) | ||||||
|  |  | ||||||
|  |         uic.load_ui.loadUi("src/recorder.ui", self) | ||||||
|  |  | ||||||
|  |         self.command_directions = { | ||||||
|  |             "w": CarControl.FORWARD, | ||||||
|  |             "s": CarControl.BACKWARD, | ||||||
|  |             "d": CarControl.RIGHT, | ||||||
|  |             "a": CarControl.LEFT, | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         self.forwardButton.pressed.connect( | ||||||
|  |             lambda: self.on_car_controlled(CarControl.FORWARD, True) | ||||||
|  |         ) | ||||||
|  |         self.forwardButton.released.connect( | ||||||
|  |             lambda: self.on_car_controlled(CarControl.FORWARD, False) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.backwardButton.pressed.connect( | ||||||
|  |             lambda: self.on_car_controlled(CarControl.BACKWARD, True) | ||||||
|  |         ) | ||||||
|  |         self.backwardButton.released.connect( | ||||||
|  |             lambda: self.on_car_controlled(CarControl.BACKWARD, False) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.rightButton.pressed.connect( | ||||||
|  |             lambda: self.on_car_controlled(CarControl.RIGHT, True) | ||||||
|  |         ) | ||||||
|  |         self.rightButton.released.connect( | ||||||
|  |             lambda: self.on_car_controlled(CarControl.RIGHT, False) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.leftButton.pressed.connect( | ||||||
|  |             lambda: self.on_car_controlled(CarControl.LEFT, True) | ||||||
|  |         ) | ||||||
|  |         self.leftButton.released.connect( | ||||||
|  |             lambda: self.on_car_controlled(CarControl.LEFT, False) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.recordDataButton.clicked.connect(self.toggle_record) | ||||||
|  |         self.resetButton.clicked.connect(self.rollback) | ||||||
|  |  | ||||||
|  |         self.autopiloting = False | ||||||
|  |  | ||||||
|  |         self.autopilotButton.clicked.connect(self.toggle_autopilot) | ||||||
|  |  | ||||||
|  |         self.saveRecordButton.clicked.connect(self.save_record) | ||||||
|  |  | ||||||
|  |         self.recording = False | ||||||
|  |  | ||||||
|  |         self.recorded_data = [] | ||||||
|  |         self.client_thread.start() | ||||||
|  |  | ||||||
|  |     def on_car_controlled(self, control: CarControl, active: bool): | ||||||
|  |         self.send_command(ControlCommand(control, active)) | ||||||
|  |  | ||||||
|  |     def toggle_record(self): | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  |     def rollback(self): | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  |     def toggle_autopilot(self): | ||||||
|  |         self.autopiloting = not self.autopiloting | ||||||
|  |         self.autopilotButton.setText( | ||||||
|  |             "AutoPilot:\n" + ("ON" if self.autopiloting else "OFF") | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def save_record(self): | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  |     @pyqtSlot(Snapshot) | ||||||
|  |     def on_snapshot_received(self, snapshot: Snapshot): | ||||||
|  |         self.recorded_data.append(snapshot) | ||||||
|  |         self.nbrSnapshotSaved.setText(str(len(self.recorded_data))) | ||||||
|  |  | ||||||
|  |     def shutdown(self): | ||||||
|  |         self.close_signal.emit() | ||||||
|  |  | ||||||
|  |     def send_command(self, command: Command): | ||||||
|  |         self.send_signal.emit(command) | ||||||
		Reference in New Issue
	
	Block a user