Writing controllers for Bliss¶
Bliss put no constrains on controllers classes and developers can start from scratch and define everything. However, there are several generic mechanisms, like loading the controller from a YML configuration (plugin) or managing the controller’s counters and axes, which are already defined in Bliss and which can be inherited while writing a new controller class.
BlissController base class¶
As a base for the implementation of controllers, Bliss provides the BlissController
class.
This class already implements the plugin mechanisms and is designed to ease the management of sub-objects under a top controller.
Examples of controllers that should inherit from BlissController
class:
-
a controller of axes
-
a controller with counters
-
a controller with axes and counter
-
a top-controller (software) managing other sub-controllers (software/hardware)
Example of the YML structure:
- plugin: generic <== BlissController works with generic plugin
module: custom_module <== module of the custom bliss controller
class: BCMockup <== class of the custom bliss controller
name: bcmock <== name of the custom bliss controller (optional)
param_1: value <== a parameter for the custom
bliss controller (optional)
section_1: <== a section where subitems config can be
declared (ex: 'counters') (optional)
- name: subitem_1 <== name of a subitem
- name: subitem_2 <== name of another subitem of the same type
section_2: <== another section for another type of
subitems (ex: 'axes') (optional)
- name: subitem_2 <== name of another subitem type
The signature of a BlissController
takes a single argument config
.
It could be a ConfigNode
object or a standard dictionary.
class BlissController(CounterContainer):
def __init__(self, config):
BlissController and subitems¶
A BlissController
subitem is an object managed by the controller and which
could have a name declared under a sub-section of the controller’s
configuration. Usually subitems are counters and axes but could be anything else
(known by the controller only).
section_1:
- name: subitem_1 <== a subitem using the default
class (defined by the controller)
- name: subitem_2 <== a subitem using a given class
class: bliss.foo.custom.myclass path (from an absolute path)
- name: subitem_3 <== a subitem using a given class
class: myitemclass name (default path known by the controller)
Subitems can be declared in the controller’s YML configuration if they are expected to be directly imported in a user session.
If not declared in the YML, they are still accessible via the controller (see BlissController._get_subitem(name)
).
To retrieve the subitems that can be identified as counters or axes, BlissController
class implements the @counters
and @axes
properties.
The BlissController
identifies the subitem type thanks to the name of the sub-section where the item was found (aka parent_key
).
Also, the controller must provides a default class for each kind of parent_key
(see BlissController._get_subitem_default_class_name
).
Examples:
def _get_subitem_default_class_name(self, cfg, parent_key):
if parent_key == "axes":
return "Axis"
elif parent_key == "encoders":
return "Encoder"
elif parent_key == "shutters":
return "Shutter"
elif parent_key == "switches":
return "Switch"
or
def _get_subitem_default_class_name(self, cfg, parent_key):
if parent_key == "counters":
tag = cfg["tag"]
if self._COUNTER_TAGS[tag][1] == "scc":
return "SamplingCounter"
elif self._COUNTER_TAGS[tag][1] == "icc":
return "IntegratingCounter"
The default subitem class can be overridden by specifing the class
key in its configuration.
The class can be given as an absolute path or as a class name.
If providing a class name the controller tries to find the item class first at its module level, else it uses a default path defined by the controller (see BlissController._get_subitem_default_module
).
Examples:
def _get_subitem_default_module(self, class_name, cfg, parent_key):
if parent_key == "axes":
return "bliss.common.axis"
elif parent_key == "encoders":
return "bliss.common.encoder"
elif parent_key == "shutters":
return "bliss.common.shutter"
elif parent_key == "switches":
return "bliss.common.switch"
or
def _get_subitem_default_module(self, class_name, cfg, parent_key):
if class_name == "IntegratingCounter":
return "bliss.common.counter"
Bliss controller plugin¶
BlissControllers
are created from the yml configuration using the generic
plugin.
The controlelr class is based on the ConfigItemContainer
base class which deals with all the mechanisms of the generic
plugin.
Any subitem can be imported in a Bliss session with the command config.get('name')
.
The bliss controller itself can have a name (optional) and can be imported in the session.
The plugin ensures that the controller and subitems are only created once.
The effective creation of subitems is performed by the BlissController
itself and the plugin just ensures that the controller is always created before subitems and only once.
The generic
plugin will also manage the resolution order of the references to other objects within the BlissController
configuration. It handles external and internal references and allows to use a reference for a subitem name.
Example of an advanced configuration using different kind of references:
- plugin: generic
module: custom_module
class: BCMockup
name: bcmock
custom_param_1: value
custom_param_2: $ref1 <== a referenced object for the controller (optional/authorized)
sub-section-1:
- name: sub_item_1
tag : item_tag_1
sub_param_1: value
device: $ref2 <== an external reference for this subitem (optional/authorized)
sub-section-2:
- name: sub_item_2
tag : item_tag_2
input: $sub_item_1 <== an internal reference to another subitem owned by the same controller (optional/authorized)
sub-section-2-1: <== nested sub-sections are possible (optional)
- name: sub_item_21
tag : item_tag_21
sub-section-3 :
- name: $ref3 <== a subitem as an external reference is possible (optional/authorized)
something: value
Subitem creation¶
In order to keep the plugin as generic as possible, all the knowledge specfic to the controller is asked by the plugin to the BlissController
.
In particular, when the plugin needs to instantiate a subitem it will call the method BlissController._create_subitem_from_config
. This abstract method must be implemented and must return the subitem instance.
To be able to decide which instance should be created, the method receives 4 arguments:
name
: subitem namecfg
: subitem configparent_key
: name of the subsection where the item was found (in controller’s config)item_class
: class of the subitem (see BlissController and sub-items ).item_obj
: the object instance of an item referenced in the config (None if not a reference)
If item_class
is None
it means that the subitem was given as a reference.
In that case the object is already instantiated and is contained in item_obj
.
Examples:
@check_disabled
def _create_subitem_from_config(self, name, cfg, parent_key, item_class, item_obj=None):
if parent_key == "axes":
if item_class is None: # it means that item was referenced in config,
axis = item_obj # so just grab the item object provided by 'item_obj'
else:
axis = item_class(name, self, cfg) # instantiate the item using the given class and decide the correct signature
# === do anything custom here ================
self._axes[name] = axis
axis_tags = cfg.get("tags")
if axis_tags:
for tag in axis_tags.split():
self._tagged.setdefault(tag, []).append(axis)
if axis.controller is self:
set_custom_members(self, axis, self._initialize_axis)
else:
# reference axis
return axis
if axis.controller is self:
axis_initialized = Cache(axis, "initialized", default_value=0)
self.__initialized_hw_axis[axis] = axis_initialized
self.__initialized_axis[axis] = False
self._add_axis(axis)
# ====================================================
return axis # return the created item
elif parent_key == "encoders": # deal with an other kind of items
encoder = self._encoder_counter_controller.create_counter(
item_class, name, motor_controller=self, config=cfg
)
self._encoders[name] = encoder
self.__initialized_encoder[encoder] = False
return encoder
or
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"]
mode = cfg.get("mode")
unit = cfg.get("unit")
convfunc = cfg.get("convfunc")
if self._COUNTER_TAGS[tag][1] == "scc":
cnt = self._counter_controllers["scc"].create_counter(
item_class, name, unit=unit, mode=mode
)
cnt.tag = tag
elif self._COUNTER_TAGS[tag][1] == "icc":
cnt = self._counter_controllers["icc"].create_counter(
item_class, name, unit=unit
)
cnt.tag = tag
else:
raise ValueError(f"cannot identify counter tag {tag}")
return cnt
elif parent_key == "operators":
return item_class(cfg)
elif parent_key == "axes":
if item_class is None: # it is a referenced axis (i.e external axis)
axis = item_obj # the axis instance
tag = cfg[
"tag"
] # ask for a tag which only concerns this ctrl (local tag)
self._tag2axis[tag] = name # store the axis tag
return axis
else:
raise ValueError(
f"{self} only accept referenced axes"
) # reject none-referenced axis
Nested BlissControllers¶
A top-bliss-controller can have multiple sub-bliss-controllers. In that case there are two ways to create the sub-bliss-controllers:
The most simple way to do this is to declare a sub-bliss-controller as an independant object with its own yml config and use a reference to this object into the top-bliss-controller config.
Else, if a sub-bliss-controller has no reason to exist independently from the top-bliss-controller, then the top-bliss-controller will create and manage its sub-bliss-controllers from the knowledge of the top-bliss-controller configuration only.
In the second case, some items declared in the top-bliss-controller are, in fact, managed by one of the sub-bliss-controllers.
Then, the author of the top-bliss-controller class must overload the _get_item_owner
method and specify which is the sub-bliss-controller that manages which items.
Example:
Consider a top-bliss-controller which has internally another sub-bliss-controller that manages pseudo axes.
(self._motor_controller = AxesBlissController(...)
)
- plugin: generic
module: custom_module
class: BCMockup
name: bcmock
axes:
- name: $xrot
tags: real xrot
- name: $yrot
tags: real yrot
- name: axis_1
tag : theta
The top-bliss-controller configuration declares the axes subitems but those items are in fact managed by the motors controller (self._motor_controller
).
In that case, developers can override the self._get_item_owner
method to specify the subitems that are managed by self._motor_controller
instead of self
.
def _get_item_owner(self, name, cfg, pkey):
""" Return the controller that owns the items declared in the config.
By default, this controller is the owner of all config items.
However if this controller has sub-controllers that are the real owners
of some items, this method should use to specify which sub-controller is
the owner of which item (identified with name and pkey).
"""
if pkey == "axes":
return self._motor_controller
else:
return self
The method receives the item name and the parent_key
. So self._motor_controller
can be associated to all subitems under the axes
parent_key (instead of doing it for each subitem name).
Note: it would have been possible to not override self._get_item_owner
and handle the axes
items in the top-controller methods but it is not recommended as the code is already in the sub-bliss-controller that handles motors.
Direct instantiation¶
A BlissController can be instantiated directly (i.e. not instantiated by the plugin) providing a configuration as a dictionary.
In that case, users must call the method self._initialize_config()
just after the controller instantiation to ensure that the controller is initialized in the same way as the plugin does.
The config dictionary should be structured like a YML file (i.e: nested dict and list) and references replaced by their corresponding object instances.
Example: bctrl = BlissController( config_dict )
=> bctrl._initialize_config()
BlissController and default chain¶
The DEFAULT_CHAIN
can be customized with DEFAULT_CHAIN.set_settings
(see Default chain).
The devices introduced in the chain must be of the type Counter
, CounterController
or BlissController
.
While introducing a BlissController
in the default chain, the method BlissController._get_default_chain_counter_controller
is called to obtain the CounterController
object that should be used. By default this method is not implemented.
Other tips¶
@autocomplete_property decorator¶
In many controllers, the @property
decorator is heavily used to protect certain
attributes of the instance or to limit the access to read-only. When using the
bliss command line interface the autocompletion will not suggest any
completion based on the return value of the method underneath the property.
This is a wanted behavior e.g. in case this would trigger hardware communication. There are however also use cases where a deeper autocompletion is wanted.
Note
“↹” represents the action of pressing the “Tab” key of the keyboard.
Example: the .counter
namespace of a controller. If implemented as
@property
:
BLISS [1]: lima_simulator.counters. ↹
Would not show any autocompletion suggestions. To enable deeper autocompletion
a special decorator called @autocomplete_property
must be used.
from bliss.common.utils import autocomplete_property
class Lima(object):
@autocomplete_property
def counters(self):
all_counters = [self.image]
...
Using this decorator would result in autocompletion suggestions:
BLISS [1]: lima_simulator.counters. ↹
_roi1_
_roi2_
_bpm_
The __info__()
method for Bliss shell¶
Info
- Any Bliss controller that is visible to the user in the command line
should have an
__info__()
function implemented! - The return type of
__info__()
must bestr
, otherwhise it fails and__repr__()
is used as fallback! - As a rule of thumb: the return value of a custom
__repr__()
implementation should not contain\n
and should be inspired by the standard implementation of__repr__()
in python.
In Bliss, __info__()
is used by the command line interface (Bliss shell or Bliss
repl) to enquire information of the internal state of any object / controller in
case it is available.
That way, a user can get information how to use the object, detailed
from the user perspective. This is in contrast to the built-in python function
__repr__()
, which should return a short summary of the concerned object from
the developer perspective. The Protocol that is put in place in the Bliss
shell is the following:
- if the return value of a statement entered into the Bliss shel is a python
object with
__info__()
implemented this__info__()
function will be called by the Bliss shell to display the output. As a fallback option (__info__()
not implemented) the standard behavior of the interactive python interpreter involving__repr__
is used. (For details about__repr__
see next section.)
Here is an example for the lima controller that is using __info__
:
LIMA_TEST_SESSION [3]: lima_simulator
Out [3]: Simulator - Generator (Simulator) - Lima Simulator
Image:
bin = [1 1]
flip = [False False]
height = 1024
roi = <0,0> <1024 x 1024>
rotation = rotation_enum.NONE
sizes = [ 0 4 1024 1024]
type = Bpp32
width = 1024
Acquisition:
expo_time = 1.0
mode = mode_enum.SINGLE
nb_frames = 1
status = Ready
status_fault_error = No error
trigger_mode = trigger_mode_enum.INTERNAL_TRIGGER
ROI Counters:
[default]
Name ROI (<X, Y> <W x H>)
---- ------------------
r1 <0, 0> <100 x 200>
The information given above is usefull from a user point of view. As a developer one might want to work in the Bliss shell with live object e.g.
LIMA [4]: my_detectors = {'my_lima':lima_simulator,'my_mca':simu1}
LIMA [5]: my_detectors
Out [5]: {'my_lima': <Lima Controller for Simulator (Lima Simulator)>,
'my_mca': <bliss.controllers.mca.simulation.SimulatedMCA
object at 0x7f2f535b5f60>}
In this case, it is desirable that the python objects themselves are clearly
represented, which is exactly the role of __repr__
(in this example the
lima_simulator
has a custom __repr__
while in simu1
there is no __repr__
implemented so the bulid in python implementation is used).
The signature of __info__()
should be def __info__(self):
the return value
must be a string.
BLISS [1]: class A(object):
...: def __repr__(self):
...: return "my repl"
...: def __str__(self):
...: return "my str"
...: def __info__(self):
...: return "my info"
BLISS [2]: a=A()
BLISS [3]: a
Out [3]: my info
BLISS [4]: [a]
Out [4]: [my repl]
Warning
If, for any reason, there is an exception raised inside __info__
, the
fallback option will be used and __repr__
is evaluated in this case.
And this will hide the error. So, any error must be treated before returning.
Example:
def __info__(self):
info_str = "bla \n"
info_str += "bli \n"
return info_str
The equivalent of repr(obj)
or str(obj)
is also available in
bliss.shell.standard
as info(obj)
which can be used also outside the Bliss
shell.
Python 3.7.3 (default, Mar 27 2019, 22:11:17)
[GCC 7.3.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from bliss.shell.standard import info
>>> class A(object):
... def __repr__(self):
... return "my repl"
... def __info__(self):
... return "my info"
...
>>> info(A())
'my info'
>>> class B(object):
... def __repr__(self):
... return "my repl"
...
>>> info(B())
'my repl'
__str__()
and __repr__()
¶
If implemented in a Python class, __repr__
and __str__
methods are
build-in functions Python to return information about an object instantiating this class.
__str__
should print a readable message-
__repr__
should print a short message about the object that is unambiguous (e.g. name of an identifier, class name, etc.). -
__str__
is called:- when the object is passed to the print() function (e.g.
print(my_obj)
). - wheh the object is used in string operations (e.g.
str(my_obj)
or'{}'.format(my_obj)
orf'some text {my_obj}'
)
- when the object is passed to the print() function (e.g.
__repr__
method is called:- when user type the name of the object in an interpreter session (a python shell).
- when displaying containers like lists and dicts (the result of
__repr__
is used to represent the objects they contain) - when explicitly asking for it in the print() function. (e.g.
print("%r" % my_object)
)
By default when no __str__
or __repr__
methods are defined, the __repr__
returns the name of the class (Length) and __str__
calls __repr__
.