Commit 24b008a1 authored by Perceval Guillou's avatar Perceval Guillou
Browse files

Regulator as BlissController

parent 31029613
...@@ -170,6 +170,8 @@ import gevent ...@@ -170,6 +170,8 @@ import gevent
import gevent.event import gevent.event
import enum import enum
from bliss.common.protocols import CounterContainer
from bliss.common.logtools import log_debug, disable_user_output from bliss.common.logtools import log_debug, disable_user_output
from bliss.common.utils import with_custom_members, autocomplete_property from bliss.common.utils import with_custom_members, autocomplete_property
from bliss.common.counter import SamplingCounter from bliss.common.counter import SamplingCounter
...@@ -200,38 +202,38 @@ def _get_external_device_name(device): ...@@ -200,38 +202,38 @@ def _get_external_device_name(device):
return 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 @with_custom_members
class Input(SamplingCounterController): class Input(CounterContainer):
"""Implements the access to an input device which is accessed via the """Implements the access to an input device which is accessed via the
regulation controller (like a sensor plugged on a channel of the controller) regulation controller (like a sensor plugged on a channel of the controller)
""" """
def __init__(self, controller, config): def __init__(self, controller, config):
""" Constructor """ """ Constructor """
self._name = config["name"]
super().__init__(name=config["name"])
self._controller = controller self._controller = controller
self._config = config self._config = config
self._channel = self._config.get("channel") self._channel = self._config.get("channel")
self.max_sampling_frequency = config.get("max_sampling_frequency", 5)
# useful attribute for a temperature controller writer # useful attribute for a temperature controller writer
self._attr_dict = {} self._attr_dict = {}
self._build_counters() @property
def name(self):
def _build_counters(self): return self._name
self.create_counter(
SamplingCounter,
self.name + "_counter",
unit=self._config.get("unit", "N/A"),
mode=self._config.get("sampling-counter-mode", "SINGLE"),
)
def read_all(self, *counters): @autocomplete_property
return [self.read()] def counters(self):
return counter_namespace({self.name: self._controller.counters[self.name]})
# ----------- BASE METHODS ----------------------------------------- # ----------- BASE METHODS -----------------------------------------
...@@ -316,7 +318,13 @@ class ExternalInput(Input): ...@@ -316,7 +318,13 @@ class ExternalInput(Input):
self.device = config.get("device") self.device = config.get("device")
self.load_base_config() 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): def __info__(self):
lines = ["\n"] lines = ["\n"]
...@@ -358,7 +366,7 @@ class ExternalInput(Input): ...@@ -358,7 +366,7 @@ class ExternalInput(Input):
@with_custom_members @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) """ 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. The Output has a ramp object.
...@@ -370,7 +378,7 @@ class Output(SamplingCounterController): ...@@ -370,7 +378,7 @@ class Output(SamplingCounterController):
def __init__(self, controller, config): def __init__(self, controller, config):
""" Constructor """ """ Constructor """
super().__init__(name=config["name"]) self._name = config["name"]
self._controller = controller self._controller = controller
self._config = config self._config = config
...@@ -387,20 +395,13 @@ class Output(SamplingCounterController): ...@@ -387,20 +395,13 @@ class Output(SamplingCounterController):
# useful attribute for a temperature controller writer # useful attribute for a temperature controller writer
self._attr_dict = {} self._attr_dict = {}
self.max_sampling_frequency = config.get("max_sampling_frequency", 5) @property
def name(self):
self._build_counters() return self._name
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"),
)
def read_all(self, *counters): @autocomplete_property
return [self.read()] def counters(self):
return counter_namespace({self.name: self._controller.counters[self.name]})
# ----------- BASE METHODS ----------------------------------------- # ----------- BASE METHODS -----------------------------------------
...@@ -608,6 +609,14 @@ class ExternalOutput(Output): ...@@ -608,6 +609,14 @@ class ExternalOutput(Output):
self.mode = config.get("mode", "relative") self.mode = config.get("mode", "relative")
self.load_base_config() self.load_base_config()
self._controller = SCC(self.name, self)
self._controller.create_counter(
SamplingCounter,
self.name,
unit=self._config.get("unit"),
mode=self._config.get("mode", "SINGLE"),
)
# ----------- BASE METHODS ----------------------------------------- # ----------- BASE METHODS -----------------------------------------
@property @property
...@@ -700,7 +709,7 @@ class ExternalOutput(Output): ...@@ -700,7 +709,7 @@ class ExternalOutput(Output):
@with_custom_members @with_custom_members
class Loop(SamplingCounterController): class Loop(CounterContainer):
""" Implements the access to the regulation loop """ Implements the access to the regulation loop
The regulation is the PID process that: The regulation is the PID process that:
...@@ -733,7 +742,7 @@ class Loop(SamplingCounterController): ...@@ -733,7 +742,7 @@ class Loop(SamplingCounterController):
def __init__(self, controller, config): def __init__(self, controller, config):
""" Constructor """ """ Constructor """
super().__init__(name=config["name"]) self._name = config["name"]
self._controller = controller self._controller = controller
self._config = config self._config = config
...@@ -761,20 +770,8 @@ class Loop(SamplingCounterController): ...@@ -761,20 +770,8 @@ class Loop(SamplingCounterController):
self.reg_plot = None self.reg_plot = None
self.max_sampling_frequency = config.get("max_sampling_frequency", 5)
self._build_counters()
self._create_soft_axis() 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): def __del__(self):
self.close() self.close()
...@@ -787,8 +784,16 @@ class Loop(SamplingCounterController): ...@@ -787,8 +784,16 @@ class Loop(SamplingCounterController):
# ----------- BASE METHODS ----------------------------------------- # ----------- BASE METHODS -----------------------------------------
def read_all(self, *counters): @lazy_init
return [self._get_working_setpoint()] 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 ##--- CONFIG METHODS
def load_base_config(self): def load_base_config(self):
...@@ -847,7 +852,7 @@ class Loop(SamplingCounterController): ...@@ -847,7 +852,7 @@ class Loop(SamplingCounterController):
all_counters = ( all_counters = (
list(self.input.counters) list(self.input.counters)
+ list(self.output.counters) + list(self.output.counters)
+ list(self._counters.values()) + [self._controller.counters[self.name]]
) )
return counter_namespace(all_counters) return counter_namespace(all_counters)
...@@ -1484,6 +1489,14 @@ class SoftLoop(Loop): ...@@ -1484,6 +1489,14 @@ class SoftLoop(Loop):
self.load_base_config() self.load_base_config()
self.max_attempts_before_failure = config.get("max_attempts_before_failure", 3) 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): def __info__(self):
lines = ["\n"] lines = ["\n"]
lines.append(f"=== SoftLoop: {self.name} ===") lines.append(f"=== SoftLoop: {self.name} ===")
...@@ -1517,6 +1530,12 @@ class SoftLoop(Loop): ...@@ -1517,6 +1530,12 @@ class SoftLoop(Loop):
self._ramp.stop() self._ramp.stop()
self._stop_regulation() self._stop_regulation()
def read(self):
""" Return the current working setpoint """
log_debug(self, "SoftLoop:read")
return self._get_working_setpoint()
@property @property
def max_attempts_before_failure(self): def max_attempts_before_failure(self):
""" """
......
...@@ -6,10 +6,22 @@ ...@@ -6,10 +6,22 @@
# Distributed under the GNU LGPLv3. See LICENSE for more info. # Distributed under the GNU LGPLv3. See LICENSE for more info.
from re import subn # ================ 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.plugins.utils import find_class_and_node
# from bliss.config.static import ConfigNode, ConfigReference from bliss.config.static import ConfigNode, ConfigList
def find_sub_names_config( def find_sub_names_config(
...@@ -34,13 +46,18 @@ def find_sub_names_config( ...@@ -34,13 +46,18 @@ def find_sub_names_config(
if not exclude_ref or not config.get("name").startswith("$"): if not exclude_ref or not config.get("name").startswith("$"):
selection[level].append((config, parent_key)) selection[level].append((config, parent_key))
for k, v in config.items(): for (
if isinstance(v, dict): 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) find_sub_names_config(v, selection, level + 1, k)
elif isinstance(v, list): elif isinstance(v, ConfigList):
for i in v: for i in v:
if isinstance(i, dict): if isinstance(i, ConfigNode):
find_sub_names_config(i, selection, level + 1, k) find_sub_names_config(i, selection, level + 1, k)
return selection return selection
...@@ -71,6 +88,7 @@ def create_objects_from_config_node(cfg_obj, cfg_node): ...@@ -71,6 +88,7 @@ def create_objects_from_config_node(cfg_obj, cfg_node):
# 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 # return the class and the associated config node
# upper_node = cfg_node.parent ??
klass, ctrl_node = find_class_and_node(cfg_node) klass, ctrl_node = find_class_and_node(cfg_node)
# print("=== FOUND BLISS CONTROLLER CLASS", klass, "WITH NODE", ctrl_node) # print("=== FOUND BLISS CONTROLLER CLASS", klass, "WITH NODE", ctrl_node)
...@@ -78,18 +96,18 @@ def create_objects_from_config_node(cfg_obj, cfg_node): ...@@ -78,18 +96,18 @@ def create_objects_from_config_node(cfg_obj, cfg_node):
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_name, ctrl_node.clone()) bctrl = klass(ctrl_node.clone())
# find all sub objects with a name in controller config # find all sub objects with a name in controller config
sub_cfgs = find_sub_names_config(ctrl_node.to_dict()) sub_cfgs = find_sub_names_config(ctrl_node) # .to_dict(resolve_references=False))
for level in sorted(sub_cfgs.keys()): for level in sorted(sub_cfgs.keys()):
if level != 0: # ignore the controller itself if level != 0: # ignore the controller itself
for cfg, pkey in sub_cfgs[level]: for cfg, pkey in sub_cfgs[level]:
subname = cfg["name"] subname = cfg["name"]
if subname == item_name: # this is the sub-object to return # if subname == item_name: # this is the sub-object to return
name2items[item_name] = bctrl._create_sub_item(item_name, cfg, pkey) # name2items[item_name] = bctrl._create_sub_item(item_name, cfg, pkey)
else: # store sub-object info for later instantiation # else: # store sub-object info for later instantiation
name2cacheditems[subname] = (bctrl, cfg, pkey) 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
if ctrl_name: if ctrl_name:
...@@ -98,6 +116,11 @@ def create_objects_from_config_node(cfg_obj, cfg_node): ...@@ -98,6 +116,11 @@ def create_objects_from_config_node(cfg_obj, cfg_node):
# update the config cache dict NOW to avoid cyclic instanciation (i.e. config.get => create_object_from_... => config.get ) # update the config cache dict NOW to avoid cyclic instanciation (i.e. config.get => create_object_from_... => config.get )
yield name2items, name2cacheditems 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. # --- 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) # --- 'create_objects_from_config_node' should never be called again for any object related to the controller instanciated here (see config.get code)
...@@ -105,4 +128,5 @@ def create_objects_from_config_node(cfg_obj, cfg_node): ...@@ -105,4 +128,5 @@ def create_objects_from_config_node(cfg_obj, cfg_node):
def create_object_from_cache(config, name, cached_object_info): def create_object_from_cache(config, name, cached_object_info):
print("===== REGULATION FROM CACHE", name) # config, name, object_info) print("===== REGULATION FROM CACHE", name) # config, name, object_info)
bctrl, cfg, pkey = cached_object_info bctrl, cfg, pkey = cached_object_info
return bctrl._create_sub_item(name, cfg, pkey) new_object = bctrl._create_sub_item(name, cfg, pkey)
return new_object
...@@ -5,11 +5,9 @@ ...@@ -5,11 +5,9 @@
# 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.
import enum
from time import perf_counter, sleep from time import perf_counter, sleep
from itertools import chain from itertools import chain
from collections import ChainMap from gevent import event, sleep as gsleep
from gevent import Timeout, event, sleep as gsleep
from bliss import global_map from bliss import global_map
from bliss.common.protocols import CounterContainer from bliss.common.protocols import CounterContainer
...@@ -30,7 +28,7 @@ from bliss.controllers.counter import ( ...@@ -30,7 +28,7 @@ from bliss.controllers.counter import (
) )
from bliss.scanning.acquisition.counter import BaseCounterAcquisitionSlave from bliss.scanning.acquisition.counter import BaseCounterAcquisitionSlave
from bliss.config.beacon_object import BeaconObject # from bliss.config.beacon_object import BeaconObject
from bliss.common.logtools import log_info, log_debug, log_debug_data, log_warning from bliss.common.logtools import log_info, log_debug, log_debug_data, log_warning
...@@ -157,10 +155,10 @@ class BlissController(CounterContainer): ...@@ -157,10 +155,10 @@ class BlissController(CounterContainer):
_COUNTER_TAGS = {} _COUNTER_TAGS = {}
def __init__(self, name, config): def __init__(self, config):
self._name = name
self._config = config self._config = config
self._name = config.get("name")
self._counter_controllers = {} self._counter_controllers = {}
self._hw_controller = None self._hw_controller = None
...@@ -217,6 +215,13 @@ class BlissController(CounterContainer): ...@@ -217,6 +215,13 @@ class BlissController(CounterContainer):
def _load_config(self): def _load_config(self):
""" Read and apply the YML configuration """ """ Read and apply the YML configuration """
# for k in self.config.keys():
# if k in self._SUB_CLASS:
# for cfg in self.config[k]:
# if cfg.get('name'):
# self._objects[cfg.get('name')] = self._SUB_CLASS[k](self, cfg)
raise NotImplementedError raise NotImplementedError
def _build_counters(self): def _build_counters(self):
......
...@@ -55,36 +55,67 @@ with the mandatory fields: ...@@ -55,36 +55,67 @@ with the mandatory fields:
""" """
from gevent import lock from gevent import lock
from itertools import chain
from bliss.common.regulation import Input, Output, Loop from bliss.common.regulation import Input, Output, Loop
from bliss.common.utils import set_custom_members from bliss.common.utils import set_custom_members
from bliss.common.logtools import log_info from bliss.common.logtools import log_info
from bliss.common.protocols import counter_namespace
from bliss.common.utils import autocomplete_property
import time import time
from bliss.controllers.bliss_controller import BlissController
from bliss.common.counter import SamplingCounter
from bliss.controllers.counter import SamplingCounterController
class Controller:
class Controller(BlissController):
""" """
Regulation controller base class Regulation controller base class
The 'Controller' class should be inherited by controller classes that are linked to an hardware The 'Controller' class should be inherited by controller classes that are linked to an hardware
which has internal PID regulation functionnalities and optionally ramping functionnalities (on setpoint or output value) . which has internal PID regulation functionnalities and optionally ramping functionnalities (on setpoint or output value) .
If controller hardware does not have ramping capabilities, the Loop objects associated to the controller will automatically use a SoftRamp. If controller hardware does not have ramping capabilities, the Loop objects associated to the controller will automatically use a SoftRamp.
""" """
class SCC(SamplingCounterController):
def __init__(self, name, bctrl):
super().__init__(name)
self.bctrl = bctrl
def read_all(self, *counters):
values = []
for cnt in counters:
item = self.bctrl.get_object(cnt.name)
if item is not None:
values.append(item.read())
return values
_SUB_CLASS = {"inputs": Input, "outputs": Output, "ctrl_loops": Loop}
def __init__(self, config): def __init__(self, config):
self.__config = config
self.__name = config.get("name")
self._objects = {} self._objects = {}
self.__lock = lock.RLock() self.__lock = lock.RLock()
self.__initialized_obj = {} self.__initialized_obj = {}
self.__hw_controller_initialized = False self.__hw_controller_initialized = False
def add_object(self, node_type_name, object_class, cfg): super().__init__(config)
""" creates an instance of the object and add it to the controller. Called by regulation plugin. """
new_obj = object_class(self, cfg) 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
"""
new_obj = self._SUB_CLASS[parent_key](self, cfg)
# --- store the new object # --- store the new object
self._objects[new_obj.name] = new_obj self._objects[new_obj.name] = new_obj
...@@ -94,6 +125,44 @@ class Controller: ...@@ -94,6 +125,44 @@ class Controller:
return new_obj return new_obj
def _load_config(self):
""" Read and apply the YML configuration """
print("=== _load_config")
pass
def _build_counters(self):
""" Build the CounterControllers and associated Counters"""
print("=== _build_counters")