reworking messages

1) start 'bin/secop-server test'
2) connect to localhost port 10767
3) enter help<enter>
4) enjoy

Change-Id: I488d5f9cdca8c91c583691ab23f541a4a8759f4e
This commit is contained in:
Enrico Faulhaber
2016-09-29 18:27:33 +02:00
parent dc2d0a10aa
commit b6af55c358
49 changed files with 3682 additions and 1034 deletions

0
secop/__init__.py Normal file
View File

192
secop/client/__init__.py Normal file
View File

@ -0,0 +1,192 @@
# -*- 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.
import code
class NameSpace(dict):
def __init__(self):
dict.__init__(self)
self.__const = set()
def setconst(self, name, value):
dict.__setitem__(self, name, value)
self.__const.add(name)
def __setitem__(self, name, value):
if name in self.__const:
raise RuntimeError('%s cannot be assigned' % name)
dict.__setitem__(self, name, value)
def __delitem__(self, name):
if name in self.__const:
raise RuntimeError('%s cannot be deleted' % name)
dict.__delitem__(self, name)
import ConfigParser
def getClientOpts(cfgfile):
parser = ConfigParser.SafeConfigParser()
if not parser.read([cfgfile + '.cfg']):
print "Error reading cfg file %r" % cfgfile
return {}
if not parser.has_section('client'):
print "No Server section found!"
return dict(item for item in parser.items('client'))
from os import path
class ClientConsole(object):
def __init__(self, cfgname, basepath):
self.namespace = NameSpace()
self.namespace.setconst('help', self.helpCmd)
cfgfile = path.join(basepath, 'etc', cfgname)
cfg = getClientOpts(cfgfile)
self.client = Client(cfg)
self.client.populateNamespace(self.namespace)
def run(self):
console = code.InteractiveConsole(self.namespace)
console.interact("Welcome to the SECoP console")
def close(self):
pass
def helpCmd(self, arg=Ellipsis):
if arg is Ellipsis:
print "No help available yet"
else:
help(arg)
import loggers
import socket
import threading
from collections import deque
from secop.protocol.transport import FRAMERS, ENCODERS
from secop.protocol.messages import *
class TCPConnection(object):
def __init__(self, connect, port, encoding, framing, **kwds):
self.log = loggers.log.getChild('connection', False)
self.encoder = ENCODERS[encoding]()
self.framer = FRAMERS[framing]()
self.connection = socket.create_connection((connect, port), 3)
self.queue = deque()
self._rcvdata = ''
self.callbacks = set()
self._thread = threading.Thread(target=self.thread)
self._thread.daemonize = True
self._thread.start()
def send(self, msg):
self.log.debug("Sending msg %r" % msg)
frame = self.encoder.encode(msg)
data = self.framer.encode(frame)
self.log.debug("raw data: %r" % data)
self.connection.sendall(data)
def thread(self):
while True:
try:
self.thread_step()
except Exception as e:
self.log.exception("Exception in RCV thread: %r" % e)
def thread_step(self):
while True:
data = self.connection.recv(1024)
self.log.debug("RCV: got raw data %r" % data)
if data:
frames = self.framer.decode(data)
self.log.debug("RCV: frames %r" % frames)
for frame in frames:
msgs = self.encoder.decode(frame)
self.log.debug("RCV: msgs %r" % msgs)
for msg in msgs:
self.handle(msg)
def handle(self, msg):
if isinstance(msg, AsyncDataUnit):
self.log.info("got Async: %r" % msg)
for cb in self.callbacks:
try:
cb(msg)
except Exception as e:
self.log.debug(
"handle_async: got exception %r" %
e, exception=true)
else:
self.queue.append(msg)
def read(self):
while not len(self.queue):
pass
return self.queue.popleft()
def register_callback(self, callback):
"""registers callback for async data"""
self.callbacks.add(callback)
def unregister_callback(self, callback):
"""unregisters callback for async data"""
self.callbacks.discard(callback)
import loggers
class Client(object):
def __init__(self, opts):
self.log = loggers.log.getChild('client', True)
self._cache = dict()
self.connection = TCPConnection(**opts)
self.connection.register_callback(self.handle_async)
def handle_async(self, msg):
self.log.info("Got async update %r" % msg)
device = msg.device
param = msg.param
value = msg.value
self._cache.getdefault(device, {})[param] = value
# XXX: further notification-callbacks needed ???
def populateNamespace(self, namespace):
self.connection.send(ListDevicesRequest())
# reply = self.connection.read()
# self.log.info("found devices %r" % reply)
# create proxies, populate cache....
namespace.setconst('connection', self.connection)

View File

312
secop/devices/core.py Normal file
View File

@ -0,0 +1,312 @@
# -*- 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 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 time
import types
import inspect
import threading
from secop.errors import ConfigError, ProgrammingError
from secop.protocol import status
from secop.validators import enum, vector, floatrange
EVENT_ONLY_ON_CHANGED_VALUES = False
# storage for PARAMeter settings:
# if readonly is False, the value can be changed (by code, or remote)
# if no default is given, the parameter MUST be specified in the configfile
# during startup, value is initialized with the default value or
# from the config file if specified there
class PARAM(object):
def __init__(self, description, validator=float, default=Ellipsis,
unit=None, readonly=True, export=True):
if isinstance(description, PARAM):
# make a copy of a PARAM object
self.__dict__.update(description.__dict__)
return
self.description = description
self.validator = validator
self.default = default
self.unit = unit
self.readonly = readonly
self.export = export
# internal caching: value and timestamp of last change...
self.value = default
self.timestamp = 0
def __repr__(self):
return '%s(%s)' % (self.__class__.__name__, ', '.join(
['%s=%r' % (k, v) for k, v in sorted(self.__dict__.items())]))
# storage for CMDs settings (description + call signature...)
class CMD(object):
def __init__(self, description, arguments, result):
# descriptive text for humans
self.description = description
# list of validators for arguments
self.arguments = arguments
# validator for result
self.resulttype = result
def __repr__(self):
return '%s(%s)' % (self.__class__.__name__, ', '.join(
['%s=%r' % (k, v) for k, v in sorted(self.__dict__.items())]))
# Meta class
# warning: MAGIC!
class DeviceMeta(type):
def __new__(mcs, name, bases, attrs):
newtype = type.__new__(mcs, name, bases, attrs)
if '__constructed__' in attrs:
return newtype
# merge PARAM and CMDS from all sub-classes
for entry in ['PARAMS', 'CMDS']:
newentry = {}
for base in reversed(bases):
if hasattr(base, entry):
newentry.update(getattr(base, entry))
newentry.update(attrs.get(entry, {}))
setattr(newtype, entry, newentry)
# check validity of PARAM entries
for pname, pobj in newtype.PARAMS.items():
# XXX: allow dicts for overriding certain aspects only.
if not isinstance(pobj, PARAM):
raise ProgrammingError('%r: device PARAM %r should be a '
'PARAM object!' % (name, pname))
# XXX: create getters for the units of params ??
# wrap of reading/writing funcs
rfunc = attrs.get('read_' + pname, None)
def wrapped_rfunc(self, maxage=0, pname=pname, rfunc=rfunc):
if rfunc:
value = rfunc(self, maxage)
setattr(self, pname, value)
return value
else:
# return cached value
return self.PARAMS[pname].value
if rfunc:
wrapped_rfunc.__doc__ = rfunc.__doc__
setattr(newtype, 'read_' + pname, wrapped_rfunc)
if not pobj.readonly:
wfunc = attrs.get('write_' + pname, None)
def wrapped_wfunc(self, value, pname=pname, wfunc=wfunc):
self.log.debug("wfunc: set %s to %r" % (pname, value))
pobj = self.PARAMS[pname]
value = pobj.validator(value) if pobj.validator else value
if wfunc:
value = wfunc(self, value) or value
# XXX: use setattr or direct manipulation
# of self.PARAMS[pname]?
setattr(self, pname, value)
return value
if wfunc:
wrapped_wfunc.__doc__ = wfunc.__doc__
setattr(newtype, 'write_' + pname, wrapped_wfunc)
def getter(self, pname=pname):
return self.PARAMS[pname].value
def setter(self, value, pname=pname):
pobj = self.PARAMS[pname]
value = pobj.validator(value) if pobj.validator else value
pobj.timestamp = time.time()
if not EVENT_ONLY_ON_CHANGED_VALUES or (value != pobj.value):
pobj.value = value
# also send notification
self.log.debug('%s is now %r' % (pname, value))
self.DISPATCHER.announce_update(self, pname, pobj)
setattr(newtype, pname, property(getter, setter))
# also collect/update information about CMD's
setattr(newtype, 'CMDS', getattr(newtype, 'CMDS', {}))
for name in attrs:
if name.startswith('do'):
if name[2:] in newtype.CMDS:
continue
value = getattr(newtype, name)
if isinstance(value, types.MethodType):
argspec = inspect.getargspec(value)
if argspec[0] and argspec[0][0] == 'self':
del argspec[0][0]
newtype.CMDS[name[2:]] = CMD(getattr(value, '__doc__'),
argspec.args, None) # XXX: find resulttype!
attrs['__constructed__'] = True
return newtype
# Basic device class
#
# within devices, parameters should only be addressed as self.<pname>
# i.e. self.value, self.target etc...
# these are accesses to the cached version.
# they can also be written to
# (which auto-calls self.write_<pname> and generate an async update)
# if you want to 'update from the hardware', call self.read_<pname>
# the return value of this method will be used as the new cached value and
# be returned.
class Device(object):
"""Basic Device, doesn't do much"""
__metaclass__ = DeviceMeta
# PARAMS and CMDS are auto-merged upon subclassing
PARAMS = {
'baseclass': PARAM('protocol defined interface class',
default="Device", validator=str),
}
CMDS = {}
DISPATCHER = None
def __init__(self, logger, cfgdict, devname, dispatcher):
# remember the dispatcher object (for the async callbacks)
self.DISPATCHER = dispatcher
self.log = logger
self.name = devname
# make local copies of PARAMS
params = {}
for k, v in self.PARAMS.items():
params[k] = PARAM(v)
mycls = self.__class__
myclassname = '%s.%s' % (mycls.__module__, mycls.__name__)
params['class'] = PARAM('implementation specific class name',
default=myclassname, validator=str)
self.PARAMS = params
# check config for problems
# only accept config items specified in PARAMS
for k, v in cfgdict.items():
if k not in self.PARAMS:
raise ConfigError('Device %s:config Parameter %r '
'not unterstood!' % (self.name, k))
# complain if a PARAM entry has no default value and
# is not specified in cfgdict
for k, v in self.PARAMS.items():
if k not in cfgdict:
if v.default is Ellipsis and k != 'value':
# Ellipsis is the one single value you can not specify....
raise ConfigError('Device %s: Parameter %r has no default '
'value and was not given in config!'
% (self.name, k))
# assume default value was given
cfgdict[k] = v.default
# replace CLASS level PARAM objects with INSTANCE level ones
self.PARAMS[k] = PARAM(self.PARAMS[k])
# now 'apply' config:
# pass values through the validators and store as attributes
for k, v in cfgdict.items():
# apply validator, complain if type does not fit
validator = self.PARAMS[k].validator
if validator is not None:
# only check if validator given
try:
v = validator(v)
except ValueError as e:
raise ConfigError('Device %s: config parameter %r:\n%r'
% (self.name, k, e))
setattr(self, k, v)
self._requestLock = threading.RLock()
def init(self):
# may be overriden in derived classes to init stuff
self.log.debug('init()')
def _pollThread(self):
# may be overriden in derived classes to init stuff
self.log.debug('init()')
class Readable(Device):
"""Basic readable device
providing the readonly parameter 'value' and 'status'
"""
PARAMS = {
'baseclass': PARAM('protocol defined interface class',
default="Readable", validator=str),
'value': PARAM('current value of the device', readonly=True, default=0.),
'pollinterval': PARAM('sleeptime between polls', readonly=False, default=5, validator=floatrange(1,120),),
'status': PARAM('current status of the device', default=status.OK,
validator=enum(**{'idle': status.OK,
'BUSY': status.BUSY,
'WARN': status.WARN,
'UNSTABLE': status.UNSTABLE,
'ERROR': status.ERROR,
'UNKNOWN': status.UNKNOWN}),
readonly=True),
'status2': PARAM('current status of the device', default=(status.OK, ''),
validator=vector(enum(**{'idle': status.OK,
'BUSY': status.BUSY,
'WARN': status.WARN,
'UNSTABLE': status.UNSTABLE,
'ERROR': status.ERROR,
'UNKNOWN': status.UNKNOWN}), str),
readonly=True),
}
def init(self):
Device.init(self)
self._pollthread = threading.Thread(target=self._pollThread)
self._pollthread.daemon = True
self._pollthread.start()
def _pollThread(self):
while True:
time.sleep(self.pollinterval)
for pname in self.PARAMS:
if pname != 'pollinterval':
rfunc = getattr(self, 'read_%s' % pname, None)
if rfunc:
rfunc()
class Driveable(Readable):
"""Basic Driveable device
providing a settable 'target' parameter to those of a Readable
"""
PARAMS = {
'baseclass': PARAM('protocol defined interface class',
default="Driveable", validator=str),
'target': PARAM('target value of the device', default=0.,
readonly=False),
}
def doStop(self):
time.sleep(1) # for testing !

327
secop/devices/cryo.py Normal file
View File

@ -0,0 +1,327 @@
#!/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>
# *****************************************************************************
"""playing implementation of a (simple) simulated cryostat"""
from math import atan
import time
import random
import threading
from secop.devices.core import Driveable, CONFIG, PARAM
from secop.protocol import status
from secop.validators import floatrange, positive, enum
from secop.lib import clamp
class Cryostat(Driveable):
"""simulated cryostat with:
- heat capacity of the sample
- cooling power
- thermal transfer between regulation and samplen
"""
PARAMS = dict(
jitter=CONFIG("amount of random noise on readout values",
validator=floatrange(0, 1),
export=False,
),
T_start=CONFIG("starting temperature for simulation",
validator=positive, export=False,
),
looptime=CONFIG("timestep for simulation",
validator=positive, default=1, unit="s",
export=False,
),
ramp=PARAM("ramping speed in K/min",
validator=floatrange(0, 1e3), default=1,
),
setpoint=PARAM("ramping speed in K/min",
validator=float, default=1, readonly=True,
),
maxpower=PARAM("Maximum heater power in W",
validator=float, default=0, readonly=True, unit="W",
),
heater=PARAM("current heater setting in %",
validator=float, default=0, readonly=True, unit="%",
),
heaterpower=PARAM("current heater power in W",
validator=float, default=0, readonly=True, unit="W",
),
target=PARAM("target temperature in K",
validator=float, default=0, unit="K",
),
p=PARAM("regulation coefficient 'p' in %/K",
validator=positive, default=40, unit="%/K",
),
i=PARAM("regulation coefficient 'i'",
validator=floatrange(0, 100), default=10,
),
d=PARAM("regulation coefficient 'd'",
validator=floatrange(0, 100), default=2,
),
mode=PARAM("mode of regulation",
validator=enum('ramp', 'pid', 'openloop'), default='pid',
),
tolerance=PARAM("temperature range for stability checking",
validator=floatrange(0, 100), default=0.1, unit='K',
),
window=PARAM("time window for stability checking",
validator=floatrange(1, 900), default=30, unit='s',
),
timeout=PARAM("max waiting time for stabilisation check",
validator=floatrange(1, 36000), default=900, unit='s',
),
)
def init(self):
self._stopflag = False
self._thread = threading.Thread(target=self.thread)
self._thread.daemon = True
self._thread.start()
def read_status(self):
# instead of asking a 'Hardware' take the value from the simulation
return self.status
def read_value(self, maxage=0):
# return regulation value (averaged regulation temp)
return self.regulationtemp + \
self.config_jitter * (0.5 - random.random())
def read_target(self, maxage=0):
return self.target
def write_target(self, value):
self.target = value
# next request will see this status, until the loop updates it
self.status = (status.BUSY, 'new target set')
def read_maxpower(self, maxage=0):
return self.maxpower
def write_maxpower(self, newpower):
# rescale heater setting in % to keep the power
heat = max(0, min(100, self.heater * self.maxpower / float(newpower)))
self.heater = heat
self.maxpower = newpower
def doStop(self):
# stop the ramp by setting current setpoint as target
# XXX: discussion: take setpoint or current value ???
self.write_target(self.setpoint)
#
# calculation helpers
#
def __coolerPower(self, temp):
"""returns cooling power in W at given temperature"""
# quadratic up to 42K, is linear from 40W@42K to 100W@600K
# return clamp((temp-2)**2 / 32., 0., 40.) + temp * 0.1
return clamp(15 * atan(temp * 0.01) ** 3, 0., 40.) + temp * 0.1 - 0.2
def __coolerCP(self, temp):
"""heat capacity of cooler at given temp"""
return 75 * atan(temp / 50)**2 + 1
def __heatLink(self, coolertemp, sampletemp):
"""heatflow from sample to cooler. may be negative..."""
flow = (sampletemp - coolertemp) * \
((coolertemp + sampletemp) ** 2) / 400.
cp = clamp(self.__coolerCP(coolertemp) * self.__sampleCP(sampletemp),
1, 10)
return clamp(flow, -cp, cp)
def __sampleCP(self, temp):
return 3 * atan(temp / 30) + \
12 * temp / ((temp - 12.)**2 + 10) + 0.5
def __sampleLeak(self, temp):
return 0.02 / temp
def thread(self):
self.sampletemp = self.config_T_start
self.regulationtemp = self.config_T_start
self.status = status.OK
while not self._stopflag:
try:
self.__sim()
except Exception as e:
self.log.exception(e)
self.status = status.ERROR, str(e)
def __sim(self):
# complex thread handling:
# a) simulation of cryo (heat flow, thermal masses,....)
# b) optional PID temperature controller with windup control
# c) generating status+updated value+ramp
# this thread is not supposed to exit!
# local state keeping:
regulation = self.regulationtemp
sample = self.sampletemp
# keep history values for stability check
window = []
timestamp = time.time()
heater = 0
lastflow = 0
last_heaters = (0, 0)
delta = 0
I = D = 0
lastD = 0
damper = 1
lastmode = self.mode
while not self._stopflag:
t = time.time()
h = t - timestamp
if h < self.looptime / damper:
time.sleep(clamp(self.looptime / damper - h, 0.1, 60))
continue
# a)
sample = self.sampletemp
regulation = self.regulationtemp
heater = self.heater
heatflow = self.__heatLink(regulation, sample)
self.log.debug('sample = %.5f, regulation = %.5f, heatflow = %.5g'
% (sample, regulation, heatflow))
newsample = max(0,
sample + (self.__sampleLeak(sample) - heatflow) /
self.__sampleCP(sample) * h)
# avoid instabilities due to too small CP
newsample = clamp(newsample, sample, regulation)
regdelta = (heater * 0.01 * self.maxpower + heatflow -
self.__coolerPower(regulation))
newregulation = max(0, regulation +
regdelta / self.__coolerCP(regulation) * h)
# b) see
# http://brettbeauregard.com/blog/2011/04/
# improving-the-beginners-pid-introduction/
if self.mode != 'openloop':
# fix artefacts due to too big timesteps
# actually i would prefer reducing looptime, but i have no
# good idea on when to increase it back again
if heatflow * lastflow != -100:
if (newregulation - newsample) * (regulation - sample) < 0:
# newregulation = (newregulation + regulation) / 2
# newsample = (newsample + sample) / 2
damper += 1
lastflow = heatflow
error = self.setpoint - newregulation
# use a simple filter to smooth delta a little
delta = (delta + regulation - newregulation) / 2.
kp = self.p / 10. # LakeShore P = 10*k_p
ki = kp * abs(self.i) / 500. # LakeShore I = 500/T_i
kd = kp * abs(self.d) / 2. # LakeShore D = 2*T_d
P = kp * error
I += ki * error * h
D = kd * delta / h
# avoid reset windup
I = clamp(I, 0., 100.) # I is in %
# avoid jumping heaterpower if switching back to pid mode
if lastmode != self.mode:
# adjust some values upon switching back on
I = self.heater - P - D
v = P + I + D
# in damping mode, use a weighted sum of old + new heaterpower
if damper > 1:
v = ((damper ** 2 - 1) * self.heater + v) / damper ** 2
# damp oscillations due to D switching signs
if D * lastD < -0.2:
v = (v + heater) / 2.
# clamp new heater power to 0..100%
heater = clamp(v, 0., 100.)
lastD = D
self.log.debug('PID: P = %.2f, I = %.2f, D = %.2f, '
'heater = %.2f' % (P, I, D, heater))
# check for turn-around points to detect oscillations ->
# increase damper
x, y = last_heaters
if (x + 0.1 < y and y > heater + 0.1) or \
(x > y + 0.1 and y + 0.1 < heater):
damper += 1
last_heaters = (y, heater)
else:
# self.heaterpower is set manually, not by pid
heater = self.heater
last_heaters = (0, 0)
heater = round(heater, 3)
sample = newsample
regulation = newregulation
lastmode = self.mode
# c)
if self.setpoint != self.target:
if self.ramp == 0:
maxdelta = 10000
else:
maxdelta = self.ramp / 60. * h
try:
self.setpoint = round(self.setpoint +
clamp(self.target - self.setpoint,
-maxdelta, maxdelta), 3)
self.log.debug('setpoint changes to %r (target %r)' %
(self.setpoint, self.target))
except (TypeError, ValueError):
# self.target might be None
pass
# temperature is stable when all recorded values in the window
# differ from setpoint by less than tolerance
currenttime = time.time()
window.append((currenttime, sample))
while window[0][0] < currenttime - self.window:
# remove old/stale entries
window.pop(0)
# obtain min/max
deviation = 0
for _, T in window:
if abs(T - self.target) > deviation:
deviation = abs(T - self.target)
if (len(window) < 3) or deviation > self.tolerance:
self.status = status.BUSY, 'unstable'
elif self.setpoint == self.target:
self.status = status.OK, 'at target'
damper -= (damper - 1) / 10. # max value for damper is 11
else:
self.status = status.BUSY, 'ramping setpoint'
damper -= (damper - 1) / 20.
self.regulationtemp = round(regulation, 3)
self.sampletemp = round(sample, 3)
self.heaterpower = round(heater * self.maxpower * 0.01, 3)
self.heater = heater
timestamp = t
def shutdown(self):
# should be called from server when the server is stopped
self._stopflag = True
if self._thread and self._thread.isAlive():
self._thread.join()

265
secop/devices/demo.py Normal file
View File

@ -0,0 +1,265 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# *****************************************************************************
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
# *****************************************************************************
"""testing devices"""
import time
import random
import threading
from secop.devices.core import Readable, Driveable, PARAM
from secop.validators import *
from secop.protocol import status
class Switch(Driveable):
"""switch it on or off....
"""
PARAMS = {
'value': PARAM('current state (on or off)',
validator=enum(on=1, off=0), default=0),
'target': PARAM('wanted state (on or off)',
validator=enum(on=1, off=0), default=0,
readonly=False),
'switch_on_time': PARAM('how long to wait after switching the switch on', validator=floatrange(0, 60), unit='s', default=10, export=False),
'switch_off_time': PARAM('how long to wait after switching the switch off', validator=floatrange(0, 60), unit='s', default=10, export=False),
}
def init(self):
self._started = 0
def read_value(self, maxage=0):
# could ask HW
# we just return the value of the target here.
self._update()
return self.value
def read_target(self, maxage=0):
# could ask HW
return self.target
def write_target(self, value):
# could tell HW
pass
# note: setting self.target to the new value is done after this....
# note: we may also return the read-back value from the hw here
def read_status(self, maxage=0):
self.log.info("read status")
self._update()
if self.target == self.value:
return status.OK
return status.BUSY
def _update(self):
started = self.PARAMS['target'].timestamp
if self.target > self.value:
if time.time() > started + self.switch_on_time:
self.log.debug('is switched ON')
self.value = self.target
elif self.target < self.value:
if time.time() > started + self.switch_off_time:
self.log.debug('is switched OFF')
self.value = self.target
class MagneticField(Driveable):
"""a liquid magnet
"""
PARAMS = {
'value': PARAM('current field in T', unit='T',
validator=floatrange(-15, 15), default=0),
'ramp': PARAM('moving speed in T/min', unit='T/min',
validator=floatrange(0, 1), default=0.1, readonly=False),
'mode': PARAM('what to do after changing field', default=0,
validator=enum(persistent=1, hold=0), readonly=False),
'heatswitch': PARAM('heat switch device',
validator=str, export=False),
}
def init(self):
self._state = 'idle'
self._heatswitch = self.DISPATCHER.get_device(self.heatswitch)
_thread = threading.Thread(target=self._thread)
_thread.daemon = True
_thread.start()
def read_value(self, maxage=0):
return self.value
def write_target(self, value):
# could tell HW
return round(value, 2)
# note: setting self.target to the new value is done after this....
# note: we may also return the read-back value from the hw here
def read_status(self, maxage=0):
return status.OK if self._state == 'idle' else status.BUSY
def _thread(self):
loopdelay = 1
while True:
ts = time.time()
if self._state == 'idle':
if self.target != self.value:
self.log.debug('got new target -> switching heater on')
self._state = 'switch_on'
self._heatswitch.write_target('on')
if self._state == 'switch_on':
# wait until switch is on
if self._heatswitch.read_value() == 'on':
self.log.debug(
'heatswitch is on -> ramp to %.3f' %
self.target)
self._state = 'ramp'
if self._state == 'ramp':
if self.target == self.value:
self.log.debug('at field! mode is %r' % self.mode)
if self.mode:
self.log.debug('at field -> switching heater off')
self._state = 'switch_off'
self._heatswitch.write_target('off')
else:
self.log.debug('at field -> hold')
self._state = 'idle'
self.status = self.read_status() # push async
else:
step = self.ramp * loopdelay / 60.
step = max(min(self.target - self.value, step), -step)
self.value += step
if self._state == 'switch_off':
# wait until switch is off
if self._heatswitch.read_value() == 'off':
self.log.debug('heatswitch is off at %.3f' % self.value)
self._state = 'idle'
self.read_status() # update async
time.sleep(max(0.01, ts + loopdelay - time.time()))
self.log.error(self, 'main thread exited unexpectedly!')
class CoilTemp(Readable):
"""a coil temperature
"""
PARAMS = {
'value': PARAM('Coil temperatur in K', unit='K',
validator=float, default=0),
'sensor': PARAM("Sensor number or calibration id",
validator=str, readonly=True),
}
def read_value(self, maxage=0):
return round(2.3 + random.random(), 3)
class SampleTemp(Driveable):
"""a sample temperature
"""
PARAMS = {
'value': PARAM('Sample temperatur in K', unit='K',
validator=float, default=10),
'sensor': PARAM("Sensor number or calibration id",
validator=str, readonly=True),
'ramp': PARAM('moving speed in K/min',
validator=floatrange(0, 100), unit='K/min', default=0.1, readonly=False),
}
def init(self):
_thread = threading.Thread(target=self._thread)
_thread.daemon = True
_thread.start()
def write_target(self, value):
# could tell HW
return round(value, 2)
# note: setting self.target to the new value is done after this....
# note: we may also return the read-back value from the hw here
def _thread(self):
loopdelay = 1
while True:
ts = time.time()
if self.value == self.target:
if self.status != status.OK:
self.status = status.OK
else:
self.status = status.BUSY
step = self.ramp * loopdelay / 60.
step = max(min(self.target - self.value, step), -step)
self.value += step
time.sleep(max(0.01, ts + loopdelay - time.time()))
self.log.error(self, 'main thread exited unexpectedly!')
class Label(Readable):
"""
"""
PARAMS = {
'system': PARAM("Name of the magnet system",
validator=str, export=False),
'subdev_mf': PARAM("name of subdevice for magnet status",
validator=str, export=False),
'subdev_ts': PARAM("name of subdevice for sample temp",
validator=str, export=False),
'value': PARAM("Value of out label string",
validator=str)
}
def read_value(self, maxage=0):
strings = [self.system]
dev_ts = self.DISPATCHER.get_device(self.subdev_ts)
if dev_ts:
strings.append('at %.3f %s' %
(dev_ts.read_value(), dev_ts.PARAMS['value'].unit))
else:
strings.append('No connection to sample temp!')
dev_mf = self.DISPATCHER.get_device(self.subdev_mf)
if dev_mf:
mf_stat = dev_mf.read_status()
mf_mode = dev_mf.mode
mf_val = dev_mf.value
mf_unit = dev_mf.PARAMS['value'].unit
if mf_stat == status.OK:
state = 'Persistent' if mf_mode else 'Non-persistent'
else:
state = 'ramping'
strings.append('%s at %.1f %s' % (state, mf_val, mf_unit))
else:
strings.append('No connection to magnetic field!')
return '; '.join(strings)
class ValidatorTest(Readable):
"""
"""
PARAMS = {
'oneof': PARAM('oneof', validator=oneof(int, 'X', 2.718), readonly=False, default=4.0),
'enum': PARAM('enum', validator=enum('boo', 'faar', z=9), readonly=False, default=1),
'vector': PARAM('vector of int, float and str', validator=vector(int, float, str), readonly=False, default=(1, 2.3, 'a')),
'array': PARAM('array: 2..3 time oneof(0,1)', validator=array(oneof(2, 3), oneof(0, 1)), readonly=False, default=[1, 0, 1]),
'nonnegative': PARAM('nonnegative', validator=nonnegative(), readonly=False, default=0),
'positive': PARAM('positive', validator=positive(), readonly=False, default=1),
'intrange': PARAM('intrange', validator=intrange(2, 9), readonly=False, default=4),
'floatrange': PARAM('floatrange', validator=floatrange(-1, 1), readonly=False, default=0,),
}

51
secop/devices/epics.py Normal file
View File

@ -0,0 +1,51 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# *****************************************************************************
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
# *****************************************************************************
"""testing devices"""
import random
from secop.devices.core import Readable, Driveable, PARAM
try:
from epics import PV
except ImportError:
PV = None
class EPICS_PV(Driveable):
"""pyepics test device."""
PARAMS = {
'sensor': PARAM("Sensor number or calibration id",
validator=str, readonly=True),
'max_rpm': PARAM("Maximum allowed rpm",
validator=str, readonly=True),
}
def read_value(self, maxage=0):
p1 = PV('testpv.VAL')
return p1.value
def write_target(self, target):
p1 = PV('test.VAL')
p1.value = target

74
secop/devices/test.py Normal file
View File

@ -0,0 +1,74 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# *****************************************************************************
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
# *****************************************************************************
"""testing devices"""
import random
from secop.devices.core import Readable, Driveable, PARAM
from secop.validators import floatrange
class LN2(Readable):
"""Just a readable.
class name indicates it to be a sensor for LN2,
but the implementation may do anything
"""
def read_value(self, maxage=0):
return round(100 * random.random(), 1)
class Heater(Driveable):
"""Just a driveable.
class name indicates it to be some heating element,
but the implementation may do anything
"""
PARAMS = {
'maxheaterpower': PARAM('maximum allowed heater power',
validator=floatrange(0, 100), unit='W'),
}
def read_value(self, maxage=0):
return round(100 * random.random(), 1)
def write_target(self, target):
pass
class Temp(Driveable):
"""Just a driveable.
class name indicates it to be some temperature controller,
but the implementation may do anything
"""
PARAMS = {
'sensor': PARAM("Sensor number or calibration id",
validator=str, readonly=True),
}
def read_value(self, maxage=0):
return round(100 * random.random(), 1)
def write_target(self, target):
pass

35
secop/errors.py Normal file
View File

@ -0,0 +1,35 @@
#!/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>
#
# *****************************************************************************
"""error class for our little framework"""
class SECoPServerError(Exception):
pass
class ConfigError(SECoPServerError):
pass
class ProgrammingError(SECoPServerError):
pass

61
secop/lib/__init__.py Normal file
View File

@ -0,0 +1,61 @@
# -*- 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"""
class attrdict(dict):
"""a normal dict, providing access also via attributes"""
def __getattr__(self, key):
return self[key]
def __setattr__(self, key, value):
self[key] = value
def clamp(_min, value, _max):
"""return the median of 3 values,
i.e. value if min <= value <= max, else min or max depending on which side
value lies outside the [min..max] interval
"""
# return median, i.e. clamp the the value between min and max
return sorted([_min, value, _max])[1]
def get_class(spec):
"""loads a class given by string in dotted notaion (as python would do)"""
modname, classname = spec.rsplit('.', 1)
import importlib
module = importlib.import_module('secop.' + modname)
# module = __import__(spec)
return getattr(module, classname)
if __name__ == '__main__':
print "minimal testing: lib"
d = attrdict(a=1, b=2)
_ = d.a + d['b']
d.c = 9
d['d'] = 'c'
assert d[d.d] == 9

309
secop/lib/parsing.py Normal file
View File

@ -0,0 +1,309 @@
# -*- 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 parsing helpers"""
import time
import datetime
def format_time(timestamp):
return datetime.datetime.fromtimestamp(
timestamp).strftime("%Y-%m-%d %H:%M:%S.%f")
def parse_time(string):
d = datetime.datetime.strptime(string, "%Y-%m-%d %H:%M:%S.%f")
return time.mktime(d.timetuple()) + 0.000001 * d.microsecond
def format_args(args):
if isinstance(args, list):
return ','.join(format_args(arg) for arg in args).join('[]')
if isinstance(args, tuple):
return ','.join(format_args(arg) for arg in args).join('()')
if isinstance(args, (str, unicode)):
# XXX: check for 'easy' strings only and omit the ''
return repr(args)
return repr(args) # for floats/ints/...
class ArgsParser(object):
"""returns a pythonic object from the input expression
grammar:
expr = number | string | array_expr | record_expr
number = int | float
string = '"' (chars - '"')* '"' | "'" (chars - "'")* "'"
array_expr = '[' (expr ',')* expr ']'
record_expr = '(' (name '=' expr ',')* ')'
int = '-' pos_int | pos_int
pos_int = [0..9]+
float = int '.' pos_int ( [eE] int )?
name = [A-Za-z_] [A-Za-z0-9_]*
"""
DIGITS_CHARS = [c for c in '0123456789']
NAME_CHARS = [
c for c in '_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz']
NAME_CHARS2 = NAME_CHARS + DIGITS_CHARS
def __init__(self, string=''):
self.string = string
self.idx = 0
self.length = len(string)
def setstring(self, string):
print repr(string)
self.string = string
self.idx = 0
self.length = len(string)
self.skip()
def peek(self):
if self.idx >= self.length:
return None
return self.string[self.idx]
def get(self):
res = self.peek()
self.idx += 1
print "get->", res
return res
def skip(self):
"""skips whitespace"""
while self.peek() in ('\t', ' '):
self.get()
def match(self, what):
if self.peek() != what:
return False
self.get()
self.skip()
return True
def parse(self, arg=None):
"""parses given or constructed_with string"""
self.setstring(arg or self.string)
res = []
while self.idx < self.length:
res.append(self.parse_exp())
self.match(',')
if len(res) > 1:
return tuple(*res)
return res[0]
def parse_exp(self):
"""expr = array_expr | record_expr | string | number"""
idx = self.idx
res = self.parse_array()
if res:
print "is Array"
return res
self.idx = idx
res = self.parse_record()
if res:
print "is record"
return res
self.idx = idx
res = self.parse_string()
if res:
print "is string"
return res
self.idx = idx
return self.parse_number()
def parse_number(self):
"""number = float | int """
idx = self.idx
number = self.parse_float()
if number is not None:
return number
self.idx = idx # rewind
return self.parse_int()
def parse_string(self):
"""string = '"' (chars - '"')* '"' | "'" (chars - "'")* "'" """
delim = self.peek()
if delim in ('"', "'"):
lastchar = self.get()
string = []
while self.peek() != delim or lastchar == '\\':
lastchar = self.peek()
string.append(self.get())
self.get()
self.skip()
return ''.join(string)
return self.parse_name()
def parse_array(self):
"""array_expr = '[' (expr ',')* expr ']' """
if self.get() != '[':
return None
self.skip()
res = []
while self.peek() != ']':
el = self.parse_exp()
if el is None:
return el
res.append(el)
if self.match(']'):
return res
if self.get() != ',':
return None
self.skip()
self.get()
self.skip()
return res
def parse_record(self):
"""record_expr = '(' (name '=' expr ',')* ')' """
if self.get != '(':
return None
self.skip()
res = {}
while self.peek() != ')':
name = self.parse_name()
if self.get() != '=':
return None
self.skip()
value = self.parse_exp()
res[name] = value
if self.peek() == ')':
self.get()
self.skip()
return res
if self.get() != ',':
return None
self.skip()
self.get()
self.skip()
return res
def parse_int(self):
"""int = '-' pos_int | pos_int"""
if self.peek() == '-':
self.get()
number = self.parse_pos_int()
if number is None:
return number
return -number
return self.parse_pos_int()
def parse_pos_int(self):
"""pos_int = [0..9]+"""
number = 0
if self.peek() not in self.DIGITS_CHARS:
return None
while (self.peek() in self.DIGITS_CHARS):
number = number * 10 + int(self.get())
self.skip()
return number
def parse_float(self):
"""float = int '.' pos_int ( [eE] int )?"""
number = self.parse_int()
if self.get() != '.':
return None
idx = self.idx
fraction = self.parse_pos_int()
while idx < self.idx:
fraction /= 10.
idx += 1
if number >= 0:
number = number + fraction
else:
number = number - fraction
exponent = 0
if self.peek() in ('e', 'E'):
self.get()
exponent = self.parse_int()
if exponent is None:
return exponent
while exponent > 0:
number *= 10.
exponent -= 1
while exponent < 0:
number /= 10.
exponent += 1
self.skip()
return number
def parse_name(self):
"""name = [A-Za-z_] [A-Za-z0-9_]*"""
name = []
if self.peek() in self.NAME_CHARS:
name.append(self.get())
while self.peek() in self.NAME_CHARS2:
name.append(self.get())
self.skip()
return ''.join(name)
return None
def parse_args(s):
# QnD Hack! try to parse lists/tuples/ints/floats, ignore dicts, specials
# XXX: replace by proper parsing. use ast?
s = s.strip()
if s.startswith('[') and s.endswith(']'):
# evaluate inner
return [parse_args(part) for part in s[1:-1].split(',')]
if s.startswith('(') and s.endswith(')'):
# evaluate inner
return tuple(parse_args(part) for part in s[1:-1].split(','))
if s.startswith('"') and s.endswith('"'):
# evaluate inner
return s[1:-1]
if s.startswith("'") and s.endswith("'"):
# evaluate inner
return s[1:-1]
if '.' in s:
return float(s)
return int(s)
__ALL__ = ['format_time', 'parse_time', 'parse_args']
if __name__ == '__main__':
print "minimal testing: lib/parsing:"
print "time_formatting:",
t = time.time()
s = format_time(t)
assert(abs(t - parse_time(s)) < 1e-6)
print "OK"
print "ArgsParser:"
a = ArgsParser()
print a.parse('[ "\'\\\"A" , "<>\'", \'",C\', [1.23e1, 123.0e-001] , ]')
#import pdb
#pdb.run('print a.parse()', globals(), locals())
print "args_formatting:",
for obj in [1, 2.3, 'X', (1, 2, 3), [1, (3, 4), 'X,y']]:
s = format_args(obj)
p = a.parse(s)
print p,
assert(parse_args(format_args(obj)) == obj)
print "OK"
print "OK"

55
secop/lib/pidfile.py Normal file
View File

@ -0,0 +1,55 @@
# -*- 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 pidfile helpers"""
import os
import atexit
import psutil
def read_pidfile(pidfile):
"""read the given pidfile, return the pid as an int
or None upon errors (file not existing)"""
try:
with open(pidfile, 'r') as f:
return int(f.read())
except (OSError, IOError):
return None
def remove_pidfile(pidfile):
"""remove the given pidfile, typically at end of the process"""
os.remove(pidfile)
def write_pidfile(pidfile, pid):
"""write the given pid to the given pidfile"""
with open(pidfile, 'w') as f:
f.write('%d\n' % pid)
atexit.register(remove_pidfile, pidfile)
def check_pidfile(pidfile):
"""check if the process from a given pidfile is still running"""
pid = read_pidfile(pidfile)
return False if pid is None else psutil.pid_exists(pid)

350
secop/loggers/__init__.py Normal file
View File

@ -0,0 +1,350 @@
# *****************************************************************************
#
# 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:
# Alexander Lenz <alexander.lenz@posteo.de>
# Georg Brandl <georg@python.org>
#
# *****************************************************************************
import os
import sys
import time
import linecache
import traceback
import logging
from os import path
from logging import Logger, Formatter, Handler, DEBUG, INFO, WARNING, ERROR, \
setLoggerClass
from . import colors
LOGFMT = '%(asctime)s : %(levelname)-7s : %(name)-15s: %(message)s'
DATEFMT = '%H:%M:%S'
DATESTAMP_FMT = '%Y-%m-%d'
SECONDS_PER_DAY = 60 * 60 * 24
LOGLEVELS = {'debug': DEBUG, 'info': INFO, 'warning': WARNING, 'error': ERROR}
INVLOGLEVELS = {value: key for key, value in LOGLEVELS.items()}
log = None
def initLogging(rootname='secop', rootlevel='info', logdir='/tmp/log'):
global log
setLoggerClass(SecopLogger)
log = SecopLogger(rootname)
log.setLevel(LOGLEVELS[rootlevel])
# console logging for fg process
log.addHandler(ColoredConsoleHandler())
# logfile for fg and bg process
if logdir.startswith('/var/log'):
log.addHandler(LogfileHandler(logdir, rootname))
else:
log.addHandler(LogfileHandler(logdir, ''))
def getLogger(name, subdir=False):
global log
return log.getChild(name, subdir)
class SecopLogger(Logger):
maxLogNameLength = 0
def __init__(self, *args, **kwargs):
Logger.__init__(self, *args, **kwargs)
SecopLogger._storeLoggerNameLength(self)
def getChild(self, suffix, ownDir=False):
child = Logger.getChild(self, suffix)
child.setLevel(self.getEffectiveLevel())
for handler in self._collectHandlers():
if ownDir and isinstance(handler, LogfileHandler):
handler = handler.getChild(suffix)
child.addHandler(handler)
child.propagate = False
return child
def getLogfileStreams(self):
result = []
for entry in self._collectHandlers():
if isinstance(entry, LogfileHandler):
result.append(entry.stream)
return result
def _collectHandlers(self):
result = []
log = self
while log is not None:
result += log.handlers
log = log.parent
return result
@staticmethod
def _storeLoggerNameLength(logObj):
# store max logger name length for formatting
if len(logObj.name) > SecopLogger.maxLogNameLength:
SecopLogger.maxLogNameLength = len(logObj.name)
class ConsoleFormatter(Formatter):
"""
A lightweight formatter for the interactive console, with optional
colored output.
"""
def __init__(self, fmt=None, datefmt=None, colorize=None):
Formatter.__init__(self, fmt, datefmt)
if colorize:
self.colorize = colorize
else:
self.colorize = lambda c, s: s
def formatException(self, exc_info):
return traceback.format_exception_only(*exc_info[0:2])[-1]
def formatTime(self, record, datefmt=None):
return time.strftime(datefmt or DATEFMT,
self.converter(record.created))
def format(self, record):
record.message = record.getMessage()
levelno = record.levelno
datefmt = self.colorize('lightgray', '[%(asctime)s] ')
namefmt = '%(name)-' + str(SecopLogger.maxLogNameLength) + 's: '
if levelno <= DEBUG:
fmtstr = self.colorize('darkgray', '%s%%(message)s' % namefmt)
elif levelno <= INFO:
fmtstr = '%s%%(message)s' % namefmt
elif levelno <= WARNING:
fmtstr = self.colorize('fuchsia', '%s%%(levelname)s: %%(message)s'
% namefmt)
else:
# Add exception type to error (if caused by exception)
msgPrefix = ''
if record.exc_info:
msgPrefix = '%s: ' % record.exc_info[0].__name__
fmtstr = self.colorize('red', '%s%%(levelname)s: %s%%(message)s'
% (namefmt, msgPrefix))
fmtstr = datefmt + fmtstr
if not getattr(record, 'nonl', False):
fmtstr += '\n'
record.asctime = self.formatTime(record, self.datefmt)
s = fmtstr % record.__dict__
# never output more exception info -- the exception message is already
# part of the log message because of our special logger behavior
# if record.exc_info:
# # *not* caching exception text on the record, since it's
# # only a short version
# s += self.formatException(record.exc_info)
return s
def format_extended_frame(frame):
ret = []
for key, value in frame.f_locals.items():
try:
valstr = repr(value)[:256]
except Exception:
valstr = '<cannot be displayed>'
ret.append(' %-20s = %s\n' % (key, valstr))
ret.append('\n')
return ret
def format_extended_traceback(etype, value, tb):
ret = ['Traceback (most recent call last):\n']
while tb is not None:
frame = tb.tb_frame
filename = frame.f_code.co_filename
item = ' File "%s", line %d, in %s\n' % (filename, tb.tb_lineno,
frame.f_code.co_name)
linecache.checkcache(filename)
line = linecache.getline(filename, tb.tb_lineno, frame.f_globals)
if line:
item = item + ' %s\n' % line.strip()
ret.append(item)
if filename != '<script>':
ret += format_extended_frame(tb.tb_frame)
tb = tb.tb_next
ret += traceback.format_exception_only(etype, value)
return ''.join(ret).rstrip('\n')
class LogfileFormatter(Formatter):
"""
The standard Formatter does not support milliseconds with an explicit
datestamp format. It also doesn't show the full traceback for exceptions.
"""
extended_traceback = True
def formatException(self, ei):
if self.extended_traceback:
s = format_extended_traceback(*ei)
else:
s = ''.join(traceback.format_exception(ei[0], ei[1], ei[2],
sys.maxsize))
if s.endswith('\n'):
s = s[:-1]
return s
def formatTime(self, record, datefmt=None):
res = time.strftime(DATEFMT, self.converter(record.created))
res += ',%03d' % record.msecs
return res
class StreamHandler(Handler):
"""Reimplemented from logging: remove cruft, remove bare excepts."""
def __init__(self, stream=None):
Handler.__init__(self)
self.stream = stream
def flush(self):
self.acquire()
try:
if self.stream and hasattr(self.stream, 'flush'):
self.stream.flush()
finally:
self.release()
def emit(self, record):
try:
msg = self.format(record)
try:
self.stream.write('%s\n' % msg)
except UnicodeEncodeError:
self.stream.write('%s\n' % msg.encode('utf-8'))
self.flush()
except Exception:
self.handleError(record)
class LogfileHandler(StreamHandler):
"""
Logs to log files with a date stamp appended, and rollover on midnight.
"""
def __init__(self, directory, filenameprefix, dayfmt=DATESTAMP_FMT):
self._directory = path.join(directory, filenameprefix)
if not path.isdir(self._directory):
os.makedirs(self._directory)
self._currentsymlink = path.join(self._directory, 'current')
self._filenameprefix = filenameprefix
self._pathnameprefix = path.join(self._directory, filenameprefix)
self._dayfmt = dayfmt
# today's logfile name
basefn = self._pathnameprefix + '-' + time.strftime(dayfmt) + '.log'
self.baseFilename = path.abspath(basefn)
self.mode = 'a'
StreamHandler.__init__(self)
# determine time of first midnight from now on
t = time.localtime()
self.rollover_at = time.mktime((t[0], t[1], t[2], 0, 0, 0,
t[6], t[7], t[8])) + SECONDS_PER_DAY
self.setFormatter(LogfileFormatter(LOGFMT, DATEFMT))
self.disabled = False
def getChild(self, name):
return LogfileHandler(self._directory, name)
def filter(self, record):
return not self.disabled
def emit(self, record):
try:
t = int(time.time())
if t >= self.rollover_at:
self.doRollover()
if self.stream is None:
self.stream = self._open()
StreamHandler.emit(self, record)
except Exception:
self.handleError(record)
def enable(self, enabled):
if enabled:
self.disabled = False
self.stream.close()
self.stream = self._open()
else:
self.disabled = True
def close(self):
self.acquire()
try:
if self.stream:
self.flush()
if hasattr(self.stream, 'close'):
self.stream.close()
StreamHandler.close(self)
self.stream = None
finally:
self.release()
def doRollover(self):
self.stream.close()
self.baseFilename = self._pathnameprefix + '-' + \
time.strftime(self._dayfmt) + '.log'
self.stream = self._open()
self.rollover_at += SECONDS_PER_DAY
def _open(self):
# update 'current' symlink upon open
try:
os.remove(self._currentsymlink)
except OSError:
# if the symlink does not (yet) exist, OSError is raised.
# should happen at most once per installation....
pass
if hasattr(os, 'symlink'):
os.symlink(path.basename(self.baseFilename), self._currentsymlink)
# finally open the new logfile....
return open(self.baseFilename, self.mode)
class ColoredConsoleHandler(StreamHandler):
"""
A handler class that writes colorized records to standard output.
"""
def __init__(self):
StreamHandler.__init__(self, sys.stdout)
self.setFormatter(ConsoleFormatter(datefmt=DATEFMT,
colorize=colors.colorize))
def emit(self, record):
msg = self.format(record)
try:
self.stream.write(msg)
except UnicodeEncodeError:
self.stream.write(msg.encode('utf-8'))
self.stream.flush()

67
secop/loggers/colors.py Normal file
View File

@ -0,0 +1,67 @@
# -*- 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:
# Alexander Lenz <alexander.lenz@posteo.de>
# Georg Brandl <georg@python.org>
#
# *****************************************************************************
"""Console coloring handlers."""
_codes = {}
_attrs = {
'reset': '39;49;00m',
'bold': '01m',
'faint': '02m',
'standout': '03m',
'underline': '04m',
'blink': '05m',
}
for _name, _value in _attrs.items():
_codes[_name] = '\x1b[' + _value
_colors = [
('black', 'darkgray'),
('darkred', 'red'),
('darkgreen', 'green'),
('brown', 'yellow'),
('darkblue', 'blue'),
('purple', 'fuchsia'),
('turquoise', 'teal'),
('lightgray', 'white'),
]
for _i, (_dark, _light) in enumerate(_colors):
_codes[_dark] = '\x1b[%im' % (_i + 30)
_codes[_light] = '\x1b[%i;01m' % (_i + 30)
def colorize(name, text):
return _codes.get(name, '') + text + _codes.get('reset', '')
def colorcode(name):
return _codes.get(name, '')
def nocolor():
for key in list(_codes):
_codes[key] = ''

33
secop/paths.py Normal file
View File

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# *****************************************************************************
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
#
# *****************************************************************************
"""Pathes. how to find what and where..."""
import sys
from os import path
basepath = path.abspath(path.join(sys.path[0], '..'))
etc_path = path.join(basepath, 'etc')
pid_path = path.join(basepath, 'pid')
log_path = path.join(basepath, 'log')
sys.path[0] = path.join(basepath, 'src')

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# *****************************************************************************
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
#
# *****************************************************************************
"""SECoP protocl specific stuff"""

76
secop/protocol/device.py Normal file
View File

@ -0,0 +1,76 @@
# -*- 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 SECoP Device classes
"""
# XXX: is this still needed ???
# see devices.core ....
from secop.lib import attrdict
from secop.protocol import status
# XXX: deriving PARS/CMDS should be done in a suitable metaclass....
class Device(object):
"""Minimalist Device
all others derive from this"""
name = None
def read_status(self):
raise NotImplementedError('All Devices need a Status!')
def read_name(self):
return self.name
class Readable(Device):
"""A Readable Device"""
unit = ''
def read_value(self):
raise NotImplementedError('A Readable MUST provide a value')
def read_unit(self):
return self.unit
class Writeable(Readable):
"""Writeable can be told to change it's vallue"""
target = None
def read_target(self):
return self.target
def write_target(self, target):
self.target = target
class Driveable(Writeable):
"""A Moveable which may take a while to reach its target,
hence stopping it may be desired"""
def do_stop(self):
raise NotImplementedError('A Driveable MUST implement the STOP() '
'command')

View File

@ -0,0 +1,359 @@
# -*- 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 modules:
- add_module(modulename, moduleobj, export=True) registers a new module under the
given name, may also register it for exporting (making accessible)
- get_module(modulename) returns the requested module or None
- remove_module(modulename_or_obj): removes the module (during shutdown)
internal stuff which may be called
- list_modules(): return a list of modules + descriptive data as dict
- list_module_params():
return a list of paramnames for this module + descriptive data
"""
import time
import threading
from messages import *
from errors import *
class Dispatcher(object):
def __init__(self, logger, options):
self.equipment_id = options.pop('equipment_id')
self.log = logger
# map ALL modulename -> moduleobj
self._dispatcher_modules = {}
# list of EXPORTED modules
self._dispatcher_export = []
# list all connections
self._dispatcher_connections = []
# active (i.e. broadcast-receiving) connections
self._dispatcher_active_connections = set()
# map eventname -> list of subscribed connections
self._dispatcher_subscriptions = {}
self._dispatcher_lock = threading.RLock()
def handle_request(self, conn, msg):
"""handles incoming request
will call 'queue.request(data)' on conn to send reply before returning
"""
self.log.debug('Dispatcher: handling msg: %r' % msg)
# play thread safe !
with self._dispatcher_lock:
reply = None
# generate reply (coded and framed)
msgname = msg.__class__.__name__
if msgname.endswith('Request'):
msgname = msgname[:-len('Request')]
if msgname.endswith('Message'):
msgname = msgname[:-len('Message')]
self.log.debug('Looking for handle_%s' % msgname)
handler = getattr(self, 'handle_%s' % msgname, None)
if handler:
try:
reply = handler(conn, msg)
except SECOPError as err:
self.log.exception(err)
reply = ErrorMessage(errorclass=err.__class__.__name__,
errorinfo=[repr(err), str(msg)])
except (ValueError, TypeError) as err:
# self.log.exception(err)
reply = ErrorMessage(errorclass='BadValue',
errorinfo=[repr(err), str(msg)])
except Exception as err:
self.log.exception(err)
reply = ErrorMessage(errorclass='InternalError',
errorinfo=[repr(err), str(msg)])
else:
self.log.debug('Can not handle msg %r' % msg)
reply = self.unhandled(conn, msg)
if reply:
conn.queue_reply(reply)
def broadcast_event(self, msg, reallyall=False):
"""broadcasts a msg to all active connections"""
if reallyall:
listeners = self._dispatcher_connections
else:
if getattr(msg, 'command', None) is None:
eventname = '%s:%s' % (msg.module, msg.parameter if msg.parameter else 'value')
else:
eventname = '%s:%s()' % (msg.module, msg.command)
listeners = self._dispatcher_subscriptions.get(eventname, [])
listeners += list(self._dispatcher_active_connections)
for conn in listeners:
conn.queue_async_reply(msg)
def announce_update(self, moduleobj, pname, pobj):
"""called by modules param setters to notify subscribers of new values
"""
msg = Value(moduleobj.name, parameter=pname, value=pobj.value, t=pobj.timestamp)
self.broadcast_event(msg)
def subscribe(self, conn, modulename, pname='value'):
eventname = '%s:%s' % (modulename, pname)
self._dispatcher_subscriptions.setdefault(eventname, set()).add(conn)
def unsubscribe(self, conn, modulename, pname='value'):
eventname = '%s:%s' % (modulename, pname)
if eventname in self._dispatcher_subscriptions:
self._dispatcher_subscriptions.remove(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)
for _evt, conns in self._dispatcher_subscriptions.items():
conns.discard(conn)
def activate_connection(self, conn):
self._dispatcher_active_connections.add(conn)
def deactivate_connection(self, conn):
self._dispatcher_active_connections.discard(conn)
def register_module(self, moduleobj, modulename, export=True):
self.log.debug('registering module %r as %s (export=%r)' %
(moduleobj, modulename, export))
self._dispatcher_modules[modulename] = moduleobj
if export:
self._dispatcher_export.append(modulename)
def get_module(self, modulename):
module = self._dispatcher_modules.get(modulename, modulename)
if module != modulename:
self.log.debug('get_module(%r) -> %r' % (modulename, module))
return module
def remove_module(self, modulename_or_obj):
moduleobj = self.get_module(modulename_or_obj) or modulename_or_obj
modulename = moduleobj.name
if modulename in self._dispatcher_export:
self._dispatcher_export.remove(modulename)
self._dispatcher_modules.pop(modulename)
# XXX: also clean _dispatcher_subscriptions
def list_module_names(self):
# return a copy of our list
return self._dispatcher_export[:]
def list_modules(self):
dn = []
dd = {}
for modulename in self._dispatcher_export:
dn.append(modulename)
module = self.get_module(modulename)
descriptive_data = {
'class': module.__class__.__name__,
#'bases': module.__bases__,
'parameters': module.PARAMS.keys(),
'commands': module.CMDS.keys(),
# XXX: what else?
}
dd[modulename] = descriptive_data
return dn, dd
def list_module_params(self, modulename):
self.log.debug('list_module_params(%r)' % modulename)
if modulename in self._dispatcher_export:
# XXX: omit export=False params!
res = {}
for paramname, param in self.get_module(modulename).PARAMS.items():
if param.export == True:
res[paramname] = param
self.log.debug('list params for module %s -> %r' %
(modulename, res))
return res
self.log.debug('-> module is not to be exported!')
return {}
def _execute_command(self, modulename, command, arguments=None):
if arguments is None:
arguments = []
moduleobj = self.get_module(modulename)
if moduleobj is None:
raise NoSuchmoduleError(module=modulename)
cmdspec = moduleobj.CMDS.get(command, None)
if cmdspec is None:
raise NoSuchCommandError(module=modulename, command=command)
if len(cmdspec.arguments) != len(arguments):
raise BadValueError(module=modulename, command=command, reason='Wrong number of arguments!')
# now call func and wrap result as value
# note: exceptions are handled in handle_request, not here!
func = getattr(moduleobj, 'do'+command)
res = Value(modulename, command=command, value=func(*arguments), t=time.time())
return res
def _setParamValue(self, modulename, pname, value):
moduleobj = self.get_module(modulename)
if moduleobj is None:
raise NoSuchmoduleError(module=modulename)
pobj = moduleobj.PARAMS.get(pname, None)
if pobj is None:
raise NoSuchParamError(module=modulename, parameter=pname)
if pobj.readonly:
raise ReadonlyError(module=modulename, parameter=pname)
writefunc = getattr(moduleobj, 'write_%s' % pname, None)
# note: exceptions are handled in handle_request, not here!
if writefunc:
value = writefunc(value)
else:
setattr(moduleobj, pname, value)
if pobj.timestamp:
return Value(modulename, pname, value=pobj.value, t=pobj.timestamp)
return Value(modulename, pname, value=pobj.value)
def _getParamValue(self, modulename, pname):
moduleobj = self.get_module(modulename)
if moduleobj is None:
raise NoSuchmoduleError(module=modulename)
pobj = moduleobj.PARAMS.get(pname, None)
if pobj is None:
raise NoSuchParamError(module=modulename, parameter=pname)
readfunc = getattr(moduleobj, 'read_%s' % pname, None)
if readfunc:
# should also update the pobj (via the setter from the metaclass)
# note: exceptions are handled in handle_request, not here!
readfunc()
if pobj.timestamp:
return Value(modulename, parameter=pname, value=pobj.value, t=pobj.timestamp)
return Value(modulename, parameter=pname, value=pobj.value)
# now the (defined) handlers for the different requests
def handle_Help(self, conn, msg):
return HelpMessage()
def handle_Identify(self, conn, msg):
return IdentifyReply(version_string='currently,is,ignored,here')
def handle_Describe(self, conn, msg):
# XXX:collect descriptive data
# XXX:how to get equipment_id?
return DescribeReply(equipment_id = self.equipment_id, description = self.list_modules())
def handle_Poll(self, conn, msg):
# XXX: trigger polling and force sending event
res = self._getParamValue(msg.module, msg.parameter or 'value')
#self.broadcast_event(res)
if conn in self._dispatcher_active_connections:
return None # already send to myself
return res # send reply to inactive conns
def handle_Write(self, conn, msg):
# notify all by sending WriteReply
#msg1 = WriteReply(**msg.as_dict())
#self.broadcast_event(msg1)
# try to actually write XXX: should this be done asyncron? we could just return the reply in that case
if msg.parameter:
res = self._setParamValue(msg.module, msg.parameter, msg.value)
else:
# first check if module has a target
if 'target' not in self.get_module(msg.module).PARAMS:
raise ReadonlyError(module=msg.module, parameter=None)
res = self._setParamValue(msg.module, 'target', msg.value)
res.parameter = 'target'
#self.broadcast_event(res)
if conn in self._dispatcher_active_connections:
return None # already send to myself
return res # send reply to inactive conns
def handle_Command(self, conn, msg):
# notify all by sending CommandReply
#msg1 = CommandReply(**msg.as_dict())
#self.broadcast_event(msg1)
# XXX: should this be done asyncron? we could just return the reply in that case
# try to actually execute command
res = self._execute_command(msg.module, msg.command, msg.arguments)
#self.broadcast_event(res)
#if conn in self._dispatcher_active_connections:
# return None # already send to myself
return res # send reply to inactive conns
def handle_Heartbeat(self, conn, msg):
return HeartbeatReply(**msg.as_dict())
def handle_Activate(self, conn, msg):
self.activate_connection(conn)
# easy approach: poll all values...
for modulename, moduleobj in self._dispatcher_modules.items():
for pname, pobj in moduleobj.PARAMS.items():
# WARNING: THIS READS ALL PARAMS FROM HW!
# XXX: should we send the cached values instead? (pbj.value)
# also: ignore errors here.
try:
res = self._getParamValue(modulename, pname)
except SECOPError as e:
self.log.error('decide what to do here!')
self.log.exception(e)
res = Value(module=modulename, parameter=pname,
value=pobj.value, t=pobj.timestamp,
unit=pobj.unit)
if res.value != Ellipsis: # means we do not have a value at all so skip this
self.broadcast_event(res)
conn.queue_async_reply(ActivateReply(**msg.as_dict()))
return None
def handle_Deactivate(self, conn, msg):
self.deactivate_connection(conn)
conn.queue_async_reply(DeactivateReply(**msg.as_dict()))
return None
def handle_Error(self, conn, msg):
return msg
def unhandled(self, conn, msg):
"""handler for unhandled Messages
(no handle_<messagename> method was defined)
"""
self.log.error('IGN: got unhandled request %s' % msg)
return ErrorMessage(errorclass="InternalError",
errorstring = 'Got Unhandled Request %r' % msg)

View File

@ -0,0 +1,59 @@
#!/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)
# 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
from demo_v2 import DemoEncoder as DemoEncoderV2
from demo_v3 import DemoEncoder as DemoEncoderV3
from demo_v4 import DemoEncoder as DemoEncoderV4
from text import TextEncoder
from pickle import PickleEncoder
from simplecomm import SCPEncoder
ENCODERS = {
'pickle': PickleEncoder,
'text': TextEncoder,
'demo_v2': DemoEncoderV2,
'demo_v3': DemoEncoderV3,
'demo_v4': DemoEncoderV4,
'demo': DemoEncoderV4,
'scp': SCPEncoder,
}
__ALL__ = ['ENCODERS']

View File

@ -0,0 +1,102 @@
#!/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 secop.protocol.encoding import MessageEncoder
from secop.protocol import messages
from secop.lib.parsing import *
import re
DEMO_RE = re.compile(
r'^([!+-])?(\*|[a-z_][a-z_0-9]*)?(?:\:(\*|[a-z_][a-z_0-9]*))?(?:\:(\*|[a-z_][a-z_0-9]*))?(?:\=(.*))?')
class DemoEncoder(MessageEncoder):
def decode(sef, encoded):
# match [!][*|devicename][: *|paramname [: *|propname]] [=value]
match = DEMO_RE.match(encoded)
if match:
novalue, devname, pname, propname, assign = match.groups()
if assign:
print "parsing", assign,
assign = parse_args(assign)
print "->", assign
return messages.DemoRequest(
novalue, devname, pname, propname, assign)
return messages.HelpRequest()
def encode(self, msg):
if isinstance(msg, messages.DemoReply):
return msg.lines
handler_name = '_encode_' + msg.__class__.__name__
handler = getattr(self, handler_name, None)
if handler is None:
print "Handler %s not yet implemented!" % handler_name
try:
args = dict((k, msg.__dict__[k]) for k in msg.ARGS)
result = handler(**args)
except Exception as e:
print "Error encoding %r with %r!" % (msg, handler)
print e
return '~InternalError~'
return result
def _encode_AsyncDataUnit(self, devname, pname, value, timestamp,
error=None, unit=''):
return '#%s:%s=%s;t=%.3f' % (devname, pname, value, timestamp)
def _encode_Error(self, error):
return '~Error~ %r' % error
def _encode_InternalError(self, error):
return '~InternalError~ %r' % error
def _encode_ProtocollError(self, msgtype, msgname, msgargs):
return '~ProtocolError~ %s.%s.%r' % (msgtype, msgname, msgargs)
def _encode_NoSuchDeviceError(self, device):
return '~NoSuchDeviceError~ %s' % device
def _encode_NoSuchParamError(self, device, param):
return '~NoSuchParameterError~ %s:%s' % (device, param)
def _encode_ParamReadonlyError(self, device, param):
return '~ParamReadOnlyError~ %s:%s' % (device, param)
def _encode_NoSuchCommandError(self, device, command):
return '~NoSuchCommandError~ %s.%s' % (device, command)
def _encode_CommandFailedError(self, device, command):
return '~CommandFailedError~ %s.%s' % (device, command)
def _encode_InvalidParamValueError(self, device, param, value):
return '~InvalidValueForParamError~ %s:%s=%r' % (device, param, value)
def _encode_HelpReply(self):
return ['Help not yet implemented!',
'ask Markus Zolliker about the protocol']

View File

@ -0,0 +1,391 @@
#!/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 secop.protocol.encoding import MessageEncoder
from secop.protocol.messages import *
from secop.protocol.errors import ProtocollError
import ast
import re
def floatify(s):
try:
return int(s)
except (ValueError, TypeError):
try:
return float(s)
except (ValueError, TypeError):
return s
def devspec(msg, result=''):
if isinstance(msg, Message):
devs = ','.join(msg.devs)
pars = ','.join(msg.pars)
props = ','.join(msg.props)
else:
devs = msg.dev
pars = msg.par
props = msg.prop
if devs:
result = '%s %s' % (result, devs)
if pars:
result = '%s:%s' % (result, pars)
if props:
result = '%s:%s' % (result, props)
return result.strip()
def encode_value(value, prefix='', targetvalue='', cmd=''):
result = [prefix]
if value.dev:
result.append(' ')
result.append(value.dev)
if value.param:
result.append(':%s' % value.param)
if value.prop:
result.append(':%s' % value.prop)
# only needed for WriteMessages
if targetvalue:
result.append('=%s' % repr(targetvalue))
# only needed for CommandMessages
if cmd:
result.append(':%s' % cmd)
if value.value != Ellipsis:
# results always have a ';'
result.append('=%s;' % repr(value.value))
result.append(';'.join('%s=%s' % (qn, repr(qv))
for qn, qv in value.qualifiers.items()))
return ''.join(result).strip()
DEMO_RE_ERROR = re.compile(
r"""^error\s(?P<errortype>\w+)\s(?P<msgtype>\w+)?(?:\s(?P<devs>\*|[\w,]+)(?:\:(?P<pars>\*|[\w,]+)(?:\:(?P<props>\*|[\w,]+))?)?)?(?:(?:\=(?P<target>[^=;\s"]*))|(?:\((?P<cmdargs>[^\)]*)\)))?(?:\s"(?P<errorstring>[^"]*)")$""",
re.X)
DEMO_RE_OTHER = re.compile(
r"""^(?P<msgtype>\w+)(?:\s(?P<devs>\*|[\w,]+)(?:\:(?P<pars>\*|[\w,]+)(?:\:(?P<props>\*|[\w,]+))?)?)?(?:(?:\=(?P<target>[^=;]*))|(?::(?P<cmd>\w+)\((?P<args>[^\)]*)\)))?(?:=(?P<readback>[^;]+);(?P<qualifiers>.*))?$""",
re.X)
class DemoEncoder(MessageEncoder):
def __init__(self, *args, **kwds):
MessageEncoder.__init__(self, *args, **kwds)
self.result = [] # for decoding
self.expect_lines = 1
#self.tests()
def encode(self, msg):
"""msg object -> transport layer message"""
# fun for Humans
if isinstance(msg, HelpMessage):
r = ['#5']
r.append("help Try one of the following:")
r.append("help 'list' to query a list of modules")
r.append("help 'read <module>' to read a module")
r.append("help 'list <module>' to query a list of parameters")
r.append("help ... more to come")
return '\n'.join(r)
if isinstance(msg, (ListMessage, SubscribeMessage,
UnsubscribeMessage, TriggerMessage)):
msgtype = msg.MSGTYPE
if msg.result:
if msg.devs:
# msg.result is always a list!
return "%s=%s" % (devspec(msg, msgtype),
','.join(map(str, msg.result)))
return "%s=%s" % (msgtype, ','.join(map(str, msg.result)))
return devspec(msg, msgtype).strip()
if isinstance(msg, (ReadMessage, PollMessage, EventMessage)):
msgtype = msg.MSGTYPE
result = []
if len(msg.result or []) > 1:
result.append("#%d" % len(msg.result))
for val in msg.result or []:
# encode 1..N replies
result.append(encode_value(val, msgtype))
if not msg.result:
# encode a request (no results -> reply, else an error would
# have been sent)
result.append(devspec(msg, msgtype))
return '\n'.join(result)
if isinstance(msg, WriteMessage):
result = []
if len(msg.result or []) > 1:
result.append("#%d" % len(msg.result))
for val in msg.result or []:
# encode 1..N replies
result.append(
encode_value(
val,
'write',
targetvalue=msg.target))
if not msg.result:
# encode a request (no results -> reply, else an error would
# have been sent)
result.append('%s=%r' % (devspec(msg, 'write'), msg.target))
return '\n'.join(result)
if isinstance(msg, CommandMessage):
result = []
if len(msg.result or []) > 1:
result.append("#%d" % len(msg.result))
for val in msg.result or []:
# encode 1..N replies
result.append(
encode_value(
val,
'command',
cmd='%s(%s)' %
(msg.cmd,
','.join(
msg.args))))
if not msg.result:
# encode a request (no results -> reply, else an error would
# have been sent)
result.append(
'%s:%s(%s)' %
(devspec(
msg, 'command'), msg.cmd, ','.join(
msg.args)))
return '\n'.join(result)
if isinstance(msg, ErrorMessage):
return ('%s %s' % (devspec(msg, 'error %s' %
msg.errortype), msg.errorstring)).strip()
return 'Can not handle object %r!' % msg
def decode(self, encoded):
if encoded.startswith('#'):
# XXX: check if last message was complete
self.expect_lines = int(encoded[1:])
if self.result:
# XXX: also flag an error?
self.result = []
return None
if encoded == '':
return HelpMessage()
# now decode the message and append to self.result
msg = self.decode_single_message(encoded)
if msg:
# XXX: check if messagetype is the same as the already existing,
# else error
self.result.append(msg)
else:
# XXX: flag an error?
return HelpMessage()
self.expect_lines -= 1
if self.expect_lines <= 0:
# reconstruct a multi-reply-message from the entries
# return the first message, but extend the result list first
# if there is only 1 message, just return this
res = self.result.pop(0)
while self.result:
m = self.result.pop(0)
res.result.append(m.result[0])
self.expect_lines = 1
return res
# no complete message yet
return None
def decode_single_message(self, encoded):
# just decode a single message line
# 1) check for error msgs (more specific first)
m = DEMO_RE_ERROR.match(encoded)
if m:
return ErrorMessage(**m.groupdict())
# 2) check for 'normal' message
m = DEMO_RE_OTHER.match(encoded)
if m:
mgroups = m.groupdict()
msgtype = mgroups.pop('msgtype')
# reformat devspec
def helper(stuff, sep=','):
if not stuff:
return []
if sep in stuff:
return stuff.split(sep)
return [stuff]
devs = helper(mgroups.pop('devs'))
pars = helper(mgroups.pop('pars'))
props = helper(mgroups.pop('props'))
# sugar for listing stuff:
# map list -> list *
# map list x -> list x:*
# map list x:y -> list x:y:*
if msgtype == LIST:
if not devs:
devs = ['*']
elif devs[0] != '*':
if not pars:
pars = ['*']
elif pars[0] != '*':
if not props:
props = ['*']
# reformat cmdargs
args = ast.literal_eval(mgroups.pop('args') or '()')
if msgtype == COMMAND:
mgroups['args'] = args
# reformat qualifiers
print mgroups
quals = dict(
qual.split(
'=',
1) for qual in helper(
mgroups.pop(
'qualifiers',
';')))
# reformat value
result = []
readback = mgroups.pop('readback')
if readback or quals:
valargs = dict()
if devs:
valargs['dev'] = devs[0]
if pars:
valargs['par'] = pars[0]
if props:
valargs['prop'] = props[0]
result = [Value(floatify(readback), quals, **valargs)]
if msgtype == LIST and result:
result = [n.strip() for n in readback.split(',')]
# construct messageobj
if msgtype in MESSAGE:
return MESSAGE[msgtype](
devs=devs, pars=pars, props=props, result=result, **mgroups)
return ErrorMessage(errortype="SyntaxError",
errorstring="Can't handle %r" % encoded)
def tests(self):
testmsg = ['list',
'list *',
'list device',
'list device:param1,param2',
'list *:*',
'list *=ts,tcoil,mf,lhe,ln2;',
'read blub=12;t=3',
'command ts:stop()',
'command mf:quench(1,"now")',
'error GibbetNich query x:y:z=9 "blubub blah"',
'#3',
'read blub:a=12;t=3',
'read blub:b=13;t=3.1',
'read blub:c=14;t=3.3',
]
for m in testmsg:
print repr(m)
print self.decode(m)
print
DEMO_RE_MZ = re.compile(r"""^(?P<type>[a-z]+)? # request type word (read/write/list/...)
\ ? # optional space
(?P<device>[a-z][a-z0-9_]*)? # optional devicename
(?:\:(?P<param>[a-z0-9_]*) # optional ':'+paramname
(?:\:(?P<prop>[a-z0-9_]*))?)? # optinal ':' + propname
(?:(?P<op>[=\?])(?P<value>[^;]+)(?:\;(?P<quals>.*))?)?$""", re.X)
class DemoEncoder_MZ(MessageEncoder):
def decode(sef, encoded):
m = DEMO_RE_MZ.match(encoded)
if m:
print "implement me !"
return HelpRequest()
def encode(self, msg):
"""msg object -> transport layer message"""
# fun for Humans
if isinstance(msg, HelpReply):
r = []
r.append("Try one of the following:")
r.append("'list' to query a list of modules")
r.append("'read <module>' to read a module")
r.append("'list <module>' to query a list of parameters")
r.append("... more to come")
return '\n'.join(r)
return {
ListDevicesRequest: lambda msg: "list",
ListDevicesReply: lambda msg: "list=%s" % ','.join(sorted(msg.list_of_devices)),
GetVersionRequest: lambda msg: "version",
GetVersionReply: lambda msg: "version=%r" % msg.version,
ListDeviceParamsRequest: lambda msg: "list %s" % msg.device,
# do not include a '.' as param name!
ListDeviceParamsReply: lambda msg: "list %s=%s" % (msg.device, ','.join(sorted(msg.params.keys()))),
ReadValueRequest: lambda msg: "read %s" % msg.device,
ReadValueReply: lambda msg: "read %s=%r" % (msg.device, msg.value),
WriteValueRequest: lambda msg: "write %s=%r" % (msg.device, msg.value),
WriteValueReply: lambda msg: "write %s=%r" % (msg.device, msg.readback_value),
ReadParamRequest: lambda msg: "read %s:%s" % (msg.device, msg.param),
ReadParamReply: lambda msg: "read %s:%s=%r" % (msg.device, msg.param, msg.value),
WriteParamRequest: lambda msg: "write %s:%s=%r" % (msg.device, msg.param, msg.value),
WriteParamReply: lambda msg: "write %s:%s=%r" % (msg.device, msg.param, msg.readback_value),
# extensions
ReadAllDevicesRequest: lambda msg: "",
ReadAllDevicesReply: lambda msg: "",
ListParamPropsRequest: lambda msg: "readprop %s:%s" % (msg.device, msg.param),
ListParamPropsReply: lambda msg: ["readprop %s:%s" % (msg.device, msg.param)] + ["%s:%s:%s=%s" % (msg.device, msg.param, k, v) for k, v in sorted(msg.props.items())],
ReadPropertyRequest: lambda msg: "readprop %s:%s:%s" % (msg.device, msg.param, msg.prop),
ReadPropertyReply: lambda msg: "readprop %s:%s:%s=%s" % (msg.device, msg.param, msg.prop, msg.value),
AsyncDataUnit: lambda msg: "",
SubscribeRequest: lambda msg: "subscribe %s:%s" % (msg.device, msg.param) if msg.param else ("subscribe %s" % msg.device),
SubscribeReply: lambda msg: "subscribe %s:%s" % (msg.device, msg.param) if msg.param else ("subscribe %s" % msg.device),
UnSubscribeRequest: lambda msg: "",
UnSubscribeReply: lambda msg: "",
CommandRequest: lambda msg: "command %s:%s" % (msg.device, msg.command),
CommandReply: lambda msg: "command %s:%s" % (msg.device, msg.command),
# errors
ErrorReply: lambda msg: "",
InternalError: lambda msg: "",
ProtocollError: lambda msg: "",
CommandFailedError: lambda msg: "error CommandError %s:%s %s" % (msg.device, msg.param, msg.error),
NoSuchCommandError: lambda msg: "error NoSuchCommand %s:%s" % (msg.device, msg.param, msg.error),
NoSuchDeviceError: lambda msg: "error NoSuchModule %s" % msg.device,
NoSuchParamError: lambda msg: "error NoSuchParameter %s:%s" % (msg.device, msg.param),
ParamReadonlyError: lambda msg: "",
UnsupportedFeatureError: lambda msg: "",
InvalidParamValueError: lambda msg: "",
}[msg.__class__](msg)

View File

@ -0,0 +1,209 @@
#!/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 secop.protocol.encoding import MessageEncoder
from secop.protocol.messages import *
from secop.protocol.errors import ProtocollError
import ast
import re
import json
# each message is like <messagetype> [ \space <messageargs> [ \space <json> ]] \lf
# note: the regex allow <> for spec for testing only!
DEMO_RE = re.compile(
r"""^(?P<msgtype>[\*\?\w]+)(?:\s(?P<spec>[\w:<>]+)(?:\s(?P<json>.*))?)?$""", re.X)
#"""
# messagetypes:
IDENTREQUEST = '*IDN?' # literal
IDENTREPLY = 'Sine2020WP7.1&ISSE, SECoP, V2016-11-30, rc1' # literal
DESCRIPTIONSREQUEST = 'describe' # literal
DESCRIPTIONREPLY = 'describing' # +<id> +json
ENABLEEVENTSREQUEST = 'activate' # literal
ENABLEEVENTSREPLY = 'active' # literal, is end-of-initial-data-transfer
DISABLEEVENTSREQUEST = 'deactivate' # literal
DISABLEEVENTSREPLY = 'inactive' # literal
COMMANDREQUEST = 'do' # +module:command +json args (if needed)
COMMANDREPLY = 'doing' # +module:command +json args (if needed)
WRITEREQUEST = 'change' # +module[:parameter] +json_value -> NO direct reply, calls TRIGGER internally!
WRITEREPLY = 'changing' # +module[:parameter] +json_value -> NO direct reply, calls TRIGGER internally!
TRIGGERREQUEST = 'read' # +module[:parameter] -> NO direct reply, calls TRIGGER internally!
HEARTBEATREQUEST = 'ping' # +nonce_without_space
HEARTBEATREPLY = 'pong' # +nonce_without_space
EVENTTRIGGERREPLY = 'update' # +module[:parameter] +json_result_value_with_qualifiers NO REQUEST (use WRITE/TRIGGER)
EVENTCOMMANDREPLY = 'done' # +module:command +json result (if needed)
#EVENTWRITEREPLY = 'changed' # +module[:parameter] +json_result_value_with_qualifiers NO REQUEST (use WRITE/TRIGGER)
ERRORREPLY = 'ERROR' # +errorclass +json_extended_info
HELPREQUEST = 'help' # literal
HELPREPLY = 'helping' # +line number +json_text
ERRORCLASSES = ['NoSuchDevice', 'NoSuchParameter', 'NoSuchCommand',
'CommandFailed', 'ReadOnly', 'BadValue', 'CommunicationFailed',
'IsBusy', 'IsError', 'SyntaxError', 'InternalError',
'CommandRunning', 'Disabled',]
# note: above strings need to be unique in the sense, that none is/or starts with another
class DemoEncoder(MessageEncoder):
# map of msg to msgtype string as defined above.
ENCODEMAP = {
IdentifyRequest : (IDENTREQUEST,),
IdentifyReply : (IDENTREPLY,),
DescribeRequest : (DESCRIPTIONSREQUEST,),
DescribeReply : (DESCRIPTIONREPLY, 'equipment_id', 'description',),
ActivateRequest : (ENABLEEVENTSREQUEST,),
ActivateReply : (ENABLEEVENTSREPLY,),
DeactivateRequest: (DISABLEEVENTSREQUEST,),
DeactivateReply : (DISABLEEVENTSREPLY,),
CommandRequest : (COMMANDREQUEST, lambda msg: "%s:%s" % (msg.module, msg.command), 'arguments',),
CommandReply : (COMMANDREPLY, lambda msg: "%s:%s" % (msg.module, msg.command), 'arguments',),
WriteRequest : (WRITEREQUEST, lambda msg: "%s:%s" % (msg.module, msg.parameter) if msg.parameter else msg.module, 'value',),
WriteReply : (WRITEREPLY, lambda msg: "%s:%s" % (msg.module, msg.parameter) if msg.parameter else msg.module, 'value',),
PollRequest : (TRIGGERREQUEST, lambda msg: "%s:%s" % (msg.module, msg.parameter) if msg.parameter else msg.module, ),
HeartbeatRequest : (HEARTBEATREQUEST, 'nonce',),
HeartbeatReply : (HEARTBEATREPLY, 'nonce',),
HelpMessage: (HELPREQUEST, ),
# EventMessage : (EVENTREPLY, lambda msg: "%s:%s" % (msg.module, msg.parameter or (msg.command+'()'))
# if msg.parameter or msg.command else msg.module, 'value',),
ErrorMessage : (ERRORREPLY, 'errorclass', 'errorinfo',),
Value: (EVENTTRIGGERREPLY, lambda msg: "%s:%s" % (msg.module, msg.parameter or (msg.command+'()'))
if msg.parameter or msg.command else msg.module,
lambda msg: [msg.value, msg.qualifiers] if msg.qualifiers else [msg.value]),
}
DECODEMAP = {
IDENTREQUEST : lambda spec, data: IdentifyRequest(),
IDENTREPLY : lambda spec, data: IdentifyReply(encoded), # handled specially, listed here for completeness
DESCRIPTIONSREQUEST : lambda spec, data: DescribeRequest(),
DESCRIPTIONREPLY : lambda spec, data: DescribeReply(equipment_id=spec[0], description=data),
ENABLEEVENTSREQUEST : lambda spec, data: ActivateRequest(),
ENABLEEVENTSREPLY: lambda spec, data:ActivateReply(),
DISABLEEVENTSREQUEST: lambda spec, data:DeactivateRequest(),
DISABLEEVENTSREPLY: lambda spec, data:DeactivateReply(),
COMMANDREQUEST: lambda spec, data:CommandRequest(module=spec[0], command=spec[1], arguments=data),
COMMANDREPLY: lambda spec, data: CommandReply(module=spec[0], command=spec[1], arguments=data),
WRITEREQUEST: lambda spec, data: WriteRequest(module=spec[0], parameter=spec[1], value=data),
WRITEREPLY:lambda spec, data:WriteReply(module=spec[0], parameter=spec[1], value=data),
TRIGGERREQUEST:lambda spec, data:PollRequest(module=spec[0], parameter=spec[1]),
HEARTBEATREQUEST:lambda spec, data:HeartbeatRequest(nonce=spec[0]),
HEARTBEATREPLY:lambda spec, data:HeartbeatReply(nonce=spec[0]),
HELPREQUEST: lambda spec, data:HelpMessage(),
# HELPREPLY: lambda spec, data:None, # ignore this
ERRORREPLY:lambda spec, data:ErrorMessage(errorclass=spec[0], errorinfo=data),
EVENTTRIGGERREPLY:lambda spec, data:Value(module=spec[0], parameter=spec[1], value=data[0], qualifiers=data[1] if len(data)>1 else {}),
EVENTCOMMANDREPLY: lambda spec, data:None, # ignore this
# EVENTWRITEREPLY:lambda spec, data:Value(module=spec[0], parameter=spec[1], value=data[0], qualifiers=data[1] if len(data)>1 else {}),
}
def __init__(self, *args, **kwds):
MessageEncoder.__init__(self, *args, **kwds)
self.tests()
def encode(self, msg):
"""msg object -> transport layer message"""
# fun for Humans
if isinstance(msg, HelpMessage):
text = """Try one of the following:
'%s' to query protocol version
'%s' to read the description
'%s <module>[:<parameter>]' to request reading a value
'%s <module>[:<parameter>] value' to request changing a value
'%s <module>[:<command>()]' to execute a command
'%s <nonce>' to request a heartbeat response
'%s' to activate async updates
'%s' to deactivate updates
""" %(IDENTREQUEST, DESCRIPTIONSREQUEST, TRIGGERREQUEST,
WRITEREQUEST, COMMANDREQUEST, HEARTBEATREQUEST,
ENABLEEVENTSREQUEST, DISABLEEVENTSREQUEST)
return '\n'.join('%s %d %s' %(HELPREPLY, i+1, l.strip()) for i,l in enumerate(text.split('\n')[:-1]))
for msgcls, parts in self.ENCODEMAP.items():
if isinstance(msg, msgcls):
# resolve lambdas
parts = [parts[0]] + [p(msg) if callable(p) else getattr(msg, p) for p in parts[1:]]
if len(parts) > 1:
parts[1] = str(parts[1])
if len(parts) == 3:
parts[2] = json.dumps(parts[2])
return ' '.join(parts)
def decode(self, encoded):
# first check beginning
match = DEMO_RE.match(encoded)
if not match:
print repr(encoded), repr(IDENTREPLY)
if encoded == IDENTREPLY: # XXX:better just check the first 2 parts...
return IdentifyReply(version_string=encoded)
return HelpMessage()
return ErrorMessage(errorclass='SyntaxError',
errorinfo='Regex did not match!',
is_request=True)
msgtype, msgspec, data = match.groups()
if msgspec is None and data:
return ErrorMessage(errorclass='InternalError',
errorinfo='Regex matched json, but not spec!',
is_request=True)
if msgtype in self.DECODEMAP:
if msgspec and ':' in msgspec:
msgspec = msgspec.split(':', 1)
else:
msgspec = (msgspec, None)
if data:
try:
data = json.loads(data)
except ValueError as err:
return ErrorMessage(errorclass='BadValue',
errorinfo=[repr(err), str(encoded)])
return self.DECODEMAP[msgtype](msgspec, data)
return ErrorMessage(errorclass='SyntaxError',
errorinfo='%r: No Such Messagetype defined!' % encoded,
is_request=True)
def tests(self):
print "---- Testing encoding -----"
for msgclass, parts in sorted(self.ENCODEMAP.items()):
print msgclass
e=self.encode(msgclass(module='<module>',parameter='<paramname>',value=2.718,equipment_id='<id>',description='descriptive data',command='<cmd>',arguments='<arguments>',nonce='<nonce>',errorclass='InternalError',errorinfo='nix'))
print e
print self.decode(e)
print
print "---- Testing decoding -----"
for msgtype, _ in sorted(self.DECODEMAP.items()):
msg = '%s a:b 3' % msgtype
if msgtype in [EVENTTRIGGERREPLY]:#, EVENTWRITEREPLY]:
msg = '%s a:b [3,{"t":193868}]' % msgtype
print msg
d=self.decode(msg)
print d
print self.encode(d)
print
print "---- Testing done -----"

View File

@ -0,0 +1,47 @@
#!/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 secop.protocol.encoding import MessageEncoder
from secop.protocol import messages
from secop.lib.parsing import *
try:
import cPickle as pickle
except ImportError:
import pickle
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)

View File

@ -0,0 +1,209 @@
#!/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 secop.protocol.encoding import MessageEncoder
from secop.protocol.messages import *
from secop.lib.parsing import *
import re
import ast
SCPMESSAGE = re.compile(
r'^(?:(?P<errorcode>[0-9@])\ )?(?P<device>[a-zA-Z0-9_\*]*)(?:/(?P<param>[a-zA-Z0-9_\*]*))+(?P<op>[-+=\?\ ])?(?P<value>.*)')
class SCPEncoder(MessageEncoder):
def encode(self, msg):
"""msg object -> transport layer message"""
# fun for Humans
if isinstance(msg, HelpReply):
r = []
r.append("Try one of the following:")
r.append("'/version?' to query the current version")
r.append("'/modules?' to query the list of modules")
r.append(
"'<module>/parameters?' to query the list of params of a module")
r.append("'<module>/value?' to query the value of a module")
r.append("'<module>/status?' to query the status of a module")
r.append("'<module>/target=<new_value>' to move a module")
r.append("replies copy the request and are prefixed with an errorcode:")
r.append(
"0=OK,3=NoSuchCommand,4=NosuchDevice,5=NoSuchParam,6=SyntaxError,7=BadValue,8=Readonly,9=Forbidden,@=Async")
r.append("extensions: @-prefix as error-code,")
r.append("'<module>/+' subscribe all params of module")
r.append("'<module>/<param>+' subscribe a param of a module")
r.append("use '-' instead of '+' to unsubscribe")
r.append("'<module>/commands?' list of commands")
r.append(
"'<module>/<command>@[possible args] execute command (ex. 'stop@')")
return '\n'.join(r)
return {
ListDevicesRequest: lambda msg: "devices?",
ListDevicesReply: lambda msg: "0 devices=" + repr(list(msg.list_of_devices)),
GetVersionRequest: lambda msg: "version?",
GetVersionReply: lambda msg: "0 version=%r" % msg.version,
ListDeviceParamsRequest: lambda msg: "%s/parameters?" % msg.device,
ListDeviceParamsReply: lambda msg: "0 %s/parameters=%r" % (msg.device, list(msg.params)),
ReadValueRequest: lambda msg: "%s/value?" % msg.device,
ReadValueReply: lambda msg: "0 %s/value?%r" % (msg.device, msg.value),
WriteValueRequest: lambda msg: "%s/value=%r" % (msg.device, msg.value),
WriteValueReply: lambda msg: "0 %s/value=%r" % (msg.device, msg.value),
ReadParamRequest: lambda msg: "%s/%s?" % (msg.device, msg.param),
ReadParamReply: lambda msg: "0 %s/%s?%r" % (msg.device, msg.param, msg.value),
WriteParamRequest: lambda msg: "%s/%s=%r" % (msg.device, msg.param, msg.value),
WriteParamReply: lambda msg: "0 %s/%s=%r" % (msg.device, msg.param, msg.readback_value),
# extensions
ReadAllDevicesRequest: lambda msg: "*/value?",
ReadAllDevicesReply: lambda msg: ["0 %s/value=%s" % (m.device, m.value) for m in msg.readValueReplies],
ListParamPropsRequest: lambda msg: "%s/%s/?" % (msg.device, msg.param),
ListParamPropsReply: lambda msg: ["0 %s/%s/%s" % (msg.device, msg.param, p) for p in msg.props],
AsyncDataUnit: lambda msg: "@ %s/%s=%r" % (msg.devname, msg.pname, msg.value),
SubscribeRequest: lambda msg: "%s/%s+" % (msg.devname, msg.pname),
# violates spec ! we would need the original request here....
SubscribeReply: lambda msg: "0 / %r" % [repr(s) for s in msg.subscriptions],
UnSubscribeRequest: lambda msg: "%s/%s+" % (msg.devname, msg.pname),
# violates spec ! we would need the original request here....
UnSubscribeReply: lambda msg: "0 / %r" % [repr(s) for s in msg.subscriptions],
# errors
# violates spec ! we would need the original request here....
ErrorReply: lambda msg: "1 /%r" % msg.error,
# violates spec ! we would need the original request here....
InternalError: lambda msg: "1 /%r" % msg.error,
# violates spec ! we would need the original request here....
ProtocollError: lambda msg: "6 /%r" % msg.error,
# violates spec ! we would need the original request here....
CommandFailedError: lambda msg: "1 %s/%s" % (msg.device, msg.command),
# violates spec ! we would need the original request here....
NoSuchCommandError: lambda msg: "3 %s/%s" % (msg.device, msg.command),
# violates spec ! we would need the original request here....
NoSuchDeviceError: lambda msg: "4 %s/ %r" % (msg.device, msg.error),
# violates spec ! we would need the original request here....
NoSuchParamError: lambda msg: "5 %s/%s %r" % (msg.device, msg.param, msg.error),
# violates spec ! we would need the original request here....
ParamReadonlyError: lambda msg: "8 %s/%s %r" % (msg.device, msg.param, msg.error),
# violates spec ! we would need the original request here....
UnsupportedFeatureError: lambda msg: "3 / %r" % msg.feature,
# violates spec ! we would need the original request here....
InvalidParamValueError: lambda msg: "7 %s/%s=%r %r" % (msg.device, msg.param, msg.value, msg.error),
}[msg.__class__](msg)
def decode(self, encoded):
"""transport layer message -> msg object"""
match = SCPMESSAGE.match(encoded)
if not(match):
return HelpRequest()
err, dev, par, op, val = match.groups()
if val is not None:
try:
val = ast.literal_eval(val)
except Exception as e:
return SyntaxError('while decoding %r: %s' % (encoded, e))
if err == '@':
# async
if op == '=':
return AsyncDataUnit(dev, par, val)
return ProtocolError("Asyncupdates must have op = '='!")
elif err is None:
# request
if op == '+':
# subscribe
if dev:
if par:
return SubscribeRequest(dev, par)
return SubscribeRequest(dev, '*')
if op == '-':
# unsubscribe
if dev:
if par:
return UnsubscribeRequest(dev, par)
return UnsubscribeRequest(dev, '*')
if op == '?':
if dev is None:
# 'server' commands
if par == 'devices':
return ListDevicesRequest()
elif par == 'version':
return GetVersionRequest()
return ProtocolError()
if par == 'parameters':
return ListDeviceParamsRequest(dev)
elif par == 'value':
return ReadValueRequest(dev)
elif dev == '*' and par == 'value':
return ReadAllDevicesRequest()
else:
return ReadParamRequest(dev, par)
elif op == '=':
if dev and (par == 'value'):
return WriteValueRequest(dev, val)
if par.endswith('/') and op == '?':
return ListParamPropsRequest(dev, par)
return WriteParamRequest(dev, par, val)
elif err == '0':
# reply
if dev == '':
if par == 'devices':
return ListDevicesReply(val)
elif par == 'version':
return GetVersionReply(val)
return ProtocolError(encoded)
if par == 'parameters':
return ListDeviceParamsReply(dev, val)
if par == 'value':
if op == '?':
return ReadValueReply(dev, val)
elif op == '=':
return WriteValueReply(dev, val)
return ProtocolError(encoded)
if op == '+':
return SubscribeReply(ast.literal_eval(dev))
if op == '-':
return UnSubscribeReply(ast.literal_eval(dev))
if op == '?':
return ReadParamReply(dev, par, val)
if op == '=':
return WriteParamReply(dev, par, val)
return ProtocolError(encoded)
else:
# error
if err in ('1', '2'):
return InternalError(encoded)
elif err == '3':
return NoSuchCommandError(dev, par)
elif err == '4':
return NoSuchDeviceError(dev, encoded)
elif err == '5':
return NoSuchParamError(dev, par, val)
elif err == '7':
return InvalidParamValueError(dev, par, val, encoded)
elif err == '8':
return ParamReadonlyError(dev, par, encoded)
else: # err == 6 or other stuff
return ProtocollError(encoded)

View File

@ -0,0 +1,65 @@
#!/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 secop.protocol.encoding import MessageEncoder
from secop.protocol import messages
from secop.lib.parsing import *
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.HelpMessage):
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') and len(m) > len("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.HelpMessage()

70
secop/protocol/errors.py Normal file
View File

@ -0,0 +1,70 @@
# -*- 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 (internal) SECoP Errors"""
class SECOPError(RuntimeError):
def __init__(self, *args, **kwds):
self.args = args
for k,v in kwds.items():
setattr(self, k, v)
class InternalError(SECOPError):
pass
class ProtocollError(SECOPError):
pass
# XXX: unifiy NoSuch...Error ?
class NoSuchModuleError(SECOPError):
pass
class NoSuchParamError(SECOPError):
pass
class NoSuchCommandError(SECOPError):
pass
class ReadonlyError(SECOPError):
pass
class CommandFailedError(SECOPError):
pass
class InvalidParamValueError(SECOPError):
pass
if __name__ == '__main__':
print("Minimal testing of errors....")
print "OK"
print

View File

@ -0,0 +1,62 @@
#!/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
from null import NullFramer
from eol import EOLFramer
from rle import RLEFramer
from demo import DemoFramer
FRAMERS = {
'null': NullFramer,
'eol': EOLFramer,
'rle': RLEFramer,
'demo': DemoFramer,
}
__ALL__ = ['FRAMERS']

View File

@ -0,0 +1,84 @@
#!/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"""
from secop.protocol.framing import Framer
class DemoFramer(Framer):
"""Text based message framer
frmes are delimited by '\n'
messages are delimited by '\n\n'
'\r' is ignored
"""
def __init__(self):
self.data = b''
self.decoded = []
def encode(self, frames):
"""add transport layer encapsulation/framing of messages"""
if isinstance(frames, (tuple, list)):
return b'\n'.join(frames) + b'\n\n'
return b'%s\n\n' % frames
def decode(self, data):
"""remove transport layer encapsulation/framing of messages
returns a list of messageframes which got decoded from data!
"""
self.data += data
res = []
while b'\n' in self.data:
frame, self.data = self.data.split(b'\n', 1)
if frame.endswith('\r'):
frame = frame[:-1]
if self.data.startswith('\r'):
self.data = self.data[1:]
res.append(frame)
return res
def decode2(self, data):
"""remove transport layer encapsulation/framing of messages
returns a _list_ of messageframes which got decoded from data!
"""
self.data += data.replace(b'\r', '')
while b'\n' in self.data:
frame, self.data = self.data.split(b'\n', 1)
if frame:
# not an empty line -> belongs to this set of messages
self.decoded.append(frame)
else:
# empty line -> our set of messages is finished decoding
res = self.decoded
self.decoded = []
return res
return None
def reset(self):
self.data = b''
self.decoded = []

View File

@ -0,0 +1,57 @@
#!/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"""
from secop.protocol.framing import Framer
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''

View File

@ -0,0 +1,40 @@
#!/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"""
from secop.protocol.framing import Framer
class NullFramer(Framer):
"""do-nothing-framer
assumes messages are framed by themselfs or the interface does it already.
"""
def encode(self, *frames):
"""add transport layer encapsulation/framing of messages"""
return ''.join(frames)
def decode(self, data):
"""remove transport layer encapsulation/framing of messages"""
return [data]

View File

@ -0,0 +1,74 @@
#!/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"""
from secop.protocol.framing import Framer
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

View File

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# *****************************************************************************
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
#
# *****************************************************************************
"""provide server interfaces to be used by clients"""
from tcp import TCPServer
INTERFACES = {
'tcp': TCPServer,
}
# for 'from protocol.interface import *' to only import the dict
__ALL__ = ['INTERFACES']

View File

@ -0,0 +1,136 @@
#!/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
from secop.protocol.encoding import ENCODERS
from secop.protocol.framing import FRAMERS
from secop.protocol.messages import HelpMessage
class TCPRequestHandler(SocketServer.BaseRequestHandler):
def setup(self):
self.log = self.server.log
self._queue = collections.deque(maxlen=100)
self.framing = self.server.framingCLS()
self.encoding = self.server.encodingCLS()
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:
# put message into encoder to get frame(s)
# put frame(s) into framer to get bytestring
# send bytestring
outmsg = self._queue.popleft()
outframes = self.encoding.encode(outmsg)
outdata = self.framing.encode(outframes)
mysocket.sendall(outdata)
# 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
# put data into (de-) framer,
# put frames into (de-) coder and if a message appear,
# call dispatcher.handle_request(self, message)
# dispatcher will queue the reply before returning
frames = self.framing.decode(data)
if frames is not None:
if not frames: # empty list
self.queue_reply(HelpMessage(MSGTYPE=reply))
for frame in frames:
reply = None
msg = self.encoding.decode(frame)
if msg:
serverobj.dispatcher.handle_request(self, msg)
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)
except Exception:
pass
finally:
self.request.close()
class TCPServer(SocketServer.ThreadingTCPServer):
daemon_threads = True
allow_reuse_address = True
def __init__(self, logger, interfaceopts, dispatcher):
self.dispatcher = dispatcher
self.log = logger
bindto = interfaceopts.pop('bindto', 'localhost')
portnum = int(interfaceopts.pop('bindport', DEF_PORT))
if ':' in bindto:
bindto, _port = bindto.rsplit(':')
portnum = int(_port)
# tcp is a byte stream, so we need Framers (to get frames)
# and encoders (to en/decode messages from frames)
self.framingCLS = FRAMERS[interfaceopts.pop('framing', 'none')]
self.encodingCLS = ENCODERS[interfaceopts.pop('encoding', 'pickle')]
self.log.debug("TCPServer binding to %s:%d" % (bindto, portnum))
self.log.debug("TCPServer using framing=%s" % self.framingCLS.__name__)
self.log.debug("TCPServer using encoding=%s" % self.encodingCLS.__name__)
SocketServer.ThreadingTCPServer.__init__(self, (bindto, portnum),
TCPRequestHandler,
bind_and_activate=True)
self.log.info("TCPServer initiated")

View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# *****************************************************************************
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
#
# *****************************************************************************
"""provide a zmq server"""
# tbd.
# use zmq frames??
# handle async and sync with different zmq ports?

160
secop/protocol/messages.py Normal file
View File

@ -0,0 +1,160 @@
# -*- 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 SECoP Messages"""
class Message(object):
"""base class for messages"""
is_request = False
is_reply = False
is_error = False
def __init__(self, **kwds):
self.ARGS = set()
for k, v in kwds.items():
self.setvalue(k, v)
def setvalue(self, key, value):
setattr(self, key, value)
self.ARGS.add(key)
def __repr__(self):
return self.__class__.__name__ + '(' + \
', '.join('%s=%s' % (k, repr(getattr(self, k)))
for k in sorted(self.ARGS)) + ')'
def as_dict(self):
"""returns set parameters as dict"""
return dict(map(lambda k:(k, getattr(self,k)),self.ARGS))
class Value(object):
def __init__(self, module, parameter=None, command=None, value=Ellipsis, **qualifiers):
self.module = module
self.parameter = parameter
self.command = command
self.value = value
self.qualifiers = qualifiers
self.msgtype = 'update' # 'changed' or 'done'
def __repr__(self):
devspec = self.module
if self.parameter:
devspec = '%s:%s' % (devspec, self.parameter)
elif self.command:
devspec = '%s:%s()' % (devspec, self.command)
return '%s:Value(%s)' % (devspec, ', '.join(
[repr(self.value)] + ['%s=%s' % (k, repr(v)) for k, v in self.qualifiers.items()]))
class IdentifyRequest(Message):
is_request = True
class IdentifyReply(Message):
is_reply = True
version_string = None
class DescribeRequest(Message):
is_request = True
class DescribeReply(Message):
is_reply = True
equipment_id = None
description = None
class ActivateRequest(Message):
is_request = True
class ActivateReply(Message):
is_reply = True
class DeactivateRequest(Message):
is_request = True
class DeactivateReply(Message):
is_reply = True
class CommandRequest(Message):
is_request = True
command = ''
arguments = []
class CommandReply(Message):
is_reply = True
command = ''
arguments = None
class WriteRequest(Message):
is_request = True
module = None
parameter = None
value = None
class WriteReply(Message):
is_reply = True
module = None
parameter = None
value = None
class PollRequest(Message):
is_request = True
module = None
parameter = None
class HeartbeatRequest(Message):
is_request = True
nonce = 'alive'
class HeartbeatReply(Message):
is_reply = True
nonce = 'undefined'
class EventMessage(Message):
# use Value directly for Replies !
is_reply = True
module = None
parameter = None
command = None
value = None # Value object ! (includes qualifiers!)
class ErrorMessage(Message):
is_error = True
errorclass = 'InternalError'
errorinfo = None
class HelpMessage(Message):
is_reply = True
is_request = True
if __name__ == '__main__':
print("Minimal testing of messages....")
m = Message(MSGTYPE='test', a=1, b=2, c='x')
print m
print ReadMessage(devs=['a'], result=[Value(12.3)])
print "OK"
print

View File

@ -0,0 +1,202 @@
# -*- 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 SECoP Messages"""
# Request Types
REQUEST = 'request'
REPLY = 'reply'
ERROR = 'error'
# Message types ('actions') hint: fetch is list+read
LIST = 'list'
READ = 'read'
WRITE = 'write'
COMMAND = 'command'
POLL = 'poll'
SUBSCRIBE = 'subscribe'
UNSUBSCRIBE = 'unsubscribe'
TRIGGER = 'trigger'
EVENT = 'event'
ERROR = 'error'
HELP = 'help'
# base class for messages
class Message(object):
MSGTYPE = 'Undefined'
devs = None
pars = None
props = None
result = None
error = None
ARGS = None
errortype = None
def __init__(self, **kwds):
self.devs = []
self.pars = []
self.props = []
self.result = []
self.ARGS = set()
for k, v in kwds.items():
self.setvalue(k, v)
def setvalue(self, key, value):
setattr(self, key, value)
self.ARGS.add(key)
@property
def NAME(self):
# generate sensible name
r = 'Message'
if self.props:
r = 'Property' if self.props != ['*'] else 'Properties'
elif self.pars:
r = 'Parameter' if self.pars != ['*'] else 'Parameters'
elif self.devs:
r = 'Device' if self.devs != ['*'] else 'Devices'
t = ''
if self.MSGTYPE in [LIST, READ, WRITE, COMMAND,
POLL, SUBSCRIBE, UNSUBSCRIBE, HELP]:
t = 'Request' if not self.result else 'Reply'
if self.errortype is None:
return self.MSGTYPE.title() + r + t
else:
return self.errortype + 'Error'
def __repr__(self):
return self.NAME + '(' + \
', '.join('%s=%r' % (k, getattr(self, k))
for k in self.ARGS if getattr(self, k) is not None) + ')'
class Value(object):
def __init__(self, value=Ellipsis, qualifiers=None, **kwds):
self.dev = ''
self.param = ''
self.prop = ''
self.value = value
self.qualifiers = qualifiers or dict()
self.__dict__.update(kwds)
def __repr__(self):
devspec = self.dev
if self.param:
devspec = '%s:%s' % (devspec, self.param)
if self.prop:
devspec = '%s:%s' % (devspec, self.prop)
return '%s:Value(%s)' % (devspec, ', '.join(
[repr(self.value)] + ['%s=%r' % (k, v) for k, v in self.qualifiers.items()]))
class ListMessage(Message):
MSGTYPE = LIST
class ReadMessage(Message):
MSGTYPE = READ # return cached value
class WriteMessage(Message):
MSGTYPE = WRITE # write value to some spec
target = None # actually float or string
class CommandMessage(Message):
MSGTYPE = COMMAND
cmd = '' # always string
args = []
result = []
class PollMessage(Message):
MSGTYPE = POLL # read HW and return hw_value
class SubscribeMessage(Message):
MSGTYPE = SUBSCRIBE
class UnsubscribeMessage(Message):
MSGTYPE = UNSUBSCRIBE
class TriggerMessage(Message):
MSGTYPE = TRIGGER
class EventMessage(Message):
MSGTYPE = EVENT
class ErrorMessage(Message):
MSGTYPE = ERROR
errorstring = 'an unhandled error occured'
errortype = 'UnknownError'
class HelpMessage(Message):
MSGTYPE = HELP
class NoSuchDeviceError(ErrorMessage):
def __init__(self, *devs):
ErrorMessage.__init__(self, devs=devs, errorstring="Device %r does not exist" % devs[0], errortype='NoSuchDevice')
class NoSuchParamError(ErrorMessage):
def __init__(self, dev, *params):
ErrorMessage.__init__(self, devs=(dev,), params=params, errorstring="Device %r has no parameter %r" % (dev, params[0]), errortype='NoSuchParam')
class ParamReadonlyError(ErrorMessage):
def __init__(self, dev, *params):
ErrorMessage.__init__(self, devs=(dev,), params=params, errorstring="Device %r, parameter %r is not writeable!" % (dev, params[0]), errortype='ParamReadOnly')
class InvalidParamValueError(ErrorMessage):
def __init__(self, dev, param, value, e):
ErrorMessage.__init__(self, devs=(dev,), params=params, values=(value), errorstring=str(e), errortype='InvalidParamValueError')
class InternalError(ErrorMessage):
def __init__(self, err, **kwds):
ErrorMessage.__init__(self, errorstring=str(err), errortype='InternalError', **kwds)
MESSAGE = dict((cls.MSGTYPE, cls) for cls in [HelpMessage, ErrorMessage, EventMessage, TriggerMessage, UnsubscribeMessage, SubscribeMessage,
PollMessage, CommandMessage, WriteMessage, ReadMessage, ListMessage])
if __name__ == '__main__':
print("Minimal testing of messages....")
m = Message(MSGTYPE='test', a=1, b=2, c='x')
print m
print ReadMessage(devs=['a'], result=[Value(12.3)])
print "OK"
print

37
secop/protocol/status.py Normal file
View File

@ -0,0 +1,37 @@
#!/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>
#
# *****************************************************************************
"""Define Status constants"""
# could also be some objects
OK = 100
BUSY = 200
WARN = 300
UNSTABLE = 350
ERROR = 400
UNKNOWN = -1
#OK = 'idle'
#BUSY = 'busy'
#WARN = 'alarm'
#UNSTABLE = 'unstable'
#ERROR = 'ERROR'
#UNKNOWN = 'unknown'

189
secop/server.py Normal file
View File

@ -0,0 +1,189 @@
# -*- 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>
# Alexander Lenz <alexander.lenz@frm2.tum.de>
#
# *****************************************************************************
"""Define helpers"""
import os
import time
import psutil
import threading
import ConfigParser
from daemon import DaemonContext
from daemon.pidfile import TimeoutPIDLockFile
import loggers
from secop.lib import get_class
from secop.protocol.dispatcher import Dispatcher
from secop.protocol.interface import INTERFACES
#from secop.protocol.encoding import ENCODERS
#from secop.protocol.framing import FRAMERS
from secop.errors import ConfigError
class Server(object):
def __init__(self, name, workdir, parentLogger=None):
self._name = name
self._workdir = workdir
if parentLogger is None:
parentLogger = loggers.log
self.log = parentLogger.getChild(name, True)
self._pidfile = os.path.join(workdir, 'pid', name + '.pid')
self._cfgfile = os.path.join(workdir, 'etc', name + '.cfg')
self._dispatcher = None
self._interface = None
def start(self):
piddir = os.path.dirname(self._pidfile)
if not os.path.isdir(piddir):
os.makedirs(piddir)
pidfile = TimeoutPIDLockFile(self._pidfile)
if pidfile.is_locked():
self.log.error('Pidfile already exists. Exiting')
with DaemonContext(working_directory=self._workdir,
pidfile=pidfile,
files_preserve=self.log.getLogfileStreams()):
self.run()
def run(self):
self._processCfg()
self.log.info('startup done, handling transport messages')
self._threads = set()
for _if in self._interfaces:
self.log.debug('starting thread for interface %r' % _if)
t = threading.Thread(target=_if.serve_forever)
t.daemon = True
t.start()
self._threads.add(t)
while self._threads:
time.sleep(1)
for t in self._threads:
if not t.is_alive():
self.log.debug('thread %r died (%d still running)' % (t,len(self._threads)))
t.join()
self._threads.discard(t)
def _processCfg(self):
self.log.debug('Parse config file %s ...' % self._cfgfile)
parser = ConfigParser.SafeConfigParser()
if not parser.read([self._cfgfile]):
self.log.error('Couldn\'t read cfg file !')
raise ConfigError('Couldn\'t read cfg file %r' % self._cfgfile)
self._interfaces = []
deviceopts = []
interfaceopts = []
equipment_id = 'unknown'
for section in parser.sections():
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:
self.log.error('Device %s needs a class option!')
raise ConfigError(
'cfgfile %r: Device %s needs a class option!'
% (self._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])
if section.lower().startswith('interface '):
# interface section
# omit leading 'interface ' string
ifname = section[len('interface '):]
ifopts = dict(item for item in parser.items(section))
if 'interface' not in ifopts:
self.log.error('Interface %s needs an interface option!')
raise ConfigError(
'cfgfile %r: Interface %s needs an interface option!'
% (self._cfgfile, ifname))
# all went well so far
interfaceopts.append([ifname, ifopts])
if parser.has_option('equipment', 'id'):
equipment_id = parser.get('equipment', 'id')
self._dispatcher = self._buildObject('Dispatcher', Dispatcher, dict(equipment_id=equipment_id))
self._processInterfaceOptions(interfaceopts)
self._processDeviceOptions(deviceopts)
def _processDeviceOptions(self, deviceopts):
# check devices opts by creating them
devs = []
for devname, devopts in deviceopts:
devclass = devopts.pop('class')
# create device
self.log.debug('Creating Device %r' % devname)
export = devopts.pop('export', '1')
export = export.lower() in ('1', 'on', 'true', 'yes')
if 'default' in devopts:
devopts['value'] = devopts.pop('default')
# strip '"
for k, v in devopts.items():
for d in ("'", '"'):
if v.startswith(d) and v.endswith(d):
devopts[k] = v[1:-1]
devobj = devclass(self.log.getChild(devname), devopts, devname,
self._dispatcher)
devs.append([devname, devobj, export])
# connect devices with dispatcher
for devname, devobj, export in devs:
self.log.info('registering device %r' % devname)
self._dispatcher.register_module(devobj, devname, export)
# also call init on the devices
devobj.init()
# call a possibly empty postinit on each device after registering all
for _devname, devobj, _export in devs:
postinit = getattr(devobj, 'postinit', None)
if postinit:
postinit()
def _processInterfaceOptions(self, interfaceopts):
# eval interfaces
self._interfaces = []
for ifname, ifopts in interfaceopts:
ifclass = ifopts.pop('interface')
ifclass = INTERFACES[ifclass]
interface = self._buildObject(ifname, ifclass,
ifopts, self._dispatcher)
self._interfaces.append(interface)
def _buildObject(self, name, cls, options, *args):
self.log.debug('Creating %s ...' % name)
# cls.__init__ should pop all used args from options!
obj = cls(self.log.getChild(name.lower()), options, *args)
if options:
raise ConfigError('%s: don\'t know how to handle option(s): %s' % (
cls.__name__,
', '.join(options.keys())))
return obj

249
secop/validators.py Normal file
View File

@ -0,0 +1,249 @@
# -*- 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 validators."""
# a Validator returns a validated object or raises an ValueError
# easy python validators: int(), float(), str()
# also validators should have a __repr__ returning a 'python' string
# which recreates them
# if a validator does a mapping, it normally maps to the external representation (used for print/log/protocol/...)
# to get the internal representation (for the code), call method convert
class ProgrammingError(Exception):
pass
class Validator(object):
# 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[0], self.__dict__[pn[0]])
for pn in self.params]
return ('%s(%s)' % (self.__class__.__name__, ', '.join(params)))
def __call__(self, value):
return self.check(self.valuetype(value))
def convert(self, value):
# transforms the 'internal' representation into the 'external'
return self.valuetype(value)
class floatrange(Validator):
params = [('lower', float), ('upper', float)]
def check(self, value):
if self.lower <= value <= self.upper:
return value
raise ValueError('Floatrange: value %r must be within %f and %f' %
(value, self.lower, self.upper))
class intrange(Validator):
params = [('lower', int), ('upper', int)]
valuetype = int
def check(self, value):
if self.lower <= value <= self.upper:
return value
raise ValueError('Intrange: value %r must be within %f and %f' %
(value, self.lower, self.upper))
class positive(Validator):
def check(self, value):
if value > 0:
return value
raise ValueError('Value %r must be > 0!' % value)
class nonnegative(Validator):
def check(self, value):
if value >= 0:
return value
raise ValueError('Value %r must be >= 0!' % value)
class array(Validator):
"""integral amount of data-elements which are described by the SAME validator
The size of the array can also be described by an validator
"""
valuetype = list
params = [('size', lambda x: x),
('datatype', lambda x: x)]
def check(self, values):
requested_size = len(values)
try:
allowed_size = self.size(requested_size)
except ValueError as e:
raise ValueError(
'illegal number of elements %d, need %r: (%s)' %
(requested_size, self.size, e))
if requested_size != allowed_size:
raise ValueError(
'need %d elements (got %d)' %
(allowed_size, requested_size))
# apply data-type validator to all elements and return
res = []
for idx, el in enumerate(values):
try:
res.append(self.datatype(el))
except ValueError as e:
raise ValueError(
'Array Element %s (=%r) not conforming to %r: (%s)' %
(idx, el, self.datatype, e))
return res
# more complicated validator may not be able to use validator base class
class vector(object):
"""fixed length, eache element has its own validator"""
def __init__(self, *args):
self.validators = args
self.argstr = ', '.join([repr(e) for e in args])
def __call__(self, args):
if len(args) != len(self.validators):
raise ValueError('Vector: need exactly %d elementes (got %d)' %
len(self.validators), len(args))
return tuple(v(e) for v, e in zip(self.validators, args))
def __repr__(self):
return ('%s(%s)' % (self.__class__.__name__, self.argstr))
class record(object):
"""fixed length, eache element has its own name and validator"""
def __init__(self, **kwds):
self.validators = args
self.argstr = ', '.join([repr(e) for e in kwds.items()])
def __call__(self, arg):
if len(args) != len(self.validators):
raise ValueError('Vector: need exactly %d elementes (got %d)' %
len(self.validators), len(args))
return tuple(v(e) for v, e in zip(self.validators, args))
def __repr__(self):
return ('%s(%s)' % (self.__class__.__name__, self.argstr))
class oneof(object):
"""needs to comply with one of the given validators/values"""
def __init__(self, *args):
self.oneof = args
self.argstr = ', '.join([repr(e) for e in args])
def __call__(self, arg):
for v in self.oneof:
if callable(v):
try:
if (v == int) and (float(arg) != int(arg)):
continue
return v(arg)
except ValueError:
pass # try next validator
elif v == arg:
return v
raise ValueError('Oneof: %r should be one of: %s' % (arg, self.argstr))
def __repr__(self):
return ('%s(%s)' % (self.__class__.__name__, self.argstr))
class enum(object):
def __init__(self, *args, **kwds):
self.mapping = {}
# use given kwds directly
self.mapping.update(kwds)
# enumerate args
i = -1
args = list(args)
while args:
i += 1
if i in self.mapping:
continue
self.mapping[args.pop(0)] = i
# generate reverse mapping too for use by protocol
self.revmapping = {}
for k, v in self.mapping.items():
self.revmapping[v] = k
def __call__(self, obj):
try:
obj = int(obj)
except ValueError:
pass
if obj in self.mapping:
return obj
if obj in self.revmapping:
return self.revmapping[obj]
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.items()]
return ('%s(%s)' % (self.__class__.__name__, ', '.join(params)))
def convert(self, arg):
return self.mapping.get(arg, arg)