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.
- {moteus-0.3.76 → moteus-0.3.77}/PKG-INFO +1 -5
- {moteus-0.3.76 → moteus-0.3.77}/moteus/calibrate_encoder.py +12 -4
- {moteus-0.3.76 → moteus-0.3.77}/moteus/moteus_tool.py +461 -152
- {moteus-0.3.76 → moteus-0.3.77}/moteus/pythoncan.py +5 -0
- {moteus-0.3.76 → moteus-0.3.77}/moteus/version.py +1 -1
- {moteus-0.3.76 → moteus-0.3.77}/moteus.egg-info/PKG-INFO +1 -5
- {moteus-0.3.76 → moteus-0.3.77}/moteus.egg-info/entry_points.txt +0 -1
- {moteus-0.3.76 → moteus-0.3.77}/setup.py +1 -1
- {moteus-0.3.76 → moteus-0.3.77}/README.md +0 -0
- {moteus-0.3.76 → moteus-0.3.77}/moteus/__init__.py +0 -0
- {moteus-0.3.76 → moteus-0.3.77}/moteus/aioserial.py +0 -0
- {moteus-0.3.76 → moteus-0.3.77}/moteus/aiostream.py +0 -0
- {moteus-0.3.76 → moteus-0.3.77}/moteus/command.py +0 -0
- {moteus-0.3.76 → moteus-0.3.77}/moteus/export.py +0 -0
- {moteus-0.3.76 → moteus-0.3.77}/moteus/fdcanusb.py +0 -0
- {moteus-0.3.76 → moteus-0.3.77}/moteus/moteus.py +0 -0
- {moteus-0.3.76 → moteus-0.3.77}/moteus/multiplex.py +0 -0
- {moteus-0.3.76 → moteus-0.3.77}/moteus/posix_aioserial.py +0 -0
- {moteus-0.3.76 → moteus-0.3.77}/moteus/reader.py +0 -0
- {moteus-0.3.76 → moteus-0.3.77}/moteus/regression.py +0 -0
- {moteus-0.3.76 → moteus-0.3.77}/moteus/router.py +0 -0
- {moteus-0.3.76 → moteus-0.3.77}/moteus/transport.py +0 -0
- {moteus-0.3.76 → moteus-0.3.77}/moteus/win32_aioserial.py +0 -0
- {moteus-0.3.76 → moteus-0.3.77}/moteus.egg-info/SOURCES.txt +0 -0
- {moteus-0.3.76 → moteus-0.3.77}/moteus.egg-info/dependency_links.txt +0 -0
- {moteus-0.3.76 → moteus-0.3.77}/moteus.egg-info/requires.txt +0 -0
- {moteus-0.3.76 → moteus-0.3.77}/moteus.egg-info/top_level.txt +0 -0
- {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.
|
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]
|
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
|
|
@@ -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,52 @@ 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
|
-
now = datetime.datetime.
|
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
|
-
'
|
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
|
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 =
|
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
|
-
|
1138
|
-
|
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
|
-
|
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.
|
1168
|
-
with open(self.args.
|
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
|
-
|
1183
|
-
|
1184
|
-
|
1185
|
-
|
1186
|
-
|
1187
|
-
|
1188
|
-
|
1189
|
-
|
1190
|
-
|
1191
|
-
|
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(
|
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
|
-
|
1237
|
-
|
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
|
-
|
1240
|
-
|
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.
|
1261
|
-
with open(self.args.
|
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
|
-
|
1289
|
-
|
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
|
-
|
1293
|
-
|
1294
|
-
|
1295
|
-
|
1296
|
-
|
1297
|
-
|
1298
|
-
|
1299
|
-
|
1300
|
-
|
1301
|
-
|
1302
|
-
|
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(
|
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
|
1492
|
+
async def calibrate_winding_resistance2(self, input_V):
|
1338
1493
|
print("Calculating winding resistance")
|
1339
1494
|
|
1340
|
-
|
1341
|
-
|
1342
|
-
|
1343
|
-
|
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
|
-
|
1515
|
+
while True:
|
1516
|
+
this_current, this_noise = await self.find_current(cal_voltage)
|
1346
1517
|
|
1347
|
-
|
1348
|
-
|
1349
|
-
|
1350
|
-
|
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
|
-
|
1353
|
-
|
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
|
-
|
1529
|
+
results.append((cal_voltage, this_current, this_resistance, this_noise))
|
1356
1530
|
|
1357
|
-
|
1358
|
-
|
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
|
-
|
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
|
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(
|
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
|
-
|
1623
|
-
|
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
|
-
|
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
|
-
|
1646
|
-
print(f"Using forced Kv: {self.args.cal_force_kv}
|
1944
|
+
motor_kv = self.args.cal_force_kv
|
1945
|
+
print(f"Using forced Kv: {self.args.cal_force_kv}")
|
1647
1946
|
|
1648
|
-
if
|
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
|
1652
|
-
raise RuntimeError(f'
|
1958
|
+
if motor_kv < 0:
|
1959
|
+
raise RuntimeError(f'Kv value ({motor_kv}) is negative')
|
1653
1960
|
|
1654
|
-
return
|
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.
|
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
|
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=
|
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
|
@@ -1,13 +1,11 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: moteus
|
3
|
-
Version: 0.3.
|
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
|
-
|
@@ -25,7 +25,7 @@ long_description = (here / 'README.md').read_text(encoding='utf-8')
|
|
25
25
|
|
26
26
|
setuptools.setup(
|
27
27
|
name = 'moteus',
|
28
|
-
version = "0.3.
|
28
|
+
version = "0.3.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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|