Skip to content

Commit 196039b

Browse files
committed
WIP
1 parent 89d9e27 commit 196039b

9 files changed

Lines changed: 1591 additions & 2 deletions
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
"""
2+
ArduPilot parameter domain model.
3+
4+
This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator
5+
6+
SPDX-FileCopyrightText: 2024-2025 Amilcar do Carmo Lucas <amilcar.lucas@iav.de>
7+
8+
SPDX-License-Identifier: GPL-3.0-or-later
9+
"""
10+
11+
from typing import Any, Optional
12+
13+
from ardupilot_methodic_configurator import _
14+
from ardupilot_methodic_configurator.annotate_params import Par
15+
from ardupilot_methodic_configurator.backend_filesystem import is_within_tolerance
16+
17+
18+
class ArduPilotParameter:
19+
"""Domain model representing an ArduPilot parameter with all its attributes."""
20+
21+
def __init__(
22+
self,
23+
name: str,
24+
par_obj: Par,
25+
metadata: Optional[dict[str, Any]] = None,
26+
default_par: Optional[Par] = None,
27+
fc_value: Optional[float] = None,
28+
is_forced: bool = False,
29+
is_derived: bool = False,
30+
) -> None:
31+
"""
32+
Initialize the parameter with all its attributes.
33+
34+
Args:
35+
name: Name of the parameter
36+
par_obj: Par object containing value and comment
37+
metadata: Dictionary of parameter metadata (from pdef.xml files)
38+
default_par: Default parameter object for comparison
39+
fc_value: Value from the flight controller, if connected
40+
is_forced: Whether this parameter is forced (cannot be edited)
41+
is_derived: Whether this parameter is derived (calculated automatically)
42+
43+
"""
44+
self.name = name
45+
self.value = par_obj.value
46+
self.comment = par_obj.comment
47+
self.metadata = metadata or {}
48+
self.default_value = default_par.value if default_par else None
49+
self.fc_value = fc_value
50+
self.is_forced = is_forced
51+
self.is_derived = is_derived
52+
53+
@property
54+
def is_calibration(self) -> bool:
55+
"""Return True if this is a calibration parameter."""
56+
return self.metadata.get("Calibration", False)
57+
58+
@property
59+
def is_readonly(self) -> bool:
60+
"""Return True if this is a readonly parameter."""
61+
return self.metadata.get("ReadOnly", False)
62+
63+
@property
64+
def is_bitmask(self) -> bool:
65+
"""Return True if this parameter uses a bitmask representation."""
66+
return "Bitmask" in self.metadata
67+
68+
@property
69+
def has_default_value(self) -> bool:
70+
"""Return True if the current value equals the default value."""
71+
return self.default_value is not None and is_within_tolerance(self.value, self.default_value)
72+
73+
@property
74+
def has_fc_value(self) -> bool:
75+
"""Return True if there is a flight controller value for this parameter."""
76+
return self.fc_value is not None
77+
78+
@property
79+
def is_different_from_fc(self) -> bool:
80+
"""Return True if the parameter value is different from the flight controller value."""
81+
return self.fc_value is not None and not is_within_tolerance(self.value, self.fc_value)
82+
83+
@property
84+
def doc_tooltip(self) -> str:
85+
"""Return the documentation tooltip for this parameter."""
86+
return self.metadata.get("doc_tooltip", _("No documentation available in apm.pdef.xml for this parameter"))
87+
88+
@property
89+
def unit(self) -> str:
90+
"""Return the unit of this parameter."""
91+
return self.metadata.get("unit", "")
92+
93+
@property
94+
def unit_tooltip(self) -> str:
95+
"""Return the unit tooltip for this parameter."""
96+
return self.metadata.get("unit_tooltip", _("No documentation available in apm.pdef.xml for this parameter"))
97+
98+
@property
99+
def values_dict(self) -> dict:
100+
"""Return the values dictionary for this parameter."""
101+
return self.metadata.get("values", {})
102+
103+
@property
104+
def bitmask_dict(self) -> dict:
105+
"""Return the bitmask dictionary for this parameter."""
106+
return self.metadata.get("Bitmask", {})
107+
108+
@property
109+
def min_value(self) -> Optional[float]:
110+
"""Return the minimum allowed value for this parameter."""
111+
min_val = self.metadata.get("min", None)
112+
return float(min_val) if min_val is not None else None
113+
114+
@property
115+
def max_value(self) -> Optional[float]:
116+
"""Return the maximum allowed value for this parameter."""
117+
max_val = self.metadata.get("max", None)
118+
return float(max_val) if max_val is not None else None
119+
120+
@property
121+
def value_as_string(self) -> str:
122+
"""Return the parameter value as a formatted string."""
123+
return format(self.value, ".6f").rstrip("0").rstrip(".")
124+
125+
@property
126+
def fc_value_as_string(self) -> str:
127+
"""Return the flight controller value as a formatted string."""
128+
if self.fc_value is None:
129+
return _("N/A")
130+
return format(self.fc_value, ".6f").rstrip("0").rstrip(".")
131+
132+
def is_in_values_dict(self) -> bool:
133+
"""Return True if the current value is in the values dictionary."""
134+
return bool(self.values_dict and self.value_as_string in self.values_dict)
135+
136+
def get_selected_value_from_dict(self) -> Optional[str]:
137+
"""Return the string representation from the values dictionary for the current value."""
138+
if self.is_in_values_dict():
139+
return self.values_dict.get(self.value_as_string)
140+
return None
141+
142+
def set_value(self, value: float) -> bool:
143+
"""
144+
Set the parameter value and return whether it changed.
145+
146+
Args:
147+
value: The new value to set
148+
149+
Returns:
150+
True if the value was changed, False otherwise
151+
152+
"""
153+
if self.is_forced or self.is_derived:
154+
return False
155+
156+
if is_within_tolerance(self.value, value):
157+
return False
158+
159+
self.value = value
160+
return True
161+
162+
def set_comment(self, comment: str) -> bool:
163+
"""
164+
Set the parameter comment and return whether it changed.
165+
166+
Args:
167+
comment: The new comment to set
168+
169+
Returns:
170+
True if the comment was changed, False otherwise
171+
172+
"""
173+
if self.is_forced or self.is_derived:
174+
return False
175+
176+
if comment == self.comment or (comment == "" and self.comment is None):
177+
return False
178+
179+
self.comment = comment
180+
return True
181+
182+
def is_editable(self) -> bool:
183+
"""
184+
Check if this parameter is editable.
185+
186+
Returns:
187+
True if the parameter can be edited, False otherwise
188+
189+
"""
190+
return not (self.is_forced or self.is_derived or self.is_readonly)
191+
192+
def is_valid_value(self, value: float) -> bool:
193+
"""
194+
Check if a value is valid for this parameter.
195+
196+
Args:
197+
value: The value to check
198+
199+
Returns:
200+
True if the value is valid, False otherwise
201+
202+
"""
203+
if self.min_value is not None and value < self.min_value:
204+
return False
205+
206+
return not (self.max_value is not None and value > self.max_value)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""
2+
ArduPilot parameter bitmask utilities.
3+
4+
This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator
5+
6+
SPDX-FileCopyrightText: 2024-2025 Amilcar do Carmo Lucas <amilcar.lucas@iav.de>
7+
8+
SPDX-License-Identifier: GPL-3.0-or-later
9+
"""
10+
11+
12+
class BitmaskHelper:
13+
"""Helper class for working with ArduPilot parameter bitmasks."""
14+
15+
@staticmethod
16+
def get_checked_keys(value: int, bitmask_dict: dict[int, str]) -> set[int]:
17+
"""
18+
Convert a decimal value to a set of checked bit keys.
19+
20+
Args:
21+
value: The decimal value to convert
22+
bitmask_dict: Dictionary mapping bit positions to descriptions
23+
24+
Returns:
25+
Set of keys (bit positions) that are set in the value
26+
27+
"""
28+
return {key for key in bitmask_dict if (value >> key) & 1}
29+
30+
@staticmethod
31+
def get_value_from_keys(checked_keys: list[int]) -> int:
32+
"""
33+
Convert a list of checked bit keys to a decimal value.
34+
35+
Args:
36+
checked_keys: List of bit positions that are set
37+
38+
Returns:
39+
The decimal value representing the bits set
40+
41+
"""
42+
return sum(1 << key for key in checked_keys)
43+
44+
@staticmethod
45+
def get_description(value: int, bitmask_dict: dict[int, str]) -> str:
46+
"""
47+
Get a human-readable description of which bits are set.
48+
49+
Args:
50+
value: The decimal value
51+
bitmask_dict: Dictionary mapping bit positions to descriptions
52+
53+
Returns:
54+
String listing the descriptions of set bits, comma-separated
55+
56+
"""
57+
checked_keys = BitmaskHelper.get_checked_keys(value, bitmask_dict)
58+
if not checked_keys:
59+
return "None"
60+
61+
return ", ".join(bitmask_dict[key] for key in sorted(checked_keys))
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"""
2+
ArduPilot parameter connection renaming utility.
3+
4+
This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator
5+
6+
SPDX-FileCopyrightText: 2024-2025 Amilcar do Carmo Lucas <amilcar.lucas@iav.de>
7+
8+
SPDX-License-Identifier: GPL-3.0-or-later
9+
"""
10+
11+
from typing import Any, Optional
12+
13+
14+
class ConnectionRenamer:
15+
"""
16+
Utility class to handle renaming parameters based on connection prefixes.
17+
18+
This captures the logic for renaming parameters when connection types change,
19+
particularly handling special cases like CAN bus connections.
20+
"""
21+
22+
@staticmethod
23+
def generate_renames(parameters: list[str], new_connection_prefix: str) -> dict[str, str]:
24+
"""
25+
Generate a dictionary of parameter renames based on a new connection prefix.
26+
27+
Args:
28+
parameters: List of parameter names to potentially rename
29+
new_connection_prefix: The new prefix to apply (like "CAN2")
30+
31+
Returns:
32+
Dictionary mapping old parameter names to new parameter names
33+
34+
"""
35+
renames = {}
36+
37+
# Extract the type and number from the new connection prefix
38+
if len(new_connection_prefix) < 2:
39+
return renames
40+
41+
new_type = new_connection_prefix[:-1] # e.g., "CAN" from "CAN2"
42+
new_number = new_connection_prefix[-1] # e.g., "2" from "CAN2"
43+
44+
for param_name in parameters:
45+
if new_type == "CAN" and param_name.startswith("CAN_"):
46+
# Handle CAN_P1_* or CAN_D1_* pattern for CAN parameters
47+
if "_P1_" in param_name:
48+
new_param_name = param_name.replace("_P1_", f"_P{new_number}_")
49+
renames[param_name] = new_param_name
50+
elif "_D1_" in param_name:
51+
new_param_name = param_name.replace("_D1_", f"_D{new_number}_")
52+
renames[param_name] = new_param_name
53+
elif param_name.startswith(f"{new_type}1_"):
54+
# Handle standard parameters like SERIAL1_BAUD
55+
new_param_name = param_name.replace(f"{new_type}1_", f"{new_type}{new_number}_")
56+
renames[param_name] = new_param_name
57+
58+
return renames
59+
60+
@staticmethod
61+
def apply_renames(
62+
parameters: dict[str, Any], new_connection_prefix: str, variables: Optional[dict[str, Any]] = None
63+
) -> tuple[dict[str, Any], set[str], list[tuple[str, str]]]:
64+
"""
65+
Apply connection prefix renames to a parameter dictionary.
66+
67+
Args:
68+
parameters: Dictionary of parameter objects to rename
69+
new_connection_prefix: The new prefix to apply
70+
variables: Optional dictionary of variables for evaluation
71+
72+
Returns:
73+
Tuple containing:
74+
- Updated parameters dictionary
75+
- Set of new parameter names
76+
- List of (old_name, new_name) pairs that were renamed
77+
78+
"""
79+
if variables:
80+
# If variables provided, evaluate the new_connection_prefix
81+
new_connection_prefix = eval(str(new_connection_prefix), {}, variables) # noqa: S307 pylint: disable=eval-used
82+
83+
# Generate the rename mapping
84+
renames = ConnectionRenamer.generate_renames(list(parameters.keys()), new_connection_prefix)
85+
86+
# Track unique new names and actual renames performed
87+
new_names = set()
88+
renamed_pairs = []
89+
90+
# Create a new dictionary to avoid modifying during iteration
91+
updated_parameters = parameters.copy()
92+
93+
# First identify and process duplicates
94+
duplicates = []
95+
for old_name, new_name in renames.items():
96+
if new_name in parameters and old_name != new_name:
97+
duplicates.append(old_name)
98+
99+
# Remove duplicates
100+
for old_name in duplicates:
101+
if old_name in updated_parameters:
102+
updated_parameters.pop(old_name)
103+
104+
# Process remaining renames
105+
for old_name, new_name in renames.items():
106+
if old_name in updated_parameters and old_name not in duplicates:
107+
renamed_pairs.append((old_name, new_name))
108+
updated_parameters[new_name] = updated_parameters.pop(old_name)
109+
new_names.add(new_name)
110+
111+
return updated_parameters, new_names, renamed_pairs

ardupilot_methodic_configurator/frontend_tkinter_parameter_editor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
from ardupilot_methodic_configurator.frontend_tkinter_base_window import BaseWindow
3939
from ardupilot_methodic_configurator.frontend_tkinter_directory_selection import VehicleDirectorySelectionWidgets
4040
from ardupilot_methodic_configurator.frontend_tkinter_parameter_editor_documentation_frame import DocumentationFrame
41-
from ardupilot_methodic_configurator.frontend_tkinter_parameter_editor_table import ParameterEditorTable
41+
from ardupilot_methodic_configurator.frontend_tkinter_parameter_editor_table_refactored import ParameterEditorTable
4242
from ardupilot_methodic_configurator.frontend_tkinter_progress_window import ProgressWindow
4343
from ardupilot_methodic_configurator.frontend_tkinter_rich_text import RichText, get_widget_font_family_and_size
4444
from ardupilot_methodic_configurator.frontend_tkinter_show import show_tooltip

ardupilot_methodic_configurator/frontend_tkinter_parameter_editor_table.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
NEW_VALUE_WIDGET_WIDTH = 9
3838

3939

40-
class ParameterEditorTable(ScrollFrame): # pylint: disable=too-many-ancestors
40+
class ParameterEditorTable2(ScrollFrame): # pylint: disable=too-many-ancestors
4141
"""
4242
A class to manage and display the parameter editor table within the GUI.
4343

0 commit comments

Comments
 (0)