import logging import serial class Error(Exception): """Base class for exceptions in this module.""" pass class CortexError(Error): """Exception raised if the Cortex controller responds to a command with 'E#/r/n', i.e. if it receives and illegal command. This should of course never happen... Attributes: command - the command string sent to the controller which resulted in the error.""" def __init__(self, command): self.command = command class RangeError(Error): """Exception raised if there is an attempt to set a Cortex parameter with an value outside the allowed range. Attributes: parameter - the parameter in question value - the illegal value requested range - the allowed range for the parameter""" def __init__(self, parameter, value, range): self.parameter = parameter self.value = value self.range = range class TimeoutError(Error): """Exception raised when there is no response from the Cortex controller. This most likely means it's been disconnected, switched off, or a command has been sent to a non-existant unit in a daisy chain. Attributes: command - the last command string sent to the controller""" def __init__(self, command): self.commmand = command class ResponseError(Error): """Exception raised when an unexpected response is received from the Cortex controller. This should of course never happen... Attributes: command - the command string sent to the controller response - the response string received""" def __init__(self, command, response): self.command = command self.response = response class cortex: """Basic class representing an interface to a Cortex controller (or daisy chain of controllers). Presents functions corresponding to each of the commands accepted by a Cortex controller. Performs range checking on arguments and returns response from the controller.""" def __init__(self, portname, units, name='cortex'): """ Arguments: portname: device name of the serial port to use units: number of cortex units in the chain name: name to be used as a label in the logs """ self.portname = portname self.units = int(units) self.name = name self.logger = logging.getLogger(name) # Open the serial port. If it doesn't work there'll be # a SerialException. try: self.__port = serial.Serial(portname, timeout=5) except serial.SerialException: # It's all gone horribly wrong self.logger.critical('Failed to open serial port %s!' % portname) return self.logger.info('Connected to serial port %s' % portname) # Tell the cortex controller (or the first in the chain) # to terminate its lines properly (i.e. use newlines). self.__port.write('\r\n') self.__setNewline(True, cortex_num=1) # To begin with disable outputs and set currents to zero. for unit in range(1, self.units + 1): self.setCurrent(0, unit) self.setOutput(0, unit) def __basicCommand(self, command): """Function used to send any command that expects the basic '#\r\n' response, and parses the response received. If the standard response is not received an appropriate exception is raised. Arguments: command - string containing the command to send.""" self.__port.flushInput() self.__port.write(command) response = self.__port.readline() if response == "#\r\n": return elif response == "E\r\n": # raise CortexError(command) self.logger.critical('Syntax error - sent ' + repr(command)) elif not response: # raise TimeoutError(command) self.logger.critical('No response on port %s - sent ' \ % self.portname + repr(command)) else: # raise ResponseError(command, response) self.logger.critical('Unexpected response ' + repr(response) + \ ' - sent ' + repr(command)) def __intCommand(self, command): """Function used to send any command that expects an integer N in a 'N#\r\n' response, parses the response received, and returns the integer. If the expected response is not received an appropriate exception is raised. Arguments: command - string containing the command to send.""" self.__port.flushInput() self.__port.write(command) response = self.__port.readline() if response[-3:] == "#\r\n": return int(response[:-3]) elif response == "E\r\n": #raise CortexError(command) self.logger.critical('Syntax error - sent "%s"' % command) elif not response: #raise TimeoutError(command) self.logger.critical('No response on port %s - sent "%s"' \ % (self.portname, command)) else: #raise ResponseError(command, response) self.logger.critical('Unexpected response "%s" - sent "%s"' % (response, command)) def getInput(self, cortex_num=1): """ Gets the status of the 4-bit input of the Cortex Controller, returning it as an integer in the range 0-15. Arguments: cortex_num - which controller of a daisy chain to query, default 1. """ if cortex_num > self.units or cortex_num < 1: self.logger.warning('Illegal unit number in getInput(%i)'\ % cortex_num) else: self.logger.debug('getInput(%i) called' % cortex_num) return self.__intCommand("%iI\n" % cortex_num) def getLimitStatus(self, cortex_num=1): """Queries the limit switch status, returns -1 if lower limit made, +1 if upper limit made and 0 is neither is. Note, this command ALWAYS stops the motors, use isActive() first to determine if the motors are in motion. Arguments: cortex_num - which controller of a daisy chain to query, default 1.""" if cortex_num > self.units or cortex_num < 1: self.logger.warning('Illegal unit number in getLimitStatus(%i)'\ % cortex_num) else: self.logger.debug('getLimitStatus(%i) called' % cortex_num) return self.__intCommand("%iRL\n" % cortex_num) def getParams(self, cortex_num=1): if cortex_num > self.units or cortex_num < 1: self.logger.warning('Illegal unit number in getParams(%i)'\ % cortex_num) else: self.logger.debug('getParams(%i) called' % cortex_num) self.__port.flushInput() self.__port.write("%i?\n" % cortex_num) return self.__port.readline() def getPosition(self, cortex_num=1): """Requests the current motor position from the controller, in steps from the datum and returns it as an integer. Arguments: cortex_num - which controller of a daisy chain to query, default 1.""" if cortex_num > self.units or cortex_num < 1: self.logger.warning('Illegal unit number in getPosition(%i)'\ % cortex_num) else: self.logger.debug('getPosition(%i) called' % cortex_num) return self.__intCommand("%iW\n" % cortex_num) def getRemaining(self, cortex_num=1): """Requests the number of steps left to go in the current move from the controller and returns the reply as an integer. Arguments: cortex_num - which controller of a daisy chain to query, default 1.""" if cortex_num > self.units or cortex_num < 1: self.logger.warning('Illegal unit number in getRemaining(%i)'\ % cortex_num) else: self.logger.debug('getRemaining(%i) called' % cortex_num) return self.__intCommand("%iRS\n" % cortex_num) def isActive(self, cortex_num=1): """Requests the motor status from the controller, returns True if the motor is active, False otherwise. Arguments: cortex_num - which controller of a daisy chain to query, default 1.""" if cortex_num > self.units or cortex_num < 1: self.logger.warning('Illegal unit number in isActive(%i)'\ % cortex_num) else: self.logger.debug('isActive(%i) called' % cortex_num) return bool(self.__intCommand("%iA\n" % cortex_num)) def saveParams(self, cortex_num=1): """Writes all current controller parameters to EEPROM, if fitted. Arguments: cortex_num - which controller of a daisy chain to command, default 1.""" if cortex_num > self.units or cortex_num < 1: self.logger.warning('Illegal unit number in saveParams(%i)'\ % cortex_num) else: self.logger.debug('saveParams(%i) called' % cortex_num) self.__basicCommand("%iQ\n" % cortex_nun) def setCurrent(self, current=160, cortex_num=1): """Sets the motor current, in mA. Allowed range is 0-600mA, in steps of 40mA (intermediate values, if given, are rounded down to the nearest multiple of 40). N.B. Do not exceed 160mA in continous use unless a heatsink is fitted. Arguments: current - motor current, in mA, integer in range 0-600. cortex_num - which controller of a daisy chain to command, default 1.""" if cortex_num > self.units or cortex_num < 1: self.logger.warning('Illegal unit number in setCurrent(%i, %i)'\ % (current, cortex_num)) return if current < 0 or current > 600: # raise RangeError("Current", current, "0:600") self.logger.warning('Illegal current in setCurrent(%i, &i)'\ % (current, cortex_num)) else: self.logger.debug('setCurrent(%i, %i) called' \ % (current, cortex_num)) self.__basicCommand("%iE %i\n" % (cortex_num, current/40)) def setDatum(self, position=0, cortex_num=1): """Change the coordinate system so that the current position is equal to the argument. Arguments: position - value to assign to the current position, default 0. cortex_num - which controller of a daisy chain to command, default 1.""" if cortex_num > self.units or cortex_num < 1: self.logger.warning('Illegal unit number in setDatum(%i, %i)'\ % (position, cortex_num)) else: self.logger.debug('setDatum(%i, %i) called' \ % (position, cortex_num)) self.__basicCommand("%iD %i\n" % (cortex_num, position)) def setDefaults(self, cortex_num=1): """Return all parameters to their default values, with the exception of the newline parameter on the first controller in the chain (newlines are required for communication using pySerial). The default values are given in the Cortex controller doumentation. Arguments: cotrex_num - which controller of a daisy chain to reset to defaults, default 1.""" if cortex_num > self.units or cortex_num < 1: self.logger.warning('Illegal unit number in setDefaults(%i)'\ % cortex_num) return self.logger.debug('setDefaults(%i) called' % cortex_num) if cortex_num == 1: command = "1F\n" self.__port.flushInput() self.__port.write(command) response = self.__port.readline() if response == "#\r": self.__setNewline(True, cortex_num=1) elif response == "E\r": self.logger.critical('Syntax error - sent "%s"' % command) #raise CortexError(command) elif not response: self.logger.critical('No response on port %s - sent "%s"' \ % (self.portname, command)) #raise TimeoutError(command) else: self.logger.critical('Unexpected response "%s" - sent "%s"' % (response, command)) #raise ResponseError(command, response) else: self.__basicCommand("%iF\n" % cortex_num) def setJoystick(self, jitter=4, deadband=20, slowband=10, sense=0, \ cortex_num=1): """Sets the joystick parameters. No sanity checking of arguments implemented yet. Arguments: jitter - allowable joystick noise, default 4 deadband - joystick deadband, default 20 slowband - slow motion band, default 10 sense - joystick not reversed, default 0 cortex_num - which controller in a daisy chain to command, default 1.""" if cortex_num > self.units or cortex_num < 1: self.logger.warning(\ 'Illegal unit number in setJoystick(%i, %i, %i, $i, %i)'\ % (jitter, deadband, slowband, sense, cortex_num)) else: self.logger.debug(\ 'setJoystick(%i, %i, %i, %i, %i) called'\ % (jitter, deadband, slowband, sense, cortex_num)) self.__basicCommand("%iJ %i %i %i %i\n" % \ (cortex_num, jitter, deadband, slowband, \ sense)) def setLocalMode(self, yes=True, cortex_num=1): """Toggles local/manual mode. If the first argument is True sets local mode on, if False sets local mode off. Arguments: yes - boolean, if True set local mode on, and vice versa, default True cortex_num - which controller in a daisy chain to command, default 1.""" if cortex_num > self.units or cortex_num < 1: self.logger.warning('Illegal unit number in setLocalMode(%i, %i)'\ % (yes, cortex_num)) return self.logger.debug('setLocalMode(%i, %i) called' % (yes, cortex_num)) if yes: self.__basicCommand("%iL 1\n" % cortex_num) else: self.__basicCommand("%iL 0\n" % cortex_num) def setMicrostepping(self, ratio, cortex_num=1): """Set the microstepping ratio using the numeric codes described in the Cortex controller documentation. The argument ratio should be an integer in the range 0-63. Arguments: ratio - microstepping ratio code, integer in range 0-63. cortex_num - which controller in a diasy chain to set, default 1.""" if cortex_num > self.units or cortex_num < 1: self.logger.warning(\ 'Illegal unit number in setMicrostepping(%i, %i)' \ % (ratio, cortex_num)) return if ratio < 0 or ratio > 63: #raise RangeError("Microstepping", ratio, "0:63") self.logger.warning(\ 'Illegal ratio in setMicrostepping(%i, %i)' \ % (ratio, cortex_num)) else: self.logger.debug('setMicrostepping(%i, %i) called'\ % (ratio, cortex_num)) self.__basicCommand("%iR %i\n" % (cortex_num, ratio)) def setMotorParams(self, baseSpeed = 1000, maxSpeed = 60000, \ acceleration = 30000, deceleration = 120000, \ cortex_num=1): """Sets the motor motion parameters, base speed, maximum speed, acceleration and deceleration. Arguments: baseSpeed - starting speed for move, moveAbs and moveLimit commands, default 1000. maxSpeed - top speed, default 60000 acceleration - acceleration, default 30000 deceleration - deceleration, default 120000 cortex_num - which controller in a daisy chain to set, default 1.""" if cortex_num > self.units or cortex_num < 1: self.logger.warning(\ 'Illegal unit number in setMotorParams(%i, %i, %i, %i, %i)'\ % (baseSpeed, maxSpeed, acceleration, \ deceleration, cortex_num)) else: self.logger.debug(\ 'setMotorParams(%i, %i, %i, %i, %i) called'\ % (baseSpeed, maxSpeed, acceleration, \ deceleration, cortex_num)) self.__basicCommand("%iP%i %i %i %i\n" % \ (cortex_num, baseSpeed, maxSpeed, \ acceleration, deceleration)) def __setNewline(self, yes=True, cortex_num=1): """Toggles the addition of newlines to the replies from the Cortex controller. If the first argument is True sets newlines on, if False sets newlines off. In order for proper communications with the first cortex controller in the chain it must have newlines set to True. For inter-controller communications to work all other controllers (if present) should have newlines False. Arguments: yes - boolean, if True set local mode on, and vice versa, default True cortex_num - which controller in a daisy chain to command, default 1.""" if cortex_num > self.units or cortex_num < 1: self.logger.warning(\ 'Illegal unit number in __setNewline(%i, %i)' \ % (yes, cortex_num)) return self.logger.debug('__setNewline(%i, %i) called' \ % (yes, cortex_num)) if yes: self.__basicCommand("%iN 1\n" % cortex_num) else: self.__basicCommand("%iN 0\n" % cortex_num) def setOutput(self, output, cortex_num=1): """Sets the state of the four bit optocoupler output of the Cortex controller. Arguments: output - integer in range 0-15 corresponding to desired state of the 4 bit output. cortex_num - which controller in a chain to set, default 1.""" if cortex_num > self.units or cortex_num < 1: self.logger.warning(\ 'Illegal unit number in setOutput(%i, %i)' \ % (output, cortex_num)) return if output < 0 or output > 15: #raise RangeError("Output", output, "0:15") self.logger.warning(\ 'Illegal output in setOutput(%i, %i)' \ % (output, cortex_num)) else: self.logger.debug('setOutput(%i, %i) called' \ % (output, cortex_num)) self.__basicCommand("%iO %i\n" % (cortex_num, output)) def setSpeed(self, speed=150, cortex_num=1): """Sets the constant speed for moveConst, moveConstAbs and moveConstLimit commands. Arguments: speed - required speed in steps/sec, integer in range 62-60000 cortex_num - which controller in a chain to set, default 1.""" if cortex_num > self.units or cortex_num < 1: self.logger.warning(\ 'Illegal unit number in setSpeed(%i, %i)' \ % (speed, cortex_num)) return if speed < 62 or speed > 60000: #raise RangeError("Speed", speed, "62:60000") self.logger.warning(\ 'Illegal speed in setSpeed(%i, %i)' \ % (speed, cortex_num)) else: self.logger.debug('setSpeed(%i, %i) called' \ % (speed, cortex_num)) self.__basicCommand("%iC %i\n" % (cortex_num, speed)) def halt(self, cortex_num=1): """Smoothly halts the motor. For emergency stops use the stop command. Arguments: cortex_num - which controller in a chain to halt, default 1.""" if cortex_num > self.units or cortex_num < 1: self.logger.warning(\ 'Illegal unit number in halt(%i)' % cortex_num) else: self.logger.debug('halt(%i) called' % cortex_num) self.__basicCommand("%iH\n" % cortex_num) def move(self, steps, cortex_num=1): """Move a given number of steps (<2000000000) with acceleration (uses motion parameters defined by setMotorParams). Arguments: steps - number of steps to move, integer in range -2000000000:+2000000000 cortex_num - which controller to instruct to move, default 1.""" if cortex_num > self.units or cortex_num < 1: self.logger.warning('Illegal unit number in move(%i, %i)' \ % (steps, cortex_num)) return if steps < -2000000000 or steps > 2000000000: #raise RangeError("steps", steps, "-2000000000:+2000000000") self.logger.warning('Illegal steps in move(%i, %i)' \ % (steps, cortex_num)) else: self.logger.debug('move(%i, %i) called' \ % (steps, cortex_num)) self.__basicCommand("%iM %i\n" % (cortex_num, steps)) def moveAbs(self, position, cortex_num=1): """Move to a given position, with acceleration (uses motion parameters defined by setMotorParams). Arguments: position - position, measure in steps from the datum to move to. cortex_num - which controller to instruct to move, default 1.""" if cortex_num > self.units or cortex_num < 1: self.logger.warning('Illegal unit number in moveAbs(%i, %i)' \ % (position, cortex_num)) else: self.logger.debug('moveAbs(%i, %i) called' \ % (position, cortex_num)) self.__basicCommand("%iMA %i\n" % (cortex_num, position)) def moveConst(self, steps, cortex_num=1): """Move a given number of steps (<2000000000) at a constant speed (uses speed set via setSpeed). Arguments: steps - number of steps to move, integer in range -2000000000:+2000000000 cortex_num - which controller to instruct to move, default 1.""" if cortex_num > self.units or cortex_num < 1: self.logger.warning('Illegal unit number in moveConst(%i, %i)' \ % (steps, cortex_num)) return if steps < -2000000000 or steps > 2000000000: #raise RangeError("steps", steps, "-2000000000:+2000000000") self.logger.warning('Illegal steps in moveConst(%i, %i)' \ % (steps, cortex_num)) else: self.logger.debug('moveConst(%i, %i) called' \ % (steps, cortex_num)) self.__basicCommand("%iMC %i\n" % (cortex_num, steps)) def moveConstAbs(self, position, cortex_num=1): """Move to a given position at a constant speed (used speed set via setSpeed). Arguments: position - position, measure in steps from the datum to move to. cortex_num - which controller to instruct to move, default 1.""" if cortex_num > self.units or cortex_num < 1: self.logger.warning('Illegal unit number in moveConstAbs(%i, %i)'\ % (position, cortex_num)) else: self.logger.debug('moveConstAbs(%i, %i) called' \ % (position, cortex_num)) self.__basicCommand("%iMCA %i\n" % (cortex_num, position)) def moveConstLimit(self, direction, cortex_num=1): """Move to a given limit at a constant speed (uses speed set via setSpeed). Arguments: direction - the direction to move in, 1 for towards the upper limit, -1 for towards the lower limit, or 0 for, er, something else (Cortex documentation doesn't explain this). cortex_num - which controller to instruct to move, default 1.""" if cortex_num > self.units or cortex_num < 1: self.logger.warning(\ 'Illegal unit number in moveConstLimit(%i, %i)'\ % (direction, cortex_num)) if direction < -1 or direction > 1: #raise RangeError("direction", direction, "-1,0,+1") self.logger.warning('Illegal direction in moveConstLimit(%i, %i)'\ % (direction, cortex_num)) else: self.logger.debug('moveConstLimit(%i, %i) called' \ % (direction, cortex_num)) self.__basicCommand("%iMCL %i\n" & (cortex_num, direction)) def moveLimit(self, direction, cortex_num=1): """Move to a given limit with acceleration (uses motion parameters defined with setMotorParams). Arguments: direction - the direction to move in, 1 for towards the upper limit, -1 for towards the lower limit, or 0 for, er, something else (Cortex documentation doesn't explain this). cortex_num - which controller to instruct to move, default 1.""" if cortex_num > self.units or cortex_num < 1: self.logger.warning(\ 'Illegal unit number in moveLimit(%i, %i)'\ % (direction, cortex_num)) if direction < -1 or direction > 1: #raise RangeError("direction", direction, "-1,0,+1") self.logger.warning('Illegal direction in moveLimit(%i, %i)'\ % (direction, cortex_num)) else: self.logger.debug('moveLimit(%i, %i) called' \ % (direction, cortex_num)) self.__basicCommand("%iML %i\n" % (cortex_num, direction)) def stop(self, cortex_num=1): """Emergency stop. Arguments: cortex_num - which controller in a chain to stop, default 1.""" if cortex_num > self.units or cortex_num < 1: self.logger.warning('Illegal unit number in stop(%i)' % cortex_num) else: self.logger.debug('stop(%i) called' % cortex_num) self.__basicCommand("%iS\n" % cortex_num) def trigger(self, input=-1, cortex_num=1): """Delay move until given a given input goes high. If no input is specified then moves occur as they are requested. Arguments: input - the number of the input to trigger on, integer in the range 0-3 (I think). If omitted triggering is suspended. cortex_num - which controller in a chain to set triggering for, default 1.""" if cortex_num > self.units or cortex_num < 1: self.logger.warning('Illegal unit number in trigger(%i, %i)' \ % (input, cortex_num)) return if input < -1 or input > 3: #raise RangeError("trigger", input, "0:3") self.logger.warning('Illegal input in trigger(%i, %i)' \ % (input, cortex_num)) elif input == -1: self.__basicCommand("%iT\n" % cortex_num) self.logger.debug('trigger(cortex_num=%i) called' % cortex_num) else: self.__basicCommand("%iT %i\n" % (cortex_num, input)) self.logger.debug('trigger(%i, %i) called' % (trigger, cortex_num))