moteus 0.3.71__tar.gz → 0.3.72__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.
- {moteus-0.3.71 → moteus-0.3.72}/PKG-INFO +3 -1
- {moteus-0.3.71 → moteus-0.3.72}/moteus/calibrate_encoder.py +91 -9
- {moteus-0.3.71 → moteus-0.3.72}/moteus/moteus.py +1 -1
- {moteus-0.3.71 → moteus-0.3.72}/moteus/moteus_tool.py +94 -11
- {moteus-0.3.71 → moteus-0.3.72}/moteus/version.py +1 -1
- {moteus-0.3.71 → moteus-0.3.72}/moteus.egg-info/PKG-INFO +3 -1
- {moteus-0.3.71 → moteus-0.3.72}/moteus.egg-info/requires.txt +2 -0
- {moteus-0.3.71 → moteus-0.3.72}/setup.py +3 -1
- {moteus-0.3.71 → moteus-0.3.72}/README.md +0 -0
- {moteus-0.3.71 → moteus-0.3.72}/moteus/__init__.py +0 -0
- {moteus-0.3.71 → moteus-0.3.72}/moteus/aioserial.py +0 -0
- {moteus-0.3.71 → moteus-0.3.72}/moteus/aiostream.py +0 -0
- {moteus-0.3.71 → moteus-0.3.72}/moteus/command.py +0 -0
- {moteus-0.3.71 → moteus-0.3.72}/moteus/export.py +0 -0
- {moteus-0.3.71 → moteus-0.3.72}/moteus/fdcanusb.py +0 -0
- {moteus-0.3.71 → moteus-0.3.72}/moteus/multiplex.py +0 -0
- {moteus-0.3.71 → moteus-0.3.72}/moteus/posix_aioserial.py +0 -0
- {moteus-0.3.71 → moteus-0.3.72}/moteus/pythoncan.py +0 -0
- {moteus-0.3.71 → moteus-0.3.72}/moteus/reader.py +0 -0
- {moteus-0.3.71 → moteus-0.3.72}/moteus/regression.py +0 -0
- {moteus-0.3.71 → moteus-0.3.72}/moteus/router.py +0 -0
- {moteus-0.3.71 → moteus-0.3.72}/moteus/transport.py +0 -0
- {moteus-0.3.71 → moteus-0.3.72}/moteus/win32_aioserial.py +0 -0
- {moteus-0.3.71 → moteus-0.3.72}/moteus.egg-info/SOURCES.txt +0 -0
- {moteus-0.3.71 → moteus-0.3.72}/moteus.egg-info/dependency_links.txt +0 -0
- {moteus-0.3.71 → moteus-0.3.72}/moteus.egg-info/entry_points.txt +0 -0
- {moteus-0.3.71 → moteus-0.3.72}/moteus.egg-info/top_level.txt +0 -0
- {moteus-0.3.71 → moteus-0.3.72}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: moteus
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.72
|
4
4
|
Summary: moteus brushless controller library and tools
|
5
5
|
Home-page: https://github.com/mjbots/moteus
|
6
6
|
Author: mjbots Robotic Systems
|
@@ -15,8 +15,10 @@ Description-Content-Type: text/markdown
|
|
15
15
|
Requires-Dist: pyserial>=3.5
|
16
16
|
Requires-Dist: python-can>=3.3
|
17
17
|
Requires-Dist: pyelftools>=0.26
|
18
|
+
Requires-Dist: scipy>=1.8.0
|
18
19
|
Requires-Dist: importlib_metadata>=3.6
|
19
20
|
Requires-Dist: pywin32; platform_system == "Windows"
|
21
|
+
Requires-Dist: numpy<2
|
20
22
|
|
21
23
|
# Python bindings for moteus brushless controller #
|
22
24
|
|
@@ -14,6 +14,9 @@
|
|
14
14
|
|
15
15
|
import json
|
16
16
|
import math
|
17
|
+
import scipy.optimize
|
18
|
+
|
19
|
+
BIN_COUNT = 64
|
17
20
|
|
18
21
|
class Entry:
|
19
22
|
direction = 0
|
@@ -159,8 +162,8 @@ def _window_average(values, window_size):
|
|
159
162
|
end = i + window_size // 2
|
160
163
|
errs = [0] * (end - start)
|
161
164
|
for j in range(start, end):
|
162
|
-
errs[j - start] =
|
163
|
-
result[i] =
|
165
|
+
errs[j - start] = values[wrap(j)]
|
166
|
+
result[i] = sum(errs) / len(errs)
|
164
167
|
|
165
168
|
return result
|
166
169
|
|
@@ -176,6 +179,8 @@ class CalibrationResult:
|
|
176
179
|
self.total_delta = None
|
177
180
|
self.ratio = None
|
178
181
|
|
182
|
+
self.fit_metric = None
|
183
|
+
|
179
184
|
self.errors = []
|
180
185
|
|
181
186
|
def __repr__(self):
|
@@ -184,6 +189,7 @@ class CalibrationResult:
|
|
184
189
|
"phase_invert": self.phase_invert,
|
185
190
|
"poles": self.poles,
|
186
191
|
"offset": self.offset,
|
192
|
+
"fit_metric": self.fit_metric,
|
187
193
|
"errors": self.errors,
|
188
194
|
})
|
189
195
|
|
@@ -193,13 +199,16 @@ class CalibrationResult:
|
|
193
199
|
'phase_invert': self.phase_invert,
|
194
200
|
'poles': self.poles,
|
195
201
|
'offset': self.offset,
|
202
|
+
'fit_metric': self.fit_metric,
|
196
203
|
}
|
197
204
|
|
198
205
|
|
199
206
|
def calibrate(parsed,
|
200
207
|
desired_direction=1,
|
201
208
|
max_remainder_error=0.1,
|
202
|
-
allow_phase_invert=True
|
209
|
+
allow_phase_invert=True,
|
210
|
+
allow_optimize=True,
|
211
|
+
force_optimize=False):
|
203
212
|
'''Calibrate the motor.
|
204
213
|
|
205
214
|
:param desired_direction: For positive unwrapped_position, should
|
@@ -315,19 +324,89 @@ def calibrate(parsed,
|
|
315
324
|
|
316
325
|
expected = [(2.0 * math.pi / 65536.0) * (result.poles / 2) * x for x in xpos]
|
317
326
|
|
318
|
-
err = [
|
327
|
+
err = [a - b for a, b in zip(avg_interp, expected)]
|
319
328
|
|
320
|
-
# Make the error
|
321
|
-
|
322
|
-
|
323
|
-
|
329
|
+
# Make the error balanced about 0.
|
330
|
+
mean_err = sum(err) / len(err)
|
331
|
+
wrapped_mean_err = _wrap_neg_pi_to_pi(mean_err)
|
332
|
+
delta_mean_err = mean_err - wrapped_mean_err
|
333
|
+
err = [x - delta_mean_err for x in err]
|
324
334
|
|
325
335
|
avg_window = int(len(err) / result.poles)
|
326
336
|
avg_err = _window_average(err, avg_window)
|
327
337
|
|
328
|
-
offset_x = list(range(0, 65536,
|
338
|
+
offset_x = list(range(0, 65536, 65536 // BIN_COUNT))
|
329
339
|
offset = _interpolate(offset_x, xpos, avg_err)
|
330
340
|
|
341
|
+
MAX_ERROR = 0.8
|
342
|
+
|
343
|
+
# The firmware will complain if individual steps are more
|
344
|
+
# than this apart.
|
345
|
+
MAX_STEP_CHANGE = 3.5
|
346
|
+
|
347
|
+
# Penalize solutions that have error greater than this.
|
348
|
+
PENALTY_RATIO = 0.80
|
349
|
+
|
350
|
+
def full_metric(x, *args):
|
351
|
+
errors = 0
|
352
|
+
result = 0
|
353
|
+
resampled = _interpolate(xpos, offset_x + [65536],
|
354
|
+
list(x) + [x[0]])
|
355
|
+
for a, b in zip(err, resampled):
|
356
|
+
this_err = abs(_wrap_neg_pi_to_pi(a - b))
|
357
|
+
# Heavily penalize if we would have an error.
|
358
|
+
if this_err > PENALTY_RATIO * MAX_ERROR:
|
359
|
+
this_err = (PENALTY_RATIO * MAX_ERROR +
|
360
|
+
(this_err - PENALTY_RATIO * MAX_ERROR) * 10)
|
361
|
+
if this_err > MAX_ERROR:
|
362
|
+
errors += 1
|
363
|
+
result += this_err ** 2
|
364
|
+
|
365
|
+
for i in range(0, len(list(x))):
|
366
|
+
nexti = (i + 1) % len(list(x))
|
367
|
+
delta = abs(x[nexti] - x[i])
|
368
|
+
if delta > (PENALTY_RATIO * MAX_STEP_CHANGE):
|
369
|
+
result += (
|
370
|
+
10 * (delta - PENALTY_RATIO * MAX_STEP_CHANGE) ** 2)
|
371
|
+
return result, errors
|
372
|
+
|
373
|
+
def metric(x, *args):
|
374
|
+
result = full_metric(x, *args)[0]
|
375
|
+
print(f"optimizing - metric={result:.5f} ", end='\r', flush=True)
|
376
|
+
return result
|
377
|
+
|
378
|
+
starting_metric, starting_errors = full_metric(offset)
|
379
|
+
|
380
|
+
if (force_optimize or (
|
381
|
+
allow_optimize and
|
382
|
+
(starting_metric > 30 or
|
383
|
+
starting_errors > 0))):
|
384
|
+
print()
|
385
|
+
# Optimize these initial offsets.
|
386
|
+
optimres = scipy.optimize.minimize(metric, offset, tol=1e1)
|
387
|
+
|
388
|
+
if not optimres.success:
|
389
|
+
result.errors.append(
|
390
|
+
f"optimization failed {result.message}")
|
391
|
+
|
392
|
+
print()
|
393
|
+
offset = list(optimres.x)
|
394
|
+
|
395
|
+
result.fit_metric, _ = full_metric(offset)
|
396
|
+
|
397
|
+
# Now double check our results.
|
398
|
+
resampled_offset = _interpolate(xpos, offset_x + [65536],
|
399
|
+
offset + [offset[0]])
|
400
|
+
any_sample_error = False
|
401
|
+
sample_errors = []
|
402
|
+
for a, b in zip(err, resampled_offset):
|
403
|
+
sample_error = _wrap_neg_pi_to_pi(a - b)
|
404
|
+
sample_errors.append(sample_error)
|
405
|
+
if not any_sample_error and abs(sample_error) > MAX_ERROR:
|
406
|
+
result.errors.append(
|
407
|
+
f"excessive error in curve fit |{sample_error}| > {MAX_ERROR}")
|
408
|
+
any_sample_error = True
|
409
|
+
|
331
410
|
result.offset = offset
|
332
411
|
|
333
412
|
result.debug = {
|
@@ -340,6 +419,9 @@ def calibrate(parsed,
|
|
340
419
|
'avg_interp' : avg_interp,
|
341
420
|
'err' : err,
|
342
421
|
'avg_err' : avg_err,
|
422
|
+
'offset_x' : offset_x,
|
423
|
+
'offset' : offset,
|
424
|
+
'sample_errors' : sample_errors,
|
343
425
|
}
|
344
426
|
|
345
427
|
return result
|
@@ -44,6 +44,14 @@ MAX_FLASH_BLOCK_SIZE = 32
|
|
44
44
|
FIND_TARGET_TIMEOUT = 0.01 if sys.platform != 'win32' else 0.05
|
45
45
|
|
46
46
|
|
47
|
+
def _wrap_neg_pi_to_pi(value):
|
48
|
+
while value > math.pi:
|
49
|
+
value -= 2.0 * math.pi
|
50
|
+
while value < -math.pi:
|
51
|
+
value += 2.0 * math.pi
|
52
|
+
return value
|
53
|
+
|
54
|
+
|
47
55
|
class FirmwareUpgrade:
|
48
56
|
'''This encodes "magic" rules about upgrading firmware, largely about
|
49
57
|
how to munge configuration options so as to not cause behavior
|
@@ -54,7 +62,7 @@ class FirmwareUpgrade:
|
|
54
62
|
self.old = old
|
55
63
|
self.new = new
|
56
64
|
|
57
|
-
SUPPORTED_ABI_VERSION =
|
65
|
+
SUPPORTED_ABI_VERSION = 0x0109
|
58
66
|
|
59
67
|
if new > SUPPORTED_ABI_VERSION:
|
60
68
|
raise RuntimeError(f"\nmoteus_tool needs to be upgraded to support this firmware\n\n (likely 'python -m pip install --upgrade moteus')\n\nThe provided firmare is ABI version 0x{new:04x} but this moteus_tool only supports up to 0x{SUPPORTED_ABI_VERSION:04x}")
|
@@ -307,6 +315,35 @@ class FirmwareUpgrade:
|
|
307
315
|
print("Upgrading servo.bemf_feedforward to 0.0")
|
308
316
|
items[b'servo.bemf_feedforward'] = b'0.0'
|
309
317
|
|
318
|
+
if self.new >= 0x0109 and self.old <= 0x0108:
|
319
|
+
# Try to fix up the motor commutation offset tables.
|
320
|
+
old_offsets = []
|
321
|
+
i = 0
|
322
|
+
while True:
|
323
|
+
key = f'motor.offset.{i}'.encode('utf8')
|
324
|
+
if key not in items:
|
325
|
+
break
|
326
|
+
old_offsets.append(float(items.get(key)))
|
327
|
+
i += 1
|
328
|
+
|
329
|
+
offsets = old_offsets[:]
|
330
|
+
|
331
|
+
# Unwrap this, then re-center the whole thing around 0.
|
332
|
+
for i in range(1, len(offsets)):
|
333
|
+
offsets[i] = (offsets[i - 1] +
|
334
|
+
_wrap_neg_pi_to_pi(offsets[i] -
|
335
|
+
offsets[i - 1]))
|
336
|
+
mean_offset = sum(offsets) / len(offsets)
|
337
|
+
delta = mean_offset - _wrap_neg_pi_to_pi(mean_offset)
|
338
|
+
offsets = [x - delta for x in offsets]
|
339
|
+
|
340
|
+
if any([abs(a - b) > 0.01
|
341
|
+
for a, b in zip(offsets, old_offsets)]):
|
342
|
+
print("Re-wrapping motor commutation offsets")
|
343
|
+
for i in range(len(offsets)):
|
344
|
+
key = f'motor.offset.{i}'.encode('utf8')
|
345
|
+
items[key] = f'{offsets[i]}'.encode('utf8')
|
346
|
+
|
310
347
|
lines = [key + b' ' + value for key, value in items.items()]
|
311
348
|
return b'\n'.join(lines)
|
312
349
|
|
@@ -1002,6 +1039,10 @@ class Stream:
|
|
1002
1039
|
raise RuntimeError(
|
1003
1040
|
'hall effect calibration requires specifying --cal-motor-poles')
|
1004
1041
|
|
1042
|
+
if (self.args.cal_motor_poles % 2) == 1:
|
1043
|
+
raise RuntimeError(
|
1044
|
+
'only motors with even numbers of poles are supported')
|
1045
|
+
|
1005
1046
|
commutation_source = await self.read_config_int(
|
1006
1047
|
"motor_position.commutation_source")
|
1007
1048
|
aux_number = await self.read_config_int(
|
@@ -1062,21 +1103,26 @@ class Stream:
|
|
1062
1103
|
await self.command("d stop")
|
1063
1104
|
|
1064
1105
|
async def ensure_valid_theta(self, encoder_cal_voltage):
|
1065
|
-
|
1066
|
-
|
1067
|
-
|
1068
|
-
|
1069
|
-
await self.command(f"conf set motor.poles 2")
|
1106
|
+
# We might need to have some sense of pole count first.
|
1107
|
+
if await self.read_config_double("motor.poles") == 0:
|
1108
|
+
# Pick something arbitrary for now.
|
1109
|
+
await self.command(f"conf set motor.poles 2")
|
1070
1110
|
|
1111
|
+
try:
|
1071
1112
|
motor_position = await self.read_data("motor_position")
|
1072
|
-
if motor_position.homed == 0 and not motor_position.theta_valid:
|
1073
|
-
# We need to find an index.
|
1074
|
-
await self.find_index(encoder_cal_voltage)
|
1075
1113
|
except RuntimeError:
|
1076
1114
|
# Odds are we don't support motor_position, in which case
|
1077
1115
|
# theta is always valid for the older versions that don't
|
1078
1116
|
# support it.
|
1079
|
-
|
1117
|
+
return
|
1118
|
+
|
1119
|
+
if motor_position.error != 0:
|
1120
|
+
raise RuntimeError(
|
1121
|
+
f"encoder error: {repr(motor_position.error)}")
|
1122
|
+
|
1123
|
+
if motor_position.homed == 0 and not motor_position.theta_valid:
|
1124
|
+
# We need to find an index.
|
1125
|
+
await self.find_index(encoder_cal_voltage)
|
1080
1126
|
|
1081
1127
|
async def calibrate_encoder_mapping_absolute(self, encoder_cal_voltage):
|
1082
1128
|
await self.ensure_valid_theta(encoder_cal_voltage)
|
@@ -1119,7 +1165,10 @@ class Stream:
|
|
1119
1165
|
cal_file,
|
1120
1166
|
desired_direction=1 if not self.args.cal_invert else -1,
|
1121
1167
|
max_remainder_error=self.args.cal_max_remainder,
|
1122
|
-
allow_phase_invert=allow_phase_invert
|
1168
|
+
allow_phase_invert=allow_phase_invert,
|
1169
|
+
allow_optimize=not self.args.cal_disable_optimize,
|
1170
|
+
force_optimize=self.args.cal_force_optimize,
|
1171
|
+
)
|
1123
1172
|
|
1124
1173
|
if cal_result.errors:
|
1125
1174
|
raise RuntimeError(f"Error(s) calibrating: {cal_result.errors}")
|
@@ -1360,9 +1409,19 @@ class Stream:
|
|
1360
1409
|
return 1 if x >= 0 else -1
|
1361
1410
|
|
1362
1411
|
velocity_samples = []
|
1412
|
+
power_samples = []
|
1363
1413
|
|
1364
1414
|
while True:
|
1365
1415
|
data = await self.read_servo_stats()
|
1416
|
+
|
1417
|
+
total_current_A = math.hypot(data.d_A, data.q_A)
|
1418
|
+
total_power_W = voltage * total_current_A
|
1419
|
+
|
1420
|
+
power_samples.append(total_power_W)
|
1421
|
+
|
1422
|
+
if len(power_samples) > AVERAGE_COUNT:
|
1423
|
+
del power_samples[0]
|
1424
|
+
|
1366
1425
|
velocity_samples.append(data.velocity)
|
1367
1426
|
|
1368
1427
|
if len(velocity_samples) > (3 * AVERAGE_COUNT):
|
@@ -1374,6 +1433,24 @@ class Stream:
|
|
1374
1433
|
if (time.time() - start_time) > 2.0:
|
1375
1434
|
return recent_average
|
1376
1435
|
|
1436
|
+
if len(power_samples) >= AVERAGE_COUNT:
|
1437
|
+
average_power_W = sum(power_samples) / len(power_samples)
|
1438
|
+
max_power_W = (self.args.cal_max_kv_power_factor *
|
1439
|
+
self.args.cal_motor_power)
|
1440
|
+
|
1441
|
+
# This is a safety. During speed measurement, current
|
1442
|
+
# should always be near 0. However, if the encoder
|
1443
|
+
# commutation calibration failed, we can sometimes
|
1444
|
+
# trigger large currents during the Kv detection phase
|
1445
|
+
# while not actually moving.
|
1446
|
+
if (abs(recent_average) < 0.2 and
|
1447
|
+
average_power_W > max_power_W):
|
1448
|
+
await self.command("d stop")
|
1449
|
+
|
1450
|
+
raise RuntimeError(
|
1451
|
+
f"Motor failed to spin, {average_power_W} > " +
|
1452
|
+
f"{max_power_W}")
|
1453
|
+
|
1377
1454
|
if (len(velocity_samples) >= AVERAGE_COUNT and
|
1378
1455
|
abs(recent_average) < 0.2):
|
1379
1456
|
return recent_average
|
@@ -1765,11 +1842,17 @@ async def async_main():
|
|
1765
1842
|
parser.add_argument('--cal-force-kv', metavar='Kv', type=float,
|
1766
1843
|
default=None,
|
1767
1844
|
help='do not calibrate Kv, but use the specified value')
|
1845
|
+
parser.add_argument('--cal-force-optimize', action='store_true',
|
1846
|
+
help='require nonlinear commutation optimization')
|
1847
|
+
parser.add_argument('--cal-disable-optimize', action='store_true',
|
1848
|
+
help='prevent nonlinear commutation optimization')
|
1768
1849
|
|
1769
1850
|
|
1770
1851
|
parser.add_argument('--cal-max-remainder', metavar='F',
|
1771
1852
|
type=float, default=0.1,
|
1772
1853
|
help='maximum allowed error in calibration')
|
1854
|
+
parser.add_argument('--cal-max-kv-power-factor', type=float,
|
1855
|
+
default=1.0)
|
1773
1856
|
parser.add_argument('--cal-raw', metavar='FILE', type=str,
|
1774
1857
|
help='write raw calibration data')
|
1775
1858
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: moteus
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.72
|
4
4
|
Summary: moteus brushless controller library and tools
|
5
5
|
Home-page: https://github.com/mjbots/moteus
|
6
6
|
Author: mjbots Robotic Systems
|
@@ -15,8 +15,10 @@ Description-Content-Type: text/markdown
|
|
15
15
|
Requires-Dist: pyserial>=3.5
|
16
16
|
Requires-Dist: python-can>=3.3
|
17
17
|
Requires-Dist: pyelftools>=0.26
|
18
|
+
Requires-Dist: scipy>=1.8.0
|
18
19
|
Requires-Dist: importlib_metadata>=3.6
|
19
20
|
Requires-Dist: pywin32; platform_system == "Windows"
|
21
|
+
Requires-Dist: numpy<2
|
20
22
|
|
21
23
|
# Python bindings for moteus brushless controller #
|
22
24
|
|
@@ -25,7 +25,7 @@ long_description = (here / 'README.md').read_text(encoding='utf-8')
|
|
25
25
|
|
26
26
|
setuptools.setup(
|
27
27
|
name = 'moteus',
|
28
|
-
version = "0.3.
|
28
|
+
version = "0.3.72",
|
29
29
|
description = 'moteus brushless controller library and tools',
|
30
30
|
long_description = long_description,
|
31
31
|
long_description_content_type = 'text/markdown',
|
@@ -50,7 +50,9 @@ setuptools.setup(
|
|
50
50
|
'pyserial>=3.5',
|
51
51
|
'python-can>=3.3',
|
52
52
|
'pyelftools>=0.26',
|
53
|
+
'scipy>=1.8.0',
|
53
54
|
'importlib_metadata>=3.6',
|
54
55
|
'pywin32;platform_system=="Windows"',
|
56
|
+
'numpy<2',
|
55
57
|
],
|
56
58
|
)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|