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:
Enrico Faulhaber 2016-09-29 18:27:33 +02:00
parent dc2d0a10aa
commit b6af55c358
49 changed files with 3682 additions and 1034 deletions

77
bin/secop-console Executable file
View 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))

View File

@ -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
View File

@ -0,0 +1,215 @@
SECoP Messages
==============
All Messages are formatted in the same way:
&lt;keyword&gt;[&lt;space&gt;&lt;specifier&gt;[&lt;space&gt;&lt;JSON_formatted_data&gt;]]&lt;linefeed&gt;
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"
&lt;keyword&gt; is one of a fixed list of defined keywords, &lt;specifier&gt; 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 -&gt; Ident
* Describe -&gt; Description
* Activate Events -&gt; initial data transfer -&gt; end-of-transfer-marker
* Deactivate Async Events -&gt; confirmation
* Command &lt;module&gt;:&lt;command&gt; -&gt; confirmation -&gt; result_event
* Heartbeat &lt;nonce&gt; -&gt; Heartbeat_reply
* Change &lt;module&gt;[:&lt;param&gt;] JSON_VALUE -&gt; confirmation -&gt; readback_event
* TRIGGER &lt;module&gt;[:&lt;param&gt;] -&gt; 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 &lt;..&gt; 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 &lt;ID&gt; {"modules":{"T1":{"baseclass":"Readable", ....'
* request the 'descriptive data'. The format needs to be better defined and
may possibly just follow the reference implementation.
&lt;ID&gt; identifies the equipment. It should be unique. Our suggestion is to use something along &lt;facility&gt;_&lt;id&gt;, 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 &lt;module&gt;:&lt;command&gt;' for commands without arguments
* Request: type C: 'do &lt;module&gt;:&lt;command&gt; JSON_argument' for commands with arguments
* Reply: type B: 'doing &lt;module&gt;:&lt;command&gt;' for commands without arguments
* Reply: type C: 'doing &lt;module&gt;:&lt;command&gt; JSON_argument' for commands with arguments
* start executing a command. When it is finished, an event is send.
Write
-----
* Request: type C: 'change &lt;module&gt;[:&lt;param&gt;] JSON_value'
* Reply: type C: 'changing &lt;module&gt;[:&lt;param&gt;] 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 &lt;module&gt;[:&lt;param&gt;]'
* 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 &lt;nonce&gt;'
* Reply: type A: 'pong'
* Reply: type B: 'pong &lt;nonce&gt;'
* Replies the given argument to check the round-trip-time or to confirm that the connection is still working.
&lt;nonce&gt; may not contain &lt;space&gt;. 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 &lt;nonce&gt; 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 &lt;module&gt;[:&lt;param&gt;] JSON_VALUE' # follows a TRIGGER
* Reply: type C: 'changed &lt;module&gt;[:&lt;param&gt;] JSON_VALUE' # follows a CHANGE
* Reply: type B: 'done &lt;module&gt;:&lt;command&gt;' # follows a COMMAND without return value
* Reply: type C: 'done &lt;module&gt;:&lt;command&gt; 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 &lt;errorclass&gt; JSON_additional_stuff'
* Following &lt;errorclass&gt; 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
View 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&uuml;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&uuml;r das Parsing wird eine Regexp empfohlen, da die syntax nicht sonderlich parsingfreundlich ist.
Um auszuw&auml;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&uuml;r Multi-Antworten endet die Antwort in `#<number of replyitems>\n` statt in `\n`.
Hier ist kein '=' zus&auml;tzlich anzugeben. Nach dieser 'Er&ouml;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&ouml;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&uuml;hrt command mit den gegebenen Arguments aus.
result=(ein) R&uuml;ckgabewert, kann auch "OK" sein, falls kein R&uuml;ckgabewert definiert wurde.
* MANDATORY
commands sind parameter deren name auf '()' endet.
(oder die argumenttypen in () enth&auml;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 &uuml;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 &uuml;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&ouml;glich Erweiterung: f&uuml;r device/param/property kann statt eines einzelnamens auch eine ',' separierte Liste verwendet werden.
Außerdem k&ouml;nnte auch ein '*' f&uuml;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
>

View 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.

View File

@ -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

View File

@ -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
View 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)

View File

@ -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 !

View File

@ -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",

View File

@ -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),

View File

@ -23,7 +23,7 @@
import random
from devices.core import Readable, Driveable, PARAM
from secop.devices.core import Readable, Driveable, PARAM
try:

View File

@ -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):

View File

@ -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
View 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"

View File

@ -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....

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

View 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']

View File

@ -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']

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

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

View File

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

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

View 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
View 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

View 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']

View 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 = []

View 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''

View File

@ -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]

View 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

View File

@ -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
View 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

View 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

View File

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

View File

@ -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 = {}

View File

@ -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')

View File

@ -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

View File

@ -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']