Skip to content

Commit 5cfb1a8

Browse files
committed
feat(component editor): Auto migrate Batt monitor protocols
1 parent c632cb5 commit 5cfb1a8

File tree

4 files changed

+77
-54
lines changed

4 files changed

+77
-54
lines changed

ardupilot_methodic_configurator/data_model_vehicle_components_base.py

Lines changed: 74 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -203,68 +203,91 @@ def post_init(self, doc_dict: dict) -> None:
203203
self.init_possible_choices(doc_dict)
204204
self.init_battery_chemistry()
205205

206-
def update_json_structure(self) -> None: # pylint: disable=too-many-branches
206+
def update_json_structure(self) -> None:
207207
"""
208208
Update the data structure to ensure all required fields are present.
209209
210210
Used to update old JSON files to the latest format.
211211
"""
212-
# Get current data
213-
data = self._data
214-
215-
# Ensure the format version is set.
216-
if "Format version" not in data:
217-
data["Format version"] = 1
218-
219-
# To update old JSON files that do not have these new fields
220-
if "Components" not in data:
221-
data["Components"] = {}
222-
223-
if "Battery" not in data["Components"]:
224-
data["Components"]["Battery"] = {}
225-
226-
if "Specifications" not in data["Components"]["Battery"]:
227-
data["Components"]["Battery"]["Specifications"] = {}
228-
229-
if "Chemistry" not in data["Components"]["Battery"]["Specifications"]:
230-
data["Components"]["Battery"]["Specifications"]["Chemistry"] = "Lipo"
231-
232-
if "Capacity mAh" not in data["Components"]["Battery"]["Specifications"]:
233-
data["Components"]["Battery"]["Specifications"]["Capacity mAh"] = 0
234-
235-
# To update old JSON files that do not have these new "Frame.Specifications.TOW * Kg" fields
236-
if "Frame" not in data["Components"]:
237-
data["Components"]["Frame"] = {}
238-
239-
if "Specifications" not in data["Components"]["Frame"]:
240-
data["Components"]["Frame"]["Specifications"] = {}
241-
242-
if "TOW min Kg" not in data["Components"]["Frame"]["Specifications"]:
243-
data["Components"]["Frame"]["Specifications"]["TOW min Kg"] = 1
212+
# Define the default structure with all required fields
213+
default_structure = {
214+
"Format version": 1,
215+
"Program version": __version__,
216+
"Components": {
217+
"Battery": {
218+
"Specifications": {
219+
"Chemistry": "Lipo",
220+
"Capacity mAh": 0,
221+
}
222+
},
223+
"Frame": {
224+
"Specifications": {
225+
"TOW min Kg": 1,
226+
"TOW max Kg": 1,
227+
}
228+
},
229+
"Flight Controller": {
230+
"Product": {},
231+
"Firmware": {},
232+
"Specifications": {"MCU Series": "Unknown"},
233+
"Notes": "",
234+
},
235+
},
236+
}
237+
238+
# Handle legacy field renaming before merging
239+
if "GNSS receiver" in self._data.get("Components", {}):
240+
components = self._data.setdefault("Components", {})
241+
components["GNSS Receiver"] = components.pop("GNSS receiver")
242+
243+
# Handle legacy battery monitor protocol migration for protocols that don't need hardware connections
244+
# This is a local import to avoid a circular import dependency
245+
from ardupilot_methodic_configurator.data_model_vehicle_components_validation import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415
246+
BATT_MONITOR_CONNECTION,
247+
OTHER_PORTS,
248+
)
244249

245-
if "TOW max Kg" not in data["Components"]["Frame"]["Specifications"]:
246-
data["Components"]["Frame"]["Specifications"]["TOW max Kg"] = 1
250+
# Calculate protocols that use OTHER_PORTS (don't require specific hardware connections)
251+
battmon_other_protocols = {
252+
str(value["protocol"]) for value in BATT_MONITOR_CONNECTION.values() if value.get("type") == OTHER_PORTS
253+
}
254+
battery_monitor_protocol = (
255+
self._data.get("Components", {}).get("Battery Monitor", {}).get("FC Connection", {}).get("Protocol")
256+
)
257+
if battery_monitor_protocol in battmon_other_protocols:
258+
# These protocols don't require specific hardware connections, so we can safely migrate them
259+
battery_monitor = self._data.setdefault("Components", {}).setdefault("Battery Monitor", {})
260+
battery_monitor.setdefault("FC Connection", {})["Type"] = "other"
247261

248-
# Older versions used receiver instead of Receiver, rename it for consistency with other fields
249-
if "GNSS receiver" in data["Components"]:
250-
data["Components"]["GNSS Receiver"] = data["Components"].pop("GNSS receiver")
262+
# Merge existing data onto default structure (preserves existing values)
263+
self._data = self._deep_merge_dicts(default_structure, self._data)
251264

252-
data["Program version"] = __version__
265+
def _deep_merge_dicts(self, default: dict[str, Any], existing: dict[str, Any]) -> dict[str, Any]:
266+
"""
267+
Deep merge two dictionaries, preserving existing values and key order.
253268
254-
# To update old JSON files that do not have this new "Flight Controller.Specifications.MCU Series" field
255-
if "Flight Controller" not in data["Components"]:
256-
data["Components"]["Flight Controller"] = {}
269+
Args:
270+
default: Default structure with fallback values
271+
existing: Existing data to preserve
257272
258-
if "Specifications" not in data["Components"]["Flight Controller"]:
259-
fc_data = data["Components"]["Flight Controller"]
260-
data["Components"]["Flight Controller"] = {
261-
"Product": fc_data.get("Product", {}),
262-
"Firmware": fc_data.get("Firmware", {}),
263-
"Specifications": {"MCU Series": "Unknown"},
264-
"Notes": fc_data.get("Notes", ""),
265-
}
273+
Returns:
274+
Merged dictionary with existing values taking precedence and preserving existing key order
266275
267-
self._data = data
276+
"""
277+
# Start with existing dictionary to preserve its key order
278+
result = existing.copy()
279+
280+
# Add any missing keys from the default structure
281+
for key, value in default.items():
282+
if key not in result:
283+
# Key doesn't exist in existing data, add it from default
284+
result[key] = value
285+
elif isinstance(result[key], dict) and isinstance(value, dict):
286+
# Both are dictionaries, recursively merge them
287+
result[key] = self._deep_merge_dicts(value, result[key])
288+
# If key exists in result but isn't a dict, keep the existing value (no change needed)
289+
290+
return result
268291

269292
def init_battery_chemistry(self) -> None:
270293
self._battery_chemistry = (

ardupilot_methodic_configurator/vehicle_templates/ArduCopter/FETtec-5/vehicle_components.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@
102102
"Version": "10_224"
103103
},
104104
"FC Connection": {
105-
"Type": "SERIAL1",
105+
"Type": "other",
106106
"Protocol": "ESC"
107107
},
108108
"Notes": "ESC Telemetry is used as battery monitor"

ardupilot_methodic_configurator/vehicle_templates/ArduCopter/Hoverit_X11+/vehicle_components.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@
102102
"Version": ""
103103
},
104104
"FC Connection": {
105-
"Type": "CAN1",
105+
"Type": "other",
106106
"Protocol": "ESC"
107107
},
108108
"Notes": "Voltage and current are monitored via the ESC."

ardupilot_methodic_configurator/vehicle_templates/ArduCopter/Hoverit_X13/vehicle_components.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@
102102
"Version": ""
103103
},
104104
"FC Connection": {
105-
"Type": "CAN1",
105+
"Type": "other",
106106
"Protocol": "ESC"
107107
},
108108
"Notes": "Voltage and current are monitored via the ESC DatalinkV2."

0 commit comments

Comments
 (0)