moteus 0.3.76__tar.gz → 0.3.77__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 (28) hide show
  1. {moteus-0.3.76 → moteus-0.3.77}/PKG-INFO +1 -5
  2. {moteus-0.3.76 → moteus-0.3.77}/moteus/calibrate_encoder.py +12 -4
  3. {moteus-0.3.76 → moteus-0.3.77}/moteus/moteus_tool.py +461 -152
  4. {moteus-0.3.76 → moteus-0.3.77}/moteus/pythoncan.py +5 -0
  5. {moteus-0.3.76 → moteus-0.3.77}/moteus/version.py +1 -1
  6. {moteus-0.3.76 → moteus-0.3.77}/moteus.egg-info/PKG-INFO +1 -5
  7. {moteus-0.3.76 → moteus-0.3.77}/moteus.egg-info/entry_points.txt +0 -1
  8. {moteus-0.3.76 → moteus-0.3.77}/setup.py +1 -1
  9. {moteus-0.3.76 → moteus-0.3.77}/README.md +0 -0
  10. {moteus-0.3.76 → moteus-0.3.77}/moteus/__init__.py +0 -0
  11. {moteus-0.3.76 → moteus-0.3.77}/moteus/aioserial.py +0 -0
  12. {moteus-0.3.76 → moteus-0.3.77}/moteus/aiostream.py +0 -0
  13. {moteus-0.3.76 → moteus-0.3.77}/moteus/command.py +0 -0
  14. {moteus-0.3.76 → moteus-0.3.77}/moteus/export.py +0 -0
  15. {moteus-0.3.76 → moteus-0.3.77}/moteus/fdcanusb.py +0 -0
  16. {moteus-0.3.76 → moteus-0.3.77}/moteus/moteus.py +0 -0
  17. {moteus-0.3.76 → moteus-0.3.77}/moteus/multiplex.py +0 -0
  18. {moteus-0.3.76 → moteus-0.3.77}/moteus/posix_aioserial.py +0 -0
  19. {moteus-0.3.76 → moteus-0.3.77}/moteus/reader.py +0 -0
  20. {moteus-0.3.76 → moteus-0.3.77}/moteus/regression.py +0 -0
  21. {moteus-0.3.76 → moteus-0.3.77}/moteus/router.py +0 -0
  22. {moteus-0.3.76 → moteus-0.3.77}/moteus/transport.py +0 -0
  23. {moteus-0.3.76 → moteus-0.3.77}/moteus/win32_aioserial.py +0 -0
  24. {moteus-0.3.76 → moteus-0.3.77}/moteus.egg-info/SOURCES.txt +0 -0
  25. {moteus-0.3.76 → moteus-0.3.77}/moteus.egg-info/dependency_links.txt +0 -0
  26. {moteus-0.3.76 → moteus-0.3.77}/moteus.egg-info/requires.txt +0 -0
  27. {moteus-0.3.76 → moteus-0.3.77}/moteus.egg-info/top_level.txt +0 -0
  28. {moteus-0.3.76 → moteus-0.3.77}/setup.cfg +0 -0
@@ -1,13 +1,11 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: moteus
3
- Version: 0.3.76
3
+ Version: 0.3.77
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
9
8
  Keywords: moteus
10
- Platform: UNKNOWN
11
9
  Classifier: Development Status :: 3 - Alpha
12
10
  Classifier: Intended Audience :: Developers
13
11
  Classifier: License :: OSI Approved :: Apache Software License
@@ -114,5 +112,3 @@ qr.torque = moteus.F32
114
112
 
115
113
  c = moteus.Controller(position_resolution=pr, query_resolution=qr)
116
114
  ```
117
-
118
-
@@ -53,11 +53,15 @@ def _parse_entry(line):
53
53
  return result
54
54
 
55
55
 
56
+ def check_line(cmd):
57
+ return ' '.join(cmd.strip().split(' ')[0:2])
58
+
59
+
56
60
  def parse_file(fp):
57
61
  lines = [x.decode('latin1') for x in fp.readlines()]
58
- if not lines[0].startswith("CAL start"):
62
+ if not check_line(lines[0]) in ["CAL start", "CALI start"]:
59
63
  raise RuntimeError("calibration does not start with magic line")
60
- if not lines[-1].startswith("CAL done"):
64
+ if not check_line(lines[-1]) in ["CAL done", "CALI done"]:
61
65
  raise RuntimeError("calibration does not end with magic line")
62
66
 
63
67
  lines = lines[1:-1]
@@ -65,8 +69,8 @@ def parse_file(fp):
65
69
  entries = [_parse_entry(line) for line in lines]
66
70
 
67
71
  result = File()
68
- result.phase_up = [x for x in entries if x.direction == 1]
69
- result.phase_down = [x for x in entries if x.direction == 2]
72
+ result.phase_up = [x for x in entries if x.direction == 1 or x.direction == 3]
73
+ result.phase_down = [x for x in entries if x.direction == 2 or x.direction == 4]
70
74
 
71
75
  return result
72
76
 
@@ -181,6 +185,8 @@ class CalibrationResult:
181
185
 
182
186
  self.fit_metric = None
183
187
 
188
+ self.current_quality_factor = None
189
+
184
190
  self.errors = []
185
191
 
186
192
  def __repr__(self):
@@ -190,6 +196,7 @@ class CalibrationResult:
190
196
  "poles": self.poles,
191
197
  "offset": self.offset,
192
198
  "fit_metric": self.fit_metric,
199
+ "current_quality_factor": self.current_quality_factor,
193
200
  "errors": self.errors,
194
201
  })
195
202
 
@@ -200,6 +207,7 @@ class CalibrationResult:
200
207
  'poles': self.poles,
201
208
  'offset': self.offset,
202
209
  'fit_metric': self.fit_metric,
210
+ 'current_quality_factor': self.current_quality_factor,
203
211
  }
204
212
 
205
213
 
@@ -43,6 +43,13 @@ MAX_FLASH_BLOCK_SIZE = 32
43
43
  # short intervals.
44
44
  FIND_TARGET_TIMEOUT = 0.01 if sys.platform != 'win32' else 0.05
45
45
 
46
+ # By default, we will only use current mode calibration if our
47
+ # expected maximum current is X times the sense noise in current.
48
+ CURRENT_QUALITY_MIN = 20
49
+
50
+ # We switch to voltage mode control if the ratio of maximum possible
51
+ # current to current noise is less than this amount.
52
+ VOLTAGE_MODE_QUALITY_MIN = 40
46
53
 
47
54
  def _wrap_neg_pi_to_pi(value):
48
55
  while value > math.pi:
@@ -64,17 +71,29 @@ def lerp(array, ratio):
64
71
  return (left_comp * (1.0 - fraction)) + (right_comp * fraction)
65
72
 
66
73
 
74
+ def stddev(data):
75
+ if len(data) == 0:
76
+ return 0
77
+
78
+ mean = sum(data) / len(data)
79
+ return math.sqrt(sum((x - mean) ** 2 for x in data) / len(data))
80
+
81
+ SUPPORTED_ABI_VERSION = 0x010a
82
+
83
+ # Old firmwares used a slightly incorrect definition of Kv/v_per_hz
84
+ # that didn't match with vendors or oscilloscope tests.
85
+ V_PER_HZ_FUDGE_010a = 1.09
86
+
67
87
  class FirmwareUpgrade:
68
88
  '''This encodes "magic" rules about upgrading firmware, largely about
69
89
  how to munge configuration options so as to not cause behavior
70
90
  change upon firmware changes.
71
91
  '''
72
92
 
73
- def __init__(self, old, new):
93
+ def __init__(self, old, new, board_family):
74
94
  self.old = old
75
95
  self.new = new
76
-
77
- SUPPORTED_ABI_VERSION = 0x0109
96
+ self.board_family = board_family
78
97
 
79
98
  if new > SUPPORTED_ABI_VERSION:
80
99
  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}")
@@ -86,6 +105,36 @@ class FirmwareUpgrade:
86
105
  lines = old_config.split(b'\n')
87
106
  items = dict([line.split(b' ') for line in lines if b' ' in line])
88
107
 
108
+ if self.new <= 0x0109 and self.old >= 0x010a:
109
+ kv = float(items.pop(b'motor.Kv'))
110
+
111
+ v_per_hz = ((V_PER_HZ_FUDGE_010a * 0.5 * 60) / kv)
112
+ items[b'motor.v_per_hz'] = str(v_per_hz).encode('utf8')
113
+
114
+ print(f"Downgrading motor.Kv to motor.v_per_hz and fixing fudge: old Kv={kv} v_per_hz={v_per_hz}")
115
+
116
+ if b'servo.max_power_W' in items:
117
+ newer_max_power_W = float(items.get(b'servo.max_power_W'))
118
+ # If this is NaN, then we'll set it back to the board
119
+ # default for the older firmware version.
120
+ if not math.isfinite(newer_max_power_W):
121
+ items[b'servo.max_power_W'] = {
122
+ 0 : b'450.0',
123
+ 1 : b'450.0',
124
+ 2 : b'100.0',
125
+ }[self.board_family or 0]
126
+ else:
127
+ # If it was finite, then we'll try to set it
128
+ # appropriately based on what the PWM rate was.
129
+ # Firmware version 0x0109 and earlier used the
130
+ # configured power as if the PWM rate were 40kHz.
131
+ pwm_rate = float(items.get(b'servo.pwm_rate_hz'))
132
+
133
+ items[b'servo.max_power_W'] = str(newer_max_power_W * (40000.0 / pwm_rate)).encode('utf8')
134
+
135
+ print(f"Downgrading servo.max_power_W to {items[b'servo.max_power_W'].decode('utf8')} for firmware <= 0x0109")
136
+
137
+
89
138
  if self.new <= 0x0108 and self.old >= 0x0109:
90
139
  # When downgrading, we should warn if a motor thermistor
91
140
  # value other than 47k is configured and enabled.
@@ -423,6 +472,33 @@ class FirmwareUpgrade:
423
472
 
424
473
  items[f'motor_position.sources.{mpsource}.compensation_scale'.encode('utf8')] = str(scale).encode('utf8')
425
474
 
475
+ if self.new >= 0x010a and self.old <= 0x0109:
476
+ v_per_hz = float(items.pop(b'motor.v_per_hz'))
477
+
478
+ kv = V_PER_HZ_FUDGE_010a * 0.5 * 60 / v_per_hz
479
+ items[b'motor.Kv'] = str(kv).encode('utf8')
480
+
481
+ print(f"Upgraded motor.v_per_hz to new motor.Kv and fixed fudge: old v_per_hz={v_per_hz} new Kv={kv}")
482
+
483
+ if b'servo.max_power_W' in items:
484
+ # If we had a power setting that was not the board
485
+ # family default, then try to keep it going forward.
486
+ # Otherwise switch it to NaN.
487
+ board_default = {
488
+ 0 : 450.0,
489
+ 1 : 450.0,
490
+ 2 : 100.0,
491
+ }[self.board_family or 0]
492
+
493
+ old_max_power = float(items[b'servo.max_power_W'])
494
+ if old_max_power == board_default:
495
+ items[b'servo.max_power_W'] = b'nan'
496
+ else:
497
+ pwm_rate = float(items.get(b'servo.pwm_rate_hz', 40000))
498
+ # The old value was set for 40kHz rate but the new
499
+ # value is absolute. Scale it appropriately.
500
+ items[b'servo.max_power_W'] = str(old_max_power * (pwm_rate / 40000)).encode('utf8')
501
+ print(f"Upgraded servo.max_power_W to {items[b'servo.max_power_W'].decode('utf8')} for 0x010a")
426
502
 
427
503
  lines = [key + b' ' + value for key, value in items.items()]
428
504
  return b'\n'.join(lines)
@@ -584,10 +660,6 @@ def _calculate_slope(x, y):
584
660
  return regression.linear_regression(x, y)[1]
585
661
 
586
662
 
587
- def _calculate_winding_resistance(voltages, currents):
588
- return 1.0 / _calculate_slope(voltages, currents)
589
-
590
-
591
663
  class FlashDataBlock:
592
664
  def __init__(self, address=-1, data=b""):
593
665
  self.address = address
@@ -814,7 +886,9 @@ class Stream:
814
886
  elf.firmware_version
815
887
  if old_firmware is None else
816
888
  old_firmware.version,
817
- elf.firmware_version)
889
+ elf.firmware_version,
890
+ None if old_firmware is None else old_firmware.family
891
+ )
818
892
 
819
893
  if not self.args.bootloader_active and not self.args.no_restore_config:
820
894
  # Read our old config.
@@ -929,26 +1003,6 @@ class Stream:
929
1003
  handle_deprecated('cal-ll-resistance-voltage', 'cal-voltage')
930
1004
  handle_deprecated('cal-ll-kv-voltage', 'cal-kv-voltage')
931
1005
 
932
- async def find_resistance_cal_voltage(self, input_V):
933
- if self.args.cal_ll_resistance_voltage:
934
- return self.args.cal_ll_resistance_voltage
935
- else:
936
- # Progressively increase this value to roughly achieve our
937
- # desired power.
938
- cal_voltage = 0.01
939
- while True:
940
- print(f"Testing {cal_voltage:.3f}V for resistance",
941
- end='\r', flush=True)
942
- this_current = await self.find_current(cal_voltage)
943
- power = this_current * cal_voltage
944
- if (power > self.args.cal_motor_power or
945
- cal_voltage > (0.4 * input_V)):
946
- break
947
- cal_voltage *= 1.1
948
- print()
949
-
950
- return cal_voltage
951
-
952
1006
  async def clear_motor_offsets(self):
953
1007
  i = 0
954
1008
  while True:
@@ -965,6 +1019,24 @@ class Stream:
965
1019
  async def do_calibrate(self):
966
1020
  self.firmware = await self.read_data("firmware")
967
1021
 
1022
+ old_config = None
1023
+ if self.args.cal_no_update:
1024
+ print("Capturing old config for --cal-no-update")
1025
+ old_config = await self.command("conf enumerate")
1026
+
1027
+ if self.firmware.version > SUPPORTED_ABI_VERSION:
1028
+ raise RuntimeError(f"\nmoteus_tool needs to be upgraded to support this firmware\n\n (likely python -m pip install --upgrade moteus')\n\nThe existing board has firmware 0x{self.firmware.version:04x} but this moteus_tool only supports up to 0x{SUPPORTED_ABI_VERSION:04x}")
1029
+
1030
+ # Verify that commutation is from source 0. It wouldn't be
1031
+ # too hard to support other sources, but for now this is
1032
+ # easier than doing so, and there is no real reason any end
1033
+ # user can't swap things around to get the commutation source
1034
+ # on slot 0.
1035
+ if await self.is_config_supported("motor_position.commutation_source"):
1036
+ commutation_source = await self.read_config_int("motor_position.commutation_source")
1037
+ if commutation_source != 0:
1038
+ raise RuntimeError("Automatic calibration only supported with commutation source of 0")
1039
+
968
1040
  # Determine what our calibration parameters are.
969
1041
  self.calculate_calibration_parameters()
970
1042
 
@@ -999,7 +1071,8 @@ class Stream:
999
1071
  control_rate_hz = 40000
1000
1072
 
1001
1073
  # The rest of the calibration procedure assumes that
1002
- # phase_invert is 0.
1074
+ # phase_invert is 0 and that the commutation encoder has a
1075
+ # positive sign.
1003
1076
  try:
1004
1077
  await self.command("conf set motor.phase_invert 0")
1005
1078
  except moteus.CommandError as e:
@@ -1009,6 +1082,13 @@ class Stream:
1009
1082
  raise
1010
1083
  pass
1011
1084
 
1085
+ try:
1086
+ await self.command("conf set motor_position.sources.0.sign 1")
1087
+ except moteus.CommandError as e:
1088
+ if not 'error setting' in e.message:
1089
+ raise
1090
+ pass
1091
+
1012
1092
  # We have 3 things to calibrate.
1013
1093
  # 1) The encoder to phase mapping
1014
1094
  # 2) The winding resistance
@@ -1019,19 +1099,15 @@ class Stream:
1019
1099
  print("Starting calibration process")
1020
1100
  await self.check_for_fault()
1021
1101
 
1022
- resistance_cal_voltage = await self.find_resistance_cal_voltage(input_V)
1023
- print(f"Using {resistance_cal_voltage:.3f} V for resistance and inductance calibration")
1024
-
1025
- winding_resistance = await self.calibrate_winding_resistance(resistance_cal_voltage)
1102
+ winding_resistance, highest_voltage, current_noise = (
1103
+ await self.calibrate_winding_resistance2(input_V))
1026
1104
  await self.check_for_fault()
1027
1105
 
1028
- cal_result = await self.calibrate_encoder_mapping(
1029
- input_V, winding_resistance)
1030
- await self.check_for_fault()
1106
+ resistance_cal_voltage = highest_voltage
1031
1107
 
1032
1108
  # Determine our inductance.
1033
1109
  inductance = await self.calibrate_inductance(
1034
- resistance_cal_voltage, winding_resistance)
1110
+ resistance_cal_voltage, input_V)
1035
1111
  await self.check_for_fault()
1036
1112
 
1037
1113
  kp, ki, torque_bw_hz = None, None, None
@@ -1050,22 +1126,52 @@ class Stream:
1050
1126
  control_rate_hz=control_rate_hz)
1051
1127
  await self.check_for_fault()
1052
1128
 
1053
- v_per_hz = await self.calibrate_kv_rating(
1129
+ cal_result = await self.calibrate_encoder_mapping(
1130
+ input_V, winding_resistance, current_noise)
1131
+ await self.check_for_fault()
1132
+
1133
+ motor_kv = await self.calibrate_kv_rating(
1054
1134
  input_V, unwrapped_position_scale, motor_output_sign)
1055
1135
  await self.check_for_fault()
1056
1136
 
1057
1137
  # Rezero the servo since we just spun it a lot.
1058
1138
  await self.command("d rezero")
1059
1139
 
1140
+ voltage_mode_control = False
1141
+
1142
+ if await self.is_config_supported("servo.voltage_mode_control"):
1143
+ # See if we should be in voltage control mode or not. The
1144
+ # heuristic is based the ratio of maximum possible phase
1145
+ # current given input voltage and phase resistance compared to
1146
+ # the phase noise. Note that this is slightly different from
1147
+ # the heuristic used to switch to voltage mode calibration.
1148
+ # There we only look at the current that would be used for
1149
+ # calibration, not the maximum possible current. Thus our
1150
+ # threshold is a bit different.
1151
+ max_possible_current = (0.5 * input_V / winding_resistance)
1152
+ max_current_quality = max_possible_current / current_noise
1153
+
1154
+ voltage_mode_control = max_current_quality < VOLTAGE_MODE_QUALITY_MIN
1155
+ if voltage_mode_control:
1156
+ print(f"Using voltage mode control: \n max possible current ({max_possible_current:.1f}) / current noise ({current_noise:.3f}) = {max_current_quality:.1f} < {VOLTAGE_MODE_QUALITY_MIN}")
1157
+
1158
+ await self.command(f"conf set servo.voltage_mode_control {1 if voltage_mode_control else 0}")
1159
+
1160
+
1161
+ device_info = await self.get_device_info()
1162
+
1060
1163
  if not self.args.cal_no_update:
1061
1164
  print("Saving to persistent storage")
1062
1165
  await self.command("conf write")
1166
+ else:
1167
+ # Restore our baseline configuration.
1168
+ print("Restoring baseline configuration for --cal-no-update")
1063
1169
 
1064
- print("Calibration complete")
1170
+ await self.restore_config(old_config)
1065
1171
 
1066
- device_info = await self.get_device_info()
1172
+ print("Calibration complete")
1067
1173
 
1068
- now = datetime.datetime.utcnow()
1174
+ now = datetime.datetime.now(datetime.UTC)
1069
1175
 
1070
1176
  report = {
1071
1177
  'timestamp' : now.strftime('%Y-%m-%d %H:%M:%S.%f'),
@@ -1079,12 +1185,11 @@ class Stream:
1079
1185
  'encoder_filter_bw_hz' : enc_bw_hz,
1080
1186
  'encoder_filter_kp' : enc_kp,
1081
1187
  'encoder_filter_ki' : enc_ki,
1082
- 'v_per_hz' : v_per_hz,
1083
- # We measure voltage to the center, not peak-to-peak, thus
1084
- # the extra 0.5.
1085
- 'kv' : (0.5 * 60.0 / v_per_hz),
1188
+ 'kv' : motor_kv,
1086
1189
  'unwrapped_position_scale' : unwrapped_position_scale,
1087
1190
  'motor_position_output_sign' : motor_output_sign,
1191
+ 'abi_version' : self.firmware.version,
1192
+ 'voltage_mode_control' : voltage_mode_control,
1088
1193
  }
1089
1194
 
1090
1195
  log_filename = f"moteus-cal-{device_info['serial_number']}-{now.strftime('%Y%m%dT%H%M%S.%f')}.log"
@@ -1102,17 +1207,19 @@ class Stream:
1102
1207
 
1103
1208
  async def find_encoder_cal_voltage(self, input_V, winding_resistance):
1104
1209
  if self.args.cal_ll_encoder_voltage:
1105
- return self.args.cal_ll_encoder_voltage
1210
+ return self.args.cal_ll_encoder_voltage,
1106
1211
 
1107
1212
  # We're going to try and select a voltage to roughly achieve
1108
1213
  # "--cal-motor-power".
1109
1214
  return min(0.4 * input_V,
1110
- math.sqrt(self.args.cal_motor_power * winding_resistance))
1215
+ math.sqrt((self.args.cal_motor_power / 1.5) *
1216
+ winding_resistance))
1111
1217
 
1112
- async def calibrate_encoder_mapping(self, input_V, winding_resistance):
1218
+ async def calibrate_encoder_mapping(self, input_V, winding_resistance, current_noise):
1113
1219
  # Figure out what voltage to use for encoder calibration.
1114
- encoder_cal_voltage = await self.find_encoder_cal_voltage(
1115
- input_V, winding_resistance)
1220
+ encoder_cal_voltage = \
1221
+ await self.find_encoder_cal_voltage(input_V, winding_resistance)
1222
+ encoder_cal_current = encoder_cal_voltage / winding_resistance
1116
1223
  self.encoder_cal_voltage = encoder_cal_voltage
1117
1224
 
1118
1225
  hall_configured = False
@@ -1133,12 +1240,34 @@ class Stream:
1133
1240
  "not configured on device")
1134
1241
  return await self.calibrate_encoder_mapping_hall(encoder_cal_voltage)
1135
1242
  else:
1243
+ old_output_sign = None
1244
+ old_voltage_mode_control = None
1136
1245
  try:
1137
- return await self.calibrate_encoder_mapping_absolute(encoder_cal_voltage)
1138
- except:
1246
+ if await self.is_config_supported("motor_position.output.sign"):
1247
+ # Some later parts of our calibration procedure can
1248
+ # handle a negative sign, but not the absolute encoder
1249
+ # mapping when using the current mode. Thus for now
1250
+ # we just force it to 1 and set it back when complete.
1251
+ old_output_sign = await self.read_config_int(
1252
+ "motor_position.output.sign")
1253
+ await self.command("conf set motor_position.output.sign 1")
1254
+
1255
+ old_voltage_mode_control = await self.read_config_int(
1256
+ "servo.voltage_mode_control")
1257
+
1258
+ return await self.calibrate_encoder_mapping_absolute(
1259
+ encoder_cal_voltage, encoder_cal_current, current_noise)
1260
+ finally:
1139
1261
  # At least try to stop.
1140
1262
  await self.command("d stop")
1141
- raise
1263
+
1264
+ if old_output_sign is not None:
1265
+ await self.command(
1266
+ f"conf set motor_position.output.sign {old_output_sign}")
1267
+
1268
+ await self.command(
1269
+ f"conf set servo.voltage_mode_control {old_voltage_mode_control}")
1270
+
1142
1271
 
1143
1272
  async def calibrate_encoder_mapping_hall(self, encoder_cal_voltage):
1144
1273
  if self.args.cal_motor_poles is None:
@@ -1164,8 +1293,8 @@ class Stream:
1164
1293
  hall_cal_data.append(
1165
1294
  (phase, motor_position.sources[commutation_source].raw))
1166
1295
 
1167
- if self.args.cal_raw:
1168
- with open(self.args.cal_raw, "wb") as f:
1296
+ if self.args.cal_write_raw:
1297
+ with open(self.args.cal_write_raw, "wb") as f:
1169
1298
  f.write(json.dumps(hall_cal_data, indent=2).encode('utf8'))
1170
1299
 
1171
1300
  await self.command("d stop")
@@ -1179,17 +1308,16 @@ class Stream:
1179
1308
  desired_direction=1 if not self.args.cal_invert else -1,
1180
1309
  allow_phase_invert=allow_phase_invert)
1181
1310
 
1182
- if not self.args.cal_no_update:
1183
- await self.command(f"conf set motor.poles {self.args.cal_motor_poles}")
1184
- await self.command(f"conf set motor_position.sources.{commutation_source}.sign {cal_result.sign}")
1185
- await self.command(f"conf set motor_position.sources.{commutation_source}.offset {cal_result.offset}")
1186
- await self.command(f"conf set aux{aux_number}.hall.polarity {cal_result.polarity}")
1187
- if allow_phase_invert:
1188
- await self.command(
1189
- f"conf set motor.phase_invert {1 if cal_result.phase_invert else 0}")
1190
-
1191
- for i in range(64):
1192
- await self.command(f"conf set motor.offset.{i} 0")
1311
+ await self.command(f"conf set motor.poles {self.args.cal_motor_poles}")
1312
+ await self.command(f"conf set motor_position.sources.{commutation_source}.sign {cal_result.sign}")
1313
+ await self.command(f"conf set motor_position.sources.{commutation_source}.offset {cal_result.offset}")
1314
+ await self.command(f"conf set aux{aux_number}.hall.polarity {cal_result.polarity}")
1315
+ if allow_phase_invert:
1316
+ await self.command(
1317
+ f"conf set motor.phase_invert {1 if cal_result.phase_invert else 0}")
1318
+
1319
+ for i in range(64):
1320
+ await self.command(f"conf set motor.offset.{i} 0")
1193
1321
 
1194
1322
  return cal_result
1195
1323
 
@@ -1230,14 +1358,39 @@ class Stream:
1230
1358
  # We need to find an index.
1231
1359
  await self.find_index(encoder_cal_voltage)
1232
1360
 
1233
- async def calibrate_encoder_mapping_absolute(self, encoder_cal_voltage):
1361
+ async def calibrate_encoder_mapping_absolute(
1362
+ self, encoder_cal_voltage, encoder_cal_current, current_noise):
1234
1363
  await self.ensure_valid_theta(encoder_cal_voltage)
1235
1364
 
1236
- await self.command(f"d pwm 0 {encoder_cal_voltage}")
1237
- await asyncio.sleep(3.0)
1365
+ current_quality_factor = encoder_cal_current / current_noise
1366
+ use_current_for_quality = (
1367
+ current_quality_factor > CURRENT_QUALITY_MIN or
1368
+ self.args.cal_force_encoder_current_mode)
1369
+ use_current_for_firmware_version = self.firmware.version >= 0x010a
1238
1370
 
1239
- await self.write_message(
1240
- (f"d cal {encoder_cal_voltage} s{self.args.cal_ll_encoder_speed}"))
1371
+ use_current_calibration = (
1372
+ use_current_for_quality and use_current_for_firmware_version)
1373
+
1374
+ if use_current_for_firmware_version and not use_current_for_quality:
1375
+ print(f"Using voltage mode calibration, current quality factor {current_quality_factor:.1f} < {CURRENT_QUALITY_MIN:.1f}")
1376
+
1377
+
1378
+ old_motor_poles = None
1379
+
1380
+ if use_current_calibration:
1381
+ old_motor_poles = await self.read_config_int("motor.poles")
1382
+ await self.command("conf set motor.poles 2")
1383
+ await self.command(f"d pos nan 0 nan c{encoder_cal_current} b1")
1384
+ await asyncio.sleep(3.0)
1385
+
1386
+ await self.write_message(
1387
+ (f"d cali i{encoder_cal_current} s{self.args.cal_ll_encoder_speed}"))
1388
+ else:
1389
+ await self.command(f"d pwm 0 {encoder_cal_voltage}")
1390
+ await asyncio.sleep(3.0)
1391
+
1392
+ await self.write_message(
1393
+ (f"d cal {encoder_cal_voltage} s{self.args.cal_ll_encoder_speed}"))
1241
1394
 
1242
1395
  cal_data = b''
1243
1396
  index = 0
@@ -1247,9 +1400,9 @@ class Stream:
1247
1400
  print("Calibrating {} ".format("/-\\|"[index]), end='\r', flush=True)
1248
1401
  index = (index + 1) % 4
1249
1402
  cal_data += (line + b'\n')
1250
- if line.startswith(b'CAL done'):
1403
+ if line.startswith(b'CAL done') or line.startswith(b'CALI done'):
1251
1404
  break
1252
- if line.startswith(b'CAL start'):
1405
+ if line.startswith(b'CAL start') or line.startswith(b'CALI start'):
1253
1406
  continue
1254
1407
  if line.startswith(b'ERR'):
1255
1408
  raise RuntimeError(f'Error calibrating: {line}')
@@ -1257,8 +1410,8 @@ class Stream:
1257
1410
  # Some problem
1258
1411
  raise RuntimeError(f'Error calibrating: {line}')
1259
1412
 
1260
- if self.args.cal_raw:
1261
- with open(self.args.cal_raw, "wb") as f:
1413
+ if self.args.cal_write_raw:
1414
+ with open(self.args.cal_write_raw, "wb") as f:
1262
1415
  f.write(cal_data)
1263
1416
 
1264
1417
  cal_file = ce.parse_file(io.BytesIO(cal_data))
@@ -1285,21 +1438,22 @@ class Stream:
1285
1438
  f"Auto-detected pole count ({cal_result.poles}) != " +
1286
1439
  f"cmdline specified ({self.args.cal_motor_poles})")
1287
1440
 
1288
- if not self.args.cal_no_update:
1289
- print("\nStoring encoder config")
1290
- await self.command(f"conf set motor.poles {cal_result.poles}")
1441
+ print("\nStoring encoder config")
1442
+ await self.command(f"conf set motor.poles {cal_result.poles}")
1291
1443
 
1292
- if await self.is_config_supported("motor_position.sources.0.sign"):
1293
- await self.command("conf set motor_position.sources.0.sign {}".format(
1294
- -1 if cal_result.invert else 1))
1295
- else:
1296
- await self.command("conf set motor.invert {}".format(
1297
- 1 if cal_result.invert else 0))
1298
- if allow_phase_invert:
1299
- await self.command("conf set motor.phase_invert {}".format(
1300
- 1 if cal_result.phase_invert else 0))
1301
- for i, offset in enumerate(cal_result.offset):
1302
- await self.command(f"conf set motor.offset.{i} {offset}")
1444
+ if await self.is_config_supported("motor_position.sources.0.sign"):
1445
+ await self.command("conf set motor_position.sources.0.sign {}".format(
1446
+ -1 if cal_result.invert else 1))
1447
+ else:
1448
+ await self.command("conf set motor.invert {}".format(
1449
+ 1 if cal_result.invert else 0))
1450
+ if allow_phase_invert:
1451
+ await self.command("conf set motor.phase_invert {}".format(
1452
+ 1 if cal_result.phase_invert else 0))
1453
+ for i, offset in enumerate(cal_result.offset):
1454
+ await self.command(f"conf set motor.offset.{i} {offset}")
1455
+
1456
+ cal_result.current_quality_factor = current_quality_factor
1303
1457
 
1304
1458
  return cal_result
1305
1459
 
@@ -1317,7 +1471,7 @@ class Stream:
1317
1471
 
1318
1472
  # Now get the servo_stats telemetry channel to read the D and Q
1319
1473
  # currents.
1320
- data = [extract(await self.read_servo_stats()) for _ in range(10)]
1474
+ data = [extract(await self.read_servo_stats()) for _ in range(20)]
1321
1475
 
1322
1476
  # Stop the current.
1323
1477
  await self.command("d stop");
@@ -1326,53 +1480,175 @@ class Stream:
1326
1480
  await asyncio.sleep(0.05);
1327
1481
 
1328
1482
  current_A = sum(data) / len(data)
1483
+ noise_A = stddev(data)
1329
1484
 
1330
- return current_A
1485
+ return current_A, noise_A
1331
1486
 
1332
1487
  async def find_current_and_print(self, voltage):
1333
- result = await self.find_current(voltage)
1488
+ result, noise = await self.find_current(voltage)
1334
1489
  print(f"{voltage:.3f}V - {result:.3f}A")
1335
1490
  return result
1336
1491
 
1337
- async def calibrate_winding_resistance(self, cal_voltage):
1492
+ async def calibrate_winding_resistance2(self, input_V):
1338
1493
  print("Calculating winding resistance")
1339
1494
 
1340
- ratios = [ 0.5, 0.6, 0.7, 0.85, 1.0 ]
1341
- voltages = [x * cal_voltage for x in ratios]
1342
- currents = [await self.find_current_and_print(voltage)
1343
- for voltage in voltages]
1495
+ # Depending upon the switching rate, there will be a region
1496
+ # around 0 current where the measured resistance is much
1497
+ # higher than actual and highly non-linear. We also want to
1498
+ # limit the maximum amount of power put into the motor per the
1499
+ # user's request. So, to balance those requirements, we start
1500
+ # at very low voltages and geometrically increase until we
1501
+ # reach the desired user power. After that point, we attempt
1502
+ # to select a region from the largest currents that is roughly
1503
+ # linear.
1504
+
1505
+ cal_voltage = 0.01
1506
+
1507
+ # This will be a list of:
1508
+ # ( voltage,
1509
+ # current,
1510
+ # step_resistance,
1511
+ # noise,
1512
+ # )
1513
+ results = []
1344
1514
 
1345
- winding_resistance = _calculate_winding_resistance(voltages, currents)
1515
+ while True:
1516
+ this_current, this_noise = await self.find_current(cal_voltage)
1346
1517
 
1347
- if winding_resistance < 0.001:
1348
- raise RuntimeError(
1349
- f'Winding resistance too small ({winding_resistance} < 0.001)' +
1350
- f', try adjusting --cal-voltage')
1518
+ this_resistance = None
1519
+ if len(results):
1520
+ this_resistance = ((cal_voltage - results[-1][0]) /
1521
+ (this_current - results[-1][1]))
1351
1522
 
1352
- if not self.args.cal_no_update:
1353
- await self.command(f"conf set motor.resistance_ohm {winding_resistance}")
1523
+ if not self.args.verbose:
1524
+ print(f"Tested {cal_voltage:6.3f}V resistance {this_resistance or 0:6.3f}ohms noise={this_noise:5.3f}A ",
1525
+ end='\r', flush=True)
1526
+ else:
1527
+ print(f" V={cal_voltage} I={this_current} R={this_resistance}")
1354
1528
 
1355
- return winding_resistance
1529
+ results.append((cal_voltage, this_current, this_resistance, this_noise))
1356
1530
 
1357
- async def calibrate_inductance(self, cal_voltage, winding_resistance):
1358
- print("Calculating motor inductance")
1531
+ power = this_current * cal_voltage * 1.5
1532
+ if (power > self.args.cal_motor_power or
1533
+ cal_voltage > (0.4 * input_V)):
1534
+ break
1535
+
1536
+ cal_voltage *= 1.1
1537
+
1538
+ print()
1539
+
1540
+ # If we had infinite precision, the "most correct" answer
1541
+ # would be the very last step_resistance we measured.
1542
+ # However, current noise will mean it is useful to incorporate
1543
+ # a reading from a more distant point. This is hard because
1544
+ # as we get closer to 0, the results will become highly
1545
+ # non-linear, corrupting the result.
1546
+
1547
+ # What we'll do is take the very last result, and the last
1548
+ # result that is less than 70% of the current of the last
1549
+ # result.
1550
+
1551
+ last_result = results[-1]
1552
+
1553
+ less_than = [x for x in results if x[1] < 0.60 * last_result[1]][-1]
1359
1554
 
1360
- try:
1361
- # High winding resistance motors typically have a much
1362
- # larger inductance, and therefore need a longer
1363
- # inductance period. We still need to keep this low for
1364
- # low resistance/inductance motors, otherwise we can have
1365
- # excessive peak currents during the measurement process.
1366
-
1367
- # For phase resistances of 0.2 ohm or less, stick with 8
1368
- # cycles, which for a 15kHz pwm rate would equal ~0.6ms of
1369
- # on time. Increase that as phase resistance increases,
1370
- # until it maxes at 32/2ms around 0.8 ohms of phase
1371
- # resistance.
1372
- ind_period = max(8, min(32, int(winding_resistance / 0.2)))
1373
1555
 
1556
+ resistance = ((last_result[0] - less_than[0]) /
1557
+ (last_result[1] - less_than[1]))
1558
+
1559
+ print(f"Resistance {resistance:.3f} ohms")
1560
+
1561
+ await self.command(f"conf set motor.resistance_ohm {resistance}")
1562
+
1563
+ return resistance, last_result[0], last_result[3]
1564
+
1565
+
1566
+ async def _test_inductance_period(self, cal_voltage, input_V, ind_period):
1567
+ ind_voltage = cal_voltage
1568
+
1569
+ if self.firmware.version < 0x010a:
1374
1570
  await asyncio.wait_for(
1375
1571
  self.command(f"d ind {cal_voltage} {ind_period}"), 0.25)
1572
+ else:
1573
+ # Our device supports inductance measurement from a
1574
+ # voltage offset.
1575
+ offset = min(0.2 * input_V, cal_voltage)
1576
+ ind_voltage = min(0.15 * input_V, 0.80 * cal_voltage)
1577
+ await asyncio.wait_for(
1578
+ self.command(f"d ind {ind_voltage} {ind_period} o{offset}"), 0.25)
1579
+
1580
+
1581
+ start = time.time()
1582
+ await asyncio.sleep(1.0)
1583
+
1584
+ if self.firmware.version < 0x010a:
1585
+ await self.command(f"d stop")
1586
+ else:
1587
+ # Hold the same position and fixed voltage.
1588
+ await self.command(f"d pos nan 0 nan o{offset} b1")
1589
+
1590
+ end = time.time()
1591
+ data = await self.read_servo_stats()
1592
+
1593
+ delta_time = end - start
1594
+ di_dt = data.meas_ind_integrator / delta_time
1595
+
1596
+ if self.args.verbose:
1597
+ print(f" inductance period={ind_period} v={cal_voltage} di_dt={di_dt}")
1598
+
1599
+ return di_dt, ind_voltage
1600
+
1601
+
1602
+ async def calibrate_inductance(self, cal_voltage, input_V):
1603
+ print("Calculating motor inductance")
1604
+
1605
+ old_motor_poles = await self.read_config_int("motor.poles")
1606
+ if old_motor_poles == 0:
1607
+ await self.command("conf set motor.poles 2")
1608
+
1609
+ # Sweep through a range of inductance measurement frequencies
1610
+ # until we have a peak or near peak. Rationale:
1611
+ #
1612
+ # For low inductance/low resistance motors, we can't actually
1613
+ # test very low frequencies without generating overly large
1614
+ # currents. Thus we start out at a high frequency and stop
1615
+ # when it looks like we have gotten near enough to the peak.
1616
+ # Similarly, for high resistance/low inductance motors, we can
1617
+ # only effectively measure the inductance at high frequencies,
1618
+ # otherwise the current will saturate at Vbus/resistance. For
1619
+ # high resistance / high inductance motors, we need to get all
1620
+ # the way to low frequencies before the measured current is
1621
+ # actually large enough to detect.
1622
+
1623
+ periods_to_test = [2, 3, 4, 6, 8, 10, 12, 16, 20, 24, 32]
1624
+
1625
+ highest_di_dt = None
1626
+ highest_cal_voltage = None
1627
+ since_highest = None
1628
+
1629
+ try:
1630
+ for period in periods_to_test:
1631
+ this_di_dt, this_ind_voltage = (
1632
+ await self._test_inductance_period(
1633
+ cal_voltage, input_V, period))
1634
+
1635
+ if highest_di_dt is None or this_di_dt > highest_di_dt:
1636
+ highest_di_dt = this_di_dt
1637
+ highest_cal_voltage = this_ind_voltage
1638
+ since_highest = 0
1639
+ else:
1640
+ if since_highest is not None:
1641
+ since_highest += 1
1642
+
1643
+ if self.args.verbose:
1644
+ print(f"inductance period {period} di_dt={this_di_dt}")
1645
+
1646
+ if (highest_di_dt > 0 and
1647
+ (this_di_dt < 0.5 * highest_di_dt or since_highest > 2)):
1648
+ # We stop early to avoid causing excessive ripple
1649
+ # current on low resistance / low inductance motors.
1650
+ break
1651
+ await self.command("d stop")
1376
1652
  except moteus.CommandError as e:
1377
1653
  # It is possible this is an old firmware that does not
1378
1654
  # support inductance measurement.
@@ -1384,15 +1660,7 @@ class Stream:
1384
1660
  print("Firmware does not support inductance measurement")
1385
1661
  return None
1386
1662
 
1387
- start = time.time()
1388
- await asyncio.sleep(1.0)
1389
- await self.command(f"d stop")
1390
- end = time.time()
1391
- data = await self.read_servo_stats()
1392
-
1393
- delta_time = end - start
1394
- inductance = (cal_voltage /
1395
- (data.meas_ind_integrator / delta_time))
1663
+ inductance = (highest_cal_voltage / highest_di_dt)
1396
1664
 
1397
1665
  if inductance < 1e-6:
1398
1666
  raise RuntimeError(f'Inductance too small ({inductance} < 1e-6)')
@@ -1521,7 +1789,7 @@ class Stream:
1521
1789
  data = await self.read_servo_stats()
1522
1790
 
1523
1791
  total_current_A = math.hypot(data.d_A, data.q_A)
1524
- total_power_W = voltage * total_current_A
1792
+ total_power_W = voltage * total_current_A * 1.5
1525
1793
 
1526
1794
  power_samples.append(total_power_W)
1527
1795
 
@@ -1582,8 +1850,11 @@ class Stream:
1582
1850
  if self.args.cal_ll_kv_voltage:
1583
1851
  return self.args.cal_ll_kv_voltage
1584
1852
 
1853
+ first_nonzero_speed_voltage = None
1854
+
1585
1855
  # Otherwise, we start small, and increase until we hit a
1586
- # reasonable speed.
1856
+ # reasonable speed, but at least twice what it takes to get a
1857
+ # non-zero speed.
1587
1858
  maybe_result = 0.01
1588
1859
  while True:
1589
1860
  print(f"Testing {maybe_result:.3f}V for Kv",
@@ -1592,9 +1863,17 @@ class Stream:
1592
1863
  return maybe_result
1593
1864
 
1594
1865
  this_speed = await self.find_speed(maybe_result) / unwrapped_position_scale
1866
+
1867
+ if (abs(this_speed) > (0.1 * self.args.cal_motor_speed) and
1868
+ first_nonzero_speed_voltage is None):
1869
+ first_nonzero_speed_voltage = maybe_result
1870
+
1595
1871
  # Aim for this many Hz
1596
- if abs(this_speed) > self.args.cal_motor_speed:
1872
+ if (first_nonzero_speed_voltage is not None and
1873
+ maybe_result > (2 * first_nonzero_speed_voltage) and
1874
+ abs(this_speed) > self.args.cal_motor_speed):
1597
1875
  break
1876
+
1598
1877
  maybe_result *= 1.1
1599
1878
 
1600
1879
  print()
@@ -1614,19 +1893,34 @@ class Stream:
1614
1893
  await self.command("conf set servopos.position_max NaN")
1615
1894
  await self.command("d index 0")
1616
1895
 
1617
- kv_cal_voltage = await self.find_kv_cal_voltage(input_V, unwrapped_position_scale)
1896
+ kv_cal_voltage = await self.find_kv_cal_voltage(
1897
+ input_V, unwrapped_position_scale)
1618
1898
  await self.stop_and_idle()
1619
1899
 
1620
1900
  voltages = [x * kv_cal_voltage for x in [
1621
1901
  0.0, 0.25, 0.5, 0.75, 1.0 ]]
1622
- speed_hzs = [ await self.find_speed_and_print(voltage, sleep_time=2)
1623
- for voltage in voltages]
1902
+ voltage_speed_hzs = list(zip(
1903
+ voltages, [ await self.find_speed_and_print(voltage, sleep_time=2)
1904
+ for voltage in voltages]))
1624
1905
 
1625
1906
  await self.stop_and_idle()
1626
1907
 
1627
1908
  await asyncio.sleep(0.5)
1628
1909
 
1629
- geared_v_per_hz = 1.0 / _calculate_slope(voltages, speed_hzs)
1910
+ await self.command(f"conf set servopos.position_min {original_position_min}")
1911
+ await self.command(f"conf set servopos.position_max {original_position_max}")
1912
+
1913
+ # Drop any measurements that are too slow. This will
1914
+ # include (hopefully) our initial zero voltage
1915
+ # measurement, but that lets us get a more accurate read
1916
+ # on the slope.
1917
+ speed_threshold = abs(0.45 * voltage_speed_hzs[-1][1])
1918
+ voltage_speed_hzs = [(v, s) for v, s in voltage_speed_hzs
1919
+ if abs(s) > speed_threshold]
1920
+
1921
+ geared_v_per_hz = 1.0 / _calculate_slope(
1922
+ [x[0] for x in voltage_speed_hzs],
1923
+ [x[1] for x in voltage_speed_hzs])
1630
1924
 
1631
1925
  v_per_hz = (geared_v_per_hz *
1632
1926
  unwrapped_position_scale)
@@ -1635,23 +1929,36 @@ class Stream:
1635
1929
 
1636
1930
  print(f"v_per_hz (pre-gearbox)={v_per_hz}")
1637
1931
 
1638
- await self.command(f"conf set servopos.position_min {original_position_min}")
1639
- await self.command(f"conf set servopos.position_max {original_position_max}")
1640
-
1641
1932
  if v_per_hz < 0.0:
1642
1933
  raise RuntimeError(
1643
1934
  f"v_per_hz measured as negative ({v_per_hz}), something wrong")
1935
+
1936
+ # Experimental verification of Kv using this protocol
1937
+ # typically results in a determination of Kv roughly 14%
1938
+ # below what an open circuit spin measures with an
1939
+ # oscilloscope. That is probably due to friction in the
1940
+ # system and other non-linearities.
1941
+ FUDGE = 1.14
1942
+ motor_kv = FUDGE * 0.5 * 60 / v_per_hz
1644
1943
  else:
1645
- v_per_hz = (0.5 * 60 / self.args.cal_force_kv)
1646
- print(f"Using forced Kv: {self.args.cal_force_kv} v_per_hz={v_per_hz}")
1944
+ motor_kv = self.args.cal_force_kv
1945
+ print(f"Using forced Kv: {self.args.cal_force_kv}")
1647
1946
 
1648
- if not self.args.cal_no_update:
1947
+ if self.firmware.version >= 0x010a:
1948
+ await self.command(f"conf set motor.Kv {motor_kv}")
1949
+ else:
1950
+ if self.firmware.family == 2:
1951
+ # moteus-c1 in older firmwares had additional
1952
+ # scaling.
1953
+ motor_kv /= 1.38
1954
+
1955
+ v_per_hz = (V_PER_HZ_FUDGE_010a * 0.5 * 60 / motor_kv)
1649
1956
  await self.command(f"conf set motor.v_per_hz {v_per_hz}")
1650
1957
 
1651
- if v_per_hz < 0:
1652
- raise RuntimeError(f'v_per_hz value ({v_per_hz}) is negative')
1958
+ if motor_kv < 0:
1959
+ raise RuntimeError(f'Kv value ({motor_kv}) is negative')
1653
1960
 
1654
- return v_per_hz
1961
+ return motor_kv
1655
1962
 
1656
1963
  async def stop_and_idle(self):
1657
1964
  await self.command("d stop")
@@ -1701,7 +2008,10 @@ class Stream:
1701
2008
  await self.command(f"conf set motor.offset.{index} {offset}")
1702
2009
 
1703
2010
  await self.command(f"conf set motor.resistance_ohm {report['winding_resistance']}")
1704
- await self.command(f"conf set motor.v_per_hz {report['v_per_hz']}")
2011
+ if await self.is_config_supported("motor.v_per_hz"):
2012
+ await self.command(f"conf set motor.v_per_hz {report['v_per_hz']}")
2013
+ elif await self.is_config_supported("motor.Kv"):
2014
+ await self.command(f"conf set motor.Kv {report['kv']}")
1705
2015
 
1706
2016
  pid_dq_kp = report.get('pid_dq_kp', None)
1707
2017
  if pid_dq_kp is not None:
@@ -1911,9 +2221,6 @@ async def async_main():
1911
2221
  parser.add_argument('--cal-ll-encoder-speed',
1912
2222
  metavar='HZ', type=float, default=1.0,
1913
2223
  help='speed in electrical rps')
1914
- parser.add_argument('--cal-ll-resistance-voltage',
1915
- metavar='V', type=float,
1916
- help='maximum voltage when measuring resistance')
1917
2224
  parser.add_argument('--cal-ll-kv-voltage',
1918
2225
  metavar='V', type=float,
1919
2226
  help='maximum voltage when measuring Kv')
@@ -1936,10 +2243,10 @@ async def async_main():
1936
2243
  # Internally, the above values are derived from these, combined
1937
2244
  # with the approximate input voltage to the controller.
1938
2245
  parser.add_argument('--cal-motor-power', metavar='W', type=float,
1939
- default=5.0,
2246
+ default=7.5,
1940
2247
  help='motor power in W to use for encoder cal')
1941
2248
  parser.add_argument('--cal-motor-speed', metavar='Hz', type=float,
1942
- default=6.0,
2249
+ default=12.0,
1943
2250
  help='max motor mechanical speed to use for kv cal')
1944
2251
 
1945
2252
  parser.add_argument('--cal-motor-poles', metavar='N', type=int,
@@ -1959,8 +2266,10 @@ async def async_main():
1959
2266
  help='maximum allowed error in calibration')
1960
2267
  parser.add_argument('--cal-max-kv-power-factor', type=float,
1961
2268
  default=1.25)
1962
- parser.add_argument('--cal-raw', metavar='FILE', type=str,
2269
+ parser.add_argument('--cal-write-raw', metavar='FILE', type=str,
1963
2270
  help='write raw calibration data')
2271
+ parser.add_argument('--cal-force-encoder-current-mode', action='store_true',
2272
+ help='always use encoder current mode calibration if supported')
1964
2273
 
1965
2274
  args = parser.parse_args()
1966
2275
 
@@ -28,6 +28,11 @@ class PythonCan:
28
28
  global can
29
29
  if not can:
30
30
  import can
31
+ try:
32
+ can.rc = can.util.load_config()
33
+ except can.CanInterfaceNotImplementedError as e:
34
+ if 'Unknown interface type "None"' not in str(e):
35
+ raise
31
36
 
32
37
  # We provide some defaults if they are not already
33
38
  # provided... this makes it more likely to just work out of
@@ -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.76"
15
+ VERSION="0.3.77"
@@ -1,13 +1,11 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: moteus
3
- Version: 0.3.76
3
+ Version: 0.3.77
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
9
8
  Keywords: moteus
10
- Platform: UNKNOWN
11
9
  Classifier: Development Status :: 3 - Alpha
12
10
  Classifier: Intended Audience :: Developers
13
11
  Classifier: License :: OSI Approved :: Apache Software License
@@ -114,5 +112,3 @@ qr.torque = moteus.F32
114
112
 
115
113
  c = moteus.Controller(position_resolution=pr, query_resolution=qr)
116
114
  ```
117
-
118
-
@@ -1,3 +1,2 @@
1
1
  [console_scripts]
2
2
  moteus_tool = moteus.moteus_tool:main
3
-
@@ -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.76",
28
+ version = "0.3.77",
29
29
  description = 'moteus brushless controller library and tools',
30
30
  long_description = long_description,
31
31
  long_description_content_type = 'text/markdown',
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