Skip to content

Writing a controller with counters

This section describes the implementation of a controller with counters in BLISS.

If you read this lines for the first time, please have a look to the General introduction first

Start from BlissController

For the implementation of controllers with counters, BLISS provides the BlissController base class. It inherits from the ConfigItemContainer class (for the management of named counters) and from the CounterContainer protocol (for compatibility with scans and counters).

Screenshot

Example of the YAML configuration of a BlissController

- class: FooController   # object class (inheriting from BlissController)
  plugin: generic        # BlissController works with generic plugin
  module: foo_module     # module where the FooController class can be found
  name: foo              # a name for the controller (optional)

  counters:              # a section to declare counters
    - name: cnt_hv       # name for a counter
      tag: hv            # a tag to identify this counter within the controller
    - name: cnt_cur      # a name for another counter
      tag: cur           # a tag to identify this counter within the controller

The signature of a BlissController takes a single argument config. It could be a ConfigNode object (from YAML configuration file) or a standard dictionary.

Inherit from BlissController

from bliss.controllers.bliss_controller import BlissController

class FooController(BlissController):
    def __init__(self, config):
        super().__init__(config)

Most of the time your controller will rely on a device to perform the measurements associated to the counters. All the methods to interact with this device should not be implemented in this controller. Instead, write a dedicated class for this device following this link writing a hardware interface.

Distinguish low level device and higher level BLISS functionalities as much as possible

It is very important to avoid mixing the low level commands of a (hardware) device with all BLISS mechanisms. The device object does not need to know anything about BLISS mechanisms and it should be kept decorrelated. That way, it will be much easier to re-use the device’s code in another context such as a server process (TANGO or RPC).

Once you have a proper object class for your device (e.g. FooDevice), it can be added as an attribute of your BlissController as shown below.

declare and access the hardware device

from bliss import global_map
from bliss.controllers.bliss_controller import BlissController

class FooDevice:
    def __init__(self, config):
        ...

class FooController(BlissController):
    def __init__(self, config):
        super().__init__(config)
        self._hw_controller = None

    @property
    def hardware(self):
        if self._hw_controller is None:
            self._hw_controller = FooDevice(self.config)
            global_map.register(self, children_list=[self._hw_controller])
        return self._hw_controller

Global map registration

The command global_map.register(self, children_list=[self._hw_controller]) will ensure that when debugging is activated on this controller, it will also activate debugging on the FooDevice object. For more details see global map registration.

Handle Counters

In BLISS, Counters objects can be of different types depending on the data that are collected. A group of counters of the same type is managed by a dedicated CounterController object.

For example, measurements that can be done instantaneously are usually performed with sampling counters and they are managed by a sampling counter controller, whereas integrated measurements are usually performed with integrating counters managed by an integrating counter controller.

For a general presentation of available counters and usage example, see this section.

Declare CounterControllers

To handle counters in a bliss controller, developers must declare one counter controller per type of counter. A counter controller is declared as an attribute of the bliss controller.

Below, an example of a controller that needs to handle two kind of counters. Therefore, two counter controllers are declared. One for sampling counters (MySCC) and another one for integrating counters (MyICC).

Declare CounterController(s)

from bliss.controllers.counter import SamplingCounterController, IntegratingCounterController

class FooController(BlissController):
    def __init__(self, config):
        super().__init__(config)
        self._hw_controller = None
        self._myscc = MySCC(self.name, self) # <= instantiate a counter controller object
        self._myicc = MyICC(self.name, self) # <= instantiate another type of counter controller


# Define a counter controller class for sampling counters
class MySCC(SamplingCounterController):
    """A default sampling counter controller"""
    def __init__(self, name, bctrl):
        super().__init__(name)
        self.bctrl = bctrl    # <= bliss controller object reference (FooController)

    def read(self, counter):
        """Read and return the value of the given counter"""
        return self.bctrl._read(counter)   # <= a method to read counter value

# Define another counter controller class for integrating counters
class MyICC(CounterController):
    """A default integrating counter controller"""
    def __init__(self, name, bctrl):
        super().__init__(name)
        self.bctrl = bctrl    # <= bliss controller object reference (FooController)

    def get_values(self, from_index, *counters):
        """Read and return all values from last index for a given list of counters"""
        return self.bctrl._read_values(from_index, *counters) # <= a method to read counters values

Hide counter controller attribute to users

It is recommended to use a underscore for the attribute’s name in order to hide this object to users while using completion on the bliss controller object: self._scc = MySCC(...)

About accessing counters values from counter controllers

While implementing a counter controller class, developers must define how to read the data associated to a counter (like in read and get_values methods above). Usually, this will be done by calling a method of the bliss controller (via self.bctrl) or of the underlying device known by the bliss controller (via self.bctrl._hw_controller).

Define special configuration keys

Counters will be loaded from the list of counters found in the controller’s YAML configuration.

  • Decide the name of a parent key that will identify the section declaring the list of counters. In the example below, parent key is counters.

  • Introduce one or more special key in the counter’s configuration to identify this counter (role and type) in the bliss controller context (remember that counter’s name is chosen by users). In the example below, role/type key is tag.

A controller’s configuration declaring counters

- class: FooController   
  plugin: generic        
  module: foo_module     
  name: foo              

  counters:              # one parent_key for all counters 
    - name: cnt_hv       
      tag: hv            # a tag to identify this counter within the controller
      unit: V
      mode: SINGLE
    - name: cnt_cur      
      tag: cur           # a tag to identify this counter within the controller
      unit: mA
    - name: cnt_sum       
      tag: sum           # a tag to identify this counter within the controller
    - name: cnt_bla      
      tag: bla           # a tag to identify this counter within the controller

It could have been decided, that two special keys is better, one for the role and one for the type.

Alternative counters declaration with more special keys

- class: FooController   
  plugin: generic        
  module: foo_module     
  name: foo              

  counters:              # one parent_key for all counters 
    - name: cnt_hv       
      role: hv            # identify the counter to measure high voltage
      type: scc           # identify that's a counter of type sampling
      unit: V
      mode: SINGLE
    - name: cnt_cur      
      role: cur           # identify the counter to measure current
      type: scc           # identify that's a counter of type sampling
      unit: mA

    - name: cnt_sum       
      role: sum           # identify the counter to measure events sum
      type: icc           # identify that's a counter of type integrating
    - name: cnt_bla      
      role: bla           # identify the counter to measure bla
      type: icc           # identify that's a counter of type integrating

It also possible to separate different kind of counters below different parent_key. But usually it is not necessary because the special keys in the counter’s configuration are enough.

Alternative counters declaration with more parent keys

- class: FooController   
  plugin: generic        
  module: foo_module     
  name: foo              

  sampling_counters:     # a section to declare sampling counters
    - name: cnt_hv       
      tag: hv            
      unit: V
      mode: SINGLE
    - name: cnt_cur      
      tag: cur           
      unit: mA

  integrating_counters:  # a section to declare integrating counters
    - name: cnt_sum       
      tag: sum            
    - name: cnt_bla      
      tag: bla           

About special keys

Developers can introduce as many special keys as they want. The only restriction concerns the name of a key which should be different from {name, class, plugin, module, package}.

Read the configuration

Once the counters special keys are defined, we can implement the method that will read the configuration and create the counters. The BlissController base class has a _load_config method to implement for this purpose.

Counters are just one kind of controller’s subitems

From the configuration point of view, a counter is just one kind of subitem. A BlissController inherits from ConfigItemContainer, so it already has dedicated base class methods to deal with subitems.

Implement base class _load_config method

class FooController(BlissController):
    ...

    def _load_config(self):
        """
        Place holder to read and apply the configuration
        """
        ...

        # iterate the list of counters found in the config below the 'counters' parent key
        for cfg in self.config["counters"]: # cfg is the config of one counter
            # call the base class method that returns a controller's subitem from its name.
            self._get_subitem(cfg["name"])
            # As this subitem does not exist yet, it will call the base class 
            # method for subitem creation (i.e. '_create_subitem_from_config') 

        ...

Implement creation methods

Below, an implementation example of the base class methods required for subitems creation. For more details see the ConfigItemContainer documentation.

Implement base class methods to create counters

class FooController(BlissController):

    _SCC_COUNTER_TAGS = ['hv', 'cur']
    _ICC_COUNTER_TAGS = ['sum', 'bla']

    ...

    def _get_subitem_default_class_name(self, cfg, parent_key):
        """Called when the `class` key cannot be found in the subitem configuration"""
        if parent_key == "counters":
            if cfg["tag"] in self._SCC_COUNTER_TAGS:
                return "SamplingCounter"
            elif cfg["tag"] in self._SCC_COUNTER_TAGS:
                return "IntegratingCounter"

    def _get_subitem_default_module(self, class_name, cfg, parent_key):
        """Called when the given `class_name` cannot be found at the controller module level"""
        if parent_key == "counters":
            return "bliss.common.counter"

    def _create_subitem_from_config(self, name, cfg, parent_key, item_class, item_obj=None):
        if parent_key == "counters":
            name = cfg["name"]
            tag = cfg["tag"]
            unit = cfg.get("unit")

            if tag in self._SCC_COUNTER_TAGS:
                mode = cfg.get("mode", "MEAN")
                # instantiate the counter passing its CounterController (self._myscc)
                cnt = item_class(name, self._myscc, mode=mode, unit=unit)
            elif tag in self._SCC_COUNTER_TAGS:
                # instantiate the counter passing its CounterController (self._myicc)
                cnt = item_class(name, self._myicc, unit=unit)
            else:
                raise ValueError(f"cannot identify counter tag {tag}")

            cnt.tag = tag # add the tag to counter object 

            return cnt

        raise NotImplementedError # raise for unknown subitems

Info

If the default classes of subitems are already imported in the controller module it is not necessary to override _get_subitem_default_module.

Alternative example using role and type special keys

class FooController(BlissController):

    ...

    def _get_subitem_default_class_name(self, cfg, parent_key):
        """Called when the `class` key cannot be found in the subitem configuration"""
        if parent_key == "counters":
            if cfg["type"] == 'scc':
                return "SamplingCounter"
            elif cfg["type"] == 'icc':
                return "IntegratingCounter"

    def _get_subitem_default_module(self, class_name, cfg, parent_key):
        """Called when the given `class_name` cannot be found at the controller module level"""
        if parent_key == "counters":
            return "bliss.common.counter"

    def _create_subitem_from_config(self, name, cfg, parent_key, item_class, item_obj=None):
        if parent_key == "counters":
            name = cfg["name"]
            role = cfg["role"]
            unit = cfg.get("unit")

            if cfg["type"] == 'scc':
                mode = cfg.get("mode", "MEAN")
                # instantiate the counter passing its CounterController (self._myscc)
                cnt = item_class(name, self._myscc, mode=mode, unit=unit)
            elif cfg["type"] == 'icc':
                # instantiate the counter passing its CounterController (self._myicc)
                cnt = item_class(name, self._myicc, unit=unit)
            else:
                raise ValueError(f"cannot identify counter type {cfg["type"]}")

            cnt.role = role # add the role to counter object 

            return cnt

        raise NotImplementedError # raise for unknown subitems

Implement the counters property

To be compatible with the scan commands, a bliss controller must implement the counters property. In this method developers will retrieve counters from the counter controllers they have declared in the bliss controller.

A counters property returning counters from the two counter controllers

@property
def counters(self):
    return self._myscc.counters + self._myicc.counters

A counters property returning sampling counters only

@property
def counters(self):
    return self._myscc.counters

Templates

With sampling counters

YAML controller configuration

- class: MyController   
  plugin: generic        
  module: bliss_controller_mockup
  name: sampbc         

  counters:             
    - name: cnt_hv       
      tag: hv            
      unit: V
      mode: SINGLE
    - name: cnt_cur      
      tag: cur           
      unit: mA  

Implementation of a BlissController with sampling counters

from bliss.common.counter import SamplingCounter
from bliss.controllers.counter import SamplingCounterController
from bliss.controllers.bliss_controller import BlissController

class MyDevice:
    def __init__(self, config):
        self._config = config

    def read_channel(self, channel):
        return channel

class MySCC(SamplingCounterController):
    def __init__(self, name, bctrl):
        super().__init__(name)
        self.bctrl = bctrl

    def read(self, counter):
        return self.bctrl._read_counter(counter)


class MyController(BlissController):
    _COUNTER_TAG_TO_CHANNEL = {'hv':1, 'cur':2}

    def __init__(self, config):
        super().__init__(config)
        self._hw_controller = None
        self._myscc = MySCC(self.name, self)

    @property
    def hardware(self):
        if self._hw_controller is None:
            self._hw_controller = MyDevice(self.config)
        return self._hw_controller

    @property
    def counters(self):
        return self._myscc.counters

    def _load_config(self):
        for cfg in self.config["counters"]:
            self._get_subitem(cfg["name"])

    def _get_subitem_default_class_name(self, cfg, parent_key):
        if parent_key == "counters":
            return "SamplingCounter"

    def _create_subitem_from_config(self, name, cfg, parent_key, item_class, item_obj=None):
        if parent_key == "counters":
            name = cfg["name"]
            tag = cfg["tag"]
            unit = cfg.get("unit")
            mode = cfg.get("mode", "MEAN")

            if tag in self._COUNTER_TAG_TO_CHANNEL.keys():
                cnt = item_class(name, self._myscc, mode=mode, unit=unit)
                cnt.tag = tag
                return cnt
            else:
                raise ValueError(f"cannot identify counter tag {tag}")

        raise NotImplementedError

    def _read_counter(self, counter):
        channel = self._COUNTER_TAG_TO_CHANNEL[counter.tag]
        return self.hardware.read_channel(channel)

Usage

TEST_SESSION [1]: config.get('sampbc')
         Out [1]: Controller: sampbc (MyController)


TEST_SESSION [2]: ct(sampbc)
             cnt_hv  =        1.00000     V (       1.00000       V/s)  sampbc
            cnt_cur  =        2.00000    mA (       2.00000      mA/s)  sampbc
         Out [2]: Scan(name=ct, path='not saved')

TEST_SESSION [3]: lscnt()

Fullname             Shape    Controller          Alias    Name 
-------------------  -------  ------------------  -------  --------
sampbc:cnt_cur       0D       sampbc                       cnt_cur
sampbc:cnt_hv        0D       sampbc                       cnt_hv


TEST_SESSION [4]: ACTIVE_MG
         Out [4]: MeasurementGroup: MG1 (state='default')
                - Existing states : 'default'

                Enabled                                      Disabled
                -------------------------------------------  -------------
                sampbc:cnt_cur
                sampbc:cnt_hv