Commit bd75ef7c authored by Perceval Guillou's avatar Perceval Guillou
Browse files

Motor controller as BlissController

parent 24b008a1
......@@ -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
......
......@@ -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._controller_init()
controller._init()
axis = controller.get_axis(name)
axis._positioner = False
......
......@@ -5,62 +5,8 @@
# Copyright (c) 2015-2020 Beamline Control Unit, ESRF
# Distributed under the GNU LGPLv3. See LICENSE for more info.
# ================ IMPORTANT NOTE_ ABOUT PLUGIN CYCLIC IMPORT =============
#
# The plugin prevent cyclic import thanks to the yield name2cacheditems tricks
# in create_objects_from_config_node() below.
#
# however the best way to avoid this problem would be to NOT allow references ($name) of
# a bliss_controller item within another item of the same bliss_controller.
# In other words, in a bliss_controller config, only references to external objects should be allowed.
# (external = not owned by the bliss controller itself)
# If a BC item needs to reference another item of the same BC, then just using the item name (without '$')
# should be enough, as the BC knows its items and associated names.
from bliss.config.plugins.utils import find_class_and_node
from bliss.config.static import ConfigNode, ConfigList
def find_sub_names_config(
config, selection=None, level=0, parent_key=None, exclude_ref=True
):
""" Search in a config the sub-sections where the key 'name' is found.
Returns a dict of tuples (sub_config, parent_key) indexed by level (0 is the top level).
sub_config: a sub-config containing 'name' key
parent_key: key under which the sub_config was found (None for level 0)
exclude_ref: if True, exclude sub-config with name as reference ($)
"""
if selection is None:
selection = {}
if selection.get(level) is None:
selection[level] = []
if config.get("name"):
if not exclude_ref or not config.get("name").startswith("$"):
selection[level].append((config, parent_key))
for (
k,
v,
) in (
config.raw_items()
): # !!! raw_items to avoid cyclic import while resloving reference !!!
if isinstance(v, ConfigNode):
find_sub_names_config(v, selection, level + 1, k)
elif isinstance(v, ConfigList):
for i in v:
if isinstance(i, ConfigNode):
find_sub_names_config(i, selection, level + 1, k)
return selection
from bliss.config.plugins.utils import find_top_class_and_node
from bliss.controllers.bliss_controller import BlissController
def create_objects_from_config_node(cfg_obj, cfg_node):
......@@ -71,62 +17,65 @@ def create_objects_from_config_node(cfg_obj, cfg_node):
This function resolves dependencies between the BlissController and its sub-objects with a name.
It looks for the 'class' key in 'cfg_node' (or at upper levels) to instantiate the BlissController.
All sub-configs of named sub-objects are stored as cached items for later instantiation via config.get.
All sub-configs of named sub-objects owned by the controller are stored as cached items for later instantiation via config.get.
args:
cfg_obj: a Config object (from config.static)
cfg_node: a ConfigNode object (from config.static)
yield:
tuple: ( created_items, cached_items)
tuple: (created_items, cached_items)
"""
print("\n===== BLISS CONTROLLER PLUGIN FROM CONFIG: ", cfg_node["name"])
name2items = {}
name2cacheditems = {}
# search the 'class' key in cfg_node or at a upper node level
# return the class and the associated config node
# upper_node = cfg_node.parent ??
klass, ctrl_node = find_class_and_node(cfg_node)
# print("=== FOUND BLISS CONTROLLER CLASS", klass, "WITH NODE", ctrl_node)
ctrl_name = ctrl_node.get("name")
# then return the class and the associated config node
klass, ctrl_node = find_top_class_and_node(cfg_node)
ctrl_name = ctrl_node.get("name") # ctrl could have a name in config
item_name = cfg_node["name"] # name of the item that should be created and returned
# always create the bliss controller first
bctrl = klass(ctrl_node.clone())
# find all sub objects with a name in controller config
sub_cfgs = find_sub_names_config(ctrl_node) # .to_dict(resolve_references=False))
for level in sorted(sub_cfgs.keys()):
if level != 0: # ignore the controller itself
for cfg, pkey in sub_cfgs[level]:
subname = cfg["name"]
# if subname == item_name: # this is the sub-object to return
# name2items[item_name] = bctrl._create_sub_item(item_name, cfg, pkey)
# else: # store sub-object info for later instantiation
name2cacheditems[subname] = (bctrl, cfg, pkey)
# --- add the controller to stored items if it has a name
if ctrl_name:
name2items[ctrl_name] = bctrl
# update the config cache dict NOW to avoid cyclic instanciation (i.e. config.get => create_object_from_... => config.get )
yield name2items, name2cacheditems
# --- don't forget to instanciate the object for which this function has been called (if not a controller)
if item_name != ctrl_name:
obj = cfg_obj.get(item_name)
yield {item_name: obj}
# --- NOW, any new object_name going through 'config.get( obj_name )' should call 'create_object_from_cache' only.
# --- 'create_objects_from_config_node' should never be called again for any object related to the controller instanciated here (see config.get code)
def create_object_from_cache(config, name, cached_object_info):
print("===== REGULATION FROM CACHE", name) # config, name, object_info)
bctrl, cfg, pkey = cached_object_info
new_object = bctrl._create_sub_item(name, cfg, pkey)
return new_object
bctrl = klass(ctrl_node)
print(f"\n=== From config: {item_name} from {bctrl.name}")
if isinstance(bctrl, BlissController):
# prepare subitems configs and cache item's controller
names_to_cache = bctrl._prepare_subitems_configs(ctrl_node)
cachednames2ctrl = {name: bctrl for name in names_to_cache}
print(f"\n=== Caching: {names_to_cache} from {bctrl.name}")
# # --- add the controller to stored items if it has a name
name2items = {}
if ctrl_name:
name2items[ctrl_name] = bctrl
# name2items[bctrl.name] = bctrl
# update the config cache dict now to avoid cyclic instanciation with internal references
# an internal reference happens when a subitem config uses a reference to another subitem owned by the same controller.
yield name2items, cachednames2ctrl
# load config and init controller
bctrl._controller_init()
# --- don't forget to instanciate the object for which this function has been called (if not a controller)
if item_name != ctrl_name:
obj = cfg_obj.get(item_name)
yield {item_name: obj}
# --- Now any new object_name going through 'config.get( obj_name )' should call 'create_object_from_cache' only.
# --- 'create_objects_from_config_node' should never be called again for any object related to the controller instanciated here (see config.get code)
elif (
item_name == ctrl_name
): # allow instantiation of top object which is not a BlissController
yield {ctrl_name: bctrl}
return
else: # prevent instantiation of an item comming from a top controller which is not a BlissController
raise TypeError(f"{bctrl} is not a BlissController object!")
def create_object_from_cache(config, name, bctrl):
print(f"\n=== From cache: {name} from {bctrl.name}")
return bctrl._get_subitem(name)
......@@ -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)
......
......@@ -5,12 +5,13 @@
# Copyright (c) 2015-2020 Beamline Control Unit, ESRF
# Distributed under the GNU LGPLv3. See LICENSE for more info.
from os import error
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 +69,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
......
......@@ -5,175 +5,184 @@
# 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 importlib import import_module
from bliss.common.protocols import CounterContainer
from bliss.common.counter import (
Counter,
CalcCounter,
SamplingCounter,
IntegratingCounter,
)
from bliss.common.protocols import counter_namespace
from bliss.common.utils import autocomplete_property
from bliss.comm.util import get_comm
from bliss.controllers.motors.mockup import Mockup, MockupAxis
from bliss.controllers.counter import (
CounterController,
SamplingCounterController,
IntegratingCounterController,
)
from bliss.scanning.acquisition.counter import BaseCounterAcquisitionSlave
from bliss.config.static import ConfigReference, ConfigNode, ConfigList
# from bliss.config.beacon_object import BeaconObject
from bliss.common.logtools import log_info, log_debug, log_debug_data, log_warning
def find_sub_names_config(config, selection=None, level=0, parent_key=None):
""" Recursively search in a config the sub-sections where the key 'name' is found.
Returns a dict of tuples (sub_config, parent_key) indexed by level (0 is the top level).
- sub_config: the sub-config containing the 'name' key
- parent_key: key under which the sub_config was found (None for level 0)
# ============ Note about BlissController ==============
#
## --- BlissController ---
# The BlissController base class is designed for the implementation of all controllers in Bliss.
# It ensures that all controllers have the following properties:
#
# class BlissController:
# @name (can be None if only sub-items are named)
# @config (yml config)
# @hardware (associated hardware controller object, can be None if no hardware)
# @counters (associated counters)
# @axes (associated axes: real/calc/soft/pseudo)
#
# Nothing else from the base class methods will be exposed at the first level object API.
#
# The BlissController is designed to ease the management of sub-objects that depend on a common device (@hardware).
# The sub-objects are declared in the yml configuration of the bliss controller under dedicated sub-sections.
#
# A sub-object is considered as a sub-item if it has a name (key 'name' in a sub-section of the config).
# Most of the time sub-items are counters and/or axes but could be anything else (known by the custom bliss controller).
#
# The BlissController has 2 properties (@counters, @axes) to retrieve sub-items that can be identified
# as counters (Counter) or axes (Axis).
#
## --- Plugin ---
# BlissController objects are created from the yml config using the bliss_controller plugin.
# Any sub-item with a name can be imported in a Bliss session with config.get('name').
# The plugin ensures that the controller and sub-items 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 sub-items.
# It looks for the 'class' key in the config to instantiate the BlissController.
# While importing any sub-item in the session, the bliss controller is instantiated first (if not alive already).
#
# !!! The effective creation of the sub-items is performed by the BlissController itself and the plugin just ensures
# that the controller is always created before sub-items and only once, that's all !!!
# The sub-items can be created during the initialization of the BlissController or via
# BlissController._create_sub_item(itemname, itemcfg, parentkey) which is called only on the first config.get('itemname')
#
## --- yml config ---
#
# - plugin: bliss_controller <== use the dedicated bliss controller 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: value <== another parameter for the custom bliss controller creation (optional)
#
# sub-section-1: <== a sub-section where sub-items can be declared (optional) (ex: 'counters')
# - name: sub_item_1 <== config of the sub-item
# tag : item_tag_1 <== a tag for this item (known and interpreted by the custom bliss controller)
# sub_param_1: value <== a custom parameter for the item creation
#
# sub-section-2: <== a sub-section where sub-items can be declared (optional) (ex: 'axes')
# - name: sub_item_2 <== config of the sub-item
# tag : item_tag_2 <== a tag for this item (known and interpreted by the custom bliss controller)
#
# 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 without sub-items (no 'name' key) (optional)
# - anything_but_name: foo <== something interpreted by the custom bliss controller
# something: value
args:
config: the config that should be explored
selection: a list containing the info of the subnames already found (for recursion)
level: an integer describing at which level the subname was found (level=0 is the top/upper level) (for recursion)
parent_key: key under which the sub_config was found (None for level 0) (for recursion)
"""
assert isinstance(config, (ConfigNode, dict))
class HardwareController:
def __init__(self, config):
self._config = config
self._last_cmd_time = perf_counter()
self._cmd_min_delta_time = 0
if selection is None:
selection = {}
self._init_com()
if selection.get(level) is None:
selection[level] = []
@property
def config(self):
return self._config
if isinstance(config, ConfigNode):
name = config.raw_get("name")
else:
name = config.get("name")
@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:
delta_t = now - self._last_cmd_time
if delta_t < self._cmd_min_delta_time:
sleep(self._cmd_min_delta_time - delta_t)
return self._send_cmd(cmd, *values)
def _send_cmd(self, cmd, *values):
if values:
return self._write_cmd(cmd, *values)
else:
return self._read_cmd(cmd)
if name is not None:
selection[level].append((config, parent_key))
def _init_com(self):
log_info(self, "_init_com", self.config)
self._comm = get_comm(self.config)
global_map.register(self._comm, parents_list=[self, "comms"])
if isinstance(config, ConfigNode):
cfg_items = (
config.raw_items()
) # !!! raw_items to avoid cyclic import while resloving reference !!!
else:
cfg_items = config.items()
# ========== NOT IMPLEMENTED METHODS ====================
def _write_cmd(self, cmd, *values):
# return self._comm.write(cmd, *values)
raise NotImplementedError
for k, v in cfg_items:
if isinstance(v, (ConfigNode, dict)):
find_sub_names_config(v, selection, level + 1, k)
def _read_cmd(self, cmd):
# return self._comm.read(cmd)
raise NotImplementedError
elif isinstance(v, (ConfigList, list)):
for i in v:
if isinstance(i, (ConfigNode, dict)):
find_sub_names_config(i, selection, level + 1, k)
return selection
class BlissController(CounterContainer):
_COUNTER_TAGS = {}
def from_config_dict(ctrl_class, cfg_dict):
""" Helper to instanciate a BlissController object from a configuration dictionary """
if not BlissController in ctrl_class.mro():
raise TypeError(f"{ctrl_class} is not a BlissController class")
bctrl = ctrl_class(cfg_dict)
bctrl._controller_init()
return bctrl
class BlissController(CounterContainer):
"""
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.
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 bliss_controller 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()
# --- Plugin limitations ----
Use references to declare subitems that also have subitems (i.e subitem of type bliss controller).
It is possible to build a bliss controller which have subitems of the type BlissController.
But in that case, the declaration of the subitems of the different bliss controllers cannot be
merged in the configuration of the top controller. Each bliss controller must be decalred separately
and one can reference this other in its config with '$name'. Using a reference to bliss_controllers
subitems will ensure that the plugin will associate the correct controller to subitems.
# --- 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._controller_init()' 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.