ardupilot-methodic-configurator 2.6.1__py3-none-any.whl → 2.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ardupilot-methodic-configurator might be problematic. Click here for more details.

Files changed (106) hide show
  1. ardupilot_methodic_configurator/__init__.py +2 -2
  2. ardupilot_methodic_configurator/__main__.py +25 -1
  3. ardupilot_methodic_configurator/annotate_params.py +49 -14
  4. ardupilot_methodic_configurator/argparse_check_range.py +1 -1
  5. ardupilot_methodic_configurator/backend_filesystem.py +7 -3
  6. ardupilot_methodic_configurator/backend_filesystem_configuration_steps.py +53 -6
  7. ardupilot_methodic_configurator/backend_filesystem_json_with_schema.py +3 -3
  8. ardupilot_methodic_configurator/backend_filesystem_program_settings.py +145 -8
  9. ardupilot_methodic_configurator/backend_filesystem_vehicle_components.py +3 -3
  10. ardupilot_methodic_configurator/backend_flightcontroller.py +37 -20
  11. ardupilot_methodic_configurator/backend_flightcontroller_info.py +1 -1
  12. ardupilot_methodic_configurator/backend_internet.py +1 -1
  13. ardupilot_methodic_configurator/backend_mavftp.py +1 -1
  14. ardupilot_methodic_configurator/battery_cell_voltages.py +1 -1
  15. ardupilot_methodic_configurator/common_arguments.py +1 -1
  16. ardupilot_methodic_configurator/configuration_manager.py +450 -115
  17. ardupilot_methodic_configurator/configuration_steps_ArduCopter.json +6 -4
  18. ardupilot_methodic_configurator/configuration_steps_ArduPlane.json +3 -1
  19. ardupilot_methodic_configurator/configuration_steps_Heli.json +3 -1
  20. ardupilot_methodic_configurator/configuration_steps_Rover.json +3 -1
  21. ardupilot_methodic_configurator/configuration_steps_strings.py +5 -3
  22. ardupilot_methodic_configurator/data_model_ardupilot_parameter.py +55 -2
  23. ardupilot_methodic_configurator/data_model_configuration_step.py +96 -46
  24. ardupilot_methodic_configurator/data_model_fc_ids.py +16 -7
  25. ardupilot_methodic_configurator/data_model_motor_test.py +1 -1
  26. ardupilot_methodic_configurator/data_model_par_dict.py +25 -11
  27. ardupilot_methodic_configurator/data_model_software_updates.py +1 -1
  28. ardupilot_methodic_configurator/data_model_template_overview.py +1 -1
  29. ardupilot_methodic_configurator/data_model_vehicle_components.py +1 -1
  30. ardupilot_methodic_configurator/data_model_vehicle_components_base.py +3 -2
  31. ardupilot_methodic_configurator/data_model_vehicle_components_display.py +1 -1
  32. ardupilot_methodic_configurator/data_model_vehicle_components_import.py +2 -1
  33. ardupilot_methodic_configurator/data_model_vehicle_components_json_schema.py +1 -1
  34. ardupilot_methodic_configurator/data_model_vehicle_components_templates.py +1 -1
  35. ardupilot_methodic_configurator/data_model_vehicle_components_validation.py +41 -1
  36. ardupilot_methodic_configurator/data_model_vehicle_project.py +1 -1
  37. ardupilot_methodic_configurator/data_model_vehicle_project_creator.py +1 -1
  38. ardupilot_methodic_configurator/data_model_vehicle_project_opener.py +1 -1
  39. ardupilot_methodic_configurator/extract_param_defaults.py +1 -1
  40. ardupilot_methodic_configurator/frontend_tkinter_autoresize_combobox.py +1 -1
  41. ardupilot_methodic_configurator/frontend_tkinter_base_window.py +5 -1
  42. ardupilot_methodic_configurator/frontend_tkinter_component_editor.py +55 -29
  43. ardupilot_methodic_configurator/frontend_tkinter_component_editor_base.py +18 -13
  44. ardupilot_methodic_configurator/frontend_tkinter_component_template_manager.py +1 -1
  45. ardupilot_methodic_configurator/frontend_tkinter_connection_selection.py +1 -1
  46. ardupilot_methodic_configurator/frontend_tkinter_directory_selection.py +1 -1
  47. ardupilot_methodic_configurator/frontend_tkinter_entry_dynamic.py +1 -1
  48. ardupilot_methodic_configurator/frontend_tkinter_flightcontroller_info.py +1 -1
  49. ardupilot_methodic_configurator/frontend_tkinter_font.py +1 -1
  50. ardupilot_methodic_configurator/frontend_tkinter_motor_test.py +1 -1
  51. ardupilot_methodic_configurator/frontend_tkinter_pair_tuple_combobox.py +7 -1
  52. ardupilot_methodic_configurator/frontend_tkinter_parameter_editor.py +50 -102
  53. ardupilot_methodic_configurator/frontend_tkinter_parameter_editor_documentation_frame.py +24 -58
  54. ardupilot_methodic_configurator/frontend_tkinter_parameter_editor_table.py +24 -56
  55. ardupilot_methodic_configurator/frontend_tkinter_progress_window.py +1 -1
  56. ardupilot_methodic_configurator/frontend_tkinter_project_creator.py +1 -1
  57. ardupilot_methodic_configurator/frontend_tkinter_project_opener.py +1 -1
  58. ardupilot_methodic_configurator/frontend_tkinter_rich_text.py +1 -1
  59. ardupilot_methodic_configurator/frontend_tkinter_scroll_frame.py +1 -1
  60. ardupilot_methodic_configurator/frontend_tkinter_show.py +1 -1
  61. ardupilot_methodic_configurator/frontend_tkinter_software_update.py +1 -1
  62. ardupilot_methodic_configurator/frontend_tkinter_stage_progress.py +19 -29
  63. ardupilot_methodic_configurator/frontend_tkinter_template_overview.py +1 -1
  64. ardupilot_methodic_configurator/frontend_tkinter_usage_popup_window.py +1 -1
  65. ardupilot_methodic_configurator/internationalization.py +1 -1
  66. ardupilot_methodic_configurator/param_pid_adjustment_update.py +43 -39
  67. ardupilot_methodic_configurator/tempcal_imu.py +1 -1
  68. ardupilot_methodic_configurator/vehicle_components.py +1 -1
  69. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/AirCar_v1/14_logging.param +3 -3
  70. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/Big_Owl/14_logging.param +3 -3
  71. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/Chimera7/14_logging.param +3 -3
  72. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/FETtec-5/14_logging.param +3 -3
  73. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/GazeboIrisWithTargetFollow/14_logging.param +3 -3
  74. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/Holybro_X500/14_logging.param +3 -3
  75. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/Holybro_X500_V2/14_logging.param +3 -3
  76. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/Holybro_X650_LTE/14_logging.param +3 -3
  77. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/Hoverit_X11+/14_logging.param +3 -3
  78. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/Hoverit_X13/14_logging.param +3 -3
  79. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/Marmotte5v2/14_logging.param +3 -3
  80. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/ReadyToSkyZD550/14_logging.param +3 -3
  81. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/TarotFY680Hexacopter/05_remote_controller.param +1 -1
  82. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/TarotFY680Hexacopter/14_logging.param +3 -3
  83. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/TarotFY680Hexacopter/47_position_controller.param +2 -2
  84. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/Tarot_X4/14_logging.param +3 -3
  85. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/X11_plus/14_logging.param +3 -3
  86. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/diatone_taycan_mxc/4.3.8-params/14_logging.param +3 -3
  87. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/diatone_taycan_mxc/4.4.4-params/14_logging.param +3 -3
  88. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/diatone_taycan_mxc/4.5.x-params/14_logging.param +3 -3
  89. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/diatone_taycan_mxc/4.6.x-params/14_logging.param +3 -3
  90. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/empty_4.5.x/10_gnss.param +1 -1
  91. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/empty_4.6.x/10_gnss.param +1 -1
  92. {ardupilot_methodic_configurator-2.6.1.dist-info → ardupilot_methodic_configurator-2.7.0.dist-info}/METADATA +11 -6
  93. {ardupilot_methodic_configurator-2.6.1.dist-info → ardupilot_methodic_configurator-2.7.0.dist-info}/RECORD +106 -106
  94. {ardupilot_methodic_configurator-2.6.1.dist-info → ardupilot_methodic_configurator-2.7.0.dist-info}/WHEEL +0 -0
  95. {ardupilot_methodic_configurator-2.6.1.dist-info → ardupilot_methodic_configurator-2.7.0.dist-info}/entry_points.txt +0 -0
  96. {ardupilot_methodic_configurator-2.6.1.dist-info → ardupilot_methodic_configurator-2.7.0.dist-info}/licenses/LICENSE.md +0 -0
  97. {ardupilot_methodic_configurator-2.6.1.dist-info → ardupilot_methodic_configurator-2.7.0.dist-info}/licenses/LICENSES/Apache-2.0.txt +0 -0
  98. {ardupilot_methodic_configurator-2.6.1.dist-info → ardupilot_methodic_configurator-2.7.0.dist-info}/licenses/LICENSES/BSD-3-Clause.txt +0 -0
  99. {ardupilot_methodic_configurator-2.6.1.dist-info → ardupilot_methodic_configurator-2.7.0.dist-info}/licenses/LICENSES/GPL-3.0-or-later.txt +0 -0
  100. {ardupilot_methodic_configurator-2.6.1.dist-info → ardupilot_methodic_configurator-2.7.0.dist-info}/licenses/LICENSES/LGPL-3.0-or-later.txt +0 -0
  101. {ardupilot_methodic_configurator-2.6.1.dist-info → ardupilot_methodic_configurator-2.7.0.dist-info}/licenses/LICENSES/MIT-CMU.txt +0 -0
  102. {ardupilot_methodic_configurator-2.6.1.dist-info → ardupilot_methodic_configurator-2.7.0.dist-info}/licenses/LICENSES/MIT.txt +0 -0
  103. {ardupilot_methodic_configurator-2.6.1.dist-info → ardupilot_methodic_configurator-2.7.0.dist-info}/licenses/LICENSES/MPL-2.0.txt +0 -0
  104. {ardupilot_methodic_configurator-2.6.1.dist-info → ardupilot_methodic_configurator-2.7.0.dist-info}/licenses/LICENSES/PSF-2.0.txt +0 -0
  105. {ardupilot_methodic_configurator-2.6.1.dist-info → ardupilot_methodic_configurator-2.7.0.dist-info}/licenses/credits/CREDITS.md +0 -0
  106. {ardupilot_methodic_configurator-2.6.1.dist-info → ardupilot_methodic_configurator-2.7.0.dist-info}/top_level.txt +0 -0
@@ -5,7 +5,7 @@ Contains state information but no GUI code.
5
5
  Aggregates flight controller and filesystem access in a single interface.
6
6
  Uses exceptions for error handling, the GUI layer will catch and display them.
7
7
 
8
- This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator
8
+ This file is part of ArduPilot Methodic Configurator. https://github.com/ArduPilot/MethodicConfigurator
9
9
 
10
10
  SPDX-FileCopyrightText: 2024-2025 Amilcar do Carmo Lucas <amilcar.lucas@iav.de>
11
11
 
@@ -17,9 +17,11 @@ from logging import error as logging_error
17
17
  from logging import info as logging_info
18
18
  from pathlib import Path
19
19
  from typing import Callable, Optional
20
+ from webbrowser import open as webbrowser_open # to open the web documentation
20
21
 
21
22
  from ardupilot_methodic_configurator import _
22
23
  from ardupilot_methodic_configurator.backend_filesystem import LocalFilesystem
24
+ from ardupilot_methodic_configurator.backend_filesystem_configuration_steps import PhaseData
23
25
  from ardupilot_methodic_configurator.backend_flightcontroller import FlightController
24
26
  from ardupilot_methodic_configurator.backend_internet import download_file_from_url
25
27
  from ardupilot_methodic_configurator.data_model_ardupilot_parameter import ArduPilotParameter
@@ -33,6 +35,7 @@ SelectFileCallback = Callable[[str, list[str]], Optional[str]] # (title, filety
33
35
  ShowWarningCallback = Callable[[str, str], None] # (title, message) -> None
34
36
  ShowErrorCallback = Callable[[str, str], None] # (title, message) -> None
35
37
  ShowInfoCallback = Callable[[str, str], None] # (title, message) -> None
38
+ AskRetryCancelCallback = Callable[[str, str], bool] # (title, message) -> bool
36
39
 
37
40
 
38
41
  class OperationNotPossibleError(Exception):
@@ -46,57 +49,62 @@ class InvalidParameterNameError(Exception):
46
49
  # pylint: disable=too-many-lines
47
50
 
48
51
 
49
- class ConfigurationManager: # pylint: disable=too-many-public-methods
52
+ class ConfigurationManager: # pylint: disable=too-many-public-methods, too-many-instance-attributes
50
53
  """
51
54
  Manages configuration state, including flight controller and filesystem access.
52
55
 
53
56
  This class aggregates the flight controller and filesystem access to provide a unified interface
54
- for managing configuration state. It holds references to the flight controller and filesystem,
57
+ for managing configuration state. It holds protected references to the flight controller and filesystem,
55
58
  and provides methods to interact with them.
56
59
  """
57
60
 
58
61
  def __init__(self, current_file: str, flight_controller: FlightController, filesystem: LocalFilesystem) -> None:
59
62
  self.current_file = current_file
60
- self.flight_controller = flight_controller
61
- self.filesystem = filesystem
62
- self.config_step_processor = ConfigurationStepProcessor(self.filesystem)
63
+ self._flight_controller = flight_controller
64
+ self._local_filesystem = filesystem
65
+ self._config_step_processor = ConfigurationStepProcessor(self._local_filesystem)
63
66
 
64
- # self.parameters is rebuilt on every repopulate(...) call and only contains the ArduPilotParameter
67
+ # self.current_step_parameters is rebuilt on every repopulate(...) call and only contains the ArduPilotParameter
65
68
  # objects needed for the current table view.
66
- self.parameters: dict[str, ArduPilotParameter] = {}
69
+ self.current_step_parameters: dict[str, ArduPilotParameter] = {}
67
70
 
71
+ # Track parameters added by user (not in original file) or renamed by the system in the current configuration step
72
+ self._added_parameters: set[str] = set()
73
+
74
+ # Track parameters deleted by user (were in original file) or renamed by the system in the current configuration step
75
+ self._deleted_parameters: set[str] = set()
76
+
77
+ self._at_least_one_changed = False
78
+
79
+ # frontend_tkinter_parameter_editor_table.py API start
68
80
  @property
69
81
  def connected_vehicle_type(self) -> str:
70
82
  return (
71
- getattr(self.flight_controller.info, "vehicle_type", "")
72
- if hasattr(self.flight_controller, "info") and self.flight_controller.info is not None
83
+ getattr(self._flight_controller.info, "vehicle_type", "")
84
+ if hasattr(self._flight_controller, "info") and self._flight_controller.info is not None
73
85
  else ""
74
86
  )
75
87
 
76
88
  @property
77
89
  def is_fc_connected(self) -> bool:
78
- return self.flight_controller.master is not None
90
+ return self._flight_controller.master is not None
79
91
 
80
92
  @property
81
93
  def fc_parameters(self) -> dict[str, float]:
82
94
  return (
83
- self.flight_controller.fc_parameters
84
- if hasattr(self.flight_controller, "fc_parameters") and self.flight_controller.fc_parameters is not None
95
+ self._flight_controller.fc_parameters
96
+ if hasattr(self._flight_controller, "fc_parameters") and self._flight_controller.fc_parameters is not None
85
97
  else {}
86
98
  )
87
99
 
88
100
  @property
89
101
  def is_mavftp_supported(self) -> bool:
90
102
  return (
91
- getattr(self.flight_controller.info, "is_mavftp_supported", False)
92
- if hasattr(self.flight_controller, "info") and self.flight_controller.info is not None
103
+ getattr(self._flight_controller.info, "is_mavftp_supported", False)
104
+ if hasattr(self._flight_controller, "info") and self._flight_controller.info is not None
93
105
  else False
94
106
  )
95
107
 
96
- @property
97
- def current_file_parameters(self) -> ParDict:
98
- return self.filesystem.file_parameters.get(self.current_file, ParDict())
99
-
100
108
  def handle_imu_temperature_calibration_workflow( # pylint: disable=too-many-arguments, too-many-positional-arguments
101
109
  self,
102
110
  selected_file: str,
@@ -126,7 +134,9 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
126
134
 
127
135
  """
128
136
  # Check if IMU temperature calibration should be offered for this file
129
- tempcal_imu_result_param_filename, tempcal_imu_result_param_fullpath = self.filesystem.tempcal_imu_result_param_tuple()
137
+ tempcal_imu_result_param_filename, tempcal_imu_result_param_fullpath = (
138
+ self._local_filesystem.tempcal_imu_result_param_tuple()
139
+ )
130
140
  if selected_file != tempcal_imu_result_param_filename:
131
141
  return False
132
142
 
@@ -161,13 +171,13 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
161
171
  log_parm=False,
162
172
  online=False,
163
173
  tclr=False,
164
- figpath=self.filesystem.vehicle_dir,
174
+ figpath=self._local_filesystem.vehicle_dir,
165
175
  progress_callback=progress_callback,
166
176
  )
167
177
 
168
178
  try:
169
179
  # Reload parameter files after calibration
170
- self.filesystem.file_parameters = self.filesystem.read_params_from_files()
180
+ self._local_filesystem.file_parameters = self._local_filesystem.read_params_from_files()
171
181
  return True
172
182
  except SystemExit as exp:
173
183
  show_error(_("Fatal error reading parameter files"), f"{exp}")
@@ -186,13 +196,13 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
186
196
  auto_changed_by contains the tool name that requires external changes.
187
197
 
188
198
  """
189
- auto_changed_by = self.filesystem.auto_changed_by(selected_file)
190
- if auto_changed_by and self.flight_controller.fc_parameters:
199
+ auto_changed_by = self._local_filesystem.auto_changed_by(selected_file)
200
+ if auto_changed_by and self._flight_controller.fc_parameters:
191
201
  # Filter relevant FC parameters for this file
192
202
  relevant_fc_params = {
193
203
  key: value
194
- for key, value in self.flight_controller.fc_parameters.items()
195
- if key in self.filesystem.file_parameters[selected_file]
204
+ for key, value in self._flight_controller.fc_parameters.items()
205
+ if key in self.current_step_parameters
196
206
  }
197
207
  return True, relevant_fc_params, auto_changed_by
198
208
  return False, None, auto_changed_by
@@ -209,7 +219,7 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
209
219
  bool: True if parameters were copied successfully.
210
220
 
211
221
  """
212
- params_copied = self.filesystem.copy_fc_values_to_file(selected_file, relevant_fc_params)
222
+ params_copied = self._local_filesystem.copy_fc_values_to_file(selected_file, relevant_fc_params)
213
223
  return bool(params_copied)
214
224
 
215
225
  def get_file_jump_options(self, selected_file: str) -> dict[str, str]:
@@ -223,7 +233,7 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
223
233
  dict: Dictionary mapping destination files to their messages.
224
234
 
225
235
  """
226
- return self.filesystem.jump_possible(selected_file)
236
+ return self._local_filesystem.jump_possible(selected_file)
227
237
 
228
238
  def should_download_file_from_url_workflow(
229
239
  self,
@@ -246,11 +256,11 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
246
256
  bool: True if download was successful or not needed, False if download failed.
247
257
 
248
258
  """
249
- url, local_filename = self.filesystem.get_download_url_and_local_filename(selected_file)
259
+ url, local_filename = self._local_filesystem.get_download_url_and_local_filename(selected_file)
250
260
  if not url or not local_filename:
251
261
  return True # No download required
252
262
 
253
- if self.filesystem.vehicle_configuration_file_exists(local_filename):
263
+ if self._local_filesystem.vehicle_configuration_file_exists(local_filename):
254
264
  return True # File already exists in the vehicle directory, no need to download it
255
265
 
256
266
  # Ask user for confirmation
@@ -291,16 +301,16 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
291
301
  bool: True if upload was successful or not needed, False if upload failed.
292
302
 
293
303
  """
294
- local_filename, remote_filename = self.filesystem.get_upload_local_and_remote_filenames(selected_file)
304
+ local_filename, remote_filename = self._local_filesystem.get_upload_local_and_remote_filenames(selected_file)
295
305
  if not local_filename or not remote_filename:
296
306
  return True # No upload required
297
307
 
298
- if not self.filesystem.vehicle_configuration_file_exists(local_filename):
308
+ if not self._local_filesystem.vehicle_configuration_file_exists(local_filename):
299
309
  error_msg = _("Local file {local_filename} does not exist")
300
310
  show_error(_("Will not upload any file"), error_msg.format(local_filename=local_filename))
301
311
  return False
302
312
 
303
- if self.flight_controller.master is None:
313
+ if self._flight_controller.master is None:
304
314
  show_warning(_("Will not upload any file"), _("No flight controller connection"))
305
315
  return False
306
316
 
@@ -312,7 +322,7 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
312
322
  return True # User declined upload
313
323
 
314
324
  # Attempt upload
315
- if not self.flight_controller.upload_file(local_filename, remote_filename, progress_callback):
325
+ if not self._flight_controller.upload_file(local_filename, remote_filename, progress_callback):
316
326
  error_msg = _("Failed to upload {local_filename} to {remote_filename}, please upload it manually")
317
327
  show_error(_("Upload failed"), error_msg.format(local_filename=local_filename, remote_filename=remote_filename))
318
328
  return False
@@ -331,18 +341,18 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
331
341
 
332
342
  """
333
343
  # Download all parameters from the flight controller
334
- fc_parameters, param_default_values = self.flight_controller.download_params(
344
+ fc_parameters, param_default_values = self._flight_controller.download_params(
335
345
  progress_callback,
336
- Path(self.filesystem.vehicle_dir) / "complete.param",
337
- Path(self.filesystem.vehicle_dir) / "00_default.param",
346
+ Path(self._local_filesystem.vehicle_dir) / "complete.param",
347
+ Path(self._local_filesystem.vehicle_dir) / "00_default.param",
338
348
  )
339
349
 
340
350
  # Update the flight controller parameters
341
- self.flight_controller.fc_parameters = fc_parameters
351
+ self._flight_controller.fc_parameters = fc_parameters
342
352
 
343
353
  # Write default values to file if available
344
354
  if param_default_values:
345
- self.filesystem.write_param_default_values_to_file(param_default_values)
355
+ self._local_filesystem.write_param_default_values_to_file(param_default_values)
346
356
 
347
357
  return fc_parameters, param_default_values
348
358
 
@@ -373,17 +383,17 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
373
383
  # Write each selected parameter to the flight controller
374
384
  for param_name, param in selected_params.items():
375
385
  try:
376
- if param_name not in self.flight_controller.fc_parameters or not is_within_tolerance(
377
- self.flight_controller.fc_parameters[param_name], param.value
386
+ if param_name not in self._flight_controller.fc_parameters or not is_within_tolerance(
387
+ self._flight_controller.fc_parameters[param_name], param.value
378
388
  ):
379
- param_metadata = self.filesystem.doc_dict.get(param_name, None)
389
+ param_metadata = self._local_filesystem.doc_dict.get(param_name, None)
380
390
  if param_metadata and param_metadata.get("RebootRequired", False):
381
- self.flight_controller.set_param(param_name, float(param.value))
382
- if param_name in self.flight_controller.fc_parameters:
391
+ self._flight_controller.set_param(param_name, float(param.value))
392
+ if param_name in self._flight_controller.fc_parameters:
383
393
  logging_info(
384
394
  _("Parameter %s changed from %f to %f, reset required"),
385
395
  param_name,
386
- self.flight_controller.fc_parameters[param_name],
396
+ self._flight_controller.fc_parameters[param_name],
387
397
  param.value,
388
398
  )
389
399
  else:
@@ -391,12 +401,12 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
391
401
  reset_required = True
392
402
  # Check if any of the selected parameters have a _TYPE, _EN, or _ENABLE suffix
393
403
  elif param_name.endswith(("_TYPE", "_EN", "_ENABLE", "SID_AXIS")):
394
- self.flight_controller.set_param(param_name, float(param.value))
395
- if param_name in self.flight_controller.fc_parameters:
404
+ self._flight_controller.set_param(param_name, float(param.value))
405
+ if param_name in self._flight_controller.fc_parameters:
396
406
  logging_info(
397
407
  _("Parameter %s changed from %f to %f, possible reset required"),
398
408
  param_name,
399
- self.flight_controller.fc_parameters[param_name],
409
+ self._flight_controller.fc_parameters[param_name],
400
410
  param.value,
401
411
  )
402
412
  else:
@@ -423,10 +433,13 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
423
433
  int: Extra sleep time in seconds.
424
434
 
425
435
  """
426
- current_file_params: ParDict = self.filesystem.file_parameters.get(self.current_file, ParDict())
427
- filesystem_boot_delay = current_file_params.get("BRD_BOOT_DELAY", Par(0.0))
428
- flightcontroller_boot_delay = self.flight_controller.fc_parameters.get("BRD_BOOT_DELAY", 0)
429
- return int(max(filesystem_boot_delay.value, flightcontroller_boot_delay) // 1000 + 1) # round up
436
+ param_boot_delay = (
437
+ self.current_step_parameters["BRD_BOOT_DELAY"].get_new_value()
438
+ if "BRD_BOOT_DELAY" in self.current_step_parameters
439
+ else 0.0
440
+ )
441
+ flightcontroller_boot_delay = self._flight_controller.fc_parameters.get("BRD_BOOT_DELAY", 0)
442
+ return int(max(param_boot_delay, flightcontroller_boot_delay) // 1000 + 1) # round up
430
443
 
431
444
  def _reset_and_reconnect_flight_controller(
432
445
  self, progress_callback: Optional[Callable] = None, sleep_time: Optional[int] = None
@@ -446,7 +459,7 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
446
459
  sleep_time = self._calculate_reset_time()
447
460
 
448
461
  # Call reset_and_reconnect with a callback to update the reset progress bar and the progress message
449
- return self.flight_controller.reset_and_reconnect(progress_callback, None, int(sleep_time))
462
+ return self._flight_controller.reset_and_reconnect(progress_callback, None, int(sleep_time))
450
463
 
451
464
  def reset_and_reconnect_workflow( # pylint: disable=too-many-arguments, too-many-positional-arguments
452
465
  self,
@@ -492,7 +505,7 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
492
505
 
493
506
  return True # No reset needed
494
507
 
495
- def upload_selected_parameters_workflow(self, selected_params: dict, show_error: Callable[[str, str], None]) -> int:
508
+ def _upload_parameters_to_fc(self, selected_params: dict, show_error: Callable[[str, str], None]) -> int:
496
509
  """
497
510
  Upload selected parameters to flight controller.
498
511
 
@@ -510,15 +523,15 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
510
523
 
511
524
  for param_name, param in selected_params.items():
512
525
  try:
513
- self.flight_controller.set_param(param_name, param.value)
514
- if param_name not in self.flight_controller.fc_parameters or not is_within_tolerance(
515
- self.flight_controller.fc_parameters[param_name], param.value
526
+ self._flight_controller.set_param(param_name, param.value)
527
+ if param_name not in self._flight_controller.fc_parameters or not is_within_tolerance(
528
+ self._flight_controller.fc_parameters[param_name], param.value
516
529
  ):
517
- if param_name in self.flight_controller.fc_parameters:
530
+ if param_name in self._flight_controller.fc_parameters:
518
531
  logging_info(
519
532
  _("Parameter %s changed from %f to %f"),
520
533
  param_name,
521
- self.flight_controller.fc_parameters[param_name],
534
+ self._flight_controller.fc_parameters[param_name],
522
535
  param.value,
523
536
  )
524
537
  else:
@@ -593,7 +606,7 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
593
606
  "39_autotune_roll_pitch_results.param",
594
607
  ]
595
608
 
596
- report_file_path = Path(getattr(self.filesystem, "vehicle_dir", ".")) / "tuning_report.csv"
609
+ report_file_path = Path(getattr(self._local_filesystem, "vehicle_dir", ".")) / "tuning_report.csv"
597
610
 
598
611
  # Write a CSV with a header ("param", <list of files>) and one row per parameter.
599
612
  with open(report_file_path, "w", newline="", encoding="utf-8") as file:
@@ -605,34 +618,107 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
605
618
  for param_file in report_files:
606
619
  try:
607
620
  if param_file == "00_default.param":
608
- value = str(self.filesystem.param_default_dict[param_name].value)
621
+ value = str(self._local_filesystem.param_default_dict[param_name].value)
609
622
  else:
610
- value = str(self.filesystem.file_parameters[param_file][param_name].value)
623
+ value = str(self._local_filesystem.file_parameters[param_file][param_name].value)
611
624
  except (KeyError, ValueError):
612
625
  # On any unexpected structure, leave the value empty (don't crash)
613
626
  value = ""
614
627
  row.append(value)
615
628
  writer.writerow(row)
616
629
 
617
- def validate_uploaded_parameters(self, selected_params: dict) -> list[str]:
630
+ def upload_selected_params_workflow( # pylint: disable=too-many-arguments, too-many-positional-arguments
631
+ self,
632
+ selected_params: dict,
633
+ ask_confirmation: AskConfirmationCallback,
634
+ ask_retry_cancel: AskRetryCancelCallback,
635
+ show_error: ShowErrorCallback,
636
+ progress_callback_for_reset: Optional[Callable] = None,
637
+ progress_callback_for_download: Optional[Callable] = None,
638
+ ) -> None:
639
+ """
640
+ Complete workflow for uploading selected parameters, including reset, upload, validation, and retry.
641
+
642
+ Args:
643
+ selected_params: Dictionary of parameters to upload.
644
+ ask_confirmation: Callback to ask user for confirmation.
645
+ ask_retry_cancel: Callback to ask user to retry or cancel on upload error.
646
+ show_error: Callback to show error messages.
647
+ progress_callback_for_reset: Optional callback for reset progress.
648
+ progress_callback_for_download: Optional callback for download progress.
649
+
650
+ """
651
+ logging_info(
652
+ _("Uploading %d selected %s parameters to flight controller..."),
653
+ len(selected_params),
654
+ self.current_file,
655
+ )
656
+
657
+ # Upload parameters that require reset
658
+ reset_happened = self.upload_parameters_that_require_reset_workflow(
659
+ selected_params,
660
+ ask_confirmation,
661
+ show_error,
662
+ progress_callback_for_reset,
663
+ )
664
+
665
+ # Upload the selected parameters
666
+ nr_changed = self._upload_parameters_to_fc(selected_params, show_error)
667
+
668
+ if reset_happened or nr_changed > 0:
669
+ self._at_least_one_changed = True
670
+
671
+ if self._at_least_one_changed:
672
+ # Re-download all parameters to validate
673
+ self.download_flight_controller_parameters(progress_callback_for_download)
674
+ param_upload_error = self._validate_uploaded_parameters(selected_params)
675
+
676
+ if param_upload_error:
677
+ if ask_retry_cancel(
678
+ _("Parameter upload error"),
679
+ _("Failed to upload the following parameters to the flight controller:\n")
680
+ + f"{(', ').join(param_upload_error)}",
681
+ ):
682
+ # Retry the entire workflow
683
+ self.upload_selected_params_workflow(
684
+ selected_params,
685
+ ask_confirmation,
686
+ ask_retry_cancel,
687
+ show_error,
688
+ progress_callback_for_reset,
689
+ progress_callback_for_download,
690
+ )
691
+ # If not retrying, continue without success message
692
+ else:
693
+ logging_info(_("All parameters uploaded to the flight controller successfully"))
694
+
695
+ self._export_fc_params_missing_or_different()
696
+
697
+ self._write_current_file()
698
+ self._at_least_one_changed = False
699
+
700
+ # frontend_tkinter_parameter_editor_table.py API end
701
+
702
+ # frontend_tkinter_parameter_editor.py API start
703
+ def _validate_uploaded_parameters(self, selected_params: dict) -> list[str]:
618
704
  logging_info(_("Re-downloaded all parameters from the flight controller"))
619
705
 
620
706
  # Validate that the read parameters are the same as the ones in the current_file
621
707
  param_upload_error = []
622
708
  for param_name, param in selected_params.items():
623
709
  if (
624
- param_name in self.flight_controller.fc_parameters
710
+ param_name in self._flight_controller.fc_parameters
625
711
  and param is not None
626
- and not is_within_tolerance(self.flight_controller.fc_parameters[param_name], float(param.value))
712
+ and not is_within_tolerance(self._flight_controller.fc_parameters[param_name], float(param.value))
627
713
  ):
628
714
  logging_error(
629
715
  _("Parameter %s upload to the flight controller failed. Expected: %f, Actual: %f"),
630
716
  param_name,
631
717
  param.value,
632
- self.flight_controller.fc_parameters[param_name],
718
+ self._flight_controller.fc_parameters[param_name],
633
719
  )
634
720
  param_upload_error.append(param_name)
635
- if param_name not in self.flight_controller.fc_parameters:
721
+ if param_name not in self._flight_controller.fc_parameters:
636
722
  logging_error(
637
723
  _("Parameter %s upload to the flight controller failed. Expected: %f, Actual: N/A"),
638
724
  param_name,
@@ -650,18 +736,20 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
650
736
 
651
737
  """
652
738
  # Create FC parameters dictionary
653
- fc_parameters = ParDict.from_fc_parameters(self.flight_controller.fc_parameters)
739
+ fc_parameters = ParDict.from_fc_parameters(self._flight_controller.fc_parameters)
654
740
 
655
741
  # Early exit if no FC parameters available
656
742
  if len(fc_parameters) == 0:
657
743
  return fc_parameters
658
744
 
659
745
  # Remove default parameters from FC parameters if default file exists
660
- fc_parameters.remove_if_value_is_similar(self.filesystem.param_default_dict, is_within_tolerance)
746
+ fc_parameters.remove_if_value_is_similar(self._local_filesystem.param_default_dict, is_within_tolerance)
661
747
 
662
748
  # Filter out read-only parameters efficiently - only check params that exist in fc_parameters
663
749
  readonly_params_to_remove = [
664
- param_name for param_name in fc_parameters if self.filesystem.doc_dict.get(param_name, {}).get("ReadOnly", False)
750
+ param_name
751
+ for param_name in fc_parameters
752
+ if self._local_filesystem.doc_dict.get(param_name, {}).get("ReadOnly", False)
665
753
  ]
666
754
  for param_name in readonly_params_to_remove:
667
755
  del fc_parameters[param_name]
@@ -681,13 +769,13 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
681
769
  last_filename: Last configuration file to process (inclusive).
682
770
 
683
771
  """
684
- if not self.flight_controller.fc_parameters:
772
+ if not self._flight_controller.fc_parameters:
685
773
  return
686
774
 
687
775
  # Create the compounded state of all parameters stored in the AMC .param files
688
776
  compound = ParDict()
689
777
  first_config_step_filename = None
690
- for file_name, file_params in self.filesystem.file_parameters.items():
778
+ for file_name, file_params in self._local_filesystem.file_parameters.items():
691
779
  if file_name != "00_default.param":
692
780
  if first_config_step_filename is None:
693
781
  first_config_step_filename = file_name
@@ -722,7 +810,7 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
722
810
  first_name_without_ext = first_config_step_filename.rsplit(".", 1)[0] if first_config_step_filename else "unknown"
723
811
  # the last filename already has the .param extension
724
812
  filename = f"fc_params_missing_or_different_in_the_amc_param_files_{first_name_without_ext}_to_{last_filename}"
725
- self.filesystem.export_to_param(params_missing_in_the_amc_param_files, filename, annotate_doc=False)
813
+ self._local_filesystem.export_to_param(params_missing_in_the_amc_param_files, filename, annotate_doc=False)
726
814
  logging_info(
727
815
  _("Exported %d FC parameters missing or different in AMC files to %s"),
728
816
  len(params_missing_in_the_amc_param_files),
@@ -731,10 +819,10 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
731
819
  else:
732
820
  logging_info(_("No FC parameters are missing or different from AMC parameter files"))
733
821
 
734
- def export_fc_params_missing_or_different(self) -> None:
822
+ def _export_fc_params_missing_or_different(self) -> None:
735
823
  non_default_non_read_only_fc_params = self._get_non_default_non_read_only_fc_params()
736
824
 
737
- last_config_step_filename = list(self.filesystem.file_parameters.keys())[-1]
825
+ last_config_step_filename = list(self._local_filesystem.file_parameters.keys())[-1]
738
826
  # Export FC parameters that are missing or different from AMC parameter files
739
827
  self._export_fc_params_missing_or_different_in_amc_files(non_default_non_read_only_fc_params, self.current_file)
740
828
  self._export_fc_params_missing_or_different_in_amc_files(
@@ -759,7 +847,7 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
759
847
  run_in_thread: Callback to run the download in a thread (optional).
760
848
 
761
849
  """
762
- if self.flight_controller.master is None:
850
+ if self._flight_controller.master is None:
763
851
  show_error(_("Error"), _("No flight controller connected"))
764
852
  return
765
853
 
@@ -771,26 +859,29 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
771
859
  if not filename:
772
860
  return
773
861
 
774
- success = self.flight_controller.download_last_flight_log(filename, progress_callback)
862
+ success = self._flight_controller.download_last_flight_log(filename, progress_callback)
775
863
  if success:
776
864
  show_info(_("Success"), _("Flight log downloaded successfully to:\n%s") % filename)
777
865
  else:
778
866
  show_error(_("Error"), _("Failed to download flight log. Check the console for details."))
779
867
 
780
- def is_configuration_step_optional(self, file_name: str, threshold_pct: int = 20) -> bool:
868
+ def is_configuration_step_optional(self, file_name: Optional[str] = None, threshold_pct: int = 20) -> bool:
781
869
  """
782
870
  Check if the configuration step for the given file is optional.
783
871
 
784
872
  Args:
785
- file_name: Name of the configuration file to check.
873
+ file_name: Name of the configuration file to check, defaults to self.current_file.
786
874
  threshold_pct: Threshold percentage below which the step is considered optional.
787
875
 
788
876
  Returns:
789
877
  bool: True if the configuration step is optional, False if mandatory.
790
878
 
791
879
  """
880
+ if file_name is None:
881
+ file_name = self.current_file
882
+
792
883
  # Check if the configuration step for the given file is optional
793
- mandatory_text, _mandatory_url = self.filesystem.get_documentation_text_and_url(file_name, "mandatory")
884
+ mandatory_text, _mandatory_url = self._local_filesystem.get_documentation_text_and_url(file_name, "mandatory")
794
885
  # Extract percentage from mandatory_text like "80% mandatory (20% optional)"
795
886
  percentage = 0
796
887
  if mandatory_text:
@@ -801,21 +892,24 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
801
892
 
802
893
  return percentage <= threshold_pct
803
894
 
804
- def get_next_non_optional_file(self, current_file: str) -> Optional[str]:
895
+ def get_next_non_optional_file(self, current_file: Optional[str] = None) -> Optional[str]:
805
896
  """
806
897
  Get the next non-optional configuration file in sequence.
807
898
 
808
899
  Args:
809
- current_file: The current parameter file being processed.
900
+ current_file: The current parameter file being processed, defaults to self.current_file.
810
901
 
811
902
  Returns:
812
903
  Optional[str]: Next non-optional file name, or None if at the end.
813
904
 
814
905
  """
815
- files = list(self.filesystem.file_parameters.keys())
906
+ files = list(self._local_filesystem.file_parameters.keys())
816
907
  if not files:
817
908
  return None
818
909
 
910
+ if current_file is None:
911
+ current_file = self.current_file
912
+
819
913
  try:
820
914
  next_file_index = files.index(current_file) + 1
821
915
 
@@ -843,14 +937,14 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
843
937
 
844
938
  """
845
939
  # Get annotated FC parameters
846
- annotated_fc_parameters = self.filesystem.annotate_intermediate_comments_to_param_dict(
847
- self.flight_controller.fc_parameters
940
+ annotated_fc_parameters = self._local_filesystem.annotate_intermediate_comments_to_param_dict(
941
+ self._flight_controller.fc_parameters
848
942
  )
849
943
  if not annotated_fc_parameters:
850
944
  return {}
851
945
 
852
946
  # Categorize parameters using filesystem logic
853
- categorized = self.filesystem.categorize_parameters(annotated_fc_parameters)
947
+ categorized = self._local_filesystem.categorize_parameters(annotated_fc_parameters)
854
948
  if not categorized or len(categorized) != 3:
855
949
  # Return empty dict if categorization fails or returns empty tuple
856
950
  return {}
@@ -1009,13 +1103,13 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
1009
1103
  return False
1010
1104
 
1011
1105
  # If file exists, ask user for confirmation
1012
- if self.filesystem.vehicle_configuration_file_exists(filename):
1106
+ if self._local_filesystem.vehicle_configuration_file_exists(filename):
1013
1107
  msg = _("{} file already exists.\nDo you want to overwrite it?")
1014
1108
  should_write_file = ask_confirmation(_("Overwrite existing file"), msg.format(filename))
1015
1109
 
1016
1110
  # Write the file using if confirmed and has parameters
1017
1111
  if should_write_file:
1018
- self.filesystem.export_to_param(param_dict, filename, annotate_doc)
1112
+ self._local_filesystem.export_to_param(param_dict, filename, annotate_doc)
1019
1113
  logging_info(_("Summary file %s written"), filename)
1020
1114
 
1021
1115
  return should_write_file
@@ -1042,14 +1136,14 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
1042
1136
  should_write_file = True # Default to writing new files
1043
1137
 
1044
1138
  # If file exists, ask user for confirmation
1045
- if self.filesystem.zip_file_exists():
1046
- zip_file_path = self.filesystem.zip_file_path()
1139
+ if self._local_filesystem.zip_file_exists():
1140
+ zip_file_path = self._local_filesystem.zip_file_path()
1047
1141
  msg = _("{} file already exists.\nDo you want to overwrite it?")
1048
1142
  should_write_file = ask_confirmation(_("Overwrite existing file"), msg.format(zip_file_path))
1049
1143
 
1050
1144
  if should_write_file:
1051
- self.filesystem.zip_files(files_to_zip)
1052
- zip_file_path = self.filesystem.zip_file_path()
1145
+ self._local_filesystem.zip_files(files_to_zip)
1146
+ zip_file_path = self._local_filesystem.zip_file_path()
1053
1147
  msg = _(
1054
1148
  "All relevant files have been zipped into the \n"
1055
1149
  "{zip_file_path} file.\n\nYou can now upload this file to the ArduPilot Methodic\n"
@@ -1061,18 +1155,75 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
1061
1155
 
1062
1156
  def repopulate_configuration_step_parameters(
1063
1157
  self,
1064
- ) -> tuple[bool, list[tuple[str, str]], list[tuple[str, str]]]:
1158
+ ) -> tuple[list[tuple[str, str]], list[tuple[str, str]]]:
1065
1159
  """
1066
1160
  Process the configuration step for the current file and update the self.parameters.
1067
1161
 
1068
1162
  Returns:
1069
- tuple: (config_step_edited, ui_errors, ui_infos)
1163
+ tuple: (ui_errors, ui_infos)
1070
1164
 
1071
1165
  """
1072
- self.parameters, config_step_edited, ui_errors, ui_infos = self.config_step_processor.process_configuration_step(
1073
- self.current_file, self.fc_parameters
1166
+ # Reset tracking sets when navigating to new file
1167
+ self._added_parameters.clear()
1168
+ self._deleted_parameters.clear()
1169
+
1170
+ # Process configuration step and get operations to apply
1171
+ self.current_step_parameters, ui_errors, ui_infos, duplicates_to_remove, renames_to_apply, derived_params = (
1172
+ self._config_step_processor.process_configuration_step(self.current_file, self.fc_parameters)
1074
1173
  )
1075
- return config_step_edited, ui_errors, ui_infos
1174
+
1175
+ # Apply derived parameters to domain model using specialized setters
1176
+ for param_name, derived_par in derived_params.items():
1177
+ if param_name in self.current_step_parameters:
1178
+ # Update existing forced/derived parameter with new value using dedicated setter
1179
+ # The setter methods will raise ValueError for invalid parameters (not forced/derived, readonly, etc.)
1180
+ try:
1181
+ self.current_step_parameters[param_name].set_forced_or_derived_value(float(derived_par.value))
1182
+ if derived_par.comment:
1183
+ self.current_step_parameters[param_name].set_forced_or_derived_change_reason(derived_par.comment)
1184
+ except (ValueError, TypeError) as e:
1185
+ logging_error(
1186
+ _("Failed to apply derived parameter %s: %s"),
1187
+ param_name,
1188
+ str(e),
1189
+ )
1190
+ else:
1191
+ # Parameter in derived_params but not in self.parameters - this is unexpected
1192
+ logging_error(
1193
+ _("Derived parameter %s not found in current parameters, skipping"),
1194
+ param_name,
1195
+ )
1196
+
1197
+ # Apply rename operations to domain model using add/delete tracking
1198
+ for old_name in duplicates_to_remove:
1199
+ # Mark duplicate as deleted
1200
+ if old_name in self._local_filesystem.file_parameters.get(self.current_file, ParDict()):
1201
+ self._deleted_parameters.add(old_name)
1202
+ # Remove from domain model
1203
+ if old_name in self.current_step_parameters:
1204
+ del self.current_step_parameters[old_name]
1205
+
1206
+ for old_name, new_name in renames_to_apply:
1207
+ # Get the parameter value from the original file
1208
+ original_params = self._local_filesystem.file_parameters.get(self.current_file, ParDict())
1209
+ if old_name in original_params:
1210
+ # Mark old parameter as deleted
1211
+ self._deleted_parameters.add(old_name)
1212
+
1213
+ # Create new parameter with renamed name
1214
+ old_par = original_params[old_name]
1215
+ self.current_step_parameters[new_name] = self._config_step_processor.create_ardupilot_parameter(
1216
+ new_name, old_par, self.current_file, self.fc_parameters
1217
+ )
1218
+
1219
+ # Mark new parameter as added
1220
+ self._added_parameters.add(new_name)
1221
+
1222
+ # Remove old parameter from domain model
1223
+ if old_name in self.current_step_parameters:
1224
+ del self.current_step_parameters[old_name]
1225
+
1226
+ return ui_errors, ui_infos
1076
1227
 
1077
1228
  def get_different_parameters(self) -> dict[str, ArduPilotParameter]:
1078
1229
  """
@@ -1082,7 +1233,7 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
1082
1233
  Dictionary of parameters that are different from FC
1083
1234
 
1084
1235
  """
1085
- return self.config_step_processor.filter_different_parameters(self.parameters)
1236
+ return self._config_step_processor.filter_different_parameters(self.current_step_parameters)
1086
1237
 
1087
1238
  def delete_parameter_from_current_file(self, param_name: str) -> None:
1088
1239
  """
@@ -1092,18 +1243,30 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
1092
1243
  param_name: The name of the parameter to delete
1093
1244
 
1094
1245
  """
1095
- del self.current_file_parameters[param_name]
1096
- if param_name in self.parameters:
1097
- del self.parameters[param_name]
1246
+ # If parameter was in original file, mark as deleted
1247
+ if param_name in self._local_filesystem.file_parameters.get(self.current_file, ParDict()):
1248
+ self._deleted_parameters.add(param_name)
1249
+
1250
+ # If it was previously added in this session, remove from added set
1251
+ self._added_parameters.discard(param_name)
1252
+
1253
+ # Remove from runtime state
1254
+ if param_name in self.current_step_parameters:
1255
+ del self.current_step_parameters[param_name]
1098
1256
 
1099
1257
  def get_possible_add_param_names(self) -> list[str]:
1100
1258
  """Return a sorted list of possible parameter names to add, or raise OperationNotPossibleError if not possible."""
1101
- param_dict = self.filesystem.doc_dict or self.fc_parameters
1259
+ param_dict = self._local_filesystem.doc_dict or self.fc_parameters
1102
1260
  if not param_dict:
1103
1261
  raise OperationNotPossibleError(
1104
1262
  _("No apm.pdef.xml file and no FC connected. Not possible autocomplete parameter names.")
1105
1263
  )
1106
- possible_add_param_names = [param_name for param_name in param_dict if param_name not in self.current_file_parameters]
1264
+
1265
+ # Build set of currently active parameters from domain model
1266
+ active_params = set(self.current_step_parameters.keys())
1267
+
1268
+ # Find parameters that aren't currently active
1269
+ possible_add_param_names = [param_name for param_name in param_dict if param_name not in active_params]
1107
1270
  possible_add_param_names.sort()
1108
1271
  return possible_add_param_names
1109
1272
 
@@ -1119,34 +1282,206 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
1119
1282
  if not param_name:
1120
1283
  raise InvalidParameterNameError(_("Parameter name can not be empty."))
1121
1284
 
1122
- if param_name in self.current_file_parameters:
1285
+ # Check if parameter already exists (in original file, added, or not deleted)
1286
+ original_file_params = self._local_filesystem.file_parameters.get(self.current_file, ParDict())
1287
+ is_in_original = param_name in original_file_params
1288
+ is_already_added = param_name in self._added_parameters
1289
+ is_deleted = param_name in self._deleted_parameters
1290
+
1291
+ if (is_in_original and not is_deleted) or is_already_added:
1123
1292
  raise InvalidParameterNameError(_("Parameter already exists, edit it instead"))
1124
1293
 
1125
1294
  fc_parameters = self.fc_parameters
1126
1295
  if fc_parameters:
1127
1296
  if param_name in fc_parameters:
1128
- self.current_file_parameters[param_name] = Par(fc_parameters[param_name], "")
1129
- self.parameters[param_name] = self.config_step_processor.create_ardupilot_parameter(
1130
- param_name, self.current_file_parameters[param_name], self.current_file, fc_parameters
1297
+ # Create the parameter in domain model
1298
+ par = Par(fc_parameters[param_name], "")
1299
+ self.current_step_parameters[param_name] = self._config_step_processor.create_ardupilot_parameter(
1300
+ param_name, par, self.current_file, fc_parameters
1131
1301
  )
1302
+
1303
+ # Track addition
1304
+ if not is_in_original:
1305
+ self._added_parameters.add(param_name)
1306
+ # If was previously deleted, remove from deleted set
1307
+ self._deleted_parameters.discard(param_name)
1308
+
1132
1309
  return True
1133
1310
  raise InvalidParameterNameError(_("Parameter name not found in the flight controller."))
1134
1311
 
1135
- if self.filesystem.doc_dict:
1136
- if param_name in self.filesystem.doc_dict:
1137
- self.current_file_parameters[param_name] = Par(
1138
- self.filesystem.param_default_dict.get(param_name, Par(0, "")).value, ""
1139
- )
1140
- self.parameters[param_name] = self.config_step_processor.create_ardupilot_parameter(
1141
- param_name, self.current_file_parameters[param_name], self.current_file, fc_parameters
1312
+ if self._local_filesystem.doc_dict:
1313
+ if param_name in self._local_filesystem.doc_dict:
1314
+ # Create the parameter in domain model
1315
+ par = Par(self._local_filesystem.param_default_dict.get(param_name, Par(0, "")).value, "")
1316
+ self.current_step_parameters[param_name] = self._config_step_processor.create_ardupilot_parameter(
1317
+ param_name, par, self.current_file, fc_parameters
1142
1318
  )
1319
+
1320
+ # Track addition
1321
+ if not is_in_original:
1322
+ self._added_parameters.add(param_name)
1323
+ # If was previously deleted, remove from deleted set
1324
+ self._deleted_parameters.discard(param_name)
1325
+
1143
1326
  return True
1144
1327
  raise InvalidParameterNameError(
1145
1328
  _("'{param_name}' not found in the apm.pdef.xml file.").format(param_name=param_name)
1146
1329
  )
1147
1330
 
1148
- if not fc_parameters and not self.filesystem.doc_dict:
1331
+ if not fc_parameters and not self._local_filesystem.doc_dict:
1149
1332
  raise OperationNotPossibleError(
1150
1333
  _("Can not add parameter when no FC is connected and no apm.pdef.xml file exists.")
1151
1334
  )
1152
1335
  return False
1336
+
1337
+ def get_parameters_as_par_dict(self, param_names: Optional[list[str]] = None) -> ParDict:
1338
+ """
1339
+ Extract Par objects from ArduPilotParameter domain models.
1340
+
1341
+ This method converts the domain model objects to data transfer objects (Par)
1342
+ that can be used for file operations or flight controller uploads.
1343
+
1344
+ Args:
1345
+ param_names: Optional list of parameter names to include.
1346
+ If None, includes all parameters.
1347
+
1348
+ Returns:
1349
+ ParDict containing Par objects with current values and change reasons
1350
+
1351
+ """
1352
+ if param_names is None:
1353
+ param_names = list(self.current_step_parameters.keys())
1354
+
1355
+ return ParDict(
1356
+ {
1357
+ name: Par(self.current_step_parameters[name].get_new_value(), self.current_step_parameters[name].change_reason)
1358
+ for name in param_names
1359
+ if name in self.current_step_parameters
1360
+ }
1361
+ )
1362
+
1363
+ def has_unsaved_changes(self) -> bool:
1364
+ """
1365
+ Check if any changes have been made that need to be saved.
1366
+
1367
+ This includes:
1368
+ - User edits to parameter values
1369
+ - Derived parameter changes (tracked via is_dirty)
1370
+ - Forced parameter changes (tracked via is_dirty)
1371
+ - Connection renaming changes (tracked via _added_parameters and _deleted_parameters)
1372
+ - Parameter additions
1373
+ - Parameter deletions
1374
+
1375
+ Returns:
1376
+ True if there are unsaved changes, False otherwise
1377
+
1378
+ """
1379
+ # Check for structural changes (additions/deletions, including from renames)
1380
+ if self._added_parameters or self._deleted_parameters:
1381
+ return True
1382
+
1383
+ # Check individual parameter edits (value or comment changes)
1384
+ return any(param.is_dirty for param in self.current_step_parameters.values())
1385
+
1386
+ def get_last_configuration_step_number(self) -> Optional[int]:
1387
+ if self._local_filesystem.configuration_phases:
1388
+ # Get the first two characters of the last configuration step filename
1389
+ last_step_filename = next(reversed(self._local_filesystem.file_parameters.keys()))
1390
+ return int(last_step_filename[:2]) + 1 if len(last_step_filename) >= 2 else 1
1391
+ return None
1392
+
1393
+ def get_sorted_phases_with_end_and_weight(self, last_step_nr: int) -> dict[str, PhaseData]:
1394
+ return self._local_filesystem.get_sorted_phases_with_end_and_weight(last_step_nr)
1395
+
1396
+ def get_vehicle_directory(self) -> str:
1397
+ return self._local_filesystem.vehicle_dir
1398
+
1399
+ def parameter_files(self) -> list[str]:
1400
+ return list(self._local_filesystem.file_parameters.keys())
1401
+
1402
+ def parameter_documentation_available(self) -> bool:
1403
+ return bool(self._local_filesystem.doc_dict)
1404
+
1405
+ def configuration_phases(self) -> dict[str, PhaseData]:
1406
+ return self._local_filesystem.configuration_phases
1407
+
1408
+ def _write_current_file(self) -> None:
1409
+ self._local_filesystem.write_last_uploaded_filename(self.current_file)
1410
+
1411
+ def export_current_file(self, annotate_doc: bool) -> None:
1412
+ # Convert domain model parameters to Par objects for export
1413
+ export_params = self.get_parameters_as_par_dict()
1414
+
1415
+ # Export to file
1416
+ self._local_filesystem.export_to_param(export_params, self.current_file, annotate_doc)
1417
+
1418
+ # Update the filesystem's file_parameters to match what was saved
1419
+ self._local_filesystem.file_parameters[self.current_file] = export_params
1420
+
1421
+ self._added_parameters.clear()
1422
+ self._deleted_parameters.clear()
1423
+ # copy parameters new values to their _values_on_file
1424
+ for param in self.current_step_parameters.values():
1425
+ param.copy_new_value_to_file()
1426
+
1427
+ def open_documentation_in_browser(self, filename: str) -> None:
1428
+ _blog_text, blog_url = self.get_documentation_text_and_url("blog", filename)
1429
+ _wiki_text, wiki_url = self.get_documentation_text_and_url("wiki", filename)
1430
+ _external_tool_text, external_tool_url = self.get_documentation_text_and_url("external_tool", filename)
1431
+ if wiki_url:
1432
+ webbrowser_open(url=wiki_url, new=0, autoraise=False)
1433
+ if external_tool_url:
1434
+ webbrowser_open(url=external_tool_url, new=0, autoraise=False)
1435
+ if blog_url:
1436
+ webbrowser_open(url=blog_url, new=0, autoraise=True)
1437
+
1438
+ # frontend_tkinter_parameter_editor.py API end
1439
+
1440
+ # frontend_tkinter_parameter_editor_documentation_frame.py API start
1441
+ def get_documentation_text_and_url(self, key: str, filename: Optional[str] = None) -> tuple[str, str]:
1442
+ if filename is None:
1443
+ filename = self.current_file
1444
+ return self._local_filesystem.get_documentation_text_and_url(filename, key)
1445
+
1446
+ def get_why_why_now_tooltip(self) -> str:
1447
+ why_tooltip_text = self._local_filesystem.get_seq_tooltip_text(self.current_file, "why")
1448
+ why_now_tooltip_text = self._local_filesystem.get_seq_tooltip_text(self.current_file, "why_now")
1449
+ tooltip_text = ""
1450
+ if why_tooltip_text:
1451
+ tooltip_text += _("Why: ") + _(why_tooltip_text) + "\n"
1452
+ if why_now_tooltip_text:
1453
+ tooltip_text += _("Why now: ") + _(why_now_tooltip_text)
1454
+ return tooltip_text
1455
+
1456
+ def get_documentation_frame_title(self) -> str:
1457
+ if self.current_file:
1458
+ title = _("{current_file} Documentation")
1459
+ return title.format(current_file=self.current_file)
1460
+ return _("Documentation")
1461
+
1462
+ def parse_mandatory_level_percentage(self, text: str) -> tuple[int, str]:
1463
+ """
1464
+ Parse and validate the mandatory level percentage from text.
1465
+
1466
+ Args:
1467
+ text: The text containing the mandatory level information
1468
+
1469
+ Returns:
1470
+ tuple: (percentage_value, tooltip_text)
1471
+ percentage_value: 0-100 for valid percentage, 0 for invalid
1472
+ tooltip_text: Formatted tooltip text
1473
+
1474
+ """
1475
+ current_file = self.current_file or ""
1476
+ try:
1477
+ # Extract up to 3 digits from the start of the mandatory text
1478
+ percentage = int("".join([c for c in text[:3] if c.isdigit()]))
1479
+ if 0 <= percentage <= 100:
1480
+ tooltip = _("This configuration step ({current_file} intermediate parameter file) is {percentage}% mandatory")
1481
+ return percentage, tooltip.format(current_file=current_file, percentage=percentage)
1482
+ raise ValueError
1483
+ except ValueError:
1484
+ tooltip = _("Mandatory level not available for this configuration step ({current_file})")
1485
+ return 0, tooltip.format(current_file=current_file)
1486
+
1487
+ # frontend_tkinter_parameter_editor_documentation_frame.py API end