313 lines
12 KiB
Python
313 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
|
# *****************************************************************************
|
|
# NICOS, the Networked Instrument Control System of the MLZ
|
|
# Copyright (c) 2009-2018 by the NICOS contributors (see AUTHORS)
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify it under
|
|
# the terms of the GNU General Public License as published by the Free Software
|
|
# Foundation; either version 2 of the License, or (at your option) any later
|
|
# version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License along with
|
|
# this program; if not, write to the Free Software Foundation, Inc.,
|
|
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
#
|
|
# Module authors:
|
|
# Markus Zolliker <markus.zolliker@psi.ch>
|
|
#
|
|
# *****************************************************************************
|
|
"""managing SECoP server and connections
|
|
|
|
SEC Node with added functionality for starting and stoping frappy servers
|
|
connected to a SEC node
|
|
"""
|
|
|
|
import os
|
|
from os.path import expanduser
|
|
|
|
from nicos import config, session
|
|
from nicos.core import Override, Param, Moveable, status, UsageError
|
|
from nicos.devices.secop import SecNodeDevice
|
|
from nicos.core import Device, anytype, listof
|
|
from nicos.utils.comparestrings import compare
|
|
from servicemanager import FrappyManager
|
|
|
|
|
|
def suggest(poi, allowed_keys):
|
|
comp = {}
|
|
for key in allowed_keys:
|
|
comp[key] = compare(poi, key)
|
|
comp = sorted(comp.items(), key=lambda t: t[1], reverse=True)
|
|
return [m[0] for m in comp[:3] if m[1] > 2]
|
|
|
|
|
|
def applyAliasConfig():
|
|
"""Apply the desired aliases from session.alias_config.
|
|
|
|
be more quiet than original
|
|
"""
|
|
# reimplemented from Session.applyAliasConfig
|
|
# apply also when target dev name does not change, as the target device might have
|
|
# be exchanged in the mean time
|
|
unused = set()
|
|
for aliasname, targets in session.alias_config.items():
|
|
if aliasname not in session.devices:
|
|
continue # silently ignore
|
|
aliasdev = session.getDevice(aliasname)
|
|
for target, _ in sorted(targets, key=lambda t: -t[1]):
|
|
if target in session.devices:
|
|
try:
|
|
aliasdev.alias = target
|
|
except Exception:
|
|
session.log.exception("could not set '%s' alias", aliasdev)
|
|
break
|
|
|
|
|
|
class FrappyConfig(Device):
|
|
# respect the order: e.g. temperature_regulation must be after temperature
|
|
# and temperature_drivable must be the last in the temperature group
|
|
parameters = {
|
|
'temperature': Param(
|
|
'config for sample temperature', type=anytype,
|
|
default={'alias': 'Ts'}),
|
|
'temperature_regulation': Param(
|
|
'config for temperature regulation', type=anytype,
|
|
default={'alias': 'Tr'}),
|
|
'temperature_drivable': Param(
|
|
'config for drivable temperature', type=anytype,
|
|
default={'alias': 'T', 'envlist': False}),
|
|
'magneticfield': Param(
|
|
'config for magnetic field', type=anytype,
|
|
default={'alias': 'B'}),
|
|
'rotation_z': Param(
|
|
'config for sample rotation', type=anytype,
|
|
default={'alias': 'stickrot', 'envlist': False}),
|
|
'stick_rotation_alias': Param(
|
|
'alias to assign stick rotation', type=str, settable=True, default=''),
|
|
'nodes': Param(
|
|
'list of names of potential SEC nodes', type=listof(str), default=[]),
|
|
}
|
|
|
|
meanings = list(parameters)
|
|
meanings.remove('nodes')
|
|
meanings.remove('stick_rotation_alias')
|
|
|
|
def remove_aliases(self):
|
|
for meaning in self.meanings:
|
|
info = getattr(self, meaning)
|
|
aliasnames = info['alias']
|
|
if isinstance(aliasnames, str):
|
|
aliasnames = [aliasnames]
|
|
for aliasname in aliasnames:
|
|
aliasdev = session.devices.get(aliasname)
|
|
if aliasdev:
|
|
session.destroyDevice(aliasname)
|
|
session.configured_devices.pop(aliasname, None)
|
|
session.dynamic_devices.pop(aliasname, None)
|
|
|
|
def get_se_aliases(self):
|
|
result = {}
|
|
for meaning in self.meanings:
|
|
info = getattr(self, meaning)
|
|
aliasnames = info['alias']
|
|
if isinstance(aliasnames, str):
|
|
aliasnames = [aliasnames]
|
|
for aliasname in aliasnames:
|
|
aliasdev = session.devices.get(aliasname)
|
|
if aliasdev:
|
|
result[aliasname] = aliasdev
|
|
return result
|
|
|
|
def set_envlist(self):
|
|
"""create aliases for SECoP devices
|
|
|
|
depending on their meaning
|
|
"""
|
|
previous_aliases = self.get_se_aliases()
|
|
# self.remove_aliases()
|
|
|
|
nodedevs = filter(None, [session.devices.get(devname) for devname in self.nodes])
|
|
sample_devices = {}
|
|
for nodedev in nodedevs:
|
|
secnode = nodedev._secnode
|
|
if not secnode:
|
|
continue
|
|
for devname, (_, desc) in nodedev.setup_info.items():
|
|
secop_module = desc['secop_module']
|
|
meaning = secnode.modules[secop_module]['properties'].get('meaning')
|
|
if meaning:
|
|
meaning_name, importance = meaning
|
|
sample_devices.setdefault(meaning_name, []).append((importance, devname))
|
|
|
|
devset = set()
|
|
newenv = {e: None for e in session.experiment.envlist} # use dict instead of set because of order
|
|
drivables = {}
|
|
for meaning in self.meanings:
|
|
info = getattr(self, meaning)
|
|
aliasnames = info['alias']
|
|
if isinstance(aliasnames, str):
|
|
aliasnames = [aliasnames]
|
|
envlistflag = info.get('envlist', True)
|
|
aliascfg = info.get('targets', {})
|
|
importance_list = sample_devices.get(meaning, [])
|
|
importance_list.extend([(nr, nam) for nam, nr in aliascfg.items() if nam in session.devices])
|
|
importance_list = sorted(importance_list, reverse=True)
|
|
session.log.debug('%s: %r', meaning, importance_list)
|
|
prefix, _, postfix = meaning.partition('_')
|
|
if postfix == 'drivable':
|
|
# append the previously collected drivables in the group to the importance_list
|
|
group = drivables.get(prefix, {})
|
|
for key in 'regulation', '':
|
|
if key in group:
|
|
importance_list.append((None, group[key]))
|
|
for _, devname, in importance_list:
|
|
dev = session.devices.get(devname)
|
|
if isinstance(dev, Moveable):
|
|
drivables.setdefault(prefix, {})[postfix] = devname
|
|
elif postfix == 'drivable':
|
|
# marked as xxx_drivable, but not really drivable: skip
|
|
continue
|
|
if dev:
|
|
for aliasname in aliasnames:
|
|
session.configured_devices[aliasname] = devcfg
|
|
session.dynamic_devices[aliasname] = 'frappy'
|
|
aliasdev = previous_aliases.pop(aliasname, None)
|
|
if aliasdev:
|
|
session.log.debug('change alias %r -> %r', aliasname, devname)
|
|
else:
|
|
session.log.debug('create alias %r -> %r', aliasname, devname)
|
|
devcfg = ('nicos.core.DeviceAlias', {})
|
|
aliasdev = session.createDevice(aliasname, recreate=True, explicit=True)
|
|
aliasdev.alias = devname
|
|
aliasname = aliasnames[0]
|
|
if devname not in devset and envlistflag:
|
|
# take only the first one
|
|
devset.add(devname)
|
|
newenv[aliasname] = None
|
|
else:
|
|
newenv.pop(aliasname, None)
|
|
break
|
|
else:
|
|
for aliasname in aliasnames:
|
|
newenv.pop(aliasname, None)
|
|
|
|
for aliasname in previous_aliases:
|
|
session.destroyDevice(aliasname)
|
|
session.configured_devices.pop(aliasname, None)
|
|
session.dynamic_devices.pop(aliasname, None)
|
|
|
|
applyAliasConfig() # for other aliases
|
|
if set(newenv) != set(session.experiment.envlist):
|
|
session.experiment.setEnvironment(list(newenv))
|
|
session.log.info('changed environment to: %s', ', '.join(session.experiment.envlist))
|
|
|
|
|
|
class FrappyNode(SecNodeDevice, Moveable):
|
|
"""SEC node device
|
|
|
|
with ability to start / restart / stop the frappy server
|
|
"""
|
|
|
|
parameter_overrides = {
|
|
'target': Override(description='configuration for the frappy server or host:port',
|
|
type=str, default=''),
|
|
}
|
|
parameters = {
|
|
'service': Param('frappy service name (main, stick or addons)', type=str, default=''),
|
|
}
|
|
|
|
_service_manager = FrappyManager()
|
|
_cfgvalue = None
|
|
|
|
def doStart(self, value):
|
|
if value == 'None':
|
|
value = None
|
|
self.restart(value)
|
|
|
|
def doStop(self):
|
|
"""never busy"""
|
|
|
|
def doRead(self, maxage=0):
|
|
if self._cfgvalue is None and self._cache:
|
|
self._cfgvalue = self._cache.get(self, 'value')
|
|
return self._cfgvalue
|
|
|
|
@classmethod
|
|
def config_dirs(cls, ins, service):
|
|
sm = cls._service_manager
|
|
sm.get_info()
|
|
return sm.config_dirs(ins, service)
|
|
|
|
@classmethod
|
|
def available_cfg(cls, service):
|
|
ins = config.instrument
|
|
available_cfg = set()
|
|
for d in cls.config_dirs(ins, service):
|
|
try:
|
|
available_cfg |= set(c[:-4] for c in os.listdir(expanduser(d)) if c.endswith('.cfg'))
|
|
except FileNotFoundError: # ignore missing directories
|
|
pass
|
|
return available_cfg
|
|
|
|
def disable(self):
|
|
seaconn = session.devices.get('seaconn')
|
|
if seaconn and seaconn._attached_secnode:
|
|
seaconn.communicate('frappy_remove %s' % self.service)
|
|
self._set_status(*self._status)
|
|
|
|
def _set_status(self, code, text):
|
|
if self.uri == '':
|
|
code, text = status.DISABLED, 'disabled'
|
|
SecNodeDevice._set_status(self, code, text)
|
|
|
|
def restart(self, cfg=None):
|
|
"""restart frappy server
|
|
|
|
if value is not given: restart with the same config
|
|
else restart with given config
|
|
"""
|
|
was_cfg = self._cfgvalue and ':' not in self._cfgvalue
|
|
if cfg is None: # do restart anyway
|
|
cfg = self._cfgvalue
|
|
elif cfg == self._cfgvalue: # probably called from doStart
|
|
if cfg == '':
|
|
self.disable()
|
|
# do not restart in this case
|
|
return
|
|
is_cfg = cfg and ':' not in cfg
|
|
ins = config.instrument
|
|
info = self._service_manager.get_ins_info(ins)
|
|
if is_cfg:
|
|
available_cfg = self.available_cfg(self.service)
|
|
failed = False
|
|
for cfgitem in cfg.split(','):
|
|
if cfgitem not in available_cfg:
|
|
failed = True
|
|
suggestions = suggest(cfgitem, available_cfg)
|
|
if suggestions:
|
|
session.log.error('%s unknown, did you mean: %s' % (cfgitem, ', '.join(suggestions)))
|
|
if failed:
|
|
raise ValueError('use "frappy_list()" to get a list of valid frappy configurations')
|
|
uri = 'localhost:%d' % info[self.service]
|
|
else:
|
|
uri = cfg
|
|
if uri != self.uri:
|
|
self.uri = '' # disconnect
|
|
if was_cfg:
|
|
self._service_manager.do_stop(ins, self.service)
|
|
if uri:
|
|
if is_cfg:
|
|
self._service_manager.do_restart(ins, self.service, cfg, self.log)
|
|
self.uri = uri # connect
|
|
else:
|
|
self.disable()
|
|
self._cfgvalue = cfg
|
|
if self._cache:
|
|
self._cache.put(self, 'value', cfg)
|
|
self._setROParam('target', cfg)
|