Provide basic client Object
also improve the describing data and core params Change-Id: I645444f2a618fdfd40a729e1007c58def24d5ffb
This commit is contained in:
parent
0432f01e16
commit
7320ac1538
@ -2,7 +2,7 @@
|
|||||||
id=Fancy_ID_without_spaces-like:MLZ_furnace7
|
id=Fancy_ID_without_spaces-like:MLZ_furnace7
|
||||||
|
|
||||||
[client]
|
[client]
|
||||||
connect=0.0.0.0
|
connectto=0.0.0.0
|
||||||
port=10767
|
port=10767
|
||||||
interface = tcp
|
interface = tcp
|
||||||
framing=eol
|
framing=eol
|
||||||
|
@ -89,11 +89,12 @@ class ClientConsole(object):
|
|||||||
else:
|
else:
|
||||||
help(arg)
|
help(arg)
|
||||||
|
|
||||||
import loggers
|
|
||||||
import socket
|
import socket
|
||||||
import threading
|
import threading
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from secop.protocol.transport import FRAMERS, ENCODERS
|
from secop import loggers
|
||||||
|
from secop.protocol.encoding import ENCODERS
|
||||||
|
from secop.protocol.framing import FRAMERS
|
||||||
from secop.protocol.messages import *
|
from secop.protocol.messages import *
|
||||||
|
|
||||||
|
|
||||||
@ -165,9 +166,6 @@ class TCPConnection(object):
|
|||||||
self.callbacks.discard(callback)
|
self.callbacks.discard(callback)
|
||||||
|
|
||||||
|
|
||||||
import loggers
|
|
||||||
|
|
||||||
|
|
||||||
class Client(object):
|
class Client(object):
|
||||||
|
|
||||||
def __init__(self, opts):
|
def __init__(self, opts):
|
||||||
|
350
secop/client/baseclient.py
Normal file
350
secop/client/baseclient.py
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
# -*- 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"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import socket
|
||||||
|
import serial
|
||||||
|
import threading
|
||||||
|
import Queue
|
||||||
|
|
||||||
|
from secop import loggers
|
||||||
|
from secop.lib import mkthread
|
||||||
|
from secop.lib.parsing import parse_time, format_time
|
||||||
|
from secop.protocol.encoding import ENCODERS
|
||||||
|
from secop.protocol.framing import FRAMERS
|
||||||
|
from secop.protocol.messages import *
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TCPConnection(object):
|
||||||
|
# disguise a TCP connection as serial one
|
||||||
|
def __init__(self, host, port):
|
||||||
|
self._host = host
|
||||||
|
self._port = int(port)
|
||||||
|
self._thread = None
|
||||||
|
self.connect()
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
self._readbuffer = Queue.Queue(100)
|
||||||
|
io = socket.create_connection((self._host, self._port))
|
||||||
|
io.setblocking(False)
|
||||||
|
io.settimeout(0.3)
|
||||||
|
self._io = io
|
||||||
|
if self._thread and self._thread.is_alive():
|
||||||
|
return
|
||||||
|
self._thread = mkthread(self._run)
|
||||||
|
|
||||||
|
def _run(self):
|
||||||
|
try:
|
||||||
|
data = u''
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
newdata = self._io.recv(1024)
|
||||||
|
except socket.timeout:
|
||||||
|
newdata = u''
|
||||||
|
pass
|
||||||
|
except Exception as err:
|
||||||
|
print err, "reconnecting"
|
||||||
|
self.connect()
|
||||||
|
data = u''
|
||||||
|
continue
|
||||||
|
data += newdata
|
||||||
|
while '\n' in data:
|
||||||
|
line, data = data.split('\n', 1)
|
||||||
|
try:
|
||||||
|
self._readbuffer.put(line.strip('\r'), block=True, timeout=1)
|
||||||
|
except Queue.Full:
|
||||||
|
self.log.debug('rcv queue full! dropping line: %r' % line)
|
||||||
|
finally:
|
||||||
|
self._thread = None
|
||||||
|
|
||||||
|
def readline(self, block=False):
|
||||||
|
"""blocks until a full line was read and returns it"""
|
||||||
|
i = 10;
|
||||||
|
while i:
|
||||||
|
try:
|
||||||
|
return self._readbuffer.get(block=True, timeout=1)
|
||||||
|
except Queue.Empty:
|
||||||
|
continue
|
||||||
|
if not block:
|
||||||
|
i -= 1
|
||||||
|
|
||||||
|
def readable(self):
|
||||||
|
return not self._readbuffer.empty()
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
self._io.sendall(data)
|
||||||
|
|
||||||
|
def writeline(self, line):
|
||||||
|
self.write(line + '\n')
|
||||||
|
|
||||||
|
def writelines(self, *lines):
|
||||||
|
for line in lines:
|
||||||
|
self.writeline(line)
|
||||||
|
|
||||||
|
|
||||||
|
class Value(object):
|
||||||
|
t = None
|
||||||
|
u = None
|
||||||
|
e = None
|
||||||
|
fmtstr = '%s'
|
||||||
|
|
||||||
|
def __init__(self, value, qualifiers={}):
|
||||||
|
self.value = value
|
||||||
|
if 't' in qualifiers:
|
||||||
|
self.t = parse_time(qualifiers.pop('t'))
|
||||||
|
self.__dict__.update(qualifiers)
|
||||||
|
|
||||||
|
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(object):
|
||||||
|
equipmentId = 'unknown'
|
||||||
|
secop_id = 'unknown'
|
||||||
|
describing_data = {}
|
||||||
|
stopflag = False
|
||||||
|
|
||||||
|
def __init__(self, opts):
|
||||||
|
self.log = loggers.log.getChild('client', True)
|
||||||
|
self._cache = dict()
|
||||||
|
if 'device' in opts:
|
||||||
|
# serial port
|
||||||
|
devport = opts.pop('device')
|
||||||
|
baudrate = int(opts.pop('baudrate', 115200))
|
||||||
|
self.contactPoint = "serial://%s:%s" % (devport, baudrate)
|
||||||
|
self.connection = serial.Serial(devport, baudrate=baudrate,
|
||||||
|
timeout=1)
|
||||||
|
else:
|
||||||
|
host = opts.pop('connectto', 'localhost')
|
||||||
|
port = int(opts.pop('port', 10767))
|
||||||
|
self.contactPoint = "tcp://%s:%d" % (host, port)
|
||||||
|
self.connection = TCPConnection(host, port)
|
||||||
|
# maps an expected reply to an list containing a single Event()
|
||||||
|
# upon rcv of that reply, the event is set and the listitem 0 is
|
||||||
|
# appended with the reply-tuple
|
||||||
|
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.cache = dict()
|
||||||
|
|
||||||
|
self._syncLock = threading.RLock()
|
||||||
|
self._thread = threading.Thread(target=self._run)
|
||||||
|
self._thread.daemon = True
|
||||||
|
self._thread.start()
|
||||||
|
|
||||||
|
def _run(self):
|
||||||
|
while not self.stopflag:
|
||||||
|
try:
|
||||||
|
self._inner_run()
|
||||||
|
except Exception as err:
|
||||||
|
self.log.exception(err)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _inner_run(self):
|
||||||
|
data = ''
|
||||||
|
self.connection.writeline('*IDN?')
|
||||||
|
idstring = self.connection.readline()
|
||||||
|
self.log.info('connected to: ' + idstring.strip())
|
||||||
|
|
||||||
|
while not self.stopflag:
|
||||||
|
line = self.connection.readline()
|
||||||
|
if line.startswith(('SECoP', 'Sine2020WP7')):
|
||||||
|
self.secop_id = line
|
||||||
|
continue
|
||||||
|
msgtype, spec, data = self._decode_message(line)
|
||||||
|
if msgtype in ('event','changed'):
|
||||||
|
# handle async stuff
|
||||||
|
self._handle_event(spec, data)
|
||||||
|
if msgtype != 'event':
|
||||||
|
# handle sync stuff
|
||||||
|
if msgtype == "ERROR" or msgtype in self.expected_replies:
|
||||||
|
# XXX: make an assignment of ERROR to an expected reply.
|
||||||
|
entry = self.expected_replies[msgtype]
|
||||||
|
entry.extend([spec, data])
|
||||||
|
# wake up calling process
|
||||||
|
entry[0].set()
|
||||||
|
else:
|
||||||
|
self.log.error('ignoring unexpected reply %r' % line)
|
||||||
|
|
||||||
|
|
||||||
|
def _encode_message(self, requesttype, spec='', data=Ellipsis):
|
||||||
|
"""encodes the given message to a string
|
||||||
|
"""
|
||||||
|
req = [str(requesttype)]
|
||||||
|
if spec:
|
||||||
|
req.append(str(spec))
|
||||||
|
if data is not Ellipsis:
|
||||||
|
req.append(json.dumps(data))
|
||||||
|
req = ' '.join(req)
|
||||||
|
return req
|
||||||
|
|
||||||
|
def _decode_message(self, msg):
|
||||||
|
"""return a decoded message tripel"""
|
||||||
|
msg = msg.strip()
|
||||||
|
if ' ' not in msg:
|
||||||
|
return msg, None, 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
|
||||||
|
return msgtype, spec, data
|
||||||
|
|
||||||
|
def _handle_event(self, spec, data):
|
||||||
|
"""handles event"""
|
||||||
|
self.log.info('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)
|
||||||
|
self.cache.setdefault(modname, {})[pname] = Value(*data)
|
||||||
|
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 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 communicate(self, msgtype, spec='', data=Ellipsis):
|
||||||
|
# maps each (sync) request to the corresponding reply
|
||||||
|
# XXX: should go to the encoder! and be imported here (or make a translating method)
|
||||||
|
REPLYMAP = {
|
||||||
|
"describe": "describing",
|
||||||
|
"do": "done",
|
||||||
|
"change": "changed",
|
||||||
|
"activate": "active",
|
||||||
|
"deactivate": "inactive",
|
||||||
|
"*IDN?": "SECoP,",
|
||||||
|
"ping": "ping",
|
||||||
|
}
|
||||||
|
if self.stopflag:
|
||||||
|
raise RuntimeError('alreading stopping!')
|
||||||
|
if msgtype == 'poll':
|
||||||
|
# send a poll request and then check incoming events
|
||||||
|
if ':' not in spec:
|
||||||
|
spec = spec + ':value'
|
||||||
|
event = threading.Event()
|
||||||
|
result = ['polled', spec]
|
||||||
|
self.single_shots.setdefault(spec, set()).add(lambda d: (result.append(d), event.set()))
|
||||||
|
self.connection.writeline(self._encode_message(msgtype, spec, data))
|
||||||
|
if event.wait(10):
|
||||||
|
return tuple(result)
|
||||||
|
raise RuntimeError("timeout upon waiting for reply!")
|
||||||
|
|
||||||
|
rply = REPLYMAP[msgtype]
|
||||||
|
if rply in self.expected_replies:
|
||||||
|
raise RuntimeError("can not have more than one requests of the same type at the same time!")
|
||||||
|
event = threading.Event()
|
||||||
|
self.expected_replies[rply] = [event]
|
||||||
|
self.connection.writeline(self._encode_message(msgtype, spec, data))
|
||||||
|
if event.wait(10): # wait 10s for reply
|
||||||
|
result = rply, self.expected_replies[rply][1], self.expected_replies[rply][2]
|
||||||
|
del self.expected_replies[rply]
|
||||||
|
return result
|
||||||
|
del self.expected_replies[rply]
|
||||||
|
raise RuntimeError("timeout upon waiting for reply!")
|
||||||
|
|
||||||
|
def quit(self):
|
||||||
|
# after calling this the client is dysfunctional!
|
||||||
|
self.communicate('deactivate')
|
||||||
|
self.stopflag = True
|
||||||
|
if self._thread and self._thread.is_alive():
|
||||||
|
self.thread.join(self._thread)
|
||||||
|
|
||||||
|
def handle_async(self, msg):
|
||||||
|
self.log.info("Got async update %r" % msg)
|
||||||
|
device = msg.device
|
||||||
|
param = msg.param
|
||||||
|
value = msg.value
|
||||||
|
self._cache.getdefault(device, {})[param] = value
|
||||||
|
# XXX: further notification-callbacks needed ???
|
||||||
|
|
||||||
|
|
||||||
|
def startup(self, async=False):
|
||||||
|
_, self.equipment_id, self.describing_data = self.communicate('describe')
|
||||||
|
# always fill our cache
|
||||||
|
self.communicate('activate')
|
||||||
|
# deactivate updates if not wanted
|
||||||
|
if not async:
|
||||||
|
self.communicate('deactivate')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def protocolVersion(self):
|
||||||
|
return self.secop_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def modules(self):
|
||||||
|
return self.describing_data['modules'].keys()
|
||||||
|
|
||||||
|
def getParameters(self, module):
|
||||||
|
return self.describing_data['modules'][module]['parameters'].keys()
|
||||||
|
|
||||||
|
def getModuleBaseClass(self, module):
|
||||||
|
return self.describing_data['modules'][module]['baseclass']
|
||||||
|
|
||||||
|
def getCommands(self, module):
|
||||||
|
return self.describing_data['modules'][module]['commands'].keys()
|
||||||
|
|
||||||
|
def getProperties(self, module, parameter):
|
||||||
|
return self.describing_data['modules'][module]['parameters'][parameter].items()
|
||||||
|
|
||||||
|
def syncCommunicate(self, msg):
|
||||||
|
return self.communicate(msg)
|
||||||
|
|
@ -67,6 +67,16 @@ class PARAM(object):
|
|||||||
return '%s(%s)' % (self.__class__.__name__, ', '.join(
|
return '%s(%s)' % (self.__class__.__name__, ', '.join(
|
||||||
['%s=%r' % (k, v) for k, v in sorted(self.__dict__.items())]))
|
['%s=%r' % (k, v) for k, v in sorted(self.__dict__.items())]))
|
||||||
|
|
||||||
|
def as_dict(self):
|
||||||
|
# used for serialisation only
|
||||||
|
return dict(description = self.description,
|
||||||
|
unit = self.unit,
|
||||||
|
readonly = self.readonly,
|
||||||
|
value = self.value,
|
||||||
|
timestamp = self.timestamp,
|
||||||
|
validator = repr(self.validator),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# storage for CMDs settings (description + call signature...)
|
# storage for CMDs settings (description + call signature...)
|
||||||
class CMD(object):
|
class CMD(object):
|
||||||
@ -83,6 +93,12 @@ class CMD(object):
|
|||||||
return '%s(%s)' % (self.__class__.__name__, ', '.join(
|
return '%s(%s)' % (self.__class__.__name__, ', '.join(
|
||||||
['%s=%r' % (k, v) for k, v in sorted(self.__dict__.items())]))
|
['%s=%r' % (k, v) for k, v in sorted(self.__dict__.items())]))
|
||||||
|
|
||||||
|
def as_dict(self):
|
||||||
|
# used for serialisation only
|
||||||
|
return dict(description = self.description,
|
||||||
|
arguments = repr(self.arguments),
|
||||||
|
resulttype = repr(self.resulttype),
|
||||||
|
)
|
||||||
|
|
||||||
# Meta class
|
# Meta class
|
||||||
# warning: MAGIC!
|
# warning: MAGIC!
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
"""Define helpers"""
|
"""Define helpers"""
|
||||||
|
|
||||||
|
import threading
|
||||||
|
|
||||||
class attrdict(dict):
|
class attrdict(dict):
|
||||||
"""a normal dict, providing access also via attributes"""
|
"""a normal dict, providing access also via attributes"""
|
||||||
@ -52,6 +53,13 @@ def get_class(spec):
|
|||||||
return getattr(module, classname)
|
return getattr(module, classname)
|
||||||
|
|
||||||
|
|
||||||
|
def mkthread(func, *args, **kwds):
|
||||||
|
t = threading.Thread(name='%s:%s' % (func.__module__, func.__name__),
|
||||||
|
target=func, args=args, kwargs=kwds)
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
return t
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
print "minimal testing: lib"
|
print "minimal testing: lib"
|
||||||
d = attrdict(a=1, b=2)
|
d = attrdict(a=1, b=2)
|
||||||
|
@ -22,19 +22,106 @@
|
|||||||
|
|
||||||
"""Define parsing helpers"""
|
"""Define parsing helpers"""
|
||||||
|
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
import datetime
|
from datetime import tzinfo, timedelta, datetime
|
||||||
|
|
||||||
|
# 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
|
||||||
|
else:
|
||||||
|
return self.STDOFFSET
|
||||||
|
|
||||||
|
def dst(self, dt):
|
||||||
|
if self._isdst(dt):
|
||||||
|
return self.DSTDIFF
|
||||||
|
else:
|
||||||
|
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")
|
||||||
|
|
||||||
|
|
||||||
def format_time(timestamp):
|
class Timezone(tzinfo):
|
||||||
return datetime.datetime.fromtimestamp(
|
def __init__(self, offset, name='unknown timezone'):
|
||||||
timestamp).strftime("%Y-%m-%d %H:%M:%S.%f")
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
def parse_time(string):
|
# possibly unusable stuff below!
|
||||||
d = datetime.datetime.strptime(string, "%Y-%m-%d %H:%M:%S.%f")
|
|
||||||
return time.mktime(d.timetuple()) + 0.000001 * d.microsecond
|
|
||||||
|
|
||||||
|
|
||||||
def format_args(args):
|
def format_args(args):
|
||||||
if isinstance(args, list):
|
if isinstance(args, list):
|
||||||
|
@ -116,7 +116,7 @@ class Dispatcher(object):
|
|||||||
listeners += list(self._dispatcher_active_connections)
|
listeners += list(self._dispatcher_active_connections)
|
||||||
for conn in listeners:
|
for conn in listeners:
|
||||||
conn.queue_async_reply(msg)
|
conn.queue_async_reply(msg)
|
||||||
|
|
||||||
def announce_update(self, moduleobj, pname, pobj):
|
def announce_update(self, moduleobj, pname, pobj):
|
||||||
"""called by modules param setters to notify subscribers of new values
|
"""called by modules param setters to notify subscribers of new values
|
||||||
"""
|
"""
|
||||||
@ -142,7 +142,7 @@ class Dispatcher(object):
|
|||||||
self._dispatcher_connections.remove(conn)
|
self._dispatcher_connections.remove(conn)
|
||||||
for _evt, conns in self._dispatcher_subscriptions.items():
|
for _evt, conns in self._dispatcher_subscriptions.items():
|
||||||
conns.discard(conn)
|
conns.discard(conn)
|
||||||
|
|
||||||
def activate_connection(self, conn):
|
def activate_connection(self, conn):
|
||||||
self._dispatcher_active_connections.add(conn)
|
self._dispatcher_active_connections.add(conn)
|
||||||
|
|
||||||
@ -188,6 +188,22 @@ class Dispatcher(object):
|
|||||||
dd[modulename] = descriptive_data
|
dd[modulename] = descriptive_data
|
||||||
return dn, dd
|
return dn, dd
|
||||||
|
|
||||||
|
def get_descriptive_data(self):
|
||||||
|
# XXX: be lazy and cache this?
|
||||||
|
result = {}
|
||||||
|
for modulename in self._dispatcher_export:
|
||||||
|
module = self.get_module(modulename)
|
||||||
|
dd = {'class' : module.__class__.__name__,
|
||||||
|
'bases' : [b.__name__ for b in module.__class__.__bases__],
|
||||||
|
'parameters': dict((pn,po.as_dict()) for pn,po in module.PARAMS.items()),
|
||||||
|
'commands': dict((cn,co.as_dict()) for cn,co in module.CMDS.items()),
|
||||||
|
'baseclass' : 'Readable',
|
||||||
|
}
|
||||||
|
result.setdefault('modules', {})[modulename] = dd
|
||||||
|
result['equipment_id'] = self.equipment_id
|
||||||
|
# XXX: what else?
|
||||||
|
return result
|
||||||
|
|
||||||
def list_module_params(self, modulename):
|
def list_module_params(self, modulename):
|
||||||
self.log.debug('list_module_params(%r)' % modulename)
|
self.log.debug('list_module_params(%r)' % modulename)
|
||||||
if modulename in self._dispatcher_export:
|
if modulename in self._dispatcher_export:
|
||||||
@ -205,7 +221,7 @@ class Dispatcher(object):
|
|||||||
def _execute_command(self, modulename, command, arguments=None):
|
def _execute_command(self, modulename, command, arguments=None):
|
||||||
if arguments is None:
|
if arguments is None:
|
||||||
arguments = []
|
arguments = []
|
||||||
|
|
||||||
moduleobj = self.get_module(modulename)
|
moduleobj = self.get_module(modulename)
|
||||||
if moduleobj is None:
|
if moduleobj is None:
|
||||||
raise NoSuchmoduleError(module=modulename)
|
raise NoSuchmoduleError(module=modulename)
|
||||||
@ -273,8 +289,7 @@ class Dispatcher(object):
|
|||||||
|
|
||||||
def handle_Describe(self, conn, msg):
|
def handle_Describe(self, conn, msg):
|
||||||
# XXX:collect descriptive data
|
# XXX:collect descriptive data
|
||||||
# XXX:how to get equipment_id?
|
return DescribeReply(equipment_id = self.equipment_id, description = self.get_descriptive_data())
|
||||||
return DescribeReply(equipment_id = self.equipment_id, description = self.list_modules())
|
|
||||||
|
|
||||||
def handle_Poll(self, conn, msg):
|
def handle_Poll(self, conn, msg):
|
||||||
# XXX: trigger polling and force sending event
|
# XXX: trigger polling and force sending event
|
||||||
@ -283,7 +298,7 @@ class Dispatcher(object):
|
|||||||
if conn in self._dispatcher_active_connections:
|
if conn in self._dispatcher_active_connections:
|
||||||
return None # already send to myself
|
return None # already send to myself
|
||||||
return res # send reply to inactive conns
|
return res # send reply to inactive conns
|
||||||
|
|
||||||
def handle_Write(self, conn, msg):
|
def handle_Write(self, conn, msg):
|
||||||
# notify all by sending WriteReply
|
# notify all by sending WriteReply
|
||||||
#msg1 = WriteReply(**msg.as_dict())
|
#msg1 = WriteReply(**msg.as_dict())
|
||||||
@ -301,7 +316,7 @@ class Dispatcher(object):
|
|||||||
if conn in self._dispatcher_active_connections:
|
if conn in self._dispatcher_active_connections:
|
||||||
return None # already send to myself
|
return None # already send to myself
|
||||||
return res # send reply to inactive conns
|
return res # send reply to inactive conns
|
||||||
|
|
||||||
def handle_Command(self, conn, msg):
|
def handle_Command(self, conn, msg):
|
||||||
# notify all by sending CommandReply
|
# notify all by sending CommandReply
|
||||||
#msg1 = CommandReply(**msg.as_dict())
|
#msg1 = CommandReply(**msg.as_dict())
|
||||||
@ -314,7 +329,7 @@ class Dispatcher(object):
|
|||||||
#if conn in self._dispatcher_active_connections:
|
#if conn in self._dispatcher_active_connections:
|
||||||
# return None # already send to myself
|
# return None # already send to myself
|
||||||
return res # send reply to inactive conns
|
return res # send reply to inactive conns
|
||||||
|
|
||||||
def handle_Heartbeat(self, conn, msg):
|
def handle_Heartbeat(self, conn, msg):
|
||||||
return HeartbeatReply(**msg.as_dict())
|
return HeartbeatReply(**msg.as_dict())
|
||||||
|
|
||||||
@ -331,14 +346,14 @@ class Dispatcher(object):
|
|||||||
except SECOPError as e:
|
except SECOPError as e:
|
||||||
self.log.error('decide what to do here!')
|
self.log.error('decide what to do here!')
|
||||||
self.log.exception(e)
|
self.log.exception(e)
|
||||||
res = Value(module=modulename, parameter=pname,
|
res = Value(module=modulename, parameter=pname,
|
||||||
value=pobj.value, t=pobj.timestamp,
|
value=pobj.value, t=pobj.timestamp,
|
||||||
unit=pobj.unit)
|
unit=pobj.unit)
|
||||||
if res.value != Ellipsis: # means we do not have a value at all so skip this
|
if res.value != Ellipsis: # means we do not have a value at all so skip this
|
||||||
self.broadcast_event(res)
|
self.broadcast_event(res)
|
||||||
conn.queue_async_reply(ActivateReply(**msg.as_dict()))
|
conn.queue_async_reply(ActivateReply(**msg.as_dict()))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def handle_Deactivate(self, conn, msg):
|
def handle_Deactivate(self, conn, msg):
|
||||||
self.deactivate_connection(conn)
|
self.deactivate_connection(conn)
|
||||||
conn.queue_async_reply(DeactivateReply(**msg.as_dict()))
|
conn.queue_async_reply(DeactivateReply(**msg.as_dict()))
|
||||||
@ -353,7 +368,7 @@ class Dispatcher(object):
|
|||||||
(no handle_<messagename> method was defined)
|
(no handle_<messagename> method was defined)
|
||||||
"""
|
"""
|
||||||
self.log.error('IGN: got unhandled request %s' % msg)
|
self.log.error('IGN: got unhandled request %s' % msg)
|
||||||
return ErrorMessage(errorclass="InternalError",
|
return ErrorMessage(errorclass="InternalError",
|
||||||
errorstring = 'Got Unhandled Request %r' % msg)
|
errorstring = 'Got Unhandled Request %r' % msg)
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
# implement as class as they may need some internal 'state' later on
|
# implement as class as they may need some internal 'state' later on
|
||||||
# (think compressors)
|
# (think compressors)
|
||||||
|
|
||||||
|
from secop.lib.parsing import format_time
|
||||||
from secop.protocol.encoding import MessageEncoder
|
from secop.protocol.encoding import MessageEncoder
|
||||||
from secop.protocol.messages import *
|
from secop.protocol.messages import *
|
||||||
from secop.protocol.errors import ProtocollError
|
from secop.protocol.errors import ProtocollError
|
||||||
@ -42,7 +43,7 @@ DEMO_RE = re.compile(
|
|||||||
#"""
|
#"""
|
||||||
# messagetypes:
|
# messagetypes:
|
||||||
IDENTREQUEST = '*IDN?' # literal
|
IDENTREQUEST = '*IDN?' # literal
|
||||||
IDENTREPLY = 'Sine2020WP7.1&ISSE, SECoP, V2016-11-30, rc1' # literal
|
IDENTREPLY = 'SECoP, SECoPTCP, V2016-11-30, rc1' # literal! first part 'SECoP' is fixed!
|
||||||
DESCRIPTIONSREQUEST = 'describe' # literal
|
DESCRIPTIONSREQUEST = 'describe' # literal
|
||||||
DESCRIPTIONREPLY = 'describing' # +<id> +json
|
DESCRIPTIONREPLY = 'describing' # +<id> +json
|
||||||
ENABLEEVENTSREQUEST = 'activate' # literal
|
ENABLEEVENTSREQUEST = 'activate' # literal
|
||||||
@ -66,6 +67,12 @@ ERRORCLASSES = ['NoSuchDevice', 'NoSuchParameter', 'NoSuchCommand',
|
|||||||
'CommandRunning', 'Disabled',]
|
'CommandRunning', 'Disabled',]
|
||||||
# note: above strings need to be unique in the sense, that none is/or starts with another
|
# note: above strings need to be unique in the sense, that none is/or starts with another
|
||||||
|
|
||||||
|
def encode_value_data(vobj):
|
||||||
|
q = vobj.qualifiers.copy()
|
||||||
|
if 't' in q:
|
||||||
|
q['t'] = format_time(q['t'])
|
||||||
|
return vobj.value, q
|
||||||
|
|
||||||
class DemoEncoder(MessageEncoder):
|
class DemoEncoder(MessageEncoder):
|
||||||
# map of msg to msgtype string as defined above.
|
# map of msg to msgtype string as defined above.
|
||||||
ENCODEMAP = {
|
ENCODEMAP = {
|
||||||
@ -88,7 +95,7 @@ class DemoEncoder(MessageEncoder):
|
|||||||
ErrorMessage : (ERRORREPLY, 'errorclass', 'errorinfo',),
|
ErrorMessage : (ERRORREPLY, 'errorclass', 'errorinfo',),
|
||||||
Value: (EVENT, lambda msg: "%s:%s" % (msg.module, msg.parameter or (msg.command+'()'))
|
Value: (EVENT, lambda msg: "%s:%s" % (msg.module, msg.parameter or (msg.command+'()'))
|
||||||
if msg.parameter or msg.command else msg.module,
|
if msg.parameter or msg.command else msg.module,
|
||||||
lambda msg: [msg.value, msg.qualifiers] if msg.qualifiers else [msg.value]),
|
encode_value_data,),
|
||||||
}
|
}
|
||||||
DECODEMAP = {
|
DECODEMAP = {
|
||||||
IDENTREQUEST : lambda spec, data: IdentifyRequest(),
|
IDENTREQUEST : lambda spec, data: IdentifyRequest(),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user