diff --git a/.gitignore b/.gitignore index f82230c..5c230b3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,7 @@ *.pyc /.idea /.venv -/dist /build +/dist +.cache +docs/_build diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..d5eb730 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: python + +python: + - "2.6" + - "2.7" + - "3.4" + - "3.6" + +install: + - pip install . + - pip install -r requirements.test.txt + +script: make tests diff --git a/CHANGELOG.txt b/CHANGELOG.txt new file mode 100644 index 0000000..54e0543 --- /dev/null +++ b/CHANGELOG.txt @@ -0,0 +1,10 @@ + +v2.0 +---- + +- Motor class is now ONLY concerned with axis-level operations on a module. + + - Axis parameters have been moved from Motor.axis to direct properties + of the Motor class. + + - Extensive inline documentation added to codebase diff --git a/LICENSE.txt b/LICENSE similarity index 100% rename from LICENSE.txt rename to LICENSE diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8ef96a0 --- /dev/null +++ b/Makefile @@ -0,0 +1,42 @@ + +# Documentation generator configuration +############################################ +SPHINXBUILD = python -msphinx +SPHINXOPTS = "html" +SPHINXPROJ = python-tmcl +SOURCEDIR = ./docs +BUILDDIR = ./docs/_build + + +.PHONY: docs build Makefile + + +clean: + rm -rf ./{build,dist} + +build: clean + python setup.py sdist bdist_wheel + + +release-test: build + twine upload -r pypi-test dist/* + +dev-dependencies: + pip install -r requirements.dev.txt + +test-dependencies: + pip install -r requirements.test.txt + +docs-dependencies: + pip install -r requirements.docs.txt + +dependencies: dev-dependencies test-dependencies docs-dependencies + + +tests: + py.test test + + +docs: + @$(SPHINXBUILD) -j 4 -c ./docs -a "$(SOURCEDIR)" "$(BUILDDIR)" + diff --git a/README.md b/README.md deleted file mode 100644 index c6fdd77..0000000 --- a/README.md +++ /dev/null @@ -1,119 +0,0 @@ -Python TMCL client library -========================== - -Python wrapper around Trinamic's TMCL serial interface for controlling TMCM stepper modules -via a serial-to-rs485 converter. - - - -Installation ------------- - -### Install using pip -```sh -> pip install tmcl -``` - -### Install without pip -```sh -> git clone https://github.com/NativeDesign/python-tmcl.git -> cd python-tmcl -> python setup.py install -``` - - -Usage ------ - -Use an RS485-to-serial adapter to connect your PC to one or more TMCM modules. -Before starting you should check the modules' serial-address and baud-rate is -a known value. Out of the box (_warning: anecdotal_) modules usually have an address -of `1` and a baud-rate of `9600` but this is not guarenteed. The easiest way to check -these values is by using the [TMCL IDE][1] on a windows machine. - -If using multiple TMCM modules attached to the same rs485 bus you __must__ ensure that -each module is set to a _different_ serial-address so that they don't clash. - - -### Example usage (single-axis modules) -```python -from serial import Serial -from time import sleep -import TMCL - -## serial-address as set on the TMCM module. -MODULE_ADDRESS = 1 - -## Open the serial port presented by your rs485 adapter -serial_port = Serial("/dev/tty.usbmodem1241") - -## Create a Bus instance using the open serial port -bus = TMCL.connect(serial_port) - -## Get the motor -motor = bus.get_motor(MODULE_ADDRESS) - -## From this point you can start issuing TMCL commands -## to the motor as per the TMCL docs. This example will -## rotate the motor left at a speed of 1234 for 2 seconds -motor.rotate_left(1234) -sleep(2) -motor.stop() -``` - - -### Example usage (multi-axis modules) -```python -from serial import Serial -import TMCL - -## Open the serial port presented by your rs485 adapter -serial_port = Serial("/dev/tty.usbmodem1241") - -## Create a Bus instance using the open serial port -bus = TMCL.connect(serial_port) - -## Get the motor on axis 0 of module with address 1 -module = bus.get_module( 1 ) - -a0 = module.get_motor(0) -a1 = module.get_motor(1) -a2 = module.get_motor(2) - -``` - - - - -API Overview ------------- - - -#### class Motor (bus, address, axis) - -##### `move_absolute (position)` -Move the motor to the specified _absolute_ position. - -##### `move_relative (offset)` -Move the motor by the specified offset _relative to current position_. - -##### `reference_search (rfs_type)` -Start a reference search routine to locate limit switches. - -##### `rotate_left (velocity)` -Rotate the motor left-wards at the specified velocity. - -##### `rotate_right (velocity)` -Rotate the motor right-wards at the specified velocity. - -##### `run_command (cmd)` -Execute a predefined user subroutine written to TMCM module firmware - -##### `send (cmd, type, motorbank, value)` -Send a raw TMCL command to the motor. - -##### `stop ()` -Stop the motor - - -[1]: https://www.trinamic.com/support/software/tmcl-ide/ diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..bf24b8d --- /dev/null +++ b/README.rst @@ -0,0 +1,24 @@ +Python TMCL client library +========================== + +Python wrapper around Trinamic's TMCL serial interface for controlling TMCM stepper modules +via a serial-to-rs485 converter. + + +Full documentation is available at http://python-tmcl.rtfd.io + + +Installation +------------ + +### Install using pip +```sh +> pip install tmcl +``` + +### Install without pip +```sh +> git clone https://github.com/NativeDesign/python-tmcl.git +> cd python-tmcl +> python setup.py install +``` diff --git a/TMCL/__init__.py b/TMCL/__init__.py index b9a8111..4796b01 100644 --- a/TMCL/__init__.py +++ b/TMCL/__init__.py @@ -1,7 +1,9 @@ from .bus import Bus from .motor import Motor -from .commands import Command +from .tmcl_commands import Command from .reply import Reply +from .tmcl_axis_parameters import AxisParams +from .tmcl_global_parameters import GlobalParams def connect ( serial_port, CAN = False ): return Bus(serial_port, CAN) diff --git a/TMCL/__module.py b/TMCL/__module.py new file mode 100644 index 0000000..5ad879d --- /dev/null +++ b/TMCL/__module.py @@ -0,0 +1,210 @@ +# -*- coding: UTF-8 -*- +from .tmcl_commands import Command +from .motor import Motor + + +class Module(object): + """ + Represents a single TMCM module present on the bus. + """ + + + def __init__ ( self, bus, address=1 ): + """ + :param bus: + A Bus instance that is connected to one or more + physical TMCM modules via serial port + :type bus: TMCL.Bus + + :param address: + Module address. This defaults to 1 + :type address: int + + """ + assert isinstance(address, int), "Address must be an int" + assert 0 <= address <= 255, "Address must be 0 <= address <= 255" + + self.bus = bus + self.address = address + + + ######################################################################## + #### Core functions + ######################################################################## + + + def send (self, cmd, type=0, motbank=0, value=0): + """ + Send a TMCL command to the module + + :param cmd: + TMCL command to send to the motor. + Must be one of TMCL.Command.* values + :type cmd: int + + :param type: TMCL type parameter + :type type: int + + :param motbank: TMCL motor/bank parameter + :type motbank: int + + :param value: TMCL value parameter + :type type: int + + :rtype: TMCL.Reply + """ + return self.bus.send( self.address, cmd, type, motbank, value) + + + def get_axis (self, *args, **kwargs): + """ + Alias of get_motor + """ + return self.get_motor(*args, **kwargs) + + + def get_motor ( self, axis=0, max_velocity=None ): + """ + Return an interface to a single axis (motor) connected to + this module. + + :param axis: + Axis ID (defaults to 0) + :type axis: int + + :param max_velocity: + Maximum allowed velocity. See Motor for details + :type max_velocity: int + + :return: An interface to the desired axis/motor + :rtype: Motor + """ + return Motor(self, axis=axis, max_velocity=max_velocity) + + + def set_axis_param ( self, param, value, axis=0): + """ + Most parameters of a TMCM module can be adjusted individually for + each axis. Although these parameters vary widely in their formats + (1 to 24 bits, signed or unsigned) and physical locations + (TMC428, TMC453, controller RAM, controller EEPROM), they all can + be set by this function. See chapter 4 for a complete list of all + axis parameters. See STAP (section 3.7) for permanent storage of a + modified value. + + :param param: Axis parameter id + :type param: int + + :param value: Value to set + :type value: int + + :param axis: Motor axis to set param on + :type axis: int + + :rtype: TMCL.Reply + """ + + + def get_axis_param (self, param, axis=0): + """ + Most parameters of a TMCM module can be adjusted individually for + each axis. Although these parameters vary widely in their formats + (1 to 24 bits, signed or unsigned) and physical locations (TMC428, + TMC453, controller RAM, controller EEPROM), they all can be read by + this function. In stand-alone mode the requested value is also + transferred to the accumulator register for further processing + purposes such as conditioned jumps. In direct mode, the value read + is only output in the “value” field of the reply, without affecting + the accumulator. See chapter 4 for a complete list of all parameters. + + :param param: Axis parameter id (see AxisParams) + :type param: int + + :return: Axis param value. Type depends on param + :rtype: int + """ + self.send(Command.GAP, type=param, motbank=axis) + + + def get_global_param (self, bank, param ): + """ + All global parameters can be read with this function. In stand-alone + mode, the result is copied to the accumulator register for further + processing purposes such as conditional jumps. In direct mode, the + result is only output in the “value” field of the reply, without + affecting the accumulator. + + :param bank: Param bank [0..2] + :type bank: int + + :param param: Parameter to get - See GlobalParams.* + :type bank: int + + :rtype: int + """ + + + def set_global_param (self, bank, param, value): + """ + Global parameters are related to the host interface, peripherals or + application specific variables. The different groups of these + parameters are organised in "banks" to allow a larger total number + for future products. + + Currently, only bank 0 and 1 are used for global parameters, and + bank 2 is used for user variables. + + :param bank: Param bank [0..2] + :type bank: int + + :param param: Parameter to set + :type param: int + + :param value: Parameter value + :type value: int + + :rtype: int + """ + + + ######################################################################## + #### Utility functions + ######################################################################## + + + def run_command( self, cmdIndex ): + """ + Execute a predefined subroutine previously written to + module firmware. + + :param cmdIndex: + :return: + """ + reply = self.send(Command.RUN_APPLICATION, type=1, value=cmdIndex) + return reply.status + + + def get_param (self, param): + """ + Utility wrapper around `get_global_param` for ease of use + + :param param: One of GlobalParams.* + :type param: tuple(int,int) + + :return: Parameter value + :rtype: int + """ + reply = self.send(Command.GGP, type=param[1], motbank=param[0]) + return reply.value + + + def set_param (self, param, value): + """ + Utility wrapper around `set_global_param` for ease of use + + :param param: One of GlobalParams.* + :type param: tuple(int,int) + + :param value: Parameter value to set + :type value: int + """ diff --git a/TMCL/bus.py b/TMCL/bus.py index 2c0ac8c..4aa4eac 100644 --- a/TMCL/bus.py +++ b/TMCL/bus.py @@ -1,5 +1,8 @@ import struct -from .motor import Module, Motor + +from TMCL.instruction import Instruction +from .__module import Module +from .motor import Motor from .reply import Reply, TrinamicException @@ -19,11 +22,27 @@ class Bus (object): + serial = None + """ + Serial port used for communication + + :type: serial.Serial + """ + + CAN = False + """ + True if talking over CANbus + + :type: bool + """ + + def __init__( self, serial, CAN = False ): self.CAN = CAN self.serial = serial - def send ( self, address, command, type, motorbank, value ): + + def send ( self, address, command, type=0, motorbank=0, value=0 ): """ Send a message to the specified module. This is a blocking function that will not return until a reply @@ -31,6 +50,8 @@ def send ( self, address, command, type, motorbank, value ): See the TMCL docs for full descriptions of the parameters + @TODO Handle 'Target Position Reached Event' messages (docs pg 48) + :param address: Module address to send command to :param command: Instruction no :param type: Type @@ -39,6 +60,14 @@ def send ( self, address, command, type, motorbank, value ): :rtype: Reply """ + + if isinstance(command, Instruction): + type = command.type + motorbank = command.motbank + value = command.value + command = command.cmd + + if self.CAN: msg = struct.pack(MSG_STRUCTURE_CAN, command, type, motorbank,value) self.serial.write(msg) @@ -55,6 +84,7 @@ def send ( self, address, command, type, motorbank, value ): reply = Reply(struct.unpack(REPLY_STRUCTURE, rep)) return self._handle_reply(reply) + def get_module( self, address=1 ): """ Returns a Module object targeting the device at address `address` @@ -68,6 +98,7 @@ def get_module( self, address=1 ): """ return Module(self, address) + def get_motor( self, address=1, motor_id=0 ): """ Returns object addressing motor number `motor_id` on module `address`. @@ -87,13 +118,15 @@ def get_motor( self, address=1, motor_id=0 ): :rtype: Motor """ - return Motor(self, address, motor_id) + return self.get_module(address).get_motor(motor_id) + def _handle_reply (self, reply): if reply.status < Reply.Status.SUCCESS: raise TrinamicException(reply) return reply + def _binaryadd( self, address, command, type, motorbank, value ): checksum_struct = struct.pack(MSG_STRUCTURE[:-1], address, command, type, motorbank, value) checksum = 0 diff --git a/TMCL/commands.py b/TMCL/commands.py deleted file mode 100644 index e2f350b..0000000 --- a/TMCL/commands.py +++ /dev/null @@ -1,42 +0,0 @@ -class Command: - ROL = 2 # Rotate left - ROR = 1 # Rotate right - MVP = 4 # Move to position - MST = 3 # Motor stop - RFS = 13 # Reference search - SCO = 30 # Store coordinate - CCO = 32 # Capture coordinate - GCO = 31 # Get coordinate - SAP = 5 # Set axis parameter - GAP = 6 # Get axis parameter - STAP = 7 # Store axis parameter into EEPROM - RSAP = 8 # Restors axis parameter from EEPROM - SGP = 9 # Set global parameter - GGP = 10 # Get global parameter - STGP = 11 # Store global parameter into EEPROM - RSGP = 12 # Restore global parameter from EEPROM - SIO = 14 # Set output - GIO = 15 # Get input - SAC = 29 # Access to external SPI device - JA = 22 # Jump always - JC = 21 # Jump conditional - COMP = 20 # Compare accumulator with constant value - CLE = 36 # Clear error flags - CSUB = 23 # Call subroutine - RSUB = 24 # Return from subroutine - WAIT = 27 # Wait for a specified event - STOP = 28 # End of a TMCL program - CALC = 19 # Calculate using the accumulator and a constant value - CALCX = 33 # Calculate using the accumulator and the X register - AAP = 34 # Copy accumulator to an axis parameter - AGP = 35 # Copy accumulator to a global parameter - STOP_APPLICATION = 128 - RUN_APPLICATION = 129 - STEP_APPLICATION = 130 - RESET_APPLICATION = 131 - START_DOWNLOAD_MODE = 132 - QUIT_DOWNLOAD_MODE = 133 - READ_TMCL_MEMORY = 134 - GET_APPLICATION_STATUS = 135 - GET_FIRMWARE_VERSION = 136 - RESTORE_FACTORY_SETTINGS = 137 diff --git a/TMCL/instruction.py b/TMCL/instruction.py new file mode 100644 index 0000000..5a74ed9 --- /dev/null +++ b/TMCL/instruction.py @@ -0,0 +1,39 @@ + +class Instruction (object): + """ + Represents a mutable TMCL instruction + """ + + cmd = None + """ + TMCL command field + + :type: int + """ + + type = 0 + """ + TMCL type field + + :type: int + """ + + motbank = 0 + """ + TMCL motor/bank field + + :type: int + """ + + value = 0 + """ + TMCL value field + + :type: int + """ + + def __init__ (self, cmd, type=0, motbank=0, value=0): + self.cmd = cmd + self.type = type + self.motbank = motbank + self.value = value diff --git a/TMCL/module.py b/TMCL/module.py new file mode 100644 index 0000000..29a7f07 --- /dev/null +++ b/TMCL/module.py @@ -0,0 +1,285 @@ +# coding=utf-8 +from warnings import warn + +from .motor import Motor +from .instruction import Instruction +from .tmcl_commands import commands, Command + + + +class Module (object): + + bus = None + """ + The Bus instance that will be used to communicate with the physical module + :type: Bus + """ + + address = None + """ + The module address on the bus + :type: int + """ + + def __init__ (self, bus, address=1): + """ + + :param bus: Bus instance to use for communication + :type bus: Bus + + :param address: SerialAddress of module on bus + :type address: int + """ + assert isinstance(address, int), 'Address must be an int' + assert 0 <= address <= 255, 'Address must be in [0..255]' + + self.bus = bus + self.address = address + + + + ################################################################# + ## Core functions + ################################################################# + + def send(self, cmd, type=0, motbank=0, value=0): + """ Send an instruction this module + + Sends a TMCL instruction to this module and waits for a reply + before returning a TMCL.Reply object. + + Args: + cmd (int): TMCL command field + type (int): TMCL type field + motbank (int): TMCL motor/bank field + value (int): TMCL value field + + Returns: + TMCL.Reply: The response from the module + + Raises: + TrinamicException: If module returns anything but SUCCESS + + """ + assert isinstance(cmd, Instruction) or isinstance(cmd, int), "cmd must be int or Instruction" + assert isinstance(type, int), 'type must be an int' + assert isinstance(motbank, int), 'motbank must be an int' + assert isinstance(value, int), 'value must be an int' + + if isinstance(cmd, Instruction): + return self.bus.send(self.address, cmd) + else: + return self.bus.send(self.address, Instruction(cmd, type, motbank, value)) + + + def get_motor (self, *args, **kwargs): + return self.get_axis(*args,**kwargs) + + + def get_axis (self, axis=0): + """ + Returns a Motor instance for the specified axis id + + :param axis: Motor axis id. Defaults to 0 + :type axis: int + + :rtype: Motor + """ + return Motor(self, axis=axis) + + + def get_param (self, param, bank=None): + """ + Get the value of a global parameter from this module + + If `param` argument is an integer, `bank` arg MUST be + set. + + If `param` is a tuple(int,int) - like GlobalParams.* - + then `bank` arg will be ignored. + + :param param: Param id -OR- one of GlobalParams.* + :type param: int | (int,int) + + :param bank: Parameter bank number (ignored if `param` is a tuple) + :type bank: int + + :return: The `value` property of the reply message from module + :rtype: int + """ + if isinstance(param, tuple): + if bank is not None: + warn(UserWarning("arg 'bank' is ignored when param is a tuple")) + + assert len(param) == 2, "Tuple param must have exactly two values" + bank = param[0] + param = param[1] + + assert isinstance(param, int), "Param must be an int" + assert isinstance(bank, int), "Bank must be an int" + assert bank in [0,1,2], "Bank must be 0|1|2, got "+str(bank) + + inst = Instruction(Command.GGP, type=param, motbank=bank) + return self.send(inst).value + + + + def set_param(self, param, value, bank=None): + """ + Set a global parameter on this module + + If `param` arg is an integer, `bank` arg MUST be set. + + If `param` arg is a tuple (one of GlobalParams.*), then + `bank` arg will be ignored. + + :param param: Parameter ID -OR- one of GlobalParams.* + :type param: int | (int,int) + + :param value: Parameter value to set + :type value: int + + :param bank: Parameter bank number (ignored if `param` is a tuple) + :type bank: int + + :return: Reply message from bus + :rtype: TMCL.Reply + """ + if isinstance(param, tuple): + if bank is not None: + warn(UserWarning("arg 'bank' is ignored when param is a tuple")) + + assert len(param) == 2, "Tuple param must have exactly two values" + bank = param[0] + param = param[1] + + assert isinstance(param, int), 'param must be an int' + assert isinstance(value, int), 'value must be an int' + assert isinstance(bank, int), 'bank must be an int' + + inst = Instruction(Command.SGP, type=param, motbank=bank, value=value) + return self.send(inst) + + + def get_axis_param (self, param, axis=0): + """ + Return the value of an axis parameter from a particular axis + on the module + + From the TCML docs: + + Most parameters of a TMCM module can be adjusted individually for + each axis. Although these parameters vary widely in their formats + (1 to 24 bits, signed or unsigned) and physical locations (TMC428, + TMC453, controller RAM, controller EEPROM), they all can be read by + this function. ... In direct mode, the value read + is only output in the “value” field of the reply, without affecting + the accumulator. See chapter 4 for a complete list of all parameters. + + + :param param: Parameter ID. One of ``AxisParams.*`` + :type param: int + + :param axis: Axis to get parameter from. Defaults to 0 + :type axis: int + + :return: The parameter value + :rtype: int + """ + assert isinstance(param, int), 'param must be an int' + assert 0 <= param <= 255, 'param must be in [0..255]' + assert isinstance(axis, int), 'axis must be an int' + assert 0 <= axis <= 255, 'axis must be in [0..255]' + + return self.send( Command.GAP, type=param, motbank=axis).value + + + def set_axis_param (self, param, value, axis=0): + """ + Set an axis parameter on a particular axis on the module + + From the TCML docs: + + Most parameters of a TMCM module can be adjusted individually for + each axis. Although these parameters vary widely in their formats + (1 to 24 bits, signed or unsigned) and physical locations + (TMC428, TMC453, controller RAM, controller EEPROM), they all can + be set by this function. See chapter 4 for a complete list of all + axis parameters. See STAP (section 3.7) for permanent storage of a + modified value. + + + :param param: Parameter ID. One of ``AxisParams.*`` + :type param: int + + :param value: Value to set parameter to + :type value: int + + :param axis: Axis to set parameter to. Defaults to 0 + :type axis: int + + :return: Reply message from Bus + :rtype: TMCL.Reply + """ + assert isinstance(param, int), 'param must be an int' + assert 0 <= param <= 255, 'param must be in [0..255]' + assert isinstance(value, int), 'value must be an int' + assert isinstance(axis, int), 'axis must be an int' + assert 0 <= axis <= 255, 'axis must be in [0..255]' + + return self.send( Command.SAP, type=param, motbank=axis, value=value) + + + + def write_firmware (self, instructions): + """ Write TMCL firmware to module + + Write a series of TMCL instructions to this module's firmware. + This will erase all current instructions held in firmware. + + Args: + instructions ([Instruction]): List of instructions to write + + Returns: + bool: True if successful + + Example:: + + import TMCL.Command as c + module = Module() + module.write_application([ + c.ROL( 0, 0, 123 ) + c.MST( 0, 0, 0 ) + ]) + + """ + raise NotImplementedError() + + + def firmware_writer (self): + """ + Returns a context manager that can be used to dynamically write + an application to module firmware + + :rtype: contextmanager + + Example:: + + module = Module() + with module.application_writer as s: + s.ROL( 0, 0, 123 ) + s.MST( 0, 0, 0 ) + + """ + raise NotImplementedError() + + +## +## Shortcuts for TMCL commands +## +for cmd in commands: + def invoke (self, type=0, motbank=0, value=0): + self.send( cmd[1], type, motbank, value) + setattr(Module, cmd[0], invoke) + + diff --git a/TMCL/motor.py b/TMCL/motor.py index 368d239..fb67803 100644 --- a/TMCL/motor.py +++ b/TMCL/motor.py @@ -1,197 +1,499 @@ -from .commands import Command +# -*- coding: UTF-8 -*- +from TMCL.instruction import Instruction +from .tmcl_commands import Command +from .tmcl_axis_parameters import AxisParams -class Module(object): +class Motor(object): + """ + Motor objects provide an axis-level abstraction layer to a + TMCM module connected to a serial bus. You can use it to + start & stop movement an individual motor attached to a module, + configure its axis parameters and initiate reference searches. + """ + + RFS_START = 0 + RFS_STOP = 1 + RFS_STATUS = 2 + + + ALLOWED_COMMANDS = [ + Command.ROR, + Command.ROL, + Command.MST, + Command.MVP, + Command.SAP, + Command.GAP, + Command.STAP, + Command.RSAP, + Command.RFS, + Command.SCO, + Command.GCO, + Command.CCO + ] + + + module = None + """ + Module that hosts this motor + + :type: TMCL.Module + """ + + axis = 0 """ - Represents a single TMCM module present on the bus. + Motor axis ID on module + + :type: int """ - def __init__( self, bus, address=1 ): - """ - :param bus: - A Bus instance that is connected to one or more - physical TMCM modules via serial port - :type bus: TMCL.Bus + def __init__( self, module, axis=0, max_velocity=2047 ): + """ + + :param module: TMCM module instance + :type module: TMCL.module.Module - :param address: - Module address. This defaults to 1 - :type address: int + :param axis: Motor axis id + :type axis: int + :param max_velocity: + Maximum valid `velocity_limit` value. Default is 2047, + which applies to all known modules except TMCM-100 which + should be set to 8191. + :type max_velocity: int """ - self.bus = bus - self.address = address + self._max_velocity = max_velocity # type: int + self.axis = axis # type: int + self.module = module # type: TMCL.module.Module - def get_motor( self, axis=0 ): + ######################################################################## + #### Core functions + ######################################################################## + + def send( self, cmd, type=0, value=0 ): """ - Return an interface to a single axis (motor) connected to - this module. + Send a TMCL command to the motor. + + Only a subset of TMCL commands are allowed to be sent direct + to a Motor instance. For more general-purpose TMCL interface, + use a Module instance. - :param axis: - Axis ID (defaults to 0) - :type axis: int + :param cmd: + TMCL command to send to the motor. + Must be one of ``TMCL.Command.*`` + :type cmd: int - :return: An interface to the desired axis/motor - :rtype: Motor + :param type: TMCL type parameter + :type type: int + + :param value: TMCL value parameter + :type value: int + + :rtype: TMCL.Reply """ - return Motor(self.bus, self.address, axis) + assert isinstance(cmd,(int,Instruction)), 'cmd must be int or Instruction' + assert isinstance(type,int), 'type must be an int' + assert 0 <= type <= 255, 'type must be in [0..255]' + assert isinstance(value, int), 'value must be int' + if isinstance(cmd, Instruction): + assert cmd.cmd in self.ALLOWED_COMMANDS, 'Specified command cannot target a motor/axis' + cmd.axis = self.axis + return self.module.send(cmd) + else: + assert cmd in self.ALLOWED_COMMANDS, 'Specified command cannot target a motor/axis' + return self.module.send(Instruction(cmd, type, self.axis, value)) -class Motor(object): - RFS_START = 0 - RFS_STOP = 1 - RFS_STATUS = 2 + def get_axis_param (self, param): + """ + Most parameters of a TMCM module can be adjusted individually for + each axis. Although these parameters vary widely in their formats + (1 to 24 bits, signed or unsigned) and physical locations (TMC428, + TMC453, controller RAM, controller EEPROM), they all can be read by + this function. In stand-alone mode the requested value is also + transferred to the accumulator register for further processing + purposes such as conditioned jumps. In direct mode, the value read + is only output in the “value” field of the reply, without affecting + the accumulator. See chapter 4 for a complete list of all parameters. + + :param param: Axis parameter id (see AxisParams) + :type param: int + + :return: Axis param value. Type depends on param + :rtype: int + """ + assert isinstance(param, int), 'param must be an int' + assert 0 <= param <= 255, 'param must be in [0..255]' - def __init__( self, bus, address=1, axis=0 ): - self.bus = bus - self.module_id = address - self.motor_id = axis - self.axis = AxisParameterInterface(self) + return self.send(Command.GAP, type=param).value - def send( self, cmd, type, motorbank, value ): - return self.bus.send(self.module_id, cmd, type, motorbank, value) - def stop( self ): - self.send(Command.MST, 0, self.motor_id, 0) + def set_axis_param (self, param, value): + """ + Set an axis parameter on this motor + + From the TMCL docs: + + Most parameters of a TMCM module can be adjusted individually for + each axis. Although these parameters vary widely in their formats + (1 to 24 bits, signed or unsigned) and physical locations + (TMC428, TMC453, controller RAM, controller EEPROM), they all can + be set by this function. See chapter 4 for a complete list of all + axis parameters. See STAP (section 3.7) for permanent storage of a + modified value. + + :param param: Axis parameter id + :type param: int + + :param value: Value to set + :type value: int + + :rtype: TMCL.Reply + """ + assert isinstance(param, int), 'param must be an int' + assert 0 <= param <= 255, 'param must be in [0..255]' + assert isinstance(value, (int,long)), 'value must be an int' - def get_user_var( self, n ): - reply = self.send(Command.GGP, n, 2, 0) - return reply.value + return self.send(Command.SAP, type=param, value=value) + + + ######################################################################## + #### Utility functions + ######################################################################## + + def stop( self ): + """ + This instruction stops the motor. + + :rtype: TMCL.Reply + """ + return self.send(Command.MST) - def set_user_var( self, n, value ): - reply = self.send(Command.SGP, n, 2, value) - return reply.status def rotate_left (self, velocity ): - reply = self.send(Command.ROL, 0, self.motor_id, velocity) + """ + This instruction starts rotation in "left" direction, + i.e. decreasing the position counter. + + :param velocity: Movement velocity + :type velocity: int [0..self._max_velocity] + + :return: Response status (TMCL.Reply.Status.*) + :rtype: int + """ + assert isinstance(velocity,int), 'velocity must be an int' + assert 0 <= velocity <= self._max_velocity, 'velocity must be in [0..'+str(self._max_velocity)+']' + + reply = self.send(Command.ROL, value=velocity) return reply.status + def rotate_right(self, velocity): - reply = self.send(Command.ROR, 0, self.motor_id, velocity) + """ + This instruction starts rotation in "right" direction, + i.e. increasing the position counter + + :param velocity: Movement velocity + :type velocity: int [0..self._max_velocity] + + :return: Response status (TMCL.Reply.Status.*) + :rtype: int + """ + assert isinstance(velocity, int) + assert 0 <= velocity <= self._max_velocity, 'velocity must be in [0..'+str(self._max_velocity)+']' + + reply = self.send(Command.ROR, value=velocity) return reply.status + def move_absolute (self, position): - reply = self.send(Command.MVP, 0, self.motor_id, position) + """ + A movement towards the specified position is started, with + automatic generation of acceleration and deceleration ramps. + + The maximum velocity and acceleration are defined by axis + parameters MAX_POSITIONING_SPEED and MAX_ACCELERATION. + + Moving to an absolute position in the range [±2^23]. + + :param position: Absolute step position to move to + :type position: int [±2^23] + :return: + """ + assert isinstance(position, int), 'position must be an int' + assert -2**23 <= position <= 2**23, 'position must be in range +-2^23' + + reply = self.send(Command.MVP, type=0, value=position) return reply.status - def move_relative (self, offset): - reply = self.send(Command.MVP, 1, self.motor_id, offset) - def run_command( self, cmdIndex ): - reply = self.send(Command.RUN_APPLICATION, 1, self.motor_id, cmdIndex) - return reply.status + def move_relative (self, offset): + """ + A movement towards the specified position is started, with + automatic generation of acceleration and deceleration ramps. + + The maximum velocity and acceleration are defined by axis + parameters MAX_POSITIONING_SPEED and MAX_ACCELERATION. + + Starting a relative movement by means of an offset to the actual position. + In this case, the resulting new position value must not exceed ±2^23. + + :param offset: Steps to move relative to current position + :type offset: int [±2^23] + :return: + """ + assert isinstance(offset, int), 'offset must be an int' + assert -2**23 <= offset <= 2**23, 'Position must be within range +-2^23' - def reference_search( self, rfs_type ): - reply = self.send(Command.RFS, rfs_type, self.motor_id, 99) + reply = self.send(Command.MVP, type=1, value=offset) return reply.status -class AxisParameterInterface(object): - def __init__( self, motor ): + def reference_search( self, rfs_type ): """ + Initiate a reference search. - :param motor: - :type motor: Motor + From the TMCL docs: + + A build-in reference point search algorithm can be started (and stopped). + The reference search algorithm provides switching point calibration and + three switch modes. The status of the reference search can also be queried + to see if it has already finished. (In a TMCL program it is better to use + the WAIT command to wait for the end of a reference search.) Please see the + appropriate parameters in the axis parameter table to configure the reference + search algorithm to meet your needs (chapter 4). The reference search can be + started or stopped, or the actual status of the reference search can be checked. + + :param rfs_type: ``RFS_START | RFS_STOP | RFS_STATUS`` + :type rfs_type: int + + :return: When `rfs_type` == ``RFS_STATUS``; nonzero means reference search in progress + :rtype: int """ - self.motor = motor + assert rfs_type in [ + self.RFS_START, + self.RFS_STATUS, + self.RFS_STOP + ], 'rfs_type must be RFS_START | RFS_STATUS | RFS_STOP' - def get( self, param ): - reply = self.motor.send(Command.GAP, param, self.motor.motor_id, 0) - return reply.value - - def set( self, param, value ): - reply = self.motor.send(Command.SAP, param, self.motor.motor_id, value) + reply = self.send(Command.RFS, type=rfs_type) return reply.status + + + ######################################################################## + #### Axis parameter helpers + ######################################################################## + @property - def target_position( self ): - return self.get(0) + def target_position ( self ): + """ + The desired position in position mode. + + :type: int + """ + return self.get_axis_param(AxisParams.TARGET_POSITION) + @target_position.setter - def target_position( self, value ): - self.set(0, value) + def target_position ( self, value ): + self.set_axis_param(AxisParams.TARGET_POSITION, value) + @property - def actual_position( self ): - return self.get(1) + def actual_position ( self ): + """ + The current position of the motor. Should only be overwritten + for reference point setting. + + :type: int + :access: readonly + """ + return self.get_axis_param(AxisParams.ACTUAL_POSITION) + @actual_position.setter - def actual_position( self, value ): - self.set(1, value) + def actual_position ( self, value ): + self.set_axis_param(AxisParams.ACTUAL_POSITION, value) + @property - def target_speed( self ): - return self.get(2) + def target_speed ( self ): + """ + The desired speed in velocity mode (see ramp mode, no. 138). + In position mode, this parameter is set by hardware: to the + maximum speed during acceleration, and to zero during + deceleration and rest. + + :type: int + """ + return self.get_axis_param(AxisParams.TARGET_SPEED) + @target_speed.setter - def target_speed( self, value ): - self.set(2, value) + def target_speed ( self, value ): + self.set_axis_param(AxisParams.TARGET_SPEED, value) + @property - def actual_speed( self ): - return self.get(3) + def actual_speed ( self ): + """ + The current rotation speed. Should never be overwritten. + + :type: int + :access: readonly + """ + return self.get_axis_param(AxisParams.ACTUAL_SPEED) + @property - def max_positioning_speed( self ): - return self.get(4) + def max_positioning_speed ( self ): + """ + Should not exceed the physically highest possible value. + Adjust the pulse divisor (no. 154), if the speed value is + very low (<50) or above the upper limit. See TMC 428 + datasheet (p.24) for calculation of physical units. + + :type: int + """ + return self.get_axis_param(AxisParams.MAX_POSITIONING_SPEED) + @max_positioning_speed.setter - def max_positioning_speed( self, value ): - self.set(4, value) + def max_positioning_speed ( self, value ): + self.set_axis_param(AxisParams.MAX_POSITIONING_SPEED, value) + @property - def max_accelleration( self ): - return self.get(5) + def max_accelleration ( self ): + """ + The limit for acceleration (and deceleration). Changing + this parameter requires re-calculation of the acceleration + factor (no. 146) and the acceleration divisor (no.137), + which is done automatically. See TMC 428 datasheet + (p.24) for calculation of physical units. + + :type: int + """ + return self.get_axis_param(AxisParams.MAX_ACCELLERATION) + @max_accelleration.setter - def max_accelleration( self, value ): - self.set(5, value) + def max_accelleration ( self, value ): + self.set_axis_param(AxisParams.MAX_ACCELLERATION, value) + @property - def max_current( self ): - return self.get(6) + def max_current ( self ): + """ + The most important motor setting, since too high values + might cause motor damage! Note that on the TMCM-300 + the phase current can not be reduced down to zero due + to the Allegro A3972 driver hardware. + On the TMCM-300, 303, 310, 110, 610, 611 and 612 the + maximum value is 1500 (which means 1.5A). + On all other modules the maximum value is 255 (which + means 100% of the maximum current of the module) + + :type: int + """ + return self.get_axis_param(AxisParams.MAX_CURRENT) + @max_current.setter - def max_current( self, value ): - self.set(6, value) + def max_current ( self, value ): + self.set_axis_param(AxisParams.MAX_CURRENT, value) + @property - def standby_current( self ): - return self.get(7) + def standby_current ( self ): + """ + The current limit two seconds after the motor has stopped. + The value range of this parameter is the same as with + parameter 6. + + :type: int + """ + return self.get_axis_param(AxisParams.STANDBY_CURRENT) + @standby_current.setter - def standby_current( self, value ): - self.set(7, value) + def standby_current ( self, value ): + self.set_axis_param(AxisParams.STANDBY_CURRENT, value) + @property - def target_position_reached( self ): - return self.get(8) + def target_position_reached ( self ): + """ + Indicates that the actual position equals the target position. + + :type: int + """ + return self.get_axis_param(AxisParams.TARGET_POSITION_REACHED) + @property - def ref_switch_status( self ): - return self.get(9) + def ref_switch_status ( self ): + """ + The logical state of the reference (left) switch. + See the TMC 428 data sheet for the different switch + modes. Default is two switch mode: the left switch as + the reference switch, the right switch as a limit (stop) + switch. + + :type: int + """ + return self.get_axis_param(AxisParams.REF_SWITCH_STATUS) + @property - def right_limit_status( self ): - return self.get(10) + def right_limit_status ( self ): + """ + The logical state of the (right) limit switch. + + :type: int + """ + return self.get_axis_param(AxisParams.RIGHT_LIMIT_SWITCH_STATUS) + @property - def left_limit_status( self ): - return self.get(11) + def left_limit_status ( self ): + """ + The logical state of the left limit switch (in three switch mode) + + :type: int + """ + return self.get_axis_param(AxisParams.LEFT_LIMIT_SWITCH_STATUS) + @property - def right_limit_switch_disabled( self ): - return True if self.get(12) == 1 else False + def right_limit_switch_disabled ( self ): + """ + If set, deactivates the stop function of the right switch + + :type: int + """ + return True if self.get_axis_param(AxisParams.RIGHT_LIMIT_SWITCH_DISABLE) == 1 else False + @right_limit_switch_disabled.setter - def right_limit_switch_disabled( self, value ): - self.set(12, 1 if value else 0) + def right_limit_switch_disabled ( self, value ): + self.set_axis_param(AxisParams.RIGHT_LIMIT_SWITCH_DISABLE, 1 if value else 0) + @property - def left_limit_switch_disabled( self ): - return True if self.get(13) == 1 else False + def left_limit_switch_disabled ( self ): + """ + Deactivates the stop function of the left switch resp. + reference switch if set. + + :type: int + """ + return True if self.get_axis_param(AxisParams.LEFT_LIMIT_SWITCH_DISABLE) == 1 else False + @left_limit_switch_disabled.setter - def left_limit_switch_disabled( self, value ): - self.set(13, 1 if value else 0) + def left_limit_switch_disabled ( self, value ): + self.set_axis_param(AxisParams.LEFT_LIMIT_SWITCH_DISABLE, 1 if value else 0) diff --git a/TMCL/reply.py b/TMCL/reply.py index d3e128a..cf06ac0 100644 --- a/TMCL/reply.py +++ b/TMCL/reply.py @@ -5,13 +5,21 @@ def __init__( self, reply ): class Reply(object): - def __init__( self, reply_struct ): - self.reply_address = reply_struct[0] - self.module_address = reply_struct[1] - self.status = reply_struct[2] - self.command = reply_struct[3] - self.value = reply_struct[4] - self.checksum = reply_struct[5] + reply_address = None + module_address = None + status = 0 + command = 0 + value = 0 + checksum = 0 + + def __init__( self, reply_struct=None ): + if reply_struct: + self.reply_address = reply_struct[0] + self.module_address = reply_struct[1] + self.status = reply_struct[2] + self.command = reply_struct[3] + self.value = reply_struct[4] + self.checksum = reply_struct[5] class Status(object): diff --git a/TMCL/tmcl_axis_parameters.py b/TMCL/tmcl_axis_parameters.py new file mode 100644 index 0000000..59633f5 --- /dev/null +++ b/TMCL/tmcl_axis_parameters.py @@ -0,0 +1,745 @@ +# -*- coding: UTF-8 -*- + +from .tmcl_commands import Command + + +class AxisParams (object): + """ + The following sections describe all axis parameters that can be used with the + SAP, GAP, AAP, STAP and RSAP commands. Please note that some parameters are + different with different module types. + + Please note that the TMCM-100 module uses a different parameter set (see chapter 4.3), + but all other TMCL stepper motor modules use these parameters. + + Some of these parameters (marked ADVANCED) influence the TMC428 directly so + that advanced understanding of the TMC428 chip is needed. + + Access values: + R = readable (GAP) + W = writable (SAP) + E = automatically restored from EEPROM after reset or power-on. + + :see: http://www.mctechnology.nl/pdf/TMCL_reference_2015.pdf (Page 52) + """ + + + TARGET_POSITION = 0 + """ + The desired position in position mode (see RAMP_MODE). + + Range: ±2^23 + Access: RW + """ + + ACTUAL_POSITION = 1 + """ + The current position of the motor. Should only be overwritten for + reference point setting. + + Range: ±2^23 + Access: RW + """ + + TARGET_SPEED = 2 + """ + The desired speed in velocity mode (see RAMP_MODE). In position + mode, this parameter is set by hardware: to the maximum speed + during acceleration, and to zero during deceleration and rest. + + Range: ±2047 + Access: RW + """ + + ACTUAL_SPEED = 3 + """ + The current rotation speed. Should never be overwritten. + + Range: 0..2047 + Access: R + """ + + MAX_POSITIONING_SPEED = 4 + """ + Should not exceed the physically highest possible value. Adjust the + pulse divisor (no. 154), if the speed value is very low (<50) or + above the upper limit. + See TMC 428 datasheet (p.24) for calculation of physical units. + + Range: 0..2047 + Access: RWE + """ + + MAX_ACCELLERATION = 5 + """ + The limit for acceleration (and deceleration). Changing this parameter + requires re-calculation of the acceleration factor (no. 146) and the + acceleration divisor (no.137), which is done automatically. + See TMC 428 datasheet (p.24) for calculation of physical units. + + Range: 0..2047 + Access: RWE + """ + + MAX_CURRENT = 6 + """ + The most important motor setting, since too high values might cause + motor damage! Note that on the TMCM-300 the phase current can not be + reduced down to zero due to the Allegro A3972 driver hardware. + On the TMCM-300, 303, 310, 110, 610, 611 and 612 the maximum value + is 1500 (which means 1.5A). On all other modules the maximum value is + 255 (which means 100% of the maximum current of the module) + + Range: 0..1500 / 0..255 (see above) + Access: RWE + """ + + STANDBY_CURRENT = 7 + """ + The current limit two seconds after the motor has stopped. + The value range of this parameter is the same as with MAX_CURRENT + + Range: 0..1500 / 0..255 (see above) + Access: RWE + """ + + TARGET_POSITION_REACHED = 8 + """ + Indicates that the actual position equals the target position. + + Range: 0/1 + Access: R + """ + + REF_SWITCH_STATUS = 9 + """ + The logical state of the reference (left) switch. See the TMC 428 + data sheet for the different switch modes. Default is two switch mode: + the left switch as the reference switch, the right switch as a + limit (stop) switch. + + Range: 0/1 + Access: R + """ + + RIGHT_LIMIT_SWITCH_STATUS = 10 + """ + The logical state of the (right) limit switch. + + Range: 0/1 + Access: R + """ + + LEFT_LIMIT_SWITCH_STATUS = 11 + """ + The logical state of the left limit switch (in three switch mode) + + Range: 0/1 + Access: R + """ + + RIGHT_LIMIT_SWITCH_DISABLE = 12 + """ + If set, deactivates the stop function of the right switch + + Range: 0/1 + Access: RWE + """ + + LEFT_LIMIT_SWITCH_DISABLE = 13 + """ + Deactivates the stop function of the left switch resp. reference + switch if set. + + Range: 0/1 + Access: RWE + """ + + + ############################################################################# + ## + ## Advanced axis parameters + ## ======================== + ## + ## These parameters are only needed if the desired needs can not be met + ## by setting the basic parameters listed in section 4.1. Some of these + ## parameters influence the TMC428 directly so that advanced understanding + ## of the TMC428 chip is needed. There are even some parameters that + ## should only be changed if recommended by TRINAMIC. Please note that + ## these paramters are not available on the TMCM-100 module. + ############################################################################# + + MIN_SPEED = 130 + """ + Should always be set 1 to ensure exact reaching of the target position. + Do not change! + + Range: 0..2047 + Access: RWE + """ + + ACTUAL_ACCELERATION = 135 + """ + The current acceleration (read only). + + Range: 0..2047 + Access: R + """ + + ACCELERATION_THRESHOLD = 136 + """ + Specifies the threshold between low and high acceleration values for + the parameters 144&145. Normally not needed. + + Range: 0..2047 + Access: RWE + """ + + ACCELERATION_DIVISOR = 137 + """ + A ramping parameter, can be adjusted in special cases, automatically + calculated by setting the maximum acceleration (e.g. during normal + initialisation). See the TMC428 data sheet for details. + Normally no need to change. + + Range: 0..13 + Access: RWE + """ + + RAMP_MODE = 138 + """ + In version 2.16 and later: automatically set when using ROR, ROL, MST and MVP. + + 0: Position mode. Steps are generated, when the parameters actual position + and target position differ. Trapezoidal speed ramps are provided. + + 2: Velocity mode. The motor will run continuously and the speed will be + changed with constant (maximum) acceleration, if the parameter + "target speed" is changed. + + For special purposes, the soft mode (value 1) with exponential decrease of + speed can be selected. + + Range: 0/1/2 + Access: RWE + """ + + INTERRUPT_FLAGS = 139 + """ + Must not be modified. See the TMC 428 datasheet for details + + Range: 16 bits + Access: RW + """ + + MICROSTEP_RESOLUTION = 140 + """ + Note that modifying this parameter will affect the rotation speed in the + same relation. Even if the module is specified for 16 microsteps only, switching + to 32 or 64 microsteps still brings an enhancement in resolution and smoothness. + The position counter will use the full resolution, but, however, the motor will + resolve a maximum of 24 different microsteps only for the 32 or 64 microstep units. + + 0: full step * + 1: half step * + 2: 4 microsteps + 3: 8 microsteps + 4: 16 microsteps + 5: 32 microsteps + 6: 64 microsteps + + * Please note that the fullstep setting as well as the half step setting are not + optimized for use without an adapted microstepping table. These settings just step + through the microstep table in steps of 64 respectively 32. To get real full + stepping use axis parameter 211 or load an adapted microstepping table. + + Range: 0..6 + Access: RWE + """ + + REF_SWITCH_TOLERANCE = 141 + """ + For three-switch mode: a position range, where an additional switch (connected to + the REFL input) won't cause motor stop. See section 6.1 for details. + + Range: 0..4095 + Access: RW + """ + + SNAPSHOP_POSITION = 142 + """ + For referencing purposes, the exact position at hitting of the reference switch + can be captured in this parameter. + A dummy value has to be written first to prepare caption. + + Range: ±2^23 + Access: RW + """ + + MAX_CURRENT_AT_REST = 143 + """ + In contrast to the standby current, this current limit becomes immediately active + when the motor speed reaches zero. The value represents a fraction of the absolute + maximum current: + + 0: no change of current at rest (default, 100%) + 1..7: 12.5%..87.5% + + See the TMC428 datasheet for details. + Normally not used, use MAX_CURRENT and STANDBY_CURRENT instead! + + Range: 0..7 + Access: RWE + """ + + MAX_CURRENT_AT_LOW_ACCELERATION = 144 + """ + An optional current reduction factor, see parameters 136 and 143 for details. + Normally not used, use MAX_CURRENT and STANDBY_CURRENT instead! + + Range: 0..7 + Access: RWE + """ + + MAX_CURRENT_AT_HIGH_ACCELERATION = 145 + """ + An optional current reduction factor, see parameters ACCELERATION_THRESHOLD + and MAX_CURRENT_AT_REST for details. + Normally not used, use MAX_CURRENT and STANDBY_CURRENT instead! + + Range: 0..7 + Access: RWE + """ + + ACCELERATION_FACTOR = 146 + """ + A ramping parameter, can be adjusted in special cases, automatically calculated + by setting the maximum acceleration (e.g. during normal initialisation). + See the TMC428 data sheet for details. Normally no need to change. + + Range: 0..128 + Access: RWE + """ + + REF_SWITCH_DISABLE_FLAG = 147 + """ + If set, the reference switch (left switch) won't cause the motor to stop. + See RIGHT_LIMIT_SWITCH_DISABLE and LEFT_LIMIT_SWITCH_DISABLE + + Range: 0/1 + Access: RWE + """ + + LIMIT_SWITCH_DISABLE_FLAG = 148 + """ + If set, the limit switch (right switch) won't cause the motor to stop. + See RIGHT_LIMIT_SWITCH_DISABLE and LEFT_LIMIT_SWITCH_DISABLE + + Range: 0/1 + Access: RWE + """ + + SOFT_STOP_FLAG = 149 + """ + If cleared, the motor will stop immediately (disregarding motor limits), + when the reference or limit switch is hit. + + Range: 0/1 + Access: RWE + """ + + POSITION_LATCH_FLAG = 151 + """ + Indicates that a position snapshot has been completed (see SNAPSHOP_POSITION). + + Range: 0/1 + Access: RWE + """ + + INTERRUPT_MASK = 152 + """ + Must not be modified. See the TMC 428 datasheet for details + + Range: 16 bits + Access: R + """ + + RAMP_DIVISOR = 153 + """ + The exponent of the scaling factor for the ramp generator should be + de/incremented carefully (in steps of one). + + Range: 0..13 + Access: RWE + """ + + PULSE_DIVISOR = 154 + """ + The exponent of the scaling factor for the pulse (step) generator + – should be de/incremented carefully (in steps of one). + + Range: 0..13 + Access: RWE + """ + + REFERENCING_MODE = 193 + """ + Set the reference search mode. Please see chapter 6.1 for details on + reference search. + + 1: Only the left reference switch is searched. + + 2: The right switch is searched, then the left switch is searched. + + 3: Three-switch-mode: the right switch is searched first, then the + reference switch will be searched. + + Range: 1/2/3 + Access: RWE + """ + + REFERENCE_SEARCH_SPEED = 194 + """ + For the reference search this value specifies the search speed as a + fraction of the maximum velocity: + + 0: full speed + 1: half of the maximum speed + 2: a quarter of the maximum speed + 3: 1/8 of the maximum speed (etc.) + + On the TMCM-34x modules the speed is given directly as a value + between 0..2047. + + Range: 0..8 (0..2047 M-34X) + Access: RWE + """ + + REFERENCE_SWITCH_SPEED = 195 + """ + Similar to parameter no. 194, the speed for the switching point + calibration can be selected. + + On the TMCM-34x modules the speed is given directly as a value + between 0..2047. + + Range: 0..8 (0..2047 M-34X) + Access: RWE + """ + + DRIVER_OFF_TIME = 198 + """ + TMCM-300 ONLY! + + A special adjustment of the motor driver A3972. + Low values may cause more mechanical vibrations, while the higher ones + lead to acoustic noise of the drivers. The default value of 20 is a good + compromise for most applications. See the Allegro A3972 datasheet for details. + + Range: 0..31 + Access: RWE + """ + + FAST_DECAY_TIME = 200 + """ + A special adjustment of the motor driver A3972 (TMCM-300 only), with less + influence than the driver off time (no. 198) in most cases. Low values generally + reduce driver noise. See the Allegro A3972 datasheet for details. + + Range: 0..15 + Access: RWE + """ + + MIXED_DECAY_THRESHOLD = 203 + """ + If the actual velocity is above this threshold, mixed decay will be used + (all modules except the TMCM-300). Since V3.13, this can also be set to –1 which + turns on mixed decay permanently also in the rising part of the microstep wave. + This can be used to fix microstep errors. + + Range: 0..2048/-1 + Access: RWE + """ + + FREEWHEELING = 204 + """ + TMCM-301 / 303 / 310 / 11x and 61x only + + Time after which the power to the motor will be cut when its velocity has + reached zero. + + 0: never + + Range: 0..65535 + Access: RWE + """ + + STALL_DETECTION_THRESHOLD = 205 + """ + Stall detection threshold. Only usable on modules equipped with TMC246 or TMC249 + motor drivers. Set it to 0 for no stall detection or to a value between 1 + (low threshold) and 7 (high threshold). The motor will be stopped if the load value + exceeds the stall detection threshold. Switch off mixed decay to get usable results. + + Range: 0..7 + Access: RWE + """ + + ACTUAL_LOAD_VALUE = 206 + """ + Readout of the actual load value used for stall detection. Only usable on modules + equipped with TMC246 or TMC249 motor drivers. On other modules this value is undefined. + + Range: 0..7 + Access: R + """ + + DRIVER_ERROR_FLAGS = 208 + """ + TMC236 Error Flags + + Access: R + """ + + ENCODER_POSITION = 209 + """ + The value of an encoder register of a TMCM-323 module connected to a TMCM-30x module + can be read or written. Please see the TMCM-323 manual for details. + + Access: RW + """ + + ENCODER_PRESCALER = 210 + """ + TMCM-323, TMCM-611, TMCM-101 & TMCM-102 + + Pre-scaler for an encoder connected to a TMCM-323 module. + Please see the TMCM-323 manual for details. This value can not be read back! + Please see the manuals of specific module for the meaning of these parameter. + + Access: W + """ + + FULLSTEP_THRESHOLD = 211 + """ + When exceeding this speed the driver will switch to real full step mode. + To disable this feature set this parameter to zero or to a value greater than 2047. + Setting a full step threshold allows higher motor torque of the motor at higher velocity. + When experimenting with this in a given application, try to reduce the motor current in + order to be able to reach a higher motor velocity! + + Range: 0..2048 + Access: RWE + """ + + MAXIMUM_ENCODER_DEVIATION = 212 + """ + TMCM-101, TMCM-102 and TMCM-611 only. + + When the actual position (parameter 1) and the encoder position (parameter 209) + differ more than set here the motor will be stopped. This function is switched + off when the maximum deviation is set to zero. + + Range: 0..65535 + Access: RWE + """ + + GROUP_INDEX = 213 + """ + TMCM-610, TMCM-611, TMCM-612 and TMCM-34x only. + + All motors on one module that have the same group index will also get the same + commands when a ROL, ROR, MST, MVP or RFS is issued for one of these motors. + + Range: 0..255 + Access: RW + """ + + + + + + +class TMCM100_AxisParameters: + """ + The axis parameters on the TMCM-100 module and on the Monopack 2 + with TMCL differ from those on the other modules. There are no “advanced” + axis parameters on the TMCM-100 module. + """ + + TARGET_POSITION = 0 + ACTUAL_POSITION = 1 + TARGET_VELOCITY = 2 + ACTUAL_VELOCITY = 3 + MAX_POSITIONING_VELOCITY = 4 + MAX_ACCELERATION = 5 + CURRENT_AT_CONSTANT_ROTATION = 6 + CURRENT_AT_STANDBY = 7 + POSITION_REACHED_FLAG = 8 + REFERENCE_SWITCH_STATUS = 9 + RIGHT_STOP_SWITCH_STATUS = 10 + LEFT_STOP_SWITCH_STATUS = 11 + STOP_SWITCH_DISABLE = 12 + STEP_RATE_PRESCALER = 14 + BOW = 15 + MICROSTEP_RESOLUTION = 16 + MICROSTEP_WAVEFORM = 17 + STEP_MODE = 18 + STEP_PULSE_LENGTH = 19 + PHASES = 20 + CURRENT_AT_ACCELERATION = 21 + REFERENCE_SEARCH_MODE = 22 + REFERENCE_SEARCH_VELOCITY = 23 + STOP_SWITCH_DECCELERATION = 24 + ENCODER_POSITION = 25 + ENCODER_CONFIGURATION = 26 + ENCODER_PREDIVIDER = 27 + ENCODER_MULTIPLIER = 28 + MAXIMUM_DEVIATION = 29 + DEVIATION_ACTION = 30 + CORRECTION_DELAY = 31 + CORRECTION_RETRIES = 32 + CORRECTION_TOLERANCE = 33 + CORRECTION_VELOCITY = 34 + + + + + +class AxisParameterInterface(object): + """ + Utility interface to Axis parameters for individual + module motor/axes. + """ + def __init__ ( self, motor ): + """ + + :param motor: + :type motor: Motor + """ + self.motor = motor + + + def get ( self, param ): + reply = self.motor.send(Command.GAP, param, self.motor.motor_id, 0) + return reply.value + + + def set ( self, param, value ): + reply = self.motor.send(Command.SAP, param, self.motor.motor_id, value) + return reply.status + + + @property + def target_position ( self ): + return self.get(0) + + + @target_position.setter + def target_position ( self, value ): + self.set(0, value) + + + @property + def actual_position ( self ): + return self.get(1) + + + @actual_position.setter + def actual_position ( self, value ): + self.set(1, value) + + + @property + def target_speed ( self ): + return self.get(2) + + + @target_speed.setter + def target_speed ( self, value ): + self.set(2, value) + + + @property + def actual_speed ( self ): + return self.get(3) + + + @property + def max_positioning_speed ( self ): + return self.get(4) + + + @max_positioning_speed.setter + def max_positioning_speed ( self, value ): + self.set(4, value) + + + @property + def max_accelleration ( self ): + return self.get(5) + + + @max_accelleration.setter + def max_accelleration ( self, value ): + self.set(5, value) + + + @property + def max_current ( self ): + return self.get(6) + + + @max_current.setter + def max_current ( self, value ): + self.set(6, value) + + + @property + def standby_current ( self ): + return self.get(7) + + + @standby_current.setter + def standby_current ( self, value ): + self.set(7, value) + + + @property + def target_position_reached ( self ): + return self.get(8) + + + @property + def ref_switch_status ( self ): + return self.get(9) + + + @property + def right_limit_status ( self ): + return self.get(10) + + + @property + def left_limit_status ( self ): + return self.get(11) + + + @property + def right_limit_switch_disabled ( self ): + return True if self.get(12) == 1 else False + + + @right_limit_switch_disabled.setter + def right_limit_switch_disabled ( self, value ): + self.set(12, 1 if value else 0) + + + @property + def left_limit_switch_disabled ( self ): + return True if self.get(13) == 1 else False + + + @left_limit_switch_disabled.setter + def left_limit_switch_disabled ( self, value ): + self.set(13, 1 if value else 0) diff --git a/TMCL/tmcl_commands.py b/TMCL/tmcl_commands.py new file mode 100644 index 0000000..4472232 --- /dev/null +++ b/TMCL/tmcl_commands.py @@ -0,0 +1,81 @@ + + +commands = [ + ('ROR',1), # Rotate right + ('ROL',2), # Rotate left + ('MST',3), # Motor stop + ('MVP',4), # Move to position + ('SAP',5), # Set axis parameter + ('GAP',6), # Get axis parameter + ('STAP',7), # Store axis parameter into EEPROM + ('RSAP',8), # Restors axis parameter from EEPROM + ('SGP',9), # Set global parameter + ('GGP',10), # Get global parameter + ('STGP',11), # Store global parameter into EEPROM + ('RSGP',12), # Restore global parameter from EEPROM + ('RFS',13), # Reference search + ('SIO',14), # Set output + ('GIO',15), # Get input + ('SCO',30), # Store coordinate + ('CCO',32), # Capture coordinate + ('GCO',31), # Get coordinate + ('SAC',29), # Access to external SPI device + ('JA',22), # Jump always + ('JC',21), # Jump conditional + ('COMP',20), # Compare accumulator with constant value + ('CLE',36), # Clear error flags + ('CSUB',23), # Call subroutine + ('RSUB',24), # Return from subroutine + ('WAIT',27), # Wait for a specified event + ('STOP',28), # End of a TMCL program + ('CALC',19), # Calculate using the accumulator and a constant value + ('CALCX',33), # Calculate using the accumulator and the X register + ('AAP',34), # Copy accumulator to an axis parameter + ('AGP',35), # Copy accumulator to a global parameter + + ('UF0', 64), # User function 1 + ('UF1', 65), # User function 2 + ('UF2', 66), # User function 3 + ('UF3', 67), # User function 4 + ('UF4', 68), # User function 5 + ('UF5', 69), # User function 6 + ('UF6', 70), # User function 7 + ('UF7', 71), # User function 8 + + ('STOP_APPLICATION',128), + ('RUN_APPLICATION',129), + ('STEP_APPLICATION',130), + ('RESET_APPLICATION',131), + ('START_DOWNLOAD_MODE',132), + ('QUIT_DOWNLOAD_MODE',133), + ('READ_TMCL_MEMORY',134), + ('GET_APPLICATION_STATUS',135), + ('GET_FIRMWARE_VERSION',136), + ('RESTORE_FACTORY_SETTINGS',137), +] + + +class Command (object): pass +for cmd in commands: + setattr(Command, cmd[0], cmd[1]) + + + + +def get_label (cmd): + """ + Get the human-readable label associated with a TMCL command + + :param cmd: The TMCL command to get a label for + :type cmd: int + + :return: Human-readable label for TMCL command + :rtype: str + """ + cmds = filter(lambda x: x[1] == cmd, commands) + if cmds: + return cmds[0] + else: + return None + + diff --git a/TMCL/tmcl_global_parameters.py b/TMCL/tmcl_global_parameters.py new file mode 100644 index 0000000..8a9ecca --- /dev/null +++ b/TMCL/tmcl_global_parameters.py @@ -0,0 +1,240 @@ +# -*- coding: UTF-8 -*- + +class GlobalParams (object): + """ + The global parameters apply for all types of TMCM modules. They are + grouped into 3 banks: bank 0 (global configuration of the module), + bank 1 (user C variables) and bank 2 (user TMCL variables). + + Access values: + R = readable (GAP) + W = writable (SAP) + E = automatically restored from EEPROM after reset or power-on. + + """ + + + ####################################################################### + ## BANK 0 + ## ------ + ## + ## The following parameters with the numbers from 64 on configure things + ## like the serial address of the module RS232 / RS485 baud rate or CAN + ## bit rate. Change these parameters to meet your needs. The best and + ## easiest way to do this is to use the appropriate functions of the TMCL + ## IDE. The parameters with numbers between 64 and 128 are stored in + ## EEPROM only, so that an SGP command on such a parameter will always + ## store it permanently (no extra STGP command needed). + ## + ## Take care when changing these parameters, and use the appropriate + ## functions of the TMCL IDE to do it in an interactive way! + ####################################################################### + + + + EEPROM_RESET = (0,64) + """ + Setting this parameter to a different value as $E4 will cause + re-initialisation of the axis and global parameters (to factory + defaults) after the next power up. This is useful in case of + miss-configuration. + + Range: 0..255 + Access: RWE + """ + + BAUD_RATE = (0,65) + """ + Set rs232 and rs485 baud rate + + 0: 9600 baud (default) + 1: 14400 baud + 2: 19200 baud + 3: 28800 baud + 4: 38400 baud + 5: 57600 baud + 6: 76800 baud Caution: Not supported by Windows! + 7: 115200 baud Caution: 115200 does not work with most host PCs, + as the baud rate error on the modules is too high + with this baud rate (-3.5% baud rate error). + + Range: 0..7 + Access: RWE + """ + + SERIAL_ADDRESS = (0,66) + """ + The module (target) address for RS-232 and RS-485. + + Range: 0..255 + Access: RWE + """ + + ASCII_MODE = (0,67) + """ + Configure the TMCL ASCII interface: + Bit 0: 0 – start up in binary (normal) mode + 1 – start up in ASCII mode + Bits 4 and 5: + 00 – Echo back each character + 01 – Echo back complete command + 10 – Do not send echo, only send command reply + + Access: RWE + """ + + CAN_BIT_RATE = (0,69) + """ + CAN bitrate + + 1: 10kBit/s + 2: 20kBit/s + 3: 50kBit/s + 4: 100kBit/s + 5: 125kBit/s + 6: 250kBit/s (default) + 7: 500kBit/s + 8: 1000kBit/s (not supported by TMCM-30x/110/111/112) + + Range: 0..7 + Access: RWE + """ + + CAN_REPLY_ID = (0,70) + """ + The CAN ID for replies from the board (default: 2) + + Range: 0..7ff + Access: RWE + """ + + CAN_ID = (0,71) + """ + The module (target) address for CAN (default: 1) + + Range: 0..7ff + Access: RWE + """ + + EEPROM_LOCK = (0,73) + """ + Write: 1234 to lock the EEPROM, 4321 to unlock it. + Read: 1=EEPROM locked, 0=EEPROM unlocked. + + Range: write: 1234/4321 read: 0/1 + Access: RWE + """ + + ENCODER_INTERFACE = (0,74) + """ + Determines if a TMCM-323 is connected to the external SPI interface + and to which SPI_SEL line it is connected. Please see TMCM-323 manual + for details! + + 0: No TMCM-323 connected + 1: Connected to SPI_SEL0 + 2: Connected to SPI_SEL1 + 3: Connected to SPI_SEL2 + + Access: RWE + """ + + TELEGRAM_PAUSE_TIME = (0,75) + """ + Pause time before the reply via RS232 or RS485 will be sent. For RS232 + set to 0, for RS485 it is often necessary to set it to 15 (for RS485 + adapters controlled by the RTS pin). + + For CAN or IIC interface this parameter has no effect! + + Range: 0..255 + Access: RWE + """ + + SERIAL_HOST_ADDRESS = (0,76) + """ + Host address used in the reply telegrams sent back via RS232 or RS485. + + Range: 0..255 + Access: RWE + """ + + AUTO_START_MODE = (0,77) + """ + Set the auto-start mode + + 0: Do not start TMCL application after power-up (default). + 1: Start TMCL application automatically after power-up + + Range: 0/1 + Access: RWE + """ + + SHUTDOWN_PIN_MODE = (0,80) + """ + Select the functionality of the SHUTDOWN pin (not with TMCM-300). + + 0: no function + 1: high active + 2: low active + + Range: 0..2 + Access: RWE + """ + + CODE_PROTECTION = (0,81) + """ + Protect a TMCL program against disassembling or overwriting. + + 0: no protection + 1: protection against disassembling + 2: protection against overwriting + 3: protection against disassembling and overwriting + + Note: When a user tries to switch off the protection against + disassembling, the program will be erased first! So, when + changing this value from 1 or 3 to 0 or 2, the TMCL program + will be erased. + + Range: 0..3 + Access: RWE + """ + + APPLICATION_STATUS = (0,128) + """ + Current status of the TMCL application running on the module + + 0: stop + 1: run + 2: step + 3: reset + + Range: 0..3 + Access: R + """ + + DOWNLOAD_MODE = (0,129) + """ + Check if module is in download mode or not + + 0: normal mode + 1: download mode + + Range: 0/1 + Access: R + """ + + PROGRAM_COUNTER = (0,130) + """ + The index of the currently executed TMCL instruction. + + Access: R + """ + + TICK_TIMER = (0,132) + """ + A 32 bit counter that gets incremented by one every millisecond. + It can also be reset to any start value. + + Access: RW + """ diff --git a/dist/TMCL-1.0.tar.gz b/dist/TMCL-1.0.tar.gz deleted file mode 100644 index 855fb53..0000000 Binary files a/dist/TMCL-1.0.tar.gz and /dev/null differ diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..7f5c036 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,17 @@ +API Overview +============ + +This is an overview of the main API interfaces you will use when +working with the python-tmcl library. More in-depth and detailed +information can be found in the source code itself, which is +commented to within an inch of its life. + + + +.. toctree:: + :maxdepth: 1 + :glob: + :caption: Classes: + + api/* + diff --git a/docs/api/bus.rst b/docs/api/bus.rst new file mode 100644 index 0000000..ef1f8ac --- /dev/null +++ b/docs/api/bus.rst @@ -0,0 +1,84 @@ +Bus +### + +Top-level interface for communicating with TMCM modules + + +**Constructor Summary** + ++-------------------------------+------------------------------+ +| Constructor | Description | ++===============================+==============================+ +| Bus_ `( serial )` | Create a new Bus instance | ++-------------------------------+------------------------------+ + + +**Property Summary** + ++----------------------------------+--------------------------------------+ +| Property + Description | ++==================================+======================================+ +| serial_ : Serial | Serial port for communication | ++----------------------------------+--------------------------------------+ +| CAN_ : bool | True if serial is a CAN bus | ++----------------------------------+--------------------------------------+ + + + +**Method Summary** + ++-----------------------------------------------------------+---------------------------------------------------+ +| Method | Description | ++===========================================================+===================================================+ +| send_ `( cmd, address, cmd, type=0, motbank=0, value=0 )` | Send message to the bus | ++-----------------------------------------------------------+---------------------------------------------------+ +| get_module_ `( address=1 )` | Get a Module instance at specified address | ++-----------------------------------------------------------+---------------------------------------------------+ +| get_motor_ `( address=1, axis=0 )` | Get a Motor instance for axis and address | ++-----------------------------------------------------------+---------------------------------------------------+ + + + +------------------------------------------------------------ + + +Constructor +=========== + +.. _Bus: +.. function:: Bus ( serial ) + + :param serial: Serial port to use for communication + :type serial: Serial + + + +Properties +========== + +serial +------ +.. autoattribute:: TMCL.bus.Bus.serial + +CAN +--- +.. autoattribute:: TMCL.bus.Bus.CAN + + + +Methods +======= + +send +---- +.. automethod:: TMCL.bus.Bus.send + +get_module +---------- +.. automethod:: TMCL.bus.Bus.get_module + +get_motor +--------- +.. automethod:: TMCL.bus.Bus.get_motor + + diff --git a/docs/api/instruction.rst b/docs/api/instruction.rst new file mode 100644 index 0000000..5fcd2a5 --- /dev/null +++ b/docs/api/instruction.rst @@ -0,0 +1,69 @@ +Instruction +########### + + +**Constructor Summary** + ++-----------------------------------------------------+------------------------------+ +| Constructor | Description | ++=====================================================+==============================+ +| Instruction_ `( cmd, type=0, motbank=0, value=0 )` | Create a new Module instance | ++-----------------------------------------------------+------------------------------+ + + +**Property Summary** + ++----------------------------------+--------------------------------------+ +| Property + Description | ++==================================+======================================+ +| cmd_ : `int` | TMCL command field | ++----------------------------------+--------------------------------------+ +| type_ : `int` | TMCL type field | ++----------------------------------+--------------------------------------+ +| motbank_ : `int` | TMCL motor/bank field | ++----------------------------------+--------------------------------------+ +| value_ : `int` | TMCL value field | ++----------------------------------+--------------------------------------+ + + +------------------------------------------------------------ + + +Constructor +=========== + +.. _Instruction: +.. function:: Instruction(cmd, type=0, motbank=0, value=0) + + :param cmd: TMCL command field + :type cmd: int [0..255] + + :param type: TMCL type field. Defaults to 0 + :type type: int [0..255] + + :param type: TMCL motor/bank field. Defaults to 0 + :type type: int [0..255] + + :param type: TMCL value field. Defaults to 0 + :type type: int [-2^31..2^31] + + +Properties +========== + +cmd +--- +.. autoattribute:: TMCL.instruction.Instruction.cmd + +type +---- +.. autoattribute:: TMCL.instruction.Instruction.type + +motbank +------- +.. autoattribute:: TMCL.instruction.Instruction.motbank + +value +----- +.. autoattribute:: TMCL.instruction.Instruction.value + diff --git a/docs/api/module.rst b/docs/api/module.rst new file mode 100644 index 0000000..b3d603f --- /dev/null +++ b/docs/api/module.rst @@ -0,0 +1,137 @@ +Module +###### + +An interface to a single TMCM stepper module with one or more axes. + + + +**Example:** + +.. code-block:: python + + from TMCM import Module, GlobalParams + module = Module( bus, address=4 ) + module.set_param (GlobalParams.SHUTDOWN_PIN_MODE, 2) + motor = module.get_motor() + + + + + +**Constructor Summary** + ++-------------------------------+------------------------------+ +| Constructor | Description | ++===============================+==============================+ +| Module_ `( bus, address=1 )` | Create a new Module instance | ++-------------------------------+------------------------------+ + + +**Property Summary** + ++----------------------------------+--------------------------------------+ +| Property + Description | ++==================================+======================================+ +| bus_ : Bus | The Bus used for communication | ++----------------------------------+--------------------------------------+ +| address_ : Bus | Serial address of module on the bus | ++----------------------------------+--------------------------------------+ + + +**Method Summary** + ++-----------------------------------------------------+---------------------------------------------------+ +| Method | Description | ++=====================================================+===================================================+ +| send_ `( cmd, type=0, motbank=0, value=0 )` | Send a TMCL instruction to this module | ++-----------------------------------------------------+---------------------------------------------------+ +| get_motor_ `( axis=0 )` | Get a Motor instance for an axis on this module | ++-----------------------------------------------------+---------------------------------------------------+ +| get_axis_ `( axis=0 )` | Get a Motor instance for an axis on this module | ++-----------------------------------------------------+---------------------------------------------------+ +| get_param_ `( param, bank=None )` | Get a global parameter from the module | ++-----------------------------------------------------+---------------------------------------------------+ +| set_param_ `( param, value, bank=None )` | Set a global parameter on the module | ++-----------------------------------------------------+---------------------------------------------------+ +| get_axis_param_ `( param, axis=0 )` | Get an axis parameter from the module | ++-----------------------------------------------------+---------------------------------------------------+ +| set_axis_param_ `( param, value, axis=0 )` | Set an axis parameter on the module | ++-----------------------------------------------------+---------------------------------------------------+ +| write_firmware_ `( param, value, axis=0 )` | Write a series of instructions to module firmware | ++-----------------------------------------------------+---------------------------------------------------+ +| firmware_writer_ `( )` | Get a ContextManager for writing firmware | ++-----------------------------------------------------+---------------------------------------------------+ + + +------------------------------------------------------------ + + +Constructor +=========== + +.. _Module: +.. function:: Module(bus, address=1) + + :param bus: The Bus instance to use for communication + :type bus: Bus + :param address: Module address on bus. Defaults to 1 + :type address: int + + + + +Properties +========== + +bus +--- +.. autoattribute:: TMCL.module.Module.bus + +address +------- +.. autoattribute:: TMCL.module.Module.address + + + + +Methods +======= +send +---- +.. automethod:: TMCL.module.Module.send + +get_motor +--------- +.. method:: Module.get_motor(axis=0) + + Alias of get_axis_ + +get_axis +-------- +.. automethod:: TMCL.module.Module.get_axis + +get_param +--------- +.. automethod:: TMCL.module.Module.get_param + +set_param +--------- +.. automethod:: TMCL.module.Module.set_param + +get_axis_param +-------------- +.. automethod:: TMCL.module.Module.get_axis_param + +set_axis_param +-------------- +.. automethod:: TMCL.module.Module.set_axis_param + +write_firmware +-------------- +.. automethod:: TMCL.module.Module.write_firmware + +firmware_writer +--------------- +.. automethod:: TMCL.module.Module.firmware_writer + + diff --git a/docs/api/motor.rst b/docs/api/motor.rst new file mode 100644 index 0000000..7f326ca --- /dev/null +++ b/docs/api/motor.rst @@ -0,0 +1,204 @@ +Motor +##### + + +**Constructor Summary** + ++-------------------------------+------------------------------+ +| Constructor | Description | ++===============================+==============================+ +| Motor_ `( module, axis=0 )` | Create a new Motor instance | ++-------------------------------+------------------------------+ + + +**Property Summary** + ++--------------------------------------+----------+----------------------------------------------------------+ +| Property | Access | Description | ++======================================+==========+==========================================================+ +| module_ : `Module` | | The module this motor belongs to | ++--------------------------------------+----------+----------------------------------------------------------+ +| axis_ : `int` | | Axis id of motor on parent module | ++--------------------------------------+----------+----------------------------------------------------------+ +| target_position_ : `int` | | Motor target position axis parameter | ++--------------------------------------+----------+----------------------------------------------------------+ +| actual_position_ : `int` |`readonly`| Current absolute position of motor | ++--------------------------------------+----------+----------------------------------------------------------+ +| target_speed_ : `int` | | Motor target speed | ++--------------------------------------+----------+----------------------------------------------------------+ +| actual_speed_ : `int` |`readonly`| Current speed of motor | ++--------------------------------------+----------+----------------------------------------------------------+ +| max_positioning_speed_ : `int` | | Max speed | ++--------------------------------------+----------+----------------------------------------------------------+ +| max_accelleration_ : `int` | | Max accelleration | ++--------------------------------------+----------+----------------------------------------------------------+ +| max_current_ : `int` | | Max allowed current draw | ++--------------------------------------+----------+----------------------------------------------------------+ +| standby_current_ : `int` | | Standby current | ++--------------------------------------+----------+----------------------------------------------------------+ +| target_position_reached_ : `int` |`readonly`| True if at target position | ++--------------------------------------+----------+----------------------------------------------------------+ +| ref_switch_status_ : `int` |`readonly`| The logical state of the reference (left) switch. | ++--------------------------------------+----------+----------------------------------------------------------+ +| right_limit_status_ : `int` |`readonly`| The logical state of the (right) limit switch. | ++--------------------------------------+----------+----------------------------------------------------------+ +| left_limit_status_ : `int` |`readonly`| The logical state of the left limit switch | ++--------------------------------------+----------+----------------------------------------------------------+ +| right_limit_switch_disabled_ : `int` | | If set, deactivates the stop function of the right switch| ++--------------------------------------+----------+----------------------------------------------------------+ +| left_limit_switch_disabled_ : `int` | | If set, deactivates the stop function of the left switch | ++--------------------------------------+----------+----------------------------------------------------------+ + + +**Method Summary** + ++-----------------------------------------------------+---------------------------------------------------+ +| Method | Description | ++=====================================================+===================================================+ +| send_ `( cmd, type=0, value=0 )` | Send a TMCL instruction to this motor | ++-----------------------------------------------------+---------------------------------------------------+ +| get_axis_param_ `( param )` | Get an axis parameter from the module | ++-----------------------------------------------------+---------------------------------------------------+ +| set_axis_param_ `( param, value )` | Set an axis parameter on the module | ++-----------------------------------------------------+---------------------------------------------------+ +| stop_ `( )` | Stop motor movement | ++-----------------------------------------------------+---------------------------------------------------+ +| rotate_left_ `( velocity )` | Make the motor rotate left | ++-----------------------------------------------------+---------------------------------------------------+ +| rotate_right_ `( velocity )` | Make the motor rotate right | ++-----------------------------------------------------+---------------------------------------------------+ +| move_absolute_ `( position )` | Move motor to absolute position | ++-----------------------------------------------------+---------------------------------------------------+ +| move_relative_ `( offset )` | Move motor by a relative amount | ++-----------------------------------------------------+---------------------------------------------------+ +| reference_search_ `( type )` | Initiate a reference search | ++-----------------------------------------------------+---------------------------------------------------+ + + + +------------------------------------------------------------ + + +Constructor +=========== + +.. _Motor: +.. function:: Motor(module, axis=0) + + :param module: The Module instance this motor belongs to + :type bus: Module + :param axis: Motor axis ID on the module + :type axis: int + + + + +Properties +========== + +module +------ +.. autoattribute:: TMCL.motor.Motor.module + +axis +---- +.. autoattribute:: TMCL.motor.Motor.axis + +target_position +--------------- +.. autoattribute:: TMCL.motor.Motor.target_position + +actual_position +--------------- +.. autoattribute:: TMCL.motor.Motor.actual_position + +target_speed +------------ +.. autoattribute:: TMCL.motor.Motor.target_speed + +actual_speed +------------ +.. autoattribute:: TMCL.motor.Motor.actual_speed + +max_positioning_speed +--------------------- +.. autoattribute:: TMCL.motor.Motor.max_positioning_speed + +max_accelleration +----------------- +.. autoattribute:: TMCL.motor.Motor.max_accelleration + +max_current +----------- +.. autoattribute:: TMCL.motor.Motor.max_current + +standby_current +--------------- +.. autoattribute:: TMCL.motor.Motor.standby_current + +target_position_reached +----------------------- +.. autoattribute:: TMCL.motor.Motor.target_position_reached + +ref_switch_status +----------------- +.. autoattribute:: TMCL.motor.Motor.ref_switch_status + +right_limit_status +------------------ +.. autoattribute:: TMCL.motor.Motor.right_limit_status + +left_limit_status +----------------- +.. autoattribute:: TMCL.motor.Motor.left_limit_status + +right_limit_switch_disabled +--------------------------- +.. autoattribute:: TMCL.motor.Motor.right_limit_switch_disabled + +left_limit_switch_disabled +-------------------------- +.. autoattribute:: TMCL.motor.Motor.left_limit_switch_disabled + + + + + +Methods +======= + +send +---- +.. automethod:: TMCL.motor.Motor.send + +get_axis_param +-------------- +.. automethod:: TMCL.motor.Motor.get_axis_param + +set_axis_param +-------------- +.. automethod:: TMCL.motor.Motor.set_axis_param + +stop +---- +.. automethod:: TMCL.motor.Motor.stop + +rotate_left +----------- +.. automethod:: TMCL.motor.Motor.rotate_left + +rotate_right +------------ +.. automethod:: TMCL.motor.Motor.rotate_right + +move_absolute +------------- +.. automethod:: TMCL.motor.Motor.move_absolute + +move_relative +------------- +.. automethod:: TMCL.motor.Motor.move_relative + +reference_search +---------------- +.. automethod:: TMCL.motor.Motor.reference_search diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..3321647 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- +# +# python-tmcl documentation build configuration file, created by +# sphinx-quickstart on Thu Jul 20 15:50:55 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + +import sphinx_rtd_theme + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.todo', + 'sphinx.ext.githubpages', + 'sphinxcontrib.napoleon'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'python-tmcl' +copyright = u'2017, Alan Pich ' +author = u'Alan Pich' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = u'2.0' +# The full version, including alpha/beta/rc tags. +release = u'2.0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "sphinx_rtd_theme" +html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# This is required for the alabaster theme +# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars +html_sidebars = { + '**': [ + 'about.html', + 'navigation.html', + 'relations.html', # needs 'show_related': True theme option to display + 'searchbox.html', + 'donate.html', + ] +} + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'python-tmcldoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'python-tmcl.tex', u'python-tmcl Documentation', + u'Alan Pich', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'python-tmcl', u'python-tmcl Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'python-tmcl', u'python-tmcl Documentation', + author, 'python-tmcl', 'One line description of project.', + 'Miscellaneous'), +] + + + diff --git a/docs/contributing.rst b/docs/contributing.rst new file mode 100644 index 0000000..5a1988f --- /dev/null +++ b/docs/contributing.rst @@ -0,0 +1,2 @@ +Contributing +============ diff --git a/docs/getting-started.rst b/docs/getting-started.rst new file mode 100644 index 0000000..2babab6 --- /dev/null +++ b/docs/getting-started.rst @@ -0,0 +1,85 @@ +Getting Started +=============== + + + +Connecting to the bus +--------------------- +Connect your TMCM module to your computer using an RS485-to-serial converter. +You can connect multiple TMCM modules to the same RS485 bus, but you must ensure +that they are all set to use the same serial baud rate, and different serial port +addresses. This is easiest done using the TMCL-IDE (provided free by Trinamic) +and the on-board micro-usb connection. + + +Open a serial port to your RS485 connection using the pySerial library and +use it to create a Bus_ instance. This example assumes that all connected +TMCM modules are set to use a serial baud rate of 9600. :: + + from serial import Serial + import TMCL + + serial_port = Serial("/dev/tty.ACM0", 9600) + bus = TMCL.Bus( serial_port ) + + + + +Adressing a TMCM module +----------------------- +Once you have a Bus_ instance, you can get a Module_ instance from it by passing +the serial-address of the module to the Bus's `get_module` method. This example +assumes that the TMCM module has been pre-configured to use a `serial-address` +of 5. :: + + from serial import Serial + import TMCL + + serial_port = Serial("/dev/tty.ACM0") + bus = TMCL.Bus( serial_port ) + + module = bus.get_module(1) + + + +Moving a motor +----------------------- +Each TMCM module hosts one or more motors. They are referred to by their `axis` on +the module. You can get a Motor_ interface from a Module_ instance by calling its +`get_motor` method. This will return a Motor_ instance that will allow you to read +and write `axis parameters` and move the motor. :: + + from serial import Serial + import TMCL + + serial_port = Serial("/dev/tty.ACM0") + bus = TMCL.Bus( serial_port ) + + module = bus.get_module(1) + + motor = module.get_motor() + + + +Once you have a Motor_ instance you can start it moving by issuing commands to the +instance. This example will make the motor rotate left at a speed of `1234` for 5 +seconds before stopping again. :: + + from serial import Serial + import TMCL + + serial_port = Serial("/dev/tty.ACM0") + bus = TMCL.Bus( serial_port ) + + module = bus.get_module(1) + + motor = module.get_motor() + + motor.rotate_left(1234) + sleep(5) + motor.stop() + + +.. _Bus: ./api/bus.html +.. _Module: ./api/module.html +.. _Motor: ./api/motor.html diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..61d3fd7 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,36 @@ +python-tmcl +=========== + +.. image:: https://img.shields.io/pypi/pyversions/tmcl.svg?style=flat-square +.. image:: https://img.shields.io/pypi/l/tmcl.svg?style=flat-square +.. image:: https://img.shields.io/pypi/v/tmcl.svg?style=flat-square + + +A python library for programming and communicating with Trinamic TMCM stepper motor modules +via their rs-485 interface. Supports both python 2.7.x and 3.x. + +.. note:: python-tmcl v2.0 is currenly in early-alpha stage and has not yet + been released to pypi. Use at your own risk (and `please` send any feedback + you have so we can improve the library ready for release). + + +Much of this documentation is gleened from Trinamic's own `TMCL Reference & Programming Manual`_ +which is an invaluable resource when working with TMCM modules. I highly recommend you read it +alongside this documentation. + + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + installation + getting-started + standalone-mode + api + contributing + license + + + + +.. _TMCL Reference & Programming Manual: http://www.mctechnology.nl/pdf/TMCL_reference_2015.pdf diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 0000000..dfe7c63 --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,33 @@ +Installation +============ + + +Requirements +------------ + +- pyserial_ + + + +Install with pip +---------------- + +.. code-block:: bash + + $> pip install tmcl + + + +Install without pip +------------------- + +.. code-block:: sh + + $> git clone https://github.com/NativeDesign/python-tmcl.git + $> cd python-tmcl + $> python setup.py install + + + + +.. _pyserial: https://pythonhosted.org/pyserial/ diff --git a/docs/license.rst b/docs/license.rst new file mode 100644 index 0000000..402ecfd --- /dev/null +++ b/docs/license.rst @@ -0,0 +1,22 @@ +Licence +======= + +Copyright © 2017 Alan Pich + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/docs/standalone-mode.rst b/docs/standalone-mode.rst new file mode 100644 index 0000000..9f7cc0d --- /dev/null +++ b/docs/standalone-mode.rst @@ -0,0 +1,15 @@ +Standalone Mode +=============== + +As well as supporting instructions via the serial bus ("`Direct Mode`"), TMCM modules +also allow writing sets of instructions into firmware on the modules themselves. This +is know as "`Standalone Mode`". Standalone Mode is useful for more automated behaviours, +as well as more safety-critical and real-time behaviours. + +.. toctree:: + :maxdepth: 2 + :glob: + + standalone/writing-firmware + standalone/controlling-firmware + standalone/assembley-files diff --git a/docs/standalone/assembley-files.rst b/docs/standalone/assembley-files.rst new file mode 100644 index 0000000..f175176 --- /dev/null +++ b/docs/standalone/assembley-files.rst @@ -0,0 +1,2 @@ +Assembley files +=============== diff --git a/docs/standalone/controlling-firmware.rst b/docs/standalone/controlling-firmware.rst new file mode 100644 index 0000000..685f118 --- /dev/null +++ b/docs/standalone/controlling-firmware.rst @@ -0,0 +1,2 @@ +Controlling firmware +==================== diff --git a/docs/standalone/writing-firmware.rst b/docs/standalone/writing-firmware.rst new file mode 100644 index 0000000..09b2768 --- /dev/null +++ b/docs/standalone/writing-firmware.rst @@ -0,0 +1,2 @@ +Writing firmware to the module +============================== diff --git a/examples/foo.py b/examples/foo.py new file mode 100644 index 0000000..0085c5c --- /dev/null +++ b/examples/foo.py @@ -0,0 +1,9 @@ + + +from TMCL.module import Module + + + +mod = Module() + +print(dir(mod)) diff --git a/requirements.dev.txt b/requirements.dev.txt new file mode 100644 index 0000000..280f279 --- /dev/null +++ b/requirements.dev.txt @@ -0,0 +1,3 @@ +wheel +setuptools +twine diff --git a/requirements.docs.txt b/requirements.docs.txt new file mode 100644 index 0000000..9bd2751 --- /dev/null +++ b/requirements.docs.txt @@ -0,0 +1,5 @@ +sphinx +sphinx-autodoc +sphinx-rtd-theme +sphinxcontrib-napolean + diff --git a/requirements.test.txt b/requirements.test.txt new file mode 100644 index 0000000..625bed6 --- /dev/null +++ b/requirements.test.txt @@ -0,0 +1,2 @@ +pytest +mock diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c59aabc --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +sphinx +sphinxcontrib-napoleon +pytest diff --git a/setup.cfg b/setup.cfg index b88034e..f48fdad 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,3 @@ [metadata] description-file = README.md +license_file = LICENSE diff --git a/setup.py b/setup.py index a5a3e18..629cd4d 100644 --- a/setup.py +++ b/setup.py @@ -1,22 +1,75 @@ from setuptools import setup +import codecs +import os -setup(name='TMCL', - version='1.2', + +dependencies = [ + 'pyserial' +] + +dev_dependencies = [ + 'pytest', + 'mock', + 'twine', + 'wheel', + 'setuptools' +] + +HERE = os.path.abspath(os.path.dirname(__file__)) + + +def read ( *parts ): + """ + Build an absolute path from *parts* and and return the contents of the + resulting file. Assume UTF-8 encoding. + """ + content = '' + with codecs.open(os.path.join(HERE, *parts), "rb", "utf-8") as f: + content = f.read() + + print(content) + return content + + +setup( + name='TMCL', + version='2.0-alpha.2', description='Talk to Trinamic Stepper Motors using TMCL over serial', + long_description=read("README.rst"), url='https://github.com/NativeDesign/python-tmcl', author='Alan Pich', author_email='alanp@native.com', license='MIT', - packages=['TMCL'], - install_requires=[ - 'pyserial' - ], - keywords = [ + keywords=[ 'tmcl', - 'trinamic', + 'trinamic', 'rs485', 'stepper', 'motor', 'tmcm' ], - zip_safe=False) + + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Natural Language :: English', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 3', + 'Topic :: Communications', + 'Topic :: Software Development :: Libraries', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], + + packages=['TMCL'], + + install_requires=dependencies, + + extras_require={ + 'dev': dev_dependencies + }, + + zip_safe=False +) diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/_test_Motor.py b/test/_test_Motor.py new file mode 100644 index 0000000..89b5a72 --- /dev/null +++ b/test/_test_Motor.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- + +import pytest + +import TMCL +from TMCL import Motor + + +EXPECTED_REPLY_VALUE = 4321 + + +class SuccessException (BaseException): pass + + + +class MockModule (object): + def send (self, cmd, type, motbank=0, value=0): + reply = TMCL.Reply() + reply.status = TMCL.Reply.Status.SUCCESS + reply.value = EXPECTED_REPLY_VALUE + return reply + + + + +class Test__Motor (object): + + + def setup_method (self): + self.motor = TMCL.Motor(MockModule()) + + + + def test__send_adds_correct_axis_to_command (self): + """ + Motor instances should append the correct axis id + to commands it sends to module + """ + AXIS = 3 + class MockModule (object): + def send (self, cmd, type, motbank=0, value=0): + assert motbank == AXIS + return TMCL.Reply() + + motor = Motor(MockModule(), axis=AXIS) + motor.send(TMCL.Command.GAP) + + + + def test__get_axis_param__checks_param_type(self): + """ + get_axis_param should throw an exception if the 'param' + parameter is not of type `int` + """ + with pytest.raises(ValueError, message="Should raise ValueError if param is not an integer"): + self.motor.get_axis_param("Not-an-integer") + + + def test__get_axis_param__returns_reply_value (self): + """ + get_axis_param should return the value property from the + Reply object returned from the bus + """ + assert self.motor.get_axis_param(2) == EXPECTED_REPLY_VALUE, \ + "Should return the Reply object's value property" + + + def test__get_axis_param__sends_correct_command (self): + """ + Check get_axis_param sends the correct command to module + """ + class MockModule: + def send (self, cmd, type, motbank=0, value=0): + assert \ + cmd == TMCL.Command.GAP and \ + type == TMCL.AxisParams.TARGET_POSITION and \ + motbank == 0 and \ + value == 0, \ + "Module.send not called with expected params" + return TMCL.Reply() + + + module = MockModule() + motor = TMCL.Motor(module) + motor.get_axis_param(TMCL.AxisParams.TARGET_POSITION) + + + + + def test__set_axis_param__checks_param_type(self): + """ + set_axis_param should throw an error if `param` parameter is + not of type int + """ + with pytest.raises(ValueError, message="Should raise ValueError if param is not of type int"): + self.motor.set_axis_param("Not an integer",123) + + + def test__set_axis_param__checks_value_type(self): + """ + set_axis_param should throw an error if `value` parameter is + not of type int + """ + with pytest.raises(ValueError, message="Should raise ValueError if param is not of type int"): + self.motor.set_axis_param(123, "Not an integer") + + + def test__set_axis_param__sends_correct_command (self): + """ + Check set_axis_param sends the correct command to module + """ + class MockModule: + def send (self, cmd, type, motbank=0, value=0): + assert \ + cmd == TMCL.Command.SAP and \ + type == TMCL.AxisParams.TARGET_POSITION and \ + motbank == 0 and \ + value == 123, \ + "Module.send not called with expected params" + return TMCL.Reply() + + module = MockModule() + motor = TMCL.Motor(module) + motor.set_axis_param(TMCL.AxisParams.TARGET_POSITION, 123) + + + + + def test__stop__sends_correct_command (self): + """ + stop() method should call module.send with the correct arguments + """ + + AXIS = 100 + + class MockModule: + def send (self, cmd, type, motbank=0, value=0): + if cmd == TMCL.Command.MST and \ + motbank == AXIS: + raise SuccessException() + + module = MockModule() + motor = TMCL.Motor(module, axis=AXIS) + with pytest.raises(SuccessException): + motor.stop() + + + + # def test__rotate_left__validates_velocity_type (self): + diff --git a/test/module/__init__.py b/test/module/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/module/test__constructor.py b/test/module/test__constructor.py new file mode 100644 index 0000000..6f24f4a --- /dev/null +++ b/test/module/test__constructor.py @@ -0,0 +1,37 @@ +from pytest import raises + +from TMCL.module import Module + + + +class Test__Module__Constructor (object): + + + def test__sets_bus_property (self): + + class Bus: pass + + bus = Bus() + module = Module(bus) + + assert module.bus == bus + + + def test__sets_address_property (self): + + class Bus: pass + bus = Bus() + + ADDRESS = 123 + + module = Module(bus, ADDRESS) + assert module.address == ADDRESS + + + def test__address_param_defaults_to_1 (self): + + class Bus: pass + bus = Bus() + + module = Module(bus) + assert module.address == 1 diff --git a/test/module/test__get_axis.py b/test/module/test__get_axis.py new file mode 100644 index 0000000..56e642f --- /dev/null +++ b/test/module/test__get_axis.py @@ -0,0 +1,31 @@ +from pytest import raises + +from TMCL import Motor +from TMCL.module import Module + + +class Test__Module__get_axis (object): + + + def test__returns_Motor_instance (self): + class Bus: pass + module = Module(Bus()) + assert isinstance(module.get_axis(), Motor) + + + def test__passes_axis_property_to_motor (self): + class Bus: pass + module = Module(Bus()) + + AXIS = 123 + + motor = module.get_axis(AXIS) + assert motor.axis == AXIS + + + def test__axis_param_defaults_to_zero (self): + class Bus: pass + module = Module(Bus()) + + motor = module.get_axis() + assert motor.axis == 0 diff --git a/test/module/test__get_param.py b/test/module/test__get_param.py new file mode 100644 index 0000000..bef5040 --- /dev/null +++ b/test/module/test__get_param.py @@ -0,0 +1,113 @@ +from pytest import raises, warns + +from TMCL import Reply +from TMCL.tmcl_commands import Command +from TMCL.module import Module + + +class SuccessException (BaseException): pass + + +class Bus (object): + def send (self, address, cmd, type=0, motbank=0, value=0): + return Reply() + + +class Test__Module__get_param (object): + + + def test__if_param_is_int_then_bank_must_be_set (self): + module = Module(Bus()) + + ## Should pass + module.get_param(123, bank=1) + module.get_param(123,2) + + ## Should raise + with raises(AssertionError): + module.get_param(123) + + + def test__if_param_is_tuple_it_must_be_valid (self): + module = Module(Bus()) + + ## Should pass + module.get_param((1,2)) + module.get_param((0,22)) + + ## Should raise + with raises(AssertionError): + module.get_param( (1,2,3) ) + + with raises(AssertionError): + module.get_param( (1, 'not an int') ) + + with raises(AssertionError): + module.get_param( () ) + + + def test__if_param_is_tuple_setting_bank_will_warn (self): + module = Module(Bus()) + + ## Should pass + with warns(None) as warnings: + module.get_param( (0, 123) ) + module.get_param( 123, 0 ) + assert len(warnings) == 0 + + with warns(UserWarning) as warnings: + module.get_param( (0, 123), 123) + module.get_param( (0, 123), bank=1) + assert len(warnings) == 2 + + + + def test__bank_arg_must_be_0_1_2 (self): + module = Module(Bus()) + + with raises(AssertionError): + module.get_param(123, 10) + + with raises(AssertionError): + module.get_param(123, 'not an integer') + + + + def test__sends_correct_instruction_to_bus (self): + + ADDRESS = 4 + CMD = Command.GGP + PARAM = 22 + BANK = 2 + VALUE = 0 + + class Bus: + def send (self, address, instruction): + assert address == ADDRESS + assert instruction.cmd == CMD + assert instruction.type == PARAM + assert instruction.motbank == BANK + assert instruction.value == 0 + raise SuccessException() + + module = Module(Bus(), address=ADDRESS) + + with raises(SuccessException): + module.get_param((BANK, PARAM)) + + with raises(SuccessException): + module.get_param( PARAM, BANK ) + + + + def test__returns_reply_value (self): + VALUE = 142 + + class Bus: + def send (self, address, instruction): + reply = Reply() + reply.value = VALUE + return reply + + module = Module(Bus()) + assert module.get_param(123,0) == VALUE diff --git a/test/module/test__send.py b/test/module/test__send.py new file mode 100644 index 0000000..9363bec --- /dev/null +++ b/test/module/test__send.py @@ -0,0 +1,123 @@ +from pytest import raises + +from TMCL import Reply +from TMCL.module import Module +from TMCL.instruction import Instruction + + +class SuccessException (BaseException): pass + + +class Bus: + def send(self, address, instruction): + return Reply() + + +class Test__Module__send (object): + + def test__cmd_param_must_be_an_int (self): + module = Module(Bus()) + + with raises(AssertionError): + module.send('not an integer') + + with raises(AssertionError): + module.send([123,456,789]) + + with raises(AssertionError): + module.send(Bus()) + + + def test__type_param_must_be_an_int (self): + module = Module(Bus()) + + with raises(AssertionError): + module.send(1, type='not an integer') + + with raises(AssertionError): + module.send(1, type=[123,456,789]) + + with raises(AssertionError): + module.send(1, type=Bus()) + + + def test__motbank_param_must_be_an_int ( self ): + module = Module(Bus()) + + with raises(AssertionError): + module.send(1, motbank='not an integer') + + with raises(AssertionError): + module.send(1, motbank=[123, 456, 789]) + + with raises(AssertionError): + module.send(1, motbank=Bus()) + + + def test__value_param_must_be_an_int ( self ): + module = Module(Bus()) + + with raises(AssertionError): + module.send(1, value='not an integer') + + with raises(AssertionError): + module.send(1, value=[123, 456, 789]) + + with raises(AssertionError): + module.send(1, value=Bus()) + + + def test__calls_bus_send_with_correct_instruction (self): + + CMD = 111 + TYPE = 222 + MOTBANK = 3 + VALUE = 12345 + + class Bus (object): + def send(self, address, instruction): + assert isinstance(instruction, Instruction) + if instruction.cmd == CMD and \ + instruction.type == TYPE and \ + instruction.motbank == MOTBANK and \ + instruction.value == VALUE: + raise SuccessException() + + + + module = Module(Bus()) + with raises(SuccessException): + module.send(CMD, TYPE, MOTBANK, VALUE) + + + def test__type_param_defaults_to_zero (self): + class Bus: + def send(self, address, instruction): + if instruction.type == 0: + raise SuccessException() + + module = Module(Bus()) + with raises(SuccessException): + module.send(123) + + + def test__motbank_param_defaults_to_zero ( self ): + class Bus: + def send ( self, address, instruction ): + if instruction.motbank == 0: + raise SuccessException() + + module = Module(Bus()) + with raises(SuccessException): + module.send(123) + + + def test__value_param_defaults_to_zero ( self ): + class Bus: + def send ( self, address, instruction ): + if instruction.value == 0: + raise SuccessException() + + module = Module(Bus()) + with raises(SuccessException): + module.send(123) diff --git a/test/motor/test__rotate_left.py b/test/motor/test__rotate_left.py new file mode 100644 index 0000000..5021a72 --- /dev/null +++ b/test/motor/test__rotate_left.py @@ -0,0 +1,64 @@ + +from pytest import raises + +from TMCL import Reply, Motor +from TMCL import Command + + +class BasicMockModule(object): + def send ( self, *args ): + return Reply() + + + + +class Test__rotate_left (object): + + + def test__velocity_must_be_an_int (self): + """ + Velocity must be validated as an integer + """ + motor = Motor(BasicMockModule()) + with raises(AssertionError): + motor.rotate_left("not an integer") + + + + def test__velocity_must_be_within_bounds (self): + """ + Velocity param must be within range 0 <= velocity <= self._max_velocity + """ + motor = Motor(BasicMockModule()) + motor._max_velocity = 100 + + with raises(AssertionError): + motor.rotate_left(-123) + + with raises(AssertionError): + motor.rotate_left(101) + + motor.rotate_left(50) + + + + def test__calls_send_with_correct_args (self): + """ + Must call motor.send() with the correct arguments + :return: + """ + + VELOCITY = 123 + + motor = Motor(BasicMockModule()) + + def mock_send (cmd, type=0, value=0): + assert cmd == Command.ROL and \ + value == VELOCITY + return Reply() + setattr(motor,'send',mock_send) + + motor.rotate_left( VELOCITY ) + + + diff --git a/test/motor/test__rotate_right.py b/test/motor/test__rotate_right.py new file mode 100644 index 0000000..c5dcfc7 --- /dev/null +++ b/test/motor/test__rotate_right.py @@ -0,0 +1,64 @@ + +from pytest import raises + +from TMCL import Reply, Motor +from TMCL import Command + + +class BasicMockModule(object): + def send ( self, *args ): + return Reply() + + + + +class Test__rotate_right (object): + + + def test__velocity_must_be_an_int (self): + """ + Velocity must be validated as an integer + """ + motor = Motor(BasicMockModule()) + with raises(AssertionError): + motor.rotate_right("not an integer") + + + + def test__velocity_must_be_within_bounds (self): + """ + Velocity param must be within range 0 <= velocity <= self._max_velocity + """ + motor = Motor(BasicMockModule()) + motor._max_velocity = 100 + + with raises(AssertionError): + motor.rotate_right(-123) + + with raises(AssertionError): + motor.rotate_right(101) + + motor.rotate_right(50) + + + + def test__calls_send_with_correct_args (self): + """ + Must call motor.send() with the correct arguments + :return: + """ + + VELOCITY = 123 + + motor = Motor(BasicMockModule()) + + def mock_send (cmd, type=0, value=0): + assert cmd == Command.ROR and \ + value == VELOCITY + return Reply() + setattr(motor,'send',mock_send) + + motor.rotate_right( VELOCITY ) + + + diff --git a/test/motor/test__send.py b/test/motor/test__send.py new file mode 100644 index 0000000..5da08da --- /dev/null +++ b/test/motor/test__send.py @@ -0,0 +1,100 @@ +""" + Test: TMCL.Motor#send() +""" +from pytest import raises +from TMCL import Reply, Motor, Command + + + +class SuccessException (BaseException): pass + +class BasicMockModule(object): + def send ( self, *args ): + return Reply() + + + +class Test__send (object): + + + def test__param_cmd_must_be_an_integer (self): + """ + `cmd` argument must be validated as integer + """ + motor = Motor(BasicMockModule()) + + with raises(AssertionError): + motor.send('not-an-integer',123,456) + + + def test__param_type_must_be_an_integer ( self ): + """ + `type` argument must be validated as integer + """ + motor = Motor(BasicMockModule()) + + with raises(AssertionError): + motor.send(123, 'not-an-integer', 456) + + + def test__param_type_defaults_to_zero (self): + """ + `type` argument defaults to zero + """ + class MockModule(object): + def send ( self, instruction ): + if instruction.type == 0: + raise SuccessException() + + motor = Motor(MockModule()) + with raises(SuccessException): + motor.send(Command.ROR, value=123) + + + def test__param_value_must_be_an_integer ( self ): + """ + `value` argument must be validated as integer + """ + motor = Motor(BasicMockModule()) + + with raises(AssertionError): + motor.send(123, 456, 'not-an-integer') + + + def test__param_value_defaults_to_zero ( self ): + """ + `value` argument defaults to zero + """ + class MockModule(object): + def send ( self, instruction ): + if instruction.value == 0: + raise SuccessException() + + motor = Motor(MockModule()) + with raises(SuccessException): + motor.send(Command.ROR, type=123) + + + + def test__param_cmd_must_be_in_allowed_list ( self ): + motor = Motor(BasicMockModule()) + for cmd in motor.ALLOWED_COMMANDS: + motor.send( cmd ) + assert True + + + def test__send_adds_correct_axis_to_command (self): + """ + Motor instances should append the correct axis id + to commands it sends to module + """ + AXIS = 3 + class MockModule (object): + def send (self, instruction): + assert instruction.motbank == AXIS + return Reply() + + motor = Motor(MockModule(), axis=AXIS) + motor.send(Command.GAP) + +