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.
- ardupilot_methodic_configurator/__init__.py +1 -1
- ardupilot_methodic_configurator/__main__.py +10 -1
- ardupilot_methodic_configurator/backend_filesystem_freedesktop.py +275 -0
- ardupilot_methodic_configurator/backend_filesystem_program_settings.py +0 -119
- ardupilot_methodic_configurator/configuration_manager.py +113 -8
- ardupilot_methodic_configurator/configuration_steps_ArduCopter.json +1 -1
- ardupilot_methodic_configurator/frontend_tkinter_base_window.py +1 -1
- ardupilot_methodic_configurator/frontend_tkinter_component_editor.py +12 -11
- ardupilot_methodic_configurator/frontend_tkinter_parameter_editor.py +60 -57
- ardupilot_methodic_configurator/frontend_tkinter_show.py +2 -2
- {ardupilot_methodic_configurator-2.7.0.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/METADATA +2 -2
- {ardupilot_methodic_configurator-2.7.0.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/RECORD +25 -24
- {ardupilot_methodic_configurator-2.7.0.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/licenses/credits/CREDITS.md +1 -0
- {ardupilot_methodic_configurator-2.7.0.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/WHEEL +0 -0
- {ardupilot_methodic_configurator-2.7.0.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/entry_points.txt +0 -0
- {ardupilot_methodic_configurator-2.7.0.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/licenses/LICENSE.md +0 -0
- {ardupilot_methodic_configurator-2.7.0.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/licenses/LICENSES/Apache-2.0.txt +0 -0
- {ardupilot_methodic_configurator-2.7.0.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/licenses/LICENSES/BSD-3-Clause.txt +0 -0
- {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
- {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
- {ardupilot_methodic_configurator-2.7.0.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/licenses/LICENSES/MIT-CMU.txt +0 -0
- {ardupilot_methodic_configurator-2.7.0.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/licenses/LICENSES/MIT.txt +0 -0
- {ardupilot_methodic_configurator-2.7.0.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/licenses/LICENSES/MPL-2.0.txt +0 -0
- {ardupilot_methodic_configurator-2.7.0.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/licenses/LICENSES/PSF-2.0.txt +0 -0
- {ardupilot_methodic_configurator-2.7.0.dist-info → ardupilot_methodic_configurator-2.7.1.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
242
|
-
show_error:
|
|
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
|
|
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
|
|
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://
|
|
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
|
-
#
|
|
117
|
+
# Get current selection and validate it against new protocols
|
|
118
118
|
current_selection = protocol_combobox.get_selected_key()
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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 {
|
|
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(
|
|
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
|
|
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:
|
|
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:
|
|
517
|
-
|
|
518
|
-
|
|
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(
|
|
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=
|
|
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[
|
|
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=
|
|
557
|
+
text=options[0], # "Close"
|
|
555
558
|
width=10,
|
|
556
|
-
command=lambda: self.__handle_dialog_choice(result, dialog, choice=
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
604
|
+
return result[-1] if result else "close"
|
|
596
605
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
return
|
|
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(
|
|
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
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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.
|
|
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.
|
|
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=
|
|
4
|
-
ardupilot_methodic_configurator/__main__.py,sha256=
|
|
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
|
|
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=
|
|
19
|
-
ardupilot_methodic_configurator/configuration_steps_ArduCopter.json,sha256=
|
|
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=
|
|
45
|
-
ardupilot_methodic_configurator/frontend_tkinter_component_editor.py,sha256=
|
|
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=
|
|
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
|
|
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.
|
|
1451
|
-
ardupilot_methodic_configurator-2.7.
|
|
1452
|
-
ardupilot_methodic_configurator-2.7.
|
|
1453
|
-
ardupilot_methodic_configurator-2.7.
|
|
1454
|
-
ardupilot_methodic_configurator-2.7.
|
|
1455
|
-
ardupilot_methodic_configurator-2.7.
|
|
1456
|
-
ardupilot_methodic_configurator-2.7.
|
|
1457
|
-
ardupilot_methodic_configurator-2.7.
|
|
1458
|
-
ardupilot_methodic_configurator-2.7.
|
|
1459
|
-
ardupilot_methodic_configurator-2.7.
|
|
1460
|
-
ardupilot_methodic_configurator-2.7.
|
|
1461
|
-
ardupilot_methodic_configurator-2.7.
|
|
1462
|
-
ardupilot_methodic_configurator-2.7.
|
|
1463
|
-
ardupilot_methodic_configurator-2.7.
|
|
1464
|
-
ardupilot_methodic_configurator-2.7.
|
|
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/)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|