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
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Python package initialization file. Loads translations and declares version information.
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
 
@@ -12,4 +12,4 @@ from ardupilot_methodic_configurator.internationalization import load_translatio
12
12
 
13
13
  _ = load_translation()
14
14
 
15
- __version__ = "2.6.1"
15
+ __version__ = "2.7.1"
@@ -11,7 +11,7 @@ Calls five sub-applications in sequence:
11
11
  4. Component and connection editor
12
12
  5. Parameter editor and uploader
13
13
 
14
- This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator
14
+ This file is part of ArduPilot Methodic Configurator. https://github.com/ArduPilot/MethodicConfigurator
15
15
 
16
16
  SPDX-FileCopyrightText: 2024-2025 Amilcar do Carmo Lucas <amilcar.lucas@iav.de>
17
17
 
@@ -19,6 +19,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
19
19
  """
20
20
 
21
21
  import argparse
22
+ import os
22
23
  from logging import basicConfig as logging_basicConfig
23
24
  from logging import debug as logging_debug
24
25
  from logging import error as logging_error
@@ -32,6 +33,7 @@ import argcomplete
32
33
 
33
34
  from ardupilot_methodic_configurator import _, __version__
34
35
  from ardupilot_methodic_configurator.backend_filesystem import LocalFilesystem
36
+ from ardupilot_methodic_configurator.backend_filesystem_freedesktop import FreeDesktop
35
37
  from ardupilot_methodic_configurator.backend_filesystem_program_settings import ProgramSettings
36
38
  from ardupilot_methodic_configurator.backend_flightcontroller import FlightController
37
39
  from ardupilot_methodic_configurator.backend_internet import verify_and_open_url
@@ -135,10 +137,13 @@ def connect_to_fc_and_set_vehicle_type(args: argparse.Namespace) -> tuple[Flight
135
137
  flight_controller = FlightController(reboot_time=args.reboot_time, baudrate=args.baudrate)
136
138
 
137
139
  error_str = flight_controller.connect(args.device, log_errors=False)
140
+
138
141
  if error_str:
139
142
  if args.device and _("No serial ports found") not in error_str:
140
143
  logging_error(error_str)
141
144
  conn_sel_window = ConnectionSelectionWindow(flight_controller, error_str, default_baudrate=args.baudrate)
145
+ # Set up startup notification for the connection selection window
146
+ FreeDesktop.setup_startup_notification(conn_sel_window.root) # type: ignore[arg-type]
142
147
  conn_sel_window.root.mainloop()
143
148
 
144
149
  vehicle_type = args.vehicle_type
@@ -209,6 +214,8 @@ def vehicle_directory_selection(state: ApplicationState) -> Union[VehicleProject
209
214
  )
210
215
  )
211
216
  vehicle_dir_window = VehicleProjectOpenerWindow(state.vehicle_project_manager)
217
+ # Set up startup notification for the vehicle directory selection window
218
+ FreeDesktop.setup_startup_notification(vehicle_dir_window.root) # type: ignore[arg-type]
212
219
  vehicle_dir_window.root.mainloop()
213
220
 
214
221
  if state.vehicle_project_manager.reset_fc_parameters_to_their_defaults:
@@ -342,6 +349,9 @@ def component_editor(state: ApplicationState) -> None:
342
349
  elif should_open_firmware_documentation(state.flight_controller):
343
350
  open_firmware_documentation(state.flight_controller.info.firmware_type)
344
351
 
352
+ # Set up startup notification for the component editor window
353
+ FreeDesktop.setup_startup_notification(component_editor_window.root) # type: ignore[arg-type]
354
+
345
355
  # Run the GUI
346
356
  component_editor_window.root.mainloop()
347
357
 
@@ -497,6 +507,9 @@ def main() -> None:
497
507
  """
498
508
  args = create_argument_parser().parse_args()
499
509
 
510
+ # Create desktop icon if needed (only on first run in venv)
511
+ FreeDesktop.create_desktop_icon_if_needed()
512
+
500
513
  state = ApplicationState(args)
501
514
 
502
515
  setup_logging(state)
@@ -517,6 +530,26 @@ def main() -> None:
517
530
  if not files:
518
531
  vehicle_directory_selection(state)
519
532
 
533
+ if (
534
+ state.flight_controller.fc_parameters
535
+ and state.flight_controller.info.flight_sw_version.startswith("4.6.")
536
+ and state.local_filesystem.doc_dict
537
+ and "FSTRATE_ENABLE" in state.local_filesystem.doc_dict
538
+ ):
539
+ show_error_message(
540
+ _("Incompatible parameter definition file detected"),
541
+ _(
542
+ "The parameter definition file 'apm.pdef.xml' is incompatible with ArduPilot 4.6.x firmware. "
543
+ "It appears to be from the master branch. The file will be deleted. "
544
+ "Please restart the ArduPilot Methodic Configurator."
545
+ ),
546
+ )
547
+ # delete apm.pdef.xml file as it is from master and not from 4.6.x
548
+ file_path = os.path.join(state.local_filesystem.vehicle_dir, "apm.pdef.xml")
549
+ if os.path.exists(file_path):
550
+ os.remove(file_path)
551
+ sys_exit(1)
552
+
520
553
  # Run component editor workflow
521
554
  component_editor(state)
522
555
 
@@ -1,4 +1,4 @@
1
- #!/usr/bin/python3
1
+ #!/usr/bin/env python3
2
2
  # PYTHON_ARGCOMPLETE_OK
3
3
 
4
4
  """
@@ -133,7 +133,9 @@ def parse_arguments() -> argparse.Namespace:
133
133
  return args
134
134
 
135
135
 
136
- def get_xml_data(base_url: str, directory: str, filename: str, vehicle_type: str) -> ET.Element: # pylint: disable=too-many-locals
136
+ def get_xml_data( # pylint: disable=too-many-locals, too-many-statements # noqa: PLR0915
137
+ base_url: str, directory: str, filename: str, vehicle_type: str, fallback_xml_url: Optional[str] = None
138
+ ) -> ET.Element:
137
139
  """
138
140
  Fetches XML data from a local file or a URL.
139
141
 
@@ -142,6 +144,7 @@ def get_xml_data(base_url: str, directory: str, filename: str, vehicle_type: str
142
144
  directory (str): The directory where the XML file is expected.
143
145
  filename (str): The name of the XML file.
144
146
  vehicle_type (str): The type of the vehicle.
147
+ fallback_xml_url (Optional[str]): Fallback URL if the main URL fails.
145
148
 
146
149
  Returns:
147
150
  ET.Element: The root element of the parsed XML data.
@@ -183,19 +186,32 @@ def get_xml_data(base_url: str, directory: str, filename: str, vehicle_type: str
183
186
  logging.warning("Unable to fetch XML data: %s", e)
184
187
  # Send a GET request to the URL to the fallback (DEV) URL
185
188
  try:
186
- url = BASE_URL + vehicle_type + "/" + PARAM_DEFINITION_XML_FILE
187
- logging.warning("Falling back to the DEV XML file: %s", url)
189
+ if fallback_xml_url is None:
190
+ msg = "No fallback XML URL provided."
191
+ raise ValueError(msg) from e
192
+ url = fallback_xml_url
193
+ logging.warning("Falling back to the latest stable release XML file: %s", url)
188
194
  response = requests_get(url, timeout=5, proxies=proxies)
189
195
  if response.status_code != 200:
190
- logging.critical("Remote URL: %s", url)
196
+ logging.warning("Remote URL: %s", url)
191
197
  msg = f"HTTP status code {response.status_code}"
192
198
  raise requests_exceptions.RequestException(msg)
193
- except requests_exceptions.RequestException as exp:
194
- logging.critical("Unable to fetch XML data: %s", exp)
195
- msg = "Unable to fetch online XML documentation."
196
- msg += f"\nDownload it manually from {url} and"
197
- msg += f"\nplace it in the {directory} directory"
198
- raise SystemExit(msg) from exp
199
+ except (ValueError, requests_exceptions.RequestException) as ex:
200
+ logging.warning("Unable to fetch XML data: %s", ex)
201
+ try:
202
+ url = BASE_URL + vehicle_type + "/" + PARAM_DEFINITION_XML_FILE
203
+ logging.warning("Falling back to the DEV XML file: %s", url)
204
+ response = requests_get(url, timeout=5, proxies=proxies)
205
+ if response.status_code != 200:
206
+ logging.critical("Remote URL: %s", url)
207
+ msg = f"HTTP status code {response.status_code}"
208
+ raise requests_exceptions.RequestException(msg)
209
+ except requests_exceptions.RequestException as exp:
210
+ logging.critical("Unable to fetch XML data: %s", exp)
211
+ msg = "Unable to fetch online XML documentation."
212
+ msg += f"\nDownload it manually from {url} and"
213
+ msg += f"\nplace it in the {directory} directory"
214
+ raise SystemExit(msg) from exp
199
215
  # Get the text content of the response
200
216
  xml_data = response.text
201
217
  try:
@@ -599,10 +615,29 @@ def get_xml_url(vehicle_type: str, firmware_version: str) -> str:
599
615
  return xml_url
600
616
 
601
617
 
602
- def parse_parameter_metadata(
603
- xml_url: str, xml_dir: str, xml_file: str, vehicle_type: str, max_line_length: int
618
+ def get_fallback_xml_url(vehicle_type: str, firmware_version: str) -> str:
619
+ vehicle_parm_subdir = {
620
+ "ArduCopter": "Copter-",
621
+ "ArduPlane": "Plane-",
622
+ "Rover": "Rover-",
623
+ "ArduSub": "Sub-",
624
+ }
625
+ try:
626
+ vehicle_subdir = vehicle_parm_subdir[vehicle_type] + firmware_version[0:3]
627
+ except KeyError as e:
628
+ msg = f"Vehicle type '{vehicle_type}' is not supported."
629
+ raise ValueError(msg) from e
630
+
631
+ xml_url = "https://raw.githubusercontent.com/ArduPilot/ParameterRepository/refs/heads/main/"
632
+ xml_url += vehicle_subdir if firmware_version else vehicle_type
633
+ xml_url += "/" + PARAM_DEFINITION_XML_FILE
634
+ return xml_url
635
+
636
+
637
+ def parse_parameter_metadata( # pylint: disable=too-many-arguments, too-many-positional-arguments
638
+ xml_url: str, xml_dir: str, xml_file: str, vehicle_type: str, max_line_length: int, fallback_xml_url: Optional[str] = None
604
639
  ) -> dict[str, Any]:
605
- xml_root = get_xml_data(xml_url, xml_dir, xml_file, vehicle_type)
640
+ xml_root = get_xml_data(xml_url, xml_dir, xml_file, vehicle_type, fallback_xml_url)
606
641
  return create_doc_dict(xml_root, vehicle_type, max_line_length)
607
642
 
608
643
 
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Check the range of an Argparse parameter.
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 Dmitriy Kovalev
7
7
 
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Filesystem operations.
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
 
@@ -38,6 +38,7 @@ from ardupilot_methodic_configurator import _
38
38
  from ardupilot_methodic_configurator.annotate_params import (
39
39
  PARAM_DEFINITION_XML_FILE,
40
40
  format_columns,
41
+ get_fallback_xml_url,
41
42
  get_xml_dir,
42
43
  get_xml_url,
43
44
  load_default_param_file,
@@ -122,8 +123,11 @@ class LocalFilesystem(VehicleComponents, ConfigurationSteps, ProgramSettings):
122
123
 
123
124
  # Read ArduPilot parameter documentation
124
125
  xml_url = get_xml_url(vehicle_type, self.fw_version)
126
+ fallback_xml_url = get_fallback_xml_url(vehicle_type, self.fw_version)
125
127
  xml_dir = get_xml_dir(vehicle_dir)
126
- self.doc_dict = parse_parameter_metadata(xml_url, xml_dir, PARAM_DEFINITION_XML_FILE, vehicle_type, TOOLTIP_MAX_LENGTH)
128
+ self.doc_dict = parse_parameter_metadata(
129
+ xml_url, xml_dir, PARAM_DEFINITION_XML_FILE, vehicle_type, TOOLTIP_MAX_LENGTH, fallback_xml_url
130
+ )
127
131
  self.param_default_dict = load_default_param_file(vehicle_dir)
128
132
 
129
133
  # Extend parameter documentation metadata if <parameter_file>.pdef.xml exists
@@ -575,7 +579,7 @@ class LocalFilesystem(VehicleComponents, ConfigurationSteps, ProgramSettings):
575
579
  dest = os_path.join(new_vehicle_dir, item)
576
580
  if blank_change_reason and item.endswith(".param"):
577
581
  # Blank the change reason in the template files, strip the comments that start with #
578
- with open(source, encoding="utf-8") as file:
582
+ with open(source, encoding="utf-8-sig") as file:
579
583
  lines = file.readlines()
580
584
  with open(dest, "w", encoding="utf-8") as file:
581
585
  file.writelines(line.split("#")[0].strip() + "\n" for line in lines)
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Manages configuration steps at the filesystem level.
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
 
@@ -10,14 +10,14 @@ SPDX-License-Identifier: GPL-3.0-or-later
10
10
 
11
11
  from json import JSONDecodeError
12
12
  from json import load as json_load
13
-
14
- # from sys import exit as sys_exit
15
- # from logging import debug as logging_debug
16
13
  from logging import error as logging_error
17
14
  from logging import info as logging_info
18
15
  from logging import warning as logging_warning
19
16
  from os import path as os_path
17
+ from typing import TypedDict
20
18
 
19
+ # from sys import exit as sys_exit
20
+ # from logging import debug as logging_debug
21
21
  from jsonschema import validate as json_validate
22
22
  from jsonschema.exceptions import ValidationError
23
23
 
@@ -25,6 +25,26 @@ from ardupilot_methodic_configurator import _
25
25
  from ardupilot_methodic_configurator.data_model_par_dict import Par, ParDict
26
26
 
27
27
 
28
+ class PhaseData(TypedDict, total=False):
29
+ """
30
+ Type definition for configuration phase data.
31
+
32
+ Attributes:
33
+ start: The starting file number for this phase
34
+ end: The ending file number for this phase (computed)
35
+ weight: The weight for UI layout proportions (computed)
36
+ description: Human-readable description of the phase
37
+ optional: Whether this phase is optional
38
+
39
+ """
40
+
41
+ start: int
42
+ end: int
43
+ weight: int
44
+ description: str
45
+ optional: bool
46
+
47
+
28
48
  class ConfigurationSteps:
29
49
  """
30
50
  A class to manage configuration steps for the ArduPilot methodic configurator.
@@ -41,7 +61,7 @@ class ConfigurationSteps:
41
61
  def __init__(self, _vehicle_dir: str, vehicle_type: str) -> None:
42
62
  self.configuration_steps_filename = "configuration_steps_" + vehicle_type + ".json"
43
63
  self.configuration_steps: dict[str, dict] = {}
44
- self.configuration_phases: dict[str, dict] = {}
64
+ self.configuration_phases: dict[str, PhaseData] = {}
45
65
  self.forced_parameters: dict[str, ParDict] = {}
46
66
  self.derived_parameters: dict[str, ParDict] = {}
47
67
  self.log_loaded_file = False
@@ -56,7 +76,7 @@ class ConfigurationSteps:
56
76
  json_content = {}
57
77
  for i, directory in enumerate(search_directories):
58
78
  try:
59
- with open(os_path.join(directory, self.configuration_steps_filename), encoding="utf-8") as file:
79
+ with open(os_path.join(directory, self.configuration_steps_filename), encoding="utf-8-sig") as file:
60
80
  json_content = json_load(file)
61
81
  file_found = True
62
82
  if self.log_loaded_file:
@@ -246,3 +266,30 @@ class ConfigurationSteps:
246
266
  text = _("No documentation available for {selected_file} in the {self.configuration_steps_filename} file")
247
267
  text = documentation.get(tooltip_key, text.format(**locals()))
248
268
  return text
269
+
270
+ def get_sorted_phases_with_end_and_weight(self, total_files: int) -> dict[str, PhaseData]:
271
+ """
272
+ Get sorted phases with added 'end' and 'weight' information.
273
+
274
+ Returns phases sorted by start position, with each phase containing:
275
+ - 'end': The end file number (start of next phase or total_files)
276
+ - 'weight': Weight for UI layout (max(2, end - start))
277
+ """
278
+ active_phases = {k: v for k, v in self.configuration_phases.items() if "start" in v}
279
+
280
+ # Sort phases by start position
281
+ sorted_phases: dict[str, PhaseData] = dict(sorted(active_phases.items(), key=lambda x: x[1].get("start", 0)))
282
+
283
+ # Add the end information to each phase using the start of the next phase
284
+ phase_names = list(sorted_phases.keys())
285
+ for i, phase_name in enumerate(phase_names):
286
+ if i < len(phase_names) - 1:
287
+ next_phase_name = phase_names[i + 1]
288
+ sorted_phases[phase_name]["end"] = sorted_phases[next_phase_name].get("start", total_files)
289
+ else:
290
+ sorted_phases[phase_name]["end"] = total_files
291
+ phase_start = sorted_phases[phase_name].get("start", 0)
292
+ phase_end = sorted_phases[phase_name].get("end", total_files)
293
+ sorted_phases[phase_name]["weight"] = max(2, phase_end - phase_start)
294
+
295
+ return sorted_phases
@@ -0,0 +1,275 @@
1
+ """
2
+ Handles FreeDesktop.org compliance and desktop integration features.
3
+
4
+ This includes creating desktop entries for application launchers, managing startup
5
+ notifications according to the FreeDesktop Startup Notification specification,
6
+ and ensuring proper integration with Linux desktop environments.
7
+
8
+ This file is part of ArduPilot Methodic Configurator. https://github.com/ArduPilot/MethodicConfigurator
9
+
10
+ SPDX-FileCopyrightText: 2024-2025 Amilcar do Carmo Lucas <amilcar.lucas@iav.de>
11
+
12
+ SPDX-License-Identifier: GPL-3.0-or-later
13
+ """
14
+
15
+ import re
16
+ import subprocess
17
+ import tkinter as tk
18
+ from logging import debug as logging_debug
19
+ from logging import error as logging_error
20
+ from os import chmod as os_chmod
21
+ from os import environ as os_environ
22
+ from os import makedirs as os_makedirs
23
+ from os import name as os_name
24
+ from os import path as os_path
25
+ from shutil import which as shutil_which
26
+ from sys import platform as sys_platform
27
+ from typing import Optional, Union
28
+
29
+ from ardupilot_methodic_configurator.backend_filesystem_program_settings import ProgramSettings
30
+
31
+
32
+ class FreeDesktop:
33
+ """
34
+ A class responsible for FreeDesktop.org compliance and desktop integration.
35
+
36
+ This includes creating desktop entries for application launchers, managing startup
37
+ notifications according to the FreeDesktop Startup Notification specification,
38
+ and ensuring proper integration with Linux desktop environments.
39
+ """
40
+
41
+ def __init__(self) -> None:
42
+ pass
43
+
44
+ @staticmethod
45
+ def _is_linux_system() -> bool:
46
+ """Check if running on a Linux system."""
47
+ return os_name == "posix" and sys_platform.startswith("linux")
48
+
49
+ @staticmethod
50
+ def _get_desktop_file_path() -> str:
51
+ """Get the path where the desktop file should be created."""
52
+ return os_path.expanduser("~/.local/share/applications/ardupilot_methodic_configurator.desktop")
53
+
54
+ @staticmethod
55
+ def _desktop_icon_exists(desktop_file_path: str) -> bool:
56
+ """Check if the desktop icon already exists."""
57
+ return os_path.exists(desktop_file_path)
58
+
59
+ @staticmethod
60
+ def _get_virtual_env_path() -> Optional[str]:
61
+ """Get the virtual environment path from environment variables."""
62
+ return os_environ.get("VIRTUAL_ENV")
63
+
64
+ @staticmethod
65
+ def _create_desktop_entry_content(venv_path: str, icon_path: str) -> str:
66
+ """Create the desktop entry file content."""
67
+ # Try to use python executable directly for better compatibility
68
+ python_exe = os_path.join(venv_path, "bin", "python")
69
+ if os_path.exists(python_exe):
70
+ # Use python executable directly
71
+ exec_cmd = f"{python_exe} -m ardupilot_methodic_configurator"
72
+ else:
73
+ # Fallback to bash -c method
74
+ bash_path = shutil_which("bash") or "/bin/bash"
75
+ activate_cmd = f"source {venv_path}/bin/activate && ardupilot_methodic_configurator"
76
+ exec_cmd = f'{bash_path} -c "{activate_cmd}"'
77
+
78
+ return f"""[Desktop Entry]
79
+ Version=1.0
80
+ Name=ArduPilot Methodic Configurator
81
+ Comment=A clear ArduPilot configuration sequence
82
+ Exec={exec_cmd}
83
+ Icon={icon_path}
84
+ Terminal=true
85
+ Type=Application
86
+ Categories=Development;
87
+ Keywords=ardupilot;arducopter;drone;parameters;configuration;scm
88
+ StartupWMClass=ArduPilotMethodicConfigurator
89
+ StartupNotify=true
90
+ """
91
+
92
+ @staticmethod
93
+ def _ensure_applications_dir_exists(desktop_file_path: str) -> str:
94
+ """Ensure the applications directory exists and return it."""
95
+ apps_dir = os_path.dirname(desktop_file_path)
96
+ os_makedirs(apps_dir, exist_ok=True)
97
+ return apps_dir
98
+
99
+ @staticmethod
100
+ def _write_desktop_file(desktop_file_path: str, content: str) -> None:
101
+ """Write the desktop file content to disk."""
102
+ with open(desktop_file_path, "w", encoding="utf-8") as f:
103
+ f.write(content)
104
+
105
+ @staticmethod
106
+ def _set_desktop_file_permissions(desktop_file_path: str) -> None:
107
+ """Set appropriate permissions on the desktop file."""
108
+ os_chmod(desktop_file_path, 0o644)
109
+
110
+ @staticmethod
111
+ def _update_desktop_database(apps_dir: str) -> None:
112
+ """Update the desktop database if the command is available."""
113
+ update_desktop_db_cmd = shutil_which("update-desktop-database")
114
+ if update_desktop_db_cmd:
115
+ subprocess.run([update_desktop_db_cmd, apps_dir], check=False, capture_output=True) # noqa: S603
116
+
117
+ @staticmethod
118
+ def create_desktop_icon_if_needed() -> None:
119
+ """
120
+ Create a desktop icon for the application if running in a virtual environment and icon doesn't exist.
121
+
122
+ This function detects if we're running in a virtual environment and creates a desktop
123
+ entry that activates the venv and runs the application with the correct icon.
124
+ """
125
+ # Only create desktop icon on Linux systems
126
+ if not FreeDesktop._is_linux_system():
127
+ return
128
+
129
+ # Check if desktop icon already exists
130
+ desktop_file_path = FreeDesktop._get_desktop_file_path()
131
+ if FreeDesktop._desktop_icon_exists(desktop_file_path):
132
+ return
133
+
134
+ # Check if we're in a virtual environment
135
+ venv_path = FreeDesktop._get_virtual_env_path()
136
+ if not venv_path:
137
+ return
138
+
139
+ # Find the icon path
140
+ icon_path = ProgramSettings.application_icon_filepath()
141
+ if not icon_path:
142
+ return
143
+
144
+ # Create the desktop entry content
145
+ desktop_entry = FreeDesktop._create_desktop_entry_content(venv_path, icon_path)
146
+
147
+ # Ensure the applications directory exists
148
+ apps_dir = FreeDesktop._ensure_applications_dir_exists(desktop_file_path)
149
+
150
+ # Write the desktop file
151
+ try:
152
+ FreeDesktop._write_desktop_file(desktop_file_path, desktop_entry)
153
+ FreeDesktop._set_desktop_file_permissions(desktop_file_path)
154
+ FreeDesktop._update_desktop_database(apps_dir)
155
+
156
+ except (OSError, subprocess.SubprocessError):
157
+ logging_error("Failed to create application launch desktop icon")
158
+
159
+ @staticmethod
160
+ def _get_desktop_startup_id() -> Union[str, None]:
161
+ """
162
+ Get the DESKTOP_STARTUP_ID environment variable.
163
+
164
+ Returns:
165
+ The startup ID string if set, None otherwise.
166
+
167
+ """
168
+ return os_environ.get("DESKTOP_STARTUP_ID")
169
+
170
+ @staticmethod
171
+ def _send_startup_notification_complete(startup_id: str) -> None:
172
+ """
173
+ Send the startup notification "remove" message to indicate the application has started.
174
+
175
+ This implements the freedesktop.org startup notification protocol.
176
+
177
+ Args:
178
+ startup_id: The DESKTOP_STARTUP_ID that was passed to the application
179
+
180
+ """
181
+ if not startup_id:
182
+ return
183
+
184
+ # Validate startup_id to prevent shell injection (should only contain alphanumeric chars, hyphens, underscores)
185
+ if not re.match(r"^[a-zA-Z0-9_-]+$", startup_id):
186
+ logging_debug("Invalid startup_id format: %s", startup_id)
187
+ return
188
+
189
+ try:
190
+ # Find the full path to xdg-startup-notify for security
191
+ xdg_notify_path = shutil_which("xdg-startup-notify")
192
+ if xdg_notify_path:
193
+ # Try to use xdg-startup-notify if available (part of xdg-utils)
194
+ result = subprocess.run( # noqa: S603
195
+ [xdg_notify_path, "remove", startup_id], capture_output=True, timeout=1.0, check=False
196
+ )
197
+ if result.returncode == 0:
198
+ logging_debug("Sent startup notification completion for ID: %s", startup_id)
199
+ else:
200
+ logging_debug("xdg-startup-notify failed: %s", result.stderr.decode().strip())
201
+ else:
202
+ logging_debug("xdg-startup-notify not found in PATH")
203
+ except (subprocess.TimeoutExpired, subprocess.SubprocessError, FileNotFoundError):
204
+ # If xdg-startup-notify is not available or fails, try manual X11 approach
205
+ FreeDesktop._send_startup_notification_x11(startup_id)
206
+
207
+ @staticmethod
208
+ def _send_startup_notification_x11(startup_id: str) -> None:
209
+ """
210
+ Send startup notification completion using direct X11 ClientMessage.
211
+
212
+ Args:
213
+ startup_id: The DESKTOP_STARTUP_ID that was passed to the application
214
+
215
+ """
216
+ if not tk:
217
+ return
218
+
219
+ try:
220
+ # Create a temporary Tk instance to access X11 if we don't have one yet
221
+ temp_root = tk.Tk()
222
+ temp_root.withdraw() # Hide the window
223
+
224
+ # Try to send the message using Tk's send command
225
+ # Format: "remove: ID=<startup_id>"
226
+ message = f"remove: ID={startup_id}"
227
+
228
+ # Use Tk's send command to broadcast to the root window
229
+ # This is a bit of a hack, but Tk doesn't expose X11 messaging directly
230
+ try:
231
+ temp_root.eval(f"send -async . {{event generate . <<StartupComplete>> -data {{{message}}}}}")
232
+
233
+ # Also try to use the X11 atoms if available
234
+ # _NET_STARTUP_INFO is the atom we need to send
235
+ temp_root.eval(f"send -async . {{wm command . _NET_STARTUP_INFO {{{message}}}}}")
236
+
237
+ except Exception: # pylint: disable=broad-exception-caught
238
+ # If all else fails, just log that we tried
239
+ logging_debug("Could not send X11 startup notification message")
240
+
241
+ temp_root.destroy()
242
+
243
+ except Exception as e: # pylint: disable=broad-exception-caught
244
+ logging_debug("Failed to send X11 startup notification: %s", e)
245
+
246
+ @staticmethod
247
+ def setup_startup_notification(main_window: tk.Tk) -> None:
248
+ """
249
+ Set up startup notification for the application.
250
+
251
+ Checks for DESKTOP_STARTUP_ID and sends the completion message when the window is ready.
252
+
253
+ Args:
254
+ main_window: The main Tkinter window
255
+
256
+ """
257
+ if not FreeDesktop._is_linux_system():
258
+ return
259
+ startup_id = FreeDesktop._get_desktop_startup_id() or ""
260
+ if startup_id:
261
+ logging_debug("Startup notification ID: %s", startup_id)
262
+
263
+ # Send the completion message after the window is mapped
264
+ def on_map(event: tk.Event) -> None:
265
+ if event and event.widget == main_window:
266
+ FreeDesktop._send_startup_notification_complete(startup_id)
267
+ # Remove the binding after first map
268
+ main_window.unbind("<Map>", on_map_handler)
269
+
270
+ # Bind to the Map event to know when the window is first shown
271
+ on_map_handler = main_window.bind("<Map>", on_map)
272
+
273
+ # Also try to send immediately in case the window is already mapped
274
+ if main_window.winfo_viewable():
275
+ FreeDesktop._send_startup_notification_complete(startup_id)
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Manages JSON files at the filesystem level.
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
 
@@ -46,7 +46,7 @@ class FilesystemJSONWithSchema:
46
46
  schema_path = os_path.join(os_path.dirname(__file__), self.schema_filename)
47
47
 
48
48
  try:
49
- with open(schema_path, encoding="utf-8") as file:
49
+ with open(schema_path, encoding="utf-8-sig") as file:
50
50
  loaded_schema: dict[Any, Any] = json_load(file)
51
51
 
52
52
  # Validate the schema itself against the JSON Schema meta-schema
@@ -90,7 +90,7 @@ class FilesystemJSONWithSchema:
90
90
  data: dict[Any, Any] = {}
91
91
  filepath = os_path.join(data_dir, self.json_filename)
92
92
  try:
93
- with open(filepath, encoding="utf-8") as file:
93
+ with open(filepath, encoding="utf-8-sig") as file:
94
94
  data = json_load(file)
95
95
 
96
96
  # Validate the loaded data against the schema