reworking messages
1) start 'bin/secop-server test' 2) connect to localhost port 10767 3) enter help<enter> 4) enjoy Change-Id: I488d5f9cdca8c91c583691ab23f541a4a8759f4e
This commit is contained in:
parent
dc2d0a10aa
commit
b6af55c358
77
bin/secop-console
Executable file
77
bin/secop-console
Executable file
@ -0,0 +1,77 @@
|
||||
#!/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>
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
import os
|
||||
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
|
||||
|
||||
import loggers
|
||||
from secop.client 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')
|
||||
loggers.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))
|
@ -33,10 +33,11 @@ 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] = path.join(basepath, 'src')
|
||||
sys.path[0] = basepath
|
||||
|
||||
import loggers
|
||||
from server import Server
|
||||
from secop import loggers
|
||||
from secop.server import Server
|
||||
|
||||
|
||||
|
||||
|
215
doc/SECoP_Messages.md
Normal file
215
doc/SECoP_Messages.md
Normal file
@ -0,0 +1,215 @@
|
||||
|
||||
SECoP Messages
|
||||
==============
|
||||
|
||||
All Messages are formatted in the same way:
|
||||
<keyword>[<space><specifier>[<space><JSON_formatted_data>]]<linefeed>
|
||||
|
||||
where [] enclose optional parts. This basically results in 3 different possible
|
||||
formattings:
|
||||
|
||||
* type A: "keyword\n"
|
||||
* type B: "keyword specifier\n"
|
||||
* type C: "keyword specifier JSON_data\n"
|
||||
|
||||
Note: numerical values and strings appear 'naturally' formatted in JSON, i.e. 5.0 or "a string"
|
||||
|
||||
<keyword> is one of a fixed list of defined keywords, <specifier> is either the
|
||||
name of the module optionally followed by ':' + the name of a command or parameter,
|
||||
or one of a fixed list of predefined keywords, depending on the message keyword.
|
||||
|
||||
All keywords are defined to be identifiers in the sense, that they are not longer than 63 characters and consist only of letters, digits and underscore and do not start with a digit. (i.e. T_9 is ok, whereas t{9} is not!)
|
||||
|
||||
We rely on the underlying transport to not split messages, i.e. all messages are transported as a whole and no message interrupts another.
|
||||
|
||||
Also: each client MUST at any time correctly handle incoming event messages, even if it didn't activate them!
|
||||
|
||||
Implementation node:
|
||||
Both SEC-node and ECS-client can close the connection at any time!
|
||||
|
||||
list of Intents/Actions:
|
||||
------------------------
|
||||
|
||||
* Identify -> Ident
|
||||
* Describe -> Description
|
||||
* Activate Events -> initial data transfer -> end-of-transfer-marker
|
||||
* Deactivate Async Events -> confirmation
|
||||
* Command <module>:<command> -> confirmation -> result_event
|
||||
* Heartbeat <nonce> -> Heartbeat_reply
|
||||
* Change <module>[:<param>] JSON_VALUE -> confirmation -> readback_event
|
||||
* TRIGGER <module>[:<param>] -> read_event
|
||||
|
||||
At any time an Event may be replied. Each request may also trigger an Error.
|
||||
|
||||
Line-ending is \lf.
|
||||
\cr is ignored and never send.
|
||||
Warning: One Message per line: Description line can be looooong!!!
|
||||
|
||||
|
||||
Allowed combinations as examples:
|
||||
(replace <..> with sensible stuff)
|
||||
|
||||
Identify
|
||||
--------
|
||||
|
||||
* Request: type A: '*IDN?'
|
||||
* Reply: special: 'Sine2020WP7.1&ISSE, SECoP, V2016-11-30, rc1'
|
||||
* queries if SECoP protocol is supported and which version it is
|
||||
Format is intentionally choosen to be compatible to SCPI (for this query only).
|
||||
It is NOT intended to transport information about the manufacturer of the hardware, but to identify this as a SECoP device and transfer the protocol version!
|
||||
|
||||
Describe
|
||||
--------
|
||||
|
||||
* Request: type A: 'describe'
|
||||
* Reply: type C: 'describing <ID> {"modules":{"T1":{"baseclass":"Readable", ....'
|
||||
* request the 'descriptive data'. The format needs to be better defined and
|
||||
may possibly just follow the reference implementation.
|
||||
<ID> identifies the equipment. It should be unique. Our suggestion is to use something along <facility>_<id>, i.e. MLZ_ccr12 or PSI_oven4.
|
||||
|
||||
|
||||
Activate Async Events
|
||||
---------------------
|
||||
|
||||
* Request: type A: 'activate'
|
||||
* Reply: several EVENT lines (initial value transfer) followed by: type A: 'active'
|
||||
* Activates sending of Async Events after transferring all live quantities once
|
||||
and an 'end-of-initial-transfer' marker. After this events are enabled.
|
||||
|
||||
|
||||
Deactivate Async Events
|
||||
-----------------------
|
||||
|
||||
* Request: type A: 'deactivate'
|
||||
* Reply: type A: 'inactive'
|
||||
* Deactivate sending of async Events. A few events may still be on their way until the 'inactive' message arrives.
|
||||
|
||||
|
||||
Execute Command
|
||||
---------------
|
||||
|
||||
* Request: type B: 'do <module>:<command>' for commands without arguments
|
||||
* Request: type C: 'do <module>:<command> JSON_argument' for commands with arguments
|
||||
* Reply: type B: 'doing <module>:<command>' for commands without arguments
|
||||
* Reply: type C: 'doing <module>:<command> JSON_argument' for commands with arguments
|
||||
* start executing a command. When it is finished, an event is send.
|
||||
|
||||
|
||||
Write
|
||||
-----
|
||||
|
||||
* Request: type C: 'change <module>[:<param>] JSON_value'
|
||||
* Reply: type C: 'changing <module>[:<param>] JSON_value' # direct reply
|
||||
* initiate setting a new value for the module or a parameter of it.
|
||||
Once this is done, the new value is confirmed by an event.
|
||||
|
||||
|
||||
Trigger
|
||||
-------
|
||||
|
||||
* Request: type B: 'read <module>[:<param>]'
|
||||
* Reply: None directly. However, one Event with the read value will be send.
|
||||
* Read the requested quantity and sends it as an event (even if events are disabled or the value is not different to the last value).
|
||||
|
||||
|
||||
Heartbeat
|
||||
---------
|
||||
|
||||
* Request: type A: 'ping'
|
||||
* Request: type B: 'ping <nonce>'
|
||||
* Reply: type A: 'pong'
|
||||
* Reply: type B: 'pong <nonce>'
|
||||
* Replies the given argument to check the round-trip-time or to confirm that the connection is still working.
|
||||
<nonce> may not contain <space>. It is suggested to limit to a string of up to 63 chars consisting of letters, digits and underscore not beginning with a digit. If <nonce> is not given (Type A), reply without it.
|
||||
|
||||
|
||||
EVENT
|
||||
-----
|
||||
Events can be emitted any time from the SEC-node (except if they would interrupt another message).
|
||||
|
||||
* Request: None. Events can be requested by Command, Change or Trigger or by Activating Async Mode.
|
||||
* Reply: type C: 'update <module>[:<param>] JSON_VALUE' # follows a TRIGGER
|
||||
* Reply: type C: 'changed <module>[:<param>] JSON_VALUE' # follows a CHANGE
|
||||
* Reply: type B: 'done <module>:<command>' # follows a COMMAND without return value
|
||||
* Reply: type C: 'done <module>:<command> JSON_VALUE' # follows a COMMAND with return value
|
||||
* Informs the client that a value has changed its value or that a command is finished (and what the return value, if any, was).
|
||||
In any case the JSON_value contain the available qualifiers as well:
|
||||
* "t" for the timestamp of the event.
|
||||
* "e" for the error of the value.
|
||||
* "u" for the unit of the value, if deviating from the descriptive data
|
||||
* further qualifiers, if needed, may be specified.
|
||||
The qualifiers are a dictionary at position 2 of a list, where the value occupies position 1.
|
||||
This holds true also for complex datatypes!
|
||||
|
||||
examples:
|
||||
|
||||
* 'update T1 [3.479, {"t":"149128925.914882", "e":0.01924}]
|
||||
* 'changed T1:p [12, {"t":"149128927.193725"}]'
|
||||
* 'done T1:stop'
|
||||
* 'update Vector [[0.01, 12.49, 3.92], {"t":"149128925.914882"}]'
|
||||
|
||||
|
||||
ERROR
|
||||
-----
|
||||
|
||||
* Request: None. can only be a reply if some request fails.
|
||||
* Reply: type C: 'ERROR <errorclass> JSON_additional_stuff'
|
||||
* Following <errorclass> are defined so far:
|
||||
* NoSuchDevice: The action can not be performed as the specified device is non-existent.
|
||||
* NoSuchParameter: The action can not be performed as the specified parameter is non-existent.
|
||||
* NoSuchCommand: The specified command does not exist.
|
||||
* CommandFailed: The command failed to execute.
|
||||
* CommandRunning: The command is already executing.
|
||||
* ReadOnly: The requested write can not be performed on a readonly value..
|
||||
* BadValue: The requested write or Command can not be performed as the value is malformed or of wrong type.
|
||||
* CommunicationFailed: Some communication (with hardware controlled by this SEC-Node) failed.
|
||||
* IsBusy: The reequested write can not be performed while the Module is Busy
|
||||
* IsError: The requested action can not be performed while the module is in error state.
|
||||
* Disabled: The requested action can not be performed at the moment. (Interlocks?)
|
||||
* SyntaxError: A malformed Request was send
|
||||
* InternalError: Something that should never happen just happened.
|
||||
The JSON part should reference the offending request and give an explanatory string.
|
||||
|
||||
examples:
|
||||
|
||||
* 'ERROR Disabled ["change", "V15", "on", "Air pressure too low to actuate the valve.", {"exception":"RuntimeException","file":"devices/blub/valve.py", "line":13127, "frames":[...]}]'
|
||||
* 'ERROR NoSuchDevice ["read","v19", "v19 is not configured on this SEC-node"]'
|
||||
* 'ERROR SyntaxError "meas:Volt?"
|
||||
|
||||
|
||||
Example
|
||||
=======
|
||||
|
||||
<pre>
|
||||
(client connects):
|
||||
(client) '*IDN?'
|
||||
(SEC-node) 'Sine2020WP7.1&ISSE, SECoP, V2016-11-30, rc1'
|
||||
(client) 'describe'
|
||||
(SEC-node) 'describing SECoP_Testing {"modules":{"T1":{"baseclass":"Readable", ...
|
||||
(client) 'activate'
|
||||
(SEC-node) 'update T1 [3.45,{"t":"149128925.914882","e":0.01924}]'
|
||||
...
|
||||
(SEC-node) 'active'
|
||||
(SEC-node) 'update T1 [3.46,{"t":"149128935.914882","e":0.01912}]'
|
||||
(client) 'ping fancy_nonce_37'
|
||||
(SEC-node) 'pong fancy_nonce_37'
|
||||
(SEC-node) 'update T1 [3.49,{"t":"149128945.921397","e":0.01897}]'
|
||||
...
|
||||
</pre>
|
||||
|
||||
Discussion & open Points
|
||||
========================
|
||||
|
||||
* If more than one connection exists: shall all events be relayed to all listeners?
|
||||
* how about WRITE/COMMAND replies? Shall they go to all connected clients?
|
||||
* structure of descriptive data needs to be specified
|
||||
* same for JSON_stuff for Error Messages
|
||||
* 'changed' may be 'readback'
|
||||
* 'change' may be 'write'
|
||||
* 'read' may be 'poll'
|
||||
* the whole message may be json object (bigger, uglier to read)
|
||||
* which events are broadcast or unicast?
|
||||
* do we need a way to correlate a reply with a request?
|
||||
* ...
|
||||
|
||||
|
277
doc/messages.md
Normal file
277
doc/messages.md
Normal file
@ -0,0 +1,277 @@
|
||||
|
||||
Struktur der Messages
|
||||
=====================
|
||||
|
||||
Es gibt folgende Messagetypen:
|
||||
|
||||
* LIST (listet namen einer Ebene auf)
|
||||
* READ+WRITE (addressiert einen spezifischen Wert)
|
||||
* FETCH (kombiniert LIST+READ, antwort ist multi)
|
||||
* COMMAND (um Befehle aufzurufen: stop(), init(),...)
|
||||
* (UN)SUBSCRIBE (bucht events, bestellt sie ab, für devices/params)
|
||||
* EVENT (ASYNC!)
|
||||
* ERROR
|
||||
* POLL (wie FETCH, aber fragt vorher die HW, kann dauern)
|
||||
* TRIGGER (Wie POLL, aber die Antworten kommen als Events, return sofort)
|
||||
|
||||
Außer EVENT und ERROR (beides nur als Reply) gibts es jede Message als Request und als Reply.
|
||||
Das Parsing einer Message ist eindeutig, so hat jeder reply mindestens ein '=', jede Mehrfachantwort endet mit '`#<anzahl messages>`', uswusf.
|
||||
Für das Parsing wird eine Regexp empfohlen, da die syntax nicht sonderlich parsingfreundlich ist.
|
||||
|
||||
Um auszuwählen, auf welche Objekte eine Nachricht wirkt, werden folgende Argumente verwendet:
|
||||
|
||||
- `*` wirkt auf alle devices
|
||||
- `device` wirkt auf das Device
|
||||
- `device:*` wirkt auf alle parameter des gegebenen devices
|
||||
- `device:param` wirkt auf den Parameter des Device
|
||||
- `device:param:*` wirkt auf alle properties des gegebenen parameters
|
||||
- `device:param:property` wirkt auf die property
|
||||
|
||||
Properties sind immer ReadOnly Softwarewerte. Somit haben WRITE, TRIGGER, (UN)SUBSCRIBE auf properties keinen sinn und sind mit einem Error zu beantworten.
|
||||
(welcher? ProtokollError?=
|
||||
|
||||
Replies enthalten immer genau eine Antwort.
|
||||
Der immer anzugebende 'Antwortwert' wird durch `=` von einer kopie des requestes abgetrennt.
|
||||
Für Multi-Antworten endet die Antwort in `#<number of replyitems>\n` statt in `\n`.
|
||||
Hier ist kein '=' zusätzlich anzugeben. Nach dieser 'Eröffnungsnachricht' kommen die angegebene Anzahl Antworten als Read-Replies.
|
||||
|
||||
Damit ergeben sich folgende Kombinationen (immer zuerst der Request, direkt drunter der Reply, direkt drunter die Beschreibung):
|
||||
Danach die 'dringlichkeit des implementierens':
|
||||
MANDATORY > RECOMMENDED > OPTIONAL
|
||||
|
||||
Werte sind entweder Zahlen (`1.23`) oder Strings (`"Ein String"` oder `Text`).
|
||||
Solange eindeutigkeit besteht (kein '" ", ",", oder " im String) können die `"` weggelassen werden. Beispiel: unit=T
|
||||
|
||||
|
||||
LIST
|
||||
----
|
||||
|
||||
* 'LIST *' oder 'LIST'
|
||||
* 'LIST *=mf,tc1,tc1,ts,...' oder 'LIST=mf,tc1,...'
|
||||
* ListDevices: returns ',' separated list of devicenames
|
||||
* MANDATORY
|
||||
|
||||
---------------
|
||||
|
||||
* 'LIST `<devname>`'
|
||||
* 'LIST `<devname>`=status, target, value,...'
|
||||
* ListParameters: returns ',' separated list of parameternames for a device
|
||||
* MANDATORY
|
||||
|
||||
---------------
|
||||
|
||||
* 'LIST `<devname>:<paramname>`'
|
||||
* 'LIST `<devname>:<paramname>`=timestamp, unit, description,...'
|
||||
* ListProperties: returns ',' separated list of propertynames for a parameter
|
||||
* MANDATORY
|
||||
|
||||
|
||||
READ/WRITE
|
||||
----------
|
||||
|
||||
* 'READ `<devname>`'
|
||||
* 'READ `<devname>=<value>; [<qualname> '=' <qualvalue> ';']`'
|
||||
* ReadDevice: returns current device value + qualifiers
|
||||
* MANDATORY
|
||||
|
||||
---------
|
||||
|
||||
* 'READ `<devname>:<paramname>`'
|
||||
* 'READ `<devname>:<paramname>=<value>; [<qualname> '=' <qualvalue> ';']`'
|
||||
* ReadParameter: returns current parameter value (+ qualifiers?)
|
||||
* MANDATORY
|
||||
|
||||
--------
|
||||
|
||||
* 'READ `<devname>:<paramname>:<property>`'
|
||||
* 'READ `<devname>:<paramname>:<property>=<value>;`'
|
||||
* ReadProperty: returns curent value of property
|
||||
* RECOMMENDED
|
||||
|
||||
--------
|
||||
|
||||
* 'WRITE `<devname>=<value>`'
|
||||
* 'WRITE `<devname>=<value>=<readbackvalue>; [<qualname> '=' <qualvalue> ';']`'
|
||||
* WriteDevice: sets new device-value and returns read-back target value + non-empty qualifiers! (at least the `;` must be present)
|
||||
* MANDATORY
|
||||
|
||||
--------
|
||||
|
||||
* 'WRITE `<devname>:<paramname>=<value>`'
|
||||
* 'WRITE `<devname>:<paramname>=<value>=<readbackvalue>; [<qualname> '=' <qualvalue> ';']`'
|
||||
* WriteParameter: sets new parameter-value and returns read-back value + non-empty qualifiers! (at least the `;` must be present)
|
||||
* MANDATORY
|
||||
|
||||
|
||||
COMMAND
|
||||
-------
|
||||
|
||||
* 'COMMAND `<device>:<command>'(' [<argument> ','] ')'`'
|
||||
* 'COMMAND `<device>:<command>'(' [<argument> ','] ')=' result`;'
|
||||
* ExecuteCommand: führt command mit den gegebenen Arguments aus.
|
||||
result=(ein) Rückgabewert, kann auch "OK" sein, falls kein Rückgabewert definiert wurde.
|
||||
* MANDATORY
|
||||
|
||||
commands sind parameter deren name auf '()' endet.
|
||||
(oder die argumenttypen in () enthält?)
|
||||
|
||||
(UN)SUBSCRIBE
|
||||
-------------
|
||||
|
||||
* 'SUBSCRIBE `<device>`'
|
||||
* 'SUBSCRIBE `<device>=OK`;'
|
||||
* SubscribeDevice: subscribed auf den devicevalue (evtl auch auf den status?)
|
||||
* RECOMMENDED
|
||||
* possible extension: include a 'FETCH `<device>`' reply as Multi
|
||||
|
||||
--------
|
||||
|
||||
* 'SUBSCRIBE `<device>`'
|
||||
* 'SUBSCRIBE `<device>=<list_of_subscribed_parameternames>`;'
|
||||
* SubscribeALLParameter: subscribed alle parameter eines device
|
||||
* RECOMMENDED
|
||||
* possible extension: include a 'FETCH `<device>:`' reply as Multi
|
||||
|
||||
--------
|
||||
|
||||
* 'SUBSCRIBE `<device>:<param>`'
|
||||
* 'SUBSCRIBE `<device>:<param>=OK`;'
|
||||
* SubscribeParameter: subscribed auf den parameter
|
||||
* RECOMMENDED
|
||||
* possible extension: include a 'FETCH `<device>:<param>`' reply as Multi
|
||||
|
||||
--------
|
||||
|
||||
* 'UNSUBSCRIBE `<device>`'
|
||||
* 'UNSUBSCRIBE `<device>=OK`;'
|
||||
* UNSubscribeDevice: unsubscribed auf den devicevalue
|
||||
* RECOMMENDED
|
||||
* possible extension: return list of remaining subscriptions as multi
|
||||
|
||||
--------
|
||||
|
||||
* 'UNSUBSCRIBE `<device>:`'
|
||||
* 'UNSUBSCRIBE `<device>:=OK`;'
|
||||
* UNSubscribeALLParameter: unsubscribed alle parameter eines device
|
||||
* RECOMMENDED
|
||||
* possible extension: return list of remaining subscriptions as multi
|
||||
|
||||
--------
|
||||
|
||||
* 'UNSUBSCRIBE `<device>:<param>`'
|
||||
* 'UNSUBSCRIBE `<device>:<param>=OK`;'
|
||||
* UNSubscribeParameter: unsubscribed auf den parameter
|
||||
* RECOMMENDED
|
||||
* possible extension: return list of remaining subscriptions as multi
|
||||
|
||||
Was ist zu tun bei einem unsubscribe auf einen nicht subscribten wert?
|
||||
(doppeltes unsubscribe nach subscribe, etc...)
|
||||
|
||||
EVENT
|
||||
-----
|
||||
|
||||
* EVENT gibt es nicht als Request, da es nur als async reply auftaucht
|
||||
* '`#3\n`EVENT READ `mf=1.2\n`EVENT READ `mf:target=2.0\n`EVENT READ `mf:status="BUSY"\n`'
|
||||
* Event: sendet ein subscribed event, kann 0..N READ-replies beinhalten
|
||||
* RECOMMENDED
|
||||
|
||||
FETCH/POLL
|
||||
----------
|
||||
|
||||
* 'FETCH :' oder 'FETCH'
|
||||
* 'FETCH `:#2\nREAD mf=1.2\nREAD ts=3.4\n`' oder 'FETCH`#2\nREAD mf=1.2\nREAD ts=3.4\n`'
|
||||
* FetchDevices: reads and returns the values of all (interesting?) devices
|
||||
* OPTIONAL
|
||||
|
||||
--------
|
||||
|
||||
* 'FETCH `<device>`'
|
||||
* 'FETCH mf#2\nREAD mf:value=1.2\nREAD mf:status="IDLE"\n`'
|
||||
* FetchDevice: reads and returns the (interesting?) parameters of a device
|
||||
* OPTIONAL
|
||||
|
||||
--------
|
||||
|
||||
* 'FETCH `<device>:`'
|
||||
* 'FETCH `mf:#3\nREAD mf:value=1.2\nREAD mf:target=1.2\nREAD mf:status="IDLE"\n`'
|
||||
* FetchParameters: reads and returns the values of all parameters of a device
|
||||
* OPTIONAL
|
||||
|
||||
--------
|
||||
|
||||
* 'FETCH `<device>:<parameter>`'
|
||||
* 'FETCH `mf:value#2\nREAD mf:value:unit="T"\nREAD mf:value:type=float\n`'
|
||||
* FetchParameter: reads and returns the properties of a single parameter
|
||||
* OPTIONAL
|
||||
|
||||
--------
|
||||
|
||||
* 'FETCH `<device>:<parameter>:`'
|
||||
* 'FETCH `mf:value:#2\nREAD mf:value:unit="T"\nREAD mf:value:type=float\n`'
|
||||
* FetchProperties: reads and returns the values of all properties of a parameter
|
||||
* OPTIONAL
|
||||
|
||||
POLL wird wie FETCH kodiert, fragt aber die HW vor der Antwort, FECTH liefert zwischengespeicherte Werte.
|
||||
|
||||
TRIGGER
|
||||
-------
|
||||
|
||||
* 'TRIGGER :' oder 'TRIGGER'
|
||||
* 'TRIGGER :=OK' oder 'TRIGGER=OK'
|
||||
* TriggerDeviceReads: startet auslesen aller devices und übertragen der (subscribed) values als events
|
||||
* OPTIONAL
|
||||
|
||||
--------
|
||||
|
||||
* 'TRIGGER `<device>`'
|
||||
* 'TRIGGER `mf=OK`'
|
||||
* TriggerDeviceRead: startet auslesen eines Devices
|
||||
* OPTIONAL
|
||||
|
||||
--------
|
||||
|
||||
* 'TRIGGER `<device>:`'
|
||||
* 'TRIGGER `mf:=OK`'
|
||||
* TriggerParameterReads: startet auslesen aller paremeter und übertragen der subscribed parameter als events
|
||||
* OPTIONAL
|
||||
|
||||
--------
|
||||
|
||||
* 'TRIGGER `<device>:<parameter>`'
|
||||
* 'TRIGGER `mf:value=OK`'
|
||||
* FetchProperties: reads and returns the values of all properties of a parameter
|
||||
* OPTIONAL
|
||||
|
||||
ERROR
|
||||
-----
|
||||
|
||||
* ERROR gibt es nicht als request, da es nur als reply auftaucht
|
||||
* 'ERROR `<errorclass> "<copy of request>" [<additional text>]`'
|
||||
* Error: zeigt einen Fehler an. folgende <errorclass> sind definiert:
|
||||
* NoSuchDevice
|
||||
* NoSuchParameter
|
||||
* NoSuchCommand
|
||||
* NoSuchProperty
|
||||
* CommandFailed
|
||||
* ReadOnly
|
||||
* BadValue
|
||||
* CommunicationFailed
|
||||
* IsBusy
|
||||
* IsError
|
||||
* ProtocolError
|
||||
* SyntaxError
|
||||
* MANDATORY
|
||||
|
||||
|
||||
Möglich Erweiterung: für device/param/property kann statt eines einzelnamens auch eine ',' separierte Liste verwendet werden.
|
||||
Außerdem könnte auch ein '*' für 'ALLE' stehen.
|
||||
Die Antworten sind dann auf jeden Fall als Multi zu kodieren. Beispiel:
|
||||
|
||||
> READ mf:target,value
|
||||
> > READ mf:target,value#2
|
||||
>
|
||||
> > READ mf:target=1.23
|
||||
>
|
||||
> > READ mf:value=0.73
|
||||
>
|
||||
|
372
doc/protocols/simple_communication_protocol.rst
Normal file
372
doc/protocols/simple_communication_protocol.rst
Normal file
@ -0,0 +1,372 @@
|
||||
Simple communication protocol
|
||||
=============================
|
||||
| *Version 0.0.2*
|
||||
| *Copyright 2012: Alexander Lenz, Dr. Enrico Faulhaber*
|
||||
|
||||
|
||||
Table of contents
|
||||
-----------------
|
||||
|
||||
.. contents::
|
||||
.. sectnum::
|
||||
|
||||
Disambiguation
|
||||
--------------
|
||||
|
||||
Device
|
||||
''''''
|
||||
A device is a logical part of the complete system. This may be any piece of hardware which
|
||||
can be accessed seperately. Also a logical axis, implemented with multiple motors can be a device.
|
||||
|
||||
Parameter
|
||||
'''''''''
|
||||
A parameter is a device depended value which represents (usually) a physical value.
|
||||
It can be read only or read-/writeable.
|
||||
|
||||
Messages
|
||||
--------
|
||||
|
||||
The messages are devided into commands and responses.
|
||||
|
||||
A command consists of the device of interest, the relevant parameter, an operator
|
||||
that specifies what should happen, and a value if neccessary.
|
||||
|
||||
::
|
||||
|
||||
Commands: <device>/<parameter><operator><value_if_any>\n
|
||||
|
||||
You will get a response for each command (\ **even if it failed!**\ ).
|
||||
These reponses consist of an error code, the mirrored command (to verify for what command the response is related)
|
||||
and a value if requested.
|
||||
|
||||
::
|
||||
|
||||
Response: <error_code> <mirrored_command><value_if_any>\n
|
||||
|
||||
|
||||
For limitations regarding the message contents, have a look at: `Limitations`_
|
||||
|
||||
|
||||
|
||||
Operators
|
||||
---------
|
||||
|
||||
? (Request)
|
||||
'''''''''''
|
||||
This operator can be used to request data.
|
||||
In the most cases, you want to request the value of device parameter:
|
||||
|
||||
**Command**
|
||||
::
|
||||
|
||||
<device>/<parameter>?\n
|
||||
|
||||
**Response**
|
||||
::
|
||||
|
||||
<error_code> <device>/<parameter>=<value_if_success>\n
|
||||
|
||||
= (Set)
|
||||
'''''''
|
||||
This operator can be used to write a device value.
|
||||
|
||||
**Command**
|
||||
::
|
||||
|
||||
<device>/<parameter>=<value>\n
|
||||
|
||||
**Response**
|
||||
::
|
||||
|
||||
<error_code> <device>/<parameter>=<value>\n
|
||||
|
||||
|
||||
Protocol error codes
|
||||
--------------------
|
||||
|
||||
In case of an error, you get the following response:
|
||||
|
||||
::
|
||||
|
||||
<error_code> <mirrored_command>
|
||||
|
||||
|
||||
The following errors describe errors of the protocol, not the device.
|
||||
|
||||
======================= ==============
|
||||
**Error** **Error code**
|
||||
======================= ==============
|
||||
No error 0
|
||||
Unknown error 1
|
||||
Connection error 2
|
||||
Command unknown 3
|
||||
Device unknown 4
|
||||
Parameter unknown 5
|
||||
Format error 6
|
||||
Value out of limits 7
|
||||
Param not writable 8
|
||||
Not allowed 9
|
||||
======================= ==============
|
||||
|
||||
Device stati
|
||||
------------
|
||||
|
||||
The following status codes describe the device status and can be requested via:
|
||||
|
||||
::
|
||||
|
||||
<device>/status:\n
|
||||
|
||||
|
||||
========== ===========
|
||||
**Status** **Meaning**
|
||||
========== ===========
|
||||
IDLE Device is alive and ready to accept commands.
|
||||
BUSY Device is performing some action and therefore busy. It doesn't accept new commands. All Parameters can be read.
|
||||
ERROR Something bad happened, a manual action is required. Some command could unexpectedly not be performed.
|
||||
UNKNOWN Unknown device state.
|
||||
========== ===========
|
||||
|
||||
|
||||
Limitations
|
||||
-----------
|
||||
|
||||
Naming & Formatting
|
||||
'''''''''''''''''''
|
||||
|
||||
- Device names are all lower case and can consist of letters, numbers and underscores (no whitespace!).
|
||||
- Device names consist of up to 80 characters.
|
||||
- Parameter names are all lower case and can consist of letters, numbers and underscores (no whitespace!).
|
||||
- Parameter names consist of up to 80 characters.
|
||||
- Floating point numbers are using a decimal point (5.23).
|
||||
- Lists are commma-separated and enclosed by square brackets ([entry1,entry2,entry3]).
|
||||
- Strings are enclosed by single ticks ('str').
|
||||
- Messages consist of up to 256 characters.
|
||||
|
||||
Devices
|
||||
'''''''
|
||||
|
||||
General
|
||||
"""""""
|
||||
All devices have to support at least the following parameters:
|
||||
|
||||
**status**
|
||||
This **read only** parameters describes the current device state.
|
||||
It contains a list with two items.
|
||||
|
||||
1. A short constant status string (Have a look at: `Device stati`_)
|
||||
2. A longer description which can contain any character except a comma!
|
||||
|
||||
**parameters**
|
||||
This **read only** parameter represents a list of all available parameters of the given device.
|
||||
It contains a comma seperated list with all parameter names.
|
||||
|
||||
Readable
|
||||
""""""""
|
||||
All devices which provide any type of readable value have to support the general parameters and at leas the following:
|
||||
**value**
|
||||
This **read only** parameter represents the current 'main value'.
|
||||
It contains a single value which can be a float or integer number, or a string.
|
||||
|
||||
Writable
|
||||
""""""""
|
||||
All devices for which you can set a value have to support at least the general parameters, all parameters of `Readable`_ devices and the following:
|
||||
**target**
|
||||
This **read/write** parameter represents the device's target value.
|
||||
It contains a single value which can be a float or integer number, or a string.
|
||||
If you set the target value, the device goes into 'BUSY' state and tries to reach that value.
|
||||
The current value can be requested by the 'value' parameter.
|
||||
|
||||
Server device
|
||||
"""""""""""""
|
||||
The server have to provide a device for direct communication with the protocol server.
|
||||
It has to provide at least the parameters of a general device (`Devices`_) plus the following:
|
||||
|
||||
**devices**
|
||||
A list of all available devices.
|
||||
**version**
|
||||
A version string which identifies the protocol version, the server implements.
|
||||
|
||||
The server device can be queried by omitting the <device> parameter (+ the '/').
|
||||
::
|
||||
|
||||
devices?\n
|
||||
version?\n
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Let's have a look at some examples:
|
||||
|
||||
+---------------+---------------------------------+
|
||||
|**Device:** |temp_ctrl |
|
||||
+---------------+---------------------------------+
|
||||
|**Type:** |Temperature controller (Moveable)|
|
||||
+---------------+---------------------------------+
|
||||
|**Parameters:**| - status **(mandatory)** |
|
||||
| | - parameters **(mandatory)** |
|
||||
| | - value **(mandatory)** |
|
||||
| | - target **(mandatory)** |
|
||||
| | - ... |
|
||||
+---------------+---------------------------------+
|
||||
|
||||
Requesting the current setpoint (target)
|
||||
''''''''''''''''''''''''''''''''''''''''
|
||||
|
||||
**Command**
|
||||
|
||||
::
|
||||
|
||||
temp_ctrl/target?\n
|
||||
|
||||
**Response**
|
||||
|
||||
::
|
||||
|
||||
0 temp_ctrl/target=0.42\n
|
||||
|
||||
Setting the setpoint (target)
|
||||
'''''''''''''''''''''''''''''
|
||||
|
||||
**Command**
|
||||
|
||||
::
|
||||
|
||||
temp_ctrl/target=0.21\n
|
||||
|
||||
**Response**
|
||||
|
||||
::
|
||||
|
||||
0 temp_ctrl/target=0.21\n
|
||||
|
||||
Setting an out-of-bounds setpoint (target)
|
||||
''''''''''''''''''''''''''''''''''''''''''
|
||||
|
||||
**Command**
|
||||
|
||||
::
|
||||
|
||||
temp_ctrl/target=-7.5\n
|
||||
|
||||
**Response**
|
||||
|
||||
::
|
||||
|
||||
7 temp_ctrl/target=-7.5\n
|
||||
|
||||
|
||||
Requesting the status
|
||||
'''''''''''''''''''''
|
||||
|
||||
**Command**
|
||||
|
||||
::
|
||||
|
||||
temp_ctrl/status?\n
|
||||
|
||||
**Response**
|
||||
|
||||
::
|
||||
|
||||
0 temp_ctrl/status=BUSY,I'm ramping!\n
|
||||
|
||||
Requesting the device list
|
||||
''''''''''''''''''''''''''
|
||||
|
||||
**Command**
|
||||
|
||||
::
|
||||
|
||||
/devices?\n
|
||||
|
||||
**Response**
|
||||
|
||||
::
|
||||
|
||||
0 /devices=temp_ctrl,another_dev1,another_dev2\n
|
||||
|
||||
.. Allowed extensions
|
||||
------------------
|
||||
|
||||
Additional operators
|
||||
''''''''''''''''''''
|
||||
|
||||
\* (Wildcard)
|
||||
"""""""""""""
|
||||
This operator is a little more advanced than the others.
|
||||
It represents a wild card and can be combined with other operators.
|
||||
The response you will get, are multiple messages which contain:
|
||||
::
|
||||
|
||||
<error_code> <the_mirrored_command> <answer_for_the_operator>
|
||||
|
||||
If you want to request all parameters of a device, it will be:
|
||||
|
||||
**Command**
|
||||
::
|
||||
|
||||
<device>/*?\n
|
||||
|
||||
**Response**
|
||||
*Multiple*
|
||||
::
|
||||
|
||||
<error_code> <device>/*? <device>/<parameter>=<value>\n
|
||||
|
||||
Examples
|
||||
^^^^^^^^
|
||||
Requesting all parameters
|
||||
*************************
|
||||
|
||||
**Command**
|
||||
|
||||
::
|
||||
|
||||
temp_ctrl/*?\n
|
||||
|
||||
**Response**
|
||||
::
|
||||
|
||||
0 temp_ctrl/*? temp_ctrl/status=BUSY,I'm ramping!\n
|
||||
0 temp_ctrl/*? temp_ctrl/parameters=status,parameters,value,target\n
|
||||
0 temp_ctrl/*? temp_ctrl/value=0.21\n
|
||||
0 temp_ctrl/*? temp_ctrl/target=0.42\n
|
||||
|
||||
Recommendations
|
||||
---------------
|
||||
Interfaces
|
||||
''''''''''
|
||||
|
||||
We provide some recommendations for the interface configuration when using the simple communication protocol:
|
||||
|
||||
Serial (RS232)
|
||||
""""""""""""""
|
||||
|
||||
If you are using a serial connection, you should use the following configuration:
|
||||
|
||||
============= ==============
|
||||
**Baudrate** 9600 or 115200
|
||||
**Data bits** 8
|
||||
**Parity** None
|
||||
**Stop bits** 1
|
||||
============= ==============
|
||||
|
||||
Network (TCP)
|
||||
"""""""""""""
|
||||
|
||||
If you are using a TCP based network connection, you should use the following configuration:
|
||||
|
||||
======== =====
|
||||
**Port** 14728
|
||||
======== =====
|
||||
|
||||
Network (UDP)
|
||||
"""""""""""""
|
||||
|
||||
We recommend not to use UDP connections at all, as the protocol was not designed for such connections.
|
||||
|
||||
|
||||
|
||||
|
18
etc/test.cfg
18
etc/test.cfg
@ -1,10 +1,22 @@
|
||||
[server]
|
||||
bindto=0.0.0.0
|
||||
bindport=10767
|
||||
[equipment]
|
||||
id=Fancy_ID_without_spaces-like:MLZ_furnace7
|
||||
|
||||
[client]
|
||||
connect=0.0.0.0
|
||||
port=10767
|
||||
interface = tcp
|
||||
framing=eol
|
||||
encoding=text
|
||||
|
||||
[interface testing]
|
||||
interface=tcp
|
||||
bindto=0.0.0.0
|
||||
bindport=10767
|
||||
# protocol to use for this interface
|
||||
framing=eol
|
||||
encoding=demo
|
||||
|
||||
|
||||
[device LN2]
|
||||
class=devices.test.LN2
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
# for generating docu
|
||||
markdown>=2.6
|
||||
# general stuff
|
||||
# daemonizing
|
||||
psutil
|
||||
python-daemon >=2.0
|
||||
# for zmq
|
||||
# for zmq interface
|
||||
#pyzmq>=13.1.0
|
||||
|
||||
|
192
secop/client/__init__.py
Normal file
192
secop/client/__init__.py
Normal file
@ -0,0 +1,192 @@
|
||||
# -*- 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"""
|
||||
|
||||
# nothing here yet.
|
||||
|
||||
|
||||
import code
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
import ConfigParser
|
||||
|
||||
|
||||
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'))
|
||||
|
||||
|
||||
from os import path
|
||||
|
||||
|
||||
class ClientConsole(object):
|
||||
|
||||
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)
|
||||
|
||||
import loggers
|
||||
import socket
|
||||
import threading
|
||||
from collections import deque
|
||||
from secop.protocol.transport import FRAMERS, ENCODERS
|
||||
from secop.protocol.messages import *
|
||||
|
||||
|
||||
class TCPConnection(object):
|
||||
|
||||
def __init__(self, connect, port, encoding, framing, **kwds):
|
||||
self.log = loggers.log.getChild('connection', False)
|
||||
self.encoder = ENCODERS[encoding]()
|
||||
self.framer = FRAMERS[framing]()
|
||||
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)
|
||||
frame = self.encoder.encode(msg)
|
||||
data = self.framer.encode(frame)
|
||||
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):
|
||||
while True:
|
||||
data = self.connection.recv(1024)
|
||||
self.log.debug("RCV: got raw data %r" % data)
|
||||
if data:
|
||||
frames = self.framer.decode(data)
|
||||
self.log.debug("RCV: frames %r" % frames)
|
||||
for frame in frames:
|
||||
msgs = self.encoder.decode(frame)
|
||||
self.log.debug("RCV: msgs %r" % msgs)
|
||||
for msg in msgs:
|
||||
self.handle(msg)
|
||||
|
||||
def handle(self, msg):
|
||||
if isinstance(msg, AsyncDataUnit):
|
||||
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 len(self.queue):
|
||||
pass
|
||||
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)
|
||||
|
||||
|
||||
import loggers
|
||||
|
||||
|
||||
class Client(object):
|
||||
|
||||
def __init__(self, opts):
|
||||
self.log = loggers.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)
|
||||
device = msg.device
|
||||
param = msg.param
|
||||
value = msg.value
|
||||
self._cache.getdefault(device, {})[param] = value
|
||||
# XXX: further notification-callbacks needed ???
|
||||
|
||||
def populateNamespace(self, namespace):
|
||||
self.connection.send(ListDevicesRequest())
|
||||
# reply = self.connection.read()
|
||||
# self.log.info("found devices %r" % reply)
|
||||
# create proxies, populate cache....
|
||||
namespace.setconst('connection', self.connection)
|
@ -30,12 +30,13 @@
|
||||
import time
|
||||
import types
|
||||
import inspect
|
||||
import threading
|
||||
|
||||
from errors import ConfigError, ProgrammingError
|
||||
from protocol import status
|
||||
from validators import mapping, vector
|
||||
from secop.errors import ConfigError, ProgrammingError
|
||||
from secop.protocol import status
|
||||
from secop.validators import enum, vector, floatrange
|
||||
|
||||
EVENT_ONLY_ON_CHANGED_VALUES = True
|
||||
EVENT_ONLY_ON_CHANGED_VALUES = False
|
||||
|
||||
# storage for PARAMeter settings:
|
||||
# if readonly is False, the value can be changed (by code, or remote)
|
||||
@ -159,13 +160,15 @@ class DeviceMeta(type):
|
||||
setattr(newtype, 'CMDS', getattr(newtype, 'CMDS', {}))
|
||||
for name in attrs:
|
||||
if name.startswith('do'):
|
||||
if name[2:] in newtype.CMDS:
|
||||
continue
|
||||
value = getattr(newtype, name)
|
||||
if isinstance(value, types.MethodType):
|
||||
argspec = inspect.getargspec(value)
|
||||
if argspec[0] and argspec[0][0] == 'self':
|
||||
del argspec[0][0]
|
||||
newtype.CMDS[name] = CMD(value.get('__doc__', name),
|
||||
*argspec)
|
||||
newtype.CMDS[name[2:]] = CMD(getattr(value, '__doc__'),
|
||||
argspec.args, None) # XXX: find resulttype!
|
||||
attrs['__constructed__'] = True
|
||||
return newtype
|
||||
|
||||
@ -202,8 +205,8 @@ class Device(object):
|
||||
params[k] = PARAM(v)
|
||||
mycls = self.__class__
|
||||
myclassname = '%s.%s' % (mycls.__module__, mycls.__name__)
|
||||
params['class'] = PARAM(
|
||||
'implementaion specific class name', default=myclassname, validator=str)
|
||||
params['class'] = PARAM('implementation specific class name',
|
||||
default=myclassname, validator=str)
|
||||
|
||||
self.PARAMS = params
|
||||
# check config for problems
|
||||
@ -240,9 +243,14 @@ class Device(object):
|
||||
raise ConfigError('Device %s: config parameter %r:\n%r'
|
||||
% (self.name, k, e))
|
||||
setattr(self, k, v)
|
||||
self._requestLock = threading.RLock()
|
||||
|
||||
def init(self):
|
||||
# may be overriden in other classes
|
||||
# may be overriden in derived classes to init stuff
|
||||
self.log.debug('init()')
|
||||
|
||||
def _pollThread(self):
|
||||
# may be overriden in derived classes to init stuff
|
||||
self.log.debug('init()')
|
||||
|
||||
|
||||
@ -255,25 +263,40 @@ class Readable(Device):
|
||||
'baseclass': PARAM('protocol defined interface class',
|
||||
default="Readable", validator=str),
|
||||
'value': PARAM('current value of the device', readonly=True, default=0.),
|
||||
'pollinterval': PARAM('sleeptime between polls', readonly=False, default=5, validator=floatrange(1,120),),
|
||||
'status': PARAM('current status of the device', default=status.OK,
|
||||
validator=mapping(**{'idle': status.OK,
|
||||
'BUSY': status.BUSY,
|
||||
'WARN': status.WARN,
|
||||
'UNSTABLE': status.UNSTABLE,
|
||||
'ERROR': status.ERROR,
|
||||
'UNKNOWN': status.UNKNOWN}),
|
||||
validator=enum(**{'idle': status.OK,
|
||||
'BUSY': status.BUSY,
|
||||
'WARN': status.WARN,
|
||||
'UNSTABLE': status.UNSTABLE,
|
||||
'ERROR': status.ERROR,
|
||||
'UNKNOWN': status.UNKNOWN}),
|
||||
readonly=True),
|
||||
'status2': PARAM('current status of the device', default=(status.OK, ''),
|
||||
validator=vector(mapping(**{'idle': status.OK,
|
||||
'BUSY': status.BUSY,
|
||||
'WARN': status.WARN,
|
||||
'UNSTABLE': status.UNSTABLE,
|
||||
'ERROR': status.ERROR,
|
||||
'UNKNOWN': status.UNKNOWN}), str),
|
||||
validator=vector(enum(**{'idle': status.OK,
|
||||
'BUSY': status.BUSY,
|
||||
'WARN': status.WARN,
|
||||
'UNSTABLE': status.UNSTABLE,
|
||||
'ERROR': status.ERROR,
|
||||
'UNKNOWN': status.UNKNOWN}), str),
|
||||
readonly=True),
|
||||
}
|
||||
|
||||
|
||||
def init(self):
|
||||
Device.init(self)
|
||||
self._pollthread = threading.Thread(target=self._pollThread)
|
||||
self._pollthread.daemon = True
|
||||
self._pollthread.start()
|
||||
|
||||
def _pollThread(self):
|
||||
while True:
|
||||
time.sleep(self.pollinterval)
|
||||
for pname in self.PARAMS:
|
||||
if pname != 'pollinterval':
|
||||
rfunc = getattr(self, 'read_%s' % pname, None)
|
||||
if rfunc:
|
||||
rfunc()
|
||||
|
||||
class Driveable(Readable):
|
||||
"""Basic Driveable device
|
||||
|
||||
@ -285,3 +308,5 @@ class Driveable(Readable):
|
||||
'target': PARAM('target value of the device', default=0.,
|
||||
readonly=False),
|
||||
}
|
||||
def doStop(self):
|
||||
time.sleep(1) # for testing !
|
@ -26,10 +26,10 @@ import time
|
||||
import random
|
||||
import threading
|
||||
|
||||
from devices.core import Driveable, CONFIG, PARAM
|
||||
from protocol import status
|
||||
from validators import floatrange, positive, mapping
|
||||
from lib import clamp
|
||||
from secop.devices.core import Driveable, CONFIG, PARAM
|
||||
from secop.protocol import status
|
||||
from secop.validators import floatrange, positive, enum
|
||||
from secop.lib import clamp
|
||||
|
||||
|
||||
class Cryostat(Driveable):
|
||||
@ -79,7 +79,7 @@ class Cryostat(Driveable):
|
||||
validator=floatrange(0, 100), default=2,
|
||||
),
|
||||
mode=PARAM("mode of regulation",
|
||||
validator=mapping('ramp', 'pid', 'openloop'), default='pid',
|
||||
validator=enum('ramp', 'pid', 'openloop'), default='pid',
|
||||
),
|
||||
|
||||
tolerance=PARAM("temperature range for stability checking",
|
@ -25,9 +25,9 @@ import time
|
||||
import random
|
||||
import threading
|
||||
|
||||
from devices.core import Readable, Driveable, PARAM
|
||||
from validators import *
|
||||
from protocol import status
|
||||
from secop.devices.core import Readable, Driveable, PARAM
|
||||
from secop.validators import *
|
||||
from secop.protocol import status
|
||||
|
||||
|
||||
class Switch(Driveable):
|
||||
@ -35,9 +35,9 @@ class Switch(Driveable):
|
||||
"""
|
||||
PARAMS = {
|
||||
'value': PARAM('current state (on or off)',
|
||||
validator=mapping(on=1, off=0), default=0),
|
||||
validator=enum(on=1, off=0), default=0),
|
||||
'target': PARAM('wanted state (on or off)',
|
||||
validator=mapping(on=1, off=0), default=0,
|
||||
validator=enum(on=1, off=0), default=0,
|
||||
readonly=False),
|
||||
'switch_on_time': PARAM('how long to wait after switching the switch on', validator=floatrange(0, 60), unit='s', default=10, export=False),
|
||||
'switch_off_time': PARAM('how long to wait after switching the switch off', validator=floatrange(0, 60), unit='s', default=10, export=False),
|
||||
@ -90,7 +90,7 @@ class MagneticField(Driveable):
|
||||
'ramp': PARAM('moving speed in T/min', unit='T/min',
|
||||
validator=floatrange(0, 1), default=0.1, readonly=False),
|
||||
'mode': PARAM('what to do after changing field', default=0,
|
||||
validator=mapping(persistent=1, hold=0), readonly=False),
|
||||
validator=enum(persistent=1, hold=0), readonly=False),
|
||||
'heatswitch': PARAM('heat switch device',
|
||||
validator=str, export=False),
|
||||
}
|
||||
@ -255,7 +255,7 @@ class ValidatorTest(Readable):
|
||||
"""
|
||||
PARAMS = {
|
||||
'oneof': PARAM('oneof', validator=oneof(int, 'X', 2.718), readonly=False, default=4.0),
|
||||
'mapping': PARAM('mapping', validator=mapping('boo', 'faar', z=9), readonly=False, default=1),
|
||||
'enum': PARAM('enum', validator=enum('boo', 'faar', z=9), readonly=False, default=1),
|
||||
'vector': PARAM('vector of int, float and str', validator=vector(int, float, str), readonly=False, default=(1, 2.3, 'a')),
|
||||
'array': PARAM('array: 2..3 time oneof(0,1)', validator=array(oneof(2, 3), oneof(0, 1)), readonly=False, default=[1, 0, 1]),
|
||||
'nonnegative': PARAM('nonnegative', validator=nonnegative(), readonly=False, default=0),
|
@ -23,7 +23,7 @@
|
||||
|
||||
import random
|
||||
|
||||
from devices.core import Readable, Driveable, PARAM
|
||||
from secop.devices.core import Readable, Driveable, PARAM
|
||||
|
||||
|
||||
try:
|
@ -23,8 +23,8 @@
|
||||
|
||||
import random
|
||||
|
||||
from devices.core import Readable, Driveable, PARAM
|
||||
from validators import floatrange
|
||||
from secop.devices.core import Readable, Driveable, PARAM
|
||||
from secop.validators import floatrange
|
||||
|
||||
|
||||
class LN2(Readable):
|
@ -47,7 +47,7 @@ def get_class(spec):
|
||||
"""loads a class given by string in dotted notaion (as python would do)"""
|
||||
modname, classname = spec.rsplit('.', 1)
|
||||
import importlib
|
||||
module = importlib.import_module(modname)
|
||||
module = importlib.import_module('secop.' + modname)
|
||||
# module = __import__(spec)
|
||||
return getattr(module, classname)
|
||||
|
309
secop/lib/parsing.py
Normal file
309
secop/lib/parsing.py
Normal file
@ -0,0 +1,309 @@
|
||||
# -*- 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"""
|
||||
|
||||
import time
|
||||
import datetime
|
||||
|
||||
|
||||
def format_time(timestamp):
|
||||
return datetime.datetime.fromtimestamp(
|
||||
timestamp).strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||
|
||||
|
||||
def parse_time(string):
|
||||
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):
|
||||
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, unicode)):
|
||||
# XXX: check for 'easy' strings only and omit the ''
|
||||
return repr(args)
|
||||
return repr(args) # for floats/ints/...
|
||||
|
||||
|
||||
class ArgsParser(object):
|
||||
"""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 = [c for c in '0123456789']
|
||||
NAME_CHARS = [
|
||||
c for c in '_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):
|
||||
print repr(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
|
||||
print "get->", res
|
||||
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:
|
||||
print "is Array"
|
||||
return res
|
||||
self.idx = idx
|
||||
res = self.parse_record()
|
||||
if res:
|
||||
print "is record"
|
||||
return res
|
||||
self.idx = idx
|
||||
res = self.parse_string()
|
||||
if res:
|
||||
print "is string"
|
||||
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 None:
|
||||
return number
|
||||
return -number
|
||||
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"
|
@ -26,8 +26,8 @@
|
||||
# XXX: is this still needed ???
|
||||
# see devices.core ....
|
||||
|
||||
from lib import attrdict
|
||||
from protocol import status
|
||||
from secop.lib import attrdict
|
||||
from secop.protocol import status
|
||||
|
||||
|
||||
# XXX: deriving PARS/CMDS should be done in a suitable metaclass....
|
359
secop/protocol/dispatcher.py
Normal file
359
secop/protocol/dispatcher.py
Normal file
@ -0,0 +1,359 @@
|
||||
# -*- 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>
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
"""Dispatcher for SECoP Messages
|
||||
|
||||
Interface to the service offering part:
|
||||
|
||||
- 'handle_request(connectionobj, data)' handles incoming request
|
||||
will call 'queue_request(data)' on connectionobj before returning
|
||||
- 'add_connection(connectionobj)' registers new connection
|
||||
- 'remove_connection(connectionobj)' removes now longer functional connection
|
||||
- may at any time call 'queue_async_request(connobj, data)' on the connobj
|
||||
|
||||
Interface to the modules:
|
||||
- add_module(modulename, moduleobj, export=True) registers a new module under the
|
||||
given name, may also register it for exporting (making accessible)
|
||||
- get_module(modulename) returns the requested module or None
|
||||
- remove_module(modulename_or_obj): removes the module (during shutdown)
|
||||
|
||||
internal stuff which may be called
|
||||
- list_modules(): return a list of modules + descriptive data as dict
|
||||
- list_module_params():
|
||||
return a list of paramnames for this module + descriptive data
|
||||
"""
|
||||
|
||||
import time
|
||||
import threading
|
||||
|
||||
from messages import *
|
||||
from errors import *
|
||||
|
||||
|
||||
class Dispatcher(object):
|
||||
def __init__(self, logger, options):
|
||||
self.equipment_id = options.pop('equipment_id')
|
||||
self.log = logger
|
||||
# map ALL modulename -> moduleobj
|
||||
self._dispatcher_modules = {}
|
||||
# list of EXPORTED modules
|
||||
self._dispatcher_export = []
|
||||
# list all connections
|
||||
self._dispatcher_connections = []
|
||||
# active (i.e. broadcast-receiving) connections
|
||||
self._dispatcher_active_connections = set()
|
||||
# map eventname -> list of subscribed connections
|
||||
self._dispatcher_subscriptions = {}
|
||||
self._dispatcher_lock = threading.RLock()
|
||||
|
||||
def handle_request(self, conn, msg):
|
||||
"""handles incoming request
|
||||
|
||||
will call 'queue.request(data)' on conn to send reply before returning
|
||||
"""
|
||||
self.log.debug('Dispatcher: handling msg: %r' % msg)
|
||||
# play thread safe !
|
||||
with self._dispatcher_lock:
|
||||
reply = None
|
||||
# generate reply (coded and framed)
|
||||
msgname = msg.__class__.__name__
|
||||
if msgname.endswith('Request'):
|
||||
msgname = msgname[:-len('Request')]
|
||||
if msgname.endswith('Message'):
|
||||
msgname = msgname[:-len('Message')]
|
||||
self.log.debug('Looking for handle_%s' % msgname)
|
||||
handler = getattr(self, 'handle_%s' % msgname, None)
|
||||
if handler:
|
||||
try:
|
||||
reply = handler(conn, msg)
|
||||
except SECOPError as err:
|
||||
self.log.exception(err)
|
||||
reply = ErrorMessage(errorclass=err.__class__.__name__,
|
||||
errorinfo=[repr(err), str(msg)])
|
||||
except (ValueError, TypeError) as err:
|
||||
# self.log.exception(err)
|
||||
reply = ErrorMessage(errorclass='BadValue',
|
||||
errorinfo=[repr(err), str(msg)])
|
||||
except Exception as err:
|
||||
self.log.exception(err)
|
||||
reply = ErrorMessage(errorclass='InternalError',
|
||||
errorinfo=[repr(err), str(msg)])
|
||||
else:
|
||||
self.log.debug('Can not handle msg %r' % msg)
|
||||
reply = self.unhandled(conn, msg)
|
||||
if reply:
|
||||
conn.queue_reply(reply)
|
||||
|
||||
def broadcast_event(self, msg, reallyall=False):
|
||||
"""broadcasts a msg to all active connections"""
|
||||
if reallyall:
|
||||
listeners = self._dispatcher_connections
|
||||
else:
|
||||
if getattr(msg, 'command', None) is None:
|
||||
eventname = '%s:%s' % (msg.module, msg.parameter if msg.parameter else 'value')
|
||||
else:
|
||||
eventname = '%s:%s()' % (msg.module, msg.command)
|
||||
listeners = self._dispatcher_subscriptions.get(eventname, [])
|
||||
listeners += list(self._dispatcher_active_connections)
|
||||
for conn in listeners:
|
||||
conn.queue_async_reply(msg)
|
||||
|
||||
def announce_update(self, moduleobj, pname, pobj):
|
||||
"""called by modules param setters to notify subscribers of new values
|
||||
"""
|
||||
msg = Value(moduleobj.name, parameter=pname, value=pobj.value, t=pobj.timestamp)
|
||||
self.broadcast_event(msg)
|
||||
|
||||
def subscribe(self, conn, modulename, pname='value'):
|
||||
eventname = '%s:%s' % (modulename, pname)
|
||||
self._dispatcher_subscriptions.setdefault(eventname, set()).add(conn)
|
||||
|
||||
def unsubscribe(self, conn, modulename, pname='value'):
|
||||
eventname = '%s:%s' % (modulename, pname)
|
||||
if eventname in self._dispatcher_subscriptions:
|
||||
self._dispatcher_subscriptions.remove(conn)
|
||||
|
||||
def add_connection(self, conn):
|
||||
"""registers new connection"""
|
||||
self._dispatcher_connections.append(conn)
|
||||
|
||||
def remove_connection(self, conn):
|
||||
"""removes now longer functional connection"""
|
||||
if conn in self._dispatcher_connections:
|
||||
self._dispatcher_connections.remove(conn)
|
||||
for _evt, conns in self._dispatcher_subscriptions.items():
|
||||
conns.discard(conn)
|
||||
|
||||
def activate_connection(self, conn):
|
||||
self._dispatcher_active_connections.add(conn)
|
||||
|
||||
def deactivate_connection(self, conn):
|
||||
self._dispatcher_active_connections.discard(conn)
|
||||
|
||||
def register_module(self, moduleobj, modulename, export=True):
|
||||
self.log.debug('registering module %r as %s (export=%r)' %
|
||||
(moduleobj, modulename, export))
|
||||
self._dispatcher_modules[modulename] = moduleobj
|
||||
if export:
|
||||
self._dispatcher_export.append(modulename)
|
||||
|
||||
def get_module(self, modulename):
|
||||
module = self._dispatcher_modules.get(modulename, modulename)
|
||||
if module != modulename:
|
||||
self.log.debug('get_module(%r) -> %r' % (modulename, module))
|
||||
return module
|
||||
|
||||
def remove_module(self, modulename_or_obj):
|
||||
moduleobj = self.get_module(modulename_or_obj) or modulename_or_obj
|
||||
modulename = moduleobj.name
|
||||
if modulename in self._dispatcher_export:
|
||||
self._dispatcher_export.remove(modulename)
|
||||
self._dispatcher_modules.pop(modulename)
|
||||
# XXX: also clean _dispatcher_subscriptions
|
||||
|
||||
def list_module_names(self):
|
||||
# return a copy of our list
|
||||
return self._dispatcher_export[:]
|
||||
|
||||
def list_modules(self):
|
||||
dn = []
|
||||
dd = {}
|
||||
for modulename in self._dispatcher_export:
|
||||
dn.append(modulename)
|
||||
module = self.get_module(modulename)
|
||||
descriptive_data = {
|
||||
'class': module.__class__.__name__,
|
||||
#'bases': module.__bases__,
|
||||
'parameters': module.PARAMS.keys(),
|
||||
'commands': module.CMDS.keys(),
|
||||
# XXX: what else?
|
||||
}
|
||||
dd[modulename] = descriptive_data
|
||||
return dn, dd
|
||||
|
||||
def list_module_params(self, modulename):
|
||||
self.log.debug('list_module_params(%r)' % modulename)
|
||||
if modulename in self._dispatcher_export:
|
||||
# XXX: omit export=False params!
|
||||
res = {}
|
||||
for paramname, param in self.get_module(modulename).PARAMS.items():
|
||||
if param.export == True:
|
||||
res[paramname] = param
|
||||
self.log.debug('list params for module %s -> %r' %
|
||||
(modulename, res))
|
||||
return res
|
||||
self.log.debug('-> module is not to be exported!')
|
||||
return {}
|
||||
|
||||
def _execute_command(self, modulename, command, arguments=None):
|
||||
if arguments is None:
|
||||
arguments = []
|
||||
|
||||
moduleobj = self.get_module(modulename)
|
||||
if moduleobj is None:
|
||||
raise NoSuchmoduleError(module=modulename)
|
||||
|
||||
cmdspec = moduleobj.CMDS.get(command, None)
|
||||
if cmdspec is None:
|
||||
raise NoSuchCommandError(module=modulename, command=command)
|
||||
if len(cmdspec.arguments) != len(arguments):
|
||||
raise BadValueError(module=modulename, command=command, reason='Wrong number of arguments!')
|
||||
|
||||
# now call func and wrap result as value
|
||||
# note: exceptions are handled in handle_request, not here!
|
||||
func = getattr(moduleobj, 'do'+command)
|
||||
res = Value(modulename, command=command, value=func(*arguments), t=time.time())
|
||||
return res
|
||||
|
||||
def _setParamValue(self, modulename, pname, value):
|
||||
moduleobj = self.get_module(modulename)
|
||||
if moduleobj is None:
|
||||
raise NoSuchmoduleError(module=modulename)
|
||||
|
||||
pobj = moduleobj.PARAMS.get(pname, None)
|
||||
if pobj is None:
|
||||
raise NoSuchParamError(module=modulename, parameter=pname)
|
||||
if pobj.readonly:
|
||||
raise ReadonlyError(module=modulename, parameter=pname)
|
||||
|
||||
writefunc = getattr(moduleobj, 'write_%s' % pname, None)
|
||||
# note: exceptions are handled in handle_request, not here!
|
||||
if writefunc:
|
||||
value = writefunc(value)
|
||||
else:
|
||||
setattr(moduleobj, pname, value)
|
||||
if pobj.timestamp:
|
||||
return Value(modulename, pname, value=pobj.value, t=pobj.timestamp)
|
||||
return Value(modulename, pname, value=pobj.value)
|
||||
|
||||
def _getParamValue(self, modulename, pname):
|
||||
moduleobj = self.get_module(modulename)
|
||||
if moduleobj is None:
|
||||
raise NoSuchmoduleError(module=modulename)
|
||||
|
||||
pobj = moduleobj.PARAMS.get(pname, None)
|
||||
if pobj is None:
|
||||
raise NoSuchParamError(module=modulename, parameter=pname)
|
||||
|
||||
readfunc = getattr(moduleobj, 'read_%s' % pname, None)
|
||||
if readfunc:
|
||||
# should also update the pobj (via the setter from the metaclass)
|
||||
# note: exceptions are handled in handle_request, not here!
|
||||
readfunc()
|
||||
if pobj.timestamp:
|
||||
return Value(modulename, parameter=pname, value=pobj.value, t=pobj.timestamp)
|
||||
return Value(modulename, parameter=pname, value=pobj.value)
|
||||
|
||||
|
||||
# now the (defined) handlers for the different requests
|
||||
def handle_Help(self, conn, msg):
|
||||
return HelpMessage()
|
||||
|
||||
def handle_Identify(self, conn, msg):
|
||||
return IdentifyReply(version_string='currently,is,ignored,here')
|
||||
|
||||
def handle_Describe(self, conn, msg):
|
||||
# XXX:collect descriptive data
|
||||
# XXX:how to get equipment_id?
|
||||
return DescribeReply(equipment_id = self.equipment_id, description = self.list_modules())
|
||||
|
||||
def handle_Poll(self, conn, msg):
|
||||
# XXX: trigger polling and force sending event
|
||||
res = self._getParamValue(msg.module, msg.parameter or 'value')
|
||||
#self.broadcast_event(res)
|
||||
if conn in self._dispatcher_active_connections:
|
||||
return None # already send to myself
|
||||
return res # send reply to inactive conns
|
||||
|
||||
def handle_Write(self, conn, msg):
|
||||
# notify all by sending WriteReply
|
||||
#msg1 = WriteReply(**msg.as_dict())
|
||||
#self.broadcast_event(msg1)
|
||||
# try to actually write XXX: should this be done asyncron? we could just return the reply in that case
|
||||
if msg.parameter:
|
||||
res = self._setParamValue(msg.module, msg.parameter, msg.value)
|
||||
else:
|
||||
# first check if module has a target
|
||||
if 'target' not in self.get_module(msg.module).PARAMS:
|
||||
raise ReadonlyError(module=msg.module, parameter=None)
|
||||
res = self._setParamValue(msg.module, 'target', msg.value)
|
||||
res.parameter = 'target'
|
||||
#self.broadcast_event(res)
|
||||
if conn in self._dispatcher_active_connections:
|
||||
return None # already send to myself
|
||||
return res # send reply to inactive conns
|
||||
|
||||
def handle_Command(self, conn, msg):
|
||||
# notify all by sending CommandReply
|
||||
#msg1 = CommandReply(**msg.as_dict())
|
||||
#self.broadcast_event(msg1)
|
||||
# XXX: should this be done asyncron? we could just return the reply in that case
|
||||
|
||||
# try to actually execute command
|
||||
res = self._execute_command(msg.module, msg.command, msg.arguments)
|
||||
#self.broadcast_event(res)
|
||||
#if conn in self._dispatcher_active_connections:
|
||||
# return None # already send to myself
|
||||
return res # send reply to inactive conns
|
||||
|
||||
def handle_Heartbeat(self, conn, msg):
|
||||
return HeartbeatReply(**msg.as_dict())
|
||||
|
||||
def handle_Activate(self, conn, msg):
|
||||
self.activate_connection(conn)
|
||||
# easy approach: poll all values...
|
||||
for modulename, moduleobj in self._dispatcher_modules.items():
|
||||
for pname, pobj in moduleobj.PARAMS.items():
|
||||
# WARNING: THIS READS ALL PARAMS FROM HW!
|
||||
# XXX: should we send the cached values instead? (pbj.value)
|
||||
# also: ignore errors here.
|
||||
try:
|
||||
res = self._getParamValue(modulename, pname)
|
||||
except SECOPError as e:
|
||||
self.log.error('decide what to do here!')
|
||||
self.log.exception(e)
|
||||
res = Value(module=modulename, parameter=pname,
|
||||
value=pobj.value, t=pobj.timestamp,
|
||||
unit=pobj.unit)
|
||||
if res.value != Ellipsis: # means we do not have a value at all so skip this
|
||||
self.broadcast_event(res)
|
||||
conn.queue_async_reply(ActivateReply(**msg.as_dict()))
|
||||
return None
|
||||
|
||||
def handle_Deactivate(self, conn, msg):
|
||||
self.deactivate_connection(conn)
|
||||
conn.queue_async_reply(DeactivateReply(**msg.as_dict()))
|
||||
return None
|
||||
|
||||
def handle_Error(self, conn, msg):
|
||||
return msg
|
||||
|
||||
def unhandled(self, conn, msg):
|
||||
"""handler for unhandled Messages
|
||||
|
||||
(no handle_<messagename> method was defined)
|
||||
"""
|
||||
self.log.error('IGN: got unhandled request %s' % msg)
|
||||
return ErrorMessage(errorclass="InternalError",
|
||||
errorstring = 'Got Unhandled Request %r' % msg)
|
||||
|
||||
|
59
secop/protocol/encoding/__init__.py
Normal file
59
secop/protocol/encoding/__init__.py
Normal file
@ -0,0 +1,59 @@
|
||||
#!/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)
|
||||
|
||||
# Base classes
|
||||
|
||||
|
||||
class MessageEncoder(object):
|
||||
"""en/decode a single Messageobject"""
|
||||
|
||||
def encode(self, messageobj):
|
||||
"""encodes the given message object into a frame"""
|
||||
raise NotImplemented
|
||||
|
||||
def decode(self, frame):
|
||||
"""decodes the given frame to a message object"""
|
||||
raise NotImplemented
|
||||
|
||||
from demo_v2 import DemoEncoder as DemoEncoderV2
|
||||
from demo_v3 import DemoEncoder as DemoEncoderV3
|
||||
from demo_v4 import DemoEncoder as DemoEncoderV4
|
||||
from text import TextEncoder
|
||||
from pickle import PickleEncoder
|
||||
from simplecomm import SCPEncoder
|
||||
|
||||
ENCODERS = {
|
||||
'pickle': PickleEncoder,
|
||||
'text': TextEncoder,
|
||||
'demo_v2': DemoEncoderV2,
|
||||
'demo_v3': DemoEncoderV3,
|
||||
'demo_v4': DemoEncoderV4,
|
||||
'demo': DemoEncoderV4,
|
||||
'scp': SCPEncoder,
|
||||
}
|
||||
|
||||
__ALL__ = ['ENCODERS']
|
@ -25,78 +25,9 @@
|
||||
# implement as class as they may need some internal 'state' later on
|
||||
# (think compressors)
|
||||
|
||||
from protocol import messages
|
||||
|
||||
|
||||
# Base classes
|
||||
class MessageEncoder(object):
|
||||
"""en/decode a single Messageobject"""
|
||||
|
||||
def encode(self, messageobj):
|
||||
"""encodes the given message object into a frame"""
|
||||
raise NotImplemented
|
||||
|
||||
def decode(self, frame):
|
||||
"""decodes the given frame to a message object"""
|
||||
raise NotImplemented
|
||||
|
||||
# now some Implementations
|
||||
try:
|
||||
import cPickle as pickle
|
||||
except ImportError:
|
||||
import pickle
|
||||
|
||||
import protocol.messages
|
||||
|
||||
|
||||
class PickleEncoder(MessageEncoder):
|
||||
|
||||
def encode(self, messageobj):
|
||||
"""msg object -> transport layer message"""
|
||||
return pickle.dumps(messageobj)
|
||||
|
||||
def decode(self, encoded):
|
||||
"""transport layer message -> msg object"""
|
||||
return pickle.loads(encoded)
|
||||
|
||||
|
||||
class TextEncoder(MessageEncoder):
|
||||
|
||||
def __init__(self):
|
||||
# build safe namespace
|
||||
ns = dict()
|
||||
for n in dir(messages):
|
||||
if n.endswith(('Request', 'Reply')):
|
||||
ns[n] = getattr(messages, n)
|
||||
self.namespace = ns
|
||||
|
||||
def encode(self, messageobj):
|
||||
"""msg object -> transport layer message"""
|
||||
# fun for Humans
|
||||
if isinstance(messageobj, messages.HelpReply):
|
||||
return "Error: try one of the following requests:\n" + \
|
||||
'\n'.join(['%s(%s)' % (getattr(messages, m).__name__,
|
||||
', '.join(getattr(messages, m).ARGS))
|
||||
for m in dir(messages)
|
||||
if m.endswith('Request')])
|
||||
res = []
|
||||
for k in messageobj.ARGS:
|
||||
res.append('%s=%r' % (k, getattr(messageobj, k, None)))
|
||||
result = '%s(%s)' % (messageobj.__class__.__name__, ', '.join(res))
|
||||
return result
|
||||
|
||||
def decode(self, encoded):
|
||||
"""transport layer message -> msg object"""
|
||||
# WARNING: highly unsafe!
|
||||
# think message='import os\nos.unlink('\')\n'
|
||||
try:
|
||||
return eval(encoded, self.namespace, {})
|
||||
except SyntaxError:
|
||||
return messages.HelpRequest()
|
||||
|
||||
|
||||
def format_time(ts):
|
||||
return float(ts) # XXX: switch to iso!
|
||||
from secop.protocol.encoding import MessageEncoder
|
||||
from secop.protocol import messages
|
||||
from secop.lib.parsing import *
|
||||
|
||||
import re
|
||||
|
||||
@ -104,29 +35,6 @@ DEMO_RE = re.compile(
|
||||
r'^([!+-])?(\*|[a-z_][a-z_0-9]*)?(?:\:(\*|[a-z_][a-z_0-9]*))?(?:\:(\*|[a-z_][a-z_0-9]*))?(?:\=(.*))?')
|
||||
|
||||
|
||||
def parse_str(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_str(part) for part in s[1:-1].split(',')]
|
||||
if s.startswith('(') and s.endswith(')'):
|
||||
# evaluate inner
|
||||
return [parse_str(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]
|
||||
for conv in (int, float, lambda x: x):
|
||||
try:
|
||||
return conv(s)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
class DemoEncoder(MessageEncoder):
|
||||
|
||||
def decode(sef, encoded):
|
||||
@ -136,7 +44,7 @@ class DemoEncoder(MessageEncoder):
|
||||
novalue, devname, pname, propname, assign = match.groups()
|
||||
if assign:
|
||||
print "parsing", assign,
|
||||
assign = parse_str(assign)
|
||||
assign = parse_args(assign)
|
||||
print "->", assign
|
||||
return messages.DemoRequest(
|
||||
novalue, devname, pname, propname, assign)
|
||||
@ -192,13 +100,3 @@ class DemoEncoder(MessageEncoder):
|
||||
def _encode_HelpReply(self):
|
||||
return ['Help not yet implemented!',
|
||||
'ask Markus Zolliker about the protocol']
|
||||
|
||||
|
||||
ENCODERS = {
|
||||
'pickle': PickleEncoder,
|
||||
'text': TextEncoder,
|
||||
'demo': DemoEncoder,
|
||||
}
|
||||
|
||||
|
||||
__ALL__ = ['ENCODERS']
|
391
secop/protocol/encoding/demo_v3.py
Normal file
391
secop/protocol/encoding/demo_v3.py
Normal file
@ -0,0 +1,391 @@
|
||||
#!/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 secop.protocol.encoding import MessageEncoder
|
||||
from secop.protocol.messages import *
|
||||
from secop.protocol.errors import ProtocollError
|
||||
|
||||
import ast
|
||||
import re
|
||||
|
||||
|
||||
def floatify(s):
|
||||
try:
|
||||
return int(s)
|
||||
except (ValueError, TypeError):
|
||||
try:
|
||||
return float(s)
|
||||
except (ValueError, TypeError):
|
||||
return s
|
||||
|
||||
|
||||
def devspec(msg, result=''):
|
||||
if isinstance(msg, Message):
|
||||
devs = ','.join(msg.devs)
|
||||
pars = ','.join(msg.pars)
|
||||
props = ','.join(msg.props)
|
||||
else:
|
||||
devs = msg.dev
|
||||
pars = msg.par
|
||||
props = msg.prop
|
||||
if devs:
|
||||
result = '%s %s' % (result, devs)
|
||||
if pars:
|
||||
result = '%s:%s' % (result, pars)
|
||||
if props:
|
||||
result = '%s:%s' % (result, props)
|
||||
return result.strip()
|
||||
|
||||
|
||||
def encode_value(value, prefix='', targetvalue='', cmd=''):
|
||||
result = [prefix]
|
||||
if value.dev:
|
||||
result.append(' ')
|
||||
result.append(value.dev)
|
||||
if value.param:
|
||||
result.append(':%s' % value.param)
|
||||
if value.prop:
|
||||
result.append(':%s' % value.prop)
|
||||
# only needed for WriteMessages
|
||||
if targetvalue:
|
||||
result.append('=%s' % repr(targetvalue))
|
||||
# only needed for CommandMessages
|
||||
if cmd:
|
||||
result.append(':%s' % cmd)
|
||||
if value.value != Ellipsis:
|
||||
# results always have a ';'
|
||||
result.append('=%s;' % repr(value.value))
|
||||
result.append(';'.join('%s=%s' % (qn, repr(qv))
|
||||
for qn, qv in value.qualifiers.items()))
|
||||
return ''.join(result).strip()
|
||||
|
||||
|
||||
DEMO_RE_ERROR = re.compile(
|
||||
r"""^error\s(?P<errortype>\w+)\s(?P<msgtype>\w+)?(?:\s(?P<devs>\*|[\w,]+)(?:\:(?P<pars>\*|[\w,]+)(?:\:(?P<props>\*|[\w,]+))?)?)?(?:(?:\=(?P<target>[^=;\s"]*))|(?:\((?P<cmdargs>[^\)]*)\)))?(?:\s"(?P<errorstring>[^"]*)")$""",
|
||||
re.X)
|
||||
DEMO_RE_OTHER = re.compile(
|
||||
r"""^(?P<msgtype>\w+)(?:\s(?P<devs>\*|[\w,]+)(?:\:(?P<pars>\*|[\w,]+)(?:\:(?P<props>\*|[\w,]+))?)?)?(?:(?:\=(?P<target>[^=;]*))|(?::(?P<cmd>\w+)\((?P<args>[^\)]*)\)))?(?:=(?P<readback>[^;]+);(?P<qualifiers>.*))?$""",
|
||||
re.X)
|
||||
|
||||
|
||||
class DemoEncoder(MessageEncoder):
|
||||
|
||||
def __init__(self, *args, **kwds):
|
||||
MessageEncoder.__init__(self, *args, **kwds)
|
||||
self.result = [] # for decoding
|
||||
self.expect_lines = 1
|
||||
#self.tests()
|
||||
|
||||
def encode(self, msg):
|
||||
"""msg object -> transport layer message"""
|
||||
# fun for Humans
|
||||
if isinstance(msg, HelpMessage):
|
||||
r = ['#5']
|
||||
r.append("help Try one of the following:")
|
||||
r.append("help 'list' to query a list of modules")
|
||||
r.append("help 'read <module>' to read a module")
|
||||
r.append("help 'list <module>' to query a list of parameters")
|
||||
r.append("help ... more to come")
|
||||
return '\n'.join(r)
|
||||
|
||||
if isinstance(msg, (ListMessage, SubscribeMessage,
|
||||
UnsubscribeMessage, TriggerMessage)):
|
||||
msgtype = msg.MSGTYPE
|
||||
if msg.result:
|
||||
if msg.devs:
|
||||
# msg.result is always a list!
|
||||
return "%s=%s" % (devspec(msg, msgtype),
|
||||
','.join(map(str, msg.result)))
|
||||
return "%s=%s" % (msgtype, ','.join(map(str, msg.result)))
|
||||
return devspec(msg, msgtype).strip()
|
||||
|
||||
if isinstance(msg, (ReadMessage, PollMessage, EventMessage)):
|
||||
msgtype = msg.MSGTYPE
|
||||
result = []
|
||||
if len(msg.result or []) > 1:
|
||||
result.append("#%d" % len(msg.result))
|
||||
for val in msg.result or []:
|
||||
# encode 1..N replies
|
||||
result.append(encode_value(val, msgtype))
|
||||
if not msg.result:
|
||||
# encode a request (no results -> reply, else an error would
|
||||
# have been sent)
|
||||
result.append(devspec(msg, msgtype))
|
||||
return '\n'.join(result)
|
||||
|
||||
if isinstance(msg, WriteMessage):
|
||||
result = []
|
||||
if len(msg.result or []) > 1:
|
||||
result.append("#%d" % len(msg.result))
|
||||
for val in msg.result or []:
|
||||
# encode 1..N replies
|
||||
result.append(
|
||||
encode_value(
|
||||
val,
|
||||
'write',
|
||||
targetvalue=msg.target))
|
||||
if not msg.result:
|
||||
# encode a request (no results -> reply, else an error would
|
||||
# have been sent)
|
||||
result.append('%s=%r' % (devspec(msg, 'write'), msg.target))
|
||||
return '\n'.join(result)
|
||||
|
||||
if isinstance(msg, CommandMessage):
|
||||
result = []
|
||||
if len(msg.result or []) > 1:
|
||||
result.append("#%d" % len(msg.result))
|
||||
for val in msg.result or []:
|
||||
# encode 1..N replies
|
||||
result.append(
|
||||
encode_value(
|
||||
val,
|
||||
'command',
|
||||
cmd='%s(%s)' %
|
||||
(msg.cmd,
|
||||
','.join(
|
||||
msg.args))))
|
||||
if not msg.result:
|
||||
# encode a request (no results -> reply, else an error would
|
||||
# have been sent)
|
||||
result.append(
|
||||
'%s:%s(%s)' %
|
||||
(devspec(
|
||||
msg, 'command'), msg.cmd, ','.join(
|
||||
msg.args)))
|
||||
return '\n'.join(result)
|
||||
|
||||
if isinstance(msg, ErrorMessage):
|
||||
return ('%s %s' % (devspec(msg, 'error %s' %
|
||||
msg.errortype), msg.errorstring)).strip()
|
||||
|
||||
return 'Can not handle object %r!' % msg
|
||||
|
||||
def decode(self, encoded):
|
||||
if encoded.startswith('#'):
|
||||
# XXX: check if last message was complete
|
||||
self.expect_lines = int(encoded[1:])
|
||||
if self.result:
|
||||
# XXX: also flag an error?
|
||||
self.result = []
|
||||
return None
|
||||
|
||||
if encoded == '':
|
||||
return HelpMessage()
|
||||
# now decode the message and append to self.result
|
||||
msg = self.decode_single_message(encoded)
|
||||
if msg:
|
||||
# XXX: check if messagetype is the same as the already existing,
|
||||
# else error
|
||||
self.result.append(msg)
|
||||
else:
|
||||
# XXX: flag an error?
|
||||
return HelpMessage()
|
||||
|
||||
self.expect_lines -= 1
|
||||
if self.expect_lines <= 0:
|
||||
# reconstruct a multi-reply-message from the entries
|
||||
# return the first message, but extend the result list first
|
||||
# if there is only 1 message, just return this
|
||||
res = self.result.pop(0)
|
||||
while self.result:
|
||||
m = self.result.pop(0)
|
||||
res.result.append(m.result[0])
|
||||
self.expect_lines = 1
|
||||
return res
|
||||
|
||||
# no complete message yet
|
||||
return None
|
||||
|
||||
def decode_single_message(self, encoded):
|
||||
# just decode a single message line
|
||||
|
||||
# 1) check for error msgs (more specific first)
|
||||
m = DEMO_RE_ERROR.match(encoded)
|
||||
if m:
|
||||
return ErrorMessage(**m.groupdict())
|
||||
|
||||
# 2) check for 'normal' message
|
||||
m = DEMO_RE_OTHER.match(encoded)
|
||||
if m:
|
||||
mgroups = m.groupdict()
|
||||
msgtype = mgroups.pop('msgtype')
|
||||
|
||||
# reformat devspec
|
||||
def helper(stuff, sep=','):
|
||||
if not stuff:
|
||||
return []
|
||||
if sep in stuff:
|
||||
return stuff.split(sep)
|
||||
return [stuff]
|
||||
devs = helper(mgroups.pop('devs'))
|
||||
pars = helper(mgroups.pop('pars'))
|
||||
props = helper(mgroups.pop('props'))
|
||||
|
||||
# sugar for listing stuff:
|
||||
# map list -> list *
|
||||
# map list x -> list x:*
|
||||
# map list x:y -> list x:y:*
|
||||
if msgtype == LIST:
|
||||
if not devs:
|
||||
devs = ['*']
|
||||
elif devs[0] != '*':
|
||||
if not pars:
|
||||
pars = ['*']
|
||||
elif pars[0] != '*':
|
||||
if not props:
|
||||
props = ['*']
|
||||
|
||||
# reformat cmdargs
|
||||
args = ast.literal_eval(mgroups.pop('args') or '()')
|
||||
if msgtype == COMMAND:
|
||||
mgroups['args'] = args
|
||||
|
||||
# reformat qualifiers
|
||||
print mgroups
|
||||
quals = dict(
|
||||
qual.split(
|
||||
'=',
|
||||
1) for qual in helper(
|
||||
mgroups.pop(
|
||||
'qualifiers',
|
||||
';')))
|
||||
|
||||
# reformat value
|
||||
result = []
|
||||
readback = mgroups.pop('readback')
|
||||
if readback or quals:
|
||||
valargs = dict()
|
||||
if devs:
|
||||
valargs['dev'] = devs[0]
|
||||
if pars:
|
||||
valargs['par'] = pars[0]
|
||||
if props:
|
||||
valargs['prop'] = props[0]
|
||||
result = [Value(floatify(readback), quals, **valargs)]
|
||||
if msgtype == LIST and result:
|
||||
result = [n.strip() for n in readback.split(',')]
|
||||
|
||||
# construct messageobj
|
||||
if msgtype in MESSAGE:
|
||||
return MESSAGE[msgtype](
|
||||
devs=devs, pars=pars, props=props, result=result, **mgroups)
|
||||
|
||||
return ErrorMessage(errortype="SyntaxError",
|
||||
errorstring="Can't handle %r" % encoded)
|
||||
|
||||
def tests(self):
|
||||
testmsg = ['list',
|
||||
'list *',
|
||||
'list device',
|
||||
'list device:param1,param2',
|
||||
'list *:*',
|
||||
'list *=ts,tcoil,mf,lhe,ln2;',
|
||||
'read blub=12;t=3',
|
||||
'command ts:stop()',
|
||||
'command mf:quench(1,"now")',
|
||||
'error GibbetNich query x:y:z=9 "blubub blah"',
|
||||
'#3',
|
||||
'read blub:a=12;t=3',
|
||||
'read blub:b=13;t=3.1',
|
||||
'read blub:c=14;t=3.3',
|
||||
]
|
||||
for m in testmsg:
|
||||
print repr(m)
|
||||
print self.decode(m)
|
||||
print
|
||||
|
||||
|
||||
DEMO_RE_MZ = re.compile(r"""^(?P<type>[a-z]+)? # request type word (read/write/list/...)
|
||||
\ ? # optional space
|
||||
(?P<device>[a-z][a-z0-9_]*)? # optional devicename
|
||||
(?:\:(?P<param>[a-z0-9_]*) # optional ':'+paramname
|
||||
(?:\:(?P<prop>[a-z0-9_]*))?)? # optinal ':' + propname
|
||||
(?:(?P<op>[=\?])(?P<value>[^;]+)(?:\;(?P<quals>.*))?)?$""", re.X)
|
||||
|
||||
|
||||
class DemoEncoder_MZ(MessageEncoder):
|
||||
|
||||
def decode(sef, encoded):
|
||||
m = DEMO_RE_MZ.match(encoded)
|
||||
if m:
|
||||
print "implement me !"
|
||||
return HelpRequest()
|
||||
|
||||
def encode(self, msg):
|
||||
"""msg object -> transport layer message"""
|
||||
# fun for Humans
|
||||
if isinstance(msg, HelpReply):
|
||||
r = []
|
||||
r.append("Try one of the following:")
|
||||
r.append("'list' to query a list of modules")
|
||||
r.append("'read <module>' to read a module")
|
||||
r.append("'list <module>' to query a list of parameters")
|
||||
r.append("... more to come")
|
||||
return '\n'.join(r)
|
||||
|
||||
return {
|
||||
ListDevicesRequest: lambda msg: "list",
|
||||
ListDevicesReply: lambda msg: "list=%s" % ','.join(sorted(msg.list_of_devices)),
|
||||
GetVersionRequest: lambda msg: "version",
|
||||
GetVersionReply: lambda msg: "version=%r" % msg.version,
|
||||
ListDeviceParamsRequest: lambda msg: "list %s" % msg.device,
|
||||
# do not include a '.' as param name!
|
||||
ListDeviceParamsReply: lambda msg: "list %s=%s" % (msg.device, ','.join(sorted(msg.params.keys()))),
|
||||
ReadValueRequest: lambda msg: "read %s" % msg.device,
|
||||
ReadValueReply: lambda msg: "read %s=%r" % (msg.device, msg.value),
|
||||
WriteValueRequest: lambda msg: "write %s=%r" % (msg.device, msg.value),
|
||||
WriteValueReply: lambda msg: "write %s=%r" % (msg.device, msg.readback_value),
|
||||
ReadParamRequest: lambda msg: "read %s:%s" % (msg.device, msg.param),
|
||||
ReadParamReply: lambda msg: "read %s:%s=%r" % (msg.device, msg.param, msg.value),
|
||||
WriteParamRequest: lambda msg: "write %s:%s=%r" % (msg.device, msg.param, msg.value),
|
||||
WriteParamReply: lambda msg: "write %s:%s=%r" % (msg.device, msg.param, msg.readback_value),
|
||||
# extensions
|
||||
ReadAllDevicesRequest: lambda msg: "",
|
||||
ReadAllDevicesReply: lambda msg: "",
|
||||
ListParamPropsRequest: lambda msg: "readprop %s:%s" % (msg.device, msg.param),
|
||||
ListParamPropsReply: lambda msg: ["readprop %s:%s" % (msg.device, msg.param)] + ["%s:%s:%s=%s" % (msg.device, msg.param, k, v) for k, v in sorted(msg.props.items())],
|
||||
ReadPropertyRequest: lambda msg: "readprop %s:%s:%s" % (msg.device, msg.param, msg.prop),
|
||||
ReadPropertyReply: lambda msg: "readprop %s:%s:%s=%s" % (msg.device, msg.param, msg.prop, msg.value),
|
||||
AsyncDataUnit: lambda msg: "",
|
||||
SubscribeRequest: lambda msg: "subscribe %s:%s" % (msg.device, msg.param) if msg.param else ("subscribe %s" % msg.device),
|
||||
SubscribeReply: lambda msg: "subscribe %s:%s" % (msg.device, msg.param) if msg.param else ("subscribe %s" % msg.device),
|
||||
UnSubscribeRequest: lambda msg: "",
|
||||
UnSubscribeReply: lambda msg: "",
|
||||
CommandRequest: lambda msg: "command %s:%s" % (msg.device, msg.command),
|
||||
CommandReply: lambda msg: "command %s:%s" % (msg.device, msg.command),
|
||||
# errors
|
||||
ErrorReply: lambda msg: "",
|
||||
InternalError: lambda msg: "",
|
||||
ProtocollError: lambda msg: "",
|
||||
CommandFailedError: lambda msg: "error CommandError %s:%s %s" % (msg.device, msg.param, msg.error),
|
||||
NoSuchCommandError: lambda msg: "error NoSuchCommand %s:%s" % (msg.device, msg.param, msg.error),
|
||||
NoSuchDeviceError: lambda msg: "error NoSuchModule %s" % msg.device,
|
||||
NoSuchParamError: lambda msg: "error NoSuchParameter %s:%s" % (msg.device, msg.param),
|
||||
ParamReadonlyError: lambda msg: "",
|
||||
UnsupportedFeatureError: lambda msg: "",
|
||||
InvalidParamValueError: lambda msg: "",
|
||||
}[msg.__class__](msg)
|
209
secop/protocol/encoding/demo_v4.py
Normal file
209
secop/protocol/encoding/demo_v4.py
Normal file
@ -0,0 +1,209 @@
|
||||
#!/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 secop.protocol.encoding import MessageEncoder
|
||||
from secop.protocol.messages import *
|
||||
from secop.protocol.errors import ProtocollError
|
||||
|
||||
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
|
||||
IDENTREPLY = 'Sine2020WP7.1&ISSE, SECoP, V2016-11-30, rc1' # literal
|
||||
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)
|
||||
COMMANDREPLY = 'doing' # +module:command +json args (if needed)
|
||||
WRITEREQUEST = 'change' # +module[:parameter] +json_value -> NO direct reply, calls TRIGGER internally!
|
||||
WRITEREPLY = 'changing' # +module[:parameter] +json_value -> NO direct reply, calls TRIGGER internally!
|
||||
TRIGGERREQUEST = 'read' # +module[:parameter] -> NO direct reply, calls TRIGGER internally!
|
||||
HEARTBEATREQUEST = 'ping' # +nonce_without_space
|
||||
HEARTBEATREPLY = 'pong' # +nonce_without_space
|
||||
EVENTTRIGGERREPLY = 'update' # +module[:parameter] +json_result_value_with_qualifiers NO REQUEST (use WRITE/TRIGGER)
|
||||
EVENTCOMMANDREPLY = 'done' # +module:command +json result (if needed)
|
||||
#EVENTWRITEREPLY = 'changed' # +module[:parameter] +json_result_value_with_qualifiers NO REQUEST (use WRITE/TRIGGER)
|
||||
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', 'SyntaxError', 'InternalError',
|
||||
'CommandRunning', 'Disabled',]
|
||||
# note: above strings need to be unique in the sense, that none is/or starts with another
|
||||
|
||||
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), 'arguments',),
|
||||
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, ),
|
||||
# EventMessage : (EVENTREPLY, lambda msg: "%s:%s" % (msg.module, msg.parameter or (msg.command+'()'))
|
||||
# if msg.parameter or msg.command else msg.module, 'value',),
|
||||
ErrorMessage : (ERRORREPLY, 'errorclass', 'errorinfo',),
|
||||
Value: (EVENTTRIGGERREPLY, lambda msg: "%s:%s" % (msg.module, msg.parameter or (msg.command+'()'))
|
||||
if msg.parameter or msg.command else msg.module,
|
||||
lambda msg: [msg.value, msg.qualifiers] if msg.qualifiers else [msg.value]),
|
||||
}
|
||||
DECODEMAP = {
|
||||
IDENTREQUEST : lambda spec, data: IdentifyRequest(),
|
||||
IDENTREPLY : lambda spec, data: IdentifyReply(encoded), # handled specially, listed here for completeness
|
||||
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], arguments=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),
|
||||
EVENTTRIGGERREPLY:lambda spec, data:Value(module=spec[0], parameter=spec[1], value=data[0], qualifiers=data[1] if len(data)>1 else {}),
|
||||
EVENTCOMMANDREPLY: lambda spec, data:None, # ignore this
|
||||
# EVENTWRITEREPLY: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]))
|
||||
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='SyntaxError',
|
||||
errorinfo='Regex did not match!',
|
||||
is_request=True)
|
||||
msgtype, msgspec, data = match.groups()
|
||||
if msgspec is None and data:
|
||||
return ErrorMessage(errorclass='InternalError',
|
||||
errorinfo='Regex matched json, but not spec!',
|
||||
is_request=True)
|
||||
|
||||
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)])
|
||||
return self.DECODEMAP[msgtype](msgspec, data)
|
||||
return ErrorMessage(errorclass='SyntaxError',
|
||||
errorinfo='%r: No Such Messagetype defined!' % encoded,
|
||||
is_request=True)
|
||||
|
||||
|
||||
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 in [EVENTTRIGGERREPLY]:#, EVENTWRITEREPLY]:
|
||||
msg = '%s a:b [3,{"t":193868}]' % msgtype
|
||||
print msg
|
||||
d=self.decode(msg)
|
||||
print d
|
||||
print self.encode(d)
|
||||
print
|
||||
print "---- Testing done -----"
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
#!/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
|
||||
@ -20,19 +20,28 @@
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
"""Define Client side proxies"""
|
||||
"""Encoding/decoding Messages"""
|
||||
|
||||
# nothing here yet.
|
||||
# implement as class as they may need some internal 'state' later on
|
||||
# (think compressors)
|
||||
|
||||
from secop.protocol.encoding import MessageEncoder
|
||||
from secop.protocol import messages
|
||||
from secop.lib.parsing import *
|
||||
|
||||
try:
|
||||
import cPickle as pickle
|
||||
except ImportError:
|
||||
import pickle
|
||||
|
||||
|
||||
def get_client(interfacespec):
|
||||
"""returns a client connected to the remote interface"""
|
||||
pass
|
||||
|
||||
class PickleEncoder(MessageEncoder):
|
||||
|
||||
class DeviceProxy(object):
|
||||
"""(In python) dynamically constructed object
|
||||
def encode(self, messageobj):
|
||||
"""msg object -> transport layer message"""
|
||||
return pickle.dumps(messageobj)
|
||||
|
||||
allowing access to the servers devices via the SECoP Protocol inbetween
|
||||
"""
|
||||
pass
|
||||
def decode(self, encoded):
|
||||
"""transport layer message -> msg object"""
|
||||
return pickle.loads(encoded)
|
209
secop/protocol/encoding/simplecomm.py
Normal file
209
secop/protocol/encoding/simplecomm.py
Normal file
@ -0,0 +1,209 @@
|
||||
#!/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 secop.protocol.encoding import MessageEncoder
|
||||
from secop.protocol.messages import *
|
||||
from secop.lib.parsing import *
|
||||
|
||||
import re
|
||||
import ast
|
||||
|
||||
|
||||
SCPMESSAGE = re.compile(
|
||||
r'^(?:(?P<errorcode>[0-9@])\ )?(?P<device>[a-zA-Z0-9_\*]*)(?:/(?P<param>[a-zA-Z0-9_\*]*))+(?P<op>[-+=\?\ ])?(?P<value>.*)')
|
||||
|
||||
|
||||
class SCPEncoder(MessageEncoder):
|
||||
|
||||
def encode(self, msg):
|
||||
"""msg object -> transport layer message"""
|
||||
# fun for Humans
|
||||
if isinstance(msg, HelpReply):
|
||||
r = []
|
||||
r.append("Try one of the following:")
|
||||
r.append("'/version?' to query the current version")
|
||||
r.append("'/modules?' to query the list of modules")
|
||||
r.append(
|
||||
"'<module>/parameters?' to query the list of params of a module")
|
||||
r.append("'<module>/value?' to query the value of a module")
|
||||
r.append("'<module>/status?' to query the status of a module")
|
||||
r.append("'<module>/target=<new_value>' to move a module")
|
||||
r.append("replies copy the request and are prefixed with an errorcode:")
|
||||
r.append(
|
||||
"0=OK,3=NoSuchCommand,4=NosuchDevice,5=NoSuchParam,6=SyntaxError,7=BadValue,8=Readonly,9=Forbidden,@=Async")
|
||||
r.append("extensions: @-prefix as error-code,")
|
||||
r.append("'<module>/+' subscribe all params of module")
|
||||
r.append("'<module>/<param>+' subscribe a param of a module")
|
||||
r.append("use '-' instead of '+' to unsubscribe")
|
||||
r.append("'<module>/commands?' list of commands")
|
||||
r.append(
|
||||
"'<module>/<command>@[possible args] execute command (ex. 'stop@')")
|
||||
return '\n'.join(r)
|
||||
|
||||
return {
|
||||
ListDevicesRequest: lambda msg: "devices?",
|
||||
ListDevicesReply: lambda msg: "0 devices=" + repr(list(msg.list_of_devices)),
|
||||
GetVersionRequest: lambda msg: "version?",
|
||||
GetVersionReply: lambda msg: "0 version=%r" % msg.version,
|
||||
ListDeviceParamsRequest: lambda msg: "%s/parameters?" % msg.device,
|
||||
ListDeviceParamsReply: lambda msg: "0 %s/parameters=%r" % (msg.device, list(msg.params)),
|
||||
ReadValueRequest: lambda msg: "%s/value?" % msg.device,
|
||||
ReadValueReply: lambda msg: "0 %s/value?%r" % (msg.device, msg.value),
|
||||
WriteValueRequest: lambda msg: "%s/value=%r" % (msg.device, msg.value),
|
||||
WriteValueReply: lambda msg: "0 %s/value=%r" % (msg.device, msg.value),
|
||||
ReadParamRequest: lambda msg: "%s/%s?" % (msg.device, msg.param),
|
||||
ReadParamReply: lambda msg: "0 %s/%s?%r" % (msg.device, msg.param, msg.value),
|
||||
WriteParamRequest: lambda msg: "%s/%s=%r" % (msg.device, msg.param, msg.value),
|
||||
WriteParamReply: lambda msg: "0 %s/%s=%r" % (msg.device, msg.param, msg.readback_value),
|
||||
# extensions
|
||||
ReadAllDevicesRequest: lambda msg: "*/value?",
|
||||
ReadAllDevicesReply: lambda msg: ["0 %s/value=%s" % (m.device, m.value) for m in msg.readValueReplies],
|
||||
ListParamPropsRequest: lambda msg: "%s/%s/?" % (msg.device, msg.param),
|
||||
ListParamPropsReply: lambda msg: ["0 %s/%s/%s" % (msg.device, msg.param, p) for p in msg.props],
|
||||
AsyncDataUnit: lambda msg: "@ %s/%s=%r" % (msg.devname, msg.pname, msg.value),
|
||||
SubscribeRequest: lambda msg: "%s/%s+" % (msg.devname, msg.pname),
|
||||
# violates spec ! we would need the original request here....
|
||||
SubscribeReply: lambda msg: "0 / %r" % [repr(s) for s in msg.subscriptions],
|
||||
UnSubscribeRequest: lambda msg: "%s/%s+" % (msg.devname, msg.pname),
|
||||
# violates spec ! we would need the original request here....
|
||||
UnSubscribeReply: lambda msg: "0 / %r" % [repr(s) for s in msg.subscriptions],
|
||||
# errors
|
||||
# violates spec ! we would need the original request here....
|
||||
ErrorReply: lambda msg: "1 /%r" % msg.error,
|
||||
# violates spec ! we would need the original request here....
|
||||
InternalError: lambda msg: "1 /%r" % msg.error,
|
||||
# violates spec ! we would need the original request here....
|
||||
ProtocollError: lambda msg: "6 /%r" % msg.error,
|
||||
# violates spec ! we would need the original request here....
|
||||
CommandFailedError: lambda msg: "1 %s/%s" % (msg.device, msg.command),
|
||||
# violates spec ! we would need the original request here....
|
||||
NoSuchCommandError: lambda msg: "3 %s/%s" % (msg.device, msg.command),
|
||||
# violates spec ! we would need the original request here....
|
||||
NoSuchDeviceError: lambda msg: "4 %s/ %r" % (msg.device, msg.error),
|
||||
# violates spec ! we would need the original request here....
|
||||
NoSuchParamError: lambda msg: "5 %s/%s %r" % (msg.device, msg.param, msg.error),
|
||||
# violates spec ! we would need the original request here....
|
||||
ParamReadonlyError: lambda msg: "8 %s/%s %r" % (msg.device, msg.param, msg.error),
|
||||
# violates spec ! we would need the original request here....
|
||||
UnsupportedFeatureError: lambda msg: "3 / %r" % msg.feature,
|
||||
# violates spec ! we would need the original request here....
|
||||
InvalidParamValueError: lambda msg: "7 %s/%s=%r %r" % (msg.device, msg.param, msg.value, msg.error),
|
||||
}[msg.__class__](msg)
|
||||
|
||||
def decode(self, encoded):
|
||||
"""transport layer message -> msg object"""
|
||||
match = SCPMESSAGE.match(encoded)
|
||||
if not(match):
|
||||
return HelpRequest()
|
||||
err, dev, par, op, val = match.groups()
|
||||
if val is not None:
|
||||
try:
|
||||
val = ast.literal_eval(val)
|
||||
except Exception as e:
|
||||
return SyntaxError('while decoding %r: %s' % (encoded, e))
|
||||
if err == '@':
|
||||
# async
|
||||
if op == '=':
|
||||
return AsyncDataUnit(dev, par, val)
|
||||
return ProtocolError("Asyncupdates must have op = '='!")
|
||||
elif err is None:
|
||||
# request
|
||||
if op == '+':
|
||||
# subscribe
|
||||
if dev:
|
||||
if par:
|
||||
return SubscribeRequest(dev, par)
|
||||
return SubscribeRequest(dev, '*')
|
||||
if op == '-':
|
||||
# unsubscribe
|
||||
if dev:
|
||||
if par:
|
||||
return UnsubscribeRequest(dev, par)
|
||||
return UnsubscribeRequest(dev, '*')
|
||||
if op == '?':
|
||||
if dev is None:
|
||||
# 'server' commands
|
||||
if par == 'devices':
|
||||
return ListDevicesRequest()
|
||||
elif par == 'version':
|
||||
return GetVersionRequest()
|
||||
return ProtocolError()
|
||||
if par == 'parameters':
|
||||
return ListDeviceParamsRequest(dev)
|
||||
elif par == 'value':
|
||||
return ReadValueRequest(dev)
|
||||
elif dev == '*' and par == 'value':
|
||||
return ReadAllDevicesRequest()
|
||||
else:
|
||||
return ReadParamRequest(dev, par)
|
||||
elif op == '=':
|
||||
if dev and (par == 'value'):
|
||||
return WriteValueRequest(dev, val)
|
||||
if par.endswith('/') and op == '?':
|
||||
return ListParamPropsRequest(dev, par)
|
||||
return WriteParamRequest(dev, par, val)
|
||||
elif err == '0':
|
||||
# reply
|
||||
if dev == '':
|
||||
if par == 'devices':
|
||||
return ListDevicesReply(val)
|
||||
elif par == 'version':
|
||||
return GetVersionReply(val)
|
||||
return ProtocolError(encoded)
|
||||
if par == 'parameters':
|
||||
return ListDeviceParamsReply(dev, val)
|
||||
if par == 'value':
|
||||
if op == '?':
|
||||
return ReadValueReply(dev, val)
|
||||
elif op == '=':
|
||||
return WriteValueReply(dev, val)
|
||||
return ProtocolError(encoded)
|
||||
if op == '+':
|
||||
return SubscribeReply(ast.literal_eval(dev))
|
||||
if op == '-':
|
||||
return UnSubscribeReply(ast.literal_eval(dev))
|
||||
if op == '?':
|
||||
return ReadParamReply(dev, par, val)
|
||||
if op == '=':
|
||||
return WriteParamReply(dev, par, val)
|
||||
return ProtocolError(encoded)
|
||||
else:
|
||||
# error
|
||||
if err in ('1', '2'):
|
||||
return InternalError(encoded)
|
||||
elif err == '3':
|
||||
return NoSuchCommandError(dev, par)
|
||||
elif err == '4':
|
||||
return NoSuchDeviceError(dev, encoded)
|
||||
elif err == '5':
|
||||
return NoSuchParamError(dev, par, val)
|
||||
elif err == '7':
|
||||
return InvalidParamValueError(dev, par, val, encoded)
|
||||
elif err == '8':
|
||||
return ParamReadonlyError(dev, par, encoded)
|
||||
else: # err == 6 or other stuff
|
||||
return ProtocollError(encoded)
|
65
secop/protocol/encoding/text.py
Normal file
65
secop/protocol/encoding/text.py
Normal file
@ -0,0 +1,65 @@
|
||||
#!/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 secop.protocol.encoding import MessageEncoder
|
||||
from secop.protocol import messages
|
||||
from secop.lib.parsing import *
|
||||
|
||||
|
||||
class TextEncoder(MessageEncoder):
|
||||
|
||||
def __init__(self):
|
||||
# build safe namespace
|
||||
ns = dict()
|
||||
for n in dir(messages):
|
||||
if n.endswith(('Request', 'Reply')):
|
||||
ns[n] = getattr(messages, n)
|
||||
self.namespace = ns
|
||||
|
||||
def encode(self, messageobj):
|
||||
"""msg object -> transport layer message"""
|
||||
# fun for Humans
|
||||
if isinstance(messageobj, messages.HelpMessage):
|
||||
return "Error: try one of the following requests:\n" + \
|
||||
'\n'.join(['%s(%s)' % (getattr(messages, m).__name__,
|
||||
', '.join(getattr(messages, m).ARGS))
|
||||
for m in dir(messages)
|
||||
if m.endswith('Request') and len(m) > len("Request")])
|
||||
res = []
|
||||
for k in messageobj.ARGS:
|
||||
res.append('%s=%r' % (k, getattr(messageobj, k, None)))
|
||||
result = '%s(%s)' % (messageobj.__class__.__name__, ', '.join(res))
|
||||
return result
|
||||
|
||||
def decode(self, encoded):
|
||||
"""transport layer message -> msg object"""
|
||||
# WARNING: highly unsafe!
|
||||
# think message='import os\nos.unlink('\')\n'
|
||||
try:
|
||||
return eval(encoded, self.namespace, {})
|
||||
except SyntaxError:
|
||||
return messages.HelpMessage()
|
70
secop/protocol/errors.py
Normal file
70
secop/protocol/errors.py
Normal file
@ -0,0 +1,70 @@
|
||||
# -*- 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 (internal) SECoP Errors"""
|
||||
|
||||
|
||||
class SECOPError(RuntimeError):
|
||||
def __init__(self, *args, **kwds):
|
||||
self.args = args
|
||||
for k,v in kwds.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
|
||||
class InternalError(SECOPError):
|
||||
pass
|
||||
|
||||
|
||||
class ProtocollError(SECOPError):
|
||||
pass
|
||||
|
||||
|
||||
# XXX: unifiy NoSuch...Error ?
|
||||
class NoSuchModuleError(SECOPError):
|
||||
pass
|
||||
|
||||
|
||||
class NoSuchParamError(SECOPError):
|
||||
pass
|
||||
|
||||
|
||||
class NoSuchCommandError(SECOPError):
|
||||
pass
|
||||
|
||||
|
||||
class ReadonlyError(SECOPError):
|
||||
pass
|
||||
|
||||
|
||||
class CommandFailedError(SECOPError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidParamValueError(SECOPError):
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("Minimal testing of errors....")
|
||||
|
||||
print "OK"
|
||||
print
|
62
secop/protocol/framing/__init__.py
Normal file
62
secop/protocol/framing/__init__.py
Normal file
@ -0,0 +1,62 @@
|
||||
#!/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 Frames"""
|
||||
|
||||
|
||||
# Base class
|
||||
class Framer(object):
|
||||
"""Frames and unframes an encoded message
|
||||
|
||||
also transforms the encoded message to the 'wire-format' (and vise-versa)
|
||||
|
||||
note: not all MessageEncoders can use all Framers,
|
||||
but the intention is to have this for as many as possible.
|
||||
"""
|
||||
|
||||
def encode(self, *frames):
|
||||
"""return the wire-data for the given messageframes"""
|
||||
raise NotImplemented
|
||||
|
||||
def decode(self, data):
|
||||
"""return a list of messageframes found in data"""
|
||||
raise NotImplemented
|
||||
|
||||
def reset(self):
|
||||
"""resets the de/encoding stage (clears internal information)"""
|
||||
raise NotImplemented
|
||||
|
||||
|
||||
# now some Implementations
|
||||
from null import NullFramer
|
||||
from eol import EOLFramer
|
||||
from rle import RLEFramer
|
||||
from demo import DemoFramer
|
||||
|
||||
FRAMERS = {
|
||||
'null': NullFramer,
|
||||
'eol': EOLFramer,
|
||||
'rle': RLEFramer,
|
||||
'demo': DemoFramer,
|
||||
}
|
||||
|
||||
__ALL__ = ['FRAMERS']
|
84
secop/protocol/framing/demo.py
Normal file
84
secop/protocol/framing/demo.py
Normal file
@ -0,0 +1,84 @@
|
||||
#!/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 Frames"""
|
||||
|
||||
from secop.protocol.framing import Framer
|
||||
|
||||
|
||||
class DemoFramer(Framer):
|
||||
"""Text based message framer
|
||||
|
||||
frmes are delimited by '\n'
|
||||
messages are delimited by '\n\n'
|
||||
'\r' is ignored
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.data = b''
|
||||
self.decoded = []
|
||||
|
||||
def encode(self, frames):
|
||||
"""add transport layer encapsulation/framing of messages"""
|
||||
if isinstance(frames, (tuple, list)):
|
||||
return b'\n'.join(frames) + b'\n\n'
|
||||
return b'%s\n\n' % frames
|
||||
|
||||
def decode(self, data):
|
||||
"""remove transport layer encapsulation/framing of messages
|
||||
|
||||
returns a list of messageframes which got decoded from data!
|
||||
"""
|
||||
self.data += data
|
||||
res = []
|
||||
while b'\n' in self.data:
|
||||
frame, self.data = self.data.split(b'\n', 1)
|
||||
if frame.endswith('\r'):
|
||||
frame = frame[:-1]
|
||||
if self.data.startswith('\r'):
|
||||
self.data = self.data[1:]
|
||||
res.append(frame)
|
||||
return res
|
||||
|
||||
def decode2(self, data):
|
||||
"""remove transport layer encapsulation/framing of messages
|
||||
|
||||
returns a _list_ of messageframes which got decoded from data!
|
||||
"""
|
||||
self.data += data.replace(b'\r', '')
|
||||
while b'\n' in self.data:
|
||||
frame, self.data = self.data.split(b'\n', 1)
|
||||
if frame:
|
||||
# not an empty line -> belongs to this set of messages
|
||||
self.decoded.append(frame)
|
||||
else:
|
||||
# empty line -> our set of messages is finished decoding
|
||||
res = self.decoded
|
||||
self.decoded = []
|
||||
return res
|
||||
return None
|
||||
|
||||
def reset(self):
|
||||
self.data = b''
|
||||
self.decoded = []
|
||||
|
||||
|
57
secop/protocol/framing/eol.py
Normal file
57
secop/protocol/framing/eol.py
Normal file
@ -0,0 +1,57 @@
|
||||
#!/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 Frames"""
|
||||
|
||||
from secop.protocol.framing import Framer
|
||||
|
||||
|
||||
class EOLFramer(Framer):
|
||||
"""Text based message framer
|
||||
|
||||
messages are delimited by '\r\n'
|
||||
upon reception the end of a message is detected by '\r\n','\n' or '\n\r'
|
||||
"""
|
||||
data = b''
|
||||
|
||||
def encode(self, *frames):
|
||||
"""add transport layer encapsulation/framing of messages"""
|
||||
return b'%s\r\n' % b'\r\n'.join(frames)
|
||||
|
||||
def decode(self, data):
|
||||
"""remove transport layer encapsulation/framing of messages
|
||||
|
||||
returns a list of messageframes which got decoded from data!
|
||||
"""
|
||||
self.data += data
|
||||
res = []
|
||||
while b'\n' in self.data:
|
||||
frame, self.data = self.data.split(b'\n', 1)
|
||||
if frame.endswith('\r'):
|
||||
frame = frame[:-1]
|
||||
if self.data.startswith('\r'):
|
||||
self.data = self.data[1:]
|
||||
res.append(frame)
|
||||
return res
|
||||
|
||||
def reset(self):
|
||||
self.data = b''
|
@ -20,7 +20,21 @@
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
"""provides transport layer encapsulation for SECoP"""
|
||||
"""Encoding/decoding Frames"""
|
||||
|
||||
from framing import FRAMERS
|
||||
from encoding import ENCODERS
|
||||
from secop.protocol.framing import Framer
|
||||
|
||||
|
||||
class NullFramer(Framer):
|
||||
"""do-nothing-framer
|
||||
|
||||
assumes messages are framed by themselfs or the interface does it already.
|
||||
"""
|
||||
|
||||
def encode(self, *frames):
|
||||
"""add transport layer encapsulation/framing of messages"""
|
||||
return ''.join(frames)
|
||||
|
||||
def decode(self, data):
|
||||
"""remove transport layer encapsulation/framing of messages"""
|
||||
return [data]
|
74
secop/protocol/framing/rle.py
Normal file
74
secop/protocol/framing/rle.py
Normal file
@ -0,0 +1,74 @@
|
||||
#!/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 Frames"""
|
||||
|
||||
from secop.protocol.framing import Framer
|
||||
|
||||
|
||||
class RLEFramer(Framer):
|
||||
data = b''
|
||||
frames_to_go = 0
|
||||
|
||||
def encode(self, *frames):
|
||||
"""add transport layer encapsulation/framing of messages"""
|
||||
# format is 'number of frames:[framelengt:frme]*N'
|
||||
frdata = ['%d:%s' % (len(frame), frame) for frame in frames]
|
||||
return b'%d:' + b''.join(frdata)
|
||||
|
||||
def decode(self, data):
|
||||
"""remove transport layer encapsulation/framing of messages
|
||||
|
||||
returns a list of messageframes which got decoded from data!
|
||||
"""
|
||||
self.data += data
|
||||
res = []
|
||||
while self.data:
|
||||
if frames_to_go == 0:
|
||||
if ':' in self.data:
|
||||
# scan for and decode 'number of frames'
|
||||
frnum, self.data = self.data.split(':', 1)
|
||||
try:
|
||||
self.frames_to_go = int(frnum)
|
||||
except ValueError:
|
||||
# can not recover, complain!
|
||||
raise FramingError('invalid start of message found!')
|
||||
else:
|
||||
# not enough data to decode number of frames,
|
||||
# return what we have
|
||||
return res
|
||||
while self.frames_to_go:
|
||||
# there are still some (partial) frames stuck inside self.data
|
||||
frlen, self.data = self.data.split(':', 1)
|
||||
if len(self.data) >= frlen:
|
||||
res.append(self.data[:frlen])
|
||||
self.data = self.data[frlen:]
|
||||
self.frames_to_go -= 1
|
||||
else:
|
||||
# not enough data for this frame, return what we have
|
||||
return res
|
||||
|
||||
def reset(self):
|
||||
self.data = b''
|
||||
self.frames_to_go = 0
|
||||
|
||||
|
@ -30,12 +30,17 @@ import SocketServer
|
||||
DEF_PORT = 10767
|
||||
MAX_MESSAGE_SIZE = 1024
|
||||
|
||||
from secop.protocol.encoding import ENCODERS
|
||||
from secop.protocol.framing import FRAMERS
|
||||
from secop.protocol.messages import HelpMessage
|
||||
|
||||
class TCPRequestHandler(SocketServer.BaseRequestHandler):
|
||||
|
||||
def setup(self):
|
||||
self.log = self.server.log
|
||||
self._queue = collections.deque(maxlen=100)
|
||||
self.framing = self.server.framingCLS()
|
||||
self.encoding = self.server.encodingCLS()
|
||||
|
||||
def handle(self):
|
||||
"""handle a new tcp-connection"""
|
||||
@ -44,16 +49,24 @@ class TCPRequestHandler(SocketServer.BaseRequestHandler):
|
||||
clientaddr = self.client_address
|
||||
serverobj = self.server
|
||||
self.log.debug("handling new connection from %s" % repr(clientaddr))
|
||||
|
||||
# notify dispatcher of us
|
||||
serverobj.dispatcher.add_connection(self)
|
||||
|
||||
mysocket.settimeout(.3)
|
||||
mysocket.setblocking(False)
|
||||
# mysocket.setblocking(False)
|
||||
# start serving
|
||||
while True:
|
||||
# send replys fist, then listen for requests, timing out after 0.1s
|
||||
while self._queue:
|
||||
mysocket.sendall(self._queue.popleft())
|
||||
# put message into encoder to get frame(s)
|
||||
# put frame(s) into framer to get bytestring
|
||||
# send bytestring
|
||||
outmsg = self._queue.popleft()
|
||||
outframes = self.encoding.encode(outmsg)
|
||||
outdata = self.framing.encode(outframes)
|
||||
mysocket.sendall(outdata)
|
||||
|
||||
# XXX: improve: use polling/select here?
|
||||
try:
|
||||
data = mysocket.recv(MAX_MESSAGE_SIZE)
|
||||
@ -62,8 +75,19 @@ class TCPRequestHandler(SocketServer.BaseRequestHandler):
|
||||
# XXX: should use select instead of busy polling
|
||||
if not data:
|
||||
continue
|
||||
# put data into (de-) framer,
|
||||
# put frames into (de-) coder and if a message appear,
|
||||
# call dispatcher.handle_request(self, message)
|
||||
# dispatcher will queue the reply before returning
|
||||
serverobj.dispatcher.handle_request(self, data)
|
||||
frames = self.framing.decode(data)
|
||||
if frames is not None:
|
||||
if not frames: # empty list
|
||||
self.queue_reply(HelpMessage(MSGTYPE=reply))
|
||||
for frame in frames:
|
||||
reply = None
|
||||
msg = self.encoding.decode(frame)
|
||||
if msg:
|
||||
serverobj.dispatcher.handle_request(self, msg)
|
||||
|
||||
def queue_async_reply(self, data):
|
||||
"""called by dispatcher for async data units"""
|
||||
@ -81,6 +105,8 @@ class TCPRequestHandler(SocketServer.BaseRequestHandler):
|
||||
# close socket
|
||||
try:
|
||||
self.request.shutdown(socket.SHUT_RDWR)
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
self.request.close()
|
||||
|
||||
@ -89,15 +115,21 @@ class TCPServer(SocketServer.ThreadingTCPServer):
|
||||
daemon_threads = True
|
||||
allow_reuse_address = True
|
||||
|
||||
def __init__(self, logger, serveropts, dispatcher):
|
||||
def __init__(self, logger, interfaceopts, dispatcher):
|
||||
self.dispatcher = dispatcher
|
||||
self.log = logger
|
||||
bindto = serveropts.pop('bindto', 'localhost')
|
||||
portnum = int(serveropts.pop('bindport', DEF_PORT))
|
||||
bindto = interfaceopts.pop('bindto', 'localhost')
|
||||
portnum = int(interfaceopts.pop('bindport', DEF_PORT))
|
||||
if ':' in bindto:
|
||||
bindto, _port = bindto.rsplit(':')
|
||||
portnum = int(_port)
|
||||
# tcp is a byte stream, so we need Framers (to get frames)
|
||||
# and encoders (to en/decode messages from frames)
|
||||
self.framingCLS = FRAMERS[interfaceopts.pop('framing', 'none')]
|
||||
self.encodingCLS = ENCODERS[interfaceopts.pop('encoding', 'pickle')]
|
||||
self.log.debug("TCPServer binding to %s:%d" % (bindto, portnum))
|
||||
self.log.debug("TCPServer using framing=%s" % self.framingCLS.__name__)
|
||||
self.log.debug("TCPServer using encoding=%s" % self.encodingCLS.__name__)
|
||||
SocketServer.ThreadingTCPServer.__init__(self, (bindto, portnum),
|
||||
TCPRequestHandler,
|
||||
bind_and_activate=True)
|
160
secop/protocol/messages.py
Normal file
160
secop/protocol/messages.py
Normal file
@ -0,0 +1,160 @@
|
||||
# -*- 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 SECoP Messages"""
|
||||
|
||||
|
||||
class Message(object):
|
||||
"""base class for messages"""
|
||||
is_request = False
|
||||
is_reply = False
|
||||
is_error = False
|
||||
|
||||
def __init__(self, **kwds):
|
||||
self.ARGS = set()
|
||||
for k, v in kwds.items():
|
||||
self.setvalue(k, v)
|
||||
|
||||
def setvalue(self, key, value):
|
||||
setattr(self, key, value)
|
||||
self.ARGS.add(key)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__class__.__name__ + '(' + \
|
||||
', '.join('%s=%s' % (k, repr(getattr(self, k)))
|
||||
for k in sorted(self.ARGS)) + ')'
|
||||
|
||||
def as_dict(self):
|
||||
"""returns set parameters as dict"""
|
||||
return dict(map(lambda k:(k, getattr(self,k)),self.ARGS))
|
||||
|
||||
|
||||
class Value(object):
|
||||
|
||||
def __init__(self, module, parameter=None, command=None, value=Ellipsis, **qualifiers):
|
||||
self.module = module
|
||||
self.parameter = parameter
|
||||
self.command = command
|
||||
self.value = value
|
||||
self.qualifiers = qualifiers
|
||||
self.msgtype = 'update' # 'changed' or 'done'
|
||||
|
||||
def __repr__(self):
|
||||
devspec = self.module
|
||||
if self.parameter:
|
||||
devspec = '%s:%s' % (devspec, self.parameter)
|
||||
elif self.command:
|
||||
devspec = '%s:%s()' % (devspec, self.command)
|
||||
return '%s:Value(%s)' % (devspec, ', '.join(
|
||||
[repr(self.value)] + ['%s=%s' % (k, repr(v)) for k, v in self.qualifiers.items()]))
|
||||
|
||||
|
||||
class IdentifyRequest(Message):
|
||||
is_request = True
|
||||
|
||||
class IdentifyReply(Message):
|
||||
is_reply = True
|
||||
version_string = None
|
||||
|
||||
class DescribeRequest(Message):
|
||||
is_request = True
|
||||
|
||||
class DescribeReply(Message):
|
||||
is_reply = True
|
||||
equipment_id = None
|
||||
description = None
|
||||
|
||||
class ActivateRequest(Message):
|
||||
is_request = True
|
||||
|
||||
class ActivateReply(Message):
|
||||
is_reply = True
|
||||
|
||||
class DeactivateRequest(Message):
|
||||
is_request = True
|
||||
|
||||
class DeactivateReply(Message):
|
||||
is_reply = True
|
||||
|
||||
class CommandRequest(Message):
|
||||
is_request = True
|
||||
command = ''
|
||||
arguments = []
|
||||
|
||||
class CommandReply(Message):
|
||||
is_reply = True
|
||||
command = ''
|
||||
arguments = None
|
||||
|
||||
class WriteRequest(Message):
|
||||
is_request = True
|
||||
module = None
|
||||
parameter = None
|
||||
value = None
|
||||
|
||||
class WriteReply(Message):
|
||||
is_reply = True
|
||||
module = None
|
||||
parameter = None
|
||||
value = None
|
||||
|
||||
class PollRequest(Message):
|
||||
is_request = True
|
||||
module = None
|
||||
parameter = None
|
||||
|
||||
class HeartbeatRequest(Message):
|
||||
is_request = True
|
||||
nonce = 'alive'
|
||||
|
||||
class HeartbeatReply(Message):
|
||||
is_reply = True
|
||||
nonce = 'undefined'
|
||||
|
||||
class EventMessage(Message):
|
||||
# use Value directly for Replies !
|
||||
is_reply = True
|
||||
module = None
|
||||
parameter = None
|
||||
command = None
|
||||
value = None # Value object ! (includes qualifiers!)
|
||||
|
||||
class ErrorMessage(Message):
|
||||
is_error = True
|
||||
errorclass = 'InternalError'
|
||||
errorinfo = None
|
||||
|
||||
|
||||
class HelpMessage(Message):
|
||||
is_reply = True
|
||||
is_request = True
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("Minimal testing of messages....")
|
||||
m = Message(MSGTYPE='test', a=1, b=2, c='x')
|
||||
print m
|
||||
print ReadMessage(devs=['a'], result=[Value(12.3)])
|
||||
|
||||
print "OK"
|
||||
print
|
202
secop/protocol/messages_old.py
Normal file
202
secop/protocol/messages_old.py
Normal file
@ -0,0 +1,202 @@
|
||||
# -*- 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 SECoP Messages"""
|
||||
|
||||
# Request Types
|
||||
REQUEST = 'request'
|
||||
REPLY = 'reply'
|
||||
ERROR = 'error'
|
||||
|
||||
# Message types ('actions') hint: fetch is list+read
|
||||
LIST = 'list'
|
||||
READ = 'read'
|
||||
WRITE = 'write'
|
||||
COMMAND = 'command'
|
||||
POLL = 'poll'
|
||||
SUBSCRIBE = 'subscribe'
|
||||
UNSUBSCRIBE = 'unsubscribe'
|
||||
TRIGGER = 'trigger'
|
||||
EVENT = 'event'
|
||||
ERROR = 'error'
|
||||
HELP = 'help'
|
||||
|
||||
# base class for messages
|
||||
|
||||
|
||||
class Message(object):
|
||||
|
||||
MSGTYPE = 'Undefined'
|
||||
devs = None
|
||||
pars = None
|
||||
props = None
|
||||
result = None
|
||||
error = None
|
||||
ARGS = None
|
||||
errortype = None
|
||||
|
||||
def __init__(self, **kwds):
|
||||
self.devs = []
|
||||
self.pars = []
|
||||
self.props = []
|
||||
self.result = []
|
||||
self.ARGS = set()
|
||||
for k, v in kwds.items():
|
||||
self.setvalue(k, v)
|
||||
|
||||
def setvalue(self, key, value):
|
||||
setattr(self, key, value)
|
||||
self.ARGS.add(key)
|
||||
|
||||
@property
|
||||
def NAME(self):
|
||||
# generate sensible name
|
||||
r = 'Message'
|
||||
if self.props:
|
||||
r = 'Property' if self.props != ['*'] else 'Properties'
|
||||
elif self.pars:
|
||||
r = 'Parameter' if self.pars != ['*'] else 'Parameters'
|
||||
elif self.devs:
|
||||
r = 'Device' if self.devs != ['*'] else 'Devices'
|
||||
|
||||
t = ''
|
||||
if self.MSGTYPE in [LIST, READ, WRITE, COMMAND,
|
||||
POLL, SUBSCRIBE, UNSUBSCRIBE, HELP]:
|
||||
t = 'Request' if not self.result else 'Reply'
|
||||
|
||||
if self.errortype is None:
|
||||
return self.MSGTYPE.title() + r + t
|
||||
else:
|
||||
return self.errortype + 'Error'
|
||||
|
||||
def __repr__(self):
|
||||
return self.NAME + '(' + \
|
||||
', '.join('%s=%r' % (k, getattr(self, k))
|
||||
for k in self.ARGS if getattr(self, k) is not None) + ')'
|
||||
|
||||
|
||||
class Value(object):
|
||||
|
||||
def __init__(self, value=Ellipsis, qualifiers=None, **kwds):
|
||||
self.dev = ''
|
||||
self.param = ''
|
||||
self.prop = ''
|
||||
self.value = value
|
||||
self.qualifiers = qualifiers or dict()
|
||||
self.__dict__.update(kwds)
|
||||
|
||||
def __repr__(self):
|
||||
devspec = self.dev
|
||||
if self.param:
|
||||
devspec = '%s:%s' % (devspec, self.param)
|
||||
if self.prop:
|
||||
devspec = '%s:%s' % (devspec, self.prop)
|
||||
return '%s:Value(%s)' % (devspec, ', '.join(
|
||||
[repr(self.value)] + ['%s=%r' % (k, v) for k, v in self.qualifiers.items()]))
|
||||
|
||||
|
||||
class ListMessage(Message):
|
||||
MSGTYPE = LIST
|
||||
|
||||
|
||||
class ReadMessage(Message):
|
||||
MSGTYPE = READ # return cached value
|
||||
|
||||
|
||||
class WriteMessage(Message):
|
||||
MSGTYPE = WRITE # write value to some spec
|
||||
target = None # actually float or string
|
||||
|
||||
|
||||
class CommandMessage(Message):
|
||||
MSGTYPE = COMMAND
|
||||
cmd = '' # always string
|
||||
args = []
|
||||
result = []
|
||||
|
||||
|
||||
class PollMessage(Message):
|
||||
MSGTYPE = POLL # read HW and return hw_value
|
||||
|
||||
|
||||
class SubscribeMessage(Message):
|
||||
MSGTYPE = SUBSCRIBE
|
||||
|
||||
|
||||
class UnsubscribeMessage(Message):
|
||||
MSGTYPE = UNSUBSCRIBE
|
||||
|
||||
|
||||
class TriggerMessage(Message):
|
||||
MSGTYPE = TRIGGER
|
||||
|
||||
|
||||
class EventMessage(Message):
|
||||
MSGTYPE = EVENT
|
||||
|
||||
|
||||
class ErrorMessage(Message):
|
||||
MSGTYPE = ERROR
|
||||
errorstring = 'an unhandled error occured'
|
||||
errortype = 'UnknownError'
|
||||
|
||||
|
||||
class HelpMessage(Message):
|
||||
MSGTYPE = HELP
|
||||
|
||||
|
||||
class NoSuchDeviceError(ErrorMessage):
|
||||
def __init__(self, *devs):
|
||||
ErrorMessage.__init__(self, devs=devs, errorstring="Device %r does not exist" % devs[0], errortype='NoSuchDevice')
|
||||
|
||||
|
||||
class NoSuchParamError(ErrorMessage):
|
||||
def __init__(self, dev, *params):
|
||||
ErrorMessage.__init__(self, devs=(dev,), params=params, errorstring="Device %r has no parameter %r" % (dev, params[0]), errortype='NoSuchParam')
|
||||
|
||||
|
||||
class ParamReadonlyError(ErrorMessage):
|
||||
def __init__(self, dev, *params):
|
||||
ErrorMessage.__init__(self, devs=(dev,), params=params, errorstring="Device %r, parameter %r is not writeable!" % (dev, params[0]), errortype='ParamReadOnly')
|
||||
|
||||
|
||||
class InvalidParamValueError(ErrorMessage):
|
||||
def __init__(self, dev, param, value, e):
|
||||
ErrorMessage.__init__(self, devs=(dev,), params=params, values=(value), errorstring=str(e), errortype='InvalidParamValueError')
|
||||
|
||||
|
||||
class InternalError(ErrorMessage):
|
||||
def __init__(self, err, **kwds):
|
||||
ErrorMessage.__init__(self, errorstring=str(err), errortype='InternalError', **kwds)
|
||||
|
||||
|
||||
MESSAGE = dict((cls.MSGTYPE, cls) for cls in [HelpMessage, ErrorMessage, EventMessage, TriggerMessage, UnsubscribeMessage, SubscribeMessage,
|
||||
PollMessage, CommandMessage, WriteMessage, ReadMessage, ListMessage])
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("Minimal testing of messages....")
|
||||
m = Message(MSGTYPE='test', a=1, b=2, c='x')
|
||||
print m
|
||||
print ReadMessage(devs=['a'], result=[Value(12.3)])
|
||||
|
||||
print "OK"
|
||||
print
|
@ -23,18 +23,21 @@
|
||||
|
||||
"""Define helpers"""
|
||||
import os
|
||||
import time
|
||||
import psutil
|
||||
import threading
|
||||
import ConfigParser
|
||||
|
||||
from daemon import DaemonContext
|
||||
from daemon.pidfile import TimeoutPIDLockFile
|
||||
|
||||
import loggers
|
||||
from lib import get_class
|
||||
from protocol.dispatcher import Dispatcher
|
||||
from protocol.interface import INTERFACES
|
||||
from protocol.transport import ENCODERS, FRAMERS
|
||||
from errors import ConfigError
|
||||
from secop.lib import get_class
|
||||
from secop.protocol.dispatcher import Dispatcher
|
||||
from secop.protocol.interface import INTERFACES
|
||||
#from secop.protocol.encoding import ENCODERS
|
||||
#from secop.protocol.framing import FRAMERS
|
||||
from secop.errors import ConfigError
|
||||
|
||||
|
||||
class Server(object):
|
||||
@ -71,7 +74,20 @@ class Server(object):
|
||||
self._processCfg()
|
||||
|
||||
self.log.info('startup done, handling transport messages')
|
||||
self._interface.serve_forever()
|
||||
self._threads = set()
|
||||
for _if in self._interfaces:
|
||||
self.log.debug('starting thread for interface %r' % _if)
|
||||
t = threading.Thread(target=_if.serve_forever)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
self._threads.add(t)
|
||||
while self._threads:
|
||||
time.sleep(1)
|
||||
for t in self._threads:
|
||||
if not t.is_alive():
|
||||
self.log.debug('thread %r died (%d still running)' % (t,len(self._threads)))
|
||||
t.join()
|
||||
self._threads.discard(t)
|
||||
|
||||
def _processCfg(self):
|
||||
self.log.debug('Parse config file %s ...' % self._cfgfile)
|
||||
@ -81,16 +97,12 @@ class Server(object):
|
||||
self.log.error('Couldn\'t read cfg file !')
|
||||
raise ConfigError('Couldn\'t read cfg file %r' % self._cfgfile)
|
||||
|
||||
if not parser.has_section('server'):
|
||||
raise ConfigError(
|
||||
'cfg file %r needs a \'server\' section!' % self._cfgfile)
|
||||
self._interfaces = []
|
||||
|
||||
deviceopts = []
|
||||
serveropts = {}
|
||||
interfaceopts = []
|
||||
equipment_id = 'unknown'
|
||||
for section in parser.sections():
|
||||
if section == 'server':
|
||||
# store for later
|
||||
serveropts = dict(item for item in parser.items('server'))
|
||||
if section.lower().startswith('device '):
|
||||
# device section
|
||||
# omit leading 'device ' string
|
||||
@ -105,8 +117,23 @@ class Server(object):
|
||||
devopts['class'] = get_class(devopts['class'])
|
||||
# all went well so far
|
||||
deviceopts.append([devname, devopts])
|
||||
if section.lower().startswith('interface '):
|
||||
# interface section
|
||||
# omit leading 'interface ' string
|
||||
ifname = section[len('interface '):]
|
||||
ifopts = dict(item for item in parser.items(section))
|
||||
if 'interface' not in ifopts:
|
||||
self.log.error('Interface %s needs an interface option!')
|
||||
raise ConfigError(
|
||||
'cfgfile %r: Interface %s needs an interface option!'
|
||||
% (self._cfgfile, ifname))
|
||||
# all went well so far
|
||||
interfaceopts.append([ifname, ifopts])
|
||||
if parser.has_option('equipment', 'id'):
|
||||
equipment_id = parser.get('equipment', 'id')
|
||||
|
||||
self._processServerOptions(serveropts)
|
||||
self._dispatcher = self._buildObject('Dispatcher', Dispatcher, dict(equipment_id=equipment_id))
|
||||
self._processInterfaceOptions(interfaceopts)
|
||||
self._processDeviceOptions(deviceopts)
|
||||
|
||||
def _processDeviceOptions(self, deviceopts):
|
||||
@ -132,26 +159,24 @@ class Server(object):
|
||||
# connect devices with dispatcher
|
||||
for devname, devobj, export in devs:
|
||||
self.log.info('registering device %r' % devname)
|
||||
self._dispatcher.register_device(devobj, devname, export)
|
||||
self._dispatcher.register_module(devobj, devname, export)
|
||||
# also call init on the devices
|
||||
devobj.init()
|
||||
# call a possibly empty postinit on each device after registering all
|
||||
for _devname, devobj, _export in devs:
|
||||
postinit = getattr(devobj, 'postinit', None)
|
||||
if postinit:
|
||||
postinit()
|
||||
|
||||
def _processServerOptions(self, serveropts):
|
||||
# eval serveropts
|
||||
framingClass = FRAMERS[serveropts.pop('framing')]
|
||||
encodingClass = ENCODERS[serveropts.pop('encoding')]
|
||||
interfaceClass = INTERFACES[serveropts.pop('interface')]
|
||||
|
||||
self._dispatcher = self._buildObject('Dispatcher', Dispatcher,
|
||||
dict(encoding=encodingClass(),
|
||||
framing=framingClass()))
|
||||
|
||||
# split 'server' section to allow more than one interface
|
||||
# also means to move encoding and framing to the interface,
|
||||
# so that the dispatcher becomes agnostic
|
||||
self._interface = self._buildObject('Interface', interfaceClass,
|
||||
serveropts,
|
||||
self._dispatcher)
|
||||
def _processInterfaceOptions(self, interfaceopts):
|
||||
# eval interfaces
|
||||
self._interfaces = []
|
||||
for ifname, ifopts in interfaceopts:
|
||||
ifclass = ifopts.pop('interface')
|
||||
ifclass = INTERFACES[ifclass]
|
||||
interface = self._buildObject(ifname, ifclass,
|
||||
ifopts, self._dispatcher)
|
||||
self._interfaces.append(interface)
|
||||
|
||||
def _buildObject(self, name, cls, options, *args):
|
||||
self.log.debug('Creating %s ...' % name)
|
@ -168,6 +168,23 @@ class vector(object):
|
||||
return ('%s(%s)' % (self.__class__.__name__, self.argstr))
|
||||
|
||||
|
||||
class record(object):
|
||||
"""fixed length, eache element has its own name and validator"""
|
||||
|
||||
def __init__(self, **kwds):
|
||||
self.validators = args
|
||||
self.argstr = ', '.join([repr(e) for e in kwds.items()])
|
||||
|
||||
def __call__(self, arg):
|
||||
if len(args) != len(self.validators):
|
||||
raise ValueError('Vector: need exactly %d elementes (got %d)' %
|
||||
len(self.validators), len(args))
|
||||
return tuple(v(e) for v, e in zip(self.validators, args))
|
||||
|
||||
def __repr__(self):
|
||||
return ('%s(%s)' % (self.__class__.__name__, self.argstr))
|
||||
|
||||
|
||||
class oneof(object):
|
||||
"""needs to comply with one of the given validators/values"""
|
||||
|
||||
@ -192,7 +209,7 @@ class oneof(object):
|
||||
return ('%s(%s)' % (self.__class__.__name__, self.argstr))
|
||||
|
||||
|
||||
class mapping(object):
|
||||
class enum(object):
|
||||
|
||||
def __init__(self, *args, **kwds):
|
||||
self.mapping = {}
|
@ -1,421 +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>
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
"""Dispatcher for SECoP Messages
|
||||
|
||||
Interface to the service offering part:
|
||||
|
||||
- 'handle_request(connectionobj, data)' handles incoming request
|
||||
will call 'queue_request(data)' on connectionobj before returning
|
||||
- 'add_connection(connectionobj)' registers new connection
|
||||
- 'remove_connection(connectionobj)' removes now longer functional connection
|
||||
- may at any time call 'queue_async_request(connobj, data)' on the connobj
|
||||
|
||||
Interface to the devices:
|
||||
- add_device(devname, devobj, export=True) registers a new device under the
|
||||
given name, may also register it for exporting (making accessible)
|
||||
- get_device(devname) returns the requested device or None
|
||||
- remove_device(devname_or_obj): removes the device (during shutdown)
|
||||
|
||||
internal stuff which may be called
|
||||
- list_devices(): return a list of devices + descriptive data as dict
|
||||
- list_device_params():
|
||||
return a list of paramnames for this device + descriptive data
|
||||
"""
|
||||
|
||||
import time
|
||||
import threading
|
||||
|
||||
from messages import *
|
||||
|
||||
|
||||
class Dispatcher(object):
|
||||
|
||||
def __init__(self, logger, options):
|
||||
self.log = logger
|
||||
# XXX: move framing and encoding to interface!
|
||||
self.framing = options.pop('framing')
|
||||
self.encoding = options.pop('encoding')
|
||||
# map ALL devname -> devobj
|
||||
self._dispatcher_devices = {}
|
||||
# list of EXPORTED devices
|
||||
self._dispatcher_export = []
|
||||
# list all connections
|
||||
self._dispatcher_connections = []
|
||||
# map eventname -> list of subscribed connections
|
||||
self._dispatcher_subscriptions = {}
|
||||
self._dispatcher_lock = threading.RLock()
|
||||
|
||||
def handle_request(self, conn, data):
|
||||
"""handles incoming request
|
||||
|
||||
will call 'queue.request(data)' on conn to send reply before returning
|
||||
"""
|
||||
self.log.debug('Dispatcher: handling data: %r' % data)
|
||||
# play thread safe !
|
||||
with self._dispatcher_lock:
|
||||
# de-frame data
|
||||
frames = self.framing.decode(data)
|
||||
if frames is None:
|
||||
# not enough data (yet) -> return and come back with more
|
||||
return None
|
||||
self.log.debug('Dispatcher: frames=%r' % frames)
|
||||
if not frames:
|
||||
conn.queue_reply(self._format_reply(HelpReply()))
|
||||
for frame in frames:
|
||||
reply = None
|
||||
# decode frame
|
||||
msg = self.encoding.decode(frame)
|
||||
self.log.debug('Dispatcher: msg=%r' % msg)
|
||||
# act upon requestobj
|
||||
msgtype = msg.TYPE
|
||||
msgname = msg.NAME
|
||||
# generate reply (coded and framed)
|
||||
if msgtype != 'request':
|
||||
reply = ProtocolError(msg)
|
||||
else:
|
||||
self.log.debug('Looking for handle_%s' % msgname)
|
||||
handler = getattr(self, 'handle_%s' % msgname, None)
|
||||
if handler:
|
||||
reply = handler(conn, msg)
|
||||
else:
|
||||
self.log.debug('Can not handle msg %r' % msg)
|
||||
reply = self.unhandled(msgname, msg)
|
||||
if reply:
|
||||
conn.queue_reply(self._format_reply(reply))
|
||||
# queue reply via conn.queue_reply(data)
|
||||
|
||||
def _format_reply(self, reply):
|
||||
self.log.debug('formatting reply %r' % reply)
|
||||
msg = self.encoding.encode(reply)
|
||||
self.log.debug('encoded is %r' % msg)
|
||||
frame = self.framing.encode(msg)
|
||||
self.log.debug('frame is %r' % frame)
|
||||
return frame
|
||||
|
||||
def announce_update(self, devobj, pname, pobj):
|
||||
"""called by devices param setters to notify subscribers of new values
|
||||
"""
|
||||
devname = devobj.name
|
||||
eventname = '%s/%s' % (devname, pname)
|
||||
subscriber = self._dispatcher_subscriptions.get(eventname, None)
|
||||
if subscriber:
|
||||
reply = AsyncDataUnit(devname=devname,
|
||||
pname=pname,
|
||||
value=str(pobj.value),
|
||||
timestamp=pobj.timestamp,
|
||||
)
|
||||
data = self._format_reply(reply)
|
||||
for conn in subscriber:
|
||||
conn.queue_async_reply(data)
|
||||
|
||||
def subscribe(self, conn, devname, pname):
|
||||
eventname = '%s/%s' % (devname, pname)
|
||||
self._dispatcher_subscriptions.setdefault(eventname, set()).add(conn)
|
||||
|
||||
def unsubscribe(self, conn, devname, pname):
|
||||
eventname = '%s/%s' % (devname, pname)
|
||||
if eventname in self._dispatcher_subscriptions:
|
||||
self._dispatcher_subscriptions.remove(conn)
|
||||
|
||||
def add_connection(self, conn):
|
||||
"""registers new connection"""
|
||||
self._dispatcher_connections.append(conn)
|
||||
|
||||
def remove_connection(self, conn):
|
||||
"""removes now longer functional connection"""
|
||||
if conn in self._dispatcher_connections:
|
||||
self._dispatcher_connections.remove(conn)
|
||||
for _evt, conns in self._dispatcher_subscriptions.items():
|
||||
conns.discard(conn)
|
||||
|
||||
def register_device(self, devobj, devname, export=True):
|
||||
self.log.debug('registering Device %r as %s (export=%r)' %
|
||||
(devobj, devname, export))
|
||||
self._dispatcher_devices[devname] = devobj
|
||||
if export:
|
||||
self._dispatcher_export.append(devname)
|
||||
|
||||
def get_device(self, devname):
|
||||
dev = self._dispatcher_devices.get(devname, None)
|
||||
self.log.debug('get_device(%r) -> %r' % (devname, dev))
|
||||
return dev
|
||||
|
||||
def remove_device(self, devname_or_obj):
|
||||
devobj = self.get_device(devname_or_obj) or devname_or_obj
|
||||
devname = devobj.name
|
||||
if devname in self._dispatcher_export:
|
||||
self._dispatcher_export.remove(devname)
|
||||
self._dispatcher_devices.pop(devname)
|
||||
# XXX: also clean _dispatcher_subscriptions
|
||||
|
||||
def list_device_names(self):
|
||||
# return a copy of our list
|
||||
return self._dispatcher_export[:]
|
||||
|
||||
def list_devices(self):
|
||||
dn = []
|
||||
dd = {}
|
||||
for devname in self._dispatcher_export:
|
||||
dn.append(devname)
|
||||
dev = self.get_device(devname)
|
||||
descriptive_data = {
|
||||
'class': dev.__class__.__name__,
|
||||
#'bases': dev.__bases__,
|
||||
'parameters': dev.PARAMS.keys(),
|
||||
'commands': dev.CMDS.keys(),
|
||||
# XXX: what else?
|
||||
}
|
||||
dd[devname] = descriptive_data
|
||||
return dn, dd
|
||||
|
||||
def list_device_params(self, devname):
|
||||
self.log.debug('list_device_params(%r)' % devname)
|
||||
if devname in self._dispatcher_export:
|
||||
# XXX: omit export=False params!
|
||||
res = {}
|
||||
for paramname, param in self.get_device(devname).PARAMS.items():
|
||||
if param.export == True:
|
||||
res[paramname] = param
|
||||
self.log.debug('list params for device %s -> %r' %
|
||||
(devname, res))
|
||||
return res
|
||||
self.log.debug('-> device is not to be exported!')
|
||||
return {}
|
||||
|
||||
def _setParamValue(self, devname, pname, value):
|
||||
devobj = self.get_device(devname)
|
||||
if devobj is None:
|
||||
return NoSuchDeviceError(devname)
|
||||
|
||||
pobj = devobj.PARAMS.get(pname, None)
|
||||
if pobj is None:
|
||||
return NoSuchParamError(devname, pname)
|
||||
if pobj.readonly:
|
||||
return ParamReadonlyError(devname, pname)
|
||||
|
||||
writefunc = getattr(devobj, 'write_%s' % pname, None)
|
||||
try:
|
||||
if writefunc:
|
||||
value = writefunc(value)
|
||||
else:
|
||||
setattr(devobj, pname, value)
|
||||
except ValueError:
|
||||
return InvalidParamValueError(devname, pname, value, e)
|
||||
except Exception as e:
|
||||
return InternalError(e)
|
||||
|
||||
return WriteParamReply(devname, pname, pobj.value, timestamp=pobj.timestamp)
|
||||
|
||||
def _getParamValue(self, devname, pname):
|
||||
devobj = self.get_device(devname)
|
||||
if devobj is None:
|
||||
return NoSuchDeviceError(devname)
|
||||
|
||||
pobj = devobj.PARAMS.get(pname, None)
|
||||
if pobj is None:
|
||||
return NoSuchParamError(devname, pname)
|
||||
|
||||
readfunc = getattr(devobj, 'read_%s' % pname, None)
|
||||
if readfunc:
|
||||
# should also update the pobj (via the setter from the metaclass)
|
||||
try:
|
||||
readfunc()
|
||||
except Exception as e:
|
||||
return InternalError(e)
|
||||
return ReadParamReply(devname, pname, pobj.value, timestamp=pobj.timestamp)
|
||||
|
||||
# demo stuff
|
||||
def handle_Demo(self, conn, msg):
|
||||
novalue = msg.novalue
|
||||
devname = msg.devname
|
||||
paramname = msg.paramname
|
||||
propname = msg.propname
|
||||
assign = msg.assign
|
||||
|
||||
res = []
|
||||
if novalue in ('+', '-'):
|
||||
# XXX: handling of subscriptions: propname is ignored
|
||||
if devname is None:
|
||||
# list all subscriptions for this connection
|
||||
for evname, conns in self._dispatcher_subscriptions.items():
|
||||
if conn in conns:
|
||||
res.append('+%s:%s' % evname.split('/'))
|
||||
devices = self._dispatcher_export if devname == '*' else [devname]
|
||||
for devname in devices:
|
||||
devobj = self.get_device(devname)
|
||||
if devname != '*' and devobj is None:
|
||||
return NoSuchDeviceError(devname)
|
||||
if paramname is None:
|
||||
pnames = ['value', 'status']
|
||||
elif paramname == '*':
|
||||
pnames = devobj.PARAMS.keys()
|
||||
else:
|
||||
pnames = [paramname]
|
||||
for pname in pnames:
|
||||
pobj = devobj.PARAMS.get(pname, None)
|
||||
if pobj and not pobj.export:
|
||||
continue
|
||||
if paramname != '*' and pobj is None:
|
||||
return NoSuchParamError(devname, paramname)
|
||||
|
||||
if novalue == '+':
|
||||
# subscribe
|
||||
self.subscribe(conn, devname, pname)
|
||||
res.append('+%s:%s' % (devname, pname))
|
||||
elif novalue == '-':
|
||||
# unsubscribe
|
||||
self.unsubscribe(conn, devname, pname)
|
||||
res.append('-%s:%s' % (devname, pname))
|
||||
return DemoReply(res)
|
||||
|
||||
if devname is None:
|
||||
return Error('no devname given!')
|
||||
devices = self._dispatcher_export if devname == '*' else [devname]
|
||||
for devname in devices:
|
||||
devobj = self.get_device(devname)
|
||||
if devname != '*' and devobj is None:
|
||||
return NoSuchDeviceError(devname)
|
||||
if paramname is None:
|
||||
# Access Devices
|
||||
val = self._setDeviceValue(
|
||||
devobj, assign) if assign else self._getDeviceValue(devobj)
|
||||
if val == Ellipsis:
|
||||
if assign:
|
||||
return ParamReadonlyError(devname, 'target')
|
||||
return NoSuchDevice(devname)
|
||||
formatfunc = lambda x: '' if novalue else ('=%r;t=%r' % x)
|
||||
res.append(devname + formatfunc(val))
|
||||
|
||||
else:
|
||||
pnames = devobj.PARAMS.keys(
|
||||
) if paramname == '*' else [paramname]
|
||||
for pname in pnames:
|
||||
pobj = devobj.PARAMS.get(pname, None)
|
||||
if pobj and not pobj.export:
|
||||
continue
|
||||
if paramname != '*' and pobj is None:
|
||||
return NoSuchParamError(devname, paramname)
|
||||
if propname is None:
|
||||
# access params
|
||||
callfunc = lambda x, y: self._setDeviceParam(x, y, assign) \
|
||||
if assign else self._getDeviceParam(x, y)
|
||||
formatfunc = lambda x: '' if novalue else (
|
||||
'=%r;t=%r' % x)
|
||||
try:
|
||||
res.append(('%s:%s' % (devname, pname)) +
|
||||
formatfunc(callfunc(devobj, pname)))
|
||||
except TypeError as e:
|
||||
return InternalError(e)
|
||||
else:
|
||||
props = pobj.__dict__.keys(
|
||||
) if propname == '*' else [propname]
|
||||
for prop in props:
|
||||
# read props
|
||||
try:
|
||||
if novalue:
|
||||
res.append(
|
||||
'%s:%s:%s' %
|
||||
(devname, pname, prop))
|
||||
else:
|
||||
res.append(
|
||||
'%s:%s:%s=%r' %
|
||||
(devname, pname, prop, getattr(
|
||||
pobj, prop)))
|
||||
except TypeError as e:
|
||||
return InternalError(e)
|
||||
|
||||
# now clean responce a little and sort value to top....
|
||||
res = [
|
||||
e.replace(
|
||||
'/v=',
|
||||
'=') for e in sorted(
|
||||
(e.replace(
|
||||
':value=',
|
||||
'/v=') for e in res))]
|
||||
return DemoReply(res)
|
||||
|
||||
# now the (defined) handlers for the different requests
|
||||
def handle_Help(self, conn, msg):
|
||||
return HelpReply()
|
||||
|
||||
def handle_ListDevices(self, conn, msg):
|
||||
# XXX: What about the descriptive data????
|
||||
# XXX: choose!
|
||||
# return ListDevicesReply(self.list_device_names())
|
||||
return ListDevicesReply(*self.list_devices())
|
||||
|
||||
def handle_ListDeviceParams(self, conn, msg):
|
||||
# reply with a list of the parameter names for a given device
|
||||
devname = msg.device
|
||||
if devname in self._dispatcher_export:
|
||||
params = self.list_device_params(devname).keys()
|
||||
return ListDeviceParamsReply(devname, params)
|
||||
else:
|
||||
return NoSuchDeviceError(device)
|
||||
|
||||
def handle_ReadAllDevices(self, conn, msg):
|
||||
# reply with a bunch of ReadValueReplies, reading ALL devices
|
||||
result = []
|
||||
for devname in sorted(self.list_device_names()):
|
||||
value = self._getParamValue(devname, 'value')
|
||||
result.append(value)
|
||||
return ReadAllDevicesReply(readValueReplies=result)
|
||||
|
||||
def handle_ReadValue(self, conn, msg):
|
||||
devname = msg.device
|
||||
devobj = self.get_device(devname)
|
||||
res = self._getParamValue(devname, 'value')
|
||||
if not isinstance(res, ReadParamReply):
|
||||
return res
|
||||
return WriteValueReply(devname, res.value, res.timestamp)
|
||||
|
||||
def handle_WriteValue(self, conn, msg):
|
||||
value = msg.value
|
||||
devname = msg.device
|
||||
res = self._setParamValue(devname, 'target', value)
|
||||
if not isinstance(res, WriteParamReply):
|
||||
return res
|
||||
return WriteValueReply(devname, res.value, res.timestamp)
|
||||
|
||||
def handle_ReadParam(self, conn, msg):
|
||||
devname = msg.device
|
||||
pname = msg.param
|
||||
return self._getParamValue(devname, pname)
|
||||
|
||||
def handle_WriteParam(self, conn, msg):
|
||||
value = msg.value
|
||||
pname = msg.param
|
||||
devname = msg.device
|
||||
return self._setParamValue(devname, pname, value)
|
||||
|
||||
# XXX: !!!
|
||||
def handle_RequestAsyncData(self, conn, msg):
|
||||
return Error('AsyncData is not (yet) supported')
|
||||
|
||||
def unhandled(self, msgname, conn, msg):
|
||||
"""handler for unhandled Messages
|
||||
|
||||
(no handle_<messagename> method was defined)
|
||||
"""
|
||||
self.log.error('IGN: got unhandled request %s' % msgname)
|
||||
return Error('Got Unhandled Request')
|
@ -1,214 +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 SECoP Messages"""
|
||||
|
||||
REQUEST = 'request'
|
||||
REPLY = 'reply'
|
||||
ERROR = 'error'
|
||||
|
||||
|
||||
# base classes
|
||||
class Message(object):
|
||||
ARGS = []
|
||||
|
||||
def __init__(self, *args, **kwds):
|
||||
names = self.ARGS[:]
|
||||
if len(args) > len(names):
|
||||
raise TypeError('%s.__init__() takes only %d argument(s) (%d given)' %
|
||||
(self.__class__, len(names), len(args)))
|
||||
for arg in args:
|
||||
self.__dict__[names.pop(0)] = arg
|
||||
# now check keyworded args if any
|
||||
for k, v in kwds.items():
|
||||
if k not in names:
|
||||
if k in self.ARGS:
|
||||
raise TypeError('__init__() got multiple values for '
|
||||
'keyword argument %r' % k)
|
||||
raise TypeError('__init__() got an unexpected keyword '
|
||||
'argument %r' % k)
|
||||
names.remove(k)
|
||||
self.__dict__[k] = v
|
||||
for name in names:
|
||||
self.__dict__[name] = None
|
||||
# if names:
|
||||
# raise TypeError('__init__() takes at least %d arguments (%d given)'
|
||||
# % (len(self.ARGS), len(args)+len(kwds)))
|
||||
self.NAME = (self.__class__.__name__[:-len(self.TYPE)] or
|
||||
self.__class__.__name__)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__class__.__name__ + '(' + \
|
||||
', '.join('%s=%r' % (k, getattr(self, k))
|
||||
for k in self.ARGS if getattr(self, k) is not None) + ')'
|
||||
|
||||
|
||||
class Request(Message):
|
||||
TYPE = REQUEST
|
||||
|
||||
|
||||
class Reply(Message):
|
||||
TYPE = REPLY
|
||||
|
||||
|
||||
class ErrorReply(Message):
|
||||
TYPE = ERROR
|
||||
|
||||
# for DEMO
|
||||
|
||||
|
||||
class DemoRequest(Request):
|
||||
ARGS = ['novalue', 'devname', 'paramname', 'propname', 'assign']
|
||||
|
||||
|
||||
class DemoReply(Reply):
|
||||
ARGS = ['lines']
|
||||
|
||||
|
||||
# actuall message objects
|
||||
class ListDevicesRequest(Request):
|
||||
pass
|
||||
|
||||
|
||||
class ListDevicesReply(Reply):
|
||||
ARGS = ['list_of_devices', 'descriptive_data']
|
||||
|
||||
|
||||
class ListDeviceParamsRequest(Request):
|
||||
ARGS = ['device']
|
||||
|
||||
|
||||
class ListDeviceParamsReply(Reply):
|
||||
ARGS = ['device', 'params']
|
||||
|
||||
|
||||
class ReadValueRequest(Request):
|
||||
ARGS = ['device', 'maxage']
|
||||
|
||||
|
||||
class ReadValueReply(Reply):
|
||||
ARGS = ['device', 'value', 'timestamp', 'error', 'unit']
|
||||
|
||||
|
||||
class WriteValueRequest(Request):
|
||||
ARGS = ['device', 'value', 'unit'] # unit???
|
||||
|
||||
|
||||
class WriteValueReply(Reply):
|
||||
ARGS = ['device', 'value', 'timestamp', 'error', 'unit']
|
||||
|
||||
|
||||
class ReadAllDevicesRequest(Request):
|
||||
ARGS = ['maxage']
|
||||
|
||||
|
||||
class ReadAllDevicesReply(Reply):
|
||||
ARGS = ['readValueReplies']
|
||||
|
||||
|
||||
class ListParamPropsRequest(Request):
|
||||
ARGS = ['device', 'param']
|
||||
|
||||
|
||||
class ListParamPropsReply(Request):
|
||||
ARGS = ['device', 'param', 'props']
|
||||
|
||||
|
||||
class ReadParamRequest(Request):
|
||||
ARGS = ['device', 'param', 'maxage']
|
||||
|
||||
|
||||
class ReadParamReply(Reply):
|
||||
ARGS = ['device', 'param', 'value', 'timestamp', 'error', 'unit']
|
||||
|
||||
|
||||
class WriteParamRequest(Request):
|
||||
ARGS = ['device', 'param', 'value']
|
||||
|
||||
|
||||
class WriteParamReply(Reply):
|
||||
ARGS = ['device', 'param', 'readback_value', 'timestamp', 'error', 'unit']
|
||||
|
||||
|
||||
class RequestAsyncDataRequest(Request):
|
||||
ARGS = ['device', 'params']
|
||||
|
||||
|
||||
class RequestAsyncDataReply(Reply):
|
||||
ARGS = ['device', 'paramvalue_list']
|
||||
|
||||
|
||||
class AsyncDataUnit(ReadParamReply):
|
||||
ARGS = ['devname', 'pname', 'value', 'timestamp', 'error', 'unit']
|
||||
|
||||
|
||||
# ERRORS
|
||||
########
|
||||
|
||||
class ErrorReply(Reply):
|
||||
ARGS = ['error']
|
||||
|
||||
|
||||
class InternalError(ErrorReply):
|
||||
ARGS = ['error']
|
||||
|
||||
|
||||
class ProtocollError(ErrorReply):
|
||||
ARGS = ['error']
|
||||
|
||||
|
||||
class NoSuchDeviceError(ErrorReply):
|
||||
ARGS = ['device']
|
||||
|
||||
|
||||
class NoSuchParamError(ErrorReply):
|
||||
ARGS = ['device', 'param']
|
||||
|
||||
|
||||
class ParamReadonlyError(ErrorReply):
|
||||
ARGS = ['device', 'param']
|
||||
|
||||
|
||||
class UnsupportedFeatureError(ErrorReply):
|
||||
ARGS = ['feature']
|
||||
|
||||
|
||||
class NoSuchCommandError(ErrorReply):
|
||||
ARGS = ['device', 'command']
|
||||
|
||||
|
||||
class CommandFailedError(ErrorReply):
|
||||
ARGS = ['device', 'command']
|
||||
|
||||
|
||||
class InvalidParamValueError(ErrorReply):
|
||||
ARGS = ['device', 'param', 'value', 'error']
|
||||
|
||||
# Fun!
|
||||
|
||||
|
||||
class HelpRequest(Request):
|
||||
pass
|
||||
|
||||
|
||||
class HelpReply(Reply):
|
||||
pass
|
@ -1,193 +0,0 @@
|
||||
#!/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 Frames"""
|
||||
|
||||
|
||||
# Base class
|
||||
class Framer(object):
|
||||
"""Frames and unframes an encoded message
|
||||
|
||||
also transforms the encoded message to the 'wire-format' (and vise-versa)
|
||||
|
||||
note: not all MessageEncoders can use all Framers,
|
||||
but the intention is to have this for as many as possible.
|
||||
"""
|
||||
|
||||
def encode(self, *frames):
|
||||
"""return the wire-data for the given messageframes"""
|
||||
raise NotImplemented
|
||||
|
||||
def decode(self, data):
|
||||
"""return a list of messageframes found in data"""
|
||||
raise NotImplemented
|
||||
|
||||
def reset(self):
|
||||
"""resets the de/encoding stage (clears internal information)"""
|
||||
raise NotImplemented
|
||||
|
||||
|
||||
# now some Implementations
|
||||
|
||||
class EOLFramer(Framer):
|
||||
"""Text based message framer
|
||||
|
||||
messages are delimited by '\r\n'
|
||||
upon reception the end of a message is detected by '\r\n','\n' or '\n\r'
|
||||
"""
|
||||
data = b''
|
||||
|
||||
def encode(self, *frames):
|
||||
"""add transport layer encapsulation/framing of messages"""
|
||||
return b'%s\r\n' % b'\r\n'.join(frames)
|
||||
|
||||
def decode(self, data):
|
||||
"""remove transport layer encapsulation/framing of messages
|
||||
|
||||
returns a list of messageframes which got decoded from data!
|
||||
"""
|
||||
self.data += data
|
||||
res = []
|
||||
while b'\n' in self.data:
|
||||
frame, self.data = self.data.split(b'\n', 1)
|
||||
if frame.endswith('\r'):
|
||||
frame = frame[:-1]
|
||||
if self.data.startswith('\r'):
|
||||
self.data = self.data[1:]
|
||||
res.append(frame)
|
||||
return res
|
||||
|
||||
def reset(self):
|
||||
self.data = b''
|
||||
|
||||
|
||||
class RLEFramer(Framer):
|
||||
data = b''
|
||||
frames_to_go = 0
|
||||
|
||||
def encode(self, *frames):
|
||||
"""add transport layer encapsulation/framing of messages"""
|
||||
# format is 'number of frames:[framelengt:frme]*N'
|
||||
frdata = ['%d:%s' % (len(frame), frame) for frame in frames]
|
||||
return b'%d:' + b''.join(frdata)
|
||||
|
||||
def decode(self, data):
|
||||
"""remove transport layer encapsulation/framing of messages
|
||||
|
||||
returns a list of messageframes which got decoded from data!
|
||||
"""
|
||||
self.data += data
|
||||
res = []
|
||||
while self.data:
|
||||
if frames_to_go == 0:
|
||||
if ':' in self.data:
|
||||
# scan for and decode 'number of frames'
|
||||
frnum, self.data = self.data.split(':', 1)
|
||||
try:
|
||||
self.frames_to_go = int(frnum)
|
||||
except ValueError:
|
||||
# can not recover, complain!
|
||||
raise FramingError('invalid start of message found!')
|
||||
else:
|
||||
# not enough data to decode number of frames,
|
||||
# return what we have
|
||||
return res
|
||||
while self.frames_to_go:
|
||||
# there are still some (partial) frames stuck inside self.data
|
||||
frlen, self.data = self.data.split(':', 1)
|
||||
if len(self.data) >= frlen:
|
||||
res.append(self.data[:frlen])
|
||||
self.data = self.data[frlen:]
|
||||
self.frames_to_go -= 1
|
||||
else:
|
||||
# not enough data for this frame, return what we have
|
||||
return res
|
||||
|
||||
def reset(self):
|
||||
self.data = b''
|
||||
self.frames_to_go = 0
|
||||
|
||||
|
||||
class DemoFramer(Framer):
|
||||
"""Text based message framer
|
||||
|
||||
frmes are delimited by '\n'
|
||||
messages are delimited by '\n\n'
|
||||
'\r' is ignored
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.data = b''
|
||||
self.decoded = []
|
||||
|
||||
def encode(self, frames):
|
||||
"""add transport layer encapsulation/framing of messages"""
|
||||
if isinstance(frames, (tuple, list)):
|
||||
return b'\n'.join(frames) + b'\n\n'
|
||||
return b'%s\n\n' % frames
|
||||
|
||||
def decode(self, data):
|
||||
"""remove transport layer encapsulation/framing of messages
|
||||
|
||||
returns a list of messageframes which got decoded from data!
|
||||
"""
|
||||
self.data += data
|
||||
res = []
|
||||
while b'\n' in self.data:
|
||||
frame, self.data = self.data.split(b'\n', 1)
|
||||
if frame.endswith('\r'):
|
||||
frame = frame[:-1]
|
||||
if self.data.startswith('\r'):
|
||||
self.data = self.data[1:]
|
||||
res.append(frame)
|
||||
return res
|
||||
|
||||
def decode2(self, data):
|
||||
"""remove transport layer encapsulation/framing of messages
|
||||
|
||||
returns a _list_ of messageframes which got decoded from data!
|
||||
"""
|
||||
self.data += data.replace(b'\r', '')
|
||||
while b'\n' in self.data:
|
||||
frame, self.data = self.data.split(b'\n', 1)
|
||||
if frame:
|
||||
# not an empty line -> belongs to this set of messages
|
||||
self.decoded.append(frame)
|
||||
else:
|
||||
# empty line -> our set of messages is finished decoding
|
||||
res = self.decoded
|
||||
self.decoded = []
|
||||
return res
|
||||
return None
|
||||
|
||||
def reset(self):
|
||||
self.data = b''
|
||||
self.decoded = []
|
||||
|
||||
|
||||
FRAMERS = {
|
||||
'eol': EOLFramer,
|
||||
'rle': RLEFramer,
|
||||
'demo': DemoFramer,
|
||||
}
|
||||
|
||||
__ALL__ = ['FRAMERS']
|
Loading…
x
Reference in New Issue
Block a user