Merge "Implement a variant of the Demo protocol from Markus"

This commit is contained in:
Enrico Faulhaber 2016-08-31 08:34:06 +02:00 committed by Gerrit Code Review
commit 69b979cdd0
20 changed files with 1093 additions and 178 deletions

View File

@ -40,7 +40,6 @@ for dirpath, dirnames, filenames in os.walk(DOC_SRC):
except OSError: except OSError:
pass pass
for fn in filenames: for fn in filenames:
full_name = path.join(dirpath, fn) full_name = path.join(dirpath, fn)
sub_name = path.relpath(full_name, DOC_SRC) sub_name = path.relpath(full_name, DOC_SRC)

View File

@ -67,7 +67,7 @@ def main(argv=None):
args = parseArgv(argv[1:]) args = parseArgv(argv[1:])
loglevel = 'debug' if args.verbose else ('error' if args.quiet else 'info') 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) srv = Server(args.name, basepath)

View File

@ -21,18 +21,18 @@
## A Server ## ## 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 * 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 ## ## 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 * supply properties for PARAMS to auto-generate async data units
* self-polling support
* generic devicethreads
* proxydevice
* make get_device uri-aware
## Testsuite ## ## 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.... * mabe use sphinx to generate docu: a pdf can then be auto-generated....
* transfer build docu into wiki via automated jobfile * transfer build docu into wiki via automated jobfile
Problem: wiki does not understand .md or .html

39
etc/demo.cfg Normal file
View File

@ -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

View File

@ -2,7 +2,7 @@
markdown>=2.6 markdown>=2.6
# general stuff # general stuff
psutil psutil
python-daemon python-daemon >=2.0
# for zmq # for zmq
#pyzmq>=13.1.0 #pyzmq>=13.1.0

View File

@ -27,45 +27,66 @@
# all others MUST derive from those, the 'interface'-class is still derived # all others MUST derive from those, the 'interface'-class is still derived
# from these base classes (how to do this?) # from these base classes (how to do this?)
import time
import types import types
import inspect import inspect
from errors import ConfigError, ProgrammingError from errors import ConfigError, ProgrammingError
from protocol import status from protocol import status
from validators import mapping
EVENT_ONLY_ON_CHANGED_VALUES = True
# storage for PARAMeter settings: # 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 # if no default is given, the parameter MUST be specified in the configfile
# during startup, currentvalue is initialized with the default value or # during startup, value is initialized with the default value or
# from the config file # from the config file if specified there
class PARAM(object): 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.description = description
self.validator = validator self.validator = validator
self.default = default self.default = default
self.unit = unit self.unit = unit
self.readonly = readonly self.readonly = readonly
self.export = export self.export = export
# internal caching... # internal caching: value and timestamp of last change...
self.currentvalue = default 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...) # storage for CMDs settings (description + call signature...)
class CMD(object): class CMD(object):
def __init__(self, description, arguments, result): def __init__(self, description, arguments, result):
# descriptive text for humans # descriptive text for humans
self.description = description self.description = description
# list of validators for arguments # list of validators for arguments
self.argumenttype = arguments self.arguments = arguments
# validator for results # validator for result
self.resulttype = 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 # Meta class
# warning: MAGIC! # warning: MAGIC!
class DeviceMeta(type): class DeviceMeta(type):
def __new__(mcs, name, bases, attrs): def __new__(mcs, name, bases, attrs):
newtype = type.__new__(mcs, name, bases, attrs) newtype = type.__new__(mcs, name, bases, attrs)
if '__constructed__' in attrs: if '__constructed__' in attrs:
@ -81,22 +102,56 @@ class DeviceMeta(type):
setattr(newtype, entry, newentry) setattr(newtype, entry, newentry)
# check validity of PARAM entries # check validity of PARAM entries
for pname, info in newtype.PARAMS.items(): for pname, pobj in newtype.PARAMS.items():
if not isinstance(info, PARAM): # XXX: allow dicts for overriding certain aspects only.
if not isinstance(pobj, PARAM):
raise ProgrammingError('%r: device PARAM %r should be a ' raise ProgrammingError('%r: device PARAM %r should be a '
'PARAM object!' % (name, pname)) '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(): def wrapped_rfunc(self, maxage=0, pname=pname, rfunc=rfunc):
return self.PARAMS[pname].currentvalue 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): if not pobj.readonly:
p = self.PARAMS[pname] wfunc = attrs.get('write_' + pname, None)
p.currentvalue = p.validator(value) if p.validator else value
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 # also send notification
self.DISPATCHER.announce_update(self, pname, value) self.log.debug('%s is now %r' % (pname, value))
self.DISPATCHER.announce_update(self, pname, pobj)
attrs[pname] = property(getter, setter) setattr(newtype, pname, property(getter, setter))
# also collect/update information about CMD's # also collect/update information about CMD's
setattr(newtype, 'CMDS', getattr(newtype, 'CMDS', {})) setattr(newtype, 'CMDS', getattr(newtype, 'CMDS', {}))
@ -114,6 +169,15 @@ class DeviceMeta(type):
# Basic device class # Basic device class
#
# within devices, parameters should only be addressed as self.<pname>
# 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_<pname> and generate an async update)
# if you want to 'update from the hardware', call self.read_<pname>
# the return value of this method will be used as the new cached value and
# be returned.
class Device(object): class Device(object):
"""Basic Device, doesn't do much""" """Basic Device, doesn't do much"""
__metaclass__ = DeviceMeta __metaclass__ = DeviceMeta
@ -123,7 +187,7 @@ class Device(object):
DISPATCHER = None DISPATCHER = None
def __init__(self, logger, cfgdict, devname, dispatcher): 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.DISPATCHER = dispatcher
self.log = logger self.log = logger
self.name = devname self.name = devname
@ -137,13 +201,17 @@ class Device(object):
# is not specified in cfgdict # is not specified in cfgdict
for k, v in self.PARAMS.items(): for k, v in self.PARAMS.items():
if k not in cfgdict: 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.... # Ellipsis is the one single value you can not specify....
raise ConfigError('Device %s: Parameter %r has no default ' raise ConfigError('Device %s: Parameter %r has no default '
'value and was not given in config!' 'value and was not given in config!'
% (self.name, k)) % (self.name, k))
# assume default value was given # assume default value was given
cfgdict[k] = v.default cfgdict[k] = v.default
# replace CLASS level PARAM objects with INSTANCE level ones
self.PARAMS[k] = PARAM(self.PARAMS[k])
# now 'apply' config: # now 'apply' config:
# pass values through the validators and store as attributes # pass values through the validators and store as attributes
for k, v in cfgdict.items(): for k, v in cfgdict.items():
@ -156,7 +224,6 @@ class Device(object):
except ValueError as e: except ValueError as e:
raise ConfigError('Device %s: config parameter %r:\n%r' raise ConfigError('Device %s: config parameter %r:\n%r'
% (self.name, k, e)) % (self.name, k, e))
# XXX: with or without prefix?
setattr(self, k, v) setattr(self, k, v)
def init(self): def init(self):
@ -172,15 +239,15 @@ class Readable(Device):
PARAMS = { PARAMS = {
'value': PARAM('current value of the device', readonly=True, default=0.), 'value': PARAM('current value of the device', readonly=True, default=0.),
'status': PARAM('current status of the device', default=status.OK, '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), readonly=True),
} }
def read_value(self, maxage=0):
raise NotImplementedError
def read_status(self):
return status.OK
class Driveable(Readable): class Driveable(Readable):
"""Basic Driveable device """Basic Driveable device
@ -188,8 +255,6 @@ class Driveable(Readable):
providing a settable 'target' parameter to those of a Readable providing a settable 'target' parameter to those of a Readable
""" """
PARAMS = { 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

265
src/devices/demo.py Normal file
View File

@ -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 <enrico.faulhaber@frm2.tum.de>
# *****************************************************************************
"""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,),
}

View File

@ -28,12 +28,14 @@ from validators import floatrange
from epics import PV from epics import PV
class LN2(Readable): class LN2(Readable):
"""Just a readable. """Just a readable.
class name indicates it to be a sensor for LN2, class name indicates it to be a sensor for LN2,
but the implementation may do anything but the implementation may do anything
""" """
def read_value(self, maxage=0): def read_value(self, maxage=0):
return round(100 * random.random(), 1) return round(100 * random.random(), 1)
@ -74,7 +76,6 @@ class Temp(Driveable):
pass pass
class EPICS_PV(Driveable): class EPICS_PV(Driveable):
"""pyepics test device.""" """pyepics test device."""

View File

@ -35,7 +35,6 @@ from logging import Logger, Formatter, Handler, DEBUG, INFO, WARNING, ERROR, \
from . import colors from . import colors
LOGFMT = '%(asctime)s : %(levelname)-7s : %(name)-15s: %(message)s' LOGFMT = '%(asctime)s : %(levelname)-7s : %(name)-15s: %(message)s'
DATEFMT = '%H:%M:%S' DATEFMT = '%H:%M:%S'
DATESTAMP_FMT = '%Y-%m-%d' DATESTAMP_FMT = '%Y-%m-%d'
@ -58,7 +57,10 @@ def initLogging(rootname='secop', rootlevel='info', logdir='/tmp/log'):
log.addHandler(ColoredConsoleHandler()) log.addHandler(ColoredConsoleHandler())
# logfile for fg and bg process # logfile for fg and bg process
if logdir.startswith('/var/log'):
log.addHandler(LogfileHandler(logdir, rootname)) log.addHandler(LogfileHandler(logdir, rootname))
else:
log.addHandler(LogfileHandler(logdir, ''))
def getLogger(name, subdir=False): def getLogger(name, subdir=False):
@ -73,7 +75,6 @@ class SecopLogger(Logger):
Logger.__init__(self, *args, **kwargs) Logger.__init__(self, *args, **kwargs)
SecopLogger._storeLoggerNameLength(self) SecopLogger._storeLoggerNameLength(self)
def getChild(self, suffix, ownDir=False): def getChild(self, suffix, ownDir=False):
child = Logger.getChild(self, suffix) child = Logger.getChild(self, suffix)
child.setLevel(self.getEffectiveLevel()) child.setLevel(self.getEffectiveLevel())
@ -111,7 +112,6 @@ class SecopLogger(Logger):
SecopLogger.maxLogNameLength = len(logObj.name) SecopLogger.maxLogNameLength = len(logObj.name)
class ConsoleFormatter(Formatter): class ConsoleFormatter(Formatter):
""" """
A lightweight formatter for the interactive console, with optional A lightweight formatter for the interactive console, with optional

View File

@ -70,6 +70,7 @@ class Driveable(Writeable):
"""A Moveable which may take a while to reach its target, """A Moveable which may take a while to reach its target,
hence stopping it may be desired""" hence stopping it may be desired"""
def do_stop(self): def do_stop(self):
raise NotImplementedError('A Driveable MUST implement the STOP() ' raise NotImplementedError('A Driveable MUST implement the STOP() '
'command') 'command')

View File

@ -37,17 +37,19 @@ Interface to the devices:
- remove_device(devname_or_obj): removes the device (during shutdown) - remove_device(devname_or_obj): removes the device (during shutdown)
internal stuff which may be called internal stuff which may be called
- get_devices(): return a list of devices + descriptive data as dict - list_devices(): return a list of devices + descriptive data as dict
- get_device_params(): - list_device_params():
return a list of paramnames for this device + descriptive data return a list of paramnames for this device + descriptive data
""" """
import time
import threading import threading
from messages import * from messages import *
class Dispatcher(object): class Dispatcher(object):
def __init__(self, logger, options): def __init__(self, logger, options):
self.log = logger self.log = logger
# XXX: move framing and encoding to interface! # XXX: move framing and encoding to interface!
@ -73,7 +75,12 @@ class Dispatcher(object):
with self._dispatcher_lock: with self._dispatcher_lock:
# de-frame data # de-frame data
frames = self.framing.decode(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) self.log.debug('Dispatcher: frames=%r' % frames)
if not frames:
conn.queue_reply(self._format_reply(HelpReply()))
for frame in frames: for frame in frames:
reply = None reply = None
# decode frame # decode frame
@ -82,45 +89,53 @@ class Dispatcher(object):
# act upon requestobj # act upon requestobj
msgtype = msg.TYPE msgtype = msg.TYPE
msgname = msg.NAME msgname = msg.NAME
msgargs = msg
# generate reply (coded and framed) # generate reply (coded and framed)
if msgtype != 'request': if msgtype != 'request':
reply = ProtocolErrorReply(msg) reply = ProtocolError(msg)
else: else:
self.log.debug('Looking for handle_%s' % msgname) self.log.debug('Looking for handle_%s' % msgname)
handler = getattr(self, 'handle_%s' % msgname, None) handler = getattr(self, 'handle_%s' % msgname, None)
if handler: if handler:
reply = handler(msgargs) reply = handler(conn, msg)
else: else:
self.log.debug('Can not handle msg %r' % msg) self.log.debug('Can not handle msg %r' % msg)
reply = self.unhandled(msgname, msgargs) reply = self.unhandled(msgname, msg)
if reply: if reply:
conn.queue_reply(self._format_reply(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): def _format_reply(self, reply):
self.log.debug('formatting reply %r' % reply)
msg = self.encoding.encode(reply) msg = self.encoding.encode(reply)
self.log.debug('encoded is %r' % msg)
frame = self.framing.encode(msg) frame = self.framing.encode(msg)
self.log.debug('frame is %r' % frame)
return 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 """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) subscriber = self._dispatcher_subscriptions.get(eventname, None)
if subscriber: if subscriber:
reply = AsyncDataUnit(device=self.get_device(device).name, reply = AsyncDataUnit(devname=devname,
param=pname, pname=pname,
value=str(value), value=str(pobj.value),
timestamp=time.time(), timestamp=pobj.timestamp,
) )
data = self._format_reply(reply) data = self._format_reply(reply)
for conn in subscriber: for conn in subscriber:
conn.queue_async_reply(data) conn.queue_async_reply(data)
def subscribe(self, conn, device, pname): def subscribe(self, conn, devname, pname):
eventname = '%s/%s' % (self.get_device(device).name, pname) eventname = '%s/%s' % (devname, pname)
self._dispatcher_subscriptions.getdefault(eventname, set()).add(conn) 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): def add_connection(self, conn):
"""registers new connection""" """registers new connection"""
@ -133,6 +148,8 @@ class Dispatcher(object):
# XXX: also clean _dispatcher_subscriptions ! # XXX: also clean _dispatcher_subscriptions !
def register_device(self, devobj, devname, export=True): 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 self._dispatcher_devices[devname] = devobj
if export: if export:
self._dispatcher_export.append(devname) self._dispatcher_export.append(devname)
@ -171,100 +188,295 @@ class Dispatcher(object):
return dn, dd return dn, dd
def list_device_params(self, devname): def list_device_params(self, devname):
self.log.debug('list_device_params(%r)' % devname)
if devname in self._dispatcher_export: if devname in self._dispatcher_export:
# XXX: omit export=False params! # 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 {} 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 # now the (defined) handlers for the different requests
def handle_Help(self, msg): def handle_Help(self, conn, msg):
return HelpReply() return HelpReply()
def handle_ListDevices(self, msgargs): def handle_ListDevices(self, conn, msg):
# XXX: What about the descriptive data????
# XXX: choose! # XXX: choose!
#return ListDevicesReply(self.list_device_names()) return ListDevicesReply(self.list_device_names())
return ListDevicesReply(*self.list_devices()) # return ListDevicesReply(*self.list_devices())
def handle_ListDeviceParams(self, msgargs): def handle_ListDeviceParams(self, conn, msg):
devobj = self.get_device(msgargs.device) # reply with a list of the parameter names for a given device
if devobj: self.log.error('Keep: ListDeviceParams')
return ListDeviceParamsReply(msgargs.device, if msg.device in self._dispatcher_export:
self.get_device_params(devobj)) params = self.list_device_params(msg.device)
return ListDeviceParamsReply(msg.device, params.keys())
else: else:
return NoSuchDeviceErrorReply(msgargs.device) return NoSuchDeviceError(msg.device)
def handle_ReadValue(self, msgargs): def handle_ReadAllDevices(self, conn, msg):
devobj = self.get_device(msgargs.device) # reply with a bunch of ReadValueReplies, reading ALL devices
if devobj: result = []
return ReadValueReply(msgargs.device, devobj.read_value(), 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()) timestamp=time.time())
else:
return NoSuchDeviceErrorReply(msgargs.device)
def handle_ReadParam(self, msgargs): return InternalError('undefined device value')
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)
def handle_WriteParam(self, msgargs): def handle_WriteValue(self, conn, msg):
devobj = self.get_device(msgargs.device) value = msg.value
if devobj: devname = msg.device
writefunc = getattr(devobj, 'write_%s' % msgargs.param, None) devobj = self.get_device(devname)
if writefunc: if devobj is None:
readbackvalue = writefunc(msgargs.value) or msgargs.value return NoSuchDeviceError(devname)
# trigger async updates
setattr(devobj, msgargs.param, readbackvalue) pobj = getattr(devobj.PARAMS, 'target', None)
return WriteParamReply(msgargs.device, msgargs.param, if pobj is None:
readbackvalue, return NoSuchParamError(devname, 'target')
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()) 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_RequestAsyncData(self, msgargs): return InternalError('undefined device value')
return ErrorReply('AsyncData is not (yet) supported')
def handle_ListOfFeatures(self, msgargs): 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) # no features supported (yet)
return ListOfFeaturesReply([]) return ListOfFeaturesReply([])
def handle_ActivateFeature(self, msgargs): def handle_ActivateFeature(self, conn, msg):
return ErrorReply('Features are not (yet) supported') return Error('Features are not (yet) supported')
def unhandled(self, msgname, msgargs): def unhandled(self, msgname, conn, msg):
"""handler for unhandled Messages """handler for unhandled Messages
(no handle_<messagename> method was defined) (no handle_<messagename> method was defined)
""" """
self.log.error('IGN: got unhandled request %s' % msgname) self.log.error('IGN: got unhandled request %s' % msgname)
return ErrorReply('Got Unhandled Request') return Error('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])

View File

@ -32,6 +32,7 @@ MAX_MESSAGE_SIZE = 1024
class TCPRequestHandler(SocketServer.BaseRequestHandler): class TCPRequestHandler(SocketServer.BaseRequestHandler):
def setup(self): def setup(self):
self.log = self.server.log self.log = self.server.log
self._queue = collections.deque(maxlen=100) self._queue = collections.deque(maxlen=100)

View File

@ -48,12 +48,19 @@ class Message(object):
'argument %r' % k) 'argument %r' % k)
names.remove(k) names.remove(k)
self.__dict__[k] = v self.__dict__[k] = v
if names: for name in names:
raise TypeError('__init__() takes at least %d arguments (%d given)' self.__dict__[name] = None
% len(self.ARGS), len(args)+len(kwds)) # 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.NAME = (self.__class__.__name__[:-len(self.TYPE)] or
self.__class__.__name__) 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): class Request(Message):
TYPE = REQUEST TYPE = REQUEST
@ -66,6 +73,16 @@ class Reply(Message):
class ErrorReply(Message): class ErrorReply(Message):
TYPE = ERROR TYPE = ERROR
# for DEMO
class DemoRequest(Request):
ARGS = ['novalue', 'devname', 'paramname', 'propname', 'assign']
class DemoReply(Reply):
ARGS = ['lines']
# actuall message objects # actuall message objects
class ListDevicesRequest(Request): class ListDevicesRequest(Request):
@ -92,6 +109,30 @@ class ReadValueReply(Reply):
ARGS = ['device', 'value', 'timestamp', 'error', 'unit'] 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): class ReadParamRequest(Request):
ARGS = ['device', 'param', 'maxage'] ARGS = ['device', 'param', 'maxage']
@ -117,7 +158,7 @@ class RequestAsyncDataReply(Reply):
class AsyncDataUnit(ReadParamReply): class AsyncDataUnit(ReadParamReply):
ARGS = ['device', 'param', 'value', 'timestamp', 'error', 'unit'] ARGS = ['devname', 'pname', 'value', 'timestamp', 'error', 'unit']
class ListOfFeaturesRequest(Request): class ListOfFeaturesRequest(Request):
@ -138,40 +179,47 @@ class ActivateFeatureReply(Reply):
pass pass
class ProtocollError(ErrorReply): # ERRORS
ARGS = ['msgtype', 'msgname', 'msgargs'] ########
class ErrorReply(Reply): class ErrorReply(Reply):
ARGS = ['error'] ARGS = ['error']
class NoSuchDeviceErrorReply(ErrorReply): class InternalError(ErrorReply):
ARGS = ['error']
class ProtocollError(ErrorReply):
ARGS = ['error']
class NoSuchDeviceError(ErrorReply):
ARGS = ['device'] ARGS = ['device']
class NoSuchParamErrorReply(ErrorReply): class NoSuchParamError(ErrorReply):
ARGS = ['device', 'param'] ARGS = ['device', 'param']
class ParamReadonlyErrorReply(ErrorReply): class ParamReadonlyError(ErrorReply):
ARGS = ['device', 'param'] ARGS = ['device', 'param']
class UnsupportedFeatureErrorReply(ErrorReply): class UnsupportedFeatureError(ErrorReply):
ARGS = ['feature'] ARGS = ['feature']
class NoSuchCommandErrorReply(ErrorReply): class NoSuchCommandError(ErrorReply):
ARGS = ['device', 'command'] ARGS = ['device', 'command']
class CommandFailedErrorReply(ErrorReply): class CommandFailedError(ErrorReply):
ARGS = ['device', 'command'] ARGS = ['device', 'command']
class InvalidParamValueErrorReply(ErrorReply): class InvalidParamValueError(ErrorReply):
ARGS = ['device', 'param', 'value'] ARGS = ['device', 'param', 'value', 'error']
# Fun! # Fun!

View File

@ -28,3 +28,10 @@ WARN = 300
UNSTABLE = 350 UNSTABLE = 350
ERROR = 400 ERROR = 400
UNKNOWN = -1 UNKNOWN = -1
#OK = 'idle'
#BUSY = 'busy'
#WARN = 'alarm'
#UNSTABLE = 'unstable'
#ERROR = 'ERROR'
#UNKNOWN = 'unknown'

View File

@ -61,6 +61,7 @@ class PickleEncoder(MessageEncoder):
class TextEncoder(MessageEncoder): class TextEncoder(MessageEncoder):
def __init__(self): def __init__(self):
# build safe namespace # build safe namespace
ns = dict() ns = dict()
@ -94,9 +95,109 @@ class TextEncoder(MessageEncoder):
return messages.HelpRequest() 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 = { ENCODERS = {
'pickle': PickleEncoder, 'pickle': PickleEncoder,
'text': TextEncoder, 'text': TextEncoder,
'demo': DemoEncoder,
} }

View File

@ -32,6 +32,7 @@ class Framer(object):
note: not all MessageEncoders can use all Framers, note: not all MessageEncoders can use all Framers,
but the intention is to have this for as many as possible. but the intention is to have this for as many as possible.
""" """
def encode(self, *frames): def encode(self, *frames):
"""return the wire-data for the given messageframes""" """return the wire-data for the given messageframes"""
raise NotImplemented raise NotImplemented
@ -126,9 +127,67 @@ class RLEFramer(Framer):
self.frames_to_go = 0 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 = { FRAMERS = {
'eol': EOLFramer, 'eol': EOLFramer,
'rle': RLEFramer, 'rle': RLEFramer,
'demo': DemoFramer,
} }
__ALL__ = ['FRAMERS'] __ALL__ = ['FRAMERS']

View File

@ -38,6 +38,7 @@ from errors import ConfigError
class Server(object): class Server(object):
def __init__(self, name, workdir, parentLogger=None): def __init__(self, name, workdir, parentLogger=None):
self._name = name self._name = name
self._workdir = workdir self._workdir = workdir
@ -115,14 +116,23 @@ class Server(object):
devclass = devopts.pop('class') devclass = devopts.pop('class')
# create device # create device
self.log.debug('Creating Device %r' % devname) 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, devobj = devclass(self.log.getChild(devname), devopts, devname,
self._dispatcher) self._dispatcher)
devs.append([devname, devobj]) devs.append([devname, devobj, export])
# connect devices with dispatcher # connect devices with dispatcher
for devname, devobj in devs: for devname, devobj, export in devs:
self.log.info('registering device %r' % devname) 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 # also call init on the devices
devobj.init() devobj.init()
@ -152,5 +162,3 @@ class Server(object):
cls.__name__, cls.__name__,
', '.join(options.keys()))) ', '.join(options.keys())))
return obj return obj

View File

@ -27,6 +27,13 @@
# also validators should have a __repr__ returning a 'python' string # also validators should have a __repr__ returning a 'python' string
# which recreates them # 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): class Validator(object):
# list of tuples: (name, converter) # list of tuples: (name, converter)
params = [] params = []
@ -61,12 +68,17 @@ class Validator(object):
', '.join(list(kwds.keys())))) ', '.join(list(kwds.keys()))))
def __repr__(self): 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))) return ('%s(%s)' % (self.__class__.__name__, ', '.join(params)))
def __call__(self, value): def __call__(self, value):
return self.check(self.valuetype(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): class floatrange(Validator):
params = [('lower', float), ('upper', float)] params = [('lower', float), ('upper', float)]
@ -78,22 +90,110 @@ class floatrange(Validator):
(value, self.lower, self.upper)) (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): class positive(Validator):
def check(self, value): def check(self, value):
if value > 0: if value > 0:
return value return value
raise ValueError('Value %r must be positive!' % obj) raise ValueError('Value %r must be > 0!' % value)
class nonnegative(Validator): class nonnegative(Validator):
def check(self, value): def check(self, value):
if value >= 0: if value >= 0:
return value 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 # 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): class mapping(object):
def __init__(self, *args, **kwds): def __init__(self, *args, **kwds):
self.mapping = {} self.mapping = {}
# use given kwds directly # use given kwds directly
@ -112,11 +212,21 @@ class mapping(object):
self.revmapping[v] = k self.revmapping[v] = k
def __call__(self, obj): def __call__(self, obj):
try:
obj = int(obj)
except ValueError:
pass
if obj in self.mapping: if obj in self.mapping:
return obj return obj
if obj in self.revmapping:
return self.revmapping[obj]
raise ValueError("%r should be one of %r" % raise ValueError("%r should be one of %r" %
(obj, list(self.mapping.keys()))) (obj, list(self.mapping.keys())))
def __repr__(self): 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))) return ('%s(%s)' % (self.__class__.__name__, ', '.join(params)))
def convert(self, arg):
return self.mapping.get(arg, arg)