diff --git a/bin/make_doc.py b/bin/make_doc.py index a75e162..ec44fb9 100755 --- a/bin/make_doc.py +++ b/bin/make_doc.py @@ -40,12 +40,11 @@ for dirpath, dirnames, filenames in os.walk(DOC_SRC): except OSError: pass - for fn in filenames: full_name = path.join(dirpath, fn) sub_name = path.relpath(full_name, DOC_SRC) final_name = path.join(DOC_DST, sub_name) - + if not fn.endswith('md'): # just copy everything else with open(full_name, 'rb') as fi: diff --git a/bin/secop-server b/bin/secop-server index fbb18ea..6fbc96d 100755 --- a/bin/secop-server +++ b/bin/secop-server @@ -67,7 +67,7 @@ def main(argv=None): args = parseArgv(argv[1:]) loglevel = 'debug' if args.verbose else ('error' if args.quiet else 'info') - loggers.initLogging('secop', loglevel, path.join(log_path)) + loggers.initLogging('secop', loglevel, log_path) srv = Server(args.name, basepath) diff --git a/doc/todo.md b/doc/todo.md index e1d06ee..9822715 100644 --- a/doc/todo.md +++ b/doc/todo.md @@ -21,18 +21,18 @@ ## A Server ## - * get daemonizing working - * handle -d (nodaemon) and -D (default, daemonize) cmd line args - * support Async data units - * support feature publishing and selection * rewrite MessageHandler to be agnostic of server - + * move encoding to interface + * allow multiple interfaces per server + * fix error handling an make it consistent ## Device framework ## - * unify PARAMS and CONFIG (if no default value is given, -it needs to be specified in cfgfile, otherwise its optional) * supply properties for PARAMS to auto-generate async data units + * self-polling support + * generic devicethreads + * proxydevice + * make get_device uri-aware ## Testsuite ## @@ -45,7 +45,6 @@ it needs to be specified in cfgfile, otherwise its optional) * mabe use sphinx to generate docu: a pdf can then be auto-generated.... * transfer build docu into wiki via automated jobfile -Problem: wiki does not understand .md or .html diff --git a/etc/demo.cfg b/etc/demo.cfg new file mode 100644 index 0000000..83f95a4 --- /dev/null +++ b/etc/demo.cfg @@ -0,0 +1,39 @@ +[server] +bindto=0.0.0.0 +bindport=10767 +interface = tcp +framing=demo +encoding=demo + +[device heatswitch] +class=devices.demo.Switch +switch_on_time=3 +switch_off_time=5 + +[device mf] +class=devices.demo.MagneticField +heatswitch = heatswitch + +[device ts] +class=devices.demo.SampleTemp +sensor = 'Q1329V7R3' +ramp = 4 +target = 10 +default = 10 + +[device tc1] +class=devices.demo.CoilTemp +sensor="X34598T7" + +[device tc2] +class=devices.demo.CoilTemp +sensor="X39284Q8' + +[device label] +class=devices.demo.Label +system=Cryomagnet MX15 +subdev_mf=mf +subdev_ts=ts + +[device vt] +class=devices.demo.ValidatorTest diff --git a/requirements.txt b/requirements.txt index 8ab1206..a8a3a8a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ markdown>=2.6 # general stuff psutil -python-daemon +python-daemon >=2.0 # for zmq #pyzmq>=13.1.0 diff --git a/src/devices/core.py b/src/devices/core.py index 62d97c7..da9fee9 100644 --- a/src/devices/core.py +++ b/src/devices/core.py @@ -27,45 +27,66 @@ # 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 from errors import ConfigError, ProgrammingError from protocol import status +from validators import mapping +EVENT_ONLY_ON_CHANGED_VALUES = True # storage for PARAMeter settings: -# if readonly is False, the value can be changed (by code, or remte) +# 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, currentvalue is initialized with the default value or -# from the config file +# 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=None, default=Ellipsis, - unit=None, readonly=False, export=True): + + 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... - self.currentvalue = default + # 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.argumenttype = arguments - # validator for results + 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: @@ -81,22 +102,56 @@ class DeviceMeta(type): setattr(newtype, entry, newentry) # check validity of PARAM entries - for pname, info in newtype.PARAMS.items(): - if not isinstance(info, PARAM): + 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: greate getters and setters, setters should send async updates + # XXX: create getters for the units of params ?? + # wrap of reading/writing funcs + rfunc = attrs.get('read_' + pname, None) - def getter(): - return self.PARAMS[pname].currentvalue + 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) - def setter(value): - p = self.PARAMS[pname] - p.currentvalue = p.validator(value) if p.validator else value - # also send notification - self.DISPATCHER.announce_update(self, pname, value) + if not pobj.readonly: + wfunc = attrs.get('write_' + pname, None) - attrs[pname] = property(getter, setter) + def wrapped_wfunc(self, value, pname=pname, wfunc=wfunc): + self.log.debug("setter: set %s to %r" % (pname, 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', {})) @@ -114,6 +169,15 @@ class DeviceMeta(type): # 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 @@ -123,7 +187,7 @@ class Device(object): DISPATCHER = None def __init__(self, logger, cfgdict, devname, dispatcher): - # remember the server object (for the async callbacks) + # remember the dispatcher object (for the async callbacks) self.DISPATCHER = dispatcher self.log = logger self.name = devname @@ -137,13 +201,17 @@ class Device(object): # is not specified in cfgdict for k, v in self.PARAMS.items(): if k not in cfgdict: - if v.default is Ellipsis: + 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(): @@ -156,7 +224,6 @@ class Device(object): except ValueError as e: raise ConfigError('Device %s: config parameter %r:\n%r' % (self.name, k, e)) - # XXX: with or without prefix? setattr(self, k, v) def init(self): @@ -172,15 +239,15 @@ class Readable(Device): PARAMS = { 'value': PARAM('current value of the device', readonly=True, default=0.), 'status': PARAM('current status of the device', default=status.OK, + validator=mapping(**{'idle': status.OK, + 'BUSY': status.BUSY, + 'WARN': status.WARN, + 'UNSTABLE': status.UNSTABLE, + 'ERROR': status.ERROR, + 'UNKNOWN': status.UNKNOWN}), readonly=True), } - def read_value(self, maxage=0): - raise NotImplementedError - - def read_status(self): - return status.OK - class Driveable(Readable): """Basic Driveable device @@ -188,8 +255,6 @@ class Driveable(Readable): providing a settable 'target' parameter to those of a Readable """ PARAMS = { - 'target': PARAM('target value of the device', default=0.), + 'target': PARAM('target value of the device', default=0., + readonly=False), } - - def write_target(self, value): - raise NotImplementedError diff --git a/src/devices/cryo.py b/src/devices/cryo.py index b007cea..10c12b5 100644 --- a/src/devices/cryo.py +++ b/src/devices/cryo.py @@ -146,7 +146,7 @@ class Cryostat(Driveable): def __heatLink(self, coolertemp, sampletemp): """heatflow from sample to cooler. may be negative...""" flow = (sampletemp - coolertemp) * \ - ((coolertemp + sampletemp) ** 2)/400. + ((coolertemp + sampletemp) ** 2) / 400. cp = clamp(self.__coolerCP(coolertemp) * self.__sampleCP(sampletemp), 1, 10) return clamp(flow, -cp, cp) @@ -156,7 +156,7 @@ class Cryostat(Driveable): 12 * temp / ((temp - 12.)**2 + 10) + 0.5 def __sampleLeak(self, temp): - return 0.02/temp + return 0.02 / temp def thread(self): self.sampletemp = self.config_T_start @@ -304,8 +304,8 @@ class Cryostat(Driveable): # obtain min/max deviation = 0 for _, T in window: - if abs(T-self.target) > deviation: - deviation = abs(T-self.target) + if abs(T - self.target) > deviation: + deviation = abs(T - self.target) if (len(window) < 3) or deviation > self.tolerance: self.status = status.BUSY, 'unstable' elif self.setpoint == self.target: diff --git a/src/devices/demo.py b/src/devices/demo.py new file mode 100644 index 0000000..d51742f --- /dev/null +++ b/src/devices/demo.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python +# -*- 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 +# ***************************************************************************** + +"""testing devices""" + +import time +import random +import threading + +from devices.core import Readable, Driveable, PARAM +from validators import * +from protocol import status + + +class Switch(Driveable): + """switch it on or off.... + """ + PARAMS = { + 'value': PARAM('current state (on or off)', + validator=mapping(on=1, off=0), default=0), + 'target': PARAM('wanted state (on or off)', + validator=mapping(on=1, off=0), default=0, + readonly=False), + 'switch_on_time': PARAM('how long to wait after switching the switch on', validator=floatrange(0, 60), unit='s', default=10, export=False), + 'switch_off_time': PARAM('how long to wait after switching the switch off', validator=floatrange(0, 60), unit='s', default=10, export=False), + } + + def init(self): + self._started = 0 + + def read_value(self, maxage=0): + # could ask HW + # we just return the value of the target here. + self._update() + return self.value + + def read_target(self, maxage=0): + # could ask HW + return self.target + + def write_target(self, value): + # could tell HW + pass + # note: setting self.target to the new value is done after this.... + # note: we may also return the read-back value from the hw here + + def read_status(self, maxage=0): + self.log.info("read status") + self._update() + if self.target == self.value: + return status.OK + return status.BUSY + + def _update(self): + started = self.PARAMS['target'].timestamp + if self.target > self.value: + if time.time() > started + self.switch_on_time: + self.log.debug('is switched ON') + self.value = self.target + elif self.target < self.value: + if time.time() > started + self.switch_off_time: + self.log.debug('is switched OFF') + self.value = self.target + + +class MagneticField(Driveable): + """a liquid magnet + """ + PARAMS = { + 'value': PARAM('current field in T', unit='T', + validator=floatrange(-15, 15), default=0), + 'ramp': PARAM('moving speed in T/min', unit='T/min', + validator=floatrange(0, 1), default=0.1, readonly=False), + 'mode': PARAM('what to do after changing field', default=0, + validator=mapping(persistent=1, hold=0), readonly=False), + 'heatswitch': PARAM('heat switch device', + validator=str, export=False), + } + + def init(self): + self._state = 'idle' + self._heatswitch = self.DISPATCHER.get_device(self.heatswitch) + _thread = threading.Thread(target=self._thread) + _thread.daemon = True + _thread.start() + + def read_value(self, maxage=0): + return self.value + + def write_target(self, value): + # could tell HW + return round(value, 2) + # note: setting self.target to the new value is done after this.... + # note: we may also return the read-back value from the hw here + + def read_status(self, maxage=0): + return status.OK if self._state == 'idle' else status.BUSY + + def _thread(self): + loopdelay = 1 + while True: + ts = time.time() + if self._state == 'idle': + if self.target != self.value: + self.log.debug('got new target -> switching heater on') + self._state = 'switch_on' + self._heatswitch.write_target('on') + if self._state == 'switch_on': + # wait until switch is on + if self._heatswitch.read_value() == 'on': + self.log.debug( + 'heatswitch is on -> ramp to %.3f' % + self.target) + self._state = 'ramp' + if self._state == 'ramp': + if self.target == self.value: + self.log.debug('at field! mode is %r' % self.mode) + if self.mode: + self.log.debug('at field -> switching heater off') + self._state = 'switch_off' + self._heatswitch.write_target('off') + else: + self.log.debug('at field -> hold') + self._state = 'idle' + self.status = self.read_status() # push async + else: + step = self.ramp * loopdelay / 60. + step = max(min(self.target - self.value, step), -step) + self.value += step + if self._state == 'switch_off': + # wait until switch is off + if self._heatswitch.read_value() == 'off': + self.log.debug('heatswitch is off at %.3f' % self.value) + self._state = 'idle' + self.read_status() # update async + time.sleep(max(0.01, ts + loopdelay - time.time())) + self.log.error(self, 'main thread exited unexpectedly!') + + +class CoilTemp(Readable): + """a coil temperature + """ + PARAMS = { + 'value': PARAM('Coil temperatur in K', unit='K', + validator=float, default=0), + 'sensor': PARAM("Sensor number or calibration id", + validator=str, readonly=True), + } + + def read_value(self, maxage=0): + return round(2.3 + random.random(), 3) + + +class SampleTemp(Driveable): + """a sample temperature + """ + PARAMS = { + 'value': PARAM('Sample temperatur in K', unit='K', + validator=float, default=10), + 'sensor': PARAM("Sensor number or calibration id", + validator=str, readonly=True), + 'ramp': PARAM('moving speed in K/min', + validator=floatrange(0, 100), unit='K/min', default=0.1, readonly=False), + } + + def init(self): + _thread = threading.Thread(target=self._thread) + _thread.daemon = True + _thread.start() + + def write_target(self, value): + # could tell HW + return round(value, 2) + # note: setting self.target to the new value is done after this.... + # note: we may also return the read-back value from the hw here + + def _thread(self): + loopdelay = 1 + while True: + ts = time.time() + if self.value == self.target: + if self.status != status.OK: + self.status = status.OK + else: + self.status = status.BUSY + step = self.ramp * loopdelay / 60. + step = max(min(self.target - self.value, step), -step) + self.value += step + time.sleep(max(0.01, ts + loopdelay - time.time())) + self.log.error(self, 'main thread exited unexpectedly!') + + +class Label(Readable): + """ + + """ + PARAMS = { + 'system': PARAM("Name of the magnet system", + validator=str, export=False), + 'subdev_mf': PARAM("name of subdevice for magnet status", + validator=str, export=False), + 'subdev_ts': PARAM("name of subdevice for sample temp", + validator=str, export=False), + 'value': PARAM("Value of out label string", + validator=str) + } + + def read_value(self, maxage=0): + strings = [self.system] + + dev_ts = self.DISPATCHER.get_device(self.subdev_ts) + if dev_ts: + strings.append('at %.3f %s' % + (dev_ts.read_value(), dev_ts.PARAMS['value'].unit)) + else: + strings.append('No connection to sample temp!') + + dev_mf = self.DISPATCHER.get_device(self.subdev_mf) + if dev_mf: + mf_stat = dev_mf.read_status() + mf_mode = dev_mf.mode + mf_val = dev_mf.value + mf_unit = dev_mf.PARAMS['value'].unit + if mf_stat == status.OK: + state = 'Persistent' if mf_mode else 'Non-persistent' + else: + state = 'ramping' + strings.append('%s at %.1f %s' % (state, mf_val, mf_unit)) + else: + strings.append('No connection to magnetic field!') + + return '; '.join(strings) + + +class ValidatorTest(Readable): + """ + """ + PARAMS = { + 'oneof': PARAM('oneof', validator=oneof(int, 'X', 2.718), readonly=False, default=4.0), + 'mapping': PARAM('mapping', validator=mapping('boo', 'faar', z=9), readonly=False, default=1), + 'vector': PARAM('vector of int, float and str', validator=vector(int, float, str), readonly=False, default=(1, 2.3, 'a')), + 'array': PARAM('array: 2..3 time oneof(0,1)', validator=array(oneof(2, 3), oneof(0, 1)), readonly=False, default=[1, 0, 1]), + 'nonnegative': PARAM('nonnegative', validator=nonnegative(), readonly=False, default=0), + 'positive': PARAM('positive', validator=positive(), readonly=False, default=1), + 'intrange': PARAM('intrange', validator=intrange(2, 9), readonly=False, default=4), + 'floatrange': PARAM('floatrange', validator=floatrange(-1, 1), readonly=False, default=0,), + } diff --git a/src/devices/test.py b/src/devices/test.py index 613c13e..15bb4c6 100644 --- a/src/devices/test.py +++ b/src/devices/test.py @@ -28,14 +28,16 @@ from validators import floatrange from epics import PV + class LN2(Readable): """Just a readable. class name indicates it to be a sensor for LN2, but the implementation may do anything """ + def read_value(self, maxage=0): - return round(100*random.random(), 1) + return round(100 * random.random(), 1) class Heater(Driveable): @@ -46,11 +48,11 @@ class Heater(Driveable): """ PARAMS = { 'maxheaterpower': PARAM('maximum allowed heater power', - validator=floatrange(0, 100), unit='W'), + validator=floatrange(0, 100), unit='W'), } def read_value(self, maxage=0): - return round(100*random.random(), 1) + return round(100 * random.random(), 1) def write_target(self, target): pass @@ -64,23 +66,22 @@ class Temp(Driveable): """ PARAMS = { 'sensor': PARAM("Sensor number or calibration id", - validator=str, readonly=True), + validator=str, readonly=True), } def read_value(self, maxage=0): - return round(100*random.random(), 1) + return round(100 * random.random(), 1) def write_target(self, target): pass - class EPICS_PV(Driveable): """pyepics test device.""" PARAMS = { 'sensor': PARAM("Sensor number or calibration id", - validator=str, readonly=True), + validator=str, readonly=True), 'max_rpm': PARAM("Maximum allowed rpm", validator=str, readonly=True), } diff --git a/src/loggers/__init__.py b/src/loggers/__init__.py index e0f1d7c..93f4905 100644 --- a/src/loggers/__init__.py +++ b/src/loggers/__init__.py @@ -35,14 +35,13 @@ from logging import Logger, Formatter, Handler, DEBUG, INFO, WARNING, ERROR, \ from . import colors - LOGFMT = '%(asctime)s : %(levelname)-7s : %(name)-15s: %(message)s' DATEFMT = '%H:%M:%S' DATESTAMP_FMT = '%Y-%m-%d' SECONDS_PER_DAY = 60 * 60 * 24 LOGLEVELS = {'debug': DEBUG, 'info': INFO, 'warning': WARNING, 'error': ERROR} -INVLOGLEVELS = {value : key for key, value in LOGLEVELS.items()} +INVLOGLEVELS = {value: key for key, value in LOGLEVELS.items()} log = None @@ -58,7 +57,10 @@ def initLogging(rootname='secop', rootlevel='info', logdir='/tmp/log'): log.addHandler(ColoredConsoleHandler()) # logfile for fg and bg process - log.addHandler(LogfileHandler(logdir, rootname)) + if logdir.startswith('/var/log'): + log.addHandler(LogfileHandler(logdir, rootname)) + else: + log.addHandler(LogfileHandler(logdir, '')) def getLogger(name, subdir=False): @@ -73,7 +75,6 @@ class SecopLogger(Logger): Logger.__init__(self, *args, **kwargs) SecopLogger._storeLoggerNameLength(self) - def getChild(self, suffix, ownDir=False): child = Logger.getChild(self, suffix) child.setLevel(self.getEffectiveLevel()) @@ -111,7 +112,6 @@ class SecopLogger(Logger): SecopLogger.maxLogNameLength = len(logObj.name) - class ConsoleFormatter(Formatter): """ A lightweight formatter for the interactive console, with optional diff --git a/src/loggers/colors.py b/src/loggers/colors.py index 2a397d7..5f1955c 100644 --- a/src/loggers/colors.py +++ b/src/loggers/colors.py @@ -27,12 +27,12 @@ _codes = {} _attrs = { - 'reset': '39;49;00m', - 'bold': '01m', - 'faint': '02m', - 'standout': '03m', + 'reset': '39;49;00m', + 'bold': '01m', + 'faint': '02m', + 'standout': '03m', 'underline': '04m', - 'blink': '05m', + 'blink': '05m', } for _name, _value in _attrs.items(): diff --git a/src/protocol/device.py b/src/protocol/device.py index 4884184..c5d3deb 100644 --- a/src/protocol/device.py +++ b/src/protocol/device.py @@ -70,6 +70,7 @@ class Driveable(Writeable): """A Moveable which may take a while to reach its target, hence stopping it may be desired""" + def do_stop(self): raise NotImplementedError('A Driveable MUST implement the STOP() ' 'command') diff --git a/src/protocol/dispatcher.py b/src/protocol/dispatcher.py index 3450cff..edf05d8 100644 --- a/src/protocol/dispatcher.py +++ b/src/protocol/dispatcher.py @@ -37,17 +37,19 @@ Interface to the devices: - remove_device(devname_or_obj): removes the device (during shutdown) internal stuff which may be called - - get_devices(): return a list of devices + descriptive data as dict - - get_device_params(): + - list_devices(): return a list of devices + descriptive data as dict + - list_device_params(): return a list of paramnames for this device + descriptive data """ +import time import threading from messages import * class Dispatcher(object): + def __init__(self, logger, options): self.log = logger # XXX: move framing and encoding to interface! @@ -73,7 +75,12 @@ class Dispatcher(object): with self._dispatcher_lock: # de-frame data frames = self.framing.decode(data) + if frames is None: + # not enough data (yet) -> return and come back with more + return None self.log.debug('Dispatcher: frames=%r' % frames) + if not frames: + conn.queue_reply(self._format_reply(HelpReply())) for frame in frames: reply = None # decode frame @@ -82,45 +89,53 @@ class Dispatcher(object): # act upon requestobj msgtype = msg.TYPE msgname = msg.NAME - msgargs = msg # generate reply (coded and framed) if msgtype != 'request': - reply = ProtocolErrorReply(msg) + reply = ProtocolError(msg) else: self.log.debug('Looking for handle_%s' % msgname) handler = getattr(self, 'handle_%s' % msgname, None) if handler: - reply = handler(msgargs) + reply = handler(conn, msg) else: self.log.debug('Can not handle msg %r' % msg) - reply = self.unhandled(msgname, msgargs) + reply = self.unhandled(msgname, msg) if reply: conn.queue_reply(self._format_reply(reply)) - # queue reply viy conn.queue_reply(data) + # queue reply via conn.queue_reply(data) def _format_reply(self, reply): + self.log.debug('formatting reply %r' % reply) msg = self.encoding.encode(reply) + self.log.debug('encoded is %r' % msg) frame = self.framing.encode(msg) + self.log.debug('frame is %r' % frame) return frame - def announce_update(self, device, pname, value): + def announce_update(self, devobj, pname, pobj): """called by devices param setters to notify subscribers of new values """ - eventname = '%s/%s' % (self.get_device(device).name, pname) + devname = devobj.name + eventname = '%s/%s' % (devname, pname) subscriber = self._dispatcher_subscriptions.get(eventname, None) if subscriber: - reply = AsyncDataUnit(device=self.get_device(device).name, - param=pname, - value=str(value), - timestamp=time.time(), + reply = AsyncDataUnit(devname=devname, + pname=pname, + value=str(pobj.value), + timestamp=pobj.timestamp, ) data = self._format_reply(reply) for conn in subscriber: conn.queue_async_reply(data) - def subscribe(self, conn, device, pname): - eventname = '%s/%s' % (self.get_device(device).name, pname) - self._dispatcher_subscriptions.getdefault(eventname, set()).add(conn) + def subscribe(self, conn, devname, pname): + eventname = '%s/%s' % (devname, pname) + self._dispatcher_subscriptions.setdefault(eventname, set()).add(conn) + + def unsubscribe(self, conn, devname, pname): + eventname = '%s/%s' % (devname, pname) + if eventname in self._dispatcher_subscriptions: + self._dispatcher_subscriptions.remove(conn) def add_connection(self, conn): """registers new connection""" @@ -133,6 +148,8 @@ class Dispatcher(object): # XXX: also clean _dispatcher_subscriptions ! def register_device(self, devobj, devname, export=True): + self.log.debug('registering Device %r as %s (export=%r)' % + (devobj, devname, export)) self._dispatcher_devices[devname] = devobj if export: self._dispatcher_export.append(devname) @@ -171,100 +188,295 @@ class Dispatcher(object): return dn, dd def list_device_params(self, devname): + self.log.debug('list_device_params(%r)' % devname) if devname in self._dispatcher_export: # XXX: omit export=False params! - return self.get_device(devname).PARAMS + res = {} + for paramname, param in self.get_device(devname).PARAMS.items(): + if param.export == True: + res[paramname] = param + self.log.debug('list params for device %s -> %r' % + (devname, res)) + return res + self.log.debug('-> device is not to be exported!') return {} + # demo stuff + def _setDeviceValue(self, devobj, value): + # set the device value. return readback value + # if return == None -> Ellispis (readonly!) + if self._getDeviceParam(devobj, 'target') != Ellipsis: + return self._setDeviceParam(devobj, 'target', value) + return Ellipsis + + def _getDeviceValue(self, devobj): + # get the device value + # if return == None -> Ellipsis + return self._getDeviceParam(devobj, 'value') + + def _setDeviceParam(self, devobj, pname, value): + # set the device param. return readback value + # if return == None -> Ellipsis (readonly!) + pobj = devobj.PARAMS.get(pname, Ellipsis) + if pobj == Ellipsis: + return pobj + if pobj.readonly: + return self._getDeviceParam(devobj, pname) + writefunc = getattr(devobj, 'write_%s' % pname, None) + validator = pobj.validator + value = validator(value) + + if writefunc: + value = writefunc(value) or value + else: + setattr(devobj, pname, value) + + return self._getDeviceParam(devobj, pname) + + def _getDeviceParam(self, devobj, pname): + # get the device value + # if return == None -> Ellipsis + readfunc = getattr(devobj, 'read_%s' % pname, None) + if readfunc: + # should also update the pobj (via the setter from the metaclass) + readfunc() + pobj = devobj.PARAMS.get(pname, None) + if pobj: + return (pobj.value, pobj.timestamp) + return getattr(devobj, pname, Ellipsis) + + def handle_Demo(self, conn, msg): + novalue = msg.novalue + devname = msg.devname + paramname = msg.paramname + propname = msg.propname + assign = msg.assign + + res = [] + if novalue in ('+', '-'): + # XXX: handling of subscriptions: propname is ignored + if devname is None: + # list all subscriptions for this connection + for evname, conns in self._dispatcher_subscriptions.items(): + if conn in conns: + res.append('+%s:%s' % evname.split('/')) + devices = self._dispatcher_export if devname == '*' else [devname] + for devname in devices: + devobj = self.get_device(devname) + if devname != '*' and devobj is None: + return NoSuchDeviceError(devname) + if paramname is None: + pnames = ['value', 'status'] + elif paramname == '*': + pnames = devobj.PARAMS.keys() + else: + pnames = [paramname] + for pname in pnames: + pobj = devobj.PARAMS.get(pname, None) + if pobj and not pobj.export: + continue + if paramname != '*' and pobj is None: + return NoSuchParamError(devname, paramname) + + if novalue == '+': + # subscribe + self.subscribe(conn, devname, pname) + res.append('+%s:%s' % (devname, pname)) + elif novalue == '-': + # unsubscribe + self.unsubscribe(conn, devname, pname) + res.append('-%s:%s' % (devname, pname)) + return DemoReply(res) + + if devname is None: + return Error('no devname given!') + devices = self._dispatcher_export if devname == '*' else [devname] + for devname in devices: + devobj = self.get_device(devname) + if devname != '*' and devobj is None: + return NoSuchDeviceError(devname) + if paramname is None: + # Access Devices + val = self._setDeviceValue( + devobj, assign) if assign else self._getDeviceValue(devobj) + if val == Ellipsis: + if assign: + return ParamReadonlyError(devname, 'target') + return NoSuchDevice(devname) + formatfunc = lambda x: '' if novalue else ('=%r;t=%r' % x) + res.append(devname + formatfunc(val)) + + else: + pnames = devobj.PARAMS.keys( + ) if paramname == '*' else [paramname] + for pname in pnames: + pobj = devobj.PARAMS.get(pname, None) + if pobj and not pobj.export: + continue + if paramname != '*' and pobj is None: + return NoSuchParamError(devname, paramname) + if propname is None: + # access params + callfunc = lambda x, y: self._setDeviceParam(x, y, assign) \ + if assign else self._getDeviceParam(x, y) + formatfunc = lambda x: '' if novalue else ( + '=%r;t=%r' % x) + try: + res.append(('%s:%s' % (devname, pname)) + + formatfunc(callfunc(devobj, pname))) + except TypeError as e: + return InternalError(e) + else: + props = pobj.__dict__.keys( + ) if propname == '*' else [propname] + for prop in props: + # read props + try: + if novalue: + res.append( + '%s:%s:%s' % + (devname, pname, prop)) + else: + res.append( + '%s:%s:%s=%r' % + (devname, pname, prop, getattr( + pobj, prop))) + except TypeError as e: + return InternalError(e) + + # now clean responce a little + res = [ + e.replace( + '/v=', + '=') for e in sorted( + (e.replace( + ':value=', + '/v=') for e in res))] + return DemoReply(res) + # now the (defined) handlers for the different requests - def handle_Help(self, msg): + def handle_Help(self, conn, msg): return HelpReply() - def handle_ListDevices(self, msgargs): + def handle_ListDevices(self, conn, msg): + # XXX: What about the descriptive data???? # XXX: choose! - #return ListDevicesReply(self.list_device_names()) - return ListDevicesReply(*self.list_devices()) + return ListDevicesReply(self.list_device_names()) + # return ListDevicesReply(*self.list_devices()) - def handle_ListDeviceParams(self, msgargs): - devobj = self.get_device(msgargs.device) - if devobj: - return ListDeviceParamsReply(msgargs.device, - self.get_device_params(devobj)) + def handle_ListDeviceParams(self, conn, msg): + # reply with a list of the parameter names for a given device + self.log.error('Keep: ListDeviceParams') + if msg.device in self._dispatcher_export: + params = self.list_device_params(msg.device) + return ListDeviceParamsReply(msg.device, params.keys()) else: - return NoSuchDeviceErrorReply(msgargs.device) + return NoSuchDeviceError(msg.device) - def handle_ReadValue(self, msgargs): - devobj = self.get_device(msgargs.device) - if devobj: - return ReadValueReply(msgargs.device, devobj.read_value(), + def handle_ReadAllDevices(self, conn, msg): + # reply with a bunch of ReadValueReplies, reading ALL devices + result = [] + for devname in sorted(self.list_device_names()): + devobj = self.get_device(devname) + value = self._getdeviceValue(devobj) + if value is not Ellipsis: + result.append(ReadValueReply(devname, value, + timestamp=time.time())) + return ReadAllDevicesReply(readValueReplies=result) + + def handle_ReadValue(self, conn, msg): + devname = msg.device + devobj = self.get_device(devname) + if devobj is None: + return NoSuchDeviceError(devname) + + value = self._getdeviceValue(devname) + if value is not Ellipsis: + return ReadValueReply(devname, value, timestamp=time.time()) - else: - return NoSuchDeviceErrorReply(msgargs.device) - def handle_ReadParam(self, msgargs): - devobj = self.get_device(msgargs.device) - if devobj: - readfunc = getattr(devobj, 'read_%s' % msgargs.param, None) - if readfunc: - return ReadParamReply(msgargs.device, msgargs.param, - readfunc(), timestamp=time.time()) - else: - return NoSuchParamErrorReply(msgargs.device, msgargs.param) - else: - return NoSuchDeviceErrorReply(msgargs.device) + return InternalError('undefined device value') - def handle_WriteParam(self, msgargs): - devobj = self.get_device(msgargs.device) - if devobj: - writefunc = getattr(devobj, 'write_%s' % msgargs.param, None) - if writefunc: - readbackvalue = writefunc(msgargs.value) or msgargs.value - # trigger async updates - setattr(devobj, msgargs.param, readbackvalue) - return WriteParamReply(msgargs.device, msgargs.param, - readbackvalue, - timestamp=time.time()) - else: - if getattr(devobj, 'read_%s' % msgargs.param, None): - return ParamReadonlyErrorReply(msgargs.device, - msgargs.param) - else: - return NoSuchParamErrorReply(msgargs.device, - msgargs.param) - else: - return NoSuchDeviceErrorReply(msgargs.device) + def handle_WriteValue(self, conn, msg): + value = msg.value + devname = msg.device + devobj = self.get_device(devname) + if devobj is None: + return NoSuchDeviceError(devname) - def handle_RequestAsyncData(self, msgargs): - return ErrorReply('AsyncData is not (yet) supported') + pobj = getattr(devobj.PARAMS, 'target', None) + if pobj is None: + return NoSuchParamError(devname, 'target') - def handle_ListOfFeatures(self, msgargs): + if pobj.readonly: + return ParamReadonlyError(devname, 'target') + + validator = pobj.validator + try: + value = validator(value) + except Exception as e: + return InvalidParamValueError(devname, 'target', value, e) + + value = self._setDeviceValue(devobj, value) or value + WriteValueReply(devname, value, timestamp=time.time()) + + def handle_ReadParam(self, conn, msg): + devname = msg.device + pname = msg.param + devobj = self.get_device(devname) + if devobj is None: + return NoSuchDeviceError(devname) + + pobj = getattr(devobj.PARAMS, pname, None) + if pobj is None: + return NoSuchParamError(devname, pname) + + value = self._getdeviceParam(devobj, pname) + if value is not Ellipsis: + return ReadParamReply(devname, pname, value, + timestamp=time.time()) + + return InternalError('undefined device value') + + def handle_WriteParam(self, conn, msg): + value = msg.value + pname = msg.param + devname = msg.device + devobj = self.get_device(devname) + if devobj is None: + return NoSuchDeviceError(devname) + + pobj = getattr(devobj.PARAMS, pname, None) + if pobj is None: + return NoSuchParamError(devname, pname) + + if pobj.readonly: + return ParamReadonlyError(devname, pname) + + validator = pobj.validator + try: + value = validator(value) + except Exception as e: + return InvalidParamValueError(devname, pname, value, e) + + value = self._setDeviceParam(devobj, pname, value) or value + WriteParamReply(devname, pname, value, timestamp=time.time()) + + # XXX: !!! + def handle_RequestAsyncData(self, conn, msg): + return Error('AsyncData is not (yet) supported') + + def handle_ListOfFeatures(self, conn, msg): # no features supported (yet) return ListOfFeaturesReply([]) - def handle_ActivateFeature(self, msgargs): - return ErrorReply('Features are not (yet) supported') + def handle_ActivateFeature(self, conn, msg): + return Error('Features are not (yet) supported') - def unhandled(self, msgname, msgargs): + def unhandled(self, msgname, conn, msg): """handler for unhandled Messages (no handle_ method was defined) """ self.log.error('IGN: got unhandled request %s' % msgname) - return ErrorReply('Got Unhandled Request') - - def parse_message(self, message): - # parses a message and returns - # msgtype, msgname and parameters of message (as dict) - msgtype = 'unknown' - msgname = 'unknown' - if isinstance(message, ErrorReply): - msgtype = message.TYPE - msgname = message.__class__.__name__[:-len('Reply')] - elif isinstance(message, Request): - msgtype = message.TYPE - msgname = message.__class__.__name__[:-len('Request')] - elif isinstance(message, Reply): - msgtype = message.TYPE - msgname = message.__class__.__name__[:-len('Reply')] - return msgtype, msgname, \ - attrdict([(k, getattr(message, k)) for k in message.ARGS]) + return Error('Got Unhandled Request') diff --git a/src/protocol/interface/tcp.py b/src/protocol/interface/tcp.py index c6393af..6056b6b 100644 --- a/src/protocol/interface/tcp.py +++ b/src/protocol/interface/tcp.py @@ -32,6 +32,7 @@ MAX_MESSAGE_SIZE = 1024 class TCPRequestHandler(SocketServer.BaseRequestHandler): + def setup(self): self.log = self.server.log self._queue = collections.deque(maxlen=100) diff --git a/src/protocol/messages.py b/src/protocol/messages.py index 8b148b8..e7ffa76 100644 --- a/src/protocol/messages.py +++ b/src/protocol/messages.py @@ -48,12 +48,19 @@ class Message(object): 'argument %r' % k) names.remove(k) self.__dict__[k] = v - if names: - raise TypeError('__init__() takes at least %d arguments (%d given)' - % len(self.ARGS), len(args)+len(kwds)) + for name in names: + self.__dict__[name] = None +# if names: +# raise TypeError('__init__() takes at least %d arguments (%d given)' +# % (len(self.ARGS), len(args)+len(kwds))) self.NAME = (self.__class__.__name__[:-len(self.TYPE)] or self.__class__.__name__) + def __repr__(self): + return self.__class__.__name__ + '(' + \ + ', '.join('%s=%r' % (k, getattr(self, k)) + for k in self.ARGS if getattr(self, k) is not None) + ')' + class Request(Message): TYPE = REQUEST @@ -66,6 +73,16 @@ class Reply(Message): class ErrorReply(Message): TYPE = ERROR +# for DEMO + + +class DemoRequest(Request): + ARGS = ['novalue', 'devname', 'paramname', 'propname', 'assign'] + + +class DemoReply(Reply): + ARGS = ['lines'] + # actuall message objects class ListDevicesRequest(Request): @@ -92,6 +109,30 @@ class ReadValueReply(Reply): ARGS = ['device', 'value', 'timestamp', 'error', 'unit'] +class WriteValueRequest(Request): + ARGS = ['device', 'value', 'unit'] # unit??? + + +class WriteValueReply(Reply): + ARGS = ['device', 'value', 'timestamp', 'error', 'unit'] + + +class ReadAllDevicesRequest(Request): + ARGS = ['maxage'] + + +class ReadAllDevicesReply(Reply): + ARGS = ['readValueReplies'] + + +class ListParamPropsRequest(Request): + ARGS = ['device', 'param'] + + +class ListParamPropsReply(Request): + ARGS = ['device', 'param', 'props'] + + class ReadParamRequest(Request): ARGS = ['device', 'param', 'maxage'] @@ -117,7 +158,7 @@ class RequestAsyncDataReply(Reply): class AsyncDataUnit(ReadParamReply): - ARGS = ['device', 'param', 'value', 'timestamp', 'error', 'unit'] + ARGS = ['devname', 'pname', 'value', 'timestamp', 'error', 'unit'] class ListOfFeaturesRequest(Request): @@ -138,40 +179,47 @@ class ActivateFeatureReply(Reply): pass -class ProtocollError(ErrorReply): - ARGS = ['msgtype', 'msgname', 'msgargs'] - +# ERRORS +######## class ErrorReply(Reply): ARGS = ['error'] -class NoSuchDeviceErrorReply(ErrorReply): +class InternalError(ErrorReply): + ARGS = ['error'] + + +class ProtocollError(ErrorReply): + ARGS = ['error'] + + +class NoSuchDeviceError(ErrorReply): ARGS = ['device'] -class NoSuchParamErrorReply(ErrorReply): +class NoSuchParamError(ErrorReply): ARGS = ['device', 'param'] -class ParamReadonlyErrorReply(ErrorReply): +class ParamReadonlyError(ErrorReply): ARGS = ['device', 'param'] -class UnsupportedFeatureErrorReply(ErrorReply): +class UnsupportedFeatureError(ErrorReply): ARGS = ['feature'] -class NoSuchCommandErrorReply(ErrorReply): +class NoSuchCommandError(ErrorReply): ARGS = ['device', 'command'] -class CommandFailedErrorReply(ErrorReply): +class CommandFailedError(ErrorReply): ARGS = ['device', 'command'] -class InvalidParamValueErrorReply(ErrorReply): - ARGS = ['device', 'param', 'value'] +class InvalidParamValueError(ErrorReply): + ARGS = ['device', 'param', 'value', 'error'] # Fun! diff --git a/src/protocol/status.py b/src/protocol/status.py index e0bc323..10fe54b 100644 --- a/src/protocol/status.py +++ b/src/protocol/status.py @@ -28,3 +28,10 @@ WARN = 300 UNSTABLE = 350 ERROR = 400 UNKNOWN = -1 + +#OK = 'idle' +#BUSY = 'busy' +#WARN = 'alarm' +#UNSTABLE = 'unstable' +#ERROR = 'ERROR' +#UNKNOWN = 'unknown' diff --git a/src/protocol/transport/encoding.py b/src/protocol/transport/encoding.py index ab28113..e2f1a50 100644 --- a/src/protocol/transport/encoding.py +++ b/src/protocol/transport/encoding.py @@ -61,6 +61,7 @@ class PickleEncoder(MessageEncoder): class TextEncoder(MessageEncoder): + def __init__(self): # build safe namespace ns = dict() @@ -94,9 +95,109 @@ class TextEncoder(MessageEncoder): return messages.HelpRequest() +def format_time(ts): + return float(ts) # XXX: switch to iso! + +import re + +DEMO_RE = re.compile( + r'^([!+-])?(\*|[a-z_][a-z_0-9]*)?(?:\:(\*|[a-z_][a-z_0-9]*))?(?:\:(\*|[a-z_][a-z_0-9]*))?(?:\=(.*))?') + + +def parse_str(s): + # QnD Hack! try to parse lists/tuples/ints/floats, ignore dicts, specials + # XXX: replace by proper parsing. use ast? + s = s.strip() + if s.startswith('[') and s.endswith(']'): + # evaluate inner + return [parse_str(part) for part in s[1:-1].split(',')] + if s.startswith('(') and s.endswith(')'): + # evaluate inner + return [parse_str(part) for part in s[1:-1].split(',')] + if s.startswith('"') and s.endswith('"'): + # evaluate inner + return s[1:-1] + if s.startswith("'") and s.endswith("'"): + # evaluate inner + return s[1:-1] + for conv in (int, float, lambda x: x): + try: + return conv(s) + except ValueError: + pass + + +class DemoEncoder(MessageEncoder): + + def decode(sef, encoded): + # match [!][*|devicename][: *|paramname [: *|propname]] [=value] + match = DEMO_RE.match(encoded) + if match: + novalue, devname, pname, propname, assign = match.groups() + if assign: + print "parsing", assign, + assign = parse_str(assign) + print "->", assign + return messages.DemoRequest( + novalue, devname, pname, propname, assign) + return messages.HelpRequest() + + def encode(self, msg): + if isinstance(msg, messages.DemoReply): + return msg.lines + handler_name = '_encode_' + msg.__class__.__name__ + handler = getattr(self, handler_name, None) + if handler is None: + print "Handler %s not yet implemented!" % handler_name + try: + args = dict((k, msg.__dict__[k]) for k in msg.ARGS) + result = handler(**args) + except Exception as e: + print "Error encoding %r with %r!" % (msg, handler) + print e + return '~InternalError~' + return result + + def _encode_AsyncDataUnit(self, devname, pname, value, timestamp, + error=None, unit=''): + return '#%s:%s=%s;t=%.3f' % (devname, pname, value, timestamp) + + def _encode_Error(self, error): + return '~Error~ %r' % error + + def _encode_InternalError(self, error): + return '~InternalError~ %r' % error + + def _encode_ProtocollError(self, msgtype, msgname, msgargs): + return '~ProtocolError~ %s.%s.%r' % (msgtype, msgname, msgargs) + + def _encode_NoSuchDeviceError(self, device): + return '~NoSuchDeviceError~ %s' % device + + def _encode_NoSuchParamError(self, device, param): + return '~NoSuchParameterError~ %s:%s' % (device, param) + + def _encode_ParamReadonlyError(self, device, param): + return '~ParamReadOnlyError~ %s:%s' % (device, param) + + def _encode_NoSuchCommandError(self, device, command): + return '~NoSuchCommandError~ %s.%s' % (device, command) + + def _encode_CommandFailedError(self, device, command): + return '~CommandFailedError~ %s.%s' % (device, command) + + def _encode_InvalidParamValueError(self, device, param, value): + return '~InvalidValueForParamError~ %s:%s=%r' % (device, param, value) + + def _encode_HelpReply(self): + return ['Help not yet implemented!', + 'ask Markus Zolliker about the protocol'] + + ENCODERS = { 'pickle': PickleEncoder, 'text': TextEncoder, + 'demo': DemoEncoder, } diff --git a/src/protocol/transport/framing.py b/src/protocol/transport/framing.py index bfafc98..fa2f6fa 100644 --- a/src/protocol/transport/framing.py +++ b/src/protocol/transport/framing.py @@ -32,6 +32,7 @@ class Framer(object): note: not all MessageEncoders can use all Framers, but the intention is to have this for as many as possible. """ + def encode(self, *frames): """return the wire-data for the given messageframes""" raise NotImplemented @@ -126,9 +127,67 @@ class RLEFramer(Framer): self.frames_to_go = 0 +class DemoFramer(Framer): + """Text based message framer + + frmes are delimited by '\n' + messages are delimited by '\n\n' + '\r' is ignored + """ + + def __init__(self): + self.data = b'' + self.decoded = [] + + def encode(self, frames): + """add transport layer encapsulation/framing of messages""" + if isinstance(frames, (tuple, list)): + return b'\n'.join(frames) + b'\n\n' + return b'%s\n\n' % frames + + def decode(self, data): + """remove transport layer encapsulation/framing of messages + + returns a list of messageframes which got decoded from data! + """ + self.data += data + res = [] + while b'\n' in self.data: + frame, self.data = self.data.split(b'\n', 1) + if frame.endswith('\r'): + frame = frame[:-1] + if self.data.startswith('\r'): + self.data = self.data[1:] + res.append(frame) + return res + + def decode2(self, data): + """remove transport layer encapsulation/framing of messages + + returns a _list_ of messageframes which got decoded from data! + """ + self.data += data.replace(b'\r', '') + while b'\n' in self.data: + frame, self.data = self.data.split(b'\n', 1) + if frame: + # not an empty line -> belongs to this set of messages + self.decoded.append(frame) + else: + # empty line -> our set of messages is finished decoding + res = self.decoded + self.decoded = [] + return res + return None + + def reset(self): + self.data = b'' + self.decoded = [] + + FRAMERS = { 'eol': EOLFramer, 'rle': RLEFramer, + 'demo': DemoFramer, } __ALL__ = ['FRAMERS'] diff --git a/src/server.py b/src/server.py index 8c1eca3..708473f 100644 --- a/src/server.py +++ b/src/server.py @@ -38,6 +38,7 @@ from errors import ConfigError class Server(object): + def __init__(self, name, workdir, parentLogger=None): self._name = name self._workdir = workdir @@ -115,14 +116,23 @@ class Server(object): devclass = devopts.pop('class') # create device self.log.debug('Creating Device %r' % devname) + export = devopts.pop('export', '1') + export = export.lower() in ('1', 'on', 'true', 'yes') + if 'default' in devopts: + devopts['value'] = devopts.pop('default') + # strip '" + for k, v in devopts.items(): + for d in ("'", '"'): + if v.startswith(d) and v.endswith(d): + devopts[k] = v[1:-1] devobj = devclass(self.log.getChild(devname), devopts, devname, self._dispatcher) - devs.append([devname, devobj]) + devs.append([devname, devobj, export]) # connect devices with dispatcher - for devname, devobj in devs: + for devname, devobj, export in devs: self.log.info('registering device %r' % devname) - self._dispatcher.register_device(devobj, devname) + self._dispatcher.register_device(devobj, devname, export) # also call init on the devices devobj.init() @@ -152,5 +162,3 @@ class Server(object): cls.__name__, ', '.join(options.keys()))) return obj - - diff --git a/src/validators.py b/src/validators.py index fae08b4..ee97aab 100644 --- a/src/validators.py +++ b/src/validators.py @@ -27,6 +27,13 @@ # also validators should have a __repr__ returning a 'python' string # which recreates them +# if a validator does a mapping, it normally maps to the external representation (used for print/log/protocol/...) +# to get the internal representation (for the code), call method convert + +class ProgrammingError(Exception): + pass + + class Validator(object): # list of tuples: (name, converter) params = [] @@ -61,12 +68,17 @@ class Validator(object): ', '.join(list(kwds.keys())))) def __repr__(self): - params = ['%s=%r' % (pn, self.__dict__[pn]) for pn in self.params] + params = ['%s=%r' % (pn[0], self.__dict__[pn[0]]) + for pn in self.params] return ('%s(%s)' % (self.__class__.__name__, ', '.join(params))) def __call__(self, value): return self.check(self.valuetype(value)) + def convert(self, value): + # transforms the 'internal' representation into the 'external' + return self.valuetype(value) + class floatrange(Validator): params = [('lower', float), ('upper', float)] @@ -78,22 +90,110 @@ class floatrange(Validator): (value, self.lower, self.upper)) +class intrange(Validator): + params = [('lower', int), ('upper', int)] + valuetype = int + + def check(self, value): + if self.lower <= value <= self.upper: + return value + raise ValueError('Intrange: value %r must be within %f and %f' % + (value, self.lower, self.upper)) + + class positive(Validator): + def check(self, value): if value > 0: return value - raise ValueError('Value %r must be positive!' % obj) + raise ValueError('Value %r must be > 0!' % value) class nonnegative(Validator): + def check(self, value): if value >= 0: return value - raise ValueError('Value %r must be positive!' % obj) + raise ValueError('Value %r must be >= 0!' % value) + + +class array(Validator): + """integral amount of data-elements which are described by the SAME validator + + The size of the array can also be described by an validator + """ + valuetype = list + params = [('size', lambda x: x), + ('datatype', lambda x: x)] + + def check(self, values): + requested_size = len(values) + try: + allowed_size = self.size(requested_size) + except ValueError as e: + raise ValueError( + 'illegal number of elements %d, need %r: (%s)' % + (requested_size, self.size, e)) + if requested_size != allowed_size: + raise ValueError( + 'need %d elements (got %d)' % + (allowed_size, requested_size)) + # apply data-type validator to all elements and return + res = [] + for idx, el in enumerate(values): + try: + res.append(self.datatype(el)) + except ValueError as e: + raise ValueError( + 'Array Element %s (=%r) not conforming to %r: (%s)' % + (idx, el, self.datatype, e)) + return res # more complicated validator may not be able to use validator base class +class vector(object): + """fixed length, eache element has its own validator""" + + def __init__(self, *args): + self.validators = args + self.argstr = ', '.join([repr(e) for e in args]) + + def __call__(self, args): + if len(args) != len(self.validators): + raise ValueError('Vector: need exactly %d elementes (got %d)' % + len(self.validators), len(args)) + return [v(e) for v, e in zip(self.validators, args)] + + def __repr__(self): + return ('%s(%s)' % (self.__class__.__name__, self.argstr)) + + +class oneof(object): + """needs to comply with one of the given validators/values""" + + def __init__(self, *args): + self.oneof = args + self.argstr = ', '.join([repr(e) for e in args]) + + def __call__(self, arg): + for v in self.oneof: + if callable(v): + try: + if (v == int) and (float(arg) != int(arg)): + continue + return v(arg) + except ValueError: + pass # try next validator + elif v == arg: + return v + raise ValueError('Oneof: %r should be one of: %s' % (arg, self.argstr)) + + def __repr__(self): + return ('%s(%s)' % (self.__class__.__name__, self.argstr)) + + class mapping(object): + def __init__(self, *args, **kwds): self.mapping = {} # use given kwds directly @@ -112,11 +212,21 @@ class mapping(object): self.revmapping[v] = k def __call__(self, obj): + try: + obj = int(obj) + except ValueError: + pass if obj in self.mapping: return obj + if obj in self.revmapping: + return self.revmapping[obj] raise ValueError("%r should be one of %r" % (obj, list(self.mapping.keys()))) def __repr__(self): - params = ['%s=%r' % (mname, mval) for mname, mval in self.mapping] + params = ['%s=%r' % (mname, mval) + for mname, mval in self.mapping.items()] return ('%s(%s)' % (self.__class__.__name__, ', '.join(params))) + + def convert(self, arg): + return self.mapping.get(arg, arg)