Spaces:
Running on Zero
Running on Zero
| """MAAS-LLM β Main Entry Point | |
| Modes: | |
| sandbox (default) β Webcam β YOLO β Analyst β Commander | |
| sitl β MAVProxy UDP β Analyst β Commander (scripted events, no camera) | |
| Environment variables: | |
| CAMERA_INDEX int (default: 0) | |
| VISION_SAMPLE_INTERVAL float (default: 0.2) | |
| VISION_CONFIDENCE float (default: 0.6) | |
| YOLO_MODEL_PATH str (default: weights/best.pt) | |
| DRY_RUN 1 Print command, wait for [y/N] before sending (default: 1 in sitl, 0 in sandbox) | |
| SITL_UDP_IN str (default: 127.0.0.1:14550) β where we receive raw MAVLink from MAVProxy | |
| TELEMETRY_IN_PORT int (default: 9000) β UDP port for sandbox telemetry | |
| COMMAND_OUT_PORT int (default: 9001) β UDP port for outbound commands | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import asyncio | |
| import json | |
| import logging | |
| import os | |
| import sys | |
| from src.core.pipeline import parse_sandbox_telemetry | |
| from src.core.mavlink_adapter import MAVLinkDependencyError, MAVLinkTelemetryDecoder | |
| from src.core.types import VisionEvent | |
| from src.core.weather_provider import WeatherProvider | |
| from src.core.weather_policy import validate_weather_for_flight, WeatherPolicyError | |
| from src.core.flight_planner import evaluate_feasibility | |
| from src.core.route_types import DroneCapabilities, FlightPlan, Waypoint | |
| from src.core.mission_profiles import PROFILES, MissionProfile | |
| from src.core.mission_profiles import PROFILES, MissionProfile | |
| from src.nodes.analyst import AnalystNode | |
| from src.core.evaluator import HackathonEvaluator | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") | |
| logger = logging.getLogger(__name__) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Global state | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| current_telemetry: dict = {} | |
| current_snapshot = None | |
| mavlink_source_addr = None # set when first MAVLink datagram arrives | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # UDP Telemetry Server | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TelemetryUDPServer: | |
| """Accepts both JSON sandbox telemetry and raw MAVLink bytes.""" | |
| def __init__(self): | |
| self.mavlink_decoder = None | |
| def connection_made(self, transport): | |
| self.transport = transport | |
| logger.info("[UDP Server] Listening for telemetry on bound port.") | |
| def connection_lost(self, exc): | |
| pass | |
| def datagram_received(self, data, addr): | |
| global current_telemetry, current_snapshot, mavlink_source_addr | |
| self.last_addr = addr | |
| # Try JSON sandbox telemetry first (e.g. from a test harness) | |
| try: | |
| payload = json.loads(data.decode("utf-8")) | |
| current_telemetry = payload | |
| current_snapshot = parse_sandbox_telemetry(data) | |
| return | |
| except (json.JSONDecodeError, UnicodeDecodeError, ValueError): | |
| pass | |
| # Fall back to raw MAVLink bytes (from MAVProxy / SITL) | |
| try: | |
| if self.mavlink_decoder is None: | |
| self.mavlink_decoder = MAVLinkTelemetryDecoder() | |
| decoded = self.mavlink_decoder.feed(data) | |
| if decoded is not None: | |
| mavlink_source_addr = addr # remember who's sending MAVLink | |
| current_snapshot = decoded | |
| current_telemetry = { | |
| "drone_id": decoded.drone_id, | |
| "timestamp": decoded.timestamp, | |
| "lat": decoded.latitude, | |
| "lon": decoded.longitude, | |
| "alt": decoded.altitude_m, | |
| "heading": decoded.heading_deg, | |
| "battery_percent": decoded.battery_percent, | |
| "flight_mode": decoded.flight_mode, | |
| "is_armed": decoded.is_armed, | |
| } | |
| logger.debug("[Telemetry] MAVLink snapshot: %s", current_telemetry) | |
| except MAVLinkDependencyError as err: | |
| logger.error("Raw MAVLink received but decoder unavailable: %s", err) | |
| except (ValueError, AttributeError) as err: | |
| logger.warning("Ignoring invalid datagram from %s: %s", addr, err) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Event Processing | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def _dry_run_gate(command: dict, dry_run: bool) -> bool: | |
| """If dry_run is True, print the command and wait for user confirmation. | |
| Returns True if the command should be sent.""" | |
| if not dry_run: | |
| return True | |
| print("\n" + "β" * 60) | |
| print(" DRY-RUN: Commander output (NOT YET SENT)") | |
| print("β" * 60) | |
| print(json.dumps(command, indent=2)) | |
| print("β" * 60) | |
| try: | |
| answer = await asyncio.get_running_loop().run_in_executor( | |
| None, lambda: input(" Send this command? [y/N]: ").strip().lower() | |
| ) | |
| return answer in ("y", "yes") | |
| except EOFError: | |
| # Non-interactive environment β auto-deny | |
| logger.info("[DryRun] Non-interactive mode β command NOT sent.") | |
| return False | |
| async def process_watchdog_events( | |
| event_queue: asyncio.Queue, | |
| analyst: AnalystNode, | |
| commander, | |
| transport, | |
| weather_provider: WeatherProvider, | |
| mission_profile: MissionProfile, | |
| dry_run: bool = False, | |
| command_out_addr: tuple[str, int] = ("127.0.0.1", 9001), | |
| ) -> None: | |
| while True: | |
| event = await event_queue.get() | |
| try: | |
| if current_snapshot is None: | |
| logger.warning("[Pipeline] Skipping event β no telemetry yet. " | |
| "Ensure MAVProxy / SITL is sending to the telemetry port.") | |
| continue | |
| # Fetch weather and evaluate feasibility | |
| weather = None | |
| feasibility = None | |
| try: | |
| weather = await weather_provider.get_weather( | |
| current_snapshot.latitude, current_snapshot.longitude | |
| ) | |
| if weather: | |
| validate_weather_for_flight(weather) | |
| single_wp = Waypoint( | |
| latitude=current_snapshot.latitude, | |
| longitude=current_snapshot.longitude, | |
| altitude_m=current_snapshot.altitude_m, | |
| ) | |
| plan = FlightPlan( | |
| plan_id="event-plan", | |
| drone_id=event.drone_id, | |
| waypoints=[single_wp], | |
| created_timestamp=event.timestamp, | |
| ) | |
| capabilities = DroneCapabilities( | |
| max_speed_mps=15.0, max_climb_rate_mps=5.0, | |
| max_descent_rate_mps=3.0, min_altitude_m=5.0, | |
| max_altitude_m=120.0, max_mission_distance_m=5000.0, | |
| max_waypoints=20, battery_reserve_percent=20.0, | |
| inspection_dwell_time_s=30.0, | |
| ) | |
| feasibility = evaluate_feasibility(plan, current_snapshot, capabilities, weather) | |
| except WeatherPolicyError as err: | |
| logger.warning("[Weather] Policy blocked event: %s", err) | |
| continue | |
| except Exception as err: | |
| logger.warning("[Weather] Fetch failed (proceeding without weather): %s", err) | |
| # Build analyst context and run Commander | |
| context = analyst.generate_event_context(event, current_snapshot, weather, feasibility, mission_profile) | |
| command = await commander.generate_mavlink_command(context, current_snapshot, mission_profile, event.anomaly_type) | |
| if command: | |
| approved = await _dry_run_gate(command, dry_run) | |
| if approved: | |
| logger.info("[Pipeline] Sending command to %s:%d β %s", *command_out_addr, command) | |
| # Direct SITL Injection via pymavlink to the MAVProxy source address | |
| if mavlink_source_addr is not None: | |
| try: | |
| from pymavlink import mavutil | |
| mav = mavutil.mavlink.MAVLink(None, srcSystem=255, srcComponent=0) | |
| dest = mavlink_source_addr | |
| logger.info("[SITL] β GUIDED mode") | |
| msg_mode = mavutil.mavlink.MAVLink_command_long_message( | |
| 1, 1, mavutil.mavlink.MAV_CMD_DO_SET_MODE, 0, | |
| 1, 4, 0, 0, 0, 0, 0 | |
| ) | |
| transport.sendto(msg_mode.pack(mav), dest) | |
| await asyncio.sleep(0.5) | |
| logger.info("[SITL] β ARM") | |
| msg_arm = mavutil.mavlink.MAVLink_command_long_message( | |
| 1, 1, mavutil.mavlink.MAV_CMD_COMPONENT_ARM_DISARM, 0, | |
| 1, 0, 0, 0, 0, 0, 0 | |
| ) | |
| transport.sendto(msg_arm.pack(mav), dest) | |
| await asyncio.sleep(1.5) | |
| logger.info("[SITL] β TAKEOFF 15m") | |
| msg_takeoff = mavutil.mavlink.MAVLink_command_long_message( | |
| 1, 1, mavutil.mavlink.MAV_CMD_NAV_TAKEOFF, 0, | |
| 0, 0, 0, 0, 0, 0, 15 | |
| ) | |
| transport.sendto(msg_takeoff.pack(mav), dest) | |
| await asyncio.sleep(8.0) | |
| x = float(command.get("x", 0)) | |
| y = float(command.get("y", 0)) | |
| z = -15.0 # always target 15 m altitude | |
| logger.info(f"[SITL] β SET_POSITION_TARGET_LOCAL_NED x={x} y={y} z={z} β {dest}") | |
| msg_pos = mavutil.mavlink.MAVLink_set_position_target_local_ned_message( | |
| 0, 1, 1, | |
| mavutil.mavlink.MAV_FRAME_LOCAL_OFFSET_NED, | |
| 0b0000111111111000, # mask: pos only | |
| x, y, z, | |
| 0, 0, 0, 0, 0, 0, 0, 0 | |
| ) | |
| transport.sendto(msg_pos.pack(mav), dest) | |
| logger.info("[SITL] MAVLink sequence complete.") | |
| except Exception as e: | |
| logger.error(f"[SITL] Direct injection failed: {e}", exc_info=True) | |
| else: | |
| logger.warning("[SITL] No MAVLink source address yet β is MAVProxy connected?") | |
| transport.sendto(json.dumps(command).encode("utf-8"), command_out_addr) | |
| else: | |
| logger.info("[Pipeline] Command NOT sent (dry-run denied).") | |
| else: | |
| logger.warning("[Pipeline] Commander returned no valid command for event: %s", event.anomaly_type) | |
| finally: | |
| event_queue.task_done() | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Startup banner | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _print_banner(mode: str, telemetry_port: int, command_out_port: int, dry_run: bool, profile: str = "sandbox", show_ui: bool = False) -> None: | |
| print() | |
| print("ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ") | |
| print("β MAAS-LLM Β· Autonomous Aerial AI β") | |
| print("β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ£") | |
| print(f"β Mode : {mode.upper():<38}β") | |
| print(f"β Profile : {profile.upper():<38}β") | |
| print(f"β Live UI : {'ENABLED' if show_ui else 'DISABLED':<38}β") | |
| print(f"β Telemetry : UDP 0.0.0.0:{telemetry_port:<26}β") | |
| print(f"β Command Out : UDP 127.0.0.1:{command_out_port:<24}β") | |
| print(f"β Dry-Run : {'ENABLED β awaiting approval' if dry_run else 'DISABLED β live commands':<38}β") | |
| if mode == "sitl": | |
| print("β SITL : Connect MAVProxy β UDP 127.0.0.1:" + f"{telemetry_port:<5}β") | |
| print("β Example : mavproxy.py --master tcp:127.0.0.1:5762 \\ β") | |
| print("β --out udp:127.0.0.1:" + f"{telemetry_port:<18}β") | |
| print("ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ") | |
| print() | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Pipeline entry points | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def run_sandbox_pipeline(args: argparse.Namespace) -> None: | |
| """Webcam β YOLO β Analyst β Commander.""" | |
| from src.nodes.watchdog import WatchdogNode | |
| from src.nodes.commander import CommanderNode | |
| dry_run = bool(int(os.getenv("DRY_RUN", "0"))) | |
| telemetry_port = int(os.getenv("TELEMETRY_IN_PORT", "9000")) | |
| command_out_port = int(os.getenv("COMMAND_OUT_PORT", "9001")) | |
| _print_banner("sandbox", telemetry_port, command_out_port, dry_run, getattr(args, "profile", "sandbox"), getattr(args, "show_ui", False)) | |
| loop = asyncio.get_running_loop() | |
| transport, _ = await loop.create_datagram_endpoint( | |
| TelemetryUDPServer, | |
| local_addr=("0.0.0.0", telemetry_port), | |
| ) | |
| analyst = AnalystNode() | |
| commander = CommanderNode() | |
| weather_provider = WeatherProvider(use_fake=False) | |
| event_queue: asyncio.Queue = asyncio.Queue(maxsize=32) | |
| profile_name = getattr(args, "profile", "sandbox") | |
| mission_profile = PROFILES.get(profile_name, PROFILES["free"]) | |
| class_map = {cls_id: "anomaly" for cls_id in mission_profile.yolo_class_watchlist} | |
| watchdog = WatchdogNode( | |
| model_path=os.getenv("YOLO_MODEL_PATH", "weights/best.pt"), | |
| event_queue=event_queue, | |
| sample_interval=float(os.getenv("VISION_SAMPLE_INTERVAL", "0.2")), | |
| confidence_threshold=float(os.getenv("VISION_CONFIDENCE", "0.6")), | |
| class_map=class_map, | |
| show_ui=getattr(args, "show_ui", False), | |
| mission_profile_name=profile_name, | |
| ) | |
| camera_index = int(os.getenv("CAMERA_INDEX", "0")) | |
| if not watchdog.start_camera(camera_index): | |
| logger.error("[Sandbox] Failed to start webcam index %d. Exiting.", camera_index) | |
| transport.close() | |
| return | |
| vision_task = asyncio.create_task(watchdog.run_vision_loop()) | |
| event_task = asyncio.create_task( | |
| process_watchdog_events( | |
| event_queue, analyst, commander, transport, | |
| weather_provider, mission_profile, dry_run, | |
| ("127.0.0.1", command_out_port), | |
| ) | |
| ) | |
| try: | |
| logger.info("[Sandbox] Pipeline running. Press Ctrl+C to stop.") | |
| while True: | |
| await asyncio.sleep(1) | |
| finally: | |
| vision_task.cancel() | |
| event_task.cancel() | |
| watchdog.stop() | |
| transport.close() | |
| async def run_sitl_pipeline(args: argparse.Namespace) -> None: | |
| """MAVProxy UDP β MAVLinkDecoder β Analyst β Commander (scripted events).""" | |
| from src.nodes.commander import CommanderNode | |
| from src.nodes.watchdog import WatchdogNode | |
| from src.core.sitl_injector import SITLInjector | |
| dry_run = bool(int(os.getenv("DRY_RUN", "1"))) # default: dry-run ON for SITL | |
| telemetry_port = int(os.getenv("TELEMETRY_IN_PORT", "9000")) | |
| command_out_port = int(os.getenv("COMMAND_OUT_PORT", "9001")) | |
| _print_banner("sitl", telemetry_port, command_out_port, dry_run, getattr(args, "profile", "sandbox"), getattr(args, "show_ui", False)) | |
| loop = asyncio.get_running_loop() | |
| transport, _ = await loop.create_datagram_endpoint( | |
| TelemetryUDPServer, | |
| local_addr=("0.0.0.0", telemetry_port), | |
| ) | |
| analyst = AnalystNode() | |
| commander = CommanderNode() | |
| weather_provider = WeatherProvider(use_fake=True) # use fake in SITL to avoid API calls | |
| event_queue: asyncio.Queue = asyncio.Queue(maxsize=32) | |
| profile_name = getattr(args, "profile", "sandbox") | |
| mission_profile = PROFILES.get(profile_name, PROFILES["free"]) | |
| injector = SITLInjector(event_queue, drone_id="sitl-drone-1", mission_profile_name=profile_name) | |
| # Allow webcam YOLO in SITL if requested | |
| show_ui = getattr(args, "show_ui", False) | |
| class_map = {cls_id: "anomaly" for cls_id in mission_profile.yolo_class_watchlist} | |
| watchdog = WatchdogNode( | |
| model_path=os.getenv("YOLO_MODEL_PATH", "weights/best.pt"), | |
| event_queue=event_queue, | |
| sample_interval=float(os.getenv("VISION_SAMPLE_INTERVAL", "0.2")), | |
| confidence_threshold=float(os.getenv("VISION_CONFIDENCE", "0.6")), | |
| class_map=class_map, | |
| show_ui=show_ui, | |
| mission_profile_name=profile_name, | |
| ) | |
| evaluator = None | |
| if getattr(args, "eval_video", None): | |
| evaluator = HackathonEvaluator(args.eval_video, profile_name) | |
| watchdog.set_evaluator(evaluator) | |
| commander.set_evaluator(evaluator) | |
| vision_task = None | |
| if show_ui or getattr(args, "eval_video", None): | |
| video_src = getattr(args, "eval_video", None) or int(os.getenv("CAMERA_INDEX", "0")) | |
| if watchdog.start_camera(video_src): | |
| vision_task = asyncio.create_task(watchdog.run_vision_loop()) | |
| else: | |
| logger.error("[SITL] Failed to start webcam index %d for UI. Continuing without it.", camera_index) | |
| logger.info("[SITL] Waiting 5 s for telemetry to arrive before injecting events...") | |
| await asyncio.sleep(5.0) | |
| inject_task = asyncio.create_task(injector.run()) | |
| event_task = asyncio.create_task( | |
| process_watchdog_events( | |
| event_queue, analyst, commander, transport, | |
| weather_provider, mission_profile, dry_run, | |
| ("127.0.0.1", command_out_port), | |
| ) | |
| ) | |
| try: | |
| if getattr(args, "eval_video", None): | |
| logger.info("[SITL] Video Eval Mode: Waiting for video to finish...") | |
| if vision_task: | |
| await vision_task | |
| logger.info("[SITL] Video finished. Giving pipeline 5s to flush events.") | |
| await asyncio.sleep(5.0) | |
| else: | |
| logger.info("[SITL] Pipeline running. Waiting for scenario to complete...") | |
| await inject_task # finishes after ~120 s | |
| logger.info("[SITL] Scenario complete. Giving pipeline 10 s to flush events.") | |
| await asyncio.sleep(10.0) | |
| finally: | |
| inject_task.cancel() | |
| event_task.cancel() | |
| if vision_task: | |
| vision_task.cancel() | |
| watchdog.stop() | |
| if evaluator: | |
| evaluator.generate_report() | |
| transport.close() | |
| logger.info("[SITL] Pipeline shut down cleanly.") | |
| async def run_free_pipeline(args: argparse.Namespace) -> None: | |
| """Live telemetry β Validates Waypoint β ARMs & Takes Off β Flies to Lat/Lon β YOLO Watchdog active.""" | |
| from src.nodes.watchdog import WatchdogNode | |
| from src.nodes.commander import CommanderNode | |
| from src.core.mavlink_adapter import MAVLinkTelemetryDecoder | |
| if args.lat is None or args.lon is None or args.alt is None: | |
| logger.error("[FreeMode] --lat, --lon, and --alt must be provided.") | |
| return | |
| dry_run = bool(int(os.getenv("DRY_RUN", "1"))) | |
| telemetry_port = int(os.getenv("TELEMETRY_IN_PORT", "9000")) | |
| command_out_port = int(os.getenv("COMMAND_OUT_PORT", "9001")) | |
| profile_name = getattr(args, "profile", "free") | |
| mission_profile = PROFILES.get(profile_name, PROFILES["free"]) | |
| _print_banner("free", telemetry_port, command_out_port, dry_run, profile_name, getattr(args, "show_ui", False)) | |
| loop = asyncio.get_running_loop() | |
| transport, server = await loop.create_datagram_endpoint( | |
| TelemetryUDPServer, | |
| local_addr=("0.0.0.0", telemetry_port), | |
| ) | |
| analyst = AnalystNode() | |
| commander = CommanderNode() | |
| weather_provider = WeatherProvider(use_fake=False) | |
| event_queue: asyncio.Queue = asyncio.Queue(maxsize=32) | |
| class_map = {cls_id: "anomaly" for cls_id in mission_profile.yolo_class_watchlist} | |
| watchdog = WatchdogNode( | |
| model_path=os.getenv("YOLO_MODEL_PATH", "weights/best.pt"), | |
| event_queue=event_queue, | |
| sample_interval=float(os.getenv("VISION_SAMPLE_INTERVAL", "0.2")), | |
| confidence_threshold=float(os.getenv("VISION_CONFIDENCE", "0.6")), | |
| class_map=class_map, | |
| show_ui=getattr(args, "show_ui", False), | |
| mission_profile_name=profile_name, | |
| ) | |
| camera_index = int(os.getenv("CAMERA_INDEX", "0")) | |
| if not watchdog.start_camera(camera_index): | |
| logger.error("[FreeMode] Failed to start webcam index %d. Exiting.", camera_index) | |
| transport.close() | |
| return | |
| logger.info("[FreeMode] Waiting for valid telemetry connection...") | |
| while current_snapshot is None: | |
| await asyncio.sleep(1.0) | |
| logger.info(f"[FreeMode] Connected! Current Pos: {current_snapshot.latitude}, {current_snapshot.longitude}, Alt: {current_snapshot.altitude_m}m") | |
| logger.info(f"[FreeMode] Target Pos: {args.lat}, {args.lon}, Alt: {args.alt}m") | |
| command = { | |
| "command": "SET_POSITION_TARGET_GLOBAL_INT", | |
| "target_system": 1, | |
| "target_component": 1, | |
| "lat": args.lat, | |
| "lon": args.lon, | |
| "alt": args.alt, | |
| "reasoning": "Free flight mode to user coordinates." | |
| } | |
| approved = await _dry_run_gate(command, dry_run) | |
| if approved: | |
| if hasattr(transport, "_protocol") and hasattr(transport._protocol, "last_addr"): | |
| try: | |
| import time | |
| from pymavlink import mavutil | |
| mav = mavutil.mavlink.MAVLink(None, srcSystem=255, srcComponent=0) | |
| if not current_snapshot.is_armed or current_snapshot.altitude_m < 2.0: | |
| logger.info("[FreeMode] Drone is grounded. Forcing GUIDED, ARMING, and TAKING OFF...") | |
| msg_mode = mavutil.mavlink.MAVLink_command_long_message(1, 1, mavutil.mavlink.MAV_CMD_DO_SET_MODE, 0, 1, 4, 0, 0, 0, 0, 0) | |
| transport.sendto(msg_mode.pack(mav), transport._protocol.last_addr) | |
| await asyncio.sleep(0.5) | |
| msg_arm = mavutil.mavlink.MAVLink_command_long_message(1, 1, mavutil.mavlink.MAV_CMD_COMPONENT_ARM_DISARM, 0, 1, 0, 0, 0, 0, 0, 0) | |
| transport.sendto(msg_arm.pack(mav), transport._protocol.last_addr) | |
| await asyncio.sleep(1.0) | |
| msg_takeoff = mavutil.mavlink.MAVLink_command_long_message(1, 1, mavutil.mavlink.MAV_CMD_NAV_TAKEOFF, 0, 0, 0, 0, 0, 0, 0, args.alt) | |
| transport.sendto(msg_takeoff.pack(mav), transport._protocol.last_addr) | |
| await asyncio.sleep(8.0) | |
| msg_pos = MAVLinkTelemetryDecoder.build_set_position_target_global_int(args.lat, args.lon, args.alt, mav) | |
| logger.info(f"[FreeMode] Injecting MAVLink position target globally.") | |
| transport.sendto(msg_pos.pack(mav), transport._protocol.last_addr) | |
| except Exception as e: | |
| logger.error(f"[FreeMode] MAVLink dispatch failed: {e}") | |
| else: | |
| logger.info("[FreeMode] Command denied.") | |
| vision_task = asyncio.create_task(watchdog.run_vision_loop()) | |
| event_task = asyncio.create_task( | |
| process_watchdog_events( | |
| event_queue, analyst, commander, transport, | |
| weather_provider, mission_profile, dry_run, | |
| ("127.0.0.1", command_out_port), | |
| ) | |
| ) | |
| try: | |
| logger.info("[FreeMode] Pipeline running. Press Ctrl+C to stop.") | |
| while True: | |
| await asyncio.sleep(1) | |
| finally: | |
| vision_task.cancel() | |
| event_task.cancel() | |
| watchdog.stop() | |
| transport.close() | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # CLI | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _parse_args() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser(description="MAAS-LLM Autonomous Aerial AI Pipeline") | |
| parser.add_argument( | |
| "--mode", | |
| choices=["sandbox", "sitl", "free"], | |
| default="sandbox", | |
| help="sandbox = webcam + real-time; sitl = MAVProxy UDP + scripted events; free = GPS Waypoint (default: sandbox)", | |
| ) | |
| parser.add_argument( | |
| "--profile", | |
| choices=list(PROFILES.keys()), | |
| default="sandbox", | |
| help="Mission profile (flood, fire, search_and_rescue, etc.)" | |
| ) | |
| parser.add_argument("--lat", type=float, help="Target latitude for free mode") | |
| parser.add_argument("--lon", type=float, help="Target longitude for free mode") | |
| parser.add_argument("--alt", type=float, help="Target altitude for free mode") | |
| parser.add_argument("--show-ui", action="store_true", help="Enable Live YOLO UI window") | |
| parser.add_argument("--eval-video", type=str, help="Path to pre-recorded video for Hackathon Evaluator mode") | |
| return parser.parse_args() | |
| if __name__ == "__main__": | |
| _args = _parse_args() | |
| try: | |
| if _args.mode == "sitl": | |
| asyncio.run(run_sitl_pipeline(_args)) | |
| elif _args.mode == "free": | |
| asyncio.run(run_free_pipeline(_args)) | |
| else: | |
| asyncio.run(run_sandbox_pipeline(_args)) | |
| except KeyboardInterrupt: | |
| logger.info("Pipeline terminated by user.") | |