tinymovr 2.3.4__tar.gz → 2.5.0__tar.gz
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.
- {tinymovr-2.3.4 → tinymovr-2.5.0}/PKG-INFO +1 -1
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tests/test_amt22.py +5 -5
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tests/test_as5047.py +6 -6
- tinymovr-2.5.0/tests/test_export_import.py +371 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tests/test_hall.py +3 -3
- tinymovr-2.5.0/tests/test_nvm.py +710 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tests/test_watchdog.py +7 -1
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/channel.py +23 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/config/__init__.py +2 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/config/config.py +33 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/gui/__init__.py +1 -1
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/gui/helpers.py +15 -6
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/gui/widgets.py +5 -1
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/gui/window.py +72 -19
- tinymovr-2.5.0/tinymovr/specs/tinymovr_2_4_x.yaml +636 -0
- tinymovr-2.5.0/tinymovr/specs/tinymovr_2_5_x.yaml +641 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr.egg-info/PKG-INFO +1 -1
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr.egg-info/SOURCES.txt +4 -1
- tinymovr-2.3.4/tests/test_nvm.py +0 -250
- {tinymovr-2.3.4 → tinymovr-2.5.0}/MANIFEST.in +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/README.md +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/pyproject.toml +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/setup.cfg +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/setup.py +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tests/test_board.py +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tests/test_dfu.py +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tests/test_homing.py +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tests/test_rates.py +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tests/test_simulation.py +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tests/test_traj_planner.py +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/__init__.py +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/bus_manager.py +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/bus_router.py +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/cli.py +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/codec/__init__.py +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/codec/codec.py +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/constants.py +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/device_discovery.py +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/dfu.py +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/gui/gui.py +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/gui/worker.py +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/resources/icons/app_icon.png +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/resources/icons/call.png +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/resources/icons/call@2x.png +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/resources/icons/call_dark.png +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/resources/icons/call_dark@2x.png +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/resources/icons/empty.png +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/resources/icons/empty@2x.png +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/resources/icons/empty_dark.png +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/resources/icons/empty_dark@2x.png +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/specs/dfu_1_0_x.yaml +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/specs/dfu_1_1_x.yaml +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/specs/tinymovr_1_3_x.yaml +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/specs/tinymovr_1_4_x.yaml +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/specs/tinymovr_1_5_x.yaml +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/specs/tinymovr_1_6_x.yaml +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/specs/tinymovr_2_0_x.yaml +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/specs/tinymovr_2_1_x.yaml +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr/specs/tinymovr_2_3_x.yaml +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr.egg-info/dependency_links.txt +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr.egg-info/entry_points.txt +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr.egg-info/requires.txt +0 -0
- {tinymovr-2.3.4 → tinymovr-2.5.0}/tinymovr.egg-info/top_level.txt +0 -0
|
@@ -40,7 +40,7 @@ class TestAMT22(TMTestCase):
|
|
|
40
40
|
WARNING: This will perform one NVRAM write and two erase cycles.
|
|
41
41
|
"""
|
|
42
42
|
self.check_state(0)
|
|
43
|
-
self.
|
|
43
|
+
self.erase_config()
|
|
44
44
|
time.sleep(0.2)
|
|
45
45
|
|
|
46
46
|
self.configure_sensors(self.tm.sensors.setup.external_spi.type.AMT22)
|
|
@@ -70,7 +70,7 @@ class TestAMT22(TMTestCase):
|
|
|
70
70
|
|
|
71
71
|
time.sleep(0.1)
|
|
72
72
|
|
|
73
|
-
self.
|
|
73
|
+
self.save_config()
|
|
74
74
|
|
|
75
75
|
time.sleep(0.2)
|
|
76
76
|
|
|
@@ -89,7 +89,7 @@ class TestAMT22(TMTestCase):
|
|
|
89
89
|
delta=200 * tick,
|
|
90
90
|
)
|
|
91
91
|
|
|
92
|
-
self.
|
|
92
|
+
self.erase_config()
|
|
93
93
|
time.sleep(0.2)
|
|
94
94
|
|
|
95
95
|
@pytest.mark.sensor_amt22
|
|
@@ -100,7 +100,7 @@ class TestAMT22(TMTestCase):
|
|
|
100
100
|
to an onboard connection, and verify if the position control maintains accuracy.
|
|
101
101
|
"""
|
|
102
102
|
self.check_state(0)
|
|
103
|
-
self.
|
|
103
|
+
self.erase_config()
|
|
104
104
|
time.sleep(0.2)
|
|
105
105
|
|
|
106
106
|
# Initially configure with external SPI sensor
|
|
@@ -151,7 +151,7 @@ class TestAMT22(TMTestCase):
|
|
|
151
151
|
delta=2000 * tick,
|
|
152
152
|
)
|
|
153
153
|
|
|
154
|
-
self.
|
|
154
|
+
self.erase_config()
|
|
155
155
|
time.sleep(0.2)
|
|
156
156
|
|
|
157
157
|
|
|
@@ -40,7 +40,7 @@ class TestAS5047(TMTestCase):
|
|
|
40
40
|
WARNING: This will perform one NVRAM write and two erase cycles.
|
|
41
41
|
"""
|
|
42
42
|
self.check_state(0)
|
|
43
|
-
self.
|
|
43
|
+
self.erase_config()
|
|
44
44
|
time.sleep(0.2)
|
|
45
45
|
|
|
46
46
|
self.configure_sensors(self.tm.sensors.setup.external_spi.type.AS5047)
|
|
@@ -69,7 +69,7 @@ class TestAS5047(TMTestCase):
|
|
|
69
69
|
|
|
70
70
|
time.sleep(0.1)
|
|
71
71
|
|
|
72
|
-
self.
|
|
72
|
+
self.save_config()
|
|
73
73
|
|
|
74
74
|
time.sleep(0.2)
|
|
75
75
|
|
|
@@ -88,7 +88,7 @@ class TestAS5047(TMTestCase):
|
|
|
88
88
|
delta=2000 * tick,
|
|
89
89
|
)
|
|
90
90
|
|
|
91
|
-
self.
|
|
91
|
+
self.erase_config()
|
|
92
92
|
time.sleep(0.2)
|
|
93
93
|
|
|
94
94
|
@pytest.mark.sensor_as5047
|
|
@@ -99,7 +99,7 @@ class TestAS5047(TMTestCase):
|
|
|
99
99
|
to an onboard connection, and verify if the position control maintains accuracy.
|
|
100
100
|
"""
|
|
101
101
|
self.check_state(0)
|
|
102
|
-
self.
|
|
102
|
+
self.erase_config()
|
|
103
103
|
time.sleep(0.2)
|
|
104
104
|
|
|
105
105
|
# Initially configure with external SPI sensor
|
|
@@ -150,7 +150,7 @@ class TestAS5047(TMTestCase):
|
|
|
150
150
|
delta=2000 * tick,
|
|
151
151
|
)
|
|
152
152
|
|
|
153
|
-
self.
|
|
153
|
+
self.erase_config()
|
|
154
154
|
time.sleep(0.2)
|
|
155
155
|
|
|
156
156
|
@pytest.mark.sensor_as5047
|
|
@@ -160,7 +160,7 @@ class TestAS5047(TMTestCase):
|
|
|
160
160
|
different baud rates.
|
|
161
161
|
"""
|
|
162
162
|
self.check_state(0)
|
|
163
|
-
self.
|
|
163
|
+
self.erase_config()
|
|
164
164
|
time.sleep(0.2)
|
|
165
165
|
|
|
166
166
|
self.configure_sensors(self.tm.sensors.setup.external_spi.type.AS5047)
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tinymovr Export/Import Tests
|
|
3
|
+
Copyright Ioannis Chatzikonstantinou 2020-2023
|
|
4
|
+
|
|
5
|
+
Tests for export/import functionality:
|
|
6
|
+
- YAML spec consistency (all settable config attrs have export: True)
|
|
7
|
+
- HITL round-trip (export -> reset -> import -> verify)
|
|
8
|
+
- HITL export completeness (all export-tagged attrs appear in output)
|
|
9
|
+
|
|
10
|
+
This program is free software: you can redistribute it and/or modify it under
|
|
11
|
+
the terms of the GNU General Public License as published by the Free Software
|
|
12
|
+
Foundation, either version 3 of the License, or (at your option) any later
|
|
13
|
+
version.
|
|
14
|
+
This program is distributed in the hope that it will be useful, but WITHOUT
|
|
15
|
+
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
16
|
+
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
17
|
+
You should have received a copy of the GNU General Public License along with
|
|
18
|
+
this program. If not, see <http://www.gnu.org/licenses/>.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import time
|
|
23
|
+
import json
|
|
24
|
+
import unittest
|
|
25
|
+
|
|
26
|
+
import yaml
|
|
27
|
+
import pytest
|
|
28
|
+
from importlib.resources import files as importlib_files
|
|
29
|
+
|
|
30
|
+
from avlos.unit_field import get_registry
|
|
31
|
+
from avlos.json_codec import AvlosEncoder
|
|
32
|
+
from avlos.definitions import RemoteAttribute, RemoteEnum, RemoteBitmask
|
|
33
|
+
|
|
34
|
+
from tinymovr.config import export_config, import_config
|
|
35
|
+
from tests import TMTestCase
|
|
36
|
+
|
|
37
|
+
ureg = get_registry()
|
|
38
|
+
A = ureg.ampere
|
|
39
|
+
Hz = ureg.hertz
|
|
40
|
+
tick = ureg.ticks
|
|
41
|
+
s = ureg.second
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Helpers for YAML spec walking
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
SPECS_DIR = str(importlib_files("tinymovr").joinpath("specs"))
|
|
48
|
+
|
|
49
|
+
# Attributes that have setters but should NOT have export: True.
|
|
50
|
+
# Includes runtime controls and dangerous-to-import parameters.
|
|
51
|
+
EXCLUDED_SETTER_PATHS = {
|
|
52
|
+
"controller.state",
|
|
53
|
+
"controller.mode",
|
|
54
|
+
"comms.can.rate", # importing a mismatched CAN rate kills communication
|
|
55
|
+
"comms.can.id", # importing CAN ID mid-batch disrupts addressing
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Meta keys that indicate a runtime setpoint rather than a config parameter.
|
|
59
|
+
RUNTIME_META_KEYS = {"jog_step"}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _walk_spec_attrs(attrs, path_prefix=""):
|
|
63
|
+
"""
|
|
64
|
+
Recursively yield (dotted_path, attr_dict) for every leaf attribute
|
|
65
|
+
in a YAML spec attribute tree.
|
|
66
|
+
"""
|
|
67
|
+
for attr in attrs:
|
|
68
|
+
name = attr["name"]
|
|
69
|
+
dotted = f"{path_prefix}.{name}" if path_prefix else name
|
|
70
|
+
if "remote_attributes" in attr:
|
|
71
|
+
yield from _walk_spec_attrs(attr["remote_attributes"], dotted)
|
|
72
|
+
else:
|
|
73
|
+
yield dotted, attr
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _is_excluded_setter(dotted_path, attr):
|
|
77
|
+
"""
|
|
78
|
+
Return True if this attribute has a setter but should NOT
|
|
79
|
+
be expected to have export: True (e.g. runtime controls,
|
|
80
|
+
dangerous-to-import parameters).
|
|
81
|
+
"""
|
|
82
|
+
if dotted_path in EXCLUDED_SETTER_PATHS:
|
|
83
|
+
return True
|
|
84
|
+
meta = attr.get("meta", {})
|
|
85
|
+
if any(k in meta for k in RUNTIME_META_KEYS):
|
|
86
|
+
return True
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# 4a. YAML Spec Consistency Test (no hardware required)
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
class TestSpecExportConsistency(unittest.TestCase):
|
|
95
|
+
"""
|
|
96
|
+
Verify that every settable config attribute in the latest YAML spec
|
|
97
|
+
has meta: {export: True}. This is a pure-Python test with no hardware
|
|
98
|
+
dependency.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def _load_spec(self, filename):
|
|
102
|
+
path = os.path.join(SPECS_DIR, filename)
|
|
103
|
+
with open(path, "r") as f:
|
|
104
|
+
return yaml.safe_load(f)
|
|
105
|
+
|
|
106
|
+
def _check_spec(self, filename):
|
|
107
|
+
spec = self._load_spec(filename)
|
|
108
|
+
missing = []
|
|
109
|
+
for dotted, attr in _walk_spec_attrs(spec.get("remote_attributes", [])):
|
|
110
|
+
if "setter_name" not in attr:
|
|
111
|
+
continue
|
|
112
|
+
if _is_excluded_setter(dotted, attr):
|
|
113
|
+
continue
|
|
114
|
+
# Also skip void callers masquerading as setters (arguments list)
|
|
115
|
+
if "arguments" in attr:
|
|
116
|
+
continue
|
|
117
|
+
meta = attr.get("meta", {})
|
|
118
|
+
if not meta.get("export"):
|
|
119
|
+
missing.append(dotted)
|
|
120
|
+
return missing
|
|
121
|
+
|
|
122
|
+
def test_latest_spec_export_tags(self):
|
|
123
|
+
"""
|
|
124
|
+
All settable config attributes in tinymovr_2_5_x.yaml must have
|
|
125
|
+
meta: {export: True}.
|
|
126
|
+
"""
|
|
127
|
+
missing = self._check_spec("tinymovr_2_5_x.yaml")
|
|
128
|
+
self.assertEqual(
|
|
129
|
+
missing,
|
|
130
|
+
[],
|
|
131
|
+
"The following settable config attributes are missing "
|
|
132
|
+
"meta: {export: True} in tinymovr_2_5_x.yaml:\n "
|
|
133
|
+
+ "\n ".join(missing),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
# 4b. HITL Export/Import Round-Trip Test
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
@pytest.mark.hitl_default
|
|
142
|
+
class TestExportImportRoundTrip(TMTestCase):
|
|
143
|
+
"""
|
|
144
|
+
Hardware-in-the-loop test: modify values, export, reset to defaults,
|
|
145
|
+
import, then verify the values were restored.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def test_a_export_import_round_trip(self):
|
|
149
|
+
"""
|
|
150
|
+
Set non-default values on key attributes, export, reset, import,
|
|
151
|
+
and verify each value is correctly restored.
|
|
152
|
+
"""
|
|
153
|
+
self.check_state(0)
|
|
154
|
+
self.erase_config()
|
|
155
|
+
time.sleep(0.2)
|
|
156
|
+
|
|
157
|
+
self.try_calibrate()
|
|
158
|
+
self.assertEqual(self.tm.motor.calibrated, True)
|
|
159
|
+
|
|
160
|
+
# --- Define test values (non-default) ---
|
|
161
|
+
test_values = {
|
|
162
|
+
"controller.position.p_gain": 35.0,
|
|
163
|
+
"controller.velocity.p_gain": 5e-5,
|
|
164
|
+
"controller.velocity.i_gain": 1.5e-2,
|
|
165
|
+
"controller.velocity.limit": 80000,
|
|
166
|
+
"controller.velocity.deadband": 300,
|
|
167
|
+
"controller.velocity.increment": 150.0,
|
|
168
|
+
"controller.current.bandwidth": 1500.0,
|
|
169
|
+
"controller.current.max_Ibrake": 2.0,
|
|
170
|
+
"traj_planner.max_vel": 90000,
|
|
171
|
+
"traj_planner.max_accel": 400000,
|
|
172
|
+
"traj_planner.max_decel": 400000,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# Conditionally add attrs that may exist in newer specs
|
|
176
|
+
if hasattr(self.tm.controller.current, "Iq_limit"):
|
|
177
|
+
test_values["controller.current.Iq_limit"] = 6.0
|
|
178
|
+
if hasattr(self.tm.controller.current, "max_Ibus_regen"):
|
|
179
|
+
test_values["controller.current.max_Ibus_regen"] = 1.5
|
|
180
|
+
|
|
181
|
+
# --- Set values ---
|
|
182
|
+
self.tm.controller.position.p_gain = test_values["controller.position.p_gain"]
|
|
183
|
+
self.tm.controller.velocity.p_gain = test_values["controller.velocity.p_gain"]
|
|
184
|
+
self.tm.controller.velocity.i_gain = test_values["controller.velocity.i_gain"]
|
|
185
|
+
self.tm.controller.velocity.limit = test_values["controller.velocity.limit"] * tick / s
|
|
186
|
+
self.tm.controller.velocity.deadband = test_values["controller.velocity.deadband"] * tick
|
|
187
|
+
self.tm.controller.velocity.increment = test_values["controller.velocity.increment"]
|
|
188
|
+
self.tm.controller.current.bandwidth = test_values["controller.current.bandwidth"]
|
|
189
|
+
self.tm.controller.current.max_Ibrake = test_values["controller.current.max_Ibrake"] * A
|
|
190
|
+
self.tm.traj_planner.max_vel = test_values["traj_planner.max_vel"] * tick / s
|
|
191
|
+
self.tm.traj_planner.max_accel = test_values["traj_planner.max_accel"] * tick / s
|
|
192
|
+
self.tm.traj_planner.max_decel = test_values["traj_planner.max_decel"] * tick / (s * s)
|
|
193
|
+
|
|
194
|
+
if "controller.current.Iq_limit" in test_values:
|
|
195
|
+
self.tm.controller.current.Iq_limit = test_values["controller.current.Iq_limit"]
|
|
196
|
+
if "controller.current.max_Ibus_regen" in test_values:
|
|
197
|
+
self.tm.controller.current.max_Ibus_regen = test_values["controller.current.max_Ibus_regen"]
|
|
198
|
+
|
|
199
|
+
time.sleep(0.1)
|
|
200
|
+
|
|
201
|
+
# --- Export ---
|
|
202
|
+
exported = export_config(self.tm)
|
|
203
|
+
self.assertIsNotNone(exported, "export_config() returned None")
|
|
204
|
+
self.assertIsInstance(exported, dict, "export_config() should return a dict")
|
|
205
|
+
|
|
206
|
+
# Verify JSON round-trip (mimics what the GUI does)
|
|
207
|
+
json_str = json.dumps(exported, cls=AvlosEncoder)
|
|
208
|
+
reimported = json.loads(json_str)
|
|
209
|
+
self.assertIsInstance(reimported, dict)
|
|
210
|
+
|
|
211
|
+
# --- Verify exported contains key sections ---
|
|
212
|
+
self.assertIn("controller", exported)
|
|
213
|
+
self.assertIn("traj_planner", exported)
|
|
214
|
+
|
|
215
|
+
# --- Reset to defaults ---
|
|
216
|
+
self.tm.controller.idle()
|
|
217
|
+
time.sleep(0.1)
|
|
218
|
+
self.erase_config()
|
|
219
|
+
time.sleep(0.2)
|
|
220
|
+
self.reset_and_wait()
|
|
221
|
+
|
|
222
|
+
# --- Import ---
|
|
223
|
+
import_config(self.tm, reimported)
|
|
224
|
+
time.sleep(0.2)
|
|
225
|
+
|
|
226
|
+
# --- Verify values restored ---
|
|
227
|
+
self.assertAlmostEqual(
|
|
228
|
+
self.tm.controller.position.p_gain,
|
|
229
|
+
test_values["controller.position.p_gain"],
|
|
230
|
+
places=1,
|
|
231
|
+
msg="p_gain not restored after import",
|
|
232
|
+
)
|
|
233
|
+
self.assertAlmostEqual(
|
|
234
|
+
self.tm.controller.velocity.p_gain,
|
|
235
|
+
test_values["controller.velocity.p_gain"],
|
|
236
|
+
places=8,
|
|
237
|
+
msg="velocity p_gain not restored after import",
|
|
238
|
+
)
|
|
239
|
+
self.assertAlmostEqual(
|
|
240
|
+
self.tm.controller.velocity.i_gain,
|
|
241
|
+
test_values["controller.velocity.i_gain"],
|
|
242
|
+
places=5,
|
|
243
|
+
msg="velocity i_gain not restored after import",
|
|
244
|
+
)
|
|
245
|
+
self.assertAlmostEqual(
|
|
246
|
+
self.tm.controller.velocity.limit,
|
|
247
|
+
test_values["controller.velocity.limit"] * tick / s,
|
|
248
|
+
msg="velocity limit not restored after import",
|
|
249
|
+
)
|
|
250
|
+
self.assertAlmostEqual(
|
|
251
|
+
self.tm.controller.velocity.deadband,
|
|
252
|
+
test_values["controller.velocity.deadband"] * tick,
|
|
253
|
+
msg="velocity deadband not restored after import",
|
|
254
|
+
)
|
|
255
|
+
self.assertAlmostEqual(
|
|
256
|
+
self.tm.controller.current.bandwidth,
|
|
257
|
+
test_values["controller.current.bandwidth"] * Hz,
|
|
258
|
+
delta=50 * Hz,
|
|
259
|
+
msg="current bandwidth not restored after import",
|
|
260
|
+
)
|
|
261
|
+
self.assertAlmostEqual(
|
|
262
|
+
self.tm.controller.current.max_Ibrake,
|
|
263
|
+
test_values["controller.current.max_Ibrake"] * A,
|
|
264
|
+
msg="max_Ibrake not restored after import",
|
|
265
|
+
)
|
|
266
|
+
self.assertAlmostEqual(
|
|
267
|
+
self.tm.traj_planner.max_vel,
|
|
268
|
+
test_values["traj_planner.max_vel"] * tick / s,
|
|
269
|
+
msg="traj_planner max_vel not restored after import",
|
|
270
|
+
)
|
|
271
|
+
self.assertAlmostEqual(
|
|
272
|
+
self.tm.traj_planner.max_accel,
|
|
273
|
+
test_values["traj_planner.max_accel"] * tick / s,
|
|
274
|
+
msg="traj_planner max_accel not restored after import",
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
if "controller.current.Iq_limit" in test_values:
|
|
278
|
+
self.assertAlmostEqual(
|
|
279
|
+
self.tm.controller.current.Iq_limit,
|
|
280
|
+
test_values["controller.current.Iq_limit"] * A,
|
|
281
|
+
msg="Iq_limit not restored after import",
|
|
282
|
+
)
|
|
283
|
+
if "controller.current.max_Ibus_regen" in test_values:
|
|
284
|
+
self.assertAlmostEqual(
|
|
285
|
+
self.tm.controller.current.max_Ibus_regen,
|
|
286
|
+
test_values["controller.current.max_Ibus_regen"] * A,
|
|
287
|
+
msg="max_Ibus_regen not restored after import",
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# --- Clean up ---
|
|
291
|
+
self.erase_config()
|
|
292
|
+
time.sleep(0.2)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# ---------------------------------------------------------------------------
|
|
296
|
+
# 4c. HITL Export Completeness Test
|
|
297
|
+
# ---------------------------------------------------------------------------
|
|
298
|
+
|
|
299
|
+
@pytest.mark.hitl_default
|
|
300
|
+
class TestExportCompleteness(TMTestCase):
|
|
301
|
+
"""
|
|
302
|
+
Verify every attribute with meta: {export: True} on the *connected
|
|
303
|
+
device* actually appears in the output of export_values().
|
|
304
|
+
|
|
305
|
+
Uses the live device attribute tree (not a YAML file) so the test
|
|
306
|
+
is accurate regardless of which firmware/spec version is running.
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
def _collect_device_export_paths(self, node, prefix=""):
|
|
310
|
+
"""
|
|
311
|
+
Walk the live Avlos node tree and collect dotted paths of all
|
|
312
|
+
attributes that have meta["export"] == True.
|
|
313
|
+
"""
|
|
314
|
+
paths = []
|
|
315
|
+
if hasattr(node, "remote_attributes"):
|
|
316
|
+
for name, child in node.remote_attributes.items():
|
|
317
|
+
dotted = f"{prefix}.{name}" if prefix else name
|
|
318
|
+
paths.extend(self._collect_device_export_paths(child, dotted))
|
|
319
|
+
else:
|
|
320
|
+
if isinstance(node, (RemoteAttribute, RemoteEnum, RemoteBitmask)):
|
|
321
|
+
if node.meta.get("export"):
|
|
322
|
+
paths.append(prefix)
|
|
323
|
+
return paths
|
|
324
|
+
|
|
325
|
+
def _lookup_nested(self, data, dotted_path):
|
|
326
|
+
"""
|
|
327
|
+
Look up a dotted path like 'controller.position.p_gain' in a
|
|
328
|
+
nested dict. Returns (True, value) or (False, None).
|
|
329
|
+
"""
|
|
330
|
+
keys = dotted_path.split(".")
|
|
331
|
+
current = data
|
|
332
|
+
for key in keys:
|
|
333
|
+
if not isinstance(current, dict) or key not in current:
|
|
334
|
+
return False, None
|
|
335
|
+
current = current[key]
|
|
336
|
+
return True, current
|
|
337
|
+
|
|
338
|
+
def test_a_all_export_tagged_attrs_present(self):
|
|
339
|
+
"""
|
|
340
|
+
Every attribute with export: True on the connected device must
|
|
341
|
+
appear in export_values() output.
|
|
342
|
+
"""
|
|
343
|
+
self.check_state(0)
|
|
344
|
+
self.try_calibrate()
|
|
345
|
+
|
|
346
|
+
exported = export_config(self.tm)
|
|
347
|
+
self.assertIsNotNone(exported, "export_config() returned None")
|
|
348
|
+
|
|
349
|
+
expected_paths = self._collect_device_export_paths(self.tm)
|
|
350
|
+
self.assertGreater(len(expected_paths), 0,
|
|
351
|
+
"No export-tagged attributes found on device")
|
|
352
|
+
|
|
353
|
+
missing = []
|
|
354
|
+
for path in expected_paths:
|
|
355
|
+
found, _ = self._lookup_nested(exported, path)
|
|
356
|
+
if not found:
|
|
357
|
+
missing.append(path)
|
|
358
|
+
|
|
359
|
+
self.assertEqual(
|
|
360
|
+
missing,
|
|
361
|
+
[],
|
|
362
|
+
"The following export-tagged attributes are missing from "
|
|
363
|
+
"export_values() output:\n " + "\n ".join(missing),
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
self.erase_config()
|
|
367
|
+
time.sleep(0.2)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
if __name__ == "__main__":
|
|
371
|
+
unittest.main()
|
|
@@ -43,7 +43,7 @@ class TestHall(TMTestCase):
|
|
|
43
43
|
WARNING: This will perform one NVRAM write and two erase cycles.
|
|
44
44
|
"""
|
|
45
45
|
self.check_state(0)
|
|
46
|
-
self.
|
|
46
|
+
self.erase_config()
|
|
47
47
|
time.sleep(0.2)
|
|
48
48
|
|
|
49
49
|
self.tm.sensors.select.position_sensor.connection = self.tm.sensors.select.position_sensor.connection.HALL
|
|
@@ -78,7 +78,7 @@ class TestHall(TMTestCase):
|
|
|
78
78
|
|
|
79
79
|
time.sleep(0.1)
|
|
80
80
|
|
|
81
|
-
self.
|
|
81
|
+
self.save_config()
|
|
82
82
|
|
|
83
83
|
time.sleep(0.2)
|
|
84
84
|
|
|
@@ -97,7 +97,7 @@ class TestHall(TMTestCase):
|
|
|
97
97
|
delta=30000 * tick,
|
|
98
98
|
)
|
|
99
99
|
|
|
100
|
-
self.
|
|
100
|
+
self.erase_config()
|
|
101
101
|
time.sleep(0.2)
|
|
102
102
|
|
|
103
103
|
|