remove obsolete code
- basic_validators is not needed any more since the implementation of datatypes.Stub - client/baseclient.y is replaced by client/__init__.py both for the gui client and NICOS SECoP client - lib/parsing.py used by baseclient only Change-Id: I15b6ac880017000e155b8f6b7e2456e1bbf56dab Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/25058 Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de> Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de> Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
parent
6a32ecf342
commit
899a07aec8
@ -1,76 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# pylint: disable=invalid-name
|
|
||||||
# -*- 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>
|
|
||||||
#
|
|
||||||
# *****************************************************************************
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import argparse
|
|
||||||
from os import path
|
|
||||||
|
|
||||||
# Path magic to make python find our stuff.
|
|
||||||
# also remember our basepath (for etc, pid lookup, etc)
|
|
||||||
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')
|
|
||||||
sys.path[0] = basepath
|
|
||||||
|
|
||||||
# do not move above!
|
|
||||||
import mlzlog
|
|
||||||
from secop.client.console import ClientConsole
|
|
||||||
|
|
||||||
|
|
||||||
def parseArgv(argv):
|
|
||||||
parser = argparse.ArgumentParser(description="Connect to a SECoP server")
|
|
||||||
loggroup = parser.add_mutually_exclusive_group()
|
|
||||||
loggroup.add_argument("-v", "--verbose",
|
|
||||||
help="Output lots of diagnostic information",
|
|
||||||
action='store_true', default=False)
|
|
||||||
loggroup.add_argument("-q", "--quiet", help="suppress non-error messages",
|
|
||||||
action='store_true', default=False)
|
|
||||||
parser.add_argument("name",
|
|
||||||
type=str,
|
|
||||||
help="Name of the instance.\n"
|
|
||||||
" Uses etc/name.cfg for configuration\n",)
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
|
||||||
if argv is None:
|
|
||||||
argv = sys.argv
|
|
||||||
|
|
||||||
args = parseArgv(argv[1:])
|
|
||||||
|
|
||||||
loglevel = 'debug' if args.verbose else ('error' if args.quiet else 'info')
|
|
||||||
mlzlog.initLogging('console', loglevel, log_path)
|
|
||||||
|
|
||||||
console = ClientConsole(args.name, basepath)
|
|
||||||
|
|
||||||
try:
|
|
||||||
console.run()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
console.close()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.exit(main(sys.argv))
|
|
@ -1,149 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# *****************************************************************************
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
|
||||||
# the terms of the GNU General Public License as published by the Free Software
|
|
||||||
# Foundation; either version 2 of the License, or (at your option) any later
|
|
||||||
# version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
||||||
# details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License along with
|
|
||||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
|
||||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
||||||
#
|
|
||||||
# Module authors:
|
|
||||||
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
|
||||||
#
|
|
||||||
# *****************************************************************************
|
|
||||||
"""basic validators (for properties)"""
|
|
||||||
|
|
||||||
# TODO: remove, as not used anymore
|
|
||||||
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
from secop.errors import ProgrammingError
|
|
||||||
|
|
||||||
|
|
||||||
def FloatProperty(value):
|
|
||||||
return float(value)
|
|
||||||
|
|
||||||
|
|
||||||
def PositiveFloatProperty(value):
|
|
||||||
value = float(value)
|
|
||||||
if value > 0:
|
|
||||||
return value
|
|
||||||
raise ValueError('Value must be >0 !')
|
|
||||||
|
|
||||||
|
|
||||||
def NonNegativeFloatProperty(value):
|
|
||||||
value = float(value)
|
|
||||||
if value >= 0:
|
|
||||||
return value
|
|
||||||
raise ValueError('Value must be >=0 !')
|
|
||||||
|
|
||||||
|
|
||||||
def IntProperty(value):
|
|
||||||
if int(value) == float(value):
|
|
||||||
return int(value)
|
|
||||||
raise ValueError('Can\'t convert %r to int!' % value)
|
|
||||||
|
|
||||||
|
|
||||||
def PositiveIntProperty(value):
|
|
||||||
value = IntProperty(value)
|
|
||||||
if value > 0:
|
|
||||||
return value
|
|
||||||
raise ValueError('Value must be >0 !')
|
|
||||||
|
|
||||||
|
|
||||||
def NonNegativeIntProperty(value):
|
|
||||||
value = IntProperty(value)
|
|
||||||
if value >= 0:
|
|
||||||
return value
|
|
||||||
raise ValueError('Value must be >=0 !')
|
|
||||||
|
|
||||||
|
|
||||||
def BoolProperty(value):
|
|
||||||
try:
|
|
||||||
if value.lower() in ['0', 'false', 'no', 'off',]:
|
|
||||||
return False
|
|
||||||
if value.lower() in ['1', 'true', 'yes', 'on', ]:
|
|
||||||
return True
|
|
||||||
except AttributeError: # was no string
|
|
||||||
if bool(value) == value:
|
|
||||||
return value
|
|
||||||
raise ValueError('%r is no valid boolean: try one of True, False, "on", "off",...' % value)
|
|
||||||
|
|
||||||
|
|
||||||
def StringProperty(value):
|
|
||||||
return str(value)
|
|
||||||
|
|
||||||
|
|
||||||
def UnitProperty(value):
|
|
||||||
# probably too simple!
|
|
||||||
for s in str(value):
|
|
||||||
if s.lower() not in '°abcdefghijklmnopqrstuvwxyz':
|
|
||||||
raise ValueError('%r is not a valid unit!')
|
|
||||||
|
|
||||||
|
|
||||||
def FmtStrProperty(value, regexp=re.compile(r'^%\.?\d+[efg]$')):
|
|
||||||
value=str(value)
|
|
||||||
if regexp.match(value):
|
|
||||||
return value
|
|
||||||
raise ValueError('%r is not a valid fmtstr!' % value)
|
|
||||||
|
|
||||||
|
|
||||||
def OneOfProperty(*args):
|
|
||||||
# literally oneof!
|
|
||||||
if not args:
|
|
||||||
raise ProgrammingError('OneOfProperty needs some argumets to check against!')
|
|
||||||
def OneOfChecker(value):
|
|
||||||
if value not in args:
|
|
||||||
raise ValueError('Value must be one of %r' % list(args))
|
|
||||||
return value
|
|
||||||
return OneOfChecker
|
|
||||||
|
|
||||||
|
|
||||||
def NoneOr(checker):
|
|
||||||
if not callable(checker):
|
|
||||||
raise ProgrammingError('NoneOr needs a basic validator as Argument!')
|
|
||||||
def NoneOrChecker(value):
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
return checker(value)
|
|
||||||
return NoneOrChecker
|
|
||||||
|
|
||||||
|
|
||||||
def EnumProperty(**kwds):
|
|
||||||
if not kwds:
|
|
||||||
raise ProgrammingError('EnumProperty needs a mapping!')
|
|
||||||
def EnumChecker(value):
|
|
||||||
if value in kwds:
|
|
||||||
return kwds[value]
|
|
||||||
if value in kwds.values():
|
|
||||||
return value
|
|
||||||
raise ValueError('Value must be one of %r' % list(kwds))
|
|
||||||
return EnumChecker
|
|
||||||
|
|
||||||
def TupleProperty(*checkers):
|
|
||||||
if not checkers:
|
|
||||||
checkers = [None]
|
|
||||||
for c in checkers:
|
|
||||||
if not callable(c):
|
|
||||||
raise ProgrammingError('TupleProperty needs basic validators as Arguments!')
|
|
||||||
def TupleChecker(values):
|
|
||||||
if len(values)==len(checkers):
|
|
||||||
return tuple(c(v) for c, v in zip(checkers, values))
|
|
||||||
raise ValueError('Value needs %d elements!' % len(checkers))
|
|
||||||
return TupleChecker
|
|
||||||
|
|
||||||
def ListOfProperty(checker):
|
|
||||||
if not callable(checker):
|
|
||||||
raise ProgrammingError('ListOfProperty needs a basic validator as Argument!')
|
|
||||||
def ListOfChecker(values):
|
|
||||||
return [checker(v) for v in values]
|
|
||||||
return ListOfChecker
|
|
@ -1,587 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# *****************************************************************************
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
|
||||||
# the terms of the GNU General Public License as published by the Free Software
|
|
||||||
# Foundation; either version 2 of the License, or (at your option) any later
|
|
||||||
# version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
||||||
# details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License along with
|
|
||||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
|
||||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
||||||
#
|
|
||||||
# Module authors:
|
|
||||||
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
|
||||||
#
|
|
||||||
# *****************************************************************************
|
|
||||||
"""Define Client side proxies"""
|
|
||||||
|
|
||||||
# TODO: remove, as currently not used
|
|
||||||
|
|
||||||
|
|
||||||
import json
|
|
||||||
import queue
|
|
||||||
import socket
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
from collections import OrderedDict
|
|
||||||
from select import select
|
|
||||||
|
|
||||||
import serial
|
|
||||||
|
|
||||||
from secop.datatypes import CommandType, EnumType, get_datatype
|
|
||||||
from secop.errors import EXCEPTIONS
|
|
||||||
from secop.lib import formatException, formatExtendedStack, mkthread
|
|
||||||
from secop.lib.parsing import format_time, parse_time
|
|
||||||
from secop.protocol.messages import BUFFERREQUEST, COMMANDREQUEST, \
|
|
||||||
DESCRIPTIONREPLY, DESCRIPTIONREQUEST, DISABLEEVENTSREQUEST, \
|
|
||||||
ENABLEEVENTSREQUEST, ERRORPREFIX, EVENTREPLY, \
|
|
||||||
HEARTBEATREQUEST, HELPREQUEST, IDENTREQUEST, READREPLY, \
|
|
||||||
READREQUEST, REQUEST2REPLY, WRITEREPLY, WRITEREQUEST
|
|
||||||
|
|
||||||
try:
|
|
||||||
import mlzlog
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TCPConnection:
|
|
||||||
# disguise a TCP connection as serial one
|
|
||||||
|
|
||||||
def __init__(self, host, port, getLogger=None):
|
|
||||||
if getLogger:
|
|
||||||
self.log = getLogger('TCPConnection')
|
|
||||||
else:
|
|
||||||
self.log = mlzlog.getLogger('TCPConnection')
|
|
||||||
self._host = host
|
|
||||||
self._port = int(port)
|
|
||||||
self._thread = None
|
|
||||||
self.callbacks = [] # called if SEC-node shuts down
|
|
||||||
self._io = None
|
|
||||||
self.connect()
|
|
||||||
|
|
||||||
def connect(self):
|
|
||||||
self._readbuffer = queue.Queue(100)
|
|
||||||
time.sleep(1)
|
|
||||||
io = socket.create_connection((self._host, self._port))
|
|
||||||
io.setblocking(False)
|
|
||||||
self.stopflag = False
|
|
||||||
self._io = io
|
|
||||||
if self._thread and self._thread.is_alive():
|
|
||||||
return
|
|
||||||
self._thread = mkthread(self._run)
|
|
||||||
|
|
||||||
def _run(self):
|
|
||||||
try:
|
|
||||||
data = b''
|
|
||||||
while not self.stopflag:
|
|
||||||
rlist, _, xlist = select([self._io], [], [self._io], 1)
|
|
||||||
if xlist:
|
|
||||||
# on some strange systems, a closed connection is indicated by
|
|
||||||
# an exceptional condition instead of "read ready" + "empty recv"
|
|
||||||
newdata = b''
|
|
||||||
else:
|
|
||||||
if not rlist:
|
|
||||||
continue # check stopflag every second
|
|
||||||
# self._io is now ready to read some bytes
|
|
||||||
try:
|
|
||||||
newdata = self._io.recv(1024)
|
|
||||||
except socket.error as err:
|
|
||||||
if err.args[0] == socket.EAGAIN:
|
|
||||||
# if we receive an EAGAIN error, just continue
|
|
||||||
continue
|
|
||||||
newdata = b''
|
|
||||||
except Exception:
|
|
||||||
newdata = b''
|
|
||||||
if not newdata: # no data on recv indicates a closed connection
|
|
||||||
raise IOError('%s:%d disconnected' % (self._host, self._port))
|
|
||||||
lines = (data + newdata).split(b'\n')
|
|
||||||
for line in lines[:-1]: # last line is incomplete or empty
|
|
||||||
try:
|
|
||||||
self._readbuffer.put(line.strip(b'\r').decode('utf-8'),
|
|
||||||
block=True, timeout=1)
|
|
||||||
except queue.Full:
|
|
||||||
self.log.debug('rcv queue full! dropping line: %r' % line)
|
|
||||||
data = lines[-1]
|
|
||||||
except Exception as err:
|
|
||||||
self.log.error(err)
|
|
||||||
try:
|
|
||||||
self._io.shutdown(socket.SHUT_RDWR)
|
|
||||||
except socket.error:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
self._io.close()
|
|
||||||
except socket.error:
|
|
||||||
pass
|
|
||||||
for cb, args in self.callbacks:
|
|
||||||
cb(*args)
|
|
||||||
|
|
||||||
def readline(self, timeout=None):
|
|
||||||
"""blocks until a full line was read and returns it
|
|
||||||
|
|
||||||
returns None when connection is stopped"""
|
|
||||||
if self.stopflag:
|
|
||||||
return None
|
|
||||||
return self._readbuffer.get(block=True, timeout=timeout)
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self.stopflag = True
|
|
||||||
self._readbuffer.put(None) # terminate pending readline
|
|
||||||
|
|
||||||
def readable(self):
|
|
||||||
return not self._readbuffer.empty()
|
|
||||||
|
|
||||||
def write(self, data):
|
|
||||||
if self._io is None:
|
|
||||||
self.connect()
|
|
||||||
self._io.sendall(data.encode('latin-1'))
|
|
||||||
|
|
||||||
def writeline(self, line):
|
|
||||||
self.write(line + '\n')
|
|
||||||
|
|
||||||
def writelines(self, *lines):
|
|
||||||
for line in lines:
|
|
||||||
self.writeline(line)
|
|
||||||
|
|
||||||
|
|
||||||
class Value:
|
|
||||||
t = None # pylint: disable = C0103
|
|
||||||
u = None
|
|
||||||
e = None
|
|
||||||
fmtstr = '%s'
|
|
||||||
|
|
||||||
def __init__(self, value, qualifiers=None):
|
|
||||||
self.value = value
|
|
||||||
if qualifiers:
|
|
||||||
self.__dict__.update(qualifiers)
|
|
||||||
if 't' in qualifiers:
|
|
||||||
try:
|
|
||||||
self.t = float(qualifiers['t'])
|
|
||||||
except Exception:
|
|
||||||
self.t = parse_time(qualifiers['t'])
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
r = []
|
|
||||||
if self.t is not None:
|
|
||||||
r.append("timestamp=%r" % format_time(self.t))
|
|
||||||
if self.u is not None:
|
|
||||||
r.append('unit=%r' % self.u)
|
|
||||||
if self.e is not None:
|
|
||||||
r.append(('error=%s' % self.fmtstr) % self.e)
|
|
||||||
if r:
|
|
||||||
return (self.fmtstr + '(%s)') % (self.value, ', '.join(r))
|
|
||||||
return self.fmtstr % self.value
|
|
||||||
|
|
||||||
|
|
||||||
class Client:
|
|
||||||
secop_id = 'unknown'
|
|
||||||
describing_data = {}
|
|
||||||
stopflag = False
|
|
||||||
connection_established = False
|
|
||||||
|
|
||||||
def __init__(self, opts, autoconnect=True, getLogger=None):
|
|
||||||
if 'testing' not in opts:
|
|
||||||
if getLogger:
|
|
||||||
self.log = getLogger('client')
|
|
||||||
else:
|
|
||||||
self.log = mlzlog.getLogger('client', True)
|
|
||||||
else:
|
|
||||||
class logStub:
|
|
||||||
|
|
||||||
def info(self, *args):
|
|
||||||
pass
|
|
||||||
debug = info
|
|
||||||
error = info
|
|
||||||
warning = info
|
|
||||||
exception = info
|
|
||||||
self.log = logStub()
|
|
||||||
self._cache = dict()
|
|
||||||
if 'module' in opts:
|
|
||||||
# serial port
|
|
||||||
devport = opts.pop('module')
|
|
||||||
baudrate = int(opts.pop('baudrate', 115200))
|
|
||||||
self.contactPoint = "serial://%s:%s" % (devport, baudrate)
|
|
||||||
self.connection = serial.Serial(
|
|
||||||
devport, baudrate=baudrate, timeout=1)
|
|
||||||
self.connection.callbacks = []
|
|
||||||
elif 'testing' not in opts:
|
|
||||||
host = opts.pop('host', 'localhost')
|
|
||||||
port = int(opts.pop('port', 10767))
|
|
||||||
self.contactPoint = "tcp://%s:%d" % (host, port)
|
|
||||||
self.connection = TCPConnection(host, port, getLogger=getLogger)
|
|
||||||
else:
|
|
||||||
self.contactPoint = 'testing'
|
|
||||||
self.connection = opts.pop('testing')
|
|
||||||
|
|
||||||
# maps an expected reply to a list containing a single Event()
|
|
||||||
# upon rcv of that reply, entry is appended with False and
|
|
||||||
# the data of the reply.
|
|
||||||
# if an error is received, the entry is appended with True and an
|
|
||||||
# appropriate Exception.
|
|
||||||
# Then the Event is set.
|
|
||||||
self.expected_replies = {}
|
|
||||||
|
|
||||||
# maps spec to a set of callback functions (or single_shot callbacks)
|
|
||||||
self.callbacks = dict()
|
|
||||||
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
|
|
||||||
|
|
||||||
self._syncLock = threading.RLock()
|
|
||||||
self._thread = threading.Thread(target=self._run)
|
|
||||||
self._thread.daemon = True
|
|
||||||
self._thread.start()
|
|
||||||
|
|
||||||
if autoconnect:
|
|
||||||
self.startup()
|
|
||||||
|
|
||||||
def _run(self):
|
|
||||||
while not self.stopflag:
|
|
||||||
try:
|
|
||||||
self._inner_run()
|
|
||||||
except Exception as err:
|
|
||||||
print(formatExtendedStack())
|
|
||||||
self.log.exception(err)
|
|
||||||
raise
|
|
||||||
|
|
||||||
def _inner_run(self):
|
|
||||||
data = ''
|
|
||||||
self.connection.writeline('*IDN?')
|
|
||||||
|
|
||||||
while not self.stopflag:
|
|
||||||
line = self.connection.readline()
|
|
||||||
if line is None: # connection stopped
|
|
||||||
break
|
|
||||||
self.connection_established = True
|
|
||||||
self.log.debug('got answer %r' % line)
|
|
||||||
if line.startswith(('SECoP', 'SINE2020&ISSE,SECoP')):
|
|
||||||
self.log.info('connected to: ' + line.strip())
|
|
||||||
self.secop_id = line
|
|
||||||
continue
|
|
||||||
msgtype, spec, data = self.decode_message(line)
|
|
||||||
if msgtype in (EVENTREPLY, READREPLY, WRITEREPLY):
|
|
||||||
# handle async stuff
|
|
||||||
self._handle_event(spec, data)
|
|
||||||
# handle sync stuff
|
|
||||||
self._handle_sync_reply(msgtype, spec, data)
|
|
||||||
|
|
||||||
def _handle_sync_reply(self, msgtype, spec, data):
|
|
||||||
# handle sync stuff
|
|
||||||
if msgtype.startswith(ERRORPREFIX):
|
|
||||||
# find originating msgtype and map to expected_reply_type
|
|
||||||
# errormessages carry to offending request as the first
|
|
||||||
# result in the resultist
|
|
||||||
request = msgtype[len(ERRORPREFIX):]
|
|
||||||
reply = REQUEST2REPLY.get(request, request)
|
|
||||||
|
|
||||||
entry = self.expected_replies.get((reply, spec), None)
|
|
||||||
if entry:
|
|
||||||
self.log.error("request %r resulted in Error %r" %
|
|
||||||
("%s %s" % (request, spec), (data[0], data[1])))
|
|
||||||
entry.extend([True, EXCEPTIONS[data[0]](*data[1:])])
|
|
||||||
entry[0].set()
|
|
||||||
return
|
|
||||||
self.log.error("got an unexpected %s %r" % (msgtype,data[0:1]))
|
|
||||||
self.log.error(repr(data))
|
|
||||||
return
|
|
||||||
if msgtype == DESCRIPTIONREPLY:
|
|
||||||
entry = self.expected_replies.get((msgtype, ''), None)
|
|
||||||
else:
|
|
||||||
entry = self.expected_replies.get((msgtype, spec), None)
|
|
||||||
|
|
||||||
if entry:
|
|
||||||
self.log.debug("got expected reply '%s %s'" % (msgtype, spec)
|
|
||||||
if spec else "got expected reply '%s'" % msgtype)
|
|
||||||
entry.extend([False, msgtype, spec, data])
|
|
||||||
entry[0].set()
|
|
||||||
|
|
||||||
def encode_message(self, requesttype, spec='', data=None):
|
|
||||||
"""encodes the given message to a string
|
|
||||||
"""
|
|
||||||
req = [str(requesttype)]
|
|
||||||
if spec:
|
|
||||||
req.append(str(spec))
|
|
||||||
if data is not None:
|
|
||||||
req.append(json.dumps(data))
|
|
||||||
req = ' '.join(req)
|
|
||||||
return req
|
|
||||||
|
|
||||||
def decode_message(self, msg):
|
|
||||||
"""return a decoded message triple"""
|
|
||||||
msg = msg.strip()
|
|
||||||
if ' ' not in msg:
|
|
||||||
return msg, '', None
|
|
||||||
msgtype, spec = msg.split(' ', 1)
|
|
||||||
data = None
|
|
||||||
if ' ' in spec:
|
|
||||||
spec, json_data = spec.split(' ', 1)
|
|
||||||
try:
|
|
||||||
data = json.loads(json_data)
|
|
||||||
except ValueError:
|
|
||||||
# keep as string
|
|
||||||
data = json_data
|
|
||||||
# print formatException()
|
|
||||||
return msgtype, spec, data
|
|
||||||
|
|
||||||
def _handle_event(self, spec, data):
|
|
||||||
"""handles event"""
|
|
||||||
# self.log.debug('handle_event %r %r' % (spec, data))
|
|
||||||
if ':' not in spec:
|
|
||||||
self.log.warning("deprecated specifier %r" % spec)
|
|
||||||
spec = '%s:value' % spec
|
|
||||||
modname, pname = spec.split(':', 1)
|
|
||||||
|
|
||||||
if data:
|
|
||||||
self._cache.setdefault(modname, {})[pname] = Value(*data)
|
|
||||||
else:
|
|
||||||
self.log.warning(
|
|
||||||
'got malformed answer! (%s,%s)' % (spec, data))
|
|
||||||
|
|
||||||
# self.log.info('cache: %s:%s=%r (was: %s)', modname, pname, data, previous)
|
|
||||||
if spec in self.callbacks:
|
|
||||||
for func in self.callbacks[spec]:
|
|
||||||
try:
|
|
||||||
mkthread(func, modname, pname, data)
|
|
||||||
except Exception as err:
|
|
||||||
self.log.exception('Exception in Callback!', err)
|
|
||||||
run = set()
|
|
||||||
if spec in self.single_shots:
|
|
||||||
for func in self.single_shots[spec]:
|
|
||||||
try:
|
|
||||||
mkthread(func, data)
|
|
||||||
except Exception as err:
|
|
||||||
self.log.exception('Exception in Single-shot Callback!',
|
|
||||||
err)
|
|
||||||
run.add(func)
|
|
||||||
self.single_shots[spec].difference_update(run)
|
|
||||||
|
|
||||||
def _getDescribingModuleData(self, module):
|
|
||||||
return self.describingModulesData[module]
|
|
||||||
|
|
||||||
def _getDescribingParameterData(self, module, parameter):
|
|
||||||
return self._getDescribingModuleData(module)['accessibles'][parameter]
|
|
||||||
|
|
||||||
def _decode_substruct(self, specialkeys=[], data={}): # pylint: disable=W0102
|
|
||||||
# take a dict and move all keys which are not in specialkeys
|
|
||||||
# into a 'properties' subdict
|
|
||||||
# specialkeys entries are converted from list to ordereddict
|
|
||||||
try:
|
|
||||||
result = {}
|
|
||||||
for k in specialkeys:
|
|
||||||
result[k] = OrderedDict(data.pop(k, []))
|
|
||||||
result['properties'] = data
|
|
||||||
return result
|
|
||||||
except Exception as err:
|
|
||||||
raise RuntimeError('Error decoding substruct of descriptive data: %r\n%r' % (err, data))
|
|
||||||
|
|
||||||
def _issueDescribe(self):
|
|
||||||
_, _, describing_data = self._communicate(DESCRIPTIONREQUEST)
|
|
||||||
try:
|
|
||||||
describing_data = self._decode_substruct(
|
|
||||||
['modules'], describing_data)
|
|
||||||
for modname, module in list(describing_data['modules'].items()):
|
|
||||||
# convert old namings of interface_classes
|
|
||||||
if 'interface_class' in module:
|
|
||||||
module['interface_classes'] = module.pop('interface_class')
|
|
||||||
elif 'interfaces' in module:
|
|
||||||
module['interface_classes'] = module.pop('interfaces')
|
|
||||||
describing_data['modules'][modname] = self._decode_substruct(
|
|
||||||
['accessibles'], module)
|
|
||||||
|
|
||||||
self.describing_data = describing_data
|
|
||||||
|
|
||||||
for module, moduleData in self.describing_data['modules'].items():
|
|
||||||
for aname, adata in moduleData['accessibles'].items():
|
|
||||||
datatype = get_datatype(adata.pop('datainfo'))
|
|
||||||
# *sigh* special handling for 'some' parameters....
|
|
||||||
if isinstance(datatype, EnumType):
|
|
||||||
datatype._enum.name = aname
|
|
||||||
if aname == 'status':
|
|
||||||
datatype.members[0]._enum.name = 'Status'
|
|
||||||
self.describing_data['modules'][module]['accessibles'] \
|
|
||||||
[aname]['datatype'] = datatype
|
|
||||||
except Exception as _exc:
|
|
||||||
print(formatException(verbose=True))
|
|
||||||
raise
|
|
||||||
|
|
||||||
def register_callback(self, module, parameter, cb):
|
|
||||||
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)
|
|
||||||
|
|
||||||
def register_shutdown_callback(self, func, *args):
|
|
||||||
self.connection.callbacks.append((func, args))
|
|
||||||
|
|
||||||
def communicate(self, msgtype, spec='', data=None):
|
|
||||||
# only return the data portion....
|
|
||||||
return self._communicate(msgtype, spec, data)[2]
|
|
||||||
|
|
||||||
def _communicate(self, msgtype, spec='', data=None):
|
|
||||||
self.log.debug('communicate: %r %r %r' % (msgtype, spec, data))
|
|
||||||
if self.stopflag:
|
|
||||||
raise RuntimeError('alreading stopping!')
|
|
||||||
if msgtype == IDENTREQUEST:
|
|
||||||
return self.secop_id
|
|
||||||
|
|
||||||
# sanitize input
|
|
||||||
msgtype = str(msgtype)
|
|
||||||
spec = str(spec)
|
|
||||||
|
|
||||||
if msgtype not in (DESCRIPTIONREQUEST, ENABLEEVENTSREQUEST,
|
|
||||||
DISABLEEVENTSREQUEST, COMMANDREQUEST,
|
|
||||||
WRITEREQUEST, BUFFERREQUEST,
|
|
||||||
READREQUEST, HEARTBEATREQUEST, HELPREQUEST):
|
|
||||||
raise EXCEPTIONS['Protocol'](args=[
|
|
||||||
self.encode_message(msgtype, spec, data),
|
|
||||||
dict(
|
|
||||||
errorclass='Protocol',
|
|
||||||
errorinfo='%r: No Such Messagetype defined!' % msgtype, ),
|
|
||||||
])
|
|
||||||
|
|
||||||
# handle syntactic sugar
|
|
||||||
if msgtype == WRITEREQUEST and ':' not in spec:
|
|
||||||
spec = spec + ':target'
|
|
||||||
if msgtype == READREQUEST and ':' not in spec:
|
|
||||||
spec = spec + ':value'
|
|
||||||
|
|
||||||
# check if such a request is already out
|
|
||||||
rply = REQUEST2REPLY[msgtype]
|
|
||||||
if (rply, spec) in self.expected_replies:
|
|
||||||
raise RuntimeError(
|
|
||||||
"can not have more than one requests of the same type at the same time!"
|
|
||||||
)
|
|
||||||
|
|
||||||
# prepare sending request
|
|
||||||
event = threading.Event()
|
|
||||||
self.expected_replies[(rply, spec)] = [event]
|
|
||||||
self.log.debug('prepared reception of %r msg' % rply)
|
|
||||||
|
|
||||||
# send request
|
|
||||||
msg = self.encode_message(msgtype, spec, data)
|
|
||||||
while not self.connection_established:
|
|
||||||
self.log.debug('connection not established yet, waiting ...')
|
|
||||||
time.sleep(0.1)
|
|
||||||
self.connection.writeline(msg)
|
|
||||||
self.log.debug('sent msg %r' % msg)
|
|
||||||
|
|
||||||
# wait for reply. timeout after 10s
|
|
||||||
if event.wait(10):
|
|
||||||
self.log.debug('checking reply')
|
|
||||||
entry = self.expected_replies.pop((rply, spec))
|
|
||||||
# entry is: event, is_error, exc_or_msgtype [,spec, date]<- if !err
|
|
||||||
is_error = entry[1]
|
|
||||||
if is_error:
|
|
||||||
# if error, entry[2] contains the rigth Exception to raise
|
|
||||||
raise entry[2]
|
|
||||||
# valid reply: entry[2:5] contain msgtype, spec, data
|
|
||||||
return tuple(entry[2:5])
|
|
||||||
|
|
||||||
# timed out
|
|
||||||
del self.expected_replies[(rply, spec)]
|
|
||||||
# XXX: raise a TimedOut ?
|
|
||||||
raise RuntimeError("timeout upon waiting for reply to %r!" % msgtype)
|
|
||||||
|
|
||||||
def quit(self):
|
|
||||||
# after calling this the client is dysfunctional!
|
|
||||||
# self.communicate(DISABLEEVENTSREQUEST)
|
|
||||||
self.stopflag = True
|
|
||||||
self.connection.stop()
|
|
||||||
if self._thread and self._thread.is_alive():
|
|
||||||
self._thread.join(10)
|
|
||||||
|
|
||||||
def startup(self, _async=False):
|
|
||||||
self._issueDescribe()
|
|
||||||
# always fill our cache
|
|
||||||
self.communicate(ENABLEEVENTSREQUEST)
|
|
||||||
# deactivate updates if not wanted
|
|
||||||
if not _async:
|
|
||||||
self.communicate(DISABLEEVENTSREQUEST)
|
|
||||||
|
|
||||||
def queryCache(self, module, parameter=None):
|
|
||||||
result = self._cache.get(module, {})
|
|
||||||
|
|
||||||
if parameter is not None:
|
|
||||||
result = result[parameter]
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def getParameter(self, module, parameter):
|
|
||||||
return self.communicate(READREQUEST, '%s:%s' % (module, parameter))
|
|
||||||
|
|
||||||
def setParameter(self, module, parameter, value):
|
|
||||||
datatype = self._getDescribingParameterData(module,
|
|
||||||
parameter)['datatype']
|
|
||||||
|
|
||||||
value = datatype.from_string(value)
|
|
||||||
value = datatype.export_value(value)
|
|
||||||
self.communicate(WRITEREQUEST, '%s:%s' % (module, parameter), value)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def describingData(self):
|
|
||||||
return self.describing_data
|
|
||||||
|
|
||||||
@property
|
|
||||||
def describingModulesData(self):
|
|
||||||
return self.describingData['modules']
|
|
||||||
|
|
||||||
@property
|
|
||||||
def equipmentId(self):
|
|
||||||
if self.describingData:
|
|
||||||
return self.describingData['properties']['equipment_id']
|
|
||||||
return 'Undetermined'
|
|
||||||
|
|
||||||
@property
|
|
||||||
def protocolVersion(self):
|
|
||||||
return self.secop_id
|
|
||||||
|
|
||||||
@property
|
|
||||||
def modules(self):
|
|
||||||
return list(self.describing_data['modules'].keys())
|
|
||||||
|
|
||||||
def getParameters(self, module):
|
|
||||||
params = filter(lambda item: not isinstance(item[1]['datatype'], CommandType),
|
|
||||||
self.describing_data['modules'][module]['accessibles'].items())
|
|
||||||
return list(param[0] for param in params)
|
|
||||||
|
|
||||||
def getModuleProperties(self, module):
|
|
||||||
return self.describing_data['modules'][module]['properties']
|
|
||||||
|
|
||||||
def getModuleBaseClass(self, module):
|
|
||||||
return self.getModuleProperties(module)['interface_classes']
|
|
||||||
|
|
||||||
def getCommands(self, module):
|
|
||||||
cmds = filter(lambda item: isinstance(item[1]['datatype'], CommandType),
|
|
||||||
self.describing_data['modules'][module]['accessibles'].items())
|
|
||||||
return OrderedDict(cmds)
|
|
||||||
|
|
||||||
def execCommand(self, module, command, args):
|
|
||||||
# ignore reply message + reply specifier, only return data
|
|
||||||
return self._communicate(COMMANDREQUEST, '%s:%s' % (module, command), list(args) if args else None)[2]
|
|
||||||
|
|
||||||
def getProperties(self, module, parameter):
|
|
||||||
return self.describing_data['modules'][module]['accessibles'][parameter]
|
|
||||||
|
|
||||||
def syncCommunicate(self, *msg):
|
|
||||||
res = self._communicate(*msg) # pylint: disable=E1120
|
|
||||||
try:
|
|
||||||
res = self.encode_message(*res)
|
|
||||||
except Exception:
|
|
||||||
res = str(res)
|
|
||||||
return res
|
|
||||||
|
|
||||||
def ping(self, pingctr=[0]): # pylint: disable=W0102
|
|
||||||
pingctr[0] = pingctr[0] + 1
|
|
||||||
self.communicate(HEARTBEATREQUEST, pingctr[0])
|
|
@ -1,193 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# *****************************************************************************
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
|
||||||
# the terms of the GNU General Public License as published by the Free Software
|
|
||||||
# Foundation; either version 2 of the License, or (at your option) any later
|
|
||||||
# version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
||||||
# details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License along with
|
|
||||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
|
||||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
||||||
#
|
|
||||||
# Module authors:
|
|
||||||
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
|
||||||
#
|
|
||||||
# *****************************************************************************
|
|
||||||
"""console client"""
|
|
||||||
|
|
||||||
# TODO: remove, as currently not used
|
|
||||||
|
|
||||||
|
|
||||||
import code
|
|
||||||
import configparser
|
|
||||||
import socket
|
|
||||||
import threading
|
|
||||||
from collections import deque
|
|
||||||
from os import path
|
|
||||||
|
|
||||||
import mlzlog
|
|
||||||
|
|
||||||
from secop.protocol.interface import decode_msg, encode_msg_frame, get_msg
|
|
||||||
from secop.protocol.messages import EVENTREPLY
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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'))
|
|
||||||
|
|
||||||
|
|
||||||
class ClientConsole:
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class TCPConnection:
|
|
||||||
|
|
||||||
def __init__(self, connect, port, **kwds):
|
|
||||||
self.log = mlzlog.log.getChild('connection', False)
|
|
||||||
port = int(port)
|
|
||||||
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)
|
|
||||||
data = encode_msg_frame(*msg.serialize())
|
|
||||||
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):
|
|
||||||
data = b''
|
|
||||||
while True:
|
|
||||||
newdata = self.connection.recv(1024)
|
|
||||||
self.log.debug("RCV: got raw data %r" % newdata)
|
|
||||||
data = data + newdata
|
|
||||||
while True:
|
|
||||||
origin, data = get_msg(data)
|
|
||||||
if origin is None:
|
|
||||||
break # no more messages to process
|
|
||||||
if not origin: # empty string
|
|
||||||
continue # ???
|
|
||||||
_ = decode_msg(origin)
|
|
||||||
# construct msgObj from msg
|
|
||||||
try:
|
|
||||||
#msgObj = Message(*msg)
|
|
||||||
#msgObj.origin = origin.decode('latin-1')
|
|
||||||
#self.handle(msgObj)
|
|
||||||
pass
|
|
||||||
except Exception:
|
|
||||||
# ??? what to do here?
|
|
||||||
pass
|
|
||||||
|
|
||||||
def handle(self, msg):
|
|
||||||
if msg.action == EVENTREPLY:
|
|
||||||
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 self.queue:
|
|
||||||
pass # XXX: remove BUSY polling
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class Client:
|
|
||||||
|
|
||||||
def __init__(self, opts):
|
|
||||||
self.log = mlzlog.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)
|
|
||||||
module = msg.module
|
|
||||||
param = msg.param
|
|
||||||
value = msg.value
|
|
||||||
self._cache.getdefault(module, {})[param] = value
|
|
||||||
# XXX: further notification-callbacks needed ???
|
|
||||||
|
|
||||||
def populateNamespace(self, namespace):
|
|
||||||
#self.connection.send(Message(DESCRIPTIONREQUEST))
|
|
||||||
# reply = self.connection.read()
|
|
||||||
# self.log.info("found modules %r" % reply)
|
|
||||||
# create proxies, populate cache....
|
|
||||||
namespace.setconst('connection', self.connection)
|
|
@ -1,406 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# *****************************************************************************
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
|
||||||
# the terms of the GNU General Public License as published by the Free Software
|
|
||||||
# Foundation; either version 2 of the License, or (at your option) any later
|
|
||||||
# version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
||||||
# details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License along with
|
|
||||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
|
||||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
||||||
#
|
|
||||||
# Module authors:
|
|
||||||
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
|
||||||
#
|
|
||||||
# *****************************************************************************
|
|
||||||
"""Define parsing helpers"""
|
|
||||||
|
|
||||||
# TODO: remove, as currently not used
|
|
||||||
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
from datetime import datetime, timedelta, tzinfo
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
if time.daylight:
|
|
||||||
DSTOFFSET = timedelta(seconds=-time.altzone)
|
|
||||||
else:
|
|
||||||
DSTOFFSET = STDOFFSET
|
|
||||||
|
|
||||||
DSTDIFF = DSTOFFSET - STDOFFSET
|
|
||||||
|
|
||||||
def utcoffset(self, dt):
|
|
||||||
if self._isdst(dt):
|
|
||||||
return self.DSTOFFSET
|
|
||||||
return self.STDOFFSET
|
|
||||||
|
|
||||||
def dst(self, dt):
|
|
||||||
if self._isdst(dt):
|
|
||||||
return self.DSTDIFF
|
|
||||||
return self.ZERO
|
|
||||||
|
|
||||||
def tzname(self, dt):
|
|
||||||
return time.tzname[self._isdst(dt)]
|
|
||||||
|
|
||||||
def _isdst(self, dt):
|
|
||||||
tt = (dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second,
|
|
||||||
dt.weekday(), 0, 0)
|
|
||||||
stamp = time.mktime(tt)
|
|
||||||
tt = time.localtime(stamp)
|
|
||||||
return tt.tm_isdst > 0
|
|
||||||
|
|
||||||
|
|
||||||
LocalTimezone = LocalTimezone()
|
|
||||||
|
|
||||||
|
|
||||||
def format_time(timestamp=None):
|
|
||||||
# get time in UTC
|
|
||||||
if timestamp is None:
|
|
||||||
d = datetime.now(LocalTimezone)
|
|
||||||
else:
|
|
||||||
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'): # pylint: disable=W0231
|
|
||||||
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(
|
|
||||||
r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})'
|
|
||||||
r'[T ](?P<hour>\d{1,2}):(?P<minute>\d{1,2})'
|
|
||||||
r'(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d*)?)?'
|
|
||||||
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,
|
|
||||||
the output uses a timezone with a fixed offset from UTC.
|
|
||||||
"""
|
|
||||||
match = datetime_re.match(isostring)
|
|
||||||
if match:
|
|
||||||
kw = match.groupdict()
|
|
||||||
if kw['microsecond']:
|
|
||||||
kw['microsecond'] = kw['microsecond'].ljust(6, '0')
|
|
||||||
_tzinfo = kw.pop('tzinfo')
|
|
||||||
if _tzinfo == 'Z':
|
|
||||||
_tzinfo = timezone.utc # pylint: disable=E0602
|
|
||||||
elif _tzinfo is not None:
|
|
||||||
offset_mins = int(_tzinfo[-2:]) if len(_tzinfo) > 3 else 0
|
|
||||||
offset_hours = int(_tzinfo[1:3])
|
|
||||||
offset = timedelta(hours=offset_hours, minutes=offset_mins)
|
|
||||||
if _tzinfo[0] == '-':
|
|
||||||
offset = -offset
|
|
||||||
_tzinfo = Timezone(offset)
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_time(isostring):
|
|
||||||
try:
|
|
||||||
return float(isostring)
|
|
||||||
except ValueError:
|
|
||||||
dt = _parse_isostring(isostring)
|
|
||||||
return time.mktime(dt.timetuple()) + dt.microsecond * 1e-6
|
|
||||||
|
|
||||||
# possibly unusable stuff below!
|
|
||||||
|
|
||||||
|
|
||||||
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):
|
|
||||||
# XXX: check for 'easy' strings only and omit the ''
|
|
||||||
return repr(args)
|
|
||||||
return repr(args) # for floats/ints/...
|
|
||||||
|
|
||||||
|
|
||||||
class ArgsParser:
|
|
||||||
"""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 = '0123456789'
|
|
||||||
NAME_CHARS = '_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):
|
|
||||||
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
|
|
||||||
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:
|
|
||||||
return res
|
|
||||||
self.idx = idx
|
|
||||||
res = self.parse_record()
|
|
||||||
if res:
|
|
||||||
return res
|
|
||||||
self.idx = idx
|
|
||||||
res = self.parse_string()
|
|
||||||
if res:
|
|
||||||
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 not None:
|
|
||||||
return -number # pylint: disable=invalid-unary-operand-type
|
|
||||||
return None
|
|
||||||
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"
|
|
@ -1,84 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# *****************************************************************************
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
|
||||||
# the terms of the GNU General Public License as published by the Free Software
|
|
||||||
# Foundation; either version 2 of the License, or (at your option) any later
|
|
||||||
# version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
||||||
# details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License along with
|
|
||||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
|
||||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
||||||
#
|
|
||||||
# Module authors:
|
|
||||||
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
|
||||||
#
|
|
||||||
# *****************************************************************************
|
|
||||||
"""test basic validators."""
|
|
||||||
|
|
||||||
# no fixtures needed
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from secop.basic_validators import BoolProperty, EnumProperty, FloatProperty, \
|
|
||||||
FmtStrProperty, IntProperty, NoneOr, NonNegativeFloatProperty, \
|
|
||||||
NonNegativeIntProperty, OneOfProperty, PositiveFloatProperty, \
|
|
||||||
PositiveIntProperty, StringProperty, TupleProperty, UnitProperty
|
|
||||||
|
|
||||||
|
|
||||||
class unprintable:
|
|
||||||
def __str__(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('validators_args', [
|
|
||||||
[FloatProperty, [None, 'a'], [1, 1.23, '1.23', '9e-12']],
|
|
||||||
[PositiveFloatProperty, ['x', -9, '-9', 0], [1, 1.23, '1.23', '9e-12']],
|
|
||||||
[NonNegativeFloatProperty, ['x', -9, '-9'], [0, 1.23, '1.23', '9e-12']],
|
|
||||||
[IntProperty, [None, 'a', 1.2, '1.2'], [1, '-1']],
|
|
||||||
[PositiveIntProperty, ['x', 1.9, '-9', '1e-4'], [1, '1']],
|
|
||||||
[NonNegativeIntProperty, ['x', 1.9, '-9', '1e-6'], [0, '1']],
|
|
||||||
[BoolProperty, ['x', 3], ['on', 'off', True, False]],
|
|
||||||
[StringProperty, [unprintable()], ['1', 1.2, [{}]]],
|
|
||||||
[UnitProperty, [unprintable(), '3', 9], ['mm', 'Gbarn', 'acre']],
|
|
||||||
[FmtStrProperty, [1, None, 'a', '%f'], ['%.0e', '%.3f','%.1g']],
|
|
||||||
])
|
|
||||||
def test_validators(validators_args):
|
|
||||||
v, fails, oks = validators_args
|
|
||||||
for value in fails:
|
|
||||||
with pytest.raises(Exception):
|
|
||||||
v(value)
|
|
||||||
for value in oks:
|
|
||||||
v(value)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('checker_inits', [
|
|
||||||
[OneOfProperty, lambda: OneOfProperty(a=3),], # pylint: disable=unexpected-keyword-arg
|
|
||||||
[NoneOr, lambda: NoneOr(None),],
|
|
||||||
[EnumProperty, lambda: EnumProperty(1),], # pylint: disable=too-many-function-args
|
|
||||||
[TupleProperty, lambda: TupleProperty(1,2,3),],
|
|
||||||
])
|
|
||||||
def test_checker_fails(checker_inits):
|
|
||||||
empty, badargs = checker_inits
|
|
||||||
with pytest.raises(Exception):
|
|
||||||
empty()
|
|
||||||
with pytest.raises(Exception):
|
|
||||||
badargs()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('checker_args', [
|
|
||||||
[OneOfProperty(1,2,3), ['x', None, 4], [1, 2, 3]],
|
|
||||||
[NoneOr(IntProperty), ['a', 1.2, '1.2'], [None, 1, '-1', '999999999999999']],
|
|
||||||
[EnumProperty(a=1, b=2), ['x', None, 3], ['a', 'b', 1, 2]],
|
|
||||||
[TupleProperty(IntProperty, StringProperty), [1, 'a', ('x', 2)], [(1,'x')]],
|
|
||||||
])
|
|
||||||
def test_checkers(checker_args):
|
|
||||||
v, fails, oks = checker_args
|
|
||||||
for value in fails:
|
|
||||||
with pytest.raises(Exception):
|
|
||||||
v(value)
|
|
||||||
for value in oks:
|
|
||||||
v(value)
|
|
@ -1,85 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# *****************************************************************************
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
|
||||||
# the terms of the GNU General Public License as published by the Free Software
|
|
||||||
# Foundation; either version 2 of the License, or (at your option) any later
|
|
||||||
# version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
||||||
# details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License along with
|
|
||||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
|
||||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
||||||
#
|
|
||||||
# Module authors:
|
|
||||||
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
|
||||||
#
|
|
||||||
# *****************************************************************************
|
|
||||||
"""test base client."""
|
|
||||||
|
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from secop.client.baseclient import Client
|
|
||||||
|
|
||||||
# define Test-only connection object
|
|
||||||
|
|
||||||
|
|
||||||
class TestConnect:
|
|
||||||
callbacks = []
|
|
||||||
|
|
||||||
def writeline(self, line):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def readline(self):
|
|
||||||
return ''
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
|
||||||
def clientobj(request):
|
|
||||||
print (" SETUP ClientObj")
|
|
||||||
testconnect = TestConnect()
|
|
||||||
yield Client(dict(testing=testconnect), autoconnect=False)
|
|
||||||
for cb, arg in testconnect.callbacks:
|
|
||||||
cb(arg)
|
|
||||||
print (" TEARDOWN ClientObj")
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=redefined-outer-name
|
|
||||||
def test_describing_data_decode(clientobj):
|
|
||||||
assert {'modules': OrderedDict(), 'properties': {}
|
|
||||||
} == clientobj._decode_substruct(['modules'], {})
|
|
||||||
describing_data = {'equipment_id': 'eid',
|
|
||||||
'modules': [['LN2', {'commands': [],
|
|
||||||
'interfaces': ['Readable', 'Module'],
|
|
||||||
'parameters': [['value', {'datatype': ['double'],
|
|
||||||
'description': 'current value',
|
|
||||||
'readonly': True,
|
|
||||||
}
|
|
||||||
]]
|
|
||||||
}
|
|
||||||
]]
|
|
||||||
}
|
|
||||||
decoded_data = {'modules': OrderedDict([('LN2', {'commands': OrderedDict(),
|
|
||||||
'parameters': OrderedDict([('value', {'datatype': ['double'],
|
|
||||||
'description': 'current value',
|
|
||||||
'readonly': True,
|
|
||||||
}
|
|
||||||
)]),
|
|
||||||
'properties': {'interfaces': ['Readable', 'Module']}
|
|
||||||
}
|
|
||||||
)]),
|
|
||||||
'properties': {'equipment_id': 'eid',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a = clientobj._decode_substruct(['modules'], describing_data)
|
|
||||||
for modname, module in a['modules'].items():
|
|
||||||
a['modules'][modname] = clientobj._decode_substruct(
|
|
||||||
['parameters', 'commands'], module)
|
|
||||||
assert a == decoded_data
|
|
Loading…
x
Reference in New Issue
Block a user