|
| 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 str(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 if it exists.""" |
| 101 | + return self.metadata.get("values", {}) or {} |
| 102 | + |
| 103 | + @property |
| 104 | + def bitmask_dict(self) -> dict: |
| 105 | + """Return the bitmask dictionary for this parameter if it exists.""" |
| 106 | + return self.metadata.get("Bitmask", {}) or {} |
| 107 | + |
| 108 | + @property |
| 109 | + def min_value(self) -> Optional[float]: |
| 110 | + """Return the minimum allowed value for this parameter.""" |
| 111 | + return self.metadata.get("min", None) |
| 112 | + |
| 113 | + @property |
| 114 | + def max_value(self) -> Optional[float]: |
| 115 | + """Return the maximum allowed value for this parameter.""" |
| 116 | + return self.metadata.get("max", None) |
| 117 | + |
| 118 | + @property |
| 119 | + def value_as_string(self) -> str: |
| 120 | + """Return the parameter value formatted as a string.""" |
| 121 | + return format(self.value, ".6f").rstrip("0").rstrip(".") |
| 122 | + |
| 123 | + @property |
| 124 | + def fc_value_as_string(self) -> str: |
| 125 | + """Return the flight controller value formatted as a string.""" |
| 126 | + if self.fc_value is not None: |
| 127 | + return format(self.fc_value, ".6f").rstrip("0").rstrip(".") |
| 128 | + return _("N/A") |
| 129 | + |
| 130 | + def is_in_values_dict(self) -> bool: |
| 131 | + """Return True if the current value is in the values dictionary.""" |
| 132 | + return bool(self.values_dict and self.value_as_string in self.values_dict) |
| 133 | + |
| 134 | + def get_selected_value_from_dict(self) -> Optional[str]: |
| 135 | + """Return the string representation from the values dictionary for the current value.""" |
| 136 | + if self.is_in_values_dict(): |
| 137 | + return self.values_dict.get(self.value_as_string, None) |
| 138 | + return None |
| 139 | + |
| 140 | + def set_value(self, value: float) -> bool: |
| 141 | + """ |
| 142 | + Set the parameter value and return whether it changed. |
| 143 | +
|
| 144 | + Args: |
| 145 | + value: The new value to set |
| 146 | +
|
| 147 | + Returns: |
| 148 | + True if the value was changed, False otherwise |
| 149 | +
|
| 150 | + """ |
| 151 | + if self.is_forced or self.is_derived: |
| 152 | + return False |
| 153 | + |
| 154 | + if is_within_tolerance(self.value, value): |
| 155 | + return False |
| 156 | + |
| 157 | + self.value = value |
| 158 | + return True |
| 159 | + |
| 160 | + def set_comment(self, comment: str) -> bool: |
| 161 | + """ |
| 162 | + Set the parameter comment and return whether it changed. |
| 163 | +
|
| 164 | + Args: |
| 165 | + comment: The new comment to set |
| 166 | +
|
| 167 | + Returns: |
| 168 | + True if the comment was changed, False otherwise |
| 169 | +
|
| 170 | + """ |
| 171 | + if self.is_forced or self.is_derived: |
| 172 | + return False |
| 173 | + |
| 174 | + if comment == self.comment or (comment == "" and self.comment is None): |
| 175 | + return False |
| 176 | + |
| 177 | + self.comment = comment |
| 178 | + return True |
| 179 | + |
| 180 | + def is_editable(self) -> bool: |
| 181 | + """ |
| 182 | + Check if this parameter is editable. |
| 183 | +
|
| 184 | + Returns: |
| 185 | + True if the parameter can be edited, False otherwise |
| 186 | +
|
| 187 | + """ |
| 188 | + return not (self.is_forced or self.is_derived or self.is_readonly) |
| 189 | + |
| 190 | + def is_valid_value(self, value: float) -> bool: |
| 191 | + """ |
| 192 | + Check if a value is valid for this parameter. |
| 193 | +
|
| 194 | + Args: |
| 195 | + value: The value to check |
| 196 | +
|
| 197 | + Returns: |
| 198 | + True if the value is valid, False otherwise |
| 199 | +
|
| 200 | + """ |
| 201 | + if self.min_value is not None and value < self.min_value: |
| 202 | + return False |
| 203 | + |
| 204 | + return not (self.max_value is not None and value > self.max_value) |
0 commit comments