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') log_path = path.join(basepath, 'log')
sys.path[0] = path.join(basepath, 'src') sys.path[0] = path.join(basepath, 'src')
import logger
import argparse import argparse
from lib import check_pidfile, start_server, kill_server 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 = parser.add_mutually_exclusive_group()
loggroup.add_argument("-v", "--verbose", help="Output lots of diagnostic information", loggroup.add_argument("-v", "--verbose",
help="Output lots of diagnostic information",
action='store_true', default=False) action='store_true', default=False)
loggroup.add_argument("-q", "--quiet", help="suppress non-error messages", action='store_true', loggroup.add_argument("-q", "--quiet", help="suppress non-error messages",
default=False) action='store_true', default=False)
parser.add_argument("action", help="What to do with the server: (re)start, status or stop", parser.add_argument("action",
choices=['start', 'status', 'stop', 'restart'], default="status") help="What to do: (re)start, status or stop",
parser.add_argument("name", help="Name of the instance. Uses etc/name.cfg for configuration\n" 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)", "may be omitted to mean ALL (which are configured)",
nargs='?', default='') nargs='?', default='')
args = parser.parse_args() args = parser.parse_args()
import logging loglevel = 'debug' if args.verbose else ('error' if args.quiet else 'info')
loglevel = logging.DEBUG if args.verbose else (logging.ERROR if args.quiet else logging.INFO) logger = logger.get_logger('startup', loglevel)
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)
logger.debug("action specified %r" % args.action) logger.debug("action specified %r" % args.action)
def handle_servername(name, action): def handle_servername(name, action):
pidfile = path.join(pid_path, name + '.pid') pidfile = path.join(pid_path, name + '.pid')
cfgfile = path.join(etc_path, name + '.cfg') cfgfile = path.join(etc_path, name + '.cfg')

View File

@ -1,7 +1,9 @@
[server] [server]
bindto=localhost bindto=localhost
bindport=10767 bindport=10767
protocol=pickle interface = tcp
framing=eol
encoding=text
[device LN2] [device LN2]
class=devices.test.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""" """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 types
import inspect import inspect
from errors import ConfigError, ProgrammingError from errors import ConfigError, ProgrammingError
from protocol import status 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:
# storage for PARAMeter settings (changeable during runtime) # 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): 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.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
# internal caching... # internal caching...
self.currentvalue = default self.currentvalue = default
# storage for CMDs settings (names + call signature...) # storage for CMDs settings (description + call signature...)
class CMD(object): class CMD(object):
def __init__(self, description, *args): def __init__(self, description, arguments, result):
# descriptive text for humans
self.description = description self.description = description
self.arguments = args # list of validators for arguments
self.argumenttype = arguments
# validator for results
self.resulttype = result
# Meta class # Meta class
# warning: MAGIC! # warning: MAGIC!
@ -62,25 +70,34 @@ class DeviceMeta(type):
newtype = type.__new__(mcs, name, bases, attrs) newtype = type.__new__(mcs, name, bases, attrs)
if '__constructed__' in attrs: if '__constructed__' in attrs:
return newtype 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 = {} newentry = {}
for base in reversed(bases): for base in reversed(bases):
if hasattr(base, entry): if hasattr(base, entry):
newentry.update(getattr(base, entry)) newentry.update(getattr(base, entry))
newentry.update(attrs.get(entry, {})) newentry.update(attrs.get(entry, {}))
setattr(newtype, entry, newentry) setattr(newtype, entry, newentry)
# check validity of entries
for cname, info in newtype.CONFIG.items(): # check validity of PARAM entries
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
for pname, info in newtype.PARAMS.items(): for pname, info in newtype.PARAMS.items():
if not isinstance(info, PARAM): if not isinstance(info, PARAM):
raise ProgrammingError("%r: device PARAM %r should be a PARAM object!" % raise ProgrammingError('%r: device PARAM %r should be a '
(name, pname)) 'PARAM object!' % (name, pname))
#XXX: greate getters and setters, setters should send async updates #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 # also collect/update information about CMD's
setattr(newtype, 'CMDS', getattr(newtype, 'CMDS', {})) setattr(newtype, 'CMDS', getattr(newtype, 'CMDS', {}))
for name in attrs: for name in attrs:
@ -90,55 +107,56 @@ class DeviceMeta(type):
argspec = inspect.getargspec(value) argspec = inspect.getargspec(value)
if argspec[0] and argspec[0][0] == 'self': if argspec[0] and argspec[0][0] == 'self':
del argspec[0][0] 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 attrs['__constructed__'] = True
return newtype return newtype
# Basic device class # Basic device class
class Device(object): class Device(object):
"""Basic Device, doesn't do much""" """Basic Device, doesn't do much"""
__metaclass__ = DeviceMeta __metaclass__ = DeviceMeta
# CONFIG, PARAMS and CMDS are auto-merged upon subclassing # PARAMS and CMDS are auto-merged upon subclassing
CONFIG = {}
PARAMS = {} PARAMS = {}
CMDS = {} CMDS = {}
SERVER = None DISPATCHER = None
def __init__(self, devname, serverobj, logger, cfgdict):
def __init__(self, logger, cfgdict, devname, dispatcher):
# remember the server object (for the async callbacks) # remember the server object (for the async callbacks)
self.SERVER = serverobj self.DISPATCHER = dispatcher
self.log = logger self.log = logger
self.name = devname self.name = devname
# check config for problems # check config for problems
# only accept config items specified in CONFIG # only accept config items specified in PARAMS
for k, v in cfgdict.items(): for k, v in cfgdict.items():
if k not in self.CONFIG: if k not in self.PARAMS:
raise ConfigError('Device %s:config Parameter %r not unterstood!' % (self.name, k)) raise ConfigError('Device %s:config Parameter %r '
# complain if a CONFIG entry has no default value and is not specified in cfgdict 'not unterstood!' % (self.name, k))
for k, v in self.CONFIG.items(): # 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 k not in cfgdict:
if 'default' not in v: if v.default is Ellipsis:
raise ConfigError('Config Parameter %r was not given and not default value exists!' % k) # Ellipsis is the one single value you can not specify....
cfgdict[k] = v['default'] # assume default value was given. raise ConfigError('Device %s: Parameter %r has no default '
# now 'apply' config, passing values through the validators and store as attributes '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(): for k, v in cfgdict.items():
# apply validator, complain if type does not fit # apply validator, complain if type does not fit
validator = self.CONFIG[k].validator validator = self.PARAMS[k].validator
if validator is not None: if validator is not None:
# only check if validator given # only check if validator given
try: try:
v = validator(v) v = validator(v)
except ValueError as e: 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? # 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) setattr(self, k, v)
def init(self): def init(self):
@ -147,12 +165,16 @@ class Device(object):
class Readable(Device): class Readable(Device):
"""Basic readable device, providing the RO parameter 'value' and 'status'""" """Basic readable device
providing the readonly parameter 'value' and 'status'
"""
PARAMS = { PARAMS = {
'value' : PARAM('current value of the device', readonly=True), 'value': PARAM('current value of the device', readonly=True, default=0.),
'status' : PARAM('current status of the device', 'status': PARAM('current status of the device', default=status.OK,
readonly=True), readonly=True),
} }
def read_value(self, maxage=0): def read_value(self, maxage=0):
raise NotImplementedError raise NotImplementedError
@ -161,10 +183,13 @@ class Readable(Device):
class Driveable(Readable): 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 = { PARAMS = {
'target' : PARAM('target value of the device'), 'target': PARAM('target value of the device', default=0.),
} }
def write_target(self, value): def write_target(self, value):
raise NotImplementedError raise NotImplementedError

View File

@ -32,22 +32,25 @@ from validators import floatrange, positive, mapping
from lib import clamp from lib import clamp
hack = []
class Cryostat(Driveable): class Cryostat(Driveable):
"""simulated cryostat with heat capacity on the sample, cooling power and thermal transfer functions""" """simulated cryostat with:
CONFIG = dict(
- heat capacity of the sample
- cooling power
- thermal transfer between regulation and samplen
"""
PARAMS = dict(
jitter=CONFIG("amount of random noise on readout values", jitter=CONFIG("amount of random noise on readout values",
validator=floatrange(0, 1), default=1, validator=floatrange(0, 1),
export=False,
), ),
T_start=CONFIG("starting temperature for simulation", T_start=CONFIG("starting temperature for simulation",
validator=positive, default=2, validator=positive, export=False,
), ),
looptime=CONFIG("timestep for simulation", looptime=CONFIG("timestep for simulation",
validator=positive, default=1, unit="s", validator=positive, default=1, unit="s",
export=False,
), ),
)
PARAMS = dict(
ramp=PARAM("ramping speed in K/min", ramp=PARAM("ramping speed in K/min",
validator=floatrange(0, 1e3), default=1, validator=floatrange(0, 1e3), default=1,
), ),
@ -95,16 +98,15 @@ class Cryostat(Driveable):
self._thread = threading.Thread(target=self.thread) self._thread = threading.Thread(target=self.thread)
self._thread.daemon = True self._thread.daemon = True
self._thread.start() 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): 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 return self.status
def read_value(self, maxage=0): def read_value(self, maxage=0):
# return regulation value (averaged regulation temp) # 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): def read_target(self, maxage=0):
return self.target return self.target
@ -119,12 +121,13 @@ class Cryostat(Driveable):
def write_maxpower(self, newpower): def write_maxpower(self, newpower):
# rescale heater setting in % to keep the power # 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 self.maxpower = newpower
def doStop(self): def doStop(self):
# stop the ramp by setting current value as target # stop the ramp by setting current setpoint as target
# XXX: there may be use case where setting the current temp may be better # XXX: discussion: take setpoint or current value ???
self.write_target(self.setpoint) self.write_target(self.setpoint)
# #
@ -176,7 +179,8 @@ class Cryostat(Driveable):
# local state keeping: # local state keeping:
regulation = self.regulationtemp regulation = self.regulationtemp
sample = self.sampletemp sample = self.sampletemp
window = [] # keep history values for stability check # keep history values for stability check
window = []
timestamp = time.time() timestamp = time.time()
heater = 0 heater = 0
lastflow = 0 lastflow = 0
@ -210,7 +214,8 @@ class Cryostat(Driveable):
newregulation = max(0, regulation + newregulation = max(0, regulation +
regdelta / self.__coolerCP(regulation) * h) regdelta / self.__coolerCP(regulation) * h)
# b) see # 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': if self.mode != 'openloop':
# fix artefacts due to too big timesteps # fix artefacts due to too big timesteps
# actually i would prefer reducing looptime, but i have no # actually i would prefer reducing looptime, but i have no
@ -320,4 +325,3 @@ class Cryostat(Driveable):
self._stopflag = True self._stopflag = True
if self._thread and self._thread.isAlive(): if self._thread and self._thread.isAlive():
self._thread.join() self._thread.join()

View File

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

View File

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

View File

@ -22,16 +22,17 @@
"""Define helpers""" """Define helpers"""
import logging
from os import path
class attrdict(dict): class attrdict(dict):
"""a normal dict, providing access also via attributes""" """a normal dict, providing access also via attributes"""
def __getattr__(self, key): def __getattr__(self, key):
return self[key] return self[key]
def __setattr__(self, key, value): def __setattr__(self, key, value):
self[key] = value self[key] = value
def clamp(_min, value, _max): def clamp(_min, value, _max):
"""return the median of 3 values, """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 median, i.e. clamp the the value between min and max
return sorted([_min, value, _max])[1] return sorted([_min, value, _max])[1]
def get_class(spec): def get_class(spec):
"""loads a class given by string in dotted notaion (as python would do)""" """loads a class given by string in dotted notaion (as python would do)"""
modname, classname = spec.rsplit('.', 1) modname, classname = spec.rsplit('.', 1)
import importlib import importlib
# module = importlib.import_module(modname) module = importlib.import_module(modname)
module = __import__(spec) # module = __import__(spec)
return getattr(module, classname) 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 # moved below definitions to break import cycle
from pidfile import * from pidfile import *
@ -73,4 +63,3 @@ if __name__ == '__main__':
d.c = 9 d.c = 9
d['d'] = 'c' d['d'] = 'c'
assert d[d.d] == 9 assert d[d.d] == 9

View File

@ -33,21 +33,23 @@ def read_pidfile(pidfile):
try: try:
with open(pidfile, 'r') as f: with open(pidfile, 'r') as f:
return int(f.read()) return int(f.read())
except OSError: except (OSError, IOError):
return None return None
def remove_pidfile(pidfile): def remove_pidfile(pidfile):
"""remove the given pidfile, typically at end of the process""" """remove the given pidfile, typically at end of the process"""
os.remove(pidfile) os.remove(pidfile)
def write_pidfile(pidfile, pid): def write_pidfile(pidfile, pid):
"""write the given pid to the given pidfile""" """write the given pid to the given pidfile"""
with open(pidfile, 'w') as f: with open(pidfile, 'w') as f:
f.write('%d\n' % pid) f.write('%d\n' % pid)
atexit.register(remove_pidfile, pidfile) atexit.register(remove_pidfile, pidfile)
def check_pidfile(pidfile): def check_pidfile(pidfile):
"""check if the process from a given pidfile is still running""" """check if the process from a given pidfile is still running"""
pid = read_pidfile(pidfile) pid = read_pidfile(pidfile)
return False if pid is None else psutil.pid_exists(pid) 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 """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 lib import attrdict
from protocol import status from protocol import status
@ -34,27 +36,36 @@ class Device(object):
all others derive from this""" all others derive from this"""
name = None name = None
def read_status(self): def read_status(self):
raise NotImplementedError('All Devices need a Status!') raise NotImplementedError('All Devices need a Status!')
def read_name(self): def read_name(self):
return self.name return self.name
class Readable(Device): class Readable(Device):
"""A Readable Device""" """A Readable Device"""
unit = '' unit = ''
def read_value(self): def read_value(self):
raise NotImplementedError('A Readable MUST provide a value') raise NotImplementedError('A Readable MUST provide a value')
def read_unit(self): def read_unit(self):
return self.unit return self.unit
class Writeable(Readable): class Writeable(Readable):
"""Writeable can be told to change it's vallue""" """Writeable can be told to change it's vallue"""
target = None target = None
def read_target(self): def read_target(self):
return self.target return self.target
def write_target(self, target): def write_target(self, target):
self.target = target self.target = target
class Driveable(Writeable): 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,
@ -62,32 +73,3 @@ class Driveable(Writeable):
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')
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,128 +22,115 @@
"""Define SECoP Messages""" """Define SECoP Messages"""
from lib import attrdict REQUEST = 'request'
import time REPLY = 'reply'
from device import get_device_pars, get_device_cmds ERROR = 'error'
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
# 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): class ListDevicesRequest(Request):
pass pass
class ListDevicesReply(Reply): class ListDevicesReply(Reply):
pars = ['list_of_devices'] ARGS = ['list_of_devices', 'descriptive_data']
def __init__(self, args):
self.list_of_devices = args
class ListDeviceParamsRequest(Request): class ListDeviceParamsRequest(Request):
pars = ['device'] ARGS = ['device']
def __init__(self, device):
self.device = device
class ListDeviceParamsReply(Reply): class ListDeviceParamsReply(Reply):
pars = ['device', 'params'] ARGS = ['device', 'params']
def __init__(self, device, params):
self.device = device
self.params = params
class ReadValueRequest(Request): class ReadValueRequest(Request):
pars = ['device', 'maxage'] ARGS = ['device', 'maxage']
def __init__(self, device, maxage=0):
self.device = device
self.maxage = maxage
class ReadValueReply(Reply): class ReadValueReply(Reply):
pars = ['device', 'value', 'timestamp', 'error', 'unit'] ARGS = ['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
class ReadParamRequest(Request): class ReadParamRequest(Request):
pars = ['device', 'param', 'maxage'] ARGS = ['device', 'param', 'maxage']
def __init__(self, device, param, maxage=0):
self.device = device
self.param = param
self.maxage = maxage
class ReadParamReply(Reply): class ReadParamReply(Reply):
pars = ['device', 'param', 'value', 'timestamp', 'error', 'unit'] ARGS = ['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
class WriteParamRequest(Request): class WriteParamRequest(Request):
pars = ['device', 'param', 'value'] ARGS = ['device', 'param', 'value']
def __init__(self, device, param, value):
self.device = device
self.param = param
self.value = value
class WriteParamReply(Reply): class WriteParamReply(Reply):
pars = ['device', 'param', 'readback_value', 'timestamp', 'error', 'unit'] ARGS = ['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
class RequestAsyncDataRequest(Request): class RequestAsyncDataRequest(Request):
pars = ['device', 'params'] ARGS = ['device', 'params']
def __init__(self, device, *args):
self.device = device
self.params = args
class RequestAsyncDataReply(Reply): class RequestAsyncDataReply(Reply):
pars = ['device', 'paramvalue_list'] ARGS = ['device', 'paramvalue_list']
def __init__(self, device, *args):
self.device = device
self.paramvalue_list = args
class AsyncDataUnit(ReadParamReply): class AsyncDataUnit(ReadParamReply):
pass ARGS = ['device', 'param', 'value', 'timestamp', 'error', 'unit']
class ListOfFeaturesRequest(Request): class ListOfFeaturesRequest(Request):
pass pass
class ListOfFeaturesReply(Reply): class ListOfFeaturesReply(Reply):
pars = ['features'] ARGS = ['features']
def __init__(self, *args):
self.features = args
class ActivateFeatureRequest(Request): class ActivateFeatureRequest(Request):
pars = ['feature'] ARGS = ['feature']
def __init__(self, feature):
self.feature = feature
class ActivateFeatureReply(Reply): class ActivateFeatureReply(Reply):
# Ack style or Error # Ack style or Error
@ -151,207 +138,55 @@ class ActivateFeatureReply(Reply):
pass 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
FEATURES = [ FEATURES = [
'Feature1', 'Feature1',
'Feature2', 'Feature2',
'Feature3', '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 UNSTABLE = 350
ERROR = 400 ERROR = 400
UNKNOWN = -1 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.""" """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() # 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): def __call__(self, value):
value = float(value) return self.check(self.valuetype(value))
if not self.lower <= value <= self.upper:
raise ValueError('Floatrange: value %r must be within %f and %f' % (value, self.lower, self.upper))
class floatrange(Validator):
params = [('lower', float), ('upper', float)]
def check(self, value):
if self.lower <= value <= self.upper:
return value return value
raise ValueError('Floatrange: value %r must be within %f and %f' %
(value, self.lower, self.upper))
def positive(obj): class positive(Validator):
if obj <= 0: def check(self, value):
if value > 0:
return value
raise ValueError('Value %r must be positive!' % obj) 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): class mapping(object):
def __init__(self, *args, **kwds): def __init__(self, *args, **kwds):
self.mapping = {} self.mapping = {}
@ -67,5 +114,9 @@ class mapping(object):
def __call__(self, obj): def __call__(self, obj):
if obj in self.mapping: if obj in self.mapping:
return obj 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)))