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).
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)
Link with a device¶
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