Skip to content

Writing a motor controller

Motors control in BLISS has been split in two parts:

  • The Axis class (bliss.common.axis module) in charge of the interaction with a single motor (aka axis) with taking care of most typical motion concepts: velocity, acceleration, backlash, limits, etc.
  • The Controller class (bliss.controllers.motor module) implementing functions of the hardware controller who drives the motors (aka axes).

To create a new motor controller, developers have to create a new controller class inheriting from the Controller base class and to implement mandatory methods. In addition, some custom methods can be defined for features that are specific to the new controller.

Usually, it is not necessary to override the Axis class which is designed as a generic interface for all motors, hiding controller’s specificities.

Note about units management

  • On the user point of view, axes are moved in user units, whatever unit is used in the controller API
  • On the programmer point of view, the motor controller class is dealing with controller units (steps, microns, …)
  • The programmer should not have to deal with units conversions.
  • see motion axis / position for more details.

New controller implementation

To write a new motor controller, the first thing to do is to implement a new controller class by inheriting from the motor controller base class. Then, override the NotImplemented methods of the Controller base class.

Motor controller child class

from bliss.controllers.motor import Controller
from bliss.comm.util import get_comm
from bliss.common.axis.state import AxisState

class MyMotorController(Controller):
    def __init__(self, config):
        super().__init__(config)

    def initialize_axis(self, axis):
        ...

Controller and axis initialization

The motor controller class relies on the ConfigItemContainer base class in association with the generic plugin.

It means that, when an axis is imported in a BLISS session, the controller is automatically created first (if not alive already). Then, the plugin asks the controller object to create and return the requested axis.

Usually the controller is not named and not directly imported in a session but there is no restriction to do so. The controller instantiation does not trigger the instantiation of its axes, they are created on demand (lazy init).

Initialization sequence

  • initialize(): called during creation of the controller object.
  • At first access to one of the controller’s axes, hardware initialization is performed in following order:
    • initialize_hardware(): called once, if not already done.
    • initialize_axis(): called once per axis at first use of this axis.
    • set_velocity() and set_acceleration() with settings values if these methods have been implemented.
    • limits application
    • initialize_hardware_axis(): called once per axis.

Initialization methods

  • __init__(self, name, config, axes)

    • Initialization of internal class attributes
  • initialize(self)

    • Pure software initialization (like communication instantiation). No access to physical hardware must be done at this stage.
    • Called at axes creation (only once, if not already initialized)
  • initialize_hardware(self)

    • Must check that controller is responding and initializes the controller.
    • It is executed only once per controller, on first access to any of the defined axes.
  • initialize_axis(self, axis)

    • Software initialization of an axis.
    • Called once per defined axis.
  • initialize_hardware_axis(self, axis)

    • Hardware initialization of one axis. Typically, power-on the axis, initialize the closed-loop, set a PID, etc.
    • Called once per defined axis.
  • set_on(self, axis)

    • Must enable the given axis (i.e. activate, power on, …)
  • set_off(self, axis)

    • Must disable the given axis (power off, breaks ? park ?)

Velocity/Acceleration methods

  • read_velocity(self, axis)

    • Must return the velocity read from the motor controller in controller unit per second
  • set_velocity(self, axis, new_velocity)

    • Must set velocity of <axis> in the controller to <new_velocity>.
    • <new_velocity> is given in controller unit per second (i.e. user units per second multiplied by steps_per_units value).
    • If set_velocity() method is defined, then velocity parameter is mandatory in config.
  • read_acceleration(self, axis)

    • Must return acceleration read from the motor controller in controller unit per second**2
  • set_acceleration(self, axis, new_acc)

    • Must set acceleration of <axis> in the controller to <new_acc>.
    • <new_acc> is given in controller unit per second**2 (i.e. user unit per second**2 multiplied by steps_per_units value).
    • If set_acceleration() function is defined, then acceleration parameter is mandatory in config.

Motion commands

Status and position methods

  • state(self, axis)

    • Must return an AxisState() object. AxisState() has the following standard states

      • MOVING: Axis is moving
      • READY: Axis is ready to be moved (not moving ?)
      • FAULT: Error from controller
      • LIMPOS: Hardware high limit active
      • LIMNEG: Hardware low limit active
      • HOME: Home signal active
      • OFF: Axis is disabled (must be enabled to move (not ready ?))
    • To allow a motion, the axis must be in READY state and not have one of LIMPOS or LIMNEG states. Once a motion is started, state is switched to MOVING. Motion will be considered finished when the MOVING state disappear.

    • Any controller can add its own state to inform user of current controller state. For example:

      Example

      from bliss.common.axis.state import AxisState
      state = AxisState()
      state.create_state("CLOSED_LOOP_ERROR", "Error on axis closed loop")
      state.create_state("HOMING_DONE", "Homing has been performed")
      
    • To activate one of those new states:

      state.set("CLOSED_LOOP_ERROR")
      
  • read_position(self, axis)

    • Must return the current position (read from the controller) of the axis in controller units
    • Called before and after a movement.
  • set_position(self, axis, new_position)

    • Must set controller position to new_position
    • Must return the current position of the axis in controller units
    • Called when changing the dial position of the controller.

single axis motion

  • prepare_move(self, motion)

    • Must prepare a movement
    • Can be used to arm a movement
    • Called just before start_one().
  • start_one(self, motion)

    • Must send to controller a start on one axis.
    • Called in commands like mymot.move(10.0)
    • NOT called in commands move(axis, pos)
  • stop(self, axis)

    • Must send a command to the controller to halt this axis movement
    • Called on a ctrl-c

Motion object holds requested motion parameters

  • motion.axis: axis object to be moved
  • motion.target_pos: absolute motion target position (controller unit)
  • motion.delta: corresponding relative motion delta (controller unit)

Group motion

  • start_all(self, *motions)

    • Must start all movements for all axes defined in motions
    • Multiple starts can be optimized on controllers allowing multi-axes commands
    • motions is a tuple of motion
    • Called in a group move
      • move(m1, 3, m2, 1) is a group move
      • move(m1, 3) is a group move as well as umvr() mvr()
        • it uses Group.move()
      • m1.move(3) is a single move (uses Axis.move())
    • Return nothing
  • stop_all(self, *motions)

    • Must stop all movements defined in motions
    • Called on a ctrl-c during a group move

Info

If start_all() is not defined, the movement is performed with start_one()

def _start_one_controller_motions(self, controller, motions):
    try:
        controller.start_all(*motions)
    except NotImplementedError:
        for motion in motions:
            controller.start_one(motion)

Jog motion

A Jog motion is a movement controlled in velocity instead of being controller in position.

  • start_jog(self, axis, velocity, direction)

    • Must start a “jog” movement: an unfinished movement at velocity speed. Movement will be finished when user calls stop_jog().
    • Called by axis.jog() function.
  • stop_jog(self, axis)

    • Must stops a jog motion.
    • Called by axis.stop() or axis.stop_jog()

Trajectory motion

The trajectory methods are used by the TrajectoryGroup class.

In Bliss, two types of trajectories can be send to a controller: Trajectory which defines one continuous movement and CyclicTrajectory which defines a trajectory pattern with a number of cycles.

Trajectory

The movement is defined by a numpy array (PVT) containing Position, Velocity and Time parameters.

This object has the following arguments and properties:

  • axis instance
  • pvt: a (position, velocity, time) numpy array
  • events_positions (property): list of PVT triplets where the controller should send events when axes reach this triplet during a trajectory motion

CyclicTrajectory

This object has the following arguments and properties:

  • origin: the absolute starting position
  • pvt_pattern (property): a numpy PVT array relative to the origin position
  • nb_cycles: number of iteration for the pvt_pattern
  • is_closed (property): True if trajectory is closed, ie: first point = last point
  • events_pattern_positions (property): list of event for this trajectory pattern
  • pvt (property): full trajectory, this one is calculated to help controller which doesn’t managed trajectory pattern
  • events_positions (property): list of all events on the full trajectory, same as above, it’s calculated

Involved methods

Methods to implement in the controller:

  • has_trajectory(self):
    • Must return True if motor controller supports trajectories
  • prepare_trajectory(self, *trajectories):
    • Must prepare the controller to perform given trajectories
  • move_to_trajectory(self, *trajectories):
    • Must move to the first (or starting) point of the trajectories
  • start_trajectory(self, *trajectories):
    • Must move motor(s) along trajectories to the final position(s)
  • stop_trajectory(self, *trajectories):
    • Must interrupt running trajectory motion

Example

def prepare_trajectory(self, *trajectories):
    for traj in trajectories:
        axis = traj.axis #get the axis for that trajectory
        pvt = traj.pvt # get the trajectory array
        times = pvt['time'] # the timing array (absciss)
        positions = pvt['position'] # all the axis positions
        velocities = pvt['velocity'] # all axis velocity (trajectory slope)

When the Bliss core ask a controller to move its axis in trajectory, the calling sequence is fixed to:

  • prepare_trajectory()
  • move_to_trajectory()
  • start_trajectory()
  • eventually stop_trajectory() in case of movement interruption.

Event on trajectory

Methods to implement in the controller:

  • has_trajectory_event should return True if capable.
  • set_trajectory_events register events on the trajectory given has argument. Uses events_positions or events_pattern_positions of Trajectory object.

Calibration methods

  • home_search(self, axis, direction)
    • Must start a home search in the positive direction if direction>0, negative otherwise
    • Called by axis.home(direction)
  • home_state(self, axis)
    • Must return the MOVING state when still performing home search, and the READY state when homing is finished
    • Called by axis when polling to wait end of home search
  • limit_search(self, axis, limit)
    • Must move axis to the hardware limit (positive if limit>0, negative otherwise)
    • Called by axis.hw_limit(limit)

Encoder methods

  • initialize_encoder(self, encoder)

    • Must perform init task related to encoder.
    • Called at first usage of the encoder
      • read() or measured_position() of related axis if linked to an axis.
  • read_encoder(self, encoder)

    • Must return the encoder position in encoder_steps
    • Called by encoder.read() method by exported Encoder object or by axis.measured_position() of related axis
    • encoder.read() is called at the end of a motion to check if final position has been reached.
  • set_encoder(self, encoder, new_value)

    • Must set the encoder position to new_value
    • new_value is in encoder_steps
    • Called by encoder.set(new_value)

Closed loop methods

  • def get_closed_loop_requirements(self)

    • Must return the list of keys that should be present in closed_loop config section. Note that state key should not be part of the list as it is mandatory in any closed loop
  • def _do_get_closed_loop_state(self, axis)

    • Return a ClosedLoopState enum by requesting hardware on the actual closed loop state
  • def activate_closed_loop(self, axis, onoff=True)

    • Activate or deactivate the closed loop depending on onoff value (True -> activate)
    • Raise an exception whenever it can’t apply the requested state
  • def set_closed_loop_param(self, axis, param, value)

    • Raise a KeyError if param is not in get_closed_loop_requirements(axis)
    • Set the corresponding param value on the hardware
  • def get_closed_loop_param(self, axis, param)

    • Raise a KeyError if param is not in get_closed_loop_requirements(axis)
    • return the corresponding param value by requesting the hardware

Information methods

  • get_id(self, axis)
  • get_info(self, axis)
    • Must return printable info for axis
    • Called by axis.get_info()

Direct communication methods

These methods allow to send arbitrary commands and read responses from the controller.

They can be useful to test, to debug or to tune a controller.

  • raw_write(self, com)

    • Must send the com command.
    • Called by user.
  • raw_write_read(self, com)

    • Must send the com command and return the answer of the controller.
    • Called by user.

Position triggers

  • set_event_positions(self, axis_or_encoder, positions)

    • This method is use to load into the controller a list of positions for event/trigger. The controller should generate an event (mainly electrical pulses) when the axis or the encoder pass through one of this position.
  • get_event_positions(self, axis_or_encoder)

Custom commands

The object_method decorator is used to create custom commands.

Example of custom command

@object_method(types_info=("None","int"))
def get_user_mode(self,axis):
    return int(self._query("UM"))

types_info parameter of this decorator allows to define types of parameters used by the created controller command.