From 34075a07afa549a8ae0e6f5d69326f691315d127 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Fri, 8 Apr 2022 09:36:17 +0200 Subject: [PATCH] move files in from repo git.psi.ch/sinqdev/nicos-sinq --- .gitignore | 19 ++++ commands.py | 207 ++++++++++++++++++++++++++++++++++++++++ devices.py | 150 +++++++++++++++++++++++++++++ setups/frappy.py | 26 +++++ setups/frappy_addons.py | 11 +++ setups/frappy_main.py | 11 +++ setups/frappy_stick.py | 11 +++ 7 files changed, 435 insertions(+) create mode 100644 .gitignore create mode 100644 commands.py create mode 100644 devices.py create mode 100644 setups/frappy.py create mode 100644 setups/frappy_addons.py create mode 100644 setups/frappy_main.py create mode 100644 setups/frappy_stick.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..710cf1a --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +log/* +html/* +*.pyc +pid/* +__pycache__ + +# ide +.idea + +# Mac +.DS_Store +._* + +# pytest +.cache +.coverage + +# pyinstaller +dist/ diff --git a/commands.py b/commands.py new file mode 100644 index 0000000..1785bd0 --- /dev/null +++ b/commands.py @@ -0,0 +1,207 @@ +# -*- coding: utf-8 -*- +# ***************************************************************************** +# +# 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 +# +# ***************************************************************************** + +import sys +from os.path import expanduser, basename, join +from glob import glob +from configparser import ConfigParser + +from nicos import session, config +from nicos.utils import printTable +from nicos.commands import helparglist, usercommand +from nicos.commands.basic import AddSetup, CreateAllDevices, CreateDevice +from nicos.devices.secop import get_attaching_devices + +home = expanduser('~') +if home not in sys.path: + # the first Frappy installations have /home/nicos in the PYTHONPATH in nicos.conf + # for newer Frappy installations this should be home (= /home/) + # the following line fixes this in case nicos.conf is not yet updated + sys.path.append(home) +from nicos_sinq.frappy.devices import FrappyNode +from servicemanager import FrappyManager + + +def cleanup_defunct(): + for devname, setupname in list(session.dynamic_devices.items()): + dev = session.devices.get(devname) + if dev and dev._defunct: + devnames = [d.name for d, _ in get_attaching_devices(dev)] + if devnames: + session.log.warning('can not remove device %r due to dependencies on %s' + % (devname, ', '.join(devnames))) + else: + session.destroyDevice(devname) + session.dynamic_devices.pop(devname, None) + + +SERVICES = FrappyManager.services + + +def all_info(all_cfg): + info = [] + for srv, cfginfo in all_cfg.items(): + if cfginfo is not None: + info.append('frappy_%s(%r)' % (srv, cfginfo)) + return 'currently configured: %s' % ', '.join(info) + + +def applyAliasConfig(): + """Apply the desired aliases from session.alias_config.""" + # 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 + for aliasname, targets in session.alias_config.items(): + if aliasname not in session.devices: + # complain about this; setups should make sure that the device + # exists when configuring it + session.log.warning("alias device '%s' does not exist, cannot set" + ' its target', aliasname) + continue + 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 + else: + session.log.warning('none of the desired targets for alias %r ' + 'actually exist', aliasname) + + +def frappy_start(service, cfg=None): + if service not in SERVICES: + raise ValueError('unknown service %s' % service) + if cfg == '': + seaconn = session.devices.get('seaconn') + if seaconn and seaconn._attached_secnode: + seaconn.communicate('frappy_remove %s' % service) + startnode = None + all_cfg = {} + for srv in SERVICES: + nodename = 'se_' + srv + secnode = session.devices.get(nodename) + + cfginfo = None if secnode is None else secnode() + if srv == service: + if cfg is not None: + if cfg and not secnode: + AddSetup('frappy_' + service) + secnode = session.devices[nodename] + if cfginfo: + secnode('') + startnode = secnode + cfginfo = cfg + elif cfg and cfginfo: + # remark: cfg might be a comma separated list of configurations + ourcfg = set(cfg.split(',')) + other = set(cfginfo.split(',')) + both = other & ourcfg + if both: + # remove ourcfg from other + cfginfo = ','.join(other - ourcfg) + # stop other server with the same cfg + # or restart with remaining cfgs + secnode(cfginfo) + all_cfg[srv] = cfginfo + if startnode and cfg: + startnode(cfg) + CreateDevice(str(startnode)) + if cfg is not None: + cleanup_defunct() + CreateAllDevices() + applyAliasConfig() + if startnode and cfg == '': + startnode.disable() + return all_cfg + + +@usercommand +@helparglist('cfg') +def frappy_main(cfg=None): + """(re)start frappy_main server with given cfg and load setup if needed + + - without argument: list running frappy servers + - cfg = "": stop frappy_main server + """ + all_cfg = frappy_start('main', cfg) + if cfg: + stickcfg = cfg + 'stick' + if stickcfg in FrappyNode.available_cfg('stick'): + # if a default stick is available, start this also + all_cfg = frappy_start('stick', stickcfg) + session.log.info(all_info(all_cfg)) + + +@usercommand +@helparglist('cfg') +def frappy_stick(cfg=None): + """(re)start frappy_stick server with given cfg and load setup if needed + + - without argument: list running frappy servers + - cfg = "": stop frappy_stick server + """ + session.log.info(all_info(frappy_start('stick', cfg))) + + +@usercommand +@helparglist('cfg,...') +def frappy_addons(cfg=None): + """(re)start frappy_addons server with given cfg and load setup if needed + + - without argument: list running frappy servers + - cfg = "": stop frappy_addons server + """ + session.log.info(all_info(frappy_start('addons', cfg))) + + +@usercommand +@helparglist('') +def frappy_list(service=None): + """list available configuration files""" + table = [] + bases = list(dict.fromkeys(expanduser(p) for p in FrappyNode.config_dirs(config.instrument, service or 'main'))) + if service is None: + session.log.info('Available configuration files') + session.log.info(' ') + session.log.info('Hint: if no config file can be found which matches your needs exactly') + session.log.info('make a copy of an existing one, and change the description accordingly') + session.log.info(' ') + session.log.info('Usage (default argument "main"):') + session.log.info(' ') + printTable(['command'], [['frappy_list(%r)' % s] for s in SERVICES], session.log.info) + session.log.info(' ') + for cfgdir in bases: + for cfgfile in glob(join(cfgdir, '*.cfg')): + parser = ConfigParser() + parser.read(cfgfile) + desc = '' + for s in parser.sections(): + if s == 'NODE' or s.startswith('node '): + desc = parser[s].get('description', '').split('\n')[0] + break + table.append([basename(cfgfile)[:-4], desc]) + printTable(['cfg file', 'description'], table, session.log.info) + + diff --git a/devices.py b/devices.py new file mode 100644 index 0000000..652ebff --- /dev/null +++ b/devices.py @@ -0,0 +1,150 @@ +# -*- 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 +# +# ***************************************************************************** +"""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 +from nicos.devices.secop import SecNodeDevice +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] + + +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) diff --git a/setups/frappy.py b/setups/frappy.py new file mode 100644 index 0000000..cec01b7 --- /dev/null +++ b/setups/frappy.py @@ -0,0 +1,26 @@ +description = 'frappy' +group = 'optional' + +modules = ['nicos_sinq.frappy.commands'] + +devices = { + 'temperature': device('nicos.devices.generic.DeviceAlias'), + 'magfield': device('nicos.devices.generic.DeviceAlias'), +} +alias_config = { + 'temperature': {'se_ts': 110, 'se_tt': 100}, + 'magfield': {'se_mf': 100}, +} + +startupcode = ''' +printinfo("=======================================================================================") +printinfo("Welcome to the NICOS frappy secnode setup!") +printinfo(" ") +printinfo("Usage:") +printinfo(" frappy_main('
') # change main SE configuration (e.g. cryostat)") +printinfo(" frappy_stick('') # change sample-stick configuration") +printinfo(" frappy_stick('') # remove stick") +printinfo(" frappy_main('') # remove main SE apparatus") +printinfo(" frappy_main() # show the current SE configuration") +printinfo("=======================================================================================") +''' diff --git a/setups/frappy_addons.py b/setups/frappy_addons.py new file mode 100644 index 0000000..ba38fc9 --- /dev/null +++ b/setups/frappy_addons.py @@ -0,0 +1,11 @@ +from os import environ +description = 'frappy addons' +group = 'optional' + +devices = { + 'se_addons': + device('nicos_sinq.frappy.devices.FrappyNode', + description='SEC node', unit='', + prefix='se_', auto_create=True, service='addons', + ), +} diff --git a/setups/frappy_main.py b/setups/frappy_main.py new file mode 100644 index 0000000..45cb75c --- /dev/null +++ b/setups/frappy_main.py @@ -0,0 +1,11 @@ +from os import environ +description = 'frappy main setup' +group = 'optional' + +devices = { + 'se_main': + device('nicos_sinq.frappy.devices.FrappyNode', + description='main SEC node', unit='', + prefix='se_', auto_create=True, service='main', + ), +} diff --git a/setups/frappy_stick.py b/setups/frappy_stick.py new file mode 100644 index 0000000..9d2feae --- /dev/null +++ b/setups/frappy_stick.py @@ -0,0 +1,11 @@ +from os import environ +description = 'frappy stick setup' +group = 'optional' + +devices = { + 'se_stick': + device('nicos_sinq.frappy.devices.FrappyNode', + description='stick SEC node', unit='', + prefix='se_', auto_create=True, service='stick', + ), +}