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/DeleteDeviceare called when an axis is created or removed (a real controller would open/close the hardware connection here).StateOnereturns the state of one axis. While you reportState.Moving, Sardana keeps polling; anything else ends the motion.ReadOnereturns the current position of one axis.StartOnereceives the target position and must trigger the motion in the hardware – it must not wait for it to finish.StopOne/AbortOnestop 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.