How to write a trigger/gate controller

The basics

An example of a hypothetical Springfield trigger/gate 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 TriggerGateController with:

  • a proper constructor

  • add and delete axis methods

  • get axis state

import springfieldlib

from sardana.pool.controller import TriggerGateController

class SpringfieldTriggerGateController(TriggerGateController):

    def __init__(self, inst, props, *args, **kwargs):
        super(SpringfieldTriggerGateController, self).__init__(inst, props, *args, **kwargs)

        # initialize hardware communication
        self.springfield = springfieldlib.SpringfieldTriggerHW()

        # do some initialization
        self._triggers = {}

    def AddDevice(self, axis):
        self._triggers[axis] = True

    def DeleteDevice(self, axis):
        del self._triggers[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 trigger/gate hardware access library.

The springfieldlib can be downloaded from here.

The Springfield trigger/gate controller can be downloaded from here.

The following code describes a minimal Springfield base trigger/gate controller which is able to return the state of an individual trigger as well as to start a synchronization:

class SpringfieldBaseTriggerGateController(TriggerGateController):
    """The most basic controller intended from demonstration purposes only.
    This is the absolute minimum you have to implement to set a proper trigger
    controller able to get a trigger value, get a trigger 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(SpringfieldBaseTriggerGateController, self).__init__(
            inst, props, *args, **kwargs)
        self.springfield = springfieldlib.SpringfieldTriggerHW()

    def StateOne(self, axis):
        """Get the specified trigger state"""
        springfield = self.springfield
        state = springfield.getState(axis)
        if state == 1:
            return State.On, "Trigger is stopped"
        elif state == 2:
            return State.Moving, "Trigger is running"
        elif state == 3:
            return State.Fault, "Trigger has an error"

    def StartOne(self, axis, value=None):
        """acquire the specified trigger"""
        self.springfield.StartChannel(axis)

    def SynchOne(self, axis, synchronization):
        self.springfield.SynchChannel(axis, synchronization)

    def StopOne(self, axis):
        """Stop the specified trigger"""
        self.springfield.stop(axis)

Get trigger state

To get the state of a trigger, sardana calls the StateOne() method. This method receives an axis as parameter and should return either:

  • state (State) or

  • a sequence of two elements:

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.

Prepare for measurement

To prepare a trigger for a measurement you can use the PrepareOne() method which receives as an argument the number of starts of the whole measurement. This information may be used to prepare the hardware for generating multiple events (triggers or gates) in a complex measurement e.g. Deterministic scans.

Load synchronization description

To load a trigger with the synchronization description sardana calls the SynchOne() method. This method receives axis and synchronization parameters.

Here is an example of the possible implementation of SynchOne():

class SpringfieldTriggerGateController(TriggerGateController):

    def SynchOne(self, axis, synchronization):
        self.springfield.SynchChannel(axis, synchronization)

Synchronization description

Synchronization is a data structure following a special convention. It is composed from the groups of equidistant intervals described by: the initial point and delay, total and active intervals and the number of repetitions. These information can be expressed in different synchronization domains if necessary: time and/or position.

../../_images/synchronization_description.png

This sketch depicts parameters describing a group.

Sardana defines two enumeration classes to help in manipulations of the synchronization description. The SynchParam defines the parameters used to describe a group. The SynchDomain defines the possible domains in which a parameter may be expressed.

The following code demonstrates creation of a synchronization description expressed in time and position domains (moveable’s velocity = 10 units/second and acceleration time = 0.1 second). It will generate 10 synchronization pulses of length 0.1 second equally spaced on a distance of 100 units.

from sardana.pool import SynchParam, SynchDomain

synchronization = [
    {
        SynchParam.Delay:   {SynchDomain.Time: 0.1, SynchDomain.Position: 0.5},
        SynchParam.Initial: {SynchDomain.Time: None, SynchDomain.Position: 0},
        SynchParam.Active:  {SynchDomain.Time: 0.1, SynchDomain.Position: 1},
        SynchParam.Total:   {SynchDomain.Time: 1, SynchDomain.Position: 10},
        SynchParam.Repeats: 10,
    }
]

Important

Synchronization description value in position domain is in dial position since version 3.3.3. The possibility of using it with user position was maintained as backwards compatibility but is deprecated. Backwards compatibility is only available if you don’t use the moveable_on_input trigger/gate element configuration.

Start a trigger

When an order comes for sardana to start a trigger, 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 SpringfieldTriggerGateController(TriggerGateController):

    def StartOne(self, axis):
        self.springfield.StartChannel(axis)

As soon as StartOne() is invoked, sardana expects the trigger to be running. It enters a high frequency synchronization loop which asks for the trigger 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 trigger is stopped and exit the synchronization loop.

For an synchronization to work properly, it is therefore, very important that StateOne() responds correctly.

Stop a trigger

It is possible to stop a trigger when it is running. When sardana is ordered to stop a trigger synchronization, it invokes the StopOne() method. This method receives an axis parameter. The controller should make sure the desired trigger is gracefully stopped.

Here is an example of the possible implementation of StopOne():

class SpringfieldTriggerGateController(TriggerGateController):

    def StopOne(self, axis):
        self.springfield.StopChannel(axis)

Abort a trigger

In an emergency situation, it is desirable to abort a synchronization as fast as possible. When sardana is ordered to abort a trigger synchronization, it invokes the AbortOne() method. This method receives an axis parameter. The controller should make sure the desired trigger is stopped as fast as it can be done.

Here is an example of the possible implementation of AbortOne():

class SpringfieldTriggerGateController(TriggerGateController):

    def AbortOne(self, axis):
        self.springfield.AbortChannel(axis)

Advanced topics

Coupled and multiplexor modes in position domain

Trigger/gate controller can work in either of two modes:

  • coupled - one input is coupled to one output

  • multiplexor - many inputs may produce synchronization signals on many outputs

See more details in Trigger/Gate API reference.

If hardware can work in both modes, then a dedicated axis number should be used to identify the outputs e.g. IcePAPTriggerGateController reserve axes 1, 2, 3, etc. for the coupled mode outputs and axis 0 for the multiplexor mode output.

In case of supporting the mutliplexor mode the trigger/gate controller plugin must implement the axis parameter, called active_input (settable with SetAxisPar()). Sardana will determine its value based on the moveable_on_input trigger/gate element configuration and the measurement group’s moveable attribute. Trigger/gate element may be requested to perform synchronization in Time domain only e.g. during the ct or timescan. In this case the active_input will be set to a special value None indicating no input should be used. The active_input parameter is set during the measurement preparation phase prior to setting the synchronization description.

Here is an example of a possible implementation of SetAxisPar():

class SpringfieldTriggerGateController(TriggerGateController):

    def SetAxisPar(self, axis, name, value):
        if name == "active_input":
            if value is None:
                self.springfield.DeactivateInput(axis)
            else:
                self.springfield.SetActiveInput(axis, value)