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:
0
secop/__init__.py
Normal file
0
secop/__init__.py
Normal file
192
secop/client/__init__.py
Normal file
192
secop/client/__init__.py
Normal 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)
|
0
secop/devices/__init__.py
Normal file
0
secop/devices/__init__.py
Normal file
312
secop/devices/core.py
Normal file
312
secop/devices/core.py
Normal 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
327
secop/devices/cryo.py
Normal 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
265
secop/devices/demo.py
Normal 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
51
secop/devices/epics.py
Normal 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
74
secop/devices/test.py
Normal 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
35
secop/errors.py
Normal 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
61
secop/lib/__init__.py
Normal 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
309
secop/lib/parsing.py
Normal 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
55
secop/lib/pidfile.py
Normal 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
350
secop/loggers/__init__.py
Normal 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
67
secop/loggers/colors.py
Normal 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
33
secop/paths.py
Normal file
@ -0,0 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
"""Pathes. how to find what and where..."""
|
||||
|
||||
import sys
|
||||
from os import path
|
||||
|
||||
|
||||
basepath = path.abspath(path.join(sys.path[0], '..'))
|
||||
etc_path = path.join(basepath, 'etc')
|
||||
pid_path = path.join(basepath, 'pid')
|
||||
log_path = path.join(basepath, 'log')
|
||||
sys.path[0] = path.join(basepath, 'src')
|
22
secop/protocol/__init__.py
Normal file
22
secop/protocol/__init__.py
Normal 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
76
secop/protocol/device.py
Normal 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')
|
359
secop/protocol/dispatcher.py
Normal file
359
secop/protocol/dispatcher.py
Normal 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)
|
||||
|
||||
|
59
secop/protocol/encoding/__init__.py
Normal file
59
secop/protocol/encoding/__init__.py
Normal 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']
|
102
secop/protocol/encoding/demo_v2.py
Normal file
102
secop/protocol/encoding/demo_v2.py
Normal 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']
|
391
secop/protocol/encoding/demo_v3.py
Normal file
391
secop/protocol/encoding/demo_v3.py
Normal 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)
|
209
secop/protocol/encoding/demo_v4.py
Normal file
209
secop/protocol/encoding/demo_v4.py
Normal 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 -----"
|
||||
|
||||
|
||||
|
47
secop/protocol/encoding/pickle.py
Normal file
47
secop/protocol/encoding/pickle.py
Normal 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)
|
209
secop/protocol/encoding/simplecomm.py
Normal file
209
secop/protocol/encoding/simplecomm.py
Normal 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)
|
65
secop/protocol/encoding/text.py
Normal file
65
secop/protocol/encoding/text.py
Normal 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
70
secop/protocol/errors.py
Normal 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
|
62
secop/protocol/framing/__init__.py
Normal file
62
secop/protocol/framing/__init__.py
Normal 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']
|
84
secop/protocol/framing/demo.py
Normal file
84
secop/protocol/framing/demo.py
Normal 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 = []
|
||||
|
||||
|
57
secop/protocol/framing/eol.py
Normal file
57
secop/protocol/framing/eol.py
Normal 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''
|
40
secop/protocol/framing/null.py
Normal file
40
secop/protocol/framing/null.py
Normal 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]
|
74
secop/protocol/framing/rle.py
Normal file
74
secop/protocol/framing/rle.py
Normal 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
|
||||
|
||||
|
31
secop/protocol/interface/__init__.py
Normal file
31
secop/protocol/interface/__init__.py
Normal file
@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
||||
#
|
||||
# *****************************************************************************
|
||||
"""provide server interfaces to be used by clients"""
|
||||
|
||||
from tcp import TCPServer
|
||||
|
||||
INTERFACES = {
|
||||
'tcp': TCPServer,
|
||||
}
|
||||
|
||||
# for 'from protocol.interface import *' to only import the dict
|
||||
__ALL__ = ['INTERFACES']
|
136
secop/protocol/interface/tcp.py
Normal file
136
secop/protocol/interface/tcp.py
Normal 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")
|
27
secop/protocol/interface/zmq.py
Normal file
27
secop/protocol/interface/zmq.py
Normal file
@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
||||
#
|
||||
# *****************************************************************************
|
||||
"""provide a zmq server"""
|
||||
|
||||
# tbd.
|
||||
|
||||
# use zmq frames??
|
||||
# handle async and sync with different zmq ports?
|
160
secop/protocol/messages.py
Normal file
160
secop/protocol/messages.py
Normal 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
|
202
secop/protocol/messages_old.py
Normal file
202
secop/protocol/messages_old.py
Normal 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
37
secop/protocol/status.py
Normal 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
189
secop/server.py
Normal 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
249
secop/validators.py
Normal 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)
|
Reference in New Issue
Block a user