How to write a counter/timer controller¶
The basics¶
An example of a hypothetical Springfield counter/timer controller will be build incrementally from scratch to aid in the explanation.
By now you should have read the general controller basics chapter. You should be able to create a CounterTimerController with:
- a proper constructor,
- add and delete axis methods
- get axis state
import springfieldlib
from sardana.pool.controller import CounterTimerController
from sardana import State
class SpringfieldCounterTimerController(CounterTimerController):
def __init__(self, inst, props, *args, **kwargs):
super(SpringfieldCounterTimerController, self).__init__(inst, props, *args, **kwargs)
# initialize hardware communication
self.springfield = springfieldlib.SpringfieldCounterHW()
# do some initialization
self._counters = {}
def AddDevice(self, axis):
self._counters[axis] = True
def DeleteDevice(self, axis):
del self._counters[axis]
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)
return state, status
The examples use a springfieldlib
module which emulates a counter/timer
hardware access library.
The springfieldlib
can be downloaded from
here
.
The Springfield counter/timer controller can be downloaded from
here
.
The following code describes a minimal Springfield base counter/timer controller which is able to return both the state and value of an individual counter as well as to start an acquisition:
class SpringfieldBaseCounterTimerController(CounterTimerController):
"""The most basic controller intended from demonstration purposes only.
This is the absolute minimum you have to implement to set a proper counter
controller able to get a counter value, get a counter state and do an
acquisition.
This example is so basic that it is not even directly described in the
documentation"""
def __init__(self, inst, props, *args, **kwargs):
"""Constructor"""
super(SpringfieldBaseCounterTimerController,
self).__init__(inst, props, *args, **kwargs)
self.springfield = springfieldlib.SpringfieldCounterHW()
def ReadOne(self, axis):
"""Get the specified counter value"""
return self.springfield.getValue(axis)
def StateOne(self, axis):
"""Get the specified counter state"""
springfield = self.springfield
state = springfield.getState(axis)
if state == 1:
return State.On, "Counter is stopped"
elif state == 2:
return State.Moving, "Counter is acquiring"
elif state == 3:
return State.Fault, "Counter has an error"
def StartOne(self, axis, value=None):
"""acquire the specified counter"""
self.springfield.StartChannel(axis)
def LoadOne(self, axis, value, repetitions):
self.springfield.LoadChannel(axis, value)
def StopOne(self, axis):
"""Stop the specified counter"""
self.springfield.stop(axis)
Get counter state¶
To get the state of a counter, sardana calls the
StateOne()
method. This method
receives an axis as parameter and should return either:
The state should be a member of State
(For backward
compatibility reasons, it is also supported to return one of
PyTango.DevState
). The status could be any string.
Load a counter¶
To load a counter with either the integration time or the monitor counts,
sardana calls the LoadOne()
method.
This method receives axis, value and repetitions parameters. For the moment
let’s focus on the first two of them.
Here is an example of the possible implementation of
LoadOne()
:
class SpringfieldCounterTimerController(CounterTimerController):
def LoadOne(self, axis, value, repetitions):
self.springfield.LoadChannel(axis, value)
Get counter value¶
To get the counter value, sardana calls the
ReadOne()
method. This method
receives an axis as parameter and should return a valid counter value. Sardana
notifies the pseudo counters about the new counter value so they can be updated
(see Pseudo counter overview for more details).
Here is an example of the possible implementation of
ReadOne()
:
class SpringfieldCounterTimerController(CounterTimerController):
def ReadOne(self, axis):
value = self.springfield.getValue(axis)
return value
Start a counter¶
When an order comes for sardana to start a counter, sardana will call the
StartOne()
method. This method receives
an axis as parameter. The controller code should trigger the hardware acquisition.
Here is an example of the possible implementation of
StartOne()
:
class SpringfieldCounterTimerController(CounterTimerController):
def StartOne(self, axis, value):
self.springfield.StartChannel(axis)
As soon as StartOne()
is invoked,
sardana expects the counter to be acquiring. It enters a high frequency acquisition
loop which asks for the counter state through calls to
StateOne()
. It will keep the loop
running as long as the controller responds with State.Moving
.
If StateOne()
raises an exception
or returns something other than State.Moving
, sardana will assume the counter
is stopped and exit the acquisition loop.
For an acquisition to work properly, it is therefore, very important that
StateOne()
responds correctly.
Stop a counter¶
It is possible to stop a counter when it is acquiring. When sardana is ordered to
stop a counter acquisition, it invokes the StopOne()
method. This method receives an axis parameter. The controller should make
sure the desired counter is gracefully stopped.
Here is an example of the possible implementation of
StopOne()
:
class SpringfieldCounterTImerController(CounterTimerController):
def StopOne(self, axis):
self.springfield.StopChannel(axis)
Abort a counter¶
In an emergency situation, it is desirable to abort an acquisition
as fast as possible. When sardana is ordered to abort a counter acquisition,
it invokes the AbortOne()
method. This method receives an axis parameter. The controller should make
sure the desired counter is stopped as fast as it can be done.
Here is an example of the possible implementation of
AbortOne()
:
class SpringfieldCounterTimerController(CounterTimerController):
def AbortOne(self, axis):
self.springfield.AbortChannel(axis)
Timer and monitor roles¶
Usually counters can work in either of two modes: timer or monitor. In both of
them, one counter in a group is assigned a special role to control when
the rest of them should stop counting. The stopping condition is based on the
integration time in case of the timer or on the monitor counts in case of the
monitor. The assignment of this special role is based on the measurement group
Configuration. The controller receives
this configuration (axis number) via the controller parameter timer
and monitor
. The currently used acquisition mode is set via the controller
parameter acquisition_mode
.
Advanced topics¶
Timestamp a counter value¶
When you read the value of a counter from the hardware sometimes it is necessary to associate a timestamp with that value so you can track the value of a counter in time.
If sardana is executed as a Tango device server, reading the value
attribute from the counter device triggers the execution of your controller’s
ReadOne()
method. Tango responds with
the value your controller returns from the call to
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
ReadOne()
an object that contains both
the value and the timestamp instead of the usual numbers.Number
.
The object must be an instance of SardanaValue
.
Here is an example of associating a timestamp in
ReadOne()
:
import time
from sardana.pool.controller import SardanaValue
class SpringfieldCounterTimerController(CounterTimerController):
def ReadOne(self, axis):
return SardanaValue(value=self.springfield.getValue(axis),
timestamp=time.time())
If your controller communicates with a Tango device, Sardana also supports
returning a DeviceAttribute
object. Sardana will use this
object’s value and timestamp. Example:
class TangoCounterTimerController(CounterTimerController):
def ReadOne(self, axis):
return self.device.read_attribute("value")
Multiple acquisition synchronization¶
This chapter describes an extended API that allows you to better synchronize acquisitions involving more than one counter, 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
acquire more than one counter at the same time
(see Measurement group overview).
Imagine that the user requires counter at axis 1 and counter at axis 2 to be
acquired.
Your controller will receive two consecutive calls to
StartOne()
:
StartOne(1)
StartOne(2)
and each StartOne will probably connect to the hardware (through serial line, socket, Tango or EPICS) and ask the counter to be started. This will do the job but, there will be a slight desynchronization between the two counters because hardware call of counter 1 will be done before hardware call to counter 2.
Sardana provides an extended start acquisition which gives you the possibility to improve the synchronization (and probably reduce communications) but your hardware controller must somehow support this feature as well.
The complete start acquisition API consists of four methods:
Except for StartOne()
, the
implementation of all other start methods is optional and their default
implementation does nothing (PreStartOne()
actually returns True
).
So, actually, a simplified algorithm for counter acquisition start in sardana is:
/FOR/ Each controller(s) implied in the acquisition
- Call PreStartAll()
/END FOR/
/FOR/ Each counter(s) implied in the acquisition
- ret = PreStartOne(counter to acquire, new position)
- /IF/ ret is not true
/RAISE/ Cannot start. Counter PreStartOne returns False
- /END IF/
- Call StartOne(counter to acquire, new position)
/END FOR/
/FOR/ Each controller(s) implied in the acquisition
- Call StartAll()
/END FOR/
So, for the example above where we acquire two counters, the complete sequence of calls to the controller is:
PreStartAll()
if not PreStartOne(1):
raise Exception("Cannot start. Counter(1) PreStartOne returns False")
if not PreStartOne(2):
raise Exception("Cannot start. Counter(2) PreStartOne returns False")
StartOne(1)
StartOne(2)
StartAll()
Sardana assures that the above sequence is never interrupted by other calls, like a call from a different user to get counter state.
Suppose the springfield library tells us in the documentation that:
… to acquire multiple counters at the same time use:
startCounters(seq<axis>)Example:
startCounters([1, 2])
We can modify our counter controller to take profit of this hardware feature:
class SpringfieldCounterTimerController(MotorController):
def PreStartAll(self):
# clear the local acquisition information dictionary
self._counters_info = []
def StartOne(self, axis):
# store information about this axis motion
self._counters_info.append(axis)
def StartAll(self):
self.springfield.startCounters(self._counters_info)
Hardware synchronization¶
The synchronization achieved in Multiple acquisition synchronization may not be enough when it comes to acquiring with multiple controllers at the same time or to executing multiple acquisitions in a row. Some of the controllers can be synchronized on an external hardware event and in this case several important aspects needs to be taken into account.
Synchronization type¶
First of all the controller needs to know which type of synchronization will
be used. This is assigned on the measurement group
Configuration level. The controller
receives one of the AcqSynch
values via the
controller parameter synchronization
.
The selected mode will change the behavior of the counter after the
StartOne()
is invoked. In case one of
the software modes was selected, the counter will immediately start acquiring.
In case one of the hardware modes was selected, the counter will immediately
get armed for the hardware events, and will wait with the acquisition until they
occur.
Here is an example of the possible implementation of
SetCtrlPar()
:
from sardana.pool import AcqSynch
class SpringfieldCounterTimerController(CounterTimerController):
SynchMap = {
AcqSynch.SoftwareTrigger : 1,
AcqSynch.SoftwareGate : 2,
AcqSynch.HardwareTrigger: 3,
AcqSynch.HardwareGate: 4
}
def SetCtrlPar(self, name, value):
super(SpringfieldMotorController, self).SetCtrlPar(name, value)
synchronization = SynchMap[value]
if name == "synchronization":
self.springfield.SetSynchronization(synchronization)
Multiple acquisitions¶
It is a very common scenario to execute multiple hardware synchronized
acquisitions in a row. One example of this type of measurements are the
Continuous scans. The controller receives the number of
acquisitions via the third argument of the
LoadOne()
method.
Here is an example of the possible implementation of
LoadOne()
:
class SpringfieldCounterTimerController(CounterTimerController):
def LoadOne(self, axis, value, repetitions):
self.springfield.LoadChannel(axis, value)
self.springfield.SetRepetitions(repetitions)
return value
Get counter values¶
During the hardware synchronized acquisitions the counter values are usually
stored in the hardware buffers. Sardana enters a high frequency acquisition loop
after the StartOne()
is invoked
which, apart of asking for the counter state through calls to the
StateOne()
method, will try to retrieve
the counter values using the ReadOne()
method.
It will keep the loop running as long as the controller responds with State.Moving
.
Sardana executes one extra readout after the state has changed in order to retrieve
the final counter values.
The ReadOne()
method is used indifferently
of the selected synchronization but its return values should depend on it and
can be:
- a single counter value: either
float
orSardanaValue
in case of theSoftwareTrigger
orSoftwareGate
synchronization- a sequence of counter values: either
float
orSardanaValue
in case of theHardwareTrigger
orHardwareGate
synchronization
Sardana assumes that the counter values are returned in the order of acquisition and that there are no gaps in between them.
Todo
document how to skip the readouts while acquiring