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')
|
log_path = path.join(basepath, 'log')
|
||||||
sys.path[0] = path.join(basepath, 'src')
|
sys.path[0] = path.join(basepath, 'src')
|
||||||
|
|
||||||
|
import logger
|
||||||
import argparse
|
import argparse
|
||||||
from lib import check_pidfile, start_server, kill_server
|
from lib import check_pidfile, start_server, kill_server
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="Manage a SECoP server")
|
parser = argparse.ArgumentParser(description="Manage a SECoP server")
|
||||||
loggroup = parser.add_mutually_exclusive_group()
|
loggroup = parser.add_mutually_exclusive_group()
|
||||||
loggroup.add_argument("-v", "--verbose", help="Output lots of diagnostic information",
|
loggroup.add_argument("-v", "--verbose",
|
||||||
|
help="Output lots of diagnostic information",
|
||||||
action='store_true', default=False)
|
action='store_true', default=False)
|
||||||
loggroup.add_argument("-q", "--quiet", help="suppress non-error messages", action='store_true',
|
loggroup.add_argument("-q", "--quiet", help="suppress non-error messages",
|
||||||
default=False)
|
action='store_true', default=False)
|
||||||
parser.add_argument("action", help="What to do with the server: (re)start, status or stop",
|
parser.add_argument("action",
|
||||||
choices=['start', 'status', 'stop', 'restart'], default="status")
|
help="What to do: (re)start, status or stop",
|
||||||
parser.add_argument("name", help="Name of the instance. Uses etc/name.cfg for configuration\n"
|
choices=['start', 'status', 'stop', 'restart'],
|
||||||
|
default="status")
|
||||||
|
parser.add_argument("name",
|
||||||
|
help="Name of the instance.\n"
|
||||||
|
" Uses etc/name.cfg for configuration\n"
|
||||||
"may be omitted to mean ALL (which are configured)",
|
"may be omitted to mean ALL (which are configured)",
|
||||||
nargs='?', default='')
|
nargs='?', default='')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
import logging
|
loglevel = 'debug' if args.verbose else ('error' if args.quiet else 'info')
|
||||||
loglevel = logging.DEBUG if args.verbose else (logging.ERROR if args.quiet else logging.INFO)
|
logger = logger.get_logger('startup', loglevel)
|
||||||
logging.basicConfig(level=loglevel, format='%(asctime)s %(levelname)s %(message)s')
|
|
||||||
logger = logging.getLogger('server')
|
|
||||||
logger.setLevel(loglevel)
|
|
||||||
fh = logging.FileHandler(path.join(log_path, 'server.log'), 'w')
|
|
||||||
fh.setLevel(loglevel)
|
|
||||||
logger.addHandler(fh)
|
|
||||||
|
|
||||||
logger.debug("action specified %r" % args.action)
|
logger.debug("action specified %r" % args.action)
|
||||||
|
|
||||||
|
|
||||||
def handle_servername(name, action):
|
def handle_servername(name, action):
|
||||||
pidfile = path.join(pid_path, name + '.pid')
|
pidfile = path.join(pid_path, name + '.pid')
|
||||||
cfgfile = path.join(etc_path, name + '.cfg')
|
cfgfile = path.join(etc_path, name + '.cfg')
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
[server]
|
[server]
|
||||||
bindto=localhost
|
bindto=localhost
|
||||||
bindport=10767
|
bindport=10767
|
||||||
protocol=pickle
|
interface = tcp
|
||||||
|
framing=eol
|
||||||
|
encoding=text
|
||||||
|
|
||||||
[device LN2]
|
[device LN2]
|
||||||
class=devices.test.LN2
|
class=devices.test.LN2
|
||||||
|
@ -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"""
|
"""Define Baseclasses for real devices implemented in the server"""
|
||||||
|
|
||||||
|
# XXX: connect with 'protocol'-Devices.
|
||||||
|
# Idea: every Device defined herein is also a 'protocol'-device,
|
||||||
|
# all others MUST derive from those, the 'interface'-class is still derived
|
||||||
|
# from these base classes (how to do this?)
|
||||||
|
|
||||||
import types
|
import types
|
||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
from errors import ConfigError, ProgrammingError
|
from errors import ConfigError, ProgrammingError
|
||||||
from protocol import status
|
from protocol import status
|
||||||
|
|
||||||
# storage for CONFIGurable settings (from configfile)
|
|
||||||
class CONFIG(object):
|
|
||||||
def __init__(self, description, validator=None, default=None, unit=None):
|
|
||||||
self.description = description
|
|
||||||
self.validator = validator
|
|
||||||
self.default = default
|
|
||||||
self.unit = unit
|
|
||||||
|
|
||||||
|
# storage for PARAMeter settings:
|
||||||
# storage for PARAMeter settings (changeable during runtime)
|
# if readonly is False, the value can be changed (by code, or remte)
|
||||||
|
# if no default is given, the parameter MUST be specified in the configfile
|
||||||
|
# during startup, currentvalue is initialized with the default value or
|
||||||
|
# from the config file
|
||||||
class PARAM(object):
|
class PARAM(object):
|
||||||
def __init__(self, description, validator=None, default=None, unit=None, readonly=False):
|
def __init__(self, description, validator=None, default=Ellipsis,
|
||||||
|
unit=None, readonly=False, export=True):
|
||||||
self.description = description
|
self.description = description
|
||||||
self.validator = validator
|
self.validator = validator
|
||||||
self.default = default
|
self.default = default
|
||||||
self.unit = unit
|
self.unit = unit
|
||||||
self.readonly = readonly
|
self.readonly = readonly
|
||||||
|
self.export = export
|
||||||
# internal caching...
|
# internal caching...
|
||||||
self.currentvalue = default
|
self.currentvalue = default
|
||||||
|
|
||||||
|
|
||||||
# storage for CMDs settings (names + call signature...)
|
# storage for CMDs settings (description + call signature...)
|
||||||
class CMD(object):
|
class CMD(object):
|
||||||
def __init__(self, description, *args):
|
def __init__(self, description, arguments, result):
|
||||||
|
# descriptive text for humans
|
||||||
self.description = description
|
self.description = description
|
||||||
self.arguments = args
|
# list of validators for arguments
|
||||||
|
self.argumenttype = arguments
|
||||||
|
# validator for results
|
||||||
|
self.resulttype = result
|
||||||
|
|
||||||
|
|
||||||
# Meta class
|
# Meta class
|
||||||
# warning: MAGIC!
|
# warning: MAGIC!
|
||||||
@ -62,25 +70,34 @@ class DeviceMeta(type):
|
|||||||
newtype = type.__new__(mcs, name, bases, attrs)
|
newtype = type.__new__(mcs, name, bases, attrs)
|
||||||
if '__constructed__' in attrs:
|
if '__constructed__' in attrs:
|
||||||
return newtype
|
return newtype
|
||||||
# merge CONFIG, PARAM, CMDS from all sub-classes
|
|
||||||
for entry in ['CONFIG', 'PARAMS', 'CMDS']:
|
# merge PARAM and CMDS from all sub-classes
|
||||||
|
for entry in ['PARAMS', 'CMDS']:
|
||||||
newentry = {}
|
newentry = {}
|
||||||
for base in reversed(bases):
|
for base in reversed(bases):
|
||||||
if hasattr(base, entry):
|
if hasattr(base, entry):
|
||||||
newentry.update(getattr(base, entry))
|
newentry.update(getattr(base, entry))
|
||||||
newentry.update(attrs.get(entry, {}))
|
newentry.update(attrs.get(entry, {}))
|
||||||
setattr(newtype, entry, newentry)
|
setattr(newtype, entry, newentry)
|
||||||
# check validity of entries
|
|
||||||
for cname, info in newtype.CONFIG.items():
|
# check validity of PARAM entries
|
||||||
if not isinstance(info, CONFIG):
|
|
||||||
raise ProgrammingError("%r: device CONFIG %r should be a CONFIG object!" %
|
|
||||||
(name, cname))
|
|
||||||
#XXX: greate getters for the config value
|
|
||||||
for pname, info in newtype.PARAMS.items():
|
for pname, info in newtype.PARAMS.items():
|
||||||
if not isinstance(info, PARAM):
|
if not isinstance(info, PARAM):
|
||||||
raise ProgrammingError("%r: device PARAM %r should be a PARAM object!" %
|
raise ProgrammingError('%r: device PARAM %r should be a '
|
||||||
(name, pname))
|
'PARAM object!' % (name, pname))
|
||||||
#XXX: greate getters and setters, setters should send async updates
|
#XXX: greate getters and setters, setters should send async updates
|
||||||
|
|
||||||
|
def getter():
|
||||||
|
return self.PARAMS[pname].currentvalue
|
||||||
|
|
||||||
|
def setter(value):
|
||||||
|
p = self.PARAMS[pname]
|
||||||
|
p.currentvalue = p.validator(value) if p.validator else value
|
||||||
|
# also send notification
|
||||||
|
self.DISPATCHER.announce_update(self, pname, value)
|
||||||
|
|
||||||
|
attrs[pname] = property(getter, setter)
|
||||||
|
|
||||||
# also collect/update information about CMD's
|
# also collect/update information about CMD's
|
||||||
setattr(newtype, 'CMDS', getattr(newtype, 'CMDS', {}))
|
setattr(newtype, 'CMDS', getattr(newtype, 'CMDS', {}))
|
||||||
for name in attrs:
|
for name in attrs:
|
||||||
@ -90,55 +107,56 @@ class DeviceMeta(type):
|
|||||||
argspec = inspect.getargspec(value)
|
argspec = inspect.getargspec(value)
|
||||||
if argspec[0] and argspec[0][0] == 'self':
|
if argspec[0] and argspec[0][0] == 'self':
|
||||||
del argspec[0][0]
|
del argspec[0][0]
|
||||||
newtype.CMDS[name] = CMD(value.get('__doc__', name), *argspec)
|
newtype.CMDS[name] = CMD(value.get('__doc__', name),
|
||||||
|
*argspec)
|
||||||
attrs['__constructed__'] = True
|
attrs['__constructed__'] = True
|
||||||
return newtype
|
return newtype
|
||||||
|
|
||||||
|
|
||||||
# Basic device class
|
# Basic device class
|
||||||
class Device(object):
|
class Device(object):
|
||||||
"""Basic Device, doesn't do much"""
|
"""Basic Device, doesn't do much"""
|
||||||
__metaclass__ = DeviceMeta
|
__metaclass__ = DeviceMeta
|
||||||
# CONFIG, PARAMS and CMDS are auto-merged upon subclassing
|
# PARAMS and CMDS are auto-merged upon subclassing
|
||||||
CONFIG = {}
|
|
||||||
PARAMS = {}
|
PARAMS = {}
|
||||||
CMDS = {}
|
CMDS = {}
|
||||||
SERVER = None
|
DISPATCHER = None
|
||||||
def __init__(self, devname, serverobj, logger, cfgdict):
|
|
||||||
|
def __init__(self, logger, cfgdict, devname, dispatcher):
|
||||||
# remember the server object (for the async callbacks)
|
# remember the server object (for the async callbacks)
|
||||||
self.SERVER = serverobj
|
self.DISPATCHER = dispatcher
|
||||||
self.log = logger
|
self.log = logger
|
||||||
self.name = devname
|
self.name = devname
|
||||||
# check config for problems
|
# check config for problems
|
||||||
# only accept config items specified in CONFIG
|
# only accept config items specified in PARAMS
|
||||||
for k, v in cfgdict.items():
|
for k, v in cfgdict.items():
|
||||||
if k not in self.CONFIG:
|
if k not in self.PARAMS:
|
||||||
raise ConfigError('Device %s:config Parameter %r not unterstood!' % (self.name, k))
|
raise ConfigError('Device %s:config Parameter %r '
|
||||||
# complain if a CONFIG entry has no default value and is not specified in cfgdict
|
'not unterstood!' % (self.name, k))
|
||||||
for k, v in self.CONFIG.items():
|
# complain if a PARAM entry has no default value and
|
||||||
|
# is not specified in cfgdict
|
||||||
|
for k, v in self.PARAMS.items():
|
||||||
if k not in cfgdict:
|
if k not in cfgdict:
|
||||||
if 'default' not in v:
|
if v.default is Ellipsis:
|
||||||
raise ConfigError('Config Parameter %r was not given and not default value exists!' % k)
|
# Ellipsis is the one single value you can not specify....
|
||||||
cfgdict[k] = v['default'] # assume default value was given.
|
raise ConfigError('Device %s: Parameter %r has no default '
|
||||||
# now 'apply' config, passing values through the validators and store as attributes
|
'value and was not given in config!'
|
||||||
|
% (self.name, k))
|
||||||
|
# assume default value was given
|
||||||
|
cfgdict[k] = v.default
|
||||||
|
# now 'apply' config:
|
||||||
|
# pass values through the validators and store as attributes
|
||||||
for k, v in cfgdict.items():
|
for k, v in cfgdict.items():
|
||||||
# apply validator, complain if type does not fit
|
# apply validator, complain if type does not fit
|
||||||
validator = self.CONFIG[k].validator
|
validator = self.PARAMS[k].validator
|
||||||
if validator is not None:
|
if validator is not None:
|
||||||
# only check if validator given
|
# only check if validator given
|
||||||
try:
|
try:
|
||||||
v = validator(v)
|
v = validator(v)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise ConfigError("Device %s: config paramter %r:\n%r" % (self.name, k, e))
|
raise ConfigError('Device %s: config parameter %r:\n%r'
|
||||||
|
% (self.name, k, e))
|
||||||
# XXX: with or without prefix?
|
# XXX: with or without prefix?
|
||||||
setattr(self, 'config_' + k, v)
|
|
||||||
# set default parameter values as inital values
|
|
||||||
for k, v in self.PARAMS.items():
|
|
||||||
# apply validator, complain if type does not fit
|
|
||||||
validator = v.validator
|
|
||||||
value = v.default
|
|
||||||
if validator is not None:
|
|
||||||
# only check if validator given
|
|
||||||
value = validator(value)
|
|
||||||
setattr(self, k, v)
|
setattr(self, k, v)
|
||||||
|
|
||||||
def init(self):
|
def init(self):
|
||||||
@ -147,12 +165,16 @@ class Device(object):
|
|||||||
|
|
||||||
|
|
||||||
class Readable(Device):
|
class Readable(Device):
|
||||||
"""Basic readable device, providing the RO parameter 'value' and 'status'"""
|
"""Basic readable device
|
||||||
|
|
||||||
|
providing the readonly parameter 'value' and 'status'
|
||||||
|
"""
|
||||||
PARAMS = {
|
PARAMS = {
|
||||||
'value' : PARAM('current value of the device', readonly=True),
|
'value': PARAM('current value of the device', readonly=True, default=0.),
|
||||||
'status' : PARAM('current status of the device',
|
'status': PARAM('current status of the device', default=status.OK,
|
||||||
readonly=True),
|
readonly=True),
|
||||||
}
|
}
|
||||||
|
|
||||||
def read_value(self, maxage=0):
|
def read_value(self, maxage=0):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@ -161,10 +183,13 @@ class Readable(Device):
|
|||||||
|
|
||||||
|
|
||||||
class Driveable(Readable):
|
class Driveable(Readable):
|
||||||
"""Basic Driveable device, providing a RW target parameter to those of a Readable"""
|
"""Basic Driveable device
|
||||||
|
|
||||||
|
providing a settable 'target' parameter to those of a Readable
|
||||||
|
"""
|
||||||
PARAMS = {
|
PARAMS = {
|
||||||
'target' : PARAM('target value of the device'),
|
'target': PARAM('target value of the device', default=0.),
|
||||||
}
|
}
|
||||||
|
|
||||||
def write_target(self, value):
|
def write_target(self, value):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@ -32,22 +32,25 @@ from validators import floatrange, positive, mapping
|
|||||||
from lib import clamp
|
from lib import clamp
|
||||||
|
|
||||||
|
|
||||||
hack = []
|
|
||||||
|
|
||||||
class Cryostat(Driveable):
|
class Cryostat(Driveable):
|
||||||
"""simulated cryostat with heat capacity on the sample, cooling power and thermal transfer functions"""
|
"""simulated cryostat with:
|
||||||
CONFIG = dict(
|
|
||||||
|
- heat capacity of the sample
|
||||||
|
- cooling power
|
||||||
|
- thermal transfer between regulation and samplen
|
||||||
|
"""
|
||||||
|
PARAMS = dict(
|
||||||
jitter=CONFIG("amount of random noise on readout values",
|
jitter=CONFIG("amount of random noise on readout values",
|
||||||
validator=floatrange(0, 1), default=1,
|
validator=floatrange(0, 1),
|
||||||
|
export=False,
|
||||||
),
|
),
|
||||||
T_start=CONFIG("starting temperature for simulation",
|
T_start=CONFIG("starting temperature for simulation",
|
||||||
validator=positive, default=2,
|
validator=positive, export=False,
|
||||||
),
|
),
|
||||||
looptime=CONFIG("timestep for simulation",
|
looptime=CONFIG("timestep for simulation",
|
||||||
validator=positive, default=1, unit="s",
|
validator=positive, default=1, unit="s",
|
||||||
|
export=False,
|
||||||
),
|
),
|
||||||
)
|
|
||||||
PARAMS = dict(
|
|
||||||
ramp=PARAM("ramping speed in K/min",
|
ramp=PARAM("ramping speed in K/min",
|
||||||
validator=floatrange(0, 1e3), default=1,
|
validator=floatrange(0, 1e3), default=1,
|
||||||
),
|
),
|
||||||
@ -95,16 +98,15 @@ class Cryostat(Driveable):
|
|||||||
self._thread = threading.Thread(target=self.thread)
|
self._thread = threading.Thread(target=self.thread)
|
||||||
self._thread.daemon = True
|
self._thread.daemon = True
|
||||||
self._thread.start()
|
self._thread.start()
|
||||||
#XXX: hack!!! use a singleton as registry for the other devices to access this one...
|
|
||||||
hack.append(self)
|
|
||||||
|
|
||||||
def read_status(self):
|
def read_status(self):
|
||||||
# instead of asking a 'Hardware' take the value from the simulation thread
|
# instead of asking a 'Hardware' take the value from the simulation
|
||||||
return self.status
|
return self.status
|
||||||
|
|
||||||
def read_value(self, maxage=0):
|
def read_value(self, maxage=0):
|
||||||
# return regulation value (averaged regulation temp)
|
# return regulation value (averaged regulation temp)
|
||||||
return self.regulationtemp + self.config_jitter * (0.5 - random.random())
|
return self.regulationtemp + \
|
||||||
|
self.config_jitter * (0.5 - random.random())
|
||||||
|
|
||||||
def read_target(self, maxage=0):
|
def read_target(self, maxage=0):
|
||||||
return self.target
|
return self.target
|
||||||
@ -119,12 +121,13 @@ class Cryostat(Driveable):
|
|||||||
|
|
||||||
def write_maxpower(self, newpower):
|
def write_maxpower(self, newpower):
|
||||||
# rescale heater setting in % to keep the power
|
# rescale heater setting in % to keep the power
|
||||||
self.heater = max(0, min(100, self.heater * self.maxpower / float(newpower)))
|
heat = max(0, min(100, self.heater * self.maxpower / float(newpower)))
|
||||||
|
self.heater = heat
|
||||||
self.maxpower = newpower
|
self.maxpower = newpower
|
||||||
|
|
||||||
def doStop(self):
|
def doStop(self):
|
||||||
# stop the ramp by setting current value as target
|
# stop the ramp by setting current setpoint as target
|
||||||
# XXX: there may be use case where setting the current temp may be better
|
# XXX: discussion: take setpoint or current value ???
|
||||||
self.write_target(self.setpoint)
|
self.write_target(self.setpoint)
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -176,7 +179,8 @@ class Cryostat(Driveable):
|
|||||||
# local state keeping:
|
# local state keeping:
|
||||||
regulation = self.regulationtemp
|
regulation = self.regulationtemp
|
||||||
sample = self.sampletemp
|
sample = self.sampletemp
|
||||||
window = [] # keep history values for stability check
|
# keep history values for stability check
|
||||||
|
window = []
|
||||||
timestamp = time.time()
|
timestamp = time.time()
|
||||||
heater = 0
|
heater = 0
|
||||||
lastflow = 0
|
lastflow = 0
|
||||||
@ -210,7 +214,8 @@ class Cryostat(Driveable):
|
|||||||
newregulation = max(0, regulation +
|
newregulation = max(0, regulation +
|
||||||
regdelta / self.__coolerCP(regulation) * h)
|
regdelta / self.__coolerCP(regulation) * h)
|
||||||
# b) see
|
# b) see
|
||||||
# http://brettbeauregard.com/blog/2011/04/improving-the-beginners-pid-introduction/
|
# http://brettbeauregard.com/blog/2011/04/
|
||||||
|
# improving-the-beginners-pid-introduction/
|
||||||
if self.mode != 'openloop':
|
if self.mode != 'openloop':
|
||||||
# fix artefacts due to too big timesteps
|
# fix artefacts due to too big timesteps
|
||||||
# actually i would prefer reducing looptime, but i have no
|
# actually i would prefer reducing looptime, but i have no
|
||||||
@ -320,4 +325,3 @@ class Cryostat(Driveable):
|
|||||||
self._stopflag = True
|
self._stopflag = True
|
||||||
if self._thread and self._thread.isAlive():
|
if self._thread and self._thread.isAlive():
|
||||||
self._thread.join()
|
self._thread.join()
|
||||||
|
|
||||||
|
@ -23,42 +23,51 @@
|
|||||||
|
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from devices.core import Readable, Driveable, CONFIG, PARAM
|
from devices.core import Readable, Driveable, PARAM
|
||||||
from validators import floatrange
|
from validators import floatrange
|
||||||
|
|
||||||
|
|
||||||
class LN2(Readable):
|
class LN2(Readable):
|
||||||
"""Just a readable.
|
"""Just a readable.
|
||||||
|
|
||||||
class name indicates it to be a sensor for LN2, but the implementation may do anything"""
|
class name indicates it to be a sensor for LN2,
|
||||||
|
but the implementation may do anything
|
||||||
|
"""
|
||||||
def read_value(self, maxage=0):
|
def read_value(self, maxage=0):
|
||||||
return round(100*random.random(), 1)
|
return round(100*random.random(), 1)
|
||||||
|
|
||||||
|
|
||||||
class Heater(Driveable):
|
class Heater(Driveable):
|
||||||
"""Just a driveable.
|
"""Just a driveable.
|
||||||
|
|
||||||
class name indicates it to be some heating element, but the implementation may do anything"""
|
class name indicates it to be some heating element,
|
||||||
CONFIG = {
|
but the implementation may do anything
|
||||||
'maxheaterpower' : CONFIG('maximum allowed heater power',
|
"""
|
||||||
|
PARAMS = {
|
||||||
|
'maxheaterpower': PARAM('maximum allowed heater power',
|
||||||
validator=floatrange(0, 100), unit='W'),
|
validator=floatrange(0, 100), unit='W'),
|
||||||
}
|
}
|
||||||
|
|
||||||
def read_value(self, maxage=0):
|
def read_value(self, maxage=0):
|
||||||
return round(100*random.random(), 1)
|
return round(100*random.random(), 1)
|
||||||
|
|
||||||
def write_target(self, target):
|
def write_target(self, target):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Temp(Driveable):
|
class Temp(Driveable):
|
||||||
"""Just a driveable.
|
"""Just a driveable.
|
||||||
|
|
||||||
class name indicates it to be some temperature controller, but the implementation may do anything"""
|
class name indicates it to be some temperature controller,
|
||||||
CONFIG = {
|
but the implementation may do anything
|
||||||
'sensor' : CONFIG("Sensor number or calibration id",
|
"""
|
||||||
validator=str),
|
PARAMS = {
|
||||||
|
'sensor': PARAM("Sensor number or calibration id",
|
||||||
|
validator=str, readonly=True),
|
||||||
}
|
}
|
||||||
|
|
||||||
def read_value(self, maxage=0):
|
def read_value(self, maxage=0):
|
||||||
return round(100*random.random(), 1)
|
return round(100*random.random(), 1)
|
||||||
|
|
||||||
def write_target(self, target):
|
def write_target(self, target):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,11 +22,14 @@
|
|||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
"""error class for our little framework"""
|
"""error class for our little framework"""
|
||||||
|
|
||||||
|
|
||||||
class SECoPServerError(Exception):
|
class SECoPServerError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ConfigError(SECoPServerError):
|
class ConfigError(SECoPServerError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ProgrammingError(SECoPServerError):
|
class ProgrammingError(SECoPServerError):
|
||||||
pass
|
pass
|
||||||
|
@ -22,16 +22,17 @@
|
|||||||
|
|
||||||
"""Define helpers"""
|
"""Define helpers"""
|
||||||
|
|
||||||
import logging
|
|
||||||
from os import path
|
|
||||||
|
|
||||||
class attrdict(dict):
|
class attrdict(dict):
|
||||||
"""a normal dict, providing access also via attributes"""
|
"""a normal dict, providing access also via attributes"""
|
||||||
|
|
||||||
def __getattr__(self, key):
|
def __getattr__(self, key):
|
||||||
return self[key]
|
return self[key]
|
||||||
|
|
||||||
def __setattr__(self, key, value):
|
def __setattr__(self, key, value):
|
||||||
self[key] = value
|
self[key] = value
|
||||||
|
|
||||||
|
|
||||||
def clamp(_min, value, _max):
|
def clamp(_min, value, _max):
|
||||||
"""return the median of 3 values,
|
"""return the median of 3 values,
|
||||||
|
|
||||||
@ -41,26 +42,15 @@ def clamp(_min, value, _max):
|
|||||||
# return median, i.e. clamp the the value between min and max
|
# return median, i.e. clamp the the value between min and max
|
||||||
return sorted([_min, value, _max])[1]
|
return sorted([_min, value, _max])[1]
|
||||||
|
|
||||||
|
|
||||||
def get_class(spec):
|
def get_class(spec):
|
||||||
"""loads a class given by string in dotted notaion (as python would do)"""
|
"""loads a class given by string in dotted notaion (as python would do)"""
|
||||||
modname, classname = spec.rsplit('.', 1)
|
modname, classname = spec.rsplit('.', 1)
|
||||||
import importlib
|
import importlib
|
||||||
# module = importlib.import_module(modname)
|
module = importlib.import_module(modname)
|
||||||
module = __import__(spec)
|
# module = __import__(spec)
|
||||||
return getattr(module, classname)
|
return getattr(module, classname)
|
||||||
|
|
||||||
def make_logger(inst='server', name='', base_path='', loglevel=logging.INFO):
|
|
||||||
# XXX: rework this! (outsource to a logging module...)
|
|
||||||
if name:
|
|
||||||
inst = '%s %s' % (inst, name)
|
|
||||||
logging.basicConfig(level=loglevel, format='%(asctime)s %(levelname)s %(message)s')
|
|
||||||
logger = logging.getLogger(inst)
|
|
||||||
logger.setLevel(loglevel)
|
|
||||||
fh = logging.FileHandler(path.join(base_path, 'log', (name or inst) + '.log'))
|
|
||||||
fh.setLevel(loglevel)
|
|
||||||
logger.addHandler(fh)
|
|
||||||
return logger
|
|
||||||
|
|
||||||
|
|
||||||
# moved below definitions to break import cycle
|
# moved below definitions to break import cycle
|
||||||
from pidfile import *
|
from pidfile import *
|
||||||
@ -73,4 +63,3 @@ if __name__ == '__main__':
|
|||||||
d.c = 9
|
d.c = 9
|
||||||
d['d'] = 'c'
|
d['d'] = 'c'
|
||||||
assert d[d.d] == 9
|
assert d[d.d] == 9
|
||||||
|
|
||||||
|
@ -33,21 +33,23 @@ def read_pidfile(pidfile):
|
|||||||
try:
|
try:
|
||||||
with open(pidfile, 'r') as f:
|
with open(pidfile, 'r') as f:
|
||||||
return int(f.read())
|
return int(f.read())
|
||||||
except OSError:
|
except (OSError, IOError):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def remove_pidfile(pidfile):
|
def remove_pidfile(pidfile):
|
||||||
"""remove the given pidfile, typically at end of the process"""
|
"""remove the given pidfile, typically at end of the process"""
|
||||||
os.remove(pidfile)
|
os.remove(pidfile)
|
||||||
|
|
||||||
|
|
||||||
def write_pidfile(pidfile, pid):
|
def write_pidfile(pidfile, pid):
|
||||||
"""write the given pid to the given pidfile"""
|
"""write the given pid to the given pidfile"""
|
||||||
with open(pidfile, 'w') as f:
|
with open(pidfile, 'w') as f:
|
||||||
f.write('%d\n' % pid)
|
f.write('%d\n' % pid)
|
||||||
atexit.register(remove_pidfile, pidfile)
|
atexit.register(remove_pidfile, pidfile)
|
||||||
|
|
||||||
|
|
||||||
def check_pidfile(pidfile):
|
def check_pidfile(pidfile):
|
||||||
"""check if the process from a given pidfile is still running"""
|
"""check if the process from a given pidfile is still running"""
|
||||||
pid = read_pidfile(pidfile)
|
pid = read_pidfile(pidfile)
|
||||||
return False if pid is None else psutil.pid_exists(pid)
|
return False if pid is None else psutil.pid_exists(pid)
|
||||||
|
|
||||||
|
@ -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
|
"""Define SECoP Device classes
|
||||||
|
|
||||||
also define helpers to derive properties of the device"""
|
"""
|
||||||
|
# XXX: is this still needed ???
|
||||||
|
# see devices.core ....
|
||||||
|
|
||||||
from lib import attrdict
|
from lib import attrdict
|
||||||
from protocol import status
|
from protocol import status
|
||||||
@ -34,27 +36,36 @@ class Device(object):
|
|||||||
|
|
||||||
all others derive from this"""
|
all others derive from this"""
|
||||||
name = None
|
name = None
|
||||||
|
|
||||||
def read_status(self):
|
def read_status(self):
|
||||||
raise NotImplementedError('All Devices need a Status!')
|
raise NotImplementedError('All Devices need a Status!')
|
||||||
|
|
||||||
def read_name(self):
|
def read_name(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
class Readable(Device):
|
class Readable(Device):
|
||||||
"""A Readable Device"""
|
"""A Readable Device"""
|
||||||
unit = ''
|
unit = ''
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
raise NotImplementedError('A Readable MUST provide a value')
|
raise NotImplementedError('A Readable MUST provide a value')
|
||||||
|
|
||||||
def read_unit(self):
|
def read_unit(self):
|
||||||
return self.unit
|
return self.unit
|
||||||
|
|
||||||
|
|
||||||
class Writeable(Readable):
|
class Writeable(Readable):
|
||||||
"""Writeable can be told to change it's vallue"""
|
"""Writeable can be told to change it's vallue"""
|
||||||
target = None
|
target = None
|
||||||
|
|
||||||
def read_target(self):
|
def read_target(self):
|
||||||
return self.target
|
return self.target
|
||||||
|
|
||||||
def write_target(self, target):
|
def write_target(self, target):
|
||||||
self.target = target
|
self.target = target
|
||||||
|
|
||||||
|
|
||||||
class Driveable(Writeable):
|
class Driveable(Writeable):
|
||||||
"""A Moveable which may take a while to reach its target,
|
"""A Moveable which may take a while to reach its target,
|
||||||
|
|
||||||
@ -62,32 +73,3 @@ class Driveable(Writeable):
|
|||||||
def do_stop(self):
|
def do_stop(self):
|
||||||
raise NotImplementedError('A Driveable MUST implement the STOP() '
|
raise NotImplementedError('A Driveable MUST implement the STOP() '
|
||||||
'command')
|
'command')
|
||||||
|
|
||||||
|
|
||||||
def get_device_pars(dev):
|
|
||||||
"""return a mapping of the devices parameter names to some
|
|
||||||
'description'"""
|
|
||||||
res = {}
|
|
||||||
for n in dir(dev):
|
|
||||||
if n.startswith('read_'):
|
|
||||||
pname = n[5:]
|
|
||||||
entry = attrdict(readonly=True, description=getattr(dev, n).__doc__)
|
|
||||||
if hasattr(dev, 'write_%s' % pname):
|
|
||||||
entry['readonly'] = False
|
|
||||||
res[pname] = entry
|
|
||||||
return res
|
|
||||||
|
|
||||||
def get_device_cmds(dev):
|
|
||||||
"""return a mapping of the devices command names to some
|
|
||||||
'description'"""
|
|
||||||
res = {}
|
|
||||||
for n in dir(dev):
|
|
||||||
if n.startswith('do_'):
|
|
||||||
cname = n[5:]
|
|
||||||
func = getattr(dev, n)
|
|
||||||
# XXX: use inspect!
|
|
||||||
entry = attrdict(description=func.__doc__, args='unknown')
|
|
||||||
res[cname] = entry
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
|
270
src/protocol/dispatcher.py
Normal file
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,128 +22,115 @@
|
|||||||
|
|
||||||
"""Define SECoP Messages"""
|
"""Define SECoP Messages"""
|
||||||
|
|
||||||
from lib import attrdict
|
REQUEST = 'request'
|
||||||
import time
|
REPLY = 'reply'
|
||||||
from device import get_device_pars, get_device_cmds
|
ERROR = 'error'
|
||||||
|
|
||||||
class Request(object):
|
|
||||||
"""Base class for all Requests"""
|
|
||||||
pars = []
|
|
||||||
def __repr__(self):
|
|
||||||
pars = ', '.join('%s=%r' % (k, self.__dict__[k]) for k in self.pars)
|
|
||||||
s = '%s(%s)' % (self.__class__.__name__, pars)
|
|
||||||
return s
|
|
||||||
|
|
||||||
class Reply(object):
|
|
||||||
"""Base class for all Replies"""
|
|
||||||
pars = []
|
|
||||||
def __repr__(self):
|
|
||||||
pars = ', '.join('%s=%r' % (k, self.__dict__[k]) for k in self.pars)
|
|
||||||
s = '%s(%s)' % (self.__class__.__name__, pars)
|
|
||||||
return s
|
|
||||||
|
|
||||||
|
|
||||||
|
# base classes
|
||||||
|
class Message(object):
|
||||||
|
ARGS = []
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwds):
|
||||||
|
names = self.ARGS[:]
|
||||||
|
if len(args) > len(names):
|
||||||
|
raise TypeError('%s.__init__() takes only %d argument(s) (%d given)' %
|
||||||
|
(self.__class__, len(names), len(args)))
|
||||||
|
for arg in args:
|
||||||
|
self.__dict__[names.pop(0)] = arg
|
||||||
|
# now check keyworded args if any
|
||||||
|
for k, v in kwds.items():
|
||||||
|
if k not in names:
|
||||||
|
if k in self.ARGS:
|
||||||
|
raise TypeError('__init__() got multiple values for '
|
||||||
|
'keyword argument %r' % k)
|
||||||
|
raise TypeError('__init__() got an unexpected keyword '
|
||||||
|
'argument %r' % k)
|
||||||
|
names.remove(k)
|
||||||
|
self.__dict__[k] = v
|
||||||
|
if names:
|
||||||
|
raise TypeError('__init__() takes at least %d arguments (%d given)'
|
||||||
|
% len(self.ARGS), len(args)+len(kwds))
|
||||||
|
self.NAME = (self.__class__.__name__[:-len(self.TYPE)] or
|
||||||
|
self.__class__.__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Request(Message):
|
||||||
|
TYPE = REQUEST
|
||||||
|
|
||||||
|
|
||||||
|
class Reply(Message):
|
||||||
|
TYPE = REPLY
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorReply(Message):
|
||||||
|
TYPE = ERROR
|
||||||
|
|
||||||
|
|
||||||
|
# actuall message objects
|
||||||
class ListDevicesRequest(Request):
|
class ListDevicesRequest(Request):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ListDevicesReply(Reply):
|
class ListDevicesReply(Reply):
|
||||||
pars = ['list_of_devices']
|
ARGS = ['list_of_devices', 'descriptive_data']
|
||||||
def __init__(self, args):
|
|
||||||
self.list_of_devices = args
|
|
||||||
|
|
||||||
|
|
||||||
class ListDeviceParamsRequest(Request):
|
class ListDeviceParamsRequest(Request):
|
||||||
pars = ['device']
|
ARGS = ['device']
|
||||||
def __init__(self, device):
|
|
||||||
self.device = device
|
|
||||||
|
|
||||||
class ListDeviceParamsReply(Reply):
|
class ListDeviceParamsReply(Reply):
|
||||||
pars = ['device', 'params']
|
ARGS = ['device', 'params']
|
||||||
def __init__(self, device, params):
|
|
||||||
self.device = device
|
|
||||||
self.params = params
|
|
||||||
|
|
||||||
class ReadValueRequest(Request):
|
class ReadValueRequest(Request):
|
||||||
pars = ['device', 'maxage']
|
ARGS = ['device', 'maxage']
|
||||||
def __init__(self, device, maxage=0):
|
|
||||||
self.device = device
|
|
||||||
self.maxage = maxage
|
|
||||||
|
|
||||||
class ReadValueReply(Reply):
|
class ReadValueReply(Reply):
|
||||||
pars = ['device', 'value', 'timestamp', 'error', 'unit']
|
ARGS = ['device', 'value', 'timestamp', 'error', 'unit']
|
||||||
def __init__(self, device, value, timestamp=0, error=0, unit=None):
|
|
||||||
self.device = device
|
|
||||||
self.value = value
|
|
||||||
self.timestamp = timestamp
|
|
||||||
self.error = error
|
|
||||||
self.unit = unit
|
|
||||||
|
|
||||||
|
|
||||||
class ReadParamRequest(Request):
|
class ReadParamRequest(Request):
|
||||||
pars = ['device', 'param', 'maxage']
|
ARGS = ['device', 'param', 'maxage']
|
||||||
def __init__(self, device, param, maxage=0):
|
|
||||||
self.device = device
|
|
||||||
self.param = param
|
|
||||||
self.maxage = maxage
|
|
||||||
|
|
||||||
class ReadParamReply(Reply):
|
class ReadParamReply(Reply):
|
||||||
pars = ['device', 'param', 'value', 'timestamp', 'error', 'unit']
|
ARGS = ['device', 'param', 'value', 'timestamp', 'error', 'unit']
|
||||||
def __init__(self, device, param, value, timestamp=0, error=0, unit=None):
|
|
||||||
self.device = device
|
|
||||||
self.param = param
|
|
||||||
self.value = value
|
|
||||||
self.timestamp = timestamp
|
|
||||||
self.error = error
|
|
||||||
self.unit = unit
|
|
||||||
|
|
||||||
|
|
||||||
class WriteParamRequest(Request):
|
class WriteParamRequest(Request):
|
||||||
pars = ['device', 'param', 'value']
|
ARGS = ['device', 'param', 'value']
|
||||||
def __init__(self, device, param, value):
|
|
||||||
self.device = device
|
|
||||||
self.param = param
|
|
||||||
self.value = value
|
|
||||||
|
|
||||||
class WriteParamReply(Reply):
|
class WriteParamReply(Reply):
|
||||||
pars = ['device', 'param', 'readback_value', 'timestamp', 'error', 'unit']
|
ARGS = ['device', 'param', 'readback_value', 'timestamp', 'error', 'unit']
|
||||||
def __init__(self, device, param, readback_value, timestamp=0, error=0,
|
|
||||||
unit=None):
|
|
||||||
self.device = device
|
|
||||||
self.param = param
|
|
||||||
self.readback_value = readback_value
|
|
||||||
self.timestamp = timestamp
|
|
||||||
self.error = error
|
|
||||||
self.unit = unit
|
|
||||||
|
|
||||||
|
|
||||||
class RequestAsyncDataRequest(Request):
|
class RequestAsyncDataRequest(Request):
|
||||||
pars = ['device', 'params']
|
ARGS = ['device', 'params']
|
||||||
def __init__(self, device, *args):
|
|
||||||
self.device = device
|
|
||||||
self.params = args
|
|
||||||
|
|
||||||
class RequestAsyncDataReply(Reply):
|
class RequestAsyncDataReply(Reply):
|
||||||
pars = ['device', 'paramvalue_list']
|
ARGS = ['device', 'paramvalue_list']
|
||||||
def __init__(self, device, *args):
|
|
||||||
self.device = device
|
|
||||||
self.paramvalue_list = args
|
|
||||||
|
|
||||||
class AsyncDataUnit(ReadParamReply):
|
class AsyncDataUnit(ReadParamReply):
|
||||||
pass
|
ARGS = ['device', 'param', 'value', 'timestamp', 'error', 'unit']
|
||||||
|
|
||||||
|
|
||||||
class ListOfFeaturesRequest(Request):
|
class ListOfFeaturesRequest(Request):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ListOfFeaturesReply(Reply):
|
class ListOfFeaturesReply(Reply):
|
||||||
pars = ['features']
|
ARGS = ['features']
|
||||||
def __init__(self, *args):
|
|
||||||
self.features = args
|
|
||||||
|
|
||||||
class ActivateFeatureRequest(Request):
|
class ActivateFeatureRequest(Request):
|
||||||
pars = ['feature']
|
ARGS = ['feature']
|
||||||
def __init__(self, feature):
|
|
||||||
self.feature = feature
|
|
||||||
|
|
||||||
class ActivateFeatureReply(Reply):
|
class ActivateFeatureReply(Reply):
|
||||||
# Ack style or Error
|
# Ack style or Error
|
||||||
@ -151,207 +138,55 @@ class ActivateFeatureReply(Reply):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ProtocollError(ErrorReply):
|
||||||
|
ARGS = ['msgtype', 'msgname', 'msgargs']
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorReply(Reply):
|
||||||
|
ARGS = ['error']
|
||||||
|
|
||||||
|
|
||||||
|
class NoSuchDeviceErrorReply(ErrorReply):
|
||||||
|
ARGS = ['device']
|
||||||
|
|
||||||
|
|
||||||
|
class NoSuchParamErrorReply(ErrorReply):
|
||||||
|
ARGS = ['device', 'param']
|
||||||
|
|
||||||
|
|
||||||
|
class ParamReadonlyErrorReply(ErrorReply):
|
||||||
|
ARGS = ['device', 'param']
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedFeatureErrorReply(ErrorReply):
|
||||||
|
ARGS = ['feature']
|
||||||
|
|
||||||
|
|
||||||
|
class NoSuchCommandErrorReply(ErrorReply):
|
||||||
|
ARGS = ['device', 'command']
|
||||||
|
|
||||||
|
|
||||||
|
class CommandFailedErrorReply(ErrorReply):
|
||||||
|
ARGS = ['device', 'command']
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidParamValueErrorReply(ErrorReply):
|
||||||
|
ARGS = ['device', 'param', 'value']
|
||||||
|
|
||||||
|
# Fun!
|
||||||
|
|
||||||
|
|
||||||
|
class HelpRequest(Request):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class HelpReply(Reply):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
FEATURES = [
|
FEATURES = [
|
||||||
'Feature1',
|
'Feature1',
|
||||||
'Feature2',
|
'Feature2',
|
||||||
'Feature3',
|
'Feature3',
|
||||||
|
'Future',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# Error replies:
|
|
||||||
|
|
||||||
class ErrorReply(Reply):
|
|
||||||
pars = ['error']
|
|
||||||
def __init__(self, error):
|
|
||||||
self.error = error
|
|
||||||
|
|
||||||
class NoSuchDeviceErrorReply(ErrorReply):
|
|
||||||
pars = ['device']
|
|
||||||
def __init__(self, device):
|
|
||||||
self.device = device
|
|
||||||
|
|
||||||
class NoSuchParamErrorReply(ErrorReply):
|
|
||||||
pars = ['device', 'param']
|
|
||||||
def __init__(self, device, param):
|
|
||||||
self.device = device
|
|
||||||
self.param = param
|
|
||||||
|
|
||||||
class ParamReadonlyErrorReply(ErrorReply):
|
|
||||||
pars = ['device', 'param']
|
|
||||||
def __init__(self, device, param):
|
|
||||||
self.device = device
|
|
||||||
self.param = param
|
|
||||||
|
|
||||||
class UnsupportedFeatureErrorReply(ErrorReply):
|
|
||||||
pars = ['feature']
|
|
||||||
def __init__(self, feature):
|
|
||||||
self.feature = feature
|
|
||||||
|
|
||||||
class NoSuchCommandErrorReply(ErrorReply):
|
|
||||||
pars = ['device', 'command']
|
|
||||||
def __init__(self, device, command):
|
|
||||||
self.device = device
|
|
||||||
self.command = command
|
|
||||||
|
|
||||||
class CommandFailedErrorReply(ErrorReply):
|
|
||||||
pars = ['device', 'command']
|
|
||||||
def __init__(self, device, command):
|
|
||||||
self.device = device
|
|
||||||
self.command = command
|
|
||||||
|
|
||||||
class InvalidParamValueErrorReply(ErrorReply):
|
|
||||||
pars = ['device', 'param', 'value']
|
|
||||||
def __init__(self, device, param, value):
|
|
||||||
self.device = device
|
|
||||||
self.param = param
|
|
||||||
self.value = value
|
|
||||||
|
|
||||||
class MessageHandler(object):
|
|
||||||
"""puts meaning to the request objects"""
|
|
||||||
def handle_ListDevices(self, msgargs):
|
|
||||||
return ListDevicesReply(self.listDevices())
|
|
||||||
|
|
||||||
def handle_ListDeviceParams(self, msgargs):
|
|
||||||
devobj = self.getDevice(msgargs.device)
|
|
||||||
if devobj:
|
|
||||||
return ListDeviceParamsReply(msgargs.device,
|
|
||||||
get_device_pars(devobj))
|
|
||||||
else:
|
|
||||||
return NoSuchDeviceErrorReply(msgargs.device)
|
|
||||||
|
|
||||||
def handle_ReadValue(self, msgargs):
|
|
||||||
devobj = self.getDevice(msgargs.device)
|
|
||||||
if devobj:
|
|
||||||
return ReadValueReply(msgargs.device, devobj.read_value(),
|
|
||||||
timestamp=time.time())
|
|
||||||
else:
|
|
||||||
return NoSuchDeviceErrorReply(msgargs.device)
|
|
||||||
|
|
||||||
def handle_ReadParam(self, msgargs):
|
|
||||||
devobj = self.getDevice(msgargs.device)
|
|
||||||
if devobj:
|
|
||||||
readfunc = getattr(devobj, 'read_%s' % msgargs.param, None)
|
|
||||||
if readfunc:
|
|
||||||
return ReadParamReply(msgargs.device, msgargs.param,
|
|
||||||
readfunc(), timestamp=time.time())
|
|
||||||
else:
|
|
||||||
return NoSuchParamErrorReply(msgargs.device, msgargs.param)
|
|
||||||
else:
|
|
||||||
return NoSuchDeviceErrorReply(msgargs.device)
|
|
||||||
|
|
||||||
def handle_WriteParam(self, msgargs):
|
|
||||||
devobj = self.getDevice(msgargs.device)
|
|
||||||
if devobj:
|
|
||||||
writefunc = getattr(devobj, 'write_%s' % msgargs.param, None)
|
|
||||||
if writefunc:
|
|
||||||
readbackvalue = writefunc(msgargs.value) or msgargs.value
|
|
||||||
return WriteParamReply(msgargs.device, msgargs.param,
|
|
||||||
readbackvalue,
|
|
||||||
timestamp=time.time())
|
|
||||||
else:
|
|
||||||
if getattr(devobj, 'read_%s' % msgargs.param, None):
|
|
||||||
return ParamReadonlyErrorReply(msgargs.device,
|
|
||||||
msgargs.param)
|
|
||||||
else:
|
|
||||||
return NoSuchParamErrorReply(msgargs.device,
|
|
||||||
msgargs.param)
|
|
||||||
else:
|
|
||||||
return NoSuchDeviceErrorReply(msgargs.device)
|
|
||||||
|
|
||||||
def handle_RequestAsyncData(self, msgargs):
|
|
||||||
return ErrorReply('AsyncData is not (yet) supported')
|
|
||||||
|
|
||||||
def handle_ListOfFeatures(self, msgargs):
|
|
||||||
# no features supported (yet)
|
|
||||||
return ListOfFeaturesReply([])
|
|
||||||
|
|
||||||
def handle_ActivateFeature(self, msgargs):
|
|
||||||
return ErrorReply('Features are not (yet) supported')
|
|
||||||
|
|
||||||
def unhandled(self, msgname, msgargs):
|
|
||||||
"""handler for unhandled Messages
|
|
||||||
|
|
||||||
(no handle_<messagename> method was defined)
|
|
||||||
"""
|
|
||||||
self.log.error('IGN: got unhandled request %s' % msgname)
|
|
||||||
return ErrorReply('Got Unhandled Request')
|
|
||||||
|
|
||||||
|
|
||||||
def parse(message):
|
|
||||||
# parses a message and returns
|
|
||||||
# msgtype, msgname and parameters of message (as dict)
|
|
||||||
msgtype = 'unknown'
|
|
||||||
msgname = 'unknown'
|
|
||||||
if isinstance(message, ErrorReply):
|
|
||||||
msgtype = 'error'
|
|
||||||
msgname = message.__class__.__name__[:-len('Reply')]
|
|
||||||
elif isinstance(message, Request):
|
|
||||||
msgtype = 'request'
|
|
||||||
msgname = message.__class__.__name__[:-len('Request')]
|
|
||||||
elif isinstance(message, Reply):
|
|
||||||
msgtype = 'reply'
|
|
||||||
msgname = message.__class__.__name__[:-len('Reply')]
|
|
||||||
return msgtype, msgname, \
|
|
||||||
attrdict([(k, getattr(message, k)) for k in message.pars])
|
|
||||||
|
|
||||||
__ALL__ = ['ErrorReply',
|
|
||||||
'NoSuchDeviceErrorReply', 'NoSuchParamErrorReply'
|
|
||||||
'ParamReadonlyErrorReply', 'UnsupportedFeatureErrorReply',
|
|
||||||
'NoSuchCommandErrorReply', 'CommandFailedErrorReply',
|
|
||||||
'InvalidParamValueErrorReply',
|
|
||||||
'Reply',
|
|
||||||
'ListDevicesReply', 'ListDeviceParamsReply', 'ReadValueReply',
|
|
||||||
'ReadParamReply', 'WriteParamReply', 'RequestAsyncDataReply',
|
|
||||||
'AsyncDataUnit', 'ListOfFeaturesReply', 'ActivateFeatureReply',
|
|
||||||
'Request',
|
|
||||||
'ListDevicesRequest', 'ListDeviceParamsRequest', 'ReadValueRequest',
|
|
||||||
'ReadParamRequest', 'WriteParamRequest', 'RequestAsyncDataRequest',
|
|
||||||
'ListOfFeaturesRequest', 'ActivateFeatureRequest',
|
|
||||||
'parse', 'MessageHandler',
|
|
||||||
]
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
print "minimal testing: transport"
|
|
||||||
testcases = dict(
|
|
||||||
error=[ErrorReply(),
|
|
||||||
NoSuchDeviceErrorReply('device3'),
|
|
||||||
NoSuchParamErrorReply('device2', 'param3'),
|
|
||||||
ParamReadonlyErrorReply('device1', 'param1'),
|
|
||||||
UnsupportedFeatureErrorReply('feature5'),
|
|
||||||
NoSuchCommandErrorReply('device1', 'fance_command'),
|
|
||||||
CommandFailedErrorReply('device1', 'stop'),
|
|
||||||
InvalidParamValueErrorReply('device1', 'param2', 'STRING_Value'),
|
|
||||||
],
|
|
||||||
reply=[Reply(),
|
|
||||||
ListDevicesReply('device1', 'device2'),
|
|
||||||
ListDeviceParamsReply('device', ['param1', 'param2']),
|
|
||||||
ReadValueReply('device2', 3.1415),
|
|
||||||
ReadParamReply('device1', 'param2', 2.718),
|
|
||||||
WriteParamReply('device1', 'param2', 2.718),
|
|
||||||
RequestAsyncDataReply('device1', '?what to put here?'),
|
|
||||||
AsyncDataUnit('device1', 'param2', 2.718),
|
|
||||||
ListOfFeaturesReply('feature1', 'feature2'),
|
|
||||||
ActivateFeatureReply(),
|
|
||||||
],
|
|
||||||
request=[Request(),
|
|
||||||
ListDevicesRequest(),
|
|
||||||
ListDeviceParamsRequest('device1'),
|
|
||||||
ReadValueRequest('device2'),
|
|
||||||
ReadParamRequest('device1', 'param2'),
|
|
||||||
WriteParamRequest('device1', 'param2', 2.718),
|
|
||||||
RequestAsyncDataRequest('device1', ['param1', 'param2']),
|
|
||||||
ListOfFeaturesRequest(),
|
|
||||||
ActivateFeatureRequest('feature1'),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
for msgtype, msgs in testcases.items():
|
|
||||||
print "___ testing %ss ___" % msgtype
|
|
||||||
for msg in msgs:
|
|
||||||
print msg.__class__.__name__, 'is', msgtype,
|
|
||||||
decoded = parse(msg)
|
|
||||||
if decoded[0] != msgtype:
|
|
||||||
print "\tFAIL, got %r but expected %r" %(decoded[0], msgtype)
|
|
||||||
else:
|
|
||||||
print "\tOk"
|
|
||||||
print
|
|
||||||
|
|
||||||
|
@ -28,4 +28,3 @@ WARN = 300
|
|||||||
UNSTABLE = 350
|
UNSTABLE = 350
|
||||||
ERROR = 400
|
ERROR = 400
|
||||||
UNKNOWN = -1
|
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."""
|
"""Define validators."""
|
||||||
|
|
||||||
|
|
||||||
# a Validator validates a given object and raises an ValueError if it doesn't fit
|
# a Validator returns a validated object or raises an ValueError
|
||||||
# easy python validators: int(), float(), str()
|
# easy python validators: int(), float(), str()
|
||||||
|
# also validators should have a __repr__ returning a 'python' string
|
||||||
|
# which recreates them
|
||||||
|
|
||||||
|
class Validator(object):
|
||||||
|
# list of tuples: (name, converter)
|
||||||
|
params = []
|
||||||
|
valuetype = float
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwds):
|
||||||
|
plist = self.params[:]
|
||||||
|
if len(args) > len(plist):
|
||||||
|
raise ProgrammingError('%s takes %d parameters only (%d given)' % (
|
||||||
|
self.__class__.__name__,
|
||||||
|
len(plist), len(args)))
|
||||||
|
for pval in args:
|
||||||
|
pname, pconv = plist.pop(0)
|
||||||
|
if pname in kwds:
|
||||||
|
raise ProgrammingError('%s: positional parameter %s als given '
|
||||||
|
'as keyword!' % (
|
||||||
|
self.__class__.__name__,
|
||||||
|
pname))
|
||||||
|
self.__dict__[pname] = pconv(pval)
|
||||||
|
for pname, pconv in plist:
|
||||||
|
if pname in kwds:
|
||||||
|
pval = kwds.pop(pname)
|
||||||
|
self.__dict__[pname] = pconv(pval)
|
||||||
|
else:
|
||||||
|
raise ProgrammingError('%s: param %s left unspecified!' % (
|
||||||
|
self.__class__.__name__,
|
||||||
|
pname))
|
||||||
|
|
||||||
|
if kwds:
|
||||||
|
raise ProgrammingError('%s got unknown arguments: %s' % (
|
||||||
|
self.__class__.__name__,
|
||||||
|
', '.join(list(kwds.keys()))))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
params = ['%s=%r' % (pn, self.__dict__[pn]) for pn in self.params]
|
||||||
|
return ('%s(%s)' % (self.__class__.__name__, ', '.join(params)))
|
||||||
|
|
||||||
class floatrange(object):
|
|
||||||
def __init__(self, lower, upper):
|
|
||||||
self.lower = float(lower)
|
|
||||||
self.upper = float(upper)
|
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
value = float(value)
|
return self.check(self.valuetype(value))
|
||||||
if not self.lower <= value <= self.upper:
|
|
||||||
raise ValueError('Floatrange: value %r must be within %f and %f' % (value, self.lower, self.upper))
|
|
||||||
|
class floatrange(Validator):
|
||||||
|
params = [('lower', float), ('upper', float)]
|
||||||
|
|
||||||
|
def check(self, value):
|
||||||
|
if self.lower <= value <= self.upper:
|
||||||
return value
|
return value
|
||||||
|
raise ValueError('Floatrange: value %r must be within %f and %f' %
|
||||||
|
(value, self.lower, self.upper))
|
||||||
|
|
||||||
|
|
||||||
def positive(obj):
|
class positive(Validator):
|
||||||
if obj <= 0:
|
def check(self, value):
|
||||||
|
if value > 0:
|
||||||
|
return value
|
||||||
raise ValueError('Value %r must be positive!' % obj)
|
raise ValueError('Value %r must be positive!' % obj)
|
||||||
return obj
|
|
||||||
|
|
||||||
def nonnegative(obj):
|
|
||||||
if obj < 0:
|
|
||||||
raise ValueError('Value %r must be zero or positive!' % obj)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
|
class nonnegative(Validator):
|
||||||
|
def check(self, value):
|
||||||
|
if value >= 0:
|
||||||
|
return value
|
||||||
|
raise ValueError('Value %r must be positive!' % obj)
|
||||||
|
|
||||||
|
|
||||||
|
# more complicated validator may not be able to use validator base class
|
||||||
class mapping(object):
|
class mapping(object):
|
||||||
def __init__(self, *args, **kwds):
|
def __init__(self, *args, **kwds):
|
||||||
self.mapping = {}
|
self.mapping = {}
|
||||||
@ -67,5 +114,9 @@ class mapping(object):
|
|||||||
def __call__(self, obj):
|
def __call__(self, obj):
|
||||||
if obj in self.mapping:
|
if obj in self.mapping:
|
||||||
return obj
|
return obj
|
||||||
raise ValueError("%r should be one of %r" % (obj, list(self.mapping.keys())))
|
raise ValueError("%r should be one of %r" %
|
||||||
|
(obj, list(self.mapping.keys())))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
params = ['%s=%r' % (mname, mval) for mname, mval in self.mapping]
|
||||||
|
return ('%s(%s)' % (self.__class__.__name__, ', '.join(params)))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user