Skip to content

Commit 91d16c3

Browse files
committed
feat(component editor): Import FRAME_CLASS from FC parameters into Frame.Specifications.Class
use a new FRAME_CLASS_DICT lookup for validating telemetry connection types.
1 parent 215dab1 commit 91d16c3

6 files changed

Lines changed: 148 additions & 0 deletions

ardupilot_methodic_configurator/data_model_vehicle_components_base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ def update_json_structure(
262262
"Specifications": {
263263
"TOW min Kg": 1,
264264
"TOW max Kg": 1,
265+
"Class": "Undefined",
265266
}
266267
},
267268
"Flight Controller": {

ardupilot_methodic_configurator/data_model_vehicle_components_import.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from typing import Any, Optional
1818

1919
from ardupilot_methodic_configurator import _
20+
from ardupilot_methodic_configurator.backend_flightcontroller_business_logic import get_frame_info
2021
from ardupilot_methodic_configurator.battery_cell_voltages import (
2122
BATTERY_CELL_VOLTAGE_TYPES,
2223
BATTERY_DEFAULT_CHEMISTRY,
@@ -36,6 +37,7 @@
3637
SERVO_FUNCTION_ESC_CONTROL,
3738
ComponentDataModelValidation,
3839
get_esc_connection_sub_dict,
40+
get_frame_class_sub_dict,
3941
)
4042

4143

@@ -183,6 +185,14 @@ def process_fc_parameters(
183185
self._verify_dict_is_uptodate(doc, get_esc_connection_sub_dict(fw_type), "MOT_PWM_TYPE", "values")
184186
self._verify_dict_is_uptodate(doc, RC_PROTOCOLS_DICT, "RC_PROTOCOLS", "Bitmask")
185187

188+
# Process frame information if FRAME_CLASS is present in FC parameters
189+
if "FRAME_CLASS" in fc_parameters:
190+
frame_class, _ = get_frame_info(fc_parameters)
191+
self.set_component_value(
192+
("Frame", "Specifications", "Class"),
193+
get_frame_class_sub_dict(fw_type).get(frame_class, "Undefined"),
194+
)
195+
186196
# Process parameters in sequence
187197
self._set_gnss_type_from_fc_parameters(fc_parameters)
188198
esc_is_serial = self._set_serial_type_from_fc_parameters(fc_parameters)

ardupilot_methodic_configurator/data_model_vehicle_components_validation.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,46 @@ def get_esc_connection_sub_dict(
316316
"65536": {"type": RC_PORTS + SERIAL_PORTS, "protocol": "MAVRadio"}, # Bit 16
317317
}
318318

319+
FRAME_CLASS_DICT: dict[str, dict[int, str]] = {
320+
"ArduCopter": {
321+
0: "Undefined",
322+
1: "Quad",
323+
2: "Hexa",
324+
3: "Octa",
325+
4: "OctaQuad",
326+
5: "Y6",
327+
6: "Heli",
328+
7: "Tri",
329+
8: "SingleCopter",
330+
9: "CoaxCopter",
331+
10: "BiCopter",
332+
11: "Heli_Dual",
333+
12: "DodecaHexa",
334+
13: "HeliQuad",
335+
14: "Deca",
336+
15: "Scripting Matrix",
337+
16: "6DoF Scripting",
338+
17: "Dynamic Scripting Matrix",
339+
},
340+
"Heli": {
341+
6: "Heli",
342+
11: "Heli_Dual",
343+
13: "HeliQuad",
344+
},
345+
"Rover": {
346+
0: "Undefined",
347+
1: "Rover",
348+
2: "Boat",
349+
3: "BalanceBot",
350+
},
351+
"ArduPlane": {},
352+
}
353+
354+
355+
def get_frame_class_sub_dict(vehicle_type: str) -> dict[int, str]:
356+
"""Return the vehicle-type-specific frame class mapping from FRAME_CLASS_DICT."""
357+
return FRAME_CLASS_DICT.get(vehicle_type, FRAME_CLASS_DICT["ArduCopter"])
358+
319359

320360
class ComponentDataModelValidation(ComponentDataModelBase):
321361
"""

ardupilot_methodic_configurator/vehicle_components_schema.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@
199199
"TOW max Kg": {
200200
"type": "number",
201201
"description": "Maximum take-off weight in kilograms"
202+
},
203+
"Class": {
204+
"type": "string",
205+
"description": "Frame class/category (e.g., Quad, Hexa, Octa, etc.)"
202206
}
203207
},
204208
"description": "Technical specifications of the vehicle frame"

tests/test_data_model_vehicle_components_import.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,17 @@ def test_system_imports_esc_aio_configuration(self, realistic_model) -> None:
309309
assert esc_type == "AIO"
310310
assert esc_protocol == "DShot600"
311311

312+
def test_user_can_import_frame_class_from_fc(self, realistic_model) -> None:
313+
"""Given FRAME_CLASS from FC, set Frame.Specifications.Class."""
314+
fc_parameters = {"FRAME_CLASS": 3, "MOT_PWM_TYPE": 6, "SERVO1_FUNCTION": 0}
315+
doc = {"MOT_PWM_TYPE": {"values": {"6": "DShot600"}}}
316+
317+
with patch.object(realistic_model, "_verify_dict_is_uptodate", return_value=True):
318+
realistic_model.process_fc_parameters(fc_parameters, doc)
319+
320+
frame_class = realistic_model.get_component_value(("Frame", "Specifications", "Class"))
321+
assert frame_class == "Octa"
322+
312323
def test_user_can_import_esc_connection_and_telemetry_from_serial_fc(self, realistic_model) -> None:
313324
"""
314325
Import ESC serial config into FC->ESC Connection and ESC->FC Telemetry.

tests/unit_data_model_vehicle_components_validation_constants.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
ESC_CONNECTION_DICT,
2222
ESC_SERIAL_SAME_PORT_PROTOCOLS,
2323
FC_CONNECTION_TYPE_PATHS,
24+
FRAME_CLASS_DICT,
2425
GNSS_RECEIVER_CONNECTION,
2526
I2C_PORTS,
2627
OTHER_PORTS,
@@ -33,6 +34,7 @@
3334
SERIAL_PROTOCOLS_DICT,
3435
SPI_PORTS,
3536
get_connection_type_tuples_with_labels,
37+
get_frame_class_sub_dict,
3638
)
3739

3840

@@ -556,3 +558,83 @@ def test_esc_serial_same_port_protocols_contents(self) -> None:
556558
assert protocol in vehicle_protocols, (
557559
f"Protocol '{protocol}' from ESC_SERIAL_SAME_PORT_PROTOCOLS not found in ESC_CONNECTION_DICT['{vtype}']"
558560
)
561+
562+
563+
class TestFrameClassDict:
564+
"""Tests for FRAME_CLASS_DICT and get_frame_class_sub_dict."""
565+
566+
def test_frame_class_dict_structure(self) -> None:
567+
"""FRAME_CLASS_DICT is keyed by vehicle type with int->str sub-dicts."""
568+
assert isinstance(FRAME_CLASS_DICT, dict)
569+
assert len(FRAME_CLASS_DICT) > 0
570+
571+
for vtype, sub_dict in FRAME_CLASS_DICT.items():
572+
assert isinstance(vtype, str), f"Vehicle type key '{vtype}' is not a string"
573+
assert isinstance(sub_dict, dict), f"Sub-dict for '{vtype}' is not a dict"
574+
for key, value in sub_dict.items():
575+
assert isinstance(key, int), f"Frame class key '{key}' in '{vtype}' is not an int"
576+
assert isinstance(value, str), f"Frame class value '{value}' in '{vtype}' is not a string"
577+
assert value.strip(), f"Frame class name for key {key} in '{vtype}' is empty"
578+
579+
def test_frame_class_dict_contains_required_vehicle_types(self) -> None:
580+
"""FRAME_CLASS_DICT contains entries for all expected vehicle types."""
581+
for required in ("ArduCopter", "Heli", "Rover", "ArduPlane"):
582+
assert required in FRAME_CLASS_DICT, f"Missing vehicle type '{required}' in FRAME_CLASS_DICT"
583+
584+
def test_arducopter_frame_class_values(self) -> None:
585+
"""ArduCopter sub-dict contains the standard multirotor frame classes."""
586+
sub = FRAME_CLASS_DICT["ArduCopter"]
587+
assert sub[1] == "Quad"
588+
assert sub[2] == "Hexa"
589+
assert sub[3] == "Octa"
590+
assert sub[6] == "Heli"
591+
assert sub[11] == "Heli_Dual"
592+
assert sub[13] == "HeliQuad"
593+
594+
def test_heli_frame_class_values(self) -> None:
595+
"""Heli sub-dict contains only helicopter-relevant frame classes."""
596+
sub = FRAME_CLASS_DICT["Heli"]
597+
assert sub[6] == "Heli"
598+
assert sub[11] == "Heli_Dual"
599+
assert sub[13] == "HeliQuad"
600+
# Non-heli classes must not appear
601+
assert 1 not in sub, "Quad should not be in Heli FRAME_CLASS_DICT"
602+
assert 2 not in sub, "Hexa should not be in Heli FRAME_CLASS_DICT"
603+
604+
def test_rover_frame_class_values(self) -> None:
605+
"""Rover sub-dict uses Rover-specific frame class values."""
606+
sub = FRAME_CLASS_DICT["Rover"]
607+
assert sub[1] == "Rover"
608+
assert sub[2] == "Boat"
609+
assert sub[3] == "BalanceBot"
610+
# Multirotor classes must not appear in Rover
611+
assert 4 not in sub, "OctaQuad should not be in Rover FRAME_CLASS_DICT"
612+
613+
def test_arduplane_frame_class_is_empty(self) -> None:
614+
"""ArduPlane has no FRAME_CLASS parameter, so its sub-dict must be empty."""
615+
assert FRAME_CLASS_DICT["ArduPlane"] == {}
616+
617+
def test_get_frame_class_sub_dict_known_vehicle_types(self) -> None:
618+
"""get_frame_class_sub_dict returns the correct sub-dict for known vehicle types."""
619+
assert get_frame_class_sub_dict("ArduCopter") is FRAME_CLASS_DICT["ArduCopter"]
620+
assert get_frame_class_sub_dict("Heli") is FRAME_CLASS_DICT["Heli"]
621+
assert get_frame_class_sub_dict("Rover") is FRAME_CLASS_DICT["Rover"]
622+
assert get_frame_class_sub_dict("ArduPlane") is FRAME_CLASS_DICT["ArduPlane"]
623+
624+
def test_get_frame_class_sub_dict_unknown_type_falls_back_to_arducopter(self) -> None:
625+
"""get_frame_class_sub_dict falls back to ArduCopter for unknown vehicle types."""
626+
result = get_frame_class_sub_dict("UnknownVehicle")
627+
assert result is FRAME_CLASS_DICT["ArduCopter"]
628+
629+
def test_get_frame_class_sub_dict_empty_string_falls_back_to_arducopter(self) -> None:
630+
"""get_frame_class_sub_dict falls back to ArduCopter when fw_type is empty."""
631+
result = get_frame_class_sub_dict("")
632+
assert result is FRAME_CLASS_DICT["ArduCopter"]
633+
634+
def test_frame_class_values_are_unique_per_vehicle_type(self) -> None:
635+
"""Within each vehicle type, frame class names must be unique."""
636+
for vtype, sub_dict in FRAME_CLASS_DICT.items():
637+
if not sub_dict:
638+
continue
639+
names = list(sub_dict.values())
640+
assert len(names) == len(set(names)), f"Duplicate frame class names found in '{vtype}'"

0 commit comments

Comments
 (0)