tinymovr 2.6.0__tar.gz → 3.2.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.
Files changed (67) hide show
  1. {tinymovr-2.6.0 → tinymovr-3.2.0}/PKG-INFO +2 -2
  2. {tinymovr-2.6.0 → tinymovr-3.2.0}/pyproject.toml +1 -0
  3. {tinymovr-2.6.0 → tinymovr-3.2.0}/setup.py +3 -3
  4. {tinymovr-2.6.0 → tinymovr-3.2.0}/tests/test_amt22.py +1 -1
  5. {tinymovr-2.6.0 → tinymovr-3.2.0}/tests/test_as5047.py +1 -1
  6. {tinymovr-2.6.0 → tinymovr-3.2.0}/tests/test_board.py +1 -1
  7. {tinymovr-2.6.0 → tinymovr-3.2.0}/tests/test_dfu.py +1 -1
  8. tinymovr-3.2.0/tests/test_differential.py +93 -0
  9. {tinymovr-2.6.0 → tinymovr-3.2.0}/tests/test_export_import.py +1 -1
  10. {tinymovr-2.6.0 → tinymovr-3.2.0}/tests/test_hall.py +67 -4
  11. tinymovr-3.2.0/tests/test_homing.py +237 -0
  12. {tinymovr-2.6.0 → tinymovr-3.2.0}/tests/test_nvm.py +29 -1
  13. {tinymovr-2.6.0 → tinymovr-3.2.0}/tests/test_rates.py +1 -1
  14. {tinymovr-2.6.0 → tinymovr-3.2.0}/tests/test_simulation.py +1 -1
  15. {tinymovr-2.6.0 → tinymovr-3.2.0}/tests/test_traj_planner.py +1 -1
  16. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/bus_manager.py +1 -1
  17. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/bus_router.py +1 -1
  18. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/channel.py +1 -1
  19. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/cli.py +1 -1
  20. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/codec/codec.py +1 -1
  21. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/config/config.py +1 -1
  22. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/constants.py +1 -1
  23. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/device_discovery.py +1 -1
  24. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/dfu.py +1 -1
  25. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/gui/__init__.py +0 -1
  26. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/gui/gui.py +18 -4
  27. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/gui/helpers.py +1 -1
  28. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/gui/widgets.py +1 -1
  29. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/gui/window.py +1 -1
  30. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/gui/worker.py +1 -1
  31. tinymovr-3.2.0/tinymovr/specs/tinymovr_3_0_x.yaml +642 -0
  32. tinymovr-3.2.0/tinymovr/specs/tinymovr_3_1_x.yaml +656 -0
  33. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr.egg-info/PKG-INFO +2 -2
  34. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr.egg-info/SOURCES.txt +4 -1
  35. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr.egg-info/entry_points.txt +1 -1
  36. tinymovr-2.6.0/tests/test_homing.py +0 -39
  37. {tinymovr-2.6.0 → tinymovr-3.2.0}/MANIFEST.in +0 -0
  38. {tinymovr-2.6.0 → tinymovr-3.2.0}/README.md +0 -0
  39. {tinymovr-2.6.0 → tinymovr-3.2.0}/setup.cfg +0 -0
  40. {tinymovr-2.6.0 → tinymovr-3.2.0}/tests/test_watchdog.py +0 -0
  41. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/__init__.py +0 -0
  42. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/codec/__init__.py +0 -0
  43. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/config/__init__.py +0 -0
  44. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/resources/icons/app_icon.png +0 -0
  45. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/resources/icons/call.png +0 -0
  46. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/resources/icons/call@2x.png +0 -0
  47. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/resources/icons/call_dark.png +0 -0
  48. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/resources/icons/call_dark@2x.png +0 -0
  49. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/resources/icons/empty.png +0 -0
  50. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/resources/icons/empty@2x.png +0 -0
  51. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/resources/icons/empty_dark.png +0 -0
  52. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/resources/icons/empty_dark@2x.png +0 -0
  53. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/specs/dfu_1_0_x.yaml +0 -0
  54. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/specs/dfu_1_1_x.yaml +0 -0
  55. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/specs/tinymovr_1_3_x.yaml +0 -0
  56. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/specs/tinymovr_1_4_x.yaml +0 -0
  57. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/specs/tinymovr_1_5_x.yaml +0 -0
  58. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/specs/tinymovr_1_6_x.yaml +0 -0
  59. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/specs/tinymovr_2_0_x.yaml +0 -0
  60. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/specs/tinymovr_2_1_x.yaml +0 -0
  61. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/specs/tinymovr_2_3_x.yaml +0 -0
  62. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/specs/tinymovr_2_4_x.yaml +0 -0
  63. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/specs/tinymovr_2_5_x.yaml +0 -0
  64. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr/specs/tinymovr_2_6_x.yaml +0 -0
  65. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr.egg-info/dependency_links.txt +0 -0
  66. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr.egg-info/requires.txt +0 -0
  67. {tinymovr-2.6.0 → tinymovr-3.2.0}/tinymovr.egg-info/top_level.txt +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinymovr
3
- Version: 2.6.0
3
+ Version: 3.2.0
4
4
  Summary: Tinymovr Studio
5
5
  Home-page: https://github.com/yconst/Tinymovr
6
- Author: Yannis Chatzikonstantinou
6
+ Author: MotionLayer P.C.
7
7
  Author-email: info@tinymovr.com
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
@@ -9,6 +9,7 @@ markers = [
9
9
  "sensor_amt22: marks tests that need the amt22 sensor",
10
10
  "sensor_as5047: marks tests that need the as5047 sensor",
11
11
  "sensor_hall: marks tests that need a hall effect sensored motor",
12
+ "differential: marks tests that need a differential (vernier) dual-sensor setup",
12
13
  "eol: marks tests for end-of-line testing",
13
14
  "dfu: marks tests for dfu testing",
14
15
  ]
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Tinymovr Setup Module
3
- Copyright Ioannis Chatzikonstantinou 2020-2023
3
+ Copyright 2020-2026 MotionLayer P.C.
4
4
 
5
5
  Standard Python module for setting up the Tinymovr package
6
6
 
@@ -27,7 +27,7 @@ README = (HERE / "README.md").read_text()
27
27
  setup(
28
28
  name="tinymovr",
29
29
  # version="", #version will be taken from git tag
30
- author="Yannis Chatzikonstantinou",
30
+ author="MotionLayer P.C.",
31
31
  author_email="info@tinymovr.com",
32
32
  description="Tinymovr Studio",
33
33
  long_description=README,
@@ -66,7 +66,7 @@ setup(
66
66
  entry_points={
67
67
  "console_scripts": [
68
68
  "tinymovr_cli=tinymovr.cli:spawn",
69
- "tinymovr=tinymovr.gui:spawn",
69
+ "tinymovr=tinymovr.gui.gui:spawn",
70
70
  "tinymovr_dfu=tinymovr.dfu:spawn"
71
71
  ]
72
72
  },
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Tinymovr Board Config Tests
3
- Copyright Ioannis Chatzikonstantinou 2020-2023
3
+ Copyright 2020-2026 MotionLayer P.C.
4
4
 
5
5
  Tests saving and loading Tinymovr configuration to/from NVRAM.
6
6
 
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Tinymovr Board Config Tests
3
- Copyright Ioannis Chatzikonstantinou 2020-2023
3
+ Copyright 2020-2026 MotionLayer P.C.
4
4
 
5
5
  Tests saving and loading Tinymovr configuration to/from NVRAM.
6
6
 
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Tinymovr Basic Board Tests
3
- Copyright Ioannis Chatzikonstantinou 2020-2023
3
+ Copyright 2020-2026 MotionLayer P.C.
4
4
 
5
5
  Tests basic functionality of Tinymovr boards.
6
6
 
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Tinymovr DFU Test Class
3
- Copyright Ioannis Chatzikonstantinou 2020-2023
3
+ Copyright 2020-2026 MotionLayer P.C.
4
4
 
5
5
  Implements convenience functionality.
6
6
 
@@ -0,0 +1,93 @@
1
+ """
2
+ Tinymovr Differential (Vernier) Mode Tests
3
+ Copyright 2020-2026 MotionLayer P.C.
4
+
5
+ Tests the differential transform-init mode, where the absolute multiturn shaft
6
+ position is recovered from two geared sensors (vernier/nonius) during
7
+ calibration and re-seeded on every reset.
8
+
9
+ This program is free software: you can redistribute it and/or modify it under
10
+ the terms of the GNU General Public License as published by the Free Software
11
+ Foundation, either version 3 of the License, or (at your option) any later
12
+ version.
13
+ This program is distributed in the hope that it will be useful, but WITHOUT
14
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15
+ FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16
+ You should have received a copy of the GNU General Public License along with
17
+ this program. If not, see <http://www.gnu.org/licenses/>.
18
+ """
19
+
20
+ import time
21
+
22
+ from avlos.unit_field import get_registry
23
+
24
+ import unittest
25
+ import pytest
26
+ from tests import TMTestCase
27
+
28
+ ureg = get_registry()
29
+ tick = ureg.ticks
30
+ tsleep = 1.50
31
+
32
+
33
+ class TestDifferential(TMTestCase):
34
+
35
+ @pytest.mark.differential
36
+ def test_multiturn_position_retained_after_reset(self):
37
+ """The differential transform must recover the same absolute multiturn
38
+ shaft position after a reset, with no offset and no full-turn slip.
39
+ """
40
+ self.check_state(0)
41
+ self.erase_config()
42
+ time.sleep(0.2)
43
+
44
+ # Vernier setup: the onboard sensor is used for both commutation and
45
+ # position, and the geared secondary (MA7XX) on external SPI is read
46
+ # during calibration/seed.
47
+ self.configure_sensors(self.tm.sensors.setup.external_spi.type.MA7XX)
48
+ self.select_sensors(
49
+ self.tm.sensors.select.commutation_sensor.connection.ONBOARD,
50
+ self.tm.sensors.select.position_sensor.connection.ONBOARD,
51
+ )
52
+ self.tm.sensors.select.transform_init.mode = (
53
+ self.tm.sensors.select.transform_init.mode.DIFFERENTIAL
54
+ )
55
+ time.sleep(0.2)
56
+
57
+ self.try_calibrate()
58
+
59
+ self.tm.controller.velocity.p_gain = 1e-5
60
+
61
+ self.tm.controller.position_mode()
62
+ self.check_state(2)
63
+
64
+ # Stay within a single output-shaft turn (the vernier resolves only one
65
+ # turn). Sweep mid-turn, then near a full turn, and verify tracking.
66
+ for target in (4000 * tick, 8000 * tick):
67
+ self.tm.controller.position.setpoint = target
68
+ time.sleep(tsleep)
69
+ self.assertAlmostEqual(
70
+ self.tm.sensors.user_frame.position_estimate,
71
+ target,
72
+ delta=2000 * tick,
73
+ )
74
+
75
+ pos_before = self.tm.sensors.user_frame.position_estimate
76
+
77
+ self.tm.controller.idle()
78
+ time.sleep(0.1)
79
+ self.save_config()
80
+ time.sleep(0.2)
81
+
82
+ # After reset the origin is re-seeded purely from the two sensors.
83
+ self.reset_and_wait()
84
+
85
+ pos_after = self.tm.sensors.user_frame.position_estimate
86
+ self.assertAlmostEqual(pos_after, pos_before, delta=500 * tick)
87
+
88
+ self.erase_config()
89
+ time.sleep(0.2)
90
+
91
+
92
+ if __name__ == "__main__":
93
+ unittest.main()
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Tinymovr Export/Import Tests
3
- Copyright Ioannis Chatzikonstantinou 2020-2023
3
+ Copyright 2020-2026 MotionLayer P.C.
4
4
 
5
5
  Tests for export/import functionality:
6
6
  - YAML spec consistency (all settable config attrs have export: True)
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Tinymovr Board Config Tests
3
- Copyright Ioannis Chatzikonstantinou 2020-2023
3
+ Copyright 2020-2026 MotionLayer P.C.
4
4
 
5
5
  Tests saving and loading Tinymovr configuration to/from NVRAM.
6
6
 
@@ -28,7 +28,7 @@ ureg = get_registry()
28
28
  A = ureg.ampere
29
29
  tick = ureg.ticks
30
30
  s = ureg.second
31
- tsleep = 1.00
31
+ tsleep = 0.9
32
32
 
33
33
 
34
34
  def set_pole_pairs(tm):
@@ -56,8 +56,8 @@ class TestHall(TMTestCase):
56
56
 
57
57
  self.try_calibrate(precheck_callback=set_pole_pairs)
58
58
 
59
- self.tm.controller.position.p_gain = 15
60
- self.tm.controller.velocity.limit = 400000
59
+ self.tm.controller.position.p_gain = 11
60
+ self.tm.controller.velocity.limit = 700000
61
61
  self.tm.controller.velocity.p_gain = 1e-5
62
62
  self.tm.controller.velocity.i_gain = 0
63
63
 
@@ -100,6 +100,69 @@ class TestHall(TMTestCase):
100
100
  self.erase_config()
101
101
  time.sleep(0.2)
102
102
 
103
+ @pytest.mark.sensor_hall
104
+ def test_b_velocity_control(self):
105
+ """
106
+ Test velocity control with Hall sensors, ramping through
107
+ velocities in both directions.
108
+ """
109
+ self.check_state(0)
110
+ self.erase_config()
111
+ time.sleep(0.2)
112
+
113
+ self.tm.sensors.select.position_sensor.connection = self.tm.sensors.select.position_sensor.connection.HALL
114
+ self.tm.sensors.select.commutation_sensor.connection = self.tm.sensors.select.commutation_sensor.connection.HALL
115
+
116
+ self.tm.sensors.select.position_sensor.bandwidth = 50
117
+ self.tm.sensors.select.commutation_sensor.bandwidth = 80
118
+ time.sleep(0.2)
119
+
120
+ self.try_calibrate(precheck_callback=set_pole_pairs)
121
+
122
+ self.tm.controller.velocity.limit = 700000
123
+ self.tm.controller.velocity.p_gain = 8e-6
124
+ self.tm.controller.velocity.i_gain = 0
125
+
126
+ self.tm.controller.velocity_mode()
127
+ self.check_state(2)
128
+
129
+ R = 10
130
+ multiplier = 60000 * tick / s
131
+ max_delta = 60000 * tick / s
132
+
133
+ velocity_pairs = []
134
+
135
+ for i in range(R):
136
+ target = i * multiplier
137
+ self.tm.controller.velocity.setpoint = target
138
+ time.sleep(tsleep)
139
+ velocity_pairs.append((target, self.tm.sensors.user_frame.velocity_estimate))
140
+
141
+ for i in range(R):
142
+ target = (R - i) * multiplier
143
+ self.tm.controller.velocity.setpoint = target
144
+ time.sleep(tsleep)
145
+ velocity_pairs.append((target, self.tm.sensors.user_frame.velocity_estimate))
146
+
147
+ for i in range(R):
148
+ target = -i * multiplier
149
+ self.tm.controller.velocity.setpoint = target
150
+ time.sleep(tsleep)
151
+ velocity_pairs.append((target, self.tm.sensors.user_frame.velocity_estimate))
152
+
153
+ for i in range(R):
154
+ target = (i - R) * multiplier
155
+ self.tm.controller.velocity.setpoint = target
156
+ time.sleep(tsleep)
157
+ velocity_pairs.append((target, self.tm.sensors.user_frame.velocity_estimate))
158
+
159
+ for target, estimate in velocity_pairs:
160
+ self.assertAlmostEqual(target, estimate, delta=max_delta)
161
+
162
+ self.tm.controller.idle()
163
+ self.erase_config()
164
+ time.sleep(0.2)
165
+
103
166
 
104
167
  if __name__ == "__main__":
105
168
  unittest.main()
@@ -0,0 +1,237 @@
1
+ """
2
+ This unit test suite tests the Homing functionality.
3
+ """
4
+ import time
5
+
6
+ from avlos.unit_field import get_registry
7
+
8
+ import unittest
9
+ import pytest
10
+ from tests import TMTestCase
11
+
12
+ ureg = get_registry()
13
+ A = ureg.ampere
14
+ ticks = ureg.ticks
15
+ s = ureg.second
16
+
17
+ HOMING_WARNINGS_NONE = 0
18
+ HOMING_WARNINGS_TIMEOUT = 1
19
+
20
+ CONTROLLER_MODE_POSITION = 2
21
+ CONTROLLER_MODE_HOMING = 4
22
+
23
+ ITERATIONS_PER_DIRECTION = 2
24
+ HOMING_SPEED = 4000.0
25
+ FREE_TRAVEL_S = 1.5
26
+ STALL_DWELL_EXTRA_S = 0.3
27
+ RETRACT_SETTLE_S = 0.3
28
+ INTER_ITERATION_PAUSE_S = 0.3
29
+
30
+
31
+ class TestHoming(TMTestCase):
32
+ def test_homing(self):
33
+ """
34
+ Test Homing
35
+ """
36
+ self.check_state(0)
37
+ self.try_calibrate()
38
+
39
+ self.tm.controller.position_mode()
40
+ self.check_state(2)
41
+
42
+ self.tm.homing.home()
43
+ time.sleep(self.tm.homing.max_homing_t.magnitude + 0.1)
44
+
45
+ self.assertEqual(self.tm.controller.mode, 2)
46
+ self.assertEqual(self.tm.homing.warnings, 1)
47
+ self.tm.controller.idle()
48
+
49
+ @pytest.mark.hitl_default
50
+ @pytest.mark.hitl_mini
51
+ def test_homing_retraction(self):
52
+ """
53
+ Regression test for the homing retraction reference bug
54
+ (fix commit 86b19db).
55
+
56
+ Per-iteration flow: motor runs freely for FREE_TRAVEL_S, the current
57
+ limit is dropped to simulate an endstop, the planner detects the
58
+ stall and installs the user-frame offset, the current limit is
59
+ restored so the motor physically retracts to user_pos =
60
+ -direction*retract_dist, and a sensor-frame displacement assertion
61
+ verifies the physical motion matches expectation.
62
+
63
+ The bug only manifests when the user-frame offset is non-zero at the
64
+ start of homing (carried over from a previous run), so the test
65
+ runs ITERATIONS_PER_DIRECTION>=2 iterations per direction in both
66
+ +1 and -1 directions. See plan file for full frame analysis.
67
+ """
68
+ self.check_state(0)
69
+ self.try_calibrate()
70
+
71
+ iq_limit_normal = self.tm.controller.current.Iq_limit.magnitude
72
+ homing_velocity_saved = self.tm.homing.velocity.magnitude
73
+ max_homing_t_saved = self.tm.homing.max_homing_t.magnitude
74
+ max_stall_t_saved = self.tm.homing.stall_detect.t.magnitude
75
+ retract_dist_saved = self.tm.homing.retract_dist.magnitude
76
+ stall_vel_saved = self.tm.homing.stall_detect.velocity.magnitude
77
+ stall_dpos_saved = self.tm.homing.stall_detect.delta_pos.magnitude
78
+ user_offset_saved = self.tm.sensors.user_frame.offset.magnitude
79
+ user_multiplier_saved = self.tm.sensors.user_frame.multiplier
80
+
81
+ iq_limit_stall = 0.1
82
+
83
+ try:
84
+ self.assertAlmostEqual(
85
+ float(user_multiplier_saved),
86
+ 1.0,
87
+ delta=1e-6,
88
+ msg=(
89
+ "Test math assumes user_frame.multiplier == 1.0 "
90
+ f"(actual: {user_multiplier_saved}). Set it to 1.0 "
91
+ "before running this test."
92
+ ),
93
+ )
94
+
95
+ self.tm.homing.max_homing_t = 6.0
96
+ self.tm.homing.stall_detect.t = 0.3
97
+ self.tm.homing.retract_dist = 1024
98
+ self.tm.homing.stall_detect.velocity = 500
99
+ self.tm.homing.stall_detect.delta_pos = 256
100
+
101
+ self.tm.controller.position_mode()
102
+ self.check_state(2)
103
+
104
+ for direction in (+1, -1):
105
+ for iteration in range(ITERATIONS_PER_DIRECTION):
106
+ self._run_homing_iteration(
107
+ direction=direction,
108
+ iteration=iteration,
109
+ iq_limit_normal=iq_limit_normal,
110
+ iq_limit_stall=iq_limit_stall,
111
+ )
112
+ time.sleep(INTER_ITERATION_PAUSE_S)
113
+ finally:
114
+ self.tm.controller.idle()
115
+ self.tm.controller.current.Iq_limit = iq_limit_normal
116
+ self.tm.homing.velocity = homing_velocity_saved
117
+ self.tm.homing.max_homing_t = max_homing_t_saved
118
+ self.tm.homing.stall_detect.t = max_stall_t_saved
119
+ self.tm.homing.retract_dist = retract_dist_saved
120
+ self.tm.homing.stall_detect.velocity = stall_vel_saved
121
+ self.tm.homing.stall_detect.delta_pos = stall_dpos_saved
122
+ self.tm.sensors.user_frame.offset = user_offset_saved
123
+ self.tm.sensors.user_frame.multiplier = user_multiplier_saved
124
+
125
+ def _run_homing_iteration(
126
+ self, direction, iteration, iq_limit_normal, iq_limit_stall
127
+ ):
128
+ """
129
+ Execute a single homing iteration in the given direction and verify
130
+ the resulting physical displacement in the sensor frame.
131
+
132
+ Returns nothing; raises AssertionError on failure.
133
+ """
134
+ retract_dist = self.tm.homing.retract_dist.magnitude
135
+ max_homing_t = self.tm.homing.max_homing_t.magnitude
136
+ stall_detect_t = self.tm.homing.stall_detect.t.magnitude
137
+
138
+ sensor_pos_before = (
139
+ self.tm.sensors.select.position_sensor.position_estimate.magnitude
140
+ )
141
+
142
+ self.tm.homing.velocity = direction * HOMING_SPEED
143
+ self.tm.controller.current.Iq_limit = iq_limit_normal
144
+ time.sleep(0.05)
145
+
146
+ self.tm.homing.home()
147
+
148
+ time.sleep(FREE_TRAVEL_S)
149
+
150
+ mode_after_free = int(self.tm.controller.mode)
151
+ self.assertEqual(
152
+ mode_after_free,
153
+ CONTROLLER_MODE_HOMING,
154
+ msg=(
155
+ f"dir={direction:+d} iter={iteration}: planner exited HOMING "
156
+ f"during free-travel phase (mode={mode_after_free}). "
157
+ f"FREE_TRAVEL_S ({FREE_TRAVEL_S}s) may be too long for the "
158
+ "current stall_detect parameters, or the motor stalled "
159
+ "without the simulated low-Iq endstop."
160
+ ),
161
+ )
162
+
163
+ self.tm.controller.current.Iq_limit = iq_limit_stall
164
+ time.sleep(stall_detect_t + STALL_DWELL_EXTRA_S)
165
+
166
+ self.tm.controller.current.Iq_limit = iq_limit_normal
167
+
168
+ timeout_s = max_homing_t + 2.0
169
+ deadline = time.monotonic() + timeout_s
170
+ while int(self.tm.controller.mode) == CONTROLLER_MODE_HOMING:
171
+ if time.monotonic() > deadline:
172
+ self.fail(
173
+ f"dir={direction:+d} iter={iteration}: homing did not "
174
+ f"exit within {timeout_s}s after stall induction"
175
+ )
176
+ time.sleep(0.05)
177
+
178
+ warnings = int(self.tm.homing.warnings)
179
+ self.assertEqual(
180
+ warnings,
181
+ HOMING_WARNINGS_NONE,
182
+ msg=(
183
+ f"dir={direction:+d} iter={iteration}: expected endstop "
184
+ f"detection (warnings=0) but got warnings={warnings}"
185
+ ),
186
+ )
187
+ self.assertEqual(
188
+ int(self.tm.controller.mode), CONTROLLER_MODE_POSITION
189
+ )
190
+
191
+ time.sleep(RETRACT_SETTLE_S)
192
+
193
+ sensor_pos_after = (
194
+ self.tm.sensors.select.position_sensor.position_estimate.magnitude
195
+ )
196
+ sensor_displacement = sensor_pos_after - sensor_pos_before
197
+ expected_displacement = direction * (
198
+ FREE_TRAVEL_S * HOMING_SPEED - retract_dist
199
+ )
200
+ displacement_tolerance = 0.5 * FREE_TRAVEL_S * HOMING_SPEED
201
+
202
+ self.assertAlmostEqual(
203
+ sensor_displacement,
204
+ expected_displacement,
205
+ delta=displacement_tolerance,
206
+ msg=(
207
+ f"dir={direction:+d} iter={iteration}: sensor-frame "
208
+ f"displacement is {sensor_displacement:.1f} ticks, expected "
209
+ f"{expected_displacement:.1f} +-{displacement_tolerance:.1f}. "
210
+ "A large deviation here on iteration>=1 indicates the "
211
+ "user-frame offset was installed in the wrong reference "
212
+ "frame during homing (regression of fix 86b19db). The "
213
+ "magnitude of the deviation should be approximately equal "
214
+ "to the offset carried over from the previous homing."
215
+ ),
216
+ )
217
+
218
+ user_pos = self.tm.sensors.user_frame.position_estimate.magnitude
219
+ expected_user_pos = -direction * retract_dist
220
+ user_pos_tolerance = 0.5 * retract_dist
221
+ self.assertAlmostEqual(
222
+ user_pos,
223
+ expected_user_pos,
224
+ delta=user_pos_tolerance,
225
+ msg=(
226
+ f"dir={direction:+d} iter={iteration}: user-frame position "
227
+ f"after homing is {user_pos:.1f} ticks, expected "
228
+ f"{expected_user_pos:.1f} +-{user_pos_tolerance:.1f}. This "
229
+ "is a self-consistency check on the planner's setpoint "
230
+ "tracking and does NOT detect the frame-offset bug; the "
231
+ "sensor-frame assertion above is the regression check."
232
+ ),
233
+ )
234
+
235
+
236
+ if __name__ == "__main__":
237
+ unittest.main(failfast=True)
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Tinymovr Board Config Tests
3
- Copyright Ioannis Chatzikonstantinou 2020-2023
3
+ Copyright 2020-2026 MotionLayer P.C.
4
4
 
5
5
  Tests saving and loading Tinymovr configuration to/from NVRAM.
6
6
 
@@ -112,6 +112,34 @@ class TestNVM(TMTestCase):
112
112
  self.erase_config()
113
113
  time.sleep(0.2)
114
114
 
115
+ def test_c2_homing_parameter_persistence(self):
116
+ """
117
+ Test persisting homing parameters across config saves.
118
+ WARNING: This will perform one NVRAM write and two erase cycles.
119
+ """
120
+ self.check_state(0)
121
+ self.erase_config()
122
+ time.sleep(0.2)
123
+
124
+ self.tm.homing.velocity = 5000
125
+ self.tm.homing.max_homing_t = 10.0
126
+ self.tm.homing.retract_dist = 2000
127
+ self.tm.homing.stall_detect.velocity = 3000
128
+ self.tm.homing.stall_detect.delta_pos = 500
129
+ self.tm.homing.stall_detect.t = 2.0
130
+ self.save_config()
131
+
132
+ time.sleep(0.2)
133
+ self.reset_and_wait()
134
+ self.assertAlmostEqual(self.tm.homing.velocity, 5000 * tick / s, delta=1 * tick / s)
135
+ self.assertAlmostEqual(self.tm.homing.max_homing_t, 10.0 * s, delta=0.01 * s)
136
+ self.assertAlmostEqual(self.tm.homing.retract_dist, 2000 * tick, delta=1 * tick)
137
+ self.assertAlmostEqual(self.tm.homing.stall_detect.velocity, 3000 * tick / s, delta=1 * tick / s)
138
+ self.assertAlmostEqual(self.tm.homing.stall_detect.delta_pos, 500 * tick, delta=1 * tick)
139
+ self.assertAlmostEqual(self.tm.homing.stall_detect.t, 2.0 * s, delta=0.01 * s)
140
+ self.erase_config()
141
+ time.sleep(0.2)
142
+
115
143
  def test_d_position_control_w_loaded_config(self):
116
144
  """
117
145
  Test position control after saving and loading config.
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Tinymovr Message Rate Tests
3
- Copyright Ioannis Chatzikonstantinou 2020-2023
3
+ Copyright 2020-2026 MotionLayer P.C.
4
4
 
5
5
  Tests the rate of CAN messages on the bus.
6
6
 
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Tinymovr Simulation Tests
3
- Copyright Ioannis Chatzikonstantinou 2020-2023
3
+ Copyright 2020-2026 MotionLayer P.C.
4
4
 
5
5
  Tests functionality of the Tinymovr Studio using a simulated Tinymovr device.
6
6
 
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Tinymovr Trajectory Planner Tests
3
- Copyright Ioannis Chatzikonstantinou 2020-2023
3
+ Copyright 2020-2026 MotionLayer P.C.
4
4
 
5
5
  Tests the functionality of the Tinymovr trajectory planner.
6
6
 
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Tinymovr BusManager Module
3
- Copyright Ioannis Chatzikonstantinou 2020-2023
3
+ Copyright 2020-2026 MotionLayer P.C.
4
4
 
5
5
  Implements a BusRouter class to distribute incoming traffic according to rules
6
6
 
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Tinymovr BusRouter Module
3
- Copyright Ioannis Chatzikonstantinou 2020-2023
3
+ Copyright 2020-2026 MotionLayer P.C.
4
4
 
5
5
  Implements a BusRouter class to distribute incoming traffic according to rules
6
6
 
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Tinymovr CAN Channel Module
3
- Copyright Ioannis Chatzikonstantinou 2020-2023
3
+ Copyright 2020-2026 MotionLayer P.C.
4
4
 
5
5
  Implements a CAN bus communications channel
6
6
 
@@ -26,7 +26,7 @@ from tinymovr.config import get_bus_config, configure_logging, add_spec
26
26
 
27
27
  """
28
28
  Tinymovr CLI Module
29
- Copyright Ioannis Chatzikonstantinou 2020-2023
29
+ Copyright 2020-2026 MotionLayer P.C.
30
30
 
31
31
  The Tinymovr Studio IPython-based command line interface
32
32
 
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Tinymovr Codecs Module
3
- Copyright Ioannis Chatzikonstantinou 2020-2023
3
+ Copyright 2020-2026 MotionLayer P.C.
4
4
 
5
5
  Implements codecs for various datatypes.
6
6
 
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Tinymovr Configuration Module
3
- Copyright Ioannis Chatzikonstantinou 2020-2023
3
+ Copyright 2020-2026 MotionLayer P.C.
4
4
 
5
5
  Implements functions to configure various aspects of Studio.
6
6
 
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Tinymovr Constants
3
- Copyright Ioannis Chatzikonstantinou 2020-2023
3
+ Copyright 2020-2026 MotionLayer P.C.
4
4
 
5
5
  Defines various constants used throughout Tinymovr Studio
6
6
 
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Tinymovr DeviceDiscovery Module
3
- Copyright Ioannis Chatzikonstantinou 2020-2023
3
+ Copyright 2020-2026 MotionLayer P.C.
4
4
 
5
5
  Implements a class to discover nodes on the CAN bus based on
6
6
  the reception of heartbeat frames
@@ -30,7 +30,7 @@ from tinymovr.channel import ResponseError
30
30
 
31
31
  """
32
32
  Tinymovr DFU Module
33
- Copyright Ioannis Chatzikonstantinou 2020-2023
33
+ Copyright 2020-2026 MotionLayer P.C.
34
34
 
35
35
  The Tinymovr Studio IPython-based command line interface
36
36