Devices infrastructure and minimal server
starts and creates (server-side) devices no daemonizing and servicing yet hint: try starting: $ bin/server.py -v start Change-Id: I6ac7a78dfff309a459cc0338a8d0d319ee72ada5
This commit is contained in:
parent
8a9d2da503
commit
d3c430e1b9
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,5 @@
|
|||||||
|
log/*
|
||||||
html/*
|
html/*
|
||||||
*.pyc
|
*.pyc
|
||||||
|
pid/*
|
||||||
|
|
||||||
|
283
.pylintrc
Normal file
283
.pylintrc
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
[MASTER]
|
||||||
|
|
||||||
|
# Specify a configuration file.
|
||||||
|
#rcfile=
|
||||||
|
|
||||||
|
# Python code to execute, usually for sys.path manipulation such as
|
||||||
|
# pygtk.require().
|
||||||
|
#init-hook=
|
||||||
|
|
||||||
|
# Profiled execution.
|
||||||
|
profile=no
|
||||||
|
|
||||||
|
# Add files or directories to the blacklist. They should be base names, not
|
||||||
|
# paths.
|
||||||
|
ignore=CVS,doc,html,pid,log,etc
|
||||||
|
|
||||||
|
# Pickle collected data for later comparisons.
|
||||||
|
persistent=yes
|
||||||
|
|
||||||
|
# List of plugins (as comma separated values of python modules names) to load,
|
||||||
|
# usually to register additional checkers.
|
||||||
|
load-plugins=
|
||||||
|
|
||||||
|
|
||||||
|
[MESSAGES CONTROL]
|
||||||
|
|
||||||
|
# Enable the message, report, category or checker with the given id(s). You can
|
||||||
|
# either give multiple identifier separated by comma (,) or put this option
|
||||||
|
# multiple time. See also the "--disable" option for examples.
|
||||||
|
#enable=
|
||||||
|
|
||||||
|
# Disable the message, report, category or checker with the given id(s). You
|
||||||
|
# can either give multiple identifiers separated by comma (,) or put this
|
||||||
|
# option multiple times (only on the command line, not in the configuration
|
||||||
|
# file where it should appear only once).You can also use "--disable=all" to
|
||||||
|
# disable everything first and then reenable specific checks. For example, if
|
||||||
|
# you want to run only the similarities checker, you can use "--disable=all
|
||||||
|
# --enable=similarities". If you want to run only the classes checker, but have
|
||||||
|
# no Warning level messages displayed, use"--disable=all --enable=classes
|
||||||
|
# --disable=W"
|
||||||
|
disable=C0301,C0103,W0614,W0403,W0142,R0903,W0212,W0401,R0904,R0913,E1103
|
||||||
|
|
||||||
|
|
||||||
|
[REPORTS]
|
||||||
|
|
||||||
|
# Set the output format. Available formats are text, parseable, colorized, msvs
|
||||||
|
# (visual studio) and html. You can also give a reporter class, eg
|
||||||
|
# mypackage.mymodule.MyReporterClass.
|
||||||
|
output-format=colorized
|
||||||
|
|
||||||
|
# Include message's id in output
|
||||||
|
include-ids=yes
|
||||||
|
|
||||||
|
# Put messages in a separate file for each module / package specified on the
|
||||||
|
# command line instead of printing them on stdout. Reports (if any) will be
|
||||||
|
# written in a file name "pylint_global.[txt|html]".
|
||||||
|
files-output=no
|
||||||
|
|
||||||
|
# Tells whether to display a full report or only the messages
|
||||||
|
reports=no
|
||||||
|
|
||||||
|
# Python expression which should return a note less than 10 (10 is the highest
|
||||||
|
# note). You have access to the variables errors warning, statement which
|
||||||
|
# respectively contain the number of errors / warnings messages and the total
|
||||||
|
# number of statements analyzed. This is used by the global evaluation report
|
||||||
|
# (RP0004).
|
||||||
|
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
|
||||||
|
|
||||||
|
# Add a comment according to your evaluation note. This is used by the global
|
||||||
|
# evaluation report (RP0004).
|
||||||
|
comment=no
|
||||||
|
|
||||||
|
# Template used to display messages. This is a python new-style format string
|
||||||
|
# used to format the message information. See doc for all details
|
||||||
|
#msg-template=
|
||||||
|
|
||||||
|
|
||||||
|
[TYPECHECK]
|
||||||
|
|
||||||
|
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||||
|
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
||||||
|
ignore-mixin-members=yes
|
||||||
|
|
||||||
|
# List of classes names for which member attributes should not be checked
|
||||||
|
# (useful for classes with attributes dynamically set).
|
||||||
|
ignored-classes=SQLObject
|
||||||
|
|
||||||
|
# When zope mode is activated, add a predefined set of Zope acquired attributes
|
||||||
|
# to generated-members.
|
||||||
|
zope=no
|
||||||
|
|
||||||
|
# List of members which are set dynamically and missed by pylint inference
|
||||||
|
# system, and so shouldn't trigger E0201 when accessed. Python regular
|
||||||
|
# expressions are accepted.
|
||||||
|
generated-members=REQUEST,acl_users,aq_parent
|
||||||
|
|
||||||
|
|
||||||
|
[BASIC]
|
||||||
|
|
||||||
|
# Required attributes for module, separated by a comma
|
||||||
|
required-attributes=
|
||||||
|
|
||||||
|
# List of builtins function names that should not be used, separated by a comma
|
||||||
|
bad-functions=map,filter,apply,input
|
||||||
|
|
||||||
|
# Regular expression which should only match correct module names
|
||||||
|
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
|
||||||
|
|
||||||
|
# Regular expression which should only match correct module level names
|
||||||
|
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
|
||||||
|
|
||||||
|
# Regular expression which should only match correct class names
|
||||||
|
class-rgx=[A-Z_][a-zA-Z0-9]+$
|
||||||
|
|
||||||
|
# Regular expression which should only match correct function names
|
||||||
|
function-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||||
|
|
||||||
|
# Regular expression which should only match correct method names
|
||||||
|
method-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||||
|
|
||||||
|
# Regular expression which should only match correct instance attribute names
|
||||||
|
attr-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||||
|
|
||||||
|
# Regular expression which should only match correct argument names
|
||||||
|
argument-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||||
|
|
||||||
|
# Regular expression which should only match correct variable names
|
||||||
|
variable-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||||
|
|
||||||
|
# Regular expression which should only match correct attribute names in class
|
||||||
|
# bodies
|
||||||
|
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
|
||||||
|
|
||||||
|
# Regular expression which should only match correct list comprehension /
|
||||||
|
# generator expression variable names
|
||||||
|
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
|
||||||
|
|
||||||
|
# Good variable names which should always be accepted, separated by a comma
|
||||||
|
good-names=i,j,k,ex,Run,_
|
||||||
|
|
||||||
|
# Bad variable names which should always be refused, separated by a comma
|
||||||
|
bad-names=foo,bar,baz,toto,tutu,tata
|
||||||
|
|
||||||
|
# Regular expression which should only match function or class names that do
|
||||||
|
# not require a docstring.
|
||||||
|
no-docstring-rgx=__.*__
|
||||||
|
|
||||||
|
# Minimum line length for functions/classes that require docstrings, shorter
|
||||||
|
# ones are exempt.
|
||||||
|
docstring-min-length=3
|
||||||
|
|
||||||
|
|
||||||
|
[MISCELLANEOUS]
|
||||||
|
|
||||||
|
# List of note tags to take in consideration, separated by a comma.
|
||||||
|
notes=FIXME,XXX,TODO,HACK
|
||||||
|
|
||||||
|
|
||||||
|
[FORMAT]
|
||||||
|
|
||||||
|
# Maximum number of characters on a single line.
|
||||||
|
max-line-length=80
|
||||||
|
|
||||||
|
# Regexp for a line that is allowed to be longer than the limit.
|
||||||
|
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||||
|
|
||||||
|
# Allow the body of an if to be on the same line as the test if there is no
|
||||||
|
# else.
|
||||||
|
single-line-if-stmt=no
|
||||||
|
|
||||||
|
# List of optional constructs for which whitespace checking is disabled
|
||||||
|
no-space-check=trailing-comma,dict-separator
|
||||||
|
|
||||||
|
# Maximum number of lines in a module
|
||||||
|
max-module-lines=1000
|
||||||
|
|
||||||
|
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||||
|
# tab).
|
||||||
|
indent-string=' '
|
||||||
|
|
||||||
|
|
||||||
|
[SIMILARITIES]
|
||||||
|
|
||||||
|
# Minimum lines number of a similarity.
|
||||||
|
min-similarity-lines=4
|
||||||
|
|
||||||
|
# Ignore comments when computing similarities.
|
||||||
|
ignore-comments=yes
|
||||||
|
|
||||||
|
# Ignore docstrings when computing similarities.
|
||||||
|
ignore-docstrings=yes
|
||||||
|
|
||||||
|
# Ignore imports when computing similarities.
|
||||||
|
ignore-imports=no
|
||||||
|
|
||||||
|
|
||||||
|
[VARIABLES]
|
||||||
|
|
||||||
|
# Tells whether we should check for unused import in __init__ files.
|
||||||
|
init-import=no
|
||||||
|
|
||||||
|
# A regular expression matching the beginning of the name of dummy variables
|
||||||
|
# (i.e. not used).
|
||||||
|
dummy-variables-rgx=_.*|dummy
|
||||||
|
|
||||||
|
# List of additional names supposed to be defined in builtins. Remember that
|
||||||
|
# you should avoid to define new builtins when possible.
|
||||||
|
additional-builtins=
|
||||||
|
|
||||||
|
|
||||||
|
[DESIGN]
|
||||||
|
|
||||||
|
# Maximum number of arguments for function / method
|
||||||
|
max-args=5
|
||||||
|
|
||||||
|
# Argument names that match this expression will be ignored. Default to name
|
||||||
|
# with leading underscore
|
||||||
|
ignored-argument-names=_.*
|
||||||
|
|
||||||
|
# Maximum number of locals for function / method body
|
||||||
|
max-locals=15
|
||||||
|
|
||||||
|
# Maximum number of return / yield for function / method body
|
||||||
|
max-returns=6
|
||||||
|
|
||||||
|
# Maximum number of branch for function / method body
|
||||||
|
max-branches=12
|
||||||
|
|
||||||
|
# Maximum number of statements in function / method body
|
||||||
|
max-statements=50
|
||||||
|
|
||||||
|
# Maximum number of parents for a class (see R0901).
|
||||||
|
max-parents=7
|
||||||
|
|
||||||
|
# Maximum number of attributes for a class (see R0902).
|
||||||
|
max-attributes=7
|
||||||
|
|
||||||
|
# Minimum number of public methods for a class (see R0903).
|
||||||
|
min-public-methods=2
|
||||||
|
|
||||||
|
# Maximum number of public methods for a class (see R0904).
|
||||||
|
max-public-methods=20
|
||||||
|
|
||||||
|
|
||||||
|
[IMPORTS]
|
||||||
|
|
||||||
|
# Deprecated modules which should not be used, separated by a comma
|
||||||
|
deprecated-modules=regsub,TERMIOS,Bastion,rexec
|
||||||
|
|
||||||
|
# Create a graph of every (i.e. internal and external) dependencies in the
|
||||||
|
# given file (report RP0402 must not be disabled)
|
||||||
|
import-graph=
|
||||||
|
|
||||||
|
# Create a graph of external dependencies in the given file (report RP0402 must
|
||||||
|
# not be disabled)
|
||||||
|
ext-import-graph=
|
||||||
|
|
||||||
|
# Create a graph of internal dependencies in the given file (report RP0402 must
|
||||||
|
# not be disabled)
|
||||||
|
int-import-graph=
|
||||||
|
|
||||||
|
|
||||||
|
[CLASSES]
|
||||||
|
|
||||||
|
# List of interface methods to ignore, separated by a comma. This is used for
|
||||||
|
# instance to not check methods defines in Zope's Interface base class.
|
||||||
|
ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
|
||||||
|
|
||||||
|
# List of method names used to declare (i.e. assign) instance attributes.
|
||||||
|
defining-attr-methods=__init__,__new__,setUp
|
||||||
|
|
||||||
|
# List of valid names for the first argument in a class method.
|
||||||
|
valid-classmethod-first-arg=cls
|
||||||
|
|
||||||
|
# List of valid names for the first argument in a metaclass class method.
|
||||||
|
valid-metaclass-classmethod-first-arg=mcs
|
||||||
|
|
||||||
|
|
||||||
|
[EXCEPTIONS]
|
||||||
|
|
||||||
|
# Exceptions that will emit a warning when being caught. Defaults to
|
||||||
|
# "Exception"
|
||||||
|
overgeneral-exceptions=Exception
|
@ -25,8 +25,83 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from os import path
|
from os import path
|
||||||
|
|
||||||
sys.path[0]=path.abspath(path.join(sys.path[0],'../src'))
|
# Pathes magic to make python find out 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')
|
||||||
|
|
||||||
import transport
|
|
||||||
|
|
||||||
transport.startup_server()
|
import argparse
|
||||||
|
from lib import check_pidfile, start_server, kill_server
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description = "Manage 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("action", help="What to do with the server: (re)start, status or stop",
|
||||||
|
choices=['start', 'status', 'stop', 'restart'], default="status")
|
||||||
|
parser.add_argument("name", help="Name of the instance. Uses etc/name.cfg for configuration\n"
|
||||||
|
"may be omitted to mean ALL (which are configured)",
|
||||||
|
nargs='?', default='')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
import logging
|
||||||
|
loglevel = logging.DEBUG if args.verbose else (logging.ERROR if args.quiet else logging.INFO)
|
||||||
|
logging.basicConfig(level=loglevel, format='%(asctime)s %(levelname)s %(message)s')
|
||||||
|
logger = logging.getLogger('server')
|
||||||
|
logger.setLevel(loglevel)
|
||||||
|
fh = logging.FileHandler(path.join(log_path, 'server.log'), 'w')
|
||||||
|
fh.setLevel(loglevel)
|
||||||
|
logger.addHandler(fh)
|
||||||
|
|
||||||
|
logger.debug("action specified %r" % args.action)
|
||||||
|
|
||||||
|
def handle_servername(name, action):
|
||||||
|
pidfile = path.join(pid_path, name + '.pid')
|
||||||
|
cfgfile = path.join(etc_path, name + '.cfg')
|
||||||
|
if action == "restart":
|
||||||
|
handle_servername(name, 'stop')
|
||||||
|
handle_servername(name, 'start')
|
||||||
|
return
|
||||||
|
elif action == "start":
|
||||||
|
logger.info("Starting server %s" % name)
|
||||||
|
# XXX also do it !
|
||||||
|
start_server(name, basepath, loglevel)
|
||||||
|
elif action == "stop":
|
||||||
|
pid = check_pidfile(pidfile)
|
||||||
|
if pid:
|
||||||
|
logger.info("Stopping server %s" % name)
|
||||||
|
# XXX also do it!
|
||||||
|
stop_server(pidfile)
|
||||||
|
else:
|
||||||
|
logger.info("Server %s already dead" % name)
|
||||||
|
elif action == "status":
|
||||||
|
if check_pidfile(pidfile):
|
||||||
|
logger.info("Server %s is running." % name)
|
||||||
|
else:
|
||||||
|
logger.info("Server %s is DEAD!" % name)
|
||||||
|
else:
|
||||||
|
logger.error("invalid action specified: How can this ever happen???")
|
||||||
|
|
||||||
|
print "================================"
|
||||||
|
if not args.name:
|
||||||
|
logger.debug("No name given, iterating over all specified servers")
|
||||||
|
for dirpath, dirs, files in os.walk(etc_path):
|
||||||
|
for fn in files:
|
||||||
|
if fn.endswith('.cfg'):
|
||||||
|
handle_servername(fn[:-4], args.action)
|
||||||
|
else:
|
||||||
|
logger.debug('configfile with strange extension found: %r'
|
||||||
|
% path.basename(fn))
|
||||||
|
# ignore subdirs!
|
||||||
|
while(dirs):
|
||||||
|
dirs.pop()
|
||||||
|
else:
|
||||||
|
handle_servername(args.name, args.action)
|
||||||
|
print "================================"
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
Markdown docu to be generated
|
Markdown docu to be generated
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
|
[Notes](notes.html)
|
||||||
|
|
||||||
|
|
||||||
TODO's
|
TODO's
|
||||||
======
|
======
|
||||||
|
39
doc/notes.md
Normal file
39
doc/notes.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
------
|
||||||
|
No installation required or recommended.
|
||||||
|
-----
|
||||||
|
|
||||||
|
everything runs directly from the checkout.
|
||||||
|
|
||||||
|
you need:
|
||||||
|
- python2.7.*
|
||||||
|
- pip
|
||||||
|
- linux OS (Mac may work as well)
|
||||||
|
|
||||||
|
install requirements with pip:
|
||||||
|
$ sudo pip install -r requirements.txt
|
||||||
|
|
||||||
|
to execute a program, prefix its name with bin/, e.g.:
|
||||||
|
$ bin/make_doc.py
|
||||||
|
$ bin/server.py start test
|
||||||
|
|
||||||
|
a testsuite is planned but nothing is there yet.
|
||||||
|
|
||||||
|
## structure ##
|
||||||
|
|
||||||
|
* bin contains the executables (make_doc.py, server.py)
|
||||||
|
* doc is the root node of the docu (see index.md)
|
||||||
|
* etc contains the configurations for the server(s) and devices
|
||||||
|
* html contains the docu after make_doc.py was run
|
||||||
|
* log contains some (hopefully) log output from the servers
|
||||||
|
* pid contains pidfiles if a server is running
|
||||||
|
* src contains the python source
|
||||||
|
* src/client: client specific stuff (proxy)
|
||||||
|
* src/devices: devices to be used by the server (and exported via SECoP)
|
||||||
|
* src/lib: helper stuff (startup, pidfiles, etc)
|
||||||
|
* src/protocol: protocol specific stuff
|
||||||
|
* src/errors.py: internal errors
|
||||||
|
* src/server.py: device-managing part of the server (transport is in src/protocol/transport)
|
||||||
|
* src/validators.py: validators used by the devices. may be moved to src/protocol
|
||||||
|
|
||||||
|
# THERE IS STILL MUCH WORK TO DO! #
|
||||||
|
|
24
doc/todo.md
24
doc/todo.md
@ -6,6 +6,7 @@
|
|||||||
* src/server for everything server related
|
* src/server for everything server related
|
||||||
* src/client for everything client related (ProxyDevice!)
|
* src/client for everything client related (ProxyDevice!)
|
||||||
* src/protocol for protocol specific things
|
* src/protocol for protocol specific things
|
||||||
|
* need subtree for different implementations to play with
|
||||||
* src/lib for helpers and other stuff
|
* src/lib for helpers and other stuff
|
||||||
* possibly a parallel src tree for cpp version
|
* possibly a parallel src tree for cpp version
|
||||||
|
|
||||||
@ -20,16 +21,31 @@
|
|||||||
|
|
||||||
## A Server ##
|
## A Server ##
|
||||||
|
|
||||||
* evaluate config.ini
|
* get daemonizing working
|
||||||
* handle cmdline args (specify different server.ini)
|
* handle -d (nodaemon) and -D (default, daemonize) cmd line args
|
||||||
* support Async data units
|
* support Async data units
|
||||||
* support feature publishing and selection
|
* support feature publishing and selection
|
||||||
* rewrite MessageHadler to be agnostic of server
|
* rewrite MessageHandler to be agnostic of server
|
||||||
|
|
||||||
|
|
||||||
|
## Device framework ##
|
||||||
|
|
||||||
|
* unify PARAMS and CONFIG (if no default value is given,
|
||||||
|
it needs to be specified in cfgfile, otherwise its optional)
|
||||||
|
* supply properties for PARAMS to auto-generate async data units
|
||||||
|
|
||||||
|
|
||||||
## Testsuite ##
|
## Testsuite ##
|
||||||
|
|
||||||
* embedded tests inside the actual files grow difficult to maintain
|
* embedded tests inside the actual files grow difficult to maintain
|
||||||
* needed ?
|
=> need a testsuite (nose+pylint?)
|
||||||
|
|
||||||
|
|
||||||
|
## docu ##
|
||||||
|
|
||||||
|
* mabe use sphinx to generate docu: a pdf can then be auto-generated....
|
||||||
|
* transfer build docu into wiki via automated jobfile
|
||||||
|
Problem: wiki does not understand .md or .html
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -3,14 +3,14 @@ bindto=localhost
|
|||||||
bindport=10767
|
bindport=10767
|
||||||
protocol=pickle
|
protocol=pickle
|
||||||
|
|
||||||
[device "LN2"]
|
[device LN2]
|
||||||
class=devices.test.LN2
|
class=devices.test.LN2
|
||||||
|
|
||||||
[device "heater"]
|
[device heater]
|
||||||
class=devices.test.Heater
|
class=devices.test.Heater
|
||||||
maxheaterpower=10
|
maxheaterpower=10
|
||||||
|
|
||||||
[device "T1"]
|
[device T1]
|
||||||
class=devices.test.temp
|
class=devices.test.Temp
|
||||||
sensor="X34598T7"
|
sensor="X34598T7"
|
||||||
|
|
@ -1,5 +1,9 @@
|
|||||||
# for generating docu
|
# for generating docu
|
||||||
markdown>=2.6
|
markdown>=2.6
|
||||||
|
# general stuff
|
||||||
|
psutil
|
||||||
|
# daemonizing not yet functional (logging problems)
|
||||||
|
#daemonize
|
||||||
# for zmq
|
# for zmq
|
||||||
#pyzmq>=13.1.0
|
#pyzmq>=13.1.0
|
||||||
|
|
||||||
|
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
0
src/client/__init__.py
Normal file
0
src/client/__init__.py
Normal file
0
src/devices/__init__.py
Normal file
0
src/devices/__init__.py
Normal file
170
src/devices/core.py
Normal file
170
src/devices/core.py
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
# -*- 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 Baseclasses for real devices implemented in the server"""
|
||||||
|
|
||||||
|
import types
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
from errors import ConfigError, ProgrammingError
|
||||||
|
from protocol import status
|
||||||
|
|
||||||
|
# storage for CONFIGurable settings (from configfile)
|
||||||
|
class CONFIG(object):
|
||||||
|
def __init__(self, description, validator=None, default=None, unit=None):
|
||||||
|
self.description = description
|
||||||
|
self.validator = validator
|
||||||
|
self.default = default
|
||||||
|
self.unit = unit
|
||||||
|
|
||||||
|
|
||||||
|
# storage for PARAMeter settings (changeable during runtime)
|
||||||
|
class PARAM(object):
|
||||||
|
def __init__(self, description, validator=None, default=None, unit=None, readonly=False):
|
||||||
|
self.description = description
|
||||||
|
self.validator = validator
|
||||||
|
self.default = default
|
||||||
|
self.unit = unit
|
||||||
|
self.readonly = readonly
|
||||||
|
# internal caching...
|
||||||
|
self.currentvalue = default
|
||||||
|
|
||||||
|
|
||||||
|
# storage for CMDs settings (names + call signature...)
|
||||||
|
class CMD(object):
|
||||||
|
def __init__(self, description, *args):
|
||||||
|
self.description = description
|
||||||
|
self.arguments = args
|
||||||
|
|
||||||
|
# Meta class
|
||||||
|
# warning: MAGIC!
|
||||||
|
class DeviceMeta(type):
|
||||||
|
def __new__(mcs, name, bases, attrs):
|
||||||
|
newtype = type.__new__(mcs, name, bases, attrs)
|
||||||
|
if '__constructed__' in attrs:
|
||||||
|
return newtype
|
||||||
|
# merge CONFIG, PARAM, CMDS from all sub-classes
|
||||||
|
for entry in ['CONFIG', 'PARAMS', 'CMDS']:
|
||||||
|
newentry = {}
|
||||||
|
for base in reversed(bases):
|
||||||
|
if hasattr(base, entry):
|
||||||
|
newentry.update(getattr(base, entry))
|
||||||
|
newentry.update(attrs.get(entry, {}))
|
||||||
|
setattr(newtype, entry, newentry)
|
||||||
|
# check validity of entries
|
||||||
|
for cname, info in newtype.CONFIG.items():
|
||||||
|
if not isinstance(info, CONFIG):
|
||||||
|
raise ProgrammingError("%r: device CONFIG %r should be a CONFIG object!" %
|
||||||
|
(name, cname))
|
||||||
|
#XXX: greate getters for the config value
|
||||||
|
for pname, info in newtype.PARAMS.items():
|
||||||
|
if not isinstance(info, PARAM):
|
||||||
|
raise ProgrammingError("%r: device PARAM %r should be a PARAM object!" %
|
||||||
|
(name, pname))
|
||||||
|
#XXX: greate getters and setters, setters should send async updates
|
||||||
|
# also collect/update information about CMD's
|
||||||
|
setattr(newtype, 'CMDS', getattr(newtype, 'CMDS', {}))
|
||||||
|
for name in attrs:
|
||||||
|
if name.startswith('do'):
|
||||||
|
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)
|
||||||
|
attrs['__constructed__'] = True
|
||||||
|
return newtype
|
||||||
|
|
||||||
|
# Basic device class
|
||||||
|
class Device(object):
|
||||||
|
"""Basic Device, doesn't do much"""
|
||||||
|
__metaclass__ = DeviceMeta
|
||||||
|
# CONFIG, PARAMS and CMDS are auto-merged upon subclassing
|
||||||
|
CONFIG = {}
|
||||||
|
PARAMS = {}
|
||||||
|
CMDS = {}
|
||||||
|
SERVER = None
|
||||||
|
def __init__(self, devname, serverobj, logger, cfgdict):
|
||||||
|
# remember the server object (for the async callbacks)
|
||||||
|
self.SERVER = serverobj
|
||||||
|
self.log = logger
|
||||||
|
self.name = devname
|
||||||
|
# check config for problems
|
||||||
|
# only accept config items specified in CONFIG
|
||||||
|
for k, v in cfgdict.items():
|
||||||
|
if k not in self.CONFIG:
|
||||||
|
raise ConfigError('Device %s:config Parameter %r not unterstood!' % (self.name, k))
|
||||||
|
# complain if a CONFIG entry has no default value and is not specified in cfgdict
|
||||||
|
for k, v in self.CONFIG.items():
|
||||||
|
if k not in cfgdict:
|
||||||
|
if 'default' not in v:
|
||||||
|
raise ConfigError('Config Parameter %r was not given and not default value exists!' % k)
|
||||||
|
cfgdict[k] = v['default'] # assume default value was given.
|
||||||
|
# now 'apply' config, passing values through the validators and store as attributes
|
||||||
|
for k, v in cfgdict.items():
|
||||||
|
# apply validator, complain if type does not fit
|
||||||
|
validator = self.CONFIG[k].validator
|
||||||
|
if validator is not None:
|
||||||
|
# only check if validator given
|
||||||
|
try:
|
||||||
|
v = validator(v)
|
||||||
|
except ValueError as e:
|
||||||
|
raise ConfigError("Device %s: config paramter %r:\n%r" % (self.name, k, e))
|
||||||
|
# XXX: with or without prefix?
|
||||||
|
setattr(self, 'config_' + k, v)
|
||||||
|
# set default parameter values as inital values
|
||||||
|
for k, v in self.PARAMS.items():
|
||||||
|
# apply validator, complain if type does not fit
|
||||||
|
validator = v.validator
|
||||||
|
value = v.default
|
||||||
|
if validator is not None:
|
||||||
|
# only check if validator given
|
||||||
|
value = validator(value)
|
||||||
|
setattr(self, k, v)
|
||||||
|
|
||||||
|
def init(self):
|
||||||
|
# may be overriden in other classes
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Readable(Device):
|
||||||
|
"""Basic readable device, providing the RO parameter 'value' and 'status'"""
|
||||||
|
PARAMS = {
|
||||||
|
'value' : PARAM('current value of the device', readonly=True),
|
||||||
|
'status' : PARAM('current status of the device',
|
||||||
|
readonly=True),
|
||||||
|
}
|
||||||
|
def read_value(self, maxage=0):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
return status.OK
|
||||||
|
|
||||||
|
|
||||||
|
class Driveable(Readable):
|
||||||
|
"""Basic Driveable device, providing a RW target parameter to those of a Readable"""
|
||||||
|
PARAMS = {
|
||||||
|
'target' : PARAM('target value of the device'),
|
||||||
|
}
|
||||||
|
def write_target(self, value):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
323
src/devices/cryo.py
Normal file
323
src/devices/cryo.py
Normal file
@ -0,0 +1,323 @@
|
|||||||
|
#!/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>
|
||||||
|
# *****************************************************************************
|
||||||
|
|
||||||
|
"""playing implementation of a (simple) simulated cryostat"""
|
||||||
|
|
||||||
|
from math import atan
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
hack = []
|
||||||
|
|
||||||
|
class Cryostat(Driveable):
|
||||||
|
"""simulated cryostat with heat capacity on the sample, cooling power and thermal transfer functions"""
|
||||||
|
CONFIG = dict(
|
||||||
|
jitter=CONFIG("amount of random noise on readout values",
|
||||||
|
validator=floatrange(0, 1), default=1,
|
||||||
|
),
|
||||||
|
T_start=CONFIG("starting temperature for simulation",
|
||||||
|
validator=positive, default=2,
|
||||||
|
),
|
||||||
|
looptime=CONFIG("timestep for simulation",
|
||||||
|
validator=positive, default=1, unit="s",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
PARAMS = dict(
|
||||||
|
ramp=PARAM("ramping speed in K/min",
|
||||||
|
validator=floatrange(0, 1e3), default=1,
|
||||||
|
),
|
||||||
|
setpoint=PARAM("ramping speed in K/min",
|
||||||
|
validator=float, default=1, readonly=True,
|
||||||
|
),
|
||||||
|
maxpower=PARAM("Maximum heater power in W",
|
||||||
|
validator=float, default=0, readonly=True, unit="W",
|
||||||
|
),
|
||||||
|
heater=PARAM("current heater setting in %",
|
||||||
|
validator=float, default=0, readonly=True, unit="%",
|
||||||
|
),
|
||||||
|
heaterpower=PARAM("current heater power in W",
|
||||||
|
validator=float, default=0, readonly=True, unit="W",
|
||||||
|
),
|
||||||
|
target=PARAM("target temperature in K",
|
||||||
|
validator=float, default=0, unit="K",
|
||||||
|
),
|
||||||
|
p=PARAM("regulation coefficient 'p' in %/K",
|
||||||
|
validator=positive, default=40, unit="%/K",
|
||||||
|
),
|
||||||
|
i=PARAM("regulation coefficient 'i'",
|
||||||
|
validator=floatrange(0, 100), default=10,
|
||||||
|
),
|
||||||
|
d=PARAM("regulation coefficient 'd'",
|
||||||
|
validator=floatrange(0, 100), default=2,
|
||||||
|
),
|
||||||
|
mode=PARAM("mode of regulation",
|
||||||
|
validator=mapping('ramp', 'pid', 'openloop'), default='pid',
|
||||||
|
),
|
||||||
|
|
||||||
|
tolerance=PARAM("temperature range for stability checking",
|
||||||
|
validator=floatrange(0, 100), default=0.1, unit='K',
|
||||||
|
),
|
||||||
|
window=PARAM("time window for stability checking",
|
||||||
|
validator=floatrange(1, 900), default=30, unit='s',
|
||||||
|
),
|
||||||
|
timeout=PARAM("max waiting time for stabilisation check",
|
||||||
|
validator=floatrange(1, 36000), default=900, unit='s',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def init(self):
|
||||||
|
self._stopflag = False
|
||||||
|
self._thread = threading.Thread(target=self.thread)
|
||||||
|
self._thread.daemon = True
|
||||||
|
self._thread.start()
|
||||||
|
#XXX: hack!!! use a singleton as registry for the other devices to access this one...
|
||||||
|
hack.append(self)
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
# instead of asking a 'Hardware' take the value from the simulation thread
|
||||||
|
return self.status
|
||||||
|
|
||||||
|
def read_value(self, maxage=0):
|
||||||
|
# return regulation value (averaged regulation temp)
|
||||||
|
return self.regulationtemp + self.config_jitter * (0.5 - random.random())
|
||||||
|
|
||||||
|
def read_target(self, maxage=0):
|
||||||
|
return self.target
|
||||||
|
|
||||||
|
def write_target(self, value):
|
||||||
|
self.target = value
|
||||||
|
# next request will see this status, until the loop updates it
|
||||||
|
self.status = (status.BUSY, 'new target set')
|
||||||
|
|
||||||
|
def read_maxpower(self, maxage=0):
|
||||||
|
return self.maxpower
|
||||||
|
|
||||||
|
def write_maxpower(self, newpower):
|
||||||
|
# rescale heater setting in % to keep the power
|
||||||
|
self.heater = max(0, min(100, self.heater * self.maxpower / float(newpower)))
|
||||||
|
self.maxpower = newpower
|
||||||
|
|
||||||
|
def doStop(self):
|
||||||
|
# stop the ramp by setting current value as target
|
||||||
|
# XXX: there may be use case where setting the current temp may be better
|
||||||
|
self.write_target(self.setpoint)
|
||||||
|
|
||||||
|
#
|
||||||
|
# calculation helpers
|
||||||
|
#
|
||||||
|
def __coolerPower(self, temp):
|
||||||
|
"""returns cooling power in W at given temperature"""
|
||||||
|
# quadratic up to 42K, is linear from 40W@42K to 100W@600K
|
||||||
|
# return clamp((temp-2)**2 / 32., 0., 40.) + temp * 0.1
|
||||||
|
return clamp(15 * atan(temp * 0.01) ** 3, 0., 40.) + temp * 0.1 - 0.2
|
||||||
|
|
||||||
|
def __coolerCP(self, temp):
|
||||||
|
"""heat capacity of cooler at given temp"""
|
||||||
|
return 75 * atan(temp / 50)**2 + 1
|
||||||
|
|
||||||
|
def __heatLink(self, coolertemp, sampletemp):
|
||||||
|
"""heatflow from sample to cooler. may be negative..."""
|
||||||
|
flow = (sampletemp - coolertemp) * \
|
||||||
|
((coolertemp + sampletemp) ** 2)/400.
|
||||||
|
cp = clamp(self.__coolerCP(coolertemp) * self.__sampleCP(sampletemp),
|
||||||
|
1, 10)
|
||||||
|
return clamp(flow, -cp, cp)
|
||||||
|
|
||||||
|
def __sampleCP(self, temp):
|
||||||
|
return 3 * atan(temp / 30) + \
|
||||||
|
12 * temp / ((temp - 12.)**2 + 10) + 0.5
|
||||||
|
|
||||||
|
def __sampleLeak(self, temp):
|
||||||
|
return 0.02/temp
|
||||||
|
|
||||||
|
def thread(self):
|
||||||
|
self.sampletemp = self.config_T_start
|
||||||
|
self.regulationtemp = self.config_T_start
|
||||||
|
self.status = status.OK
|
||||||
|
while not self._stopflag:
|
||||||
|
try:
|
||||||
|
self.__sim()
|
||||||
|
except Exception as e:
|
||||||
|
self.log.exception(e)
|
||||||
|
self.status = status.ERROR, str(e)
|
||||||
|
|
||||||
|
def __sim(self):
|
||||||
|
# complex thread handling:
|
||||||
|
# a) simulation of cryo (heat flow, thermal masses,....)
|
||||||
|
# b) optional PID temperature controller with windup control
|
||||||
|
# c) generating status+updated value+ramp
|
||||||
|
# this thread is not supposed to exit!
|
||||||
|
|
||||||
|
# local state keeping:
|
||||||
|
regulation = self.regulationtemp
|
||||||
|
sample = self.sampletemp
|
||||||
|
window = [] # keep history values for stability check
|
||||||
|
timestamp = time.time()
|
||||||
|
heater = 0
|
||||||
|
lastflow = 0
|
||||||
|
last_heaters = (0, 0)
|
||||||
|
delta = 0
|
||||||
|
I = D = 0
|
||||||
|
lastD = 0
|
||||||
|
damper = 1
|
||||||
|
lastmode = self.mode
|
||||||
|
while not self._stopflag:
|
||||||
|
t = time.time()
|
||||||
|
h = t - timestamp
|
||||||
|
if h < self.looptime / damper:
|
||||||
|
time.sleep(clamp(self.looptime / damper - h, 0.1, 60))
|
||||||
|
continue
|
||||||
|
# a)
|
||||||
|
sample = self.sampletemp
|
||||||
|
regulation = self.regulationtemp
|
||||||
|
heater = self.heater
|
||||||
|
|
||||||
|
heatflow = self.__heatLink(regulation, sample)
|
||||||
|
self.log.debug('sample = %.5f, regulation = %.5f, heatflow = %.5g'
|
||||||
|
% (sample, regulation, heatflow))
|
||||||
|
newsample = max(0,
|
||||||
|
sample + (self.__sampleLeak(sample) - heatflow) /
|
||||||
|
self.__sampleCP(sample) * h)
|
||||||
|
# avoid instabilities due to too small CP
|
||||||
|
newsample = clamp(newsample, sample, regulation)
|
||||||
|
regdelta = (heater * 0.01 * self.maxpower + heatflow -
|
||||||
|
self.__coolerPower(regulation))
|
||||||
|
newregulation = max(0, regulation +
|
||||||
|
regdelta / self.__coolerCP(regulation) * h)
|
||||||
|
# b) see
|
||||||
|
# http://brettbeauregard.com/blog/2011/04/improving-the-beginners-pid-introduction/
|
||||||
|
if self.mode != 'openloop':
|
||||||
|
# fix artefacts due to too big timesteps
|
||||||
|
# actually i would prefer reducing looptime, but i have no
|
||||||
|
# good idea on when to increase it back again
|
||||||
|
if heatflow * lastflow != -100:
|
||||||
|
if (newregulation - newsample) * (regulation - sample) < 0:
|
||||||
|
# newregulation = (newregulation + regulation) / 2
|
||||||
|
# newsample = (newsample + sample) / 2
|
||||||
|
damper += 1
|
||||||
|
lastflow = heatflow
|
||||||
|
|
||||||
|
error = self.setpoint - newregulation
|
||||||
|
# use a simple filter to smooth delta a little
|
||||||
|
delta = (delta + regulation - newregulation) / 2.
|
||||||
|
|
||||||
|
kp = self.p / 10. # LakeShore P = 10*k_p
|
||||||
|
ki = kp * abs(self.i) / 500. # LakeShore I = 500/T_i
|
||||||
|
kd = kp * abs(self.d) / 2. # LakeShore D = 2*T_d
|
||||||
|
P = kp * error
|
||||||
|
I += ki * error * h
|
||||||
|
D = kd * delta / h
|
||||||
|
|
||||||
|
# avoid reset windup
|
||||||
|
I = clamp(I, 0., 100.) # I is in %
|
||||||
|
|
||||||
|
# avoid jumping heaterpower if switching back to pid mode
|
||||||
|
if lastmode != self.mode:
|
||||||
|
# adjust some values upon switching back on
|
||||||
|
I = self.heater - P - D
|
||||||
|
|
||||||
|
v = P + I + D
|
||||||
|
# in damping mode, use a weighted sum of old + new heaterpower
|
||||||
|
if damper > 1:
|
||||||
|
v = ((damper ** 2 - 1) * self.heater + v) / damper ** 2
|
||||||
|
|
||||||
|
# damp oscillations due to D switching signs
|
||||||
|
if D * lastD < -0.2:
|
||||||
|
v = (v + heater) / 2.
|
||||||
|
# clamp new heater power to 0..100%
|
||||||
|
heater = clamp(v, 0., 100.)
|
||||||
|
lastD = D
|
||||||
|
|
||||||
|
self.log.debug('PID: P = %.2f, I = %.2f, D = %.2f, '
|
||||||
|
'heater = %.2f' % (P, I, D, heater))
|
||||||
|
|
||||||
|
# check for turn-around points to detect oscillations ->
|
||||||
|
# increase damper
|
||||||
|
x, y = last_heaters
|
||||||
|
if (x + 0.1 < y and y > heater + 0.1) or \
|
||||||
|
(x > y + 0.1 and y + 0.1 < heater):
|
||||||
|
damper += 1
|
||||||
|
last_heaters = (y, heater)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# self.heaterpower is set manually, not by pid
|
||||||
|
heater = self.heater
|
||||||
|
last_heaters = (0, 0)
|
||||||
|
|
||||||
|
heater = round(heater, 3)
|
||||||
|
sample = newsample
|
||||||
|
regulation = newregulation
|
||||||
|
lastmode = self.mode
|
||||||
|
# c)
|
||||||
|
if self.setpoint != self.target:
|
||||||
|
if self.ramp == 0:
|
||||||
|
maxdelta = 10000
|
||||||
|
else:
|
||||||
|
maxdelta = self.ramp / 60. * h
|
||||||
|
try:
|
||||||
|
self.setpoint = round(self.setpoint +
|
||||||
|
clamp(self.target - self.setpoint,
|
||||||
|
-maxdelta, maxdelta), 3)
|
||||||
|
self.log.debug('setpoint changes to %r (target %r)' %
|
||||||
|
(self.setpoint, self.target))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
# self.target might be None
|
||||||
|
pass
|
||||||
|
|
||||||
|
# temperature is stable when all recorded values in the window
|
||||||
|
# differ from setpoint by less than tolerance
|
||||||
|
currenttime = time.time()
|
||||||
|
window.append((currenttime, sample))
|
||||||
|
while window[0][0] < currenttime - self.window:
|
||||||
|
# remove old/stale entries
|
||||||
|
window.pop(0)
|
||||||
|
# obtain min/max
|
||||||
|
deviation = 0
|
||||||
|
for _, T in window:
|
||||||
|
if abs(T-self.target) > deviation:
|
||||||
|
deviation = abs(T-self.target)
|
||||||
|
if (len(window) < 3) or deviation > self.tolerance:
|
||||||
|
self.status = status.BUSY, 'unstable'
|
||||||
|
elif self.setpoint == self.target:
|
||||||
|
self.status = status.OK, 'at target'
|
||||||
|
damper -= (damper - 1) / 10. # max value for damper is 11
|
||||||
|
else:
|
||||||
|
self.status = status.BUSY, 'ramping setpoint'
|
||||||
|
damper -= (damper - 1) / 20.
|
||||||
|
self.regulationtemp = round(regulation, 3)
|
||||||
|
self.sampletemp = round(sample, 3)
|
||||||
|
self.heaterpower = round(heater * self.maxpower * 0.01, 3)
|
||||||
|
self.heater = heater
|
||||||
|
timestamp = t
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
# should be called from server when the server is stopped
|
||||||
|
self._stopflag = True
|
||||||
|
if self._thread and self._thread.isAlive():
|
||||||
|
self._thread.join()
|
||||||
|
|
64
src/devices/test.py
Normal file
64
src/devices/test.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
#!/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>
|
||||||
|
# *****************************************************************************
|
||||||
|
|
||||||
|
"""testing devices"""
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
|
from devices.core import Readable, Driveable, CONFIG, PARAM
|
||||||
|
from validators import floatrange
|
||||||
|
|
||||||
|
class LN2(Readable):
|
||||||
|
"""Just a readable.
|
||||||
|
|
||||||
|
class name indicates it to be a sensor for LN2, but the implementation may do anything"""
|
||||||
|
def read_value(self, maxage=0):
|
||||||
|
return round(100*random.random(), 1)
|
||||||
|
|
||||||
|
class Heater(Driveable):
|
||||||
|
"""Just a driveable.
|
||||||
|
|
||||||
|
class name indicates it to be some heating element, but the implementation may do anything"""
|
||||||
|
CONFIG = {
|
||||||
|
'maxheaterpower' : CONFIG('maximum allowed heater power',
|
||||||
|
validator=floatrange(0, 100), unit='W'),
|
||||||
|
}
|
||||||
|
def read_value(self, maxage=0):
|
||||||
|
return round(100*random.random(), 1)
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Temp(Driveable):
|
||||||
|
"""Just a driveable.
|
||||||
|
|
||||||
|
class name indicates it to be some temperature controller, but the implementation may do anything"""
|
||||||
|
CONFIG = {
|
||||||
|
'sensor' : CONFIG("Sensor number or calibration id",
|
||||||
|
validator=str),
|
||||||
|
}
|
||||||
|
def read_value(self, maxage=0):
|
||||||
|
return round(100*random.random(), 1)
|
||||||
|
|
||||||
|
def write_target(self, target):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
#
|
#
|
||||||
@ -19,20 +20,13 @@
|
|||||||
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
||||||
#
|
#
|
||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
|
"""error class for our little framework"""
|
||||||
|
|
||||||
"""Define helpers"""
|
class SECoPServerError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
class attrdict(dict):
|
class ConfigError(SECoPServerError):
|
||||||
def __getattr__(self, key):
|
pass
|
||||||
return self[key]
|
|
||||||
def __setattr__(self, key, value):
|
|
||||||
self[key] = value
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
print "minimal testing: lib"
|
|
||||||
d = attrdict(a=1, b=2)
|
|
||||||
_ = d.a + d['b']
|
|
||||||
d.c = 9
|
|
||||||
d['d'] = 'c'
|
|
||||||
assert d[d.d] == 9
|
|
||||||
|
|
||||||
|
class ProgrammingError(SECoPServerError):
|
||||||
|
pass
|
76
src/lib/__init__.py
Normal file
76
src/lib/__init__.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# -*- 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 helpers"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from os import path
|
||||||
|
|
||||||
|
class attrdict(dict):
|
||||||
|
"""a normal dict, providing access also via attributes"""
|
||||||
|
def __getattr__(self, key):
|
||||||
|
return self[key]
|
||||||
|
def __setattr__(self, key, value):
|
||||||
|
self[key] = value
|
||||||
|
|
||||||
|
def clamp(_min, value, _max):
|
||||||
|
"""return the median of 3 values,
|
||||||
|
|
||||||
|
i.e. value if min <= value <= max, else min or max depending on which side
|
||||||
|
value lies outside the [min..max] interval
|
||||||
|
"""
|
||||||
|
# return median, i.e. clamp the the value between min and max
|
||||||
|
return sorted([_min, value, _max])[1]
|
||||||
|
|
||||||
|
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 = __import__(spec)
|
||||||
|
return getattr(module, classname)
|
||||||
|
|
||||||
|
def make_logger(inst='server', name='', base_path='', loglevel=logging.INFO):
|
||||||
|
# XXX: rework this! (outsource to a logging module...)
|
||||||
|
if name:
|
||||||
|
inst = '%s %s' % (inst, name)
|
||||||
|
logging.basicConfig(level=loglevel, format='%(asctime)s %(levelname)s %(message)s')
|
||||||
|
logger = logging.getLogger(inst)
|
||||||
|
logger.setLevel(loglevel)
|
||||||
|
fh = logging.FileHandler(path.join(base_path, 'log', (name or inst) + '.log'))
|
||||||
|
fh.setLevel(loglevel)
|
||||||
|
logger.addHandler(fh)
|
||||||
|
return logger
|
||||||
|
|
||||||
|
|
||||||
|
# moved below definitions to break import cycle
|
||||||
|
from pidfile import *
|
||||||
|
from startup import *
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print "minimal testing: lib"
|
||||||
|
d = attrdict(a=1, b=2)
|
||||||
|
_ = d.a + d['b']
|
||||||
|
d.c = 9
|
||||||
|
d['d'] = 'c'
|
||||||
|
assert d[d.d] == 9
|
||||||
|
|
53
src/lib/pidfile.py
Normal file
53
src/lib/pidfile.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# -*- 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 pidfile helpers"""
|
||||||
|
import os
|
||||||
|
import atexit
|
||||||
|
import psutil
|
||||||
|
|
||||||
|
|
||||||
|
def read_pidfile(pidfile):
|
||||||
|
"""read the given pidfile, return the pid as an int
|
||||||
|
|
||||||
|
or None upon errors (file not existing)"""
|
||||||
|
try:
|
||||||
|
with open(pidfile, 'r') as f:
|
||||||
|
return int(f.read())
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def remove_pidfile(pidfile):
|
||||||
|
"""remove the given pidfile, typically at end of the process"""
|
||||||
|
os.remove(pidfile)
|
||||||
|
|
||||||
|
def write_pidfile(pidfile, pid):
|
||||||
|
"""write the given pid to the given pidfile"""
|
||||||
|
with open(pidfile, 'w') as f:
|
||||||
|
f.write('%d\n' % pid)
|
||||||
|
atexit.register(remove_pidfile, pidfile)
|
||||||
|
|
||||||
|
def check_pidfile(pidfile):
|
||||||
|
"""check if the process from a given pidfile is still running"""
|
||||||
|
pid = read_pidfile(pidfile)
|
||||||
|
return False if pid is None else psutil.pid_exists(pid)
|
||||||
|
|
133
src/lib/startup.py
Normal file
133
src/lib/startup.py
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
# -*- 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 helpers"""
|
||||||
|
import os
|
||||||
|
import psutil
|
||||||
|
import daemonize
|
||||||
|
import ConfigParser
|
||||||
|
|
||||||
|
from lib import read_pidfile, write_pidfile, get_class, make_logger
|
||||||
|
from server import DeviceServer as Server
|
||||||
|
from errors import ConfigError
|
||||||
|
|
||||||
|
__ALL__ = ['kill_server', 'start_server']
|
||||||
|
|
||||||
|
def kill_server(pidfile):
|
||||||
|
"""kill a server specified by a pidfile"""
|
||||||
|
pid = read_pidfile(pidfile)
|
||||||
|
if pid is None:
|
||||||
|
# already dead/not started yet
|
||||||
|
return
|
||||||
|
# get process for this pid
|
||||||
|
for proc in psutil.process_iter():
|
||||||
|
if proc.pid == pid:
|
||||||
|
break
|
||||||
|
proc.terminate()
|
||||||
|
proc.wait(3)
|
||||||
|
proc.kill()
|
||||||
|
|
||||||
|
def start_server(srvname, base_path, loglevel, daemon=False):
|
||||||
|
"""start a server, part1
|
||||||
|
|
||||||
|
handle the daemonizing and logging stuff and call the second step
|
||||||
|
"""
|
||||||
|
pidfile = os.path.join(base_path, 'pid', srvname + '.pid')
|
||||||
|
if daemon:
|
||||||
|
# dysfunctional :(
|
||||||
|
daemonproc = daemonize.Daemonize("server %s" % srvname,
|
||||||
|
pid=pidfile,
|
||||||
|
action=lambda: startup(srvname, base_path, loglevel),
|
||||||
|
)
|
||||||
|
daemonproc.start()
|
||||||
|
|
||||||
|
|
||||||
|
else:
|
||||||
|
write_pidfile(pidfile, os.getpid())
|
||||||
|
startup(srvname, base_path, loglevel) # blocks!
|
||||||
|
|
||||||
|
# unexported stuff here
|
||||||
|
def startup(srvname, base_path, loglevel):
|
||||||
|
"""really start a server (part2)
|
||||||
|
|
||||||
|
loads the config, initiate all objects, link them together
|
||||||
|
and finally start the interface server.
|
||||||
|
Never returns. (may raise)
|
||||||
|
"""
|
||||||
|
cfgfile = os.path.join(base_path, 'etc', srvname + '.cfg')
|
||||||
|
|
||||||
|
logger = make_logger('server', srvname, base_path=base_path, loglevel=loglevel)
|
||||||
|
logger.debug("parsing %r" % cfgfile)
|
||||||
|
|
||||||
|
parser = ConfigParser.SafeConfigParser()
|
||||||
|
if not parser.read([cfgfile]):
|
||||||
|
logger.error("Couldn't read cfg file !")
|
||||||
|
raise ConfigError("Couldn't read cfg file %r" % cfgfile)
|
||||||
|
|
||||||
|
# evaluate Server specific stuff
|
||||||
|
if not parser.has_section('server'):
|
||||||
|
logger.error("cfg file needs a 'server' section!")
|
||||||
|
raise ConfigError("cfg file %r needs a 'server' section!" % cfgfile)
|
||||||
|
serveropts = dict(item for item in parser.items('server'))
|
||||||
|
|
||||||
|
# check serveropts (init server)
|
||||||
|
# this raises if something wouldn't work
|
||||||
|
logger.debug("Creating device server")
|
||||||
|
server = Server(logger, serveropts)
|
||||||
|
|
||||||
|
# iterate over all sections, checking for devices
|
||||||
|
deviceopts = []
|
||||||
|
for section in parser.sections():
|
||||||
|
if section == "server":
|
||||||
|
continue # already handled, see above
|
||||||
|
if section.lower().startswith("device"):
|
||||||
|
# device section
|
||||||
|
devname = section[len('device '):] # omit leading 'device ' string
|
||||||
|
devopts = dict(item for item in parser.items(section))
|
||||||
|
if 'class' not in devopts:
|
||||||
|
logger.error("Device %s needs a class option!")
|
||||||
|
raise ConfigError("cfgfile %r: Device %s needs a class option!" % (cfgfile, devname))
|
||||||
|
# try to import the class, raise if this fails
|
||||||
|
devopts['class'] = get_class(devopts['class'])
|
||||||
|
# all went well so far
|
||||||
|
deviceopts.append([devname, devopts])
|
||||||
|
|
||||||
|
# check devices by creating them
|
||||||
|
devs = {}
|
||||||
|
for devname, devopts in deviceopts:
|
||||||
|
devclass = devopts.pop('class')
|
||||||
|
# create device
|
||||||
|
logger.debug("Creating Device %r" % devname)
|
||||||
|
devobj = devclass(devname, server, logger, devopts)
|
||||||
|
devs[devname] = devobj
|
||||||
|
|
||||||
|
# connect devices with server
|
||||||
|
for devname, devobj in devs.items():
|
||||||
|
logger.info("registering device %r" % devname)
|
||||||
|
server.register_device(devobj, devname)
|
||||||
|
# also call init on the devices
|
||||||
|
logger.debug("device.init()")
|
||||||
|
devobj.init()
|
||||||
|
|
||||||
|
# handle requests until stop is requsted
|
||||||
|
logger.info('startup done, handling transport messages')
|
||||||
|
server.serve_forever()
|
0
src/protocol/__init__.py
Normal file
0
src/protocol/__init__.py
Normal file
@ -25,19 +25,7 @@
|
|||||||
also define helpers to derive properties of the device"""
|
also define helpers to derive properties of the device"""
|
||||||
|
|
||||||
from lib import attrdict
|
from lib import attrdict
|
||||||
|
from protocol import status
|
||||||
class Status(object):
|
|
||||||
"""Map Menaing of a devices status to some constants
|
|
||||||
|
|
||||||
which may be used for transport"""
|
|
||||||
OK = 100
|
|
||||||
MOVING = 200
|
|
||||||
WARN = 300
|
|
||||||
UNSTABLE = 350
|
|
||||||
ERROR = 400
|
|
||||||
UNKNOWN = 999
|
|
||||||
|
|
||||||
status = Status()
|
|
||||||
|
|
||||||
|
|
||||||
# XXX: deriving PARS/CMDS should be done in a suitable metaclass....
|
# XXX: deriving PARS/CMDS should be done in a suitable metaclass....
|
31
src/protocol/status.py
Normal file
31
src/protocol/status.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#!/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>
|
||||||
|
#
|
||||||
|
# *****************************************************************************
|
||||||
|
"""Define Status constants"""
|
||||||
|
|
||||||
|
# could also be some objects
|
||||||
|
OK = 100
|
||||||
|
BUSY = 200
|
||||||
|
WARN = 300
|
||||||
|
UNSTABLE = 350
|
||||||
|
ERROR = 400
|
||||||
|
UNKNOWN = -1
|
||||||
|
|
@ -136,9 +136,14 @@ class SECoPRequestHandler(SocketServer.BaseRequestHandler):
|
|||||||
|
|
||||||
class SECoPServer(SocketServer.ThreadingTCPServer, DeviceServer):
|
class SECoPServer(SocketServer.ThreadingTCPServer, DeviceServer):
|
||||||
daemon_threads = False
|
daemon_threads = False
|
||||||
|
def __init__(self, logger, serveropts):
|
||||||
def startup_server():
|
bindto = serveropts.pop('bindto', 'localhost')
|
||||||
srv = SECoPServer(('localhost', DEF_PORT), SECoPRequestHandler,
|
portnum = DEF_PORT
|
||||||
bind_and_activate=True)
|
if ':' in bindto:
|
||||||
srv.serve_forever()
|
bindto, _port = bindto.rsplit(':')
|
||||||
srv.server_close()
|
portnum = int(_port)
|
||||||
|
logger.debug("binding to %s:%d" % (bindto, portnum))
|
||||||
|
super(SECoPServer, self).__init__((bindto, portnum),
|
||||||
|
SECoPRequestHandler, bind_and_activate=True)
|
||||||
|
logger.info("SECoPServer initiated")
|
||||||
|
logger.debug('serveropts remaining: %r' % serveropts)
|
@ -22,23 +22,25 @@
|
|||||||
|
|
||||||
"""Define basic SECoP DeviceServer"""
|
"""Define basic SECoP DeviceServer"""
|
||||||
|
|
||||||
import logging
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from messages import parse, ListDevicesRequest, ListDeviceParamsRequest, \
|
from protocol.messages import parse, ListDevicesRequest, ListDeviceParamsRequest, \
|
||||||
ReadParamRequest, ErrorReply, MessageHandler
|
ReadParamRequest, ErrorReply, MessageHandler
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceServer(MessageHandler):
|
class DeviceServer(MessageHandler):
|
||||||
def __init__(self):
|
def __init__(self, logger, serveropts):
|
||||||
self._devices = {}
|
self._devices = {}
|
||||||
self.log = logging
|
self.log = logger
|
||||||
|
# XXX: check serveropts and raise if problems exist
|
||||||
|
# mandatory serveropts: interface=tcpip, encoder=pickle, frame=eol
|
||||||
|
# XXX: remaining opts are checked by the corresponding interface server
|
||||||
|
|
||||||
self.log.basicConfig(level=logging.WARNING,
|
def serve_forever(self):
|
||||||
format='%(asctime)s %(levelname)s %(message)s')
|
self.log.error("Serving not yet implemented!")
|
||||||
|
|
||||||
def registerDevice(self, deviceobj, devicename):
|
def register_device(self, deviceobj, devicename):
|
||||||
# make the server export a deviceobj under a given name.
|
# make the server export a deviceobj under a given name.
|
||||||
# all exportet properties are taken from the device
|
# all exportet properties are taken from the device
|
||||||
if devicename in self._devices:
|
if devicename in self._devices:
|
||||||
@ -47,7 +49,7 @@ class DeviceServer(MessageHandler):
|
|||||||
self._devices[devicename] = deviceobj
|
self._devices[devicename] = deviceobj
|
||||||
deviceobj.name = devicename
|
deviceobj.name = devicename
|
||||||
|
|
||||||
def unRegisterDevice(self, device_obj_or_name):
|
def unregister_device(self, device_obj_or_name):
|
||||||
if not device_obj_or_name in self._devices:
|
if not device_obj_or_name in self._devices:
|
||||||
self.log.error('IGN: Device %r not registered!' %
|
self.log.error('IGN: Device %r not registered!' %
|
||||||
device_obj_or_name)
|
device_obj_or_name)
|
||||||
@ -55,12 +57,12 @@ class DeviceServer(MessageHandler):
|
|||||||
del self._devices[device_obj_or_name]
|
del self._devices[device_obj_or_name]
|
||||||
# may need to do more
|
# may need to do more
|
||||||
|
|
||||||
def getDevice(self, devname):
|
def get_device(self, devname):
|
||||||
"""returns the requested deviceObj or None"""
|
"""returns the requested deviceObj or None"""
|
||||||
devobj = self._devices.get(devname, None)
|
devobj = self._devices.get(devname, None)
|
||||||
return devobj
|
return devobj
|
||||||
|
|
||||||
def listDevices(self):
|
def list_devices(self):
|
||||||
return list(self._devices.keys())
|
return list(self._devices.keys())
|
||||||
|
|
||||||
def handle(self, msg):
|
def handle(self, msg):
|
||||||
@ -85,7 +87,8 @@ class DeviceServer(MessageHandler):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
from device import Driveable, status
|
from devices.core import Driveable
|
||||||
|
from protocol import status
|
||||||
class TestDevice(Driveable):
|
class TestDevice(Driveable):
|
||||||
name = 'Unset'
|
name = 'Unset'
|
||||||
unit = 'Oinks'
|
unit = 'Oinks'
|
||||||
@ -119,8 +122,8 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
print "minimal testing: server"
|
print "minimal testing: server"
|
||||||
srv = DeviceServer()
|
srv = DeviceServer()
|
||||||
srv.registerDevice(TestDevice(), 'dev1')
|
srv.register_device(TestDevice(), 'dev1')
|
||||||
srv.registerDevice(TestDevice(), 'dev2')
|
srv.register_device(TestDevice(), 'dev2')
|
||||||
devices = parse(srv.handle(ListDevicesRequest()))[2]['list_of_devices']
|
devices = parse(srv.handle(ListDevicesRequest()))[2]['list_of_devices']
|
||||||
print 'Srv exports these devices:', devices
|
print 'Srv exports these devices:', devices
|
||||||
for dev in sorted(devices):
|
for dev in sorted(devices):
|
||||||
|
71
src/validators.py
Normal file
71
src/validators.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
# -*- 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 validators."""
|
||||||
|
|
||||||
|
|
||||||
|
# a Validator validates a given object and raises an ValueError if it doesn't fit
|
||||||
|
# easy python validators: int(), float(), str()
|
||||||
|
|
||||||
|
class floatrange(object):
|
||||||
|
def __init__(self, lower, upper):
|
||||||
|
self.lower = float(lower)
|
||||||
|
self.upper = float(upper)
|
||||||
|
def __call__(self, value):
|
||||||
|
value = float(value)
|
||||||
|
if not self.lower <= value <= self.upper:
|
||||||
|
raise ValueError('Floatrange: value %r must be within %f and %f' % (value, self.lower, self.upper))
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def positive(obj):
|
||||||
|
if obj <= 0:
|
||||||
|
raise ValueError('Value %r must be positive!' % obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def nonnegative(obj):
|
||||||
|
if obj < 0:
|
||||||
|
raise ValueError('Value %r must be zero or positive!' % obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
class mapping(object):
|
||||||
|
def __init__(self, *args, **kwds):
|
||||||
|
self.mapping = {}
|
||||||
|
# use given kwds directly
|
||||||
|
self.mapping.update(kwds)
|
||||||
|
# enumerate args
|
||||||
|
i = -1
|
||||||
|
args = list(args)
|
||||||
|
while args:
|
||||||
|
i += 1
|
||||||
|
if i in self.mapping:
|
||||||
|
continue
|
||||||
|
self.mapping[args.pop(0)] = i
|
||||||
|
# generate reverse mapping too for use by protocol
|
||||||
|
self.revmapping = {}
|
||||||
|
for k, v in self.mapping.items():
|
||||||
|
self.revmapping[v] = k
|
||||||
|
|
||||||
|
def __call__(self, obj):
|
||||||
|
if obj in self.mapping:
|
||||||
|
return obj
|
||||||
|
raise ValueError("%r should be one of %r" % (obj, list(self.mapping.keys())))
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user