Skip to content

Allow user to pass custom options to SITL autopilot executable#3933

Open
Shono1 wants to merge 7 commits into
bluerobotics:masterfrom
wpi-automata:sitl-options-pr
Open

Allow user to pass custom options to SITL autopilot executable#3933
Shono1 wants to merge 7 commits into
bluerobotics:masterfrom
wpi-automata:sitl-options-pr

Conversation

@Shono1
Copy link
Copy Markdown
Contributor

@Shono1 Shono1 commented May 18, 2026

I raised this issue #3421 last year, but just got around to writing the code for it now.

Currently, BlueOS does not provide users a way of changing the arguments to the autopilot executable aside from modifying the source code. This PR allows the user to define custom arguments to be passed to the executable in the settings.json file in the autopilot_logs folder. This file is where the existing minimal customization for sitl_frame was stored, so it seems like a logical spot to store more arguments.

As is, my code does not have a frontend and it is not configured to work with non-SITL executables--though the code is written such that extending it to other executable types would not be difficult. I decided to leave this functionality out for now, as my ROVs are mothballed and I am unable to test.

Summary by Sourcery

Allow configuration and persistence of custom execution arguments for the SITL autopilot executable via settings and API endpoints.

New Features:

  • Support per-firmware, per-board custom execution arguments for the SITL autopilot executable loaded from settings and defaulted from a JSON file.
  • Expose REST endpoints to set and retrieve execution arguments for specific firmware and board combinations.

Enhancements:

  • Refactor SITL startup to build the autopilot subprocess argument list from configurable settings, including an optional custom endpoint with a fallback to the previous default.

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 18, 2026

CLA assistant check
All committers have signed the CLA.

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • In start_sitl, self.configuration is checked for exec_arguments before calling self.settings.load(), which means the default-arguments branch can run off stale in-memory config; consider loading settings (or otherwise ensuring self.configuration is current) before the first exec_arguments existence check to avoid surprising overwrites.
  • The structure and keys of arguments["endpoint"] are passed directly into Endpoint(**...), which will raise if the user supplies unexpected keys or omits required ones; it may be safer to normalize/validate this dict before constructing the Endpoint to avoid runtime errors from malformed settings.json.
  • Both set_exec_arguments and get_exec_arguments silently swallow all exceptions and either log and return None or do nothing; consider tightening the exception handling (or re-raising a domain-specific error) so callers and the HTTP API can distinguish between "no config" and actual failures.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `start_sitl`, `self.configuration` is checked for `exec_arguments` before calling `self.settings.load()`, which means the default-arguments branch can run off stale in-memory config; consider loading settings (or otherwise ensuring `self.configuration` is current) before the first `exec_arguments` existence check to avoid surprising overwrites.
- The structure and keys of `arguments["endpoint"]` are passed directly into `Endpoint(**...)`, which will raise if the user supplies unexpected keys or omits required ones; it may be safer to normalize/validate this dict before constructing the `Endpoint` to avoid runtime errors from malformed `settings.json`.
- Both `set_exec_arguments` and `get_exec_arguments` silently swallow all exceptions and either log and return `None` or do nothing; consider tightening the exception handling (or re-raising a domain-specific error) so callers and the HTTP API can distinguish between "no config" and actual failures.

## Individual Comments

### Comment 1
<location path="core/services/ardupilot_manager/autopilot_manager.py" line_range="479-483" />
<code_context>
+        else:
+            master_endpoint = Endpoint(**arguments["endpoint"])
+
+        arg_list = [firmware_path]
+        for k, v in arguments.items():
+            if k != "endpoint":
+                arg_list.append(k)
+                if v != "":
+                    arg_list.append(v)
         # pylint: disable=consider-using-with
</code_context>
<issue_to_address>
**issue (bug_risk):** Coerce argument values to strings when building `arg_list` to avoid `Popen` type errors.

In this loop, `v` can be non-string (e.g., ints, bools) depending on `arguments`. `subprocess.Popen` requires each arg to be `str`/`bytes`/`os.PathLike`, so passing arbitrary types risks a runtime error. Please normalize both keys and values to strings, e.g.:

```python
arg_list = [str(firmware_path)]
for k, v in arguments.items():
    if k == "endpoint":
        continue
    arg_list.append(str(k))
    if v != "":
        arg_list.append(str(v))
```
</issue_to_address>

### Comment 2
<location path="core/services/ardupilot_manager/autopilot_manager.py" line_range="465-477" />
<code_context>
-            argument=5760,
-            protected=True,
-        )
+        if "endpoint" not in arguments:
+            logger.warning("Using default endpoint for SITL because none was provided in settings.json.")
+            master_endpoint = Endpoint(
+                name="Master",
+                owner=self.settings.app_name,
+                connection_type=EndpointType.TCPClient,
+                place="127.0.0.1",
+                argument=5760,
+                protected=True,
+            )
+        else:
+            master_endpoint = Endpoint(**arguments["endpoint"])
+
+        arg_list = [firmware_path]
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Consider validating or defaulting fields for the user-provided endpoint dict to avoid inconsistent endpoint instances.

The default path constructs a fully specified `Endpoint`, while the configured path unpacks `arguments["endpoint"]` as-is. If that dict is missing required fields or contains unexpected keys/types, it can raise at runtime or yield endpoints with different behavior. Normalizing the dict (e.g., apply defaults for missing keys and ignore unknown fields) before unpacking would avoid these inconsistencies.

```suggestion
        # ArduPilot SITL binary will bind TCP port 5760 (server) and the mavlink router will connect to it as a client
        default_endpoint_args = {
            "name": "Master",
            "owner": self.settings.app_name,
            "connection_type": EndpointType.TCPClient,
            "place": "127.0.0.1",
            "argument": 5760,
            "protected": True,
        }

        endpoint_config = arguments.get("endpoint")
        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:
            if not isinstance(endpoint_config, dict):
                logger.warning(
                    "Invalid endpoint configuration type '%s'; falling back to default endpoint for SITL.",
                    type(endpoint_config).__name__,
                )
                master_endpoint_args = default_endpoint_args
            else:
                # Start with defaults, override only known fields, ignore unknown keys
                master_endpoint_args = dict(default_endpoint_args)
                for key, value in endpoint_config.items():
                    if key in default_endpoint_args:
                        master_endpoint_args[key] = value
                    else:
                        logger.debug("Ignoring unknown endpoint field '%s' in settings.json.", key)

        master_endpoint = Endpoint(**master_endpoint_args)
```
</issue_to_address>

### Comment 3
<location path="core/services/ardupilot_manager/autopilot_manager.py" line_range="455-457" />
<code_context>
         self.firmware_manager.validate_firmware(firmware_path, self._current_board.platform)

+        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="ascii") as f:
+                default_config = json.load(f)
+            logger.warning(f"Setting defaults parameters for SITL to {default_config}")
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Using ASCII encoding for `default_arguments.json` is brittle; UTF-8 is safer for JSON.

Using `encoding="ascii"` will raise `UnicodeDecodeError` as soon as non-ASCII characters appear in this JSON (e.g., in labels or paths). Please switch to `encoding="utf-8"`, which is the de-facto standard for JSON and preserves current behavior for ASCII-only content while being more robust.

```suggestion
        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)
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread core/services/ardupilot_manager/autopilot_manager.py Outdated
Comment thread core/services/ardupilot_manager/autopilot_manager.py Outdated
Comment thread core/services/ardupilot_manager/autopilot_manager.py
Shono1 and others added 3 commits May 18, 2026 16:32
ASCII encoding --> UTF-8

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
Comment thread core/services/ardupilot_manager/autopilot_manager.py Outdated
Comment thread core/services/ardupilot_manager/autopilot_manager.py Outdated
Copy link
Copy Markdown
Member

@joaoantoniocardoso joaoantoniocardoso left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem being SITL-only; let's follow up with a plan to address what's needed to get to the linux-based as well.

Comment on lines +492 to +498
arg_list = [str(firmware_path)]
for k, v in arguments.items():
if k == "endpoint":
continue
arg_list.append(str(k))
if v != "":
arg_list.append(str(v))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here we could also use a function

Comment on lines +470 to +490
endpoint_config = arguments.get("endpoint")
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
elif not isinstance(endpoint_config, dict):
logger.warning(
"Invalid endpoint configuration type '%s'; falling back to default endpoint for SITL.",
type(endpoint_config).__name__,
)
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 default_endpoint_args:
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)

master_endpoint = Endpoint(**master_endpoint_args.dict())
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be a function

@nicoschmdt
Copy link
Copy Markdown
Collaborator

It'd be nice to check the commit messages too, some of them are out of the standard we use in the project like "Implemented sourcery feedback..." and "Merge-branch". As a suggestion, git fixup is great for adding fixes to previous commits.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants