network_manager.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717
  1. """
  2. Network Manager for Trixy System
  3. This module provides the main NetworkManager class that handles:
  4. - Command socket management (Port 2101)
  5. - Audio streaming sockets (Ports 2102, 2103, 2104)
  6. - Multi-client support with thread safety
  7. - Connection management and monitoring
  8. - Protocol serialization/deserialization
  9. - Integration with event system and application container
  10. The NetworkManager serves as the central hub for all network communication
  11. in the Trixy system, supporting both server and client modes.
  12. """
  13. import socket
  14. import threading
  15. import time
  16. import queue
  17. import weakref
  18. from typing import Dict, Any, Optional, List, Callable, Union, Tuple
  19. from dataclasses import dataclass, field
  20. from enum import Enum, IntEnum
  21. from concurrent.futures import ThreadPoolExecutor
  22. import select
  23. from .protocol import TrixyProtocol, TrixyMessage, MessageFlags, ProtocolError
  24. from .cmd import TrixyCommand, CommandResponse
  25. def pprint(message: str) -> None:
  26. """Network manager logging function."""
  27. print(f"[NETWORK_MANAGER] {message}")
  28. class ConnectionState(Enum):
  29. """Connection state enumeration."""
  30. DISCONNECTED = "disconnected"
  31. CONNECTING = "connecting"
  32. CONNECTED = "connected"
  33. AUTHENTICATING = "authenticating"
  34. AUTHENTICATED = "authenticated"
  35. DISCONNECTING = "disconnecting"
  36. ERROR = "error"
  37. class AudioStreamType(Enum):
  38. """Audio stream type enumeration."""
  39. INPUT = "input" # Port 2102 - Raw audio from satellites
  40. OUTPUT = "output" # Port 2103 - TTS/response audio
  41. MUSIC = "music" # Port 2104 - Music/media playback
  42. @dataclass
  43. class AudioStreamConfig:
  44. """Configuration for audio streams."""
  45. sample_rate: int = 16000
  46. channels: int = 1
  47. sample_width: int = 2 # bytes
  48. format: str = "PCM"
  49. buffer_size: int = 4096
  50. @dataclass
  51. class ConnectionInfo:
  52. """Information about a network connection."""
  53. connection_id: str
  54. remote_address: Tuple[str, int]
  55. local_address: Tuple[str, int]
  56. state: ConnectionState
  57. connected_time: float
  58. last_activity: float
  59. bytes_sent: int = 0
  60. bytes_received: int = 0
  61. commands_sent: int = 0
  62. commands_received: int = 0
  63. satellite_id: Optional[str] = None
  64. session_id: Optional[str] = None
  65. class NetworkManagerError(Exception):
  66. """Base exception for network manager errors."""
  67. pass
  68. class ConnectionManager:
  69. """Manages individual connections and their state."""
  70. def __init__(self):
  71. self._connections: Dict[str, ConnectionInfo] = {}
  72. self._sockets: Dict[str, socket.socket] = {}
  73. self._lock = threading.RLock()
  74. def add_connection(self, connection_id: str, sock: socket.socket, remote_addr: Tuple[str, int]) -> ConnectionInfo:
  75. """Add a new connection."""
  76. with self._lock:
  77. local_addr = sock.getsockname()
  78. connection = ConnectionInfo(
  79. connection_id=connection_id,
  80. remote_address=remote_addr,
  81. local_address=local_addr,
  82. state=ConnectionState.CONNECTED,
  83. connected_time=time.time(),
  84. last_activity=time.time()
  85. )
  86. self._connections[connection_id] = connection
  87. self._sockets[connection_id] = sock
  88. pprint(f"Connection added: {connection_id} from {remote_addr}")
  89. return connection
  90. def remove_connection(self, connection_id: str) -> Optional[ConnectionInfo]:
  91. """Remove a connection."""
  92. with self._lock:
  93. connection = self._connections.pop(connection_id, None)
  94. sock = self._sockets.pop(connection_id, None)
  95. if sock:
  96. try:
  97. sock.close()
  98. except:
  99. pass
  100. if connection:
  101. pprint(f"Connection removed: {connection_id}")
  102. return connection
  103. def get_connection(self, connection_id: str) -> Optional[ConnectionInfo]:
  104. """Get connection info."""
  105. with self._lock:
  106. return self._connections.get(connection_id)
  107. def get_socket(self, connection_id: str) -> Optional[socket.socket]:
  108. """Get socket for connection."""
  109. with self._lock:
  110. return self._sockets.get(connection_id)
  111. def update_activity(self, connection_id: str, bytes_delta: int = 0, command_delta: int = 0):
  112. """Update connection activity."""
  113. with self._lock:
  114. connection = self._connections.get(connection_id)
  115. if connection:
  116. connection.last_activity = time.time()
  117. if bytes_delta > 0:
  118. connection.bytes_received += bytes_delta
  119. if command_delta > 0:
  120. connection.commands_received += command_delta
  121. def get_all_connections(self) -> Dict[str, ConnectionInfo]:
  122. """Get all connections."""
  123. with self._lock:
  124. return self._connections.copy()
  125. class AudioStreamManager:
  126. """Manages audio streaming sockets."""
  127. def __init__(self):
  128. self._streams: Dict[AudioStreamType, Dict[str, Any]] = {}
  129. self._configs: Dict[AudioStreamType, AudioStreamConfig] = {}
  130. self._lock = threading.RLock()
  131. # Initialize default configs
  132. for stream_type in AudioStreamType:
  133. if stream_type == AudioStreamType.MUSIC:
  134. self._configs[stream_type] = AudioStreamConfig(
  135. sample_rate=48000,
  136. channels=2,
  137. sample_width=2
  138. )
  139. else:
  140. self._configs[stream_type] = AudioStreamConfig()
  141. def start_stream(self, stream_type: AudioStreamType, port: int, config: Optional[AudioStreamConfig] = None):
  142. """Start an audio stream."""
  143. with self._lock:
  144. if stream_type in self._streams:
  145. raise NetworkManagerError(f"Audio stream {stream_type.value} already active")
  146. if config:
  147. self._configs[stream_type] = config
  148. # Create server socket for audio stream
  149. server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  150. server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  151. server_sock.bind(('', port))
  152. server_sock.listen(10)
  153. self._streams[stream_type] = {
  154. 'server_socket': server_sock,
  155. 'port': port,
  156. 'connections': {},
  157. 'active': True
  158. }
  159. pprint(f"Audio stream {stream_type.value} started on port {port}")
  160. def stop_stream(self, stream_type: AudioStreamType):
  161. """Stop an audio stream."""
  162. with self._lock:
  163. stream_info = self._streams.pop(stream_type, None)
  164. if stream_info:
  165. stream_info['active'] = False
  166. server_sock = stream_info.get('server_socket')
  167. if server_sock:
  168. server_sock.close()
  169. # Close all connections
  170. for conn_sock in stream_info.get('connections', {}).values():
  171. try:
  172. conn_sock.close()
  173. except:
  174. pass
  175. pprint(f"Audio stream {stream_type.value} stopped")
  176. def get_stream_info(self, stream_type: AudioStreamType) -> Optional[Dict[str, Any]]:
  177. """Get stream information."""
  178. with self._lock:
  179. return self._streams.get(stream_type)
  180. class NetworkManager:
  181. """
  182. Main network manager for the Trixy system.
  183. Handles all network communication including command sockets and audio streams.
  184. Integrates with the event system and application container.
  185. """
  186. def __init__(self, application_container):
  187. """
  188. Initialize the network manager.
  189. Args:
  190. application_container: The main application container
  191. """
  192. self.application = application_container
  193. self.application_ref = weakref.ref(application_container)
  194. # Core components
  195. self.protocol = TrixyProtocol()
  196. self.connection_manager = ConnectionManager()
  197. self.audio_stream_manager = AudioStreamManager()
  198. # Server state
  199. self._server_running = False
  200. self._command_server_socket: Optional[socket.socket] = None
  201. self._command_port = 2101
  202. # Client state
  203. self._client_connected = False
  204. self._client_socket: Optional[socket.socket] = None
  205. self._server_address: Optional[Tuple[str, int]] = None
  206. # Threading
  207. self._thread_pool = ThreadPoolExecutor(max_workers=20)
  208. self._running = True
  209. self._lock = threading.RLock()
  210. # Message handling
  211. self._message_queue = queue.Queue()
  212. self._message_handlers: Dict[str, Callable] = {}
  213. self._response_handlers: Dict[str, Callable] = {}
  214. # Event system integration
  215. self._event_handler = None
  216. pprint("NetworkManager initialized")
  217. def start_server(self, command_port: int = 2101, audio_ports: Optional[Dict[str, int]] = None) -> None:
  218. """
  219. Start the network manager in server mode.
  220. Args:
  221. command_port: Port for command socket
  222. audio_ports: Ports for audio streams
  223. """
  224. if self._server_running:
  225. raise NetworkManagerError("Server already running")
  226. pprint(f"Starting network server on port {command_port}")
  227. self._command_port = command_port
  228. try:
  229. # Create command server socket
  230. self._command_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  231. self._command_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  232. self._command_server_socket.bind(('', command_port))
  233. self._command_server_socket.listen(50)
  234. # Start audio streams
  235. if audio_ports is None:
  236. audio_ports = {
  237. 'input': 2102,
  238. 'output': 2103,
  239. 'music': 2104
  240. }
  241. for stream_name, port in audio_ports.items():
  242. if stream_name in ['input', 'output', 'music']:
  243. stream_type = AudioStreamType(stream_name)
  244. self.audio_stream_manager.start_stream(stream_type, port)
  245. self._server_running = True
  246. # Start accepting connections
  247. self._thread_pool.submit(self._accept_connections)
  248. self._thread_pool.submit(self._process_messages)
  249. # Trigger server started event
  250. self._trigger_event("network_server_started", {
  251. 'command_port': command_port,
  252. 'audio_ports': audio_ports
  253. })
  254. pprint(f"Network server started successfully")
  255. except Exception as e:
  256. self._cleanup_server()
  257. raise NetworkManagerError(f"Failed to start server: {e}") from e
  258. def stop_server(self) -> None:
  259. """Stop the network server."""
  260. if not self._server_running:
  261. return
  262. pprint("Stopping network server...")
  263. self._server_running = False
  264. self._cleanup_server()
  265. self._trigger_event("network_server_stopped", {})
  266. pprint("Network server stopped")
  267. def connect_to_server(self, host: str, port: int = 2101) -> bool:
  268. """
  269. Connect to a Trixy server as a client.
  270. Args:
  271. host: Server hostname or IP
  272. port: Server command port
  273. Returns:
  274. bool: True if connected successfully
  275. """
  276. if self._client_connected:
  277. raise NetworkManagerError("Client already connected")
  278. pprint(f"Connecting to server {host}:{port}")
  279. try:
  280. self._client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  281. self._client_socket.settimeout(10.0)
  282. self._client_socket.connect((host, port))
  283. self._server_address = (host, port)
  284. self._client_connected = True
  285. # Start client message processing
  286. connection_id = f"client_{int(time.time())}"
  287. self.connection_manager.add_connection(
  288. connection_id,
  289. self._client_socket,
  290. self._server_address
  291. )
  292. self._thread_pool.submit(self._handle_client_connection, connection_id)
  293. self._thread_pool.submit(self._process_messages)
  294. self._trigger_event("network_client_connected", {
  295. 'server_address': self._server_address
  296. })
  297. pprint(f"Connected to server successfully")
  298. return True
  299. except Exception as e:
  300. self._cleanup_client()
  301. pprint(f"Failed to connect to server: {e}")
  302. return False
  303. def disconnect_from_server(self) -> None:
  304. """Disconnect from server."""
  305. if not self._client_connected:
  306. return
  307. pprint("Disconnecting from server...")
  308. self._client_connected = False
  309. self._cleanup_client()
  310. self._trigger_event("network_client_disconnected", {})
  311. pprint("Disconnected from server")
  312. def send_command(self, command: TrixyCommand, connection_id: Optional[str] = None) -> bool:
  313. """
  314. Send a command over the network.
  315. Args:
  316. command: Command to send
  317. connection_id: Target connection (None for client mode)
  318. Returns:
  319. bool: True if sent successfully
  320. """
  321. try:
  322. # Create protocol message
  323. message = self.protocol.create_message(
  324. command.get_command_name(),
  325. command
  326. )
  327. # Serialize message
  328. data = message.serialize()
  329. # Send based on mode
  330. if self._client_connected:
  331. # Client mode - send to server
  332. sock = self._client_socket
  333. if sock:
  334. sock.send(data)
  335. pprint(f"Sent command: {command.get_command_name()}")
  336. return True
  337. else:
  338. # Server mode - send to specific connection
  339. if connection_id:
  340. sock = self.connection_manager.get_socket(connection_id)
  341. if sock:
  342. sock.send(data)
  343. pprint(f"Sent command to {connection_id}: {command.get_command_name()}")
  344. return True
  345. return False
  346. except Exception as e:
  347. pprint(f"Failed to send command: {e}")
  348. return False
  349. def broadcast_command(self, command: TrixyCommand) -> int:
  350. """
  351. Broadcast a command to all connected clients.
  352. Args:
  353. command: Command to broadcast
  354. Returns:
  355. int: Number of clients that received the command
  356. """
  357. if not self._server_running:
  358. return 0
  359. connections = self.connection_manager.get_all_connections()
  360. sent_count = 0
  361. for connection_id in connections:
  362. if self.send_command(command, connection_id):
  363. sent_count += 1
  364. return sent_count
  365. def register_message_handler(self, command_name: str, handler: Callable[[TrixyMessage, str], None]) -> None:
  366. """
  367. Register a handler for incoming messages.
  368. Args:
  369. command_name: Name of command to handle
  370. handler: Handler function (message, connection_id) -> None
  371. """
  372. self._message_handlers[command_name] = handler
  373. pprint(f"Registered message handler for: {command_name}")
  374. def _accept_connections(self) -> None:
  375. """Accept incoming connections (server thread)."""
  376. pprint("Started accepting connections")
  377. while self._server_running and self._command_server_socket:
  378. try:
  379. # Use select to check for new connections with timeout
  380. ready, _, _ = select.select([self._command_server_socket], [], [], 1.0)
  381. if ready:
  382. client_sock, addr = self._command_server_socket.accept()
  383. connection_id = f"client_{addr[0]}_{addr[1]}_{int(time.time())}"
  384. self.connection_manager.add_connection(connection_id, client_sock, addr)
  385. # Handle connection in separate thread
  386. self._thread_pool.submit(self._handle_client_connection, connection_id)
  387. self._trigger_event("network_client_connected", {
  388. 'connection_id': connection_id,
  389. 'remote_address': addr
  390. })
  391. except Exception as e:
  392. # Only log errors if server is still supposed to be running
  393. if self._server_running and self._running:
  394. pprint(f"Error accepting connection: {e}")
  395. def _handle_client_connection(self, connection_id: str) -> None:
  396. """Handle messages from a client connection."""
  397. sock = self.connection_manager.get_socket(connection_id)
  398. if not sock:
  399. return
  400. pprint(f"Handling connection: {connection_id}")
  401. try:
  402. while self._running and sock:
  403. # Receive data with timeout
  404. sock.settimeout(1.0)
  405. try:
  406. data = sock.recv(4096)
  407. if not data:
  408. break
  409. # Update connection activity
  410. self.connection_manager.update_activity(connection_id, len(data))
  411. # Process the received data
  412. self._process_received_data(data, connection_id)
  413. except socket.timeout:
  414. continue
  415. except Exception as e:
  416. pprint(f"Error receiving data from {connection_id}: {e}")
  417. break
  418. except Exception as e:
  419. pprint(f"Error handling connection {connection_id}: {e}")
  420. finally:
  421. self.connection_manager.remove_connection(connection_id)
  422. self._trigger_event("network_client_disconnected", {
  423. 'connection_id': connection_id
  424. })
  425. def _process_received_data(self, data: bytes, connection_id: str) -> None:
  426. """Process received data and extract messages."""
  427. try:
  428. # Check if it's a hard-coded command
  429. if self.protocol.is_hardcoded_command(data):
  430. command, args = self.protocol.parse_hardcoded_command(data)
  431. pprint(f"Received hard-coded command from {connection_id}: {command}")
  432. # Handle hard-coded commands immediately
  433. self._handle_hardcoded_command(command, args, connection_id)
  434. else:
  435. # Parse as protocol message
  436. message = self.protocol.deserialize_message(data)
  437. # Queue for processing
  438. self._message_queue.put((message, connection_id))
  439. except Exception as e:
  440. pprint(f"Error processing received data: {e}")
  441. def _process_messages(self) -> None:
  442. """Process queued messages."""
  443. pprint("Started message processing")
  444. while self._running:
  445. try:
  446. # Get message with timeout
  447. message, connection_id = self._message_queue.get(timeout=1.0)
  448. # Handle the message
  449. self._handle_message(message, connection_id)
  450. self._message_queue.task_done()
  451. except queue.Empty:
  452. continue
  453. except Exception as e:
  454. pprint(f"Error processing message: {e}")
  455. def _handle_message(self, message: TrixyMessage, connection_id: str) -> None:
  456. """Handle a received message."""
  457. try:
  458. command_name = message.class_name
  459. # Check for registered handler
  460. handler = self._message_handlers.get(command_name)
  461. if handler:
  462. handler(message, connection_id)
  463. else:
  464. # Default handling - trigger event
  465. self._trigger_event("network_message_received", {
  466. 'message': message,
  467. 'connection_id': connection_id,
  468. 'command_name': command_name
  469. })
  470. pprint(f"Handled message: {command_name} from {connection_id}")
  471. except Exception as e:
  472. pprint(f"Error handling message: {e}")
  473. def _handle_hardcoded_command(self, command: str, args: str, connection_id: str) -> None:
  474. """Handle hard-coded commands."""
  475. try:
  476. if command == "TRXINOOP":
  477. # No-op/heartbeat - just update activity
  478. self.connection_manager.update_activity(connection_id)
  479. elif command == "TRXIPING":
  480. # Respond with pong
  481. pong_data = f"TRXIPONG {args}".encode('utf-8')
  482. sock = self.connection_manager.get_socket(connection_id)
  483. if sock:
  484. sock.send(pong_data)
  485. elif command == "TRXIPRNT":
  486. # Print command
  487. pprint(f"Print from {connection_id}: {args}")
  488. elif command == "TRXYHELO":
  489. # Hello command
  490. pprint(f"Hello from {connection_id}: {args}")
  491. except Exception as e:
  492. pprint(f"Error handling hard-coded command {command}: {e}")
  493. def _trigger_event(self, event_name: str, event_data: Dict[str, Any]) -> None:
  494. """Trigger an event through the event system."""
  495. try:
  496. # Don't trigger events if shutting down
  497. if not self._running:
  498. return
  499. if not self._event_handler:
  500. app = self.application_ref()
  501. if app:
  502. self._event_handler = app.get_event_handler()
  503. if self._event_handler:
  504. self._event_handler.trigger_event(event_name, event_data)
  505. except Exception as e:
  506. # Only log errors if still running (avoid shutdown-related errors)
  507. if self._running:
  508. pprint(f"Error triggering event {event_name}: {e}")
  509. def _cleanup_server(self) -> None:
  510. """Clean up server resources."""
  511. if self._command_server_socket:
  512. self._command_server_socket.close()
  513. self._command_server_socket = None
  514. # Stop all audio streams
  515. for stream_type in AudioStreamType:
  516. self.audio_stream_manager.stop_stream(stream_type)
  517. # Close all connections
  518. for connection_id in list(self.connection_manager.get_all_connections().keys()):
  519. self.connection_manager.remove_connection(connection_id)
  520. def _cleanup_client(self) -> None:
  521. """Clean up client resources."""
  522. if self._client_socket:
  523. self._client_socket.close()
  524. self._client_socket = None
  525. self._server_address = None
  526. def shutdown(self) -> None:
  527. """Shutdown the network manager."""
  528. pprint("Shutting down network manager...")
  529. self._running = False
  530. if self._server_running:
  531. self.stop_server()
  532. if self._client_connected:
  533. self.disconnect_from_server()
  534. self._thread_pool.shutdown(wait=True)
  535. pprint("Network manager shutdown complete")
  536. def get_status(self) -> Dict[str, Any]:
  537. """Get network manager status."""
  538. connections = self.connection_manager.get_all_connections()
  539. return {
  540. 'server_running': self._server_running,
  541. 'client_connected': self._client_connected,
  542. 'command_port': self._command_port,
  543. 'server_address': self._server_address,
  544. 'active_connections': len(connections),
  545. 'connection_details': {cid: {
  546. 'remote_address': conn.remote_address,
  547. 'state': conn.state.value,
  548. 'connected_time': conn.connected_time,
  549. 'bytes_sent': conn.bytes_sent,
  550. 'bytes_received': conn.bytes_received
  551. } for cid, conn in connections.items()}
  552. }
  553. # Module exports
  554. __all__ = [
  555. "NetworkManager",
  556. "NetworkManagerError",
  557. "ConnectionManager",
  558. "ConnectionState",
  559. "ConnectionInfo",
  560. "AudioStreamManager",
  561. "AudioStreamType",
  562. "AudioStreamConfig",
  563. ]