# -*- 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: # Enrico Faulhaber # # ***************************************************************************** """Define Baseclasses for real devices implemented in the server""" # XXX: connect with 'protocol'-Devices. # Idea: every Device defined herein is also a 'protocol'-device, # all others MUST derive from those, the 'interface'-class is still derived # from these base classes (how to do this?) import time import types import inspect import threading from secop.errors import ConfigError, ProgrammingError from secop.protocol import status from secop.validators import enum, vector, floatrange EVENT_ONLY_ON_CHANGED_VALUES = False # storage for PARAMeter settings: # if readonly is False, the value can be changed (by code, or remote) # if no default is given, the parameter MUST be specified in the configfile # during startup, value is initialized with the default value or # from the config file if specified there class PARAM(object): def __init__(self, description, validator=float, default=Ellipsis, unit=None, readonly=True, export=True): if isinstance(description, PARAM): # make a copy of a PARAM object self.__dict__.update(description.__dict__) return self.description = description self.validator = validator self.default = default self.unit = unit self.readonly = readonly self.export = export # internal caching: value and timestamp of last change... self.value = default self.timestamp = 0 def __repr__(self): return '%s(%s)' % (self.__class__.__name__, ', '.join( ['%s=%r' % (k, v) for k, v in sorted(self.__dict__.items())])) # storage for CMDs settings (description + call signature...) class CMD(object): def __init__(self, description, arguments, result): # descriptive text for humans self.description = description # list of validators for arguments self.arguments = arguments # validator for result self.resulttype = result def __repr__(self): return '%s(%s)' % (self.__class__.__name__, ', '.join( ['%s=%r' % (k, v) for k, v in sorted(self.__dict__.items())])) # Meta class # warning: MAGIC! class DeviceMeta(type): def __new__(mcs, name, bases, attrs): newtype = type.__new__(mcs, name, bases, attrs) if '__constructed__' in attrs: return newtype # merge PARAM and CMDS from all sub-classes for entry in ['PARAMS', 'CMDS']: newentry = {} for base in reversed(bases): if hasattr(base, entry): newentry.update(getattr(base, entry)) newentry.update(attrs.get(entry, {})) setattr(newtype, entry, newentry) # check validity of PARAM entries for pname, pobj in newtype.PARAMS.items(): # XXX: allow dicts for overriding certain aspects only. if not isinstance(pobj, PARAM): raise ProgrammingError('%r: device PARAM %r should be a ' 'PARAM object!' % (name, pname)) # XXX: create getters for the units of params ?? # wrap of reading/writing funcs rfunc = attrs.get('read_' + pname, None) def wrapped_rfunc(self, maxage=0, pname=pname, rfunc=rfunc): if rfunc: value = rfunc(self, maxage) setattr(self, pname, value) return value else: # return cached value return self.PARAMS[pname].value if rfunc: wrapped_rfunc.__doc__ = rfunc.__doc__ setattr(newtype, 'read_' + pname, wrapped_rfunc) if not pobj.readonly: wfunc = attrs.get('write_' + pname, None) def wrapped_wfunc(self, value, pname=pname, wfunc=wfunc): self.log.debug("wfunc: set %s to %r" % (pname, value)) pobj = self.PARAMS[pname] value = pobj.validator(value) if pobj.validator else value if wfunc: value = wfunc(self, value) or value # XXX: use setattr or direct manipulation # of self.PARAMS[pname]? setattr(self, pname, value) return value if wfunc: wrapped_wfunc.__doc__ = wfunc.__doc__ setattr(newtype, 'write_' + pname, wrapped_wfunc) def getter(self, pname=pname): return self.PARAMS[pname].value def setter(self, value, pname=pname): pobj = self.PARAMS[pname] value = pobj.validator(value) if pobj.validator else value pobj.timestamp = time.time() if not EVENT_ONLY_ON_CHANGED_VALUES or (value != pobj.value): pobj.value = value # also send notification self.log.debug('%s is now %r' % (pname, value)) self.DISPATCHER.announce_update(self, pname, pobj) setattr(newtype, pname, property(getter, setter)) # also collect/update information about CMD's setattr(newtype, 'CMDS', getattr(newtype, 'CMDS', {})) for name in attrs: if name.startswith('do'): if name[2:] in newtype.CMDS: continue value = getattr(newtype, name) if isinstance(value, types.MethodType): argspec = inspect.getargspec(value) if argspec[0] and argspec[0][0] == 'self': del argspec[0][0] newtype.CMDS[name[2:]] = CMD(getattr(value, '__doc__'), argspec.args, None) # XXX: find resulttype! attrs['__constructed__'] = True return newtype # Basic device class # # within devices, parameters should only be addressed as self. # i.e. self.value, self.target etc... # these are accesses to the cached version. # they can also be written to # (which auto-calls self.write_ and generate an async update) # if you want to 'update from the hardware', call self.read_ # the return value of this method will be used as the new cached value and # be returned. class Device(object): """Basic Device, doesn't do much""" __metaclass__ = DeviceMeta # PARAMS and CMDS are auto-merged upon subclassing PARAMS = { 'baseclass': PARAM('protocol defined interface class', default="Device", validator=str), } CMDS = {} DISPATCHER = None def __init__(self, logger, cfgdict, devname, dispatcher): # remember the dispatcher object (for the async callbacks) self.DISPATCHER = dispatcher self.log = logger self.name = devname # make local copies of PARAMS params = {} for k, v in self.PARAMS.items(): params[k] = PARAM(v) mycls = self.__class__ myclassname = '%s.%s' % (mycls.__module__, mycls.__name__) params['class'] = PARAM('implementation specific class name', default=myclassname, validator=str) self.PARAMS = params # check config for problems # only accept config items specified in PARAMS for k, v in cfgdict.items(): if k not in self.PARAMS: raise ConfigError('Device %s:config Parameter %r ' 'not unterstood!' % (self.name, k)) # complain if a PARAM entry has no default value and # is not specified in cfgdict for k, v in self.PARAMS.items(): if k not in cfgdict: if v.default is Ellipsis and k != 'value': # Ellipsis is the one single value you can not specify.... raise ConfigError('Device %s: Parameter %r has no default ' 'value and was not given in config!' % (self.name, k)) # assume default value was given cfgdict[k] = v.default # replace CLASS level PARAM objects with INSTANCE level ones self.PARAMS[k] = PARAM(self.PARAMS[k]) # now 'apply' config: # pass values through the validators and store as attributes for k, v in cfgdict.items(): # apply validator, complain if type does not fit validator = self.PARAMS[k].validator if validator is not None: # only check if validator given try: v = validator(v) except ValueError as e: raise ConfigError('Device %s: config parameter %r:\n%r' % (self.name, k, e)) setattr(self, k, v) self._requestLock = threading.RLock() def init(self): # may be overriden in derived classes to init stuff self.log.debug('init()') def _pollThread(self): # may be overriden in derived classes to init stuff self.log.debug('init()') class Readable(Device): """Basic readable device providing the readonly parameter 'value' and 'status' """ PARAMS = { 'baseclass': PARAM('protocol defined interface class', default="Readable", validator=str), 'value': PARAM('current value of the device', readonly=True, default=0.), 'pollinterval': PARAM('sleeptime between polls', readonly=False, default=5, validator=floatrange(1,120),), 'status': PARAM('current status of the device', default=status.OK, validator=enum(**{'idle': status.OK, 'BUSY': status.BUSY, 'WARN': status.WARN, 'UNSTABLE': status.UNSTABLE, 'ERROR': status.ERROR, 'UNKNOWN': status.UNKNOWN}), readonly=True), 'status2': PARAM('current status of the device', default=(status.OK, ''), validator=vector(enum(**{'idle': status.OK, 'BUSY': status.BUSY, 'WARN': status.WARN, 'UNSTABLE': status.UNSTABLE, 'ERROR': status.ERROR, 'UNKNOWN': status.UNKNOWN}), str), readonly=True), } def init(self): Device.init(self) self._pollthread = threading.Thread(target=self._pollThread) self._pollthread.daemon = True self._pollthread.start() def _pollThread(self): while True: time.sleep(self.pollinterval) for pname in self.PARAMS: if pname != 'pollinterval': rfunc = getattr(self, 'read_%s' % pname, None) if rfunc: rfunc() class Driveable(Readable): """Basic Driveable device providing a settable 'target' parameter to those of a Readable """ PARAMS = { 'baseclass': PARAM('protocol defined interface class', default="Driveable", validator=str), 'target': PARAM('target value of the device', default=0., readonly=False), } def doStop(self): time.sleep(1) # for testing !