import readline import os import time from typing import Optional from utils.display import Display from cli.help import HelpManager from core.transport import Transport from proto.c2_pb2 import Command from streams.udp_receiver import UDPReceiver from streams.config import UDP_HOST, UDP_PORT, IMAGE_DIR, MULTILAT_AUTH_TOKEN from web.server import UnifiedWebServer from web.mlat import MlatEngine DEV_MODE = True class CLI: def __init__(self, registry, commands, groups, transport: Transport): self.registry = registry self.commands = commands self.groups = groups self.transport = transport self.help_manager = HelpManager(commands, DEV_MODE) self.active_commands = {} # {request_id: {"device_id": ..., "command_name": ..., "start_time": ..., "status": "running"}} # Separate server instances self.web_server: Optional[UnifiedWebServer] = None self.udp_receiver: Optional[UDPReceiver] = None self.mlat_engine = MlatEngine() readline.parse_and_bind("tab: complete") readline.set_completer(self._complete) # ================= TAB COMPLETION ================= def _complete(self, text, state): buffer = readline.get_line_buffer() parts = buffer.split() options = [] if len(parts) == 1: options = ["send", "list", "modules", "group", "help", "clear", "exit", "active_commands", "web", "camera"] elif parts[0] == "send": if len(parts) == 2: # Completing target (device ID, 'all', 'group') options = ["all", "group"] + self.registry.ids() elif len(parts) == 3 and parts[1] == "group": # Completing group name after 'send group' options = list(self.groups.all_groups().keys()) elif (len(parts) == 3 and parts[1] != "group") or (len(parts) == 4 and parts[1] == "group"): # Completing command name options = self.commands.list() elif parts[0] == "web": if len(parts) == 2: options = ["start", "stop", "status"] elif parts[0] == "camera": if len(parts) == 2: options = ["start", "stop", "status"] elif parts[0] == "group": if len(parts) == 2: # Completing group action options = ["add", "remove", "list", "show"] elif parts[1] == "add" and len(parts) >= 3: # Completing device IDs for 'group add' # Suggest available device IDs that are not already in the group being added to group_name = parts[2] if len(parts) > 2 else "" current_group_members = self.groups.get(group_name) if group_name else [] all_device_ids = set(self.registry.ids()) options = sorted(list(all_device_ids - set(current_group_members))) elif parts[1] in ("remove", "show") and len(parts) == 3: # Completing group names for 'group remove/show' options = list(self.groups.all_groups().keys()) elif parts[1] == "remove" and len(parts) >= 4: # Completing device IDs for 'group remove' group_name = parts[2] options = self.groups.get(group_name) matches = [o for o in options if o.startswith(text)] return matches[state] if state < len(matches) else None # ================= MAIN LOOP ================= def loop(self): while True: cmd = input(Display.cli_prompt()).strip() if not cmd: continue if cmd == "exit": return self.execute_command(cmd) def execute_command(self, cmd: str): """Execute a command string. Used by both CLI loop and TUI.""" if not cmd: return parts = cmd.split() action = parts[0] if action == "help": self.help_manager.show(parts[1:]) return if action == "exit": return if action == "clear": os.system("cls" if os.name == "nt" else "clear") return if action == "list": self._handle_list() return if action == "modules": self.help_manager.show_modules() return if action == "group": self._handle_group(parts[1:]) return if action == "send": self._handle_send(parts) return if action == "active_commands": self._handle_active_commands() return if action == "web": self._handle_web(parts[1:]) return if action == "camera": self._handle_camera(parts[1:]) return Display.error("Unknown command") # ================= HANDLERS ================= def _handle_list(self): now = time.time() active_devices = self.registry.all() if not active_devices: Display.system_message("No devices currently connected.") return Display.system_message("Connected Devices:") Display.print_table_header(["ID", "IP Address", "Status", "Connected For", "Last Seen"]) for d in active_devices: connected_for = Display.format_duration(now - d.connected_at) last_seen_duration = Display.format_duration(now - d.last_seen) Display.print_table_row([d.id, d.address[0], d.status, connected_for, last_seen_duration]) def _handle_send(self, parts): if len(parts) < 3: Display.error("Usage: send [args...]") return target_specifier = parts[1] command_parts = parts[2:] devices_to_target = [] target_description = "" # Resolve devices based on target_specifier if target_specifier == "all": devices_to_target = self.registry.all() target_description = "all connected devices" elif target_specifier == "group": if len(command_parts) < 2: Display.error("Usage: send group [args...]") return group_name = command_parts[0] command_parts = command_parts[1:] group_members_ids = self.groups.get(group_name) if not group_members_ids: Display.error(f"Group '{group_name}' not found or is empty.") return active_group_devices = [] for esp_id in group_members_ids: dev = self.registry.get(esp_id) if dev: active_group_devices.append(dev) else: Display.device_event(esp_id, f"Device in group '{group_name}' is not currently connected.") if not active_group_devices: Display.error(f"No active devices found in group '{group_name}'.") return devices_to_target = active_group_devices target_description = f"group '{group_name}' ({', '.join([d.id for d in devices_to_target])})" else: dev = self.registry.get(target_specifier) if dev: devices_to_target.append(dev) target_description = f"device '{target_specifier}'" else: Display.error(f"Device '{target_specifier}' not found.") return if not devices_to_target: Display.error("No target devices resolved for sending command.") return # Build Command cmd_name = command_parts[0] argv = command_parts[1:] request_id_base = f"req-{int(time.time())}" Display.system_message(f"Sending command '{cmd_name}' to {target_description}...") for i, d in enumerate(devices_to_target): cmd = Command() cmd.device_id = d.id cmd.command_name = cmd_name cmd.argv.extend(argv) request_id = f"{request_id_base}-{i}" cmd.request_id = request_id Display.command_sent(d.id, cmd_name, request_id) self.transport.send_command(d.sock, cmd) self.active_commands[request_id] = { "device_id": d.id, "command_name": cmd_name, "start_time": time.time(), "status": "running", "output": [] } def handle_command_response(self, request_id: str, device_id: str, payload: str, eof: bool): if request_id in self.active_commands: command_info = self.active_commands[request_id] command_info["output"].append(payload) if eof: command_info["status"] = "completed" Display.command_response(request_id, device_id, f"Command completed in {Display.format_duration(time.time() - command_info['start_time'])}") # Optionally print full output here if not already streamed # Display.command_response(request_id, device_id, "\n".join(command_info["output"])) del self.active_commands[request_id] else: # For streaming output, Display.command_response already prints each line pass else: Display.device_event(device_id, f"Received response for unknown command {request_id}: {payload}") def _handle_group(self, parts): if not parts: Display.error("Usage: group ") return cmd = parts[0] if cmd == "add" and len(parts) >= 3: group = parts[1] added_devices = [] for esp_id in parts[2:]: if self.registry.get(esp_id): # Only add if device exists self.groups.add_device(group, esp_id) added_devices.append(esp_id) else: Display.device_event(esp_id, "Device not found, skipping group add.") if added_devices: Display.system_message(f"Group '{group}' updated. Added: {', '.join(added_devices)}") else: Display.system_message(f"No valid devices to add to group '{group}'.") elif cmd == "remove" and len(parts) >= 3: group = parts[1] removed_devices = [] for esp_id in parts[2:]: if esp_id in self.groups.get(group): self.groups.remove_device(group, esp_id) removed_devices.append(esp_id) else: Display.device_event(esp_id, f"Device not in group '{group}', skipping remove.") if removed_devices: Display.system_message(f"Group '{group}' updated. Removed: {', '.join(removed_devices)}") else: Display.system_message(f"No specified devices found in group '{group}' to remove.") elif cmd == "list": all_groups = self.groups.all_groups() if not all_groups: Display.system_message("No groups defined.") return Display.system_message("Defined Groups:") for g, members in all_groups.items(): Display.system_message(f" {g}: {', '.join(members) if members else 'No members'}") elif cmd == "show" and len(parts) == 2: group_name = parts[1] members = self.groups.get(group_name) if members: Display.system_message(f"Members of group '{group_name}': {', '.join(members)}") else: Display.system_message(f"Group '{group_name}' not found or empty.") else: Display.error("Invalid group command usage. See 'help group' for details.") def _handle_active_commands(self): if not self.active_commands: Display.system_message("No commands are currently active.") return Display.system_message("Active Commands:") Display.print_table_header(["Request ID", "Device ID", "Command", "Status", "Elapsed Time"]) now = time.time() for req_id, cmd_info in self.active_commands.items(): elapsed_time = Display.format_duration(now - cmd_info["start_time"]) Display.print_table_row([ req_id, cmd_info["device_id"], cmd_info["command_name"], cmd_info["status"], elapsed_time ]) def _handle_web(self, parts): """Handle web server commands (frontend + multilateration API).""" if not parts: Display.error("Usage: web ") return cmd = parts[0] if cmd == "start": if self.web_server and self.web_server.is_running: Display.system_message("Web server is already running.") return self.web_server = UnifiedWebServer( device_registry=self.registry, mlat_engine=self.mlat_engine, multilat_token=MULTILAT_AUTH_TOKEN, camera_receiver=self.udp_receiver ) if self.web_server.start(): Display.system_message(f"Web server started at {self.web_server.get_url()}") else: Display.error("Web server failed to start") elif cmd == "stop": if not self.web_server or not self.web_server.is_running: Display.system_message("Web server is not running.") return self.web_server.stop() Display.system_message("Web server stopped.") self.web_server = None elif cmd == "status": Display.system_message("Web Server Status:") if self.web_server and self.web_server.is_running: Display.system_message(f" Status: Running") Display.system_message(f" URL: {self.web_server.get_url()}") else: Display.system_message(f" Status: Stopped") # MLAT stats Display.system_message("MLAT Engine:") state = self.mlat_engine.get_state() Display.system_message(f" Mode: {state.get('coord_mode', 'gps').upper()}") Display.system_message(f" Scanners: {state['scanners_count']}") if state['target']: pos = state['target']['position'] if 'lat' in pos: Display.system_message(f" Target: ({pos['lat']:.6f}, {pos['lon']:.6f})") else: Display.system_message(f" Target: ({pos['x']:.2f}m, {pos['y']:.2f}m)") else: Display.system_message(f" Target: Not calculated") else: Display.error("Invalid web command. Use: start, stop, status") def _handle_camera(self, parts): """Handle camera UDP receiver commands.""" if not parts: Display.error("Usage: camera ") return cmd = parts[0] if cmd == "start": if self.udp_receiver and self.udp_receiver.is_running: Display.system_message("Camera UDP receiver is already running.") return self.udp_receiver = UDPReceiver( host=UDP_HOST, port=UDP_PORT, image_dir=IMAGE_DIR, device_registry=self.registry ) if self.udp_receiver.start(): Display.system_message(f"Camera UDP receiver started on {UDP_HOST}:{UDP_PORT}") # Update web server if running if self.web_server and self.web_server.is_running: self.web_server.set_camera_receiver(self.udp_receiver) Display.system_message("Web server updated with camera receiver") else: Display.error("Camera UDP receiver failed to start") elif cmd == "stop": if not self.udp_receiver or not self.udp_receiver.is_running: Display.system_message("Camera UDP receiver is not running.") return self.udp_receiver.stop() Display.system_message("Camera UDP receiver stopped.") self.udp_receiver = None # Update web server if self.web_server and self.web_server.is_running: self.web_server.set_camera_receiver(None) elif cmd == "status": Display.system_message("Camera UDP Receiver Status:") if self.udp_receiver and self.udp_receiver.is_running: stats = self.udp_receiver.get_stats() Display.system_message(f" Status: Running on {UDP_HOST}:{UDP_PORT}") Display.system_message(f" Packets received: {stats['packets_received']}") Display.system_message(f" Frames decoded: {stats['frames_received']}") Display.system_message(f" Decode errors: {stats['decode_errors']}") Display.system_message(f" Invalid tokens: {stats['invalid_tokens']}") Display.system_message(f" Active cameras: {stats['active_cameras']}") else: Display.system_message(f" Status: Stopped") else: Display.error("Invalid camera command. Use: start, stop, status")