Pep8 improvements + cleanup

Change-Id: I9052e703b58e93b639c027521b47f693ae853f6e
This commit is contained in:
Enrico Faulhaber 2016-12-15 14:36:12 +01:00
parent 7320ac1538
commit 78bb3b5f96
20 changed files with 395 additions and 250 deletions

View File

@ -35,11 +35,11 @@ log_path = path.join(basepath, 'log')
# sys.path[0] = path.join(basepath, 'src')
sys.path[0] = basepath
# do not move above!
import loggers
from secop.client import ClientConsole
def parseArgv(argv):
parser = argparse.ArgumentParser(description="Connect to a SECoP server")
loggroup = parser.add_mutually_exclusive_group()
@ -64,7 +64,6 @@ def main(argv=None):
loglevel = 'debug' if args.verbose else ('error' if args.quiet else 'info')
loggers.initLogging('console', loglevel, log_path)
console = ClientConsole(args.name, basepath)
try:

View File

@ -40,7 +40,6 @@ from secop import loggers
from secop.server import Server
def parseArgv(argv):
parser = argparse.ArgumentParser(description="Manage a SECoP server")
loggroup = parser.add_mutually_exclusive_group()

View File

@ -23,7 +23,8 @@ In replies the SEC-node (in the playground) will always use the correct paramete
On change-requests the parameter is assumed to be 'target', on trigger-requests it is assumed to be 'value'.
Clients should not rely on this and explicitly state the parametername!
All keywords are defined to be identifiers in the sense, that they are not longer than 63 characters and consist only of letters, digits and underscore and do not start with a digit. (i.e. T_9 is ok, whereas t{9} is not!)
All names and keywords are defined to be identifiers in the sense, that they are not longer than 63 characters and consist only of letters, digits and underscore and do not start with a digit. (i.e. T_9 is ok, whereas t{9} is not!)
No rule is without exception, there is exactly ONE special case: the identify request consists of the literal string '*IDN?\n' and its answer is formatted like an valid SCPI response for *IDN?.
We rely on the underlying transport to not split messages, i.e. all messages are transported as a whole and no message interrupts another.
@ -58,7 +59,7 @@ Identify
--------
* Request: type A: '*IDN?'
* Reply: special: 'Sine2020WP7.1&ISSE, SECoP, V2016-11-30, rc1'
* Reply: special: 'SECoP, SECoPTCP, V2016-11-30, rc1'
* queries if SECoP protocol is supported and which version it is
Format is intentionally choosen to be compatible to SCPI (for this query only).
It is NOT intended to transport information about the manufacturer of the hardware, but to identify this as a SECoP device and transfer the protocol version!

View File

@ -100,6 +100,7 @@ def determineServerStatus(name):
else:
print('%s: dead' % name)
def main(argv=None):
if argv is None:
argv = sys.argv

View File

@ -36,9 +36,9 @@ from secop.protocol.framing import FRAMERS
from secop.protocol.messages import *
class TCPConnection(object):
# disguise a TCP connection as serial one
def __init__(self, host, port):
self._host = host
self._port = int(port)
@ -73,15 +73,17 @@ class TCPConnection(object):
while '\n' in data:
line, data = data.split('\n', 1)
try:
self._readbuffer.put(line.strip('\r'), block=True, timeout=1)
self._readbuffer.put(
line.strip('\r'), block=True, timeout=1)
except Queue.Full:
self.log.debug('rcv queue full! dropping line: %r' % line)
self.log.debug(
'rcv queue full! dropping line: %r' % line)
finally:
self._thread = None
def readline(self, block=False):
"""blocks until a full line was read and returns it"""
i = 10;
i = 10
while i:
try:
return self._readbuffer.get(block=True, timeout=1)
@ -160,7 +162,8 @@ class Client(object):
self.single_shots = dict()
# mapping the modulename to a dict mapping the parameter names to their values
# note: the module value is stored as the value of the parameter value of the module
# note: the module value is stored as the value of the parameter value
# of the module
self.cache = dict()
self._syncLock = threading.RLock()
@ -202,7 +205,6 @@ class Client(object):
else:
self.log.error('ignoring unexpected reply %r' % line)
def _encode_message(self, requesttype, spec='', data=Ellipsis):
"""encodes the given message to a string
"""
@ -250,21 +252,28 @@ class Client(object):
try:
mkthread(func, data)
except Exception as err:
self.log.exception('Exception in Single-shot Callback!', err)
self.log.exception(
'Exception in Single-shot Callback!', err)
run.add(func)
self.single_shots[spec].difference_update(run)
def register_callback(self, module, parameter, cb):
self.log.debug('registering callback %r for %s:%s' % (cb, module, parameter))
self.log.debug(
'registering callback %r for %s:%s' %
(cb, module, parameter))
self.callbacks.setdefault('%s:%s' % (module, parameter), set()).add(cb)
def unregister_callback(self, module, parameter, cb):
self.log.debug('unregistering callback %r for %s:%s' % (cb, module, parameter))
self.callbacks.setdefault('%s:%s' % (module, parameter), set()).discard(cb)
self.log.debug(
'unregistering callback %r for %s:%s' %
(cb, module, parameter))
self.callbacks.setdefault('%s:%s' %
(module, parameter), set()).discard(cb)
def communicate(self, msgtype, spec='', data=Ellipsis):
# maps each (sync) request to the corresponding reply
# XXX: should go to the encoder! and be imported here (or make a translating method)
# XXX: should go to the encoder! and be imported here (or make a
# translating method)
REPLYMAP = {
"describe": "describing",
"do": "done",
@ -282,20 +291,25 @@ class Client(object):
spec = spec + ':value'
event = threading.Event()
result = ['polled', spec]
self.single_shots.setdefault(spec, set()).add(lambda d: (result.append(d), event.set()))
self.connection.writeline(self._encode_message(msgtype, spec, data))
self.single_shots.setdefault(spec, set()).add(
lambda d: (result.append(d), event.set()))
self.connection.writeline(
self._encode_message(
msgtype, spec, data))
if event.wait(10):
return tuple(result)
raise RuntimeError("timeout upon waiting for reply!")
rply = REPLYMAP[msgtype]
if rply in self.expected_replies:
raise RuntimeError("can not have more than one requests of the same type at the same time!")
raise RuntimeError(
"can not have more than one requests of the same type at the same time!")
event = threading.Event()
self.expected_replies[rply] = [event]
self.connection.writeline(self._encode_message(msgtype, spec, data))
if event.wait(10): # wait 10s for reply
result = rply, self.expected_replies[rply][1], self.expected_replies[rply][2]
result = rply, self.expected_replies[rply][
1], self.expected_replies[rply][2]
del self.expected_replies[rply]
return result
del self.expected_replies[rply]
@ -316,9 +330,9 @@ class Client(object):
self._cache.getdefault(device, {})[param] = value
# XXX: further notification-callbacks needed ???
def startup(self, async=False):
_, self.equipment_id, self.describing_data = self.communicate('describe')
_, self.equipment_id, self.describing_data = self.communicate(
'describe')
# always fill our cache
self.communicate('activate')
# deactivate updates if not wanted
@ -343,8 +357,8 @@ class Client(object):
return self.describing_data['modules'][module]['commands'].keys()
def getProperties(self, module, parameter):
return self.describing_data['modules'][module]['parameters'][parameter].items()
return self.describing_data['modules'][
module]['parameters'][parameter].items()
def syncCommunicate(self, msg):
return self.communicate(msg)

View File

@ -102,6 +102,8 @@ class CMD(object):
# Meta class
# warning: MAGIC!
class DeviceMeta(type):
def __new__(mcs, name, bases, attrs):
@ -183,7 +185,8 @@ class DeviceMeta(type):
argspec = inspect.getargspec(value)
if argspec[0] and argspec[0][0] == 'self':
del argspec[0][0]
newtype.CMDS[name[2:]] = CMD(getattr(value, '__doc__'),
newtype.CMDS[name[2:]] = CMD(
getattr(value, '__doc__'),
argspec.args, None) # XXX: find resulttype!
attrs['__constructed__'] = True
return newtype
@ -313,6 +316,7 @@ class Readable(Device):
if rfunc:
rfunc()
class Driveable(Readable):
"""Basic Driveable device
@ -324,5 +328,6 @@ class Driveable(Readable):
'target': PARAM('target value of the device', default=0.,
readonly=False),
}
def doStop(self):
time.sleep(1) # for testing !

View File

@ -37,10 +37,14 @@ class Switch(Driveable):
'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),
validator=enum(on=1, off=0),
default=0, readonly=False),
'switch_on_time': PARAM('seconds to wait after activating the switch',
validator=floatrange(0, 60), unit='s',
default=10, export=False),
'switch_off_time': PARAM('cool-down time in seconds',
validator=floatrange(0, 60), unit='s',
default=10, export=False),
}
def init(self):
@ -177,8 +181,8 @@ class SampleTemp(Driveable):
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),
'ramp': PARAM('moving speed in K/min', validator=floatrange(0, 100),
unit='K/min', default=0.1, readonly=False),
}
def init(self):

View File

@ -24,6 +24,7 @@
import threading
class attrdict(dict):
"""a normal dict, providing access also via attributes"""

View File

@ -26,7 +26,12 @@ import re
import time
from datetime import tzinfo, timedelta, datetime
# format_time and parse_time could be simplified with external dateutil lib
# http://stackoverflow.com/a/15228038
# based on http://stackoverflow.com/a/39418771
class LocalTimezone(tzinfo):
ZERO = timedelta(0)
STDOFFSET = timedelta(seconds=-time.timezone)
@ -62,6 +67,7 @@ class LocalTimezone(tzinfo):
LocalTimezone = LocalTimezone()
def format_time(timestamp=None):
# get time in UTC
if timestamp is None:
@ -70,15 +76,22 @@ def format_time(timestamp=None):
d = datetime.fromtimestamp(timestamp, LocalTimezone)
return d.isoformat("T")
# Solution based on
# https://bugs.python.org/review/15873/diff/16581/Lib/datetime.py#newcode1418Lib/datetime.py:1418
class Timezone(tzinfo):
def __init__(self, offset, name='unknown timezone'):
self.offset = offset
self.name = name
def tzname(self, dt):
return self.name
def utcoffset(self, dt):
return self.offset
def dst(self, dt):
return timedelta(0)
datetime_re = re.compile(
@ -88,6 +101,7 @@ datetime_re = re.compile(
r'(?P<tzinfo>Z|[+-]\d{2}(?::?\d{2})?)?$'
)
def _parse_isostring(isostring):
"""Parses a string and return a datetime.datetime.
This function supports time zone offsets. When the input contains one,
@ -111,7 +125,10 @@ def _parse_isostring(isostring):
kw = {k: int(v) for k, v in kw.items() if v is not None}
kw['tzinfo'] = _tzinfo
return datetime(**kw)
raise ValueError("%s is not a valid ISO8601 string I can parse!" % isostring)
raise ValueError(
"%s is not a valid ISO8601 string I can parse!" %
isostring)
def parse_time(isostring):
try:

View File

@ -36,10 +36,6 @@ Interface to the modules:
- 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
@ -50,20 +46,21 @@ 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 = {}
self._modules = {}
# list of EXPORTED modules
self._dispatcher_export = []
self._export = []
# list all connections
self._dispatcher_connections = []
self._connections = []
# active (i.e. broadcast-receiving) connections
self._dispatcher_active_connections = set()
self._active_connections = set()
# map eventname -> list of subscribed connections
self._dispatcher_subscriptions = {}
self._dispatcher_lock = threading.RLock()
self._subscriptions = {}
self._lock = threading.RLock()
def handle_request(self, conn, msg):
"""handles incoming request
@ -72,7 +69,7 @@ class Dispatcher(object):
"""
self.log.debug('Dispatcher: handling msg: %r' % msg)
# play thread safe !
with self._dispatcher_lock:
with self._lock:
reply = None
# generate reply (coded and framed)
msgname = msg.__class__.__name__
@ -106,118 +103,123 @@ class Dispatcher(object):
def broadcast_event(self, msg, reallyall=False):
"""broadcasts a msg to all active connections"""
if reallyall:
listeners = self._dispatcher_connections
listeners = self._connections
else:
if getattr(msg, 'command', None) is None:
eventname = '%s:%s' % (msg.module, msg.parameter if msg.parameter else 'value')
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)
listeners = self._subscriptions.get(eventname, [])
listeners += list(self._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)
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)
self._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)
if eventname in self._subscriptions:
self._subscriptions.remove(conn)
def add_connection(self, conn):
"""registers new connection"""
self._dispatcher_connections.append(conn)
self._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():
if conn in self._connections:
self._connections.remove(conn)
for _evt, conns in self._subscriptions.items():
conns.discard(conn)
def activate_connection(self, conn):
self._dispatcher_active_connections.add(conn)
self._active_connections.add(conn)
def deactivate_connection(self, conn):
self._dispatcher_active_connections.discard(conn)
self._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
self._modules[modulename] = moduleobj
if export:
self._dispatcher_export.append(modulename)
self._export.append(modulename)
def get_module(self, modulename):
module = self._dispatcher_modules.get(modulename, modulename)
module = self._modules.get(modulename, modulename)
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
if modulename in self._export:
self._export.remove(modulename)
self._modules.pop(modulename)
# XXX: also clean _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 get_descriptive_data(self):
# XXX: be lazy and cache this?
result = {}
for modulename in self._dispatcher_export:
module = self.get_module(modulename)
dd = {'class' : module.__class__.__name__,
'bases' : [b.__name__ for b in module.__class__.__bases__],
'parameters': dict((pn,po.as_dict()) for pn,po in module.PARAMS.items()),
'commands': dict((cn,co.as_dict()) for cn,co in module.CMDS.items()),
'baseclass' : 'Readable',
}
result.setdefault('modules', {})[modulename] = dd
result['equipment_id'] = self.equipment_id
# XXX: what else?
return result
return self._export[:]
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!
if modulename in self._export:
# omit export=False params!
res = {}
for paramname, param in self.get_module(modulename).PARAMS.items():
if param.export == True:
res[paramname] = param
if param.export:
res[paramname] = param.as_dict()
self.log.debug('list params for module %s -> %r' %
(modulename, res))
return res
self.log.debug('-> module is not to be exported!')
return {}
def list_module_cmds(self, modulename):
self.log.debug('list_module_cmds(%r)' % modulename)
if modulename in self._export:
# omit export=False params!
res = {}
for cmdname, cmdobj in self.get_module(modulename).CMDS.items():
res[cmdname] = cmdobj.as_dict()
self.log.debug('list cmds for module %s -> %r' %
(modulename, res))
return res
self.log.debug('-> module is not to be exported!')
return {}
def get_descriptive_data(self):
# XXX: be lazy and cache this?
result = {'modules':{}}
for modulename in self._export:
module = self.get_module(modulename)
# some of these need rework !
dd = {'class': module.__class__.__name__,
'bases': [b.__name__ for b in module.__class__.__bases__],
'parameters': self.list_module_params(modulename),
'commands': self.list_module_cmds(modulename),
'baseclass': 'Readable',
}
result['modules'][modulename] = dd
result['equipment_id'] = self.equipment_id
result['firmware'] = 'The SECoP playground'
result['version'] = "2016.12"
# XXX: what else?
return result
def _execute_command(self, modulename, command, arguments=None):
if arguments is None:
arguments = []
@ -230,13 +232,22 @@ class Dispatcher(object):
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!')
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 = func(*arguments)
res = CommandReply(module=modulename, command=command, result=[res, dict(t=time.time())])
res = CommandReply(
module=modulename,
command=command,
result=[
res,
dict(
t=time.time())])
# res = Value(modulename, command=command, value=func(*arguments), t=time.time())
return res
@ -258,8 +269,16 @@ class Dispatcher(object):
else:
setattr(moduleobj, pname, value)
if pobj.timestamp:
return WriteReply(module=modulename, parameter=pname, value=[pobj.value, dict(t=pobj.timestamp)])
return WriteReply(module=modulename, parameter=pname, value=[pobj.value, {}])
return WriteReply(
module=modulename, parameter=pname, value=[
pobj.value, dict(
t=pobj.timestamp)])
return WriteReply(
module=modulename,
parameter=pname,
value=[
pobj.value,
{}])
def _getParamValue(self, modulename, pname):
moduleobj = self.get_module(modulename)
@ -276,10 +295,13 @@ class Dispatcher(object):
# 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,
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()
@ -289,13 +311,15 @@ class Dispatcher(object):
def handle_Describe(self, conn, msg):
# XXX:collect descriptive data
return DescribeReply(equipment_id = self.equipment_id, description = self.get_descriptive_data())
return DescribeReply(
equipment_id=self.equipment_id,
description=self.get_descriptive_data())
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:
if conn in self._active_connections:
return None # already send to myself
return res # send reply to inactive conns
@ -303,7 +327,8 @@ class Dispatcher(object):
# 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
# 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:
@ -313,7 +338,7 @@ class Dispatcher(object):
res = self._setParamValue(msg.module, 'target', msg.value)
res.parameter = 'target'
# self.broadcast_event(res)
if conn in self._dispatcher_active_connections:
if conn in self._active_connections:
return None # already send to myself
return res # send reply to inactive conns
@ -321,12 +346,13 @@ class Dispatcher(object):
# 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
# 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:
# if conn in self._active_connections:
# return None # already send to myself
return res # send reply to inactive conns
@ -336,7 +362,7 @@ class Dispatcher(object):
def handle_Activate(self, conn, msg):
self.activate_connection(conn)
# easy approach: poll all values...
for modulename, moduleobj in self._dispatcher_modules.items():
for modulename, moduleobj in self._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)
@ -370,5 +396,3 @@ class Dispatcher(object):
self.log.error('IGN: got unhandled request %s' % msg)
return ErrorMessage(errorclass="InternalError",
errorstring='Got Unhandled Request %r' % msg)

View File

@ -179,8 +179,13 @@ class DemoEncoder(MessageEncoder):
return '\n'.join(result)
if isinstance(msg, ErrorMessage):
return ('%s %s' % (devspec(msg, 'error %s' %
msg.errortype), msg.errorstring)).strip()
return (
'%s %s' %
(devspec(
msg,
'error %s' %
msg.errortype),
msg.errorstring)).strip()
return 'Can not handle object %r!' % msg
@ -292,7 +297,11 @@ class DemoEncoder(MessageEncoder):
# construct messageobj
if msgtype in MESSAGE:
return MESSAGE[msgtype](
devs=devs, pars=pars, props=props, result=result, **mgroups)
devs=devs,
pars=pars,
props=props,
result=result,
**mgroups)
return ErrorMessage(errortype="SyntaxError",
errorstring="Can't handle %r" % encoded)

View File

@ -34,16 +34,19 @@ import ast
import re
import json
# each message is like <messagetype> [ \space <messageargs> [ \space <json> ]] \lf
# 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)
r"""^(?P<msgtype>[\*\?\w]+)(?:\s(?P<spec>[\w:<>]+)(?:\s(?P<json>.*))?)?$""",
re.X)
#"""
# messagetypes:
IDENTREQUEST = '*IDN?' # literal
IDENTREPLY = 'SECoP, SECoPTCP, V2016-11-30, rc1' # literal! first part 'SECoP' is fixed!
# literal! first part 'SECoP' is fixed!
IDENTREPLY = 'SECoP, SECoPTCP, V2016-11-30, rc1'
DESCRIPTIONSREQUEST = 'describe' # literal
DESCRIPTIONREPLY = 'describing' # +<id> +json
ENABLEEVENTSREQUEST = 'activate' # literal
@ -51,10 +54,14 @@ ENABLEEVENTSREPLY = 'active' # literal, is end-of-initial-data-transfer
DISABLEEVENTSREQUEST = 'deactivate' # literal
DISABLEEVENTSREPLY = 'inactive' # literal
COMMANDREQUEST = 'do' # +module:command +json args (if needed)
COMMANDREPLY = 'done' # +module:command +json args (if needed) # send after the command finished !
WRITEREQUEST = 'change' # +module[:parameter] +json_value -> NO direct reply, calls TRIGGER internally!
WRITEREPLY = 'changed' # +module[:parameter] +json_value # send with the read back value
TRIGGERREQUEST = 'poll' # +module[:parameter] -> NO direct reply, calls TRIGGER internally!
# +module:command +json args (if needed) # send after the command finished !
COMMANDREPLY = 'done'
# +module[:parameter] +json_value -> NO direct reply, calls TRIGGER internally!
WRITEREQUEST = 'change'
# +module[:parameter] +json_value # send with the read back value
WRITEREPLY = 'changed'
# +module[:parameter] -> NO direct reply, calls TRIGGER internally!
TRIGGERREQUEST = 'poll'
EVENT = 'event' # +module[:parameter] +json_value (value, qualifiers_as_dict)
HEARTBEATREQUEST = 'ping' # +nonce_without_space
HEARTBEATREPLY = 'pong' # +nonce_without_space
@ -65,7 +72,9 @@ 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
# note: above strings need to be unique in the sense, that none is/or
# starts with another
def encode_value_data(vobj):
q = vobj.qualifiers.copy()
@ -73,6 +82,7 @@ def encode_value_data(vobj):
q['t'] = format_time(q['t'])
return vobj.value, q
class DemoEncoder(MessageEncoder):
# map of msg to msgtype string as defined above.
ENCODEMAP = {
@ -99,7 +109,8 @@ class DemoEncoder(MessageEncoder):
}
DECODEMAP = {
IDENTREQUEST: lambda spec, data: IdentifyRequest(),
IDENTREPLY : lambda spec, data: IdentifyReply(encoded), # handled specially, listed here for completeness
# handled specially, listed here for completeness
IDENTREPLY: lambda spec, data: IdentifyReply(encoded),
DESCRIPTIONSREQUEST: lambda spec, data: DescribeRequest(),
DESCRIPTIONREPLY: lambda spec, data: DescribeReply(equipment_id=spec[0], description=data),
ENABLEEVENTSREQUEST: lambda spec, data: ActivateRequest(),
@ -139,18 +150,19 @@ class DemoEncoder(MessageEncoder):
""" % (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]))
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:]]
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)
@ -181,16 +193,28 @@ class DemoEncoder(MessageEncoder):
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,
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'))
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
@ -205,4 +229,3 @@ class DemoEncoder(MessageEncoder):
print self.encode(d)
print
print "---- Testing done -----"

View File

@ -35,7 +35,6 @@ except ImportError:
import pickle
class PickleEncoder(MessageEncoder):
def encode(self, messageobj):

View File

@ -24,6 +24,7 @@
class SECOPError(RuntimeError):
def __init__(self, *args, **kwds):
self.args = args
for k, v in kwds.items():

View File

@ -80,5 +80,3 @@ class DemoFramer(Framer):
def reset(self):
self.data = b''
self.decoded = []

View File

@ -70,5 +70,3 @@ class RLEFramer(Framer):
def reset(self):
self.data = b''
self.frames_to_go = 0

View File

@ -34,6 +34,7 @@ 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):
@ -129,7 +130,8 @@ class TCPServer(SocketServer.ThreadingTCPServer):
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__)
self.log.debug("TCPServer using encoding=%s" %
self.encodingCLS.__name__)
SocketServer.ThreadingTCPServer.__init__(self, (bindto, portnum),
TCPRequestHandler,
bind_and_activate=True)

View File

@ -50,7 +50,8 @@ class Message(object):
class Value(object):
def __init__(self, module, parameter=None, command=None, value=Ellipsis, **qualifiers):
def __init__(self, module, parameter=None, command=None, value=Ellipsis,
**qualifiers):
self.module = module
self.parameter = parameter
self.command = command
@ -65,71 +66,87 @@ class Value(object):
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()]))
[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 = ''
result = 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
@ -138,6 +155,7 @@ class EventMessage(Message):
command = None
value = None # Value object ! (includes qualifiers!)
class ErrorMessage(Message):
is_error = True
errorclass = 'InternalError'
@ -149,7 +167,6 @@ class HelpMessage(Message):
is_request = True
if __name__ == '__main__':
print("Minimal testing of messages....")
m = Message(MSGTYPE='test', a=1, b=2, c='x')

View File

@ -110,8 +110,10 @@ class Value(object):
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()]))
return '%s:Value(%s)' % (
devspec, ', '.join(
[repr(self.value)] +
['%s=%r' % (k, v) for k, v in self.qualifiers.items()]))
class ListMessage(Message):
@ -165,32 +167,59 @@ class HelpMessage(Message):
class NoSuchDeviceError(ErrorMessage):
def __init__(self, *devs):
ErrorMessage.__init__(self, devs=devs, errorstring="Device %r does not exist" % devs[0], errortype='NoSuchDevice')
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')
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')
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')
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)
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])
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....")

View File

@ -85,7 +85,9 @@ class Server(object):
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)))
self.log.debug(
'thread %r died (%d still running)' %
(t, len(self._threads)))
t.join()
self._threads.discard(t)
@ -132,7 +134,9 @@ class Server(object):
if parser.has_option('equipment', 'id'):
equipment_id = parser.get('equipment', 'id')
self._dispatcher = self._buildObject('Dispatcher', Dispatcher, dict(equipment_id=equipment_id))
self._dispatcher = self._buildObject(
'Dispatcher', Dispatcher, dict(
equipment_id=equipment_id))
self._processInterfaceOptions(interfaceopts)
self._processDeviceOptions(deviceopts)