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