diff --git a/secop/client/interactive.py b/secop/client/interactive.py new file mode 100644 index 0000000..85d6428 --- /dev/null +++ b/secop/client/interactive.py @@ -0,0 +1,263 @@ +# -*- 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 +# +# ***************************************************************************** +"""simple interactive python client""" + +import sys +import time +import json +from queue import Queue +from secop.client import SecopClient +from secop.errors import SECoPError + +USAGE = """ +Usage: + +from secop.client.interactive import Client + +client = Client('localhost:5000') # start client. +# this connects and creates objects for all SECoP modules in the main namespace + + # list all parameters +. = # change parameter +() # set target and wait until not busy + # 'status' and 'value' changes are shown every 1 sec +client.mininterval = 0.2 # change minimal update interval to 0.2 sec (default is 1 second) + +.watch(1) # watch changes of all parameters of a module +.watch(0) # remove all watching +.watch(status=1, value=1) # add 'status' and 'value' to watched parameters +.watch(value=0) # remove 'value' from watched parameters +""" + +main = sys.modules['__main__'] + + +class Logger: + def __init__(self, loglevel='info'): + func = self.noop + for lev in 'debug', 'info', 'warning', 'error': + if lev == loglevel: + func = self.emit + setattr(self, lev, func) + + @staticmethod + def emit(fmt, *args, **kwds): + print(str(fmt) % args) + + @staticmethod + def noop(fmt, *args, **kwds): + pass + + +class PrettyFloat(float): + def __repr__(self): + result = '%.12g' % self + if '.' in result or 'e' in result: + return result + return result + '.' + + +class Module: + def __init__(self, name, secnode): + self._name = name + self._secnode = secnode + self._parameters = list(secnode.modules[name]['parameters']) + self._commands = list(secnode.modules[name]['commands']) + self._running = None + self._status = None + props = secnode.modules[name]['properties'] + self._title = '# %s (%s)' % (props.get('implementation', ''), props.get('interface_classes', [''])[0]) + + def _one_line(self, pname, minwid=0): + """return . = truncated to one line""" + try: + value = getattr(self, pname) + # make floats appear with 7 digits only + r = repr(json.loads(json.dumps(value), parse_float=PrettyFloat)) + except Exception as e: + r = repr(e) + unit = getattr(type(self), pname).unit + if unit: + r += ' %s' % unit + pname = pname.ljust(minwid) + vallen = 113 - len(self._name) - len(pname) + if len(r) > vallen: + r = r[:vallen - 4] + ' ...' + return '%s.%s = %s' % (self._name, pname, r) + + def _isBusy(self): + return 300 <= self.status[0] < 400 + + def _status_value_update(self, m, p, status, t, e): + if self._running: + try: + self._running.put(True) + if self._running and not self._isBusy(): + self._running.put(False) + except TypeError: # may happen when _running is removed during above lines + pass + + def _watch_parameter(self, m, pname, *args, forced=False, mininterval=0): + """show parameter update""" + pobj = getattr(type(self), pname) + if not args: + args = self._secnode.cache[self._name, pname] + value = args[0] + now = time.time() + if (value != pobj.prev and now >= pobj.prev_time + mininterval) or forced: + self._secnode.log.info('%s', self._one_line(pname)) + pobj.prev = value + pobj.prev_time = now + + def watch(self, *args, **kwds): + enabled = {} + for arg in args: + if arg == 1: # or True + enabled.update({k: True for k in self._parameters}) + elif arg == 0: # or False + enabled.update({k: False for k in self._parameters}) + else: + enabled.update(arg) + enabled.update(kwds) + for pname, enable in enabled.items(): + self._secnode.unregister_callback((self._name, pname), updateEvent=self._watch_parameter) + if enable: + self._secnode.register_callback((self._name, pname), updateEvent=self._watch_parameter) + + def read(self, pname='value'): + value, _, error = self._secnode.readParameter(self._name, pname) + if error: + raise error + return value + + def __call__(self, target=None): + if target is None: + return self.read() + self.target = target # this sets self._running + type(self).value.prev = None # show at least one value + show_final_value = True + try: + while self._running.get(): + self._watch_parameter(self._name, 'value', mininterval=self._secnode.mininterval) + self._watch_parameter(self._name, 'status') + except KeyboardInterrupt: + self._secnode.log.info('-- interrupted --') + self._running = None + self._watch_parameter(self._name, 'status') + self._secnode.readParameter(self._name, 'value') + self._watch_parameter(self._name, 'value', forced=show_final_value) + return self.value + + def __repr__(self): + wid = max(len(k) for k in self._parameters) + return '%s\n%s\nCommands: %s' % ( + self._title, + '\n'.join(self._one_line(k, wid) for k in self._parameters), + ', '.join(k + '()' for k in self._commands)) + + +class Param: + def __init__(self, name, unit=None): + self.name = name + self.prev = None + self.prev_time = 0 + self.unit = unit + + def __get__(self, obj, owner): + if obj is None: + return self + value, _, error = obj._secnode.cache[obj._name, self.name] + if error: + raise error + return value + + def __set__(self, obj, value): + if self.name == 'target': + obj._running = Queue() + try: + obj._secnode.setParameter(obj._name, self.name, value) + except SECoPError as e: + obj._secnode.log.error(repr(e)) + + +class Command: + def __init__(self, name, modname, secnode): + self.name = name + self.modname = modname + self.exec = secnode.execCommand + + def call(self, *args, **kwds): + if kwds: + if args: + raise TypeError('mixed arguments forbidden') + result, _ = self.exec(self.modname, self.name, kwds) + else: + result, _ = self.exec(self.modname, self.name, args or None) + return result + + def __get__(self, obj, owner=None): + if obj is None: + return self + return self.call + + +class Client(SecopClient): + activate = True + secnodes = {} + mininterval = 1 + + def __init__(self, uri, loglevel='info'): + # remove previous client: + prev = self.secnodes.pop(uri, None) + if prev: + prev.log.info('remove previous client to %s', uri) + for modname in prev.modules: + prevnode = getattr(getattr(main, modname, None), 'secnode', None) + if prevnode == prev: + prev.log.info('remove previous module %s', modname) + delattr(main, modname) + prev.disconnect() + self.secnodes[uri] = self + super().__init__(uri, Logger(loglevel)) + self.connect() + for modname, moddesc in self.modules.items(): + prev = getattr(main, modname, None) + if prev is None: + self.log.info('create module %s', modname) + else: + if getattr(prev, 'secnode', None) is None: + self.log.error('skip module %s overwriting a global variable' % modname) + continue + self.log.info('overwrite module %s', modname) + attrs = {} + for pname, pinfo in moddesc['parameters'].items(): + unit = pinfo['datainfo'].get('unit') + attrs[pname] = Param(pname, unit) + for cname in moddesc['commands']: + attrs[cname] = Command(cname, modname, self) + mobj = type('M_%s' % modname, (Module,), attrs)(modname, self) + if 'status' in mobj._parameters: + self.register_callback((modname, 'status'), updateEvent=mobj._status_value_update) + self.register_callback((modname, 'value'), updateEvent=mobj._status_value_update) + + setattr(main, modname, mobj) + self.log.info('%s', USAGE)