diff --git a/bliss/common/axis.py b/bliss/common/axis.py index d3cc2be02ca03727d5e12e3f0ca1d5f6ab270044..d38a0df630a8732ff627ec597878cbf209a70543 100644 --- a/bliss/common/axis.py +++ b/bliss/common/axis.py @@ -12,7 +12,7 @@ and :class:`~bliss.common.axis.GroupMove`) """ from bliss import global_map from bliss.common.cleanup import capture_exceptions -from bliss.common.motor_config import StaticConfig +from bliss.common.motor_config import MotorConfig from bliss.common.motor_settings import AxisSettings from bliss.common import event from bliss.common.greenlet_utils import protect_from_one_kill @@ -341,8 +341,7 @@ class GroupMove: for motion in motions: motion.axis._set_moving_state() - for _, chan in motion.axis._beacon_channels.items(): - chan.unregister_callback(chan._setting_update_cb) + motion.axis.settings.unregister_channels_callbacks() with capture_exceptions(raise_index=0) as capture: with capture(): @@ -402,10 +401,9 @@ class GroupMove: for hook in axis.motion_hooks: hooks[hook].append(motion) - # set move done - for _, chan in axis._beacon_channels.items(): - chan.register_callback(chan._setting_update_cb) + axis.settings.register_channels_callbacks() + # set move done motion.axis._set_move_done() if self._interrupted_move: @@ -489,7 +487,7 @@ class Motion: return f"Moving {self.axis.name} from {start_} to {end_}" -class Trajectory(object): +class Trajectory: """ Trajectory information Represents a specific trajectory motion. @@ -674,23 +672,12 @@ class Axis: self.__encoder = config.get("encoder") if self.__encoder is not None: self.__encoder.axis = self - self.__config = StaticConfig(config) + self.__config = MotorConfig(config) self.__settings = AxisSettings(self) - self.__init_config_properties() + self._init_config_properties() + self.__no_offset = False self._group_move = GroupMove() - self._beacon_channels = dict() - self._move_stop_channel = Channel( - f"axis.{self.name}.move_stop", - default_value=False, - callback=self._external_stop, - ) - self._jog_velocity_channel = Channel( - f"axis.{self.name}.change_jog_velocity", - default_value=None, - callback=self._set_jog_velocity, - ) self._lock = gevent.lock.Semaphore() - self.__no_offset = False try: config.parent @@ -703,12 +690,25 @@ class Axis: "disabled_cache", [] ) # get it from controller (parent) disabled_cache.extend(config.get("disabled_cache", [])) # get it for this axis - for settings_name in disabled_cache: - self.settings.disable_cache(settings_name) + for setting_name in disabled_cache: + self.settings.disable_cache(setting_name) self._unit = self.config.get("unit", str, None) self._polling_time = config.get("polling_time", DEFAULT_POLLING_TIME) global_map.register(self, parents_list=["axes", controller]) + # create Beacon channels + self.settings.init_channels() + self._move_stop_channel = Channel( + f"axis.{self.name}.move_stop", + default_value=False, + callback=self._external_stop, + ) + self._jog_velocity_channel = Channel( + f"axis.{self.name}.change_jog_velocity", + default_value=None, + callback=self._set_jog_velocity, + ) + def __close__(self): try: controller_close = self.__controller.close @@ -742,7 +742,7 @@ class Axis: @property def config(self): - """Reference to the :class:`~bliss.common.motor_config.StaticConfig`""" + """Reference to the :class:`~bliss.common.motor_config.MotorConfig`""" return self.__config @property @@ -760,20 +760,20 @@ class Axis: """ return not self.__move_done.is_set() - def __init_config_properties( + def _init_config_properties( self, velocity=True, acceleration=True, limits=True, sign=True, backlash=True ): self.__steps_per_unit = self.config.get("steps_per_unit", float, 1) self.__tolerance = self.config.get("tolerance", float, 1e-4) if velocity: - if self.controller.axis_settings.config_setting["velocity"]: + if "velocity" in self.settings.config_settings: self.__config_velocity = self.config.get("velocity", float) - if self.controller.axis_settings.config_setting["jog_velocity"]: + if "jog_velocity" in self.settings.config_settings: self.__config_jog_velocity = self.config.get( "jog_velocity", float, self.__config_velocity ) if acceleration: - if self.controller.axis_settings.config_setting["acceleration"]: + if "acceleration" in self.settings.config_settings: self.__config_acceleration = self.config.get("acceleration", float) if limits: self.__config_low_limit = self.config.get("low_limit", float, float("-inf")) @@ -851,6 +851,7 @@ class Axis: return sign @sign.setter + @lazy_init def sign(self, new_sign): new_sign = float( new_sign @@ -922,6 +923,7 @@ class Axis: return position @_set_position.setter + @lazy_init def _set_position(self, new_set_pos): new_set_pos = float( new_set_pos @@ -992,6 +994,7 @@ class Axis: return dial_pos @dial.setter + @lazy_init def dial(self, new_dial): if self.is_moving: raise RuntimeError( @@ -1038,9 +1041,11 @@ class Axis: pos = self.settings.get("position") if pos is None: pos = self.dial2user(self.dial) + self.settings.set("position", pos) return pos @position.setter + @lazy_init def position(self, new_pos): log_debug(self, "axis.py : position(new_pos=%r)" % new_pos) if self.is_moving: @@ -1097,6 +1102,7 @@ class Axis: if state is None: # really read from hw state = self.hw_state + self.settings.set("state", state) return state @property @@ -1222,6 +1228,7 @@ class Axis: return _user_vel @velocity.setter + @lazy_init def velocity(self, new_velocity): # Write -> Converts into motor units to change velocity of axis." new_velocity = float( @@ -1233,6 +1240,7 @@ class Axis: return _user_vel @property + @lazy_init def config_velocity(self): """ Return the config velocity. @@ -1327,6 +1335,7 @@ class Axis: return _user_jog_vel @jog_velocity.setter + @lazy_init def jog_velocity(self, new_velocity): new_velocity = float( new_velocity @@ -1336,6 +1345,7 @@ class Axis: self._jog_velocity_channel.value = new_velocity @property + @lazy_init def config_jog_velocity(self): """ Return the config jog velocity. @@ -1359,6 +1369,7 @@ class Axis: return _acceleration @acceleration.setter + @lazy_init def acceleration(self, new_acc): if self.is_moving: raise RuntimeError( @@ -1373,6 +1384,7 @@ class Axis: return _acceleration @property + @lazy_init def config_acceleration(self): return self.__config_acceleration @@ -1388,6 +1400,7 @@ class Axis: return abs(self.velocity / self.acceleration) @acctime.setter + @lazy_init def acctime(self, new_acctime): # Converts acctime into acceleration. new_acctime = float( @@ -1432,6 +1445,7 @@ class Axis: return ll, hl @dial_limits.setter + @lazy_init def dial_limits(self, limits): """ Set low, high limits in dial units @@ -1458,13 +1472,14 @@ class Axis: return tuple(map(self.dial2user, self.dial_limits)) @limits.setter + @lazy_init def limits(self, limits): # Set limits (low, high) in user units. try: if len(limits) != 2: raise TypeError except TypeError: - raise ValueError("Usage: .limits(low, high)") + raise ValueError("Usage: .limits = low, high") # accepts iterable (incl. numpy array) self.low_limit, self.high_limit = ( @@ -1479,6 +1494,7 @@ class Axis: return self.dial2user(ll) @low_limit.setter + @lazy_init def low_limit(self, limit): # Sets Low Limit # must be given in USER units @@ -1496,6 +1512,7 @@ class Axis: return self.dial2user(hl) @high_limit.setter + @lazy_init def high_limit(self, limit): # Sets High Limit (given in USER units) # Saved in settings in DIAL units. @@ -1505,6 +1522,7 @@ class Axis: self.settings.set("high_limit", limit) @property + @lazy_init def config_limits(self): """ Return a tuple (low_limit, high_limit) from IN-MEMORY config in @@ -2033,7 +2051,7 @@ class Axis: if any((velocity, acceleration, limits, sign, backlash)): self.__config.save() - self.__init_config_properties( + self._init_config_properties( velocity=velocity, acceleration=acceleration, limits=limits, @@ -2056,7 +2074,7 @@ class Axis: if reload: self.config.reload() - self.__init_config_properties( + self._init_config_properties( velocity=velocity, acceleration=acceleration, limits=limits, @@ -2065,18 +2083,18 @@ class Axis: ) if velocity: - self.controller.axis_settings._clear(self, "velocity") + self.settings.clear("velocity") if acceleration: - self.controller.axis_settings._clear(self, "acceleration") + self.settings.clear("acceleration") if limits: - self.controller.axis_settings._clear(self, "low_limit") - self.controller.axis_settings._clear(self, "high_limit") + self.settings.clear("low_limit") + self.settings.clear("high_limit") if sign: - self.controller.axis_settings._clear(self, "sign") + self.settings.clear("sign") if backlash: - self.controller.axis_settings._clear(self, "backlash") + self.settings.clear("backlash") - self.controller._init_settings(self) + self.settings.init() # update position (needed for sign change) pos = self.dial2user(self.dial) @@ -2434,6 +2452,6 @@ class ModuloAxis(Axis): class NoSettingsAxis(Axis): def __init__(self, *args, **kwags): - Axis.__init__(self, *args, **kwags) - for setting_name in self.settings: + super().__init__(*args, **kwags) + for setting_name in self.settings.setting_names: self.settings.disable_cache(setting_name) diff --git a/bliss/common/encoder.py b/bliss/common/encoder.py index 10d228d9f77b89b748c8f053507e723cbf653424..cdea3c3787aae1764d1c164d456a8bdf6aeb32a0 100644 --- a/bliss/common/encoder.py +++ b/bliss/common/encoder.py @@ -5,7 +5,7 @@ # Copyright (c) 2015-2020 Beamline Control Unit, ESRF # Distributed under the GNU LGPLv3. See LICENSE for more info. -from bliss.common.motor_config import StaticConfig +from bliss.common.motor_config import MotorConfig from bliss.common.counter import SamplingCounter from bliss.controllers import counter from bliss.common import event @@ -37,7 +37,7 @@ class Encoder: self._counter_controller.create_counter( SamplingCounter, "position", unit=config.get("unit") ) - self.__config = StaticConfig(config) + self.__config = MotorConfig(config) self.__axis_ref = None @property diff --git a/bliss/common/motor_config.py b/bliss/common/motor_config.py index ffc13e17450f0d228b426c0c09bacacd599a276a..6ae67815fa4f4b6c7dc03b3981f43c749aa3b122 100644 --- a/bliss/common/motor_config.py +++ b/bliss/common/motor_config.py @@ -10,10 +10,7 @@ from bliss.common.utils import Null import time -class StaticConfig: - - NO_VALUE = Null() - +class MotorConfig: def __init__(self, config_node): self.__config_dict = config_node self.__config_has_changed = False @@ -32,14 +29,13 @@ class StaticConfig: def config_dict(self): return self.__config_dict - def get(self, property_name, converter=str, default=NO_VALUE): + def get(self, property_name, converter=str, default=None): """Get static property Args: property_name (str): Property name converter (function): Default :func:`str`, Conversion function from configuration format to Python - default: Default: NO_VALUE, default value for property - inherited (bool): Default: False, Property can be inherited + default: default value for property if key does not exist (defaults to None) Returns: Property value @@ -51,18 +47,14 @@ class StaticConfig: if self.__config_has_changed: self.reload() self.__config_has_changed = False - property_value = self.config_dict.get(property_name) - if property_value is not None: + if property_value is None: + return default + else: if callable(converter): return converter(property_value) else: return property_value - else: - if default != self.NO_VALUE: - return default - - raise KeyError("no property '%s` in config" % property_name) def set(self, property_name, value): if self.__config_has_changed: diff --git a/bliss/common/motor_settings.py b/bliss/common/motor_settings.py index 6333e5342454c0e58aa61da3bf7495587674ba94..76db27b65cd8dfe1217fd4d71042ef433a2017ad 100644 --- a/bliss/common/motor_settings.py +++ b/bliss/common/motor_settings.py @@ -5,10 +5,16 @@ # Copyright (c) 2015-2020 Beamline Control Unit, ESRF # Distributed under the GNU LGPLv3. See LICENSE for more info. +import collections +import functools +import inspect +import math +import sys + from bliss.common import event from bliss.common.greenlet_utils import KillMask +from bliss.config.channels import Channel from bliss.config import settings, settings_cache -import sys def setting_update_from_channel(value, setting_name=None, axis=None): @@ -41,29 +47,29 @@ def stateSetting(state): class ControllerAxisSettings: def __init__(self): self.setting_names = [] - self.disabled_settings = {} self.convert_func = {} self.persistent_setting = {} self.config_setting = {} # 'offset' must be set BEFORE limits to ensure good dial/user conversion. - self.add("offset", float) self.add("sign", int) + self.add("offset", float) self.add("backlash", float) + self.add("low_limit", floatOrNone) + self.add("high_limit", floatOrNone) self.add("velocity", float, config=True) self.add("jog_velocity", float) self.add("acceleration", float, config=True) - self.add("low_limit", floatOrNone) - self.add("high_limit", floatOrNone) self.add("dial_position", float) self.add("_set_position", float) self.add("position", float) self.add("state", stateSetting, persistent=False) self.add("steps_per_unit", float, config=True) + @property def config_settings(self): return tuple( - [setting for setting, config in self.config_setting.items() if config] + setting for setting, config in self.config_setting.items() if config ) def add(self, setting_name, convert_func=str, persistent=True, config=False): @@ -73,68 +79,20 @@ class ControllerAxisSettings: self.persistent_setting[setting_name] = persistent self.config_setting[setting_name] = config - def get(self, axis, setting_name): - if setting_name not in self.setting_names: - raise ValueError( - "No setting '%s` for axis '%s`" % (setting_name, axis.name) - ) - disabled_settings = self.disabled_settings.get(axis, set()) - - if ( - self.persistent_setting[setting_name] - and not setting_name in disabled_settings - ): - with KillMask(): - value = axis.settings._hash.get(setting_name) - else: - chan = axis._beacon_channels.get(setting_name) - if chan: - value = chan.value - else: - value = None - if value is not None: - convert_func = self.convert_func.get(setting_name) - if convert_func is not None: - value = convert_func(value) - return value - - def _clear(self, axis, setting_name): - if not setting_name in self.disabled_settings: - axis.settings._hash[setting_name] = None - - def set(self, axis, setting_name, value): - """ - * set setting - * send event - * write - """ - if setting_name not in self.setting_names: - raise ValueError( - "No setting '%s` for axis '%s`" % (setting_name, axis.name) - ) - convert_func = self.convert_func.get(setting_name) - if convert_func is not None: - value = convert_func(value) - - if not setting_name in self.disabled_settings: - if self.persistent_setting[setting_name]: - with KillMask(): - axis.settings._hash[setting_name] = value - - axis._beacon_channels[setting_name].value = value - - event.send(axis, "internal_" + setting_name, value) - try: - event.send(axis, setting_name, value) - except Exception: - sys.excepthook(*sys.exc_info()) +disabled_settings_namedtuple = collections.namedtuple( + "disabled_settings", "names config_dict" +) class AxisSettings: def __init__(self, axis): self.__axis = axis self.__state = None + self._beacon_channels = {} + self._disabled_settings = disabled_settings_namedtuple( + set(), dict(axis.config.config_dict) + ) cnx_cache = settings_cache.get_redis_client_cache() self._hash = settings.HashSetting( "axis.%s" % axis.name, @@ -144,26 +102,217 @@ class AxisSettings: # Activate prefetch cnx_cache.add_prefetch(self._hash) + @property + def setting_names(self): + yield from self.__axis.controller.axis_settings.setting_names + + def convert_func(self, setting_name): + return self.__axis.controller.axis_settings.convert_func.get(setting_name) + + @property + def config_settings(self): + return self.__axis.controller.axis_settings.config_settings + + def register_channels_callbacks(self): + for chan in self._beacon_channels.values(): + chan.register_callback(chan._setting_update_cb) + + def unregister_channels_callbacks(self): + for chan in self._beacon_channels.values(): + chan.unregister_callback(chan._setting_update_cb) + + def _create_channel(self, setting_name): + chan_name = "axis.%s.%s" % (self.__axis.name, setting_name) + chan = Channel(chan_name) + cb = functools.partial( + setting_update_from_channel, setting_name=setting_name, axis=self.__axis + ) + chan._setting_update_cb = cb + return chan + + def init_channels(self): + self._beacon_channels.clear() + for setting_name in self.__axis.controller.axis_settings.setting_names: + self._beacon_channels[setting_name] = self._create_channel(setting_name) + self.register_channels_callbacks() + + def _get_setting_or_config_value(self, name, default=None): + # return setting or config parameter + converter = self.convert_func(name) + value = self.get(name) + if value is None: + value = self.__axis.config.get(name, converter, default=default) + return value + + def check_config_settings(self): + axis = self.__axis + props = dict( + inspect.getmembers(axis.__class__, lambda o: isinstance(o, property)) + ) + config_settings = [] + for setting_name in self.config_settings: + # check if setting is in config + value = axis.config.get(setting_name) + if value is None: + raise RuntimeError( + "Axis %s: missing configuration key '%s`" + % (axis.name, setting_name) + ) + if setting_name == "steps_per_unit": + # steps_per_unit is read-only + continue + config_settings.append(setting_name) + # check if setting has a method to initialize (set) its value, + # without actually executing the property + try: + assert callable(props[setting_name].fset) + except AssertionError: + raise RuntimeError( + "Axis %s: missing method '%s` to set setting value" + % (axis.name, setting_name) + ) + return config_settings + + def init(self): + """ Initialize settings + + "config settings" are those that **must** be in YML config like + steps per unit, acceleration and velocity ; otherwise settings + can optionally be present in the config file. + Config settings must have a property setter on the Axis object. + "persistent settings" are stored in redis, like position for example; + in any case, when a setting is set its value is emitted via a + dedicated channel. + Some settings can be both config+persistent (like velocity) or + none (like state, which is only emitted when it changes, but not stored + at all) + """ + axis = self.__axis + + config_settings = self.check_config_settings() + config_steps_per_unit = axis.config.get("steps_per_unit", float) + + if axis.no_offset: + sign = 1 + offset = 0 + else: + sign = self._get_setting_or_config_value("sign", 1) + offset = self._get_setting_or_config_value("offset", 0) + self.set("sign", sign) + self.set("offset", offset) + + self.set("backlash", self._get_setting_or_config_value("backlash", 0)) + + low_limit_dial = self._get_setting_or_config_value("low_limit") + high_limit_dial = self._get_setting_or_config_value("high_limit") + + if config_steps_per_unit: + cval = config_steps_per_unit + rval = self._hash.raw_get("steps_per_unit") + # Record steps_per_unit + if rval is None: + self.set("steps_per_unit", cval) + else: + rval = float(rval) + if cval != rval: + ratio = rval / cval + new_dial = axis.dial * ratio + + self.set("steps_per_unit", cval) + if not axis.no_offset: + # calculate offset so user pos stays the same + self.set("offset", axis.position - axis.sign * new_dial) + self.set("dial_position", new_dial) + + if math.copysign(rval, cval) != rval: + ll, hl = low_limit_dial, high_limit_dial + low_limit_dial, high_limit_dial = -hl, -ll + + self.set("low_limit", low_limit_dial) + self.set("high_limit", high_limit_dial) + + for setting_name in config_settings: + value = self._get_setting_or_config_value(setting_name) + setattr(axis, setting_name, value) + def set(self, setting_name, value): if setting_name == "state": if self.__state == value: return self.__state = value - return self.__axis.controller.axis_settings.set( - self.__axis, setting_name, value - ) - def convert_func(self, setting_name): - return self.__axis.controller.axis_settings.convert_func[setting_name] + axis = self.__axis + axis_settings = axis.controller.axis_settings + if setting_name not in axis_settings.setting_names: + raise ValueError( + "No setting '%s` for axis '%s`" % (setting_name, axis.name) + ) + convert_func = self.convert_func(setting_name) + if convert_func is not None: + value = convert_func(value) - def config_settings(self): - return self.__axis.controller.axis_settings.config_settings() + disabled_settings = self._disabled_settings + if setting_name in disabled_settings.names: + if ( + setting_name not in ("position", "dial_position") + and axis_settings.persistent_setting[setting_name] + ): + disabled_settings.config_dict[setting_name] = value + else: + if axis_settings.persistent_setting[setting_name]: + with KillMask(): + self._hash[setting_name] = value + + self._beacon_channels[setting_name].value = value + + event.send(axis, "internal_" + setting_name, value) + try: + event.send(axis, setting_name, value) + except Exception: + sys.excepthook(*sys.exc_info()) def get(self, setting_name): - return self.__axis.controller.axis_settings.get(self.__axis, setting_name) + axis = self.__axis + axis_settings = axis.controller.axis_settings + disabled_settings = self._disabled_settings + + if setting_name not in axis_settings.setting_names: + raise NameError("No setting '%s` for axis '%s`" % (setting_name, axis.name)) + + if setting_name in disabled_settings.names: + return disabled_settings.config_dict.get(setting_name) + + if axis_settings.persistent_setting[setting_name]: + with KillMask(): + value = self._hash.get(setting_name) + else: + chan = self._beacon_channels.get(setting_name) + if chan: + value = chan.value + else: + value = None + + if value is not None: + convert_func = self.convert_func(setting_name) + if convert_func is not None: + value = convert_func(value) + return value def clear(self, setting_name): - self.__axis.controller.axis_settings._clear(self.__axis, setting_name) + disabled_settings = self._disabled_settings + + if setting_name in disabled_settings.names: + disabled_settings.config_dict[setting_name] = None + else: + self._hash[setting_name] = None + # reset beacon channel, if it is there + try: + channel = self._beacon_channels[setting_name] + except KeyError: + pass + else: + channel.close() + self._beacon_channels[setting_name] = self._create_channel(setting_name) def disable_cache(self, setting_name, flag=True): """ @@ -172,17 +321,12 @@ class AxisSettings: if setting_name == "position": self.disable_cache("dial_position", flag) - disabled_settings = self.__axis.controller.axis_settings.disabled_settings.setdefault( - self.__axis, set() - ) + self.clear(setting_name) + if flag: - disabled_settings.add(setting_name) + self._disabled_settings.names.add(setting_name) else: try: - disabled_settings.remove(setting_name) + self._disabled_settings.names.remove(setting_name) except KeyError: pass - - def __iter__(self): - for name in self.__axis.controller.axis_settings.setting_names: - yield name diff --git a/bliss/config/channels.py b/bliss/config/channels.py index f4f61b7a0aa729e10e0b38a8662966ed3877f753..e7510d83d160db1722edfad4c4cbb864758364b6 100644 --- a/bliss/config/channels.py +++ b/bliss/config/channels.py @@ -115,15 +115,22 @@ class Bus(AdvancedInstantiationInterface): self._send_task = gevent.spawn(self._send) self._send_event = gevent.event.Event() + self.__closing = False + # Close + @property + def closing(self): + return self.__closing + def close(self): - for channel in list(self._channels.values()): - channel.close() + self.__closing = True if self._send_task: self._send_task.kill() if self._listen_task: self._listen_task.kill() + for channel in list(self._channels.values()): + channel.close() @classmethod def clear_cache(cls): @@ -270,7 +277,7 @@ class Bus(AdvancedInstantiationInterface): raise ConnectionError( "Connection to Beacon server lost. " + "This is a serious problem! " - + "Please quite the bliss session and try to restart it. (" + + "Quit the bliss session and try to restart it. (" + str(e) + ")" ) @@ -455,7 +462,8 @@ class Channel(AdvancedInstantiationInterface): reply_value = self._default_value # Set the value - self._set_raw_value(reply_value) + if not self._bus.closing: + self._set_raw_value(reply_value) # Unregister task if everything went smoothly self._query_task = None @@ -482,7 +490,7 @@ class Channel(AdvancedInstantiationInterface): def _fire_callbacks(self): value = self._raw_value.value - callbacks = [_f for _f in [ref() for ref in self._callback_refs] if _f] + callbacks = filter(None, [ref() for ref in self._callback_refs]) # Run callbacks for cb in callbacks: @@ -491,15 +499,12 @@ class Channel(AdvancedInstantiationInterface): self._firing_callbacks = True cb(value) # Catch and display exception - except: + except Exception: sys.excepthook(*sys.exc_info()) # Clean up the flag finally: self._firing_callbacks = False - # Clean up - self._callbacks = {ref for ref in self._callback_refs if ref() is not None} - # Representation def __repr__(self): @@ -606,9 +611,9 @@ class EventChannel(AdvancedInstantiationInterface): def _set_raw_value(self, raw_value): value = raw_value.value - callbacks = [_f for _f in [ref() for ref in self._callback_refs] if _f] + callbacks = filter(None, [ref() for ref in self._callback_refs]) for cb in callbacks: try: cb(value) - except: + except Exception: sys.excepthook(*sys.exc_info()) diff --git a/bliss/config/conductor/connection.py b/bliss/config/conductor/connection.py index 96c76cff12e7c3883e7976e3b77869589d9a99d2..373d9c6ddf138eac8fb7294b39a98a90ae64e1c2 100644 --- a/bliss/config/conductor/connection.py +++ b/bliss/config/conductor/connection.py @@ -32,23 +32,8 @@ class GreenletSafeConnectionPool(redis.ConnectionPool): # we do not care of being "fork-safe" return - def get_connection(self, command_name, *keys, **options): - connection = super().get_connection(command_name, *keys, **options) - - if command_name == "pubsub": - finalize = weakref.finalize( - gevent.getcurrent(), self.clean_pubsub, connection - ) - else: - finalize = weakref.finalize(gevent.getcurrent(), self.release, connection) - connection.__finalize__ = finalize - - return connection - def release(self, connection): with self._lock: - if hasattr(connection, "__finalize__"): - connection.__finalize__.detach() # As we register callback when greenlet disappear, # Connection might been removed before the greenlet try: diff --git a/bliss/controllers/motor.py b/bliss/controllers/motor.py index 26573e49357b29bcbd596520de9778de708079d0..8eba6af9a17f368e8190fb7a6ed7462dd34f8689 100644 --- a/bliss/controllers/motor.py +++ b/bliss/controllers/motor.py @@ -7,14 +7,8 @@ import math import numpy -import inspect -import functools -from bliss.common.motor_config import StaticConfig -from bliss.common.motor_settings import ( - ControllerAxisSettings, - setting_update_from_channel, - floatOrNone, -) +from bliss.common.motor_config import MotorConfig +from bliss.common.motor_settings import ControllerAxisSettings, floatOrNone from bliss.common.axis import Trajectory from bliss.common.motor_group import Group, TrajectoryGroup from bliss.common import event @@ -26,18 +20,6 @@ from bliss.config import settings from gevent import lock -# apply settings or config parameters -def get_setting_or_config_value(axis, name): - converter = axis.settings.convert_func(name) - value = axis.settings.get(name) - if value is None: - try: - value = axis.config.get(name, converter) - except BaseException: - return None - return value - - class Controller: """ Motor controller base class @@ -48,7 +30,7 @@ class Controller: def __init__(self, name, config, axes, encoders, shutters, switches): self.__name = name - self.__config = StaticConfig(config) + self.__config = MotorConfig(config) self.__initialized_hw = Cache(self, "initialized", default_value=False) self.__initialized_hw_axis = dict() self.__initialized_encoder = dict() @@ -171,10 +153,10 @@ class Controller: def _initialize_axis(self, axis, *args, **kwargs): """ """ - if self.__initialized_axis[axis]: - return - with self.__lock: + if self.__initialized_axis[axis]: + return + # Initialize controller hardware only once. if not self.__initialized_hw.value: self.initialize_hardware() @@ -193,90 +175,15 @@ class Controller: axis_initialized = self.__initialized_hw_axis[axis] if not axis_initialized.value: self.initialize_hardware_axis(axis) + axis.settings.check_config_settings() + axis.settings.init() # get settings, from config or from cache, and apply to hardware axis_initialized.value = 1 - self._init_settings(axis) except BaseException: # Failed to initialize self.__initialized_axis[axis] = False raise - def _init_settings(self, axis): - """ Initialize hardware with settings - """ - props = dict( - inspect.getmembers(axis.__class__, lambda o: isinstance(o, property)) - ) - - sign = get_setting_or_config_value(axis, "sign") - if sign is None or axis.no_offset: - sign = 1 - axis.settings.set("sign", sign) - - offset = get_setting_or_config_value(axis, "offset") - if offset is None or axis.no_offset: - offset = 0 - axis.settings.set("offset", offset) - - backlash = get_setting_or_config_value(axis, "backlash") - if backlash is None: - backlash = 0 - axis.backlash = backlash - - low_limit_dial = get_setting_or_config_value(axis, "low_limit") - high_limit_dial = get_setting_or_config_value(axis, "high_limit") - axis.dial_limits = low_limit_dial, high_limit_dial - - for setting_name in axis.settings.config_settings(): - # check if setting is in config - if axis.config.get(setting_name) is None: - raise RuntimeError( - "Axis %s: missing configuration key '%s`" - % (axis.name, setting_name) - ) - # check if setting has a method to initialize (set) its value, - # without actually executing the property - try: - props[setting_name].fset - except AttributeError: - raise RuntimeError( - "Axis %s: missing method '%s` to set setting value" - % (axis.name, setting_name) - ) - - for setting_name in axis.settings.config_settings(): - if setting_name == "steps_per_unit": - cval = float(axis.config.get(setting_name)) - rval = axis.settings._hash.raw_get(setting_name) - # Record steps_per_unit - if rval is None: - axis.settings.set(setting_name, cval) - continue - else: - rval = float(rval) - if cval != rval: - ratio = rval / cval - new_dial = axis.dial * ratio - - axis.settings.set("steps_per_unit", cval) - if not axis.no_offset: - # calculate offset so user pos stays the same - axis.settings.set( - "offset", axis.position - axis.sign * new_dial - ) - else: - axis.settings.set("offset", 0) - axis.settings.set("dial_position", new_dial) - - if math.copysign(rval, cval) != rval: - ll = axis.settings.get("low_limit") - hl = axis.settings.get("high_limit") - axis.settings.set("low_limit", -hl) - axis.settings.set("high_limit", -ll) - else: - value = get_setting_or_config_value(axis, setting_name) - setattr(axis, setting_name, value) - def get_axis(self, axis_name): axis = self._axes.get(axis_name) if axis is None: # create it @@ -303,23 +210,13 @@ class Controller: # reference axis return - axis._beacon_channels.clear() - - for setting_name in axis.settings: - setting_value = get_setting_or_config_value(axis, setting_name) - chan_name = "axis.%s.%s" % (axis.name, setting_name) - cb = functools.partial( - setting_update_from_channel, setting_name=setting_name, axis=axis - ) - chan = Channel(chan_name, default_value=setting_value, callback=cb) - chan._setting_update_cb = cb - axis._beacon_channels[setting_name] = chan - if axis.controller is self: axis_initialized = Cache(axis, "initialized", default_value=0) self.__initialized_hw_axis[axis] = axis_initialized self.__initialized_axis[axis] = False + self._add_axis(axis) + return axis def _add_axis(self, axis): @@ -583,11 +480,17 @@ class CalcController(Controller): setpos_dict[self._axis_tag(axis)] = axis.user2dial(axis._set_position) return setpos_dict - def _real_position_update(self, *args): + def _real_position_update(self, pos, sender=None): for axis in self.pseudos: self._initialize_axis(axis) - return self._calc_from_real(*args) + try: + # avoid recursion by disconnecting the signal + event.disconnect(sender, "internal_position", self._real_position_update) + return self._calc_from_real() + finally: + # reconnect + event.connect(sender, "internal_position", self._real_position_update) def _real_setpos_update(self, _): real_setpos = dict() @@ -674,7 +577,7 @@ class CalcController(Controller): ) return self.calc_from_real(real_positions) - def _calc_from_real(self, *args, **kwargs): + def _calc_from_real(self, *args): new_positions = self._do_calc_from_real() for tagged_axis_name, dial_pos in new_positions.items(): diff --git a/bliss/controllers/motors/esrf_undulator.py b/bliss/controllers/motors/esrf_undulator.py index 8096cbc6e8862cd003b993818b25da547cb193fa..e9211f8cb3a1286c97646f267e662cb7965e84e6 100644 --- a/bliss/controllers/motors/esrf_undulator.py +++ b/bliss/controllers/motors/esrf_undulator.py @@ -87,24 +87,15 @@ class ESRF_Undulator(Controller): log_debug(self, f"attr_pos_name={attr_pos_name}") - try: - attr_vel_name = axis.config.get("attribute_velocity", str) - except KeyError: - attr_vel_name = "Velocity" + attr_vel_name = axis.config.get("attribute_velocity", str, "Velocity") log_debug(self, f"attr_vel_name={attr_vel_name}") - try: - attr_fvel_name = axis.config.get("attribute_first_velocity", str) - except KeyError: - attr_fvel_name = "FirstVelocity" - + attr_fvel_name = axis.config.get( + "attribute_first_velocity", str, "FirstVelocity" + ) log_debug(self, f"attr_fvel_name={attr_fvel_name}") - try: - attr_acc_name = axis.config.get("attribute_acceleration", str) - except KeyError: - attr_acc_name = "Acceleration" - + attr_acc_name = axis.config.get("attribute_acceleration", str, "Acceleration") log_debug(self, f"attr_acc_name={attr_acc_name}") alpha = axis.config.get("alpha", float, 0.0) @@ -112,19 +103,18 @@ class ESRF_Undulator(Controller): log_debug(self, f"alpha={alpha} period={period}") - try: - undu_prefix = axis.config.get("undu_prefix", str) - attr_pos_name = undu_prefix + attr_pos_name - attr_vel_name = undu_prefix + attr_vel_name - attr_fvel_name = undu_prefix + attr_fvel_name - attr_acc_name = undu_prefix + attr_acc_name - - except KeyError: + undu_prefix = axis.config.get("undu_prefix", str) + if undu_prefix is None: log_debug(self, "'undu_prefix' not specified in config") if attr_pos_name == "Position": raise RuntimeError("'undu_prefix' must be specified in config") else: undu_prefix = "" + else: + attr_pos_name = undu_prefix + attr_pos_name + attr_vel_name = undu_prefix + attr_vel_name + attr_fvel_name = undu_prefix + attr_fvel_name + attr_acc_name = undu_prefix + attr_acc_name # check for revolver undulator is_revolver = False diff --git a/bliss/controllers/motors/mockup.py b/bliss/controllers/motors/mockup.py index da63e8226e7a8292fe54ff04ece5962c48e1c7a6..a4ff07906a54570c54ec8ef39a0d4540fddbe10e 100644 --- a/bliss/controllers/motors/mockup.py +++ b/bliss/controllers/motors/mockup.py @@ -8,6 +8,7 @@ import math import time import random import gevent +import collections from bliss.physics.trajectory import LinearTrajectory from bliss.controllers.motor import Controller, CalcController @@ -15,7 +16,7 @@ from bliss.common.axis import Axis, AxisState from bliss.common.switch import Switch as BaseSwitch from bliss.common import event from bliss.config.static import get_config - +from bliss.config.settings import SimpleSetting from bliss.common.hook import MotionHook from bliss.common.utils import object_method from bliss.common.utils import object_attribute_get, object_attribute_set @@ -58,6 +59,7 @@ class Mockup(Controller): self._axis_moves = {} self.__encoders = {} self.__switches = {} + self._axes_data = collections.defaultdict(dict) # Custom attributes. self.__voltages = {} @@ -68,30 +70,36 @@ class Mockup(Controller): self._hw_state.create_state("PARKED", "mot au parking") - # Adds Mockup-specific settings. - self.axis_settings.add("init_count", int) - self.axis_settings.add("hw_position", float) - # those 2 are to simulate a real controller (one with internal settings, that - # keep those for multiple clients) - self.axis_settings.add("curr_acc", float) - self.axis_settings.add("curr_velocity", float) - def steps_position_precision(self, axis): """Mockup is really a stepper motor controller""" return 1 def read_hw_position(self, axis): - return axis.settings.get("hw_position") + return self._axes_data[axis]["hw_position"].get() def set_hw_position(self, axis, position): - axis.settings.set("hw_position", position) + self._axes_data[axis]["hw_position"].set(position) """ Axes initialization actions. """ def _add_axis(self, axis): - axis.settings.set("init_count", 0) + # this is a counter to check if an axis is added multiple times, + # it is incremented in `initalize_axis()` + self._axes_data[axis]["init_count"] = 0 + # those 3 are to simulate a real controller (one with internal settings, that + # keep those for multiple clients) + self._axes_data[axis]["hw_position"] = SimpleSetting( + f"motor_mockup:{axis.name}:hw_position" + ) + self._axes_data[axis]["curr_acc"] = SimpleSetting( + f"motor_mockup:{axis.name}:curr_acc" + ) + self._axes_data[axis]["curr_velocity"] = SimpleSetting( + f"motor_mockup:{axis.name}:curr_velocity" + ) + encoder = axis.config.get("encoder", converter=None, default=None) if encoder: self.initialize_encoder(encoder) @@ -100,6 +108,11 @@ class Mockup(Controller): if self.read_hw_position(axis) is None: self.set_hw_position(axis, 0) + def initialize_hardware_axis(self, axis): + self._axes_data[axis]["hw_position"].set(0) + self._axes_data[axis]["curr_acc"].set(0) + self._axes_data[axis]["curr_velocity"].set(0) + def initialize_axis(self, axis): log_debug(self, "initializing axis %s", axis.name) @@ -109,7 +122,7 @@ class Mockup(Controller): ) # this is to test axis are initialized only once - axis.settings.set("init_count", axis.settings.get("init_count") + 1) + self._axes_data[axis]["init_count"] += 1 axis.stop_jog_called = False # the next lines are there to test issue #1601 @@ -250,7 +263,7 @@ class Mockup(Controller): Return the current velocity taken from controller in motor units. """ - return axis.settings.get("curr_velocity") * abs(axis.steps_per_unit) + return self._axes_data[axis]["curr_velocity"].get() * abs(axis.steps_per_unit) def set_velocity(self, axis, new_velocity): """ @@ -259,7 +272,7 @@ class Mockup(Controller): vel = new_velocity / abs(axis.steps_per_unit) if vel >= 1e9: raise RuntimeError("Invalid velocity") - axis.settings.set("curr_velocity", vel) + self._axes_data[axis]["curr_velocity"].set(vel) return vel """ @@ -270,7 +283,7 @@ class Mockup(Controller): """ must return acceleration in controller units / s2 """ - return axis.settings.get("curr_acc") * abs(axis.steps_per_unit) + return self._axes_data[axis]["curr_acc"].get() * abs(axis.steps_per_unit) def set_acceleration(self, axis, new_acceleration): """ @@ -279,7 +292,7 @@ class Mockup(Controller): acc = new_acceleration / abs(axis.steps_per_unit) if acc >= 1e9: raise RuntimeError("Invalid acceleration") - axis.settings.set("curr_acc", acc) + self._axes_data[axis]["curr_acc"].set(acc) return acc """ diff --git a/bliss/controllers/motors/soft.py b/bliss/controllers/motors/soft.py index c658c6cbb7d82c6d15fd69678919bfebd5c52300..4f8f3da45cf49079ec4b81cb024caf6944e64980 100644 --- a/bliss/controllers/motors/soft.py +++ b/bliss/controllers/motors/soft.py @@ -75,12 +75,18 @@ def get_state_func(obj, state): return state_func +class SoftAxis(NoSettingsAxis): + pass + + class SoftController(Controller): def __init__( self, axis_name, obj, axis_config, position, move, stop=None, state=None ): - axes = {axis_name: (NoSettingsAxis, axis_config)} + axis_config.setdefault("steps_per_unit", 1) + + axes = {axis_name: (SoftAxis, axis_config)} super(SoftController, self).__init__( "__soft_controller__", {}, axes, {}, {}, {} diff --git a/bliss/tango/servers/axis_ds.py b/bliss/tango/servers/axis_ds.py index 5d1b3c434348dd2500ea2ef6cba70ee9bef733cf..cedabc11624a86d7c73800cb59bcc7b8d6df219b 100644 --- a/bliss/tango/servers/axis_ds.py +++ b/bliss/tango/servers/axis_ds.py @@ -1442,7 +1442,7 @@ def __create_tango_axis_class(axis): % axis.name ) - for setting_name in axis.settings: + for setting_name in axis.settings.setting_names: if setting_name in [ "velocity", "position", @@ -1456,7 +1456,7 @@ def __create_tango_axis_class(axis): ]: elog.debug(" BlissAxisManager.py -- std SETTING %s " % (setting_name)) else: - _setting_type = axis.controller.axis_settings.convert_func[setting_name] + _setting_type = axis.settings.convert_func(setting_name) _attr_type = types_conv_tab[_setting_type] elog.debug( " BlissAxisManager.py -- adds SETTING %s as %s attribute" diff --git a/requirements-test-conda.txt b/requirements-test-conda.txt index d4896bd7921f545282b0983668c87e87b12c1a9c..a9fbf063a7a60c4f7406f3806a9c19593201b426 100644 --- a/requirements-test-conda.txt +++ b/requirements-test-conda.txt @@ -6,7 +6,8 @@ pytest == 5.3.5 pytest-cov pytest-mock lima-core == 1.9.4 -lima-camera-simulator-tango +lima-camera-simulator >= 1.9.2 +lima-camera-simulator-tango >= 1.9.2 lima-tango-server scipy black == 18.6b4 diff --git a/tests/conftest.py b/tests/conftest.py index df592d09e404f41bf4b4a98bfaf1bbcee59ccd77..cf473dceb4a5fb6cccf33fb05ca253b734971999 100755 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -239,11 +239,11 @@ def beacon(ports): client._default_connection = connection.Connection("localhost", ports.beacon_port) config = static.get_config() yield config - settings_cache.close_all_client_cache() config.close() client._default_connection.close() # Ensure no connections are created due to garbage collection: client._default_connection = None + settings_cache.close_all_client_cache() @pytest.fixture diff --git a/tests/motors/test_axis.py b/tests/motors/test_axis.py index bb0773339b138c6fd27c0169c125ffb74768223b..afd6110471e259efdb65820ac5e7b6b23c56503e 100644 --- a/tests/motors/test_axis.py +++ b/tests/motors/test_axis.py @@ -248,7 +248,7 @@ def test_axis_multiple_move(robz): def test_axis_init(robz): assert robz.state.READY - assert robz.settings.get("init_count") == 1 + assert robz.controller._axes_data[robz]["init_count"] == 1 def test_stop(robz): @@ -725,7 +725,6 @@ def test_no_offset(roby): @pytest.mark.parametrize("motor_name", ["roby", "nsa"]) def test_offset_property(beacon, motor_name): - breakpoint() mot = beacon.get(motor_name) mot.move(1) @@ -757,18 +756,30 @@ def test_settings_to_config(beacon, motor_name): cfg = beacon.get_config(motor_name) cfg_acc = cfg.get("acceleration") cfg_vel = cfg.get("velocity") + cfg_ll = cfg.get("low_limit") + cfg_hl = cfg.get("high_limit") mot.velocity = 3 mot.acceleration = 10 mot.limits = None, None assert mot.config_velocity == cfg_vel assert mot.config_acceleration == cfg_acc + assert mot.config_limits == (cfg_ll, cfg_hl) mot.settings_to_config() - assert mot.config_velocity == 3 - assert mot.config_acceleration == 10 - mot.velocity = cfg_vel - mot.acceleration = cfg_acc - mot.settings_to_config() + try: + assert mot.config_velocity == 3 + assert mot.config_acceleration == 10 + assert mot.config_limits == (float("-inf"), float("inf")) + finally: + # put back config files as they were, + # since the fixture that starts + # all servers (and that copy config. to a temp folder + # for the test) has a session scope + mot.velocity = cfg_vel + mot.acceleration = cfg_acc + mot.low_limit = cfg_ll + mot.high_limit = cfg_hl + mot.settings_to_config() @pytest.mark.parametrize("motor_name", ["roby", "nsa"]) @@ -891,10 +902,18 @@ def test_axis_no_state_setting(m1): with mock.patch.object(m1.controller, "state") as new_state: new_state.return_value = AxisState("FAULT") assert m1.state == state + assert m1.hw_state == AxisState("FAULT") m1.settings.disable_cache("state") assert m1.state == AxisState("FAULT") + # re-enable cache + new_state.return_value = AxisState("READY") m1.settings.disable_cache("state", False) - assert m1.state == state + assert m1.hw_state == AxisState("READY") + # the beacon channel has been removed, there is no source of data + # so this will read the hw_state + assert m1.state == m1.hw_state + new_state.return_value = AxisState("FAULT") + assert m1.state == AxisState("READY") def test_axis_disable_cache_settings_from_config(beacon): @@ -1004,3 +1023,25 @@ def test_no_settings_offset(beacon): nsa.offset = 1 assert nsa.position == pytest.approx(1) assert nsa.dial == pytest.approx(0) + + +def test_invalid_config(beacon): + invalid_cfg = beacon.get_config("invalid_cfg_axis") + + invalid_mot = beacon.get("invalid_cfg_axis") + with pytest.raises(RuntimeError) as exc: + invalid_mot.position # lazy init + assert "velocity" in str(exc.value) + + invalid_cfg["velocity"] = 1 + with pytest.raises(RuntimeError) as exc: + invalid_mot.position # lazy init + assert "acceleration" in str(exc.value) + + invalid_cfg["acceleration"] = 1 + with pytest.raises(RuntimeError) as exc: + invalid_mot.position # lazy init + assert "steps_per_unit" in str(exc.value) + + invalid_cfg["steps_per_unit"] = 1 + assert invalid_mot.position == 0 diff --git a/tests/test_configuration/motors/mockup.yml b/tests/test_configuration/motors/mockup.yml index 9cc1378fc51c710bc054028384e0616d2ee90c21..4b23f3f6e1d67c9f5114f3c880ba103e0d534b15 100644 --- a/tests/test_configuration/motors/mockup.yml +++ b/tests/test_configuration/motors/mockup.yml @@ -247,6 +247,9 @@ controller: steps_per_unit: 10 acceleration: 100 velocity: 1e9 +- class: mockup + axes: + - name: invalid_cfg_axis - class: EnergyWavelength axes: - name: $mono