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
|
@@ -104,7 +104,7 @@ class ParDict(dict[str, Par]):
|
|
|
104
104
|
"""
|
|
105
105
|
parameter_dict = ParDict()
|
|
106
106
|
try:
|
|
107
|
-
with open(param_file, encoding="utf-8") as f_handle:
|
|
107
|
+
with open(param_file, encoding="utf-8-sig") as f_handle:
|
|
108
108
|
for i, f_line in enumerate(f_handle, start=1):
|
|
109
109
|
original_line = f_line
|
|
110
110
|
line = f_line.strip()
|
|
@@ -125,14 +125,18 @@ class ParDict(dict[str, Par]):
|
|
|
125
125
|
elif "\t" in line:
|
|
126
126
|
parameter, value = line.split("\t", 1)
|
|
127
127
|
else:
|
|
128
|
-
msg =
|
|
128
|
+
msg = _("Missing parameter-value separator: {line} in {param_file} line {i}").format(
|
|
129
|
+
line=line, param_file=param_file, i=i
|
|
130
|
+
)
|
|
129
131
|
raise SystemExit(msg)
|
|
130
132
|
# Strip whitespace from both parameter name and value immediately after splitting
|
|
131
133
|
parameter = parameter.strip()
|
|
132
134
|
value = value.strip()
|
|
133
135
|
ParDict._validate_parameter(param_file, parameter_dict, i, original_line, comment, parameter, value)
|
|
134
136
|
except UnicodeDecodeError as exp:
|
|
135
|
-
msg =
|
|
137
|
+
msg = _("Fatal error reading {param_file}, file must be UTF-8 encoded: {exp}").format(
|
|
138
|
+
param_file=param_file, exp=exp
|
|
139
|
+
)
|
|
136
140
|
raise SystemExit(msg) from exp
|
|
137
141
|
return parameter_dict
|
|
138
142
|
|
|
@@ -147,26 +151,34 @@ class ParDict(dict[str, Par]):
|
|
|
147
151
|
value: str,
|
|
148
152
|
) -> None:
|
|
149
153
|
if len(parameter_name) > PARAM_NAME_MAX_LEN:
|
|
150
|
-
msg =
|
|
154
|
+
msg = _("Too long parameter name: {parameter_name} in {param_file} line {i}").format(
|
|
155
|
+
parameter_name=parameter_name, param_file=param_file, i=i
|
|
156
|
+
)
|
|
151
157
|
raise SystemExit(msg)
|
|
152
158
|
if not re.match(PARAM_NAME_REGEX, parameter_name):
|
|
153
|
-
msg =
|
|
159
|
+
msg = _("Invalid characters in parameter name {parameter_name} in {param_file} line {i}").format(
|
|
160
|
+
parameter_name=parameter_name, param_file=param_file, i=i
|
|
161
|
+
)
|
|
154
162
|
raise SystemExit(msg)
|
|
155
163
|
if parameter_name in parameter_dict:
|
|
156
|
-
msg =
|
|
164
|
+
msg = _("Duplicated parameter {parameter_name} in {param_file} line {i}").format(
|
|
165
|
+
parameter_name=parameter_name, param_file=param_file, i=i
|
|
166
|
+
)
|
|
157
167
|
raise SystemExit(msg)
|
|
158
168
|
try:
|
|
159
169
|
fvalue = float(value)
|
|
160
170
|
parameter_dict[parameter_name] = Par(fvalue, comment)
|
|
161
171
|
except ValueError as exc:
|
|
162
|
-
msg =
|
|
172
|
+
msg = _("Invalid parameter value {value} in {param_file} line {i}").format(value=value, param_file=param_file, i=i)
|
|
163
173
|
raise SystemExit(msg) from exc
|
|
164
174
|
except OSError as exc:
|
|
165
175
|
_exc_type, exc_value, exc_traceback = sys_exc_info()
|
|
166
176
|
if isinstance(exc_traceback, TracebackType):
|
|
167
177
|
fname = os_path.split(exc_traceback.tb_frame.f_code.co_filename)[1]
|
|
168
178
|
logging.critical("in line %s of file %s: %s", exc_traceback.tb_lineno, fname, exc_value)
|
|
169
|
-
msg =
|
|
179
|
+
msg = _("Caused by line {i} of file {param_file}: {original_line}").format(
|
|
180
|
+
i=i, param_file=param_file, original_line=original_line
|
|
181
|
+
)
|
|
170
182
|
raise SystemExit(msg) from exc
|
|
171
183
|
|
|
172
184
|
@staticmethod
|
|
@@ -205,7 +217,7 @@ class ParDict(dict[str, Par]):
|
|
|
205
217
|
elif file_format == "mavproxy":
|
|
206
218
|
sorted_dict = ParDict(dict(sorted(self.items())))
|
|
207
219
|
else:
|
|
208
|
-
msg =
|
|
220
|
+
msg = _("ERROR: Unsupported file format {file_format}").format(file_format=file_format)
|
|
209
221
|
raise SystemExit(msg)
|
|
210
222
|
|
|
211
223
|
if file_format == "missionplanner":
|
|
@@ -277,10 +289,12 @@ class ParDict(dict[str, Par]):
|
|
|
277
289
|
rows = int(rows_str) - 2 # -2 for the next print and the input line
|
|
278
290
|
|
|
279
291
|
# Convert rows
|
|
280
|
-
print(
|
|
292
|
+
print( # noqa: T201
|
|
293
|
+
_("\n{name} has {len_formatted_params} parameters:").format(name=name, len_formatted_params=len(formatted_params))
|
|
294
|
+
)
|
|
281
295
|
for i, line in enumerate(formatted_params):
|
|
282
296
|
if i % rows == 0 and __name__ == "__main__":
|
|
283
|
-
input(
|
|
297
|
+
input(_("\n{name} list is long hit enter to continue").format(name=name))
|
|
284
298
|
rows_str, _columns = os_popen("stty size", "r").read().split() # noqa: S605, S607
|
|
285
299
|
rows = int(rows_str) - 2 # -2 for the next print and the input line
|
|
286
300
|
print(line) # noqa: T201
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"""
|
|
4
4
|
Check for software updates and install them if available.
|
|
5
5
|
|
|
6
|
-
This file is part of
|
|
6
|
+
This file is part of ArduPilot Methodic Configurator. https://github.com/ArduPilot/MethodicConfigurator
|
|
7
7
|
|
|
8
8
|
SPDX-FileCopyrightText: 2024-2025 Amilcar Lucas
|
|
9
9
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Vehicle template configuration overview information data model.
|
|
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
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Data model for vehicle components.
|
|
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
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Data model for vehicle components.
|
|
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
|
|
|
@@ -9,6 +9,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
# from logging import debug as logging_debug
|
|
12
|
+
from copy import deepcopy
|
|
12
13
|
from logging import error as logging_error
|
|
13
14
|
from logging import warning as logging_warning
|
|
14
15
|
from typing import Any, Optional, Union
|
|
@@ -34,7 +35,7 @@ class ComponentDataModelBase:
|
|
|
34
35
|
def __init__(
|
|
35
36
|
self, initial_data: ComponentData, component_datatypes: dict[str, Any], schema: VehicleComponentsJsonSchema
|
|
36
37
|
) -> None:
|
|
37
|
-
self._data: ComponentData = initial_data if initial_data else {"Components": {}, "Format version": 1}
|
|
38
|
+
self._data: ComponentData = deepcopy(initial_data) if initial_data else {"Components": {}, "Format version": 1}
|
|
38
39
|
self._battery_chemistry: str = ""
|
|
39
40
|
self._possible_choices: dict[ComponentPath, tuple[str, ...]] = {}
|
|
40
41
|
self._mot_pwm_types: tuple[str, ...] = ()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Data model for vehicle component display logic.
|
|
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
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Data model for vehicle components import from FC parameters.
|
|
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
|
|
|
@@ -199,6 +199,7 @@ class ComponentDataModelImport(ComponentDataModelBase):
|
|
|
199
199
|
|
|
200
200
|
if component == "RC Receiver" and rc == 1:
|
|
201
201
|
self.set_component_value(("RC Receiver", "FC Connection", "Type"), serial)
|
|
202
|
+
# Note: Protocol is set by RC_PROTOCOLS processing, not SERIAL_PROTOCOLS
|
|
202
203
|
rc += 1
|
|
203
204
|
elif component == "Telemetry" and telem == 1:
|
|
204
205
|
self.set_component_value(("Telemetry", "FC Connection", "Type"), serial)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Data model for vehicle components JSON schema.
|
|
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
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Data model interface for vehicle components templates.
|
|
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
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Data model for vehicle components.
|
|
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
|
|
|
@@ -36,6 +36,46 @@ RC_PORTS = ["RCin/SBUS"]
|
|
|
36
36
|
SPI_PORTS = ["SPI"]
|
|
37
37
|
OTHER_PORTS = ["other"]
|
|
38
38
|
|
|
39
|
+
# Bus labels for SERIAL ports - maps SERIAL port names to their common bus labels
|
|
40
|
+
# These labels help users identify ports by their typical usage on flight controllers:
|
|
41
|
+
# - Telem1/Telem2: Commonly used for telemetry connections
|
|
42
|
+
# - GPS1/GPS2: Commonly used for GNSS receiver connections
|
|
43
|
+
# - SERIAL5-8: No standard labels, use port name as label
|
|
44
|
+
SERIAL_BUS_LABELS: dict[str, str] = {
|
|
45
|
+
"SERIAL1": "Telem1 (SERIAL1)",
|
|
46
|
+
"SERIAL2": "Telem2 (SERIAL2)",
|
|
47
|
+
"SERIAL3": "GPS1 (SERIAL3)",
|
|
48
|
+
"SERIAL4": "GPS2 (SERIAL4)",
|
|
49
|
+
"SERIAL5": "SERIAL5",
|
|
50
|
+
"SERIAL6": "SERIAL6",
|
|
51
|
+
"SERIAL7": "SERIAL7",
|
|
52
|
+
"SERIAL8": "SERIAL8",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_connection_type_tuples_with_labels(connection_types: tuple[str, ...]) -> list[tuple[str, str]]:
|
|
57
|
+
"""
|
|
58
|
+
Convert connection type values to tuples with bus labels for display.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
connection_types: Tuple of connection type values (e.g., ("SERIAL1", "SERIAL2", ...))
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
List of tuples where first element is the value and second is the display string.
|
|
65
|
+
For SERIAL ports, returns (value, "Label (value)"), e.g., ("SERIAL3", "GPS1 (SERIAL3)")
|
|
66
|
+
For other ports, returns (value, value), e.g., ("CAN1", "CAN1")
|
|
67
|
+
|
|
68
|
+
"""
|
|
69
|
+
result = []
|
|
70
|
+
for conn_type in connection_types:
|
|
71
|
+
if conn_type in SERIAL_BUS_LABELS:
|
|
72
|
+
label = SERIAL_BUS_LABELS[conn_type]
|
|
73
|
+
result.append((conn_type, label))
|
|
74
|
+
else:
|
|
75
|
+
result.append((conn_type, conn_type))
|
|
76
|
+
return result
|
|
77
|
+
|
|
78
|
+
|
|
39
79
|
# Map paths to component names for unified protocol update
|
|
40
80
|
FC_CONNECTION_TYPE_PATHS: list[ComponentPath] = [
|
|
41
81
|
("RC Receiver", "FC Connection", "Type"),
|
|
@@ -5,7 +5,7 @@ This file contains the unified interface for all vehicle project operations,
|
|
|
5
5
|
acting as a facade that coordinates between different data models and provides
|
|
6
6
|
a single point of contact for the frontend layer.
|
|
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
|
|
|
@@ -4,7 +4,7 @@ Data model for vehicle project creation.
|
|
|
4
4
|
This file contains the business logic for creating new vehicle configurations
|
|
5
5
|
from templates, separated from the GUI layer.
|
|
6
6
|
|
|
7
|
-
This file is part of
|
|
7
|
+
This file is part of ArduPilot Methodic Configurator. https://github.com/ArduPilot/MethodicConfigurator
|
|
8
8
|
|
|
9
9
|
SPDX-FileCopyrightText: 2024-2025 Amilcar do Carmo Lucas <amilcar.lucas@iav.de>
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ Data model for vehicle project opening.
|
|
|
4
4
|
This file contains the business logic for opening existing vehicle configurations,
|
|
5
5
|
separated from the GUI layer.
|
|
6
6
|
|
|
7
|
-
This file is part of
|
|
7
|
+
This file is part of ArduPilot Methodic Configurator. https://github.com/ArduPilot/MethodicConfigurator
|
|
8
8
|
|
|
9
9
|
SPDX-FileCopyrightText: 2024-2025 Amilcar do Carmo Lucas <amilcar.lucas@iav.de>
|
|
10
10
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
TKinter base classes reused in multiple parts of the code.
|
|
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
|
|
|
@@ -11,7 +11,7 @@ Key Features:
|
|
|
11
11
|
- Graceful icon loading with fallback for test environments
|
|
12
12
|
- Utility methods for window positioning and image handling
|
|
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
|
|
|
@@ -101,7 +101,7 @@ class BaseWindow:
|
|
|
101
101
|
if root_tk:
|
|
102
102
|
self.root = tk.Toplevel(root_tk)
|
|
103
103
|
else:
|
|
104
|
-
self.root = tk.Tk()
|
|
104
|
+
self.root = tk.Tk(className="ArduPilotMethodicConfigurator")
|
|
105
105
|
# Only set icon for main windows, and only outside test environments
|
|
106
106
|
self._setup_application_icon()
|
|
107
107
|
|
|
@@ -421,3 +421,7 @@ def show_error_popup(title: str, message: str) -> None:
|
|
|
421
421
|
|
|
422
422
|
def ask_yesno_popup(title: str, message: str) -> bool:
|
|
423
423
|
return messagebox.askyesno(title, message)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def ask_retry_cancel_popup(title: str, message: str) -> bool:
|
|
427
|
+
return messagebox.askretrycancel(title, message)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"""
|
|
4
4
|
Data-dependent part of the component editor GUI.
|
|
5
5
|
|
|
6
|
-
This file is part of
|
|
6
|
+
This file is part of ArduPilot Methodic Configurator. https://github.com/ArduPilot/MethodicConfigurator
|
|
7
7
|
|
|
8
8
|
SPDX-FileCopyrightText: 2024-2025 Amilcar do Carmo Lucas <amilcar.lucas@iav.de>
|
|
9
9
|
|
|
@@ -27,9 +27,10 @@ from ardupilot_methodic_configurator.data_model_vehicle_components_base import C
|
|
|
27
27
|
from ardupilot_methodic_configurator.data_model_vehicle_components_validation import (
|
|
28
28
|
BATTERY_CELL_VOLTAGE_PATHS,
|
|
29
29
|
FC_CONNECTION_TYPE_PATHS,
|
|
30
|
+
get_connection_type_tuples_with_labels,
|
|
30
31
|
)
|
|
31
32
|
from ardupilot_methodic_configurator.frontend_tkinter_component_editor_base import ComponentEditorWindowBase
|
|
32
|
-
from ardupilot_methodic_configurator.frontend_tkinter_pair_tuple_combobox import
|
|
33
|
+
from ardupilot_methodic_configurator.frontend_tkinter_pair_tuple_combobox import PairTupleCombobox
|
|
33
34
|
|
|
34
35
|
# from ardupilot_methodic_configurator.frontend_tkinter_show import show_tooltip
|
|
35
36
|
from ardupilot_methodic_configurator.frontend_tkinter_show import show_error_message, show_warning_message
|
|
@@ -109,20 +110,30 @@ class ComponentEditorWindow(ComponentEditorWindowBase):
|
|
|
109
110
|
err_msg = ""
|
|
110
111
|
if protocol_path in self.entry_widgets:
|
|
111
112
|
protocol_combobox = self.entry_widgets[protocol_path]
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
113
|
+
# Only update if this is actually a PairTupleCombobox (protocol comboboxes should be)
|
|
114
|
+
if isinstance(protocol_combobox, PairTupleCombobox):
|
|
115
|
+
# Rebuild the (key, display) pairs for PairTupleCombobox
|
|
116
|
+
protocol_tuples = [(p, p) for p in protocols]
|
|
117
|
+
# Get current selection and validate it against new protocols
|
|
118
|
+
current_selection = protocol_combobox.get_selected_key()
|
|
119
|
+
if current_selection and current_selection not in protocols:
|
|
120
|
+
# Current selection is not valid for new protocols, clear it
|
|
121
|
+
invalid_selection = current_selection
|
|
122
|
+
current_selection = None
|
|
123
|
+
component: str = " > ".join(protocol_path)
|
|
124
|
+
err_msg = _(
|
|
125
|
+
"On {component} the selected\nprotocol '{invalid_selection}' "
|
|
126
|
+
"is not available for the selected connection Type."
|
|
127
|
+
)
|
|
128
|
+
err_msg = err_msg.format(component=component, invalid_selection=invalid_selection)
|
|
129
|
+
|
|
130
|
+
# Update the combobox entries using set_entries_tuple with validated selection
|
|
131
|
+
protocol_combobox.set_entries_tuple(protocol_tuples, current_selection)
|
|
132
|
+
|
|
133
|
+
if err_msg:
|
|
134
|
+
show_error_message(_("Error"), err_msg)
|
|
135
|
+
protocol_combobox.configure(style="comb_input_invalid.TCombobox")
|
|
136
|
+
protocol_combobox.update_idletasks() # re-draw the combobox ASAP
|
|
126
137
|
return err_msg
|
|
127
138
|
|
|
128
139
|
def update_cell_voltage_limits_entries(self, component_path: ComponentPath, chemistry: str) -> str:
|
|
@@ -163,7 +174,7 @@ class ComponentEditorWindow(ComponentEditorWindowBase):
|
|
|
163
174
|
|
|
164
175
|
def add_entry_or_combobox(
|
|
165
176
|
self, value: Union[str, float], entry_frame: ttk.Frame, path: ComponentPath, is_optional: bool = False
|
|
166
|
-
) -> Union[ttk.Entry,
|
|
177
|
+
) -> Union[ttk.Entry, PairTupleCombobox]:
|
|
167
178
|
# Get combobox values from data model
|
|
168
179
|
combobox_values = self.data_model.get_combobox_values_for_path(path)
|
|
169
180
|
|
|
@@ -174,15 +185,27 @@ class ComponentEditorWindow(ComponentEditorWindowBase):
|
|
|
174
185
|
return self._validate_combobox(event, path)
|
|
175
186
|
|
|
176
187
|
if combobox_values:
|
|
177
|
-
|
|
188
|
+
# Convert all combobox values to tuples for PairTupleCombobox
|
|
189
|
+
# For FC Connection Type paths, add bus labels; for others, use value as both key and display
|
|
190
|
+
if path in FC_CONNECTION_TYPE_PATHS:
|
|
191
|
+
combobox_tuples = get_connection_type_tuples_with_labels(combobox_values)
|
|
192
|
+
else:
|
|
193
|
+
combobox_tuples = [(val, val) for val in combobox_values]
|
|
194
|
+
|
|
195
|
+
# Always use PairTupleCombobox for consistency and simplicity
|
|
196
|
+
cb = PairTupleCombobox(
|
|
197
|
+
entry_frame,
|
|
198
|
+
combobox_tuples,
|
|
199
|
+
str(value) if value else None,
|
|
200
|
+
f"{' > '.join(path)}",
|
|
201
|
+
)
|
|
202
|
+
cb.config(foreground=fg_color)
|
|
203
|
+
|
|
178
204
|
cb.bind("<FocusOut>", on_validate_combobox)
|
|
179
205
|
cb.bind("<KeyRelease>", on_validate_combobox)
|
|
180
206
|
cb.bind("<Return>", on_validate_combobox)
|
|
181
207
|
cb.bind("<ButtonRelease>", on_validate_combobox)
|
|
182
208
|
|
|
183
|
-
# Set up mouse wheel handling to prevent unwanted value changes
|
|
184
|
-
setup_combobox_mousewheel_handling(cb)
|
|
185
|
-
|
|
186
209
|
# Override the FocusOut binding to also handle validation
|
|
187
210
|
def combined_focus_out(event: tk.Event) -> None:
|
|
188
211
|
# First handle the dropdown closing logic (already done by setup function)
|
|
@@ -192,9 +215,10 @@ class ComponentEditorWindow(ComponentEditorWindowBase):
|
|
|
192
215
|
cb.bind("<FocusOut>", combined_focus_out, add="+")
|
|
193
216
|
|
|
194
217
|
if path in FC_CONNECTION_TYPE_PATHS:
|
|
195
|
-
|
|
218
|
+
# immediate update of Protocol combobox choices after changing connection Type selection
|
|
219
|
+
cb.bind(
|
|
196
220
|
"<<ComboboxSelected>>",
|
|
197
|
-
lambda
|
|
221
|
+
lambda _event: self.update_component_protocol_combobox_entries(path, cb.get_selected_key() or ""),
|
|
198
222
|
)
|
|
199
223
|
|
|
200
224
|
# When battery chemistry changes, the max, low and crit voltages will change to the
|
|
@@ -202,10 +226,9 @@ class ComponentEditorWindow(ComponentEditorWindowBase):
|
|
|
202
226
|
if path == ("Battery", "Specifications", "Chemistry"):
|
|
203
227
|
cb.bind(
|
|
204
228
|
"<<ComboboxSelected>>",
|
|
205
|
-
lambda
|
|
229
|
+
lambda _event: self.update_cell_voltage_limits_entries(path, cb.get_selected_key() or ""),
|
|
206
230
|
)
|
|
207
231
|
|
|
208
|
-
cb.set(value)
|
|
209
232
|
return cb
|
|
210
233
|
|
|
211
234
|
entry = ttk.Entry(entry_frame, foreground=fg_color)
|
|
@@ -220,12 +243,16 @@ class ComponentEditorWindow(ComponentEditorWindowBase):
|
|
|
220
243
|
return entry
|
|
221
244
|
|
|
222
245
|
def _validate_combobox(self, event: tk.Event, path: ComponentPath) -> bool:
|
|
223
|
-
"""Validates the value of a
|
|
246
|
+
"""Validates the value of a PairTupleCombobox."""
|
|
224
247
|
combobox = event.widget # Get the combobox widget that triggered the event
|
|
225
|
-
if not isinstance(combobox,
|
|
248
|
+
if not isinstance(combobox, PairTupleCombobox):
|
|
226
249
|
return False
|
|
227
|
-
|
|
228
|
-
|
|
250
|
+
|
|
251
|
+
# Get the selected key (internal value)
|
|
252
|
+
value = combobox.get_selected_key() or ""
|
|
253
|
+
|
|
254
|
+
# Get allowed values from list_keys
|
|
255
|
+
allowed_values = combobox.list_keys
|
|
229
256
|
|
|
230
257
|
# Events that should trigger data model update (when value is valid)
|
|
231
258
|
should_update_data_model = event.type in {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"""
|
|
4
4
|
Component editor GUI that is not data dependent.
|
|
5
5
|
|
|
6
|
-
This file is part of
|
|
6
|
+
This file is part of ArduPilot Methodic Configurator. https://github.com/ArduPilot/MethodicConfigurator
|
|
7
7
|
|
|
8
8
|
SPDX-FileCopyrightText: 2024-2025 Amilcar do Carmo Lucas <amilcar.lucas@iav.de>
|
|
9
9
|
|
|
@@ -33,6 +33,7 @@ from ardupilot_methodic_configurator.data_model_vehicle_components_json_schema i
|
|
|
33
33
|
from ardupilot_methodic_configurator.frontend_tkinter_base_window import BaseWindow
|
|
34
34
|
from ardupilot_methodic_configurator.frontend_tkinter_component_template_manager import ComponentTemplateManager
|
|
35
35
|
from ardupilot_methodic_configurator.frontend_tkinter_font import create_scaled_font, get_safe_font_config
|
|
36
|
+
from ardupilot_methodic_configurator.frontend_tkinter_pair_tuple_combobox import PairTupleCombobox
|
|
36
37
|
from ardupilot_methodic_configurator.frontend_tkinter_rich_text import RichText
|
|
37
38
|
from ardupilot_methodic_configurator.frontend_tkinter_scroll_frame import ScrollFrame
|
|
38
39
|
from ardupilot_methodic_configurator.frontend_tkinter_show import show_error_message, show_tooltip
|
|
@@ -63,7 +64,7 @@ def argument_parser() -> Namespace:
|
|
|
63
64
|
|
|
64
65
|
|
|
65
66
|
# Type aliases to improve code readability
|
|
66
|
-
EntryWidget = Union[ttk.Entry,
|
|
67
|
+
EntryWidget = Union[ttk.Entry, PairTupleCombobox]
|
|
67
68
|
|
|
68
69
|
WINDOW_WIDTH_PIX = 880
|
|
69
70
|
VEHICLE_IMAGE_WIDTH_PIX = 100
|
|
@@ -293,6 +294,10 @@ class ComponentEditorWindowBase(BaseWindow): # pylint: disable=too-many-instanc
|
|
|
293
294
|
|
|
294
295
|
def _display_component_editor_usage_instructions(self, parent: tk.Tk) -> None:
|
|
295
296
|
"""Display usage instructions for the component editor."""
|
|
297
|
+
# Check if parent window still exists before showing popup
|
|
298
|
+
if not parent.winfo_exists():
|
|
299
|
+
return
|
|
300
|
+
|
|
296
301
|
# pylint: disable=duplicate-code
|
|
297
302
|
usage_popup_window = BaseWindow(parent)
|
|
298
303
|
style = ttk.Style()
|
|
@@ -482,8 +487,11 @@ class ComponentEditorWindowBase(BaseWindow): # pylint: disable=too-many-instanc
|
|
|
482
487
|
def validate_data_and_highlight_errors_in_red(self) -> str:
|
|
483
488
|
"""Validate all data using the data model."""
|
|
484
489
|
# Collect all entry values
|
|
490
|
+
# For PairTupleCombobox, get the selected key; for ttk.Entry, get the text
|
|
485
491
|
entry_values = {
|
|
486
|
-
path: entry.
|
|
492
|
+
path: entry.get_selected_key() or "" if isinstance(entry, PairTupleCombobox) else entry.get()
|
|
493
|
+
for path, entry in self.entry_widgets.items()
|
|
494
|
+
if isinstance(entry, (PairTupleCombobox, ttk.Entry))
|
|
487
495
|
}
|
|
488
496
|
|
|
489
497
|
# Use data model for validation
|
|
@@ -491,19 +499,16 @@ class ComponentEditorWindowBase(BaseWindow): # pylint: disable=too-many-instanc
|
|
|
491
499
|
|
|
492
500
|
if not is_valid:
|
|
493
501
|
# Update UI to show invalid states and display errors
|
|
494
|
-
for path, entry in (
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
# Check combobox validation
|
|
500
|
-
if isinstance(entry, ttk.Combobox):
|
|
501
|
-
combobox_values = self.data_model.get_combobox_values_for_path(path)
|
|
502
|
-
if combobox_values and value not in combobox_values:
|
|
502
|
+
for path, entry in self.entry_widgets.items():
|
|
503
|
+
if isinstance(entry, PairTupleCombobox):
|
|
504
|
+
value = entry.get_selected_key() or ""
|
|
505
|
+
# Check combobox validation
|
|
506
|
+
if value not in entry.list_keys:
|
|
503
507
|
entry.configure(style="comb_input_invalid.TCombobox")
|
|
504
508
|
else:
|
|
505
509
|
entry.configure(style="comb_input_valid.TCombobox")
|
|
506
|
-
|
|
510
|
+
elif isinstance(entry, ttk.Entry):
|
|
511
|
+
value = entry.get()
|
|
507
512
|
# Check entry validation
|
|
508
513
|
error_message, _corr = self.data_model.validate_entry_limits(value, path)
|
|
509
514
|
if error_message:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Component template manager for the ArduPilot methodic configurator.
|
|
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>, 2025 Francisco Fonseca
|
|
7
7
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"""
|
|
4
4
|
GUI to select the connection to the FC.
|
|
5
5
|
|
|
6
|
-
This file is part of
|
|
6
|
+
This file is part of ArduPilot Methodic Configurator. https://github.com/ArduPilot/MethodicConfigurator
|
|
7
7
|
|
|
8
8
|
SPDX-FileCopyrightText: 2024-2025 Amilcar do Carmo Lucas <amilcar.lucas@iav.de>
|
|
9
9
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
GUI to select the directory to store the vehicle configuration files.
|
|
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
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"""
|
|
4
4
|
GUI entry Widget with autocompletion features.
|
|
5
5
|
|
|
6
|
-
This file is part of
|
|
6
|
+
This file is part of ArduPilot Methodic Configurator. https://github.com/ArduPilot/MethodicConfigurator
|
|
7
7
|
|
|
8
8
|
https://code.activestate.com/recipes/580770-combobox-autocomplete/
|
|
9
9
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Flight controller information GUI.
|
|
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,7 +10,7 @@ The module offers functions to safely retrieve font configurations, families,
|
|
|
10
10
|
and sizes with appropriate platform-specific fallbacks. This ensures consistent
|
|
11
11
|
text rendering across Windows, macOS, and Linux systems.
|
|
12
12
|
|
|
13
|
-
This file is part of
|
|
13
|
+
This file is part of ArduPilot Methodic Configurator. https://github.com/ArduPilot/MethodicConfigurator
|
|
14
14
|
|
|
15
15
|
SPDX-FileCopyrightText: 2024-2025 Amilcar do Carmo Lucas <amilcar.lucas@iav.de>
|
|
16
16
|
|
|
@@ -13,7 +13,7 @@ The MotorTestView class provides:
|
|
|
13
13
|
|
|
14
14
|
MotorTestWindow class provides a standalone window for development/testing.
|
|
15
15
|
|
|
16
|
-
This file is part of
|
|
16
|
+
This file is part of ArduPilot Methodic Configurator. https://github.com/ArduPilot/MethodicConfigurator
|
|
17
17
|
|
|
18
18
|
SPDX-FileCopyrightText: 2024-2025 Amilcar do Carmo Lucas <amilcar.lucas@iav.de>
|
|
19
19
|
|