import asyncio import websockets import json import uuid # To generate a unique client ID from drb_cdb_api import DRBCDBAPI, ConfigGenerator from server_api import RadioAPIClient from enum import Enum # --- Client Configuration --- SERVER_WS_URI = "ws://localhost:8765" SERVER_API_URL = "http://localhost:5000" CLIENT_API_URL = "http://localhost:8001" # Generate or define a unique ID for THIS client instance # In a real app, this might come from config or a login process CLIENT_ID = f"client-{uuid.uuid4().hex[:8]}" # TODO - Implement persistent ID # ---------------------------- # Dictionary mapping command names (strings) to local client functions command_handlers = {} # Init DRB API handler drb_api = DRBCDBAPI(CLIENT_API_URL) srv_api = RadioAPIClient(SERVER_API_URL) # --- Define the client status object --- class StatusValues(Enum): ONLINE = "online" # The client is online LISTENING = "listening" # The client bot is online and listening to a system client_status = StatusValues.ONLINE # --- Define decorator creation function --- def command(func): """Decorator to register a function as a command handler.""" command_handlers[func.__name__] = func return func # --- Define Client-Side Command Handlers (The "API" functions) --- # Join server @command async def join_server(system_id, guild_id, channel_id): # Takes system ID, guild ID, channel ID bot_status = drb_api.get_bot_status() # Check if the bot is running if 'bot_running' not in bot_status or not bot_status['bot_running']: # Run the bot if not drb_api.start_bot() # Update status client_status = StatusValues.LISTENING op25_status = drb_api.get_op25_status() # Check if OP25 is stopped, if so set the selected channel, otherwise if op25_status == "stopped": chn_details = srv_api.get_channel_details(channel_id) if not chn_details: # TODO - handle not having channel details pass # Generate the config for the channel requested chn_config = ConfigGenerator( type=chn_details['decode_mode'], systemName=chn_details['name'], channels=chn_details['frequency_list_khz'], tags=chn_details['tags'], whitelist=chn_details['tag_whitelist']) # Set the OP25 config drb_api.generate_op25_config(chn_config) # Start OP25 drb_api.start_op25() # Leave server @command async def leave_server(guild_id): # Takes guild ID bot_status = drb_api.get_bot_status() # Check if the bot is running if 'bot_running' not in bot_status or not bot_status['bot_running']: # If not, do nothing return # Check if the bot is in the guild if not "connected_guilds" in bot_status or guild_id not in bot_status['connected_guilds']: return # Leave the server specified drb_api.leave_voice_channel(guild_id) # Update status client_status = StatusValues.ONLINE @command async def set_status(status_text): """Example command: Sets or displays a status.""" print(f"\n--- Server Command: set_status ---") print(f"Status updated to: {status_text}") print("----------------------------------") @command async def run_task(task_id, duration_seconds): """Example command: Simulates running a task.""" print(f"\n--- Server Command: run_task ---") print(f"Starting task {task_id} for {duration_seconds} seconds...") await asyncio.sleep(duration_seconds) print(f"Task {task_id} finished.") print("------------------------------") # Add more command handlers as needed... # ------------------------------------------------------------------ async def receive_commands(websocket): """Listens for and processes command messages from the server.""" async for message in websocket: try: data = json.loads(message) if data.get("type") == "command": command_name = data.get("name") args = data.get("args", []) if command_name in command_handlers: print(f"Executing command: {command_name} with args {args}") # Execute the registered async function await command_handlers[command_name](*args) else: print(f"Received unknown command: {command_name}") elif data.get("type") == "handshake_ack": print(f"Server acknowledged handshake.") else: print(f"Received unknown message type: {data.get('type')}") except json.JSONDecodeError: print(f"Received invalid JSON: {message}") except Exception as e: print(f"Error processing message: {e}") async def main_client(): """Connects to the server and handles communication.""" print(f"Client {CLIENT_ID} connecting to {SERVER_WS_URI}...") try: async with websockets.connect(SERVER_WS_URI) as websocket: print("Connection established.") # Handshake: Send client ID immediately after connecting handshake_message = json.dumps({"type": "handshake", "id": CLIENT_ID}) await websocket.send(handshake_message) print(f"Sent handshake with ID: {CLIENT_ID}") # Start receiving commands and keep the connection alive await receive_commands(websocket) except ConnectionRefusedError: print(f"Connection refused. Is the server running at {SERVER_WS_URI}?") except websockets.exceptions.ConnectionClosedOK: print("Connection closed gracefully by server.") except websockets.exceptions.ConnectionClosedError as e: print(f"Connection closed unexpectedly: {e}") except Exception as e: print(f"An error occurred: {e}") print(f"Client {CLIENT_ID} stopped.") if __name__ == "__main__": # Note: In a real application, you'd want a more robust way # to manage the event loop and handle potential reconnections. asyncio.run(main_client())