moteus 0.3.76__py3-none-any.whl → 0.3.78__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 +12 -4
- moteus/moteus_tool.py +464 -152
- moteus/pythoncan.py +5 -0
- moteus/version.py +1 -1
- {moteus-0.3.76.dist-info → moteus-0.3.78.dist-info}/METADATA +7 -11
- {moteus-0.3.76.dist-info → moteus-0.3.78.dist-info}/RECORD +9 -9
- {moteus-0.3.76.dist-info → moteus-0.3.78.dist-info}/WHEEL +1 -1
- {moteus-0.3.76.dist-info → moteus-0.3.78.dist-info}/entry_points.txt +0 -1
- {moteus-0.3.76.dist-info → moteus-0.3.78.dist-info}/top_level.txt +0 -0
moteus/calibrate_encoder.py
CHANGED
@@ -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]
|
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]
|
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
|
|
moteus/moteus_tool.py
CHANGED
@@ -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
|
-
|
1023
|
-
|
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
|
-
|
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,
|
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,55 @@ class Stream:
|
|
1050
1126
|
control_rate_hz=control_rate_hz)
|
1051
1127
|
await self.check_for_fault()
|
1052
1128
|
|
1053
|
-
|
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
|
-
|
1170
|
+
await self.restore_config(old_config)
|
1065
1171
|
|
1066
|
-
|
1172
|
+
print("Calibration complete")
|
1067
1173
|
|
1068
|
-
|
1174
|
+
try:
|
1175
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
1176
|
+
except:
|
1177
|
+
now = datetime.datetime.utcnow()
|
1069
1178
|
|
1070
1179
|
report = {
|
1071
1180
|
'timestamp' : now.strftime('%Y-%m-%d %H:%M:%S.%f'),
|
@@ -1079,12 +1188,11 @@ class Stream:
|
|
1079
1188
|
'encoder_filter_bw_hz' : enc_bw_hz,
|
1080
1189
|
'encoder_filter_kp' : enc_kp,
|
1081
1190
|
'encoder_filter_ki' : enc_ki,
|
1082
|
-
'
|
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),
|
1191
|
+
'kv' : motor_kv,
|
1086
1192
|
'unwrapped_position_scale' : unwrapped_position_scale,
|
1087
1193
|
'motor_position_output_sign' : motor_output_sign,
|
1194
|
+
'abi_version' : self.firmware.version,
|
1195
|
+
'voltage_mode_control' : voltage_mode_control,
|
1088
1196
|
}
|
1089
1197
|
|
1090
1198
|
log_filename = f"moteus-cal-{device_info['serial_number']}-{now.strftime('%Y%m%dT%H%M%S.%f')}.log"
|
@@ -1102,17 +1210,19 @@ class Stream:
|
|
1102
1210
|
|
1103
1211
|
async def find_encoder_cal_voltage(self, input_V, winding_resistance):
|
1104
1212
|
if self.args.cal_ll_encoder_voltage:
|
1105
|
-
return self.args.cal_ll_encoder_voltage
|
1213
|
+
return self.args.cal_ll_encoder_voltage,
|
1106
1214
|
|
1107
1215
|
# We're going to try and select a voltage to roughly achieve
|
1108
1216
|
# "--cal-motor-power".
|
1109
1217
|
return min(0.4 * input_V,
|
1110
|
-
math.sqrt(self.args.cal_motor_power
|
1218
|
+
math.sqrt((self.args.cal_motor_power / 1.5) *
|
1219
|
+
winding_resistance))
|
1111
1220
|
|
1112
|
-
async def calibrate_encoder_mapping(self, input_V, winding_resistance):
|
1221
|
+
async def calibrate_encoder_mapping(self, input_V, winding_resistance, current_noise):
|
1113
1222
|
# Figure out what voltage to use for encoder calibration.
|
1114
|
-
encoder_cal_voltage =
|
1115
|
-
input_V, winding_resistance)
|
1223
|
+
encoder_cal_voltage = \
|
1224
|
+
await self.find_encoder_cal_voltage(input_V, winding_resistance)
|
1225
|
+
encoder_cal_current = encoder_cal_voltage / winding_resistance
|
1116
1226
|
self.encoder_cal_voltage = encoder_cal_voltage
|
1117
1227
|
|
1118
1228
|
hall_configured = False
|
@@ -1133,12 +1243,34 @@ class Stream:
|
|
1133
1243
|
"not configured on device")
|
1134
1244
|
return await self.calibrate_encoder_mapping_hall(encoder_cal_voltage)
|
1135
1245
|
else:
|
1246
|
+
old_output_sign = None
|
1247
|
+
old_voltage_mode_control = None
|
1136
1248
|
try:
|
1137
|
-
|
1138
|
-
|
1249
|
+
if await self.is_config_supported("motor_position.output.sign"):
|
1250
|
+
# Some later parts of our calibration procedure can
|
1251
|
+
# handle a negative sign, but not the absolute encoder
|
1252
|
+
# mapping when using the current mode. Thus for now
|
1253
|
+
# we just force it to 1 and set it back when complete.
|
1254
|
+
old_output_sign = await self.read_config_int(
|
1255
|
+
"motor_position.output.sign")
|
1256
|
+
await self.command("conf set motor_position.output.sign 1")
|
1257
|
+
|
1258
|
+
old_voltage_mode_control = await self.read_config_int(
|
1259
|
+
"servo.voltage_mode_control")
|
1260
|
+
|
1261
|
+
return await self.calibrate_encoder_mapping_absolute(
|
1262
|
+
encoder_cal_voltage, encoder_cal_current, current_noise)
|
1263
|
+
finally:
|
1139
1264
|
# At least try to stop.
|
1140
1265
|
await self.command("d stop")
|
1141
|
-
|
1266
|
+
|
1267
|
+
if old_output_sign is not None:
|
1268
|
+
await self.command(
|
1269
|
+
f"conf set motor_position.output.sign {old_output_sign}")
|
1270
|
+
|
1271
|
+
await self.command(
|
1272
|
+
f"conf set servo.voltage_mode_control {old_voltage_mode_control}")
|
1273
|
+
|
1142
1274
|
|
1143
1275
|
async def calibrate_encoder_mapping_hall(self, encoder_cal_voltage):
|
1144
1276
|
if self.args.cal_motor_poles is None:
|
@@ -1164,8 +1296,8 @@ class Stream:
|
|
1164
1296
|
hall_cal_data.append(
|
1165
1297
|
(phase, motor_position.sources[commutation_source].raw))
|
1166
1298
|
|
1167
|
-
if self.args.
|
1168
|
-
with open(self.args.
|
1299
|
+
if self.args.cal_write_raw:
|
1300
|
+
with open(self.args.cal_write_raw, "wb") as f:
|
1169
1301
|
f.write(json.dumps(hall_cal_data, indent=2).encode('utf8'))
|
1170
1302
|
|
1171
1303
|
await self.command("d stop")
|
@@ -1179,17 +1311,16 @@ class Stream:
|
|
1179
1311
|
desired_direction=1 if not self.args.cal_invert else -1,
|
1180
1312
|
allow_phase_invert=allow_phase_invert)
|
1181
1313
|
|
1182
|
-
|
1183
|
-
|
1184
|
-
|
1185
|
-
|
1186
|
-
|
1187
|
-
|
1188
|
-
|
1189
|
-
|
1190
|
-
|
1191
|
-
|
1192
|
-
await self.command(f"conf set motor.offset.{i} 0")
|
1314
|
+
await self.command(f"conf set motor.poles {self.args.cal_motor_poles}")
|
1315
|
+
await self.command(f"conf set motor_position.sources.{commutation_source}.sign {cal_result.sign}")
|
1316
|
+
await self.command(f"conf set motor_position.sources.{commutation_source}.offset {cal_result.offset}")
|
1317
|
+
await self.command(f"conf set aux{aux_number}.hall.polarity {cal_result.polarity}")
|
1318
|
+
if allow_phase_invert:
|
1319
|
+
await self.command(
|
1320
|
+
f"conf set motor.phase_invert {1 if cal_result.phase_invert else 0}")
|
1321
|
+
|
1322
|
+
for i in range(64):
|
1323
|
+
await self.command(f"conf set motor.offset.{i} 0")
|
1193
1324
|
|
1194
1325
|
return cal_result
|
1195
1326
|
|
@@ -1230,14 +1361,39 @@ class Stream:
|
|
1230
1361
|
# We need to find an index.
|
1231
1362
|
await self.find_index(encoder_cal_voltage)
|
1232
1363
|
|
1233
|
-
async def calibrate_encoder_mapping_absolute(
|
1364
|
+
async def calibrate_encoder_mapping_absolute(
|
1365
|
+
self, encoder_cal_voltage, encoder_cal_current, current_noise):
|
1234
1366
|
await self.ensure_valid_theta(encoder_cal_voltage)
|
1235
1367
|
|
1236
|
-
|
1237
|
-
|
1368
|
+
current_quality_factor = encoder_cal_current / current_noise
|
1369
|
+
use_current_for_quality = (
|
1370
|
+
current_quality_factor > CURRENT_QUALITY_MIN or
|
1371
|
+
self.args.cal_force_encoder_current_mode)
|
1372
|
+
use_current_for_firmware_version = self.firmware.version >= 0x010a
|
1373
|
+
|
1374
|
+
use_current_calibration = (
|
1375
|
+
use_current_for_quality and use_current_for_firmware_version)
|
1376
|
+
|
1377
|
+
if use_current_for_firmware_version and not use_current_for_quality:
|
1378
|
+
print(f"Using voltage mode calibration, current quality factor {current_quality_factor:.1f} < {CURRENT_QUALITY_MIN:.1f}")
|
1379
|
+
|
1380
|
+
|
1381
|
+
old_motor_poles = None
|
1238
1382
|
|
1239
|
-
|
1240
|
-
|
1383
|
+
if use_current_calibration:
|
1384
|
+
old_motor_poles = await self.read_config_int("motor.poles")
|
1385
|
+
await self.command("conf set motor.poles 2")
|
1386
|
+
await self.command(f"d pos nan 0 nan c{encoder_cal_current} b1")
|
1387
|
+
await asyncio.sleep(3.0)
|
1388
|
+
|
1389
|
+
await self.write_message(
|
1390
|
+
(f"d cali i{encoder_cal_current} s{self.args.cal_ll_encoder_speed}"))
|
1391
|
+
else:
|
1392
|
+
await self.command(f"d pwm 0 {encoder_cal_voltage}")
|
1393
|
+
await asyncio.sleep(3.0)
|
1394
|
+
|
1395
|
+
await self.write_message(
|
1396
|
+
(f"d cal {encoder_cal_voltage} s{self.args.cal_ll_encoder_speed}"))
|
1241
1397
|
|
1242
1398
|
cal_data = b''
|
1243
1399
|
index = 0
|
@@ -1247,9 +1403,9 @@ class Stream:
|
|
1247
1403
|
print("Calibrating {} ".format("/-\\|"[index]), end='\r', flush=True)
|
1248
1404
|
index = (index + 1) % 4
|
1249
1405
|
cal_data += (line + b'\n')
|
1250
|
-
if line.startswith(b'CAL done'):
|
1406
|
+
if line.startswith(b'CAL done') or line.startswith(b'CALI done'):
|
1251
1407
|
break
|
1252
|
-
if line.startswith(b'CAL start'):
|
1408
|
+
if line.startswith(b'CAL start') or line.startswith(b'CALI start'):
|
1253
1409
|
continue
|
1254
1410
|
if line.startswith(b'ERR'):
|
1255
1411
|
raise RuntimeError(f'Error calibrating: {line}')
|
@@ -1257,8 +1413,8 @@ class Stream:
|
|
1257
1413
|
# Some problem
|
1258
1414
|
raise RuntimeError(f'Error calibrating: {line}')
|
1259
1415
|
|
1260
|
-
if self.args.
|
1261
|
-
with open(self.args.
|
1416
|
+
if self.args.cal_write_raw:
|
1417
|
+
with open(self.args.cal_write_raw, "wb") as f:
|
1262
1418
|
f.write(cal_data)
|
1263
1419
|
|
1264
1420
|
cal_file = ce.parse_file(io.BytesIO(cal_data))
|
@@ -1285,21 +1441,22 @@ class Stream:
|
|
1285
1441
|
f"Auto-detected pole count ({cal_result.poles}) != " +
|
1286
1442
|
f"cmdline specified ({self.args.cal_motor_poles})")
|
1287
1443
|
|
1288
|
-
|
1289
|
-
|
1290
|
-
await self.command(f"conf set motor.poles {cal_result.poles}")
|
1444
|
+
print("\nStoring encoder config")
|
1445
|
+
await self.command(f"conf set motor.poles {cal_result.poles}")
|
1291
1446
|
|
1292
|
-
|
1293
|
-
|
1294
|
-
|
1295
|
-
|
1296
|
-
|
1297
|
-
|
1298
|
-
|
1299
|
-
|
1300
|
-
|
1301
|
-
|
1302
|
-
|
1447
|
+
if await self.is_config_supported("motor_position.sources.0.sign"):
|
1448
|
+
await self.command("conf set motor_position.sources.0.sign {}".format(
|
1449
|
+
-1 if cal_result.invert else 1))
|
1450
|
+
else:
|
1451
|
+
await self.command("conf set motor.invert {}".format(
|
1452
|
+
1 if cal_result.invert else 0))
|
1453
|
+
if allow_phase_invert:
|
1454
|
+
await self.command("conf set motor.phase_invert {}".format(
|
1455
|
+
1 if cal_result.phase_invert else 0))
|
1456
|
+
for i, offset in enumerate(cal_result.offset):
|
1457
|
+
await self.command(f"conf set motor.offset.{i} {offset}")
|
1458
|
+
|
1459
|
+
cal_result.current_quality_factor = current_quality_factor
|
1303
1460
|
|
1304
1461
|
return cal_result
|
1305
1462
|
|
@@ -1317,7 +1474,7 @@ class Stream:
|
|
1317
1474
|
|
1318
1475
|
# Now get the servo_stats telemetry channel to read the D and Q
|
1319
1476
|
# currents.
|
1320
|
-
data = [extract(await self.read_servo_stats()) for _ in range(
|
1477
|
+
data = [extract(await self.read_servo_stats()) for _ in range(20)]
|
1321
1478
|
|
1322
1479
|
# Stop the current.
|
1323
1480
|
await self.command("d stop");
|
@@ -1326,53 +1483,175 @@ class Stream:
|
|
1326
1483
|
await asyncio.sleep(0.05);
|
1327
1484
|
|
1328
1485
|
current_A = sum(data) / len(data)
|
1486
|
+
noise_A = stddev(data)
|
1329
1487
|
|
1330
|
-
return current_A
|
1488
|
+
return current_A, noise_A
|
1331
1489
|
|
1332
1490
|
async def find_current_and_print(self, voltage):
|
1333
|
-
result = await self.find_current(voltage)
|
1491
|
+
result, noise = await self.find_current(voltage)
|
1334
1492
|
print(f"{voltage:.3f}V - {result:.3f}A")
|
1335
1493
|
return result
|
1336
1494
|
|
1337
|
-
async def
|
1495
|
+
async def calibrate_winding_resistance2(self, input_V):
|
1338
1496
|
print("Calculating winding resistance")
|
1339
1497
|
|
1340
|
-
|
1341
|
-
|
1342
|
-
|
1343
|
-
|
1498
|
+
# Depending upon the switching rate, there will be a region
|
1499
|
+
# around 0 current where the measured resistance is much
|
1500
|
+
# higher than actual and highly non-linear. We also want to
|
1501
|
+
# limit the maximum amount of power put into the motor per the
|
1502
|
+
# user's request. So, to balance those requirements, we start
|
1503
|
+
# at very low voltages and geometrically increase until we
|
1504
|
+
# reach the desired user power. After that point, we attempt
|
1505
|
+
# to select a region from the largest currents that is roughly
|
1506
|
+
# linear.
|
1507
|
+
|
1508
|
+
cal_voltage = 0.01
|
1509
|
+
|
1510
|
+
# This will be a list of:
|
1511
|
+
# ( voltage,
|
1512
|
+
# current,
|
1513
|
+
# step_resistance,
|
1514
|
+
# noise,
|
1515
|
+
# )
|
1516
|
+
results = []
|
1344
1517
|
|
1345
|
-
|
1518
|
+
while True:
|
1519
|
+
this_current, this_noise = await self.find_current(cal_voltage)
|
1346
1520
|
|
1347
|
-
|
1348
|
-
|
1349
|
-
|
1350
|
-
|
1521
|
+
this_resistance = None
|
1522
|
+
if len(results):
|
1523
|
+
this_resistance = ((cal_voltage - results[-1][0]) /
|
1524
|
+
(this_current - results[-1][1]))
|
1351
1525
|
|
1352
|
-
|
1353
|
-
|
1526
|
+
if not self.args.verbose:
|
1527
|
+
print(f"Tested {cal_voltage:6.3f}V resistance {this_resistance or 0:6.3f}ohms noise={this_noise:5.3f}A ",
|
1528
|
+
end='\r', flush=True)
|
1529
|
+
else:
|
1530
|
+
print(f" V={cal_voltage} I={this_current} R={this_resistance}")
|
1354
1531
|
|
1355
|
-
|
1532
|
+
results.append((cal_voltage, this_current, this_resistance, this_noise))
|
1356
1533
|
|
1357
|
-
|
1358
|
-
|
1534
|
+
power = this_current * cal_voltage * 1.5
|
1535
|
+
if (power > self.args.cal_motor_power or
|
1536
|
+
cal_voltage > (0.4 * input_V)):
|
1537
|
+
break
|
1359
1538
|
|
1360
|
-
|
1361
|
-
|
1362
|
-
|
1363
|
-
|
1364
|
-
|
1365
|
-
|
1366
|
-
|
1367
|
-
|
1368
|
-
|
1369
|
-
|
1370
|
-
|
1371
|
-
|
1372
|
-
|
1539
|
+
cal_voltage *= 1.1
|
1540
|
+
|
1541
|
+
print()
|
1542
|
+
|
1543
|
+
# If we had infinite precision, the "most correct" answer
|
1544
|
+
# would be the very last step_resistance we measured.
|
1545
|
+
# However, current noise will mean it is useful to incorporate
|
1546
|
+
# a reading from a more distant point. This is hard because
|
1547
|
+
# as we get closer to 0, the results will become highly
|
1548
|
+
# non-linear, corrupting the result.
|
1549
|
+
|
1550
|
+
# What we'll do is take the very last result, and the last
|
1551
|
+
# result that is less than 70% of the current of the last
|
1552
|
+
# result.
|
1553
|
+
|
1554
|
+
last_result = results[-1]
|
1555
|
+
|
1556
|
+
less_than = [x for x in results if x[1] < 0.60 * last_result[1]][-1]
|
1373
1557
|
|
1558
|
+
|
1559
|
+
resistance = ((last_result[0] - less_than[0]) /
|
1560
|
+
(last_result[1] - less_than[1]))
|
1561
|
+
|
1562
|
+
print(f"Resistance {resistance:.3f} ohms")
|
1563
|
+
|
1564
|
+
await self.command(f"conf set motor.resistance_ohm {resistance}")
|
1565
|
+
|
1566
|
+
return resistance, last_result[0], last_result[3]
|
1567
|
+
|
1568
|
+
|
1569
|
+
async def _test_inductance_period(self, cal_voltage, input_V, ind_period):
|
1570
|
+
ind_voltage = cal_voltage
|
1571
|
+
|
1572
|
+
if self.firmware.version < 0x010a:
|
1374
1573
|
await asyncio.wait_for(
|
1375
1574
|
self.command(f"d ind {cal_voltage} {ind_period}"), 0.25)
|
1575
|
+
else:
|
1576
|
+
# Our device supports inductance measurement from a
|
1577
|
+
# voltage offset.
|
1578
|
+
offset = min(0.2 * input_V, cal_voltage)
|
1579
|
+
ind_voltage = min(0.15 * input_V, 0.80 * cal_voltage)
|
1580
|
+
await asyncio.wait_for(
|
1581
|
+
self.command(f"d ind {ind_voltage} {ind_period} o{offset}"), 0.25)
|
1582
|
+
|
1583
|
+
|
1584
|
+
start = time.time()
|
1585
|
+
await asyncio.sleep(1.0)
|
1586
|
+
|
1587
|
+
if self.firmware.version < 0x010a:
|
1588
|
+
await self.command(f"d stop")
|
1589
|
+
else:
|
1590
|
+
# Hold the same position and fixed voltage.
|
1591
|
+
await self.command(f"d pos nan 0 nan o{offset} b1")
|
1592
|
+
|
1593
|
+
end = time.time()
|
1594
|
+
data = await self.read_servo_stats()
|
1595
|
+
|
1596
|
+
delta_time = end - start
|
1597
|
+
di_dt = data.meas_ind_integrator / delta_time
|
1598
|
+
|
1599
|
+
if self.args.verbose:
|
1600
|
+
print(f" inductance period={ind_period} v={cal_voltage} di_dt={di_dt}")
|
1601
|
+
|
1602
|
+
return di_dt, ind_voltage
|
1603
|
+
|
1604
|
+
|
1605
|
+
async def calibrate_inductance(self, cal_voltage, input_V):
|
1606
|
+
print("Calculating motor inductance")
|
1607
|
+
|
1608
|
+
old_motor_poles = await self.read_config_int("motor.poles")
|
1609
|
+
if old_motor_poles == 0:
|
1610
|
+
await self.command("conf set motor.poles 2")
|
1611
|
+
|
1612
|
+
# Sweep through a range of inductance measurement frequencies
|
1613
|
+
# until we have a peak or near peak. Rationale:
|
1614
|
+
#
|
1615
|
+
# For low inductance/low resistance motors, we can't actually
|
1616
|
+
# test very low frequencies without generating overly large
|
1617
|
+
# currents. Thus we start out at a high frequency and stop
|
1618
|
+
# when it looks like we have gotten near enough to the peak.
|
1619
|
+
# Similarly, for high resistance/low inductance motors, we can
|
1620
|
+
# only effectively measure the inductance at high frequencies,
|
1621
|
+
# otherwise the current will saturate at Vbus/resistance. For
|
1622
|
+
# high resistance / high inductance motors, we need to get all
|
1623
|
+
# the way to low frequencies before the measured current is
|
1624
|
+
# actually large enough to detect.
|
1625
|
+
|
1626
|
+
periods_to_test = [2, 3, 4, 6, 8, 10, 12, 16, 20, 24, 32]
|
1627
|
+
|
1628
|
+
highest_di_dt = None
|
1629
|
+
highest_cal_voltage = None
|
1630
|
+
since_highest = None
|
1631
|
+
|
1632
|
+
try:
|
1633
|
+
for period in periods_to_test:
|
1634
|
+
this_di_dt, this_ind_voltage = (
|
1635
|
+
await self._test_inductance_period(
|
1636
|
+
cal_voltage, input_V, period))
|
1637
|
+
|
1638
|
+
if highest_di_dt is None or this_di_dt > highest_di_dt:
|
1639
|
+
highest_di_dt = this_di_dt
|
1640
|
+
highest_cal_voltage = this_ind_voltage
|
1641
|
+
since_highest = 0
|
1642
|
+
else:
|
1643
|
+
if since_highest is not None:
|
1644
|
+
since_highest += 1
|
1645
|
+
|
1646
|
+
if self.args.verbose:
|
1647
|
+
print(f"inductance period {period} di_dt={this_di_dt}")
|
1648
|
+
|
1649
|
+
if (highest_di_dt > 0 and
|
1650
|
+
(this_di_dt < 0.5 * highest_di_dt or since_highest > 2)):
|
1651
|
+
# We stop early to avoid causing excessive ripple
|
1652
|
+
# current on low resistance / low inductance motors.
|
1653
|
+
break
|
1654
|
+
await self.command("d stop")
|
1376
1655
|
except moteus.CommandError as e:
|
1377
1656
|
# It is possible this is an old firmware that does not
|
1378
1657
|
# support inductance measurement.
|
@@ -1384,15 +1663,7 @@ class Stream:
|
|
1384
1663
|
print("Firmware does not support inductance measurement")
|
1385
1664
|
return None
|
1386
1665
|
|
1387
|
-
|
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))
|
1666
|
+
inductance = (highest_cal_voltage / highest_di_dt)
|
1396
1667
|
|
1397
1668
|
if inductance < 1e-6:
|
1398
1669
|
raise RuntimeError(f'Inductance too small ({inductance} < 1e-6)')
|
@@ -1521,7 +1792,7 @@ class Stream:
|
|
1521
1792
|
data = await self.read_servo_stats()
|
1522
1793
|
|
1523
1794
|
total_current_A = math.hypot(data.d_A, data.q_A)
|
1524
|
-
total_power_W = voltage * total_current_A
|
1795
|
+
total_power_W = voltage * total_current_A * 1.5
|
1525
1796
|
|
1526
1797
|
power_samples.append(total_power_W)
|
1527
1798
|
|
@@ -1582,8 +1853,11 @@ class Stream:
|
|
1582
1853
|
if self.args.cal_ll_kv_voltage:
|
1583
1854
|
return self.args.cal_ll_kv_voltage
|
1584
1855
|
|
1856
|
+
first_nonzero_speed_voltage = None
|
1857
|
+
|
1585
1858
|
# Otherwise, we start small, and increase until we hit a
|
1586
|
-
# reasonable speed
|
1859
|
+
# reasonable speed, but at least twice what it takes to get a
|
1860
|
+
# non-zero speed.
|
1587
1861
|
maybe_result = 0.01
|
1588
1862
|
while True:
|
1589
1863
|
print(f"Testing {maybe_result:.3f}V for Kv",
|
@@ -1592,9 +1866,17 @@ class Stream:
|
|
1592
1866
|
return maybe_result
|
1593
1867
|
|
1594
1868
|
this_speed = await self.find_speed(maybe_result) / unwrapped_position_scale
|
1869
|
+
|
1870
|
+
if (abs(this_speed) > (0.1 * self.args.cal_motor_speed) and
|
1871
|
+
first_nonzero_speed_voltage is None):
|
1872
|
+
first_nonzero_speed_voltage = maybe_result
|
1873
|
+
|
1595
1874
|
# Aim for this many Hz
|
1596
|
-
if
|
1875
|
+
if (first_nonzero_speed_voltage is not None and
|
1876
|
+
maybe_result > (2 * first_nonzero_speed_voltage) and
|
1877
|
+
abs(this_speed) > self.args.cal_motor_speed):
|
1597
1878
|
break
|
1879
|
+
|
1598
1880
|
maybe_result *= 1.1
|
1599
1881
|
|
1600
1882
|
print()
|
@@ -1614,19 +1896,34 @@ class Stream:
|
|
1614
1896
|
await self.command("conf set servopos.position_max NaN")
|
1615
1897
|
await self.command("d index 0")
|
1616
1898
|
|
1617
|
-
kv_cal_voltage = await self.find_kv_cal_voltage(
|
1899
|
+
kv_cal_voltage = await self.find_kv_cal_voltage(
|
1900
|
+
input_V, unwrapped_position_scale)
|
1618
1901
|
await self.stop_and_idle()
|
1619
1902
|
|
1620
1903
|
voltages = [x * kv_cal_voltage for x in [
|
1621
1904
|
0.0, 0.25, 0.5, 0.75, 1.0 ]]
|
1622
|
-
|
1623
|
-
|
1905
|
+
voltage_speed_hzs = list(zip(
|
1906
|
+
voltages, [ await self.find_speed_and_print(voltage, sleep_time=2)
|
1907
|
+
for voltage in voltages]))
|
1624
1908
|
|
1625
1909
|
await self.stop_and_idle()
|
1626
1910
|
|
1627
1911
|
await asyncio.sleep(0.5)
|
1628
1912
|
|
1629
|
-
|
1913
|
+
await self.command(f"conf set servopos.position_min {original_position_min}")
|
1914
|
+
await self.command(f"conf set servopos.position_max {original_position_max}")
|
1915
|
+
|
1916
|
+
# Drop any measurements that are too slow. This will
|
1917
|
+
# include (hopefully) our initial zero voltage
|
1918
|
+
# measurement, but that lets us get a more accurate read
|
1919
|
+
# on the slope.
|
1920
|
+
speed_threshold = abs(0.45 * voltage_speed_hzs[-1][1])
|
1921
|
+
voltage_speed_hzs = [(v, s) for v, s in voltage_speed_hzs
|
1922
|
+
if abs(s) > speed_threshold]
|
1923
|
+
|
1924
|
+
geared_v_per_hz = 1.0 / _calculate_slope(
|
1925
|
+
[x[0] for x in voltage_speed_hzs],
|
1926
|
+
[x[1] for x in voltage_speed_hzs])
|
1630
1927
|
|
1631
1928
|
v_per_hz = (geared_v_per_hz *
|
1632
1929
|
unwrapped_position_scale)
|
@@ -1635,23 +1932,36 @@ class Stream:
|
|
1635
1932
|
|
1636
1933
|
print(f"v_per_hz (pre-gearbox)={v_per_hz}")
|
1637
1934
|
|
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
1935
|
if v_per_hz < 0.0:
|
1642
1936
|
raise RuntimeError(
|
1643
1937
|
f"v_per_hz measured as negative ({v_per_hz}), something wrong")
|
1938
|
+
|
1939
|
+
# Experimental verification of Kv using this protocol
|
1940
|
+
# typically results in a determination of Kv roughly 14%
|
1941
|
+
# below what an open circuit spin measures with an
|
1942
|
+
# oscilloscope. That is probably due to friction in the
|
1943
|
+
# system and other non-linearities.
|
1944
|
+
FUDGE = 1.14
|
1945
|
+
motor_kv = FUDGE * 0.5 * 60 / v_per_hz
|
1644
1946
|
else:
|
1645
|
-
|
1646
|
-
print(f"Using forced Kv: {self.args.cal_force_kv}
|
1947
|
+
motor_kv = self.args.cal_force_kv
|
1948
|
+
print(f"Using forced Kv: {self.args.cal_force_kv}")
|
1647
1949
|
|
1648
|
-
if
|
1950
|
+
if self.firmware.version >= 0x010a:
|
1951
|
+
await self.command(f"conf set motor.Kv {motor_kv}")
|
1952
|
+
else:
|
1953
|
+
if self.firmware.family == 2:
|
1954
|
+
# moteus-c1 in older firmwares had additional
|
1955
|
+
# scaling.
|
1956
|
+
motor_kv /= 1.38
|
1957
|
+
|
1958
|
+
v_per_hz = (V_PER_HZ_FUDGE_010a * 0.5 * 60 / motor_kv)
|
1649
1959
|
await self.command(f"conf set motor.v_per_hz {v_per_hz}")
|
1650
1960
|
|
1651
|
-
if
|
1652
|
-
raise RuntimeError(f'
|
1961
|
+
if motor_kv < 0:
|
1962
|
+
raise RuntimeError(f'Kv value ({motor_kv}) is negative')
|
1653
1963
|
|
1654
|
-
return
|
1964
|
+
return motor_kv
|
1655
1965
|
|
1656
1966
|
async def stop_and_idle(self):
|
1657
1967
|
await self.command("d stop")
|
@@ -1701,7 +2011,10 @@ class Stream:
|
|
1701
2011
|
await self.command(f"conf set motor.offset.{index} {offset}")
|
1702
2012
|
|
1703
2013
|
await self.command(f"conf set motor.resistance_ohm {report['winding_resistance']}")
|
1704
|
-
await self.
|
2014
|
+
if await self.is_config_supported("motor.v_per_hz"):
|
2015
|
+
await self.command(f"conf set motor.v_per_hz {report['v_per_hz']}")
|
2016
|
+
elif await self.is_config_supported("motor.Kv"):
|
2017
|
+
await self.command(f"conf set motor.Kv {report['kv']}")
|
1705
2018
|
|
1706
2019
|
pid_dq_kp = report.get('pid_dq_kp', None)
|
1707
2020
|
if pid_dq_kp is not None:
|
@@ -1911,9 +2224,6 @@ async def async_main():
|
|
1911
2224
|
parser.add_argument('--cal-ll-encoder-speed',
|
1912
2225
|
metavar='HZ', type=float, default=1.0,
|
1913
2226
|
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
2227
|
parser.add_argument('--cal-ll-kv-voltage',
|
1918
2228
|
metavar='V', type=float,
|
1919
2229
|
help='maximum voltage when measuring Kv')
|
@@ -1936,10 +2246,10 @@ async def async_main():
|
|
1936
2246
|
# Internally, the above values are derived from these, combined
|
1937
2247
|
# with the approximate input voltage to the controller.
|
1938
2248
|
parser.add_argument('--cal-motor-power', metavar='W', type=float,
|
1939
|
-
default=5
|
2249
|
+
default=7.5,
|
1940
2250
|
help='motor power in W to use for encoder cal')
|
1941
2251
|
parser.add_argument('--cal-motor-speed', metavar='Hz', type=float,
|
1942
|
-
default=
|
2252
|
+
default=12.0,
|
1943
2253
|
help='max motor mechanical speed to use for kv cal')
|
1944
2254
|
|
1945
2255
|
parser.add_argument('--cal-motor-poles', metavar='N', type=int,
|
@@ -1959,8 +2269,10 @@ async def async_main():
|
|
1959
2269
|
help='maximum allowed error in calibration')
|
1960
2270
|
parser.add_argument('--cal-max-kv-power-factor', type=float,
|
1961
2271
|
default=1.25)
|
1962
|
-
parser.add_argument('--cal-raw', metavar='FILE', type=str,
|
2272
|
+
parser.add_argument('--cal-write-raw', metavar='FILE', type=str,
|
1963
2273
|
help='write raw calibration data')
|
2274
|
+
parser.add_argument('--cal-force-encoder-current-mode', action='store_true',
|
2275
|
+
help='always use encoder current mode calibration if supported')
|
1964
2276
|
|
1965
2277
|
args = parser.parse_args()
|
1966
2278
|
|
moteus/pythoncan.py
CHANGED
@@ -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
|
moteus/version.py
CHANGED
@@ -1,25 +1,23 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: moteus
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.78
|
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
|
14
12
|
Classifier: Programming Language :: Python :: 3
|
15
13
|
Requires-Python: >=3.7, <4
|
16
14
|
Description-Content-Type: text/markdown
|
17
|
-
Requires-Dist: importlib-metadata
|
18
|
-
Requires-Dist: numpy
|
19
|
-
Requires-Dist: pyelftools
|
20
|
-
Requires-Dist: pyserial
|
21
|
-
Requires-Dist: python-can
|
22
|
-
Requires-Dist: scipy
|
15
|
+
Requires-Dist: importlib-metadata >=3.6
|
16
|
+
Requires-Dist: numpy <2
|
17
|
+
Requires-Dist: pyelftools >=0.26
|
18
|
+
Requires-Dist: pyserial >=3.5
|
19
|
+
Requires-Dist: python-can >=3.3
|
20
|
+
Requires-Dist: scipy >=1.8.0
|
23
21
|
Requires-Dist: pywin32 ; platform_system == "Windows"
|
24
22
|
|
25
23
|
# Python bindings for moteus brushless controller #
|
@@ -121,5 +119,3 @@ qr.torque = moteus.F32
|
|
121
119
|
|
122
120
|
c = moteus.Controller(position_resolution=pr, query_resolution=qr)
|
123
121
|
```
|
124
|
-
|
125
|
-
|
@@ -1,23 +1,23 @@
|
|
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=Ami5e-LFw4RLoLseKcZx9QfS1PjQZJUwygvNZfPqd04,15494
|
5
5
|
moteus/command.py,sha256=UkOsbtkso6Oyex8CfbpAKpBNriik519ymxL86EZGkRs,1169
|
6
6
|
moteus/export.py,sha256=XitBUuf4MDRIneXQSUptizIhZi2BdHyFO2Vo_2d2CFI,1742
|
7
7
|
moteus/fdcanusb.py,sha256=7PrQiCTROY96gdT2zSZYU1bOCriw-I7H6NspaZpiEx4,7431
|
8
8
|
moteus/moteus.py,sha256=vImSRBn6VEmoijD6hvrSRVxuv1_xVaEJU3F_3Wi6GiE,52498
|
9
|
-
moteus/moteus_tool.py,sha256=
|
9
|
+
moteus/moteus_tool.py,sha256=AjAS0krgdv9QjflNYim0lS1kP1aGvRjN_OPiwwjFFSU,93656
|
10
10
|
moteus/multiplex.py,sha256=2tdNX5JSh21TOjN6N9LKribLQtVYyyYbXjzwXB64sfA,12119
|
11
11
|
moteus/posix_aioserial.py,sha256=2oDrw8TBEwuEQjY41g9rHeuFeffcPHqMwNS3nf5NVq8,3137
|
12
|
-
moteus/pythoncan.py,sha256=
|
12
|
+
moteus/pythoncan.py,sha256=4CZygTWU0hGx_7Av2kETdmtOa4VDe0Hpt9ZdPDbBKYw,4289
|
13
13
|
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=JXN2ng5QaWlVJIklxx1-9wW-AslvI84wOsdoAKsNRpI,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.78.dist-info/METADATA,sha256=qe6UldA-jcFnbCPVLkCp4IvnJ63ByAOVSOAmSkcbyEA,3441
|
20
|
+
moteus-0.3.78.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
21
|
+
moteus-0.3.78.dist-info/entry_points.txt,sha256=accRcwir_K8wCf7i3qHb5R6CPh5SiSgd5a1A92ibb9E,56
|
22
|
+
moteus-0.3.78.dist-info/top_level.txt,sha256=aZzmI_yecTaDrdSp29pTJuowaSQ9dlIZheQpshGg4YQ,7
|
23
|
+
moteus-0.3.78.dist-info/RECORD,,
|
File without changes
|