moteus 0.3.72__py3-none-any.whl → 0.3.73__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.
@@ -382,6 +382,7 @@ def calibrate(parsed,
382
382
  (starting_metric > 30 or
383
383
  starting_errors > 0))):
384
384
  print()
385
+ print(f"Initial metric: {starting_metric}")
385
386
  # Optimize these initial offsets.
386
387
  optimres = scipy.optimize.minimize(metric, offset, tol=1e1)
387
388
 
moteus/moteus.py CHANGED
@@ -252,6 +252,17 @@ class Register(enum.IntEnum):
252
252
  MILLISECOND_COUNTER = 0x070
253
253
  CLOCK_TRIM = 0x071
254
254
 
255
+ AUX1_PWM1 = 0x076,
256
+ AUX1_PWM2 = 0x077,
257
+ AUX1_PWM3 = 0x078,
258
+ AUX1_PWM4 = 0x079,
259
+ AUX1_PWM5 = 0x07a,
260
+ AUX2_PWM1 = 0x07b,
261
+ AUX2_PWM2 = 0x07c,
262
+ AUX2_PWM3 = 0x07d,
263
+ AUX2_PWM4 = 0x07e,
264
+ AUX2_PWM5 = 0x07f,
265
+
255
266
  REGISTER_MAP_VERSION = 0x102
256
267
  SERIAL_NUMBER = 0x120
257
268
  SERIAL_NUMBER1 = 0x120
@@ -267,6 +278,16 @@ class Register(enum.IntEnum):
267
278
  DRIVER_FAULT1 = 0x140
268
279
  DRIVER_FAULT2 = 0x141
269
280
 
281
+ UUID1 = 0x150
282
+ UUID2 = 0x151
283
+ UUID3 = 0x152
284
+ UUID4 = 0x153
285
+
286
+ UUID_MASK1 = 0x0154
287
+ UUID_MASK2 = 0x0155
288
+ UUID_MASK3 = 0x0156
289
+ UUID_MASK4 = 0x0157
290
+
270
291
 
271
292
  class Mode(enum.IntEnum):
272
293
  """Valid values for the Register.MODE register"""
@@ -347,6 +368,19 @@ class CurrentResolution:
347
368
  q_A = mp.F32
348
369
 
349
370
 
371
+ class PwmResolution:
372
+ aux1_pwm1 = mp.INT16
373
+ aux1_pwm2 = mp.INT16
374
+ aux1_pwm3 = mp.INT16
375
+ aux1_pwm4 = mp.INT16
376
+ aux1_pwm5 = mp.INT16
377
+ aux2_pwm1 = mp.INT16
378
+ aux2_pwm2 = mp.INT16
379
+ aux2_pwm3 = mp.INT16
380
+ aux2_pwm4 = mp.INT16
381
+ aux2_pwm5 = mp.INT16
382
+
383
+
350
384
  class Parser(mp.RegisterParser):
351
385
  def read_position(self, resolution):
352
386
  return self.read_mapped(resolution, 0.01, 0.0001, 0.00001)
@@ -507,6 +541,9 @@ def parse_register(parser, register, resolution):
507
541
  return parser.read_int(resolution)
508
542
  elif register == Register.CLOCK_TRIM:
509
543
  return parser.read_int(resolution)
544
+ elif (register >= Register.AUX1_PWM1 and
545
+ register <= Register.AUX2_PWM5):
546
+ return parser.read_pwm(resolution)
510
547
  else:
511
548
  # We don't know what kind of value this is, so we don't know
512
549
  # the units.
@@ -609,6 +646,7 @@ class Controller:
609
646
  position_resolution=PositionResolution(),
610
647
  vfoc_resolution=VFOCResolution(),
611
648
  current_resolution=CurrentResolution(),
649
+ pwm_resolution=PwmResolution(),
612
650
  transport=None,
613
651
  can_prefix=0x0000):
614
652
  self.id = id
@@ -616,6 +654,7 @@ class Controller:
616
654
  self.position_resolution = position_resolution
617
655
  self.vfoc_resolution = vfoc_resolution
618
656
  self.current_resolution = current_resolution
657
+ self.pwm_resolution = pwm_resolution
619
658
  self.transport = transport
620
659
  self._parser = make_parser(id)
621
660
  self._can_prefix = can_prefix
@@ -1315,6 +1354,70 @@ class Controller:
1315
1354
  async def set_trim(self, *args, **kwargs):
1316
1355
  return await self.execute(self.make_set_trim(*args, **kwargs))
1317
1356
 
1357
+ def make_aux_pwm(self, *,
1358
+ aux1_pwm1=None,
1359
+ aux1_pwm2=None,
1360
+ aux1_pwm3=None,
1361
+ aux1_pwm4=None,
1362
+ aux1_pwm5=None,
1363
+ aux2_pwm1=None,
1364
+ aux2_pwm2=None,
1365
+ aux2_pwm3=None,
1366
+ aux2_pwm4=None,
1367
+ aux2_pwm5=None,
1368
+ query=False,
1369
+ query_override=None):
1370
+ result = self._make_command(query=query, query_override=query_override)
1371
+
1372
+ pr = self.pwm_resolution
1373
+ resolutions = [
1374
+ pr.aux1_pwm1 if aux1_pwm1 is not None else mp.IGNORE,
1375
+ pr.aux1_pwm2 if aux1_pwm2 is not None else mp.IGNORE,
1376
+ pr.aux1_pwm3 if aux1_pwm3 is not None else mp.IGNORE,
1377
+ pr.aux1_pwm4 if aux1_pwm4 is not None else mp.IGNORE,
1378
+ pr.aux1_pwm5 if aux1_pwm5 is not None else mp.IGNORE,
1379
+ pr.aux2_pwm1 if aux2_pwm1 is not None else mp.IGNORE,
1380
+ pr.aux2_pwm2 if aux2_pwm2 is not None else mp.IGNORE,
1381
+ pr.aux2_pwm3 if aux2_pwm3 is not None else mp.IGNORE,
1382
+ pr.aux2_pwm4 if aux2_pwm4 is not None else mp.IGNORE,
1383
+ pr.aux2_pwm5 if aux2_pwm5 is not None else mp.IGNORE,
1384
+ ]
1385
+
1386
+ data_buf = io.BytesIO()
1387
+ writer = Writer(data_buf)
1388
+ combiner = mp.WriteCombiner(
1389
+ writer, 0x00, int(Register.AUX1_PWM1), resolutions)
1390
+
1391
+ if combiner.maybe_write():
1392
+ writer.write_pwm(aux1_pwm1, pr.aux1_pwm1)
1393
+ if combiner.maybe_write():
1394
+ writer.write_pwm(aux1_pwm2, pr.aux1_pwm2)
1395
+ if combiner.maybe_write():
1396
+ writer.write_pwm(aux1_pwm3, pr.aux1_pwm3)
1397
+ if combiner.maybe_write():
1398
+ writer.write_pwm(aux1_pwm4, pr.aux1_pwm4)
1399
+ if combiner.maybe_write():
1400
+ writer.write_pwm(aux1_pwm5, pr.aux1_pwm5)
1401
+ if combiner.maybe_write():
1402
+ writer.write_pwm(aux2_pwm1, pr.aux2_pwm1)
1403
+ if combiner.maybe_write():
1404
+ writer.write_pwm(aux2_pwm2, pr.aux2_pwm2)
1405
+ if combiner.maybe_write():
1406
+ writer.write_pwm(aux2_pwm3, pr.aux2_pwm3)
1407
+ if combiner.maybe_write():
1408
+ writer.write_pwm(aux2_pwm4, pr.aux2_pwm4)
1409
+ if combiner.maybe_write():
1410
+ writer.write_pwm(aux2_pwm5, pr.aux2_pwm5)
1411
+
1412
+ self._format_query(query, query_override, data_buf, result)
1413
+
1414
+ result.data = data_buf.getvalue()
1415
+
1416
+ return result
1417
+
1418
+ async def set_aux_pwm(self, *args, **kwargs):
1419
+ return await self.execute(self.make_aux_pwm(*args, **kwargs))
1420
+
1318
1421
  def _extract(self, value):
1319
1422
  if len(value):
1320
1423
  return value[0]
moteus/moteus_tool.py CHANGED
@@ -52,6 +52,18 @@ def _wrap_neg_pi_to_pi(value):
52
52
  return value
53
53
 
54
54
 
55
+ def lerp(array, ratio):
56
+ array_size = len(array)
57
+
58
+ left_index = int(math.floor(ratio * array_size))
59
+ right_index = (left_index + 1) % array_size
60
+ fraction = (ratio - left_index / array_size) * array_size
61
+ left_comp = array[left_index]
62
+ right_comp = array[right_index]
63
+
64
+ return (left_comp * (1.0 - fraction)) + (right_comp * fraction)
65
+
66
+
55
67
  class FirmwareUpgrade:
56
68
  '''This encodes "magic" rules about upgrading firmware, largely about
57
69
  how to munge configuration options so as to not cause behavior
@@ -74,6 +86,42 @@ class FirmwareUpgrade:
74
86
  lines = old_config.split(b'\n')
75
87
  items = dict([line.split(b' ') for line in lines if b' ' in line])
76
88
 
89
+ if self.new <= 0x0108 and self.old >= 0x0109:
90
+ # When downgrading, we should warn if a motor thermistor
91
+ # value other than 47k is configured and enabled.
92
+ if (int(items.get(b'servo.enable_motor_temperature', '0')) != 0 and
93
+ int(float(items.get(b'servo.motor_thermistor_ohm', b'0.0'))) != 47000):
94
+ print("Motor thermistor values other than 47000 ohm not supported in firmware <= 0x0108, disabling")
95
+ items[b'servo.enable_motor_temperature'] = b'0'
96
+
97
+ items.pop(b'servo.motor_thermistor_ohm')
98
+
99
+ # Aux PWM out is not supported on <= 0x0108.
100
+ items.pop(b'aux1.pwm_period_us')
101
+ items.pop(b'aux2.pwm_period_us')
102
+
103
+ def make_key(mpsource, index):
104
+ return f'motor_position.sources.{mpsource}.compensation_table.{index}'.encode('utf8')
105
+
106
+ if make_key(0, 0) in items:
107
+ # Downsample encoder compensation bins.
108
+ print("Downsampling encoder compensation tables from version >= 0x0109")
109
+ for mpsource in range(0, 3):
110
+ bins = []
111
+
112
+ scale = float(items.pop(f'motor_position.sources.{mpsource}.compensation_scale'.encode('utf8'), 0.0)) / 127.0
113
+ new_size = 256
114
+ old_size = 32
115
+ ratio = new_size // old_size
116
+
117
+ for i in range(0, new_size):
118
+ key = make_key(mpsource, i)
119
+ bins.append(float(items.get(key)) * scale)
120
+ del items[key]
121
+
122
+ for i in range(0, old_size):
123
+ items[make_key(mpsource, i)] = str(bins[i*ratio]).encode('utf8')
124
+
77
125
  if self.new <= 0x0107 and self.old >= 0x0108:
78
126
  if float(items.get(b'servo.bemf_feedforward', '0')) == 0.0:
79
127
  print("Reverting servo.bemf_feedforward to 1.0")
@@ -83,7 +131,7 @@ class FirmwareUpgrade:
83
131
  # motor_position.output.sign was broken in older versions.
84
132
  if int(items[b'motor_position.output.sign']) != 1:
85
133
  print("WARNING: motor_position.output.sign==-1 is broken in order versions, disabling")
86
- items[b'motor_position.output.sign'] = '1'
134
+ items[b'motor_position.output.sign'] = b'1'
87
135
  pass
88
136
 
89
137
  if self.new <= 0x0105 and self.old >= 0x0106:
@@ -344,6 +392,38 @@ class FirmwareUpgrade:
344
392
  key = f'motor.offset.{i}'.encode('utf8')
345
393
  items[key] = f'{offsets[i]}'.encode('utf8')
346
394
 
395
+ if self.new >= 0x0109 and self.old <= 0x0108:
396
+ # If we had a motor thermistor enabled in previous
397
+ # versions, it was with a value of 47000.
398
+ if int(items.get(b'servo.enable_motor_temperature', b'0')) != 0:
399
+ print("Thermistor from <= 0x0109 assumed to be 47000")
400
+ items[b'servo.motor_thermistor_ohm'] = b'47000'
401
+
402
+ def make_key(mpsource, index):
403
+ return f'motor_position.sources.{mpsource}.compensation_table.{index}'.encode('utf8')
404
+
405
+ new_size = 256
406
+ old_size = 32
407
+ ratio = new_size // old_size
408
+
409
+ if make_key(0, 0) in items:
410
+ print("Upsampling encoder compensation tables for version >= 0x0109")
411
+ for mpsource in range(0, 3):
412
+ old_bins = [float(items.get(make_key(mpsource, i)).decode('latin1')) for i in range(0, 32)]
413
+ scale = max([abs(x) for x in old_bins])
414
+ bins = []
415
+ for i in range(new_size):
416
+ if scale != 0.0:
417
+ old_i = i // ratio
418
+ value = lerp(old_bins, i / new_size)
419
+ int_value = int(127 * value / scale)
420
+ else:
421
+ int_value = 0
422
+ items[make_key(mpsource, i)] = str(int_value).encode('utf8')
423
+
424
+ items[f'motor_position.sources.{mpsource}.compensation_scale'.encode('utf8')] = str(scale).encode('utf8')
425
+
426
+
347
427
  lines = [key + b' ' + value for key, value in items.items()]
348
428
  return b'\n'.join(lines)
349
429
 
@@ -688,7 +768,13 @@ class Stream:
688
768
  async def write_config_stream(self, fp):
689
769
  errors = []
690
770
 
691
- for line in fp.readlines():
771
+ config_lines = fp.readlines()
772
+
773
+ for i, line in enumerate(config_lines):
774
+ if i % 20 == 0:
775
+ print(f"Writing config {100*i/len(config_lines):.0f}% ",
776
+ end='\r', flush=True)
777
+
692
778
  line = line.rstrip()
693
779
  if len(line) == 0:
694
780
  continue
@@ -698,6 +784,8 @@ class Stream:
698
784
  except moteus.CommandError as ce:
699
785
  errors.append(line.decode('latin1'))
700
786
 
787
+ print()
788
+
701
789
  if len(errors):
702
790
  print("\nSome config could not be set:")
703
791
  for line in errors:
@@ -861,6 +949,19 @@ class Stream:
861
949
 
862
950
  return cal_voltage
863
951
 
952
+ async def clear_motor_offsets(self):
953
+ i = 0
954
+ while True:
955
+ try:
956
+ await self.command(f"conf set motor.offset.{i} 0")
957
+ except moteus.CommandError as ce:
958
+ if 'error setting' in ce.message:
959
+ # This means we hit the end of the offsets.
960
+ break
961
+ else:
962
+ raise
963
+ i += 1
964
+
864
965
  async def do_calibrate(self):
865
966
  self.firmware = await self.read_data("firmware")
866
967
 
@@ -870,6 +971,11 @@ class Stream:
870
971
  print("This will move the motor, ensure it can spin freely!")
871
972
  await asyncio.sleep(2.0)
872
973
 
974
+ # Force all existing offsets to 0, that way if we had a
975
+ # discontinuous offset error, sending the stop will be able to
976
+ # clear it (and we are about to overwrite them anyway).
977
+ await self.clear_motor_offsets()
978
+
873
979
  # Clear any faults that may be there.
874
980
  await self.command("d stop")
875
981
 
@@ -1852,7 +1958,7 @@ async def async_main():
1852
1958
  type=float, default=0.1,
1853
1959
  help='maximum allowed error in calibration')
1854
1960
  parser.add_argument('--cal-max-kv-power-factor', type=float,
1855
- default=1.0)
1961
+ default=1.25)
1856
1962
  parser.add_argument('--cal-raw', metavar='FILE', type=str,
1857
1963
  help='write raw calibration data')
1858
1964
 
moteus/version.py CHANGED
@@ -12,4 +12,4 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- VERSION="0.3.72"
15
+ VERSION="0.3.73"
@@ -1,23 +1,25 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: moteus
3
- Version: 0.3.72
3
+ Version: 0.3.73
4
4
  Summary: moteus brushless controller library and tools
5
5
  Home-page: https://github.com/mjbots/moteus
6
6
  Author: mjbots Robotic Systems
7
7
  Author-email: info@mjbots.com
8
+ License: UNKNOWN
8
9
  Keywords: moteus
10
+ Platform: UNKNOWN
9
11
  Classifier: Development Status :: 3 - Alpha
10
12
  Classifier: Intended Audience :: Developers
11
13
  Classifier: License :: OSI Approved :: Apache Software License
12
14
  Classifier: Programming Language :: Python :: 3
13
15
  Requires-Python: >=3.7, <4
14
16
  Description-Content-Type: text/markdown
15
- Requires-Dist: pyserial >=3.5
16
- Requires-Dist: python-can >=3.3
17
- Requires-Dist: pyelftools >=0.26
18
- Requires-Dist: scipy >=1.8.0
19
- Requires-Dist: importlib-metadata >=3.6
20
- Requires-Dist: numpy <2
17
+ Requires-Dist: importlib-metadata (>=3.6)
18
+ Requires-Dist: numpy (<2)
19
+ Requires-Dist: pyelftools (>=0.26)
20
+ Requires-Dist: pyserial (>=3.5)
21
+ Requires-Dist: python-can (>=3.3)
22
+ Requires-Dist: scipy (>=1.8.0)
21
23
  Requires-Dist: pywin32 ; platform_system == "Windows"
22
24
 
23
25
  # Python bindings for moteus brushless controller #
@@ -119,3 +121,5 @@ qr.torque = moteus.F32
119
121
 
120
122
  c = moteus.Controller(position_resolution=pr, query_resolution=qr)
121
123
  ```
124
+
125
+
@@ -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=6ecNPY1CnCgoa7z9YmeIK471-TQLirNbz_B5erBGarU,15115
4
+ moteus/calibrate_encoder.py,sha256=R3gWoKY-ikYlXViiCrhX_0BH7Ahx3T7zvDjX47go-Wc,15167
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=GFNGB4XedCOLJSr_ukhvIsGfY1y5p1y-7nUKyFfuOl8,48998
9
- moteus/moteus_tool.py,sha256=ZdUisFMTy_d5OYqTltSRlung6Zf-5ezQjylgaGSYnjw,75146
8
+ moteus/moteus.py,sha256=vImSRBn6VEmoijD6hvrSRVxuv1_xVaEJU3F_3Wi6GiE,52498
9
+ moteus/moteus_tool.py,sha256=ALwXiDWVwiAx9yJu0o-2eKd47wyKH5mRPq264mZrn5E,79662
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=QAXlfPdKUv7iVUDMlfA0IophEOfw4S5Pf7yO4Kiq5k4,627
17
+ moteus/version.py,sha256=5_fWVR7dSRZGbeKSGDI9a1MBAxaHKTOCWtgVlCY30BE,627
18
18
  moteus/win32_aioserial.py,sha256=culdl-vYxBKD5n2s5LkIMGyUaHyCcEc8BL5-DWEaxX8,2025
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,,
19
+ moteus-0.3.73.dist-info/METADATA,sha256=HIVrIqvoZY517yT3s_Euh10uzoZjYn4KmSHj2r_7K04,3490
20
+ moteus-0.3.73.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
21
+ moteus-0.3.73.dist-info/entry_points.txt,sha256=indCsEML1fmtWJU1WiV-d7UmmTaAMhyBLEc1iiKnexQ,57
22
+ moteus-0.3.73.dist-info/top_level.txt,sha256=aZzmI_yecTaDrdSp29pTJuowaSQ9dlIZheQpshGg4YQ,7
23
+ moteus-0.3.73.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.41.2)
2
+ Generator: bdist_wheel (0.37.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
2
  moteus_tool = moteus.moteus_tool:main
3
+