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:
parent
d3c430e1b9
commit
c11bca3c37
@ -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):
|
||||
|
@ -1,7 +1,9 @@
|
||||
[server]
|
||||
bindto=localhost
|
||||
bindport=10767
|
||||
protocol=pickle
|
||||
interface = tcp
|
||||
framing=eol
|
||||
encoding=text
|
||||
|
||||
[device LN2]
|
||||
class=devices.test.LN2
|
||||
|
@ -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
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -22,11 +22,14 @@
|
||||
# *****************************************************************************
|
||||
"""error class for our little framework"""
|
||||
|
||||
|
||||
class SECoPServerError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ConfigError(SECoPServerError):
|
||||
pass
|
||||
|
||||
|
||||
class ProgrammingError(SECoPServerError):
|
||||
pass
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
47
src/logger.py
Normal 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
33
src/paths.py
Normal 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')
|
@ -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"""
|
@ -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
270
src/protocol/dispatcher.py
Normal 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])
|
31
src/protocol/interface/__init__.py
Normal file
31
src/protocol/interface/__init__.py
Normal 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']
|
103
src/protocol/interface/tcp.py
Normal file
103
src/protocol/interface/tcp.py
Normal 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")
|
27
src/protocol/interface/zmq.py
Normal file
27
src/protocol/interface/zmq.py
Normal 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?
|
@ -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
|
||||
|
||||
|
@ -28,4 +28,3 @@ WARN = 300
|
||||
UNSTABLE = 350
|
||||
ERROR = 400
|
||||
UNKNOWN = -1
|
||||
|
||||
|
@ -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)
|
26
src/protocol/transport/__init__.py
Normal file
26
src/protocol/transport/__init__.py
Normal 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
|
103
src/protocol/transport/encoding.py
Normal file
103
src/protocol/transport/encoding.py
Normal 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']
|
134
src/protocol/transport/framing.py
Normal file
134
src/protocol/transport/framing.py
Normal 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']
|
150
src/server.py
150
src/server.py
@ -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
167
src/startup.py
Normal 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()
|
@ -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)))
|
||||
|
Loading…
x
Reference in New Issue
Block a user