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
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
# update the config cache dict NOW to avoid cyclic instanciation (i.e. config.get => create_object_from_... => config.get )
yield name2items, name2cacheditems
# 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.
# --- 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, 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
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
......
This diff is collapsed.
# -*- 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 collections import ChainMap
from gevent import event, sleep as gsleep
from bliss import global_map
from bliss.common.counter import (
SamplingCounter
) # make it available at ctrl level for plugin and tests
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, log_debug, log_debug_data, log_warning
from bliss.controllers.bliss_controller import BlissController, from_config_dict
from bliss.controllers.motors.mockup import Mockup, calc_motor_mockup
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:
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):
log_info(self, "_init_com", self.config)
self._comm = get_comm(self.config)
global_map.register(self._comm, parents_list=[self, "comms"])
# ========== NOT IMPLEMENTED METHODS ====================
def _write_cmd(self, cmd, *values):
# return self._comm.write(cmd, *values)
raise NotImplementedError
def _read_cmd(self, cmd):
# return self._comm.read(cmd)
raise NotImplementedError
class HCMockup(HardwareController):
class FakeCom:
def __init__(self, config):
pass
def read(self, cmd):
return 69
def write(self, cmd, *values):
print("HCMockup write", cmd, values)
return True
def _init_com(self):
log_info(self, "_init_com", self.config)
self._comm = HCMockup.FakeCom(self.config)
global_map.register(self._comm, parents_list=[self, "comms"])
def _write_cmd(self, cmd, *values):
return self._comm.write(cmd, *values)
def _read_cmd(self, cmd):
return self._comm.read(cmd)
class BCMockup(BlissController):
_COUNTER_TAGS = {
"current_temperature": ("cur_temp_ch1", "scc"),
"integration_time": ("int_time", "icc"),
}
def _get_hardware(self):
""" return the low level hardware controller interface """
return HCMockup(self.config["com"])
def _get_subitem_default_module(self, class_name, cfg, parent_key):
if class_name == "IntegratingCounter":
return "bliss.common.counter"
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"
def _get_config_subitem(self, name, cfg, parent_key, item_class):
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: # mean it is a referenced axis (i.e external axis)
axis = name # the axis instance
name = axis.name # the axis name
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
def _load_config(self):
self._calc_mot = None
if self.config.get("energy"):
self.energy = self.config.get("energy")
# create different counter controllers
self._counter_controllers = {}
self._counter_controllers["scc"] = BCSCC("scc", self)
self._counter_controllers["icc"] = BCICC("icc", self)
self._counter_controllers["scc"].max_sampling_frequency = self.config.get(
"max_sampling_frequency", 1
)
# create the counter subitems now in order to have all of them immediately available after ctrl init
for cfg, pkey in self._subitems_config.values():
if pkey == "counters":
self._get_subitem(cfg["name"]) # force item creation now
# prepare a storage for the tags associated to the axes referenced in the config
if self.config.get("axes") is not None:
self._tag2axis = {}
@autocomplete_property
def counters(self):
cnts = [ctrl.counters for ctrl in self._counter_controllers.values()]
return counter_namespace(chain(*cnts))
@autocomplete_property
def axes(self):
return IterableNamespace(
**{name: self._subitems[name] for name in self._tag2axis.values()}
)
def get_axis(self, name):
return self._get_subitem(name)
def available_axis_names(self):
return [k for k, v in self._subitems_config.items() if v[1] == "axes"]
@property
def calc_mot(self):
if self._calc_mot is None:
self._calc_mot = self.config.get("calc_controller")
return self._calc_mot
class BCSCC(SamplingCounterController):
def __init__(self, name, bctrl):
super().__init__(name)
self.bctrl = bctrl
def read_all(self, *counters):
values = []
for cnt in counters:
tag_info = self.bctrl._COUNTER_TAGS.get(cnt.tag)
if tag_info:
values.append(self.bctrl.hardware.send_cmd(tag_info[0]))
else:
# returned number of data must be equal to the length of '*counters'
# so raiseError if one of the received counter is not handled
raise ValueError(f"Unknown counter {cnt} with tag {cnt.tag} !")
return values
class BCICC(CounterController):
def __init__(self, name, bctrl):
super().__init__(name)
self.bctrl = bctrl
self.count_time = None
def get_acquisition_object(self, acq_params, ctrl_params, parent_acq_params):
return BCIAS(self, ctrl_params=ctrl_params, **acq_params)
def get_default_chain_parameters(self, scan_params, acq_params):
try:
count_time = acq_params["count_time"]
except KeyError:
count_time = scan_params["count_time"]