diff --git a/etc/cryo.cfg b/etc/cryo.cfg
index 474e8b2..88ba4bb 100644
--- a/etc/cryo.cfg
+++ b/etc/cryo.cfg
@@ -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
diff --git a/etc/demo.cfg b/etc/demo.cfg
index 59d8f80..9a2241e 100644
--- a/etc/demo.cfg
+++ b/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
diff --git a/etc/test.cfg b/etc/test.cfg
index a9603cc..e45829f 100644
--- a/etc/test.cfg
+++ b/etc/test.cfg
@@ -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"
diff --git a/secop/datatypes.py b/secop/datatypes.py
index c30c2df..e390906 100644
--- a/secop/datatypes.py
+++ b/secop/datatypes.py
@@ -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)
diff --git a/secop/gui/modulectrl.py b/secop/gui/modulectrl.py
index ad12bf6..45a4a9c 100644
--- a/secop/gui/modulectrl.py
+++ b/secop/gui/modulectrl.py
@@ -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()
diff --git a/secop/gui/nodectrl.py b/secop/gui/nodectrl.py
index 3dd3cb4..c979e9b 100644
--- a/secop/gui/nodectrl.py
+++ b/secop/gui/nodectrl.py
@@ -157,7 +157,7 @@ class NodeCtrl(QWidget):
row += 1
self._moduleWidgets.extend((label, widget))
-
+ layout.setRowStretch(row, 1)
class ReadableWidget(QWidget):
diff --git a/secop/gui/ui/nodectrl.ui b/secop/gui/ui/nodectrl.ui
index f26005e..284e0a4 100644
--- a/secop/gui/ui/nodectrl.ui
+++ b/secop/gui/ui/nodectrl.ui
@@ -163,7 +163,11 @@ p, li { white-space: pre-wrap; }
324
-
+
+
+ QLayout::SetMinimumSize
+
+
diff --git a/secop/protocol/encoding/demo_v5.py b/secop/protocol/encoding/demo_v5.py
new file mode 100644
index 0000000..c4eb18f
--- /dev/null
+++ b/secop/protocol/encoding/demo_v5.py
@@ -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
+#
+# *****************************************************************************
+"""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 [ \space [ \space
+# ]] \lf
+
+# note: the regex allow <> for spec for testing only!
+DEMO_RE = re.compile(
+ r"""^(?P[\*\?\w]+)(?:\s(?P[\w:<>]+)(?:\s(?P.*))?)?$""",
+ 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' # + +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 [:]' to request reading a value
+ '%s [:] value' to request changing a value
+ '%s [:()]' to execute a command
+ '%s ' 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='',
+ parameter='',
+ value=2.718,
+ equipment_id='',
+ description='descriptive data',
+ command='',
+ arguments='',
+ 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 -----")