Commit 53b83cb1 authored by Matias Guijarro's avatar Matias Guijarro
Browse files

Merge branch '2682-Bliss-controller-and-generic-plugin' into 'master'

Resolve "Bliss controller and generic plugin"

Closes #2682

See merge request !3630
parents 256df282 02181978
Pipeline #49891 failed with stages
in 102 minutes and 39 seconds
......@@ -610,7 +610,7 @@ class FilterSet:
def get_filters(self):
"""
Return the list of the public filters, a list of dictionnary items with at least:
Return the list of the public filters, a list of dictionary items with at least:
- position
- density_calc
- transmission_calc
......
......@@ -241,7 +241,7 @@ class FilterSet_Wago(FilterSet):
def get_filters(self):
"""
Return the list of the public filters, a list of dictionnary items with at least:
Return the list of the public filters, a list of dictionary items with at least:
- position
- transmission_calc
"""
......
......@@ -206,7 +206,7 @@ class FilterSet_Wheel(FilterSet):
def get_filters(self):
"""
Return the list of the public filters, a list of dictionnary items with at least:
Return the list of the public filters, a list of dictionary items with at least:
- position
- transmission_calc
For the wheel filterset _filters = _config_filters
......
......@@ -170,6 +170,8 @@ import gevent
import gevent.event
import enum
from bliss.common.protocols import CounterContainer
from bliss.common.logtools import log_debug, disable_user_output
from bliss.common.utils import with_custom_members, autocomplete_property
from bliss.common.counter import SamplingCounter
......@@ -200,38 +202,38 @@ def _get_external_device_name(device):
return device
class SCC(SamplingCounterController):
def __init__(self, name, boss):
super().__init__(name)
self.boss = boss
def read_all(self, *counters):
return [self.boss.read()]
@with_custom_members
class Input(SamplingCounterController):
class Input(CounterContainer):
"""Implements the access to an input device which is accessed via the
regulation controller (like a sensor plugged on a channel of the controller)
"""
def __init__(self, controller, config):
""" Constructor """
super().__init__(name=config["name"])
self._name = config["name"]
self._controller = controller
self._config = config
self._channel = self._config.get("channel")
self.max_sampling_frequency = config.get("max_sampling_frequency", 5)
# useful attribute for a temperature controller writer
self._attr_dict = {}
self._build_counters()
def _build_counters(self):
self.create_counter(
SamplingCounter,
self.name + "_counter",
unit=self._config.get("unit", "N/A"),
mode=self._config.get("sampling-counter-mode", "SINGLE"),
)
@property
def name(self):
return self._name
def read_all(self, *counters):
return [self.read()]
@autocomplete_property
def counters(self):
return counter_namespace({self.name: self._controller.counters[self.name]})
# ----------- BASE METHODS -----------------------------------------
......@@ -316,7 +318,13 @@ class ExternalInput(Input):
self.device = config.get("device")
self.load_base_config()
# ----------- METHODS THAT A CHILD CLASS MAY CUSTOMIZE ------------------
self._controller = SCC(self.name, self)
self._controller.create_counter(
SamplingCounter,
self.name,
unit=self._config.get("unit"),
mode=self._config.get("mode", "SINGLE"),
)
def __info__(self):
lines = ["\n"]
......@@ -358,7 +366,7 @@ class ExternalInput(Input):
@with_custom_members
class Output(SamplingCounterController):
class Output(CounterContainer):
""" Implements the access to an output device which is accessed via the regulation controller (like an heater plugged on a channel of the controller)
The Output has a ramp object.
......@@ -370,7 +378,7 @@ class Output(SamplingCounterController):
def __init__(self, controller, config):
""" Constructor """
super().__init__(name=config["name"])
self._name = config["name"]
self._controller = controller
self._config = config
......@@ -387,20 +395,13 @@ class Output(SamplingCounterController):
# useful attribute for a temperature controller writer
self._attr_dict = {}
self.max_sampling_frequency = config.get("max_sampling_frequency", 5)
self._build_counters()
def _build_counters(self):
self.create_counter(
SamplingCounter,
self.name + "_counter",
unit=self._config.get("unit", "N/A"),
mode=self._config.get("sampling-counter-mode", "SINGLE"),
)
@property
def name(self):
return self._name
def read_all(self, *counters):
return [self.read()]
@autocomplete_property
def counters(self):
return counter_namespace({self.name: self._controller.counters[self.name]})
# ----------- BASE METHODS -----------------------------------------
......@@ -608,6 +609,11 @@ class ExternalOutput(Output):
self.mode = config.get("mode", "relative")
self.load_base_config()
self._controller = SCC(self.name, self)
self._controller.create_counter(
SamplingCounter, self.name, unit=self._config.get("unit"), mode="SINGLE"
)
# ----------- BASE METHODS -----------------------------------------
@property
......@@ -700,7 +706,7 @@ class ExternalOutput(Output):
@with_custom_members
class Loop(SamplingCounterController):
class Loop(CounterContainer):
""" Implements the access to the regulation loop
The regulation is the PID process that:
......@@ -733,7 +739,7 @@ class Loop(SamplingCounterController):
def __init__(self, controller, config):
""" Constructor """
super().__init__(name=config["name"])
self._name = config["name"]
self._controller = controller
self._config = config
......@@ -761,20 +767,8 @@ class Loop(SamplingCounterController):
self.reg_plot = None
self.max_sampling_frequency = config.get("max_sampling_frequency", 5)
self._build_counters()
self._create_soft_axis()
def _build_counters(self):
self.create_counter(
SamplingCounter,
self.name + "_setpoint",
unit=self.input.config.get("unit", "N/A"),
mode="SINGLE",
)
def __del__(self):
self.close()
......@@ -787,8 +781,16 @@ class Loop(SamplingCounterController):
# ----------- BASE METHODS -----------------------------------------
def read_all(self, *counters):
return [self._get_working_setpoint()]
@lazy_init
def read(self):
""" Return the current working setpoint """
log_debug(self, "Loop:read")
return self._get_working_setpoint()
@property
def name(self):
return self._name
##--- CONFIG METHODS
def load_base_config(self):
......@@ -847,7 +849,7 @@ class Loop(SamplingCounterController):
all_counters = (
list(self.input.counters)
+ list(self.output.counters)
+ list(self._counters.values())
+ [self._controller.counters[self.name]]
)
return counter_namespace(all_counters)
......@@ -1484,6 +1486,14 @@ class SoftLoop(Loop):
self.load_base_config()
self.max_attempts_before_failure = config.get("max_attempts_before_failure", 3)
self._controller = SCC(self.name, self)
self._controller.create_counter(
SamplingCounter,
self.name,
unit=self._config.get("unit"),
mode=self._config.get("mode", "SINGLE"),
)
def __info__(self):
lines = ["\n"]
lines.append(f"=== SoftLoop: {self.name} ===")
......@@ -1517,6 +1527,12 @@ class SoftLoop(Loop):
self._ramp.stop()
self._stop_regulation()
def read(self):
""" Return the current working setpoint """
log_debug(self, "SoftLoop:read")
return self._get_working_setpoint()
@property
def max_attempts_before_failure(self):
"""
......
......@@ -23,19 +23,7 @@ def SoftAxis(
unit=None,
):
# if callable(position):
# position = position.__name__
# if callable(move):
# move = move.__name__
# if callable(stop):
# stop = stop.__name__
config = {
"low_limit": low_limit,
"high_limit": high_limit,
"limits": (low_limit, high_limit),
"name": name,
}
config = {"low_limit": low_limit, "high_limit": high_limit, "name": name}
if tolerance is not None:
config["tolerance"] = tolerance
......@@ -44,8 +32,8 @@ def SoftAxis(
config["unit"] = unit
controller = SoftController(name, obj, config, position, move, stop, state)
controller._initialize_config()
controller._init()
axis = controller.get_axis(name)
axis._positioner = False
......
......@@ -68,9 +68,9 @@ def get_axes_info(diffracto):
def create_hkl_motors(diffracto, axes_info):
hklmots = HKLMotors(
f"{diffracto.name}_motors", diffracto, diffracto.config, axes_info
)
config = diffracto.config.clone()
config["name"] = f"{diffracto.name}_motors"
hklmots = HKLMotors(diffracto, diffracto.config, axes_info)
# --- force axis init before CalcController._init (see emotion)
for axname in axes_info:
hklmots.get_axis(axname)
......
This diff is collapsed.
......@@ -7,10 +7,10 @@
import re
from importlib.util import find_spec
from importlib import import_module
# ---- Alias defined for default BLISS controllers
_ALIAS_TO_MODULE_NAME = {"Lima": "bliss.controllers.lima.lima_base"}
"""Alias defined for default BLISS controllers"""
def camel_case_to_snake_style(name):
......@@ -68,3 +68,72 @@ def find_class_and_node(cfg_node, base_path="bliss.controllers"):
klass = getattr(module, class_name.title())
return klass, node
def find_top_class_and_node(cfg_node, base_paths=None):
if base_paths is None:
base_paths = ["bliss.controllers", "bliss.controllers.motors"]
node = cfg_node.get_top_key_node("class")
class_name = node["class"]
candidates = set()
errors = []
for base_path in base_paths:
module = None
# --- Find module and try import -----------
module_name = resolve_module_name(class_name, node, base_path)
try:
module = import_module(module_name)
except ModuleNotFoundError as e:
errors.append(e)
module_name = "%s.%s" % (base_path, camel_case_to_snake_style(class_name))
try:
module = import_module(module_name)
except ModuleNotFoundError as e2:
errors.append(e2)
if module is not None:
# --- Find class and try import -----------
if hasattr(module, class_name):
klass = getattr(module, class_name)
candidates.add((module, klass))
else:
kname = class_name.title()
if kname != class_name:
if hasattr(module, kname):
klass = getattr(module, kname)
candidates.add((module, klass))
else:
errors.append(f"cannot find {class_name} in {module}")
# --- return if a single candidate was found else raise error
if len(candidates) == 1:
return candidates.pop()[1], node
elif len(candidates) > 1:
for mod, klass in candidates:
if "package" in node:
if node["package"] == mod.__name__:
return klass, node
elif "module" in node:
if node["module"] in mod.__name__:
return klass, node
msg = f"Multiple candidates found for class '{class_name}':"
for mod, _ in candidates:
msg += f"\n - {mod}"
msg += "\nResolve by providing the 'module' key in yml config\n"
raise ModuleNotFoundError(msg)
else:
msg = f"Config could not find {class_name}:"
for err in errors:
msg += f"\n{err}"
msg += (
f"\nCheck that module is located under one of these modules: {base_paths}"
)
msg += "\nElse, resolve by providing the 'package' key in yml config\n"
raise ModuleNotFoundError(msg)
......@@ -462,6 +462,17 @@ class ConfigNode(MutableMapping):
return self._parent.is_service
return through_server
def get_top_key_node(self, key):
topnode = None
node = self
while True:
if node.get(key):
topnode = node
node = node._parent
if node is None or "__children__" in node.keys():
break
return topnode
def get_inherited_value_and_node(self, key):
"""
@see get_inherited
......@@ -1014,6 +1025,10 @@ class Config(metaclass=Singleton):
module_name = config_node.plugin
if module_name is None:
module_name = "default"
if module_name in ["emotion", "regulation", "diffractometer", "bliss"]:
module_name = "generic"
m = __import__("bliss.config.plugins.%s" % (module_name), fromlist=[None])
if hasattr(m, "create_object_from_cache"):
cache_object = self._name2cache.pop(name, None)
......
# -*- coding: utf-8 -*-
#
# This file is part of the bliss project
#
# Copyright (c) 2015-2020 Beamline Control Unit, ESRF
# Distributed under the GNU LGPLv3. See LICENSE for more info.
from bliss.common.protocols import CounterContainer
from bliss.common.utils import autocomplete_property
from bliss.config.plugins.generic import ConfigItemContainer
class BlissController(CounterContainer, ConfigItemContainer):
"""
BlissController base class is made for the implementation of all Bliss controllers.
It is designed to ease the management of sub-objects that depend on a shared controller (see ConfigItemContainer).
Sub-objects are declared in the yml configuration of the controller under dedicated sub-sections.
A sub-object is considered as a subitem if it has a name (key 'name' in a sub-section of the config).
Usually subitems are counters and axes but could be anything else (known by the controller).
The BlissController has properties @counters and @axes to retrieve subitems that can be identified
as counters or axes.
# --- Plugin ---
BlissController objects are created from the yml config using the generic plugin.
Any subitem with a name can be imported in a Bliss session with config.get('name').
The plugin ensures that the controller and subitems are only created once.
The bliss controller itself can have a name (optional) and can be imported in the session.
The plugin resolves dependencies between the BlissController and its subitems.
It looks for the top 'class' key in the config to instantiate the BlissController.
While importing any subitem in the session, the bliss controller is instantiated first (if not alive already).
The effective creation of the subitems is performed by the BlissController itself and the plugin just ensures
that the controller is always created before subitems and only once.
Example: config.get(bctrl_name) or config.get(item_name) with config = bliss.config.static.get_config()
# --- Items and sub-controllers ---
A controller (top) can have sub-controllers. In that case there are two ways to create the sub_controllers:
- The most simple way to do this is to declare the sub-controller as an independant object with its own yml config
and use a reference to this object into the top-controller config.
- If a sub-controller has no reason to exist independently from the top-controller, then the top-controller
will create and manage its sub-controllers from the knowledge of the top-controller config only.
In that case, some items declared in the top-controller are, in fact, managed by one of the sub-controllers.
In that case, the author of the top controller class must overload the '_get_item_owner' method and specify
which is the sub-controller that manages which items.
Example: Consider a top controller which manages a motors controller internally. The top controller config
declares the axes subitems but those items are in fact managed by the motors controller.
In that case, '_get_item_owner' should specify that the axes subitems are managed by 'self.motor_controller'
instead of '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).
# --- From config dict ---
A BlissController can be instantiated directly (i.e. not via plugin) providing a config 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()
# --- yml config example ---
- plugin: generic <== use the dedicated 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)
com: <== communication config for associated hardware (optional)
tcp:
url: bcmock
custom_param_1: value <== a parameter for the custom bliss controller creation (optional)
custom_param_2: $ref1 <== a referenced object for the controller (optional/authorized)
sub-section-1: <== a sub-section where subitems can be declared (optional) (ex: 'counters')
- name: sub_item_1 <== name of the subitem (and its config)
tag : item_tag_1 <== a tag for this item (known and interpreted by the custom bliss controller) (optional)
sub_param_1: value <== a custom parameter for the item creation (optional)
device: $ref2 <== an external reference for this subitem (optional/authorized)
sub-section-2: <== another sub-section where subitems can be declared (optional) (ex: 'axes')
- name: sub_item_2 <== name of the subitem (and its config)
tag : item_tag_2 <== a tag for this item (known and interpreted by the custom bliss controller) (optional)
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 : <== a third sub-section
- name: $ref3 <== a subitem as an external reference is possible (optional/authorized)
something: value
"""
# ========== SIGNATURE ======================================
# def __init__(self, config):
# super().__init__(config)
# ========== STANDARD PROPERTIES ============================
@autocomplete_property
def hardware(self):
if self._hw_controller is None:
self._hw_controller = self._create_hardware()
return self._hw_controller
# ========== ABSTRACT METHODS ====================
def _get_default_chain_counter_controller(self):
""" return the default counter controller that should be used
when this controller is used to customize the DEFAULT_CHAIN
"""
raise NotImplementedError
def _create_hardware(self):
""" return the low level hardware controller interface """
raise NotImplementedError
@autocomplete_property
def counters(self):
raise NotImplementedError
@autocomplete_property
def axes(self):
raise NotImplementedError
# ========== CUSTOMIZABLE METHODS ==================
def __info__(self):
""" Return controller info as a string """
info_str = f"Controller: {self.name} ({self.__class__.__name__})\n"
return info_str
# -*- coding: utf-8 -*-
#
# This file is part of the bliss project
#
# Copyright (c) 2015-2020 Beamline Control Unit, ESRF
# Distributed under the GNU LGPLv3. See LICENSE for more info.
from time import perf_counter, sleep
from itertools import chain
from gevent import event, sleep as gsleep
from bliss import global_map
from bliss.common.counter import SamplingCounter
from bliss.common.protocols import counter_namespace, IterableNamespace
from bliss.common.utils import autocomplete_property
from bliss.comm.util import get_comm
from bliss.controllers.counter import CounterController, SamplingCounterController
from bliss.scanning.acquisition.counter import BaseCounterAcquisitionSlave
from bliss.common.logtools import log_info
from bliss.controllers.bliss_controller import BlissController
class HardwareController:
def __init__(self, config):
self._config = config
self._last_cmd_time = perf_counter()
self._cmd_min_delta_time = 0
self._init_com()
@property
def config(self):
return self._config
@property
def comm(self):
return self._comm
def send_cmd(self, cmd, *values):
now = perf_counter()
log_info(self, f"@{now:.3f} send_cmd", cmd, values)
if self._cmd_min_delta_time: