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
|
@@ -5,7 +5,7 @@ Contains state information but no GUI code.
|
|
|
5
5
|
Aggregates flight controller and filesystem access in a single interface.
|
|
6
6
|
Uses exceptions for error handling, the GUI layer will catch and display them.
|
|
7
7
|
|
|
8
|
-
This file is part of
|
|
8
|
+
This file is part of ArduPilot Methodic Configurator. https://github.com/ArduPilot/MethodicConfigurator
|
|
9
9
|
|
|
10
10
|
SPDX-FileCopyrightText: 2024-2025 Amilcar do Carmo Lucas <amilcar.lucas@iav.de>
|
|
11
11
|
|
|
@@ -16,10 +16,13 @@ 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
|
|
21
|
+
from webbrowser import open as webbrowser_open # to open the web documentation
|
|
20
22
|
|
|
21
23
|
from ardupilot_methodic_configurator import _
|
|
22
24
|
from ardupilot_methodic_configurator.backend_filesystem import LocalFilesystem
|
|
25
|
+
from ardupilot_methodic_configurator.backend_filesystem_configuration_steps import PhaseData
|
|
23
26
|
from ardupilot_methodic_configurator.backend_flightcontroller import FlightController
|
|
24
27
|
from ardupilot_methodic_configurator.backend_internet import download_file_from_url
|
|
25
28
|
from ardupilot_methodic_configurator.data_model_ardupilot_parameter import ArduPilotParameter
|
|
@@ -33,6 +36,9 @@ SelectFileCallback = Callable[[str, list[str]], Optional[str]] # (title, filety
|
|
|
33
36
|
ShowWarningCallback = Callable[[str, str], None] # (title, message) -> None
|
|
34
37
|
ShowErrorCallback = Callable[[str, str], None] # (title, message) -> None
|
|
35
38
|
ShowInfoCallback = Callable[[str, str], None] # (title, message) -> None
|
|
39
|
+
AskRetryCancelCallback = Callable[[str, str], bool] # (title, message) -> bool
|
|
40
|
+
ExperimentChoice = Literal["close", True, False]
|
|
41
|
+
ExperimentChoiceCallback = Callable[[str, str, list[str]], ExperimentChoice]
|
|
36
42
|
|
|
37
43
|
|
|
38
44
|
class OperationNotPossibleError(Exception):
|
|
@@ -46,57 +52,64 @@ class InvalidParameterNameError(Exception):
|
|
|
46
52
|
# pylint: disable=too-many-lines
|
|
47
53
|
|
|
48
54
|
|
|
49
|
-
class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
55
|
+
class ConfigurationManager: # pylint: disable=too-many-public-methods, too-many-instance-attributes
|
|
50
56
|
"""
|
|
51
57
|
Manages configuration state, including flight controller and filesystem access.
|
|
52
58
|
|
|
53
59
|
This class aggregates the flight controller and filesystem access to provide a unified interface
|
|
54
|
-
for managing configuration state. It holds references to the flight controller and filesystem,
|
|
60
|
+
for managing configuration state. It holds protected references to the flight controller and filesystem,
|
|
55
61
|
and provides methods to interact with them.
|
|
56
62
|
"""
|
|
57
63
|
|
|
58
64
|
def __init__(self, current_file: str, flight_controller: FlightController, filesystem: LocalFilesystem) -> None:
|
|
59
65
|
self.current_file = current_file
|
|
60
|
-
self.
|
|
61
|
-
self.
|
|
62
|
-
self.
|
|
66
|
+
self._flight_controller = flight_controller
|
|
67
|
+
self._local_filesystem = filesystem
|
|
68
|
+
self._config_step_processor = ConfigurationStepProcessor(self._local_filesystem)
|
|
63
69
|
|
|
64
|
-
# self.
|
|
70
|
+
# self.current_step_parameters is rebuilt on every repopulate(...) call and only contains the ArduPilotParameter
|
|
65
71
|
# objects needed for the current table view.
|
|
66
|
-
self.
|
|
72
|
+
self.current_step_parameters: dict[str, ArduPilotParameter] = {}
|
|
67
73
|
|
|
74
|
+
# Track parameters added by user (not in original file) or renamed by the system in the current configuration step
|
|
75
|
+
self._added_parameters: set[str] = set()
|
|
76
|
+
|
|
77
|
+
# Track parameters deleted by user (were in original file) or renamed by the system in the current configuration step
|
|
78
|
+
self._deleted_parameters: set[str] = set()
|
|
79
|
+
|
|
80
|
+
self._at_least_one_changed = False
|
|
81
|
+
|
|
82
|
+
self._last_time_asked_to_save: float = 0
|
|
83
|
+
|
|
84
|
+
# frontend_tkinter_parameter_editor_table.py API start
|
|
68
85
|
@property
|
|
69
86
|
def connected_vehicle_type(self) -> str:
|
|
70
87
|
return (
|
|
71
|
-
getattr(self.
|
|
72
|
-
if hasattr(self.
|
|
88
|
+
getattr(self._flight_controller.info, "vehicle_type", "")
|
|
89
|
+
if hasattr(self._flight_controller, "info") and self._flight_controller.info is not None
|
|
73
90
|
else ""
|
|
74
91
|
)
|
|
75
92
|
|
|
76
93
|
@property
|
|
77
94
|
def is_fc_connected(self) -> bool:
|
|
78
|
-
return self.
|
|
95
|
+
return self._flight_controller.master is not None
|
|
79
96
|
|
|
80
97
|
@property
|
|
81
98
|
def fc_parameters(self) -> dict[str, float]:
|
|
82
99
|
return (
|
|
83
|
-
self.
|
|
84
|
-
if hasattr(self.
|
|
100
|
+
self._flight_controller.fc_parameters
|
|
101
|
+
if hasattr(self._flight_controller, "fc_parameters") and self._flight_controller.fc_parameters is not None
|
|
85
102
|
else {}
|
|
86
103
|
)
|
|
87
104
|
|
|
88
105
|
@property
|
|
89
106
|
def is_mavftp_supported(self) -> bool:
|
|
90
107
|
return (
|
|
91
|
-
getattr(self.
|
|
92
|
-
if hasattr(self.
|
|
108
|
+
getattr(self._flight_controller.info, "is_mavftp_supported", False)
|
|
109
|
+
if hasattr(self._flight_controller, "info") and self._flight_controller.info is not None
|
|
93
110
|
else False
|
|
94
111
|
)
|
|
95
112
|
|
|
96
|
-
@property
|
|
97
|
-
def current_file_parameters(self) -> ParDict:
|
|
98
|
-
return self.filesystem.file_parameters.get(self.current_file, ParDict())
|
|
99
|
-
|
|
100
113
|
def handle_imu_temperature_calibration_workflow( # pylint: disable=too-many-arguments, too-many-positional-arguments
|
|
101
114
|
self,
|
|
102
115
|
selected_file: str,
|
|
@@ -126,7 +139,9 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
126
139
|
|
|
127
140
|
"""
|
|
128
141
|
# Check if IMU temperature calibration should be offered for this file
|
|
129
|
-
tempcal_imu_result_param_filename, tempcal_imu_result_param_fullpath =
|
|
142
|
+
tempcal_imu_result_param_filename, tempcal_imu_result_param_fullpath = (
|
|
143
|
+
self._local_filesystem.tempcal_imu_result_param_tuple()
|
|
144
|
+
)
|
|
130
145
|
if selected_file != tempcal_imu_result_param_filename:
|
|
131
146
|
return False
|
|
132
147
|
|
|
@@ -161,19 +176,19 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
161
176
|
log_parm=False,
|
|
162
177
|
online=False,
|
|
163
178
|
tclr=False,
|
|
164
|
-
figpath=self.
|
|
179
|
+
figpath=self._local_filesystem.vehicle_dir,
|
|
165
180
|
progress_callback=progress_callback,
|
|
166
181
|
)
|
|
167
182
|
|
|
168
183
|
try:
|
|
169
184
|
# Reload parameter files after calibration
|
|
170
|
-
self.
|
|
185
|
+
self._local_filesystem.file_parameters = self._local_filesystem.read_params_from_files()
|
|
171
186
|
return True
|
|
172
187
|
except SystemExit as exp:
|
|
173
188
|
show_error(_("Fatal error reading parameter files"), f"{exp}")
|
|
174
189
|
raise
|
|
175
190
|
|
|
176
|
-
def
|
|
191
|
+
def _should_copy_fc_values_to_file(self, selected_file: str) -> tuple[bool, Optional[dict], Optional[str]]:
|
|
177
192
|
"""
|
|
178
193
|
Check if flight controller values should be copied to the specified file.
|
|
179
194
|
|
|
@@ -186,18 +201,18 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
186
201
|
auto_changed_by contains the tool name that requires external changes.
|
|
187
202
|
|
|
188
203
|
"""
|
|
189
|
-
auto_changed_by = self.
|
|
190
|
-
if auto_changed_by and self.
|
|
204
|
+
auto_changed_by = self._local_filesystem.auto_changed_by(selected_file)
|
|
205
|
+
if auto_changed_by and self._flight_controller.fc_parameters:
|
|
191
206
|
# Filter relevant FC parameters for this file
|
|
192
207
|
relevant_fc_params = {
|
|
193
208
|
key: value
|
|
194
|
-
for key, value in self.
|
|
195
|
-
if key in self.
|
|
209
|
+
for key, value in self._flight_controller.fc_parameters.items()
|
|
210
|
+
if key in self.current_step_parameters
|
|
196
211
|
}
|
|
197
212
|
return True, relevant_fc_params, auto_changed_by
|
|
198
213
|
return False, None, auto_changed_by
|
|
199
214
|
|
|
200
|
-
def
|
|
215
|
+
def _copy_fc_values_to_file(self, selected_file: str, relevant_fc_params: dict) -> bool:
|
|
201
216
|
"""
|
|
202
217
|
Copy FC values to the specified file.
|
|
203
218
|
|
|
@@ -209,10 +224,78 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
209
224
|
bool: True if parameters were copied successfully.
|
|
210
225
|
|
|
211
226
|
"""
|
|
212
|
-
params_copied = self.
|
|
227
|
+
params_copied = self._local_filesystem.copy_fc_values_to_file(selected_file, relevant_fc_params)
|
|
213
228
|
return bool(params_copied)
|
|
214
229
|
|
|
215
|
-
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]:
|
|
216
299
|
"""
|
|
217
300
|
Get available file jump options for the selected file.
|
|
218
301
|
|
|
@@ -223,13 +306,45 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
223
306
|
dict: Dictionary mapping destination files to their messages.
|
|
224
307
|
|
|
225
308
|
"""
|
|
226
|
-
return self.
|
|
309
|
+
return self._local_filesystem.jump_possible(selected_file)
|
|
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
|
|
227
342
|
|
|
228
343
|
def should_download_file_from_url_workflow(
|
|
229
344
|
self,
|
|
230
345
|
selected_file: str,
|
|
231
|
-
ask_confirmation:
|
|
232
|
-
show_error:
|
|
346
|
+
ask_confirmation: AskConfirmationCallback,
|
|
347
|
+
show_error: ShowErrorCallback,
|
|
233
348
|
) -> bool:
|
|
234
349
|
"""
|
|
235
350
|
Handle file download workflow with injected GUI callbacks.
|
|
@@ -246,11 +361,11 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
246
361
|
bool: True if download was successful or not needed, False if download failed.
|
|
247
362
|
|
|
248
363
|
"""
|
|
249
|
-
url, local_filename = self.
|
|
364
|
+
url, local_filename = self._local_filesystem.get_download_url_and_local_filename(selected_file)
|
|
250
365
|
if not url or not local_filename:
|
|
251
366
|
return True # No download required
|
|
252
367
|
|
|
253
|
-
if self.
|
|
368
|
+
if self._local_filesystem.vehicle_configuration_file_exists(local_filename):
|
|
254
369
|
return True # File already exists in the vehicle directory, no need to download it
|
|
255
370
|
|
|
256
371
|
# Ask user for confirmation
|
|
@@ -291,16 +406,16 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
291
406
|
bool: True if upload was successful or not needed, False if upload failed.
|
|
292
407
|
|
|
293
408
|
"""
|
|
294
|
-
local_filename, remote_filename = self.
|
|
409
|
+
local_filename, remote_filename = self._local_filesystem.get_upload_local_and_remote_filenames(selected_file)
|
|
295
410
|
if not local_filename or not remote_filename:
|
|
296
411
|
return True # No upload required
|
|
297
412
|
|
|
298
|
-
if not self.
|
|
413
|
+
if not self._local_filesystem.vehicle_configuration_file_exists(local_filename):
|
|
299
414
|
error_msg = _("Local file {local_filename} does not exist")
|
|
300
415
|
show_error(_("Will not upload any file"), error_msg.format(local_filename=local_filename))
|
|
301
416
|
return False
|
|
302
417
|
|
|
303
|
-
if self.
|
|
418
|
+
if self._flight_controller.master is None:
|
|
304
419
|
show_warning(_("Will not upload any file"), _("No flight controller connection"))
|
|
305
420
|
return False
|
|
306
421
|
|
|
@@ -312,7 +427,7 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
312
427
|
return True # User declined upload
|
|
313
428
|
|
|
314
429
|
# Attempt upload
|
|
315
|
-
if not self.
|
|
430
|
+
if not self._flight_controller.upload_file(local_filename, remote_filename, progress_callback):
|
|
316
431
|
error_msg = _("Failed to upload {local_filename} to {remote_filename}, please upload it manually")
|
|
317
432
|
show_error(_("Upload failed"), error_msg.format(local_filename=local_filename, remote_filename=remote_filename))
|
|
318
433
|
return False
|
|
@@ -331,18 +446,18 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
331
446
|
|
|
332
447
|
"""
|
|
333
448
|
# Download all parameters from the flight controller
|
|
334
|
-
fc_parameters, param_default_values = self.
|
|
449
|
+
fc_parameters, param_default_values = self._flight_controller.download_params(
|
|
335
450
|
progress_callback,
|
|
336
|
-
Path(self.
|
|
337
|
-
Path(self.
|
|
451
|
+
Path(self._local_filesystem.vehicle_dir) / "complete.param",
|
|
452
|
+
Path(self._local_filesystem.vehicle_dir) / "00_default.param",
|
|
338
453
|
)
|
|
339
454
|
|
|
340
455
|
# Update the flight controller parameters
|
|
341
|
-
self.
|
|
456
|
+
self._flight_controller.fc_parameters = fc_parameters
|
|
342
457
|
|
|
343
458
|
# Write default values to file if available
|
|
344
459
|
if param_default_values:
|
|
345
|
-
self.
|
|
460
|
+
self._local_filesystem.write_param_default_values_to_file(param_default_values)
|
|
346
461
|
|
|
347
462
|
return fc_parameters, param_default_values
|
|
348
463
|
|
|
@@ -373,17 +488,17 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
373
488
|
# Write each selected parameter to the flight controller
|
|
374
489
|
for param_name, param in selected_params.items():
|
|
375
490
|
try:
|
|
376
|
-
if param_name not in self.
|
|
377
|
-
self.
|
|
491
|
+
if param_name not in self._flight_controller.fc_parameters or not is_within_tolerance(
|
|
492
|
+
self._flight_controller.fc_parameters[param_name], param.value
|
|
378
493
|
):
|
|
379
|
-
param_metadata = self.
|
|
494
|
+
param_metadata = self._local_filesystem.doc_dict.get(param_name, None)
|
|
380
495
|
if param_metadata and param_metadata.get("RebootRequired", False):
|
|
381
|
-
self.
|
|
382
|
-
if param_name in self.
|
|
496
|
+
self._flight_controller.set_param(param_name, float(param.value))
|
|
497
|
+
if param_name in self._flight_controller.fc_parameters:
|
|
383
498
|
logging_info(
|
|
384
499
|
_("Parameter %s changed from %f to %f, reset required"),
|
|
385
500
|
param_name,
|
|
386
|
-
self.
|
|
501
|
+
self._flight_controller.fc_parameters[param_name],
|
|
387
502
|
param.value,
|
|
388
503
|
)
|
|
389
504
|
else:
|
|
@@ -391,12 +506,12 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
391
506
|
reset_required = True
|
|
392
507
|
# Check if any of the selected parameters have a _TYPE, _EN, or _ENABLE suffix
|
|
393
508
|
elif param_name.endswith(("_TYPE", "_EN", "_ENABLE", "SID_AXIS")):
|
|
394
|
-
self.
|
|
395
|
-
if param_name in self.
|
|
509
|
+
self._flight_controller.set_param(param_name, float(param.value))
|
|
510
|
+
if param_name in self._flight_controller.fc_parameters:
|
|
396
511
|
logging_info(
|
|
397
512
|
_("Parameter %s changed from %f to %f, possible reset required"),
|
|
398
513
|
param_name,
|
|
399
|
-
self.
|
|
514
|
+
self._flight_controller.fc_parameters[param_name],
|
|
400
515
|
param.value,
|
|
401
516
|
)
|
|
402
517
|
else:
|
|
@@ -423,10 +538,13 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
423
538
|
int: Extra sleep time in seconds.
|
|
424
539
|
|
|
425
540
|
"""
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
541
|
+
param_boot_delay = (
|
|
542
|
+
self.current_step_parameters["BRD_BOOT_DELAY"].get_new_value()
|
|
543
|
+
if "BRD_BOOT_DELAY" in self.current_step_parameters
|
|
544
|
+
else 0.0
|
|
545
|
+
)
|
|
546
|
+
flightcontroller_boot_delay = self._flight_controller.fc_parameters.get("BRD_BOOT_DELAY", 0)
|
|
547
|
+
return int(max(param_boot_delay, flightcontroller_boot_delay) // 1000 + 1) # round up
|
|
430
548
|
|
|
431
549
|
def _reset_and_reconnect_flight_controller(
|
|
432
550
|
self, progress_callback: Optional[Callable] = None, sleep_time: Optional[int] = None
|
|
@@ -446,7 +564,7 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
446
564
|
sleep_time = self._calculate_reset_time()
|
|
447
565
|
|
|
448
566
|
# Call reset_and_reconnect with a callback to update the reset progress bar and the progress message
|
|
449
|
-
return self.
|
|
567
|
+
return self._flight_controller.reset_and_reconnect(progress_callback, None, int(sleep_time))
|
|
450
568
|
|
|
451
569
|
def reset_and_reconnect_workflow( # pylint: disable=too-many-arguments, too-many-positional-arguments
|
|
452
570
|
self,
|
|
@@ -492,7 +610,7 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
492
610
|
|
|
493
611
|
return True # No reset needed
|
|
494
612
|
|
|
495
|
-
def
|
|
613
|
+
def _upload_parameters_to_fc(self, selected_params: dict, show_error: Callable[[str, str], None]) -> int:
|
|
496
614
|
"""
|
|
497
615
|
Upload selected parameters to flight controller.
|
|
498
616
|
|
|
@@ -510,15 +628,15 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
510
628
|
|
|
511
629
|
for param_name, param in selected_params.items():
|
|
512
630
|
try:
|
|
513
|
-
self.
|
|
514
|
-
if param_name not in self.
|
|
515
|
-
self.
|
|
631
|
+
self._flight_controller.set_param(param_name, param.value)
|
|
632
|
+
if param_name not in self._flight_controller.fc_parameters or not is_within_tolerance(
|
|
633
|
+
self._flight_controller.fc_parameters[param_name], param.value
|
|
516
634
|
):
|
|
517
|
-
if param_name in self.
|
|
635
|
+
if param_name in self._flight_controller.fc_parameters:
|
|
518
636
|
logging_info(
|
|
519
637
|
_("Parameter %s changed from %f to %f"),
|
|
520
638
|
param_name,
|
|
521
|
-
self.
|
|
639
|
+
self._flight_controller.fc_parameters[param_name],
|
|
522
640
|
param.value,
|
|
523
641
|
)
|
|
524
642
|
else:
|
|
@@ -593,7 +711,7 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
593
711
|
"39_autotune_roll_pitch_results.param",
|
|
594
712
|
]
|
|
595
713
|
|
|
596
|
-
report_file_path = Path(getattr(self.
|
|
714
|
+
report_file_path = Path(getattr(self._local_filesystem, "vehicle_dir", ".")) / "tuning_report.csv"
|
|
597
715
|
|
|
598
716
|
# Write a CSV with a header ("param", <list of files>) and one row per parameter.
|
|
599
717
|
with open(report_file_path, "w", newline="", encoding="utf-8") as file:
|
|
@@ -605,34 +723,107 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
605
723
|
for param_file in report_files:
|
|
606
724
|
try:
|
|
607
725
|
if param_file == "00_default.param":
|
|
608
|
-
value = str(self.
|
|
726
|
+
value = str(self._local_filesystem.param_default_dict[param_name].value)
|
|
609
727
|
else:
|
|
610
|
-
value = str(self.
|
|
728
|
+
value = str(self._local_filesystem.file_parameters[param_file][param_name].value)
|
|
611
729
|
except (KeyError, ValueError):
|
|
612
730
|
# On any unexpected structure, leave the value empty (don't crash)
|
|
613
731
|
value = ""
|
|
614
732
|
row.append(value)
|
|
615
733
|
writer.writerow(row)
|
|
616
734
|
|
|
617
|
-
def
|
|
735
|
+
def upload_selected_params_workflow( # pylint: disable=too-many-arguments, too-many-positional-arguments
|
|
736
|
+
self,
|
|
737
|
+
selected_params: dict,
|
|
738
|
+
ask_confirmation: AskConfirmationCallback,
|
|
739
|
+
ask_retry_cancel: AskRetryCancelCallback,
|
|
740
|
+
show_error: ShowErrorCallback,
|
|
741
|
+
progress_callback_for_reset: Optional[Callable] = None,
|
|
742
|
+
progress_callback_for_download: Optional[Callable] = None,
|
|
743
|
+
) -> None:
|
|
744
|
+
"""
|
|
745
|
+
Complete workflow for uploading selected parameters, including reset, upload, validation, and retry.
|
|
746
|
+
|
|
747
|
+
Args:
|
|
748
|
+
selected_params: Dictionary of parameters to upload.
|
|
749
|
+
ask_confirmation: Callback to ask user for confirmation.
|
|
750
|
+
ask_retry_cancel: Callback to ask user to retry or cancel on upload error.
|
|
751
|
+
show_error: Callback to show error messages.
|
|
752
|
+
progress_callback_for_reset: Optional callback for reset progress.
|
|
753
|
+
progress_callback_for_download: Optional callback for download progress.
|
|
754
|
+
|
|
755
|
+
"""
|
|
756
|
+
logging_info(
|
|
757
|
+
_("Uploading %d selected %s parameters to flight controller..."),
|
|
758
|
+
len(selected_params),
|
|
759
|
+
self.current_file,
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
# Upload parameters that require reset
|
|
763
|
+
reset_happened = self.upload_parameters_that_require_reset_workflow(
|
|
764
|
+
selected_params,
|
|
765
|
+
ask_confirmation,
|
|
766
|
+
show_error,
|
|
767
|
+
progress_callback_for_reset,
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
# Upload the selected parameters
|
|
771
|
+
nr_changed = self._upload_parameters_to_fc(selected_params, show_error)
|
|
772
|
+
|
|
773
|
+
if reset_happened or nr_changed > 0:
|
|
774
|
+
self._at_least_one_changed = True
|
|
775
|
+
|
|
776
|
+
if self._at_least_one_changed:
|
|
777
|
+
# Re-download all parameters to validate
|
|
778
|
+
self.download_flight_controller_parameters(progress_callback_for_download)
|
|
779
|
+
param_upload_error = self._validate_uploaded_parameters(selected_params)
|
|
780
|
+
|
|
781
|
+
if param_upload_error:
|
|
782
|
+
if ask_retry_cancel(
|
|
783
|
+
_("Parameter upload error"),
|
|
784
|
+
_("Failed to upload the following parameters to the flight controller:\n")
|
|
785
|
+
+ f"{(', ').join(param_upload_error)}",
|
|
786
|
+
):
|
|
787
|
+
# Retry the entire workflow
|
|
788
|
+
self.upload_selected_params_workflow(
|
|
789
|
+
selected_params,
|
|
790
|
+
ask_confirmation,
|
|
791
|
+
ask_retry_cancel,
|
|
792
|
+
show_error,
|
|
793
|
+
progress_callback_for_reset,
|
|
794
|
+
progress_callback_for_download,
|
|
795
|
+
)
|
|
796
|
+
# If not retrying, continue without success message
|
|
797
|
+
else:
|
|
798
|
+
logging_info(_("All parameters uploaded to the flight controller successfully"))
|
|
799
|
+
|
|
800
|
+
self._export_fc_params_missing_or_different()
|
|
801
|
+
|
|
802
|
+
self._write_current_file()
|
|
803
|
+
self._at_least_one_changed = False
|
|
804
|
+
|
|
805
|
+
# frontend_tkinter_parameter_editor_table.py API end
|
|
806
|
+
|
|
807
|
+
# frontend_tkinter_parameter_editor.py API start
|
|
808
|
+
def _validate_uploaded_parameters(self, selected_params: dict) -> list[str]:
|
|
618
809
|
logging_info(_("Re-downloaded all parameters from the flight controller"))
|
|
619
810
|
|
|
620
811
|
# Validate that the read parameters are the same as the ones in the current_file
|
|
621
812
|
param_upload_error = []
|
|
622
813
|
for param_name, param in selected_params.items():
|
|
623
814
|
if (
|
|
624
|
-
param_name in self.
|
|
815
|
+
param_name in self._flight_controller.fc_parameters
|
|
625
816
|
and param is not None
|
|
626
|
-
and not is_within_tolerance(self.
|
|
817
|
+
and not is_within_tolerance(self._flight_controller.fc_parameters[param_name], float(param.value))
|
|
627
818
|
):
|
|
628
819
|
logging_error(
|
|
629
820
|
_("Parameter %s upload to the flight controller failed. Expected: %f, Actual: %f"),
|
|
630
821
|
param_name,
|
|
631
822
|
param.value,
|
|
632
|
-
self.
|
|
823
|
+
self._flight_controller.fc_parameters[param_name],
|
|
633
824
|
)
|
|
634
825
|
param_upload_error.append(param_name)
|
|
635
|
-
if param_name not in self.
|
|
826
|
+
if param_name not in self._flight_controller.fc_parameters:
|
|
636
827
|
logging_error(
|
|
637
828
|
_("Parameter %s upload to the flight controller failed. Expected: %f, Actual: N/A"),
|
|
638
829
|
param_name,
|
|
@@ -650,18 +841,20 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
650
841
|
|
|
651
842
|
"""
|
|
652
843
|
# Create FC parameters dictionary
|
|
653
|
-
fc_parameters = ParDict.from_fc_parameters(self.
|
|
844
|
+
fc_parameters = ParDict.from_fc_parameters(self._flight_controller.fc_parameters)
|
|
654
845
|
|
|
655
846
|
# Early exit if no FC parameters available
|
|
656
847
|
if len(fc_parameters) == 0:
|
|
657
848
|
return fc_parameters
|
|
658
849
|
|
|
659
850
|
# Remove default parameters from FC parameters if default file exists
|
|
660
|
-
fc_parameters.remove_if_value_is_similar(self.
|
|
851
|
+
fc_parameters.remove_if_value_is_similar(self._local_filesystem.param_default_dict, is_within_tolerance)
|
|
661
852
|
|
|
662
853
|
# Filter out read-only parameters efficiently - only check params that exist in fc_parameters
|
|
663
854
|
readonly_params_to_remove = [
|
|
664
|
-
param_name
|
|
855
|
+
param_name
|
|
856
|
+
for param_name in fc_parameters
|
|
857
|
+
if self._local_filesystem.doc_dict.get(param_name, {}).get("ReadOnly", False)
|
|
665
858
|
]
|
|
666
859
|
for param_name in readonly_params_to_remove:
|
|
667
860
|
del fc_parameters[param_name]
|
|
@@ -681,13 +874,13 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
681
874
|
last_filename: Last configuration file to process (inclusive).
|
|
682
875
|
|
|
683
876
|
"""
|
|
684
|
-
if not self.
|
|
877
|
+
if not self._flight_controller.fc_parameters:
|
|
685
878
|
return
|
|
686
879
|
|
|
687
880
|
# Create the compounded state of all parameters stored in the AMC .param files
|
|
688
881
|
compound = ParDict()
|
|
689
882
|
first_config_step_filename = None
|
|
690
|
-
for file_name, file_params in self.
|
|
883
|
+
for file_name, file_params in self._local_filesystem.file_parameters.items():
|
|
691
884
|
if file_name != "00_default.param":
|
|
692
885
|
if first_config_step_filename is None:
|
|
693
886
|
first_config_step_filename = file_name
|
|
@@ -722,7 +915,7 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
722
915
|
first_name_without_ext = first_config_step_filename.rsplit(".", 1)[0] if first_config_step_filename else "unknown"
|
|
723
916
|
# the last filename already has the .param extension
|
|
724
917
|
filename = f"fc_params_missing_or_different_in_the_amc_param_files_{first_name_without_ext}_to_{last_filename}"
|
|
725
|
-
self.
|
|
918
|
+
self._local_filesystem.export_to_param(params_missing_in_the_amc_param_files, filename, annotate_doc=False)
|
|
726
919
|
logging_info(
|
|
727
920
|
_("Exported %d FC parameters missing or different in AMC files to %s"),
|
|
728
921
|
len(params_missing_in_the_amc_param_files),
|
|
@@ -731,10 +924,10 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
731
924
|
else:
|
|
732
925
|
logging_info(_("No FC parameters are missing or different from AMC parameter files"))
|
|
733
926
|
|
|
734
|
-
def
|
|
927
|
+
def _export_fc_params_missing_or_different(self) -> None:
|
|
735
928
|
non_default_non_read_only_fc_params = self._get_non_default_non_read_only_fc_params()
|
|
736
929
|
|
|
737
|
-
last_config_step_filename = list(self.
|
|
930
|
+
last_config_step_filename = list(self._local_filesystem.file_parameters.keys())[-1]
|
|
738
931
|
# Export FC parameters that are missing or different from AMC parameter files
|
|
739
932
|
self._export_fc_params_missing_or_different_in_amc_files(non_default_non_read_only_fc_params, self.current_file)
|
|
740
933
|
self._export_fc_params_missing_or_different_in_amc_files(
|
|
@@ -759,7 +952,7 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
759
952
|
run_in_thread: Callback to run the download in a thread (optional).
|
|
760
953
|
|
|
761
954
|
"""
|
|
762
|
-
if self.
|
|
955
|
+
if self._flight_controller.master is None:
|
|
763
956
|
show_error(_("Error"), _("No flight controller connected"))
|
|
764
957
|
return
|
|
765
958
|
|
|
@@ -771,26 +964,29 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
771
964
|
if not filename:
|
|
772
965
|
return
|
|
773
966
|
|
|
774
|
-
success = self.
|
|
967
|
+
success = self._flight_controller.download_last_flight_log(filename, progress_callback)
|
|
775
968
|
if success:
|
|
776
969
|
show_info(_("Success"), _("Flight log downloaded successfully to:\n%s") % filename)
|
|
777
970
|
else:
|
|
778
971
|
show_error(_("Error"), _("Failed to download flight log. Check the console for details."))
|
|
779
972
|
|
|
780
|
-
def is_configuration_step_optional(self, file_name: str, threshold_pct: int = 20) -> bool:
|
|
973
|
+
def is_configuration_step_optional(self, file_name: Optional[str] = None, threshold_pct: int = 20) -> bool:
|
|
781
974
|
"""
|
|
782
975
|
Check if the configuration step for the given file is optional.
|
|
783
976
|
|
|
784
977
|
Args:
|
|
785
|
-
file_name: Name of the configuration file to check.
|
|
978
|
+
file_name: Name of the configuration file to check, defaults to self.current_file.
|
|
786
979
|
threshold_pct: Threshold percentage below which the step is considered optional.
|
|
787
980
|
|
|
788
981
|
Returns:
|
|
789
982
|
bool: True if the configuration step is optional, False if mandatory.
|
|
790
983
|
|
|
791
984
|
"""
|
|
985
|
+
if file_name is None:
|
|
986
|
+
file_name = self.current_file
|
|
987
|
+
|
|
792
988
|
# Check if the configuration step for the given file is optional
|
|
793
|
-
mandatory_text, _mandatory_url = self.
|
|
989
|
+
mandatory_text, _mandatory_url = self._local_filesystem.get_documentation_text_and_url(file_name, "mandatory")
|
|
794
990
|
# Extract percentage from mandatory_text like "80% mandatory (20% optional)"
|
|
795
991
|
percentage = 0
|
|
796
992
|
if mandatory_text:
|
|
@@ -801,21 +997,24 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
801
997
|
|
|
802
998
|
return percentage <= threshold_pct
|
|
803
999
|
|
|
804
|
-
def get_next_non_optional_file(self, current_file: str) -> Optional[str]:
|
|
1000
|
+
def get_next_non_optional_file(self, current_file: Optional[str] = None) -> Optional[str]:
|
|
805
1001
|
"""
|
|
806
1002
|
Get the next non-optional configuration file in sequence.
|
|
807
1003
|
|
|
808
1004
|
Args:
|
|
809
|
-
current_file: The current parameter file being processed.
|
|
1005
|
+
current_file: The current parameter file being processed, defaults to self.current_file.
|
|
810
1006
|
|
|
811
1007
|
Returns:
|
|
812
1008
|
Optional[str]: Next non-optional file name, or None if at the end.
|
|
813
1009
|
|
|
814
1010
|
"""
|
|
815
|
-
files = list(self.
|
|
1011
|
+
files = list(self._local_filesystem.file_parameters.keys())
|
|
816
1012
|
if not files:
|
|
817
1013
|
return None
|
|
818
1014
|
|
|
1015
|
+
if current_file is None:
|
|
1016
|
+
current_file = self.current_file
|
|
1017
|
+
|
|
819
1018
|
try:
|
|
820
1019
|
next_file_index = files.index(current_file) + 1
|
|
821
1020
|
|
|
@@ -843,14 +1042,14 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
843
1042
|
|
|
844
1043
|
"""
|
|
845
1044
|
# Get annotated FC parameters
|
|
846
|
-
annotated_fc_parameters = self.
|
|
847
|
-
self.
|
|
1045
|
+
annotated_fc_parameters = self._local_filesystem.annotate_intermediate_comments_to_param_dict(
|
|
1046
|
+
self._flight_controller.fc_parameters
|
|
848
1047
|
)
|
|
849
1048
|
if not annotated_fc_parameters:
|
|
850
1049
|
return {}
|
|
851
1050
|
|
|
852
1051
|
# Categorize parameters using filesystem logic
|
|
853
|
-
categorized = self.
|
|
1052
|
+
categorized = self._local_filesystem.categorize_parameters(annotated_fc_parameters)
|
|
854
1053
|
if not categorized or len(categorized) != 3:
|
|
855
1054
|
# Return empty dict if categorization fails or returns empty tuple
|
|
856
1055
|
return {}
|
|
@@ -1009,13 +1208,13 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
1009
1208
|
return False
|
|
1010
1209
|
|
|
1011
1210
|
# If file exists, ask user for confirmation
|
|
1012
|
-
if self.
|
|
1211
|
+
if self._local_filesystem.vehicle_configuration_file_exists(filename):
|
|
1013
1212
|
msg = _("{} file already exists.\nDo you want to overwrite it?")
|
|
1014
1213
|
should_write_file = ask_confirmation(_("Overwrite existing file"), msg.format(filename))
|
|
1015
1214
|
|
|
1016
1215
|
# Write the file using if confirmed and has parameters
|
|
1017
1216
|
if should_write_file:
|
|
1018
|
-
self.
|
|
1217
|
+
self._local_filesystem.export_to_param(param_dict, filename, annotate_doc)
|
|
1019
1218
|
logging_info(_("Summary file %s written"), filename)
|
|
1020
1219
|
|
|
1021
1220
|
return should_write_file
|
|
@@ -1042,14 +1241,14 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
1042
1241
|
should_write_file = True # Default to writing new files
|
|
1043
1242
|
|
|
1044
1243
|
# If file exists, ask user for confirmation
|
|
1045
|
-
if self.
|
|
1046
|
-
zip_file_path = self.
|
|
1244
|
+
if self._local_filesystem.zip_file_exists():
|
|
1245
|
+
zip_file_path = self._local_filesystem.zip_file_path()
|
|
1047
1246
|
msg = _("{} file already exists.\nDo you want to overwrite it?")
|
|
1048
1247
|
should_write_file = ask_confirmation(_("Overwrite existing file"), msg.format(zip_file_path))
|
|
1049
1248
|
|
|
1050
1249
|
if should_write_file:
|
|
1051
|
-
self.
|
|
1052
|
-
zip_file_path = self.
|
|
1250
|
+
self._local_filesystem.zip_files(files_to_zip)
|
|
1251
|
+
zip_file_path = self._local_filesystem.zip_file_path()
|
|
1053
1252
|
msg = _(
|
|
1054
1253
|
"All relevant files have been zipped into the \n"
|
|
1055
1254
|
"{zip_file_path} file.\n\nYou can now upload this file to the ArduPilot Methodic\n"
|
|
@@ -1061,18 +1260,75 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
1061
1260
|
|
|
1062
1261
|
def repopulate_configuration_step_parameters(
|
|
1063
1262
|
self,
|
|
1064
|
-
) -> tuple[
|
|
1263
|
+
) -> tuple[list[tuple[str, str]], list[tuple[str, str]]]:
|
|
1065
1264
|
"""
|
|
1066
1265
|
Process the configuration step for the current file and update the self.parameters.
|
|
1067
1266
|
|
|
1068
1267
|
Returns:
|
|
1069
|
-
tuple: (
|
|
1268
|
+
tuple: (ui_errors, ui_infos)
|
|
1070
1269
|
|
|
1071
1270
|
"""
|
|
1072
|
-
|
|
1073
|
-
|
|
1271
|
+
# Reset tracking sets when navigating to new file
|
|
1272
|
+
self._added_parameters.clear()
|
|
1273
|
+
self._deleted_parameters.clear()
|
|
1274
|
+
|
|
1275
|
+
# Process configuration step and get operations to apply
|
|
1276
|
+
self.current_step_parameters, ui_errors, ui_infos, duplicates_to_remove, renames_to_apply, derived_params = (
|
|
1277
|
+
self._config_step_processor.process_configuration_step(self.current_file, self.fc_parameters)
|
|
1074
1278
|
)
|
|
1075
|
-
|
|
1279
|
+
|
|
1280
|
+
# Apply derived parameters to domain model using specialized setters
|
|
1281
|
+
for param_name, derived_par in derived_params.items():
|
|
1282
|
+
if param_name in self.current_step_parameters:
|
|
1283
|
+
# Update existing forced/derived parameter with new value using dedicated setter
|
|
1284
|
+
# The setter methods will raise ValueError for invalid parameters (not forced/derived, readonly, etc.)
|
|
1285
|
+
try:
|
|
1286
|
+
self.current_step_parameters[param_name].set_forced_or_derived_value(float(derived_par.value))
|
|
1287
|
+
if derived_par.comment:
|
|
1288
|
+
self.current_step_parameters[param_name].set_forced_or_derived_change_reason(derived_par.comment)
|
|
1289
|
+
except (ValueError, TypeError) as e:
|
|
1290
|
+
logging_error(
|
|
1291
|
+
_("Failed to apply derived parameter %s: %s"),
|
|
1292
|
+
param_name,
|
|
1293
|
+
str(e),
|
|
1294
|
+
)
|
|
1295
|
+
else:
|
|
1296
|
+
# Parameter in derived_params but not in self.parameters - this is unexpected
|
|
1297
|
+
logging_error(
|
|
1298
|
+
_("Derived parameter %s not found in current parameters, skipping"),
|
|
1299
|
+
param_name,
|
|
1300
|
+
)
|
|
1301
|
+
|
|
1302
|
+
# Apply rename operations to domain model using add/delete tracking
|
|
1303
|
+
for old_name in duplicates_to_remove:
|
|
1304
|
+
# Mark duplicate as deleted
|
|
1305
|
+
if old_name in self._local_filesystem.file_parameters.get(self.current_file, ParDict()):
|
|
1306
|
+
self._deleted_parameters.add(old_name)
|
|
1307
|
+
# Remove from domain model
|
|
1308
|
+
if old_name in self.current_step_parameters:
|
|
1309
|
+
del self.current_step_parameters[old_name]
|
|
1310
|
+
|
|
1311
|
+
for old_name, new_name in renames_to_apply:
|
|
1312
|
+
# Get the parameter value from the original file
|
|
1313
|
+
original_params = self._local_filesystem.file_parameters.get(self.current_file, ParDict())
|
|
1314
|
+
if old_name in original_params:
|
|
1315
|
+
# Mark old parameter as deleted
|
|
1316
|
+
self._deleted_parameters.add(old_name)
|
|
1317
|
+
|
|
1318
|
+
# Create new parameter with renamed name
|
|
1319
|
+
old_par = original_params[old_name]
|
|
1320
|
+
self.current_step_parameters[new_name] = self._config_step_processor.create_ardupilot_parameter(
|
|
1321
|
+
new_name, old_par, self.current_file, self.fc_parameters
|
|
1322
|
+
)
|
|
1323
|
+
|
|
1324
|
+
# Mark new parameter as added
|
|
1325
|
+
self._added_parameters.add(new_name)
|
|
1326
|
+
|
|
1327
|
+
# Remove old parameter from domain model
|
|
1328
|
+
if old_name in self.current_step_parameters:
|
|
1329
|
+
del self.current_step_parameters[old_name]
|
|
1330
|
+
|
|
1331
|
+
return ui_errors, ui_infos
|
|
1076
1332
|
|
|
1077
1333
|
def get_different_parameters(self) -> dict[str, ArduPilotParameter]:
|
|
1078
1334
|
"""
|
|
@@ -1082,7 +1338,7 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
1082
1338
|
Dictionary of parameters that are different from FC
|
|
1083
1339
|
|
|
1084
1340
|
"""
|
|
1085
|
-
return self.
|
|
1341
|
+
return self._config_step_processor.filter_different_parameters(self.current_step_parameters)
|
|
1086
1342
|
|
|
1087
1343
|
def delete_parameter_from_current_file(self, param_name: str) -> None:
|
|
1088
1344
|
"""
|
|
@@ -1092,18 +1348,30 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
1092
1348
|
param_name: The name of the parameter to delete
|
|
1093
1349
|
|
|
1094
1350
|
"""
|
|
1095
|
-
|
|
1096
|
-
if param_name in self.
|
|
1097
|
-
|
|
1351
|
+
# If parameter was in original file, mark as deleted
|
|
1352
|
+
if param_name in self._local_filesystem.file_parameters.get(self.current_file, ParDict()):
|
|
1353
|
+
self._deleted_parameters.add(param_name)
|
|
1354
|
+
|
|
1355
|
+
# If it was previously added in this session, remove from added set
|
|
1356
|
+
self._added_parameters.discard(param_name)
|
|
1357
|
+
|
|
1358
|
+
# Remove from runtime state
|
|
1359
|
+
if param_name in self.current_step_parameters:
|
|
1360
|
+
del self.current_step_parameters[param_name]
|
|
1098
1361
|
|
|
1099
1362
|
def get_possible_add_param_names(self) -> list[str]:
|
|
1100
1363
|
"""Return a sorted list of possible parameter names to add, or raise OperationNotPossibleError if not possible."""
|
|
1101
|
-
param_dict = self.
|
|
1364
|
+
param_dict = self._local_filesystem.doc_dict or self.fc_parameters
|
|
1102
1365
|
if not param_dict:
|
|
1103
1366
|
raise OperationNotPossibleError(
|
|
1104
1367
|
_("No apm.pdef.xml file and no FC connected. Not possible autocomplete parameter names.")
|
|
1105
1368
|
)
|
|
1106
|
-
|
|
1369
|
+
|
|
1370
|
+
# Build set of currently active parameters from domain model
|
|
1371
|
+
active_params = set(self.current_step_parameters.keys())
|
|
1372
|
+
|
|
1373
|
+
# Find parameters that aren't currently active
|
|
1374
|
+
possible_add_param_names = [param_name for param_name in param_dict if param_name not in active_params]
|
|
1107
1375
|
possible_add_param_names.sort()
|
|
1108
1376
|
return possible_add_param_names
|
|
1109
1377
|
|
|
@@ -1119,34 +1387,206 @@ class ConfigurationManager: # pylint: disable=too-many-public-methods
|
|
|
1119
1387
|
if not param_name:
|
|
1120
1388
|
raise InvalidParameterNameError(_("Parameter name can not be empty."))
|
|
1121
1389
|
|
|
1122
|
-
if
|
|
1390
|
+
# Check if parameter already exists (in original file, added, or not deleted)
|
|
1391
|
+
original_file_params = self._local_filesystem.file_parameters.get(self.current_file, ParDict())
|
|
1392
|
+
is_in_original = param_name in original_file_params
|
|
1393
|
+
is_already_added = param_name in self._added_parameters
|
|
1394
|
+
is_deleted = param_name in self._deleted_parameters
|
|
1395
|
+
|
|
1396
|
+
if (is_in_original and not is_deleted) or is_already_added:
|
|
1123
1397
|
raise InvalidParameterNameError(_("Parameter already exists, edit it instead"))
|
|
1124
1398
|
|
|
1125
1399
|
fc_parameters = self.fc_parameters
|
|
1126
1400
|
if fc_parameters:
|
|
1127
1401
|
if param_name in fc_parameters:
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1402
|
+
# Create the parameter in domain model
|
|
1403
|
+
par = Par(fc_parameters[param_name], "")
|
|
1404
|
+
self.current_step_parameters[param_name] = self._config_step_processor.create_ardupilot_parameter(
|
|
1405
|
+
param_name, par, self.current_file, fc_parameters
|
|
1131
1406
|
)
|
|
1407
|
+
|
|
1408
|
+
# Track addition
|
|
1409
|
+
if not is_in_original:
|
|
1410
|
+
self._added_parameters.add(param_name)
|
|
1411
|
+
# If was previously deleted, remove from deleted set
|
|
1412
|
+
self._deleted_parameters.discard(param_name)
|
|
1413
|
+
|
|
1132
1414
|
return True
|
|
1133
1415
|
raise InvalidParameterNameError(_("Parameter name not found in the flight controller."))
|
|
1134
1416
|
|
|
1135
|
-
if self.
|
|
1136
|
-
if param_name in self.
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
param_name, self.current_file_parameters[param_name], self.current_file, fc_parameters
|
|
1417
|
+
if self._local_filesystem.doc_dict:
|
|
1418
|
+
if param_name in self._local_filesystem.doc_dict:
|
|
1419
|
+
# Create the parameter in domain model
|
|
1420
|
+
par = Par(self._local_filesystem.param_default_dict.get(param_name, Par(0, "")).value, "")
|
|
1421
|
+
self.current_step_parameters[param_name] = self._config_step_processor.create_ardupilot_parameter(
|
|
1422
|
+
param_name, par, self.current_file, fc_parameters
|
|
1142
1423
|
)
|
|
1424
|
+
|
|
1425
|
+
# Track addition
|
|
1426
|
+
if not is_in_original:
|
|
1427
|
+
self._added_parameters.add(param_name)
|
|
1428
|
+
# If was previously deleted, remove from deleted set
|
|
1429
|
+
self._deleted_parameters.discard(param_name)
|
|
1430
|
+
|
|
1143
1431
|
return True
|
|
1144
1432
|
raise InvalidParameterNameError(
|
|
1145
1433
|
_("'{param_name}' not found in the apm.pdef.xml file.").format(param_name=param_name)
|
|
1146
1434
|
)
|
|
1147
1435
|
|
|
1148
|
-
if not fc_parameters and not self.
|
|
1436
|
+
if not fc_parameters and not self._local_filesystem.doc_dict:
|
|
1149
1437
|
raise OperationNotPossibleError(
|
|
1150
1438
|
_("Can not add parameter when no FC is connected and no apm.pdef.xml file exists.")
|
|
1151
1439
|
)
|
|
1152
1440
|
return False
|
|
1441
|
+
|
|
1442
|
+
def get_parameters_as_par_dict(self, param_names: Optional[list[str]] = None) -> ParDict:
|
|
1443
|
+
"""
|
|
1444
|
+
Extract Par objects from ArduPilotParameter domain models.
|
|
1445
|
+
|
|
1446
|
+
This method converts the domain model objects to data transfer objects (Par)
|
|
1447
|
+
that can be used for file operations or flight controller uploads.
|
|
1448
|
+
|
|
1449
|
+
Args:
|
|
1450
|
+
param_names: Optional list of parameter names to include.
|
|
1451
|
+
If None, includes all parameters.
|
|
1452
|
+
|
|
1453
|
+
Returns:
|
|
1454
|
+
ParDict containing Par objects with current values and change reasons
|
|
1455
|
+
|
|
1456
|
+
"""
|
|
1457
|
+
if param_names is None:
|
|
1458
|
+
param_names = list(self.current_step_parameters.keys())
|
|
1459
|
+
|
|
1460
|
+
return ParDict(
|
|
1461
|
+
{
|
|
1462
|
+
name: Par(self.current_step_parameters[name].get_new_value(), self.current_step_parameters[name].change_reason)
|
|
1463
|
+
for name in param_names
|
|
1464
|
+
if name in self.current_step_parameters
|
|
1465
|
+
}
|
|
1466
|
+
)
|
|
1467
|
+
|
|
1468
|
+
def _has_unsaved_changes(self) -> bool:
|
|
1469
|
+
"""
|
|
1470
|
+
Check if any changes have been made that need to be saved.
|
|
1471
|
+
|
|
1472
|
+
This includes:
|
|
1473
|
+
- User edits to parameter values
|
|
1474
|
+
- Derived parameter changes (tracked via is_dirty)
|
|
1475
|
+
- Forced parameter changes (tracked via is_dirty)
|
|
1476
|
+
- Connection renaming changes (tracked via _added_parameters and _deleted_parameters)
|
|
1477
|
+
- Parameter additions
|
|
1478
|
+
- Parameter deletions
|
|
1479
|
+
|
|
1480
|
+
Returns:
|
|
1481
|
+
True if there are unsaved changes, False otherwise
|
|
1482
|
+
|
|
1483
|
+
"""
|
|
1484
|
+
# Check for structural changes (additions/deletions, including from renames)
|
|
1485
|
+
if self._added_parameters or self._deleted_parameters:
|
|
1486
|
+
return True
|
|
1487
|
+
|
|
1488
|
+
# Check individual parameter edits (value or comment changes)
|
|
1489
|
+
return any(param.is_dirty for param in self.current_step_parameters.values())
|
|
1490
|
+
|
|
1491
|
+
def get_last_configuration_step_number(self) -> Optional[int]:
|
|
1492
|
+
if self._local_filesystem.configuration_phases:
|
|
1493
|
+
# Get the first two characters of the last configuration step filename
|
|
1494
|
+
last_step_filename = next(reversed(self._local_filesystem.file_parameters.keys()))
|
|
1495
|
+
return int(last_step_filename[:2]) + 1 if len(last_step_filename) >= 2 else 1
|
|
1496
|
+
return None
|
|
1497
|
+
|
|
1498
|
+
def get_sorted_phases_with_end_and_weight(self, last_step_nr: int) -> dict[str, PhaseData]:
|
|
1499
|
+
return self._local_filesystem.get_sorted_phases_with_end_and_weight(last_step_nr)
|
|
1500
|
+
|
|
1501
|
+
def get_vehicle_directory(self) -> str:
|
|
1502
|
+
return self._local_filesystem.vehicle_dir
|
|
1503
|
+
|
|
1504
|
+
def parameter_files(self) -> list[str]:
|
|
1505
|
+
return list(self._local_filesystem.file_parameters.keys())
|
|
1506
|
+
|
|
1507
|
+
def parameter_documentation_available(self) -> bool:
|
|
1508
|
+
return bool(self._local_filesystem.doc_dict)
|
|
1509
|
+
|
|
1510
|
+
def configuration_phases(self) -> dict[str, PhaseData]:
|
|
1511
|
+
return self._local_filesystem.configuration_phases
|
|
1512
|
+
|
|
1513
|
+
def _write_current_file(self) -> None:
|
|
1514
|
+
self._local_filesystem.write_last_uploaded_filename(self.current_file)
|
|
1515
|
+
|
|
1516
|
+
def _export_current_file(self, annotate_doc: bool) -> None:
|
|
1517
|
+
# Convert domain model parameters to Par objects for export
|
|
1518
|
+
export_params = self.get_parameters_as_par_dict()
|
|
1519
|
+
|
|
1520
|
+
# Export to file
|
|
1521
|
+
self._local_filesystem.export_to_param(export_params, self.current_file, annotate_doc)
|
|
1522
|
+
|
|
1523
|
+
# Update the filesystem's file_parameters to match what was saved
|
|
1524
|
+
self._local_filesystem.file_parameters[self.current_file] = export_params
|
|
1525
|
+
|
|
1526
|
+
self._added_parameters.clear()
|
|
1527
|
+
self._deleted_parameters.clear()
|
|
1528
|
+
# copy parameters new values to their _values_on_file
|
|
1529
|
+
for param in self.current_step_parameters.values():
|
|
1530
|
+
param.copy_new_value_to_file()
|
|
1531
|
+
|
|
1532
|
+
def open_documentation_in_browser(self, filename: str) -> None:
|
|
1533
|
+
_blog_text, blog_url = self.get_documentation_text_and_url("blog", filename)
|
|
1534
|
+
_wiki_text, wiki_url = self.get_documentation_text_and_url("wiki", filename)
|
|
1535
|
+
_external_tool_text, external_tool_url = self.get_documentation_text_and_url("external_tool", filename)
|
|
1536
|
+
if wiki_url:
|
|
1537
|
+
webbrowser_open(url=wiki_url, new=0, autoraise=False)
|
|
1538
|
+
if external_tool_url:
|
|
1539
|
+
webbrowser_open(url=external_tool_url, new=0, autoraise=False)
|
|
1540
|
+
if blog_url:
|
|
1541
|
+
webbrowser_open(url=blog_url, new=0, autoraise=True)
|
|
1542
|
+
|
|
1543
|
+
# frontend_tkinter_parameter_editor.py API end
|
|
1544
|
+
|
|
1545
|
+
# frontend_tkinter_parameter_editor_documentation_frame.py API start
|
|
1546
|
+
def get_documentation_text_and_url(self, key: str, filename: Optional[str] = None) -> tuple[str, str]:
|
|
1547
|
+
if filename is None:
|
|
1548
|
+
filename = self.current_file
|
|
1549
|
+
return self._local_filesystem.get_documentation_text_and_url(filename, key)
|
|
1550
|
+
|
|
1551
|
+
def get_why_why_now_tooltip(self) -> str:
|
|
1552
|
+
why_tooltip_text = self._local_filesystem.get_seq_tooltip_text(self.current_file, "why")
|
|
1553
|
+
why_now_tooltip_text = self._local_filesystem.get_seq_tooltip_text(self.current_file, "why_now")
|
|
1554
|
+
tooltip_text = ""
|
|
1555
|
+
if why_tooltip_text:
|
|
1556
|
+
tooltip_text += _("Why: ") + _(why_tooltip_text) + "\n"
|
|
1557
|
+
if why_now_tooltip_text:
|
|
1558
|
+
tooltip_text += _("Why now: ") + _(why_now_tooltip_text)
|
|
1559
|
+
return tooltip_text
|
|
1560
|
+
|
|
1561
|
+
def get_documentation_frame_title(self) -> str:
|
|
1562
|
+
if self.current_file:
|
|
1563
|
+
title = _("{current_file} Documentation")
|
|
1564
|
+
return title.format(current_file=self.current_file)
|
|
1565
|
+
return _("Documentation")
|
|
1566
|
+
|
|
1567
|
+
def parse_mandatory_level_percentage(self, text: str) -> tuple[int, str]:
|
|
1568
|
+
"""
|
|
1569
|
+
Parse and validate the mandatory level percentage from text.
|
|
1570
|
+
|
|
1571
|
+
Args:
|
|
1572
|
+
text: The text containing the mandatory level information
|
|
1573
|
+
|
|
1574
|
+
Returns:
|
|
1575
|
+
tuple: (percentage_value, tooltip_text)
|
|
1576
|
+
percentage_value: 0-100 for valid percentage, 0 for invalid
|
|
1577
|
+
tooltip_text: Formatted tooltip text
|
|
1578
|
+
|
|
1579
|
+
"""
|
|
1580
|
+
current_file = self.current_file or ""
|
|
1581
|
+
try:
|
|
1582
|
+
# Extract up to 3 digits from the start of the mandatory text
|
|
1583
|
+
percentage = int("".join([c for c in text[:3] if c.isdigit()]))
|
|
1584
|
+
if 0 <= percentage <= 100:
|
|
1585
|
+
tooltip = _("This configuration step ({current_file} intermediate parameter file) is {percentage}% mandatory")
|
|
1586
|
+
return percentage, tooltip.format(current_file=current_file, percentage=percentage)
|
|
1587
|
+
raise ValueError
|
|
1588
|
+
except ValueError:
|
|
1589
|
+
tooltip = _("Mandatory level not available for this configuration step ({current_file})")
|
|
1590
|
+
return 0, tooltip.format(current_file=current_file)
|
|
1591
|
+
|
|
1592
|
+
# frontend_tkinter_parameter_editor_documentation_frame.py API end
|