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