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/*
|
||||
*.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
|
||||
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
|
||||
=============================
|
||||
|
||||
[Notes](notes.html)
|
||||
|
||||
|
||||
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/client for everything client related (ProxyDevice!)
|
||||
* src/protocol for protocol specific things
|
||||
* need subtree for different implementations to play with
|
||||
* src/lib for helpers and other stuff
|
||||
* possibly a parallel src tree for cpp version
|
||||
|
||||
@ -20,16 +21,31 @@
|
||||
|
||||
## A Server ##
|
||||
|
||||
* evaluate config.ini
|
||||
* handle cmdline args (specify different server.ini)
|
||||
* get daemonizing working
|
||||
* handle -d (nodaemon) and -D (default, daemonize) cmd line args
|
||||
* support Async data units
|
||||
* 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 ##
|
||||
|
||||
* 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
|
||||
protocol=pickle
|
||||
|
||||
[device "LN2"]
|
||||
[device LN2]
|
||||
class=devices.test.LN2
|
||||
|
||||
[device "heater"]
|
||||
[device heater]
|
||||
class=devices.test.Heater
|
||||
maxheaterpower=10
|
||||
|
||||
[device "T1"]
|
||||
class=devices.test.temp
|
||||
[device T1]
|
||||
class=devices.test.Temp
|
||||
sensor="X34598T7"
|
||||
|
@ -1,5 +1,9 @@
|
||||
# for generating docu
|
||||
markdown>=2.6
|
||||
# general stuff
|
||||
psutil
|
||||
# daemonizing not yet functional (logging problems)
|
||||
#daemonize
|
||||
# for zmq
|
||||
#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 -*-
|
||||
# *****************************************************************************
|
||||
#
|
||||
@ -19,20 +20,13 @@
|
||||
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
||||
#
|
||||
# *****************************************************************************
|
||||
"""error class for our little framework"""
|
||||
|
||||
"""Define helpers"""
|
||||
class SECoPServerError(Exception):
|
||||
pass
|
||||
|
||||
class attrdict(dict):
|
||||
def __getattr__(self, key):
|
||||
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 ConfigError(SECoPServerError):
|
||||
pass
|
||||
|
||||
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"""
|
||||
|
||||
from lib import attrdict
|
||||
|
||||
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()
|
||||
from protocol import status
|
||||
|
||||
|
||||
# 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):
|
||||
daemon_threads = False
|
||||
|
||||
def startup_server():
|
||||
srv = SECoPServer(('localhost', DEF_PORT), SECoPRequestHandler,
|
||||
bind_and_activate=True)
|
||||
srv.serve_forever()
|
||||
srv.server_close()
|
||||
def __init__(self, logger, serveropts):
|
||||
bindto = serveropts.pop('bindto', 'localhost')
|
||||
portnum = DEF_PORT
|
||||
if ':' in bindto:
|
||||
bindto, _port = bindto.rsplit(':')
|
||||
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"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
from messages import parse, ListDevicesRequest, ListDeviceParamsRequest, \
|
||||
from protocol.messages import parse, ListDevicesRequest, ListDeviceParamsRequest, \
|
||||
ReadParamRequest, ErrorReply, MessageHandler
|
||||
|
||||
|
||||
|
||||
class DeviceServer(MessageHandler):
|
||||
def __init__(self):
|
||||
def __init__(self, logger, serveropts):
|
||||
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,
|
||||
format='%(asctime)s %(levelname)s %(message)s')
|
||||
def serve_forever(self):
|
||||
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.
|
||||
# all exportet properties are taken from the device
|
||||
if devicename in self._devices:
|
||||
@ -47,7 +49,7 @@ class DeviceServer(MessageHandler):
|
||||
self._devices[devicename] = deviceobj
|
||||
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:
|
||||
self.log.error('IGN: Device %r not registered!' %
|
||||
device_obj_or_name)
|
||||
@ -55,12 +57,12 @@ class DeviceServer(MessageHandler):
|
||||
del self._devices[device_obj_or_name]
|
||||
# may need to do more
|
||||
|
||||
def getDevice(self, devname):
|
||||
def get_device(self, devname):
|
||||
"""returns the requested deviceObj or None"""
|
||||
devobj = self._devices.get(devname, None)
|
||||
return devobj
|
||||
|
||||
def listDevices(self):
|
||||
def list_devices(self):
|
||||
return list(self._devices.keys())
|
||||
|
||||
def handle(self, msg):
|
||||
@ -85,7 +87,8 @@ class DeviceServer(MessageHandler):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from device import Driveable, status
|
||||
from devices.core import Driveable
|
||||
from protocol import status
|
||||
class TestDevice(Driveable):
|
||||
name = 'Unset'
|
||||
unit = 'Oinks'
|
||||
@ -119,8 +122,8 @@ if __name__ == '__main__':
|
||||
|
||||
print "minimal testing: server"
|
||||
srv = DeviceServer()
|
||||
srv.registerDevice(TestDevice(), 'dev1')
|
||||
srv.registerDevice(TestDevice(), 'dev2')
|
||||
srv.register_device(TestDevice(), 'dev1')
|
||||
srv.register_device(TestDevice(), 'dev2')
|
||||
devices = parse(srv.handle(ListDevicesRequest()))[2]['list_of_devices']
|
||||
print 'Srv exports these devices:', 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