├── README.md ├── LICENSE.md ├── ODrive Python GUI 051.py └── ODrive Python GUI 056.py /README.md: -------------------------------------------------------------------------------- 1 | Before you can use the GUI, you need to install tkinter tooltips and odrive: 2 | 3 | > pip install tkinter-tooltip 4 | 5 | > pip install odrive 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This code, created by Stijn Carelsbergh is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. 2 | 3 | You are free to: 4 | - Share: copy and redistribute the material in any medium or format 5 | - Adapt: remix, transform, and build upon the material 6 | 7 | Under the following terms: 8 | - Attribution: You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. 9 | - NonCommercial: You may not use the material for commercial purposes. 10 | - ShareAlike: If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original. 11 | 12 | For more details, see the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License: 13 | https://creativecommons.org/licenses/by-nc-sa/4.0/ 14 | -------------------------------------------------------------------------------- /ODrive Python GUI 051.py: -------------------------------------------------------------------------------- 1 | import odrive 2 | from odrive.utils import * 3 | import tkinter as tk 4 | from tkinter import ttk 5 | import tkinter.font as tkFont 6 | from tktooltip import ToolTip 7 | import time 8 | 9 | width = 1000 10 | height = 500 11 | 12 | currentrow = 0 13 | currentsection = 0 14 | 15 | ###################################### odrive dicts ###################################### 16 | 17 | motortypes = {"MOTOR_TYPE_HIGH_CURRENT": 0, "MOTOR_TYPE_GIMBAL": 2, "MOTOR_TYPE_ACIM": 3} 18 | axisstates = {"UNDEFINED": 0, "IDLE": 1, "STARTUP_SEQUENCE": 2, "FULL_CALIBRATION_SEQUENCE": 3, "MOTOR_CALIBRATION": 4, "ENCODER_INDEX_SEARCH": 6, "ENCODER_OFFSET_CALIBRATION": 7, "CLOSED_LOOP_CONTROL": 8, "LOCKIN_SPIN": 9, "INPUT_TORQUE_CONTROL": 10, "INPUT_VOLTAGE_CONTROL": 11, "CURRENT_CONTROL": 12, "VELOCITY_CONTROL": 13, "POSITION_CONTROL": 14, "TRAJECTORY_CONTROL": 15, "DISCOVERING_USB": 16, "UNKNOWN": 255} 19 | encodermode = {"ENCODER_MODE_INCREMENTAL": 0, "ENCODER_MODE_HALL": 1, "ENCODER_MODE_SINCOS": 2, "ENCODER_MODE_SPI_ABS_CUI": 256, "ENCODER_MODE_SPI_ABS_AMS": 257, "ENCODER_MODE_SPI_ABS_AEAT": 258, "ENCODER_MODE_SPI_ABS_AEAT_MARK": 259, "ENCODER_MODE_SPI_ABS_CUI_2048": 260, "ENCODER_MODE_SPI_ABS_CUI_4096": 261, "ENCODER_MODE_SPI_ABS_CUI_8192": 262, "ENCODER_MODE_SPI_ABS_AMS_512": 263, "ENCODER_MODE_SPI_ABS_AMS_1024": 264, "ENCODER_MODE_SPI_ABS_AMS_2048": 265, "ENCODER_MODE_SPI_ABS_AMS_4096": 266, "ENCODER_MODE_SPI_ABS_AMS_8192": 267, "ENCODER_MODE_SPI_ABS_AMS_16384": 268, "ENCODER_MODE_SPI_ABS_AMS_32768": 269, "ENCODER_MODE_SPI_ABS_AMS_65536": 270, "ENCODER_MODE_SPI_ABS_AMS_131072": 271, "ENCODER_MODE_SPI_ABS_AMS_262144": 272, "ENCODER_MODE_SPI_ABS_AMS_524288": 273, "ENCODER_MODE_SPI_ABS_AMS_1048576": 274, "ENCODER_MODE_SPI_ABS_AMS_2097152": 275, "ENCODER_MODE_SPI_ABS_AMS_4194304": 276, "ENCODER_MODE_SPI_ABS_AMS_8388608": 277, "ENCODER_MODE_SPI_ABS_AMS_16777216": 278, "ENCODER_MODE_SPI_ABS_AMS_33554432": 279, "ENCODER_MODE_SPI_ABS_AMS_67108864": 280, "ENCODER_MODE_SPI_ABS_AMS_134217728": 281, "ENCODER_MODE_SPI_ABS_AMS_268435456": 282, "ENCODER_MODE_SPI_ABS_AMS_MASK": 4294901760} 20 | controlmodes = {"CTRL_MODE_VOLTAGE_CONTROL": 0, "CTRL_MODE_TORQUE_CONTROL": 1, "CTRL_MODE_VELOCITY_CONTROL": 2, "CTRL_MODE_POSITION_CONTROL": 3, "CTRL_MODE_TRAJECTORY_CONTROL": 4} 21 | inputmodes = {"INPUT_MODE_INACTIVE": 0, "INPUT_MODE_PASSTHROUGH": 1, "INPUT_MODE_VEL_RAMP": 2, "INPUT_MODE_POS_FILTER": 3} 22 | bools = { "True": 1, "False": 0} 23 | board_errors = {"NONE": 0, "CONTROL_ITERATION_MISSED": 1, "DC_BUS_UNDER_VOLTAGE": 2, "DC_BUS_OVER_VOLTAGE": 4, "DC_BUS_OVER_REGEN_CURRENT": 8, "DC_BUS_OVER_CURRENT": 16, "BRAKE_DEADTIME_VIOLATION": 32, "BRAKE_DUTY_CYCLE_NAN": 64, "INVALID_BRAKE_RESISTANCE": 128} 24 | axis_errors = {"NONE": 0, "INVALID_STATE": 1, "WATCHDOG_TIMER_EXPIRED": 2048, "MIN_ENDSTOP_PRESSED": 4096, "MAX_ENDSTOP_PRESSED": 8192, "ESTOP_REQUESTED": 16384, "HOMING_WITHOUT_ENDSTOP": 131072, "OVER_TEMP": 262144, "UNKNOWN_POSITION": 524288} 25 | motor_errors = {"NONE": 0, "PHASE_RESISTANCE_OUT_OF_RANGE": 1, "PHASE_INDUCTANCE_OUT_OF_RANGE": 2, "DRV_FAULT": 8, "CONTROL_DEADLINE_MISSED": 16, "MODULATION_MAGNITUDE": 128, "CURRENT_SENSE_SATURATION": 1024, "CURRENT_LIMIT_VIOLATION": 4096, "MODULATION_IS_NAN": 65536, "MOTOR_THERMISTOR_OVER_TEMP": 131072, "FET_THERMISTOR_OVER_TEMP": 262144, "TIMER_UPDATE_MISSED": 524288, "CURRENT_MEASUREMENT_UNAVAILABLE": 1048576, "CONTROLLER_FAILED": 2097152, "I_BUS_OUT_OF_RANGE": 4194304, "BRAKE_RESISTOR_DISARMED": 8388608, "SYSTEM_LEVEL": 16777216, "BAD_TIMING": 33554432, "UNKNOWN_PHASE_ESTIMATE": 67108864, "UNKNOWN_PHASE_VEL": 134217728, "UNKNOWN_TORQUE": 268435456, "UNKNOWN_CURRENT_COMMAND": 536870912, "UNKNOWN_CURRENT_MEASUREMENT": 1073741824, "UNKNOWN_VBUS_VOLTAGE": 2147483648, "UNKNOWN_VOLTAGE_COMMAND": 4294967296, "UNKNOWN_GAINS": 8589934592, "CONTROLLER_INITIALIZING": 17179869184, "UNBALANCED_PHASES": 34359738368} 26 | encoder_errors = {"NONE": 0, "UNSTABLE_GAIN": 1, "CPR_POLEPAIRS_MISMATCH": 2, "NO_RESPONSE": 4, "UNSUPPORTED_ENCODER_MODE": 8, "ILLEGAL_HALL_STATE": 16, "INDEX_NOT_FOUND_YET": 32, "ABS_SPI_TIMEOUT": 64, "ABS_SPI_COM_FAIL": 128, "ABS_SPI_NOT_READY": 256, "HALL_NOT_CALIBRATED_YET": 512} 27 | controller_errors = {"NONE": 0, "OVERSPEED": 1, "INVALID_INPUT_MODE": 2, "UNSTABLE_GAIN": 4, "INVALID_MIRROR_AXIS": 8, "INVALID_LOAD_ENCODER": 16, "INVALID_ESTIMATE": 32, "INVALID_CIRCULAR_RANGE": 64, "SPINOUT_DETECTED": 128} 28 | 29 | ######################################## objects ######################################### 30 | 31 | error_objects = [] 32 | 33 | ##################################### dict functions ##################################### 34 | 35 | def getkey(value, dict): 36 | matching_keys = [key for key, val in dict.items() if val == value] 37 | if len(matching_keys) == 1: 38 | return matching_keys[0] 39 | elif len(matching_keys) == 0: 40 | raise ValueError(f"No key found for value {value}") 41 | else: 42 | raise ValueError(f"Multiple keys found for value {value}: {matching_keys}") 43 | 44 | def getvalue(key, dict): 45 | matching_value = dict.get(key) 46 | if matching_value is not None: 47 | return matching_value 48 | else: 49 | raise ValueError(f"No value found for key {key}") 50 | 51 | ######################################### functions ######################################### 52 | 53 | def labelobject(container, label_text, info, row, column): 54 | label = tk.Button(container) 55 | label["bg"] = "#f0f0f0" 56 | label["font"] = tkFont.Font(family='Consolas', size=16) 57 | label["fg"] = "#000000" 58 | label["anchor"] = "w" 59 | label["text"] = label_text 60 | label["relief"] = "flat" 61 | label.grid(row=row, column=column, sticky='ew', padx=4, pady=4) 62 | if info != "": 63 | ToolTip(label, msg=info, font=tkFont.Font(family='Consolas', size=16)) 64 | return label 65 | 66 | def labelframeobject(container, section_name, row, column): 67 | labelframe = tk.LabelFrame(container) 68 | labelframe["bg"] = "#f0f0f0" 69 | labelframe["fg"] = "#000000" 70 | labelframe["text"] = section_name 71 | labelframe.configure(font=tkFont.Font(family='Consolas', size=18, weight="bold")) 72 | labelframe.grid(row=row, column=column, sticky='ew', padx=4, pady=4) 73 | return labelframe 74 | 75 | def entryobject(container, row, column): 76 | entry = tk.Entry(container) 77 | entry["borderwidth"] = "1px" 78 | entry["font"] = tkFont.Font(family='Consolas', size=16) 79 | entry["fg"] = "#333333" 80 | entry["justify"] = "center" 81 | entry["relief"] = "solid" 82 | entry.grid(row=row, column=column, sticky='nsew', padx=4, pady=4) 83 | return entry 84 | 85 | def dropdownobject(container, row, column, options): 86 | variable = tk.StringVar(container) 87 | variable.set(options[0]) 88 | dropdown = tk.OptionMenu(container, variable, *options) 89 | dropdown["relief"] = "solid" 90 | dropdown["borderwidth"] = "1px" 91 | dropdown["font"] = tkFont.Font(family='Consolas', size=16) 92 | container.nametowidget(dropdown.menuname).config(font=tkFont.Font(family='Consolas', size=16)) 93 | dropdown["highlightthickness"] = "0px" 94 | dropdown.grid(row=row, column=column, sticky='nsew', padx=4, pady=4) 95 | return variable 96 | 97 | def buttonobject(container, row, column, command, text, columnspan=1): 98 | button = tk.Button(container) 99 | button["bg"] = "#f0f0f0" 100 | button["font"] = tkFont.Font(family='Consolas', size=16) 101 | button["fg"] = "#000000" 102 | button["justify"] = "center" 103 | button["text"] = str(text) 104 | button["command"] = command 105 | button["relief"] = "solid" 106 | button["borderwidth"] = "1px" 107 | button.grid(row=row, column=column, sticky='nsew', padx=4, pady=4, columnspan=columnspan) 108 | return button 109 | 110 | def sendbuttonobject(container, row, column, command): 111 | buttonobject(container, row, column, command, "send") 112 | 113 | def displayobject(container, row, column, value): 114 | display = tk.Label(container) 115 | display["font"] = tkFont.Font(family='Consolas', size=16) 116 | display["fg"] = "#333333" 117 | display["anchor"] = "w" 118 | display["text"] = value 119 | display.grid(row=row, column=column, sticky='ewns', padx=4, pady=4) 120 | return display 121 | 122 | def update_setting(path, value): 123 | exec(f'board.{path} = {value}') 124 | 125 | def submit_entry(entry, display, path): 126 | value = entry.get() 127 | update_setting(path, value) 128 | display.configure(text=value) 129 | entry.delete(0, tk.END) 130 | 131 | def submit_dropdown(variable, dict, display, path): 132 | value = getvalue(variable.get(), dict) 133 | update_setting(path, value) 134 | display.configure(text=getkey(value, dict)) 135 | 136 | def submit_requested_state(requested_state, dict, path): 137 | value = getvalue(requested_state, dict) 138 | update_setting(path, value) 139 | 140 | def onsavereboot(container): 141 | global board 142 | container.grab_set() 143 | try: 144 | board.save_configuration() 145 | except: 146 | board = odrive.find_any() 147 | container.grab_release() 148 | 149 | def onupdate_errors(): 150 | global board 151 | for error in error_objects: 152 | display = error[0] 153 | dict = error[1] 154 | if dict == axis_errors: 155 | try: 156 | update = getkey(board.axis0.error, axis_errors) 157 | except: 158 | update = board.axis0.error 159 | elif dict == motor_errors: 160 | try: 161 | update = getkey(board.axis0.motor.error, motor_errors) 162 | except: 163 | update = board.axis0.motor.error 164 | elif dict == encoder_errors: 165 | try: 166 | update = getkey(board.axis0.encoder.error, encoder_errors) 167 | except: 168 | update = board.axis0.encoder.error 169 | elif dict == controller_errors: 170 | try: 171 | update = getkey(board.axis0.controller.error, controller_errors) 172 | except: 173 | update = board.axis0.controller.error 174 | display.configure(text=update) 175 | 176 | def ontestposition(): 177 | global board 178 | submit_requested_state("CTRL_MODE_POSITION_CONTROL", controlmodes, "axis0.controller.config.control_mode") 179 | board.axis0.controller.input_pos = 1 180 | time.sleep(1) 181 | board.axis0.controller.input_pos = 0 182 | 183 | def ontestvelocity(): 184 | global board 185 | submit_requested_state("CTRL_MODE_VELOCITY_CONTROL", controlmodes, "axis0.controller.config.control_mode") 186 | board.axis0.controller.input_vel = 1 187 | time.sleep(1) 188 | board.axis0.controller.input_vel = 0 189 | 190 | ###################################### GUI sections ###################################### 191 | 192 | def labelframe_section(container, name): 193 | global currentrow 194 | global currentsection 195 | currentrow = 0 196 | currentsection += 1 197 | labelframe = labelframeobject(container, name, currentsection, 0) 198 | return labelframe 199 | 200 | ######################################### GUI rows ######################################### 201 | 202 | def textbox_setting(container, label_text, path, info): 203 | global currentrow 204 | label = labelobject(container, label_text, info, currentrow, 0) 205 | entry = entryobject(container, currentrow, 1) 206 | display = displayobject(container, currentrow, 3, round(eval(f'board.{path}'), 5)) 207 | button = sendbuttonobject(container, currentrow, 2, lambda: submit_entry(entry, display, path)) 208 | currentrow += 1 209 | 210 | def dropdown_setting(container, label_text, path, info, dict): 211 | global currentrow 212 | label = labelobject(container, label_text, info, currentrow, 0) 213 | variable = dropdownobject(container, currentrow, 1, list(dict.keys())) 214 | display = displayobject(container, currentrow, 3, getkey(eval(f'board.{path}'), dict)) 215 | button = sendbuttonobject(container, currentrow, 2, lambda: submit_dropdown(variable, dict, display, path)) 216 | currentrow += 1 217 | 218 | def requested_state(container, requested_state, dict, column=0, columnspan=1): 219 | global currentrow 220 | button = buttonobject(container, currentrow, column, lambda: submit_requested_state(requested_state, dict, "axis0.requested_state"), requested_state, columnspan) 221 | currentrow += 1 222 | 223 | def error_display(container, label_text, error, dict): 224 | global currentrow 225 | label = labelobject(container, label_text, "", currentrow, 0) 226 | try: 227 | errormessage = getkey(error, dict) 228 | except: 229 | errormessage = error 230 | display = displayobject(container, currentrow, 1, errormessage) 231 | error_objects.append([display, dict]) 232 | currentrow += 1 233 | return display 234 | 235 | def test_position(container, button_text, dict): 236 | global currentrow 237 | button = buttonobject(container, currentrow, 0, lambda: ontestposition(), button_text, 1) 238 | currentrow += 1 239 | 240 | def test_velocity(container, label_text, dict): 241 | global currentrow 242 | button = buttonobject(container, currentrow, 0, lambda: ontestvelocity(), label_text, 1) 243 | currentrow += 1 244 | 245 | def erase_configuration(container, column, columnspan): 246 | global currentrow 247 | erase_configuration_button = tk.Button(container) 248 | erase_configuration_button["bg"] = "#f0f0f0" 249 | erase_configuration_button["font"] = tkFont.Font(family='Consolas', size=16) 250 | erase_configuration_button["fg"] = "#000000" 251 | erase_configuration_button["justify"] = "center" 252 | erase_configuration_button["text"] = "Erase configuration" 253 | erase_configuration_button.grid(row=currentrow, column=column, columnspan=columnspan, sticky='ew', padx=4, pady=4) 254 | erase_configuration_button["command"] = lambda: eval("board.erase_configuration()") 255 | erase_configuration_button["relief"] = "solid" 256 | erase_configuration_button["borderwidth"] = "1px" 257 | currentrow += 1 258 | 259 | def save_config_reboot(container, column, columnspan): 260 | global currentrow 261 | save_config_reboot_button = tk.Button(container) 262 | save_config_reboot_button["bg"] = "#f0f0f0" 263 | save_config_reboot_button["font"] = tkFont.Font(family='Consolas', size=16) 264 | save_config_reboot_button["fg"] = "#000000" 265 | save_config_reboot_button["justify"] = "center" 266 | save_config_reboot_button["text"] = "Save and Reboot" 267 | save_config_reboot_button.grid(row=currentrow, column=column, columnspan=columnspan, sticky='ew', padx=4, pady=4) 268 | save_config_reboot_button["command"] = lambda: onsavereboot(container) 269 | save_config_reboot_button["relief"] = "solid" 270 | save_config_reboot_button["borderwidth"] = "1px" 271 | currentrow += 1 272 | 273 | def update_errors(container, column, columnspan): 274 | global currentrow 275 | update_errorsbutton = tk.Button(container) 276 | update_errorsbutton["bg"] = "#f0f0f0" 277 | update_errorsbutton["font"] = tkFont.Font(family='Consolas', size=16) 278 | update_errorsbutton["fg"] = "#000000" 279 | update_errorsbutton["justify"] = "center" 280 | update_errorsbutton["text"] = "Update errors" 281 | update_errorsbutton.grid(row=currentrow, column=column, columnspan=columnspan, sticky='ew', padx=4, pady=4) 282 | update_errorsbutton["command"] = lambda: onupdate_errors() 283 | update_errorsbutton["relief"] = "solid" 284 | update_errorsbutton["borderwidth"] = "1px" 285 | currentrow += 1 286 | 287 | def clear_errors(container, column, columnspan): 288 | global currentrow 289 | clear_errorsbutton = tk.Button(container) 290 | clear_errorsbutton["bg"] = "#f0f0f0" 291 | clear_errorsbutton["font"] = tkFont.Font(family='Consolas', size=16) 292 | clear_errorsbutton["fg"] = "#000000" 293 | clear_errorsbutton["justify"] = "center" 294 | clear_errorsbutton["text"] = "Clear errors" 295 | clear_errorsbutton.grid(row=currentrow, column=column, columnspan=columnspan, sticky='ew', padx=4, pady=4) 296 | clear_errorsbutton["command"] = lambda: eval("board.clear_errors()") 297 | clear_errorsbutton["relief"] = "solid" 298 | clear_errorsbutton["borderwidth"] = "1px" 299 | currentrow += 1 300 | 301 | ############################################### app ############################################### 302 | 303 | def on_configure(event): 304 | canvas.configure(scrollregion=canvas.bbox('all')) 305 | 306 | width = 600 307 | height = 400 308 | 309 | window = tk.Tk() 310 | window.title("ODrive 3.6 GUI") 311 | window.geometry(f"{width}x{height}+0+0") 312 | window.minsize(400, 200) 313 | 314 | window.grid_rowconfigure(0, weight=1) 315 | window.grid_columnconfigure(0, weight=1) 316 | 317 | canvas = tk.Canvas(window) 318 | canvas.grid(row=0, column=0, sticky=tk.NSEW) 319 | 320 | scrollbar_y = ttk.Scrollbar(window, orient=tk.VERTICAL, command=canvas.yview) 321 | scrollbar_y.grid(row=0, column=1, sticky='ns') 322 | canvas.configure(yscrollcommand=scrollbar_y.set) 323 | 324 | scrollbar_x = ttk.Scrollbar(window, orient=tk.HORIZONTAL, command=canvas.xview) 325 | scrollbar_x.grid(row=1, column=0, sticky='ew') 326 | canvas.configure(xscrollcommand=scrollbar_x.set) 327 | 328 | frame = ttk.Frame(canvas) 329 | canvas.create_window((0, 0), window=frame, anchor="nw") 330 | frame.bind('', on_configure) 331 | 332 | window.bind('', lambda event: canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")) 333 | window.bind('', lambda event: canvas.xview_scroll(int(-1 * (event.delta / 120)), "units")) 334 | 335 | canvas.yview_moveto(0) 336 | 337 | boardlabel = labelobject(frame, "Searching for an ODrive", "", 0, 0) 338 | 339 | def show_after_started(): 340 | 341 | global board 342 | board = odrive.find_any() 343 | 344 | boardlabel.destroy() 345 | 346 | ###################################################### Essential Settings ###################################################### 347 | 348 | section = labelframe_section(frame, "Essential Settings") 349 | 350 | erase_configuration(section, 0, 2) 351 | 352 | dropdown_setting(section, "Motor type", "axis0.motor.config.motor_type", "Select the type of motor you are using, brushless (BLDC), asynchronous (ACIM) or GIMBAL", motortypes) 353 | 354 | settings = [ 355 | {"label": "Pole pairs", "path": "axis0.motor.config.pole_pairs", "info": "Number of pole pairs of the motor. This is equal to the number of permanent magnets in the rotor divided by 2."}, 356 | {"label": "Current limit [A]", "path": "axis0.motor.config.current_lim", "info": "Maximum motor current, this should be less than the rated current of the motor. If you need more than 60A, you must change the current range."}, 357 | {"label": "Calibration current [A]", "path": "axis0.motor.config.calibration_current", "info": "Current during motor calibration."}, 358 | {"label": "Velocity limit [rev/s]", "path": "axis0.controller.config.vel_limit", "info": "Maximum velocity of the motor in rotations per second."}, 359 | {"label": "Velocity limit tolerance [ratio]", "path": "axis0.controller.config.vel_limit_tolerance", "info": "Maximum velocity tolerance in rotations per second. This is used to determine when the axis is stationary."}, 360 | {"label": "Brake resistance [Ohm]", "path": "config.brake_resistance", "info": "Resistance of the brake resistor, this is used to dissipate the energy of the motor when decelerating. If you don't have a brake resistor, you can set this to 0."} 361 | ] 362 | 363 | for setting in settings: 364 | textbox_setting(section, setting["label"], setting["path"], setting["info"]) 365 | 366 | dropdown_setting(section, "Encoder type", "axis0.encoder.config.mode", "Select the type of encoder you are using.", encodermode) 367 | 368 | dropdown_setting(section, "Use index", "axis0.encoder.config.use_index", "Enable/disable the use of the index signal, this is only used for incremental encoders with index pulse.", bools) 369 | 370 | settings = [ 371 | {"label": "Chip select GPIO pin", "path": "axis0.encoder.config.abs_spi_cs_gpio_pin", "info": "GPIO pin used for the SPI chip select signal. This is only used when using an SPI encoder. Power cycle may be required for changes to take effect."}, 372 | {"label": "Encoder CPR", "path": "axis0.encoder.config.cpr", "info": "Number of counts per revolution, this is the same as 4 times the pulses per revolution (PPR)."} 373 | ] 374 | 375 | for setting in settings: 376 | textbox_setting(section, setting["label"], setting["path"], setting["info"]) 377 | 378 | save_config_reboot(section, 1, 2) 379 | 380 | #################################################### Test settings #################################################### 381 | 382 | section = labelframe_section(frame, "Test settings") 383 | 384 | requested_state(section, "MOTOR_CALIBRATION", axisstates) 385 | requested_state(section, "ENCODER_OFFSET_CALIBRATION", axisstates) 386 | requested_state(section, "FULL_CALIBRATION_SEQUENCE", axisstates) 387 | requested_state(section, "CLOSED_LOOP_CONTROL", axisstates) 388 | requested_state(section, "IDLE", axisstates) 389 | test_position(section, "Test position", axisstates) 390 | test_velocity(section, "Test velocity", axisstates) 391 | 392 | ########################################################## Errors ########################################################## 393 | 394 | section = labelframe_section(frame, "Errors") 395 | 396 | update_errors(section, 0, 2) 397 | clear_errors(section, 0, 2) 398 | error_display(section, "Axis error: ", board.axis0.error, axis_errors) 399 | error_display(section, "Motor error: ", board.axis0.motor.error, motor_errors) 400 | error_display(section, "Encoder error: ", board.axis0.encoder.error, encoder_errors) 401 | error_display(section, "Controller error: ", board.axis0.controller.error, controller_errors) 402 | 403 | ######################################################## Troubleshooting ######################################################## 404 | 405 | section = labelframe_section(frame, "Troubleshooting") 406 | 407 | settings = [ 408 | {"label": "Calibration voltage [V]", "path": "axis0.motor.config.resistance_calib_max_voltage", "info": "The default value is not enough for high resistance motors. In general, you need resistance_calib_max_voltage > calibration_current * phase_resistance resistance_calib_max_voltage < 0.5 * vbus_voltage"}, 409 | {"label": "Scan distance [rad]", "path": "axis0.encoder.config.calib_scan_distance", "info": "The length of the calibration movement, for low CPR position sensors, this value should be increased. If you want a scan distance of n mechanical revolutions, you need to set this to n * 2 * pi * pole pairs."}, 410 | {"label": "Current control bandwidth", "path": "axis0.motor.config.current_control_bandwidth", "info": "Low KV motors may vibrate in position hold, reducing the current control bandwidth may solve this. This uses a biquad filter, so the resulting phase margin is actually half of what you set."}, 411 | {"label": "Encoder bandwidth", "path": "axis0.encoder.config.bandwidth", "info": "Encoder bandwidth in radians per second. This is the cutoff frequency of the encoder velocity estimation. It should be set to a bit less than the electrical bandwidth of the motor."} 412 | ] 413 | 414 | for setting in settings: 415 | textbox_setting(section, setting["label"], setting["path"], setting["info"]) 416 | 417 | save_config_reboot(section, 1, 2) 418 | 419 | ######################################################## Start-up sequence ######################################################## 420 | 421 | section = labelframe_section(frame, "Start-up sequence") 422 | 423 | settings = [ 424 | {"label": "Startup motor calibration", "path": "axis0.config.startup_motor_calibration", "info": "If true, the motor will be calibrated on startup."}, 425 | {"label": "Save motor calibration", "path": "axis0.motor.config.pre_calibrated", "info": "If true, the motor calibration will be saved to the board so you don't need motor startup calibration. Before setting this to true, you must perform motor calibration."} 426 | ] 427 | 428 | for setting in settings: 429 | dropdown_setting(section, setting["label"], setting["path"], setting["info"], bools) 430 | 431 | requested_state(section, "MOTOR_CALIBRATION", axisstates, 0, 3) 432 | 433 | settings = [ 434 | {"label": "Startup encoder offset calibration", "path": "axis0.config.startup_encoder_offset_calibration", "info": "If true, the encoder offset calibration will be performed on startup. "}, 435 | {"label": "Save encoder offset calibration", "path": "axis0.encoder.config.pre_calibrated", "info": "If true, the encoder offset calibration will be saved to the board so you don't need encoder offset calibration. This can only be done when using an encoder with index, hall effect sensors or an absolute encoder, when using an encoder with index, you must also enable index search on startup. Before setting this to true, you must perform encoder offset calibration."}, 436 | ] 437 | 438 | for setting in settings: 439 | dropdown_setting(section, setting["label"], setting["path"], setting["info"], bools) 440 | 441 | requested_state(section, "ENCODER_OFFSET_CALIBRATION", axisstates, 0, 3) 442 | 443 | settings = [ 444 | {"label": "Startup encoder index search", "path": "axis0.config.startup_encoder_index_search", "info": "If true, the encoder index search will be performed on startup."}, 445 | {"label": "Startup closed loop control", "path": "axis0.config.startup_closed_loop_control", "info": "If true, the closed loop control will be started on startup"} 446 | ] 447 | 448 | for setting in settings: 449 | dropdown_setting(section, setting["label"], setting["path"], setting["info"], bools) 450 | 451 | save_config_reboot(section, 1, 2) 452 | 453 | ######################################################## Extra Settings ######################################################## 454 | 455 | section = labelframe_section(frame, "Extra Settings") 456 | 457 | settings = [ 458 | {"label": "Calibration current [A]", "path": "axis0.motor.config.calibration_current", "info": "Current during motor calibration."}, 459 | {"label": "Torque constant [Nm/A]", "path": "axis0.motor.config.torque_constant", "info": "Torque constant KT of the motor, this is equal to 8.27/KV where KV is the motor velocity constant in rpm/Volt. If you want to input current instead of torque in torque control mode, you can set this to 1."}, 460 | {"label": "Velocity limit [rev/s]", "path": "axis0.controller.config.vel_limit", "info": "Maximum velocity of the motor in rotations per second."}, 461 | {"label": "Velocity limit tolerance [ratio]", "path": "axis0.controller.config.vel_limit_tolerance", "info": "Maximum velocity tolerance in rotations per second. This is used to determine when the axis is stationary."}, 462 | {"label": "Current range [A]", "path": "axis0.motor.config.requested_current_range", "info": "This changes the amplifier gain to get a more accurate current measurement. The default is 60 A and the maximum is 120 A."} 463 | ] 464 | 465 | for setting in settings: 466 | textbox_setting(section, setting["label"], setting["path"], setting["info"]) 467 | 468 | save_config_reboot(section, 1, 2) 469 | 470 | ######################################################## Application setup ######################################################## 471 | 472 | section = labelframe_section(frame, "Application setup") 473 | 474 | dropdown_setting(section, "Control mode", "axis0.controller.config.control_mode", "Select the control mode you want to use", controlmodes) 475 | dropdown_setting(section, "Input mode", "axis0.controller.config.input_mode", "Select the input mode you want to use", inputmodes) 476 | 477 | save_config_reboot(section, 1, 2) 478 | 479 | ########################################################## PID tuning ########################################################## 480 | 481 | section = labelframe_section(frame, "PID tuning") 482 | 483 | settings = [ 484 | {"label": "Position gain", "path": "axis0.controller.config.pos_gain", "info": "Position gain"}, 485 | {"label": "Velocity gain", "path": "axis0.controller.config.vel_gain", "info": "Velocity gain"}, 486 | {"label": "Velocity integral gain", "path": "axis0.controller.config.vel_integrator_gain", "info": "Velocity integral gain"} 487 | ] 488 | 489 | for setting in settings: 490 | textbox_setting(section, setting["label"], setting["path"], setting["info"]) 491 | 492 | save_config_reboot(section, 1, 2) 493 | 494 | window.after(100, show_after_started) 495 | 496 | window.mainloop() 497 | -------------------------------------------------------------------------------- /ODrive Python GUI 056.py: -------------------------------------------------------------------------------- 1 | import odrive 2 | from odrive.utils import * 3 | import tkinter as tk 4 | from tkinter import ttk 5 | import tkinter.font as tkFont 6 | from tktooltip import ToolTip 7 | import time 8 | 9 | width = 1000 10 | height = 500 11 | 12 | currentrow = 0 13 | currentsection = 0 14 | 15 | ###################################### odrive dicts ###################################### 16 | 17 | motortypes = {"MOTOR_TYPE_HIGH_CURRENT": 0, "MOTOR_TYPE_GIMBAL": 2, "MOTOR_TYPE_ACIM": 3} 18 | axisstates = {"UNDEFINED": 0, "IDLE": 1, "STARTUP_SEQUENCE": 2, "FULL_CALIBRATION_SEQUENCE": 3, "MOTOR_CALIBRATION": 4, "ENCODER_INDEX_SEARCH": 6, "ENCODER_OFFSET_CALIBRATION": 7, "CLOSED_LOOP_CONTROL": 8, "LOCKIN_SPIN": 9, "INPUT_TORQUE_CONTROL": 10, "INPUT_VOLTAGE_CONTROL": 11, "CURRENT_CONTROL": 12, "VELOCITY_CONTROL": 13, "POSITION_CONTROL": 14, "TRAJECTORY_CONTROL": 15, "DISCOVERING_USB": 16, "UNKNOWN": 255} 19 | encodermode = {"ENCODER_MODE_INCREMENTAL": 0, "ENCODER_MODE_HALL": 1, "ENCODER_MODE_SINCOS": 2, "ENCODER_MODE_SPI_ABS_CUI": 256, "ENCODER_MODE_SPI_ABS_AMS": 257, "ENCODER_MODE_SPI_ABS_AEAT": 258, "ENCODER_MODE_SPI_ABS_AEAT_MARK": 259, "ENCODER_MODE_SPI_ABS_CUI_2048": 260, "ENCODER_MODE_SPI_ABS_CUI_4096": 261, "ENCODER_MODE_SPI_ABS_CUI_8192": 262, "ENCODER_MODE_SPI_ABS_AMS_512": 263, "ENCODER_MODE_SPI_ABS_AMS_1024": 264, "ENCODER_MODE_SPI_ABS_AMS_2048": 265, "ENCODER_MODE_SPI_ABS_AMS_4096": 266, "ENCODER_MODE_SPI_ABS_AMS_8192": 267, "ENCODER_MODE_SPI_ABS_AMS_16384": 268, "ENCODER_MODE_SPI_ABS_AMS_32768": 269, "ENCODER_MODE_SPI_ABS_AMS_65536": 270, "ENCODER_MODE_SPI_ABS_AMS_131072": 271, "ENCODER_MODE_SPI_ABS_AMS_262144": 272, "ENCODER_MODE_SPI_ABS_AMS_524288": 273, "ENCODER_MODE_SPI_ABS_AMS_1048576": 274, "ENCODER_MODE_SPI_ABS_AMS_2097152": 275, "ENCODER_MODE_SPI_ABS_AMS_4194304": 276, "ENCODER_MODE_SPI_ABS_AMS_8388608": 277, "ENCODER_MODE_SPI_ABS_AMS_16777216": 278, "ENCODER_MODE_SPI_ABS_AMS_33554432": 279, "ENCODER_MODE_SPI_ABS_AMS_67108864": 280, "ENCODER_MODE_SPI_ABS_AMS_134217728": 281, "ENCODER_MODE_SPI_ABS_AMS_268435456": 282, "ENCODER_MODE_SPI_ABS_AMS_MASK": 4294901760} 20 | controlmodes = {"CTRL_MODE_VOLTAGE_CONTROL": 0, "CTRL_MODE_TORQUE_CONTROL": 1, "CTRL_MODE_VELOCITY_CONTROL": 2, "CTRL_MODE_POSITION_CONTROL": 3, "CTRL_MODE_TRAJECTORY_CONTROL": 4} 21 | inputmodes = {"INPUT_MODE_INACTIVE": 0, "INPUT_MODE_PASSTHROUGH": 1, "INPUT_MODE_VEL_RAMP": 2, "INPUT_MODE_POS_FILTER": 3} 22 | bools = { "True": 1, "False": 0} 23 | board_errors = {"NONE": 0, "CONTROL_ITERATION_MISSED": 1, "DC_BUS_UNDER_VOLTAGE": 2, "DC_BUS_OVER_VOLTAGE": 4, "DC_BUS_OVER_REGEN_CURRENT": 8, "DC_BUS_OVER_CURRENT": 16, "BRAKE_DEADTIME_VIOLATION": 32, "BRAKE_DUTY_CYCLE_NAN": 64, "INVALID_BRAKE_RESISTANCE": 128} 24 | axis_errors = {"NONE": 0, "INVALID_STATE": 1, "WATCHDOG_TIMER_EXPIRED": 2048, "MIN_ENDSTOP_PRESSED": 4096, "MAX_ENDSTOP_PRESSED": 8192, "ESTOP_REQUESTED": 16384, "HOMING_WITHOUT_ENDSTOP": 131072, "OVER_TEMP": 262144, "UNKNOWN_POSITION": 524288} 25 | motor_errors = {"NONE": 0, "PHASE_RESISTANCE_OUT_OF_RANGE": 1, "PHASE_INDUCTANCE_OUT_OF_RANGE": 2, "DRV_FAULT": 8, "CONTROL_DEADLINE_MISSED": 16, "MODULATION_MAGNITUDE": 128, "CURRENT_SENSE_SATURATION": 1024, "CURRENT_LIMIT_VIOLATION": 4096, "MODULATION_IS_NAN": 65536, "MOTOR_THERMISTOR_OVER_TEMP": 131072, "FET_THERMISTOR_OVER_TEMP": 262144, "TIMER_UPDATE_MISSED": 524288, "CURRENT_MEASUREMENT_UNAVAILABLE": 1048576, "CONTROLLER_FAILED": 2097152, "I_BUS_OUT_OF_RANGE": 4194304, "BRAKE_RESISTOR_DISARMED": 8388608, "SYSTEM_LEVEL": 16777216, "BAD_TIMING": 33554432, "UNKNOWN_PHASE_ESTIMATE": 67108864, "UNKNOWN_PHASE_VEL": 134217728, "UNKNOWN_TORQUE": 268435456, "UNKNOWN_CURRENT_COMMAND": 536870912, "UNKNOWN_CURRENT_MEASUREMENT": 1073741824, "UNKNOWN_VBUS_VOLTAGE": 2147483648, "UNKNOWN_VOLTAGE_COMMAND": 4294967296, "UNKNOWN_GAINS": 8589934592, "CONTROLLER_INITIALIZING": 17179869184, "UNBALANCED_PHASES": 34359738368} 26 | encoder_errors = {"NONE": 0, "UNSTABLE_GAIN": 1, "CPR_POLEPAIRS_MISMATCH": 2, "NO_RESPONSE": 4, "UNSUPPORTED_ENCODER_MODE": 8, "ILLEGAL_HALL_STATE": 16, "INDEX_NOT_FOUND_YET": 32, "ABS_SPI_TIMEOUT": 64, "ABS_SPI_COM_FAIL": 128, "ABS_SPI_NOT_READY": 256, "HALL_NOT_CALIBRATED_YET": 512} 27 | controller_errors = {"NONE": 0, "OVERSPEED": 1, "INVALID_INPUT_MODE": 2, "UNSTABLE_GAIN": 4, "INVALID_MIRROR_AXIS": 8, "INVALID_LOAD_ENCODER": 16, "INVALID_ESTIMATE": 32, "INVALID_CIRCULAR_RANGE": 64, "SPINOUT_DETECTED": 128} 28 | 29 | ######################################## objects ######################################### 30 | 31 | error_objects = [] 32 | 33 | ##################################### dict functions ##################################### 34 | 35 | def getkey(value, dict): 36 | matching_keys = [key for key, val in dict.items() if val == value] 37 | if len(matching_keys) == 1: 38 | return matching_keys[0] 39 | elif len(matching_keys) == 0: 40 | raise ValueError(f"No key found for value {value}") 41 | else: 42 | raise ValueError(f"Multiple keys found for value {value}: {matching_keys}") 43 | 44 | def getvalue(key, dict): 45 | matching_value = dict.get(key) 46 | if matching_value is not None: 47 | return matching_value 48 | else: 49 | raise ValueError(f"No value found for key {key}") 50 | 51 | ######################################### functions ######################################### 52 | 53 | def labelobject(container, label_text, info, row, column): 54 | label = tk.Button(container) 55 | label["bg"] = "#f0f0f0" 56 | label["font"] = tkFont.Font(family='Consolas', size=16) 57 | label["fg"] = "#000000" 58 | label["anchor"] = "w" 59 | label["text"] = label_text 60 | label["relief"] = "flat" 61 | label.grid(row=row, column=column, sticky='ew', padx=4, pady=4) 62 | if info != "": 63 | ToolTip(label, msg=info, font=tkFont.Font(family='Consolas', size=16)) 64 | return label 65 | 66 | def labelframeobject(container, section_name, row, column): 67 | labelframe = tk.LabelFrame(container) 68 | labelframe["bg"] = "#f0f0f0" 69 | labelframe["fg"] = "#000000" 70 | labelframe["text"] = section_name 71 | labelframe.configure(font=tkFont.Font(family='Consolas', size=18, weight="bold")) 72 | labelframe.grid(row=row, column=column, sticky='ew', padx=4, pady=4) 73 | return labelframe 74 | 75 | def entryobject(container, row, column): 76 | entry = tk.Entry(container) 77 | entry["borderwidth"] = "1px" 78 | entry["font"] = tkFont.Font(family='Consolas', size=16) 79 | entry["fg"] = "#333333" 80 | entry["justify"] = "center" 81 | entry["relief"] = "solid" 82 | entry.grid(row=row, column=column, sticky='nsew', padx=4, pady=4) 83 | return entry 84 | 85 | def dropdownobject(container, row, column, options): 86 | variable = tk.StringVar(container) 87 | variable.set(options[0]) 88 | dropdown = tk.OptionMenu(container, variable, *options) 89 | dropdown["relief"] = "solid" 90 | dropdown["borderwidth"] = "1px" 91 | dropdown["font"] = tkFont.Font(family='Consolas', size=16) 92 | container.nametowidget(dropdown.menuname).config(font=tkFont.Font(family='Consolas', size=16)) 93 | dropdown["highlightthickness"] = "0px" 94 | dropdown.grid(row=row, column=column, sticky='nsew', padx=4, pady=4) 95 | return variable 96 | 97 | def buttonobject(container, row, column, command, text, columnspan=1): 98 | button = tk.Button(container) 99 | button["bg"] = "#f0f0f0" 100 | button["font"] = tkFont.Font(family='Consolas', size=16) 101 | button["fg"] = "#000000" 102 | button["justify"] = "center" 103 | button["text"] = str(text) 104 | button["command"] = command 105 | button["relief"] = "solid" 106 | button["borderwidth"] = "1px" 107 | button.grid(row=row, column=column, sticky='nsew', padx=4, pady=4, columnspan=columnspan) 108 | return button 109 | 110 | def sendbuttonobject(container, row, column, command): 111 | buttonobject(container, row, column, command, "send") 112 | 113 | def displayobject(container, row, column, value): 114 | display = tk.Label(container) 115 | display["font"] = tkFont.Font(family='Consolas', size=16) 116 | display["fg"] = "#333333" 117 | display["anchor"] = "w" 118 | display["text"] = value 119 | display.grid(row=row, column=column, sticky='ewns', padx=4, pady=4) 120 | return display 121 | 122 | def update_setting(path, value): 123 | exec(f'board.{path} = {value}') 124 | 125 | def submit_entry(entry, display, path): 126 | value = entry.get() 127 | update_setting(path, value) 128 | display.configure(text=value) 129 | entry.delete(0, tk.END) 130 | 131 | def submit_dropdown(variable, dict, display, path): 132 | value = getvalue(variable.get(), dict) 133 | update_setting(path, value) 134 | display.configure(text=getkey(value, dict)) 135 | 136 | def submit_requested_state(requested_state, dict, path): 137 | value = getvalue(requested_state, dict) 138 | update_setting(path, value) 139 | 140 | def onsavereboot(container): 141 | global board 142 | container.grab_set() 143 | try: 144 | board.save_configuration() 145 | except: 146 | board = odrive.find_any() 147 | container.grab_release() 148 | 149 | def onupdate_errors(): 150 | global board 151 | for error in error_objects: 152 | display = error[0] 153 | dict = error[1] 154 | if dict == board_errors: 155 | try: 156 | update = getkey(board.error, board_errors) 157 | except: 158 | update = board.error 159 | elif dict == axis_errors: 160 | try: 161 | update = getkey(board.axis0.error, axis_errors) 162 | except: 163 | update = board.axis0.error 164 | elif dict == motor_errors: 165 | try: 166 | update = getkey(board.axis0.motor.error, motor_errors) 167 | except: 168 | update = board.axis0.motor.error 169 | elif dict == encoder_errors: 170 | try: 171 | update = getkey(board.axis0.encoder.error, encoder_errors) 172 | except: 173 | update = board.axis0.encoder.error 174 | elif dict == controller_errors: 175 | try: 176 | update = getkey(board.axis0.controller.error, controller_errors) 177 | except: 178 | update = board.axis0.controller.error 179 | display.configure(text=update) 180 | 181 | def ontestposition(): 182 | global board 183 | submit_requested_state("CTRL_MODE_POSITION_CONTROL", controlmodes, "axis0.controller.config.control_mode") 184 | board.axis0.controller.input_pos = 1 185 | time.sleep(1) 186 | board.axis0.controller.input_pos = 0 187 | 188 | def ontestvelocity(): 189 | global board 190 | submit_requested_state("CTRL_MODE_VELOCITY_CONTROL", controlmodes, "axis0.controller.config.control_mode") 191 | board.axis0.controller.input_vel = 1 192 | time.sleep(1) 193 | board.axis0.controller.input_vel = 0 194 | 195 | ###################################### GUI sections ###################################### 196 | 197 | def labelframe_section(container, name): 198 | global currentrow 199 | global currentsection 200 | currentrow = 0 201 | currentsection += 1 202 | labelframe = labelframeobject(container, name, currentsection, 0) 203 | return labelframe 204 | 205 | ######################################### GUI rows ######################################### 206 | 207 | def textbox_setting(container, label_text, path, info): 208 | global currentrow 209 | label = labelobject(container, label_text, info, currentrow, 0) 210 | entry = entryobject(container, currentrow, 1) 211 | display = displayobject(container, currentrow, 3, round(eval(f'board.{path}'), 5)) 212 | button = sendbuttonobject(container, currentrow, 2, lambda: submit_entry(entry, display, path)) 213 | currentrow += 1 214 | 215 | def dropdown_setting(container, label_text, path, info, dict): 216 | global currentrow 217 | label = labelobject(container, label_text, info, currentrow, 0) 218 | variable = dropdownobject(container, currentrow, 1, list(dict.keys())) 219 | display = displayobject(container, currentrow, 3, getkey(eval(f'board.{path}'), dict)) 220 | button = sendbuttonobject(container, currentrow, 2, lambda: submit_dropdown(variable, dict, display, path)) 221 | currentrow += 1 222 | 223 | def requested_state(container, requested_state, dict, column=0, columnspan=1): 224 | global currentrow 225 | button = buttonobject(container, currentrow, column, lambda: submit_requested_state(requested_state, dict, "axis0.requested_state"), requested_state, columnspan) 226 | currentrow += 1 227 | 228 | def error_display(container, label_text, error, dict): 229 | global currentrow 230 | label = labelobject(container, label_text, "", currentrow, 0) 231 | try: 232 | errormessage = getkey(error, dict) 233 | except: 234 | errormessage = error 235 | display = displayobject(container, currentrow, 1, errormessage) 236 | error_objects.append([display, dict]) 237 | currentrow += 1 238 | return display 239 | 240 | def test_position(container, button_text, dict): 241 | global currentrow 242 | button = buttonobject(container, currentrow, 0, lambda: ontestposition(), button_text, 1) 243 | currentrow += 1 244 | 245 | def test_velocity(container, label_text, dict): 246 | global currentrow 247 | button = buttonobject(container, currentrow, 0, lambda: ontestvelocity(), label_text, 1) 248 | currentrow += 1 249 | 250 | def erase_configuration(container, column, columnspan): 251 | global currentrow 252 | erase_configuration_button = tk.Button(container) 253 | erase_configuration_button["bg"] = "#f0f0f0" 254 | erase_configuration_button["font"] = tkFont.Font(family='Consolas', size=16) 255 | erase_configuration_button["fg"] = "#000000" 256 | erase_configuration_button["justify"] = "center" 257 | erase_configuration_button["text"] = "Erase configuration" 258 | erase_configuration_button.grid(row=currentrow, column=column, columnspan=columnspan, sticky='ew', padx=4, pady=4) 259 | erase_configuration_button["command"] = lambda: eval("board.erase_configuration()") 260 | erase_configuration_button["relief"] = "solid" 261 | erase_configuration_button["borderwidth"] = "1px" 262 | currentrow += 1 263 | 264 | def save_config_reboot(container, column, columnspan): 265 | global currentrow 266 | save_config_reboot_button = tk.Button(container) 267 | save_config_reboot_button["bg"] = "#f0f0f0" 268 | save_config_reboot_button["font"] = tkFont.Font(family='Consolas', size=16) 269 | save_config_reboot_button["fg"] = "#000000" 270 | save_config_reboot_button["justify"] = "center" 271 | save_config_reboot_button["text"] = "Save and Reboot" 272 | save_config_reboot_button.grid(row=currentrow, column=column, columnspan=columnspan, sticky='ew', padx=4, pady=4) 273 | save_config_reboot_button["command"] = lambda: onsavereboot(container) 274 | save_config_reboot_button["relief"] = "solid" 275 | save_config_reboot_button["borderwidth"] = "1px" 276 | currentrow += 1 277 | 278 | def update_errors(container, column, columnspan): 279 | global currentrow 280 | update_errorsbutton = tk.Button(container) 281 | update_errorsbutton["bg"] = "#f0f0f0" 282 | update_errorsbutton["font"] = tkFont.Font(family='Consolas', size=16) 283 | update_errorsbutton["fg"] = "#000000" 284 | update_errorsbutton["justify"] = "center" 285 | update_errorsbutton["text"] = "Update errors" 286 | update_errorsbutton.grid(row=currentrow, column=column, columnspan=columnspan, sticky='ew', padx=4, pady=4) 287 | update_errorsbutton["command"] = lambda: onupdate_errors() 288 | update_errorsbutton["relief"] = "solid" 289 | update_errorsbutton["borderwidth"] = "1px" 290 | currentrow += 1 291 | 292 | def clear_errors(container, column, columnspan): 293 | global currentrow 294 | clear_errorsbutton = tk.Button(container) 295 | clear_errorsbutton["bg"] = "#f0f0f0" 296 | clear_errorsbutton["font"] = tkFont.Font(family='Consolas', size=16) 297 | clear_errorsbutton["fg"] = "#000000" 298 | clear_errorsbutton["justify"] = "center" 299 | clear_errorsbutton["text"] = "Clear errors" 300 | clear_errorsbutton.grid(row=currentrow, column=column, columnspan=columnspan, sticky='ew', padx=4, pady=4) 301 | clear_errorsbutton["command"] = lambda: eval("board.clear_errors()") 302 | clear_errorsbutton["relief"] = "solid" 303 | clear_errorsbutton["borderwidth"] = "1px" 304 | currentrow += 1 305 | 306 | ############################################### app ############################################### 307 | 308 | def on_configure(event): 309 | canvas.configure(scrollregion=canvas.bbox('all')) 310 | 311 | width = 600 312 | height = 400 313 | 314 | window = tk.Tk() 315 | window.title("ODrive 3.6 GUI") 316 | window.geometry(f"{width}x{height}+0+0") 317 | window.minsize(400, 200) 318 | 319 | window.grid_rowconfigure(0, weight=1) 320 | window.grid_columnconfigure(0, weight=1) 321 | 322 | canvas = tk.Canvas(window) 323 | canvas.grid(row=0, column=0, sticky=tk.NSEW) 324 | 325 | scrollbar_y = ttk.Scrollbar(window, orient=tk.VERTICAL, command=canvas.yview) 326 | scrollbar_y.grid(row=0, column=1, sticky='ns') 327 | canvas.configure(yscrollcommand=scrollbar_y.set) 328 | 329 | scrollbar_x = ttk.Scrollbar(window, orient=tk.HORIZONTAL, command=canvas.xview) 330 | scrollbar_x.grid(row=1, column=0, sticky='ew') 331 | canvas.configure(xscrollcommand=scrollbar_x.set) 332 | 333 | frame = ttk.Frame(canvas) 334 | canvas.create_window((0, 0), window=frame, anchor="nw") 335 | frame.bind('', on_configure) 336 | 337 | window.bind('', lambda event: canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")) 338 | window.bind('', lambda event: canvas.xview_scroll(int(-1 * (event.delta / 120)), "units")) 339 | 340 | canvas.yview_moveto(0) 341 | 342 | boardlabel = labelobject(frame, "Searching for an ODrive", "", 0, 0) 343 | 344 | def show_after_started(): 345 | 346 | global board 347 | board = odrive.find_any() 348 | 349 | boardlabel.destroy() 350 | 351 | ###################################################### Essential Settings ###################################################### 352 | 353 | section = labelframe_section(frame, "Essential Settings") 354 | 355 | erase_configuration(section, 0, 2) 356 | 357 | dropdown_setting(section, "Motor type", "axis0.motor.config.motor_type", "Select the type of motor you are using, brushless (BLDC), asynchronous (ACIM) or GIMBAL", motortypes) 358 | 359 | settings = [ 360 | {"label": "Pole pairs", "path": "axis0.motor.config.pole_pairs", "info": "Number of pole pairs of the motor. This is equal to the number of permanent magnets in the rotor divided by 2."}, 361 | {"label": "Current limit [A]", "path": "axis0.motor.config.current_lim", "info": "Maximum motor current, this should be less than the rated current of the motor. If you need more than 60A, you must change the current range."}, 362 | {"label": "Calibration current [A]", "path": "axis0.motor.config.calibration_current", "info": "Current during motor calibration."}, 363 | {"label": "Velocity limit [rev/s]", "path": "axis0.controller.config.vel_limit", "info": "Maximum velocity of the motor in rotations per second."}, 364 | {"label": "Velocity limit tolerance [ratio]", "path": "axis0.controller.config.vel_limit_tolerance", "info": "Maximum velocity tolerance in rotations per second. This is used to determine when the axis is stationary."} 365 | ] 366 | 367 | for setting in settings: 368 | textbox_setting(section, setting["label"], setting["path"], setting["info"]) 369 | 370 | dropdown_setting(section, "Enable brake resistor", "config.enable_brake_resistor", "Enable/disable the brake resistor.", bools) 371 | 372 | dropdown_setting(section, "Encoder type", "axis0.encoder.config.mode", "Select the type of encoder you are using.", encodermode) 373 | 374 | dropdown_setting(section, "Use index", "axis0.encoder.config.use_index", "Enable/disable the use of the index signal, this is only used for incremental encoders with index pulse.", bools) 375 | 376 | settings = [ 377 | {"label": "Chip select GPIO pin", "path": "axis0.encoder.config.abs_spi_cs_gpio_pin", "info": "GPIO pin used for the SPI chip select signal. This is only used when using an SPI encoder. Power cycle may be required for changes to take effect."}, 378 | {"label": "Encoder CPR", "path": "axis0.encoder.config.cpr", "info": "Number of counts per revolution, this is the same as 4 times the pulses per revolution (PPR)."} 379 | ] 380 | 381 | for setting in settings: 382 | textbox_setting(section, setting["label"], setting["path"], setting["info"]) 383 | 384 | save_config_reboot(section, 1, 2) 385 | 386 | #################################################### Test settings #################################################### 387 | 388 | section = labelframe_section(frame, "Test settings") 389 | 390 | requested_state(section, "MOTOR_CALIBRATION", axisstates) 391 | requested_state(section, "ENCODER_OFFSET_CALIBRATION", axisstates) 392 | requested_state(section, "FULL_CALIBRATION_SEQUENCE", axisstates) 393 | requested_state(section, "CLOSED_LOOP_CONTROL", axisstates) 394 | requested_state(section, "IDLE", axisstates) 395 | test_position(section, "Test position", axisstates) 396 | test_velocity(section, "Test velocity", axisstates) 397 | 398 | ########################################################## Errors ########################################################## 399 | 400 | section = labelframe_section(frame, "Errors") 401 | 402 | update_errors(section, 0, 2) 403 | clear_errors(section, 0, 2) 404 | error_display(section, "Board error: ", board.error, board_errors) 405 | error_display(section, "Axis error: ", board.axis0.error, axis_errors) 406 | error_display(section, "Motor error: ", board.axis0.motor.error, motor_errors) 407 | error_display(section, "Encoder error: ", board.axis0.encoder.error, encoder_errors) 408 | error_display(section, "Controller error: ", board.axis0.controller.error, controller_errors) 409 | 410 | ######################################################## Troubleshooting ######################################################## 411 | 412 | section = labelframe_section(frame, "Troubleshooting") 413 | 414 | settings = [ 415 | {"label": "Calibration voltage [V]", "path": "axis0.motor.config.resistance_calib_max_voltage", "info": "The default value is not enough for high resistance motors. In general, you need resistance_calib_max_voltage > calibration_current * phase_resistance resistance_calib_max_voltage < 0.5 * vbus_voltage"}, 416 | {"label": "Scan distance [rad]", "path": "axis0.encoder.config.calib_scan_distance", "info": "The length of the calibration movement, for low CPR position sensors, this value should be increased. If you want a scan distance of n mechanical revolutions, you need to set this to n * 2 * pi * pole pairs."}, 417 | {"label": "Current control bandwidth", "path": "axis0.motor.config.current_control_bandwidth", "info": "Low KV motors may vibrate in position hold, reducing the current control bandwidth may solve this. This uses a biquad filter, so the resulting phase margin is actually half of what you set."}, 418 | {"label": "Encoder bandwidth", "path": "axis0.encoder.config.bandwidth", "info": "Encoder bandwidth in radians per second. This is the cutoff frequency of the encoder velocity estimation. It should be set to a bit less than the electrical bandwidth of the motor."} 419 | ] 420 | 421 | for setting in settings: 422 | textbox_setting(section, setting["label"], setting["path"], setting["info"]) 423 | 424 | save_config_reboot(section, 1, 2) 425 | 426 | ######################################################## Start-up sequence ######################################################## 427 | 428 | section = labelframe_section(frame, "Start-up sequence") 429 | 430 | settings = [ 431 | {"label": "Startup motor calibration", "path": "axis0.config.startup_motor_calibration", "info": "If true, the motor will be calibrated on startup."}, 432 | {"label": "Save motor calibration", "path": "axis0.motor.config.pre_calibrated", "info": "If true, the motor calibration will be saved to the board so you don't need motor startup calibration. Before setting this to true, you must perform motor calibration."} 433 | ] 434 | 435 | for setting in settings: 436 | dropdown_setting(section, setting["label"], setting["path"], setting["info"], bools) 437 | 438 | requested_state(section, "MOTOR_CALIBRATION", axisstates, 0, 3) 439 | 440 | settings = [ 441 | {"label": "Startup encoder offset calibration", "path": "axis0.config.startup_encoder_offset_calibration", "info": "If true, the encoder offset calibration will be performed on startup. "}, 442 | {"label": "Save encoder offset calibration", "path": "axis0.encoder.config.pre_calibrated", "info": "If true, the encoder offset calibration will be saved to the board so you don't need encoder offset calibration. This can only be done when using an encoder with index, hall effect sensors or an absolute encoder, when using an encoder with index, you must also enable index search on startup. Before setting this to true, you must perform encoder offset calibration."}, 443 | ] 444 | 445 | for setting in settings: 446 | dropdown_setting(section, setting["label"], setting["path"], setting["info"], bools) 447 | 448 | requested_state(section, "ENCODER_OFFSET_CALIBRATION", axisstates, 0, 3) 449 | 450 | settings = [ 451 | {"label": "Startup encoder index search", "path": "axis0.config.startup_encoder_index_search", "info": "If true, the encoder index search will be performed on startup."}, 452 | {"label": "Startup closed loop control", "path": "axis0.config.startup_closed_loop_control", "info": "If true, the closed loop control will be started on startup"} 453 | ] 454 | 455 | for setting in settings: 456 | dropdown_setting(section, setting["label"], setting["path"], setting["info"], bools) 457 | 458 | save_config_reboot(section, 1, 2) 459 | 460 | ######################################################## Extra Settings ######################################################## 461 | 462 | section = labelframe_section(frame, "Extra Settings") 463 | 464 | settings = [ 465 | {"label": "Calibration current [A]", "path": "axis0.motor.config.calibration_current", "info": "Current during motor calibration."}, 466 | {"label": "Torque constant [Nm/A]", "path": "axis0.motor.config.torque_constant", "info": "Torque constant KT of the motor, this is equal to 8.27/KV where KV is the motor velocity constant in rpm/Volt. If you want to input current instead of torque in torque control mode, you can set this to 1."}, 467 | {"label": "Velocity limit [rev/s]", "path": "axis0.controller.config.vel_limit", "info": "Maximum velocity of the motor in rotations per second."}, 468 | {"label": "Velocity limit tolerance [ratio]", "path": "axis0.controller.config.vel_limit_tolerance", "info": "Maximum velocity tolerance in rotations per second. This is used to determine when the axis is stationary."}, 469 | {"label": "Current range [A]", "path": "axis0.motor.config.requested_current_range", "info": "This changes the amplifier gain to get a more accurate current measurement. The default is 60 A and the maximum is 120 A."} 470 | ] 471 | 472 | for setting in settings: 473 | textbox_setting(section, setting["label"], setting["path"], setting["info"]) 474 | 475 | save_config_reboot(section, 1, 2) 476 | 477 | ######################################################## Application setup ######################################################## 478 | 479 | section = labelframe_section(frame, "Application setup") 480 | 481 | dropdown_setting(section, "Control mode", "axis0.controller.config.control_mode", "Select the control mode you want to use", controlmodes) 482 | dropdown_setting(section, "Input mode", "axis0.controller.config.input_mode", "Select the input mode you want to use", inputmodes) 483 | 484 | save_config_reboot(section, 1, 2) 485 | 486 | ########################################################## PID tuning ########################################################## 487 | 488 | section = labelframe_section(frame, "PID tuning") 489 | 490 | settings = [ 491 | {"label": "Position gain", "path": "axis0.controller.config.pos_gain", "info": "Position gain"}, 492 | {"label": "Velocity gain", "path": "axis0.controller.config.vel_gain", "info": "Velocity gain"}, 493 | {"label": "Velocity integral gain", "path": "axis0.controller.config.vel_integrator_gain", "info": "Velocity integral gain"} 494 | ] 495 | 496 | for setting in settings: 497 | textbox_setting(section, setting["label"], setting["path"], setting["info"]) 498 | 499 | save_config_reboot(section, 1, 2) 500 | 501 | window.after(100, show_after_started) 502 | 503 | window.mainloop() 504 | --------------------------------------------------------------------------------