Skip to content
This repository has been archived by the owner on Nov 3, 2021. It is now read-only.

Latest commit

 

History

History
719 lines (525 loc) · 26.2 KB

howto_motorcontroller.rst

File metadata and controls

719 lines (525 loc) · 26.2 KB

sardana.pool.controller

How to write a motor controller

The basics

An example of a hypothetical Springfield motor controller will be build incrementally from scratch to aid in the explanation.

By now you should have read the general controller basics <sardana-controller-api> chapter. You should now have a MotorController with a proper constructor, add and delete axis methods:

import springfieldlib

from sardana.pool.controller import MotorController

class SpringfieldMotorController(MotorController):

    def __init__(self, inst, props, *args, **kwargs):
        super(SpringfieldMotorController, self).__init__(inst, props, *args, **kwargs)

        # initialize hardware communication
        self.springfield = springfieldlib.SpringfieldMotorHW()

        # do some initialization
        self._motors = {}

    def AddDevice(self, axis):
        self._motors[axis] = True 

    def DeleteDevice(self, axis):
        del self._motor[axis]

The get axis state method has some details that will be explained below.

The examples use a springfieldlib module which emulates a motor hardware access library.

The springfieldlib can be downloaded from here <springfieldlib.py>.

The Springfield motor controller can be downloaded from here <sf_motor_ctrl.py>.

The following code describes a minimal Springfield base motor controller which is able to return both the state and position of a motor as well as move a motor to the desired position:

sf_motor_ctrl.py

This code is shown only to demonstrate the minimal controller API. The advanced motor controller chapters describe how to account for more complex behaviour like reducing the number of hardware accesses or synchronize motion of multiple motors.

Get motor state

To get the state of a motor, sardana calls the ~sardana.pool.controller.Controller.StateOne method. This method receives an axis as parameter and should return either:

  • state (~sardana.sardanadefs.State) or
  • a sequence of two elements:
    • state (~sardana.sardanadefs.State)
    • status (str) or limit switches (int)
  • a sequence of three elements:
    • state (~sardana.sardanadefs.State)
    • status (str)
    • limit switches (int)

The state should be a member of ~sardana.sardanadefs.State (For backward compatibility reasons, it is also supported to return one of PyTango.DevState). The status could be any string. The limit switches is a integer with bits representing the three possible limits: home, upper and lower. Sardana provides three constants which can be ored together to provide the desired limit switch:

  • ~MotorController.NoLimitSwitch
  • ~MotorController.HomeLimitSwitch
  • ~MotorController.UpperLimitSwitch
  • ~MotorController.LowerLimitSwitch

To say both home and lower limit switches are active (rare!) you can do:

limit_switches = MotorController.HomeLimitSwitch | MotorController.LowerLimitSwitch 

If you don't return a status, sardana will compose a status string with:

<axis name> is in <state name>

If you don't return limit switches, sardana will assume all limit switches are off.

Here is an example of the possible implementation of ~sardana.pool.controller.Controller.StateOne:

from sardana import State

class SpringfieldMotorController(MotorController):

    StateMap = {
        1 : State.On,
        2 : State.Moving,
        3 : State.Fault,
    }

    def StateOne(self, axis):
        springfield = self.springfield
        state = self.StateMap[ springfield.getState(axis) ]
        status = springfield.getStatus(axis)

        limit_switches = MotorController.NoLimitSwitch
        hw_limit_switches = springfield.getLimits(axis)
        if hw_limit_switches[0]:
            limit_switches |= MotorController.HomeLimitSwitch
        if hw_limit_switches[1]:
            limit_switches |= MotorController.UpperLimitSwitch
        if hw_limit_switches[2]:
            limit_switches |= MotorController.LowerLimitSwitch
        return state, status, limit_switches

Get motor position

To get the motor position, sardana calls the ~sardana.pool.controller.Readable.ReadOne method. This method receives an axis as parameter and should return a valid position. Sardana interprets the returned position as a dial position.

Here is an example of the possible implementation of ~sardana.pool.controller.Readable.ReadOne:

class SpringfieldMotorController(MotorController):

    def ReadOne(self, axis):
        position = self.springfield.getPosition(axis)
        return position

Move a motor

When an order comes for sardana to move a motor, sardana will call the ~sardana.pool.controller.Startable.StartOne method. This method receives an axis and a position. The controller code should trigger the hardware motion. The given position is always the dial position.

Here is an example of the possible implementation of ~sardana.pool.controller.Startable.StartOne:

class SpringfieldMotorController(MotorController):

    def StartOne(self, axis, position):
        self.springfield.move(axis, position)

As soon as ~sardana.pool.controller.Startable.StartOne is invoked, sardana expects the motor to be moving. It enters a high frequency motion loop which asks for the motor state through calls to ~sardana.pool.controller.Controller.StateOne. It will keep the loop running as long as the controller responds with State.Moving. If ~sardana.pool.controller.Controller.StateOne raises an exception or returns something other than State.Moving, sardana will assume the motor is stopped and exit the motion loop.

For a motion to work properly, it is therefore, very important that ~sardana.pool.controller.Controller.StateOne responds correctly.

Stop a motor

It is possible to stop a motor when it is moving. When sardana is ordered to stop a motor motion, it invokes the ~sardana.pool.controller.Stopable.StopOne method. This method receives an axis parameter. The controller should make sure the desired motor is gracefully stopped, if possible, respecting the configured motion parameters (like deceleration and base_rate).

Here is an example of the possible implementation of ~sardana.pool.controller.Stopable.StopOne:

class SpringfieldMotorController(MotorController):

    def StopOne(self, axis):
        self.springfield.stop(axis)

Abort a motor

In a danger situation (motor moving a table about to hit a wall), it is desirable to abort a motion as fast as possible. When sardana is ordered to abort a motor motion, it invokes the ~sardana.pool.controller.Stopable.AbortOne method. This method receives an axis parameter. The controller should make sure the desired motor is stopped as fast as it can be done, possibly losing track of position.

Here is an example of the possible implementation of ~sardana.pool.controller.Stopable.AbortOne:

class SpringfieldMotorController(MotorController):

    def AbortOne(self, axis):
        self.springfield.abort(axis)

Note

The default implementation of ~sardana.pool.controller.Stopable.StopOne calls ~sardana.pool.controller.Stopable.AbortOne so, if your controller cannot distinguish stopping from aborting, it is sufficient to implement ~sardana.pool.controller.Stopable.AbortOne.

Standard axis attributes

By default, sardana expects every axis to have a set of attributes:

  • acceleration
  • deceleration
  • velocity
  • base rate
  • steps per unit

To set and retrieve the value of these attributes, sardana invokes pair of methods: ~sardana.pool.controller.Controller.GetAxisPar /~sardana.pool.controller.Controller.SetAxisPar

Here is an example of the possible implementation:

class SpringfieldMotorController(MotorController):

    def GetAxisPar(self, axis, name):
        springfield = self.springfield
        name = name.lower()
        if name == "acceleration":
            v = springfield.getAccelerationTime(axis)
        elif name == "deceleration":
            v = springfield.getDecelerationTime(axis)
        elif name == "base_rate":
            v = springfield.getMinVelocity(axis)
        elif name == "velocity":
            v = springfield.getMaxVelocity(axis)
        elif name == "step_per_unit":
            v = springfield.getStepPerUnit(axis)
        return v

    def SetAxisPar(self, axis, name, value):
        springfield = self.springfield
        name = name.lower()
        if name == "acceleration":
            springfield.setAccelerationTime(axis, value)
        elif name == "deceleration":
            springfield.setDecelerationTime(axis, value)
        elif name == "base_rate":
            springfield.setMinVelocity(axis, value)
        elif name == "velocity":
            springfield.setMaxVelocity(axis, value)
        elif name == "step_per_unit":
            springfield.setStepPerUnit(axis, value)
sardana-motorcontroller-what-to-do

What to do when your hardware motor controller doesn't support steps per unit

Define a position

Sometimes it is useful to reset the current position to a certain value. Imagine you are writing a controller for a hardware controller which handles stepper motors. When the hardware is asked for a motor position it will probably answer some value from an internal register which is incremented/decremented each time the motor goes up/down a step. Probably this value as physical meaning so the usual procedure is to move the motor to a known position (home switch, for example) and once there, set a meaningful position to the current position. Some motor controllers support reseting the internal register to the desired value. If your motor controller can do this the implementation is as easy as writing the ~sardana.pool.controller.MotorController.DefinePosition and call the proper code of your hardware library to do it:

class SpringfieldMotorController(MotorController):

    def DefinePosition(self, axis, position):
        self.springfield.setCurrentPosition(axis, position)

sardana-motorcontroller-what-to-do

What to do when your hardware motor controller doesn't support defining the position

What to do when...

This chapter describes common difficult situations you may face when writing a motor controller in sardana, and possible solutions to solve them.

my controller doesn't support steps per unit

Many (probably, most) hardware motor controllers don't support steps per unit at the hardware level. This means that your sardana controller should be able to emulate steps per unit at the software level. This can be easily done, but it requires you to make some changes in your code.

We will assume now that the Springfield motor controller doesn't support steps per unit feature. The first that needs to be done is to modify the ~sardana.pool.controller.Controller.AddDevice method so it is able to to store the resulting conversion factor between the hardware read position and the position the should be returned (the step_per_unit). The ~sardana.pool.controller.Readable.ReadOne also needs to be rewritten to make the proper calculation. Finally ~sardana.pool.controller.Controller.GetAxisPar / ~sardana.pool.controller.Controller.SetAxisPar methods need to be rewritten to properly get/set the step per unit value:

class SpringfieldMotorController(MotorController):

    def AddDevice(self, axis):
        self._motor[axis] = dict(step_per_unit=1.0)

    def ReadOne(self, axis):
        step_per_unit = self._motor[axis]["step_per_unit"]
        position = self.springfield.getPosition(axis)
        return position / step_per_unit

    def GetAxisPar(self, axis, name):
        springfield = self.springfield
        name = name.lower()
        if name == "acceleration":
            v = springfield.getAccelerationTime(axis)
        elif name == "deceleration":
            v = springfield.getDecelerationTime(axis)
        elif name == "base_rate":
            v = springfield.getMinVelocity(axis)
        elif name == "velocity":
            v = springfield.getMaxVelocity(axis)
        elif name == "step_per_unit":
            v = self._motor[axis]["step_per_unit"]
        return v

    def SetAxisPar(self, axis, name, value):
        springfield = self.springfield
        name = name.lower()
        if name == "acceleration":
            springfield.setAccelerationTime(axis, value)
        elif name == "deceleration":
            springfield.setDecelerationTime(axis, value)
        elif name == "base_rate":
            springfield.setMinVelocity(axis, value)
        elif name == "velocity":
            springfield.setMaxVelocity(axis, value)
        elif name == "step_per_unit":
            self._motor[axis]["step_per_unit"] = value                
my controller doesn't support defining the position

Some controllers may not be able to reset the position to a different value. In these cases, your controller code should be able to emulate such a feature. This can be easily done, but it requires you to make some changes in your code.

We will now assume that the Springfield motor controller doesn't support steps per unit feature. The first thing that needs to be done is to modify the ~sardana.pool.controller.Controller.AddDevice method so it is able to store the resulting offset between the hardware read position and the position the should be returned (the define_position_offset). The ~sardana.pool.controller.Readable.ReadOne also needs to be rewritten to take the define_position_offset into account. Finally ~sardana.pool.controller.MotorController.DefinePosition needs to be written to update the define_position_offset to the desired value:

class SpringfieldMotorController(MotorController):

    def AddDevice(self, axis):
        self._motor[axis] = dict(define_position_offset=0.0)

    def ReadOne(self, axis):
        dp_offset = self._motor[axis]["define_position_offset"]
        position = self.springfield.getPosition(axis)
        return position + dp_offset

    def DefinePosition(self, axis, position):
        current_position = self.springfield.getPosition(axis)
        self._motor[axis]["define_position_offset"] = position - current_position

Advanced topics

Timestamp a motor position

When you read the position of a motor from the hardware sometimes it is necessary to associate a timestamp with that position so you can track the position of a motor in time.

If sardana is executed as a Tango device server, reading the position attribute from the motor device triggers the execution of your controller's ~sardana.pool.controller.Readable.ReadOne method. Tango responds with the value your controller returns from the call to ~sardana.pool.controller.Readable.ReadOne and automatically assigns a timestamp. However this timestamp has a certain delay since the time the value was actually read from hardware and the time Tango generates the timestamp.

To avoid this, sardana supports returning in ~sardana.pool.controller.Readable.ReadOne an object that contains both the value and the timestamp instead of the usual numbers.Number. The object must be an instance of ~sardana.sardanavalue.SardanaValue.

Here is an example of associating a timestamp in ~sardana.pool.controller.Readable.ReadOne:

import time
from sardana.pool.controller import SardanaValue 

class SpringfieldMotorController(MotorController):

   def ReadOne(self, axis):
       return SardanaValue(value=self.springfield.getPosition(axis),
                           timestamp=time.time())

If your controller communicates with a Tango device, Sardana also supports returning a ~PyTango.DeviceAttribute object. Sardana will use this object's value and timestamp. Example:

class TangoMotorController(MotorController):

   def ReadOne(self, axis):
       return self.device.read_attribute("position")

Multiple motion synchronization

This chapter describes an extended API that allows you to better synchronize motions involing more than one motor, as well as optimize hardware communication (in case the hardware interface also supports this).

Often it is the case that the experiment/procedure the user runs requires to move more than one motor at the same time. Imagine that the user requires motor at axis 1 to be moved to 100mm and motor axis 2 to be moved to -20mm. Your controller will receive two consecutive calls to ~sardana.pool.controller.Startable.StartOne:

StartOne(1, 100)
StartOne(2, -20)

and each StartOne will probably connect to the hardware (through serial line, socket, Tango or EPICS) and ask the motor to be moved. This will do the job but, there will be a slight desynchronization between the two motors because hardware call of motor 1 will be done before hardware call to motor 2.

Sardana provides an extended start motion which gives you the possibility to improve the syncronization (and probably reduce communications) but your hardware controller must somehow support this feature as well.

The complete start motion API consists of four methods:

  • ~sardana.pool.controller.Startable.PreStartAll
  • ~sardana.pool.controller.Startable.PreStartOne
  • ~sardana.pool.controller.Startable.StartOne
  • ~sardana.pool.controller.Startable.StartAll

Except for ~sardana.pool.controller.Startable.StartOne, the implemenation of all other start methods is optional and their default implementation does nothing (~sardana.pool.controller.Startable.PreStartOne actually returns True).

So, actually, the complete algorithm for motor motion in sardana is:

/FOR/ Each controller(s) implied in the motion
     - Call PreStartAll()
/END FOR/

/FOR/ Each motor(s) implied in the motion
     - ret = PreStartOne(motor to move, new position)
     - /IF/ ret is not true
        /RAISE/ Cannot start. Motor PreStartOne returns False
     - /END IF/         
     - Call StartOne(motor to move, new position)
/END FOR/

/FOR/ Each controller(s) implied in the motion
     - Call StartAll()
/END FOR/

So, for the example above where we move two motors, the complete sequence of calls to the controller is:

PreStartAll()

if not PreStartOne(1, 100):
    raise Exception("Cannot start. Motor(1) PreStartOne returns False")
if not PreStartOne(2, -20):
    raise Exception("Cannot start. Motor(2) PreStartOne returns False")

StartOne(1, 100)
StartOne(2, -20)

StartAll()

Sardana assures that the above sequence is never interrupted by other calls, like a call from a different user to get motor state.

Suppose the springfield library tells us in the documentation that:

... to move multiple motors at the same time use:

moveMultiple(seq<pair<axis, position>>)

Example:

moveMultiple([[1, 100], [2, -20]])

We can modify our motor controller to take profit of this hardware feature:

class SpringfieldMotorController(MotorController):

    def PreStartAll(self):
        # clear the local motion information dictionary
        self._moveable_info = []

    def StartOne(self, axis, position):
        # store information about this axis motion
        motion_info = axis, position
        self._moveable_info.append(motion_info)

    def StartAll(self):
        self.springfield.moveMultiple(self._moveable_info)

In case of stopping/aborting of the motors (or any other stoppable/abortable elements) the synchronization may be as important as in case of starting them. Let's take an example of a motorized two-legged table and its translational movement. A desynchronized stop/abort of the motors may introduce an extra angle of the table that in very specific cases may be not desired e.g. activation of the safety limits, closed loop errors, etc.

In this case the complete algorithm for stopping/aborting the motor motion in sardana is:

/FOR/ Each controller(s) implied in the motion

    - Call PreStopAll()

    /FOR/ Each motor of the given controller implied in the motion
        - ret = PreStopOne(motor to stop)
        - /IF/ ret is not true
            /RAISE/ Cannot stop. Motor PreStopOne returns False
        - /END IF/
        - Call StopOne(motor to stop)
    /END FOR/

    - Call StopAll()

/END FOR/

Each of the hardware controller method calls is protected in case of errors so the stopping/aborting algorithm tries to stop/abort as many axes/controllers.

A similar principle applies when sardana asks for the state and position of multiple axis. The two sets of methods are, in these cases:

  • ~sardana.pool.controller.Controller.PreStateAll
  • ~sardana.pool.controller.Controller.PreStateOne
  • ~sardana.pool.controller.Controller.StateAll
  • ~sardana.pool.controller.Controller.StateOne
  • ~sardana.pool.controller.Readable.PreReadAll
  • ~sardana.pool.controller.Readable.PreReadOne
  • ~sardana.pool.controller.Readable.ReadAll
  • ~sardana.pool.controller.Readable.ReadOne

The main differences between these sets of methods and the ones from start motion is that ~sardana.pool.controller.Controller.StateOne / ~sardana.pool.controller.Readable.ReadOne methods are called AFTER the corresponding ~sardana.pool.controller.Controller.StateAll / ~sardana.pool.controller.Readable.ReadAll counterparts and they are expeced to return the state/position of the requested axis.

The internal sardana algorithm to read position is:

/FOR/ Each controller(s) implied in the reading (executed concurrently)

     - Call PreReadAll()

    /FOR/ Each motor(s) of the given controller implied in the reading
         - PreReadOne(motor to read)
    /END FOR/

    - Call ReadAll()

    /FOR/ Each motor(s) of the given controller implied in the reading
         - ReadOne(motor to read)
    /END FOR/

/END FOR/

Here is an example assuming the springfield library tells us in the documentation that:

... to read the position of multiple motors at the same time use:

getMultiplePosition(seq<axis>) -> dict<axis, position>

Example:

positions = getMultiplePosition([1, 2])

The new improved code could look like this:

class SpringfieldMotorController(MotorController):

    def PreRealAll(self):
        # clear the local position information dictionary
        self._position_info = []

    def PreReadOne(self, axis):
        self._position_info.append(axis)

    def ReadAll(self):
        self._positions = self.springfield.getMultiplePosition(self._position_info)

    def ReadOne(self, axis):
        return self._positions[axis]