Merge "Implement a variant of the Demo protocol from Markus"
This commit is contained in:
commit
69b979cdd0
@ -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)
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
15
doc/todo.md
15
doc/todo.md
@ -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
39
etc/demo.cfg
Normal 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
|
@ -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
|
||||||
|
|
||||||
|
@ -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
265
src/devices/demo.py
Normal 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,),
|
||||||
|
}
|
@ -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."""
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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')
|
||||||
|
@ -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])
|
|
||||||
|
@ -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)
|
||||||
|
@ -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!
|
||||||
|
|
||||||
|
@ -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'
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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']
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user