Write your first controller

A controller is a small Python plugin that translates between the Sardana API and the API of a specific piece of hardware. By writing one class with a handful of methods you make a whole family of devices – motors, counters, detectors – available to every macro, scan and GUI in the system.

In this lesson you will write a minimal motor controller that simulates its hardware in pure software, load it into the Pool, create a motor from it and move that motor from Spock. You need the simulated beamline from Set up a simulated beamline up and running.

The controller class

A motor controller must inherit from MotorController (other element types have their own base classes: CounterTimerController, TwoDController, …). Each controller instance manages a set of axes – in our case, motors – identified by a number.

Create a directory for your controllers, for example:

mkdir -p /home/homer/sardana/controllers

and create the file /home/homer/sardana/controllers/tutmotctrl.py:

import time

from sardana import State
from sardana.pool.controller import MotorController


class TutorialMotorController(MotorController):
    """A minimal motor controller for the Sardana tutorial.

    Instead of talking to real hardware it simulates motors that
    "arrive" at the target position after a constant-velocity delay.
    """

    MaxDevice = 4  # this controller supports up to 4 axes

    VELOCITY = 10.0  # simulated velocity in units/s

    def __init__(self, inst, props, *args, **kwargs):
        super().__init__(inst, props, *args, **kwargs)
        self._motors = {}

    def AddDevice(self, axis):
        self._motors[axis] = {"position": 0.0, "end_time": 0.0}

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

    def StateOne(self, axis):
        if time.time() < self._motors[axis]["end_time"]:
            return State.Moving, "Motor is moving"
        return State.On, "Motor is ready"

    def ReadOne(self, axis):
        return self._motors[axis]["position"]

    def StartOne(self, axis, position):
        motor = self._motors[axis]
        travel_time = abs(position - motor["position"]) / self.VELOCITY
        motor["end_time"] = time.time() + travel_time
        motor["position"] = position

    def StopOne(self, axis):
        self._motors[axis]["end_time"] = 0.0

    def AbortOne(self, axis):
        self._motors[axis]["end_time"] = 0.0

The methods map directly onto what Sardana needs from a motor:

  • AddDevice / DeleteDevice are called when an axis is created or removed (a real controller would open/close the hardware connection here).

  • StateOne returns the state of one axis. While you report State.Moving, Sardana keeps polling; anything else ends the motion.

  • ReadOne returns the current position of one axis.

  • StartOne receives the target position and must trigger the motion in the hardware – it must not wait for it to finish.

  • StopOne / AbortOne stop a motion gracefully / as fast as possible.

Note

To keep the example short, our simulated motor “jumps” to the target position immediately and merely reports Moving for the travel time. A real controller would read back the actual instantaneous position from the hardware in ReadOne.

Tell the Pool where to find it

Just like macros, controller plugins are discovered through a list of directories: the PoolPath property of the Pool Tango device. Configure it once, from any Python or itango console:

import tango
pool = tango.DeviceProxy("Pool/demo1/1")
pool.put_property({"PoolPath": ["/home/homer/sardana/controllers"]})

and restart the Sardana demo1 server. After the restart you can verify that your class was found:

Door_demo1_1 [1]: lsctrllib Tutorial.*
          Name                            Type      Library    Family
-------------------------- ------------------ ------------ ---------
TutorialMotorController                 Motor   tutmotctrl   Generic

Hint

After editing the controller file, reload it without restarting the server using the relctrllib tutmotctrl macro (or addctrllib for a library added after the server started).

Create a controller and a motor

A controller class becomes usable hardware when you create a controller instance and then elements (axes) on it. From Spock, use defctrl:

Door_demo1_1 [2]: defctrl TutorialMotorController tutctrl01
Created tutctrl01 in Pool_demo1_1

and then create a motor on axis 1 with defm (defelem is the generic equivalent for any element type):

Door_demo1_1 [3]: defm tutmot01 tutctrl01 1
Created tutmot01 in Pool_demo1_1

Move it!

Your home-made motor is now a first-class Sardana citizen. It shows up in lsm and wa next to the demo motors, and every standard macro works with it:

Door_demo1_1 [4]: umv tutmot01 5
    tutmot01
      5.0000

Door_demo1_1 [5]: wm tutmot01
              tutmot01
User
 High             Not specified
 Current                 5.0000
 Low              Not specified

When you are done experimenting, you can remove the motor and the controller with udefelem tutmot01 followed by udefctrl tutctrl01.

Next steps

You have closed the loop: hardware plugin, server, macro, client. Continue with Run your first scan to combine motors and counters in scans. To write controllers for real hardware – extra attributes and properties, error handling, other element types like counters or 2D detectors – read the general controller overview and the full motor controller how-to, and check the Sardana plugins catalogue first: a controller for your hardware may already exist.