diff --git a/src/pymodaq_plugins_arduino/daq_move_plugins/daq_move_StepperMotor.py b/src/pymodaq_plugins_arduino/daq_move_plugins/daq_move_StepperMotor.py new file mode 100644 index 0000000..7279670 --- /dev/null +++ b/src/pymodaq_plugins_arduino/daq_move_plugins/daq_move_StepperMotor.py @@ -0,0 +1,126 @@ +from typing import Union, List, Dict, Optional +from pymodaq.control_modules.move_utility_classes import ( + DAQ_Move_base, comon_parameters_fun, main, DataActuatorType, DataActuator +) +from pymodaq_gui.parameter import Parameter +from pymodaq_plugins_arduino.hardware.arduino_telemetrix import Arduino +from pymodaq_plugins_arduino.utils import Config + +config = Config() + + +class DAQ_Move_StepperMotor(DAQ_Move_base): + """Plugin to control stepper motor using Arduino controller and PyMoDAQ. + + This object inherits all functionalities to communicate with PyMoDAQ’s DAQ_Move module through inheritance via + DAQ_Move_base. It makes a bridge between the DAQ_Move module and the Python wrapper of a particular instrument. + + Use the arduino_telemetrix wrapper to communicate with the Arduino Board. + Telemetrix supports up to 4 stepper motors, but this plugin currently drives a single axis. + It does not consider the daisy chain option: only one controller. + Moves are non-blocking and can be stopped with the Stop button. + There is no encoder: the position is the step counter of the Arduino, reset to 0 at initialization. + Tested with Arduino Uno and one motor NEMA17 (1 axis). + PyMoDAQ version during the test was PyMoDAQ==5.3.2. + The operating system used was Windows 11. + Telemetrix4Arduino has to be uploaded on the Arduino board. + + Attributes + ---------- + controller: object + The particular object that allows communication with the hardware, in general, a Python wrapper around the + hardware library. + """ + _axis_names: Union[List[str], Dict[str, int]] = {'Stepper': config('stepper', 'name')} + _controller_units: Union[str, Dict[str, str]] = {'Stepper': 'count'} + _epsilons: Union[float, Dict[str, float]] = {'Stepper': 1} + data_actuator_type = DataActuatorType.DataActuator + + params = [ + { + 'title': 'Ports:', + 'name': 'com_port', + 'type': 'list', + 'value': config('com_port'), + 'limits': Arduino.COM_PORTS, + } + ] + comon_parameters_fun(axis_names=_axis_names) + + def ini_attributes(self): + self.controller: Optional[Arduino] = None + + def get_actuator_value(self) -> DataActuator: + """Get the current position of the motor, in steps, with scaling conversion.""" + pos = DataActuator(data=self.controller.get_stepper_position(), + units=self.axis_unit) + pos = self.get_position_with_scaling(pos) + return pos + + def close(self): + """Terminate the communication with the Arduino.""" + if self.controller is not None: + self.controller.shutdown() + + def commit_settings(self, param: Parameter): + """Apply the consequences of a change of value in the detector settings. + + Parameters + ---------- + param: Parameter + A given parameter (within detector_settings) whose value has been changed by the user. + """ + pass + + def ini_stage(self, controller=None): + """Actuator communication initialization. + + Parameters + ---------- + controller: object + Custom object of a PyMoDAQ plugin (Slave case). None if only one actuator by controller (Master case). + + Returns + ------- + info: str + initialized: bool + False if initialization failed otherwise True. + """ + if self.is_master: + self.controller = Arduino(com_port=self.settings['com_port']) + self.controller.initialize_stepper_motor( + config('stepper', 'pins', 'pul_pin'), + config('stepper', 'pins', 'dir_pin'), + config('stepper', 'pins', 'ena_pin'), + ) # Pulse, direction, enable pins + else: + self.controller = controller + + info = "Stepper motor connected with config file" + initialized = self.controller.stepper_motor is not None + return info, initialized + + def move_abs(self, value: DataActuator): + """Move the motor to the absolute target defined by value.""" + value = self.check_bound(value) + self.target_value = value + value = self.set_position_with_scaling(value) + self.controller.move_stepper_to_position(value.value(self.axis_unit)) + + def move_rel(self, value: DataActuator): + """Move the motor by the relative amount defined by value.""" + self.move_abs(self.current_value + value) + + def move_home(self): + """Move the motor back to its reference position (step 0).""" + home = DataActuator(data=0, units=self.axis_unit) + self.target_value = self.get_position_with_scaling(home) + self.controller.move_stepper_to_position(0) + + def stop_motion(self): + """Stop the motor immediately, then emit the move_done signal""" + self.controller.stop_stepper() + self.move_done() + + +if __name__ == '__main__': + main(__file__) \ No newline at end of file diff --git a/src/pymodaq_plugins_arduino/hardware/arduino_telemetrix.py b/src/pymodaq_plugins_arduino/hardware/arduino_telemetrix.py index 24fa194..be42623 100644 --- a/src/pymodaq_plugins_arduino/hardware/arduino_telemetrix.py +++ b/src/pymodaq_plugins_arduino/hardware/arduino_telemetrix.py @@ -1,5 +1,5 @@ import numbers -from threading import Lock +from threading import Lock, Event from pyvisa import ResourceManager from telemetrix import telemetrix @@ -27,6 +27,8 @@ def __init__(self, *args, **kwargs): 3: 0, 4: 0, 5: 0} # Initialized dictionary for 6 analog channels + self.stepper_motor = None + self.position = 0 @staticmethod def round_value(value): @@ -96,6 +98,49 @@ def servo_move_degree(self, pin: int, value: float): self.pin_values_output[pin] = value lock.release() + # Stepper motor methods + def initialize_stepper_motor(self, pulse_pin, direction_pin, enable_pin=7): + """ Initialize the stepper motor with the given pins """ + self.stepper_motor = self.set_pin_mode_stepper(interface=1, pin1=pulse_pin, pin2=direction_pin) + self.enable = enable_pin + self.set_pin_mode_digital_output(self.enable) # Set the enable pin as digital output + self.digital_write(self.enable, 1) # Disable the motor driver to avoid electrical consumption + self.stepper_set_current_position(self.stepper_motor, 0) # Set the current position to 0 + + def move_stepper_to_position(self, position: float, max_speed=200, acceleration=400): + """ Start moving the stepper motor to the specified position, without waiting for the end """ + if self.stepper_motor is None: + raise ValueError("Stepper motor not initialized. Call initialize_stepper_motor first.") + self.stepper_set_max_speed(self.stepper_motor, max_speed) + self.stepper_set_acceleration(self.stepper_motor, acceleration) + self.stepper_move_to(self.stepper_motor, int(position)) + self.digital_write(self.enable, 0) # Enable the motor driver + self.stepper_run(self.stepper_motor, completion_callback=self._stepper_move_completed) + + def _stepper_move_completed(self, data): + """ Called by Telemetrix when the motor has reached its target """ + self.digital_write(self.enable, 1) # Disable the motor driver to avoid electrical consumption + + def stop_stepper(self): + """ Stop the stepper motor immediately and disable its driver """ + if self.stepper_motor is not None: + self.stepper_stop(self.stepper_motor) + self.digital_write(self.enable, 1) # Disable the motor driver to avoid electrical consumption + + def get_stepper_position(self): + """ Retrieve the current position of the stepper motor """ + position_event = Event() + + def position_callback(data): + """ Store the current position of the stepper motor """ + self.position = data[2] + position_event.set() + + self.stepper_get_current_position(self.stepper_motor, + current_position_callback=position_callback) + position_event.wait() + return self.position + if __name__ == '__main__': import time diff --git a/src/pymodaq_plugins_arduino/resources/config_template.toml b/src/pymodaq_plugins_arduino/resources/config_template.toml index 50c49c8..a4fc22f 100644 --- a/src/pymodaq_plugins_arduino/resources/config_template.toml +++ b/src/pymodaq_plugins_arduino/resources/config_template.toml @@ -19,4 +19,11 @@ rows = 2 [servo] pin = 2 pos_1 = 55 -pos_2 = 80 \ No newline at end of file +pos_2 = 80 + +[stepper] +name = 0 +[stepper.pins] +ena_pin = 7 +pul_pin = 8 +dir_pin = 9 \ No newline at end of file