| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522 |
- """
- Satellite Management Command Classes
- This module implements commands for satellite (client) management in the
- Trixy system. These commands handle:
- - Satellite registration and authentication
- - Connection establishment and teardown
- - Status monitoring and health checks
- - Room and alias management
- These commands implement the satellite workflow specified in CLAUDE.md:
- 1. Registration process (with 60-second timeout)
- 2. Connection process with MAC validation
- 3. Status updates and disconnection handling
- """
- import time
- from typing import Optional, Dict, Any, List
- from dataclasses import dataclass, field
- from .base import TrixyCommand, CommandResponse, ValidationError, CommandStatus
- def pprint(message: str) -> None:
- """Satellite command logging function."""
- print(f"[CMD.SATELLITE] {message}")
- @dataclass
- class SatelliteHelloCommand(TrixyCommand):
- """
- Initial hello command from satellite to server.
-
- This is the first command sent by a satellite when attempting to connect.
- It provides basic identification information for the satellite.
- """
-
- room_id: str = field(default="")
- alias: str = field(default="")
- mac_address: str = field(default="")
- version: str = field(default="1.0.0")
- capabilities: List[str] = field(default_factory=list)
-
- def validate(self) -> bool:
- """Validate satellite hello command."""
- if not self.room_id:
- raise ValidationError("SatelliteHelloCommand must have a room_id")
- if not self.alias:
- raise ValidationError("SatelliteHelloCommand must have an alias")
- if not self.mac_address:
- raise ValidationError("SatelliteHelloCommand must have a mac_address")
-
- # Validate MAC address format (basic check)
- if len(self.mac_address.split(':')) != 6:
- raise ValidationError("MAC address must be in format aa:bb:cc:dd:ee:ff")
-
- return True
-
- def get_command_name(self) -> str:
- """Get command name."""
- return "SatelliteHelloCommand"
-
- def get_response_class(self):
- """Expect a hello response."""
- return SatelliteHelloResponse
-
- def get_timeout(self) -> float:
- """Timeout for hello command."""
- return 15.0
-
- def get_priority(self) -> int:
- """High priority for connection establishment."""
- return 10
- @dataclass
- class SatelliteRegistrationCommand(TrixyCommand):
- """
- Command to register a new satellite with the server.
-
- This command is used during the registration process when a satellite
- is not yet known to the server. The server must be in "registration mode"
- to accept this command.
- """
-
- room_id: str = field(default="")
- alias: str = field(default="")
- mac_address: str = field(default="")
- device_info: Dict[str, Any] = field(default_factory=dict)
- audio_capabilities: Dict[str, Any] = field(default_factory=dict)
-
- def validate(self) -> bool:
- """Validate satellite registration command."""
- if not self.room_id:
- raise ValidationError("SatelliteRegistrationCommand must have a room_id")
- if not self.alias:
- raise ValidationError("SatelliteRegistrationCommand must have an alias")
- if not self.mac_address:
- raise ValidationError("SatelliteRegistrationCommand must have a mac_address")
-
- # Validate MAC address format
- if len(self.mac_address.split(':')) != 6:
- raise ValidationError("MAC address must be in format aa:bb:cc:dd:ee:ff")
-
- return True
-
- def get_command_name(self) -> str:
- """Get command name."""
- return "SatelliteRegistrationCommand"
-
- def get_response_class(self):
- """Expect a registration response."""
- return SatelliteRegistrationResponse
-
- def get_timeout(self) -> float:
- """Timeout for registration (must complete within registration window)."""
- return 60.0
-
- def get_priority(self) -> int:
- """Very high priority for registration."""
- return 5
- @dataclass
- class SatelliteConnectionCommand(TrixyCommand):
- """
- Command to establish connection with a registered satellite.
-
- This command is sent after successful registration to establish
- the working connection and set up audio streams.
- """
-
- satellite_id: str = field(default="")
- mac_address: str = field(default="")
- requested_audio_ports: Dict[str, int] = field(default_factory=dict)
- session_id: Optional[str] = field(default=None)
-
- def validate(self) -> bool:
- """Validate satellite connection command."""
- if not self.satellite_id:
- raise ValidationError("SatelliteConnectionCommand must have a satellite_id")
- if not self.mac_address:
- raise ValidationError("SatelliteConnectionCommand must have a mac_address")
-
- # Validate MAC address format
- if len(self.mac_address.split(':')) != 6:
- raise ValidationError("MAC address must be in format aa:bb:cc:dd:ee:ff")
-
- return True
-
- def get_command_name(self) -> str:
- """Get command name."""
- return "SatelliteConnectionCommand"
-
- def get_response_class(self):
- """Expect a connection response."""
- return SatelliteConnectionResponse
-
- def get_timeout(self) -> float:
- """Timeout for connection establishment."""
- return 30.0
-
- def get_priority(self) -> int:
- """High priority for connection."""
- return 15
- @dataclass
- class SatelliteDisconnectCommand(TrixyCommand):
- """
- Command to gracefully disconnect a satellite.
-
- This command can be sent by either the satellite (to disconnect itself)
- or the server (to disconnect a satellite).
- """
-
- satellite_id: str = field(default="")
- reason: str = field(default="normal_disconnect")
- graceful: bool = field(default=True)
-
- def validate(self) -> bool:
- """Validate satellite disconnect command."""
- if not self.satellite_id:
- raise ValidationError("SatelliteDisconnectCommand must have a satellite_id")
- return True
-
- def get_command_name(self) -> str:
- """Get command name."""
- return "SatelliteDisconnectCommand"
-
- def requires_response(self) -> bool:
- """Disconnect may not always get a response."""
- return self.graceful
-
- def get_timeout(self) -> float:
- """Short timeout for disconnect."""
- return 10.0
-
- def get_priority(self) -> int:
- """High priority for disconnect."""
- return 20
- @dataclass
- class SatelliteStatusCommand(TrixyCommand):
- """
- Command to request or provide satellite status information.
-
- This can be used for health monitoring and status reporting.
- """
-
- satellite_id: str = field(default="")
- status_type: str = field(default="general") # general, audio, network, health
- status_data: Optional[Dict[str, Any]] = field(default=None)
-
- def validate(self) -> bool:
- """Validate satellite status command."""
- valid_types = ["general", "audio", "network", "health", "capabilities"]
- if self.status_type not in valid_types:
- raise ValidationError(f"status_type must be one of: {valid_types}")
- return True
-
- def get_command_name(self) -> str:
- """Get command name."""
- return "SatelliteStatusCommand"
-
- def get_timeout(self) -> float:
- """Timeout for status request."""
- return 15.0
-
- def get_priority(self) -> int:
- """Normal priority for status."""
- return 50
- # Response Classes
- @dataclass
- class SatelliteHelloResponse(CommandResponse):
- """Response to satellite hello command."""
-
- accepted: bool = field(default=False)
- server_version: str = field(default="1.0.0")
- assigned_satellite_id: Optional[str] = field(default=None)
- registration_required: bool = field(default=False)
- next_steps: List[str] = field(default_factory=list)
-
- def validate(self) -> bool:
- """Validate hello response."""
- return True
-
- def get_response_name(self) -> str:
- """Get response name."""
- return "SatelliteHelloResponse"
- @dataclass
- class SatelliteRegistrationResponse(CommandResponse):
- """Response to satellite registration command."""
-
- registration_accepted: bool = field(default=False)
- satellite_id: Optional[str] = field(default=None)
- registration_file_created: bool = field(default=False)
- next_steps: List[str] = field(default_factory=list)
- rejection_reason: Optional[str] = field(default=None)
-
- def validate(self) -> bool:
- """Validate registration response."""
- if not self.registration_accepted and not self.rejection_reason:
- raise ValidationError("Registration rejection must include reason")
- return True
-
- def get_response_name(self) -> str:
- """Get response name."""
- return "SatelliteRegistrationResponse"
- @dataclass
- class SatelliteConnectionResponse(CommandResponse):
- """Response to satellite connection command."""
-
- connection_accepted: bool = field(default=False)
- assigned_audio_ports: Dict[str, int] = field(default_factory=dict)
- session_id: Optional[str] = field(default=None)
- server_capabilities: List[str] = field(default_factory=list)
- rejection_reason: Optional[str] = field(default=None)
-
- def validate(self) -> bool:
- """Validate connection response."""
- if not self.connection_accepted and not self.rejection_reason:
- raise ValidationError("Connection rejection must include reason")
- return True
-
- def get_response_name(self) -> str:
- """Get response name."""
- return "SatelliteConnectionResponse"
- # Utility functions for satellite commands
- def create_satellite_hello(
- room_id: str,
- alias: str,
- mac_address: str,
- version: str = "1.0.0",
- capabilities: Optional[List[str]] = None
- ) -> SatelliteHelloCommand:
- """
- Create a satellite hello command.
-
- Args:
- room_id: Room identifier
- alias: Satellite alias/name
- mac_address: MAC address
- version: Client version
- capabilities: List of capabilities
-
- Returns:
- SatelliteHelloCommand: Hello command
- """
- return SatelliteHelloCommand(
- room_id=room_id,
- alias=alias,
- mac_address=mac_address,
- version=version,
- capabilities=capabilities or []
- )
- def create_satellite_registration(
- room_id: str,
- alias: str,
- mac_address: str,
- device_info: Optional[Dict[str, Any]] = None,
- audio_capabilities: Optional[Dict[str, Any]] = None
- ) -> SatelliteRegistrationCommand:
- """
- Create a satellite registration command.
-
- Args:
- room_id: Room identifier
- alias: Satellite alias/name
- mac_address: MAC address
- device_info: Device information
- audio_capabilities: Audio capabilities
-
- Returns:
- SatelliteRegistrationCommand: Registration command
- """
- return SatelliteRegistrationCommand(
- room_id=room_id,
- alias=alias,
- mac_address=mac_address,
- device_info=device_info or {},
- audio_capabilities=audio_capabilities or {}
- )
- def create_satellite_connection(
- satellite_id: str,
- mac_address: str,
- requested_audio_ports: Optional[Dict[str, int]] = None,
- session_id: Optional[str] = None
- ) -> SatelliteConnectionCommand:
- """
- Create a satellite connection command.
-
- Args:
- satellite_id: Satellite identifier
- mac_address: MAC address
- requested_audio_ports: Requested audio port mapping
- session_id: Optional session ID
-
- Returns:
- SatelliteConnectionCommand: Connection command
- """
- return SatelliteConnectionCommand(
- satellite_id=satellite_id,
- mac_address=mac_address,
- requested_audio_ports=requested_audio_ports or {},
- session_id=session_id
- )
- def create_satellite_disconnect(
- satellite_id: str,
- reason: str = "normal_disconnect",
- graceful: bool = True
- ) -> SatelliteDisconnectCommand:
- """
- Create a satellite disconnect command.
-
- Args:
- satellite_id: Satellite identifier
- reason: Disconnect reason
- graceful: Whether disconnect is graceful
-
- Returns:
- SatelliteDisconnectCommand: Disconnect command
- """
- return SatelliteDisconnectCommand(
- satellite_id=satellite_id,
- reason=reason,
- graceful=graceful
- )
- def create_hello_response(
- command_id: str,
- accepted: bool,
- server_version: str = "1.0.0",
- assigned_satellite_id: Optional[str] = None,
- registration_required: bool = False,
- next_steps: Optional[List[str]] = None
- ) -> SatelliteHelloResponse:
- """
- Create a satellite hello response.
-
- Args:
- command_id: Original command ID
- accepted: Whether hello is accepted
- server_version: Server version
- assigned_satellite_id: Assigned satellite ID
- registration_required: Whether registration is required
- next_steps: List of next steps
-
- Returns:
- SatelliteHelloResponse: Hello response
- """
- status = CommandStatus.SUCCESS if accepted else CommandStatus.ERROR
-
- return SatelliteHelloResponse(
- command_id=command_id,
- status=status,
- accepted=accepted,
- server_version=server_version,
- assigned_satellite_id=assigned_satellite_id,
- registration_required=registration_required,
- next_steps=next_steps or []
- )
- def create_registration_response(
- command_id: str,
- registration_accepted: bool,
- satellite_id: Optional[str] = None,
- rejection_reason: Optional[str] = None
- ) -> SatelliteRegistrationResponse:
- """
- Create a satellite registration response.
-
- Args:
- command_id: Original command ID
- registration_accepted: Whether registration is accepted
- satellite_id: Assigned satellite ID
- rejection_reason: Reason for rejection if not accepted
-
- Returns:
- SatelliteRegistrationResponse: Registration response
- """
- status = CommandStatus.SUCCESS if registration_accepted else CommandStatus.ERROR
-
- return SatelliteRegistrationResponse(
- command_id=command_id,
- status=status,
- registration_accepted=registration_accepted,
- satellite_id=satellite_id,
- registration_file_created=registration_accepted,
- rejection_reason=rejection_reason
- )
- def create_connection_response(
- command_id: str,
- connection_accepted: bool,
- assigned_audio_ports: Optional[Dict[str, int]] = None,
- session_id: Optional[str] = None,
- rejection_reason: Optional[str] = None
- ) -> SatelliteConnectionResponse:
- """
- Create a satellite connection response.
-
- Args:
- command_id: Original command ID
- connection_accepted: Whether connection is accepted
- assigned_audio_ports: Assigned audio port mapping
- session_id: Session ID
- rejection_reason: Reason for rejection if not accepted
-
- Returns:
- SatelliteConnectionResponse: Connection response
- """
- status = CommandStatus.SUCCESS if connection_accepted else CommandStatus.ERROR
-
- return SatelliteConnectionResponse(
- command_id=command_id,
- status=status,
- connection_accepted=connection_accepted,
- assigned_audio_ports=assigned_audio_ports or {},
- session_id=session_id,
- rejection_reason=rejection_reason
- )
- # Module exports
- __all__ = [
- "SatelliteHelloCommand",
- "SatelliteRegistrationCommand",
- "SatelliteConnectionCommand",
- "SatelliteDisconnectCommand",
- "SatelliteStatusCommand",
- "SatelliteHelloResponse",
- "SatelliteRegistrationResponse",
- "SatelliteConnectionResponse",
- "create_satellite_hello",
- "create_satellite_registration",
- "create_satellite_connection",
- "create_satellite_disconnect",
- "create_hello_response",
- "create_registration_response",
- "create_connection_response",
- ]
|