remote-car / Python / server.py
server.py
Raw

import socket
import threading
import struct
from enum import IntEnum

from printer import perror

class METRIC(IntEnum):
    MODEM_TEMP = 3                      # celsius   | int
    CAMERA_TEMP = 5                     # celsius   | int
    CPU_TEMP = 6                        # celsius   | int  (Average temp of the high performance core cluster)
    GPU_TEMP = 12                       # celsius   | int
    BATTERY_TEMP = 42                   # celsius   | int
    PHONE_BATTERY_PERCENT = 100         # [0-100]   | int
    POSITION = 101                      # LATITUDE  | float,
                                        # LONGITUDE | float,
                                        # ACCURACY  | int
    HEADING = 102                       # degrees   | int
    SIGNAL_LEVEL = 103                  # [0-5]     | int
    CAR_BATTERY_VOLTAGE = 104           # centiVolt | int
    ELECTRONICS_BATTERY_VOLTAGE = 105   # centiVolt | int

# Metrics that are displayed on the video stream
STREAM_METRICS = [
    METRIC.PHONE_BATTERY_PERCENT, 
    METRIC.HEADING, 
    METRIC.SIGNAL_LEVEL, 
    METRIC.CAR_BATTERY_VOLTAGE,
    METRIC.ELECTRONICS_BATTERY_VOLTAGE
]
TEMP_METRICS = [
    METRIC.MODEM_TEMP, 
    METRIC.CAMERA_TEMP, 
    METRIC.CPU_TEMP, 
    METRIC.GPU_TEMP, 
    METRIC.BATTERY_TEMP
]

class Server:
    def __init__(self, telemetry_callback):
        self.host = '0.0.0.0'
        self.control_port = 8003

        self.control_socket = None
        self.control_client = None
        self.telemetry_callback = telemetry_callback
        self.running = False
            
    def start_control_server(self):
        try:
            self.control_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.control_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            self.control_socket.bind((self.host, self.control_port))
            self.control_socket.listen(1)
            print(f"CONTROL SERVER thread listening on TCP {self.host}:{self.control_port}")
        except Exception as e:
            perror(f"Failed to start the control server: {e}")
        
        # Control socket initialized, check forever for incoming connections to accept
        while self.running:
            try:
                # Set a timeout to check the running flag periodically
                self.control_socket.settimeout(1.0)
                client, addr = self.control_socket.accept()
                client.settimeout(1.0)
                self.control_client = client
                print(f"Control client connected from {addr}")
            except socket.timeout:
                continue # No one is connecting (expected, just loop again)

            # Client succesfully connected, check forever for incoming messages to decode
            while self.running:
                try:
                    self._recv_telemetry()
                except socket.timeout:
                    # The client simply is not sending anything (expected, just loop again)
                    continue
                except Exception as e:
                    perror(f"Control client error: {e}")
                    break
            
            # Connection broke - either gracefully or not
            self.control_client.close()
            self.control_client = None
            print("Control client disconnected")

    def _recv_telemetry(self):
        # The first byte identifies the metric type
        packet = self.control_client.recv(1)
        if not packet:
            raise ConnectionResetError("connection lost")
        metric = METRIC(packet[0])
        
        # Next, each metric encodes its values in 4-bytes numbers
        # Every metric except for position uses one 4-byte integer
        # Position uses three 4-byte floats
        data = b''
        expected_bytes = 4
        if metric == METRIC.POSITION:
            expected_bytes *= 3

        while len(data) < expected_bytes:
            packet = self.control_client.recv(expected_bytes - len(data))
            if not packet:
                raise ConnectionResetError("connection lost")
            data += packet
        
        if metric == METRIC.POSITION:
            value = []
            value.append(struct.unpack('>f', data[:4])[0])  # big-endian float
            value.append(struct.unpack('>f', data[4:8])[0]) # big-endian float
            value.append(struct.unpack('>i', data[8:])[0])  # big-endian int
        else:
            value = struct.unpack('>i', data)[0] # big-endian int

        # Consume the decoded data
        self.telemetry_callback(metric, value)

    def send_command(self, command):
        # There might be race conditions because self.control_client
        #  is used both here and by the listening deamon
        if not self.running:
            print("The server is not running")
            return
        if not self.control_client: 
            print("No control client connected")
            return
    
        try:
            msglen = len(command)
            totalsent = 0
            while totalsent < msglen:
                sent = self.control_client.send(command[totalsent:])
                if sent == 0:
                    raise ConnectionResetError("connection lost")
                totalsent = totalsent + sent
        except Exception as e:
            perror(f"Unable to send command: {e}")
            self.control_client.close()
            self.control_client = None
            print("Control client disconnected")

    def stop(self):
        self.running = False
        if self.control_client:
            self.control_client.close()
            self.control_client = None
        if self.control_socket:
            self.control_socket.close()
            self.control_socket = None
        print("CONTROL SERVER thread terminated")

    def start(self):
        self.running = True
        threading.Thread(
           target=self.start_control_server, 
           daemon=True
        ).start()
        print("CONTROL SERVER thread launched")