ardupilot-methodic-configurator 2.7.0__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 (25) hide show
  1. ardupilot_methodic_configurator/__init__.py +1 -1
  2. ardupilot_methodic_configurator/__main__.py +10 -1
  3. ardupilot_methodic_configurator/backend_filesystem_freedesktop.py +275 -0
  4. ardupilot_methodic_configurator/backend_filesystem_program_settings.py +0 -119
  5. ardupilot_methodic_configurator/configuration_manager.py +113 -8
  6. ardupilot_methodic_configurator/configuration_steps_ArduCopter.json +1 -1
  7. ardupilot_methodic_configurator/frontend_tkinter_base_window.py +1 -1
  8. ardupilot_methodic_configurator/frontend_tkinter_component_editor.py +12 -11
  9. ardupilot_methodic_configurator/frontend_tkinter_parameter_editor.py +60 -57
  10. ardupilot_methodic_configurator/frontend_tkinter_show.py +2 -2
  11. {ardupilot_methodic_configurator-2.7.0.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/METADATA +2 -2
  12. {ardupilot_methodic_configurator-2.7.0.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/RECORD +25 -24
  13. {ardupilot_methodic_configurator-2.7.0.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/licenses/credits/CREDITS.md +1 -0
  14. {ardupilot_methodic_configurator-2.7.0.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/WHEEL +0 -0
  15. {ardupilot_methodic_configurator-2.7.0.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/entry_points.txt +0 -0
  16. {ardupilot_methodic_configurator-2.7.0.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/licenses/LICENSE.md +0 -0
  17. {ardupilot_methodic_configurator-2.7.0.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/licenses/LICENSES/Apache-2.0.txt +0 -0
  18. {ardupilot_methodic_configurator-2.7.0.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/licenses/LICENSES/BSD-3-Clause.txt +0 -0
  19. {ardupilot_methodic_configurator-2.7.0.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/licenses/LICENSES/GPL-3.0-or-later.txt +0 -0
  20. {ardupilot_methodic_configurator-2.7.0.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/licenses/LICENSES/LGPL-3.0-or-later.txt +0 -0
  21. {ardupilot_methodic_configurator-2.7.0.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/licenses/LICENSES/MIT-CMU.txt +0 -0
  22. {ardupilot_methodic_configurator-2.7.0.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/licenses/LICENSES/MIT.txt +0 -0
  23. {ardupilot_methodic_configurator-2.7.0.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/licenses/LICENSES/MPL-2.0.txt +0 -0
  24. {ardupilot_methodic_configurator-2.7.0.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/licenses/LICENSES/PSF-2.0.txt +0 -0
  25. {ardupilot_methodic_configurator-2.7.0.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/top_level.txt +0 -0
@@ -12,4 +12,4 @@ from ardupilot_methodic_configurator.internationalization import load_translatio
12
12
 
13
13
  _ = load_translation()
14
14
 
15
- __version__ = "2.7.0"
15
+ __version__ = "2.7.1"
@@ -33,6 +33,7 @@ import argcomplete
33
33
 
34
34
  from ardupilot_methodic_configurator import _, __version__
35
35
  from ardupilot_methodic_configurator.backend_filesystem import LocalFilesystem
36
+ from ardupilot_methodic_configurator.backend_filesystem_freedesktop import FreeDesktop
36
37
  from ardupilot_methodic_configurator.backend_filesystem_program_settings import ProgramSettings
37
38
  from ardupilot_methodic_configurator.backend_flightcontroller import FlightController
38
39
  from ardupilot_methodic_configurator.backend_internet import verify_and_open_url
@@ -136,10 +137,13 @@ def connect_to_fc_and_set_vehicle_type(args: argparse.Namespace) -> tuple[Flight
136
137
  flight_controller = FlightController(reboot_time=args.reboot_time, baudrate=args.baudrate)
137
138
 
138
139
  error_str = flight_controller.connect(args.device, log_errors=False)
140
+
139
141
  if error_str:
140
142
  if args.device and _("No serial ports found") not in error_str:
141
143
  logging_error(error_str)
142
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]
143
147
  conn_sel_window.root.mainloop()
144
148
 
145
149
  vehicle_type = args.vehicle_type
@@ -210,6 +214,8 @@ def vehicle_directory_selection(state: ApplicationState) -> Union[VehicleProject
210
214
  )
211
215
  )
212
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]
213
219
  vehicle_dir_window.root.mainloop()
214
220
 
215
221
  if state.vehicle_project_manager.reset_fc_parameters_to_their_defaults:
@@ -343,6 +349,9 @@ def component_editor(state: ApplicationState) -> None:
343
349
  elif should_open_firmware_documentation(state.flight_controller):
344
350
  open_firmware_documentation(state.flight_controller.info.firmware_type)
345
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
+
346
355
  # Run the GUI
347
356
  component_editor_window.root.mainloop()
348
357
 
@@ -499,7 +508,7 @@ def main() -> None:
499
508
  args = create_argument_parser().parse_args()
500
509
 
501
510
  # Create desktop icon if needed (only on first run in venv)
502
- ProgramSettings.create_desktop_icon_if_needed()
511
+ FreeDesktop.create_desktop_icon_if_needed()
503
512
 
504
513
  state = ApplicationState(args)
505
514
 
@@ -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)
@@ -9,7 +9,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
9
9
  """
10
10
 
11
11
  # from sys import exit as sys_exit
12
- import subprocess
13
12
  from contextlib import suppress as contextlib_suppress
14
13
  from glob import glob as glob_glob
15
14
  from importlib.resources import files as importlib_files
@@ -17,10 +16,7 @@ from json import dump as json_dump
17
16
  from json import load as json_load
18
17
  from logging import debug as logging_debug
19
18
  from logging import error as logging_error
20
- from os import chmod as os_chmod
21
- from os import environ as os_environ
22
19
  from os import makedirs as os_makedirs
23
- from os import name as os_name
24
20
  from os import path as os_path
25
21
  from os import sep as os_sep
26
22
  from pathlib import Path
@@ -28,8 +24,6 @@ from platform import system as platform_system
28
24
  from re import escape as re_escape
29
25
  from re import match as re_match
30
26
  from re import sub as re_sub
31
- from shutil import which as shutil_which
32
- from sys import platform as sys_platform
33
27
  from typing import Any, Optional, Union
34
28
 
35
29
  from platformdirs import site_config_dir, user_config_dir
@@ -422,116 +416,3 @@ class ProgramSettings:
422
416
  """
423
417
  filepath, _error_msg = ProgramSettings.motor_diagram_filepath(frame_class, frame_type)
424
418
  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")
@@ -16,7 +16,8 @@ from csv import writer as csv_writer
16
16
  from logging import error as logging_error
17
17
  from logging import info as logging_info
18
18
  from pathlib import Path
19
- from typing import Callable, Optional
19
+ from time import time
20
+ from typing import Callable, Literal, Optional
20
21
  from webbrowser import open as webbrowser_open # to open the web documentation
21
22
 
22
23
  from ardupilot_methodic_configurator import _
@@ -36,6 +37,8 @@ ShowWarningCallback = Callable[[str, str], None] # (title, message) -> None
36
37
  ShowErrorCallback = Callable[[str, str], None] # (title, message) -> None
37
38
  ShowInfoCallback = Callable[[str, str], None] # (title, message) -> None
38
39
  AskRetryCancelCallback = Callable[[str, str], bool] # (title, message) -> bool
40
+ ExperimentChoice = Literal["close", True, False]
41
+ ExperimentChoiceCallback = Callable[[str, str, list[str]], ExperimentChoice]
39
42
 
40
43
 
41
44
  class OperationNotPossibleError(Exception):
@@ -76,6 +79,8 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods, too-many
76
79
 
77
80
  self._at_least_one_changed = False
78
81
 
82
+ self._last_time_asked_to_save: float = 0
83
+
79
84
  # frontend_tkinter_parameter_editor_table.py API start
80
85
  @property
81
86
  def connected_vehicle_type(self) -> str:
@@ -183,7 +188,7 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods, too-many
183
188
  show_error(_("Fatal error reading parameter files"), f"{exp}")
184
189
  raise
185
190
 
186
- def should_copy_fc_values_to_file(self, selected_file: str) -> tuple[bool, Optional[dict], Optional[str]]:
191
+ def _should_copy_fc_values_to_file(self, selected_file: str) -> tuple[bool, Optional[dict], Optional[str]]:
187
192
  """
188
193
  Check if flight controller values should be copied to the specified file.
189
194
 
@@ -207,7 +212,7 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods, too-many
207
212
  return True, relevant_fc_params, auto_changed_by
208
213
  return False, None, auto_changed_by
209
214
 
210
- def copy_fc_values_to_file(self, selected_file: str, relevant_fc_params: dict) -> bool:
215
+ def _copy_fc_values_to_file(self, selected_file: str, relevant_fc_params: dict) -> bool:
211
216
  """
212
217
  Copy FC values to the specified file.
213
218
 
@@ -222,7 +227,75 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods, too-many
222
227
  params_copied = self._local_filesystem.copy_fc_values_to_file(selected_file, relevant_fc_params)
223
228
  return bool(params_copied)
224
229
 
225
- def get_file_jump_options(self, selected_file: str) -> dict[str, str]:
230
+ def handle_copy_fc_values_workflow(
231
+ self,
232
+ selected_file: str,
233
+ ask_user_choice: ExperimentChoiceCallback,
234
+ show_info: ShowInfoCallback,
235
+ ) -> ExperimentChoice:
236
+ """
237
+ Handle the complete workflow for copying FC values to file with user interaction.
238
+
239
+ Args:
240
+ selected_file: The configuration file to potentially update.
241
+ ask_user_choice: Callback to ask user for choice (Yes/No/Close).
242
+ show_info: Callback to show information messages.
243
+
244
+ Returns:
245
+ ExperimentChoice: "close" if user chose to close, True if copied, False if no copy.
246
+
247
+ """
248
+ should_copy, relevant_fc_params, auto_changed_by = self._should_copy_fc_values_to_file(selected_file)
249
+ if should_copy and relevant_fc_params and auto_changed_by:
250
+ msg = _(
251
+ "This configuration step requires external changes by: {auto_changed_by}\n\n"
252
+ "The external tool experiment procedure is described in the tuning guide.\n\n"
253
+ "Choose an option:\n"
254
+ "* CLOSE - Close the application and go perform the experiment\n"
255
+ "* YES - Copy current FC values to {selected_file} (if you've already completed the experiment)\n"
256
+ "* NO - Continue without copying values (if you haven't performed the experiment yet,"
257
+ " but know what you are doing)"
258
+ ).format(auto_changed_by=auto_changed_by, selected_file=selected_file)
259
+
260
+ user_choice = ask_user_choice(_("Update file with values from FC?"), msg, [_("Close"), _("Yes"), _("No")])
261
+
262
+ if user_choice is True: # Yes option
263
+ params_copied = self._copy_fc_values_to_file(selected_file, relevant_fc_params)
264
+ if params_copied:
265
+ show_info(
266
+ _("Parameters copied"),
267
+ _("FC values have been copied to {selected_file}").format(selected_file=selected_file),
268
+ )
269
+ return user_choice
270
+ return False
271
+
272
+ def handle_file_jump_workflow(
273
+ self,
274
+ selected_file: str,
275
+ gui_complexity: str,
276
+ ask_user_confirmation: AskConfirmationCallback,
277
+ ) -> str:
278
+ """
279
+ Handle the complete workflow for file jumping with user interaction.
280
+
281
+ Args:
282
+ selected_file: The current configuration file.
283
+ gui_complexity: The GUI complexity setting ("simple" or other).
284
+ ask_user_confirmation: Callback to ask user for confirmation.
285
+
286
+ Returns:
287
+ str: The destination file to jump to, or the original file if no jump.
288
+
289
+ """
290
+ jump_options = self._get_file_jump_options(selected_file)
291
+ for dest_file, msg in jump_options.items():
292
+ if gui_complexity == "simple" or ask_user_confirmation(
293
+ _("Skip some steps?"), _(msg) if msg else _("Skip to {dest_file}?").format(dest_file=dest_file)
294
+ ):
295
+ return dest_file
296
+ return selected_file
297
+
298
+ def _get_file_jump_options(self, selected_file: str) -> dict[str, str]:
226
299
  """
227
300
  Get available file jump options for the selected file.
228
301
 
@@ -235,11 +308,43 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods, too-many
235
308
  """
236
309
  return self._local_filesystem.jump_possible(selected_file)
237
310
 
311
+ def handle_write_changes_workflow(
312
+ self,
313
+ annotate_params_into_files: bool,
314
+ ask_user_confirmation: AskConfirmationCallback,
315
+ ) -> bool:
316
+ """
317
+ Handle the workflow for writing changes to intermediate parameter file.
318
+
319
+ Args:
320
+ at_least_one_param_edited: Whether any parameters have been edited.
321
+ annotate_params_into_files: Whether to annotate documentation into files.
322
+ ask_user_confirmation: Callback to ask user for confirmation.
323
+
324
+ Returns:
325
+ bool: True if changes were written, False otherwise.
326
+
327
+ """
328
+ elapsed_since_last_ask = time() - self._last_time_asked_to_save
329
+ # if annotate parameters into files is true, we always need to write to file, because
330
+ # the parameter metadata might have changed, or not be present in the file.
331
+ # In that situation, avoid asking multiple times to write the file, by checking the time last asked
332
+ # But only if annotate_params_into_files is True
333
+ if self._has_unsaved_changes() or (annotate_params_into_files and elapsed_since_last_ask > 1.0):
334
+ msg = _("Do you want to write the changes to the {current_filename} file?").format(
335
+ current_filename=self.current_file
336
+ )
337
+ if ask_user_confirmation(_("One or more parameters have been edited"), msg):
338
+ self._export_current_file(annotate_doc=annotate_params_into_files)
339
+ self._last_time_asked_to_save = time()
340
+ return True
341
+ return False
342
+
238
343
  def should_download_file_from_url_workflow(
239
344
  self,
240
345
  selected_file: str,
241
- ask_confirmation: Callable[[str, str], bool],
242
- show_error: Callable[[str, str], None],
346
+ ask_confirmation: AskConfirmationCallback,
347
+ show_error: ShowErrorCallback,
243
348
  ) -> bool:
244
349
  """
245
350
  Handle file download workflow with injected GUI callbacks.
@@ -1360,7 +1465,7 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods, too-many
1360
1465
  }
1361
1466
  )
1362
1467
 
1363
- def has_unsaved_changes(self) -> bool:
1468
+ def _has_unsaved_changes(self) -> bool:
1364
1469
  """
1365
1470
  Check if any changes have been made that need to be saved.
1366
1471
 
@@ -1408,7 +1513,7 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods, too-many
1408
1513
  def _write_current_file(self) -> None:
1409
1514
  self._local_filesystem.write_last_uploaded_filename(self.current_file)
1410
1515
 
1411
- def export_current_file(self, annotate_doc: bool) -> None:
1516
+ def _export_current_file(self, annotate_doc: bool) -> None:
1412
1517
  # Convert domain model parameters to Par objects for export
1413
1518
  export_params = self.get_parameters_as_par_dict()
1414
1519
 
@@ -214,7 +214,7 @@
214
214
  "wiki_text": "Follow the blog instructions and use Mission Planner instead of this tool to configure the mandatory hardware parameters.",
215
215
  "wiki_url": "",
216
216
  "external_tool_text": "Mission Planner",
217
- "external_tool_url": "https://github.com/ArduPilot/MethodicConfigurator/blob/latest/TUNING_GUIDE_ArduCopter.md#212-configure-mandatory-hardware-parameters-17",
217
+ "external_tool_url": "https://ardupilot.org/copter/docs/configuring-hardware.html",
218
218
  "mandatory_text": "100% mandatory (0% optional)",
219
219
  "auto_changed_by": "Mission Planner. If you have not done this step in Mission Planner yet, close this application and use Mission Planner",
220
220
  "old_filenames": ["11_mp_setup_mandatory_hardware.param"]
@@ -101,7 +101,7 @@ class BaseWindow:
101
101
  if root_tk:
102
102
  self.root = tk.Toplevel(root_tk)
103
103
  else:
104
- self.root = tk.Tk()
104
+ self.root = tk.Tk(className="ArduPilotMethodicConfigurator")
105
105
  # Only set icon for main windows, and only outside test environments
106
106
  self._setup_application_icon()
107
107
 
@@ -114,21 +114,22 @@ class ComponentEditorWindow(ComponentEditorWindowBase):
114
114
  if isinstance(protocol_combobox, PairTupleCombobox):
115
115
  # Rebuild the (key, display) pairs for PairTupleCombobox
116
116
  protocol_tuples = [(p, p) for p in protocols]
117
- # Update the combobox entries using set_entries_tuple
117
+ # Get current selection and validate it against new protocols
118
118
  current_selection = protocol_combobox.get_selected_key()
119
- protocol_combobox.set_entries_tuple(protocol_tuples, current_selection)
120
-
121
- # Validate the current selection
122
- selected_protocol = protocol_combobox.get_selected_key() or ""
123
- if selected_protocol and selected_protocol not in protocol_combobox.list_keys:
124
- # Clear the selection when current key is no longer allowed
125
- protocol_combobox.set_entries_tuple(protocol_tuples, None)
126
- _component: str = " > ".join(protocol_path)
119
+ if current_selection and current_selection not in protocols:
120
+ # Current selection is not valid for new protocols, clear it
121
+ invalid_selection = current_selection
122
+ current_selection = None
123
+ component: str = " > ".join(protocol_path)
127
124
  err_msg = _(
128
- "On {_component} the selected\nprotocol '{selected_protocol}' "
125
+ "On {component} the selected\nprotocol '{invalid_selection}' "
129
126
  "is not available for the selected connection Type."
130
127
  )
131
- err_msg = err_msg.format(**locals())
128
+ err_msg = err_msg.format(component=component, invalid_selection=invalid_selection)
129
+
130
+ # Update the combobox entries using set_entries_tuple with validated selection
131
+ protocol_combobox.set_entries_tuple(protocol_tuples, current_selection)
132
+
132
133
  if err_msg:
133
134
  show_error_message(_("Error"), err_msg)
134
135
  protocol_combobox.configure(style="comb_input_invalid.TCombobox")
@@ -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
 
@@ -21,17 +20,18 @@ from logging import error as logging_error
21
20
  from logging import getLevelName as logging_getLevelName
22
21
  from logging import warning as logging_warning
23
22
  from tkinter import filedialog, messagebox, ttk
24
- from typing import Literal, Optional, Union
23
+ from typing import Optional, Union
25
24
 
26
25
  # from logging import critical as logging_critical
27
26
  from webbrowser import open as webbrowser_open # to open the blog post documentation
28
27
 
29
28
  from ardupilot_methodic_configurator import _, __version__
30
29
  from ardupilot_methodic_configurator.backend_filesystem import LocalFilesystem
30
+ from ardupilot_methodic_configurator.backend_filesystem_freedesktop import FreeDesktop
31
31
  from ardupilot_methodic_configurator.backend_filesystem_program_settings import ProgramSettings
32
32
  from ardupilot_methodic_configurator.backend_flightcontroller import FlightController
33
33
  from ardupilot_methodic_configurator.common_arguments import add_common_arguments
34
- from ardupilot_methodic_configurator.configuration_manager import ConfigurationManager
34
+ from ardupilot_methodic_configurator.configuration_manager import ConfigurationManager, ExperimentChoice
35
35
  from ardupilot_methodic_configurator.frontend_tkinter_autoresize_combobox import AutoResizeCombobox
36
36
  from ardupilot_methodic_configurator.frontend_tkinter_base_window import (
37
37
  BaseWindow,
@@ -166,7 +166,6 @@ class ParameterEditorWindow(BaseWindow): # pylint: disable=too-many-instance-at
166
166
  self.tempcal_imu_progress_window: ProgressWindow
167
167
  self.file_upload_progress_window: ProgressWindow
168
168
  self.skip_button: ttk.Button
169
- self.last_time_asked_to_save: float = 0
170
169
  self.gui_complexity = str(ProgramSettings.get_setting("gui_complexity"))
171
170
 
172
171
  self.root.title(
@@ -209,6 +208,10 @@ class ParameterEditorWindow(BaseWindow): # pylint: disable=too-many-instance-at
209
208
  # this one should be on top of the previous one hence the longer time
210
209
  if UsagePopupWindow.should_display("parameter_editor"):
211
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
+
212
215
  self.root.mainloop()
213
216
 
214
217
  def __create_conf_widgets(self, version: str) -> None:
@@ -509,40 +512,40 @@ class ParameterEditorWindow(BaseWindow): # pylint: disable=too-many-instance-at
509
512
  finally:
510
513
  self.tempcal_imu_progress_window.destroy()
511
514
 
512
- 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:
513
516
  result.append(choice)
514
517
  dialog.destroy()
515
518
 
516
- def __should_copy_fc_values_to_file(self, selected_file: str) -> None: # pylint: disable=too-many-locals
517
- should_copy, relevant_fc_params, auto_changed_by = self.configuration_manager.should_copy_fc_values_to_file(
518
- selected_file
519
- )
520
- if should_copy and relevant_fc_params and auto_changed_by:
521
- msg = _(
522
- "This configuration step requires external changes by: {auto_changed_by}\n\n"
523
- "The external tool experiment procedure is described in the tuning guide.\n\n"
524
- "Choose an option:\n"
525
- "* CLOSE - Close the application and go perform the experiment\n"
526
- "* YES - Copy current FC values to {selected_file} (if you've already completed the experiment)\n"
527
- "* NO - Continue without copying values (if you haven't performed the experiment yet,"
528
- " but know what you are doing)"
529
- ).format(auto_changed_by=auto_changed_by, selected_file=selected_file)
530
-
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."""
531
522
  # Create custom dialog with Close, Yes, No buttons
532
523
  dialog = tk.Toplevel(self.root)
533
524
  # Hide dialog initially to prevent flickering
534
525
  dialog.withdraw()
535
526
  dialog.transient(self.root)
536
- dialog.title(_("Update file with values from FC?"))
527
+ dialog.title(title)
537
528
  dialog.resizable(width=False, height=False)
538
529
  dialog.protocol("WM_DELETE_WINDOW", dialog.destroy)
539
530
 
540
531
  # Message text
541
- 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)
542
533
  message_label.pack(padx=10, pady=10)
543
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
+
544
547
  # Result variable
545
- result: list[Optional[Literal[True, False]]] = [None]
548
+ result: list[ExperimentChoice] = []
546
549
 
547
550
  # Button frame
548
551
  button_frame = tk.Frame(dialog)
@@ -551,25 +554,31 @@ class ParameterEditorWindow(BaseWindow): # pylint: disable=too-many-instance-at
551
554
  # Close button (default)
552
555
  close_button = tk.Button(
553
556
  button_frame,
554
- text=_("Close"),
557
+ text=options[0], # "Close"
555
558
  width=10,
556
- command=lambda: self.__handle_dialog_choice(result, dialog, choice=None),
559
+ command=lambda: self.__handle_dialog_choice(result, dialog, choice="close"),
557
560
  )
558
561
  close_button.pack(side=tk.LEFT, padx=5)
559
562
 
560
563
  # Yes button
561
564
  yes_button = tk.Button(
562
- 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),
563
569
  )
564
570
  yes_button.pack(side=tk.LEFT, padx=5)
565
571
 
566
572
  # No button
567
573
  no_button = tk.Button(
568
- 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),
569
578
  )
570
579
  no_button.pack(side=tk.LEFT, padx=5)
571
580
 
572
- 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"))
573
582
 
574
583
  # Center the dialog on the parent window
575
584
  dialog.deiconify()
@@ -592,23 +601,27 @@ class ParameterEditorWindow(BaseWindow): # pylint: disable=too-many-instance-at
592
601
 
593
602
  # Wait until dialog is closed
594
603
  self.root.wait_window(dialog)
595
- response = result[-1] if len(result) > 1 else None
604
+ return result[-1] if result else "close"
596
605
 
597
- if response is True: # Yes option
598
- _params_copied = self.configuration_manager.copy_fc_values_to_file(selected_file, relevant_fc_params)
599
- elif response is None: # Close option
600
- sys.exit(0)
601
- # 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)
602
615
 
603
616
  def __should_jump_to_file(self, selected_file: str) -> str:
604
- jump_options = self.configuration_manager.get_file_jump_options(selected_file)
605
- for dest_file, msg in jump_options.items():
606
- if self.gui_complexity == "simple" or messagebox.askyesno(
607
- _("Skip some steps?"), _(msg) if msg else _("Skip to {dest_file}?").format(**locals())
608
- ):
609
- self.file_selection_combobox.set(dest_file)
610
- return dest_file
611
- 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
612
625
 
613
626
  def __should_download_file_from_url(self, selected_file: str) -> None:
614
627
  self.configuration_manager.should_download_file_from_url_workflow(
@@ -687,7 +700,7 @@ class ParameterEditorWindow(BaseWindow): # pylint: disable=too-many-instance-at
687
700
 
688
701
  def on_upload_selected_click(self) -> None:
689
702
  self.write_changes_to_intermediate_parameter_file()
690
- 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)
691
704
  if selected_params:
692
705
  if self.configuration_manager.fc_parameters:
693
706
  self.upload_selected_params(selected_params)
@@ -792,20 +805,10 @@ class ParameterEditorWindow(BaseWindow): # pylint: disable=too-many-instance-at
792
805
  self.on_param_file_combobox_change(None)
793
806
 
794
807
  def write_changes_to_intermediate_parameter_file(self) -> None:
795
- elapsed_since_last_ask = time.time() - self.last_time_asked_to_save
796
- # if annotate parameters into files is true, we always need to write to file, because
797
- # the parameter metadata might have changed, or not be present in the file.
798
- # In that situation, avoid asking multiple times to write the file, by checking the time last asked
799
- # But only if self.annotate_params_into_files.get()
800
- if self.configuration_manager.has_unsaved_changes() or (
801
- self.annotate_params_into_files.get() and elapsed_since_last_ask > 1.0
802
- ):
803
- msg = _("Do you want to write the changes to the {current_filename} file?").format(
804
- current_filename=self.configuration_manager.current_file
805
- )
806
- if messagebox.askyesno(_("One or more parameters have been edited"), msg.format(**locals())):
807
- self.configuration_manager.export_current_file(annotate_doc=self.annotate_params_into_files.get())
808
- 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
+ )
809
812
 
810
813
  def close_connection_and_quit(self) -> None:
811
814
  focused_widget = self.parameter_editor_table.view_port.focus_get()
@@ -19,7 +19,7 @@ from ardupilot_methodic_configurator import _
19
19
 
20
20
 
21
21
  def show_error_message(title: str, message: str) -> None:
22
- root = tk.Tk()
22
+ root = tk.Tk(className="ArduPilotMethodicConfigurator")
23
23
  # Set the theme to 'alt'
24
24
  style = ttk.Style()
25
25
  style.theme_use("alt")
@@ -29,7 +29,7 @@ def show_error_message(title: str, message: str) -> None:
29
29
 
30
30
 
31
31
  def show_warning_message(title: str, message: str) -> None:
32
- root = tk.Tk()
32
+ root = tk.Tk(className="ArduPilotMethodicConfigurator")
33
33
  # Set the theme to 'alt'
34
34
  style = ttk.Style()
35
35
  style.theme_use("alt")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ardupilot_methodic_configurator
3
- Version: 2.7.0
3
+ Version: 2.7.1
4
4
  Summary: A clear configuration sequence for ArduPilot vehicles
5
5
  Author-email: Amilcar do Carmo Lucas <amilcar.lucas@iav.de>
6
6
  Maintainer-email: Amilcar do Carmo Lucas <amilcar.lucas@iav.de>
@@ -56,7 +56,7 @@ Requires-Dist: matplotlib==3.9.4; python_version < "3.10"
56
56
  Requires-Dist: matplotlib==3.10.3; python_version >= "3.10" and python_version < "3.14"
57
57
  Requires-Dist: matplotlib==3.10.7; python_version >= "3.14"
58
58
  Requires-Dist: numpy==2.0.2; python_version < "3.13"
59
- Requires-Dist: numpy==2.2.2; python_version >= "3.13"
59
+ Requires-Dist: numpy==2.3.4; python_version >= "3.13"
60
60
  Requires-Dist: pillow==11.3.0
61
61
  Requires-Dist: pip_system_certs==4.0
62
62
  Requires-Dist: platformdirs==4.4.0
@@ -1,13 +1,14 @@
1
1
  ardupilot_methodic_configurator/AP_Motors_test.json,sha256=t9IN8K8titisdD4k24bFWXnml_SlYPWOY52CT_6jC4g,105884
2
2
  ardupilot_methodic_configurator/AP_Motors_test_schema.json,sha256=Z2ajo4IkDx80ZvTIY-lxkD_2X6nO9q-iVP-LHMBaW5w,6224
3
- ardupilot_methodic_configurator/__init__.py,sha256=hmPJJ6EPNLj0YkqCkchRKnVVmHx6IXPOVpTjvHjtUJw,456
4
- ardupilot_methodic_configurator/__main__.py,sha256=kQ7CFBtAuX9ahAYWGYOXGrtnE_6sYZiBHvYUMSVIPnc,22519
3
+ ardupilot_methodic_configurator/__init__.py,sha256=T3lj3jDVku_7dCvZPsUfvl-skibVx6ta22GuW0SJm-w,456
4
+ ardupilot_methodic_configurator/__main__.py,sha256=snUgab35hT7AlzlsETu5sLeIlksZMYXpgkxz3r957Es,23117
5
5
  ardupilot_methodic_configurator/annotate_params.py,sha256=0fymVFfP4y3bcNi3qJU51R7DwIMmy9XqFTRiLWk-CMU,27381
6
6
  ardupilot_methodic_configurator/argparse_check_range.py,sha256=ZxQQb7gXypG-fcrn_5rjOzq6MjVtFHT6nz0e8kJFF_Y,2474
7
7
  ardupilot_methodic_configurator/backend_filesystem.py,sha256=xVHUVkpRyKqAY2M3SoD3bVwyTgdGdh9mWS5_nF1Ditc,43152
8
8
  ardupilot_methodic_configurator/backend_filesystem_configuration_steps.py,sha256=fcItXCpuHDJsTzG-yD8Zf0eLD4dWbxU3AcjcFuEYhr0,14572
9
+ ardupilot_methodic_configurator/backend_filesystem_freedesktop.py,sha256=d0M_jHSMrEx0RfJMQCRW0HpArDH79N-_2i5PPo-a0VM,10890
9
10
  ardupilot_methodic_configurator/backend_filesystem_json_with_schema.py,sha256=GuAhGf2liZenohgnEv9vSxQYwtuNPeZ88l1WfW1NUPs,6525
10
- ardupilot_methodic_configurator/backend_filesystem_program_settings.py,sha256=LDijaVMKLqc-b2ndk9iKgdKHNrFStr-1U0mZm1zt1Ds,21630
11
+ ardupilot_methodic_configurator/backend_filesystem_program_settings.py,sha256=-CxHgX6uoTYumwEreYX6yLXx8JxC1Je2LAxcPelSrhc,16874
11
12
  ardupilot_methodic_configurator/backend_filesystem_vehicle_components.py,sha256=aDJoHk9Ovo8-AC1VkrhEypwuQD6GuByobe6r0UuTQoY,20005
12
13
  ardupilot_methodic_configurator/backend_flightcontroller.py,sha256=CDx8VjR0LO9j9uKGca99z1Ip0Lul4WhH3YpozZzgA7Q,63124
13
14
  ardupilot_methodic_configurator/backend_flightcontroller_info.py,sha256=nd4XxRLsBhAIzeMWmjUsHbOpkp4AI5nAohojyyjM0-A,12260
@@ -15,8 +16,8 @@ ardupilot_methodic_configurator/backend_internet.py,sha256=GMVZOGnyraaSsvSjbv0H2
15
16
  ardupilot_methodic_configurator/backend_mavftp.py,sha256=N4_IGDQ8dq8YfjOg6NhLkf5ZnhMMDhENkmPSzk_56aw,70861
16
17
  ardupilot_methodic_configurator/battery_cell_voltages.py,sha256=br5g5SBDss6au7HV1khpQemzo0CO9CxxHLG9roz82Yc,3143
17
18
  ardupilot_methodic_configurator/common_arguments.py,sha256=T1ioTOZn1VASBWrx968kgD8WIzE25E1_dExobkzuGoE,1140
18
- ardupilot_methodic_configurator/configuration_manager.py,sha256=P6qanuWepYrJedpF3vzjaea-P84vYx9BWLyeX44-ANc,65311
19
- ardupilot_methodic_configurator/configuration_steps_ArduCopter.json,sha256=es7arbnZMvreuDjQCUlTj5IPD2iJ7aw_icGp5oAKfkY,71012
19
+ ardupilot_methodic_configurator/configuration_manager.py,sha256=vy83pv1NczXuinLU5YPlwLO4Xq6u_3VZbrtrz7k9Kus,70133
20
+ ardupilot_methodic_configurator/configuration_steps_ArduCopter.json,sha256=UXIijcKNM6ig8hHZiERHL5uCqQPsoucOKZCXGOJQhJo,70936
20
21
  ardupilot_methodic_configurator/configuration_steps_ArduPlane.json,sha256=sZ2eNwhH7iwcZ5c37dvH8D_ST8eSUNophQm-yICTkJo,69045
21
22
  ardupilot_methodic_configurator/configuration_steps_Heli.json,sha256=7zzQfMrqLgoobAnKSNOLexQuntEYclbI0xBLp69sQXo,68914
22
23
  ardupilot_methodic_configurator/configuration_steps_Rover.json,sha256=3ypq0_i7IZJ3hqNRePqCh2N8GFVhbY14Ei0AOtzOlWQ,68823
@@ -41,8 +42,8 @@ ardupilot_methodic_configurator/data_model_vehicle_project_creator.py,sha256=Evy
41
42
  ardupilot_methodic_configurator/data_model_vehicle_project_opener.py,sha256=694-NPdLcjJ6uL4Fz_FBlvFFGAwVTi38Plio5ALJ-BI,7158
42
43
  ardupilot_methodic_configurator/extract_param_defaults.py,sha256=7irpBZl2tgqu5l766Jcr8yMZqrdP5tQCF7Ahh7gR9Xw,9832
43
44
  ardupilot_methodic_configurator/frontend_tkinter_autoresize_combobox.py,sha256=1lBF8oJncQEOlUT_6hY5xhz8wGJ4kYQKmiQeAsu_5d4,2716
44
- ardupilot_methodic_configurator/frontend_tkinter_base_window.py,sha256=w0lAtGqgIG7cG5A_myKIQ7btNE6dByohiuzu1KVOt34,16077
45
- ardupilot_methodic_configurator/frontend_tkinter_component_editor.py,sha256=OF9tj3QAd25Ai-ZcMxah9br5Pa0lfXIz32uXLOK1kUg,15496
45
+ ardupilot_methodic_configurator/frontend_tkinter_base_window.py,sha256=Z0vUiH5O_WKry0It96v7AKjuMTnpdipxv65La9g8-9E,16118
46
+ ardupilot_methodic_configurator/frontend_tkinter_component_editor.py,sha256=eiYAP_0NGx4qcrZov1aEIRXZEvPNfT2vE_Lx1FRgdKQ,15523
46
47
  ardupilot_methodic_configurator/frontend_tkinter_component_editor_base.py,sha256=jUHTLnk4RkoPN8OVmBCq_NMoV4UVMXo7xI2mrZPJmqw,33029
47
48
  ardupilot_methodic_configurator/frontend_tkinter_component_template_manager.py,sha256=BKc824yeNSz0LaqQwxHYM6jyXtXg8lJAmh_aRGPUHR8,8875
48
49
  ardupilot_methodic_configurator/frontend_tkinter_connection_selection.py,sha256=VPQGFyF-JYhB4FnsYMPCyPbAvcqn90xihcn1Z1FG2uI,17878
@@ -52,7 +53,7 @@ ardupilot_methodic_configurator/frontend_tkinter_flightcontroller_info.py,sha256
52
53
  ardupilot_methodic_configurator/frontend_tkinter_font.py,sha256=QMEQNlSMjo6SCb2tXyzVkwNRqfJ9OOA9lDtlK-Jk7jw,7733
53
54
  ardupilot_methodic_configurator/frontend_tkinter_motor_test.py,sha256=EYZVAxnM68UXRMRqdj6tqbQTBi1DXCtLie1UTgc-kAk,38478
54
55
  ardupilot_methodic_configurator/frontend_tkinter_pair_tuple_combobox.py,sha256=32PuQooNVgRacTFM27YGfcBQv1zJYhGXXHROPRdl1mE,16555
55
- ardupilot_methodic_configurator/frontend_tkinter_parameter_editor.py,sha256=lk-k7A6JESxQswig2xq1zaujyzJ_NT5vxVufRTd_DA0,42508
56
+ ardupilot_methodic_configurator/frontend_tkinter_parameter_editor.py,sha256=GgBIAK13i3c0r3IDpry88vydwW80ML6gRY5Bmti0ORs,41557
56
57
  ardupilot_methodic_configurator/frontend_tkinter_parameter_editor_documentation_frame.py,sha256=nCrjx9q4D-iVg9ebe3dAMj0xHl4v_GmWWzy0tuNLBnE,7978
57
58
  ardupilot_methodic_configurator/frontend_tkinter_parameter_editor_table.py,sha256=XPgTyKcfd87bt15teoM_wLcuBYMMKQjUgOZgyzsVhbU,39016
58
59
  ardupilot_methodic_configurator/frontend_tkinter_progress_window.py,sha256=I3M93OTsV7CR_MDys5YOWnizvK8yFtOQf2I29JUw_KA,5117
@@ -60,7 +61,7 @@ ardupilot_methodic_configurator/frontend_tkinter_project_creator.py,sha256=ioRBO
60
61
  ardupilot_methodic_configurator/frontend_tkinter_project_opener.py,sha256=RbNTHMJdxkwMAByW8rmNpjQ2ANEDLMY_L_Z5vEfDbb4,9819
61
62
  ardupilot_methodic_configurator/frontend_tkinter_rich_text.py,sha256=C-DwALNgsZi2TssZ9206tITcbb5X5v-wQOWD8zIHBMk,3789
62
63
  ardupilot_methodic_configurator/frontend_tkinter_scroll_frame.py,sha256=IxYe05zzhd4lfa6oenGYqVNxXFjk10bIXlLPMqQQn-g,5320
63
- ardupilot_methodic_configurator/frontend_tkinter_show.py,sha256=-URnqHdoi1fDERpaUqYAzPbyFLUzugSpLa8-FZyiZ2g,5733
64
+ ardupilot_methodic_configurator/frontend_tkinter_show.py,sha256=dfNbW0edWiqNWGfqACjcnD7uICF3lD9TMnbDQaE4sw0,5815
64
65
  ardupilot_methodic_configurator/frontend_tkinter_software_update.py,sha256=Lwj2Dk0C529S3dtkNVkFC_JdGxX_UeHEmVrlU54WxY0,4484
65
66
  ardupilot_methodic_configurator/frontend_tkinter_stage_progress.py,sha256=Hwf7dHwbQUZmHPctzv9toqyoLB14jOYKyHG0x7h8QtY,10140
66
67
  ardupilot_methodic_configurator/frontend_tkinter_template_overview.py,sha256=Vp6_hSMEkgSlixOD_b8mdUXSzSq_chfnTIh164mLeCc,18249
@@ -1447,18 +1448,18 @@ ardupilot_methodic_configurator/vehicle_templates/Rover/AION_R1/52_use_optical_f
1447
1448
  ardupilot_methodic_configurator/vehicle_templates/Rover/AION_R1/53_everyday_use.param,sha256=cmWo5YitMy2DF46ljT2inyQWoQa5WKy3iz3oKe6G2B4,557
1448
1449
  ardupilot_methodic_configurator/vehicle_templates/Rover/AION_R1/apm.pdef.xml,sha256=uJcmAzQ6o3tIOCVGZXq6wtWaMJdTcOVJfWix8qIMXs4,1796429
1449
1450
  ardupilot_methodic_configurator/vehicle_templates/Rover/AION_R1/vehicle_components.json,sha256=7TxmsfW_S9mpfQ5edOhzs0w3A-7i5YUMXYAuDoGT8xc,5768
1450
- ardupilot_methodic_configurator-2.7.0.dist-info/licenses/LICENSE.md,sha256=9pLwN-5iuDanZz6Zm0V2sUrcjxyx87w1WwGV1r4Q5F4,35826
1451
- ardupilot_methodic_configurator-2.7.0.dist-info/licenses/LICENSES/Apache-2.0.txt,sha256=m_Togr3XXo0Q_HiMU9Cnh-8PWc6tH-Hwh4wkCJb8hhA,10353
1452
- ardupilot_methodic_configurator-2.7.0.dist-info/licenses/LICENSES/BSD-3-Clause.txt,sha256=6Nd7--lm3uRacqEuyyJ4NP9UjMfbob3GP6hibyOxcHs,1470
1453
- ardupilot_methodic_configurator-2.7.0.dist-info/licenses/LICENSES/GPL-3.0-or-later.txt,sha256=-5gWaMGKJ54oX8TYP7oeg2zITdTapzyWl9PP0tispuA,34674
1454
- ardupilot_methodic_configurator-2.7.0.dist-info/licenses/LICENSES/LGPL-3.0-or-later.txt,sha256=nUeUYpsPD58HjDfHv-sti_MT71zWdBgDij3e3a1JGyE,42402
1455
- ardupilot_methodic_configurator-2.7.0.dist-info/licenses/LICENSES/MIT-CMU.txt,sha256=NmPnxSbDhJtpLqezuy_J6VvC3_4MDs-RX5ZZPdNK43g,1157
1456
- ardupilot_methodic_configurator-2.7.0.dist-info/licenses/LICENSES/MIT.txt,sha256=MjA6uIe4yQtub-SeLwMcoBsjtRkOI2RH2U4ZRGhNx2k,1096
1457
- ardupilot_methodic_configurator-2.7.0.dist-info/licenses/LICENSES/MPL-2.0.txt,sha256=kiHC-TYVm4RG0ykkn7TA8lvlEPRHODoPEzNqx5hWaKM,17099
1458
- ardupilot_methodic_configurator-2.7.0.dist-info/licenses/LICENSES/PSF-2.0.txt,sha256=j5YgHA_eqI9Frr3lhzVZqmyR65psFGCSM4-PnA0Cfc4,2474
1459
- ardupilot_methodic_configurator-2.7.0.dist-info/licenses/credits/CREDITS.md,sha256=TNrMvP82trH7zgDA-gMzXzPoRydpD-WY9MopdwQyflw,8425
1460
- ardupilot_methodic_configurator-2.7.0.dist-info/METADATA,sha256=8TG9f-mbZPqsxnKoTay8L7JIm_NLXV330nfk3YgPSdc,32883
1461
- ardupilot_methodic_configurator-2.7.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
1462
- ardupilot_methodic_configurator-2.7.0.dist-info/entry_points.txt,sha256=Ymzhv3OMn_WEeBVIfldx4ceUSjGxJtaCSbZH4Y9F-dg,918
1463
- ardupilot_methodic_configurator-2.7.0.dist-info/top_level.txt,sha256=StTz5FGzOEMSseJ1vR6azxeDQOIDg3Cq5B1_dgMjrAI,32
1464
- ardupilot_methodic_configurator-2.7.0.dist-info/RECORD,,
1451
+ ardupilot_methodic_configurator-2.7.1.dist-info/licenses/LICENSE.md,sha256=9pLwN-5iuDanZz6Zm0V2sUrcjxyx87w1WwGV1r4Q5F4,35826
1452
+ ardupilot_methodic_configurator-2.7.1.dist-info/licenses/LICENSES/Apache-2.0.txt,sha256=m_Togr3XXo0Q_HiMU9Cnh-8PWc6tH-Hwh4wkCJb8hhA,10353
1453
+ ardupilot_methodic_configurator-2.7.1.dist-info/licenses/LICENSES/BSD-3-Clause.txt,sha256=6Nd7--lm3uRacqEuyyJ4NP9UjMfbob3GP6hibyOxcHs,1470
1454
+ ardupilot_methodic_configurator-2.7.1.dist-info/licenses/LICENSES/GPL-3.0-or-later.txt,sha256=-5gWaMGKJ54oX8TYP7oeg2zITdTapzyWl9PP0tispuA,34674
1455
+ ardupilot_methodic_configurator-2.7.1.dist-info/licenses/LICENSES/LGPL-3.0-or-later.txt,sha256=nUeUYpsPD58HjDfHv-sti_MT71zWdBgDij3e3a1JGyE,42402
1456
+ ardupilot_methodic_configurator-2.7.1.dist-info/licenses/LICENSES/MIT-CMU.txt,sha256=NmPnxSbDhJtpLqezuy_J6VvC3_4MDs-RX5ZZPdNK43g,1157
1457
+ ardupilot_methodic_configurator-2.7.1.dist-info/licenses/LICENSES/MIT.txt,sha256=MjA6uIe4yQtub-SeLwMcoBsjtRkOI2RH2U4ZRGhNx2k,1096
1458
+ ardupilot_methodic_configurator-2.7.1.dist-info/licenses/LICENSES/MPL-2.0.txt,sha256=kiHC-TYVm4RG0ykkn7TA8lvlEPRHODoPEzNqx5hWaKM,17099
1459
+ ardupilot_methodic_configurator-2.7.1.dist-info/licenses/LICENSES/PSF-2.0.txt,sha256=j5YgHA_eqI9Frr3lhzVZqmyR65psFGCSM4-PnA0Cfc4,2474
1460
+ ardupilot_methodic_configurator-2.7.1.dist-info/licenses/credits/CREDITS.md,sha256=5IpN1DQO8dsbw8bI2UwtVoQFocM8BVinr6O4XV36AqE,8573
1461
+ ardupilot_methodic_configurator-2.7.1.dist-info/METADATA,sha256=Ai5nfcijuThpOoohUcHIABpRg5wFmQqOHn-bTSAGmAU,32883
1462
+ ardupilot_methodic_configurator-2.7.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
1463
+ ardupilot_methodic_configurator-2.7.1.dist-info/entry_points.txt,sha256=Ymzhv3OMn_WEeBVIfldx4ceUSjGxJtaCSbZH4Y9F-dg,918
1464
+ ardupilot_methodic_configurator-2.7.1.dist-info/top_level.txt,sha256=StTz5FGzOEMSseJ1vR6azxeDQOIDg3Cq5B1_dgMjrAI,32
1465
+ ardupilot_methodic_configurator-2.7.1.dist-info/RECORD,,
@@ -91,3 +91,4 @@ These books helped shape this software:
91
91
  - [Clean Architecture by Robert C. Martin](https://www.oreilly.com/library/view/clean-architecture/9780134494272/)
92
92
  - [Modern Software Engineering by David Farley](https://www.oreilly.com/library/view/modern-software-engineering/9780137314942/)
93
93
  - [The DevOps Handbook by Gene Kim, Patrick Debois, John Willis, and Jez Humble](https://www.oreilly.com/library/view/the-devsecops-handbook/9781098182281/)
94
+ - [Fundamentals of Software Architecture by Mark Richards, Neal Ford](https://www.oreilly.com/library/view/fundamentals-of-software/9781492043447/)