""" Communicator manager. Cosa fa: crea e gestisce i communicator (SFP, Serial, TFTP), connessioni e callback. Principali: CommunicatorManager Ingressi/Uscite: configurazione via set_config(); operazioni di connect/disconnect come side-effect. """ from typing import Optional, Dict, Any, Callable, List, Tuple from target_simulator.core.sfp_communicator import SFPCommunicator from target_simulator.core.serial_communicator import SerialCommunicator from target_simulator.core.tftp_communicator import TFTPCommunicator from target_simulator.core.communicator_interface import CommunicatorInterface class CommunicatorManager: """Encapsulates creation, initialization and basic lifecycle of communicators. code Code This is a small, testable façade that mirrors the behavior previously implemented directly inside MainView. It intentionally keeps a thin API so MainView can migrate to it without changing external behavior. """ def __init__(self, simulation_hub, logger=None, defer_sfp_connection: bool = True): self.simulation_hub = simulation_hub self.logger = logger self.defer_sfp_connection = defer_sfp_connection self.config: Dict[str, Any] = {} self.target_communicator: Optional[CommunicatorInterface] = None self.lru_communicator: Optional[CommunicatorInterface] = None # Callbacks that want to be notified about connection state changes self._connection_state_callbacks: List[Callable[[bool], None]] = [] def set_config(self, config: Dict[str, Any]): """Sets the configuration for the communicators.""" self.config = config or {} def initialize_communicators( self, ) -> Tuple[Optional[CommunicatorInterface], bool, Optional[CommunicatorInterface], bool]: """Create and (optionally) connect communicator instances. Returns: (target_comm, target_connected, lru_comm, lru_connected) """ # Disconnect any existing connections before re-initializing self._disconnect_communicator(self.target_communicator, "target") self._disconnect_communicator(self.lru_communicator, "LRU") target_cfg = (self.config or {}).get("target", {}) lru_cfg = (self.config or {}).get("lru", {}) # Initialize Target Communicator self.target_communicator, target_connected = self._setup_communicator( target_cfg, "Target" ) # Initialize LRU Communicator self.lru_communicator, lru_connected = self._setup_communicator(lru_cfg, "LRU") return ( self.target_communicator, bool(target_connected), self.lru_communicator, bool(lru_connected), ) def _setup_communicator(self, config: Dict[str, Any], name: str): """Helper to create and connect a single communicator.""" comm_type = (config or {}).get("type") if self.logger: self.logger.info(f"Initializing {name} communicator of type: {comm_type}") communicator = None config_data = None try: if comm_type == "serial": communicator = SerialCommunicator() config_data = config.get("serial", {}) elif comm_type == "tftp": communicator = TFTPCommunicator() config_data = config.get("tftp", {}) elif comm_type == "sfp": communicator = SFPCommunicator(simulation_hub=self.simulation_hub) if hasattr(communicator, "add_connection_state_callback"): communicator.add_connection_state_callback( self._notify_connection_state ) config_data = config.get("sfp", {}) if self.defer_sfp_connection: return communicator, False if communicator and config_data: if communicator.connect(config_data): return communicator, True except Exception: if self.logger: self.logger.exception( f"Failed to initialize or connect {name} communicator." ) if self.logger: self.logger.warning(f"Failed to initialize or connect {name} communicator.") return None, False def connect_target(self) -> bool: """Attempt to connect the target communicator using current config.""" try: if not self.target_communicator: self.initialize_communicators() cfg = (self.config or {}).get("target", {}) sfp_cfg = cfg.get("sfp") if cfg else None if cfg.get("type") == "sfp" and sfp_cfg and self.target_communicator: return bool(self.target_communicator.connect(sfp_cfg)) self.initialize_communicators() return bool( self.target_communicator and getattr(self.target_communicator, "is_open", False) ) except Exception: if self.logger: self.logger.exception("Unhandled exception in connect_target") return False def disconnect_target(self): """Disconnects only the target communicator.""" self._disconnect_communicator(self.target_communicator, "target") def _disconnect_communicator(self, comm: Optional[CommunicatorInterface], name: str): """Safely disconnects a single communicator instance if it's open.""" if not comm or not getattr(comm, "is_open", False): return try: comm.disconnect() except Exception: if self.logger: self.logger.exception(f"Error disconnecting {name} communicator") def shutdown(self): """Disconnects all managed communicators cleanly.""" self.logger.info("Shutting down all communicators.") self._disconnect_communicator(self.target_communicator, "target") self._disconnect_communicator(self.lru_communicator, "LRU") def add_connection_state_callback(self, cb: Callable[[bool], None]): """Registers a callback for connection state changes.""" if cb not in self._connection_state_callbacks: self._connection_state_callbacks.append(cb) def remove_connection_state_callback(self, cb: Callable[[bool], None]): """Unregisters a connection state callback.""" try: self._connection_state_callbacks.remove(cb) except ValueError: pass def _notify_connection_state(self, is_connected: bool): """Notifies all registered callbacks about a connection state change.""" for cb in list(self._connection_state_callbacks): try: cb(is_connected) except Exception: if self.logger: self.logger.exception("Connection state callback failed")