rework on transport (encoding/framing) and dispatcher

put encoders and framers into their own files.
also rework messages and dispatcher
make tcpserver functional

FIRST WORKING VERSION!
(no daemon mode yet, sorry)

start bin/server.py, connect a terminal to localhost:10767
and press enter....

note: not all requests are bug free yet, ListDevicesRequest() works

Change-Id: I46d6e469bca32fc53057d64ff48cce4f41ea12ea
This commit is contained in:
Enrico Faulhaber 2016-06-22 18:05:47 +02:00
parent d3c430e1b9
commit c11bca3c37
27 changed files with 1374 additions and 904 deletions

View File

@ -33,35 +33,35 @@ pid_path = path.join(basepath, 'pid')
log_path = path.join(basepath, 'log')
sys.path[0] = path.join(basepath, 'src')
import logger
import argparse
from lib import check_pidfile, start_server, kill_server
parser = argparse.ArgumentParser(description = "Manage a SECoP server")
parser = argparse.ArgumentParser(description="Manage a SECoP server")
loggroup = parser.add_mutually_exclusive_group()
loggroup.add_argument("-v", "--verbose", help="Output lots of diagnostic information",
action='store_true', default=False)
loggroup.add_argument("-q", "--quiet", help="suppress non-error messages", action='store_true',
default=False)
parser.add_argument("action", help="What to do with the server: (re)start, status or stop",
choices=['start', 'status', 'stop', 'restart'], default="status")
parser.add_argument("name", help="Name of the instance. Uses etc/name.cfg for configuration\n"
loggroup.add_argument("-v", "--verbose",
help="Output lots of diagnostic information",
action='store_true', default=False)
loggroup.add_argument("-q", "--quiet", help="suppress non-error messages",
action='store_true', default=False)
parser.add_argument("action",
help="What to do: (re)start, status or stop",
choices=['start', 'status', 'stop', 'restart'],
default="status")
parser.add_argument("name",
help="Name of the instance.\n"
" Uses etc/name.cfg for configuration\n"
"may be omitted to mean ALL (which are configured)",
nargs='?', default='')
args = parser.parse_args()
import logging
loglevel = logging.DEBUG if args.verbose else (logging.ERROR if args.quiet else logging.INFO)
logging.basicConfig(level=loglevel, format='%(asctime)s %(levelname)s %(message)s')
logger = logging.getLogger('server')
logger.setLevel(loglevel)
fh = logging.FileHandler(path.join(log_path, 'server.log'), 'w')
fh.setLevel(loglevel)
logger.addHandler(fh)
loglevel = 'debug' if args.verbose else ('error' if args.quiet else 'info')
logger = logger.get_logger('startup', loglevel)
logger.debug("action specified %r" % args.action)
def handle_servername(name, action):
pidfile = path.join(pid_path, name + '.pid')
cfgfile = path.join(etc_path, name + '.cfg')
@ -97,7 +97,7 @@ if not args.name:
if fn.endswith('.cfg'):
handle_servername(fn[:-4], args.action)
else:
logger.debug('configfile with strange extension found: %r'
logger.debug('configfile with strange extension found: %r'
% path.basename(fn))
# ignore subdirs!
while(dirs):

View File

@ -1,7 +1,9 @@
[server]
bindto=localhost
bindport=10767
protocol=pickle
interface = tcp
framing=eol
encoding=text
[device LN2]
class=devices.test.LN2

View File

@ -0,0 +1,38 @@
# -*- 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>
#
# *****************************************************************************
"""Define Client side proxies"""
# nothing here yet.
def get_client(interfacespec):
"""returns a client connected to the remote interface"""
pass
class DeviceProxy(object):
"""(In python) dynamically constructed object
allowing access to the servers devices via the SECoP Protocol inbetween
"""
pass

View File

@ -22,38 +22,46 @@
"""Define Baseclasses for real devices implemented in the server"""
# XXX: connect with 'protocol'-Devices.
# Idea: every Device defined herein is also a 'protocol'-device,
# all others MUST derive from those, the 'interface'-class is still derived
# from these base classes (how to do this?)
import types
import inspect
from errors import ConfigError, ProgrammingError
from protocol import status
# storage for CONFIGurable settings (from configfile)
class CONFIG(object):
def __init__(self, description, validator=None, default=None, unit=None):
self.description = description
self.validator = validator
self.default = default
self.unit = unit
# storage for PARAMeter settings (changeable during runtime)
# storage for PARAMeter settings:
# if readonly is False, the value can be changed (by code, or remte)
# if no default is given, the parameter MUST be specified in the configfile
# during startup, currentvalue is initialized with the default value or
# from the config file
class PARAM(object):
def __init__(self, description, validator=None, default=None, unit=None, readonly=False):
def __init__(self, description, validator=None, default=Ellipsis,
unit=None, readonly=False, export=True):
self.description = description
self.validator = validator
self.default = default
self.unit = unit
self.readonly = readonly
self.export = export
# internal caching...
self.currentvalue = default
# storage for CMDs settings (names + call signature...)
# storage for CMDs settings (description + call signature...)
class CMD(object):
def __init__(self, description, *args):
def __init__(self, description, arguments, result):
# descriptive text for humans
self.description = description
self.arguments = args
# list of validators for arguments
self.argumenttype = arguments
# validator for results
self.resulttype = result
# Meta class
# warning: MAGIC!
@ -62,25 +70,34 @@ class DeviceMeta(type):
newtype = type.__new__(mcs, name, bases, attrs)
if '__constructed__' in attrs:
return newtype
# merge CONFIG, PARAM, CMDS from all sub-classes
for entry in ['CONFIG', 'PARAMS', 'CMDS']:
# merge PARAM and CMDS from all sub-classes
for entry in ['PARAMS', 'CMDS']:
newentry = {}
for base in reversed(bases):
if hasattr(base, entry):
newentry.update(getattr(base, entry))
newentry.update(attrs.get(entry, {}))
setattr(newtype, entry, newentry)
# check validity of entries
for cname, info in newtype.CONFIG.items():
if not isinstance(info, CONFIG):
raise ProgrammingError("%r: device CONFIG %r should be a CONFIG object!" %
(name, cname))
#XXX: greate getters for the config value
# check validity of PARAM entries
for pname, info in newtype.PARAMS.items():
if not isinstance(info, PARAM):
raise ProgrammingError("%r: device PARAM %r should be a PARAM object!" %
(name, pname))
raise ProgrammingError('%r: device PARAM %r should be a '
'PARAM object!' % (name, pname))
#XXX: greate getters and setters, setters should send async updates
def getter():
return self.PARAMS[pname].currentvalue
def setter(value):
p = self.PARAMS[pname]
p.currentvalue = p.validator(value) if p.validator else value
# also send notification
self.DISPATCHER.announce_update(self, pname, value)
attrs[pname] = property(getter, setter)
# also collect/update information about CMD's
setattr(newtype, 'CMDS', getattr(newtype, 'CMDS', {}))
for name in attrs:
@ -90,55 +107,56 @@ class DeviceMeta(type):
argspec = inspect.getargspec(value)
if argspec[0] and argspec[0][0] == 'self':
del argspec[0][0]
newtype.CMDS[name] = CMD(value.get('__doc__', name), *argspec)
newtype.CMDS[name] = CMD(value.get('__doc__', name),
*argspec)
attrs['__constructed__'] = True
return newtype
# Basic device class
class Device(object):
"""Basic Device, doesn't do much"""
__metaclass__ = DeviceMeta
# CONFIG, PARAMS and CMDS are auto-merged upon subclassing
CONFIG = {}
# PARAMS and CMDS are auto-merged upon subclassing
PARAMS = {}
CMDS = {}
SERVER = None
def __init__(self, devname, serverobj, logger, cfgdict):
DISPATCHER = None
def __init__(self, logger, cfgdict, devname, dispatcher):
# remember the server object (for the async callbacks)
self.SERVER = serverobj
self.DISPATCHER = dispatcher
self.log = logger
self.name = devname
# check config for problems
# only accept config items specified in CONFIG
# only accept config items specified in PARAMS
for k, v in cfgdict.items():
if k not in self.CONFIG:
raise ConfigError('Device %s:config Parameter %r not unterstood!' % (self.name, k))
# complain if a CONFIG entry has no default value and is not specified in cfgdict
for k, v in self.CONFIG.items():
if k not in self.PARAMS:
raise ConfigError('Device %s:config Parameter %r '
'not unterstood!' % (self.name, k))
# complain if a PARAM entry has no default value and
# is not specified in cfgdict
for k, v in self.PARAMS.items():
if k not in cfgdict:
if 'default' not in v:
raise ConfigError('Config Parameter %r was not given and not default value exists!' % k)
cfgdict[k] = v['default'] # assume default value was given.
# now 'apply' config, passing values through the validators and store as attributes
if v.default is Ellipsis:
# Ellipsis is the one single value you can not specify....
raise ConfigError('Device %s: Parameter %r has no default '
'value and was not given in config!'
% (self.name, k))
# assume default value was given
cfgdict[k] = v.default
# now 'apply' config:
# pass values through the validators and store as attributes
for k, v in cfgdict.items():
# apply validator, complain if type does not fit
validator = self.CONFIG[k].validator
validator = self.PARAMS[k].validator
if validator is not None:
# only check if validator given
try:
v = validator(v)
except ValueError as e:
raise ConfigError("Device %s: config paramter %r:\n%r" % (self.name, k, e))
raise ConfigError('Device %s: config parameter %r:\n%r'
% (self.name, k, e))
# XXX: with or without prefix?
setattr(self, 'config_' + k, v)
# set default parameter values as inital values
for k, v in self.PARAMS.items():
# apply validator, complain if type does not fit
validator = v.validator
value = v.default
if validator is not None:
# only check if validator given
value = validator(value)
setattr(self, k, v)
def init(self):
@ -147,12 +165,16 @@ class Device(object):
class Readable(Device):
"""Basic readable device, providing the RO parameter 'value' and 'status'"""
"""Basic readable device
providing the readonly parameter 'value' and 'status'
"""
PARAMS = {
'value' : PARAM('current value of the device', readonly=True),
'status' : PARAM('current status of the device',
readonly=True),
'value': PARAM('current value of the device', readonly=True, default=0.),
'status': PARAM('current status of the device', default=status.OK,
readonly=True),
}
def read_value(self, maxage=0):
raise NotImplementedError
@ -161,10 +183,13 @@ class Readable(Device):
class Driveable(Readable):
"""Basic Driveable device, providing a RW target parameter to those of a Readable"""
"""Basic Driveable device
providing a settable 'target' parameter to those of a Readable
"""
PARAMS = {
'target' : PARAM('target value of the device'),
'target': PARAM('target value of the device', default=0.),
}
def write_target(self, value):
raise NotImplementedError

View File

@ -32,62 +32,65 @@ from validators import floatrange, positive, mapping
from lib import clamp
hack = []
class Cryostat(Driveable):
"""simulated cryostat with heat capacity on the sample, cooling power and thermal transfer functions"""
CONFIG = dict(
"""simulated cryostat with:
- heat capacity of the sample
- cooling power
- thermal transfer between regulation and samplen
"""
PARAMS = dict(
jitter=CONFIG("amount of random noise on readout values",
validator=floatrange(0, 1), default=1,
),
T_start=CONFIG("starting temperature for simulation",
validator=positive, default=2,
validator=floatrange(0, 1),
export=False,
),
T_start=CONFIG("starting temperature for simulation",
validator=positive, export=False,
),
looptime=CONFIG("timestep for simulation",
validator=positive, default=1, unit="s",
),
)
PARAMS = dict(
export=False,
),
ramp=PARAM("ramping speed in K/min",
validator=floatrange(0, 1e3), default=1,
),
),
setpoint=PARAM("ramping speed in K/min",
validator=float, default=1, readonly=True,
),
),
maxpower=PARAM("Maximum heater power in W",
validator=float, default=0, readonly=True, unit="W",
),
),
heater=PARAM("current heater setting in %",
validator=float, default=0, readonly=True, unit="%",
),
),
heaterpower=PARAM("current heater power in W",
validator=float, default=0, readonly=True, unit="W",
),
),
target=PARAM("target temperature in K",
validator=float, default=0, unit="K",
),
),
p=PARAM("regulation coefficient 'p' in %/K",
validator=positive, default=40, unit="%/K",
),
),
i=PARAM("regulation coefficient 'i'",
validator=floatrange(0, 100), default=10,
),
),
d=PARAM("regulation coefficient 'd'",
validator=floatrange(0, 100), default=2,
),
),
mode=PARAM("mode of regulation",
validator=mapping('ramp', 'pid', 'openloop'), default='pid',
),
),
tolerance=PARAM("temperature range for stability checking",
validator=floatrange(0, 100), default=0.1, unit='K',
),
validator=floatrange(0, 100), default=0.1, unit='K',
),
window=PARAM("time window for stability checking",
validator=floatrange(1, 900), default=30, unit='s',
),
),
timeout=PARAM("max waiting time for stabilisation check",
validator=floatrange(1, 36000), default=900, unit='s',
),
validator=floatrange(1, 36000), default=900, unit='s',
),
)
def init(self):
@ -95,16 +98,15 @@ class Cryostat(Driveable):
self._thread = threading.Thread(target=self.thread)
self._thread.daemon = True
self._thread.start()
#XXX: hack!!! use a singleton as registry for the other devices to access this one...
hack.append(self)
def read_status(self):
# instead of asking a 'Hardware' take the value from the simulation thread
# instead of asking a 'Hardware' take the value from the simulation
return self.status
def read_value(self, maxage=0):
# return regulation value (averaged regulation temp)
return self.regulationtemp + self.config_jitter * (0.5 - random.random())
return self.regulationtemp + \
self.config_jitter * (0.5 - random.random())
def read_target(self, maxage=0):
return self.target
@ -119,12 +121,13 @@ class Cryostat(Driveable):
def write_maxpower(self, newpower):
# rescale heater setting in % to keep the power
self.heater = max(0, min(100, self.heater * self.maxpower / float(newpower)))
heat = max(0, min(100, self.heater * self.maxpower / float(newpower)))
self.heater = heat
self.maxpower = newpower
def doStop(self):
# stop the ramp by setting current value as target
# XXX: there may be use case where setting the current temp may be better
# stop the ramp by setting current setpoint as target
# XXX: discussion: take setpoint or current value ???
self.write_target(self.setpoint)
#
@ -176,7 +179,8 @@ class Cryostat(Driveable):
# local state keeping:
regulation = self.regulationtemp
sample = self.sampletemp
window = [] # keep history values for stability check
# keep history values for stability check
window = []
timestamp = time.time()
heater = 0
lastflow = 0
@ -210,7 +214,8 @@ class Cryostat(Driveable):
newregulation = max(0, regulation +
regdelta / self.__coolerCP(regulation) * h)
# b) see
# http://brettbeauregard.com/blog/2011/04/improving-the-beginners-pid-introduction/
# http://brettbeauregard.com/blog/2011/04/
# improving-the-beginners-pid-introduction/
if self.mode != 'openloop':
# fix artefacts due to too big timesteps
# actually i would prefer reducing looptime, but i have no
@ -320,4 +325,3 @@ class Cryostat(Driveable):
self._stopflag = True
if self._thread and self._thread.isAlive():
self._thread.join()

View File

@ -23,42 +23,51 @@
import random
from devices.core import Readable, Driveable, CONFIG, PARAM
from devices.core import Readable, Driveable, PARAM
from validators import floatrange
class LN2(Readable):
"""Just a readable.
class name indicates it to be a sensor for LN2, but the implementation may do anything"""
class name indicates it to be a sensor for LN2,
but the implementation may do anything
"""
def read_value(self, maxage=0):
return round(100*random.random(), 1)
class Heater(Driveable):
"""Just a driveable.
class name indicates it to be some heating element, but the implementation may do anything"""
CONFIG = {
'maxheaterpower' : CONFIG('maximum allowed heater power',
validator=floatrange(0, 100), unit='W'),
class name indicates it to be some heating element,
but the implementation may do anything
"""
PARAMS = {
'maxheaterpower': PARAM('maximum allowed heater power',
validator=floatrange(0, 100), unit='W'),
}
def read_value(self, maxage=0):
return round(100*random.random(), 1)
def write_target(self, target):
pass
class Temp(Driveable):
"""Just a driveable.
class name indicates it to be some temperature controller, but the implementation may do anything"""
CONFIG = {
'sensor' : CONFIG("Sensor number or calibration id",
validator=str),
class name indicates it to be some temperature controller,
but the implementation may do anything
"""
PARAMS = {
'sensor': PARAM("Sensor number or calibration id",
validator=str, readonly=True),
}
def read_value(self, maxage=0):
return round(100*random.random(), 1)
def write_target(self, target):
pass

View File

@ -22,11 +22,14 @@
# *****************************************************************************
"""error class for our little framework"""
class SECoPServerError(Exception):
pass
class ConfigError(SECoPServerError):
pass
class ProgrammingError(SECoPServerError):
pass

View File

@ -22,16 +22,17 @@
"""Define helpers"""
import logging
from os import path
class attrdict(dict):
"""a normal dict, providing access also via attributes"""
def __getattr__(self, key):
return self[key]
def __setattr__(self, key, value):
self[key] = value
def clamp(_min, value, _max):
"""return the median of 3 values,
@ -41,26 +42,15 @@ def clamp(_min, value, _max):
# return median, i.e. clamp the the value between min and max
return sorted([_min, value, _max])[1]
def get_class(spec):
"""loads a class given by string in dotted notaion (as python would do)"""
modname, classname = spec.rsplit('.', 1)
import importlib
# module = importlib.import_module(modname)
module = __import__(spec)
module = importlib.import_module(modname)
# module = __import__(spec)
return getattr(module, classname)
def make_logger(inst='server', name='', base_path='', loglevel=logging.INFO):
# XXX: rework this! (outsource to a logging module...)
if name:
inst = '%s %s' % (inst, name)
logging.basicConfig(level=loglevel, format='%(asctime)s %(levelname)s %(message)s')
logger = logging.getLogger(inst)
logger.setLevel(loglevel)
fh = logging.FileHandler(path.join(base_path, 'log', (name or inst) + '.log'))
fh.setLevel(loglevel)
logger.addHandler(fh)
return logger
# moved below definitions to break import cycle
from pidfile import *
@ -73,4 +63,3 @@ if __name__ == '__main__':
d.c = 9
d['d'] = 'c'
assert d[d.d] == 9

View File

@ -33,21 +33,23 @@ def read_pidfile(pidfile):
try:
with open(pidfile, 'r') as f:
return int(f.read())
except OSError:
except (OSError, IOError):
return None
def remove_pidfile(pidfile):
"""remove the given pidfile, typically at end of the process"""
os.remove(pidfile)
def write_pidfile(pidfile, pid):
"""write the given pid to the given pidfile"""
with open(pidfile, 'w') as f:
f.write('%d\n' % pid)
atexit.register(remove_pidfile, pidfile)
def check_pidfile(pidfile):
"""check if the process from a given pidfile is still running"""
pid = read_pidfile(pidfile)
return False if pid is None else psutil.pid_exists(pid)

View File

@ -1,133 +0,0 @@
# -*- 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>
#
# *****************************************************************************
"""Define helpers"""
import os
import psutil
import daemonize
import ConfigParser
from lib import read_pidfile, write_pidfile, get_class, make_logger
from server import DeviceServer as Server
from errors import ConfigError
__ALL__ = ['kill_server', 'start_server']
def kill_server(pidfile):
"""kill a server specified by a pidfile"""
pid = read_pidfile(pidfile)
if pid is None:
# already dead/not started yet
return
# get process for this pid
for proc in psutil.process_iter():
if proc.pid == pid:
break
proc.terminate()
proc.wait(3)
proc.kill()
def start_server(srvname, base_path, loglevel, daemon=False):
"""start a server, part1
handle the daemonizing and logging stuff and call the second step
"""
pidfile = os.path.join(base_path, 'pid', srvname + '.pid')
if daemon:
# dysfunctional :(
daemonproc = daemonize.Daemonize("server %s" % srvname,
pid=pidfile,
action=lambda: startup(srvname, base_path, loglevel),
)
daemonproc.start()
else:
write_pidfile(pidfile, os.getpid())
startup(srvname, base_path, loglevel) # blocks!
# unexported stuff here
def startup(srvname, base_path, loglevel):
"""really start a server (part2)
loads the config, initiate all objects, link them together
and finally start the interface server.
Never returns. (may raise)
"""
cfgfile = os.path.join(base_path, 'etc', srvname + '.cfg')
logger = make_logger('server', srvname, base_path=base_path, loglevel=loglevel)
logger.debug("parsing %r" % cfgfile)
parser = ConfigParser.SafeConfigParser()
if not parser.read([cfgfile]):
logger.error("Couldn't read cfg file !")
raise ConfigError("Couldn't read cfg file %r" % cfgfile)
# evaluate Server specific stuff
if not parser.has_section('server'):
logger.error("cfg file needs a 'server' section!")
raise ConfigError("cfg file %r needs a 'server' section!" % cfgfile)
serveropts = dict(item for item in parser.items('server'))
# check serveropts (init server)
# this raises if something wouldn't work
logger.debug("Creating device server")
server = Server(logger, serveropts)
# iterate over all sections, checking for devices
deviceopts = []
for section in parser.sections():
if section == "server":
continue # already handled, see above
if section.lower().startswith("device"):
# device section
devname = section[len('device '):] # omit leading 'device ' string
devopts = dict(item for item in parser.items(section))
if 'class' not in devopts:
logger.error("Device %s needs a class option!")
raise ConfigError("cfgfile %r: Device %s needs a class option!" % (cfgfile, devname))
# try to import the class, raise if this fails
devopts['class'] = get_class(devopts['class'])
# all went well so far
deviceopts.append([devname, devopts])
# check devices by creating them
devs = {}
for devname, devopts in deviceopts:
devclass = devopts.pop('class')
# create device
logger.debug("Creating Device %r" % devname)
devobj = devclass(devname, server, logger, devopts)
devs[devname] = devobj
# connect devices with server
for devname, devobj in devs.items():
logger.info("registering device %r" % devname)
server.register_device(devobj, devname)
# also call init on the devices
logger.debug("device.init()")
devobj.init()
# handle requests until stop is requsted
logger.info('startup done, handling transport messages')
server.serve_forever()

47
src/logger.py Normal file
View File

@ -0,0 +1,47 @@
# -*- 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>
#
# *****************************************************************************
"""Loggers"""
import logging
from os import path
from paths import log_path
def get_logger(inst='', loglevel=logging.INFO):
loglevelmap = {'debug': logging.DEBUG,
'info': logging.INFO,
'warning': logging.WARNING,
'error': logging.ERROR,
}
loglevel = loglevelmap.get(loglevel, loglevel)
logging.basicConfig(level=loglevel,
format='#[%(asctime)-15s][%(levelname)s]: %(message)s')
logger = logging.getLogger(inst)
logger.setLevel(loglevel)
fh = logging.FileHandler(path.join(log_path, inst + '.log'))
fh.setLevel(loglevel)
logger.addHandler(fh)
logging.root.addHandler(fh) # ???
return logger

33
src/paths.py Normal file
View File

@ -0,0 +1,33 @@
# -*- 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>
#
# *****************************************************************************
"""Pathes. how to find what and where..."""
import sys
from os import path
basepath = path.abspath(path.join(sys.path[0], '..'))
etc_path = path.join(basepath, 'etc')
pid_path = path.join(basepath, 'pid')
log_path = path.join(basepath, 'log')
sys.path[0] = path.join(basepath, 'src')

View File

@ -0,0 +1,22 @@
# -*- 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>
#
# *****************************************************************************
"""SECoP protocl specific stuff"""

View File

@ -22,7 +22,9 @@
"""Define SECoP Device classes
also define helpers to derive properties of the device"""
"""
# XXX: is this still needed ???
# see devices.core ....
from lib import attrdict
from protocol import status
@ -34,27 +36,36 @@ class Device(object):
all others derive from this"""
name = None
def read_status(self):
raise NotImplementedError('All Devices need a Status!')
def read_name(self):
return self.name
class Readable(Device):
"""A Readable Device"""
unit = ''
def read_value(self):
raise NotImplementedError('A Readable MUST provide a value')
def read_unit(self):
return self.unit
class Writeable(Readable):
"""Writeable can be told to change it's vallue"""
target = None
def read_target(self):
return self.target
def write_target(self, target):
self.target = target
class Driveable(Writeable):
"""A Moveable which may take a while to reach its target,
@ -62,32 +73,3 @@ class Driveable(Writeable):
def do_stop(self):
raise NotImplementedError('A Driveable MUST implement the STOP() '
'command')
def get_device_pars(dev):
"""return a mapping of the devices parameter names to some
'description'"""
res = {}
for n in dir(dev):
if n.startswith('read_'):
pname = n[5:]
entry = attrdict(readonly=True, description=getattr(dev, n).__doc__)
if hasattr(dev, 'write_%s' % pname):
entry['readonly'] = False
res[pname] = entry
return res
def get_device_cmds(dev):
"""return a mapping of the devices command names to some
'description'"""
res = {}
for n in dir(dev):
if n.startswith('do_'):
cname = n[5:]
func = getattr(dev, n)
# XXX: use inspect!
entry = attrdict(description=func.__doc__, args='unknown')
res[cname] = entry
return res

270
src/protocol/dispatcher.py Normal file
View File

@ -0,0 +1,270 @@
# -*- 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>
#
# *****************************************************************************
"""Dispatcher for SECoP Messages
Interface to the service offering part:
- 'handle_request(connectionobj, data)' handles incoming request
will call 'queue_request(data)' on connectionobj before returning
- 'add_connection(connectionobj)' registers new connection
- 'remove_connection(connectionobj)' removes now longer functional connection
- may at any time call 'queue_async_request(connobj, data)' on the connobj
Interface to the devices:
- add_device(devname, devobj, export=True) registers a new device under the
given name, may also register it for exporting (making accessible)
- get_device(devname) returns the requested device or None
- remove_device(devname_or_obj): removes the device (during shutdown)
internal stuff which may be called
- get_devices(): return a list of devices + descriptive data as dict
- get_device_params():
return a list of paramnames for this device + descriptive data
"""
import threading
from messages import *
class Dispatcher(object):
def __init__(self, logger, options):
self.log = logger
# XXX: move framing and encoding to interface!
self.framing = options.pop('framing')
self.encoding = options.pop('encoding')
# map ALL devname -> devobj
self._dispatcher_devices = {}
# list of EXPORTED devices
self._dispatcher_export = []
# list all connections
self._dispatcher_connections = []
# map eventname -> list of subscribed connections
self._dispatcher_subscriptions = {}
self._dispatcher_lock = threading.RLock()
def handle_request(self, conn, data):
"""handles incoming request
will call 'queue.request(data)' on conn to send reply before returning
"""
self.log.debug('Dispatcher: handling data: %r' % data)
# play thread safe !
with self._dispatcher_lock:
# de-frame data
frames = self.framing.decode(data)
self.log.debug('Dispatcher: frames=%r' % frames)
for frame in frames:
reply = None
# decode frame
msg = self.encoding.decode(frame)
self.log.debug('Dispatcher: msg=%r' % msg)
# act upon requestobj
msgtype = msg.TYPE
msgname = msg.NAME
msgargs = msg
# generate reply (coded and framed)
if msgtype != 'request':
reply = ProtocolErrorReply(msg)
else:
self.log.debug('Looking for handle_%s' % msgname)
handler = getattr(self, 'handle_%s' % msgname, None)
if handler:
reply = handler(msgargs)
else:
self.log.debug('Can not handle msg %r' % msg)
reply = self.unhandled(msgname, msgargs)
if reply:
conn.queue_reply(self._format_reply(reply))
# queue reply viy conn.queue_reply(data)
def _format_reply(self, reply):
msg = self.encoding.encode(reply)
frame = self.framing.encode(msg)
return frame
def announce_update(self, device, pname, value):
"""called by devices param setters to notify subscribers of new values
"""
eventname = '%s/%s' % (self.get_device(device).name, pname)
subscriber = self._dispatcher_subscriptions.get(eventname, None)
if subscriber:
reply = AsyncDataUnit(device=self.get_device(device).name,
param=pname,
value=str(value),
timestamp=time.time(),
)
data = self._format_reply(reply)
for conn in subscriber:
conn.queue_async_reply(data)
def subscribe(self, conn, device, pname):
eventname = '%s/%s' % (self.get_device(device).name, pname)
self._dispatcher_subscriptions.getdefault(eventname, set()).add(conn)
def add_connection(self, conn):
"""registers new connection"""
self._dispatcher_connections.append(conn)
def remove_connection(self, conn):
"""removes now longer functional connection"""
if conn in self._dispatcher_connections:
self._dispatcher_connections.remove(conn)
# XXX: also clean _dispatcher_subscriptions !
def register_device(self, devobj, devname, export=True):
self._dispatcher_devices[devname] = devobj
if export:
self._dispatcher_export.append(devname)
def get_device(self, devname):
dev = self._dispatcher_devices.get(devname, None)
self.log.debug('get_device(%r) -> %r' % (devname, dev))
return dev
def remove_device(self, devname_or_obj):
devobj = self.get_device(devname_or_obj) or devname_or_obj
devname = devobj.name
if devname in self._dispatcher_export:
self._dispatcher_export.remove(devname)
self._dispatcher_devices.pop(devname)
# XXX: also clean _dispatcher_subscriptions
def list_device_names(self):
# return a copy of our list
return self._dispatcher_export[:]
def list_devices(self):
dn = []
dd = {}
for devname in self._dispatcher_export:
dn.append(devname)
dev = self.get_device(devname)
descriptive_data = {
'class': dev.__class__,
#'bases': dev.__bases__,
'parameters': dev.PARAMS.keys(),
'commands': dev.CMDS.keys(),
# XXX: what else?
}
dd[devname] = descriptive_data
return dn, dd
def list_device_params(self, devname):
if devname in self._dispatcher_export:
# XXX: omit export=False params!
return self.get_device(devname).PARAMS
return {}
# now the (defined) handlers for the different requests
def handle_Help(self, msg):
return HelpReply()
def handle_ListDevices(self, msgargs):
# XXX: choose!
#return ListDevicesReply(self.list_device_names())
return ListDevicesReply(*self.list_devices())
def handle_ListDeviceParams(self, msgargs):
devobj = self.get_device(msgargs.device)
if devobj:
return ListDeviceParamsReply(msgargs.device,
self.get_device_params(devobj))
else:
return NoSuchDeviceErrorReply(msgargs.device)
def handle_ReadValue(self, msgargs):
devobj = self.get_device(msgargs.device)
if devobj:
return ReadValueReply(msgargs.device, devobj.read_value(),
timestamp=time.time())
else:
return NoSuchDeviceErrorReply(msgargs.device)
def handle_ReadParam(self, msgargs):
devobj = self.get_device(msgargs.device)
if devobj:
readfunc = getattr(devobj, 'read_%s' % msgargs.param, None)
if readfunc:
return ReadParamReply(msgargs.device, msgargs.param,
readfunc(), timestamp=time.time())
else:
return NoSuchParamErrorReply(msgargs.device, msgargs.param)
else:
return NoSuchDeviceErrorReply(msgargs.device)
def handle_WriteParam(self, msgargs):
devobj = self.get_device(msgargs.device)
if devobj:
writefunc = getattr(devobj, 'write_%s' % msgargs.param, None)
if writefunc:
readbackvalue = writefunc(msgargs.value) or msgargs.value
# trigger async updates
setattr(devobj, msgargs.param, readbackvalue)
return WriteParamReply(msgargs.device, msgargs.param,
readbackvalue,
timestamp=time.time())
else:
if getattr(devobj, 'read_%s' % msgargs.param, None):
return ParamReadonlyErrorReply(msgargs.device,
msgargs.param)
else:
return NoSuchParamErrorReply(msgargs.device,
msgargs.param)
else:
return NoSuchDeviceErrorReply(msgargs.device)
def handle_RequestAsyncData(self, msgargs):
return ErrorReply('AsyncData is not (yet) supported')
def handle_ListOfFeatures(self, msgargs):
# no features supported (yet)
return ListOfFeaturesReply([])
def handle_ActivateFeature(self, msgargs):
return ErrorReply('Features are not (yet) supported')
def unhandled(self, msgname, msgargs):
"""handler for unhandled Messages
(no handle_<messagename> method was defined)
"""
self.log.error('IGN: got unhandled request %s' % msgname)
return ErrorReply('Got Unhandled Request')
def parse_message(self, message):
# parses a message and returns
# msgtype, msgname and parameters of message (as dict)
msgtype = 'unknown'
msgname = 'unknown'
if isinstance(message, ErrorReply):
msgtype = message.TYPE
msgname = message.__class__.__name__[:-len('Reply')]
elif isinstance(message, Request):
msgtype = message.TYPE
msgname = message.__class__.__name__[:-len('Request')]
elif isinstance(message, Reply):
msgtype = message.TYPE
msgname = message.__class__.__name__[:-len('Reply')]
return msgtype, msgname, \
attrdict([(k, getattr(message, k)) for k in message.ARGS])

View File

@ -0,0 +1,31 @@
# -*- 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>
#
# *****************************************************************************
"""provide server interfaces to be used by clients"""
from tcp import TCPServer
INTERFACES = {
'tcp': TCPServer,
}
# for 'from protocol.interface import *' to only import the dict
__ALL__ = ['INTERFACES']

View File

@ -0,0 +1,103 @@
#!/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>
#
# *****************************************************************************
"""provides tcp interface to the SECoP Server"""
import os
import socket
import collections
import SocketServer
DEF_PORT = 10767
MAX_MESSAGE_SIZE = 1024
class TCPRequestHandler(SocketServer.BaseRequestHandler):
def setup(self):
self.log = self.server.log
self._queue = collections.deque(maxlen=100)
def handle(self):
"""handle a new tcp-connection"""
# copy state info
mysocket = self.request
clientaddr = self.client_address
serverobj = self.server
self.log.debug("handling new connection from %s" % repr(clientaddr))
# notify dispatcher of us
serverobj.dispatcher.add_connection(self)
mysocket.settimeout(.3)
mysocket.setblocking(False)
# start serving
while True:
# send replys fist, then listen for requests, timing out after 0.1s
while self._queue:
mysocket.sendall(self._queue.popleft())
# XXX: improve: use polling/select here?
try:
data = mysocket.recv(MAX_MESSAGE_SIZE)
except (socket.timeout, socket.error) as e:
continue
# XXX: should use select instead of busy polling
if not data:
continue
# dispatcher will queue the reply before returning
serverobj.dispatcher.handle_request(self, data)
def queue_async_reply(self, data):
"""called by dispatcher for async data units"""
self._queue.append(data)
def queue_reply(self, data):
"""called by dispatcher to queue (sync) replies"""
# sync replies go first!
self._queue.appendleft(data)
def finish(self):
"""called when handle() terminates, i.e. the socket closed"""
# notify dispatcher
self.server.dispatcher.remove_connection(self)
# close socket
try:
self.request.shutdown(socket.SHUT_RDWR)
finally:
self.request.close()
class TCPServer(SocketServer.ThreadingTCPServer):
daemon_threads = True
allow_reuse_address = True
def __init__(self, logger, serveropts, dispatcher):
self.dispatcher = dispatcher
self.log = logger
bindto = serveropts.pop('bindto', 'localhost')
portnum = int(serveropts.pop('bindport', DEF_PORT))
if ':' in bindto:
bindto, _port = bindto.rsplit(':')
portnum = int(_port)
self.log.debug("TCPServer binding to %s:%d" % (bindto, portnum))
SocketServer.ThreadingTCPServer.__init__(self, (bindto, portnum),
TCPRequestHandler,
bind_and_activate=True)
self.log.info("TCPServer initiated")

View File

@ -0,0 +1,27 @@
# -*- 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>
#
# *****************************************************************************
"""provide a zmq server"""
# tbd.
# use zmq frames??
# handle async and sync with different zmq ports?

View File

@ -22,132 +22,165 @@
"""Define SECoP Messages"""
from lib import attrdict
import time
from device import get_device_pars, get_device_cmds
class Request(object):
"""Base class for all Requests"""
pars = []
def __repr__(self):
pars = ', '.join('%s=%r' % (k, self.__dict__[k]) for k in self.pars)
s = '%s(%s)' % (self.__class__.__name__, pars)
return s
class Reply(object):
"""Base class for all Replies"""
pars = []
def __repr__(self):
pars = ', '.join('%s=%r' % (k, self.__dict__[k]) for k in self.pars)
s = '%s(%s)' % (self.__class__.__name__, pars)
return s
REQUEST = 'request'
REPLY = 'reply'
ERROR = 'error'
# base classes
class Message(object):
ARGS = []
def __init__(self, *args, **kwds):
names = self.ARGS[:]
if len(args) > len(names):
raise TypeError('%s.__init__() takes only %d argument(s) (%d given)' %
(self.__class__, len(names), len(args)))
for arg in args:
self.__dict__[names.pop(0)] = arg
# now check keyworded args if any
for k, v in kwds.items():
if k not in names:
if k in self.ARGS:
raise TypeError('__init__() got multiple values for '
'keyword argument %r' % k)
raise TypeError('__init__() got an unexpected keyword '
'argument %r' % k)
names.remove(k)
self.__dict__[k] = v
if names:
raise TypeError('__init__() takes at least %d arguments (%d given)'
% len(self.ARGS), len(args)+len(kwds))
self.NAME = (self.__class__.__name__[:-len(self.TYPE)] or
self.__class__.__name__)
class Request(Message):
TYPE = REQUEST
class Reply(Message):
TYPE = REPLY
class ErrorReply(Message):
TYPE = ERROR
# actuall message objects
class ListDevicesRequest(Request):
pass
class ListDevicesReply(Reply):
pars = ['list_of_devices']
def __init__(self, args):
self.list_of_devices = args
ARGS = ['list_of_devices', 'descriptive_data']
class ListDeviceParamsRequest(Request):
pars = ['device']
def __init__(self, device):
self.device = device
ARGS = ['device']
class ListDeviceParamsReply(Reply):
pars = ['device', 'params']
def __init__(self, device, params):
self.device = device
self.params = params
ARGS = ['device', 'params']
class ReadValueRequest(Request):
pars = ['device', 'maxage']
def __init__(self, device, maxage=0):
self.device = device
self.maxage = maxage
ARGS = ['device', 'maxage']
class ReadValueReply(Reply):
pars = ['device', 'value', 'timestamp', 'error', 'unit']
def __init__(self, device, value, timestamp=0, error=0, unit=None):
self.device = device
self.value = value
self.timestamp = timestamp
self.error = error
self.unit = unit
ARGS = ['device', 'value', 'timestamp', 'error', 'unit']
class ReadParamRequest(Request):
pars = ['device', 'param', 'maxage']
def __init__(self, device, param, maxage=0):
self.device = device
self.param = param
self.maxage = maxage
ARGS = ['device', 'param', 'maxage']
class ReadParamReply(Reply):
pars = ['device', 'param', 'value', 'timestamp', 'error', 'unit']
def __init__(self, device, param, value, timestamp=0, error=0, unit=None):
self.device = device
self.param = param
self.value = value
self.timestamp = timestamp
self.error = error
self.unit = unit
ARGS = ['device', 'param', 'value', 'timestamp', 'error', 'unit']
class WriteParamRequest(Request):
pars = ['device', 'param', 'value']
def __init__(self, device, param, value):
self.device = device
self.param = param
self.value = value
ARGS = ['device', 'param', 'value']
class WriteParamReply(Reply):
pars = ['device', 'param', 'readback_value', 'timestamp', 'error', 'unit']
def __init__(self, device, param, readback_value, timestamp=0, error=0,
unit=None):
self.device = device
self.param = param
self.readback_value = readback_value
self.timestamp = timestamp
self.error = error
self.unit = unit
ARGS = ['device', 'param', 'readback_value', 'timestamp', 'error', 'unit']
class RequestAsyncDataRequest(Request):
pars = ['device', 'params']
def __init__(self, device, *args):
self.device = device
self.params = args
ARGS = ['device', 'params']
class RequestAsyncDataReply(Reply):
pars = ['device', 'paramvalue_list']
def __init__(self, device, *args):
self.device = device
self.paramvalue_list = args
ARGS = ['device', 'paramvalue_list']
class AsyncDataUnit(ReadParamReply):
pass
ARGS = ['device', 'param', 'value', 'timestamp', 'error', 'unit']
class ListOfFeaturesRequest(Request):
pass
class ListOfFeaturesReply(Reply):
pars = ['features']
def __init__(self, *args):
self.features = args
ARGS = ['features']
class ActivateFeatureRequest(Request):
pars = ['feature']
def __init__(self, feature):
self.feature = feature
ARGS = ['feature']
class ActivateFeatureReply(Reply):
# Ack style or Error
# may be should reply with active features?
# maybe should reply with active features?
pass
class ProtocollError(ErrorReply):
ARGS = ['msgtype', 'msgname', 'msgargs']
class ErrorReply(Reply):
ARGS = ['error']
class NoSuchDeviceErrorReply(ErrorReply):
ARGS = ['device']
class NoSuchParamErrorReply(ErrorReply):
ARGS = ['device', 'param']
class ParamReadonlyErrorReply(ErrorReply):
ARGS = ['device', 'param']
class UnsupportedFeatureErrorReply(ErrorReply):
ARGS = ['feature']
class NoSuchCommandErrorReply(ErrorReply):
ARGS = ['device', 'command']
class CommandFailedErrorReply(ErrorReply):
ARGS = ['device', 'command']
class InvalidParamValueErrorReply(ErrorReply):
ARGS = ['device', 'param', 'value']
# Fun!
class HelpRequest(Request):
pass
class HelpReply(Reply):
pass
@ -155,203 +188,5 @@ FEATURES = [
'Feature1',
'Feature2',
'Feature3',
'Future',
]
# Error replies:
class ErrorReply(Reply):
pars = ['error']
def __init__(self, error):
self.error = error
class NoSuchDeviceErrorReply(ErrorReply):
pars = ['device']
def __init__(self, device):
self.device = device
class NoSuchParamErrorReply(ErrorReply):
pars = ['device', 'param']
def __init__(self, device, param):
self.device = device
self.param = param
class ParamReadonlyErrorReply(ErrorReply):
pars = ['device', 'param']
def __init__(self, device, param):
self.device = device
self.param = param
class UnsupportedFeatureErrorReply(ErrorReply):
pars = ['feature']
def __init__(self, feature):
self.feature = feature
class NoSuchCommandErrorReply(ErrorReply):
pars = ['device', 'command']
def __init__(self, device, command):
self.device = device
self.command = command
class CommandFailedErrorReply(ErrorReply):
pars = ['device', 'command']
def __init__(self, device, command):
self.device = device
self.command = command
class InvalidParamValueErrorReply(ErrorReply):
pars = ['device', 'param', 'value']
def __init__(self, device, param, value):
self.device = device
self.param = param
self.value = value
class MessageHandler(object):
"""puts meaning to the request objects"""
def handle_ListDevices(self, msgargs):
return ListDevicesReply(self.listDevices())
def handle_ListDeviceParams(self, msgargs):
devobj = self.getDevice(msgargs.device)
if devobj:
return ListDeviceParamsReply(msgargs.device,
get_device_pars(devobj))
else:
return NoSuchDeviceErrorReply(msgargs.device)
def handle_ReadValue(self, msgargs):
devobj = self.getDevice(msgargs.device)
if devobj:
return ReadValueReply(msgargs.device, devobj.read_value(),
timestamp=time.time())
else:
return NoSuchDeviceErrorReply(msgargs.device)
def handle_ReadParam(self, msgargs):
devobj = self.getDevice(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):
devobj = self.getDevice(msgargs.device)
if devobj:
writefunc = getattr(devobj, 'write_%s' % msgargs.param, None)
if writefunc:
readbackvalue = writefunc(msgargs.value) or msgargs.value
return WriteParamReply(msgargs.device, msgargs.param,
readbackvalue,
timestamp=time.time())
else:
if getattr(devobj, 'read_%s' % msgargs.param, None):
return ParamReadonlyErrorReply(msgargs.device,
msgargs.param)
else:
return NoSuchParamErrorReply(msgargs.device,
msgargs.param)
else:
return NoSuchDeviceErrorReply(msgargs.device)
def handle_RequestAsyncData(self, msgargs):
return ErrorReply('AsyncData is not (yet) supported')
def handle_ListOfFeatures(self, msgargs):
# no features supported (yet)
return ListOfFeaturesReply([])
def handle_ActivateFeature(self, msgargs):
return ErrorReply('Features are not (yet) supported')
def unhandled(self, msgname, msgargs):
"""handler for unhandled Messages
(no handle_<messagename> method was defined)
"""
self.log.error('IGN: got unhandled request %s' % msgname)
return ErrorReply('Got Unhandled Request')
def parse(message):
# parses a message and returns
# msgtype, msgname and parameters of message (as dict)
msgtype = 'unknown'
msgname = 'unknown'
if isinstance(message, ErrorReply):
msgtype = 'error'
msgname = message.__class__.__name__[:-len('Reply')]
elif isinstance(message, Request):
msgtype = 'request'
msgname = message.__class__.__name__[:-len('Request')]
elif isinstance(message, Reply):
msgtype = 'reply'
msgname = message.__class__.__name__[:-len('Reply')]
return msgtype, msgname, \
attrdict([(k, getattr(message, k)) for k in message.pars])
__ALL__ = ['ErrorReply',
'NoSuchDeviceErrorReply', 'NoSuchParamErrorReply'
'ParamReadonlyErrorReply', 'UnsupportedFeatureErrorReply',
'NoSuchCommandErrorReply', 'CommandFailedErrorReply',
'InvalidParamValueErrorReply',
'Reply',
'ListDevicesReply', 'ListDeviceParamsReply', 'ReadValueReply',
'ReadParamReply', 'WriteParamReply', 'RequestAsyncDataReply',
'AsyncDataUnit', 'ListOfFeaturesReply', 'ActivateFeatureReply',
'Request',
'ListDevicesRequest', 'ListDeviceParamsRequest', 'ReadValueRequest',
'ReadParamRequest', 'WriteParamRequest', 'RequestAsyncDataRequest',
'ListOfFeaturesRequest', 'ActivateFeatureRequest',
'parse', 'MessageHandler',
]
if __name__ == '__main__':
print "minimal testing: transport"
testcases = dict(
error=[ErrorReply(),
NoSuchDeviceErrorReply('device3'),
NoSuchParamErrorReply('device2', 'param3'),
ParamReadonlyErrorReply('device1', 'param1'),
UnsupportedFeatureErrorReply('feature5'),
NoSuchCommandErrorReply('device1', 'fance_command'),
CommandFailedErrorReply('device1', 'stop'),
InvalidParamValueErrorReply('device1', 'param2', 'STRING_Value'),
],
reply=[Reply(),
ListDevicesReply('device1', 'device2'),
ListDeviceParamsReply('device', ['param1', 'param2']),
ReadValueReply('device2', 3.1415),
ReadParamReply('device1', 'param2', 2.718),
WriteParamReply('device1', 'param2', 2.718),
RequestAsyncDataReply('device1', '?what to put here?'),
AsyncDataUnit('device1', 'param2', 2.718),
ListOfFeaturesReply('feature1', 'feature2'),
ActivateFeatureReply(),
],
request=[Request(),
ListDevicesRequest(),
ListDeviceParamsRequest('device1'),
ReadValueRequest('device2'),
ReadParamRequest('device1', 'param2'),
WriteParamRequest('device1', 'param2', 2.718),
RequestAsyncDataRequest('device1', ['param1', 'param2']),
ListOfFeaturesRequest(),
ActivateFeatureRequest('feature1'),
],
)
for msgtype, msgs in testcases.items():
print "___ testing %ss ___" % msgtype
for msg in msgs:
print msg.__class__.__name__, 'is', msgtype,
decoded = parse(msg)
if decoded[0] != msgtype:
print "\tFAIL, got %r but expected %r" %(decoded[0], msgtype)
else:
print "\tOk"
print

View File

@ -28,4 +28,3 @@ WARN = 300
UNSTABLE = 350
ERROR = 400
UNKNOWN = -1

View File

@ -1,149 +0,0 @@
#!/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>
#
# *****************************************************************************
"""provides transport layer of SECoP"""
# currently implements pickling Python-objects over plain TCP
# WARNING: This is not (really) portable to other languages!
import time
import socket
import SocketServer
try:
import cPickle as pickle
except ImportError:
import pickle
from server import DeviceServer
from messages import ListOfFeaturesRequest
DEF_PORT = 10767
MAX_MESSAGE_SIZE = 1024
def decodeMessage(msg):
"""transport layer message -> msg object"""
return pickle.loads(msg)
def encodeMessage(msgobj):
"""msg object -> transport layer message"""
return pickle.dumps(msgobj)
def encodeMessageFrame(msg):
"""add transport layer encapsulation/framing of messages"""
return '%s\n' % msg
def decodeMessageFrame(frame):
"""remove transport layer encapsulation/framing of messages"""
if '\n' in frame:
# WARNING: ignores everything after first '\n'
return frame.split('\n', 1)[0]
# invalid/incomplete frames return nothing here atm.
return None
class SECoPClient(object):
"""connects to a SECoPServer and provides communication"""
_socket = None
def connect(self, server='localhost'):
if self._socket:
raise Exception('%r is already connected!' % self)
if ':' not in server:
server = '%s:%d' % (server, DEF_PORT)
host, port = server.split(':')
port = int(port)
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._socket.connect((host, port))
self._negotiateServerSettings()
def close(self):
if not self._socket:
raise Exception('%r is not connected!' % self)
self._socket.shutdown(socket.SHUT_WR)
self._socket.shutdown(socket.SHUT_RDWR)
self._socket.close()
self._socket = None
def _sendRequest(self, request):
if not self._socket:
raise Exception('%r is not connected!' % self)
self._socket.send(encodeMessageFrame(encodeMessage(request)))
def _recvReply(self):
if not self._socket:
raise Exception('%r is not connected!' % self)
rawdata = ''
while True:
data = self._socket.recv(MAX_MESSAGE_SIZE)
if not data:
time.sleep(0.1)
# XXX: needs timeout mechanism!
continue
rawdata = rawdata + data
msg = decodeMessageFrame(rawdata)
if msg:
return decodeMessage(msg)
def _negotiateServerSettings(self):
self._sendRequest(ListOfFeaturesRequest())
print self._recvReply()
# XXX: fill with life!
class SECoPRequestHandler(SocketServer.BaseRequestHandler):
def handle(self):
"""handle a new tcp-connection"""
# self.client_address
mysocket = self.request
frame = ''
# start serving
while True:
_frame = mysocket.recv(MAX_MESSAGE_SIZE)
if not _frame:
time.sleep(0.1)
continue
frame = frame + _frame
msg = decodeMessageFrame(frame)
if msg:
requestObj = decodeMessage(msg)
replyObj = self.handle_request(requestObj)
mysocket.send(encodeMessageFrame(encodeMessage(replyObj)))
frame = ''
def handle_request(self, requestObj):
# XXX: handle connection/Server specific Requests
# pass other (Device) requests to the DeviceServer
return self.server.handle(requestObj)
class SECoPServer(SocketServer.ThreadingTCPServer, DeviceServer):
daemon_threads = False
def __init__(self, logger, serveropts):
bindto = serveropts.pop('bindto', 'localhost')
portnum = DEF_PORT
if ':' in bindto:
bindto, _port = bindto.rsplit(':')
portnum = int(_port)
logger.debug("binding to %s:%d" % (bindto, portnum))
super(SECoPServer, self).__init__((bindto, portnum),
SECoPRequestHandler, bind_and_activate=True)
logger.info("SECoPServer initiated")
logger.debug('serveropts remaining: %r' % serveropts)

View File

@ -0,0 +1,26 @@
#!/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>
#
# *****************************************************************************
"""provides transport layer encapsulation for SECoP"""
from framing import FRAMERS
from encoding import ENCODERS

View File

@ -0,0 +1,103 @@
#!/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>
#
# *****************************************************************************
"""Encoding/decoding Messages"""
# implement as class as they may need some internal 'state' later on
# (think compressors)
from protocol import messages
# Base classes
class MessageEncoder(object):
"""en/decode a single Messageobject"""
def encode(self, messageobj):
"""encodes the given message object into a frame"""
raise NotImplemented
def decode(self, frame):
"""decodes the given frame to a message object"""
raise NotImplemented
# now some Implementations
try:
import cPickle as pickle
except ImportError:
import pickle
import protocol.messages
class PickleEncoder(MessageEncoder):
def encode(self, messageobj):
"""msg object -> transport layer message"""
return pickle.dumps(messageobj)
def decode(self, encoded):
"""transport layer message -> msg object"""
return pickle.loads(encoded)
class TextEncoder(MessageEncoder):
def __init__(self):
# build safe namespace
ns = dict()
for n in dir(messages):
if n.endswith(('Request', 'Reply')):
ns[n] = getattr(messages, n)
self.namespace = ns
def encode(self, messageobj):
"""msg object -> transport layer message"""
# fun for Humans
if isinstance(messageobj, messages.HelpReply):
return "Error: try one of the following requests:\n" + \
'\n'.join(['%s(%s)' % (getattr(messages, m).__name__,
', '.join(getattr(messages, m).ARGS))
for m in dir(messages)
if m.endswith('Request')])
res = []
for k in messageobj.ARGS:
res.append('%s=%r' % (k, getattr(messageobj, k, None)))
result = '%s(%s)' % (messageobj.__class__.__name__, ', '.join(res))
return result
def decode(self, encoded):
"""transport layer message -> msg object"""
# WARNING: highly unsafe!
# think message='import os\nos.unlink('\')\n'
try:
return eval(encoded, self.namespace, {})
except SyntaxError:
return messages.HelpRequest()
ENCODERS = {
'pickle': PickleEncoder,
'text': TextEncoder,
}
__ALL__ = ['ENCODERS']

View File

@ -0,0 +1,134 @@
#!/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>
#
# *****************************************************************************
"""Encoding/decoding Frames"""
# Base class
class Framer(object):
"""Frames and unframes an encoded message
also transforms the encoded message to the 'wire-format' (and vise-versa)
note: not all MessageEncoders can use all Framers,
but the intention is to have this for as many as possible.
"""
def encode(self, *frames):
"""return the wire-data for the given messageframes"""
raise NotImplemented
def decode(self, data):
"""return a list of messageframes found in data"""
raise NotImplemented
def reset(self):
"""resets the de/encoding stage (clears internal information)"""
raise NotImplemented
# now some Implementations
class EOLFramer(Framer):
"""Text based message framer
messages are delimited by '\r\n'
upon reception the end of a message is detected by '\r\n','\n' or '\n\r'
"""
data = b''
def encode(self, *frames):
"""add transport layer encapsulation/framing of messages"""
return b'%s\r\n' % b'\r\n'.join(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 reset(self):
self.data = b''
class RLEFramer(Framer):
data = b''
frames_to_go = 0
def encode(self, *frames):
"""add transport layer encapsulation/framing of messages"""
# format is 'number of frames:[framelengt:frme]*N'
frdata = ['%d:%s' % (len(frame), frame) for frame in frames]
return b'%d:' + b''.join(frdata)
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 self.data:
if frames_to_go == 0:
if ':' in self.data:
# scan for and decode 'number of frames'
frnum, self.data = self.data.split(':', 1)
try:
self.frames_to_go = int(frnum)
except ValueError:
# can not recover, complain!
raise FramingError('invalid start of message found!')
else:
# not enough data to decode number of frames,
# return what we have
return res
while self.frames_to_go:
# there are still some (partial) frames stuck inside self.data
frlen, self.data = self.data.split(':', 1)
if len(self.data) >= frlen:
res.append(self.data[:frlen])
self.data = self.data[frlen:]
self.frames_to_go -= 1
else:
# not enough data for this frame, return what we have
return res
def reset(self):
self.data = b''
self.frames_to_go = 0
FRAMERS = {
'eol': EOLFramer,
'rle': RLEFramer,
}
__ALL__ = ['FRAMERS']

View File

@ -1,150 +0,0 @@
# -*- 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>
#
# *****************************************************************************
"""Define basic SECoP DeviceServer"""
import time
from protocol.messages import parse, ListDevicesRequest, ListDeviceParamsRequest, \
ReadParamRequest, ErrorReply, MessageHandler
class DeviceServer(MessageHandler):
def __init__(self, logger, serveropts):
self._devices = {}
self.log = logger
# XXX: check serveropts and raise if problems exist
# mandatory serveropts: interface=tcpip, encoder=pickle, frame=eol
# XXX: remaining opts are checked by the corresponding interface server
def serve_forever(self):
self.log.error("Serving not yet implemented!")
def register_device(self, deviceobj, devicename):
# make the server export a deviceobj under a given name.
# all exportet properties are taken from the device
if devicename in self._devices:
self.log.error('IGN: Device %r already registered' % devicename)
else:
self._devices[devicename] = deviceobj
deviceobj.name = devicename
def unregister_device(self, device_obj_or_name):
if not device_obj_or_name in self._devices:
self.log.error('IGN: Device %r not registered!' %
device_obj_or_name)
else:
del self._devices[device_obj_or_name]
# may need to do more
def get_device(self, devname):
"""returns the requested deviceObj or None"""
devobj = self._devices.get(devname, None)
return devobj
def list_devices(self):
return list(self._devices.keys())
def handle(self, msg):
# server got a message, handle it
msgtype, msgname, msgargs = parse(msg)
if msgtype != 'request':
self.log.error('IGN: Server only handles request, but got %s/%s!' %
(msgtype, msgname))
return
try:
self.log.info('handling message %s with %r' % (msgname, msgargs))
handler = getattr(self, 'handle_%s' * msgname, None)
if handler is None:
handler = self.unhandled
res = handler(msgargs)
self.log.info('replying with %r' % res)
return res
except Exception as err:
res = ErrorReply('Exception:\n%r' % err)
self.log.info('replying with %r' % res)
return res
if __name__ == '__main__':
from devices.core import Driveable
from protocol import status
class TestDevice(Driveable):
name = 'Unset'
unit = 'Oinks'
def read_status(self):
return status.OK
def read_value(self):
"""The devices main value"""
return 3.1415
def read_testpar1(self):
return 2.718
def read_fail(self):
raise KeyError()
def read_none(self):
pass
def read_NotImplemented(self):
raise NotImplementedError('funny errors should be transported')
def do_wait(self):
time.sleep(3)
def do_stop(self):
pass
def do_count(self):
print "counting:"
for d in range(10-1, -1, -1):
print '%d',
time.sleep(1)
print
def do_add_args(self, arg1, arg2):
return arg1 + arg2
def do_return_stuff(self):
return [{'a':1}, (2, 3)]
print "minimal testing: server"
srv = DeviceServer()
srv.register_device(TestDevice(), 'dev1')
srv.register_device(TestDevice(), 'dev2')
devices = parse(srv.handle(ListDevicesRequest()))[2]['list_of_devices']
print 'Srv exports these devices:', devices
for dev in sorted(devices):
print '___ testing device %s ___' % dev
params = parse(srv.handle(ListDeviceParamsRequest(dev)))[2]['params']
print '-has params: ', sorted(params.keys())
for p in sorted(params.keys()):
pinfo = params[p]
if pinfo.readonly:
print ' - param %r is readonly' % p
if pinfo.description:
print ' - param %r\'s description is: %r' % (p,
pinfo.description)
else:
print ' - param %r has no description' % p
replytype, replyname, rv = parse(srv.handle(ReadParamRequest(dev,
p)))
if replytype == 'error':
print ' - reading param %r resulted in error/%s' % (p,
replyname)
else:
print ' - param %r current value is %r' % (p, rv.value)
print ' - param %r current unit is %r' % (p, rv.unit)

167
src/startup.py Normal file
View File

@ -0,0 +1,167 @@
# -*- 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>
#
# *****************************************************************************
"""Define helpers"""
import os
import psutil
import ConfigParser
# apt install python-daemon !!!do not use pip install daemon <- wrong version!
import daemon
from daemon import pidlockfile
from lib import read_pidfile, write_pidfile, get_class
from protocol.dispatcher import Dispatcher
from protocol.interface import INTERFACES
from protocol.transport import ENCODERS, FRAMERS
from errors import ConfigError
from logger import get_logger
__ALL__ = ['kill_server', 'start_server']
def kill_server(pidfile):
"""kill a server specified by a pidfile"""
pid = read_pidfile(pidfile)
if pid is None:
# already dead/not started yet
return
# get process for this pid
for proc in psutil.process_iter():
if proc.pid == pid:
break
proc.terminate()
proc.wait(3)
proc.kill()
def start_server(srvname, base_path, loglevel, daemonize=False):
"""start a server, part1
handle the daemonizing and logging stuff and call the second step
"""
pidfilename = os.path.join(base_path, 'pid', srvname + '.pid')
pidfile = pidlockfile.TimeoutPIDLockFile(pidfilename, 3)
if daemonize:
with daemon.DaemonContext(
#files_preserve=[logFileHandler.stream],
pidfile=pidfile,
):
try:
#write_pidfile(pidfilename, os.getpid())
startup(srvname, base_path, loglevel)
except Exception as e:
logging.exception(e)
else:
write_pidfile(pidfilename, os.getpid())
startup(srvname, base_path, loglevel) # blocks!
# unexported stuff here
def startup(srvname, base_path, loglevel):
"""really start a server (part2)
loads the config, initiate all objects, link them together
and finally start the interface server.
Never returns. (may raise)
"""
cfgfile = os.path.join(base_path, 'etc', srvname + '.cfg')
logger = get_logger(srvname, loglevel=loglevel)
logger.debug('parsing %r' % cfgfile)
parser = ConfigParser.SafeConfigParser()
if not parser.read([cfgfile]):
logger.error('Couldn\'t read cfg file !')
raise ConfigError('Couldn\'t read cfg file %r' % cfgfile)
# iterate over all sections, checking for devices/server
deviceopts = []
serveropts = {}
for section in parser.sections():
if section == 'server':
# store for later
serveropts = dict(item for item in parser.items('server'))
if section.lower().startswith('device '):
# device section
# omit leading 'device ' string
devname = section[len('device '):]
devopts = dict(item for item in parser.items(section))
if 'class' not in devopts:
logger.error('Device %s needs a class option!')
raise ConfigError('cfgfile %r: Device %s needs a class option!'
% (cfgfile, devname))
# try to import the class, raise if this fails
devopts['class'] = get_class(devopts['class'])
# all went well so far
deviceopts.append([devname, devopts])
# there are several sections which resultin almost identical code: refactor
def init_object(name, cls, logger, options={}, *args):
logger.debug('Creating ' + name)
# cls.__init__ should pop all used args from options!
obj = cls(logger, options, *args)
if options:
raise ConfigError('%s: don\'t know how to handle option(s): %s' % (
cls.__name__,
', '.join(options.keys())))
return obj
# evaluate Server specific stuff
if not serveropts:
raise ConfigError('cfg file %r needs a \'server\' section!' % cfgfile)
# eval serveropts
Framing = FRAMERS[serveropts.pop('framing')]
Encoding = ENCODERS[serveropts.pop('encoding')]
Interface = INTERFACES[serveropts.pop('interface')]
dispatcher = init_object('Dispatcher', Dispatcher, logger,
dict(encoding=Encoding(),
framing=Framing()))
# split 'server' section to allow more than one interface
# also means to move encoding and framing to the interface,
# so that the dispatcher becomes agnostic
interface = init_object('Interface', Interface, logger, serveropts,
dispatcher)
# check devices opts by creating them
devs = []
for devname, devopts in deviceopts:
devclass = devopts.pop('class')
# create device
logger.debug('Creating Device %r' % devname)
devobj = devclass(logger, devopts, devname, dispatcher)
devs.append([devname, devobj])
# connect devices with dispatcher
for devname, devobj in devs:
logger.info('registering device %r' % devname)
dispatcher.register_device(devobj, devname)
# also call init on the devices
logger.debug('device.init()')
devobj.init()
# handle requests until stop is requsted
logger.info('startup done, handling transport messages')
interface.serve_forever()

View File

@ -22,30 +22,77 @@
"""Define validators."""
# a Validator validates a given object and raises an ValueError if it doesn't fit
# a Validator returns a validated object or raises an ValueError
# easy python validators: int(), float(), str()
# also validators should have a __repr__ returning a 'python' string
# which recreates them
class Validator(object):
# list of tuples: (name, converter)
params = []
valuetype = float
def __init__(self, *args, **kwds):
plist = self.params[:]
if len(args) > len(plist):
raise ProgrammingError('%s takes %d parameters only (%d given)' % (
self.__class__.__name__,
len(plist), len(args)))
for pval in args:
pname, pconv = plist.pop(0)
if pname in kwds:
raise ProgrammingError('%s: positional parameter %s als given '
'as keyword!' % (
self.__class__.__name__,
pname))
self.__dict__[pname] = pconv(pval)
for pname, pconv in plist:
if pname in kwds:
pval = kwds.pop(pname)
self.__dict__[pname] = pconv(pval)
else:
raise ProgrammingError('%s: param %s left unspecified!' % (
self.__class__.__name__,
pname))
if kwds:
raise ProgrammingError('%s got unknown arguments: %s' % (
self.__class__.__name__,
', '.join(list(kwds.keys()))))
def __repr__(self):
params = ['%s=%r' % (pn, self.__dict__[pn]) for pn in self.params]
return ('%s(%s)' % (self.__class__.__name__, ', '.join(params)))
class floatrange(object):
def __init__(self, lower, upper):
self.lower = float(lower)
self.upper = float(upper)
def __call__(self, value):
value = float(value)
if not self.lower <= value <= self.upper:
raise ValueError('Floatrange: value %r must be within %f and %f' % (value, self.lower, self.upper))
return value
return self.check(self.valuetype(value))
def positive(obj):
if obj <= 0:
class floatrange(Validator):
params = [('lower', float), ('upper', float)]
def check(self, value):
if self.lower <= value <= self.upper:
return value
raise ValueError('Floatrange: value %r must be within %f and %f' %
(value, self.lower, self.upper))
class positive(Validator):
def check(self, value):
if value > 0:
return value
raise ValueError('Value %r must be positive!' % obj)
return obj
def nonnegative(obj):
if obj < 0:
raise ValueError('Value %r must be zero or positive!' % obj)
return obj
class nonnegative(Validator):
def check(self, value):
if value >= 0:
return value
raise ValueError('Value %r must be positive!' % obj)
# more complicated validator may not be able to use validator base class
class mapping(object):
def __init__(self, *args, **kwds):
self.mapping = {}
@ -67,5 +114,9 @@ class mapping(object):
def __call__(self, obj):
if obj in self.mapping:
return obj
raise ValueError("%r should be one of %r" % (obj, list(self.mapping.keys())))
raise ValueError("%r should be one of %r" %
(obj, list(self.mapping.keys())))
def __repr__(self):
params = ['%s=%r' % (mname, mval) for mname, mval in self.mapping]
return ('%s(%s)' % (self.__class__.__name__, ', '.join(params)))