diff --git a/README.md b/README.md index 287cbf9c..668304b5 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ pip install pytrinamic ## Getting Started -Please have a look at the [code examples on GitHub](https://github.com/trinamic/PyTrinamic/tree/feature_feature_hierarchy_v2/examples). +Please have a look at the [code examples on GitHub](https://github.com/trinamic/PyTrinamic/tree/master/examples). ## Migration Guide diff --git a/examples/evalboards/MAX22216/plunger_move.py b/examples/evalboards/MAX22216/plunger_move.py index ed576d23..768d4b88 100644 --- a/examples/evalboards/MAX22216/plunger_move.py +++ b/examples/evalboards/MAX22216/plunger_move.py @@ -1,12 +1,18 @@ + +import logging +import time + import pytrinamic from pytrinamic.connections import ConnectionManager from pytrinamic.ic import MAX22216 from pytrinamic.evalboards import MAX22216_eval -import time + + +logging.basicConfig(level=logging.DEBUG) pytrinamic.show_info() -with ConnectionManager(debug=True).connect() as my_interface: +with ConnectionManager().connect() as my_interface: print(my_interface) eval = MAX22216_eval(my_interface) @@ -14,9 +20,10 @@ solenoid = ic.motors[0] solenoid.u_supply = 24.0 # V - solenoid.u_dc_h = 24.0 # V + + solenoid.u_dc_h = 10.0 # V solenoid.u_dc_l = 0.0 # V - solenoid.u_dc_l2h = 24.0 # V + solenoid.u_dc_l2h = 10.0 # V solenoid.u_dc_h2l = 0.0 # V solenoid.u_ac = 1.0 # V ampl solenoid.f_ac = 50.0 # Hz diff --git a/examples/evalboards/MAX22216/ramdebug.py b/examples/evalboards/MAX22216/ramdebug.py index 9511ffe5..27c6b290 100644 --- a/examples/evalboards/MAX22216/ramdebug.py +++ b/examples/evalboards/MAX22216/ramdebug.py @@ -1,11 +1,14 @@ +import logging + import pytrinamic from pytrinamic.connections import ConnectionManager from pytrinamic.ic import MAX22216 from pytrinamic.RAMDebug import Channel, RAMDebug, RAMDebug_Trigger +logging.basicConfig(level=logging.DEBUG) pytrinamic.show_info() -with ConnectionManager(debug=True).connect() as my_interface: +with ConnectionManager().connect() as my_interface: print(my_interface) ch = Channel.field(0, MAX22216.FIELD.ADC_VM_RAW, signed=True, eval_channel=1) diff --git a/examples/evalboards/TMC2240/register_dump.py b/examples/evalboards/TMC2240/register_dump.py new file mode 100644 index 00000000..45161331 --- /dev/null +++ b/examples/evalboards/TMC2240/register_dump.py @@ -0,0 +1,66 @@ +""" +Dump all register values of the TMC2240 IC. + +The connection to a Landungsbrücke is established over USB. TMCL commands are used for communicating with the IC. +""" +import time +import pytrinamic +from pytrinamic.connections import ConnectionManager +from pytrinamic.evalboards import TMC2240_eval + +pytrinamic.show_info() + +with ConnectionManager().connect() as my_interface: + print(my_interface) + + eval_board = TMC2240_eval(my_interface) + mc = eval_board.ics[0] + + + print("GCONF: 0x{0:08X}".format(eval_board.read_register(mc.REG.GCONF))) + print("GSTAT: 0x{0:08X}".format(eval_board.read_register(mc.REG.GSTAT))) + print("IFCNT: 0x{0:08X}".format(eval_board.read_register(mc.REG.IFCNT))) + print("SLAVECONF: 0x{0:08X}".format(eval_board.read_register(mc.REG.SLAVECONF))) + print("IOIN: 0x{0:08X}".format(eval_board.read_register(mc.REG.IOIN))) + print("DRV_CONF: 0x{0:08X}".format(eval_board.read_register(mc.REG.DRV_CONF))) + print("GLOBAL_SCALE 0x{0:08X}".format(eval_board.read_register(mc.REG.GLOBAL_SCALER))) + print("IHOLD_IRUN: 0x{0:08X}".format(eval_board.read_register(mc.REG.IHOLD_IRUN))) + print("TPOWERDOWN: 0x{0:08X}".format(eval_board.read_register(mc.REG.TPOWERDOWN))) + print("TSTEP: 0x{0:08X}".format(eval_board.read_register(mc.REG.TSTEP))) + print("TPWMTHRS: 0x{0:08X}".format(eval_board.read_register(mc.REG.TPWMTHRS))) + print("TCOOLTHRS: 0x{0:08X}".format(eval_board.read_register(mc.REG.TCOOLTHRS))) + print("THIGH: 0x{0:08X}".format(eval_board.read_register(mc.REG.THIGH))) + print("DIRECT_MODE: 0x{0:08X}".format(eval_board.read_register(mc.REG.DIRECT_MODE))) + print("ENCMODE: 0x{0:08X}".format(eval_board.read_register(mc.REG.ENCMODE))) + print("X_ENC: 0x{0:08X}".format(eval_board.read_register(mc.REG.X_ENC))) + print("ENC_CONST: 0x{0:08X}".format(eval_board.read_register(mc.REG.ENC_CONST))) + print("ENC_STATUS: 0x{0:08X}".format(eval_board.read_register(mc.REG.ENC_STATUS))) + print("ENC_LATCH: 0x{0:08X}".format(eval_board.read_register(mc.REG.ENC_LATCH))) + print("ADC_VSUPPLY_AIN: 0x{0:08X}".format(eval_board.read_register(mc.REG.ADC_VSUPPLY_AIN))) + print("ADC_TEMP: 0x{0:08X}".format(eval_board.read_register(mc.REG.ADC_TEMP))) + print("OTW_OV_VTH: 0x{0:08X}".format(eval_board.read_register(mc.REG.OTW_OV_VTH))) + print("MSLUT_0: 0x{0:08X}".format(eval_board.read_register(mc.REG.MSLUT_0))) + print("MSLUT_1: 0x{0:08X}".format(eval_board.read_register(mc.REG.MSLUT_1))) + print("MSLUT_2: 0x{0:08X}".format(eval_board.read_register(mc.REG.MSLUT_2))) + print("MSLUT_3: 0x{0:08X}".format(eval_board.read_register(mc.REG.MSLUT_3))) + print("MSLUT_4: 0x{0:08X}".format(eval_board.read_register(mc.REG.MSLUT_4))) + print("MSLUT_5: 0x{0:08X}".format(eval_board.read_register(mc.REG.MSLUT_5))) + print("MSLUT_6: 0x{0:08X}".format(eval_board.read_register(mc.REG.MSLUT_6))) + print("MSLUT_7: 0x{0:08X}".format(eval_board.read_register(mc.REG.MSLUT_7))) + print("MSLUTSEL: 0x{0:08X}".format(eval_board.read_register(mc.REG.MSLUTSEL))) + print("MSLUTSTART: 0x{0:08X}".format(eval_board.read_register(mc.REG.MSLUTSTART))) + print("MSCNT: 0x{0:08X}".format(eval_board.read_register(mc.REG.MSCNT))) + print("MSCURACT: 0x{0:08X}".format(eval_board.read_register(mc.REG.MSCURACT))) + print("CHOPCONF: 0x{0:08X}".format(eval_board.read_register(mc.REG.CHOPCONF))) + print("COOLCONF: 0x{0:08X}".format(eval_board.read_register(mc.REG.COOLCONF))) + print("DCCTRL: 0x{0:08X}".format(eval_board.read_register(mc.REG.DCCTRL))) + print("DRV_STATUS: 0x{0:08X}".format(eval_board.read_register(mc.REG.DRV_STATUS))) + print("PWMCONF: 0x{0:08X}".format(eval_board.read_register(mc.REG.PWMCONF))) + print("PWM_SCALE: 0x{0:08X}".format(eval_board.read_register(mc.REG.PWM_SCALE))) + print("PWM_AUTO: 0x{0:08X}".format(eval_board.read_register(mc.REG.PWM_AUTO))) + print("SG4_THRS: 0x{0:08X}".format(eval_board.read_register(mc.REG.SG4_THRS))) + print("SG4_RESULT: 0x{0:08X}".format(eval_board.read_register(mc.REG.SG4_RESULT))) + print("SG4_IND: 0x{0:08X}".format(eval_board.read_register(mc.REG.SG4_IND))) + + +print("\nReady.") diff --git a/examples/evalboards/TMC2240/rotate_demo.py b/examples/evalboards/TMC2240/rotate_demo.py new file mode 100644 index 00000000..f6575590 --- /dev/null +++ b/examples/evalboards/TMC2240/rotate_demo.py @@ -0,0 +1,45 @@ +""" +Move a motor back and forth using velocity and position mode of the TMC2240 +""" +import time +import pytrinamic +from pytrinamic.connections import ConnectionManager +from pytrinamic.evalboards import TMC2240_eval + +pytrinamic.show_info() + +with ConnectionManager().connect() as my_interface: + print(my_interface) + + # Create TMC2240-EVAL class which communicates over the Landungsbrücke via TMCL + eval_board = TMC2240_eval(my_interface) + mc = eval_board.ics[0] + motor = eval_board.motors[0] + + print("Preparing parameter...") + motor.set_axis_parameter(motor.AP.MaxAcceleration, 20000) + motor.set_axis_parameter(motor.AP.MaxVelocity, 100000) + motor.set_axis_parameter(motor.AP.MaxCurrent, 30) + + # Clear actual positions + motor.actual_position = 0 + + print("Rotating...") + motor.rotate(7*25600) + time.sleep(5) + + print("Stopping...") + motor.stop() + time.sleep(1) + + print("Moving back to 0...") + motor.move_to(0, 100000) + + # Wait until position 0 is reached + while motor.actual_position != 0: + print("Actual position: " + str(motor.actual_position)) + time.sleep(0.2) + + print("Reached position 0") + +print("\nReady.") diff --git a/examples/evalboards/TMC4671/TMC4671_eval_BLDC_open_loop.py b/examples/evalboards/TMC4671/TMC4671_eval_BLDC_open_loop.py index c1f63e4f..16d8d19b 100644 --- a/examples/evalboards/TMC4671/TMC4671_eval_BLDC_open_loop.py +++ b/examples/evalboards/TMC4671/TMC4671_eval_BLDC_open_loop.py @@ -8,7 +8,6 @@ pytrinamic.show_info() with ConnectionManager().connect() as my_interface: - # my_interface.enable_debug(True) print(my_interface) if isinstance(my_interface, UartIcInterface): diff --git a/examples/evalboards/TMC4671/TMC4671_eval_TMC6100_eval_BLDC_open_loop.py b/examples/evalboards/TMC4671/TMC4671_eval_TMC6100_eval_BLDC_open_loop.py index af29b61f..1f306a26 100644 --- a/examples/evalboards/TMC4671/TMC4671_eval_TMC6100_eval_BLDC_open_loop.py +++ b/examples/evalboards/TMC4671/TMC4671_eval_TMC6100_eval_BLDC_open_loop.py @@ -7,7 +7,6 @@ pytrinamic.show_info() with ConnectionManager().connect() as my_interface: - # my_interface.enable_debug(True) print(my_interface) # Create a TMC4671-EVAL and TMC6100-EVAL which communicates over the Landungsbrücke via TMCL diff --git a/examples/evalboards/TMC4671/TMC4671_eval_TMC6200_eval_BLDC_open_loop.py b/examples/evalboards/TMC4671/TMC4671_eval_TMC6200_eval_BLDC_open_loop.py index c5ec69eb..250ebbca 100644 --- a/examples/evalboards/TMC4671/TMC4671_eval_TMC6200_eval_BLDC_open_loop.py +++ b/examples/evalboards/TMC4671/TMC4671_eval_TMC6200_eval_BLDC_open_loop.py @@ -7,7 +7,6 @@ pytrinamic.show_info() with ConnectionManager().connect() as my_interface: - # myInterface.enable_debug(True) print(my_interface) # Create a TMC4671-EVAL and TMC6200-EVAL which communicates over the Landungsbrücke via TMCL diff --git a/examples/evalboards/TMC5160/rotate_demo.py b/examples/evalboards/TMC5160/rotate_demo.py index f07d3be3..bbdab8d0 100644 --- a/examples/evalboards/TMC5160/rotate_demo.py +++ b/examples/evalboards/TMC5160/rotate_demo.py @@ -1,5 +1,9 @@ """ Move a motor back and forth using velocity and position mode of the TMC5160 + +Line 31, we set a lower run/standby current for the motor. Using NEMA17, this should result in a coil current around 800mA. +If the motor is stalling due to too low current, set motorCurrent higher. +If a lower value still is needed, set GLOBAL_SCALER register to 128 to half motor current. """ import time import pytrinamic @@ -16,7 +20,7 @@ mc = eval_board.ics[0] motor = eval_board.motors[0] - print("Preparing parameter...") + print("Preparing parameters...") eval_board.write_register(mc.REG.A1, 1000) eval_board.write_register(mc.REG.V1, 50000) eval_board.write_register(mc.REG.D1, 500) @@ -25,11 +29,16 @@ eval_board.write_register(mc.REG.VSTOP, 10) eval_board.write_register(mc.REG.AMAX, 1000) + # Set lower run/standby current + motorCurrent = 2 + motor.set_axis_parameter(motor.AP.MaxCurrent, motorCurrent) + motor.set_axis_parameter(motor.AP.StandbyCurrent, motorCurrent) + # Clear actual positions motor.actual_position = 0 print("Rotating...") - motor.rotate(7*25600) + motor.rotate(7 * 25600) time.sleep(5) print("Stopping...") @@ -37,7 +46,7 @@ time.sleep(1) print("Moving back to 0...") - motor.move_to(0, 100000) + motor.move_to(0, 7 * 25600) # Wait until position 0 is reached while motor.actual_position != 0: diff --git a/examples/modules/TMCM1231/TMCL/StallGuard2_demo.py b/examples/modules/TMCM1231/TMCL/StallGuard2_demo.py new file mode 100644 index 00000000..36077618 --- /dev/null +++ b/examples/modules/TMCM1231/TMCL/StallGuard2_demo.py @@ -0,0 +1,78 @@ +""" +Sets the StallGuard2 threshold such that the stall guard value (i.e SG value) is zero +when the motor comes close to stall and also sets the stop on stall velocity to a value +one less than the actual velocity of the motor +""" +import pytrinamic +from pytrinamic.connections import ConnectionManager +from pytrinamic.modules import TMCM1231 +import time + +def stallguard2_init(motor, init_velocity): + # Resetting SG2 threshold and stop on stall velocity to zero + motor.stallguard2.set_threshold(0) + motor.stallguard2.stop_velocity = 0 + print("Initial StallGuard2 values:") + print(motor.stallguard2) + print("Rotating...") + motor.rotate(init_velocity) + sgthresh = 0 + sgt = 0 + load_samples = [] + while (sgt == 0) and (sgthresh < 64): + load_samples = [] + motor.stallguard2.set_threshold(sgthresh) + time.sleep(0.2) + sgthresh += 1 + for i in range(50): + load_samples.append(motor.stallguard2.get_load_value()) + if not any(load_samples): + sgt = 0 + else: + sgt = max(load_samples) + while 1: + load_samples = [] + for i in range(50): + load_samples.append(motor.stallguard2.get_load_value()) + if 0 in load_samples: + motor.drive_settings.max_current = motor.drive_settings.max_current - 1 + else: + break + + motor.stallguard2.stop_velocity = motor.get_actual_velocity() - 1 + print("Configured StallGuard2 parameters:") + print(motor.stallguard2) + +def main(): + pytrinamic.show_info() + + # This example is using PCAN, if you want to use another connection please change the next line. + connection_manager = ConnectionManager("--interface pcan_tmcl") + with connection_manager.connect() as my_interface: + module = TMCM1231(my_interface) + motor = module.motors[0] + + print("Preparing parameters") + # preparing drive settings + motor.drive_settings.max_current = 20 + motor.drive_settings.standby_current = 8 + motor.drive_settings.boost_current = 0 + motor.drive_settings.microstep_resolution = motor.ENUM.microstep_resolution_256_microsteps + print(motor.drive_settings) + print(motor.linear_ramp) + + time.sleep(1.0) + + # clear position counter + motor.actual_position = 0 + + # set up StallGuard2 + print("Configuring StallGuard2 parameters...") + stallguard2_init(motor, init_velocity = 10000) + print("Apply load and try to stall the motor...") + while not (motor.actual_velocity == 0): + pass + print("Motor stopped by StallGuard2!") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/modules/TMCM1231/TMCL/rotate_demo.py b/examples/modules/TMCM1231/TMCL/rotate_demo.py new file mode 100644 index 00000000..c4eeed1f --- /dev/null +++ b/examples/modules/TMCM1231/TMCL/rotate_demo.py @@ -0,0 +1,68 @@ +import pytrinamic +from pytrinamic.connections import ConnectionManager +from pytrinamic.modules import TMCM1231 +import time + +pytrinamic.show_info() + +# This example is using PCAN, if you want to use another connection please change the next line. +connectionManager = ConnectionManager("--interface pcan_tmcl") +with connectionManager.connect() as myInterface: + module = TMCM1231(myInterface) + motor = module.motors[0] + + # Please be sure not to use a too high current setting for your motor. + + print("Preparing parameters") + # preparing drive settings + motor.drive_settings.max_current = 128 + motor.drive_settings.standby_current = 0 + motor.drive_settings.boost_current = 0 + motor.drive_settings.microstep_resolution = motor.ENUM.microstep_resolution_256_microsteps + print(motor.drive_settings) + + + # preparing linear ramp settings + motor.max_acceleration = 51200 + motor.max_velocity = 51200 + + # reset actual position + motor.actual_position = 0 + + print(motor.linear_ramp) + + # start rotating motor in different directions + print("Rotating") + motor.rotate(51200) + time.sleep(5) + + # stop rotating motor + print("Stopping") + motor.stop() + + # read actual position + print("ActualPostion = {}".format(motor.actual_position)) + time.sleep(2) + + # doubling moved distance + print("Doubling moved distance") + motor.move_by(motor.actual_position) + + # wait till position_reached + while not(motor.get_position_reached()): + print("target position motor: " + str(motor.target_position) + " actual position motor: " + str(motor.actual_position)) + + time.sleep(0.2) + print("Furthest point reached") + print("ActualPostion motor = {}".format(motor.actual_position)) + + # short delay and move back to start + time.sleep(2) + print("Moving back to 0") + motor.move_to(0) + + # wait until position 0 is reached + while not(motor.get_position_reached()): + print("target position motor: " + str(motor.target_position) + " actual position motor: " + str(motor.actual_position)) + + print("Reached Position 0") diff --git a/examples/modules/TMCM1240/TMCL/StallGuard2_demo.py b/examples/modules/TMCM1240/TMCL/StallGuard2_demo.py new file mode 100644 index 00000000..b866efd8 --- /dev/null +++ b/examples/modules/TMCM1240/TMCL/StallGuard2_demo.py @@ -0,0 +1,81 @@ +""" +Sets the StallGuard2 threshold such that the stall guard value (i.e SG value) is zero +when the motor comes close to stall and also sets the stop on stall velocity to a value +one less than the actual velocity of the motor +""" + +import pytrinamic +from pytrinamic.connections import ConnectionManager +from pytrinamic.modules import TMCM1240 +import time + +def stallguard2_init(motor, init_velocity): + # Resetting SG2 threshold and stop on stall velocity to zero + motor.stallguard2.set_threshold(0) + motor.stallguard2.stop_velocity = 0 + print("Initial StallGuard2 values:") + print(motor.stallguard2) + print("Rotating...") + motor.rotate(init_velocity) + sgthresh = 0 + sgt = 0 + load_samples = [] + while (sgt == 0) and (sgthresh < 64): + load_samples = [] + motor.stallguard2.set_threshold(sgthresh) + time.sleep(0.2) + sgthresh += 1 + for i in range(50): + load_samples.append(motor.stallguard2.get_load_value()) + + if not any(load_samples): + sgt = 0 + else: + sgt = max(load_samples) + while 1: + load_samples = [] + for i in range(50): + load_samples.append(motor.stallguard2.get_load_value()) + if 0 in load_samples: + motor.drive_settings.max_current = motor.drive_settings.max_current - 8 + else: + break + + motor.stallguard2.stop_velocity = motor.get_actual_velocity() - 1 + print("Configured StallGuard2 parameters:") + print(motor.stallguard2) + +def main(): + pytrinamic.show_info() + + connection_manager = ConnectionManager() + with connection_manager.connect() as my_interface: + module = TMCM1240(my_interface) + motor = module.motors[0] + + + print("Preparing parameters") + # preparing drive settings + motor.drive_settings.max_current = 60 + motor.drive_settings.standby_current = 8 + motor.drive_settings.boost_current = 0 + motor.drive_settings.microstep_resolution = motor.ENUM.MicrostepResolution256Microsteps + + print(motor.drive_settings) + print(motor.linear_ramp) + + time.sleep(1.0) + + # clear position counter + motor.actual_position = 0 + + # set up StallGuard2 + print("Configuring StallGuard2 parameters...") + stallguard2_init(motor,init_velocity = 10000) + print("Apply load and try to stall the motor...") + while not (motor.actual_velocity == 0): + pass + print("Motor stopped by StallGuard2!") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/modules/TMCM1276/TMCL/six_point_ramp_demo.py b/examples/modules/TMCM1276/TMCL/six_point_ramp_demo.py new file mode 100644 index 00000000..48dbc954 --- /dev/null +++ b/examples/modules/TMCM1276/TMCL/six_point_ramp_demo.py @@ -0,0 +1,73 @@ +import dataclasses + +import matplotlib.pyplot as plt +import pytrinamic +from pytrinamic.connections import ConnectionManager +from pytrinamic.modules import TMCM1276 +import time + + +@dataclasses.dataclass +class Sample: + timestamp: float + position: int + velocity: int + + +pytrinamic.show_info() + +# This example is using PCAN, if you want to use another connection please change the next line. +connectionManager = ConnectionManager("--interface pcan_tmcl") + +with connectionManager.connect() as myInterface: + module = TMCM1276(myInterface) + motor = module.motors[0] + + # Setting axis parameters for configuring SixPoint ramp + motor.set_axis_parameter(motor.AP.MaxVelocity, 40000) + motor.set_axis_parameter(motor.AP.MaxAcceleration, 30000) + motor.set_axis_parameter(motor.AP.A1, 5000) + motor.set_axis_parameter(motor.AP.V1, 10000) + motor.set_axis_parameter(motor.AP.MaxDeceleration, 20000) + motor.set_axis_parameter(motor.AP.D1, 5000) + motor.set_axis_parameter(motor.AP.StartVelocity, 5000) + motor.set_axis_parameter(motor.AP.StopVelocity, 5000) + motor.set_axis_parameter(motor.AP.RampWaitTime, 31250) + + # Setting initial position to zero + motor.actual_position = 0 + + samples = [] + motor.move_to(100000) + while not motor.get_position_reached(): + samples.append(Sample(time.perf_counter(), format(motor.actual_position), format(motor.actual_velocity))) + + motor.move_to(0) + while not motor.get_position_reached(): + samples.append(Sample(time.perf_counter(), format(motor.actual_position), format(motor.actual_velocity))) + + fig, ax = plt.subplots(2) + t = [float(s.timestamp - samples[0].timestamp) for s in samples] + pos = [float(s.position) for s in samples] + vel = [float(s.velocity) for s in samples] + + ax[0].plot(t, pos, label='Position') + ax[0].set_title('Pos vs Time') + ax[0].set_xlabel('Time') + ax[0].set_ylabel('Pos') + ax[0].legend() + ax[0].grid() + + ax[1].plot(t, vel, label='Velocity') + ax[1].set_title('Vel vs Time') + ax[1].set_xlabel('Time') + ax[1].set_ylabel('Vel') + ax[1].legend() + ax[1].grid() + plt.show() + + + + + + diff --git a/examples/modules/TMCM1276/TMCL/stop_switch_demo.py b/examples/modules/TMCM1276/TMCL/stop_switch_demo.py new file mode 100644 index 00000000..a9c2fe47 --- /dev/null +++ b/examples/modules/TMCM1276/TMCL/stop_switch_demo.py @@ -0,0 +1,32 @@ +import pytrinamic +from pytrinamic.connections import ConnectionManager +from pytrinamic.modules import TMCM1276 +import time + +pytrinamic.show_info() + +# This example is using PCAN, if you want to use another connection please change the next line. +connectionManager = ConnectionManager("--interface pcan_tmcl") + +with connectionManager.connect() as myInterface: + module = TMCM1276(myInterface) + motor = module.motors[0] + + print("Preparing parameters") + # preparing linear ramp settings + motor.max_acceleration = 20000 + + while 1: + if motor.get_axis_parameter(motor.AP.RightEndstop): + motor.stop() + time.sleep(5) + print("Rotating in opposite direction") + motor.rotate(-50000) + time.sleep(5) + motor.stop() + break + else: + print("Rotating") + motor.rotate(50000) + time.sleep(5) + diff --git a/examples/modules/TMCM6214/TMCL/StallGuard2_demo.py b/examples/modules/TMCM6214/TMCL/StallGuard2_demo.py new file mode 100644 index 00000000..f9e1d3b5 --- /dev/null +++ b/examples/modules/TMCM6214/TMCL/StallGuard2_demo.py @@ -0,0 +1,80 @@ +""" +Sets the StallGuard2 threshold such that the stall guard value (i.e SG value) is zero +when the motor comes close to stall and also sets the stop on stall velocity to a value +one less than the actual velocity of the motor +""" +import pytrinamic +from pytrinamic.connections import ConnectionManager +from pytrinamic.modules import TMCM6214 +import time + +def stallguard2_init(motor, init_velocity): + # Resetting SG2 threshold and stop on stall velocity to zero + motor.stallguard2.set_threshold(0) + motor.stallguard2.stop_velocity = 0 + print("Initial StallGuard2 values:") + print(motor.stallguard2) + print("Rotating...") + motor.rotate(init_velocity) + sgthresh = 0 + sgt = 0 + load_samples = [] + while (sgt == 0) and (sgthresh < 64): + load_samples = [] + motor.stallguard2.set_threshold(sgthresh) + time.sleep(0.2) + sgthresh += 1 + for i in range(50): + load_samples.append(motor.stallguard2.get_load_value()) + if not any(load_samples): + sgt = 0 + else: + sgt = max(load_samples) + while 1: + load_samples = [] + for i in range(50): + load_samples.append(motor.stallguard2.get_load_value()) + print(load_samples) + if 0 in load_samples: + motor.drive_settings.max_current = motor.drive_settings.max_current - 8 + else: + break + + motor.stallguard2.stop_velocity = motor.get_actual_velocity() - 1 + print("Configured StallGuard2 parameters:") + print(motor.stallguard2) + +def main(): + pytrinamic.show_info() + + connection_manager = ConnectionManager() + with connection_manager.connect() as my_interface: + module = TMCM6214(my_interface) + motor = module.motors[0] + + print("Preparing parameters") + # preparing drive settings + motor.drive_settings.max_current = 128 + motor.drive_settings.standby_current = 8 + motor.drive_settings.boost_current = 0 + motor.drive_settings.microstep_resolution = motor.ENUM.MicrostepResolution256Microsteps + print(motor.drive_settings) + print(motor.linear_ramp) + + time.sleep(1.0) + + # clear position counter + motor.actual_position = 0 + + # set up StallGuard2 + print("Configuring StallGuard2 parameters...") + stallguard2_init(motor, init_velocity = 10000) + print("Apply load and try to stall the motor...") + while not (motor.actual_velocity == 0): + pass + print("Motor stopped by StallGuard2!") + +if __name__ == "__main__": + main() + + diff --git a/examples/modules/TMCM6214/TMCL/rotate_demo.py b/examples/modules/TMCM6214/TMCL/rotate_demo.py new file mode 100644 index 00000000..0802b1b5 --- /dev/null +++ b/examples/modules/TMCM6214/TMCL/rotate_demo.py @@ -0,0 +1,47 @@ +import pytrinamic +from pytrinamic.connections import ConnectionManager +from pytrinamic.modules import TMCM6214 +import time + +pytrinamic.show_info() +connectionManager = ConnectionManager() + +with connectionManager.connect() as myInterface: + module = TMCM6214(myInterface) + motor_0 = module.motors[0] + + print("Preparing parameters") + motor_0.max_acceleration = 20000 + + print("Rotating") + motor_0.rotate(50000) + + time.sleep(5) + + print("Stopping") + motor_0.stop() + + print("ActualPostion = {}".format(motor_0.actual_position)) + + time.sleep(5) + + print("Doubling moved distance") + motor_0.move_by(motor_0.actual_position, 50000) + while not(motor_0.get_position_reached()): + pass + + print("Furthest point reached") + print("ActualPostion = {}".format(motor_0.actual_position)) + + time.sleep(5) + + print("Moving back to 0") + motor_0.move_to(0, 100000) + + # Wait until position 0 is reached + while not(motor_0.get_position_reached()): + pass + + print("Reached Position 0") + + diff --git a/examples/modules/TMCM6214/TMCL/six_point_ramp_demo.py b/examples/modules/TMCM6214/TMCL/six_point_ramp_demo.py new file mode 100644 index 00000000..13c34d27 --- /dev/null +++ b/examples/modules/TMCM6214/TMCL/six_point_ramp_demo.py @@ -0,0 +1,66 @@ +import dataclasses +import matplotlib.pyplot as plt +import pytrinamic +from pytrinamic.connections import ConnectionManager +from pytrinamic.modules import TMCM6214 +import time + + +@dataclasses.dataclass +class Sample: + timestamp: float + position: int + velocity: int + + +pytrinamic.show_info() +# This example is using USB. +connectionManager = ConnectionManager() + +with connectionManager.connect() as myInterface: + module = TMCM6214(myInterface) + motor = module.motors[0] + + # Setting axis parameters for configuring SixPoint ramp + motor.set_axis_parameter(motor.AP.MaxVelocity, 40000) + motor.set_axis_parameter(motor.AP.MaxAcceleration, 30000) + motor.set_axis_parameter(motor.AP.A1, 5000) + motor.set_axis_parameter(motor.AP.V1, 10000) + motor.set_axis_parameter(motor.AP.MaxDeceleration, 20000) + motor.set_axis_parameter(motor.AP.D1, 5000) + motor.set_axis_parameter(motor.AP.StartVelocity, 5000) + motor.set_axis_parameter(motor.AP.StopVelocity, 5000) + motor.set_axis_parameter(motor.AP.RampWaitTime, 31250) + + # Setting initial position to zero + motor.actual_position = 0 + + samples = [] + motor.move_to(100000) + while not motor.get_position_reached(): + samples.append(Sample(time.perf_counter(), format(motor.actual_position), format(motor.actual_velocity))) + + motor.move_to(0) + while not motor.get_position_reached(): + samples.append(Sample(time.perf_counter(), format(motor.actual_position), format(motor.actual_velocity))) + + fig, ax = plt.subplots(2) + t = [float(s.timestamp - samples[0].timestamp) for s in samples] + pos = [float(s.position) for s in samples] + vel = [float(s.velocity) for s in samples] + + ax[0].plot(t, pos, label='Position') + ax[0].set_title('Pos vs Time') + ax[0].set_xlabel('Time') + ax[0].set_ylabel('Pos') + ax[0].legend() + ax[0].grid() + + ax[1].plot(t, vel, label='Velocity') + ax[1].set_title('Vel vs Time') + ax[1].set_xlabel('Time') + ax[1].set_ylabel('Vel') + ax[1].legend() + ax[1].grid() + plt.show() + diff --git a/examples/tools/FirmwareUpdate.py b/examples/tools/FirmwareUpdate.py index 45b89e88..64747e30 100644 --- a/examples/tools/FirmwareUpdate.py +++ b/examples/tools/FirmwareUpdate.py @@ -71,26 +71,30 @@ print("Connecting") myInterface = connectionManager.connect() -# Send the boot command -print("Switching to bootloader mode") -myInterface.send_boot(1) -myInterface.close() - -# Reconnect after a small delay -print("Reconnecting") -timestamp = time.time() -while (time.time() - timestamp) < SERIAL_BOOT_TIMEOUT: - try: - # Attempt to connect - myInterface = connectionManager.connect() - # If no exception occurred, exit the retry loop - break - except (ConnectionError, TypeError): - myInterface = None +# If not already in bootloader, enter it +if not "B" in myInterface.get_version_string().upper(): + # Send the boot command + print("Switching to bootloader mode") + myInterface.send_boot(1) + myInterface.close() + + # Reconnect after a small delay + print("Reconnecting") + timestamp = time.time() + while (time.time() - timestamp) < SERIAL_BOOT_TIMEOUT: + try: + # Attempt to connect + myInterface = connectionManager.connect() + # If no exception occurred, exit the retry loop + break + except (ConnectionError, TypeError): + myInterface = None + + if not myInterface: + print("Error: Timeout when attempting to reconnect to bootloader") + exit(1) -if not myInterface: - print("Error: Timeout when attempting to reconnect to bootloader") - exit(1) +time.sleep(1) # Retrieve the bootloader version bootloaderVersion = myInterface.get_version_string(1) @@ -123,6 +127,9 @@ print("Error: No matching version string found in firmware image") exit(1) +start = file.minaddr() +length = file.maxaddr() - start + print("Bootloader version: " + bootloaderVersion) print("Firmware version: " + found.group(0)) diff --git a/pytrinamic/RAMDebug.py b/pytrinamic/RAMDebug.py index b1f13bc7..50a34f4c 100644 --- a/pytrinamic/RAMDebug.py +++ b/pytrinamic/RAMDebug.py @@ -1,4 +1,4 @@ -from pytrinamic.tmcl import TMCLCommand +from pytrinamic.tmcl import TMCLCommand, TMCLReplyStatusError, TMCLStatus from enum import IntEnum class RAMDebug_Command(IntEnum): @@ -56,6 +56,12 @@ class RAMDebug_State(IntEnum): COMPLETE = 3 PRETRIGGER = 4 + UNKNOWN_STATUS = -1 # Placeholder value in case invalid state values were returned + + @classmethod + def _missing_(cls, value): + return cls.UNKNOWN_STATUS + class Channel(): def __init__(self, channel_type, value, address = 0, signed = False, mask = 0xFFFF_FFFF, shift = 0): #TODO: add signed self.value = value @@ -132,7 +138,7 @@ def __init__(self, connection): self._trigger_mask = 0x0000_0000 self._trigger_shift = 0x0000_0000 self.channels = [] - self.samples = [] + self.samples = None def get_sample_count(self): return self._sample_count @@ -147,26 +153,54 @@ def set_process_frequency(self, process_frequency): self._process_frequency = process_frequency def set_prescaler(self, prescaler): + """ + Set the capture prescaler to divide the capture frequency. + The actual capture frequency is MAX_FREQUENCY/(prescaler+1). + """ self._prescaler = prescaler + def set_divider(self, divider): + """ + Set the capture prescaler to divide the capture frequency. + The actual capture frequency is MAX_FREQUENCY/divider. + """ + if not (1 <= divider <= 0xFFFF_FFFF): + raise ValueError("Invalid divider value. Possible divider values are [1; 2^32-1]") + + self._prescaler = divider-1 + def set_trigger_type(self, trigger_type): - if isinstance(trigger_type, RAMDebug_Trigger): - self._trigger_type = trigger_type + if not isinstance(trigger_type, RAMDebug_Trigger): + raise ValueError("Invalid trigger type - you must pass a RAMDebug_Trigger object") + + self._trigger_type = trigger_type def set_trigger_threshold(self, trigger_threshold): self._trigger_threshold = trigger_threshold def set_trigger_channel(self, channel): - if isinstance(channel, Channel): - self._trigger_channel = channel - self._trigger_mask = channel._mask - self._trigger_shift = channel._shift + if not isinstance(channel, Channel): + raise ValueError("Invalid channel - you must pass a Channel object") + self._trigger_channel = channel + self._trigger_mask = channel.mask + self._trigger_shift = channel.shift + + def set_trigger(self, trigger_channel, trigger_type, trigger_threshold): + """ + Fully configure the RAMDebug trigger + """ + self.set_trigger_type(trigger_type) + self.set_trigger_threshold(trigger_threshold) + self.set_trigger_channel(trigger_channel) def set_pretrigger_samples(self, pretrigger_samples): self._pretrigger_samples = pretrigger_samples def set_channel(self, channel): + if not isinstance(channel, Channel): + raise ValueError("Invalid channel - you must pass a Channel object") + if self.channel_count() >= self.MAX_CHANNELS: raise RuntimeError("Out of channels!") @@ -175,12 +209,47 @@ def set_channel(self, channel): def get_channels(self): return self.channels - def start_measurement(self): + def start_measurement(self, *, strict=True): + """ + Start the measurement. + If you are waiting for a trigger, wait until is_pretriggering() returns false before causing + your trigger event. + + Arguments: + - strict: + When set to True, reject invalid sample counts. + When set to False, automatically adjust too high sample counts. + """ + samples = self.get_total_samples() + if self.get_total_samples() > self.MAX_ELEMENTS: + if strict: + raise RuntimeError(f"Too many samples requested! Requested {self.get_total_samples()} ({self._sample_count} for {self.channel_count()} channels). Maximum available samples: {self.MAX_ELEMENTS}. Either adjust your sample count or pass strict=False to this function to let RAMDebug reduce sample count automatically.") + else: + # Non-strict mode: Limit the sample count + samples = self.MAX_ELEMENTS - (self.MAX_ELEMENTS % self.channel_count()) + + pretrigger_samples = self._pretrigger_samples * self.channel_count() + if pretrigger_samples > samples: + if strict: + raise RuntimeError(f"Too many pretrigger samples requested! Requested {pretrigger_samples} pretrigger samples, but only capturing {samples} samples.") + else: + # Non-strict mode: Limit the pretrigger sample count + pretrigger_samples = samples + self._command(RAMDebug_Command.INIT.value, 0, 0) - self._command(RAMDebug_Command.SET_SAMPLE_COUNT.value, 0, self.get_total_samples()) - self._command(RAMDebug_Command.SET_PROCESS_FREQUENCY, 0, self._process_frequency) + self._command(RAMDebug_Command.SET_SAMPLE_COUNT.value, 0, samples) self._command(RAMDebug_Command.SET_PRESCALER.value, 0, self._prescaler) + try: + self._command(RAMDebug_Command.SET_PROCESS_FREQUENCY, 0, self._process_frequency) + except TMCLReplyStatusError as e: + if e.status_code == TMCLStatus.WRONG_TYPE: + # SET_PROCESS_FREQUENCY not supported -> skip exception + pass + else: + # A different error occurred -> reraise exception + raise e + for channel in self.channels: self._command(RAMDebug_Command.SET_CHANNEL.value, channel.type.value, channel.value) @@ -189,14 +258,21 @@ def start_measurement(self): self._command(RAMDebug_Command.SET_TRIGGER_CHANNEL.value, self._trigger_channel.type.value, self._trigger_channel.value) self._command(RAMDebug_Command.ENABLE_TRIGGER.value, self._trigger_type.value, self._trigger_threshold) + def is_pretriggering(self): + return self.get_state() == RAMDebug_State.PRETRIGGER + def is_measurement_done(self): - return self.get_state() == RAMDebug_State.COMPLETE.value + return self.get_state() == RAMDebug_State.COMPLETE def get_samples(self): + # If the samples were already downloaded, just return them + if self.samples: + return self.samples + i = 0 data = [] - while i < self.get_total_samples(): + while i < min(self.get_total_samples(), self.MAX_ELEMENTS): reply = self._command(RAMDebug_Command.GET_SAMPLE.value, 0, i) done = reply.status != 0x64 if done: @@ -242,7 +318,10 @@ def channel_count(self): return len(self.channels) def get_state(self): - return self._command(RAMDebug_Command.GET_STATE.value, 0, 0).value + """ + Returns the state of this measurement as a RAMDebug_State enum + """ + return RAMDebug_State(self._command(RAMDebug_Command.GET_STATE.value, 0, 0).value) def __str__(self): text = f"RAMDebug handler for connection {self._connection}\n" diff --git a/pytrinamic/connections/__init__.py b/pytrinamic/connections/__init__.py index ddae80e1..6f98e0eb 100644 --- a/pytrinamic/connections/__init__.py +++ b/pytrinamic/connections/__init__.py @@ -1,10 +1,10 @@ from .dummy_tmcl_interface import DummyTmclInterface -from .pcan_tmcl_interface import PcanTmclInterface -from .socketcan_tmcl_interface import SocketcanTmclInterface -from .kvaser_tmcl_interface import KvaserTmclInterface +from .can_tmcl.pcan_tmcl_interface import PcanTmclInterface +from .can_tmcl.socketcan_tmcl_interface import SocketcanTmclInterface +from .can_tmcl.kvaser_tmcl_interface import KvaserTmclInterface from .serial_tmcl_interface import SerialTmclInterface from .uart_ic_interface import UartIcInterface from .usb_tmcl_interface import UsbTmclInterface -from .slcan_tmcl_interface import SlcanTmclInterface -from .ixxat_tmcl_interface import IxxatTmclInterface +from .can_tmcl.slcan_tmcl_interface import SlcanTmclInterface +from .can_tmcl.ixxat_tmcl_interface import IxxatTmclInterface from .connection_manager import ConnectionManager diff --git a/pytrinamic/connections/can_tmcl/__init__.py b/pytrinamic/connections/can_tmcl/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pytrinamic/connections/can_tmcl/ixxat_tmcl_interface.py b/pytrinamic/connections/can_tmcl/ixxat_tmcl_interface.py new file mode 100644 index 00000000..4f198c98 --- /dev/null +++ b/pytrinamic/connections/can_tmcl/ixxat_tmcl_interface.py @@ -0,0 +1,58 @@ +import can +from ...connections.can_tmcl_interface import CanTmclInterface + + +class IxxatTmclInterface(CanTmclInterface): + """ + This class implements a TMCL connection for IXXAT USB-to-CAN adapter. + Backend is provided by the IXXAT Virtual CAN Interface V3 SDK. + + Port number is assigned as adapters are plugged in, arbitrarely, + it is possible to use multiple channels of one IXXAT, CAN1 is port "0", CAN2 is port "1", etc. + + This class, and the parser implementation DOES NOT support multiple IXXATs connected to one computer. + + To add this functionality, python-can version 4.0.0 is necessary as it allows enumerating IXXAT devices IDs, + (see https://github.com/hardbyte/python-can/pull/926). + To use multiple IXXAT devices, you must provide hardware IDs (this needs to be added to this class and to the parser). + + Snippet to list IXXAT by hardware ID using python-can 4.0.0: + from can.interfaces.ixxat import IXXATBus + for hwid in IXXATBus.list_adapters(): + print("Found IXXAT adapter with hardware id '%s'." % hwid) + """ + + # Providing 5 channels here, this should cover all use cases. + # Ixxat USB-to-CAN provides 1, 2 or 4 channels. + _CHANNELS = ["0", "1", "2", "3", "4"] + + def __init__(self, port="0", datarate=1000000, host_id=2, module_id=1, timeout_s=5): + if not isinstance(port, str): + raise TypeError + + if port not in self._CHANNELS: + raise ValueError("Invalid port!") + + CanTmclInterface.__init__(self, port, datarate, host_id, module_id, timeout_s) + + self.logger.info("Connect to bus with bit-rate %s.", self._bitrate) + try: + self._connection = can.Bus(interface="ixxat", + channel=self._channel, + bitrate=self._bitrate, + can_filters=[{"can_id": host_id, "can_mask": 0x7F}]) + except can.CanError as e: + self._connection = None + raise ConnectionError( + f"Failed to connect to {self.__class__.__name__} on channel {str(self._channel)}" + ) from e + + @classmethod + def list(cls): + """ + Return a list of available connection ports as a list of strings. + + This function is required for using this interface with the + connection manager. + """ + return cls._CHANNELS diff --git a/pytrinamic/connections/can_tmcl/kvaser_tmcl_interface.py b/pytrinamic/connections/can_tmcl/kvaser_tmcl_interface.py new file mode 100644 index 00000000..d6015f73 --- /dev/null +++ b/pytrinamic/connections/can_tmcl/kvaser_tmcl_interface.py @@ -0,0 +1,39 @@ +import can +from ...connections.can_tmcl_interface import CanTmclInterface + + +class KvaserTmclInterface(CanTmclInterface): + """ + This class implements a TMCL connection for Kvaser adapter using CANLIB. + Try 0 as default channel. + """ + _CHANNELS = ["0", "1", "2"] + + def __init__(self, port="0", datarate=1000000, host_id=2, module_id=1, timeout_s=5): + if not isinstance(port, str): + raise TypeError + + if port not in self._CHANNELS: + raise ValueError("Invalid port!") + + CanTmclInterface.__init__(self, port, datarate, host_id, module_id, timeout_s) + + self.logger.info("Connect to bus with bit-rate %s.", self._bitrate) + try: + self._connection = can.Bus(interface="kvaser", + channel=self._channel, + bitrate=self._bitrate, + can_filters=[{"can_id": host_id, "can_mask": 0x7F}]) + except can.CanError as e: + self._connection = None + raise ConnectionError("Failed to connect to Kvaser CAN bus") from e + + @classmethod + def list(cls): + """ + Return a list of available connection ports as a list of strings. + + This function is required for using this interface with the + connection manager. + """ + return cls._CHANNELS diff --git a/pytrinamic/connections/can_tmcl/pcan_tmcl_interface.py b/pytrinamic/connections/can_tmcl/pcan_tmcl_interface.py new file mode 100644 index 00000000..6d82078a --- /dev/null +++ b/pytrinamic/connections/can_tmcl/pcan_tmcl_interface.py @@ -0,0 +1,62 @@ + +import can +from can.interfaces.pcan.pcan import PcanError +from ...connections.can_tmcl_interface import CanTmclInterface + + +class PcanTmclInterface(CanTmclInterface): + """ + This class implements a TMCL connection over a PCAN adapter. + """ + _CHANNELS = [ + "PCAN_USBBUS1", "PCAN_USBBUS2", "PCAN_USBBUS3", "PCAN_USBBUS4", + "PCAN_USBBUS5", "PCAN_USBBUS6", "PCAN_USBBUS7", "PCAN_USBBUS8", + "PCAN_USBBUS9", "PCAN_USBBUS10", "PCAN_USBBUS11", "PCAN_USBBUS12", + "PCAN_USBBUS13", "PCAN_USBBUS14", "PCAN_USBBUS15", "PCAN_USBBUS16", + + "PCAN_ISABUS1", "PCAN_ISABUS2", "PCAN_ISABUS3", "PCAN_ISABUS4", + "PCAN_ISABUS5", "PCAN_ISABUS6", "PCAN_ISABUS7", "PCAN_ISABUS8", + + "PCAN_DNGBUS1", + + "PCAN_PCIBUS1", "PCAN_PCIBUS2", "PCAN_PCIBUS3", "PCAN_PCIBUS4", + "PCAN_PCIBUS5", "PCAN_PCIBUS6", "PCAN_PCIBUS7", "PCAN_PCIBUS8", + "PCAN_PCIBUS9", "PCAN_PCIBUS10", "PCAN_PCIBUS11", "PCAN_PCIBUS12", + "PCAN_PCIBUS13", "PCAN_PCIBUS14", "PCAN_PCIBUS15", "PCAN_PCIBUS16", + + "PCAN_PCCBUS1", "PCAN_PCCBUS2", + + "PCAN_LANBUS1", "PCAN_LANBUS2", "PCAN_LANBUS3", "PCAN_LANBUS4", + "PCAN_LANBUS5", "PCAN_LANBUS6", "PCAN_LANBUS7", "PCAN_LANBUS8", + "PCAN_LANBUS9", "PCAN_LANBUS10", "PCAN_LANBUS11", "PCAN_LANBUS12", + "PCAN_LANBUS13", "PCAN_LANBUS14", "PCAN_LANBUS15", "PCAN_LANBUS16" + ] + + def __init__(self, port, datarate=1000000, host_id=2, module_id=1, timeout_s=5): + if not isinstance(port, str): + raise TypeError + + if port not in self._CHANNELS: + raise ValueError("Invalid port!") + + CanTmclInterface.__init__(self, port, datarate, host_id, module_id, timeout_s) + self._channel = port + self._bitrate = datarate + + self.logger.info("Connect to bus with bit-rate %s.", self._bitrate) + try: + self._connection = can.Bus(interface="pcan", channel=self._channel, bitrate=self._bitrate) + self._connection.set_filters([{"can_id": host_id, "can_mask": 0xFFFFFFFF}]) + except PcanError as e: + self._connection = None + raise ConnectionError("Failed to connect to PCAN bus") from e + + @classmethod + def list(cls): + """ + Return a list of available connection ports as a list of strings. + + This function is required for using this interface with the + connection manager. + """ + return cls._CHANNELS diff --git a/pytrinamic/connections/can_tmcl/slcan_tmcl_interface.py b/pytrinamic/connections/can_tmcl/slcan_tmcl_interface.py new file mode 100644 index 00000000..ff3096eb --- /dev/null +++ b/pytrinamic/connections/can_tmcl/slcan_tmcl_interface.py @@ -0,0 +1,45 @@ +import can +from can import CanError +from serial.tools.list_ports import comports +from ...connections.can_tmcl_interface import CanTmclInterface + + +class SlcanTmclInterface(CanTmclInterface): + """ + This class implements a TMCL connection for CAN over Serial / SLCAN. + Compatible with CANable running slcan firmware and similar. + Set underlying serial device as channel. (e.g. /dev/ttyUSB0, COM8, …) + Maybe SerialBaudrate has to be changed based on adapter. + """ + + def __init__(self, com_port, datarate=1000000, host_id=2, module_id=1, timeout_s=5, serial_baudrate=115200): + if not isinstance(com_port, str): + raise TypeError + + CanTmclInterface.__init__(self, com_port, datarate, host_id, module_id, timeout_s) + self._serial_baudrate = serial_baudrate + + self.logger.info("Connect to bus. (Baudrate=%s)", self._serial_baudrate) + try: + self._connection = can.Bus(interface='slcan', + channel=self._channel, + bitrate=self._bitrate, + ttyBaudrate=self._serial_baudrate) + self._connection.set_filters([{"can_id": host_id, "can_mask": 0x7F}]) + except CanError as e: + self._connection = None + raise ConnectionError("Failed to connect to CAN bus") from e + + @classmethod + def list(cls): + """ + Return a list of available connection ports as a list of strings. + + This function is required for using this interface with the + connection manager. + """ + connected = [] + for element in sorted(comports()): + connected.append(element.device) + + return connected diff --git a/pytrinamic/connections/can_tmcl/socketcan_tmcl_interface.py b/pytrinamic/connections/can_tmcl/socketcan_tmcl_interface.py new file mode 100644 index 00000000..3886177f --- /dev/null +++ b/pytrinamic/connections/can_tmcl/socketcan_tmcl_interface.py @@ -0,0 +1,40 @@ +import can +from can import CanError +from ...connections.can_tmcl_interface import CanTmclInterface + + +class SocketcanTmclInterface(CanTmclInterface): + """ + This class implements a TMCL connection over a SocketCAN adapter. + + Use following command under linux to activate can socket + sudo ip link set can0 down type can bitrate 1000000 + """ + _CHANNELS = ["can0", "can1", "can2", "can3", "can4", "can5", "can6", "can7"] + + def __init__(self, port, datarate=1000000, host_id=2, module_id=1, timeout_s=5): + if not isinstance(port, str): + raise TypeError + + if port not in self._CHANNELS: + raise ValueError("Invalid port") + + CanTmclInterface.__init__(self, port, datarate, host_id, module_id, timeout_s) + + self.logger.info("Connect to bus with bit-rate %s.", self._bitrate) + try: + self._connection = can.Bus(interface="socketcan", channel=self._channel, bitrate=self._bitrate) + self._connection.set_filters([{"can_id": host_id, "can_mask": 0x7F}]) + except CanError as e: + self._connection = None + raise ConnectionError("Failed to connect to SocketCAN bus") from e + + @classmethod + def list(cls): + """ + Return a list of available connection ports as a list of strings. + + This function is required for using this interface with the + connection manager. + """ + return cls._CHANNELS diff --git a/pytrinamic/connections/can_tmcl_interface.py b/pytrinamic/connections/can_tmcl_interface.py new file mode 100644 index 00000000..d8813505 --- /dev/null +++ b/pytrinamic/connections/can_tmcl_interface.py @@ -0,0 +1,85 @@ + +import logging +import can +from ..connections.tmcl_interface import TmclInterface + + +class CanTmclInterface(TmclInterface): + """Generic CAN interface class for the CAN adapters.""" + + def __init__(self, channel, datarate, host_id, default_module_id, timeout_s): + + TmclInterface.__init__(self, host_id, default_module_id) + self._connection = None + self._channel = channel + self._bitrate = datarate + if timeout_s == 0: + self._timeout_s = None + else: + self._timeout_s = timeout_s + + self.logger = logging.getLogger(f"{self.__class__.__name__}.{self._channel}") + + def __enter__(self): + return self + + def __exit__(self, exit_type, value, traceback): + """ + Close the connection at the end of a with-statement block. + """ + del exit_type, value, traceback + self.close() + + def close(self): + self.logger.info("Shutdown.") + + self._connection.shutdown() + + def _send(self, host_id, module_id, data): + """ + Send the bytearray parameter [data]. + + This is a required override function for using the tmcl_interface class. + """ + del host_id + + msg = can.Message(arbitration_id=module_id, is_extended_id=False, data=data[1:]) + + try: + self._connection.send(msg) + except can.CanError as e: + raise ConnectionError( + f"Failed to send a TMCL message on {self.__class__.__name__} (channel {str(self._channel)})" + ) from e + + def _recv(self, host_id, module_id): + """ + Read 9 bytes and return them as a bytearray. + + This is a required override function for using the tmcl_interface class. + """ + del module_id + + try: + msg = self._connection.recv(timeout=self._timeout_s) + except can.CanError as e: + raise ConnectionError( + f"Failed to receive a TMCL message from {self.__class__.__name__} (channel {str(self._channel)})" + ) from e + + if not msg: + raise ConnectionError(f"Recv timed out ({self.__class__.__name__}, on channel {str(self._channel)})") + + if msg.arbitration_id != host_id: + # The filter shouldn't let wrong messages through. + # This is just a sanity check + self.logger.warning("Received a CAN Frame with unexpected ID (received: %d; expected: %d)", msg.arbitration_id, host_id) + + return bytearray([msg.arbitration_id]) + msg.data + + @staticmethod + def supports_tmcl(): + return True + + def __str__(self): + return f"Connection: Type = {self.__class__.__name__}, Channel = {self._channel}, Bitrate = {self._bitrate}" diff --git a/pytrinamic/connections/connection_manager.py b/pytrinamic/connections/connection_manager.py index d85ea916..f149a31e 100644 --- a/pytrinamic/connections/connection_manager.py +++ b/pytrinamic/connections/connection_manager.py @@ -1,4 +1,5 @@ import sys +import logging import argparse from ..connections import DummyTmclInterface @@ -11,6 +12,8 @@ from ..connections import SlcanTmclInterface from ..connections import IxxatTmclInterface +logger = logging.getLogger(__name__) + class ConnectionManager: """ @@ -63,7 +66,17 @@ class which allows repeated connect() and disconnect() calls. interpreted depends on the interface used. E.g. the serial connection uses this value as the baud rate. - Default value: 115200 + The Default value also depends on the interface. + * for any CAN interface its 1000000 + * for the serial_tmcl and uard_id interface it is 9600 + * for usb_tmcl it is 115200 + + --timeout + The rx timeout in seconds. Accepts only values >= 0. + If 0 is given the rx function will block forever. + This might be useful for debugging. + + Default value: 5.0 --host-id The host id to use with a TMCL connection. @@ -90,24 +103,23 @@ class which allows repeated connect() and disconnect() calls. ("ixxat_tmcl", IxxatTmclInterface, 1000000), ] - def __init__(self, arg_list=None, connection_type="any", debug=False): + def __init__(self, arg_list=None, connection_type="any"): # Attributes - self.__debug = debug self.__connection = None arg_parser = argparse.ArgumentParser(description='ConnectionManager to setup connections dynamically and interactively') ConnectionManager.argparse(arg_parser) if not arg_list: - if self.__debug: - print("Using arguments from the command line") + logger.info("Using arguments from the command line.") arg_list = sys.argv if isinstance(arg_list, str): arg_list = arg_list.split() - if self.__debug: - print("Splitting string:", arg_list) + logger.debug("List of input arguments: %s", arg_list) + + # Parse the command line args = arg_parser.parse_known_args(arg_list)[0] # Argument storage - default parameters are set here @@ -118,11 +130,7 @@ def __init__(self, arg_list=None, connection_type="any", debug=False): self.__host_id = 2 self.__module_id = 1 - # Parse the command line - if self.__debug: - print("Commandline argument list: {0:s}".format(str(arg_list))) - print("Parsed commandline arguments: {0:s}".format(str(args))) - print() + logger.debug("Combined default and parsed arguments: %s", args) # ## Interpret given arguments # Interface @@ -156,6 +164,9 @@ def __init__(self, arg_list=None, connection_type="any", debug=False): # No data rate has been set -> keep old value pass + # Timeout + self.__timeout_s = args.timeout_s + # Host ID try: self.__host_id = int(args.host_id[0]) @@ -168,17 +179,18 @@ def __init__(self, arg_list=None, connection_type="any", debug=False): except ValueError as exc: raise ValueError("Invalid module id: " + args.module_id[0]) from exc - if self.__debug: - print("Connection parameters:") - print("\tInterface: " + self.__interface.__qualname__) - print("\tPort: " + self.__port) - print("\tBlacklist: " + str(self.__no_port)) - print("\tData rate: " + str(self.__data_rate)) - print("\tHost ID: " + str(self.__host_id)) - print("\tModule ID: " + str(self.__module_id)) - print() - - def connect(self, debug_interface=None): + logger.info("ConnectionManager created with [" + "Interface: %s; " + "Port: %s; " + "Blacklist: %s; " + "Data rate: %s; " + "Timeout: %s;" + "Host ID: %s; " + "Module ID: %s]", + self.__interface.__qualname__, self.__port, self.__no_port, self.__data_rate, self.__timeout_s, + self.__host_id, self.__module_id) + + def connect(self): """ Attempt to connect to a module with the stored connection parameters. @@ -187,19 +199,7 @@ def connect(self, debug_interface=None): If no connections are available or a connection attempt fails, a ConnectionError exception is raised - - Parameters: - debug_interface: - Type: bool, optional, default value: None - Control whether the connection should be created in - debug mode. A boolean value will enable or disable the debug mode, - a None value will set the connections debug mode according to the - ConnectionManagers debug mode. """ - # If no debug selection has been passed, inherit the debug state from the connection manager - if debug_interface is None: - debug_interface = self.__debug - # Get all available ports port_list = self.list_connections() @@ -243,10 +243,10 @@ def connect(self, debug_interface=None): if self.__interface.supports_tmcl(): # Open the connection to a TMCL interface self.__connection = self.__interface(port, self.__data_rate, self.__host_id, self.__module_id, - debug=debug_interface) + timeout_s=self.__timeout_s) else: # Open the connection to a direct IC interface - self.__connection = self.__interface(port, self.__data_rate, debug=debug_interface) + self.__connection = self.__interface(port, self.__data_rate, timeout_s=self.__timeout_s) except ConnectionError as e: raise ConnectionError("Couldn't connect to port " + port + ". Connection failed.") from e @@ -303,6 +303,16 @@ def argparse(arg_parser): script, this function adds the arguments of the ConnectionManager to the argparse parser. """ + def _positive_float(value): + """ + Argparse checker for float a positive float type. + """ + value_float = float(value) + if value_float < 0: + raise argparse.ArgumentTypeError("Expected a positive float, got {}".format(value_float)) + + return value_float + group = arg_parser.add_argument_group("ConnectionManager options") group.add_argument('--interface', dest='interface', action='store', nargs=1, type=str, choices=[actual_interface[0] for actual_interface in ConnectionManager.INTERFACES], @@ -313,6 +323,8 @@ def argparse(arg_parser): help='Exclude ports') group.add_argument('--data-rate', dest='data_rate', action='store', nargs=1, type=int, help='Connection data-rate (default: %(default)s)') + group.add_argument('--timeout', dest='timeout_s', action='store', type=_positive_float, default=5.0, + help='Connection rx timeout in seconds (default: %(default)s)', metavar="SECONDS") group = arg_parser.add_argument_group("ConnectionManager TMCL options") diff --git a/pytrinamic/connections/dummy_tmcl_interface.py b/pytrinamic/connections/dummy_tmcl_interface.py index 537f82c7..f9dfd88e 100644 --- a/pytrinamic/connections/dummy_tmcl_interface.py +++ b/pytrinamic/connections/dummy_tmcl_interface.py @@ -1,22 +1,22 @@ +import logging + from ..connections.tmcl_interface import TmclInterface class DummyTmclInterface(TmclInterface): - def __init__(self, port, datarate=115200, host_id=2, module_id=1, debug=True): + def __init__(self, port, datarate=115200, host_id=2, module_id=1, timeout_s=5): """ Opens a dummy TMCL connection """ if not isinstance(port, str): raise TypeError - TmclInterface.__init__(self, host_id, module_id, debug) + TmclInterface.__init__(self, host_id, module_id) + + self.logger = logging.getLogger("{}.{}".format(self.__class__.__name__, port)) - if self._debug: - print("Opened dummy TMCL interface on port '" + port + "'") - print("\tData rate: " + str(datarate)) - print("\tHost ID: " + str(host_id)) - print("\tModule ID: " + str(module_id)) + self.logger.debug("Opening port (baudrate=%s).", datarate) def __enter__(self): return self @@ -32,8 +32,7 @@ def close(self): """ Closes the dummy TMCL connection """ - if self._debug: - print("Closed dummy TMCL interface") + def _send(self, host_id, module_id, data): """ diff --git a/pytrinamic/connections/ixxat_tmcl_interface.py b/pytrinamic/connections/ixxat_tmcl_interface.py deleted file mode 100644 index b0513eab..00000000 --- a/pytrinamic/connections/ixxat_tmcl_interface.py +++ /dev/null @@ -1,149 +0,0 @@ -import can -from can import CanError -from ..connections.tmcl_interface import TmclInterface - -# Providing 5 channels here, this should cover all use cases. -# Ixxat USB-to-CAN provides 1, 2 or 4 channels. -_CHANNELS = ["0", "1", "2", "3", "4"] - - -class IxxatTmclInterface(TmclInterface): - """ - This class implements a TMCL connection for IXXAT USB-to-CAN adapter. - Backend is provided by the IXXAT Virtual CAN Interface V3 SDK. - - Port number is assigned as adapters are plugged in, arbitrarely, - it is possible to use multiple channels of one IXXAT, CAN1 is port "0", CAN2 is port "1", etc. - - This class, and the parser implementation DOES NOT support multiple IXXATs connected to one computer. - - To add this functionality, python-can version 4.0.0 is necessary as it allows enumerating IXXAT devices IDs, - (see https://github.com/hardbyte/python-can/pull/926). - To use multiple IXXAT devices, you must provide hardware IDs (this needs to be added to this class and to the parser). - - Snippet to list IXXAT by hardware ID using python-can 4.0.0: - from can.interfaces.ixxat import IXXATBus - for hwid in IXXATBus.list_adapters(): - print("Found IXXAT adapter with hardware id '%s'." % hwid) - """ - - def __init__(self, port="0", datarate=1000000, host_id=2, module_id=1, debug=False): - if not isinstance(port, str): - raise TypeError - - if port not in _CHANNELS: - raise ValueError("Invalid port!") - - TmclInterface.__init__(self, host_id, module_id, debug) - self._channel = port - self._bitrate = datarate - - try: - if self._debug: - self._connection = can.Bus(interface="ixxat", channel=self._channel, bitrate=self._bitrate) - else: - self._connection = can.Bus( - interface="ixxat", - channel=self._channel, - bitrate=self._bitrate, - can_filters=[{"can_id": host_id, "can_mask": 0x7F}], - ) - except CanError as e: - self._connection = None - raise ConnectionError( - f"Failed to connect to {self.__class__.__name__} on channel {str(self._channel)}" - ) from e - - if self._debug: - print(f"Opened {self.__class__.__name__} on channel {str(self._channel)}") - - def __enter__(self): - return self - - def __exit__(self, exit_type, value, traceback): - """ - Close the connection at the end of a with-statement block. - """ - del exit_type, value, traceback - self.close() - - def close(self): - if self._debug: - print(f"Closing {self.__class__.__name__} (channel {str(self._channel)})") - - self._connection.shutdown() - - def _send(self, host_id, module_id, data): - """ - Send the bytearray parameter [data]. - - This is a required override function for using the tmcl_interface class. - """ - del host_id - - msg = can.Message(arbitration_id=module_id, is_extended_id=False, data=data[1:]) - - try: - self._connection.send(msg) - except CanError as e: - raise ConnectionError( - f"Failed to send a TMCL message on {self.__class__.__name__} (channel {str(self._channel)})" - ) from e - - def _recv(self, host_id, module_id): - """ - Read 9 bytes and return them as a bytearray. - - This is a required override function for using the tmcl_interface - class. - """ - del module_id - - try: - msg = self._connection.recv(timeout=3) - except CanError as e: - raise ConnectionError( - f"Failed to receive a TMCL message from {self.__class__.__name__} (channel {str(self._channel)})" - ) from e - - if not msg: - # Todo: Timeout retry mechanism - raise ConnectionError(f"Recv timed out ({self.__class__.__name__}, on channel {str(self._channel)})") - - if msg.arbitration_id != host_id: - # The filter shouldn't let wrong messages through. - # This is just a sanity check - if self._debug: - print(f"Received wrong ID ({self.__class__.__name__}, on channel {str(self._channel)})") - - return bytearray([msg.arbitration_id]) + msg.data - - @staticmethod - def supports_tmcl(): - return True - - @staticmethod - def list(): - """ - Return a list of available connection ports as a list of strings. - - This function is required for using this interface with the - connection manager. - """ - return _CHANNELS - - def __str__(self): - return f"Connection: Type = {self.__class__.__name__}, Channel = {self._channel}, Bitrate = {self._bitrate}" - - -""" -# Snippet to test connection between one (or two) ports of a single IXXAT USB-to-CAN and TMCL device(s). -if __name__ == "__main__": - CAN1 = IxxatTmclInterface(port="0") - # CAN2 = IxxatTmclInterface(port="1") - print(CAN1) - # print(CAN2) - - print(CAN1.get_version_string()) - # print(CAN2.get_version_string()) -""" diff --git a/pytrinamic/connections/kvaser_tmcl_interface.py b/pytrinamic/connections/kvaser_tmcl_interface.py deleted file mode 100644 index 1df9e7fe..00000000 --- a/pytrinamic/connections/kvaser_tmcl_interface.py +++ /dev/null @@ -1,109 +0,0 @@ -import can -from can import CanError -from ..connections.tmcl_interface import TmclInterface - -_CHANNELS = ["0", "1", "2"] - - -class KvaserTmclInterface(TmclInterface): - """ - This class implements a TMCL connection for Kvaser adapter using CANLIB. - Try 0 as default channel. - """ - def __init__(self, port="0", datarate=1000000, host_id=2, module_id=1, debug=False): - if not isinstance(port, str): - raise TypeError - - if port not in _CHANNELS: - raise ValueError("Invalid port!") - - TmclInterface.__init__(self, host_id, module_id, debug) - self._channel = port - self._bitrate = datarate - - try: - if self._debug: - self._connection = can.Bus(interface="kvaser", channel=self._channel, bitrate=self._bitrate) - else: - self._connection = can.Bus(interface="kvaser", channel=self._channel, bitrate=self._bitrate, - can_filters=[{"can_id": host_id, "can_mask": 0x7F}]) - except CanError as e: - self._connection = None - raise ConnectionError("Failed to connect to Kvaser CAN bus") from e - - if self._debug: - print("Opened bus on channel " + str(self._channel)) - - def __enter__(self): - return self - - def __exit__(self, exit_type, value, traceback): - """ - Close the connection at the end of a with-statement block. - """ - del exit_type, value, traceback - self.close() - - def close(self): - if self._debug: - print("Closing CAN bus") - - self._connection.shutdown() - - def _send(self, host_id, module_id, data): - """ - Send the bytearray parameter [data]. - - This is a required override function for using the tmcl_interface class. - """ - del host_id - - msg = can.Message(arbitration_id=module_id, is_extended_id=False, data=data[1:]) - - try: - self._connection.send(msg) - except CanError as e: - raise ConnectionError("Failed to send a TMCL message") from e - - def _recv(self, host_id, module_id): - """ - Read 9 bytes and return them as a bytearray. - - This is a required override function for using the tmcl_interface - class. - """ - del module_id - - try: - msg = self._connection.recv(timeout=3) - except CanError as e: - raise ConnectionError("Failed to receive a TMCL message") from e - - if not msg: - # Todo: Timeout retry mechanism - raise ConnectionError("Recv timed out") - - if msg.arbitration_id != host_id: - # The filter shouldn't let wrong messages through. - # This is just a sanity check - if self._debug: - print("Received wrong ID") - - return bytearray([msg.arbitration_id]) + msg.data - - @staticmethod - def supports_tmcl(): - return True - - @staticmethod - def list(): - """ - Return a list of available connection ports as a list of strings. - - This function is required for using this interface with the - connection manager. - """ - return _CHANNELS - - def __str__(self): - return "Connection: type={} channel={} bitrate={}".format(type(self).__name__, self._channel, self._bitrate) diff --git a/pytrinamic/connections/pcan_tmcl_interface.py b/pytrinamic/connections/pcan_tmcl_interface.py deleted file mode 100644 index 5611b007..00000000 --- a/pytrinamic/connections/pcan_tmcl_interface.py +++ /dev/null @@ -1,128 +0,0 @@ - -import can -from can import CanError -from can.interfaces.pcan.pcan import PcanError -from ..connections.tmcl_interface import TmclInterface - -_CHANNELS = [ - "PCAN_USBBUS1", "PCAN_USBBUS2", "PCAN_USBBUS3", "PCAN_USBBUS4", - "PCAN_USBBUS5", "PCAN_USBBUS6", "PCAN_USBBUS7", "PCAN_USBBUS8", - "PCAN_USBBUS9", "PCAN_USBBUS10", "PCAN_USBBUS11", "PCAN_USBBUS12", - "PCAN_USBBUS13", "PCAN_USBBUS14", "PCAN_USBBUS15", "PCAN_USBBUS16", - - "PCAN_ISABUS1", "PCAN_ISABUS2", "PCAN_ISABUS3", "PCAN_ISABUS4", - "PCAN_ISABUS5", "PCAN_ISABUS6", "PCAN_ISABUS7", "PCAN_ISABUS8", - - "PCAN_DNGBUS1", - - "PCAN_PCIBUS1", "PCAN_PCIBUS2", "PCAN_PCIBUS3", "PCAN_PCIBUS4", - "PCAN_PCIBUS5", "PCAN_PCIBUS6", "PCAN_PCIBUS7", "PCAN_PCIBUS8", - "PCAN_PCIBUS9", "PCAN_PCIBUS10", "PCAN_PCIBUS11", "PCAN_PCIBUS12", - "PCAN_PCIBUS13", "PCAN_PCIBUS14", "PCAN_PCIBUS15", "PCAN_PCIBUS16", - - "PCAN_PCCBUS1", "PCAN_PCCBUS2", - - "PCAN_LANBUS1", "PCAN_LANBUS2", "PCAN_LANBUS3", "PCAN_LANBUS4", - "PCAN_LANBUS5", "PCAN_LANBUS6", "PCAN_LANBUS7", "PCAN_LANBUS8", - "PCAN_LANBUS9", "PCAN_LANBUS10", "PCAN_LANBUS11", "PCAN_LANBUS12", - "PCAN_LANBUS13", "PCAN_LANBUS14", "PCAN_LANBUS15", "PCAN_LANBUS16" - ] - - -class PcanTmclInterface(TmclInterface): - """ - This class implements a TMCL connection over a PCAN adapter. - """ - def __init__(self, port, datarate=1000000, host_id=2, module_id=1, debug=False): - if not isinstance(port, str): - raise TypeError - - if port not in _CHANNELS: - raise ValueError("Invalid port!") - - TmclInterface.__init__(self, host_id, module_id, debug) - self._channel = port - self._bitrate = datarate - - try: - self._connection = can.Bus(interface="pcan", channel=self._channel, bitrate=self._bitrate) - self._connection.set_filters([{"can_id": host_id, "can_mask": 0xFFFFFFFF}]) - except PcanError as e: - self._connection = None - raise ConnectionError("Failed to connect to PCAN bus") from e - - if self._debug: - print("Opened bus on channel " + self._channel) - - def __enter__(self): - return self - - def __exit__(self, exit_type, value, traceback): - """ - Close the connection at the end of a with-statement block. - """ - del exit_type, value, traceback - self.close() - - def close(self): - if self._debug: - print("Closing PCAN bus") - - self._connection.shutdown() - - def _send(self, host_id, module_id, data): - """ - Send the bytearray parameter [data]. - - This is a required override function for using the tmcl_interface class. - """ - del host_id - - msg = can.Message(arbitration_id=module_id, is_extended_id=False, data=data[1:]) - - try: - self._connection.send(msg) - except CanError as e: - raise ConnectionError("Failed to send a TMCL message") from e - - def _recv(self, host_id, module_id): - """ - Read 9 bytes and return them as a bytearray. - - This is a required override function for using the tmcl_interface - class. - """ - del module_id - - try: - msg = self._connection.recv(timeout=3) - except CanError as e: - raise ConnectionError("Failed to receive a TMCL message") from e - - if not msg: - # Todo: Timeout retry mechanism - raise ConnectionError("Recv timed out") - - if msg.arbitration_id != host_id: - # The filter shouldn't let wrong messages through. - # This is just a sanity check - raise ConnectionError("Received wrong ID") - - return bytearray([msg.arbitration_id]) + msg.data - - @staticmethod - def supports_tmcl(): - return True - - @staticmethod - def list(): - """ - Return a list of available connection ports as a list of strings. - - This function is required for using this interface with the - connection manager. - """ - return _CHANNELS - - def __str__(self): - return "Connection: type={} channel={} bitrate={}".format(type(self).__name__, self._channel, self._bitrate) diff --git a/pytrinamic/connections/serial_tmcl_interface.py b/pytrinamic/connections/serial_tmcl_interface.py index 2a83848d..ab612cc7 100644 --- a/pytrinamic/connections/serial_tmcl_interface.py +++ b/pytrinamic/connections/serial_tmcl_interface.py @@ -1,3 +1,5 @@ +import logging + from serial import Serial, SerialException import serial.tools.list_ports from ..connections.tmcl_interface import TmclInterface @@ -8,23 +10,23 @@ class SerialTmclInterface(TmclInterface): """ Opens a serial TMCL connection """ - def __init__(self, com_port, datarate=115200, host_id=2, module_id=1, debug=False): + def __init__(self, com_port, datarate=115200, host_id=2, module_id=1, timeout_s=5): if not isinstance(com_port, str): raise TypeError - TmclInterface.__init__(self, host_id, module_id, debug) + TmclInterface.__init__(self, host_id, module_id) self._baudrate = datarate + if timeout_s == 0: + timeout_s = None + + self.logger = logging.getLogger("{}.{}".format(self.__class__.__name__, com_port)) + self.logger.debug("Opening port (baudrate=%s).", datarate) try: - self._serial = Serial(com_port, self._baudrate) + self._serial = Serial(com_port, self._baudrate, timeout=timeout_s) except SerialException as e: raise ConnectionError from e - self._serial.timeout = 5 - - if self._debug: - print("Opened port: " + self._serial.portstr) - def __enter__(self): return self @@ -36,9 +38,7 @@ def __exit__(self, exit_type, value, traceback): self.close() def close(self): - if self._debug: - print("Closing port: " + self._serial.portstr) - + self.logger.info("Closing port.") self._serial.close() def _send(self, host_id, module_id, data): diff --git a/pytrinamic/connections/slcan_tmcl_interface.py b/pytrinamic/connections/slcan_tmcl_interface.py deleted file mode 100644 index 35e0e92a..00000000 --- a/pytrinamic/connections/slcan_tmcl_interface.py +++ /dev/null @@ -1,111 +0,0 @@ -import can -from can import CanError -from serial.tools.list_ports import comports -from ..connections.tmcl_interface import TmclInterface - - -class SlcanTmclInterface(TmclInterface): - """ - This class implements a TMCL connection for CAN over Serial / SLCAN. - Compatible with CANable running slcan firmware and similar. - Set underlying serial device as channel. (e.g. /dev/ttyUSB0, COM8, …) - Maybe SerialBaudrate has to be changed based on adapter. - """ - - def __init__(self, com_port, datarate=1000000, host_id=2, module_id=1, debug=True, serial_baudrate=115200): - if not isinstance(com_port, str): - raise TypeError - - TmclInterface.__init__(self, host_id, module_id, debug) - self._bitrate = datarate - self._port = com_port - self._serial_baudrate = serial_baudrate - - try: - self._connection = can.Bus(interface='slcan', channel=self._port, bitrate=self._bitrate, - ttyBaudrate=self._serialBaudrate) - self._connection.set_filters([{"can_id": host_id, "can_mask": 0x7F}]) - except CanError as e: - self.__connection = None - raise ConnectionError("Failed to connect to CAN bus") from e - - if self._debug: - print("Opened slcan bus on channel " + self._port) - - def __enter__(self): - return self - - def __exit__(self, exit_type, value, traceback): - """ - Close the connection at the end of a with-statement block. - """ - del exit_type, value, traceback - self.close() - - def close(self): - if self._debug: - print("Closing CAN bus") - - self._connection.shutdown() - - def _send(self, host_id, module_id, data): - """ - Send the bytearray parameter [data]. - - This is a required override function for using the tmcl_interface - class. - """ - del host_id - - msg = can.Message(arbitration_id=module_id, is_extended_id=False, data=data[1:]) - - try: - self.__connection.send(msg) - except CanError as e: - raise ConnectionError("Failed to send a TMCL message") from e - - def _recv(self, host_id, module_id): - """ - Read 9 bytes and return them as a bytearray. - - This is a required override function for using the tmcl_interface - class. - """ - del module_id - - try: - msg = self._connection.recv(timeout=3) - except CanError as e: - raise ConnectionError("Failed to receive a TMCL message") from e - - if not msg: - # Todo: Timeout retry mechanism - raise ConnectionError("Recv timed out") - - if msg.arbitration_id != host_id: - # The filter shouldn't let wrong messages through. - # This is just a sanity check - raise ConnectionError("Received wrong ID") - - return bytearray([msg.arbitration_id]) + msg.data - - @staticmethod - def supports_tmcl(): - return True - - @staticmethod - def list(): - """ - Return a list of available connection ports as a list of strings. - - This function is required for using this interface with the - connection manager. - """ - connected = [] - for element in sorted(comports()): - connected.append(element.device) - - return connected - - def __str__(self): - return "Connection: type={} channel={} bitrate={}".format(type(self).__name__, self._port, self._bitrate) diff --git a/pytrinamic/connections/socketcan_tmcl_interface.py b/pytrinamic/connections/socketcan_tmcl_interface.py deleted file mode 100644 index d2a5b89e..00000000 --- a/pytrinamic/connections/socketcan_tmcl_interface.py +++ /dev/null @@ -1,109 +0,0 @@ -import can -from can import CanError -from ..connections.tmcl_interface import TmclInterface - -_CHANNELS = ["can0", "can1", "can2", "can3", "can4", "can5", "can6", "can7"] - - -class SocketcanTmclInterface(TmclInterface): - """ - This class implements a TMCL connection over a SocketCAN adapter. - - Use following command under linux to activate can socket - sudo ip link set can0 down type can bitrate 1000000 - """ - def __init__(self, port, datarate=1000000, host_id=2, module_id=1, debug=False): - if not isinstance(port, str): - raise TypeError - - if port not in _CHANNELS: - raise ValueError("Invalid port") - - TmclInterface.__init__(self, host_id, module_id, debug) - self._channel = port - self._bitrate = datarate - - try: - self._connection = can.Bus(interface="socketcan", channel=self._channel, bitrate=self._bitrate) - self._connection.set_filters([{"can_id": host_id, "can_mask": 0x7F}]) - - except CanError as e: - self._connection = None - raise ConnectionError("Failed to connect to SocketCAN bus") from e - - if self._debug: - print("Opened bus on channel " + self._channel) - - def __enter__(self): - return self - - def __exit__(self, exit_type, value, traceback): - """ - Close the connection at the end of a with-statement block. - """ - del exit_type, value, traceback - self.close() - - def close(self): - if self._debug: - print("Closing socketcan bus") - - self._connection.shutdown() - - def _send(self, host_id, module_id, data): - """ - Send the bytearray parameter [data]. - - This is a required override function for using the tmcl_interface - class. - """ - del host_id - - msg = can.Message(arbitration_id=module_id, is_extended_id=False, data=data[1:]) - - try: - self._connection.send(msg) - except CanError as e: - raise ConnectionError("Failed to send a TMCL message") from e - - def _recv(self, host_id, module_id): - """ - Read 9 bytes and return them as a bytearray. - - This is a required override function for using the tmcl_interface - class. - """ - del module_id - - try: - msg = self._connection.recv(timeout=3) - except CanError as e: - raise ConnectionError("Failed to receive a TMCL message") from e - - if not msg: - # Todo: Timeout retry mechanism - raise ConnectionError("Recv timed out") - - if msg.arbitration_id != host_id: - # The filter shouldn't let wrong messages through. - # This is just a sanity check - raise ConnectionError("Received wrong ID") - - return bytearray([msg.arbitration_id]) + msg.data - - @staticmethod - def supports_tmcl(): - return True - - @staticmethod - def list(): - """ - Return a list of available connection ports as a list of strings. - - This function is required for using this interface with the - connection manager. - """ - return _CHANNELS - - def __str__(self): - return "Connection: type={} channel={} bitrate={}".format(type(self).__name__, self._channel, self._bitrate) diff --git a/pytrinamic/connections/tmcl_interface.py b/pytrinamic/connections/tmcl_interface.py index dae5c686..5f1b6543 100644 --- a/pytrinamic/connections/tmcl_interface.py +++ b/pytrinamic/connections/tmcl_interface.py @@ -1,3 +1,4 @@ +import logging from abc import ABC from ..tmcl import TMCL, TMCLRequest, TMCLCommand, TMCLReply, TMCLReplyChecksumError, TMCLReplyStatusError from ..helpers import TMC_helpers @@ -19,13 +20,10 @@ class TmclInterface(ABC): _send(self, host_id, module_id, data) _recv(self, host_id, module_id) - A subclass may use the boolean _debug attribute to toggle printing further - debug output. - A subclass may read the _host_id and _module_id parameters. """ - def __init__(self, host_id=2, default_module_id=1, debug=False): + def __init__(self, host_id=2, default_module_id=1): """ Parameters: host_id: @@ -38,40 +36,14 @@ def __init__(self, host_id=2, default_module_id=1, debug=False): tmcl_interface functions. When only communicating with one module a script can omit the moduleID for all TMCL interface calls by declaring this default value once at the start. - debug: - Type: bool, optional, default: False - A switch for enabling debug mode. Can be changed with - enableDebug(). In debug mode all sent and received TMCL packets - get dumped to stdout. The boolean _debug attribute holds the - current state of debug mode - subclasses may read it to print - further debug output. """ + self.logger = logging.getLogger("TmclInterfaceAbstractBaseClassObject") # Will be overwritten in derived classes TMCL.validate_host_id(host_id) TMCL.validate_module_id(default_module_id) - if not isinstance(debug, bool): - raise TypeError - self._host_id = host_id self._module_id = default_module_id - self._debug = debug - - def enable_debug(self, enable): - """ - Set the debug mode, which dumps all TMCL datagrams written and read. - """ - if not isinstance(enable, bool): - raise TypeError("Expected boolean value") - - self._debug = enable - - def print_info(self): - info = "ConnectionInterface {" - info += "'debug_enabled':" + str(self._debug) + ", " - info = info[:-2] - info += "}" - print(info) def _send(self, host_id, module_id, data): """ @@ -106,14 +78,12 @@ def send_request(self, request): Send a TMCL_Request and read back a TMCL_Reply. This function blocks until the reply has been received. """ - if self._debug: - request.dump() + self.logger.debug("Tx: %s", request) self._send(self._host_id, request.moduleAddress, request.to_buffer()) reply = TMCLReply.from_buffer(self._recv(self._host_id, request.moduleAddress)) - if self._debug: - reply.dump() + self.logger.debug("Rx: %s", reply) self._reply_check(reply) @@ -151,8 +121,7 @@ def send_boot(self, module_id=None): request = TMCLRequest(module_id, TMCLCommand.BOOT, 0x81, 0x92, 0xA3B4C5D6) - if self._debug: - request.dump() + self.logger.debug("Tx: %s", request) # Send the request self._send(self._host_id, module_id, request.to_buffer()) diff --git a/pytrinamic/connections/uart_ic_interface.py b/pytrinamic/connections/uart_ic_interface.py index cd6f6132..48f8ac3d 100644 --- a/pytrinamic/connections/uart_ic_interface.py +++ b/pytrinamic/connections/uart_ic_interface.py @@ -1,3 +1,4 @@ +import logging import struct from serial import Serial import serial.tools.list_ports @@ -14,8 +15,8 @@ def __init__(self, address, value): def to_buffer(self): return struct.pack(REGISTER_PACKAGE_STRUCTURE, self.address, self.value) - def dump(self): - print("RegisterRequest: " + str(self.address) + "," + str(self.value)) + def __str__(self): + return "RegisterRequest: [Addr:{:02x}, Value:{}]".format(self.address, self.value) class RegisterReply: @@ -23,8 +24,8 @@ def __init__(self, reply_struct): self.address = reply_struct[0] self.value = reply_struct[1] - def dump(self): - print("RegisterReply: " + str(self.address) + "," + str(self.value)) + def __str__(self): + return "RegisterReply: [Addr:{:02x}, Value:{}]".format(self.address, self.value) def value(self): return self.value @@ -32,11 +33,15 @@ def value(self): class UartIcInterface: - def __init__(self, com_port, datarate=9600, debug=False): - self._debug = debug + def __init__(self, com_port, datarate=9600, timeout_s=5): self.baudrate = datarate - self.serial = Serial(com_port, self.baudrate) - print("Open port: " + self.serial.portstr) + if timeout_s == 0: + timeout_s = None + + self.logger = logging.getLogger("{}.{}".format(self.__class__.__name__, com_port)) + + self.logger.debug("Opening port (baudrate=%s).", datarate) + self.serial = Serial(com_port, self.baudrate, timeout=timeout_s) def __enter__(self): return self @@ -49,18 +54,13 @@ def __exit__(self, exit_type, value, traceback): self.close() def close(self): - if self._debug: - print("Close port: " + self.serial.portstr) - + self.logger.info("Closing port.") self.serial.close() def send_datagram(self, data, recv_size): self.serial.write(data) return self.serial.read(recv_size) - def enable_debug(self, enable): - self._debug = enable - @staticmethod def supports_tmcl(): return False @@ -69,15 +69,11 @@ def send(self, address, value): # prepare TMCL request request = RegisterRequest(address, value) - if self._debug: - request.dump() - # send request, wait, and handle reply + self.logger.debug("Tx %s", request) self.serial.write(request.to_buffer()) reply = RegisterReply(struct.unpack(REGISTER_PACKAGE_STRUCTURE, self.serial.read(REGISTER_PACKAGE_LENGTH))) - - if self._debug: - reply.dump() + self.logger.debug("Rx %s", reply) return reply diff --git a/pytrinamic/evalboards/MAX22216_eval.py b/pytrinamic/evalboards/MAX22216_eval.py index 17c767a1..9e1bf822 100644 --- a/pytrinamic/evalboards/MAX22216_eval.py +++ b/pytrinamic/evalboards/MAX22216_eval.py @@ -17,23 +17,21 @@ def __init__(self, connection, module_id=1): values have to be configured with the module first. """ TMCLEval.__init__(self, connection, module_id) - self.motors = [self._MotorTypeA(self, 0)] + self.motors = [ + self._MotorTypeA(self, 0), + self._MotorTypeA(self, 1), + self._MotorTypeA(self, 2), + self._MotorTypeA(self, 3) + ] self.ics = [MAX22216(self)] # Use the driver controller functions for register access def write_register(self, register_address, value): - return self._connection.write_mc(register_address, value, self._module_id) + return self._connection.write_drv(register_address, value, self._module_id) def read_register(self, register_address, signed=False): - return self._connection.read_mc(register_address, self._module_id, signed) - - def write_register_field(self, field, value): - return self.write_register(field[0], TMC_helpers.field_set(self.read_register(field[0]), - field[1], field[2], value)) - - def read_register_field(self, field): - return TMC_helpers.field_get(self.read_register(field[0]), field[1], field[2]) + return self._connection.read_drv(register_address, self._module_id, signed) class _MotorTypeA(object): """ diff --git a/pytrinamic/evalboards/TMC2240_eval.py b/pytrinamic/evalboards/TMC2240_eval.py new file mode 100644 index 00000000..c3091265 --- /dev/null +++ b/pytrinamic/evalboards/TMC2240_eval.py @@ -0,0 +1,129 @@ +from pytrinamic.evalboards import TMCLEval +from pytrinamic.ic import TMC2240 +from pytrinamic.features import MotorControlModule +from pytrinamic.helpers import TMC_helpers + + +class TMC2240_eval(TMCLEval): + """ + This class represents a TMC2240 Evaluation board. + + Communication is done over the TMCL commands writeMC and readMC. An + implementation without TMCL may still use this class if these two functions + are provided properly. See __init__ for details on the function + requirements. + """ + def __init__(self, connection, module_id=1): + """ + Parameters: + connection: + Type: class + A class that provides the necessary functions for communicating + with a TMC2240. The required functions are + connection.writeMC(registerAddress, value, moduleID) + connection.readMC(registerAddress, moduleID, signed) + for writing/reading to registers of the TMC2240. + module_id: + Type: int, optional, default value: 1 + The TMCL module ID of the TMC2240. This ID is used as a + parameter for the writeMC and readMC functions. + """ + TMCLEval.__init__(self, connection, module_id) + self.motors = [self._MotorTypeA(self, 0)] + self.ics = [TMC2240()] + + # Use the motion controller functions for register access + + def write_register(self, register_address, value): + return self._connection.read_drv(register_address, value, self._module_id) + + def read_register(self, register_address, signed=False): + return self._connection.read_drv(register_address, self._module_id, signed) + + def write_register_field(self, field, value): + return self.write_register(field[0], TMC_helpers.field_set(self.read_register(field[0]), + field[1], field[2], value)) + + def read_register_field(self, field): + return TMC_helpers.field_get(self.read_register(field[0]), field[1], field[2]) + + # Motion control functions + + def rotate(self, motor, value): + self._connection.rotate(motor, value) + + def stop(self, motor): + self._connection.stop(motor) + + def move_to(self, motor, position, velocity=None): + if velocity and velocity != 0: + # Set maximum positioning velocity + self.motors[motor].set_axis_parameter(self.motors[motor].AP.MaxVelocity, velocity) + self._connection.move_to(motor, position, self._module_id) + + class _MotorTypeA(MotorControlModule): + def __init__(self, eval_board, axis): + MotorControlModule.__init__(self, eval_board, axis, self.AP) + + class AP: + TargetPosition = 0 + ActualPosition = 1 + TargetVelocity = 2 + ActualVelocity = 3 + MaxVelocity = 4 + MaxAcceleration = 5 + MaxCurrent = 6 + StandbyCurrent = 7 + PositionReachedFlag = 8 + THIGH = 26 + HighSpeedChopperMode = 28 + HighSpeedFullstepMode = 29 + MeasuredSpeed = 30 + internal_Rsense = 34 + GlobalCurrentScaler = 35 + MicrostepResolution = 140 + ChopperBlankTime = 162 + ConstantTOffMode = 163 + DisableFastDecayComparator = 164 + ChopperHysteresisEnd = 165 + ChopperHysteresisStart = 166 + TOff = 167 + SEIMIN = 168 + SECDS = 169 + smartEnergyHysteresis = 170 + SECUS = 171 + smartEnergyHysteresisStart = 172 + SG2FilterEnable = 173 + SG2Threshold = 174 + smartEnergyActualCurrent = 180 + smartEnergyStallVelocity = 181 + smartEnergyThresholdSpeed = 182 + SG4FilterEnable = 183 + SGAngleOffset = 184 + ChopperSynchronization = 185 + PWMThresholdSpeed = 186 + PWMGrad = 187 + PWMAmplitude = 188 + PWMFrequency = 191 + PWMAutoscale = 192 + PWMScaleSum = 193 + MSCNT = 194 + MEAS_SD_EN = 195 + DIS_REG_STST = 196 + FreewheelingMode = 204 + LoadValue = 206 + EncoderPosition = 209 + EncoderResolution = 210 + CurrentScalingSelector = 211 + CurrentRange = 212 + ADCTemperature = 213 + ADCIN = 214 + ADCSupply = 215 + ADCOvervoltageLimit = 216 + ADCOvertemperatureWarningLimit = 217 + Temperature = 218 + AIN = 219 + VSupply = 220 + OvervoltageLimit = 221 + OvertemperatureWarningLimit = 222 + nSLEEP = 223 diff --git a/pytrinamic/evalboards/__init__.py b/pytrinamic/evalboards/__init__.py index 386690a7..078e6791 100644 --- a/pytrinamic/evalboards/__init__.py +++ b/pytrinamic/evalboards/__init__.py @@ -7,6 +7,7 @@ from .TMC2209_eval import TMC2209_eval from .TMC2224_eval import TMC2224_eval from .TMC2225_eval import TMC2225_eval +from .TMC2240_eval import TMC2240_eval from .TMC2300_eval import TMC2300_eval from .TMC2590_eval import TMC2590_eval from .TMC2660_eval import TMC2660_eval diff --git a/pytrinamic/features/solenoid_ic.py b/pytrinamic/features/solenoid_ic.py index 904372fe..d4074b6b 100644 --- a/pytrinamic/features/solenoid_ic.py +++ b/pytrinamic/features/solenoid_ic.py @@ -94,7 +94,8 @@ def set_voltage_high(self, u_dc_h): vdr = (self._parent.read_axis_field(self._axis, self._ic.FIELD.VDR_NDUTY) == 1) cdr = (self._parent.read_axis_field(self._axis, self._ic.FIELD.CTRL_MODE) == 1) fsf = self.__map_fsf.get(self._parent.read_axis_field(self._axis, self._ic.FIELD.CTRL_MODE), 1.0) - self._parent.write_axis_field(self._axis, self._ic.FIELD.DC_H, self.__u_dc_value(u_dc_h, self.__u_supply, vdr, cdr, fsf)) + print("U_DC_H {}".format(round(self.__u_dc_value(u_dc_h, self.__u_supply, vdr, cdr, fsf)))) + self._parent.write_axis_field(self._axis, self._ic.FIELD.DC_H, round(self.__u_dc_value(u_dc_h, self.__u_supply, vdr, cdr, fsf))) def get_voltage_high(self): """ @@ -120,7 +121,7 @@ def set_voltage_low(self, u_dc_l): vdr = (self._parent.read_axis_field(self._axis, self._ic.FIELD.VDR_NDUTY) == 1) cdr = (self._parent.read_axis_field(self._axis, self._ic.FIELD.CTRL_MODE) == 1) fsf = self.__map_fsf.get(self._parent.read_axis_field(self._axis, self._ic.FIELD.CTRL_MODE), 1.0) - self._parent.write_axis_field(self._axis, self._ic.FIELD.DC_L, self.__u_dc_value(u_dc_l, self.__u_supply, vdr, cdr, fsf)) + self._parent.write_axis_field(self._axis, self._ic.FIELD.DC_L, round(self.__u_dc_value(u_dc_l, self.__u_supply, vdr, cdr, fsf))) def get_voltage_low(self): """ @@ -146,7 +147,7 @@ def set_voltage_low_high(self, u_dc_l2h): vdr = (self._parent.read_axis_field(self._axis, self._ic.FIELD.VDR_NDUTY) == 1) cdr = (self._parent.read_axis_field(self._axis, self._ic.FIELD.CTRL_MODE) == 1) fsf = self.__map_fsf.get(self._parent.read_axis_field(self._axis, self._ic.FIELD.CTRL_MODE), 1.0) - self._parent.write_axis_field(self._axis, self._ic.FIELD.DC_L2H, self.__u_dc_value(u_dc_l2h, self.__u_supply, vdr, cdr, fsf)) + self._parent.write_axis_field(self._axis, self._ic.FIELD.DC_L2H, round(self.__u_dc_value(u_dc_l2h, self.__u_supply, vdr, cdr, fsf))) def get_voltage_low_high(self): """ @@ -172,7 +173,7 @@ def set_voltage_high_low(self, u_dc_h2l): vdr = (self._parent.read_axis_field(self._axis, self._ic.FIELD.VDR_NDUTY) == 1) cdr = (self._parent.read_axis_field(self._axis, self._ic.FIELD.CTRL_MODE) == 1) fsf = self.__map_fsf.get(self._parent.read_axis_field(self._axis, self._ic.FIELD.CTRL_MODE), 1.0) - self._parent.write_axis_field(self._axis, self._ic.FIELD.DC_H2L, self.__u_dc_value(u_dc_h2l, self.__u_supply, vdr, cdr, fsf)) + self._parent.write_axis_field(self._axis, self._ic.FIELD.DC_H2L, round(self.__u_dc_value(u_dc_h2l, self.__u_supply, vdr, cdr, fsf))) def get_voltage_high_low(self): """ @@ -196,8 +197,8 @@ def set_frequency(self, u_ac_freq): Parameters: u_ac_freq: AC frequency. """ - pwm_freq = __map_pwm_freq.get(self._parent.read_axis_field(self._axis, self._ic.FIELD.F_PWM_M), 100E3) - self._parent.write_axis_field(self._axis, self._ic.FIELD.DELTA_PHI, self.__delta_phi(pwm_freq, u_ac_freq)) + pwm_freq = self.__map_pwm_freq.get(self._parent.read_axis_field(self._axis, self._ic.FIELD.F_PWM_M), 100E3) + self._parent.write_axis_field(self._axis, self._ic.FIELD.DELTA_PHI, round(self.__delta_phi(pwm_freq, u_ac_freq))) def get_frequency(self): """ @@ -208,7 +209,7 @@ def get_frequency(self): Returns: AC frequency. """ - pwm_freq = __map_pwm_freq.get(self._parent.read_axis_field(self._axis, self._ic.FIELD.F_PWM_M), 100E3) + pwm_freq = self.__map_pwm_freq.get(self._parent.read_axis_field(self._axis, self._ic.FIELD.F_PWM_M), 100E3) return self.__f_AC(pwm_freq, self._parent.read_axis_field(self._axis, self._ic.FIELD.DELTA_PHI)) def set_voltage_ac(self, u_ac): @@ -220,7 +221,7 @@ def set_voltage_ac(self, u_ac): u_ac: AC voltage. """ vdr = (self._parent.read_axis_field(self._axis, self._ic.FIELD.VDR_NDUTY) == 1) - self._parent.write_axis_field(self._axis, self._ic.FIELD.U_AC, self.__u_ac_value(u_ac, self.__u_supply, vdr)) + self._parent.write_axis_field(self._axis, self._ic.FIELD.U_AC, round(self.__u_ac_value(u_ac, self.__u_supply, vdr))) def get_voltage_ac(self): """ diff --git a/pytrinamic/ic/TMC2240.py b/pytrinamic/ic/TMC2240.py new file mode 100644 index 00000000..c5e20454 --- /dev/null +++ b/pytrinamic/ic/TMC2240.py @@ -0,0 +1,229 @@ +from ..ic.tmc_ic import TMCIc + + +class TMC2240(TMCIc): + """ + The TMC2240-A is a stepper motor controller and driver IC with serial communication interfaces. + Supply voltage: 4.5-36V. + """ + def __init__(self): + super().__init__("TMC2240", self.__doc__) + + class REG: + """ + Define all registers of the TMC2240. + """ + GCONF = 0x00 + GSTAT = 0x01 + IFCNT = 0x02 + SLAVECONF = 0x03 + IOIN = 0x04 + DRV_CONF = 0x0A + GLOBAL_SCALER = 0x0B + IHOLD_IRUN = 0x10 + TPOWERDOWN = 0x11 + TSTEP = 0x12 + TPWMTHRS = 0x13 + TCOOLTHRS = 0x14 + THIGH = 0x15 + DIRECT_MODE = 0x2D + ENCMODE = 0x38 + X_ENC = 0x39 + ENC_CONST = 0x3A + ENC_STATUS = 0x3B + ENC_LATCH = 0x3C + ADC_VSUPPLY_AIN = 0x50 + ADC_TEMP = 0x51 + OTW_OV_VTH = 0x52 + MSLUT_0 = 0x60 + MSLUT_1 = 0x61 + MSLUT_2 = 0x62 + MSLUT_3 = 0x63 + MSLUT_4 = 0x64 + MSLUT_5 = 0x65 + MSLUT_6 = 0x66 + MSLUT_7 = 0x67 + MSLUTSEL = 0x68 + MSLUTSTART = 0x69 + MSCNT = 0x6A + MSCURACT = 0x6B + CHOPCONF = 0x6C + COOLCONF = 0x6D + DCCTRL = 0x6E + DRV_STATUS = 0x6F + PWMCONF = 0x70 + PWM_SCALE = 0x71 + PWM_AUTO = 0x72 + SG4_THRS = 0x74 + SG4_RESULT = 0x75 + SG4_IND = 0x76 + + class FIELD: + """ + Define all register bitfields of the TMC2240. + + Each field is defined as a tuple consisting of ( Address, Mask, Shift ). + + The name of the register is written as a comment behind each tuple. This is + intended for IDE users viewing the definition of a field by hovering over + it. This allows the user to see the corresponding register name of a field + without opening this file and searching for the definition. + """ + + FAST_STANDSTILL = ( 0x00, 0x00000002, 1 ) + EN_PWM_MODE = ( 0x00, 0x00000004, 2 ) + MULTISTEP_FILT = ( 0x00, 0x00000008, 3 ) + SHAFT = ( 0x00, 0x00000010, 4 ) + DIAG0_ERROR = ( 0x00, 0x00000020, 5 ) + DIAG0_OTPW = ( 0x00, 0x00000040, 6 ) + DIAG0_STALL = ( 0x00, 0x00000080, 7 ) + DIAG1_STALL = ( 0x00, 0x00000100, 8 ) + DIAG1_INDEX = ( 0x00, 0x00000200, 9 ) + DIAG1_ONSTATE = ( 0x00, 0x00000400, 10 ) + DIAG0_PUSHPULL = ( 0x00, 0x00001000, 12 ) + DIAG1_PUSHPULL = ( 0x00, 0x00002000, 13 ) + SMALL_HYSTERESIS = ( 0x00, 0x00004000, 14 ) + STOP_ENABLE = ( 0x00, 0x00008000, 15 ) + DIRECT_MODE = ( 0x00, 0x00010000, 16 ) + RESET = ( 0x01, 0x00000001, 0 ) + DRV_ERR = ( 0x01, 0x00000002, 1 ) + UV_CP = ( 0x01, 0x00000004, 2 ) + REGISTER_RESET = ( 0x01, 0x00000008, 3 ) + VM_UVLO = ( 0x01, 0x00000010, 4 ) + IFCNT = ( 0x02, 0x000000FF, 0 ) + SLAVEADDR = ( 0x03, 0x000000FF, 0 ) + SENDDELAY = ( 0x03, 0x00000F00, 8 ) + REFL_STEP = ( 0x04, 0x00000001, 0 ) + REFR_DIR = ( 0x04, 0x00000002, 1 ) + ENCB_CFG4 = ( 0x04, 0x00000004, 2 ) + ENCA_CFG5 = ( 0x04, 0x00000008, 3 ) + DRV_ENN = ( 0x04, 0x00000010, 4 ) + ENCN_CFG6 = ( 0x04, 0x00000020, 5 ) + UART_EN = ( 0x04, 0x00000040, 6 ) + RESERVED = ( 0x04, 0x00000080, 7 ) + COMP_A = ( 0x04, 0x00000100, 8 ) + COMP_B = ( 0x04, 0x00000200, 9 ) + COMP_A1_A2 = ( 0x04, 0x00000400, 10 ) + COMP_B1_B2 = ( 0x04, 0x00000800, 11 ) + OUTPUT = ( 0x04, 0x00001000, 12 ) + EXT_RES_DET = ( 0x04, 0x00002000, 13 ) + EXT_CLK = ( 0x04, 0x00004000, 14 ) + ADC_ERR = ( 0x04, 0x00008000, 15 ) + SILICON_RV = ( 0x04, 0x00070000, 16 ) + VERSION = ( 0x04, 0xFF000000, 24 ) + CURRENT_RANGE = ( 0x0A, 0x00000003, 0 ) + SLOPE_CONTROL = ( 0x0A, 0x00000030, 4 ) + GLOBALSCALER = ( 0x0B, 0x000000FF, 0 ) + IHOLD = ( 0x10, 0x0000001F, 0 ) + IRUN = ( 0x10, 0x00001F00, 8 ) + IHOLDDELAY = ( 0x10, 0x000F0000, 16 ) + IRUNDELAY = ( 0x10, 0x0F000000, 24 ) + TPOWERDOWN = ( 0x11, 0x000000FF, 0 ) + TSTEP = ( 0x12, 0x000FFFFF, 0 ) + TPWMTHRS = ( 0x13, 0x000FFFFF, 0 ) + TCOOLTHRS = ( 0x14, 0x000FFFFF, 0 ) + THIGH = ( 0x15, 0x000FFFFF, 0 ) + DIRECT_COIL_A = ( 0x2D, 0x000001FF, 0 ) + DIRECT_COIL_B = ( 0x2D, 0x01FF0000, 16 ) + POL_A = ( 0x38, 0x00000001, 0 ) + POL_B = ( 0x38, 0x00000002, 1 ) + POL_N = ( 0x38, 0x00000004, 2 ) + IGNORE_AB = ( 0x38, 0x00000008, 3 ) + CLR_CONT = ( 0x38, 0x00000010, 4 ) + CLR_ONCE = ( 0x38, 0x00000020, 5 ) + POS_NEG_EDGE = ( 0x38, 0x000000C0, 6 ) + CLR_ENC_X = ( 0x38, 0x00000100, 8 ) + LATCH_X_ACT = ( 0x38, 0x00000200, 9 ) + ENC_SEL_DECIMAL = ( 0x38, 0x00000400, 10 ) + X_ENC = ( 0x39, 0xFFFFFFFF, 0 ) + ENC_CONST = ( 0x3A, 0xFFFFFFFF, 0 ) + N_EVENT = ( 0x3B, 0x00000001, 0 ) + DEVIATION_WARN = ( 0x3B, 0x00000002, 1 ) + ENC_LATCH = ( 0x3C, 0xFFFFFFFF, 0 ) + ADC_VSUPPLY = ( 0x50, 0x00001FFF, 0 ) + ADC_AIN = ( 0x50, 0x1FFF0000, 16 ) + ADC_TEMP = ( 0x51, 0x00001FFF, 0 ) + #RESERVED = ( 0x51, 0x1FFF0000, 16 ) + OVERVOLTAGE_VTH = ( 0x52, 0x00001FFF, 0 ) + OVERTEMPPREWARNING_VTH = ( 0x52, 0x1FFF0000, 16 ) + MSLUT__ = ( 0x60, 0xFFFFFFFF, 0 ) + #MSLUT__ = ( 0x61, 0xFFFFFFFF, 0 ) + #MSLUT__ = ( 0x62, 0xFFFFFFFF, 0 ) + #MSLUT__ = ( 0x63, 0xFFFFFFFF, 0 ) + #MSLUT__ = ( 0x64, 0xFFFFFFFF, 0 ) + #MSLUT__ = ( 0x65, 0xFFFFFFFF, 0 ) + #MSLUT__ = ( 0x66, 0xFFFFFFFF, 0 ) + #MSLUT__ = ( 0x67, 0xFFFFFFFF, 0 ) + W0 = ( 0x68, 0x00000003, 0 ) + W1 = ( 0x68, 0x0000000C, 2 ) + W2 = ( 0x68, 0x00000030, 4 ) + W3 = ( 0x68, 0x000000C0, 6 ) + X1 = ( 0x68, 0x0000FF00, 8 ) + X2 = ( 0x68, 0x00FF0000, 16 ) + X3 = ( 0x68, 0xFF000000, 24 ) + START_SIN = ( 0x69, 0x000000FF, 0 ) + START_SIN90 = ( 0x69, 0x00FF0000, 16 ) + OFFSET_SIN90 = ( 0x69, 0xFF000000, 24 ) + MSCNT = ( 0x6A, 0x000003FF, 0 ) + CUR_B = ( 0x6B, 0x000001FF, 0 ) + CUR_A = ( 0x6B, 0x01FF0000, 16 ) + TOFF = ( 0x6C, 0x0000000F, 0 ) + HSTRT_TFD210 = ( 0x6C, 0x00000070, 4 ) + HEND_OFFSET = ( 0x6C, 0x00000780, 7 ) + FD3 = ( 0x6C, 0x00000800, 11 ) + DISFDCC = ( 0x6C, 0x00001000, 12 ) + CHM = ( 0x6C, 0x00004000, 14 ) + TBL = ( 0x6C, 0x00018000, 15 ) + VHIGHFS = ( 0x6C, 0x00040000, 18 ) + VHIGHCHM = ( 0x6C, 0x00080000, 19 ) + TPFD = ( 0x6C, 0x00F00000, 20 ) + MRES = ( 0x6C, 0x0F000000, 24 ) + INTPOL = ( 0x6C, 0x10000000, 28 ) + DEDGE = ( 0x6C, 0x20000000, 29 ) + DISS2G = ( 0x6C, 0x40000000, 30 ) + DISS2VS = ( 0x6C, 0x80000000, 31 ) + SEMIN = ( 0x6D, 0x0000000F, 0 ) + SEUP = ( 0x6D, 0x00000060, 5 ) + SEMAX = ( 0x6D, 0x00000F00, 8 ) + SEDN = ( 0x6D, 0x00006000, 13 ) + SEIMIN = ( 0x6D, 0x00008000, 15 ) + SGT = ( 0x6D, 0x007F0000, 16 ) + SFILT = ( 0x6D, 0x01000000, 24 ) + DC_TIME = ( 0x6E, 0x000003FF, 0 ) + DC_SG = ( 0x6E, 0x00FF0000, 16 ) + SG_RESULT = ( 0x6F, 0x000003FF, 0 ) + S2VSA = ( 0x6F, 0x00001000, 12 ) + S2VSB = ( 0x6F, 0x00002000, 13 ) + STEALTH = ( 0x6F, 0x00004000, 14 ) + FSACTIVE = ( 0x6F, 0x00008000, 15 ) + CS_ACTUAL = ( 0x6F, 0x001F0000, 16 ) + STALLGUARD = ( 0x6F, 0x01000000, 24 ) + OT = ( 0x6F, 0x02000000, 25 ) + OTPW = ( 0x6F, 0x04000000, 26 ) + S2GA = ( 0x6F, 0x08000000, 27 ) + S2GB = ( 0x6F, 0x10000000, 28 ) + OLA = ( 0x6F, 0x20000000, 29 ) + OLB = ( 0x6F, 0x40000000, 30 ) + STST = ( 0x6F, 0x80000000, 31 ) + PWM_OFS = ( 0x70, 0x000000FF, 0 ) + PWM_GRAD = ( 0x70, 0x0000FF00, 8 ) + PWM_FREQ = ( 0x70, 0x00030000, 16 ) + PWM_AUTOSCALE = ( 0x70, 0x00040000, 18 ) + PWM_AUTOGRAD = ( 0x70, 0x00080000, 19 ) + FREEWHEEL = ( 0x70, 0x00300000, 20 ) + PWM_MEAS_SD_ENABLE = ( 0x70, 0x00400000, 22 ) + PWM_DIS_REG_STST = ( 0x70, 0x00800000, 23 ) + PWM_REG = ( 0x70, 0x0F000000, 24 ) + PWM_LIM = ( 0x70, 0xF0000000, 28 ) + PWM_SCALE_SUM = ( 0x71, 0x000003FF, 0 ) + PWM_SCALE_AUTO = ( 0x71, 0x01FF0000, 16 ) + PWM_OFS_AUTO = ( 0x72, 0x000000FF, 0 ) + PWM_GRAD_AUTO = ( 0x72, 0x00FF0000, 16 ) + SG4_THRS = ( 0x74, 0x000000FF, 0 ) + SG4_FILT_EN = ( 0x74, 0x00000100, 8 ) + SG_ANGLE_OFFSET = ( 0x74, 0x00000200, 9 ) + SG4_RESULT = ( 0x75, 0x000003FF, 0 ) + SG4_IND_0 = ( 0x76, 0x000000FF, 0 ) + SG4_IND_1 = ( 0x76, 0x0000FF00, 8 ) + SG4_IND_2 = ( 0x76, 0x00FF0000, 16 ) + SG4_IND_3 = ( 0x76, 0xFF000000, 24 ) \ No newline at end of file diff --git a/pytrinamic/ic/__init__.py b/pytrinamic/ic/__init__.py index d7e8a836..1a84dec6 100644 --- a/pytrinamic/ic/__init__.py +++ b/pytrinamic/ic/__init__.py @@ -6,6 +6,7 @@ from .TMC2209 import TMC2209 from .TMC2224 import TMC2224 from .TMC2225 import TMC2225 +from .TMC2240 import TMC2240 from .TMC2300 import TMC2300 from .TMC2590 import TMC2590 from .TMC2660 import TMC2660 diff --git a/pytrinamic/modules/TMCM1231.py b/pytrinamic/modules/TMCM1231.py new file mode 100644 index 00000000..68042100 --- /dev/null +++ b/pytrinamic/modules/TMCM1231.py @@ -0,0 +1,179 @@ +from ..modules import TMCLModule + +# features +from ..features import MotorControlModule, DriveSettingModule, LinearRampModule +from ..features import StallGuard2Module, CoolStepModule + + +class TMCM1231(TMCLModule): + """ + The TMCM-1231 is a single axis controller/driver module for 2-phase bipolar stepper motors. + """ + def __init__(self, connection, module_id=1): + super().__init__(connection, module_id) + self.name = "TMCM-1231" + self.desc = self.__doc__ + self.motors = [self._MotorTypeA(self, 0)] + + def rotate(self, axis, velocity): + self.connection.rotate(axis, velocity, self.module_id) + + def stop(self, axis): + self.connection.stop(axis, self.module_id) + + def move_to(self, axis, position, velocity=None): + if velocity: + self.motors[axis].linear_ramp.max_velocity = velocity + self.connection.move_to(axis, position, self.module_id) + + def move_by(self, axis, difference, velocity=None): + if velocity: + self.motors[axis].linear_ramp.max_velocity = velocity + self.connection.move_by(axis, difference, self.module_id) + + class _MotorTypeA(MotorControlModule): + + def __init__(self, module, axis): + MotorControlModule.__init__(self, module, axis, self.AP) + self.drive_settings = DriveSettingModule(module, axis, self.AP) + self.linear_ramp = LinearRampModule(module, axis, self.AP) + self.stallguard2 = StallGuard2Module(module, axis, self.AP) + self.coolstep = CoolStepModule(module, axis, self.AP, self.stallguard2) + + def get_position_reached(self): + return self.get_axis_parameter(self.AP.PositionReachedFlag) + + class AP: + TargetPosition = 0 + ActualPosition = 1 + TargetVelocity = 2 + ActualVelocity = 3 + MaxVelocity = 4 + MaxAcceleration = 5 + MaxCurrent = 6 + RunCurrent = MaxCurrent + StandbyCurrent = 7 + PositionReachedFlag = 8 + HomeSwitch = 9 + RightEndstop = 10 + LeftEndstop = 11 + RightLimit = 12 + LeftLimit = 13 + RampType = 14 + StartVelocity = 15 + StartAcceleration = 16 + MaxDeceleration = 17 + BreakVelocity = 18 + FinalDeceleration = 19 + StopVelocity = 20 + StopDeceleration = 21 + Bow1 = 22 + Bow2 = 23 + Bow3 = 24 + Bow4 = 25 + VirtualStopLeft = 26 + VirtualStopRight = 27 + VirtualStopEnable = 28 + VirtualStopMode = 29 + SwapStopSwitches = 33 + EnableSoftStop = 34 + RelativePositioningOption = 127 + MicrostepResolution = 140 + ChopperBlankTime = 162 + ConstantTOffMode = 163 + DisableFastDecayComparator = 164 + ChopperHysteresisEnd = 165 + ChopperHysteresisStart = 166 + TOff = 167 + SEIMIN = 168 + SECDS = 169 + SmartEnergyHysteresis = 170 + SECUS = 171 + SmartEnergyHysteresisStart = 172 + SG2FilterEnable = 173 + SG2Threshold = 174 + SmartEnergyActualCurrent = 180 + SmartEnergyStallVelocity = 181 + SmartEnergyThresholdSpeed = 182 + RandomTOffMode = 184 + ChopperSynchronization = 185 + PWMThresholdSpeed = 186 + PWMGrad = 187 + PWMAmplitude = 188 + PWMScale = 189 + PWMMode = 190 + PWMFrequency = 191 + PWMAutoscale = 192 + ReferenceSearchMode = 193 + ReferenceSearchSpeed = 194 + RefSwitchSpeed = 195 + EndSwitchDistance = 196 + LastReferencePosition = 197 + LatchedActualPosition = 198 + LatchedEncoderPosition = 199 + BoostCurrent = 200 + EncoderMode = 201 + MotorFullStepResolution = 202 + FreewheelingMode = 204 + LoadValue = 206 + ExtendedErrorFlags = 207 + DriverErrorFlags = 208 + EncoderPosition = 209 + EncoderResolution = 210 + MaxPositionEncoderDeviation = 212 + MaxVelocityEncoderDeviation = 213 + PowerDownDelay = 214 + ReverseShaft = 251 + StepDirectionMode = 254 + + class ENUM: + microstep_resolution_fullstep = 0 + microstep_resolution_halfstep = 1 + microstep_resolution_4_microsteps = 2 + microstep_resolution_8_microsteps = 3 + microstep_resolution_16_microsteps = 4 + microstep_resolution_32_microsteps = 5 + microstep_resolution_64_microsteps = 6 + microstep_resolution_128_microsteps = 7 + microstep_resolution_256_microsteps = 8 + + class GP0: + SerialBaudRate = 65 + SerialAddress = 66 + SerialHearbeat = 68 + CANBitRate = 69 + CANsendID = 70 + CANreceiveID = 71 + TelegramPauseTime = 75 + SerialHostAddress = 76 + AutoStartMode = 77 + TMCLCodeProtection = 81 + CANHeartbeat = 82 + CANSecondaryAddress = 83 + eepromCoordinateStore = 84 + zeroUserVariables = 85 + serialSecondaryAddress = 87 + ApplicationStatus = 128 + DownloadMode = 129 + ProgramCounter = 130 + TickTimer = 132 + RandomNumber = 133 + SuppressReply = 255 + + class GP3: + timer_0 = 0 + timer_1 = 1 + timer_2 = 2 + stopLeft_0 = 27 + stopRight_0 = 28 + input_0 = 39 + input_1 = 40 + input_2 = 41 + input_3 = 42 + + class IO: + GPO0 = 0 + AIN0 = 0 + GPI0 = 2 + GPI1 = 3 + diff --git a/pytrinamic/modules/TMCM6214.py b/pytrinamic/modules/TMCM6214.py new file mode 100644 index 00000000..016c7a91 --- /dev/null +++ b/pytrinamic/modules/TMCM6214.py @@ -0,0 +1,274 @@ +from ..modules import TMCLModule + +# features +from ..features import MotorControlModule, DriveSettingModule, LinearRampModule +from ..features import StallGuard2Module, CoolStepModule + + +class TMCM6214(TMCLModule): + """ + The TMCM-6214 is a six axis controller/driver module. Supply voltage is 24V. + """ + def __init__(self, connection, module_id=1): + super().__init__(connection, module_id) + self.name = "TMCM-6214" + self.desc = self.__doc__ + self.motors = [self._MotorTypeA(self, 0), self._MotorTypeA(self, 1), self._MotorTypeA(self, 2), + self._MotorTypeA(self, 3), self._MotorTypeA(self, 4), self._MotorTypeA(self, 5)] + + def rotate(self, axis, velocity): + """ + Rotates the motor on the given axis with the given velocity. + + Parameters: + axis: Axis index. + velocity: Target velocity to rotate the motor with. Units are module specific. + + Returns: None + """ + self.connection.rotate(axis, velocity, self.module_id) + + def stop(self, axis): + """ + Stops the motor on the given axis. + + Parameters: + axis: Axis index. + + Returns: None + """ + self.connection.stop(axis, self.module_id) + + def move_to(self, axis, position, velocity=None): + """ + Moves the motor on the given axis to the given target position. + + Parameters: + axis: Axis index. + position: Target position to move the motor to. Units are module specific. + velocity: Maximum position velocity to position the motor. Units are module specific. + If no velocity is given, the previously configured maximum positioning velocity (AP 4) + will be used. + + Returns: None + """ + if velocity: + self.motors[axis].linear_ramp.max_velocity = velocity + self.connection.move_to(axis, position, self.module_id) + + def move_by(self, axis, difference, velocity=None): + """ + Moves the motor on the given axis by the given position difference. + + Parameters: + axis: Axis index. + difference: Position difference to move the motor by. Units are module specific. + velocity: Maximum position velocity to position the motor. Units are module specific. + If no velocity is given, the previously configured maximum positioning velocity (AP 4) + will be used. + + Returns: None + """ + if velocity: + self.motors[axis].linear_ramp.max_velocity = velocity + self.connection.move_by(axis, difference, self.module_id) + + class _MotorTypeA(MotorControlModule): + """ + Motor class for the motor on axis 0. + """ + def __init__(self, module, axis): + MotorControlModule.__init__(self, module, axis, self.AP) + self.drive_settings = DriveSettingModule(module, axis, self.AP) + self.linear_ramp = LinearRampModule(module, axis, self.AP) + self.stallguard2 = StallGuard2Module(module, axis, self.AP) + self.coolstep = CoolStepModule(module, axis, self.AP, self.stallguard2) + + def get_position_reached(self): + """ + Indicates whether a positioning task has been completed. + + Returns: + 1, if target position has been reached. + 0, if target position has not been reached. + """ + return self.get_axis_parameter(self.AP.PositionReachedFlag) + + class AP: + # Axis parameter map for this axis. + TargetPosition = 0 + ActualPosition = 1 + TargetVelocity = 2 + ActualVelocity = 3 + MaxVelocity = 4 + MaxAcceleration = 5 + MaxCurrent = 6 + RunCurrent = MaxCurrent + StandbyCurrent = 7 + PositionReachedFlag = 8 + HomeSwitch = 9 + RightEndstop = 10 + LeftEndstop = 11 + RightLimitSwitchDisable = 12 + LeftLimitSwitchDisable = 13 + SwapLimitSwitches = 14 + A1 = 15 + V1 = 16 + MaxDeceleration = 17 + D1 = 18 + StartVelocity = 19 + StopVelocity = 20 + RampWaitTime = 21 + HighSpeedTheshold = 22 + MinDcStepSpeed = 23 + RightSwitchPolarity = 24 + LeftSwitchPolarity = 25 + Softstop = 26 + HighSpeedChopperMode = 27 + HighSpeedFullstepMode = 28 + MeasuredSpeed = 29 + PowerDownRamp = 31 + DcStepTime = 32 + DcStepStallGuard = 33 + PositionCompareStart = 40 + PositionCompareDistance = 41 + PositionCompareOutput = 42 + PositionCompareOutputPulseLength = 43 + RelativePositioningOptionCode = 127 + MicrostepResolution = 140 + ChopperBlankTime = 162 + ConstantTOffMode = 163 + DisableFastDecayComparator = 164 + ChopperHysteresisEnd = 165 + ChopperHysteresisStart = 166 + TOff = 167 + SEIMIN = 168 + SECDS = 169 + SmartEnergyHysteresis = 170 + SECUS = 171 + SmartEnergyHysteresisStart = 172 + SG2FilterEnable = 173 + SG2Threshold = 174 + ShortToGroundProtection = 177 + VSense = 179 + SmartEnergyActualCurrent = 180 + SmartEnergyStallVelocity = 181 + SmartEnergyThresholdSpeed = 182 + RandomTOffMode = 184 + ChopperSynchronization = 185 + PWMThresholdSpeed = 186 + PWMGrad = 187 + PWMAmplitude = 188 + PWMScale = 189 + PWMMode = 190 + PWMFrequency = 191 + PWMAutoscale = 192 + ReferenceSearchMode = 193 + ReferenceSearchSpeed = 194 + RefSwitchSpeed = 195 + RightLimitSwitchPosition = 196 + LastReferencePosition = 197 + LatchedActualPosition = 198 + LatchedEncoderPosition = 199 + EncoderMode = 201 + MotorFullStepResolution = 202 + FreewheelingMode = 204 + LoadValue = 206 + ErrorFlags = 207 + StatusFlags = 208 + EncoderPosition = 209 + EncoderResolution = 210 + EncoderDeviationMax = 212 + GroupIndex = 213 + PowerDownDelay = 214 + DeviationAction = 240 + ReverseShaft = 251 + UnitMode = 255 + + class ENUM: + """ + Constant enums for parameters of this module. + """ + MicrostepResolutionFullstep = 0 + MicrostepResolutionHalfstep = 1 + MicrostepResolution4Microsteps = 2 + MicrostepResolution8Microsteps = 3 + MicrostepResolution16Microsteps = 4 + MicrostepResolution32Microsteps = 5 + MicrostepResolution64Microsteps = 6 + MicrostepResolution128Microsteps = 7 + MicrostepResolution256Microsteps = 8 + + class GP0: + """ + Global parameter map for this module. + """ + RS485Baudrate = 65 + SerialAddress = 66 + SerialHeartbeat = 68 + CANBitrate = 69 + CANSendId = 70 + CANReceiveId = 71 + CANSecondaryId = 72 #not in datasheet + TelegramPauseTime = 75 + SerialHostAddress = 76 + AutoStartMode = 77 + ProtectionMode = 81 + CANHeartbeat = 82 + CANSecondaryAddress = 83 + EepromCoordinateStore = 84 + ZeroUserVariables = 85 + SerialSecondaryAddress = 86 + ApplicationStatus = 128 + DownloadMode = 129 + ProgramCounter = 130 + TickTimer = 132 + RandomNumber = 133 + SuppressReply = 255 + + class GP3: + Timer_0 = 0 + Timer_1 = 1 + Timer_2 = 2 + StopLeft_0 = 27 + StopRight_0 = 28 + StopLeft_1 = 29 + StopRight_1 = 30 + StopLeft_2 = 31 + StopRight_2 = 32 + StopLeft_3 = 33 + StopRight_3 = 34 + StopLeft_4 = 35 + StopRight_4 = 36 + StopLeft_5 = 37 + StopRight_5 = 38 + Input_0 = 39 + Input_1 = 40 + Input_2 = 41 + Input_3 = 42 + Input_4 = 43 + Input_5 = 44 + Input_6 = 45 + Input_7 = 46 + + + class IO: + OUT0 = 0 + OUT1 = 1 + OUT2 = 2 + OUT3 = 3 + OUT4 = 4 + OUT5 = 5 + OUT6 = 6 + OUT7 = 7 + AIN0 = 0 + IN1 = 1 + IN2 = 2 + IN3 = 3 + AIN4 = 4 + IN5 = 5 + IN6 = 6 + IN7 = 7 + STO = 10 + STO1 = 13 + STO2 = 14 diff --git a/pytrinamic/modules/__init__.py b/pytrinamic/modules/__init__.py index ef54fd69..c0babf3b 100644 --- a/pytrinamic/modules/__init__.py +++ b/pytrinamic/modules/__init__.py @@ -22,3 +22,5 @@ from .TMCM3351 import TMCM3351 from .TMCM6110 import TMCM6110 from .TMCM6212 import TMCM6212 +from .TMCM6214 import TMCM6214 +from .TMCM1231 import TMCM1231 \ No newline at end of file diff --git a/pytrinamic/tmcl.py b/pytrinamic/tmcl.py index 0b79f439..edc780b4 100644 --- a/pytrinamic/tmcl.py +++ b/pytrinamic/tmcl.py @@ -151,9 +151,6 @@ def __str__(self): self.checksum ) - def dump(self): - print(self) - class TMCLReply: def __init__(self, reply_address, module_address, status, command, value, checksum=None, special=False): @@ -194,9 +191,6 @@ def __str__(self): self.checksum ) - def dump(self): - print(self) - def value(self): return self.value diff --git a/pytrinamic/tools/__init__.py b/pytrinamic/tools/__init__.py new file mode 100644 index 00000000..31a979de --- /dev/null +++ b/pytrinamic/tools/__init__.py @@ -0,0 +1 @@ +from .velocity_ramp_runner import VelocityRampRunner diff --git a/pytrinamic/tools/tests/test_ramp_runner.py b/pytrinamic/tools/tests/test_ramp_runner.py new file mode 100644 index 00000000..c78107c4 --- /dev/null +++ b/pytrinamic/tools/tests/test_ramp_runner.py @@ -0,0 +1,82 @@ +"""Testing the ramp module + +Run this test using ether the command-line of the unittest framework [1] or simply call the script directly + +[1]: https://docs.python.org/3/library/unittest.html#command-line-interface +""" + +import time +import unittest +from unittest.mock import Mock, call + +from pytrinamic.tools import VelocityRampRunner + + +class TestVelocityRampRunner(unittest.TestCase): + """Contains the tests for the VelocityRampRunner""" + + def test_fixed_cycle_time(self): + """Run a linear ramp and see if the velocity is updated as expected.""" + update_mock = Mock() + ramp_runner = VelocityRampRunner(update_mock, update_cycle_time_ms=10) + + # call the method under test + ramp_runner.run_linear_ramp(0, 100, 40) + + expected_velocity_update_calls = [call(0), call(25), call(50), call(75), call(100)] + update_mock.assert_has_calls(expected_velocity_update_calls) + + def test_fixed_cycle_time_delay_time(self): + """Run a linear ramp and see if the delay is plausible""" + update_mock = Mock() + ramp_runner = VelocityRampRunner(update_mock, update_cycle_time_ms=100) + + # call the method under test + start_time = time.perf_counter() + ramp_runner.run_linear_ramp(0, 100, 400) + stop_time = time.perf_counter() + + delay_s = stop_time-start_time + assert 0.35 < delay_s < 0.45 + + def test_fixed_cycle_time_imprecise_25(self): + """Run a linear ramp with an interval that is not a multiple of update_cycle_time_ms""" + update_mock = Mock() + ramp_runner = VelocityRampRunner(update_mock, update_cycle_time_ms=10) + + # call the method under test + ramp_runner.run_linear_ramp(0, 100, 25) + + expected_velocity_update_calls = [call(0), call(40), call(80), call(100)] + update_mock.assert_has_calls(expected_velocity_update_calls) + + def test_fixed_cycle_time_imprecise_35(self): + """Run a linear ramp with an interval that is not a multiple of update_cycle_time_ms""" + update_mock = Mock() + ramp_runner = VelocityRampRunner(update_mock, update_cycle_time_ms=10) + + # call the method under test + ramp_runner.run_linear_ramp(0, 100, 35) + + expected_velocity_update_calls = [call(0), call(28), call(57), call(85), call(100)] + update_mock.assert_has_calls(expected_velocity_update_calls) + + def test_without_given_cycle_time(self): + """Run a linear ramp and see if the velocity is updated as expected.""" + update_mock = Mock() + ramp_runner = VelocityRampRunner(update_mock) + + # we mock the internal time method to emulate calls to time.time + ramp_runner._time_ms = Mock() + # the _time_ms method is about to return thees values + ramp_runner._time_ms.side_effect = [10, 30, 50] + + # call the method under test + ramp_runner.run_linear_ramp(0, 100, 40) + + expected_velocity_update_calls = [call(0), call(50), call(100)] + update_mock.assert_has_calls(expected_velocity_update_calls) + + +if __name__ == "__main__": + unittest.main() diff --git a/pytrinamic/tools/velocity_ramp_runner.py b/pytrinamic/tools/velocity_ramp_runner.py new file mode 100644 index 00000000..8d1fc207 --- /dev/null +++ b/pytrinamic/tools/velocity_ramp_runner.py @@ -0,0 +1,78 @@ +"""Module to run different ramp motions. + +Please note that thees functions work in blocking mode. +""" + +import time +import math + + +class VelocityRampRunner: + """The VelocityRampRunner allows you to ramp up or down the velocity of a motor. + + This is helpful for modules/drives that do not have a velocity ramp features build in. + + You just give a start velocity, a target velocity and the time in which you like the velocity to go up/down from + the start to the target velocity, and run_linear_ramp will update the velocity at the maximum possible interval, or + by the interval given in the constructor (update_cycle_time_ms). + """ + + def __init__(self, velocity_update_callback, update_cycle_time_ms=0): + """If update_cycle_time_ms is set to 0, the velocity is updated as fast as possible. + + :argument velocity_update_callback: You need to give a function that updates the velocity of your motor. + The signature of the function should look like this: + def callback(int velocity): + :argument update_cycle_time_ms: Optional update interval, if set to zero or omitted the velocity will be updated + as fast as possible. + """ + self._velocity_update_callback = velocity_update_callback + self._update_cycle_time_ms = update_cycle_time_ms + + def run_linear_ramp(self, start_velocity_rpm, target_velocity_rpm, time_delta_ms): + """Update the velocity on a linear basis from start_velocity to the target_velocity within the given time.""" + if self._update_cycle_time_ms: + self._velocity_ramp_fixed_cycle(start_velocity_rpm, target_velocity_rpm, time_delta_ms) + else: + self._velocity_ramp_fast(start_velocity_rpm, target_velocity_rpm, time_delta_ms) + + def _velocity_ramp_fixed_cycle(self, start_velocity_rpm, target_velocity_rpm, time_delta_ms): + """Sub function that updates the velocity on a update_cycle_time_ms basis. + + Note that if time_delta_ms can not be divided by update_cycle_time_ms, + the linear ramp will be longer than expected. This way we make sure there is + no high acceleration at the end. + """ + update_cycles = math.ceil(time_delta_ms / self._update_cycle_time_ms) + acceleration = (target_velocity_rpm - start_velocity_rpm) / time_delta_ms + for i in range(update_cycles): + start_time = self._time_ms() + velocity_update = acceleration * (i * self._update_cycle_time_ms) + start_velocity_rpm + self._velocity_update_callback(int(velocity_update)) + stop_time = self._time_ms() + delay_time_ms = (stop_time - start_time) + if delay_time_ms < self._update_cycle_time_ms: + time.sleep((self._update_cycle_time_ms - delay_time_ms) / 1000) + # always set the target_velocity_rpm at the end + self._velocity_update_callback(int(target_velocity_rpm)) + + def _velocity_ramp_fast(self, start_velocity_rpm, target_velocity_rpm, time_delta_ms): + """Sub function that updates the velocity as fast as possible. + + Remark: Seems to update the velocity on a 1/4 ms basis on my machine + """ + start_time = self._time_ms() + acceleration = (target_velocity_rpm - start_velocity_rpm) / time_delta_ms + self._velocity_update_callback(start_velocity_rpm) + stop_time = self._time_ms() + delay_time_ms = (stop_time - start_time) + while delay_time_ms < time_delta_ms: + velocity_update = acceleration * delay_time_ms + start_velocity_rpm + self._velocity_update_callback(int(velocity_update)) + stop_time = self._time_ms() + delay_time_ms = (stop_time - start_time) + self._velocity_update_callback(target_velocity_rpm) + + @classmethod + def _time_ms(cls): + return time.perf_counter() * 1000 diff --git a/pytrinamic/version.py b/pytrinamic/version.py index 1f4ca897..d31c31ea 100644 --- a/pytrinamic/version.py +++ b/pytrinamic/version.py @@ -1,2 +1 @@ -__version__ = "0.2.2" - +__version__ = "0.2.3" diff --git a/tests/test_can_adapters.py b/tests/test_can_adapters.py new file mode 100644 index 00000000..9bc6aff5 --- /dev/null +++ b/tests/test_can_adapters.py @@ -0,0 +1,81 @@ +"""Testing the all CAN Adapters in a combined Network + +To recreate the tests create a CAN network by combining: + * Kvaser Leaf Light v2 + * PEAK PCAN + * Ixxat USB-to-CAN + * CANable (Original) + * 3x TMCM-1241 with CAN-ID 3,4 and 5 +""" + +import pytest + +from pytrinamic.modules import TMCLModule + +from pytrinamic.connections import ConnectionManager + +from pytrinamic.connections import KvaserTmclInterface +from pytrinamic.connections import PcanTmclInterface +from pytrinamic.connections import SlcanTmclInterface +from pytrinamic.connections import IxxatTmclInterface + +slcan_com_port = 'COM15' + +ap = { + 'Maximum current': 6, + 'Microstep resolution': 140, +} + + +@pytest.fixture(scope='function', params=[ + KvaserTmclInterface, + PcanTmclInterface, + SlcanTmclInterface, + IxxatTmclInterface, +]) +def can_adapter(request): + can_tmcl_interface_class = request.param + if can_tmcl_interface_class == PcanTmclInterface: + ports = PcanTmclInterface.list() + adptr = can_tmcl_interface_class(port=ports[0]) + elif can_tmcl_interface_class == SlcanTmclInterface: + adptr = can_tmcl_interface_class(com_port=slcan_com_port) + else: + adptr = can_tmcl_interface_class() + yield adptr + adptr.close() + + +def test_adapter_classes(can_adapter): + tmcm1241s = [TMCLModule(can_adapter, module_id=mid) for mid in range(3, 6)] + for tmcm1241 in tmcm1241s: + assert tmcm1241.get_global_parameter(71, 0) == tmcm1241.module_id + assert tmcm1241.get_axis_parameter(ap['Microstep resolution'], 0) == 8 + for tmcm1241 in tmcm1241s: + tmcm1241.set_axis_parameter(ap['Maximum current'], 0, 10+tmcm1241.module_id) + assert tmcm1241.get_axis_parameter(ap['Maximum current'], 0) == 10+tmcm1241.module_id + for tmcm1241 in tmcm1241s: + tmcm1241.set_axis_parameter(ap['Maximum current'], 0, 20+tmcm1241.module_id) + assert tmcm1241.get_axis_parameter(ap['Maximum current'], 0) == 20+tmcm1241.module_id + + +@pytest.mark.parametrize('cm_call', [ + f"--interface ixxat_tmcl", + f"--interface kvaser_tmcl", + f"--interface pcan_tmcl", + f"--interface slcan_tmcl --port {slcan_com_port}", +]) +def test_connection_manager(cm_call): + cm = ConnectionManager(cm_call) + with cm.connect() as interface: + tmcm1241s = [TMCLModule(interface, module_id=mid) for mid in range(3, 6)] + for tmcm1241 in tmcm1241s: + assert tmcm1241.get_global_parameter(71, 0) == tmcm1241.module_id + assert tmcm1241.get_axis_parameter(ap['Microstep resolution'], 0) == 8 + for tmcm1241 in tmcm1241s: + tmcm1241.set_axis_parameter(ap['Maximum current'], 0, 10+tmcm1241.module_id) + assert tmcm1241.get_axis_parameter(ap['Maximum current'], 0) == 10+tmcm1241.module_id + for tmcm1241 in tmcm1241s: + tmcm1241.set_axis_parameter(ap['Maximum current'], 0, 20+tmcm1241.module_id) + assert tmcm1241.get_axis_parameter(ap['Maximum current'], 0) == 20+tmcm1241.module_id + diff --git a/tests/test_interface_timeouts.py b/tests/test_interface_timeouts.py new file mode 100644 index 00000000..d3f5c839 --- /dev/null +++ b/tests/test_interface_timeouts.py @@ -0,0 +1,108 @@ +"""Test for the ConnectionManager timeout parameter. + +A (virtual) comport and a Kvaser CAN-Adapter are needed to run these tests. +Note, you may need change the comport number. +""" + +import time + +import pytest + +from pytrinamic.connections import ConnectionManager +from pytrinamic.connections import KvaserTmclInterface +from pytrinamic.connections import SerialTmclInterface +from pytrinamic.connections import UartIcInterface + + +@pytest.mark.parametrize('interface,con_man_parameters', [ + ('serial_tmcl', ' --port COM4 --data-rate 115200'), + ('uart_ic', ' --port COM4'), + ('kvaser_tmcl', '') +]) +@pytest.mark.parametrize('add_argument,expected_timeout', [ + ('', 5), + (' --timeout 7', 7), + (' --timeout 200', 200), + (' --timeout 33.3', 33.3), + (' --timeout 0.0', None), + (' --timeout 0', None), + (' --timeout -0', None), +]) +def test_valid_input_cm(interface, con_man_parameters, add_argument, expected_timeout): + """Check if the timeout is forwarded to the serial interface.""" + + cm = ConnectionManager(f"--interface {interface}{con_man_parameters}" + add_argument) + + with cm.connect() as myinterface: + if interface == 'serial_tmcl': + assert myinterface._serial.timeout == expected_timeout + elif interface == 'uart_ic': + assert myinterface.serial.timeout == expected_timeout + elif interface == 'kvaser_tmcl': + assert myinterface._timeout_s == expected_timeout + else: + pytest.fail('Unexpected interface!') + + +@pytest.mark.parametrize('interface_class', [ + 'kvaser_tmcl', + 'serial_tmcl', + 'uart_ic', +]) +@pytest.mark.parametrize('timeout_input,expected_timeout', [ + ('', 5), + (None, None), + (0, None), + (7, 7), + (33.3, 33.3), +]) +def test_valid_input_direct(interface_class, timeout_input, expected_timeout): + """Test like the test_valid_input_cm() but without the connection manager.""" + if interface_class == 'kvaser_tmcl': + if timeout_input == '': + myinterface = KvaserTmclInterface() + else: + myinterface = KvaserTmclInterface(timeout_s=timeout_input) + assert myinterface._timeout_s == expected_timeout + elif interface_class == 'serial_tmcl': + if timeout_input == '': + myinterface = SerialTmclInterface('COM4') + else: + myinterface = SerialTmclInterface('COM4', timeout_s=timeout_input) + assert myinterface._serial.timeout == expected_timeout + elif interface_class == 'uart_ic': + if timeout_input == '': + myinterface = UartIcInterface('COM4') + else: + myinterface = UartIcInterface('COM4', timeout_s=timeout_input) + assert myinterface.serial.timeout == expected_timeout + + +@pytest.mark.parametrize('con_man_call,expected_exception', [ + ('--interface serial_tmcl --port COM4 --data-rate 115200', RuntimeError), + ('--interface kvaser_tmcl', ConnectionError), +]) +def test_actual_timeout(con_man_call, expected_exception): + """We just call the receive-function without sending anything and check if a timeout will be raised.""" + + cm = ConnectionManager(f"{con_man_call} --timeout 1.5") + + with pytest.raises(expected_exception): + with cm.connect() as myinterface: + start_time = time.perf_counter() + myinterface._recv(0, 0) + stop_time = time.perf_counter() + duration = stop_time - start_time + assert 1.5 < duration < 1.7 + + +@pytest.mark.parametrize('timeout_argument', [ + 'string', + '0x1F', + '-0.1', + '-100', +]) +def test_invalid_timeout(timeout_argument): + """Test some invalid arguments for the timeout value.""" + with pytest.raises(SystemExit) as exec_info: + ConnectionManager(f"--interface serial_tmcl --port COM4 --data-rate 115200 --timeout {timeout_argument}")