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

Motor controller as BlissController

parent 24b008a1
...@@ -610,7 +610,7 @@ class FilterSet: ...@@ -610,7 +610,7 @@ class FilterSet:
def get_filters(self): 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 - position
- density_calc - density_calc
- transmission_calc - transmission_calc
......
...@@ -241,7 +241,7 @@ class FilterSet_Wago(FilterSet): ...@@ -241,7 +241,7 @@ class FilterSet_Wago(FilterSet):
def get_filters(self): 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 - position
- transmission_calc - transmission_calc
""" """
......
...@@ -206,7 +206,7 @@ class FilterSet_Wheel(FilterSet): ...@@ -206,7 +206,7 @@ class FilterSet_Wheel(FilterSet):
def get_filters(self): 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 - position
- transmission_calc - transmission_calc
For the wheel filterset _filters = _config_filters For the wheel filterset _filters = _config_filters
......
...@@ -23,19 +23,7 @@ def SoftAxis( ...@@ -23,19 +23,7 @@ def SoftAxis(
unit=None, unit=None,
): ):
# if callable(position): config = {"low_limit": low_limit, "high_limit": high_limit, "name": name}
# 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,
}
if tolerance is not None: if tolerance is not None:
config["tolerance"] = tolerance config["tolerance"] = tolerance
...@@ -44,8 +32,8 @@ def SoftAxis( ...@@ -44,8 +32,8 @@ def SoftAxis(
config["unit"] = unit config["unit"] = unit
controller = SoftController(name, obj, config, position, move, stop, state) controller = SoftController(name, obj, config, position, move, stop, state)
controller._controller_init()
controller._init()
axis = controller.get_axis(name) axis = controller.get_axis(name)
axis._positioner = False axis._positioner = False
......
...@@ -5,62 +5,8 @@ ...@@ -5,62 +5,8 @@
# Copyright (c) 2015-2020 Beamline Control Unit, ESRF # Copyright (c) 2015-2020 Beamline Control Unit, ESRF
# Distributed under the GNU LGPLv3. See LICENSE for more info. # Distributed under the GNU LGPLv3. See LICENSE for more info.
from bliss.config.plugins.utils import find_top_class_and_node
# ================ IMPORTANT NOTE_ ABOUT PLUGIN CYCLIC IMPORT ============= from bliss.controllers.bliss_controller import BlissController
#
# 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
def create_objects_from_config_node(cfg_obj, cfg_node): def create_objects_from_config_node(cfg_obj, cfg_node):
...@@ -71,62 +17,65 @@ 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. 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. 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: args:
cfg_obj: a Config object (from config.static) cfg_obj: a Config object (from config.static)
cfg_node: a ConfigNode object (from config.static) cfg_node: a ConfigNode object (from config.static)
yield: 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 # search the 'class' key in cfg_node or at a upper node level
# return the class and the associated config node # then return the class and the associated config node
# upper_node = cfg_node.parent ?? klass, ctrl_node = find_top_class_and_node(cfg_node)
klass, ctrl_node = find_class_and_node(cfg_node) ctrl_name = ctrl_node.get("name") # ctrl could have a name in config
# print("=== FOUND BLISS CONTROLLER CLASS", klass, "WITH NODE", ctrl_node)
ctrl_name = ctrl_node.get("name")
item_name = cfg_node["name"] # name of the item that should be created and returned item_name = cfg_node["name"] # name of the item that should be created and returned
# always create the bliss controller first # always create the bliss controller first
bctrl = klass(ctrl_node.clone()) bctrl = klass(ctrl_node)
# find all sub objects with a name in controller config print(f"\n=== From config: {item_name} from {bctrl.name}")
sub_cfgs = find_sub_names_config(ctrl_node) # .to_dict(resolve_references=False))
for level in sorted(sub_cfgs.keys()): if isinstance(bctrl, BlissController):
if level != 0: # ignore the controller itself
for cfg, pkey in sub_cfgs[level]: # prepare subitems configs and cache item's controller
subname = cfg["name"] names_to_cache = bctrl._prepare_subitems_configs(ctrl_node)
# if subname == item_name: # this is the sub-object to return cachednames2ctrl = {name: bctrl for name in names_to_cache}
# name2items[item_name] = bctrl._create_sub_item(item_name, cfg, pkey)
# else: # store sub-object info for later instantiation print(f"\n=== Caching: {names_to_cache} from {bctrl.name}")
name2cacheditems[subname] = (bctrl, cfg, pkey)
# # --- add the controller to stored items if it has a name
# --- add the controller to stored items if it has a name name2items = {}
if ctrl_name: if ctrl_name:
name2items[ctrl_name] = bctrl name2items[ctrl_name] = bctrl
# name2items[bctrl.name] = bctrl
# update the config cache dict NOW to avoid cyclic instanciation (i.e. config.get => create_object_from_... => config.get )
yield name2items, name2cacheditems # 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.
# --- don't forget to instanciate the object for which this function has been called (if not a controller) yield name2items, cachednames2ctrl
if item_name != ctrl_name:
obj = cfg_obj.get(item_name) # load config and init controller
yield {item_name: obj} bctrl._controller_init()
# --- NOW, any new object_name going through 'config.get( obj_name )' should call 'create_object_from_cache' only. # --- don't forget to instanciate the object for which this function has been called (if not a controller)
# --- 'create_objects_from_config_node' should never be called again for any object related to the controller instanciated here (see config.get code) if item_name != ctrl_name:
obj = cfg_obj.get(item_name)
yield {item_name: obj}
def create_object_from_cache(config, name, cached_object_info):
print("===== REGULATION FROM CACHE", name) # config, name, object_info) # --- Now any new object_name going through 'config.get( obj_name )' should call 'create_object_from_cache' only.
bctrl, cfg, pkey = cached_object_info # --- 'create_objects_from_config_node' should never be called again for any object related to the controller instanciated here (see config.get code)
new_object = bctrl._create_sub_item(name, cfg, pkey)
return new_object 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): ...@@ -68,9 +68,9 @@ def get_axes_info(diffracto):
def create_hkl_motors(diffracto, axes_info): def create_hkl_motors(diffracto, axes_info):
hklmots = HKLMotors( config = diffracto.config.clone()
f"{diffracto.name}_motors", diffracto, diffracto.config, axes_info config["name"] = f"{diffracto.name}_motors"
) hklmots = HKLMotors(diffracto, diffracto.config, axes_info)
# --- force axis init before CalcController._init (see emotion) # --- force axis init before CalcController._init (see emotion)
for axname in axes_info: for axname in axes_info:
hklmots.get_axis(axname) hklmots.get_axis(axname)
......
...@@ -5,12 +5,13 @@ ...@@ -5,12 +5,13 @@
# Copyright (c) 2015-2020 Beamline Control Unit, ESRF # Copyright (c) 2015-2020 Beamline Control Unit, ESRF
# Distributed under the GNU LGPLv3. See LICENSE for more info. # Distributed under the GNU LGPLv3. See LICENSE for more info.
from os import error
import re import re
from importlib.util import find_spec 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_TO_MODULE_NAME = {"Lima": "bliss.controllers.lima.lima_base"}
"""Alias defined for default BLISS controllers"""
def camel_case_to_snake_style(name): def camel_case_to_snake_style(name):
...@@ -68,3 +69,72 @@ def find_class_and_node(cfg_node, base_path="bliss.controllers"): ...@@ -68,3 +69,72 @@ def find_class_and_node(cfg_node, base_path="bliss.controllers"):
klass = getattr(module, class_name.title()) klass = getattr(module, class_name.title())
return klass, node 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): ...@@ -462,6 +462,17 @@ class ConfigNode(MutableMapping):
return self._parent.is_service return self._parent.is_service
return through_server 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): def get_inherited_value_and_node(self, key):
""" """
@see get_inherited @see get_inherited
......
...@@ -5,175 +5,184 @@ ...@@ -5,175 +5,184 @@
# Copyright (c) 2015-2020 Beamline Control Unit, ESRF # Copyright (c) 2015-2020 Beamline Control Unit, ESRF
# Distributed under the GNU LGPLv3. See LICENSE for more info. # Distributed under the GNU LGPLv3. See LICENSE for more info.
from time import perf_counter, sleep from importlib import import_module
from itertools import chain
from gevent import event, sleep as gsleep
from bliss import global_map
from bliss.common.protocols import CounterContainer 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.common.utils import autocomplete_property
from bliss.comm.util import get_comm from bliss.config.static import ConfigReference, ConfigNode, ConfigList
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.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 ============== args:
# config: the config that should be explored
## --- BlissController --- selection: a list containing the info of the subnames already found (for recursion)
# The BlissController base class is designed for the implementation of all controllers in Bliss. level: an integer describing at which level the subname was found (level=0 is the top/upper level) (for recursion)
# It ensures that all controllers have the following properties: parent_key: key under which the sub_config was found (None for level 0) (for recursion)
# """
# 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
assert isinstance(config, (ConfigNode, dict))
class HardwareController: if selection is None:
def __init__(self, config): selection = {}
self._config = config
self._last_cmd_time = perf_counter()
self._cmd_min_delta_time = 0
self._init_com() if selection.get(level) is None:
selection[level] = []
@property if isinstance(config, ConfigNode):
def config(self): name = config.raw_get("name")
return self._config else:
name = config.get("name")
@property if name is not None:
def comm(self): selection[level].append((config, parent_key))
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)
def _init_com(self): if isinstance(config, ConfigNode):
log_info(self, "_init_com", self.config) cfg_items = (
self._comm = get_comm(self.config) config.raw_items()
global_map.register(self._comm, parents_list=[self, "comms"]) ) # !!! raw_items to avoid cyclic import while resloving reference !!!
else:
cfg_items = config.items()
# ========== NOT IMPLEMENTED METHODS ==================== for k, v in cfg_items:
def _write_cmd(self, cmd, *values): if isinstance(v, (ConfigNode, dict)):
# return self._comm.write(cmd, *values) find_sub_names_config(v, selection, level + 1, k)
raise NotImplementedError
def _read_cmd(self, cmd): elif isinstance(v, (ConfigList, list)):
# return self._comm.read(cmd) for i in v:
raise NotImplementedError if isinstance(i, (ConfigNode, dict)):
find_sub_names_config(i, selection, level + 1, k)
return selection