ardupilot-methodic-configurator 2.6.1__py3-none-any.whl → 2.7.1__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 (107) hide show
  1. ardupilot_methodic_configurator/__init__.py +2 -2
  2. ardupilot_methodic_configurator/__main__.py +34 -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_freedesktop.py +275 -0
  8. ardupilot_methodic_configurator/backend_filesystem_json_with_schema.py +3 -3
  9. ardupilot_methodic_configurator/backend_filesystem_program_settings.py +26 -8
  10. ardupilot_methodic_configurator/backend_filesystem_vehicle_components.py +3 -3
  11. ardupilot_methodic_configurator/backend_flightcontroller.py +37 -20
  12. ardupilot_methodic_configurator/backend_flightcontroller_info.py +1 -1
  13. ardupilot_methodic_configurator/backend_internet.py +1 -1
  14. ardupilot_methodic_configurator/backend_mavftp.py +1 -1
  15. ardupilot_methodic_configurator/battery_cell_voltages.py +1 -1
  16. ardupilot_methodic_configurator/common_arguments.py +1 -1
  17. ardupilot_methodic_configurator/configuration_manager.py +561 -121
  18. ardupilot_methodic_configurator/configuration_steps_ArduCopter.json +7 -5
  19. ardupilot_methodic_configurator/configuration_steps_ArduPlane.json +3 -1
  20. ardupilot_methodic_configurator/configuration_steps_Heli.json +3 -1
  21. ardupilot_methodic_configurator/configuration_steps_Rover.json +3 -1
  22. ardupilot_methodic_configurator/configuration_steps_strings.py +5 -3
  23. ardupilot_methodic_configurator/data_model_ardupilot_parameter.py +55 -2
  24. ardupilot_methodic_configurator/data_model_configuration_step.py +96 -46
  25. ardupilot_methodic_configurator/data_model_fc_ids.py +16 -7
  26. ardupilot_methodic_configurator/data_model_motor_test.py +1 -1
  27. ardupilot_methodic_configurator/data_model_par_dict.py +25 -11
  28. ardupilot_methodic_configurator/data_model_software_updates.py +1 -1
  29. ardupilot_methodic_configurator/data_model_template_overview.py +1 -1
  30. ardupilot_methodic_configurator/data_model_vehicle_components.py +1 -1
  31. ardupilot_methodic_configurator/data_model_vehicle_components_base.py +3 -2
  32. ardupilot_methodic_configurator/data_model_vehicle_components_display.py +1 -1
  33. ardupilot_methodic_configurator/data_model_vehicle_components_import.py +2 -1
  34. ardupilot_methodic_configurator/data_model_vehicle_components_json_schema.py +1 -1
  35. ardupilot_methodic_configurator/data_model_vehicle_components_templates.py +1 -1
  36. ardupilot_methodic_configurator/data_model_vehicle_components_validation.py +41 -1
  37. ardupilot_methodic_configurator/data_model_vehicle_project.py +1 -1
  38. ardupilot_methodic_configurator/data_model_vehicle_project_creator.py +1 -1
  39. ardupilot_methodic_configurator/data_model_vehicle_project_opener.py +1 -1
  40. ardupilot_methodic_configurator/extract_param_defaults.py +1 -1
  41. ardupilot_methodic_configurator/frontend_tkinter_autoresize_combobox.py +1 -1
  42. ardupilot_methodic_configurator/frontend_tkinter_base_window.py +6 -2
  43. ardupilot_methodic_configurator/frontend_tkinter_component_editor.py +56 -29
  44. ardupilot_methodic_configurator/frontend_tkinter_component_editor_base.py +18 -13
  45. ardupilot_methodic_configurator/frontend_tkinter_component_template_manager.py +1 -1
  46. ardupilot_methodic_configurator/frontend_tkinter_connection_selection.py +1 -1
  47. ardupilot_methodic_configurator/frontend_tkinter_directory_selection.py +1 -1
  48. ardupilot_methodic_configurator/frontend_tkinter_entry_dynamic.py +1 -1
  49. ardupilot_methodic_configurator/frontend_tkinter_flightcontroller_info.py +1 -1
  50. ardupilot_methodic_configurator/frontend_tkinter_font.py +1 -1
  51. ardupilot_methodic_configurator/frontend_tkinter_motor_test.py +1 -1
  52. ardupilot_methodic_configurator/frontend_tkinter_pair_tuple_combobox.py +7 -1
  53. ardupilot_methodic_configurator/frontend_tkinter_parameter_editor.py +107 -156
  54. ardupilot_methodic_configurator/frontend_tkinter_parameter_editor_documentation_frame.py +24 -58
  55. ardupilot_methodic_configurator/frontend_tkinter_parameter_editor_table.py +24 -56
  56. ardupilot_methodic_configurator/frontend_tkinter_progress_window.py +1 -1
  57. ardupilot_methodic_configurator/frontend_tkinter_project_creator.py +1 -1
  58. ardupilot_methodic_configurator/frontend_tkinter_project_opener.py +1 -1
  59. ardupilot_methodic_configurator/frontend_tkinter_rich_text.py +1 -1
  60. ardupilot_methodic_configurator/frontend_tkinter_scroll_frame.py +1 -1
  61. ardupilot_methodic_configurator/frontend_tkinter_show.py +3 -3
  62. ardupilot_methodic_configurator/frontend_tkinter_software_update.py +1 -1
  63. ardupilot_methodic_configurator/frontend_tkinter_stage_progress.py +19 -29
  64. ardupilot_methodic_configurator/frontend_tkinter_template_overview.py +1 -1
  65. ardupilot_methodic_configurator/frontend_tkinter_usage_popup_window.py +1 -1
  66. ardupilot_methodic_configurator/internationalization.py +1 -1
  67. ardupilot_methodic_configurator/param_pid_adjustment_update.py +43 -39
  68. ardupilot_methodic_configurator/tempcal_imu.py +1 -1
  69. ardupilot_methodic_configurator/vehicle_components.py +1 -1
  70. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/AirCar_v1/14_logging.param +3 -3
  71. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/Big_Owl/14_logging.param +3 -3
  72. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/Chimera7/14_logging.param +3 -3
  73. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/FETtec-5/14_logging.param +3 -3
  74. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/GazeboIrisWithTargetFollow/14_logging.param +3 -3
  75. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/Holybro_X500/14_logging.param +3 -3
  76. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/Holybro_X500_V2/14_logging.param +3 -3
  77. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/Holybro_X650_LTE/14_logging.param +3 -3
  78. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/Hoverit_X11+/14_logging.param +3 -3
  79. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/Hoverit_X13/14_logging.param +3 -3
  80. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/Marmotte5v2/14_logging.param +3 -3
  81. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/ReadyToSkyZD550/14_logging.param +3 -3
  82. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/TarotFY680Hexacopter/05_remote_controller.param +1 -1
  83. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/TarotFY680Hexacopter/14_logging.param +3 -3
  84. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/TarotFY680Hexacopter/47_position_controller.param +2 -2
  85. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/Tarot_X4/14_logging.param +3 -3
  86. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/X11_plus/14_logging.param +3 -3
  87. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/diatone_taycan_mxc/4.3.8-params/14_logging.param +3 -3
  88. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/diatone_taycan_mxc/4.4.4-params/14_logging.param +3 -3
  89. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/diatone_taycan_mxc/4.5.x-params/14_logging.param +3 -3
  90. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/diatone_taycan_mxc/4.6.x-params/14_logging.param +3 -3
  91. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/empty_4.5.x/10_gnss.param +1 -1
  92. ardupilot_methodic_configurator/vehicle_templates/ArduCopter/empty_4.6.x/10_gnss.param +1 -1
  93. {ardupilot_methodic_configurator-2.6.1.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/METADATA +12 -7
  94. {ardupilot_methodic_configurator-2.6.1.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/RECORD +107 -106
  95. {ardupilot_methodic_configurator-2.6.1.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/licenses/credits/CREDITS.md +1 -0
  96. {ardupilot_methodic_configurator-2.6.1.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/WHEEL +0 -0
  97. {ardupilot_methodic_configurator-2.6.1.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/entry_points.txt +0 -0
  98. {ardupilot_methodic_configurator-2.6.1.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/licenses/LICENSE.md +0 -0
  99. {ardupilot_methodic_configurator-2.6.1.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/licenses/LICENSES/Apache-2.0.txt +0 -0
  100. {ardupilot_methodic_configurator-2.6.1.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/licenses/LICENSES/BSD-3-Clause.txt +0 -0
  101. {ardupilot_methodic_configurator-2.6.1.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/licenses/LICENSES/GPL-3.0-or-later.txt +0 -0
  102. {ardupilot_methodic_configurator-2.6.1.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/licenses/LICENSES/LGPL-3.0-or-later.txt +0 -0
  103. {ardupilot_methodic_configurator-2.6.1.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/licenses/LICENSES/MIT-CMU.txt +0 -0
  104. {ardupilot_methodic_configurator-2.6.1.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/licenses/LICENSES/MIT.txt +0 -0
  105. {ardupilot_methodic_configurator-2.6.1.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/licenses/LICENSES/MPL-2.0.txt +0 -0
  106. {ardupilot_methodic_configurator-2.6.1.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/licenses/LICENSES/PSF-2.0.txt +0 -0
  107. {ardupilot_methodic_configurator-2.6.1.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/top_level.txt +0 -0
@@ -3,7 +3,7 @@
3
3
  """
4
4
  A combobox GUI with support for complex lists.
5
5
 
6
- This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator
6
+ This file is part of ArduPilot Methodic Configurator. https://github.com/ArduPilot/MethodicConfigurator
7
7
 
8
8
  SPDX-FileCopyrightText: 2024-2025 Amilcar do Carmo Lucas <amilcar.lucas@iav.de>
9
9
 
@@ -111,6 +111,12 @@ class PairTupleCombobox(ttk.Combobox): # pylint: disable=too-many-ancestors
111
111
  setup_combobox_mousewheel_handling(self)
112
112
 
113
113
  def set_entries_tuple(self, list_pair_tuple: list[tuple[str, str]], selected_element: Union[None, str]) -> None:
114
+ # Clear existing entries before setting new ones
115
+ self.list_keys.clear()
116
+ self.list_shows.clear()
117
+ self.append_entries_tuple(list_pair_tuple, selected_element)
118
+
119
+ def append_entries_tuple(self, list_pair_tuple: list[tuple[str, str]], selected_element: Union[None, str]) -> None:
114
120
  if isinstance(list_pair_tuple, list):
115
121
  for tpl in list_pair_tuple:
116
122
  self.list_keys.append(tpl[0])
@@ -3,7 +3,7 @@
3
3
  """
4
4
  Parameter editor GUI.
5
5
 
6
- This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator
6
+ This file is part of ArduPilot Methodic Configurator. https://github.com/ArduPilot/MethodicConfigurator
7
7
 
8
8
  SPDX-FileCopyrightText: 2024-2025 Amilcar do Carmo Lucas <amilcar.lucas@iav.de>
9
9
 
@@ -11,7 +11,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
11
11
  """
12
12
 
13
13
  import sys
14
- import time
15
14
  import tkinter as tk
16
15
  from argparse import ArgumentParser, Namespace
17
16
 
@@ -19,23 +18,24 @@ from argparse import ArgumentParser, Namespace
19
18
  from logging import basicConfig as logging_basicConfig
20
19
  from logging import error as logging_error
21
20
  from logging import getLevelName as logging_getLevelName
22
- from logging import info as logging_info
23
21
  from logging import warning as logging_warning
24
22
  from tkinter import filedialog, messagebox, ttk
25
- from typing import Literal, Optional, Union
23
+ from typing import Optional, Union
26
24
 
27
25
  # from logging import critical as logging_critical
28
26
  from webbrowser import open as webbrowser_open # to open the blog post documentation
29
27
 
30
28
  from ardupilot_methodic_configurator import _, __version__
31
29
  from ardupilot_methodic_configurator.backend_filesystem import LocalFilesystem
30
+ from ardupilot_methodic_configurator.backend_filesystem_freedesktop import FreeDesktop
32
31
  from ardupilot_methodic_configurator.backend_filesystem_program_settings import ProgramSettings
33
32
  from ardupilot_methodic_configurator.backend_flightcontroller import FlightController
34
33
  from ardupilot_methodic_configurator.common_arguments import add_common_arguments
35
- from ardupilot_methodic_configurator.configuration_manager import ConfigurationManager
34
+ from ardupilot_methodic_configurator.configuration_manager import ConfigurationManager, ExperimentChoice
36
35
  from ardupilot_methodic_configurator.frontend_tkinter_autoresize_combobox import AutoResizeCombobox
37
36
  from ardupilot_methodic_configurator.frontend_tkinter_base_window import (
38
37
  BaseWindow,
38
+ ask_retry_cancel_popup,
39
39
  ask_yesno_popup,
40
40
  show_error_popup,
41
41
  show_info_popup,
@@ -156,10 +156,7 @@ class ParameterEditorWindow(BaseWindow): # pylint: disable=too-many-instance-at
156
156
  def __init__(self, configuration_manager: ConfigurationManager) -> None:
157
157
  super().__init__()
158
158
  self.configuration_manager = configuration_manager
159
- # Maintain backward compatibility with existing code
160
- self.local_filesystem = configuration_manager.filesystem
161
159
 
162
- self.at_least_one_changed_parameter_written = False
163
160
  self.file_selection_combobox: AutoResizeCombobox
164
161
  self.show_only_differences: tk.BooleanVar
165
162
  self.annotate_params_into_files: tk.BooleanVar
@@ -169,7 +166,6 @@ class ParameterEditorWindow(BaseWindow): # pylint: disable=too-many-instance-at
169
166
  self.tempcal_imu_progress_window: ProgressWindow
170
167
  self.file_upload_progress_window: ProgressWindow
171
168
  self.skip_button: ttk.Button
172
- self.last_time_asked_to_save: float = 0
173
169
  self.gui_complexity = str(ProgramSettings.get_setting("gui_complexity"))
174
170
 
175
171
  self.root.title(
@@ -193,20 +189,15 @@ class ParameterEditorWindow(BaseWindow): # pylint: disable=too-many-instance-at
193
189
 
194
190
  self.__create_conf_widgets(__version__)
195
191
 
196
- if self.local_filesystem.configuration_phases:
197
- # Get the first two characters of the last configuration step filename
198
- last_step_filename = next(reversed(self.local_filesystem.file_parameters.keys()))
199
- last_step_nr = int(last_step_filename[:2]) + 1 if len(last_step_filename) >= 2 else 1
192
+ last_step_nr = self.configuration_manager.get_last_configuration_step_number()
193
+ if last_step_nr is not None:
194
+ phases = self.configuration_manager.get_sorted_phases_with_end_and_weight(last_step_nr)
200
195
 
201
- self.stage_progress_bar = StageProgressBar(
202
- self.main_frame, self.local_filesystem.configuration_phases, last_step_nr, self.gui_complexity
203
- )
196
+ self.stage_progress_bar = StageProgressBar(self.main_frame, phases, last_step_nr, self.gui_complexity)
204
197
  self.stage_progress_bar.pack(side=tk.TOP, fill="x", expand=False, pady=(2, 2), padx=(4, 4))
205
198
 
206
199
  # Create a DocumentationFrame object for the Documentation Content
207
- self.documentation_frame = DocumentationFrame(
208
- self.main_frame, self.local_filesystem, self.configuration_manager.current_file
209
- )
200
+ self.documentation_frame = DocumentationFrame(self.main_frame, self.configuration_manager)
210
201
  self.documentation_frame.documentation_frame.pack(side=tk.TOP, fill="x", expand=False, pady=(2, 2), padx=(4, 4))
211
202
 
212
203
  self.__create_parameter_area_widgets()
@@ -217,6 +208,10 @@ class ParameterEditorWindow(BaseWindow): # pylint: disable=too-many-instance-at
217
208
  # this one should be on top of the previous one hence the longer time
218
209
  if UsagePopupWindow.should_display("parameter_editor"):
219
210
  self.root.after(100, self.__display_usage_popup_window(self.root)) # type: ignore[arg-type]
211
+
212
+ # Set up startup notification for the main application window
213
+ FreeDesktop.setup_startup_notification(self.root) # type: ignore[arg-type]
214
+
220
215
  self.root.mainloop()
221
216
 
222
217
  def __create_conf_widgets(self, version: str) -> None:
@@ -231,7 +226,7 @@ class ParameterEditorWindow(BaseWindow): # pylint: disable=too-many-instance-at
231
226
  directory_selection_frame = VehicleDirectorySelectionWidgets(
232
227
  self,
233
228
  config_subframe,
234
- self.local_filesystem.vehicle_dir,
229
+ self.configuration_manager.get_vehicle_directory(),
235
230
  destroy_parent_on_open=False,
236
231
  )
237
232
  if self.gui_complexity != "simple":
@@ -250,7 +245,7 @@ class ParameterEditorWindow(BaseWindow): # pylint: disable=too-many-instance-at
250
245
  # Create Combobox for intermediate parameter file selection
251
246
  self.file_selection_combobox = AutoResizeCombobox(
252
247
  file_selection_frame,
253
- list(self.local_filesystem.file_parameters.keys()),
248
+ self.configuration_manager.parameter_files(),
254
249
  self.configuration_manager.current_file,
255
250
  _(
256
251
  "Select the intermediate parameter file from the list of available"
@@ -326,7 +321,7 @@ class ParameterEditorWindow(BaseWindow): # pylint: disable=too-many-instance-at
326
321
 
327
322
  # Create a Scrollable parameter editor table
328
323
  self.parameter_editor_table = ParameterEditorTable(self.main_frame, self.configuration_manager, self)
329
- self.repopulate_parameter_table()
324
+ self.repopulate_parameter_table(regenerate_from_disk=True)
330
325
  self.parameter_editor_table.pack(side="top", fill="both", expand=True)
331
326
 
332
327
  # Create a frame for the buttons
@@ -354,7 +349,7 @@ class ParameterEditorWindow(BaseWindow): # pylint: disable=too-many-instance-at
354
349
  annotate_params_checkbox = ttk.Checkbutton(
355
350
  checkboxes_frame,
356
351
  text=_("Annotate docs into .param files"),
357
- state="normal" if self.local_filesystem.doc_dict else "disabled",
352
+ state="normal" if self.configuration_manager.parameter_documentation_available() else "disabled",
358
353
  variable=self.annotate_params_into_files,
359
354
  command=lambda: ProgramSettings.set_setting(
360
355
  "annotate_docs_into_param_files", self.annotate_params_into_files.get()
@@ -419,7 +414,7 @@ class ParameterEditorWindow(BaseWindow): # pylint: disable=too-many-instance-at
419
414
  state=(
420
415
  "normal"
421
416
  if self.gui_complexity != "simple"
422
- or self.configuration_manager.is_configuration_step_optional(self.configuration_manager.current_file)
417
+ or self.configuration_manager.is_configuration_step_optional()
423
418
  or not self.configuration_manager.is_fc_connected
424
419
  else "disabled"
425
420
  )
@@ -505,7 +500,7 @@ class ParameterEditorWindow(BaseWindow): # pylint: disable=too-many-instance-at
505
500
 
506
501
  try:
507
502
  # Inject GUI callbacks into business logic workflow
508
- success = self.configuration_manager.handle_imu_temperature_calibration_workflow(
503
+ _success = self.configuration_manager.handle_imu_temperature_calibration_workflow(
509
504
  selected_file,
510
505
  ask_user_confirmation=ask_yesno_popup,
511
506
  select_file=select_file,
@@ -514,47 +509,43 @@ class ParameterEditorWindow(BaseWindow): # pylint: disable=too-many-instance-at
514
509
  progress_callback=self.tempcal_imu_progress_window.update_progress_bar_300_pct,
515
510
  )
516
511
 
517
- if success:
518
- # Force writing doc annotations to file
519
- self.parameter_editor_table.set_at_least_one_param_edited(True)
520
-
521
512
  finally:
522
513
  self.tempcal_imu_progress_window.destroy()
523
514
 
524
- def __handle_dialog_choice(self, result: list, dialog: tk.Toplevel, choice: Optional[bool]) -> None:
515
+ def __handle_dialog_choice(self, result: list, dialog: tk.Toplevel, choice: ExperimentChoice) -> None:
525
516
  result.append(choice)
526
517
  dialog.destroy()
527
518
 
528
- def __should_copy_fc_values_to_file(self, selected_file: str) -> None: # pylint: disable=too-many-locals
529
- should_copy, relevant_fc_params, auto_changed_by = self.configuration_manager.should_copy_fc_values_to_file(
530
- selected_file
531
- )
532
- if should_copy and relevant_fc_params and auto_changed_by:
533
- msg = _(
534
- "This configuration step requires external changes by: {auto_changed_by}\n\n"
535
- "The external tool experiment procedure is described in the tuning guide.\n\n"
536
- "Choose an option:\n"
537
- "* CLOSE - Close the application and go perform the experiment\n"
538
- "* YES - Copy current FC values to {selected_file} (if you've already completed the experiment)\n"
539
- "* NO - Continue without copying values (if you haven't performed the experiment yet,"
540
- " but know what you are doing)"
541
- ).format(auto_changed_by=auto_changed_by, selected_file=selected_file)
542
-
519
+ def __should_copy_fc_values_to_file(self, selected_file: str) -> None:
520
+ def ask_user_choice(title: str, message: str, options: list[str]) -> ExperimentChoice: # pylint: disable=too-many-locals
521
+ """GUI callback for asking user choice with custom dialog."""
543
522
  # Create custom dialog with Close, Yes, No buttons
544
523
  dialog = tk.Toplevel(self.root)
545
524
  # Hide dialog initially to prevent flickering
546
525
  dialog.withdraw()
547
526
  dialog.transient(self.root)
548
- dialog.title(_("Update file with values from FC?"))
527
+ dialog.title(title)
549
528
  dialog.resizable(width=False, height=False)
550
529
  dialog.protocol("WM_DELETE_WINDOW", dialog.destroy)
551
530
 
552
531
  # Message text
553
- message_label = tk.Label(dialog, text=msg, justify=tk.LEFT, padx=20, pady=10)
532
+ message_label = tk.Label(dialog, text=message, justify=tk.LEFT, padx=20, pady=10)
554
533
  message_label.pack(padx=10, pady=10)
555
534
 
535
+ # Clickable link to tuning guide
536
+ safe_font_config = get_safe_font_config()
537
+ link_label = tk.Label(
538
+ dialog,
539
+ text=_("Click here to open the Tuning Guide relevant Section"),
540
+ fg="blue",
541
+ cursor="hand2",
542
+ font=(str(safe_font_config["family"]), int(safe_font_config["size"]), "underline"),
543
+ )
544
+ link_label.pack(pady=(0, 10))
545
+ link_label.bind("<Button-1>", lambda _e: self.configuration_manager.open_documentation_in_browser(selected_file))
546
+
556
547
  # Result variable
557
- result: list[Optional[Literal[True, False]]] = [None]
548
+ result: list[ExperimentChoice] = []
558
549
 
559
550
  # Button frame
560
551
  button_frame = tk.Frame(dialog)
@@ -563,25 +554,31 @@ class ParameterEditorWindow(BaseWindow): # pylint: disable=too-many-instance-at
563
554
  # Close button (default)
564
555
  close_button = tk.Button(
565
556
  button_frame,
566
- text=_("Close"),
557
+ text=options[0], # "Close"
567
558
  width=10,
568
- command=lambda: self.__handle_dialog_choice(result, dialog, choice=None),
559
+ command=lambda: self.__handle_dialog_choice(result, dialog, choice="close"),
569
560
  )
570
561
  close_button.pack(side=tk.LEFT, padx=5)
571
562
 
572
563
  # Yes button
573
564
  yes_button = tk.Button(
574
- button_frame, text=_("Yes"), width=10, command=lambda: self.__handle_dialog_choice(result, dialog, choice=True)
565
+ button_frame,
566
+ text=options[1],
567
+ width=10, # "Yes"
568
+ command=lambda: self.__handle_dialog_choice(result, dialog, choice=True),
575
569
  )
576
570
  yes_button.pack(side=tk.LEFT, padx=5)
577
571
 
578
572
  # No button
579
573
  no_button = tk.Button(
580
- button_frame, text=_("No"), width=10, command=lambda: self.__handle_dialog_choice(result, dialog, choice=False)
574
+ button_frame,
575
+ text=options[2],
576
+ width=10, # "No"
577
+ command=lambda: self.__handle_dialog_choice(result, dialog, choice=False),
581
578
  )
582
579
  no_button.pack(side=tk.LEFT, padx=5)
583
580
 
584
- dialog.bind("<Return>", lambda _event: self.__handle_dialog_choice(result, dialog, None))
581
+ dialog.bind("<Return>", lambda _event: self.__handle_dialog_choice(result, dialog, choice="close"))
585
582
 
586
583
  # Center the dialog on the parent window
587
584
  dialog.deiconify()
@@ -604,25 +601,27 @@ class ParameterEditorWindow(BaseWindow): # pylint: disable=too-many-instance-at
604
601
 
605
602
  # Wait until dialog is closed
606
603
  self.root.wait_window(dialog)
607
- response = result[-1] if len(result) > 1 else None
604
+ return result[-1] if result else "close"
608
605
 
609
- if response is True: # Yes option
610
- params_copied = self.configuration_manager.copy_fc_values_to_file(selected_file, relevant_fc_params)
611
- if params_copied:
612
- self.parameter_editor_table.set_at_least_one_param_edited(True)
613
- elif response is None: # Close option
614
- sys.exit(0)
615
- # If response is False (No option), do nothing and continue
606
+ result = self.configuration_manager.handle_copy_fc_values_workflow(
607
+ selected_file,
608
+ ask_user_choice,
609
+ show_info_popup,
610
+ )
611
+
612
+ if result == "close":
613
+ # User chose to close the application
614
+ sys.exit(0)
616
615
 
617
616
  def __should_jump_to_file(self, selected_file: str) -> str:
618
- jump_options = self.configuration_manager.get_file_jump_options(selected_file)
619
- for dest_file, msg in jump_options.items():
620
- if self.gui_complexity == "simple" or messagebox.askyesno(
621
- _("Skip some steps?"), _(msg) if msg else _("Skip to {dest_file}?").format(**locals())
622
- ):
623
- self.file_selection_combobox.set(dest_file)
624
- return dest_file
625
- return selected_file
617
+ dest_file = self.configuration_manager.handle_file_jump_workflow(
618
+ selected_file,
619
+ self.gui_complexity,
620
+ ask_yesno_popup,
621
+ )
622
+ if dest_file != selected_file:
623
+ self.file_selection_combobox.set(dest_file)
624
+ return dest_file
626
625
 
627
626
  def __should_download_file_from_url(self, selected_file: str) -> None:
628
627
  self.configuration_manager.should_download_file_from_url_workflow(
@@ -657,22 +656,22 @@ class ParameterEditorWindow(BaseWindow): # pylint: disable=too-many-instance-at
657
656
  self.__do_tempcal_imu(selected_file)
658
657
  # open the documentation of the next step in the browser,
659
658
  # before giving the user the option to close the SW in the __should_copy_fc_values_to_file method
660
- self.documentation_frame.open_documentation_in_browser(selected_file)
659
+ if self.documentation_frame.get_auto_open_documentation_in_browser() or self.gui_complexity == "simple":
660
+ self.configuration_manager.open_documentation_in_browser(selected_file)
661
661
  self.__should_copy_fc_values_to_file(selected_file)
662
662
  selected_file = self.__should_jump_to_file(selected_file)
663
663
  self.__should_download_file_from_url(selected_file)
664
664
  self.__should_upload_file_to_fc(selected_file)
665
665
 
666
- # Update the current_file attribute to the selected file
666
+ # current_file might have been changed by jump, so update again
667
667
  self.configuration_manager.current_file = selected_file
668
- self.at_least_one_changed_parameter_written = False
669
- self.documentation_frame.refresh_documentation_labels(selected_file)
670
- self.documentation_frame.update_why_why_now_tooltip(selected_file)
671
- self.repopulate_parameter_table()
668
+ self.documentation_frame.refresh_documentation_labels()
669
+ self.documentation_frame.update_why_why_now_tooltip()
670
+ self.repopulate_parameter_table(regenerate_from_disk=True)
672
671
  self._update_skip_button_state()
673
672
 
674
673
  def _update_progress_bar_from_file(self, selected_file: str) -> None:
675
- if self.local_filesystem.configuration_phases:
674
+ if self.configuration_manager.configuration_phases():
676
675
  try:
677
676
  step_nr = int(selected_file[:2])
678
677
  self.stage_progress_bar.update_progress(step_nr)
@@ -690,41 +689,18 @@ class ParameterEditorWindow(BaseWindow): # pylint: disable=too-many-instance-at
690
689
  if not redownload:
691
690
  self.on_param_file_combobox_change(None, forced=True) # the initial param read will trigger a table update
692
691
 
693
- def repopulate_parameter_table(self) -> None:
692
+ def repopulate_parameter_table(self, regenerate_from_disk: bool) -> None:
694
693
  if not self.configuration_manager.current_file:
695
694
  return # no file was yet selected, so skip it
696
695
  # Re-populate the table with the new parameters
697
- self.parameter_editor_table.repopulate(self.show_only_differences.get(), self.gui_complexity)
696
+ self.parameter_editor_table.repopulate(self.show_only_differences.get(), self.gui_complexity, regenerate_from_disk)
698
697
 
699
698
  def on_show_only_changed_checkbox_change(self) -> None:
700
- self.repopulate_parameter_table()
701
-
702
- def upload_params_that_require_reset(self, selected_params: dict) -> None:
703
- """
704
- Write only the selected parameters to the flight controller that require a reset.
705
-
706
- After the reset, the other parameters that do not require a reset must still be written to the flight controller.
707
- """
708
- self.reset_progress_window = ProgressWindow(
709
- self.root,
710
- _("Resetting Flight Controller"),
711
- _("Waiting for {} of {} seconds"),
712
- only_show_when_update_progress_called=True,
713
- )
714
-
715
- if self.configuration_manager.upload_parameters_that_require_reset_workflow(
716
- selected_params,
717
- ask_confirmation=ask_yesno_popup,
718
- show_error=show_error_popup,
719
- progress_callback=self.reset_progress_window.update_progress_bar,
720
- ):
721
- self.at_least_one_changed_parameter_written = True
722
-
723
- self.reset_progress_window.destroy() # for the case that we are doing a test and there is no real FC connected
699
+ self.repopulate_parameter_table(regenerate_from_disk=False)
724
700
 
725
701
  def on_upload_selected_click(self) -> None:
726
702
  self.write_changes_to_intermediate_parameter_file()
727
- selected_params = self.parameter_editor_table.get_upload_selected_params(str(self.gui_complexity))
703
+ selected_params = self.parameter_editor_table.get_upload_selected_params(self.gui_complexity)
728
704
  if selected_params:
729
705
  if self.configuration_manager.fc_parameters:
730
706
  self.upload_selected_params(selected_params)
@@ -741,41 +717,31 @@ class ParameterEditorWindow(BaseWindow): # pylint: disable=too-many-instance-at
741
717
 
742
718
  # This function can recurse multiple times if there is an upload error
743
719
  def upload_selected_params(self, selected_params: dict) -> None:
744
- logging_info(
745
- _("Uploading %d selected %s parameters to flight controller..."),
746
- len(selected_params),
747
- self.configuration_manager.current_file,
720
+ # Create progress windows
721
+ self.reset_progress_window = ProgressWindow(
722
+ self.root,
723
+ _("Resetting Flight Controller"),
724
+ _("Waiting for {} of {} seconds"),
725
+ only_show_when_update_progress_called=True,
748
726
  )
749
-
750
- self.upload_params_that_require_reset(selected_params)
751
-
752
- # Use ConfigurationManager to handle the business logic
753
- nr_changed = self.configuration_manager.upload_selected_parameters_workflow(
754
- selected_params, show_error=show_error_popup
727
+ self.param_download_progress_window = ProgressWindow(
728
+ self.root,
729
+ _("Re-downloading FC parameters"),
730
+ _("Downloaded {} of {} parameters"),
755
731
  )
756
732
 
757
- # Update GUI state if any parameters were changed
758
- if nr_changed > 0:
759
- self.at_least_one_changed_parameter_written = True
760
-
761
- if self.at_least_one_changed_parameter_written:
762
- # Re-download all parameters, in case one of them changed, and validate that all uploads were successful
763
- self.download_flight_controller_parameters(redownload=True)
764
- param_upload_error = self.configuration_manager.validate_uploaded_parameters(selected_params)
765
-
766
- if param_upload_error:
767
- if messagebox.askretrycancel(
768
- _("Parameter upload error"),
769
- _("Failed to upload the following parameters to the flight controller:\n")
770
- + f"{(', ').join(param_upload_error)}",
771
- ):
772
- self.upload_selected_params(selected_params)
773
- else:
774
- logging_info(_("All parameters uploaded to the flight controller successfully"))
775
-
776
- self.configuration_manager.export_fc_params_missing_or_different()
777
-
778
- self.local_filesystem.write_last_uploaded_filename(self.configuration_manager.current_file)
733
+ try:
734
+ self.configuration_manager.upload_selected_params_workflow(
735
+ selected_params,
736
+ ask_confirmation=ask_yesno_popup,
737
+ ask_retry_cancel=ask_retry_cancel_popup,
738
+ show_error=show_error_popup,
739
+ progress_callback_for_reset=self.reset_progress_window.update_progress_bar,
740
+ progress_callback_for_download=self.param_download_progress_window.update_progress_bar,
741
+ )
742
+ finally:
743
+ self.reset_progress_window.destroy()
744
+ self.param_download_progress_window.destroy()
779
745
 
780
746
  def on_download_last_flight_log_click(self) -> None:
781
747
  """Handle the download last flight log button click."""
@@ -811,7 +777,7 @@ class ParameterEditorWindow(BaseWindow): # pylint: disable=too-many-instance-at
811
777
  skip_button_state = (
812
778
  "normal"
813
779
  if self.gui_complexity != "simple"
814
- or self.configuration_manager.is_configuration_step_optional(self.configuration_manager.current_file)
780
+ or self.configuration_manager.is_configuration_step_optional()
815
781
  or not self.configuration_manager.is_fc_connected
816
782
  else "disabled"
817
783
  )
@@ -821,7 +787,7 @@ class ParameterEditorWindow(BaseWindow): # pylint: disable=too-many-instance-at
821
787
  self.write_changes_to_intermediate_parameter_file()
822
788
 
823
789
  # Use ConfigurationManager to get the next non-optional file
824
- next_file = self.configuration_manager.get_next_non_optional_file(self.configuration_manager.current_file)
790
+ next_file = self.configuration_manager.get_next_non_optional_file()
825
791
 
826
792
  if next_file is None:
827
793
  # No more files to process, write summary and close
@@ -839,25 +805,10 @@ class ParameterEditorWindow(BaseWindow): # pylint: disable=too-many-instance-at
839
805
  self.on_param_file_combobox_change(None)
840
806
 
841
807
  def write_changes_to_intermediate_parameter_file(self) -> None:
842
- elapsed_since_last_ask = time.time() - self.last_time_asked_to_save
843
- # if annotate parameters into files is true, we always need to write to file, because
844
- # the parameter metadata might have changed, or not be present in the file.
845
- # In that situation, avoid asking multiple times to write the file, by checking the time last asked
846
- # But only if self.annotate_params_into_files.get()
847
- if self.parameter_editor_table.get_at_least_one_param_edited() or (
848
- self.annotate_params_into_files.get() and elapsed_since_last_ask > 1.0
849
- ):
850
- msg = _("Do you want to write the changes to the {current_filename} file?").format(
851
- current_filename=self.configuration_manager.current_file
852
- )
853
- if messagebox.askyesno(_("One or more parameters have been edited"), msg.format(**locals())):
854
- self.local_filesystem.export_to_param(
855
- self.local_filesystem.file_parameters[self.configuration_manager.current_file],
856
- self.configuration_manager.current_file,
857
- annotate_doc=self.annotate_params_into_files.get(),
858
- )
859
- self.parameter_editor_table.set_at_least_one_param_edited(False)
860
- self.last_time_asked_to_save = time.time()
808
+ self.configuration_manager.handle_write_changes_workflow(
809
+ self.annotate_params_into_files.get(),
810
+ ask_yesno_popup,
811
+ )
861
812
 
862
813
  def close_connection_and_quit(self) -> None:
863
814
  focused_widget = self.parameter_editor_table.view_port.focus_get()
@@ -1,7 +1,7 @@
1
1
  """
2
2
  The documentation frame containing the documentation for the current configuration step.
3
3
 
4
- This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator
4
+ This file is part of ArduPilot Methodic Configurator. https://github.com/ArduPilot/MethodicConfigurator
5
5
 
6
6
  SPDX-FileCopyrightText: 2024-2025 Amilcar do Carmo Lucas <amilcar.lucas@iav.de>
7
7
 
@@ -11,11 +11,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
11
11
  import tkinter as tk
12
12
  from platform import system as platform_system
13
13
  from tkinter import ttk
14
- from webbrowser import open as webbrowser_open # to open the blog post documentation
14
+ from webbrowser import open as webbrowser_open # to open the web documentation
15
15
 
16
16
  from ardupilot_methodic_configurator import _
17
- from ardupilot_methodic_configurator.backend_filesystem import LocalFilesystem
18
17
  from ardupilot_methodic_configurator.backend_filesystem_program_settings import ProgramSettings
18
+ from ardupilot_methodic_configurator.configuration_manager import ConfigurationManager
19
19
  from ardupilot_methodic_configurator.frontend_tkinter_rich_text import get_widget_font_family_and_size
20
20
  from ardupilot_methodic_configurator.frontend_tkinter_show import show_tooltip
21
21
 
@@ -49,16 +49,16 @@ class DocumentationFrame:
49
49
  ),
50
50
  )
51
51
 
52
- def __init__(self, root: tk.Widget, local_filesystem: LocalFilesystem, current_file: str) -> None:
52
+ def __init__(self, root: tk.Widget, configuration_manager: ConfigurationManager) -> None:
53
53
  self.root = root
54
- self.local_filesystem = local_filesystem
54
+ self.configuration_manager = configuration_manager
55
55
  self.documentation_frame: ttk.LabelFrame
56
56
  self.documentation_labels: dict[str, ttk.Label] = {}
57
57
  self.mandatory_level: ttk.Progressbar
58
58
  self.auto_open_var = tk.BooleanVar(value=bool(ProgramSettings.get_setting("auto_open_doc_in_browser")))
59
- self._create_documentation_frame(current_file)
59
+ self._create_documentation_frame()
60
60
 
61
- def _create_documentation_frame(self, current_file: str) -> None:
61
+ def _create_documentation_frame(self) -> None:
62
62
  self.documentation_frame = ttk.LabelFrame(self.root, text=_("Documentation"))
63
63
 
64
64
  # Create a grid structure within the documentation_frame
@@ -86,8 +86,8 @@ class DocumentationFrame:
86
86
  documentation_grid.columnconfigure(1, weight=1)
87
87
 
88
88
  # Dynamically update the documentation text and URL links
89
- self.refresh_documentation_labels(current_file)
90
- self.update_why_why_now_tooltip(current_file)
89
+ self.refresh_documentation_labels()
90
+ self.update_why_why_now_tooltip()
91
91
 
92
92
  def _create_bottom_row(self, documentation_grid: ttk.Frame, row: int) -> None:
93
93
  bottom_frame = ttk.Frame(documentation_grid)
@@ -111,52 +111,28 @@ class DocumentationFrame:
111
111
  )
112
112
  auto_open_checkbox.pack(side=tk.LEFT, expand=False)
113
113
 
114
- def update_why_why_now_tooltip(self, current_file: str) -> None:
115
- why_tooltip_text = self.local_filesystem.get_seq_tooltip_text(current_file, "why")
116
- why_now_tooltip_text = self.local_filesystem.get_seq_tooltip_text(current_file, "why_now")
117
- tooltip_text = ""
118
- if why_tooltip_text:
119
- tooltip_text += _("Why: ") + _(why_tooltip_text) + "\n"
120
- if why_now_tooltip_text:
121
- tooltip_text += _("Why now: ") + _(why_now_tooltip_text)
114
+ def update_why_why_now_tooltip(self) -> None:
115
+ tooltip_text = self.configuration_manager.get_why_why_now_tooltip()
122
116
  if tooltip_text:
123
117
  show_tooltip(self.documentation_frame, tooltip_text, position_below=False)
124
118
 
125
- def open_documentation_in_browser(self, current_file: str) -> None:
126
- _blog_text, blog_url = self.local_filesystem.get_documentation_text_and_url(current_file, "blog")
127
- _wiki_text, wiki_url = self.local_filesystem.get_documentation_text_and_url(current_file, "wiki")
128
- _external_tool_text, external_tool_url = self.local_filesystem.get_documentation_text_and_url(
129
- current_file, "external_tool"
130
- )
119
+ def get_auto_open_documentation_in_browser(self) -> bool:
120
+ return self.auto_open_var.get()
131
121
 
132
- if self.auto_open_var.get() or ProgramSettings.get_setting("gui_complexity") == "simple":
133
- if wiki_url:
134
- webbrowser_open(url=wiki_url, new=0, autoraise=False)
135
- if external_tool_url:
136
- webbrowser_open(url=external_tool_url, new=0, autoraise=False)
137
- if blog_url:
138
- webbrowser_open(url=blog_url, new=0, autoraise=True)
139
-
140
- def refresh_documentation_labels(self, current_file: str) -> None:
141
- if current_file:
142
- title = _("{current_file} Documentation")
143
- frame_title = title.format(**locals())
144
- else:
145
- frame_title = _("Documentation")
122
+ def refresh_documentation_labels(self) -> None:
123
+ frame_title = self.configuration_manager.get_documentation_frame_title()
146
124
  self.documentation_frame.config(text=frame_title)
147
125
 
148
- blog_text, blog_url = self.local_filesystem.get_documentation_text_and_url(current_file, "blog")
126
+ blog_text, blog_url = self.configuration_manager.get_documentation_text_and_url("blog")
149
127
  self._refresh_documentation_label(self.BLOG_LABEL, _(blog_text) if blog_text else "", blog_url)
150
- wiki_text, wiki_url = self.local_filesystem.get_documentation_text_and_url(current_file, "wiki")
128
+ wiki_text, wiki_url = self.configuration_manager.get_documentation_text_and_url("wiki")
151
129
  self._refresh_documentation_label(self.WIKI_LABEL, _(wiki_text) if wiki_text else "", wiki_url)
152
- external_tool_text, external_tool_url = self.local_filesystem.get_documentation_text_and_url(
153
- current_file, "external_tool"
154
- )
130
+ external_tool_text, external_tool_url = self.configuration_manager.get_documentation_text_and_url("external_tool")
155
131
  self._refresh_documentation_label(
156
132
  self.EXTERNAL_TOOL_LABEL, _(external_tool_text) if external_tool_text else "", external_tool_url
157
133
  )
158
- mandatory_text, _mandatory_url = self.local_filesystem.get_documentation_text_and_url(current_file, "mandatory")
159
- self._refresh_mandatory_level(current_file, mandatory_text)
134
+ mandatory_text, _mandatory_url = self.configuration_manager.get_documentation_text_and_url("mandatory")
135
+ self._refresh_mandatory_level(mandatory_text)
160
136
 
161
137
  def _refresh_documentation_label(self, label_key: str, text: str, url: str, url_expected: bool = True) -> None:
162
138
  label = self.documentation_labels[label_key]
@@ -175,17 +151,7 @@ class DocumentationFrame:
175
151
  if url_expected:
176
152
  show_tooltip(label, _("Documentation URL not available"))
177
153
 
178
- def _refresh_mandatory_level(self, current_file: str, text: str) -> None:
179
- _used_indirectly_by_the_tooltip = current_file
180
- try:
181
- # Extract up to 3 digits from the start of the mandatory text
182
- percentage = int("".join([c for c in text[:3] if c.isdigit()]))
183
- if 0 <= percentage <= 100:
184
- self.mandatory_level.config(value=percentage)
185
- tooltip = _("This configuration step ({current_file} intermediate parameter file) is {percentage}% mandatory")
186
- else:
187
- raise ValueError
188
- except ValueError:
189
- self.mandatory_level.config(value=0)
190
- tooltip = _("Mandatory level not available for this configuration step ({current_file})")
191
- show_tooltip(self.mandatory_level, tooltip.format(**locals()))
154
+ def _refresh_mandatory_level(self, text: str) -> None:
155
+ percentage, tooltip = self.configuration_manager.parse_mandatory_level_percentage(text)
156
+ self.mandatory_level.config(value=percentage)
157
+ show_tooltip(self.mandatory_level, tooltip)