odrive 0.6.9.dev0__py37-none-win_amd64.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.
Files changed (49) hide show
  1. odrive/__init__.py +142 -0
  2. odrive/api_client.py +54 -0
  3. odrive/config.py +1296 -0
  4. odrive/crypto.py +369 -0
  5. odrive/data/brakeRs.json +18 -0
  6. odrive/data/drvs.json +82 -0
  7. odrive/data/encoders.json +86 -0
  8. odrive/data/motors.json +128 -0
  9. odrive/data/odrive-micro-x1.json +48 -0
  10. odrive/data/odrive-micro-x3.json +40 -0
  11. odrive/data/odrive-micro-x4.json +40 -0
  12. odrive/data/odrive-s1-x4.json +52 -0
  13. odrive/data/odrive-v4.4.json +58 -0
  14. odrive/data/schema-brakeR.json +6 -0
  15. odrive/data/schema-drv.json +6 -0
  16. odrive/data/schema-encoders.json +6 -0
  17. odrive/data/schema-motors.json +6 -0
  18. odrive/data/schema-odrive.json +5 -0
  19. odrive/data/schema.json +244 -0
  20. odrive/database.py +184 -0
  21. odrive/dfu.py +195 -0
  22. odrive/dfuse/DfuDevice.py +297 -0
  23. odrive/dfuse/__init__.py +1 -0
  24. odrive/enums.py +415 -0
  25. odrive/firmware.py +102 -0
  26. odrive/hw_version.py +67 -0
  27. odrive/legacy.py +73 -0
  28. odrive/legacy_config.py +123 -0
  29. odrive/legacy_dfu.py +442 -0
  30. odrive/lib/libodrive-windows-x64.dll +0 -0
  31. odrive/libodrive.py +325 -0
  32. odrive/pyfibre/fibre/__init__.py +4 -0
  33. odrive/pyfibre/fibre/libfibre-windows-amd64.dll +0 -0
  34. odrive/pyfibre/fibre/libfibre.py +1078 -0
  35. odrive/pyfibre/fibre/libwinpthread-1.dll +0 -0
  36. odrive/pyfibre/fibre/shell.py +162 -0
  37. odrive/pyfibre/fibre/utils.py +133 -0
  38. odrive/release_api.py +184 -0
  39. odrive/rich_text.py +134 -0
  40. odrive/shell.py +144 -0
  41. odrive/utils.py +881 -0
  42. odrive/version.py +1 -0
  43. odrive-0.6.9.dev0.data/scripts/odrive_demo.py +50 -0
  44. odrive-0.6.9.dev0.data/scripts/odrivetool +263 -0
  45. odrive-0.6.9.dev0.data/scripts/odrivetool.bat +2 -0
  46. odrive-0.6.9.dev0.dist-info/METADATA +22 -0
  47. odrive-0.6.9.dev0.dist-info/RECORD +49 -0
  48. odrive-0.6.9.dev0.dist-info/WHEEL +5 -0
  49. odrive-0.6.9.dev0.dist-info/top_level.txt +1 -0
odrive/utils.py ADDED
@@ -0,0 +1,881 @@
1
+
2
+ import asyncio
3
+ import struct
4
+ import sys
5
+ import time
6
+ import threading
7
+ import platform
8
+ import re
9
+ import subprocess
10
+ import os
11
+ import odrive.enums
12
+ from odrive.enums import *
13
+ import odrive.database
14
+ import odrive.legacy
15
+ from odrive.rich_text import RichText, Color, Style
16
+ from odrive.hw_version import HwVersion
17
+ from typing import Callable, List
18
+
19
+ try:
20
+ if platform.system() == 'Windows':
21
+ import win32console
22
+ import colorama
23
+ colorama.init()
24
+ except ImportError:
25
+ print("Could not init terminal features.")
26
+ print("Refer to install instructions at http://docs.odriverobotics.com/#downloading-and-installing-tools")
27
+ sys.stdout.flush()
28
+ pass
29
+
30
+ _db = None
31
+
32
+ def fw_version_str_to_tuple(version_string):
33
+ """
34
+ Converts a version string to a tuple of the form
35
+ (major, minor, revision, prerelease)
36
+
37
+ Example: "fw-v0.3.6-23" => (0, 3, 6, True)
38
+
39
+ If version_string does not match the pattern above, this function throws an
40
+ Exception.
41
+ """
42
+ regex=r'.*v([0-9]+)\.([0-9]+)\.([0-9]+)(.*)'
43
+ if not re.match(regex, version_string):
44
+ raise Exception()
45
+ return (int(re.sub(regex, r"\1", version_string)),
46
+ int(re.sub(regex, r"\2", version_string)),
47
+ int(re.sub(regex, r"\3", version_string)),
48
+ (re.sub(regex, r"\4", version_string) != ""))
49
+
50
+ async def get_serial_number_str(device):
51
+ if hasattr(device, '_serial_number_property'):
52
+ return format(await device._serial_number_property.read(), 'x').upper()
53
+ else:
54
+ return "[unknown serial number]"
55
+
56
+ def get_serial_number_str_sync(device):
57
+ if hasattr(device, '_serial_number_property'):
58
+ return format(device._serial_number_property.read(), 'x').upper()
59
+ else:
60
+ return "[unknown serial number]"
61
+
62
+ async def attach_metadata(device):
63
+ global _db
64
+ device.__sealed__ = False
65
+ try:
66
+ if hasattr(device, '_hw_version_major_property') and hasattr(device, '_hw_version_minor_property') and hasattr(device, '_hw_version_variant_property'):
67
+ device._board = HwVersion(
68
+ await device._hw_version_major_property.read(),
69
+ await device._hw_version_minor_property.read(),
70
+ await device._hw_version_variant_property.read()
71
+ )
72
+
73
+ if _db is None:
74
+ _db = odrive.database.load()
75
+
76
+ try:
77
+ metadata = _db.get_product(device._board)
78
+ except odrive.database.NotFoundError:
79
+ metadata = None
80
+
81
+ if not metadata is None:
82
+ device._metadata = metadata
83
+ for name in [a for a in dir(device) if a.startswith("axis")]:
84
+ axis = getattr(device, name)
85
+ axis.__sealed__ = False
86
+ try:
87
+ axis._metadata = metadata['inverters'][int(name[4:])]
88
+ finally:
89
+ axis.__sealed__ = True
90
+ else:
91
+ print('Device has no hw_version properties')
92
+
93
+ if hasattr(device, '_fw_version_major_property') and hasattr(device, '_fw_version_minor_property') and hasattr(device, '_fw_version_revision_property'):
94
+ device._fw_version = (
95
+ await device._fw_version_major_property.read(),
96
+ await device._fw_version_minor_property.read(),
97
+ await device._fw_version_revision_property.read()
98
+ )
99
+ else:
100
+ device._fw_version = None
101
+ print('Device has no fw_version properties')
102
+
103
+ if hasattr(device, '_serial_number_property'):
104
+ device._serial_number = "{:08X}".format(await device._serial_number_property.read())
105
+ else:
106
+ print('Device has no serial number properties')
107
+ finally:
108
+ device.__sealed__ = True
109
+
110
+
111
+
112
+ def calculate_thermistor_coeffs(degree, Rload, R_25, Beta, Tmin, Tmax, thermistor_bottom = False, plot = False):
113
+ import numpy as np
114
+ T_25 = 25 + 273.15 #Kelvin
115
+ temps = np.linspace(Tmin, Tmax, 1000)
116
+ tempsK = temps + 273.15
117
+
118
+ # https://en.wikipedia.org/wiki/Thermistor#B_or_%CE%B2_parameter_equation
119
+ r_inf = R_25 * np.exp(-Beta/T_25)
120
+ R_temps = r_inf * np.exp(Beta/tempsK)
121
+ if thermistor_bottom:
122
+ V = R_temps / (Rload + R_temps)
123
+ else:
124
+ V = Rload / (Rload + R_temps)
125
+
126
+ fit = np.polyfit(V, temps, degree)
127
+ p1 = np.poly1d(fit)
128
+ fit_temps = p1(V)
129
+
130
+ if plot:
131
+ import matplotlib.pyplot as plt
132
+ print(fit)
133
+ plt.plot(V, temps, label='actual')
134
+ plt.plot(V, fit_temps, label='fit')
135
+ plt.xlabel('normalized voltage')
136
+ plt.ylabel('Temp [C]')
137
+ plt.legend(loc=0)
138
+ plt.show()
139
+
140
+ return p1
141
+
142
+ class OperationAbortedException(Exception):
143
+ pass
144
+
145
+ def set_motor_thermistor_coeffs(axis, Rload, R_25, Beta, Tmin, Tmax, thermistor_bottom = True):
146
+ coeffs = calculate_thermistor_coeffs(3, Rload, R_25, Beta, Tmin, Tmax, thermistor_bottom)
147
+ axis.motor.motor_thermistor.config.poly_coefficient_0 = float(coeffs[3])
148
+ axis.motor.motor_thermistor.config.poly_coefficient_1 = float(coeffs[2])
149
+ axis.motor.motor_thermistor.config.poly_coefficient_2 = float(coeffs[1])
150
+ axis.motor.motor_thermistor.config.poly_coefficient_3 = float(coeffs[0])
151
+
152
+ def decode_drv_faults(metadata, code):
153
+ active_flags = []
154
+ i = 0
155
+ for bit, name in metadata['faults']:
156
+ if code & (1 << bit):
157
+ active_flags.append(name)
158
+ code ^= (1 << bit)
159
+ assert(code == 0)
160
+
161
+ if len(active_flags) == len(metadata['faults']):
162
+ return "unpowered"
163
+ if any(active_flags):
164
+ return ", ".join(active_flags)
165
+ else:
166
+ return "None"
167
+
168
+ def get_issues(odrv):
169
+ return [odrv.issues.get(i) for i in range(odrv.issues.length)]
170
+
171
+ def format_errors(odrv, clear=False):
172
+ """
173
+ Returns a summary of the error status of the device as RichText.
174
+ """
175
+ is_legacy_firmware = (odrv.fw_version_major, odrv.fw_version_minor) < (0, 6)
176
+ if is_legacy_firmware:
177
+ return odrive.legacy.format_errors(odrv, clear)
178
+
179
+ lines = []
180
+
181
+ STYLE_GOOD = (Color.GREEN, Color.DEFAULT, Style.BOLD)
182
+ STYLE_WARN = (Color.YELLOW, Color.DEFAULT, Style.BOLD)
183
+ STYLE_BAD = (Color.RED, Color.DEFAULT, Style.BOLD)
184
+
185
+ axes = [(name, getattr(odrv, name)) for name in dir(odrv) if name.startswith('axis')]
186
+ axes.sort()
187
+
188
+ def decode_flags(val, enum_type):
189
+ errorcodes = {v.value: f"{enum_type.__name__}.{v.name}" for v in enum_type}
190
+ if val == 0:
191
+ return [RichText("no error", *STYLE_GOOD)]
192
+ else:
193
+ return [RichText("Error(s):", *STYLE_BAD)] + [
194
+ RichText(errorcodes.get((1 << bit), 'UNKNOWN ERROR: 0x{:08X}'.format(1 << bit)), *STYLE_BAD)
195
+ for bit in range(64) if val & (1 << bit) != 0]
196
+
197
+ def decode_enum(val, enum_type):
198
+ errorcodes = {v.value: f"{enum_type.__name__}.{v.name}" for v in enum_type}
199
+ return [RichText(errorcodes.get(val, 'Unknown value: ' + str(val)), *(STYLE_GOOD if (val == 0) else STYLE_BAD))]
200
+
201
+ def decode_drv_fault(axis, val):
202
+ if val == 0:
203
+ return [RichText("none", *STYLE_GOOD)]
204
+ elif not hasattr(axis, '_metadata'):
205
+ return [RichText("metadata not loaded", *STYLE_WARN)]
206
+ else:
207
+ return [RichText(odrive.utils.decode_drv_faults(axis._metadata['drv'], val), *STYLE_BAD)]
208
+
209
+ def dump_item(indent, name, obj, path, decoder):
210
+ prefix = indent + name.strip('0123456789') + ": "
211
+ for elem in path.split('.'):
212
+ if not hasattr(obj, elem):
213
+ return []
214
+ obj = getattr(obj, elem)
215
+
216
+ lines = decoder(obj)
217
+ lines = [indent + name + ": " + lines[0]] + [
218
+ indent + " " + line
219
+ for line in lines[1:]
220
+ ]
221
+ return lines
222
+
223
+ lines += dump_item("", "system", odrv, 'error', lambda x: decode_flags(x, odrive.enums.ODriveError))
224
+
225
+ for name, axis in axes:
226
+ lines.append(name)
227
+ lines += dump_item(" ", 'error', axis, 'error', lambda x: decode_flags(x, odrive.enums.AxisError)) # this is 0.6.0 legacy
228
+ lines += dump_item(" ", 'active_errors', axis, 'active_errors', lambda x: decode_flags(x, odrive.enums.ODriveError))
229
+ lines += dump_item(" ", 'disarm_reason', axis, 'disarm_reason', lambda x: decode_flags(x, odrive.enums.ODriveError))
230
+ lines += dump_item(" ", 'procedure_result', axis, 'procedure_result', lambda x: decode_enum(x, odrive.enums.ProcedureResult))
231
+ lines += dump_item(" ", 'last_drv_fault', axis, 'last_drv_fault', lambda x: decode_drv_fault(axis, x))
232
+
233
+ if hasattr(odrv, 'issues') and hasattr(odrv.issues, 'length') and hasattr(odrv.issues, 'get'):
234
+ issues = get_issues(odrv)
235
+ if len(issues) == 0:
236
+ lines.append("internal issues: " + RichText("none", *STYLE_GOOD))
237
+ else:
238
+ lines.append("internal issues: " + RichText(str(len(issues)), *STYLE_BAD))
239
+ lines.append("details for bug report: " + RichText(str(issues), *STYLE_WARN))
240
+
241
+ if clear:
242
+ odrv.clear_errors()
243
+
244
+ if clear:
245
+ odrv.clear_errors()
246
+
247
+ return RichText('\n').join(lines)
248
+
249
+ def dump_errors(odrv, clear=False):
250
+ """
251
+ Prints a summary of the error status of the device on stdout.
252
+ """
253
+ odrive.rich_text.print_rich_text(format_errors(odrv, clear))
254
+
255
+ def oscilloscope_dump(odrv, num_vals, filename='oscilloscope.csv'):
256
+ with open(filename, 'w') as f:
257
+ for x in range(num_vals):
258
+ f.write(str(odrv.oscilloscope.get_val(x)))
259
+ f.write('\n')
260
+
261
+ data_rate = 100
262
+ plot_rate = 1
263
+ num_samples = 200
264
+ def start_liveplotter(get_var_callback: Callable, legend: List[str] = None):
265
+ """
266
+ Starts a liveplotter.
267
+ The variable that is plotted is retrieved from get_var_callback.
268
+ This function returns immediately and the liveplotter quits when
269
+ the user closes it.
270
+ """
271
+
272
+ import numpy as np
273
+ import matplotlib.pyplot as plt
274
+ import matplotlib.animation as animation
275
+
276
+ cancellation_token = threading.Event()
277
+
278
+ global vals
279
+ vals = get_var_callback()
280
+ legend_vals = (legend + [str(n+1) for n in range(len(legend), len(vals))]) if legend else list(range(len(vals)))
281
+ vals = []
282
+ def fetch_data():
283
+ global vals
284
+ while not cancellation_token.is_set():
285
+ try:
286
+ data = get_var_callback()
287
+ except Exception as ex:
288
+ print(str(ex))
289
+ time.sleep(1)
290
+ continue
291
+ vals.append(data)
292
+ while len(vals) > num_samples:
293
+ vals.pop(0)
294
+
295
+ time.sleep(1/data_rate)
296
+
297
+ def plot_data():
298
+ global vals
299
+
300
+ def closed(evt):
301
+ cancellation_token.set()
302
+
303
+ plt.ion()
304
+ fig, ax = plt.subplots()
305
+ fig.canvas.mpl_connect('close_event', closed)
306
+ lines = ax.plot(vals)
307
+ ax.legend(legend_vals, loc=1)
308
+
309
+ def animate(*fargs):
310
+ data = np.array(vals)
311
+ x_data = range(len(data[:,0]))
312
+ ax.set_xlim(min(x_data), max(x_data))
313
+ if np.all(np.isnan(data)):
314
+ y_min, y_max = (0, 1)
315
+ else:
316
+ y_min = np.nanmin(data)
317
+ y_max = np.nanmax(data)
318
+ y_delta = y_max - y_min
319
+ ax.set_ylim(bottom=y_min - 0.1 * y_delta, top=y_max + 0.1 * y_delta)
320
+ for index, line in enumerate(lines):
321
+ line.set_data(x_data, data[:,index])
322
+
323
+ _ = animation.FuncAnimation(fig, animate, interval=plot_rate)
324
+
325
+ while not cancellation_token.is_set():
326
+ fig.canvas.draw()
327
+ fig.canvas.start_event_loop(1/plot_rate)
328
+
329
+ fig.canvas.stop_event_loop()
330
+
331
+ fetch_t = threading.Thread(target=fetch_data)
332
+ fetch_t.daemon = True
333
+ fetch_t.start()
334
+
335
+ plot_t = threading.Thread(target=plot_data)
336
+ plot_t.daemon = True
337
+ plot_t.start()
338
+
339
+ return cancellation_token
340
+
341
+
342
+ class BulkCapture:
343
+ '''
344
+ Asynchronously captures a bulk set of data when instance is created.
345
+
346
+ get_var_callback: a function that returns the data you want to collect (see the example below)
347
+ data_rate: Rate in hz
348
+ length: Length of time to capture in seconds
349
+
350
+ Example Usage:
351
+ capture = BulkCapture(lambda :[odrv0.axis0.encoder.pos_estimate, odrv0.axis0.controller.pos_setpoint])
352
+ # Do stuff while capturing (like sending position commands)
353
+ capture.event.wait() # When you're done doing stuff, wait for the capture to be completed.
354
+ print(capture.data) # Do stuff with the data
355
+ capture.plot_data() # Helper method to plot the data
356
+ '''
357
+
358
+ def __init__(self,
359
+ get_var_callback,
360
+ data_rate=500.0,
361
+ duration=2.0):
362
+ import numpy as np
363
+
364
+ self.get_var_callback = get_var_callback
365
+ self.event = threading.Event()
366
+ def loop():
367
+ vals = []
368
+ start_time = time.monotonic()
369
+ period = 1.0 / data_rate
370
+ while time.monotonic() - start_time < duration:
371
+ try:
372
+ data = get_var_callback()
373
+ except Exception as ex:
374
+ print(str(ex))
375
+ print("Waiting 1 second before next data point")
376
+ time.sleep(1)
377
+ continue
378
+ relative_time = time.monotonic() - start_time
379
+ vals.append([relative_time] + data)
380
+ time.sleep(period - (relative_time % period)) # this ensures consistently timed samples
381
+ self.data = np.array(vals) # A lock is not really necessary due to the event
382
+ print("Capture complete")
383
+ achieved_data_rate = len(self.data) / self.data[-1, 0]
384
+ if achieved_data_rate < (data_rate * 0.9):
385
+ print("Achieved average data rate: {}Hz".format(achieved_data_rate))
386
+ print("If this rate is significantly lower than what you specified, consider lowering it below the achieved value for more consistent sampling.")
387
+ self.event.set() # tell the main thread that the bulk capture is complete
388
+ threading.Thread(target=loop, daemon=True).start()
389
+
390
+ def plot(self):
391
+ import matplotlib.pyplot as plt
392
+ import inspect
393
+ from textwrap import wrap
394
+ plt.plot(self.data[:,0], self.data[:,1:])
395
+ plt.xlabel("Time (seconds)")
396
+ title = (str(inspect.getsource(self.get_var_callback))
397
+ .strip("['\\n']")
398
+ .split(" = ")[1])
399
+ plt.title("\n".join(wrap(title, 60)))
400
+ plt.legend(range(self.data.shape[1]-1))
401
+ plt.show()
402
+
403
+
404
+ def step_and_plot( axis,
405
+ step_size=100.0,
406
+ settle_time=0.5,
407
+ data_rate=500.0,
408
+ ctrl_mode=ControlMode.POSITION_CONTROL):
409
+
410
+ if ctrl_mode == ControlMode.POSITION_CONTROL:
411
+ get_var_callback = lambda :[axis.encoder.pos_estimate, axis.controller.pos_setpoint]
412
+ initial_setpoint = axis.encoder.pos_estimate
413
+ def set_setpoint(setpoint):
414
+ axis.controller.pos_setpoint = setpoint
415
+ elif ctrl_mode == ControlMode.VELOCITY_CONTROL:
416
+ get_var_callback = lambda :[axis.encoder.vel_estimate, axis.controller.vel_setpoint]
417
+ initial_setpoint = 0
418
+ def set_setpoint(setpoint):
419
+ axis.controller.vel_setpoint = setpoint
420
+ else:
421
+ print("Invalid control mode")
422
+ return
423
+
424
+ initial_settle_time = 0.5
425
+ initial_control_mode = axis.controller.config.control_mode # Set it back afterwards
426
+ print(initial_control_mode)
427
+ axis.controller.config.control_mode = ctrl_mode
428
+ axis.requested_state = AxisState.CLOSED_LOOP_CONTROL
429
+
430
+ capture = BulkCapture(get_var_callback,
431
+ data_rate=data_rate,
432
+ duration=initial_settle_time + settle_time)
433
+
434
+ set_setpoint(initial_setpoint)
435
+ time.sleep(initial_settle_time)
436
+ set_setpoint(initial_setpoint + step_size) # relative/incremental movement
437
+
438
+ capture.event.wait() # wait for Bulk Capture to be complete
439
+
440
+ axis.requested_state = AxisState.IDLE
441
+ axis.controller.config.control_mode = initial_control_mode
442
+ capture.plot()
443
+
444
+
445
+ def print_drv_regs(name, motor):
446
+ """
447
+ Dumps the current gate driver regisers for the specified motor
448
+ """
449
+ fault = motor.gate_driver.drv_fault
450
+ status_reg_1 = motor.gate_driver.status_reg_1
451
+ status_reg_2 = motor.gate_driver.status_reg_2
452
+ ctrl_reg_1 = motor.gate_driver.ctrl_reg_1
453
+ ctrl_reg_2 = motor.gate_driver.ctrl_reg_2
454
+ print(name + ": " + str(fault))
455
+ print("DRV Fault Code: " + str(fault))
456
+ print("Status Reg 1: " + str(status_reg_1) + " (" + format(status_reg_1, '#010b') + ")")
457
+ print("Status Reg 2: " + str(status_reg_2) + " (" + format(status_reg_2, '#010b') + ")")
458
+ print("Control Reg 1: " + str(ctrl_reg_1) + " (" + format(ctrl_reg_1, '#013b') + ")")
459
+ print("Control Reg 2: " + str(ctrl_reg_2) + " (" + format(ctrl_reg_2, '#09b') + ")")
460
+
461
+ def show_oscilloscope(odrv):
462
+ size = 18000
463
+ values = []
464
+ for i in range(size):
465
+ values.append(odrv.oscilloscope.get_val(i))
466
+
467
+ import matplotlib.pyplot as plt
468
+ plt.plot(values)
469
+ plt.show()
470
+
471
+ def rate_test(device):
472
+ """
473
+ Tests how many integers per second can be transmitted
474
+ """
475
+
476
+ # import matplotlib.pyplot as plt
477
+ # plt.ion()
478
+
479
+ print("reading 10000 values...")
480
+ numFrames = 10000
481
+ vals = []
482
+ for _ in range(numFrames):
483
+ vals.append(device.n_evt_control_loop)
484
+
485
+ loopsPerFrame = (vals[-1] - vals[0])/numFrames
486
+ loopsPerSec = (168000000/(6*3500))
487
+ FramePerSec = loopsPerSec/loopsPerFrame
488
+ print("Frames per second: " + str(FramePerSec))
489
+
490
+ # plt.plot(vals)
491
+ # plt.show(block=True)
492
+
493
+ def usb_burn_in_test(get_var_callback, cancellation_token):
494
+ """
495
+ Starts background threads that read a values form the USB device in a spin-loop
496
+ """
497
+
498
+ def fetch_data():
499
+ global vals
500
+ i = 0
501
+ while not cancellation_token.is_set():
502
+ try:
503
+ get_var_callback()
504
+ i += 1
505
+ except Exception as ex:
506
+ print(str(ex))
507
+ time.sleep(1)
508
+ i = 0
509
+ continue
510
+ if i % 1000 == 0:
511
+ print("read {} values".format(i))
512
+ threading.Thread(target=fetch_data, daemon=True).start()
513
+
514
+ def yes_no_prompt(question, default=None):
515
+ if default is None:
516
+ question += " [y/n] "
517
+ elif default == True:
518
+ question += " [Y/n] "
519
+ elif default == False:
520
+ question += " [y/N] "
521
+
522
+ while True:
523
+ print(question, end='')
524
+
525
+ choice = input().lower()
526
+ if choice in {'yes', 'y'}:
527
+ return True
528
+ elif choice in {'no', 'n'}:
529
+ return False
530
+ elif choice == '' and default is not None:
531
+ return default
532
+
533
+ def dump_interrupts(odrv):
534
+ interrupts = [
535
+ (-12, "MemoryManagement_IRQn"),
536
+ (-11, "BusFault_IRQn"),
537
+ (-10, "UsageFault_IRQn"),
538
+ (-5, "SVCall_IRQn"),
539
+ (-4, "DebugMonitor_IRQn"),
540
+ (-2, "PendSV_IRQn"),
541
+ (-1, "SysTick_IRQn"),
542
+ (0, "WWDG_IRQn"),
543
+ (1, "PVD_IRQn"),
544
+ (2, "TAMP_STAMP_IRQn"),
545
+ (3, "RTC_WKUP_IRQn"),
546
+ (4, "FLASH_IRQn"),
547
+ (5, "RCC_IRQn"),
548
+ (6, "EXTI0_IRQn"),
549
+ (7, "EXTI1_IRQn"),
550
+ (8, "EXTI2_IRQn"),
551
+ (9, "EXTI3_IRQn"),
552
+ (10, "EXTI4_IRQn"),
553
+ (11, "DMA1_Stream0_IRQn"),
554
+ (12, "DMA1_Stream1_IRQn"),
555
+ (13, "DMA1_Stream2_IRQn"),
556
+ (14, "DMA1_Stream3_IRQn"),
557
+ (15, "DMA1_Stream4_IRQn"),
558
+ (16, "DMA1_Stream5_IRQn"),
559
+ (17, "DMA1_Stream6_IRQn"),
560
+ (18, "ADC_IRQn"),
561
+ (19, "CAN1_TX_IRQn"),
562
+ (20, "CAN1_RX0_IRQn"),
563
+ (21, "CAN1_RX1_IRQn"),
564
+ (22, "CAN1_SCE_IRQn"),
565
+ (23, "EXTI9_5_IRQn"),
566
+ (24, "TIM1_BRK_TIM9_IRQn"),
567
+ (25, "TIM1_UP_TIM10_IRQn"),
568
+ (26, "TIM1_TRG_COM_TIM11_IRQn"),
569
+ (27, "TIM1_CC_IRQn"),
570
+ (28, "TIM2_IRQn"),
571
+ (29, "TIM3_IRQn"),
572
+ (30, "TIM4_IRQn"),
573
+ (31, "I2C1_EV_IRQn"),
574
+ (32, "I2C1_ER_IRQn"),
575
+ (33, "I2C2_EV_IRQn"),
576
+ (34, "I2C2_ER_IRQn"),
577
+ (35, "SPI1_IRQn"),
578
+ (36, "SPI2_IRQn"),
579
+ (37, "USART1_IRQn"),
580
+ (38, "USART2_IRQn"),
581
+ (39, "USART3_IRQn"),
582
+ (40, "EXTI15_10_IRQn"),
583
+ (41, "RTC_Alarm_IRQn"),
584
+ (42, "OTG_FS_WKUP_IRQn"),
585
+ (43, "TIM8_BRK_TIM12_IRQn"),
586
+ (44, "TIM8_UP_TIM13_IRQn"),
587
+ (45, "TIM8_TRG_COM_TIM14_IRQn"),
588
+ (46, "TIM8_CC_IRQn"),
589
+ (47, "DMA1_Stream7_IRQn"),
590
+ (48, "FMC_IRQn"),
591
+ (49, "SDMMC1_IRQn"),
592
+ (50, "TIM5_IRQn"),
593
+ (51, "SPI3_IRQn"),
594
+ (52, "UART4_IRQn"),
595
+ (53, "UART5_IRQn"),
596
+ (54, "TIM6_DAC_IRQn"),
597
+ (55, "TIM7_IRQn"),
598
+ (56, "DMA2_Stream0_IRQn"),
599
+ (57, "DMA2_Stream1_IRQn"),
600
+ (58, "DMA2_Stream2_IRQn"),
601
+ (59, "DMA2_Stream3_IRQn"),
602
+ (60, "DMA2_Stream4_IRQn"),
603
+ (61, "ETH_IRQn"),
604
+ (62, "ETH_WKUP_IRQn"),
605
+ (63, "CAN2_TX_IRQn"),
606
+ (64, "CAN2_RX0_IRQn"),
607
+ (65, "CAN2_RX1_IRQn"),
608
+ (66, "CAN2_SCE_IRQn"),
609
+ (67, "OTG_FS_IRQn"),
610
+ (68, "DMA2_Stream5_IRQn"),
611
+ (69, "DMA2_Stream6_IRQn"),
612
+ (70, "DMA2_Stream7_IRQn"),
613
+ (71, "USART6_IRQn"),
614
+ (72, "I2C3_EV_IRQn"),
615
+ (73, "I2C3_ER_IRQn"),
616
+ (74, "OTG_HS_EP1_OUT_IRQn"),
617
+ (75, "OTG_HS_EP1_IN_IRQn"),
618
+ (76, "OTG_HS_WKUP_IRQn"),
619
+ (77, "OTG_HS_IRQn"),
620
+ # gap
621
+ (80, "RNG_IRQn"),
622
+ (81, "FPU_IRQn"),
623
+ (82, "UART7_IRQn"),
624
+ (83, "UART8_IRQn"),
625
+ (84, "SPI4_IRQn"),
626
+ (85, "SPI5_IRQn"),
627
+ # gap
628
+ (87, "SAI1_IRQn"),
629
+ # gap
630
+ (91, "SAI2_IRQn"),
631
+ (92, "QUADSPI_IRQn"),
632
+ (93, "LPTIM1_IRQn"),
633
+ # gap
634
+ (103, "SDMMC2_IRQn"),
635
+ (117, "TIM16_IRQn")
636
+ ]
637
+
638
+ print("| # | Name | Prio | En | Count |")
639
+ print("|-----|-------------------------|------|----|---------|")
640
+ for irqn, irq_name in interrupts:
641
+ status = odrv.get_interrupt_status(irqn)
642
+ if (status != 0):
643
+ print("| {} | {} | {} | {} | {} |".format(
644
+ str(irqn).rjust(3),
645
+ irq_name.ljust(23),
646
+ str(status & 0xff).rjust(4),
647
+ " *" if (status & 0x80000000) else " ",
648
+ str((status >> 8) & 0x7fffff).rjust(7)))
649
+
650
+ def dump_threads(odrv):
651
+ prefixes = ["max_stack_usage_", "stack_size_", "prio_"]
652
+ keys = [k[len(prefix):] for k in dir(odrv.system_stats) for prefix in prefixes if k.startswith(prefix)]
653
+ good_keys = set([k for k in set(keys) if keys.count(k) == len(prefixes)])
654
+ if len(good_keys) > len(set(keys)):
655
+ print("Warning: incomplete thread information for threads {}".format(set(keys) - good_keys))
656
+
657
+ print("| Name | Stack Size [B] | Max Ever Stack Usage [B] | Prio |")
658
+ print("|---------|----------------|--------------------------|------|")
659
+ for k in sorted(good_keys):
660
+ sz = getattr(odrv.system_stats, "stack_size_" + k)
661
+ use = getattr(odrv.system_stats, "max_stack_usage_" + k)
662
+ print("| {} | {} | {} | {} |".format(
663
+ k.ljust(7),
664
+ str(sz).rjust(14),
665
+ "{} ({:.1f}%)".format(use, use / sz * 100).rjust(24),
666
+ str(getattr(odrv.system_stats, "prio_" + k)).rjust(4)
667
+ ))
668
+
669
+
670
+ def dump_dma(odrv):
671
+ if odrv.hw_version_major == 3:
672
+ dma_functions = [[
673
+ # https://www.st.com/content/ccc/resource/technical/document/reference_manual/3d/6d/5a/66/b4/99/40/d4/DM00031020.pdf/files/DM00031020.pdf/jcr:content/translations/en.DM00031020.pdf Table 42
674
+ ["SPI3_RX", "-", "SPI3_RX", "SPI2_RX", "SPI2_TX", "SPI3_TX", "-", "SPI3_TX"],
675
+ ["I2C1_RX", "-", "TIM7_UP", "-", "TIM7_UP", "I2C1_RX", "I2C1_TX", "I2C1_TX"],
676
+ ["TIM4_CH1", "-", "I2S3_EXT_RX", "TIM4_CH2", "I2S2_EXT_TX", "I2S3_EXT_TX", "TIM4_UP", "TIM4_CH3"],
677
+ ["I2S3_EXT_RX", "TIM2_UP/TIM2_CH3", "I2C3_RX", "I2S2_EXT_RX", "I2C3_TX", "TIM2_CH1", "TIM2_CH2/TIM2_CH4", "TIM2_UP/TIM2_CH4"],
678
+ ["UART5_RX", "USART3_RX", "UART4_RX", "USART3_TX", "UART4_TX", "USART2_RX", "USART2_TX", "UART5_TX"],
679
+ ["UART8_TX", "UART7_TX", "TIM3_CH4/TIM3_UP", "UART7_RX", "TIM3_CH1/TIM3_TRIG", "TIM3_CH2", "UART8_RX", "TIM3_CH3"],
680
+ ["TIM5_CH3/TIM5_UP", "TIM5_CH4/TIM5_TRIG", "TIM5_CH1", "TIM5_CH4/TIM5_TRIG", "TIM5_CH2", "-", "TIM5_UP", "-"],
681
+ ["-", "TIM6_UP", "I2C2_RX", "I2C2_RX", "USART3_TX", "DAC1", "DAC2", "I2C2_TX"],
682
+ ], [
683
+ # https://www.st.com/content/ccc/resource/technical/document/reference_manual/3d/6d/5a/66/b4/99/40/d4/DM00031020.pdf/files/DM00031020.pdf/jcr:content/translations/en.DM00031020.pdf Table 43
684
+ ["ADC1", "SAI1_A", "TIM8_CH1/TIM8_CH2/TIM8_CH3", "SAI1_A", "ADC1", "SAI1_B", "TIM1_CH1/TIM1_CH2/TIM1_CH3", "-"],
685
+ ["-", "DCMI", "ADC2", "ADC2", "SAI1_B", "SPI6_TX", "SPI6_RX", "DCMI"],
686
+ ["ADC3", "ADC3", "-", "SPI5_RX", "SPI5_TX", "CRYP_OUT", "CRYP_IN", "HASH_IN"],
687
+ ["SPI1_RX", "-", "SPI1_RX", "SPI1_TX", "-", "SPI1_TX", "-", "-"],
688
+ ["SPI4_RX", "SPI4_TX", "USART1_RX", "SDIO", "-", "USART1_RX", "SDIO", "USART1_TX"],
689
+ ["-", "USART6_RX", "USART6_RX", "SPI4_RX", "SPI4_TX", "-", "USART6_TX", "USART6_TX"],
690
+ ["TIM1_TRIG", "TIM1_CH1", "TIM1_CH2", "TIM1_CH1", "TIM1_CH4/TIM1_TRIG/TIM1_COM", "TIM1_UP", "TIM1_CH3", "-"],
691
+ ["-", "TIM8_UP", "TIM8_CH1", "TIM8_CH2", "TIM8_CH3", "SPI5_RX", "SPI5_TX", "TIM8_CH4/TIM8_TRIG/TIM8_COM"],
692
+ ]]
693
+ elif odrv.hw_version_major == 4:
694
+ dma_functions = [[
695
+ # https://www.st.com/resource/en/reference_manual/dm00305990-stm32f72xxx-and-stm32f73xxx-advanced-armbased-32bit-mcus-stmicroelectronics.pdf Table 26
696
+ ["SPI3_RX", "-", "SPI3_RX", "SPI2_RX", "SPI2_TX", "SPI3_TX", "-", "SPI3_TX"],
697
+ ["I2C1_RX", "I2C3_RX", "TIM7_UP", "-", "TIM7_UP", "I2C1_RX", "I2C1_TX", "I2C1_TX"],
698
+ ["TIM4_CH1", "-", "-", "TIM4_CH2", "-", "-", "TIM4_UP", "TIM4_CH3"],
699
+ ["-", "TIM2_UP/TIM2_CH3", "I2C3_RX", "-", "I2C3_TX", "TIM2_CH1", "TIM2_CH2/TIM2_CH4", "TIM2_UP/TIM2_CH4"],
700
+ ["UART5_RX", "USART3_RX", "UART4_RX", "USART3_TX", "UART4_TX", "USART2_RX", "USART2_TX", "UART5_TX"],
701
+ ["UART8_TX", "UART7_TX", "TIM3_CH4/TIM3_UP", "UART7_RX", "TIM3_CH1/TIM3_TRIG", "TIM3_CH2", "UART8_RX", "TIM3_CH3"],
702
+ ["TIM5_CH3/TIM5_UP", "TIM5_CH4/TIM5_TRIG", "TIM5_CH1", "TIM5_CH4/TIM5_TRIG", "TIM5_CH2", "-", "TIM5_UP", "-"],
703
+ ["-", "TIM6_UP", "I2C2_RX", "I2C2_RX", "USART3_TX", "DAC1", "DAC2", "I2C2_TX"],
704
+ ], [
705
+ # https://www.st.com/resource/en/reference_manual/dm00305990-stm32f72xxx-and-stm32f73xxx-advanced-armbased-32bit-mcus-stmicroelectronics.pdf Table 27
706
+ ["ADC1", "SAI1_A", "TIM8_CH1/TIM8_CH2/TIM8_CH3", "SAI1_A", "ADC1", "SAI1_B", "TIM1_CH1/TIM1_CH2/TIM1_CH3", "SAI2_B"],
707
+ ["-", "-", "ADC2", "ADC2", "SAI1_B", "-", "-", "-"],
708
+ ["ADC3", "ADC3", "-", "SPI5_RX", "SPI5_TX", "AES_OUT", "AES_IN", "-"],
709
+ ["SPI1_RX", "-", "SPI1_RX", "SPI1_TX", "SAI2_A", "SPI1_TX", "SAI2_B", "QUADSPI"],
710
+ ["SPI4_RX", "SPI4_TX", "USART1_RX", "SDMMC1", "-", "USART1_RX", "SDMMC1", "USART1_TX"],
711
+ ["-", "USART6_RX", "USART6_RX", "SPI4_RX", "SPI4_TX", "-", "USART6_TX", "USART6_TX"],
712
+ ["TIM1_TRIG", "TIM1_CH1", "TIM1_CH2", "TIM1_CH1", "TIM1_CH4/TIM1_TRIG/TIM1_COM", "TIM1_UP", "TIM1_CH3", "-"],
713
+ ["-", "TIM8_UP", "TIM8_CH1", "TIM8_CH2", "TIM8_CH3", "SPI5_RX", "SPI5_TX", "TIM8_CH4/TIM8_TRIG/TIM8_COM"],
714
+ None,
715
+ None,
716
+ None,
717
+ ["SDMMC2", "-", "-", "-", "-", "SDMMC2", "-", "-"],
718
+ ]]
719
+
720
+ print("| Name | Prio | Channel | Configured |")
721
+ print("|--------------|------|----------------------------------|------------|")
722
+ for stream_num in range(16):
723
+ status = odrv.get_dma_status(stream_num)
724
+ if (status != 0):
725
+ channel = (status >> 2) & 0x7
726
+ ch_name = dma_functions[stream_num >> 3][channel][stream_num & 0x7]
727
+ print("| DMA{}_Stream{} | {} | {} {} | {} |".format(
728
+ (stream_num >> 3) + 1,
729
+ (stream_num & 0x7),
730
+ (status & 0x3),
731
+ channel,
732
+ ("(" + ch_name + ")").ljust(30),
733
+ "*" if (status & 0x80000000) else " "))
734
+
735
+ def dump_timing(odrv, n_samples=100, path='/tmp/timings.png', reset_timings=False):
736
+ import matplotlib.pyplot as plt
737
+ import re
738
+ import numpy as np
739
+
740
+ timings = []
741
+
742
+ for attr in dir(odrv.task_times):
743
+ if not attr.startswith('_'):
744
+ timings.append((attr, getattr(odrv.task_times, attr), [], [])) # (name, obj, start_times, lengths)
745
+ for k in dir(odrv):
746
+ if re.match(r'axis[0-9]+', k):
747
+ for attr in dir(getattr(odrv, k).task_times):
748
+ if not attr.startswith('_'):
749
+ timings.append((k + '.' + attr, getattr(getattr(odrv, k).task_times, attr), [], [])) # (name, obj, start_times, lengths)
750
+
751
+ # reset max-timings
752
+ if reset_timings:
753
+ for name, obj, start_times, lengths in timings:
754
+ obj.max_length = 0
755
+
756
+ # Take a couple of samples
757
+ print("sampling...")
758
+ for i in range(n_samples):
759
+ odrv.task_timers_armed = True # Trigger sample and wait for it to finish
760
+ while odrv.task_timers_armed: pass
761
+ for name, obj, start_times, lengths in timings:
762
+ start_times.append(obj.start_time)
763
+ lengths.append(obj.length)
764
+ print("done")
765
+
766
+ # sort by start time
767
+ timings = sorted(timings, key = lambda x: np.mean(x[2]))
768
+
769
+ plt.rcParams['figure.figsize'] = 21, 9
770
+ plt.figure()
771
+ plt.grid(True)
772
+ plt.barh(
773
+ [-i for i in range(len(timings))], # y positions
774
+ [np.mean(lengths) for name, obj, start_times, lengths in timings], # lengths
775
+ left = [np.mean(start_times) for name, obj, start_times, lengths in timings], # starts
776
+ xerr = (
777
+ [np.std(lengths) for name, obj, start_times, lengths in timings], # error bars to the left side
778
+ [(min(obj.max_length, 20100) - np.mean(lengths)) for name, obj, start_times, lengths in timings], # error bars to the right side - TODO: remove artificial min()
779
+ ),
780
+ tick_label = [name for name, obj, start_times, lengths in timings], # labels
781
+ )
782
+ plt.savefig(path, bbox_inches='tight')
783
+
784
+
785
+ def _ensure_debug_info(odrv):
786
+ try:
787
+ import odrive_private.more_utils
788
+ except ImportError:
789
+ raise NotImplementedError()
790
+ odrive_private.more_utils.ensure_debug_info(odrv)
791
+
792
+ async def ram_osci_config(odrv, expressions):
793
+ max_addresses = len(odrv.oscilloscope.config._inputs) - 1
794
+ assert len(expressions) > 0
795
+ assert len(expressions) <= max_addresses
796
+
797
+ addresses = [0] * len(expressions)
798
+ for i in range(len(expressions)):
799
+ if isinstance(expressions[i], int):
800
+ addresses[i] = expressions[i]
801
+ else:
802
+ _ensure_debug_info(odrv)
803
+ addresses[i], _ = await odrv._dbg_info.resolve_symbol(expressions[i])
804
+
805
+ print(f"Recording memory locations " + ", ".join(['0x{:08x}'.format(addr) for addr in addresses]) + "...")
806
+
807
+ odrv.oscilloscope.config(*addresses, *([0] * (max_addresses - len(expressions))))
808
+
809
+ def ram_osci_trigger(odrv, trigger_point: float):
810
+ assert trigger_point >= 0 and trigger_point <= 1
811
+ odrv.oscilloscope.trigger(trigger_point)
812
+
813
+ async def ram_osci_download(odrv, expressions):
814
+ while odrv.oscilloscope.recording:
815
+ await asyncio.sleep(0.1)
816
+
817
+ type_infos = [None] * len(expressions)
818
+ for i in range(len(expressions)):
819
+ if isinstance(expressions[i], int):
820
+ class RegTypeInfo:
821
+ byte_size = 4
822
+ def bytes_to_val(self, b):
823
+ return int.from_bytes(b, 'little')
824
+ type_infos[i] = RegTypeInfo()
825
+ else:
826
+ _ensure_debug_info(odrv)
827
+ _, type_infos[i] = await odrv._dbg_info.resolve_symbol(expressions[i])
828
+
829
+ print(f"Fetching buffer...")
830
+
831
+ buf = b''
832
+ for i in range(0, odrv.oscilloscope.size, 32):
833
+ buf += struct.pack('<QQQQ', *odrv.oscilloscope.get_raw(i))
834
+
835
+ outputs = []
836
+ offset = 0
837
+ sample_size = sum(4 for type_info in type_infos)
838
+ while True:
839
+ elem = ()
840
+ for type_info in type_infos:
841
+ byte_size = type_info.byte_size
842
+ if offset + byte_size > len(buf):
843
+ elem = None
844
+ break
845
+ elem += (type_info.bytes_to_val(buf[offset:(offset+byte_size)]),)
846
+ assert byte_size <= 4
847
+ offset += max(byte_size, 4)
848
+ if elem is None:
849
+ break
850
+ outputs.append(elem)
851
+
852
+ # Rotate
853
+ pos = int(odrv.oscilloscope.pos / sample_size)
854
+ outputs_part0 = outputs[:pos]
855
+ outputs_part1 = outputs[pos:]
856
+ if odrv.oscilloscope.rollover:
857
+ outputs = outputs_part1 + outputs_part0
858
+ else:
859
+ outputs = outputs_part0
860
+
861
+ sampling_freq = 8000
862
+ print(f"Collected {len(outputs)} samples, which corresponds to {len(outputs)/sampling_freq*1000:.2f} ms of data (assuming a sampling frequency of {sampling_freq})")
863
+
864
+ # transpose output
865
+ return [[outputs[i][j] for i in range(len(outputs))] for j in range(len(outputs[0]))]
866
+
867
+ async def ram_osci_run(odrv, expressions, trigger_point=0.0):
868
+ """
869
+ Configures the RAM oscilloscope with the specified expressions, optionally
870
+ triggers it and waits for it to finish before downloading the data.
871
+
872
+ trigger_point: A number in [0.0, 1.0] or None.
873
+ If None, it is expected that a custom trigger in firmware is used after
874
+ configuring the osci.
875
+ """
876
+ await ram_osci_config(odrv, expressions)
877
+ if not trigger_point is None:
878
+ ram_osci_trigger(odrv, trigger_point)
879
+ else:
880
+ print("waiting for in-firmware trigger...")
881
+ return await ram_osci_download(odrv, expressions)