# compatibile Windows 11 # compatibile Ubuntu 24.10 # compatibile python 3.12.7 # Assicurati che questo codice NON sia salvato in un file chiamato serial.py # Su Linux #python serial_tcp_bridge.py /dev/ttyS0 -b 9600 -p 5000 #oppure (ambiente virtuale) #sudo .venv/bin/python serial_tcp_bridge.py /dev/ttyS0 -b 9600 -p 5000 # Su Windows #python serial_tcp_bridge.py COM4 -b 9600 -p 5000 #Opzioni: # -b : Imposta il baud rate (default 9600). # -p : Imposta la porta TCP su cui ascoltare (default 5000). # -l : Imposta l'IP su cui ascoltare (default 0.0.0.0, ascolta su tutte le interfacce). # --encoding : Specifica l'encoding (default utf-8), anche se ora il bridge lavora principalmente con i bytes. # --serial-timeout : Timeout lettura seriale (default 0.1). import serial import threading import queue import time import sys import socket import argparse # Per gestire argomenti da linea di comando # --- Classe SerialManager (leggermente adattata per chiarezza nel contesto bridge) --- class SerialManager: """ Gestisce la comunicazione seriale con ricezione in un thread separato. """ def __init__(self, port, baudrate=9600, timeout=0.1, encoding='utf-8', errors='ignore'): self.port = port self.baudrate = baudrate # Timeout basso per non bloccare troppo il thread RX e permettere check stop_event self.serial_timeout = timeout self.encoding = encoding self.errors = errors self.ser = None self.receive_thread = None self.data_queue = queue.Queue() # Coda per dati DA seriale VERSO TCP self._running = False self._thread_stop_event = threading.Event() def _receiver_loop(self): """Loop eseguito nel thread di ricezione seriale.""" print(f"[Serial RX Thread] Avviato per {self.port}") try: while not self._thread_stop_event.is_set(): if self.ser is None or not self.ser.is_open: print("[Serial RX Thread] Porta seriale non aperta. Attesa...") time.sleep(0.5) continue # Torna all'inizio del while per ricontrollare lo stato try: # Legge i byte disponibili senza bloccare indefinitamente if self.ser.in_waiting > 0: # Legge tutto quello che c'è nel buffer o fino a un newline/timeout # Usiamo read_until() per catturare linee complete se possibile, # ma con timeout per non bloccare se non arriva il terminatore. # Nota: Se ti aspetti dati binari, usa self.ser.read(self.ser.in_waiting) line_bytes = self.ser.read_until(b'\n') # Legge fino a newline o timeout if line_bytes: # Metti i bytes raw nella coda, la decodifica/invio # verrà gestita dal loop principale che manda a TCP self.data_queue.put(line_bytes) # print(f"[DEBUG Serial RX] Ricevuto bytes: {line_bytes}") # Debug else: # Nessun dato in attesa, breve pausa time.sleep(0.02) except serial.SerialException as e: print(f"[Serial RX Thread] Errore seriale: {e}. Tentativo di chiusura...") self._close_serial_port() # Tenta di chiudere la porta self._thread_stop_event.set() # Ferma questo thread self.data_queue.put(None) # Segnala errore al main loop break # Esce dal while loop except OSError as e: print(f"[Serial RX Thread] Errore OS (probabile disconnessione): {e}") self._close_serial_port() self._thread_stop_event.set() self.data_queue.put(None) break except Exception as e: print(f"[Serial RX Thread] Errore inaspettato: {e}") # Non necessariamente fatale, logga e continua (o ferma se preferisci) time.sleep(0.1) finally: print("[Serial RX Thread] Terminato.") self.data_queue.put(None) # Assicura che il segnale di fine sia in coda def _close_serial_port(self): """Metodo helper per chiudere la porta seriale in modo sicuro.""" if self.ser and self.ser.is_open: try: self.ser.close() print(f"[SerialManager] Porta {self.port} chiusa.") except Exception as e: print(f"[SerialManager] Errore durante chiusura porta {self.port}: {e}") self.ser = None def start(self): """Apre la porta seriale e avvia il thread di ricezione.""" if self._running: print("[SerialManager] Già avviato.") return True try: print(f"[SerialManager] Tentativo di aprire {self.port}@{self.baudrate}...") self.ser = serial.Serial( port=self.port, baudrate=self.baudrate, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, timeout=self.serial_timeout # Timeout per le letture ) print(f"[SerialManager] Porta {self.port} aperta.") self._thread_stop_event.clear() self._running = True self.receive_thread = threading.Thread(target=self._receiver_loop, daemon=True) self.receive_thread.start() return True except serial.SerialException as e: print(f"[SerialManager] Errore apertura porta {self.port}: {e}") self.ser = None self._running = False return False except Exception as e: print(f"[SerialManager] Errore generico start: {e}") self.ser = None self._running = False return False def stop(self): """Ferma il thread e chiude la porta.""" print("[SerialManager] Tentativo di stop...") self._running = False self._thread_stop_event.set() if self.receive_thread and self.receive_thread.is_alive(): print("[SerialManager] Attesa terminazione thread RX...") self.receive_thread.join(timeout=self.serial_timeout + 1) if self.receive_thread.is_alive(): print("[SerialManager] Attenzione: Thread RX non terminato.") self._close_serial_port() # Svuota la coda while not self.data_queue.empty(): try: self.data_queue.get_nowait() except queue.Empty: break print("[SerialManager] Fermato.") def send(self, data_bytes): """Invia bytes sulla porta seriale.""" if not self._running or not self.ser or not self.ser.is_open: print("[SerialManager] Errore invio: porta non attiva/aperta.") return False try: if not isinstance(data_bytes, bytes): print(f"[SerialManager] Errore invio: attesi bytes, ricevuto {type(data_bytes)}") return False self.ser.write(data_bytes) # print(f"[DEBUG Serial TX] Inviato bytes: {data_bytes}") # Debug return True except serial.SerialException as e: print(f"[SerialManager] Errore invio seriale: {e}") self._thread_stop_event.set() # Segnala errore grave self.data_queue.put(None) return False except Exception as e: print(f"[SerialManager] Errore generico invio: {e}") return False def get_received_data(self, block=False, timeout=None): """Recupera bytes ricevuti dalla seriale (messi in coda dal thread RX).""" try: # Restituisce bytes (o None se la coda è vuota/segnale di stop) data = self.data_queue.get(block=block, timeout=timeout) return data except queue.Empty: return None def is_running(self): return self._running and self.receive_thread and self.receive_thread.is_alive() def __enter__(self): if not self.start(): raise RuntimeError(f"Impossibile avviare SerialManager su {self.port}") return self def __exit__(self, exc_type, exc_val, exc_tb): self.stop() return False # Propaga eccezioni # --- Gestione Client TCP (Thread per leggere da TCP e inviare a Seriale) --- def client_handler_thread(client_socket, client_address, serial_manager, stop_event): """Gestisce la ricezione da un client TCP e l'invio a SerialManager.""" client_ip, client_port = client_address print(f"[TCP Handler {client_ip}:{client_port}] Thread avviato.") try: while not stop_event.is_set(): try: # Legge dati dal client TCP (bloccante con timeout basso) client_socket.settimeout(0.5) # Timeout per controllare stop_event data_bytes = client_socket.recv(1024) # Buffer di 1KB if not data_bytes: # Connessione chiusa dal client print(f"[TCP Handler {client_ip}:{client_port}] Connessione chiusa dal client.") break # Esce dal loop while # Invia i bytes ricevuti alla porta seriale # print(f"[DEBUG TCP->Serial] Ricevuto da TCP: {data_bytes}") # Debug if not serial_manager.send(data_bytes): # Errore invio seriale, probabilmente grave print(f"[TCP Handler {client_ip}:{client_port}] Fallito invio seriale. Chiusura connessione.") stop_event.set() # Segnala errore generale break except socket.timeout: # Timeout su recv(), normale, permette di controllare stop_event continue except socket.error as e: #print(f"[TCP Handler {client_ip}:{client_port}] Errore socket: {e}") break # Esce dal loop in caso di errore socket except Exception as e: print(f"[TCP Handler {client_ip}:{client_port}] Errore inaspettato: {e}") break finally: print(f"[TCP Handler {client_ip}:{client_port}] Chiusura thread e socket client.") try: client_socket.shutdown(socket.SHUT_RDWR) except OSError: pass # Ignora se già chiuso except Exception as e: print(f"Errore shutdown socket: {e}") try: client_socket.close() except Exception as e: print(f"Errore close socket: {e}") # Non impostiamo stop_event qui, solo se c'è un errore grave o 'quit' # --- Main Application --- if __name__ == "__main__": parser = argparse.ArgumentParser(description="Bridge TCP-Seriale") parser.add_argument('serial_port', help='Porta seriale (es. COM3, /dev/ttyUSB0)') parser.add_argument('-b', '--baudrate', type=int, default=9600, help='Baud rate (default: 9600)') parser.add_argument('-l', '--listen', default='0.0.0.0', help='Indirizzo IP su cui ascoltare (default: 0.0.0.0)') parser.add_argument('-p', '--port', type=int, default=5000, help='Porta TCP su cui ascoltare (default: 5000)') parser.add_argument('--encoding', default='utf-8', help='Encoding per i dati testuali (default: utf-8)') parser.add_argument('--serial-timeout', type=float, default=0.1, help='Timeout lettura seriale (sec, default: 0.1)') args = parser.parse_args() print("--- Avvio Bridge TCP-Seriale ---") print(f" Seriale: {args.serial_port} @ {args.baudrate} baud") print(f" TCP Server: {args.listen}:{args.port}") print(f" Encoding: {args.encoding}") main_stop_event = threading.Event() current_client_socket = None current_client_address = None client_thread = None server_socket = None # Socket del server TCP try: # Istanzia e avvia il SerialManager with SerialManager(port=args.serial_port, baudrate=args.baudrate, timeout=args.serial_timeout, encoding=args.encoding) as sm: # --- Setup Server TCP --- try: server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Permette il riutilizzo dell'indirizzo subito dopo la chiusura server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server_socket.bind((args.listen, args.port)) server_socket.listen(1) # Accetta max 1 connessione in coda server_socket.settimeout(0.5) # Timeout su accept per controllare stop_event print(f"[TCP Server] In ascolto su {args.listen}:{args.port}") except Exception as e: print(f"[TCP Server] Errore avvio server: {e}") raise # Rilancia l'eccezione per fermare tutto # --- Loop Principale --- while not main_stop_event.is_set(): # 1. Controlla nuove connessioni TCP new_client_socket = None if server_socket: try: # Accetta nuove connessioni (non bloccante grazie al timeout) new_client_socket, new_client_address = server_socket.accept() print(f"[TCP Server] Connessione accettata da: {new_client_address}") # Se c'era già un client, chiudi la vecchia connessione if current_client_socket: print("[TCP Server] Chiusura connessione client precedente.") if client_thread and client_thread.is_alive(): # Non c'è un modo pulito per fermare il thread dall'esterno # se è bloccato su recv(), ma possiamo chiudere il socket try: current_client_socket.shutdown(socket.SHUT_RDWR) except OSError: pass current_client_socket.close() # Dare un attimo al vecchio thread per terminare (opzionale) # if client_thread: client_thread.join(0.1) # Imposta il nuovo client come corrente current_client_socket = new_client_socket current_client_address = new_client_address # Avvia il thread handler per il nuovo client client_thread = threading.Thread( target=client_handler_thread, args=(current_client_socket, current_client_address, sm, main_stop_event), daemon=True ) client_thread.start() except socket.timeout: # Nessuna nuova connessione, normale pass except Exception as e: print(f"[TCP Server] Errore accept(): {e}") main_stop_event.set() # Errore grave, ferma tutto break # 2. Controlla dati dalla Seriale (coda RX) e invia a TCP se connesso serial_data_bytes = sm.get_received_data(block=False) if serial_data_bytes is not None: # Abbiamo ricevuto bytes dalla seriale # print(f"[DEBUG Serial->TCP] Dati da seriale: {serial_data_bytes}") # Debug if current_client_socket: # Se un client è connesso, invia i dati try: # Invia i bytes così come sono arrivati dalla seriale current_client_socket.sendall(serial_data_bytes) # print(f"[DEBUG Serial->TCP] Inviato a TCP: {serial_data_bytes}") # Debug except socket.error as e: print(f"[Main Loop] Errore invio a TCP client {current_client_address}: {e}") # Errore invio, il client probabilmente si è disconnesso try: current_client_socket.close() except Exception: pass # Ignora errori sulla chiusura current_client_socket = None current_client_address = None # Non è necessario fermare il client_thread qui, # dovrebbe terminare da solo a causa dell'errore socket # else: # print("[DEBUG Serial->TCP] Nessun client connesso, dati scartati.") # Debug elif serial_data_bytes is None and not sm.is_running(): # Il thread seriale ha messo None E non è più in esecuzione if not main_stop_event.is_set(): print("[Main Loop] Thread ricezione seriale terminato. Chiusura...") main_stop_event.set() # Ferma tutto break # Esce dal loop principale # 3. Controlla se il thread client è terminato (disconnessione) if client_thread and not client_thread.is_alive() and current_client_socket: print(f"[Main Loop] Rilevata terminazione thread client per {current_client_address}.") try: current_client_socket.close() # Assicura chiusura socket except Exception: pass current_client_socket = None current_client_address = None client_thread = None # Resetta il riferimento al thread # 4. Pausa per non usare 100% CPU time.sleep(0.05) # Uscita dal loop while print("[Main Loop] Uscita richiesta...") except RuntimeError as e: print(f"Errore critico avvio SerialManager: {e}") except serial.SerialException as e: print(f"Errore critico seriale: {e}") except KeyboardInterrupt: print("\n[Main Loop] Interruzione da tastiera (Ctrl+C). Chiusura...") except Exception as e: print(f"\n[Main Loop] Errore inaspettato: {e}") import traceback traceback.print_exc() finally: # --- Pulizia Finale --- print("[Main Loop] Avvio pulizia finale...") main_stop_event.set() # Assicura che tutti i loop si fermino # Chiusura socket client (se ancora attivo) if current_client_socket: print(f"[Main Loop] Chiusura socket client residuo {current_client_address}...") try: current_client_socket.shutdown(socket.SHUT_RDWR) except OSError: pass except Exception as e: print(f"Errore shutdown socket client: {e}") try: current_client_socket.close() except Exception as e: print(f"Errore close socket client: {e}") # Attesa terminazione thread client (breve) if client_thread and client_thread.is_alive(): # print("[Main Loop] Attesa (breve) terminazione thread client...") client_thread.join(timeout=0.5) # Chiusura socket server if server_socket: print("[Main Loop] Chiusura socket server TCP...") try: server_socket.close() except Exception as e: print(f"Errore close socket server: {e}") # Lo stop di SerialManager è gestito da 'with' statement print("Programma terminato.")