fixups
Change-Id: Ib0c6fddd38f594d735feadce31e4809e7a4c5d44
This commit is contained in:
parent
dc891466cb
commit
b26b64032e
@ -9,7 +9,7 @@ description = short description
|
||||
[interface tcp]
|
||||
interface=tcp
|
||||
bindto=0.0.0.0
|
||||
bindport=10767
|
||||
bindport=10769
|
||||
# protocol to use for this interface
|
||||
framing=eol
|
||||
encoding=demo
|
||||
|
14
etc/demo.cfg
14
etc/demo.cfg
@ -10,34 +10,34 @@ framing=eol
|
||||
encoding=demo
|
||||
|
||||
[device heatswitch]
|
||||
class=secop_demo.demo.Switch
|
||||
class=secop_demo.modules.Switch
|
||||
switch_on_time=5
|
||||
switch_off_time=10
|
||||
|
||||
[device mf]
|
||||
class=secop_demo.demo.MagneticField
|
||||
class=secop_demo.modules.MagneticField
|
||||
heatswitch = heatswitch
|
||||
|
||||
[device ts]
|
||||
class=secop_demo.demo.SampleTemp
|
||||
class=secop_demo.modules.SampleTemp
|
||||
sensor = 'Q1329V7R3'
|
||||
ramp = 4
|
||||
target = 10
|
||||
default = 10
|
||||
|
||||
[device tc1]
|
||||
class=secop_demo.demo.CoilTemp
|
||||
class=secop_demo.modules.CoilTemp
|
||||
sensor="X34598T7"
|
||||
|
||||
[device tc2]
|
||||
class=secop_demo.demo.CoilTemp
|
||||
class=secop_demo.modules.CoilTemp
|
||||
sensor="X39284Q8'
|
||||
|
||||
[device label]
|
||||
class=secop_demo.demo.Label
|
||||
class=secop_demo.modules.Label
|
||||
system=Cryomagnet MX15
|
||||
subdev_mf=mf
|
||||
subdev_ts=ts
|
||||
|
||||
#[device vt]
|
||||
#class=secop_demo.demo.ValidatorTest
|
||||
#class=secop_demo.modules.ValidatorTest
|
||||
|
@ -22,10 +22,10 @@ class=secop_demo.test.Temp
|
||||
sensor="X34598T7"
|
||||
|
||||
[device T2]
|
||||
class=secop_demo.demo.CoilTemp
|
||||
class=secop_demo.modules.CoilTemp
|
||||
sensor="X34598T8"
|
||||
|
||||
[device T3]
|
||||
class=secop_demo.demo.CoilTemp
|
||||
class=secop_demo.modules.CoilTemp
|
||||
sensor="X34598T9"
|
||||
|
||||
|
@ -548,7 +548,12 @@ def get_datatype(json):
|
||||
if json is None:
|
||||
return json
|
||||
if not isinstance(json, list):
|
||||
raise ValueError('Argument must be a properly formatted list!')
|
||||
import mlzlog
|
||||
mlzlog.getLogger('datatypes').warning(
|
||||
"WARNING: invalid datatype specified! trying fallback mechanism. ymmv!")
|
||||
return get_datatype([json])
|
||||
raise ValueError(
|
||||
'Can not interpret datatype %r, it should be a list!', json)
|
||||
if len(json) < 1:
|
||||
raise ValueError('can not validate %r', json)
|
||||
base = json[0]
|
||||
@ -563,5 +568,5 @@ def get_datatype(json):
|
||||
try:
|
||||
return DATATYPES[base](*args)
|
||||
except (TypeError, AttributeError) as exc:
|
||||
raise ValueError('Invalid datatype descriptor')
|
||||
raise ValueError('can not validate %r', json)
|
||||
raise ValueError('Invalid datatype descriptor in %r', json)
|
||||
raise ValueError('can not convert %r to datatype', json)
|
||||
|
@ -40,7 +40,8 @@ def showCommandResultDialog(command, args, result, extras=''):
|
||||
|
||||
|
||||
def showErrorDialog(error):
|
||||
m = QMessageBox(str(error))
|
||||
m = QMessageBox()
|
||||
m.setText('Error %r' % error)
|
||||
m.exec_()
|
||||
|
||||
|
||||
@ -77,27 +78,45 @@ class ParameterGroup(QWidget):
|
||||
w.hide()
|
||||
|
||||
|
||||
class CommandButton(QWidget):
|
||||
class CommandArgumentsDialog(QDialog):
|
||||
|
||||
def __init__(self, cmdname, argin, cb, parent=None):
|
||||
def __init__(self, commandname, argtypes, parent=None):
|
||||
super(CommandArgumentsDialog, self).__init__(parent)
|
||||
|
||||
# XXX: fill in apropriate widgets + OK/Cancel
|
||||
|
||||
def exec_(self):
|
||||
print('CommandArgumentsDialog result is', super(
|
||||
CommandArgumentsDialog, self).exec_())
|
||||
return None # XXX: if there were arguments, return them after validation or None for 'Cancel'
|
||||
|
||||
|
||||
class CommandButton(QButton):
|
||||
|
||||
def __init__(self, cmdname, cmdinfo, cb, parent=None):
|
||||
super(CommandButton, self).__init__(parent)
|
||||
loadUi(self, 'cmdbuttons.ui')
|
||||
|
||||
self._cmdname = cmdname
|
||||
self._argin = argin # list of datatypes
|
||||
self._argintypes = cmdinfo['arguments'] # list of datatypes
|
||||
self.resulttype = cmdinfo['resulttype']
|
||||
self._cb = cb # callback function for exection
|
||||
|
||||
if not argin:
|
||||
self.cmdLineEdit.setHidden(True)
|
||||
self.cmdPushButton.setText(cmdname)
|
||||
self.setText(cmdname)
|
||||
if cmdinfo['description']:
|
||||
self.setToolTip(cmdinfo['description'])
|
||||
self.pressed.connect(self.on_pushButton_pressed)
|
||||
|
||||
def on_cmdPushButton_pressed(self):
|
||||
self.cmdPushButton.setEnabled(False)
|
||||
if self._argin:
|
||||
self._cb(self._cmdname, self.cmdLineEdit.text())
|
||||
def on_pushButton_pressed(self):
|
||||
self.setEnabled(False)
|
||||
if self._argintypes or 1:
|
||||
args = CommandArgumentsDialog(self._cmdname, self._argintypes)
|
||||
if args: # not 'Cancel' clicked
|
||||
print('############# %s', args)
|
||||
self._cb(self._cmdname, args)
|
||||
else:
|
||||
# no need for arguments
|
||||
self._cb(self._cmdname, None)
|
||||
self.cmdPushButton.setEnabled(True)
|
||||
self.setEnabled(True)
|
||||
|
||||
|
||||
class ModuleCtrl(QWidget):
|
||||
@ -120,16 +139,19 @@ class ModuleCtrl(QWidget):
|
||||
|
||||
self._node.newData.connect(self._updateValue)
|
||||
|
||||
def _execCommand(self, command, arg=None):
|
||||
if arg: # try to validate input
|
||||
def _execCommand(self, command, args=None):
|
||||
if args: # try to validate input
|
||||
# XXX: check datatypes with their validators?
|
||||
import ast
|
||||
try:
|
||||
arg = ast.literal_eval(arg)
|
||||
args = ast.literal_eval(args)
|
||||
except Exception as e:
|
||||
return showErrorDialog(e)
|
||||
result, qualifiers = self._node.execCommand(self._module, command, arg)
|
||||
showCommandResultDialog(command, arg, result, qualifiers)
|
||||
if not args:
|
||||
args = tuple()
|
||||
result, qualifiers = self._node.execCommand(
|
||||
self._module, command, *args)
|
||||
showCommandResultDialog(command, args, result, qualifiers)
|
||||
|
||||
def _initModuleWidgets(self):
|
||||
initValues = self._node.queryCache(self._module)
|
||||
@ -141,11 +163,12 @@ class ModuleCtrl(QWidget):
|
||||
self.cmdWidgets = cmdWidgets = {}
|
||||
# create and insert widgets into our QGridLayout
|
||||
for command in sorted(commands):
|
||||
w = CommandButton(command, [], self._execCommand)
|
||||
# XXX: fetch and use correct datatypes here!
|
||||
w = CommandButton(command, commands[command], self._execCommand)
|
||||
cmdWidgets[command] = w
|
||||
self.commandGroupBox.layout().addWidget(w, row, 0, 1, 0)
|
||||
self.commandGroupBox.layout().addWidget(w, 0, row)
|
||||
row += 1
|
||||
|
||||
row = 0
|
||||
# collect grouping information
|
||||
paramsByGroup = {} # groupname -> [paramnames]
|
||||
allGroups = set()
|
||||
|
@ -157,7 +157,7 @@ class NodeCtrl(QWidget):
|
||||
|
||||
row += 1
|
||||
self._moduleWidgets.extend((label, widget))
|
||||
|
||||
layout.setRowStretch(row, 1)
|
||||
|
||||
class ReadableWidget(QWidget):
|
||||
|
||||
|
@ -163,7 +163,11 @@ p, li { white-space: pre-wrap; }
|
||||
<height>324</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_5"/>
|
||||
<layout class="QGridLayout" name="gridLayout_5">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetMinimumSize</enum>
|
||||
</property>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
|
307
secop/protocol/encoding/demo_v5.py
Normal file
307
secop/protocol/encoding/demo_v5.py
Normal file
@ -0,0 +1,307 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
||||
#
|
||||
# *****************************************************************************
|
||||
"""Encoding/decoding Messages"""
|
||||
|
||||
# implement as class as they may need some internal 'state' later on
|
||||
# (think compressors)
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
#from secop.lib.parsing import format_time
|
||||
from secop.protocol.encoding import MessageEncoder
|
||||
from secop.protocol.messages import *
|
||||
#from secop.protocol.errors import ProtocolError
|
||||
|
||||
import ast
|
||||
import re
|
||||
import json
|
||||
|
||||
# each message is like <messagetype> [ \space <messageargs> [ \space
|
||||
# <json> ]] \lf
|
||||
|
||||
# note: the regex allow <> for spec for testing only!
|
||||
DEMO_RE = re.compile(
|
||||
r"""^(?P<msgtype>[\*\?\w]+)(?:\s(?P<spec>[\w:<>]+)(?:\s(?P<json>.*))?)?$""",
|
||||
re.X)
|
||||
|
||||
#"""
|
||||
# messagetypes:
|
||||
IDENTREQUEST = '*IDN?' # literal
|
||||
# literal! first part is fixed!
|
||||
#IDENTREPLY = 'SECoP, SECoPTCP, V2016-11-30, rc1'
|
||||
#IDENTREPLY = 'SINE2020&ISSE,SECoP,V2016-11-30,rc1'
|
||||
IDENTREPLY = 'SINE2020&ISSE,SECoP,V2017-01-25,rc1'
|
||||
DESCRIPTIONSREQUEST = 'describe' # literal
|
||||
DESCRIPTIONREPLY = 'describing' # +<id> +json
|
||||
ENABLEEVENTSREQUEST = 'activate' # literal
|
||||
ENABLEEVENTSREPLY = 'active' # literal, is end-of-initial-data-transfer
|
||||
DISABLEEVENTSREQUEST = 'deactivate' # literal
|
||||
DISABLEEVENTSREPLY = 'inactive' # literal
|
||||
COMMANDREQUEST = 'do' # +module:command +json args (if needed)
|
||||
# +module:command +json args (if needed) # send after the command finished !
|
||||
COMMANDREPLY = 'done'
|
||||
# +module[:parameter] +json_value -> NO direct reply, calls TRIGGER internally!
|
||||
WRITEREQUEST = 'change'
|
||||
# +module[:parameter] +json_value # send with the read back value
|
||||
WRITEREPLY = 'changed'
|
||||
# +module[:parameter] -> NO direct reply, calls TRIGGER internally!
|
||||
TRIGGERREQUEST = 'read'
|
||||
EVENT = 'update' # +module[:parameter] +json_value (value, qualifiers_as_dict)
|
||||
HEARTBEATREQUEST = 'ping' # +nonce_without_space
|
||||
HEARTBEATREPLY = 'pong' # +nonce_without_space
|
||||
ERRORREPLY = 'error' # +errorclass +json_extended_info
|
||||
HELPREQUEST = 'help' # literal
|
||||
HELPREPLY = 'helping' # +line number +json_text
|
||||
ERRORCLASSES = [
|
||||
'NoSuchDevice',
|
||||
'NoSuchParameter',
|
||||
'NoSuchCommand',
|
||||
'CommandFailed',
|
||||
'ReadOnly',
|
||||
'BadValue',
|
||||
'CommunicationFailed',
|
||||
'IsBusy',
|
||||
'IsError',
|
||||
'ProtocolError',
|
||||
'InternalError',
|
||||
'CommandRunning',
|
||||
'Disabled',
|
||||
]
|
||||
|
||||
# note: above strings need to be unique in the sense, that none is/or
|
||||
# starts with another
|
||||
|
||||
|
||||
def encode_cmd_result(msgobj):
|
||||
q = msgobj.qualifiers.copy()
|
||||
if 't' in q:
|
||||
q['t'] = str(q['t'])
|
||||
return msgobj.result, q
|
||||
|
||||
|
||||
def encode_value_data(vobj):
|
||||
q = vobj.qualifiers.copy()
|
||||
if 't' in q:
|
||||
q['t'] = str(q['t'])
|
||||
return vobj.value, q
|
||||
|
||||
|
||||
def encode_error_msg(emsg):
|
||||
# note: result is JSON-ified....
|
||||
return [
|
||||
emsg.origin, dict((k, getattr(emsg, k)) for k in emsg.ARGS
|
||||
if k != 'origin')
|
||||
]
|
||||
|
||||
|
||||
class DemoEncoder(MessageEncoder):
|
||||
# map of msg to msgtype string as defined above.
|
||||
ENCODEMAP = {
|
||||
IdentifyRequest: (IDENTREQUEST, ),
|
||||
IdentifyReply: (IDENTREPLY, ),
|
||||
DescribeRequest: (DESCRIPTIONSREQUEST, ),
|
||||
DescribeReply: (
|
||||
DESCRIPTIONREPLY,
|
||||
'equipment_id',
|
||||
'description', ),
|
||||
ActivateRequest: (ENABLEEVENTSREQUEST, ),
|
||||
ActivateReply: (ENABLEEVENTSREPLY, ),
|
||||
DeactivateRequest: (DISABLEEVENTSREQUEST, ),
|
||||
DeactivateReply: (DISABLEEVENTSREPLY, ),
|
||||
CommandRequest: (
|
||||
COMMANDREQUEST,
|
||||
lambda msg: "%s:%s" % (msg.module, msg.command),
|
||||
'arguments', ),
|
||||
CommandReply: (
|
||||
COMMANDREPLY,
|
||||
lambda msg: "%s:%s" % (msg.module, msg.command),
|
||||
encode_cmd_result, ),
|
||||
WriteRequest: (
|
||||
WRITEREQUEST,
|
||||
lambda msg: "%s:%s" % (
|
||||
msg.module, msg.parameter) if msg.parameter else msg.module,
|
||||
'value', ),
|
||||
WriteReply: (
|
||||
WRITEREPLY,
|
||||
lambda msg: "%s:%s" % (
|
||||
msg.module, msg.parameter) if msg.parameter else msg.module,
|
||||
'value', ),
|
||||
PollRequest: (
|
||||
TRIGGERREQUEST,
|
||||
lambda msg: "%s:%s" % (
|
||||
msg.module, msg.parameter) if msg.parameter else msg.module,
|
||||
),
|
||||
HeartbeatRequest: (
|
||||
HEARTBEATREQUEST,
|
||||
'nonce', ),
|
||||
HeartbeatReply: (
|
||||
HEARTBEATREPLY,
|
||||
'nonce', ),
|
||||
HelpMessage: (HELPREQUEST, ),
|
||||
ErrorMessage: (
|
||||
ERRORREPLY,
|
||||
"errorclass",
|
||||
encode_error_msg, ),
|
||||
Value: (
|
||||
EVENT,
|
||||
lambda msg: "%s:%s" % (msg.module, msg.parameter or (
|
||||
msg.command + '()')) if msg.parameter or msg.command else msg.module,
|
||||
encode_value_data, ),
|
||||
}
|
||||
DECODEMAP = {
|
||||
IDENTREQUEST: lambda spec, data: IdentifyRequest(),
|
||||
# handled specially, listed here for completeness
|
||||
IDENTREPLY: lambda spec, data: IdentifyReply(encoded),
|
||||
DESCRIPTIONSREQUEST: lambda spec, data: DescribeRequest(),
|
||||
DESCRIPTIONREPLY: lambda spec, data: DescribeReply(equipment_id=spec[0], description=data),
|
||||
ENABLEEVENTSREQUEST: lambda spec, data: ActivateRequest(),
|
||||
ENABLEEVENTSREPLY: lambda spec, data: ActivateReply(),
|
||||
DISABLEEVENTSREQUEST: lambda spec, data: DeactivateRequest(),
|
||||
DISABLEEVENTSREPLY: lambda spec, data: DeactivateReply(),
|
||||
COMMANDREQUEST: lambda spec, data: CommandRequest(module=spec[0], command=spec[1], arguments=data),
|
||||
COMMANDREPLY: lambda spec, data: CommandReply(module=spec[0], command=spec[1], result=data),
|
||||
WRITEREQUEST: lambda spec, data: WriteRequest(module=spec[0], parameter=spec[1], value=data),
|
||||
WRITEREPLY: lambda spec, data: WriteReply(module=spec[0], parameter=spec[1], value=data),
|
||||
TRIGGERREQUEST: lambda spec, data: PollRequest(module=spec[0], parameter=spec[1]),
|
||||
HEARTBEATREQUEST: lambda spec, data: HeartbeatRequest(nonce=spec[0]),
|
||||
HEARTBEATREPLY: lambda spec, data: HeartbeatReply(nonce=spec[0]),
|
||||
HELPREQUEST: lambda spec, data: HelpMessage(),
|
||||
# HELPREPLY: lambda spec, data:None, # ignore this
|
||||
ERRORREPLY: lambda spec, data: ErrorMessage(errorclass=spec[0], errorinfo=data),
|
||||
EVENT: lambda spec, data: Value(module=spec[0], parameter=spec[1], value=data[0], qualifiers=data[1] if len(data) > 1 else {}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwds):
|
||||
MessageEncoder.__init__(self, *args, **kwds)
|
||||
# self.tests()
|
||||
|
||||
def encode(self, msg):
|
||||
"""msg object -> transport layer message"""
|
||||
# fun for Humans
|
||||
if isinstance(msg, HelpMessage):
|
||||
text = """Try one of the following:
|
||||
'%s' to query protocol version
|
||||
'%s' to read the description
|
||||
'%s <module>[:<parameter>]' to request reading a value
|
||||
'%s <module>[:<parameter>] value' to request changing a value
|
||||
'%s <module>[:<command>()]' to execute a command
|
||||
'%s <nonce>' to request a heartbeat response
|
||||
'%s' to activate async updates
|
||||
'%s' to deactivate updates
|
||||
""" % (IDENTREQUEST, DESCRIPTIONSREQUEST, TRIGGERREQUEST,
|
||||
WRITEREQUEST, COMMANDREQUEST, HEARTBEATREQUEST,
|
||||
ENABLEEVENTSREQUEST, DISABLEEVENTSREQUEST)
|
||||
return '\n'.join('%s %d %s' % (HELPREPLY, i + 1, l.strip())
|
||||
for i, l in enumerate(text.split('\n')[:-1]))
|
||||
if isinstance(msg, HeartbeatRequest):
|
||||
if msg.nonce:
|
||||
return 'ping %s' % msg.nonce
|
||||
return 'ping'
|
||||
if isinstance(msg, HeartbeatReply):
|
||||
if msg.nonce:
|
||||
return 'pong %s' % msg.nonce
|
||||
return 'pong'
|
||||
for msgcls, parts in self.ENCODEMAP.items():
|
||||
if isinstance(msg, msgcls):
|
||||
# resolve lambdas
|
||||
parts = [parts[0]] + [
|
||||
p(msg) if callable(p) else getattr(msg, p)
|
||||
for p in parts[1:]
|
||||
]
|
||||
if len(parts) > 1:
|
||||
parts[1] = str(parts[1])
|
||||
if len(parts) == 3:
|
||||
parts[2] = json.dumps(parts[2])
|
||||
return ' '.join(parts)
|
||||
|
||||
def decode(self, encoded):
|
||||
# first check beginning
|
||||
match = DEMO_RE.match(encoded)
|
||||
if not match:
|
||||
print(repr(encoded), repr(IDENTREPLY))
|
||||
if encoded == IDENTREPLY: # XXX:better just check the first 2 parts...
|
||||
return IdentifyReply(version_string=encoded)
|
||||
|
||||
return HelpMessage()
|
||||
# return ErrorMessage(errorclass='Protocol',
|
||||
# errorinfo='Regex did not match!',
|
||||
# is_request=True)
|
||||
msgtype, msgspec, data = match.groups()
|
||||
if msgspec is None and data:
|
||||
return ErrorMessage(
|
||||
errorclass='Internal',
|
||||
errorinfo='Regex matched json, but not spec!',
|
||||
is_request=True,
|
||||
origin=encoded)
|
||||
|
||||
if msgtype in self.DECODEMAP:
|
||||
if msgspec and ':' in msgspec:
|
||||
msgspec = msgspec.split(':', 1)
|
||||
else:
|
||||
msgspec = (msgspec, None)
|
||||
if data:
|
||||
try:
|
||||
data = json.loads(data)
|
||||
except ValueError as err:
|
||||
return ErrorMessage(
|
||||
errorclass='BadValue',
|
||||
errorinfo=[repr(err), str(encoded)],
|
||||
origin=encoded)
|
||||
msg = self.DECODEMAP[msgtype](msgspec, data)
|
||||
msg.setvalue("origin", encoded)
|
||||
return msg
|
||||
return ErrorMessage(
|
||||
errorclass='Protocol',
|
||||
errorinfo='%r: No Such Messagetype defined!' % encoded,
|
||||
is_request=True,
|
||||
origin=encoded)
|
||||
|
||||
def tests(self):
|
||||
print("---- Testing encoding -----")
|
||||
for msgclass, parts in sorted(self.ENCODEMAP.items()):
|
||||
print(msgclass)
|
||||
e = self.encode(
|
||||
msgclass(
|
||||
module='<module>',
|
||||
parameter='<paramname>',
|
||||
value=2.718,
|
||||
equipment_id='<id>',
|
||||
description='descriptive data',
|
||||
command='<cmd>',
|
||||
arguments='<arguments>',
|
||||
nonce='<nonce>',
|
||||
errorclass='InternalError',
|
||||
errorinfo='nix'))
|
||||
print(e)
|
||||
print(self.decode(e))
|
||||
print()
|
||||
print("---- Testing decoding -----")
|
||||
for msgtype, _ in sorted(self.DECODEMAP.items()):
|
||||
msg = '%s a:b 3' % msgtype
|
||||
if msgtype == EVENT:
|
||||
msg = '%s a:b [3,{"t":193868}]' % msgtype
|
||||
print(msg)
|
||||
d = self.decode(msg)
|
||||
print(d)
|
||||
print(self.encode(d))
|
||||
print()
|
||||
print("---- Testing done -----")
|
Loading…
x
Reference in New Issue
Block a user