Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,16 @@
"autoimport_nondefault_regexp": ["BRD_BOOT_DELAY", "GPS.*", "GNSS.*"],
"derived_parameters": {
"GPS_TYPE": { "if": "Version(vehicle_components['Flight Controller']['Firmware']['Version'].split(' ')[0]) < Version('4.6')", "New Value": "vehicle_components['GNSS Receiver']['FC Connection']['Protocol']", "Change Reason": "Defined in component editor" },
"GPS1_TYPE": { "if": "Version(vehicle_components['Flight Controller']['Firmware']['Version'].split(' ')[0]) >= Version('4.6')","New Value": "vehicle_components['GNSS Receiver']['FC Connection']['Protocol']", "Change Reason": "Defined in component editor" }
"GPS1_TYPE": { "if": "Version(vehicle_components['Flight Controller']['Firmware']['Version'].split(' ')[0]) >= Version('4.6')","New Value": "vehicle_components['GNSS Receiver']['FC Connection']['Protocol']", "Change Reason": "Defined in component editor" },
"SERIAL1_PROTOCOL": { "if": "vehicle_components['GNSS Receiver']['FC Connection']['Type'] == 'SERIAL1' and vehicle_components['GNSS Receiver']['FC Connection']['Protocol'] in ['AUTO', 'uBlox', 'NMEA', 'SiRF', 'SwiftNav', 'Septentrio(SBF)', 'Trimble(GSOF)', 'ERB', 'NOVA', 'HemisphereNMEA', 'uBlox-MovingBaseline-Base', 'uBlox-MovingBaseline-Rover', 'AllyStar', 'UnicoreNMEA', 'UnicoreMovingBaselineNMEA', 'Septentrio-DualAntenna(SBF)']", "New Value": "5", "Change Reason": "Physical GNSS receiver requires GPS serial transport" },
"SERIAL2_PROTOCOL": { "if": "vehicle_components['GNSS Receiver']['FC Connection']['Type'] == 'SERIAL2' and vehicle_components['GNSS Receiver']['FC Connection']['Protocol'] in ['AUTO', 'uBlox', 'NMEA', 'SiRF', 'SwiftNav', 'Septentrio(SBF)', 'Trimble(GSOF)', 'ERB', 'NOVA', 'HemisphereNMEA', 'uBlox-MovingBaseline-Base', 'uBlox-MovingBaseline-Rover', 'AllyStar', 'UnicoreNMEA', 'UnicoreMovingBaselineNMEA', 'Septentrio-DualAntenna(SBF)']", "New Value": "5", "Change Reason": "Physical GNSS receiver requires GPS serial transport" },
"SERIAL3_PROTOCOL": { "if": "vehicle_components['GNSS Receiver']['FC Connection']['Type'] == 'SERIAL3' and vehicle_components['GNSS Receiver']['FC Connection']['Protocol'] in ['AUTO', 'uBlox', 'NMEA', 'SiRF', 'SwiftNav', 'Septentrio(SBF)', 'Trimble(GSOF)', 'ERB', 'NOVA', 'HemisphereNMEA', 'uBlox-MovingBaseline-Base', 'uBlox-MovingBaseline-Rover', 'AllyStar', 'UnicoreNMEA', 'UnicoreMovingBaselineNMEA', 'Septentrio-DualAntenna(SBF)']", "New Value": "5", "Change Reason": "Physical GNSS receiver requires GPS serial transport" },
"SERIAL4_PROTOCOL": { "if": "vehicle_components['GNSS Receiver']['FC Connection']['Type'] == 'SERIAL4' and vehicle_components['GNSS Receiver']['FC Connection']['Protocol'] in ['AUTO', 'uBlox', 'NMEA', 'SiRF', 'SwiftNav', 'Septentrio(SBF)', 'Trimble(GSOF)', 'ERB', 'NOVA', 'HemisphereNMEA', 'uBlox-MovingBaseline-Base', 'uBlox-MovingBaseline-Rover', 'AllyStar', 'UnicoreNMEA', 'UnicoreMovingBaselineNMEA', 'Septentrio-DualAntenna(SBF)']", "New Value": "5", "Change Reason": "Physical GNSS receiver requires GPS serial transport" },
"SERIAL5_PROTOCOL": { "if": "vehicle_components['GNSS Receiver']['FC Connection']['Type'] == 'SERIAL5' and vehicle_components['GNSS Receiver']['FC Connection']['Protocol'] in ['AUTO', 'uBlox', 'NMEA', 'SiRF', 'SwiftNav', 'Septentrio(SBF)', 'Trimble(GSOF)', 'ERB', 'NOVA', 'HemisphereNMEA', 'uBlox-MovingBaseline-Base', 'uBlox-MovingBaseline-Rover', 'AllyStar', 'UnicoreNMEA', 'UnicoreMovingBaselineNMEA', 'Septentrio-DualAntenna(SBF)']", "New Value": "5", "Change Reason": "Physical GNSS receiver requires GPS serial transport" },
"SERIAL6_PROTOCOL": { "if": "vehicle_components['GNSS Receiver']['FC Connection']['Type'] == 'SERIAL6' and vehicle_components['GNSS Receiver']['FC Connection']['Protocol'] in ['AUTO', 'uBlox', 'NMEA', 'SiRF', 'SwiftNav', 'Septentrio(SBF)', 'Trimble(GSOF)', 'ERB', 'NOVA', 'HemisphereNMEA', 'uBlox-MovingBaseline-Base', 'uBlox-MovingBaseline-Rover', 'AllyStar', 'UnicoreNMEA', 'UnicoreMovingBaselineNMEA', 'Septentrio-DualAntenna(SBF)']", "New Value": "5", "Change Reason": "Physical GNSS receiver requires GPS serial transport" },
"SERIAL7_PROTOCOL": { "if": "vehicle_components['GNSS Receiver']['FC Connection']['Type'] == 'SERIAL7' and vehicle_components['GNSS Receiver']['FC Connection']['Protocol'] in ['AUTO', 'uBlox', 'NMEA', 'SiRF', 'SwiftNav', 'Septentrio(SBF)', 'Trimble(GSOF)', 'ERB', 'NOVA', 'HemisphereNMEA', 'uBlox-MovingBaseline-Base', 'uBlox-MovingBaseline-Rover', 'AllyStar', 'UnicoreNMEA', 'UnicoreMovingBaselineNMEA', 'Septentrio-DualAntenna(SBF)']", "New Value": "5", "Change Reason": "Physical GNSS receiver requires GPS serial transport" },
"SERIAL8_PROTOCOL": { "if": "vehicle_components['GNSS Receiver']['FC Connection']['Type'] == 'SERIAL8' and vehicle_components['GNSS Receiver']['FC Connection']['Protocol'] in ['AUTO', 'uBlox', 'NMEA', 'SiRF', 'SwiftNav', 'Septentrio(SBF)', 'Trimble(GSOF)', 'ERB', 'NOVA', 'HemisphereNMEA', 'uBlox-MovingBaseline-Base', 'uBlox-MovingBaseline-Rover', 'AllyStar', 'UnicoreNMEA', 'UnicoreMovingBaselineNMEA', 'Septentrio-DualAntenna(SBF)']", "New Value": "5", "Change Reason": "Physical GNSS receiver requires GPS serial transport" },
"SERIAL9_PROTOCOL": { "if": "vehicle_components['GNSS Receiver']['FC Connection']['Type'] == 'SERIAL9' and vehicle_components['GNSS Receiver']['FC Connection']['Protocol'] in ['AUTO', 'uBlox', 'NMEA', 'SiRF', 'SwiftNav', 'Septentrio(SBF)', 'Trimble(GSOF)', 'ERB', 'NOVA', 'HemisphereNMEA', 'uBlox-MovingBaseline-Base', 'uBlox-MovingBaseline-Rover', 'AllyStar', 'UnicoreNMEA', 'UnicoreMovingBaselineNMEA', 'Septentrio-DualAntenna(SBF)']", "New Value": "5", "Change Reason": "Physical GNSS receiver requires GPS serial transport" }
},
"rename_connection": "vehicle_components['GNSS Receiver']['FC Connection']['Type']",
"old_filenames": [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ def configuration_steps_strings() -> None: # noqa: PLR0915 # pylint: disable=to
_config_steps_strings = _("PID notch filters require batch logging, not raw logging")
_config_steps_strings = _("PID notch filters require batch pre- and post- filters logging")
_config_steps_strings = _("Perform all arming checks. If you have a problem fix its source. Do NOT change this")
_config_steps_strings = _("Physical GNSS receiver requires GPS serial transport")
_config_steps_strings = _("Pitch")
_config_steps_strings = _("Pitch and Roll")
_config_steps_strings = _("Q_M_SPIN_ARM plus at least 0.03")
Expand Down
12 changes: 8 additions & 4 deletions ardupilot_methodic_configurator/data_model_parameter_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@
from ardupilot_methodic_configurator.plugins.plugin_factory import PluginModelContext, plugin_factory
from ardupilot_methodic_configurator.tempcal_imu import IMUfit

# GNSS MAVLink protocol default value (ArduPilot GPS_TYPE value 5 = MAVLink GPS)
_GNSS_MAVLINK_PROTOCOL_VALUE = "5"
# ArduPilot SERIALn_PROTOCOL value 5 selects GPS serial transport, not a GPS1_TYPE driver.
_GNSS_SERIAL_GPS_PROTOCOL_VALUE = "5"
# The rename_connection expression that identifies a GNSS-via-MAVLink step
_GNSS_FC_CONNECTION_RENAME_EXPR = "vehicle_components['GNSS Receiver']['FC Connection']['Type']"

Expand Down Expand Up @@ -2806,9 +2806,13 @@ def refresh_current_step_computed_parameters(self) -> None:
self._refresh_current_step_connection_renames(step_info, variables)

def _apply_gnss_protocol_value(self, target_name: str) -> None:
"""Set the GNSS MAVLink protocol value on a newly renamed _PROTOCOL parameter, ignoring no-op updates."""
"""Apply the legacy GNSS serial value only when no step rule owns the destination."""
parameter = self.current_step_parameters[target_name]
step_info = self._local_filesystem.configuration_steps.get(self.current_file, {})
if parameter.is_forced or parameter.is_derived or target_name in step_info.get("derived_parameters", {}):
return
with contextlib.suppress(ParameterUnchangedError):
self.current_step_parameters[target_name].set_new_value(_GNSS_MAVLINK_PROTOCOL_VALUE, ignore_out_of_range=True)
parameter.set_new_value(_GNSS_SERIAL_GPS_PROTOCOL_VALUE, ignore_out_of_range=True)

def _refresh_current_step_connection_renames(self, step_info: dict, variables: dict) -> None: # pylint: disable=too-many-branches
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,3 @@ GPS1_POS_X,0
GPS1_POS_Y,0
GPS1_POS_Z,0
GPS1_TYPE,1 # Defined in component editor
SERIAL3_PROTOCOL,5
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,3 @@ GPS_POS1_X,0
GPS_POS1_Y,0
GPS_POS1_Z,0
GPS_TYPE,1 # Defined in component editor
SERIAL3_PROTOCOL,5
156 changes: 156 additions & 0 deletions tests/test_arduplane_gnss_serial_default.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
#!/usr/bin/env python3

"""
Regression tests for ArduPlane GNSS serial transport selection.

This file is part of ArduPilot Methodic Configurator. https://github.com/ArduPilot/MethodicConfigurator

SPDX-FileCopyrightText: 2026 ArduPilot Methodic Configurator contributors

SPDX-License-Identifier: GPL-3.0-or-later
"""

import json
from pathlib import Path
from unittest.mock import MagicMock

import pytest

from ardupilot_methodic_configurator.backend_filesystem_configuration_steps import ConfigurationSteps
from ardupilot_methodic_configurator.data_model_configuration_step import ConfigurationStepProcessor
from ardupilot_methodic_configurator.data_model_par_dict import Par, ParDict
from ardupilot_methodic_configurator.data_model_parameter_editor import ParameterEditor

ROOT = Path(__file__).resolve().parents[1] / "ardupilot_methodic_configurator"
STEP = "18_gnss.param"
TEMPLATES = ("empty_4.7.x", "normal_plane")
GPS_DRIVERS = {
"None": 0,
"AUTO": 1,
"uBlox": 2,
"NMEA": 5,
"HIL": 7,
"DroneCAN": 9,
"MAVLink": 14,
"MSP": 19,
"ExternalAHRS": 21,
}


def gnss_filesystem(connection: str, driver: str, version: str, saved: ParDict) -> MagicMock:
"""Use the real Plane step calculations with a project and component selection."""
step = json.loads((ROOT / "configuration_steps_ArduPlane.json").read_text(encoding="utf-8"))["steps"][STEP]
calculations = ConfigurationSteps("", "ArduPlane")
filesystem = MagicMock()
filesystem.file_parameters = {STEP: saved}
filesystem.configuration_steps = {STEP: step}
filesystem.forced_parameters = calculations.forced_parameters
filesystem.derived_parameters = calculations.derived_parameters
filesystem.add_parameters = calculations.add_parameters
filesystem.param_default_dict = {"GPS1_GNSS_MODE": Par(0), "GPS1_TYPE": Par(1)}
filesystem.doc_dict = {
"GPS_TYPE": {"values": {str(number): name for name, number in GPS_DRIVERS.items()}},
"GPS1_TYPE": {"values": {str(number): name for name, number in GPS_DRIVERS.items()}},
}
filesystem.get_eval_variables.return_value = {
"vehicle_components": {
"Flight Controller": {"Firmware": {"Version": version}},
"GNSS Receiver": {"FC Connection": {"Type": connection, "Protocol": driver}},
},
"doc_dict": filesystem.doc_dict,
}
filesystem.compute_forced_and_derived_parameters.side_effect = calculations.compute_forced_and_derived_parameters
filesystem.compute_parameters.side_effect = calculations.compute_parameters
filesystem.compute_deletions.side_effect = calculations.compute_deletions
filesystem.compute_add_parameters.side_effect = calculations.compute_add_parameters
return filesystem


@pytest.mark.parametrize("template", TEMPLATES)
def test_generic_plane_gnss_template_does_not_choose_serial3(template: str) -> None:
"""The Plane templates keep all other GNSS settings but no UART default."""
parameters = ParDict.from_file(str(ROOT / "vehicle_templates" / "ArduPlane" / template / STEP))
assert "SERIAL3_PROTOCOL" not in parameters
assert "BRD_SAFETY_DEFLT" in parameters
assert {name for name in parameters if name.startswith("SERIAL")} == set()


@pytest.mark.parametrize("template_case", [("empty_4.7.x", "4.7.2", "GPS1_TYPE"), ("normal_plane", "4.5.7", "GPS_TYPE")])
@pytest.mark.parametrize(
"gnss_case",
[
("None", "None", None),
("SERIAL3", "AUTO", "SERIAL3_PROTOCOL"),
("SERIAL2", "uBlox", "SERIAL2_PROTOCOL"),
("SERIAL4", "NMEA", "SERIAL4_PROTOCOL"),
("CAN1", "DroneCAN", None),
("SERIAL3", "MAVLink", None),
("SERIAL3", "MSP", None),
("SERIAL3", "HIL", None),
("SERIAL3", "ExternalAHRS", None),
],
)
def test_gnss_step_derives_driver_and_only_physical_gps_serial_transport(
template_case: tuple[str, str, str], gnss_case: tuple[str, str, str | None]
) -> None:
"""Physical serial GNSS gets transport 5 while other positioning paths do not."""
template, version, gps_parameter = template_case
saved = ParDict.from_file(str(ROOT / "vehicle_templates" / "ArduPlane" / template / STEP))
filesystem = gnss_filesystem(gnss_case[0], gnss_case[1], version, saved)
_parameters, errors, _infos, _duplicates, _renames, derived, _imported = ConfigurationStepProcessor(
filesystem
).process_configuration_step(STEP, {})

assert not errors
assert derived[gps_parameter].value == GPS_DRIVERS[gnss_case[1]]
serial = {name: param.value for name, param in derived.items() if name.startswith("SERIAL")}
assert serial == ({gnss_case[2]: 5} if gnss_case[2] else {})


def test_saved_serial_setting_and_eligible_fc_import_are_preserved() -> None:
"""Saved port settings survive and the existing GNSS import rule still applies."""
saved = ParDict({"SERIAL2_PROTOCOL": Par(5, "saved GPS port"), "SERIAL2_BAUD": Par(115, "saved baud")})
filesystem = gnss_filesystem("SERIAL2", "uBlox", "4.7.2", saved)
parameters, errors, _infos, duplicates, renames, derived, imported = ConfigurationStepProcessor(
filesystem
).process_configuration_step(
STEP, {"SERIAL2_PROTOCOL": 5, "SERIAL2_BAUD": 115, "GPS1_GNSS_MODE": 3, "SERIAL3_PROTOCOL": 2}
)

assert not errors
assert not duplicates
assert not renames
assert parameters["SERIAL2_PROTOCOL"].get_new_value() == 5
assert parameters["SERIAL2_BAUD"].get_new_value() == 115
assert parameters["SERIAL2_BAUD"].change_reason == "saved baud"
assert derived["SERIAL2_PROTOCOL"].value == 5
assert imported == {"GPS1_GNSS_MODE"}
assert parameters["GPS1_GNSS_MODE"].get_new_value() == 3
assert "SERIAL3_PROTOCOL" not in parameters


def test_existing_gnss_connection_rename_moves_saved_baud() -> None:
"""A project baud setting follows the selected port through existing rename logic."""
filesystem = gnss_filesystem("SERIAL4", "uBlox", "4.7.2", ParDict({"SERIAL3_BAUD": Par(115)}))
_parameters, errors, _infos, _duplicates, renames, derived, _imported = ConfigurationStepProcessor(
filesystem
).process_configuration_step(STEP, {})

assert not errors
assert renames == [("SERIAL3_BAUD", "SERIAL4_BAUD")]
assert derived["SERIAL4_PROTOCOL"].value == 5


def test_editor_displays_derived_gnss_serial_protocol() -> None:
"""A newly selected physical UART protocol appears in the editor table model."""
saved = ParDict.from_file(str(ROOT / "vehicle_templates" / "ArduPlane" / "empty_4.7.x" / STEP))
filesystem = gnss_filesystem("SERIAL2", "uBlox", "4.7.2", saved)
flight_controller = MagicMock()
flight_controller.fc_parameters = {}
editor = ParameterEditor(STEP, flight_controller, filesystem)

errors, _infos = editor._repopulate_configuration_step_parameters() # pylint: disable=protected-access

assert not errors
assert editor.current_step_parameters["SERIAL2_PROTOCOL"].get_new_value() == 5
assert "SERIAL2_PROTOCOL" in editor._added_parameters # pylint: disable=protected-access
Loading
Loading