diff --git a/ardupilot_methodic_configurator/backend_flightcontroller_connection.py b/ardupilot_methodic_configurator/backend_flightcontroller_connection.py index 0f497195a..19c7672a0 100644 --- a/ardupilot_methodic_configurator/backend_flightcontroller_connection.py +++ b/ardupilot_methodic_configurator/backend_flightcontroller_connection.py @@ -30,6 +30,7 @@ from serial.tools.list_ports_common import ListPortInfo from ardupilot_methodic_configurator import _ +from ardupilot_methodic_configurator.backend_filesystem_vehicle_components import VehicleComponents from ardupilot_methodic_configurator.backend_flightcontroller_factory_mavlink import ( MavlinkConnectionFactory, SystemMavlinkConnectionFactory, @@ -688,6 +689,13 @@ def _extract_firmware_type_from_banner(self, banner_msgs: list[str], os_custom_v """ firmware_type = "" + known_firmware_types = VehicleComponents.supported_vehicles() + for message in banner_msgs: + parts = message.split(maxsplit=1) + first_word = parts[0] if parts else "" + if first_word in known_firmware_types: + return first_word + # Try to extract from message after ChibiOS version if os_custom_version_index is not None and os_custom_version_index + 1 < len(banner_msgs): firmware_type_banner_substrings = banner_msgs[os_custom_version_index + 1].split(" ") @@ -745,6 +753,9 @@ def _process_autopilot_version(self, m: MAVLink_autopilot_version_message | None # Extract firmware type from banner messages firmware_type = self._extract_firmware_type_from_banner(banner_msgs, os_custom_version_index) + if firmware_type and " or " in self.info.vehicle_type: # vehicle_type detection in HEARTBEAT message was ambiguous + self.info.vehicle_type = firmware_type # fallback to banner detected vehicle type + # Update firmware type if found and different from AUTOPILOT_VERSION if firmware_type and firmware_type != self.info.firmware_type: logging_debug( diff --git a/ardupilot_methodic_configurator/data_model_flightcontroller_info.py b/ardupilot_methodic_configurator/data_model_flightcontroller_info.py index a71a3d2b2..eda035bad 100644 --- a/ardupilot_methodic_configurator/data_model_flightcontroller_info.py +++ b/ardupilot_methodic_configurator/data_model_flightcontroller_info.py @@ -249,7 +249,7 @@ def __classify_vehicle_type(mav_type_int: int) -> str: mavutil.mavlink.MAV_TYPE_FLAPPING_WING: "ArduPlane", mavutil.mavlink.MAV_TYPE_KITE: "ArduPlane", mavutil.mavlink.MAV_TYPE_ONBOARD_CONTROLLER: "AP_Periph", - mavutil.mavlink.MAV_TYPE_VTOL_DUOROTOR: "ArduPlane", + mavutil.mavlink.MAV_TYPE_VTOL_DUOROTOR: "ArduPlane or ArduCopter", mavutil.mavlink.MAV_TYPE_VTOL_QUADROTOR: "ArduPlane", mavutil.mavlink.MAV_TYPE_VTOL_TILTROTOR: "ArduPlane", mavutil.mavlink.MAV_TYPE_VTOL_RESERVED2: "ArduPlane", diff --git a/ardupilot_methodic_configurator/plugins/data_model_motor_test.py b/ardupilot_methodic_configurator/plugins/data_model_motor_test.py index fa2a1920c..5de421eef 100644 --- a/ardupilot_methodic_configurator/plugins/data_model_motor_test.py +++ b/ardupilot_methodic_configurator/plugins/data_model_motor_test.py @@ -24,6 +24,7 @@ from ardupilot_methodic_configurator.backend_filesystem_json_with_schema import FilesystemJSONWithSchema from ardupilot_methodic_configurator.backend_filesystem_program_settings import ProgramSettings from ardupilot_methodic_configurator.backend_flightcontroller import FlightController +from ardupilot_methodic_configurator.data_model_vehicle_components_validation import FRAME_CLASS_DICT from ardupilot_methodic_configurator.plugins.data_model_battery_monitor import BatteryMonitorDataModel # pylint: disable=too-many-lines @@ -117,6 +118,7 @@ def __init__( self._test_order: list[int] = [] # default to empty self._motor_directions: list[str] = [] # default to empty self._frame_layout: dict[str, Any] = {} # default to empty + self._invalid_frame_type_error: str | None = None self._test_throttle_pct = 0.0 self._test_duration_s = 0.0 @@ -178,33 +180,19 @@ def _configure_frame_layout(self, frame_class: int, frame_type: int) -> None: # Update frame parameters self._frame_class = frame_class self._frame_type = frame_type - self._motor_count = 0 - self._frame_layout = {} + self._invalid_frame_type_error = None + has_frame_class_configuration = False + frame_type_is_available = False - # Find matching layout in motor data and populate motor arrays + # Check available layouts before loading the matching motor layout. if self._motor_data_loader.data and "layouts" in self._motor_data_loader.data: for layout in self._motor_data_loader.data["layouts"]: - if layout["Class"] == self._frame_class and layout["Type"] == self._frame_type and "motors" in layout: - self._frame_layout = layout - self._motor_count = len(layout["motors"]) - # Generate motor labels: A-Z for first 26, then AA, AB, AC... for motors 27-32 - self._motor_labels = [] - for i in range(self._motor_count): - if i < 26: - self._motor_labels.append(chr(ord("A") + i)) - else: - # For motors 27-32: AA, AB, AC, AD, AE, AF - self._motor_labels.append("A" + chr(ord("A") + (i - 26))) - self._motor_numbers = [0] * self._motor_count - self._test_order = [0] * self._motor_count - self._motor_directions = [""] * self._motor_count - for i, motor in enumerate(self._frame_layout.get("motors", [])): - test_order = motor.get("TestOrder") - if test_order and 1 <= test_order <= self._motor_count: - self._motor_numbers[test_order - 1] = motor.get("Number") - self._motor_directions[test_order - 1] = motor.get("Rotation") - self._test_order[i] = test_order - break + if layout.get("Class") == self._frame_class and layout.get("motors"): + has_frame_class_configuration = True + if layout.get("Class") == self._frame_class and layout.get("Type") == self._frame_type: + frame_type_is_available = True + + self._load_motor_layout() if self._motor_count == 0: if self._frame_class == 0: @@ -214,6 +202,16 @@ def _configure_frame_layout(self, frame_class: int, frame_type: int) -> None: self._frame_type, ) return + metadata_frame_types = self._get_frame_types_from_parameter_metadata(self._frame_class) + frame_type_is_available = frame_type_is_available or self._frame_type in metadata_frame_types + if frame_type_is_available: + return + if has_frame_class_configuration or metadata_frame_types: + self._invalid_frame_type_error = _( + "No motor configuration found for frame class %(class)d and type %(type)d; select a valid frame type" + ) % {"class": self._frame_class, "type": self._frame_type} + logging_error(self._invalid_frame_type_error) + return raise RuntimeError( _("No motor configuration found for frame class %(class)d and type %(type)d") % {"class": self._frame_class, "type": self._frame_type} @@ -228,6 +226,44 @@ def _configure_frame_layout(self, frame_class: int, frame_type: int) -> None: }, ) + def _load_motor_layout(self) -> None: + """Reset and populate motor data for the current frame class and type.""" + self._motor_count = 0 + self._motor_labels = [] + self._motor_numbers = [] + self._test_order = [] + self._motor_directions = [] + self._frame_layout = {} + + if not self._motor_data_loader.data or "layouts" not in self._motor_data_loader.data: + return + + for layout in self._motor_data_loader.data["layouts"]: + if layout.get("Class") != self._frame_class or layout.get("Type") != self._frame_type or "motors" not in layout: + continue + + self._frame_layout = layout + self._motor_count = len(layout["motors"]) + self._motor_labels = [ + chr(ord("A") + index) if index < 26 else "A" + chr(ord("A") + (index - 26)) + for index in range(self._motor_count) + ] + self._motor_numbers = [0] * self._motor_count + self._test_order = [0] * self._motor_count + self._motor_directions = [""] * self._motor_count + for index, motor in enumerate(layout["motors"]): + test_order = motor.get("TestOrder") + if test_order and 1 <= test_order <= self._motor_count: + self._motor_numbers[test_order - 1] = motor.get("Number") + self._motor_directions[test_order - 1] = motor.get("Rotation") + self._test_order[index] = test_order + return + + @property + def invalid_frame_type_error(self) -> str | None: + """Return the invalid frame type error detected during initialization, if any.""" + return self._invalid_frame_type_error + def _get_test_settings_from_disk(self) -> None: """Load test settings from disk.""" self._test_throttle_pct = self._get_test_throttle_pct() @@ -753,6 +789,8 @@ def get_motor_diagram_path(self) -> tuple[str, str]: or empty string if not available """ + if self._invalid_frame_type_error: + return "", "" return ProgramSettings.motor_diagram_filepath(self._frame_class, self._frame_type) def motor_diagram_exists(self) -> bool: @@ -763,7 +801,9 @@ def motor_diagram_exists(self) -> bool: bool: True if diagram exists, False otherwise """ - return ProgramSettings.motor_diagram_exists(self._frame_class, self._frame_type) + return bool( + not self._invalid_frame_type_error and ProgramSettings.motor_diagram_exists(self._frame_class, self._frame_type) + ) def _get_test_duration_s(self) -> float: """ @@ -902,16 +942,8 @@ def update_frame_configuration(self, frame_class: int, frame_type: int) -> None: # Update internal state self._frame_class = frame_class self._frame_type = frame_type - - # Recalculate motor count using motor data loader - self._motor_count = 0 - if self._motor_data_loader.data and "layouts" in self._motor_data_loader.data: - # Find a layout that matches the current frame class and type - for layout in self._motor_data_loader.data["layouts"]: - if layout.get("Class") == frame_class and layout.get("Type") == frame_type and "motors" in layout: - self._frame_layout = layout - self._motor_count = len(layout["motors"]) - break + self._invalid_frame_type_error = None + self._load_motor_layout() logging_info( _("Frame configuration updated: Class=%(class)d, Type=%(type)d, Motors=%(motors)d"), @@ -975,6 +1007,14 @@ def get_current_frame_class_types(self) -> dict[int, str]: ) return types_for_class + parameter_metadata_types = self._get_frame_types_from_parameter_metadata(frame_class_int) + if parameter_metadata_types: + logging_debug( + _("Found %(count)d frame types for current frame class %(class)d in parameter metadata"), + {"count": len(parameter_metadata_types), "class": frame_class_int}, + ) + return parameter_metadata_types + # Class number not found in motor data max_class = max(class_number_to_name.keys()) if class_number_to_name else 0 logging_warning( @@ -983,6 +1023,42 @@ def get_current_frame_class_types(self) -> dict[int, str]: ) return {} + def _get_frame_types_from_parameter_metadata(self, frame_class: int) -> dict[int, str]: + """Get frame types for a class from flight-controller parameter metadata.""" + doc_dict = getattr(self.filesystem, "doc_dict", None) + if not doc_dict: + return {} + + class_values = doc_dict.get("FRAME_CLASS", {}).get("values", {}) + class_name = None + for code, name in class_values.items(): + try: + if int(code) == frame_class: + class_name = str(name).strip().upper() + break + except (TypeError, ValueError): + continue + if not class_name: + class_name = FRAME_CLASS_DICT.get("ArduCopter", {}).get(frame_class, "").upper() + if not class_name: + return {} + + frame_type_values = doc_dict.get("FRAME_TYPE", {}).get("values", {}) + frame_types: dict[int, str] = {} + for code, name in frame_type_values.items(): + if ":" not in str(name): + continue + metadata_class_name, type_name = str(name).split(":", 1) + if metadata_class_name.strip().upper() != class_name: + continue + try: + frame_types[int(code)] = type_name.strip() + except (TypeError, ValueError): + continue + if not frame_types and class_name == "BICOPTER": + frame_types[0] = "PLUS" + return frame_types + def get_frame_options(self) -> dict[str, dict[int, str]]: # pylint: disable=too-many-branches """ Get all available frame configuration options. @@ -1193,18 +1269,8 @@ def update_frame_type_from_selection( # Update internal state and recalculate motor count self._frame_class = frame_class_code self._frame_type = frame_type_code - self._motor_count = 0 - if self._motor_data_loader.data and "layouts" in self._motor_data_loader.data: - # Find a layout that matches the current frame class and type - for layout in self._motor_data_loader.data["layouts"]: - if ( - layout.get("Class") == frame_class_code - and layout.get("Type") == frame_type_code - and "motors" in layout - ): - self._frame_layout = layout - self._motor_count = len(layout["motors"]) - break + self._invalid_frame_type_error = None + self._load_motor_layout() return True diff --git a/ardupilot_methodic_configurator/plugins/frontend_tkinter_motor_test.py b/ardupilot_methodic_configurator/plugins/frontend_tkinter_motor_test.py index be74674d5..27f0dcf4d 100644 --- a/ardupilot_methodic_configurator/plugins/frontend_tkinter_motor_test.py +++ b/ardupilot_methodic_configurator/plugins/frontend_tkinter_motor_test.py @@ -169,7 +169,8 @@ def _create_widgets(self) -> None: # pylint: disable=too-many-statements # noqa # Create PairTupleCombobox with frame type pairs frame_type_pairs = self.model.get_frame_type_pairs() - current_selection = self.model.get_current_frame_selection_key() if frame_type_pairs else None + current_selection_key = self.model.get_current_frame_selection_key() if frame_type_pairs else None + current_selection = current_selection_key if any(key == current_selection_key for key, _ in frame_type_pairs) else None self.frame_type_combobox = PairTupleCombobox( frame_type_frame, frame_type_pairs, current_selection, "Frame Type", state="readonly" @@ -524,6 +525,8 @@ def _on_frame_type_change(self, _event: object) -> None: # Update UI components self._update_motor_buttons_layout() + self._update_diagram() + self._diagram_needs_update = False except (ValidationError, ParameterError, FrameConfigurationError) as e: showerror(_("Parameter Update Error"), str(e)) @@ -762,6 +765,8 @@ def on_activate(self) -> None: # Refresh frame configuration when becoming active if not self.model.refresh_from_flight_controller(): logging_warning(_("Could not refresh frame configuration from flight controller")) + self._diagrams_path = "" + self._diagram_needs_update = True self._update_view() def on_deactivate(self) -> None: @@ -926,7 +931,10 @@ def _create_motor_test_view( def _create_motor_test_model(context: PluginModelContext) -> MotorTestDataModel: """Create the plugin data model from registered application dependencies.""" - return MotorTestDataModel(context.flight_controller, context.local_filesystem) + model = MotorTestDataModel(context.flight_controller, context.local_filesystem) + if model.invalid_frame_type_error: + showerror(_("Invalid Frame Type"), model.invalid_frame_type_error) + return model def register_motor_test_plugin() -> None: diff --git a/tests/plugins/test_data_model_motor_test.py b/tests/plugins/test_data_model_motor_test.py index cec214e80..56322fbdb 100755 --- a/tests/plugins/test_data_model_motor_test.py +++ b/tests/plugins/test_data_model_motor_test.py @@ -476,6 +476,9 @@ def test_user_can_update_frame_configuration_successfully(self, motor_test_model assert motor_test_model.frame_class == 2 assert motor_test_model.frame_type == 1 assert motor_test_model.motor_count == 6 # HEXA X has 6 motors + assert motor_test_model.motor_labels == ["A", "B", "C", "D", "E", "F"] + assert len(motor_test_model.motor_numbers) == 6 + assert len(motor_test_model.motor_directions) == 6 def test_user_can_update_frame_configuration_and_motor_count(self, motor_test_model) -> None: """ @@ -1696,6 +1699,57 @@ def test_model_raises_error_with_empty_layouts(self, mock_flight_controller, moc with pytest.raises(RuntimeError, match="No motor configuration found for frame class 1 and type 1"): MotorTestDataModel(mock_flight_controller, mock_filesystem) + def test_model_allows_correcting_invalid_frame_type(self, motor_test_model) -> None: + """ + Model remains usable when the controller reports an unsupported type. + + GIVEN: A frame class has a valid motor layout, but the controller reports an unsupported type + WHEN: The model configures the reported frame + THEN: It should keep the reported type and allow the user to select a valid type + """ + motor_test_model._motor_data_loader.data = { + "layouts": [ + { + "Class": 1, + "Type": 0, + "motors": [{"Number": 1, "TestOrder": 1, "Rotation": "CW"}], + } + ] + } + motor_test_model.filesystem.doc_dict["FRAME_CLASS"]["values"]["10"] = "BI" + motor_test_model.filesystem.doc_dict["FRAME_TYPE"]["values"]["0"] = "BI: PLUS" + motor_test_model.flight_controller.fc_parameters["FRAME_CLASS"] = 10 + + motor_test_model._configure_frame_layout(frame_class=10, frame_type=1) + + assert motor_test_model.frame_class == 10 + assert motor_test_model.frame_type == 1 + assert motor_test_model.motor_count == 0 + assert motor_test_model.motor_directions == [] + assert motor_test_model.invalid_frame_type_error == ( + "No motor configuration found for frame class 10 and type 1; select a valid frame type" + ) + assert motor_test_model.get_frame_type_pairs() == [("0", "0: PLUS")] + + def test_model_uses_metadata_when_motor_layout_data_is_unavailable(self, motor_test_model) -> None: + """Metadata keeps recovery available when the motor-layout catalog cannot be loaded.""" + motor_test_model._motor_data_loader.data = {"layouts": []} + motor_test_model.filesystem.doc_dict["FRAME_CLASS"]["values"]["10"] = "BI" + motor_test_model.filesystem.doc_dict["FRAME_TYPE"]["values"]["0"] = "BI: PLUS" + + motor_test_model._configure_frame_layout(frame_class=10, frame_type=0) + + assert motor_test_model.motor_count == 0 + assert motor_test_model.invalid_frame_type_error is None + + def test_metadata_frame_types_skip_invalid_class_codes(self, motor_test_model) -> None: + """Malformed class metadata entries do not prevent valid recovery options.""" + motor_test_model.filesystem.doc_dict["FRAME_CLASS"]["values"]["not-a-code"] = "BROKEN" + motor_test_model.filesystem.doc_dict["FRAME_CLASS"]["values"]["10"] = "BI" + motor_test_model.filesystem.doc_dict["FRAME_TYPE"]["values"]["0"] = "BI: PLUS" + + assert motor_test_model._get_frame_types_from_parameter_metadata(10) == {0: "PLUS"} + def test_model_handles_json_loading_failure_gracefully(self, mock_flight_controller, mock_filesystem) -> None: """ Model raises RuntimeError when JSON loading fails. @@ -2483,6 +2537,9 @@ def test_user_updates_frame_type_from_dropdown_text(self, motor_test_model) -> N spy.assert_called() assert motor_test_model.frame_type == 0 + assert motor_test_model.motor_labels == ["A", "B", "C", "D"] + assert motor_test_model.motor_numbers == [3, 1, 4, 2] + assert motor_test_model.motor_directions == ["CW", "CCW", "CW", "CCW"] def test_user_updates_frame_type_using_combobox_key(self, motor_test_model) -> None: """ @@ -3171,13 +3228,13 @@ def test_user_can_select_dotriaconta_frame_class_with_class_15(self, motor_test_ assert frame_types[0] == "PLUS" assert frame_types[1] == "DOTRIACONTA/X" - def test_user_receives_error_when_selecting_undefined_frame_class(self, motor_test_model) -> None: + def test_user_can_correct_frame_type_for_known_class_without_motor_layout(self, motor_test_model) -> None: """ - User selecting undefined frame class sees helpful error. + User can correct a frame type for a known class without a motor layout. GIVEN: Motor data with classes 1, 2, 5, 15 (non-sequential) - WHEN: Flight controller reports FRAME_CLASS=10 (not defined) - THEN: Warning logged showing max defined class and empty dict returned + WHEN: Flight controller reports FRAME_CLASS=10 (BiCopter) + THEN: The valid type 0 option is available for correction """ motor_test_model._motor_data_loader.data = { "layouts": [ @@ -3191,7 +3248,7 @@ def test_user_receives_error_when_selecting_undefined_frame_class(self, motor_te frame_types = motor_test_model.get_current_frame_class_types() - assert frame_types == {} + assert frame_types == {0: "PLUS"} def test_dotriaconta_32_motors_configuration_loads_correctly(self, motor_test_model) -> None: """ diff --git a/tests/test_backend_flightcontroller_connection.py b/tests/test_backend_flightcontroller_connection.py index 3155ff66b..9140dd388 100755 --- a/tests/test_backend_flightcontroller_connection.py +++ b/tests/test_backend_flightcontroller_connection.py @@ -1310,6 +1310,21 @@ def test_select_supported_autopilot_returns_empty_string_on_success(self) -> Non assert result == "" assert connection.info.is_supported + def test_select_supported_autopilot_logs_vehicle_type_before_firmware_is_known(self, caplog) -> None: + """Heartbeat classification logs vehicle type before AUTOPILOT_VERSION is processed.""" + connection = FlightControllerConnection(info=FlightControllerInfo()) + mock_heartbeat = Mock() + mock_heartbeat.autopilot = mavutil.mavlink.MAV_AUTOPILOT_ARDUPILOTMEGA + mock_heartbeat.type = mavutil.mavlink.MAV_TYPE_FIXED_WING + + with caplog.at_level("INFO"): + result = connection._select_supported_autopilot({(1, 1): mock_heartbeat}) + + assert result == "" + assert connection.info.vehicle_type == "ArduPlane" + assert "Vehicle type: Fixed wing aircraft." in caplog.text + assert "running ArduPlane firmware" in caplog.text + def test_select_supported_autopilot_returns_error_when_none_supported(self) -> None: """ _select_supported_autopilot returns an error when no autopilot is supported. @@ -1798,6 +1813,58 @@ def test_process_autopilot_version_firmware_mismatch_uses_banner_value(self) -> # Should have logged a debug message about the mismatch mock_debug.assert_called() + def test_process_autopilot_version_resolves_ambiguous_vtol_vehicle_from_banner(self) -> None: + """An ambiguous VTOL MAV type uses the firmware name from the banner.""" + connection = FlightControllerConnection(info=FlightControllerInfo()) + connection.info.vehicle_type = "ArduPlane or ArduCopter" + + mock_m = Mock() + mock_m.capabilities = 0 + mock_m.flight_sw_version = 0x040700FF + mock_m.vendor_id = 0 + mock_m.product_id = 0 + mock_m.board_version = 0 + mock_m.flight_custom_version = [0] * 8 + mock_m.os_custom_version = [0] * 8 + + result = connection._process_autopilot_version( + mock_m, + [ + "ArduCopter V4.7.0 (1511f271)", + "ChibiOS: 4f34e217", + "CubeBlack 00460038 30365109 31353833", + ], + ) + + assert result == "" + assert connection.info.vehicle_type == "ArduCopter" + assert connection.info.firmware_type == "ArduCopter" + + def test_process_autopilot_version_keeps_ambiguous_vtol_without_banner_firmware(self) -> None: + """ + An ambiguous VTOL type remains available when no firmware banner is received. + + GIVEN: An ambiguous VTOL MAV type and no banner firmware name + WHEN: AUTOPILOT_VERSION is processed + THEN: The ambiguous vehicle type should not be replaced with an empty value + """ + connection = FlightControllerConnection(info=FlightControllerInfo()) + connection.info.vehicle_type = "ArduPlane or ArduCopter" + + mock_m = Mock() + mock_m.capabilities = 0 + mock_m.flight_sw_version = 0x040700FF + mock_m.vendor_id = 0 + mock_m.product_id = 0 + mock_m.board_version = 0 + mock_m.flight_custom_version = [0] * 8 + mock_m.os_custom_version = [0] * 8 + + result = connection._process_autopilot_version(mock_m, []) + + assert result == "" + assert connection.info.vehicle_type == "ArduPlane or ArduCopter" + def test_process_autopilot_version_returns_empty_without_mismatch(self) -> None: """ _process_autopilot_version returns empty string when successfully processed. diff --git a/tests/test_data_model_flightcontroller_info.py b/tests/test_data_model_flightcontroller_info.py index fbea5df90..e3d52b18b 100755 --- a/tests/test_data_model_flightcontroller_info.py +++ b/tests/test_data_model_flightcontroller_info.py @@ -442,7 +442,7 @@ def test_classify_vehicle_type_comprehensive(self) -> None: (mavutil.mavlink.MAV_TYPE_HEXAROTOR, "ArduCopter"), (mavutil.mavlink.MAV_TYPE_OCTOROTOR, "ArduCopter"), (mavutil.mavlink.MAV_TYPE_TRICOPTER, "ArduCopter"), - (mavutil.mavlink.MAV_TYPE_VTOL_DUOROTOR, "ArduPlane"), + (mavutil.mavlink.MAV_TYPE_VTOL_DUOROTOR, "ArduPlane or ArduCopter"), (mavutil.mavlink.MAV_TYPE_VTOL_QUADROTOR, "ArduPlane"), ]: vehicle_type = FlightControllerInfo._FlightControllerInfo__classify_vehicle_type(mav_type)