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:
zolliker 2021-02-25 09:14:53 +01:00
parent 6a32ecf342
commit 899a07aec8
7 changed files with 0 additions and 1580 deletions

View File

@ -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))

View File

@ -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

View File

@ -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])

View File

@ -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)

View File

@ -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"

View File

@ -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)

View File

@ -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