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

bliss controller plugin and subitem management

parent 2fab0745
......@@ -6,26 +6,103 @@
# Distributed under the GNU LGPLv3. See LICENSE for more info.
from re import subn
from bliss.config.plugins.utils import find_class_and_node
from bliss.config.static import ConfigNode, ConfigReference
# from bliss.config.static import ConfigNode, ConfigReference
def create_objects_from_config_node(config, cfg_node):
klass, node = find_class_and_node(cfg_node)
item_name = cfg_node["name"]
if node.get("name") != item_name:
cfg_node = ConfigNode.indexed_nodes[item_name]
else:
cfg_node = node
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 ($)
"""
o = klass(item_name, cfg_node.clone())
if selection is None:
selection = {}
for key, value in cfg_node.items():
if isinstance(cfg_node.raw_get(key), ConfigReference):
if hasattr(o, key):
continue
else:
setattr(o, key, value)
if selection.get(level) is None:
selection[level] = []
return {item_name: o}
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.items():
if isinstance(v, dict):
find_sub_names_config(v, selection, level + 1, k)
elif isinstance(v, list):
for i in v:
if isinstance(i, dict):
find_sub_names_config(i, selection, level + 1, k)
return selection
def create_objects_from_config_node(cfg_obj, cfg_node):
"""
Create an object from the config with a given name (unique).
It ensures that the controller and sub-objects are only created once.
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.
args:
cfg_obj: a Config object (from config.static)
cfg_node: a ConfigNode object (from config.static)
yield:
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
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")
item_name = cfg_node["name"] # name of the item that should be created and returned
# always create the bliss controller first
bctrl = klass(ctrl_name, ctrl_node.clone())
# find all sub objects with a name in controller config
sub_cfgs = find_sub_names_config(ctrl_node.to_dict())
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
# --- 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
return bctrl._create_sub_item(name, cfg, pkey)
......@@ -13,10 +13,16 @@ from gevent import Timeout, event, sleep as gsleep
from bliss import global_map
from bliss.common.protocols import CounterContainer
from bliss.common.counter import Counter, CalcCounter, SamplingCounter
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,
......@@ -29,6 +35,77 @@ from bliss.config.beacon_object import BeaconObject
from bliss.common.logtools import log_info, log_debug, log_debug_data, log_warning
# ============ 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
class HardwareController:
def __init__(self, config):
self._config = config
......@@ -78,7 +155,7 @@ class HardwareController:
class BlissController(CounterContainer):
COUNTER_TAGS = {}
_COUNTER_TAGS = {}
def __init__(self, name, config):
......@@ -92,10 +169,13 @@ class BlissController(CounterContainer):
self._build_axes()
self._build_counters()
print("=== Create BlissController")
@autocomplete_property
def hw_controller(self):
def hardware(self):
if self._hw_controller is None:
self._hw_controller = self._get_hardware()
print("=== _get_hardware", self._hw_controller)
return self._hw_controller
@property
......@@ -107,10 +187,34 @@ class BlissController(CounterContainer):
return self._config
# ========== NOT IMPLEMENTED METHODS ====================
def _get_hardware(self):
""" Must return an HardwareController object """
raise NotImplementedError
def _create_sub_item(self, name, cfg, parent_key):
""" Create/get and return an object which has a config name and which is owned by this controller
This method is called by the Bliss Controller Plugin and is called after the controller __init__().
This method is called only once per item on the first config.get('item_name') call (see plugin).
args:
'name': sub item name
'cfg' : sub item config
'parent_key': the config key under which the sub item was found (ex: 'counters').
return: the sub item object
"""
# === Example ===
# if parent_key == 'counters': #and name in self.counters._fields
# return self.counters[name]
# elif parent_key == 'axes': # and name in self.axes._fields
# return self.axes[name]
raise NotImplementedError
def _load_config(self):
""" Read and apply the YML configuration """
raise NotImplementedError
......@@ -123,20 +227,19 @@ class BlissController(CounterContainer):
""" Build the Axes (real and pseudo) """
raise NotImplementedError
@property
@autocomplete_property
def counters(self):
# cnts = [ctrl.counters for ctrl in self._counter_controllers.values()]
# return counter_namespace(chain(*cnts))
raise NotImplementedError
@property
@autocomplete_property
def axes(self):
# axes = [ctrl.axes for ctrl in self._axis_controllers]
# return dict(ChainMap(*axes))
raise NotImplementedError
# ========== MOCKUP CLASSES ==============================
......@@ -166,22 +269,46 @@ class HCMockup(HardwareController):
class BCMockup(BlissController):
COUNTER_TAGS = {
_COUNTER_TAGS = {
"current_temperature": ("cur_temp_ch1", "scc"),
"integration_time": ("int_time", "icc"),
}
def _create_sub_item(self, name, cfg, parent_key):
""" Create/get and return an object which has a config name and which is owned by this controller
This method is called by the Bliss Controller Plugin and is called after the controller __init__().
This method is called only once per item on the first config.get('item_name') call (see plugin).
args:
'name': sub item name
'cfg' : sub item config
'parent_key': the config key under which the sub item was found (ex: 'counters').
return: the sub item object
"""
if parent_key == "counters":
return self.counters[name]
elif parent_key == "axes":
return self.axes[name]
# return self._motor_controller.get_axis(name)
def _get_hardware(self):
""" Must return an HardwareController object """
return HCMockup(self.config["com"])
def _load_config(self):
""" Read and apply the YML configuration """
print("load config", self.config)
# print("load config", self.config)
if self.config.get("energy"):
self.energy = self.config.get("energy")
def _build_counters(self):
""" Build the CounterControllers and associated Counters"""
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
)
......@@ -193,29 +320,43 @@ class BCMockup(BlissController):
unit = cfg.get("unit")
convfunc = cfg.get("convfunc")
if self.COUNTER_TAGS[tag][1] == "scc":
if self._COUNTER_TAGS[tag][1] == "scc":
cnt = self._counter_controllers["scc"].create_counter(
SamplingCounter, name, unit=unit, mode=mode
)
cnt.tag = tag
elif self._COUNTER_TAGS[tag][1] == "icc":
cnt = self._counter_controllers["icc"].create_counter(
IntegratingCounter, name, unit=unit
)
cnt.tag = tag
def _build_axes(self):
""" Build the Axes (real and pseudo) """
# raise NotImplementedError
pass
@property
axes_cfg = {
cfg["name"]: (MockupAxis, cfg) for cfg in self.config.get("axes", [])
}
self._motor_controller = Mockup(
"motmock", {}, axes_cfg, [], [], []
) # self.config
# === ??? initialize all now ???
for name in axes_cfg.keys():
self._motor_controller.get_axis(name)
@autocomplete_property
def counters(self):
cnts = [ctrl.counters for ctrl in self._counter_controllers.values()]
return counter_namespace(chain(*cnts))
@property
@autocomplete_property
def axes(self):
# axes = [ctrl.axes for ctrl in self._axis_controllers]
# return dict(ChainMap(*axes))
# raise NotImplementedError
return counter_namespace({})
return counter_namespace(self._motor_controller.axes)
class BCSCC(SamplingCounterController):
......@@ -226,9 +367,9 @@ class BCSCC(SamplingCounterController):
def read_all(self, *counters):
values = []
for cnt in counters:
tag_info = self.bctrl.COUNTER_TAGS.get(cnt.tag)
tag_info = self.bctrl._COUNTER_TAGS.get(cnt.tag)
if tag_info:
values.append(self.bctrl.hw_controller.send_cmd(tag_info[0]))
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
......@@ -236,74 +377,54 @@ class BCSCC(SamplingCounterController):
return values
# class BCICC(IntegratingCounterController):
# def __init__(self, name, bctrl):
# super().__init__(name)
# self.bctrl = bctrl
# def get_values(self, from_index, *counters):
# class BCICC(CounterController):
# def __init__(self, name, bctrl):
# super().__init__(name)
# self.bctrl = bctrl
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_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):
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"]
try:
count_time = acq_params["count_time"]
except KeyError:
count_time = scan_params["count_time"]
# try:
# npoints = acq_params["npoints"]
# except KeyError:
# npoints = scan_params["npoints"]
try:
npoints = acq_params["npoints"]
except KeyError:
npoints = scan_params["npoints"]
# params = {"count_time": count_time, "npoints": npoints}
params = {"count_time": count_time, "npoints": npoints}
# return params
return params
# def read_counts(self):
# """ returns status, nremain, ntotal, time, counts """
# return self.bctrl.hw_controller.send_cmd()
def read_data(self):
gsleep(self.count_time)
# class BCIAS(BaseCounterAcquisitionSlave):
# def __init__(self, *args, **kwargs):
# super().__init__(*args, **kwargs)
# self._reading_event = event.Event()
class BCIAS(BaseCounterAcquisitionSlave):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._reading_event = event.Event()
# def prepare(self):
# pass
def prepare(self):
self.device.count_time = self.count_time
# def start(self):
# self._stop_flag = False
# self._reading_event.clear()
def start(self):
pass
# def stop(self):
# self._stop_flag = True
# self._reading_event.set()
# if not self.device.counter_is_ready:
# self.device.counting_stop()
def stop(self):
pass
# def trigger(self):
# self.device.counting_start(self.count_time)
# gsleep(self.count_time)
# self._reading_event.set()
def trigger(self):
pass
# def reading(self):
# self._reading_event.wait()
# self._reading_event.clear()
# with Timeout(2.0):
# while not self._stop_flag:
# status, nremain, ntotal, ctime, counts = self.device.read_counts()
# if status == "D":
# self._emit_new_data([[counts]])
# break
# else:
# gevent.sleep(0.001)
def reading(self):
t0 = perf_counter()
self.device.read_data()
dt = perf_counter() - t0
self._emit_new_data([[dt]])
......@@ -7,7 +7,8 @@
tcp:
url: bcmock
energy: $robz
counters:
- name: bctemp
tag: current_temperature
......@@ -33,4 +34,8 @@
- name: axis2
tag: zgap
steps_per_unit: 10000
velocity: 2500.0
acceleration: 1000.0
low_limit: -180
high_limit: 180
\ No newline at end of file
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment