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

Regulator as BlissController

parent 31029613
......@@ -170,6 +170,8 @@ import gevent
import gevent.event
import enum
from bliss.common.protocols import CounterContainer
from bliss.common.logtools import log_debug, disable_user_output
from bliss.common.utils import with_custom_members, autocomplete_property
from bliss.common.counter import SamplingCounter
......@@ -200,38 +202,38 @@ def _get_external_device_name(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
class Input(SamplingCounterController):
class Input(CounterContainer):
"""Implements the access to an input device which is accessed via the
regulation controller (like a sensor plugged on a channel of the controller)
"""
def __init__(self, controller, config):
""" Constructor """
super().__init__(name=config["name"])
self._name = config["name"]
self._controller = controller
self._config = config
self._channel = self._config.get("channel")
self.max_sampling_frequency = config.get("max_sampling_frequency", 5)
# useful attribute for a temperature controller writer
self._attr_dict = {}
self._build_counters()
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"),
)
@property
def name(self):
return self._name
def read_all(self, *counters):
return [self.read()]
@autocomplete_property
def counters(self):
return counter_namespace({self.name: self._controller.counters[self.name]})
# ----------- BASE METHODS -----------------------------------------
......@@ -316,7 +318,13 @@ class ExternalInput(Input):
self.device = config.get("device")
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):
lines = ["\n"]
......@@ -358,7 +366,7 @@ class ExternalInput(Input):
@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)
The Output has a ramp object.
......@@ -370,7 +378,7 @@ class Output(SamplingCounterController):
def __init__(self, controller, config):
""" Constructor """
super().__init__(name=config["name"])
self._name = config["name"]
self._controller = controller
self._config = config
......@@ -387,20 +395,13 @@ class Output(SamplingCounterController):
# useful attribute for a temperature controller writer
self._attr_dict = {}
self.max_sampling_frequency = config.get("max_sampling_frequency", 5)
self._build_counters()
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"),
)
@property
def name(self):
return self._name
def read_all(self, *counters):
return [self.read()]
@autocomplete_property
def counters(self):
return counter_namespace({self.name: self._controller.counters[self.name]})
# ----------- BASE METHODS -----------------------------------------
......@@ -608,6 +609,14 @@ class ExternalOutput(Output):
self.mode = config.get("mode", "relative")
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 -----------------------------------------
@property
......@@ -700,7 +709,7 @@ class ExternalOutput(Output):
@with_custom_members
class Loop(SamplingCounterController):
class Loop(CounterContainer):
""" Implements the access to the regulation loop
The regulation is the PID process that:
......@@ -733,7 +742,7 @@ class Loop(SamplingCounterController):
def __init__(self, controller, config):
""" Constructor """
super().__init__(name=config["name"])
self._name = config["name"]
self._controller = controller
self._config = config
......@@ -761,20 +770,8 @@ class Loop(SamplingCounterController):
self.reg_plot = None
self.max_sampling_frequency = config.get("max_sampling_frequency", 5)
self._build_counters()
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):
self.close()
......@@ -787,8 +784,16 @@ class Loop(SamplingCounterController):
# ----------- BASE METHODS -----------------------------------------
def read_all(self, *counters):
return [self._get_working_setpoint()]
@lazy_init
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
def load_base_config(self):
......@@ -847,7 +852,7 @@ class Loop(SamplingCounterController):
all_counters = (
list(self.input.counters)
+ list(self.output.counters)
+ list(self._counters.values())
+ [self._controller.counters[self.name]]
)
return counter_namespace(all_counters)
......@@ -1484,6 +1489,14 @@ class SoftLoop(Loop):
self.load_base_config()
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):
lines = ["\n"]
lines.append(f"=== SoftLoop: {self.name} ===")
......@@ -1517,6 +1530,12 @@ class SoftLoop(Loop):
self._ramp.stop()
self._stop_regulation()
def read(self):
""" Return the current working setpoint """
log_debug(self, "SoftLoop:read")
return self._get_working_setpoint()
@property
def max_attempts_before_failure(self):
"""
......
......@@ -6,10 +6,22 @@
# 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.static import ConfigNode, ConfigReference
from bliss.config.static import ConfigNode, ConfigList
def find_sub_names_config(
......@@ -34,13 +46,18 @@ def find_sub_names_config(
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):
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, list):
elif isinstance(v, ConfigList):
for i in v:
if isinstance(i, dict):
if isinstance(i, ConfigNode):
find_sub_names_config(i, selection, level + 1, k)
return selection
......@@ -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
# 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)
......@@ -78,17 +96,17 @@ 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
# 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
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()):
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
# 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
......@@ -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 )
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)
......@@ -105,4 +128,5 @@ def create_objects_from_config_node(cfg_obj, cfg_node):
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)
new_object = bctrl._create_sub_item(name, cfg, pkey)
return new_object
......@@ -5,11 +5,9 @@
# Copyright (c) 2015-2020 Beamline Control Unit, ESRF
# Distributed under the GNU LGPLv3. See LICENSE for more info.
import enum
from time import perf_counter, sleep
from itertools import chain
from collections import ChainMap
from gevent import Timeout, event, sleep as gsleep
from gevent import event, sleep as gsleep
from bliss import global_map
from bliss.common.protocols import CounterContainer
......@@ -30,7 +28,7 @@ from bliss.controllers.counter import (
)
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
......@@ -157,10 +155,10 @@ class BlissController(CounterContainer):
_COUNTER_TAGS = {}
def __init__(self, name, config):
def __init__(self, config):
self._name = name
self._config = config
self._name = config.get("name")
self._counter_controllers = {}
self._hw_controller = None
......@@ -217,6 +215,13 @@ class BlissController(CounterContainer):
def _load_config(self):
""" 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
def _build_counters(self):
......
......@@ -55,13 +55,20 @@ with the mandatory fields:
"""
from gevent import lock
from itertools import chain
from bliss.common.regulation import Input, Output, Loop
from bliss.common.utils import set_custom_members
from bliss.common.logtools import log_info
from bliss.common.protocols import counter_namespace
from bliss.common.utils import autocomplete_property
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
......@@ -72,19 +79,43 @@ class Controller:
"""
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):
self.__config = config
self.__name = config.get("name")
self._objects = {}
self.__lock = lock.RLock()
self.__initialized_obj = {}
self.__hw_controller_initialized = False
def add_object(self, node_type_name, object_class, cfg):
""" creates an instance of the object and add it to the controller. Called by regulation plugin. """
super().__init__(config)
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).
new_obj = object_class(self, cfg)
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
self._objects[new_obj.name] = new_obj
......@@ -94,6 +125,44 @@ class Controller:
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")
self._counter_controllers["scc"] = self.SCC("scc", self)
self._counter_controllers["scc"].max_sampling_frequency = self.config.get(
"max_sampling_frequency", 1
)
for k in self._SUB_CLASS:
for cfg in self.config.get(k, []):
name = cfg["name"]
mode = cfg.get("mode", "SINGLE")
unit = cfg.get("unit")
self._counter_controllers["scc"].create_counter(
SamplingCounter, name, unit=unit, mode=mode
)
def _build_axes(self):
""" Build the Axes (real and pseudo) """
print("=== _build_axes")
pass
@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 counter_namespace(self._motor_controller.axes)
pass
def init_obj(self, obj):
""" Initialize objects under the controller. Called by @lazy_init. """
......@@ -103,6 +172,7 @@ class Controller:
if not self.__hw_controller_initialized:
self.initialize_controller()
print("=== initialize_controller")
self.__hw_controller_initialized = True
if self.__initialized_obj.get(obj):
......@@ -116,33 +186,27 @@ class Controller:
self.__initialized_obj[obj.input] = True
obj.input.load_base_config()
self.initialize_input(obj.input)
print("=== initialize_input")
if not self.__initialized_obj.get(obj.output):
self.__initialized_obj[obj.output] = True
obj.output.load_base_config()
self.initialize_output(obj.output)
print("=== initialize_output")
obj.load_base_config()
self.initialize_loop(obj)
print("=== initialize_loop")
else:
self.__initialized_obj[obj] = True
obj.load_base_config()
if isinstance(obj, Input):
self.initialize_input(obj)
print("=== initialize_input")
elif isinstance(obj, Output):
self.initialize_output(obj)
@property
def name(self):
return self.__name
@property
def config(self):
"""
returns the config node
"""
return self.__config
print("=== initialize_output")
def get_object(self, name):
"""
......
plugin: regulation
plugin: bliss_controller #regulation
......@@ -42,6 +42,7 @@
-
class: ExternalInput # <== declare an 'ExternalInput' object
package: bliss.common.regulation
name: diode_input
device: $diode # <== a SamplingCounter object reference (pointing to a counter declared somewhere else in a YML config file)
unit: N/A
......@@ -49,6 +50,7 @@
-
class: ExternalOutput # <== declare an 'ExternalOutput' object
package: bliss.common.regulation
name: robz_output
device: $robz # <== an axis object reference (pointing to an object declared somewhere else in a YML config file)
unit: mm
......@@ -60,6 +62,7 @@
-
class: SoftLoop # <== declare a 'SoftLoop' object
package: bliss.common.regulation
name: soft_regul
input: $custom_input
output: $custom_output
......@@ -77,6 +80,7 @@
-
class: SoftLoop # <== declare a 'SoftLoop' object
package: bliss.common.regulation
name: soft_regul2
input: $bound_input
output: $robz_output
......@@ -96,7 +100,8 @@
-
class: Mockup
module: temperature.mockup
# plugin: bliss_controller
module: regulation.temperature.mockup
host: lid42
inputs:
-
......
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