From 1ccf2cd796316cc16db51c3dba61d0aeeb3fab1a Mon Sep 17 00:00:00 2001 From: Sam Honor Date: Sun, 17 May 2026 12:22:43 -0400 Subject: [PATCH 1/3] core: Added api endpoints for storing autopilot execution arguments to settings file --- .../ardupilot_manager/api/v1/routers/index.py | 8 ++++++++ core/services/ardupilot_manager/autopilot_manager.py | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/core/services/ardupilot_manager/api/v1/routers/index.py b/core/services/ardupilot_manager/api/v1/routers/index.py index 30fbea116e..ddffd03d70 100644 --- a/core/services/ardupilot_manager/api/v1/routers/index.py +++ b/core/services/ardupilot_manager/api/v1/routers/index.py @@ -261,6 +261,14 @@ def available_routers() -> Any: return autopilot.get_available_routers() +@index_router_v1.post("/exec_arguments", summary="Set arguments to be passed to autopilot executable") +@index_to_http_exception +async def set_exec_arguments(firmware: str, arguments: dict[str, str]) -> Any: + logger.info(f"Setting execution arguments of {firmware} to {arguments}") + await autopilot.set_exec_arguments(firmware, arguments) + logger.info("Execution arguments successfully set") + + @index_router_v1.post("/stop", summary="Stop the autopilot.") @index_to_http_exception async def stop() -> Any: diff --git a/core/services/ardupilot_manager/autopilot_manager.py b/core/services/ardupilot_manager/autopilot_manager.py index b64682ce7a..989d62e988 100644 --- a/core/services/ardupilot_manager/autopilot_manager.py +++ b/core/services/ardupilot_manager/autopilot_manager.py @@ -518,6 +518,17 @@ def get_preferred_board(self) -> FlightController: raise NoPreferredBoardSet("Preferred board not set yet.") return FlightController(**preferred_board) + async def set_exec_arguments(self, firmware_name: str, settings: dict[str, str]) -> None: + try: + if "exec_arguments" not in self.configuration: + self.configuration["exec_arguments"] = {} + self.configuration["exec_arguments"][firmware_name] = settings + self.settings.save(self.configuration) + logger.info("Execution arguments set.") + except Exception as e: + logger.error("Error while saving execution arguments") + logger.error(repr(e)) + def get_board_to_be_used(self, boards: List[FlightController]) -> FlightController: """Check if preferred board exists and is connected. If so, use it, otherwise, choose by priority.""" try: From 2ebb4bd4d82e90cd6412a6e5c8c4475bfec5b3bc Mon Sep 17 00:00:00 2001 From: Sam Honor Date: Sun, 17 May 2026 13:13:56 -0400 Subject: [PATCH 2/3] core: Added endpoint for getting arguments to be passed to autopilot executable --- .../ardupilot_manager/api/v1/routers/index.py | 20 ++++++++++++++----- .../ardupilot_manager/autopilot_manager.py | 14 +++++++++++-- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/core/services/ardupilot_manager/api/v1/routers/index.py b/core/services/ardupilot_manager/api/v1/routers/index.py index ddffd03d70..97b924513a 100644 --- a/core/services/ardupilot_manager/api/v1/routers/index.py +++ b/core/services/ardupilot_manager/api/v1/routers/index.py @@ -261,12 +261,22 @@ def available_routers() -> Any: return autopilot.get_available_routers() -@index_router_v1.post("/exec_arguments", summary="Set arguments to be passed to autopilot executable") +@index_router_v1.post("/set_exec_arguments", summary="Set arguments to be passed to autopilot executable") @index_to_http_exception -async def set_exec_arguments(firmware: str, arguments: dict[str, str]) -> Any: - logger.info(f"Setting execution arguments of {firmware} to {arguments}") - await autopilot.set_exec_arguments(firmware, arguments) - logger.info("Execution arguments successfully set") +def set_exec_arguments(firmware: str, board: str, arguments: dict[str, str]) -> Any: + logger.info(f"Setting execution arguments of {firmware} on board {board} to {arguments}") + autopilot.set_exec_arguments(firmware, board, arguments) + + +@index_router_v1.post( + "/get_exec_arguments", + response_model=dict[str, str], + summary="Get arguments to be passed to specified autopilot executable", +) +@index_to_http_exception +async def get_exec_arguments(firmware: str, board: str) -> Any: + logger.info(f"Getting execution arguments for firmware {firmware} on board {board}") + return autopilot.get_exec_arguments(firmware, board) @index_router_v1.post("/stop", summary="Stop the autopilot.") diff --git a/core/services/ardupilot_manager/autopilot_manager.py b/core/services/ardupilot_manager/autopilot_manager.py index 989d62e988..b13c83a5f1 100644 --- a/core/services/ardupilot_manager/autopilot_manager.py +++ b/core/services/ardupilot_manager/autopilot_manager.py @@ -518,17 +518,27 @@ def get_preferred_board(self) -> FlightController: raise NoPreferredBoardSet("Preferred board not set yet.") return FlightController(**preferred_board) - async def set_exec_arguments(self, firmware_name: str, settings: dict[str, str]) -> None: + def set_exec_arguments(self, firmware_name: str, board: str, settings: dict[str, str]) -> None: try: if "exec_arguments" not in self.configuration: self.configuration["exec_arguments"] = {} - self.configuration["exec_arguments"][firmware_name] = settings + if firmware_name not in self.configuration["exec_arguments"]: + self.configuration["exec_arguments"][firmware_name] = {} + self.configuration["exec_arguments"][firmware_name][board] = settings self.settings.save(self.configuration) logger.info("Execution arguments set.") except Exception as e: logger.error("Error while saving execution arguments") logger.error(repr(e)) + def get_exec_arguments(self, firmware_name: str, board: str) -> Any: + try: + return self.configuration["exec_arguments"][firmware_name][board] + except Exception as e: + logger.error("Error while getting execution arguments") + logger.error(repr(e)) + return None + def get_board_to_be_used(self, boards: List[FlightController]) -> FlightController: """Check if preferred board exists and is connected. If so, use it, otherwise, choose by priority.""" try: From e023a04fa63b107fa30f3055c0c8d6d574e42bda Mon Sep 17 00:00:00 2001 From: Sam Honor Date: Sun, 17 May 2026 23:27:51 -0400 Subject: [PATCH 3/3] core: User can specify custom arguments in ardupilot settings JSON file --- .../ardupilot_manager/api/v1/routers/index.py | 3 +- .../ardupilot_manager/autopilot_manager.py | 79 ++++++++++++++----- .../ardupilot_manager/default_arguments.json | 25 ++++++ core/services/ardupilot_manager/typedefs.py | 9 +++ 4 files changed, 95 insertions(+), 21 deletions(-) create mode 100644 core/services/ardupilot_manager/default_arguments.json diff --git a/core/services/ardupilot_manager/api/v1/routers/index.py b/core/services/ardupilot_manager/api/v1/routers/index.py index 97b924513a..ee1fefb714 100644 --- a/core/services/ardupilot_manager/api/v1/routers/index.py +++ b/core/services/ardupilot_manager/api/v1/routers/index.py @@ -263,14 +263,13 @@ def available_routers() -> Any: @index_router_v1.post("/set_exec_arguments", summary="Set arguments to be passed to autopilot executable") @index_to_http_exception -def set_exec_arguments(firmware: str, board: str, arguments: dict[str, str]) -> Any: +def set_exec_arguments(firmware: str, board: str, arguments: dict[str, str | dict[str, str]]) -> Any: logger.info(f"Setting execution arguments of {firmware} on board {board} to {arguments}") autopilot.set_exec_arguments(firmware, board, arguments) @index_router_v1.post( "/get_exec_arguments", - response_model=dict[str, str], summary="Get arguments to be passed to specified autopilot executable", ) @index_to_http_exception diff --git a/core/services/ardupilot_manager/autopilot_manager.py b/core/services/ardupilot_manager/autopilot_manager.py index b13c83a5f1..710cbce9ac 100644 --- a/core/services/ardupilot_manager/autopilot_manager.py +++ b/core/services/ardupilot_manager/autopilot_manager.py @@ -1,4 +1,5 @@ import asyncio +import json import os import pathlib import subprocess @@ -24,6 +25,7 @@ from mavlink_proxy.Manager import Manager as MavlinkManager from settings import Settings from typedefs import ( + EndpointDefinition, Firmware, FlightController, FlightControllerFlags, @@ -438,6 +440,7 @@ async def start_manual_board(self, board: FlightController) -> None: self.ardupilot_subprocess = None await self.start_mavlink_manager(self.master_endpoint) + # pylint: disable=too-many-locals async def start_sitl(self) -> None: self._current_board = BoardDetector.detect_sitl() if not self.firmware_manager.is_firmware_installed(self._current_board): @@ -451,26 +454,23 @@ async def start_sitl(self) -> None: firmware_path = self.firmware_manager.firmware_path(self._current_board.platform) self.firmware_manager.validate_firmware(firmware_path, self._current_board.platform) - # ArduPilot SITL binary will bind TCP port 5760 (server) and the mavlink router will connect to it as a client - master_endpoint = Endpoint( - name="Master", - owner=self.settings.app_name, - connection_type=EndpointType.TCPClient, - place="127.0.0.1", - argument=5760, - protected=True, - ) + if "exec_arguments" not in self.configuration or str(firmware_path) not in self.configuration["exec_arguments"]: + with open(pathlib.Path(__file__).parent.resolve() / "default_arguments.json", "r", encoding="utf-8") as f: + default_config = json.load(f) + logger.warning(f"Setting defaults parameters for SITL to {default_config}") + self.set_exec_arguments(str(firmware_path), "SITL", default_config["SITL"]) + # Refresh configuration, as user may have changed settings since restart + self.settings.load() + self.configuration = deepcopy(self.settings.content) + arguments = self.configuration["exec_arguments"][str(firmware_path)]["SITL"] + + # ArduPilot SITL binary will bind TCP port specified by user (or default to port 5760) and the mavlink router will connect to it as a client + endpoint_config = arguments.get("endpoint") + master_endpoint = self._create_endpoint_from_config(endpoint_config) + # pylint: disable=consider-using-with self.ardupilot_subprocess = subprocess.Popen( - [ - firmware_path, - "--model", - self.current_sitl_frame.value, - "--base-port", - str(master_endpoint.argument), - "--home", - "-27.563,-48.459,0.0,270.0", - ], + self._create_execution_string(str(firmware_path), arguments), shell=False, encoding="utf-8", errors="ignore", @@ -479,6 +479,43 @@ async def start_sitl(self) -> None: await self.start_mavlink_manager(master_endpoint) + def _create_endpoint_from_config(self, endpoint_config: dict[str, str] | None) -> Endpoint: + default_endpoint_args = EndpointDefinition() + if endpoint_config is None: + logger.warning("Using default endpoint for SITL because none was provided in settings.json.") + master_endpoint_args = default_endpoint_args + else: + # Start with defaults, override only known fields, ignore unknown keys + arg_dict = EndpointDefinition().dict() + for key, value in endpoint_config.items(): + if key in arg_dict.keys(): + arg_dict[key] = self._sanitize_endpoint_argument(key, value) + else: + logger.debug("Ignoring unknown endpoint field '%s' in settings.json.", key) + master_endpoint_args = EndpointDefinition(**arg_dict) + return Endpoint(**master_endpoint_args.dict()) + + def _sanitize_endpoint_argument(self, key: str, value: str) -> int | bool | str: + if key == "argument": + return int(value) + if key == "protected": + if value == "True": + return True + if value == "False": + return False + raise ValueError("Invalid value for 'protected' argument of endpoint") + return value + + def _create_execution_string(self, firmware_path: str, arguments: dict[str, str | dict[str, str]]) -> list[str]: + arg_list = [firmware_path] + for k, v in arguments.items(): + if k == "endpoint": + continue + arg_list.append(str(k)) + if v != "": + arg_list.append(str(v)) + return arg_list + async def start_mavlink_manager(self, device: Endpoint) -> None: for endpoint in self.autopilot_default_endpoints: try: @@ -518,7 +555,9 @@ def get_preferred_board(self) -> FlightController: raise NoPreferredBoardSet("Preferred board not set yet.") return FlightController(**preferred_board) - def set_exec_arguments(self, firmware_name: str, board: str, settings: dict[str, str]) -> None: + def set_exec_arguments(self, firmware_name: str, board: str, settings: dict[str, str | dict[str, str]]) -> None: + self.settings.load() + self.configuration = deepcopy(self.settings.content) try: if "exec_arguments" not in self.configuration: self.configuration["exec_arguments"] = {} @@ -532,6 +571,8 @@ def set_exec_arguments(self, firmware_name: str, board: str, settings: dict[str, logger.error(repr(e)) def get_exec_arguments(self, firmware_name: str, board: str) -> Any: + self.settings.load() + self.configuration = deepcopy(self.settings.content) try: return self.configuration["exec_arguments"][firmware_name][board] except Exception as e: diff --git a/core/services/ardupilot_manager/default_arguments.json b/core/services/ardupilot_manager/default_arguments.json new file mode 100644 index 0000000000..6e4a80f9a6 --- /dev/null +++ b/core/services/ardupilot_manager/default_arguments.json @@ -0,0 +1,25 @@ +{ + "SITL" : { + "endpoint" : { + "name" : "Master", + "owner" : "Ardupilot Manager", + "connection_type" : "tcpout", + "place" : "127.0.0.1", + "argument" : "5760", + "protected" : "True" + }, + "--model" : "vectored", + "--home" : "-27.563,-48.459,0.0,270.0" + }, + "Linux" : { + "endpoint" : { + "name" : "Master", + "owner" : "Ardupilot Manager", + "connection_type" : "udpin", + "place" : "127.0.0.1", + "argument" : "8852", + "protected" : "True" + }, + "use_defaults" : "True" + } +} \ No newline at end of file diff --git a/core/services/ardupilot_manager/typedefs.py b/core/services/ardupilot_manager/typedefs.py index 4861693a02..a8f05c59c2 100644 --- a/core/services/ardupilot_manager/typedefs.py +++ b/core/services/ardupilot_manager/typedefs.py @@ -209,3 +209,12 @@ def valid_endpoint(cls: Any, value: str) -> str: def __hash__(self) -> int: # make hashable BaseModel subclass return hash(self.port + self.endpoint) + + +class EndpointDefinition(BaseModel): + name: str = "Master" + owner: str = "Ardupilot Manager" + connection_type: str = "tcpout" + place: str = "127.0.0.1" + argument: Optional[int] = 5760 + protected: Optional[bool] = True