@@ -203,68 +203,91 @@ def post_init(self, doc_dict: dict) -> None:
203
203
self .init_possible_choices (doc_dict )
204
204
self .init_battery_chemistry ()
205
205
206
- def update_json_structure (self ) -> None : # pylint: disable=too-many-branches
206
+ def update_json_structure (self ) -> None :
207
207
"""
208
208
Update the data structure to ensure all required fields are present.
209
209
210
210
Used to update old JSON files to the latest format.
211
211
"""
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
+ )
244
249
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"
247
261
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 )
251
264
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.
253
268
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
257
272
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
266
275
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
268
291
269
292
def init_battery_chemistry (self ) -> None :
270
293
self ._battery_chemistry = (
0 commit comments