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 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 bysteps_per_units
value).- If
set_velocity()
method is defined, then velocity parameter is mandatory in config.
- Must set velocity of
-
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 bysteps_per_units
value).- If
set_acceleration()
function is defined, then acceleration parameter is mandatory in config.
- Must set acceleration of
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 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.
- Must set controller position to
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 movedmotion.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 ofmotion
- Called in a group move
move(m1, 3, m2, 1)
is a group movemove(m1, 3)
is a group move as well asumvr()
mvr()
- it uses
Group.move()
- it uses
m1.move(3)
is a single move (usesAxis.move()
)
- Return nothing
- Must start all movements for all axes defined in
-
stop_all(self, *motions)
- Must stop all movements defined in
motions
- Called on a
ctrl-c
during a group move
- Must stop all movements defined in
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 callsstop_jog()
. - Called by
axis.jog()
function.
- Must start a “jog” movement: an unfinished movement at
-
stop_jog(self, axis)
- Must stops a jog motion.
- Called by
axis.stop()
oraxis.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
- Must return
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 returnTrue
if capable.set_trajectory_events
register events on the trajectory given has argument. Uses events_positions or events_pattern_positions ofTrajectory
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)
- Must start a home search in the positive direction if
home_state(self, axis)
- Must return the
MOVING
state when still performing home search, and theREADY
state when homing is finished - Called by axis when polling to wait end of home search
- Must return the
limit_search(self, axis, limit)
- Must move axis to the hardware limit (positive if
limit
>0, negative otherwise) - Called by
axis.hw_limit(limit)
- Must move axis to the hardware limit (positive if
Encoder methods¶
-
initialize_encoder(self, encoder)
- Must perform init task related to encoder.
- Called at first usage of the encoder
read()
ormeasured_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 byaxis.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)
- Must set the encoder position to
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
- Must return the list of keys that should be present in
-
def _do_get_closed_loop_state(self, axis)
- Return a
ClosedLoopState
enum by requesting hardware on the actual closed loop state
- Return a
-
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
- Activate or deactivate the closed loop depending on
-
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
- Raise a KeyError if
-
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
- Raise a KeyError if
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.
- Must send the
-
raw_write_read(self, com)
- Must send the
com
command and return the answer of the controller. - Called by user.
- Must send the
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.