moteus 0.3.70__py3-none-any.whl → 0.3.72__py3-none-any.whl
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/calibrate_encoder.py +91 -9
- moteus/moteus.py +1 -1
- moteus/moteus_tool.py +107 -11
- moteus/version.py +1 -1
- {moteus-0.3.70.dist-info → moteus-0.3.72.dist-info}/METADATA +3 -1
- {moteus-0.3.70.dist-info → moteus-0.3.72.dist-info}/RECORD +9 -9
- {moteus-0.3.70.dist-info → moteus-0.3.72.dist-info}/WHEEL +0 -0
- {moteus-0.3.70.dist-info → moteus-0.3.72.dist-info}/entry_points.txt +0 -0
- {moteus-0.3.70.dist-info → moteus-0.3.72.dist-info}/top_level.txt +0 -0
moteus/calibrate_encoder.py
CHANGED
@@ -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
|
moteus/moteus.py
CHANGED
moteus/moteus_tool.py
CHANGED
@@ -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,15 +62,23 @@ 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}")
|
61
69
|
|
70
|
+
if old > SUPPORTED_ABI_VERSION:
|
71
|
+
raise RuntimeError(f"\nmoteus_tool needs to be upgraded to support this board\n\n (likely 'python -m pip install --upgrade moteus')\n\nThe board firmware is ABI version 0x{old:04x} but this moteus_tool only supports up to 0x{SUPPORTED_ABI_VERSION:04x}")
|
72
|
+
|
62
73
|
def fix_config(self, old_config):
|
63
74
|
lines = old_config.split(b'\n')
|
64
75
|
items = dict([line.split(b' ') for line in lines if b' ' in line])
|
65
76
|
|
77
|
+
if self.new <= 0x0107 and self.old >= 0x0108:
|
78
|
+
if float(items.get(b'servo.bemf_feedforward', '0')) == 0.0:
|
79
|
+
print("Reverting servo.bemf_feedforward to 1.0")
|
80
|
+
items[b'servo.bemf_feedforward'] = b'1.0'
|
81
|
+
|
66
82
|
if self.new <= 0x0106 and self.old >= 0x0107:
|
67
83
|
# motor_position.output.sign was broken in older versions.
|
68
84
|
if int(items[b'motor_position.output.sign']) != 1:
|
@@ -294,6 +310,40 @@ class FirmwareUpgrade:
|
|
294
310
|
# No actual configuration updating is required here.
|
295
311
|
pass
|
296
312
|
|
313
|
+
if self.new >= 0x0108 and self.old <= 0x0107:
|
314
|
+
if float(items.get(b'servo.bemf_feedforward', 1.0)) == 1.0:
|
315
|
+
print("Upgrading servo.bemf_feedforward to 0.0")
|
316
|
+
items[b'servo.bemf_feedforward'] = b'0.0'
|
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
|
+
|
297
347
|
lines = [key + b' ' + value for key, value in items.items()]
|
298
348
|
return b'\n'.join(lines)
|
299
349
|
|
@@ -989,6 +1039,10 @@ class Stream:
|
|
989
1039
|
raise RuntimeError(
|
990
1040
|
'hall effect calibration requires specifying --cal-motor-poles')
|
991
1041
|
|
1042
|
+
if (self.args.cal_motor_poles % 2) == 1:
|
1043
|
+
raise RuntimeError(
|
1044
|
+
'only motors with even numbers of poles are supported')
|
1045
|
+
|
992
1046
|
commutation_source = await self.read_config_int(
|
993
1047
|
"motor_position.commutation_source")
|
994
1048
|
aux_number = await self.read_config_int(
|
@@ -1049,21 +1103,26 @@ class Stream:
|
|
1049
1103
|
await self.command("d stop")
|
1050
1104
|
|
1051
1105
|
async def ensure_valid_theta(self, encoder_cal_voltage):
|
1052
|
-
|
1053
|
-
|
1054
|
-
|
1055
|
-
|
1056
|
-
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")
|
1057
1110
|
|
1111
|
+
try:
|
1058
1112
|
motor_position = await self.read_data("motor_position")
|
1059
|
-
if motor_position.homed == 0 and not motor_position.theta_valid:
|
1060
|
-
# We need to find an index.
|
1061
|
-
await self.find_index(encoder_cal_voltage)
|
1062
1113
|
except RuntimeError:
|
1063
1114
|
# Odds are we don't support motor_position, in which case
|
1064
1115
|
# theta is always valid for the older versions that don't
|
1065
1116
|
# support it.
|
1066
|
-
|
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)
|
1067
1126
|
|
1068
1127
|
async def calibrate_encoder_mapping_absolute(self, encoder_cal_voltage):
|
1069
1128
|
await self.ensure_valid_theta(encoder_cal_voltage)
|
@@ -1106,7 +1165,10 @@ class Stream:
|
|
1106
1165
|
cal_file,
|
1107
1166
|
desired_direction=1 if not self.args.cal_invert else -1,
|
1108
1167
|
max_remainder_error=self.args.cal_max_remainder,
|
1109
|
-
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
|
+
)
|
1110
1172
|
|
1111
1173
|
if cal_result.errors:
|
1112
1174
|
raise RuntimeError(f"Error(s) calibrating: {cal_result.errors}")
|
@@ -1347,9 +1409,19 @@ class Stream:
|
|
1347
1409
|
return 1 if x >= 0 else -1
|
1348
1410
|
|
1349
1411
|
velocity_samples = []
|
1412
|
+
power_samples = []
|
1350
1413
|
|
1351
1414
|
while True:
|
1352
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
|
+
|
1353
1425
|
velocity_samples.append(data.velocity)
|
1354
1426
|
|
1355
1427
|
if len(velocity_samples) > (3 * AVERAGE_COUNT):
|
@@ -1361,6 +1433,24 @@ class Stream:
|
|
1361
1433
|
if (time.time() - start_time) > 2.0:
|
1362
1434
|
return recent_average
|
1363
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
|
+
|
1364
1454
|
if (len(velocity_samples) >= AVERAGE_COUNT and
|
1365
1455
|
abs(recent_average) < 0.2):
|
1366
1456
|
return recent_average
|
@@ -1752,11 +1842,17 @@ async def async_main():
|
|
1752
1842
|
parser.add_argument('--cal-force-kv', metavar='Kv', type=float,
|
1753
1843
|
default=None,
|
1754
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')
|
1755
1849
|
|
1756
1850
|
|
1757
1851
|
parser.add_argument('--cal-max-remainder', metavar='F',
|
1758
1852
|
type=float, default=0.1,
|
1759
1853
|
help='maximum allowed error in calibration')
|
1854
|
+
parser.add_argument('--cal-max-kv-power-factor', type=float,
|
1855
|
+
default=1.0)
|
1760
1856
|
parser.add_argument('--cal-raw', metavar='FILE', type=str,
|
1761
1857
|
help='write raw calibration data')
|
1762
1858
|
|
moteus/version.py
CHANGED
@@ -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,7 +15,9 @@ 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
|
20
|
+
Requires-Dist: numpy <2
|
19
21
|
Requires-Dist: pywin32 ; platform_system == "Windows"
|
20
22
|
|
21
23
|
# Python bindings for moteus brushless controller #
|
@@ -1,12 +1,12 @@
|
|
1
1
|
moteus/__init__.py,sha256=DxasQre5HmK3BS3I35K8GEUvFZF3_OE_hjVzQy-seIE,739
|
2
2
|
moteus/aioserial.py,sha256=GeWuvsZKCRrfBN33JZFjtBXPr-0sKpQv9shRn2ulcDA,1079
|
3
3
|
moteus/aiostream.py,sha256=YAkVF6QWsA49vqO-GgXEohDghqm_-nnajJzhO_Q9qNQ,3696
|
4
|
-
moteus/calibrate_encoder.py,sha256=
|
4
|
+
moteus/calibrate_encoder.py,sha256=6ecNPY1CnCgoa7z9YmeIK471-TQLirNbz_B5erBGarU,15115
|
5
5
|
moteus/command.py,sha256=UkOsbtkso6Oyex8CfbpAKpBNriik519ymxL86EZGkRs,1169
|
6
6
|
moteus/export.py,sha256=vRIfldaqz1eHtWo3993SvatATtu73TZbejL0hzQe8YE,1646
|
7
7
|
moteus/fdcanusb.py,sha256=7PrQiCTROY96gdT2zSZYU1bOCriw-I7H6NspaZpiEx4,7431
|
8
|
-
moteus/moteus.py,sha256=
|
9
|
-
moteus/moteus_tool.py,sha256=
|
8
|
+
moteus/moteus.py,sha256=GFNGB4XedCOLJSr_ukhvIsGfY1y5p1y-7nUKyFfuOl8,48998
|
9
|
+
moteus/moteus_tool.py,sha256=ZdUisFMTy_d5OYqTltSRlung6Zf-5ezQjylgaGSYnjw,75146
|
10
10
|
moteus/multiplex.py,sha256=LF6MuelzYHqqsCJuCB9YeEyUA03eBaTYRwAVotX3qm8,10120
|
11
11
|
moteus/posix_aioserial.py,sha256=2oDrw8TBEwuEQjY41g9rHeuFeffcPHqMwNS3nf5NVq8,3137
|
12
12
|
moteus/pythoncan.py,sha256=ofotOrDuaFhTLvaokaO3EJK6quVc75Bq-ue70lDMtXI,4071
|
@@ -14,10 +14,10 @@ moteus/reader.py,sha256=9i1-h4aGd4syfqtWJcpg70Bl-bmunkGU4FmXmOLyRt8,12121
|
|
14
14
|
moteus/regression.py,sha256=M5gjDBYJQ64iBXIrvBhMkD8TYhtlnQ85x8U4py0niGA,1196
|
15
15
|
moteus/router.py,sha256=501W5GZ12rFoc1lmcH3S7IYsoc-Q_-FJ4B3i37RzE3Q,2061
|
16
16
|
moteus/transport.py,sha256=WhkW2G9i25lkOlO55eI5_oXmU0PhDmxTeJ75Sg_7nTI,1021
|
17
|
-
moteus/version.py,sha256=
|
17
|
+
moteus/version.py,sha256=QAXlfPdKUv7iVUDMlfA0IophEOfw4S5Pf7yO4Kiq5k4,627
|
18
18
|
moteus/win32_aioserial.py,sha256=culdl-vYxBKD5n2s5LkIMGyUaHyCcEc8BL5-DWEaxX8,2025
|
19
|
-
moteus-0.3.
|
20
|
-
moteus-0.3.
|
21
|
-
moteus-0.3.
|
22
|
-
moteus-0.3.
|
23
|
-
moteus-0.3.
|
19
|
+
moteus-0.3.72.dist-info/METADATA,sha256=ulIsuC8AzRG9A9h-prW5cKfBLihFDiR-K9xxBGGvHuw,3441
|
20
|
+
moteus-0.3.72.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
|
21
|
+
moteus-0.3.72.dist-info/entry_points.txt,sha256=accRcwir_K8wCf7i3qHb5R6CPh5SiSgd5a1A92ibb9E,56
|
22
|
+
moteus-0.3.72.dist-info/top_level.txt,sha256=aZzmI_yecTaDrdSp29pTJuowaSQ9dlIZheQpshGg4YQ,7
|
23
|
+
moteus-0.3.72.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|