ardupilot-methodic-configurator 2.6.0__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.0.dist-info → ardupilot_methodic_configurator-2.7.0.dist-info}/METADATA +11 -6
  93. {ardupilot_methodic_configurator-2.6.0.dist-info → ardupilot_methodic_configurator-2.7.0.dist-info}/RECORD +106 -106
  94. {ardupilot_methodic_configurator-2.6.0.dist-info → ardupilot_methodic_configurator-2.7.0.dist-info}/WHEEL +0 -0
  95. {ardupilot_methodic_configurator-2.6.0.dist-info → ardupilot_methodic_configurator-2.7.0.dist-info}/entry_points.txt +0 -0
  96. {ardupilot_methodic_configurator-2.6.0.dist-info → ardupilot_methodic_configurator-2.7.0.dist-info}/licenses/LICENSE.md +0 -0
  97. {ardupilot_methodic_configurator-2.6.0.dist-info → ardupilot_methodic_configurator-2.7.0.dist-info}/licenses/LICENSES/Apache-2.0.txt +0 -0
  98. {ardupilot_methodic_configurator-2.6.0.dist-info → ardupilot_methodic_configurator-2.7.0.dist-info}/licenses/LICENSES/BSD-3-Clause.txt +0 -0
  99. {ardupilot_methodic_configurator-2.6.0.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.0.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.0.dist-info → ardupilot_methodic_configurator-2.7.0.dist-info}/licenses/LICENSES/MIT-CMU.txt +0 -0
  102. {ardupilot_methodic_configurator-2.6.0.dist-info → ardupilot_methodic_configurator-2.7.0.dist-info}/licenses/LICENSES/MIT.txt +0 -0
  103. {ardupilot_methodic_configurator-2.6.0.dist-info → ardupilot_methodic_configurator-2.7.0.dist-info}/licenses/LICENSES/MPL-2.0.txt +0 -0
  104. {ardupilot_methodic_configurator-2.6.0.dist-info → ardupilot_methodic_configurator-2.7.0.dist-info}/licenses/LICENSES/PSF-2.0.txt +0 -0
  105. {ardupilot_methodic_configurator-2.6.0.dist-info → ardupilot_methodic_configurator-2.7.0.dist-info}/licenses/credits/CREDITS.md +0 -0
  106. {ardupilot_methodic_configurator-2.6.0.dist-info → ardupilot_methodic_configurator-2.7.0.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.0"
15
+ __version__ = "2.7.0"
@@ -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
@@ -497,6 +498,9 @@ def main() -> None:
497
498
  """
498
499
  args = create_argument_parser().parse_args()
499
500
 
501
+ # Create desktop icon if needed (only on first run in venv)
502
+ ProgramSettings.create_desktop_icon_if_needed()
503
+
500
504
  state = ApplicationState(args)
501
505
 
502
506
  setup_logging(state)
@@ -517,6 +521,26 @@ def main() -> None:
517
521
  if not files:
518
522
  vehicle_directory_selection(state)
519
523
 
524
+ if (
525
+ state.flight_controller.fc_parameters
526
+ and state.flight_controller.info.flight_sw_version.startswith("4.6.")
527
+ and state.local_filesystem.doc_dict
528
+ and "FSTRATE_ENABLE" in state.local_filesystem.doc_dict
529
+ ):
530
+ show_error_message(
531
+ _("Incompatible parameter definition file detected"),
532
+ _(
533
+ "The parameter definition file 'apm.pdef.xml' is incompatible with ArduPilot 4.6.x firmware. "
534
+ "It appears to be from the master branch. The file will be deleted. "
535
+ "Please restart the ArduPilot Methodic Configurator."
536
+ ),
537
+ )
538
+ # delete apm.pdef.xml file as it is from master and not from 4.6.x
539
+ file_path = os.path.join(state.local_filesystem.vehicle_dir, "apm.pdef.xml")
540
+ if os.path.exists(file_path):
541
+ os.remove(file_path)
542
+ sys_exit(1)
543
+
520
544
  # Run component editor workflow
521
545
  component_editor(state)
522
546
 
@@ -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
@@ -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
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Manages program settings 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
 
@@ -9,13 +9,18 @@ SPDX-License-Identifier: GPL-3.0-or-later
9
9
  """
10
10
 
11
11
  # from sys import exit as sys_exit
12
- import glob
12
+ import subprocess
13
+ from contextlib import suppress as contextlib_suppress
14
+ from glob import glob as glob_glob
13
15
  from importlib.resources import files as importlib_files
14
16
  from json import dump as json_dump
15
17
  from json import load as json_load
16
18
  from logging import debug as logging_debug
17
19
  from logging import error as logging_error
20
+ from os import chmod as os_chmod
21
+ from os import environ as os_environ
18
22
  from os import makedirs as os_makedirs
23
+ from os import name as os_name
19
24
  from os import path as os_path
20
25
  from os import sep as os_sep
21
26
  from pathlib import Path
@@ -23,6 +28,8 @@ from platform import system as platform_system
23
28
  from re import escape as re_escape
24
29
  from re import match as re_match
25
30
  from re import sub as re_sub
31
+ from shutil import which as shutil_which
32
+ from sys import platform as sys_platform
26
33
  from typing import Any, Optional, Union
27
34
 
28
35
  from platformdirs import site_config_dir, user_config_dir
@@ -108,8 +115,18 @@ class ProgramSettings:
108
115
 
109
116
  @staticmethod
110
117
  def application_icon_filepath() -> str:
111
- package_path = importlib_files("ardupilot_methodic_configurator")
112
- return str(package_path / "images" / "ArduPilot_icon.png")
118
+ """Get the application icon path, with fallback options."""
119
+ try:
120
+ package_path = importlib_files("ardupilot_methodic_configurator")
121
+ except (ImportError, FileNotFoundError):
122
+ # Fallback: try to find icon relative to the script
123
+ package_path = Path(os_path.dirname(os_path.abspath(__file__)))
124
+
125
+ icon_path = str(package_path / "images" / "ArduPilot_icon.png")
126
+ if os_path.exists(icon_path):
127
+ return icon_path
128
+ # If no icon found, return empty string (GUI will handle the error)
129
+ return ""
113
130
 
114
131
  @staticmethod
115
132
  def application_logo_filepath() -> str:
@@ -152,9 +169,12 @@ class ProgramSettings:
152
169
  @staticmethod
153
170
  def _user_config_dir() -> str:
154
171
  user_config_directory = user_config_dir(
155
- ".ardupilot_methodic_configurator", appauthor=False, roaming=True, ensure_exists=True
172
+ ".ardupilot_methodic_configurator", appauthor=False, roaming=True, ensure_exists=False
156
173
  )
157
174
 
175
+ if not os_path.exists(user_config_directory):
176
+ os_makedirs(user_config_directory, exist_ok=True)
177
+
158
178
  if not os_path.exists(user_config_directory):
159
179
  error_msg = _("The user configuration directory '{user_config_directory}' does not exist.")
160
180
  raise FileNotFoundError(error_msg.format(**locals()))
@@ -167,9 +187,13 @@ class ProgramSettings:
167
187
  @staticmethod
168
188
  def _site_config_dir() -> str:
169
189
  site_config_directory = site_config_dir(
170
- ".ardupilot_methodic_configurator", appauthor=False, version=None, multipath=False, ensure_exists=True
190
+ ".ardupilot_methodic_configurator", appauthor=False, version=None, multipath=False, ensure_exists=False
171
191
  )
172
192
 
193
+ if not os_path.exists(site_config_directory):
194
+ with contextlib_suppress(OSError):
195
+ os_makedirs(site_config_directory, exist_ok=True)
196
+
173
197
  if not os_path.exists(site_config_directory):
174
198
  error_msg = _("The site configuration directory '{site_config_directory}' does not exist.")
175
199
  raise FileNotFoundError(error_msg.format(**locals()))
@@ -189,7 +213,7 @@ class ProgramSettings:
189
213
 
190
214
  """
191
215
  try:
192
- with open(settings_path, encoding="utf-8") as settings_file:
216
+ with open(settings_path, encoding="utf-8-sig") as settings_file:
193
217
  loaded_settings: dict[str, Any] = json_load(settings_file)
194
218
  return loaded_settings
195
219
  except FileNotFoundError:
@@ -365,7 +389,7 @@ class ProgramSettings:
365
389
  filename = f"m_{frame_class:02d}_{frame_type:02d}_*.png"
366
390
 
367
391
  # Search for matching PNG file (since exact naming varies)
368
- matching_files = glob.glob(str(images_dir / filename))
392
+ matching_files = glob_glob(str(images_dir / filename))
369
393
 
370
394
  err_msg = (
371
395
  ""
@@ -398,3 +422,116 @@ class ProgramSettings:
398
422
  """
399
423
  filepath, _error_msg = ProgramSettings.motor_diagram_filepath(frame_class, frame_type)
400
424
  return filepath != "" and os_path.exists(filepath)
425
+
426
+ @staticmethod
427
+ def _is_linux_system() -> bool:
428
+ """Check if running on a Linux system."""
429
+ return os_name == "posix" and sys_platform.startswith("linux")
430
+
431
+ @staticmethod
432
+ def _get_desktop_file_path() -> str:
433
+ """Get the path where the desktop file should be created."""
434
+ return os_path.expanduser("~/.local/share/applications/ardupilot_methodic_configurator.desktop")
435
+
436
+ @staticmethod
437
+ def _desktop_icon_exists(desktop_file_path: str) -> bool:
438
+ """Check if the desktop icon already exists."""
439
+ return os_path.exists(desktop_file_path)
440
+
441
+ @staticmethod
442
+ def _get_virtual_env_path() -> Optional[str]:
443
+ """Get the virtual environment path from environment variables."""
444
+ return os_environ.get("VIRTUAL_ENV")
445
+
446
+ @staticmethod
447
+ def _create_desktop_entry_content(venv_path: str, icon_path: str) -> str:
448
+ """Create the desktop entry file content."""
449
+ # Try to use python executable directly for better compatibility
450
+ python_exe = os_path.join(venv_path, "bin", "python")
451
+ if os_path.exists(python_exe):
452
+ # Use python executable directly
453
+ exec_cmd = f"{python_exe} -m ardupilot_methodic_configurator"
454
+ else:
455
+ # Fallback to bash -c method
456
+ bash_path = shutil_which("bash") or "/bin/bash"
457
+ activate_cmd = f"source {venv_path}/bin/activate && ardupilot_methodic_configurator"
458
+ exec_cmd = f'{bash_path} -c "{activate_cmd}"'
459
+
460
+ return f"""[Desktop Entry]
461
+ Version=1.0
462
+ Name=ArduPilot Methodic Configurator
463
+ Comment=A clear ArduPilot configuration sequence
464
+ Exec={exec_cmd}
465
+ Icon={icon_path}
466
+ Terminal=true
467
+ Type=Application
468
+ Categories=Development;
469
+ Keywords=ardupilot;arducopter;drone;parameters;configuration;scm
470
+ """
471
+
472
+ @staticmethod
473
+ def _ensure_applications_dir_exists(desktop_file_path: str) -> str:
474
+ """Ensure the applications directory exists and return it."""
475
+ apps_dir = os_path.dirname(desktop_file_path)
476
+ os_makedirs(apps_dir, exist_ok=True)
477
+ return apps_dir
478
+
479
+ @staticmethod
480
+ def _write_desktop_file(desktop_file_path: str, content: str) -> None:
481
+ """Write the desktop file content to disk."""
482
+ with open(desktop_file_path, "w", encoding="utf-8") as f:
483
+ f.write(content)
484
+
485
+ @staticmethod
486
+ def _set_desktop_file_permissions(desktop_file_path: str) -> None:
487
+ """Set appropriate permissions on the desktop file."""
488
+ os_chmod(desktop_file_path, 0o644)
489
+
490
+ @staticmethod
491
+ def _update_desktop_database(apps_dir: str) -> None:
492
+ """Update the desktop database if the command is available."""
493
+ update_desktop_db_cmd = shutil_which("update-desktop-database")
494
+ if update_desktop_db_cmd:
495
+ subprocess.run([update_desktop_db_cmd, apps_dir], check=False, capture_output=True) # noqa: S603
496
+
497
+ @staticmethod
498
+ def create_desktop_icon_if_needed() -> None:
499
+ """
500
+ Create a desktop icon for the application if running in a virtual environment and icon doesn't exist.
501
+
502
+ This function detects if we're running in a virtual environment and creates a desktop
503
+ entry that activates the venv and runs the application with the correct icon.
504
+ """
505
+ # Only create desktop icon on Linux systems
506
+ if not ProgramSettings._is_linux_system():
507
+ return
508
+
509
+ # Check if desktop icon already exists
510
+ desktop_file_path = ProgramSettings._get_desktop_file_path()
511
+ if ProgramSettings._desktop_icon_exists(desktop_file_path):
512
+ return
513
+
514
+ # Check if we're in a virtual environment
515
+ venv_path = ProgramSettings._get_virtual_env_path()
516
+ if not venv_path:
517
+ return
518
+
519
+ # Find the icon path
520
+ icon_path = ProgramSettings.application_icon_filepath()
521
+ if not icon_path:
522
+ return
523
+
524
+ # Create the desktop entry content
525
+ desktop_entry = ProgramSettings._create_desktop_entry_content(venv_path, icon_path)
526
+
527
+ # Ensure the applications directory exists
528
+ apps_dir = ProgramSettings._ensure_applications_dir_exists(desktop_file_path)
529
+
530
+ # Write the desktop file
531
+ try:
532
+ ProgramSettings._write_desktop_file(desktop_file_path, desktop_entry)
533
+ ProgramSettings._set_desktop_file_permissions(desktop_file_path)
534
+ ProgramSettings._update_desktop_database(apps_dir)
535
+
536
+ except (OSError, subprocess.SubprocessError):
537
+ logging_error("Failed to create application launch desktop icon")
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Manages vehicle components 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
 
@@ -99,7 +99,7 @@ class VehicleComponents:
99
99
 
100
100
  templates = {}
101
101
  try:
102
- with open(filepath, encoding="utf-8") as file:
102
+ with open(filepath, encoding="utf-8-sig") as file:
103
103
  templates = json_load(file)
104
104
  except FileNotFoundError:
105
105
  logging_debug(_("System component templates file '%s' not found."), filepath)
@@ -119,7 +119,7 @@ class VehicleComponents:
119
119
 
120
120
  templates = {}
121
121
  try:
122
- with open(filepath, encoding="utf-8") as file:
122
+ with open(filepath, encoding="utf-8-sig") as file:
123
123
  templates = json_load(file)
124
124
  except FileNotFoundError:
125
125
  logging_debug(_("User component templates file '%s' not found."), filepath)