diff --git a/Makefile b/Makefile index 7dda827..4594e1c 100644 --- a/Makefile +++ b/Makefile @@ -13,3 +13,9 @@ doc: doc/*.md @echo "Generating html tree" @bin/make_doc.py +demo: + @bin/secop-server -q demo & + @bin/secop-server -q test & + @bin/secop-server -q cryo & + @bin/secop-gui localhost:10767 localhost:10768 localhost:10769 + @ps aux|grep [s]ecop-server|awk '{print $$2}'|xargs kill diff --git a/etc/cryo.cfg b/etc/cryo.cfg new file mode 100644 index 0000000..70250a9 --- /dev/null +++ b/etc/cryo.cfg @@ -0,0 +1,29 @@ +[equipment] +id=cryo_7 + +[interface tcp] +interface=tcp +bindto=0.0.0.0 +bindport=10769 +# protocol to use for this interface +framing=eol +encoding=demo + + +[device cryo] +class=devices.cryo.Cryostat +jitter=0.1 +T_start=10.0 +target=10.0 +looptime=1 +ramp=6 +maxpower=20.0 +heater=4.1 +p=40 +i=10 +d=2 +mode=pid +tolerance=0.1 +window=30 +timeout=900 + diff --git a/etc/demo.cfg b/etc/demo.cfg index ecab565..0f4f894 100644 --- a/etc/demo.cfg +++ b/etc/demo.cfg @@ -1,7 +1,7 @@ [equipment] -id=demonstration +id=Equipment_ID_for_demonstration -[interface testing] +[interface tcp] interface=tcp bindto=0.0.0.0 bindport=10767 @@ -11,8 +11,8 @@ encoding=demo [device heatswitch] class=devices.demo.Switch -switch_on_time=3 -switch_off_time=5 +switch_on_time=5 +switch_off_time=10 [device mf] class=devices.demo.MagneticField @@ -39,5 +39,5 @@ system=Cryomagnet MX15 subdev_mf=mf subdev_ts=ts -[device vt] -class=devices.demo.ValidatorTest +#[device vt] +#class=devices.demo.ValidatorTest diff --git a/etc/test.cfg b/etc/test.cfg index 4a8c7d4..16ece1b 100644 --- a/etc/test.cfg +++ b/etc/test.cfg @@ -1,17 +1,10 @@ [equipment] -id=Fancy_ID_without_spaces-like:MLZ_furnace7 +id=test config -[client] -connectto=0.0.0.0 -port=10767 -interface = tcp -framing=eol -encoding=text - -[interface testing] +[interface tcp] interface=tcp bindto=0.0.0.0 -bindport=10767 +bindport=10768 # protocol to use for this interface framing=eol encoding=demo diff --git a/secop/client/__init__.py b/secop/client/__init__.py index 0876181..8fc92ad 100644 --- a/secop/client/__init__.py +++ b/secop/client/__init__.py @@ -19,17 +19,14 @@ # Enrico Faulhaber # # ***************************************************************************** - """Define Client side proxies""" # nothing here yet. - import code class NameSpace(dict): - def __init__(self): dict.__init__(self) self.__const = set() @@ -66,7 +63,6 @@ from os import path class ClientConsole(object): - def __init__(self, cfgname, basepath): self.namespace = NameSpace() self.namespace.setconst('help', self.helpCmd) @@ -89,6 +85,7 @@ class ClientConsole(object): else: help(arg) + import socket import threading from collections import deque @@ -99,7 +96,6 @@ from secop.protocol.messages import * class TCPConnection(object): - def __init__(self, connect, port, encoding, framing, **kwds): self.log = loggers.log.getChild('connection', False) self.encoder = ENCODERS[encoding]() @@ -147,8 +143,7 @@ class TCPConnection(object): cb(msg) except Exception as e: self.log.debug( - "handle_async: got exception %r" % - e, exception=true) + "handle_async: got exception %r" % e, exception=true) else: self.queue.append(msg) @@ -167,7 +162,6 @@ class TCPConnection(object): class Client(object): - def __init__(self, opts): self.log = loggers.log.getChild('client', True) self._cache = dict() @@ -184,7 +178,7 @@ class Client(object): def populateNamespace(self, namespace): self.connection.send(ListDevicesRequest()) -# reply = self.connection.read() -# self.log.info("found devices %r" % reply) + # reply = self.connection.read() + # self.log.info("found devices %r" % reply) # create proxies, populate cache.... namespace.setconst('connection', self.connection) diff --git a/secop/client/baseclient.py b/secop/client/baseclient.py index e23c433..4eeaa89 100644 --- a/secop/client/baseclient.py +++ b/secop/client/baseclient.py @@ -19,12 +19,12 @@ # Enrico Faulhaber # # ***************************************************************************** - """Define Client side proxies""" import json import socket import serial +from select import select import threading import Queue @@ -45,6 +45,7 @@ class TCPConnection(object): self._host = host self._port = int(port) self._thread = None + self.callbacks = [] # called if SEC-node shuts down self.connect() def connect(self): @@ -62,24 +63,33 @@ class TCPConnection(object): data = u'' while True: try: - newdata = self._io.recv(1024) - except socket.timeout: newdata = u'' + dlist = [self._io.fileno()] + rlist, wlist, xlist = select(dlist, dlist, dlist, 1) + if dlist[0] in rlist + wlist: + newdata = self._io.recv(1024) + if dlist[0] in xlist: + print "Problem: exception on socket, reconnecting!" + for cb, arg in self.callbacks: + cb(arg) + return + except socket.timeout: pass except Exception as err: print err, "reconnecting" - self.connect() - data = u'' - continue + for cb, arg in self.callbacks: + cb(arg) + return data += newdata while '\n' in data: line, data = data.split('\n', 1) try: - self._readbuffer.put( - line.strip('\r'), block=True, timeout=1) + self._readbuffer.put(line.strip('\r'), + block=True, + timeout=1) except Queue.Full: - self.log.debug( - 'rcv queue full! dropping line: %r' % line) + self.log.debug('rcv queue full! dropping line: %r' % + line) finally: self._thread = None @@ -147,8 +157,9 @@ class Client(object): devport = opts.pop('device') baudrate = int(opts.pop('baudrate', 115200)) self.contactPoint = "serial://%s:%s" % (devport, baudrate) - self.connection = serial.Serial(devport, baudrate=baudrate, - timeout=1) + self.connection = serial.Serial( + devport, baudrate=baudrate, timeout=1) + self.connection.callbacks = [] else: host = opts.pop('connectto', 'localhost') port = int(opts.pop('port', 10767)) @@ -198,7 +209,7 @@ class Client(object): self.secop_id = line continue msgtype, spec, data = self.decode_message(line) - if msgtype in ('update', 'changed'): + if msgtype in ('event', 'update', 'changed'): # handle async stuff self._handle_event(spec, data) # handle sync stuff @@ -217,21 +228,20 @@ class Client(object): if entry: self.log.error("request %r resulted in Error %r" % (data[0], spec)) - entry.extend([True, EXCEPTIONS[spec](data)]) + entry.extend([True, EXCEPTIONS[spec](*data)]) entry[0].set() return - self.log.error("got an unexpected error %s %r" % - (spec, data[0])) + self.log.error("got an unexpected error %s %r" % (spec, data[0])) return if msgtype == "describing": - data = [spec, data] - spec = '' - entry = self.expected_replies.get((msgtype, spec), None) + entry = self.expected_replies.get((msgtype, ''), None) + else: + entry = self.expected_replies.get((msgtype, spec), None) + if entry: - self.log.debug("got expected reply '%s %s'" % - (msgtype, spec) if spec else - "got expected reply '%s'" % msgtype) - entry.extend([False, data]) + self.log.debug("got expected reply '%s %s'" % (msgtype, spec) + if spec else "got expected reply '%s'" % msgtype) + entry.extend([False, msgtype, spec, data]) entry[0].set() return @@ -282,8 +292,8 @@ class Client(object): try: mkthread(func, data) except Exception as err: - self.log.exception( - 'Exception in Single-shot Callback!', err) + self.log.exception('Exception in Single-shot Callback!', + err) run.add(func) self.single_shots[spec].difference_update(run) @@ -294,7 +304,8 @@ class Client(object): return self._getDescribingModuleData(module)['parameters'][parameter] def _issueDescribe(self): - self.equipment_id, self.describing_data = self.communicate('describe') + _, self.equipment_id, self.describing_data = self._communicate( + 'describe') for module, moduleData in self.describing_data['modules'].items(): for parameter, parameterData in moduleData['parameters'].items(): @@ -303,17 +314,18 @@ class Client(object): [parameter]['validator'] = validator def register_callback(self, module, parameter, cb): - self.log.debug( - 'registering callback %r for %s:%s' % - (cb, module, parameter)) + self.log.debug('registering callback %r for %s:%s' % + (cb, module, parameter)) self.callbacks.setdefault('%s:%s' % (module, parameter), set()).add(cb) def unregister_callback(self, module, parameter, cb): - self.log.debug( - 'unregistering callback %r for %s:%s' % - (cb, module, parameter)) - self.callbacks.setdefault('%s:%s' % - (module, parameter), set()).discard(cb) + self.log.debug('unregistering callback %r for %s:%s' % + (cb, module, parameter)) + self.callbacks.setdefault('%s:%s' % (module, parameter), + set()).discard(cb) + + def register_shutdown_callback(self, func, arg): + self.connection.callbacks.append((func, arg)) def _get_reply_from_request(self, requesttype): # maps each (sync) request to the corresponding reply @@ -331,18 +343,24 @@ class Client(object): return REPLYMAP.get(requesttype, requesttype) def communicate(self, msgtype, spec='', data=None): + # only return the data portion.... + return self._communicate(msgtype, spec, data)[2] + + def _communicate(self, msgtype, spec='', data=None): self.log.debug('communicate: %r %r %r' % (msgtype, spec, data)) if self.stopflag: raise RuntimeError('alreading stopping!') if msgtype == "*IDN?": return self.secop_id - if msgtype not in ('*IDN?', 'describe', 'activate', - 'deactivate', 'do', 'change', 'read', 'ping', 'help'): - raise EXCEPTIONS['Protocol'](errorclass='Protocol', - errorinfo='%r: No Such Messagetype defined!' % - msgtype, - origin=self.encode_message(msgtype, spec, data)) + if msgtype not in ('*IDN?', 'describe', 'activate', 'deactivate', 'do', + 'change', 'read', 'ping', 'help'): + raise EXCEPTIONS['Protocol'](args=[ + self.encode_message(msgtype, spec, data), + dict( + errorclass='Protocol', + errorinfo='%r: No Such Messagetype defined!' % msgtype, ), + ]) # sanitize input + handle syntactic sugar msgtype = str(msgtype) @@ -356,7 +374,8 @@ class Client(object): rply = self._get_reply_from_request(msgtype) if (rply, spec) in self.expected_replies: raise RuntimeError( - "can not have more than one requests of the same type at the same time!") + "can not have more than one requests of the same type at the same time!" + ) # prepare sending request event = threading.Event() @@ -371,11 +390,14 @@ class Client(object): # wait for reply. timeout after 10s if event.wait(10): self.log.debug('checking reply') - event, is_error, result = self.expected_replies.pop((rply, spec)) + entry = self.expected_replies.pop((rply, spec)) + # entry is: event, is_error, exc_or_msgtype [,spec, date]<- if !err + is_error = entry[1] if is_error: - # if error, result contains the rigth Exception to raise - raise result - return result + # if error, entry[2] contains the rigth Exception to raise + raise entry[2] + # valid reply: entry[2:5] contain msgtype, spec, data + return tuple(entry[2:5]) # timed out del self.expected_replies[(rply, spec)] @@ -447,17 +469,21 @@ class Client(object): return self.describing_data['modules'][module]['parameters'].keys() def getModuleBaseClass(self, module): - return self.describing_data['modules'][module]['baseclass'] + return self.describing_data['modules'][module]['interfaceclass'] def getCommands(self, module): return self.describing_data['modules'][module]['commands'].keys() def getProperties(self, module, parameter): - return self.describing_data['modules'][ - module]['parameters'][parameter] + return self.describing_data['modules'][module]['parameters'][parameter] def syncCommunicate(self, *msg): - return self.communicate(*msg) + res = self._communicate(*msg) + try: + res = self.encode_message(*res) + except Exception: + res = str(res) + return res def ping(self, pingctr=[0]): pingctr[0] = pingctr[0] + 1 diff --git a/secop/devices/core.py b/secop/devices/core.py index 2af8a63..155df32 100644 --- a/secop/devices/core.py +++ b/secop/devices/core.py @@ -19,7 +19,6 @@ # Enrico Faulhaber # # ***************************************************************************** - """Define Baseclasses for real devices implemented in the server""" # XXX: connect with 'protocol'-Devices. @@ -47,9 +46,13 @@ EVENT_ONLY_ON_CHANGED_VALUES = False class PARAM(object): - - def __init__(self, description, validator=float, default=Ellipsis, - unit=None, readonly=True, export=True): + def __init__(self, + description, + validator=float, + default=Ellipsis, + unit=None, + readonly=True, + export=True): if isinstance(description, PARAM): # make a copy of a PARAM object self.__dict__.update(description.__dict__) @@ -70,19 +73,17 @@ class PARAM(object): def as_dict(self): # used for serialisation only - return dict(description=self.description, - unit=self.unit, - readonly=self.readonly, - value=self.value, - timestamp=format_time( - self.timestamp) if self.timestamp else None, - validator=validator_to_str(self.validator), - ) + return dict( + description=self.description, + unit=self.unit, + readonly=self.readonly, + value=self.value, + timestamp=format_time(self.timestamp) if self.timestamp else None, + validator=validator_to_str(self.validator), ) # storage for CMDs settings (description + call signature...) class CMD(object): - def __init__(self, description, arguments, result): # descriptive text for humans self.description = description @@ -97,17 +98,16 @@ class CMD(object): def as_dict(self): # used for serialisation only - return dict(description=self.description, - arguments=repr(self.arguments), - resulttype=repr(self.resulttype), - ) + return dict( + description=self.description, + arguments=repr(self.arguments), + resulttype=repr(self.resulttype), ) # Meta class # warning: MAGIC! class DeviceMeta(type): - def __new__(mcs, name, bases, attrs): newtype = type.__new__(mcs, name, bases, attrs) if '__constructed__' in attrs: @@ -140,6 +140,7 @@ class DeviceMeta(type): else: # return cached value return self.PARAMS[pname].value + if rfunc: wrapped_rfunc.__doc__ = rfunc.__doc__ setattr(newtype, 'read_' + pname, wrapped_rfunc) @@ -157,6 +158,7 @@ class DeviceMeta(type): # of self.PARAMS[pname]? setattr(self, pname, value) return value + if wfunc: wrapped_wfunc.__doc__ = wfunc.__doc__ setattr(newtype, 'write_' + pname, wrapped_wfunc) @@ -188,8 +190,8 @@ class DeviceMeta(type): if argspec[0] and argspec[0][0] == 'self': del argspec[0][0] newtype.CMDS[name[2:]] = CMD( - getattr(value, '__doc__'), - argspec.args, None) # XXX: find resulttype! + getattr(value, '__doc__'), argspec.args, + None) # XXX: find resulttype! attrs['__constructed__'] = True return newtype @@ -209,8 +211,9 @@ class Device(object): __metaclass__ = DeviceMeta # PARAMS and CMDS are auto-merged upon subclassing PARAMS = { - 'baseclass': PARAM('protocol defined interface class', - default="Device", validator=str), + "interfaceclass": PARAM("protocol defined interface class", + default="Device", + validator=str), } CMDS = {} DISPATCHER = None @@ -226,8 +229,10 @@ class Device(object): params[k] = PARAM(v) mycls = self.__class__ myclassname = '%s.%s' % (mycls.__module__, mycls.__name__) - params['class'] = PARAM('implementation specific class name', - default=myclassname, validator=str) + params['implementationclass'] = PARAM( + 'implementation specific class name', + default=myclassname, + validator=str) self.PARAMS = params # check config for problems @@ -243,8 +248,8 @@ class Device(object): if v.default is Ellipsis and k != 'value': # Ellipsis is the one single value you can not specify.... raise ConfigError('Device %s: Parameter %r has no default ' - 'value and was not given in config!' - % (self.name, k)) + 'value and was not given in config!' % + (self.name, k)) # assume default value was given cfgdict[k] = v.default @@ -261,8 +266,8 @@ class Device(object): try: v = validator(v) except (ValueError, TypeError) as e: - raise ConfigError('Device %s: config parameter %r:\n%r' - % (self.name, k, e)) + raise ConfigError('Device %s: config parameter %r:\n%r' % + (self.name, k, e)) setattr(self, k, v) self._requestLock = threading.RLock() @@ -281,10 +286,17 @@ class Readable(Device): providing the readonly parameter 'value' and 'status' """ PARAMS = { - 'baseclass': PARAM('protocol defined interface class', - default="Readable", validator=str), - 'value': PARAM('current value of the device', readonly=True, default=0.), - 'pollinterval': PARAM('sleeptime between polls', readonly=False, default=5, validator=floatrange(1, 120),), + 'interfaceclass': PARAM( + 'protocol defined interface class', + default="Readable", + validator=str), + 'value': PARAM( + 'current value of the device', readonly=True, default=0.), + 'pollinterval': PARAM( + 'sleeptime between polls', + readonly=False, + default=5, + validator=floatrange(0.1, 120), ), # 'status': PARAM('current status of the device', default=status.OK, # validator=enum(**{'idle': status.OK, # 'BUSY': status.BUSY, @@ -293,14 +305,19 @@ class Readable(Device): # 'ERROR': status.ERROR, # 'UNKNOWN': status.UNKNOWN}), # readonly=True), - 'status': PARAM('current status of the device', default=(status.OK, ''), - validator=vector(enum(**{'idle': status.OK, - 'BUSY': status.BUSY, - 'WARN': status.WARN, - 'UNSTABLE': status.UNSTABLE, - 'ERROR': status.ERROR, - 'UNKNOWN': status.UNKNOWN}), str), - readonly=True), + 'status': PARAM( + 'current status of the device', + default=(status.OK, ''), + validator=vector( + enum(**{ + 'idle': status.OK, + 'BUSY': status.BUSY, + 'WARN': status.WARN, + 'UNSTABLE': status.UNSTABLE, + 'ERROR': status.ERROR, + 'UNKNOWN': status.UNKNOWN + }), str), + readonly=True), } def init(self): @@ -310,6 +327,7 @@ class Readable(Device): self._pollthread.start() def _pollThread(self): + """super simple and super stupid per-module polling thread""" while True: time.sleep(self.pollinterval) for pname in self.PARAMS: @@ -325,11 +343,25 @@ class Driveable(Readable): providing a settable 'target' parameter to those of a Readable """ PARAMS = { - 'baseclass': PARAM('protocol defined interface class', - default="Driveable", validator=str), - 'target': PARAM('target value of the device', default=0., - readonly=False), + "interfaceclass": PARAM("protocol defined interface class", + default="Driveable", + validator=str, + ), + 'target': PARAM('target value of the device', + default=0., + readonly=False, + ), } + def doStart(self): + """normally does nothing, + + but there may be modules which _start_ the action here + """ + def doStop(self): + """Testing command implementation + + wait a second""" time.sleep(1) # for testing ! + diff --git a/secop/devices/cryo.py b/secop/devices/cryo.py index 48540bc..263d427 100644 --- a/secop/devices/cryo.py +++ b/secop/devices/cryo.py @@ -18,7 +18,6 @@ # Module authors: # Enrico Faulhaber # ***************************************************************************** - """playing implementation of a (simple) simulated cryostat""" from math import atan @@ -26,10 +25,10 @@ import time import random import threading -from secop.devices.core import Driveable, CONFIG, PARAM +from secop.devices.core import Driveable, CMD, PARAM from secop.protocol import status -from secop.validators import floatrange, positive, enum -from secop.lib import clamp +from secop.validators import floatrange, positive, enum, nonnegative, vector +from secop.lib import clamp, mkthread class Cryostat(Driveable): @@ -40,81 +39,112 @@ class Cryostat(Driveable): - thermal transfer between regulation and samplen """ PARAMS = dict( - jitter=CONFIG("amount of random noise on readout values", - validator=floatrange(0, 1), + jitter=PARAM("amount of random noise on readout values", + validator=floatrange(0, 1), unit="K", + default=0.1, readonly=False, export=False, + ), + T_start=PARAM("starting temperature for simulation", + validator=positive, default=10, export=False, + ), + looptime=PARAM("timestep for simulation", + validator=floatrange(0.01, 10), unit="s", default=1, + readonly=False, export=False, ), - T_start=CONFIG("starting temperature for simulation", - validator=positive, export=False, - ), - looptime=CONFIG("timestep for simulation", - validator=positive, default=1, unit="s", - export=False, - ), - ramp=PARAM("ramping speed in K/min", - validator=floatrange(0, 1e3), default=1, + ramp=PARAM("ramping speed of the setpoint", + validator=floatrange(0, 1e3), unit="K/min", default=1, + readonly=False, + ), + setpoint=PARAM("current setpoint during ramping else target", + validator=float, default=1, unit='K', + ), + maxpower=PARAM("Maximum heater power", + validator=nonnegative, default=1, unit="W", + readonly=False, + ), + heater=PARAM("current heater setting", + validator=floatrange(0, 100), default=0, unit="%", + ), + heaterpower=PARAM("current heater power", + validator=nonnegative, default=0, unit="W", + ), + target=PARAM("target temperature", + validator=nonnegative, default=0, unit="K", + readonly=False, + ), + value=PARAM("regulation temperature", + validator=nonnegative, default=0, unit="K", ), - 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", - ), + pid=PARAM("regulation coefficients", + validator=vector(nonnegative, floatrange(0, 100), floatrange(0, 100)), + default=(40, 10, 2), readonly=False, + # export=False, + ), + p=PARAM("regulation coefficient 'p'", + validator=nonnegative, default=40, unit="%/K", readonly=False, + export=False, + ), i=PARAM("regulation coefficient 'i'", - validator=floatrange(0, 100), default=10, - ), + validator=floatrange(0, 100), default=10, readonly=False, + export=False, + ), d=PARAM("regulation coefficient 'd'", - validator=floatrange(0, 100), default=2, - ), + validator=floatrange(0, 100), default=2, readonly=False, + export=False, + ), mode=PARAM("mode of regulation", - validator=enum('ramp', 'pid', 'openloop'), default='pid', - ), - + validator=enum('ramp', 'pid', 'openloop'), default='ramp', + readonly=False, + ), + pollinterval=PARAM("polling interval", + validator=positive, default=5, + export=False, + ), tolerance=PARAM("temperature range for stability checking", validator=floatrange(0, 100), default=0.1, unit='K', - ), + readonly=False, + ), window=PARAM("time window for stability checking", validator=floatrange(1, 900), default=30, unit='s', - ), + readonly=False, + export=False, + ), timeout=PARAM("max waiting time for stabilisation check", validator=floatrange(1, 36000), default=900, unit='s', - ), + readonly=False, + export=False, + ), + ) + CMDS = dict( + Stop=CMD("Stop ramping the setpoint\n\nby setting the current setpoint as new target", + [], None), ) def init(self): self._stopflag = False - self._thread = threading.Thread(target=self.thread) - self._thread.daemon = True - self._thread.start() + self._thread = mkthread(self.thread) - def read_status(self): + def read_status(self, maxage=0): # instead of asking a 'Hardware' take the value from the simulation 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()) + self.jitter * (0.5 - random.random()) def read_target(self, maxage=0): return self.target def write_target(self, value): + value = round(value, 2) + if value == self.target: + # nothing to do + return value self.target = value - # next request will see this status, until the loop updates it - self.status = (status.BUSY, 'new target set') + # next read_status will see this status, until the loop updates it + self.status = status.BUSY, 'new target set' + return value def read_maxpower(self, maxage=0): return self.maxpower @@ -124,6 +154,14 @@ class Cryostat(Driveable): heat = max(0, min(100, self.heater * self.maxpower / float(newpower))) self.heater = heat self.maxpower = newpower + return newpower + + def write_pid(self, newpid): + self.p, self.i, self.d = newpid + return (self.p, self.i, self.d) + + def read_pid(self, maxage=0): + return (self.p, self.i, self.d) def doStop(self): # stop the ramp by setting current setpoint as target @@ -137,7 +175,7 @@ class Cryostat(Driveable): """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 + 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""" @@ -147,8 +185,8 @@ class Cryostat(Driveable): """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) + cp = clamp( + self.__coolerCP(coolertemp) * self.__sampleCP(sampletemp), 1, 10) return clamp(flow, -cp, cp) def __sampleCP(self, temp): @@ -159,9 +197,9 @@ class Cryostat(Driveable): return 0.02 / temp def thread(self): - self.sampletemp = self.config_T_start - self.regulationtemp = self.config_T_start - self.status = status.OK + self.sampletemp = self.T_start + self.regulationtemp = self.T_start + self.status = status.OK, '' while not self._stopflag: try: self.__sim() @@ -176,6 +214,7 @@ class Cryostat(Driveable): # c) generating status+updated value+ramp # this thread is not supposed to exit! + self.setpoint = self.target # local state keeping: regulation = self.regulationtemp sample = self.sampletemp @@ -204,15 +243,14 @@ class Cryostat(Driveable): 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) + 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) + newregulation = max( + 0, regulation + regdelta / self.__coolerCP(regulation) * h) # b) see # http://brettbeauregard.com/blog/2011/04/ # improving-the-beginners-pid-introduction/ @@ -231,9 +269,9 @@ class Cryostat(Driveable): # use a simple filter to smooth delta a little delta = (delta + regulation - newregulation) / 2. - kp = self.p / 10. # LakeShore P = 10*k_p + 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 + kd = kp * abs(self.d) / 2. # LakeShore D = 2*T_d P = kp * error I += ki * error * h D = kd * delta / h @@ -249,7 +287,7 @@ class Cryostat(Driveable): 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 + v = ((damper**2 - 1) * self.heater + v) / damper**2 # damp oscillations due to D switching signs if D * lastD < -0.2: @@ -274,7 +312,7 @@ class Cryostat(Driveable): heater = self.heater last_heaters = (0, 0) - heater = round(heater, 3) + heater = round(heater, 1) sample = newsample regulation = newregulation lastmode = self.mode @@ -285,9 +323,8 @@ class Cryostat(Driveable): else: maxdelta = self.ramp / 60. * h try: - self.setpoint = round(self.setpoint + - clamp(self.target - self.setpoint, - -maxdelta, maxdelta), 3) + 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): @@ -319,6 +356,7 @@ class Cryostat(Driveable): self.heaterpower = round(heater * self.maxpower * 0.01, 3) self.heater = heater timestamp = t + self.read_value() def shutdown(self): # should be called from server when the server is stopped diff --git a/secop/devices/demo.py b/secop/devices/demo.py index ade76b0..bb1a184 100644 --- a/secop/devices/demo.py +++ b/secop/devices/demo.py @@ -18,7 +18,6 @@ # Module authors: # Enrico Faulhaber # ***************************************************************************** - """testing devices""" import time @@ -35,16 +34,20 @@ class Switch(Driveable): """ PARAMS = { 'value': PARAM('current state (on or off)', - validator=enum(on=1, off=0), default=0), + validator=enum(on=1, off=0), default=0, + ), 'target': PARAM('wanted state (on or off)', - validator=enum(on=1, off=0), - default=0, readonly=False), + validator=enum(on=1, off=0), default=0, + readonly=False, + ), 'switch_on_time': PARAM('seconds to wait after activating the switch', validator=floatrange(0, 60), unit='s', - default=10, export=False), + default=10, export=False, + ), 'switch_off_time': PARAM('cool-down time in seconds', validator=floatrange(0, 60), unit='s', - default=10, export=False), + default=10, export=False, + ), } def init(self): @@ -95,14 +98,24 @@ class MagneticField(Driveable): """a liquid magnet """ PARAMS = { - 'value': PARAM('current field in T', unit='T', - validator=floatrange(-15, 15), default=0), - 'ramp': PARAM('moving speed in T/min', unit='T/min', - validator=floatrange(0, 1), default=0.1, readonly=False), - 'mode': PARAM('what to do after changing field', default=0, - validator=enum(persistent=1, hold=0), readonly=False), - 'heatswitch': PARAM('heat switch device', - validator=str, export=False), + 'value': PARAM('current field in T', + unit='T', validator=floatrange(-15, 15), default=0, + ), + 'target': PARAM('target field in T', + unit='T', validator=floatrange(-15, 15), default=0, + readonly=False, + ), + 'ramp': PARAM('ramping speed', + unit='T/min', validator=floatrange(0, 1), default=0.1, + readonly=False, + ), + 'mode': PARAM('what to do after changing field', + default=1, validator=enum(persistent=1, hold=0), + readonly=False, + ), + 'heatswitch': PARAM('name of heat switch device', + validator=str, export=False, + ), } def init(self): @@ -122,8 +135,8 @@ class MagneticField(Driveable): # note: we may also return the read-back value from the hw here def read_status(self, maxage=0): - return (status.OK, '') if self._state == 'idle' else ( - status.BUSY, self._state) + return (status.OK, '') if self._state == 'idle' else (status.BUSY, + self._state) def _thread(self): loopdelay = 1 @@ -137,9 +150,8 @@ class MagneticField(Driveable): if self._state == 'switch_on': # wait until switch is on if self._heatswitch.read_value() == 'on': - self.log.debug( - 'heatswitch is on -> ramp to %.3f' % - self.target) + self.log.debug('heatswitch is on -> ramp to %.3f' % + self.target) self._state = 'ramp' if self._state == 'ramp': if self.target == self.value: @@ -170,10 +182,12 @@ class CoilTemp(Readable): """a coil temperature """ PARAMS = { - 'value': PARAM('Coil temperatur in K', unit='K', - validator=float, default=0), + 'value': PARAM('Coil temperatur', + unit='K', validator=float, default=0, + ), 'sensor': PARAM("Sensor number or calibration id", - validator=str, readonly=True), + validator=str, readonly=True, + ), } def read_value(self, maxage=0): @@ -184,12 +198,16 @@ class SampleTemp(Driveable): """a sample temperature """ PARAMS = { - 'value': PARAM('Sample temperatur in K', unit='K', - validator=float, default=10), + 'value': PARAM('Sample temperature', + unit='K', validator=float, default=10, + ), 'sensor': PARAM("Sensor number or calibration id", - validator=str, readonly=True), - 'ramp': PARAM('moving speed in K/min', validator=floatrange(0, 100), - unit='K/min', default=0.1, readonly=False), + validator=str, readonly=True, + ), + 'ramp': PARAM('moving speed in K/min', + validator=floatrange(0, 100), unit='K/min', default=0.1, + readonly=False, + ), } def init(self): @@ -225,13 +243,17 @@ class Label(Readable): """ PARAMS = { 'system': PARAM("Name of the magnet system", - validator=str, export=False), + validator=str, export=False, + ), 'subdev_mf': PARAM("name of subdevice for magnet status", - validator=str, export=False), + validator=str, export=False, + ), 'subdev_ts': PARAM("name of subdevice for sample temp", - validator=str, export=False), - 'value': PARAM("Value of out label string", - validator=str) + validator=str, export=False, + ), + 'value': PARAM("final value of label string", + validator=str, + ), } def read_value(self, maxage=0): @@ -250,10 +272,10 @@ class Label(Readable): mf_mode = dev_mf.mode mf_val = dev_mf.value mf_unit = dev_mf.PARAMS['value'].unit - if mf_stat == status.OK: + if mf_stat[0] == status.OK: state = 'Persistent' if mf_mode else 'Non-persistent' else: - state = 'ramping' + state = mf_stat[1] or 'ramping' strings.append('%s at %.1f %s' % (state, mf_val, mf_unit)) else: strings.append('No connection to magnetic field!') @@ -265,12 +287,21 @@ class ValidatorTest(Readable): """ """ PARAMS = { - 'oneof': PARAM('oneof', validator=oneof(int, 'X', 2.718), readonly=False, default=4.0), - 'enum': PARAM('enum', validator=enum('boo', 'faar', z=9), readonly=False, default=1), - 'vector': PARAM('vector of int, float and str', validator=vector(int, float, str), readonly=False, default=(1, 2.3, 'a')), - 'array': PARAM('array: 2..3 time oneof(0,1)', validator=array(oneof(2, 3), oneof(0, 1)), readonly=False, default=[1, 0, 1]), - 'nonnegative': PARAM('nonnegative', validator=nonnegative, readonly=False, default=0), - 'positive': PARAM('positive', validator=positive, readonly=False, default=1), - 'intrange': PARAM('intrange', validator=intrange(2, 9), readonly=False, default=4), - 'floatrange': PARAM('floatrange', validator=floatrange(-1, 1), readonly=False, default=0,), + 'oneof': PARAM('oneof', + validator=oneof(int, 'X', 2.718), readonly=False, default=4.0), + 'enum': PARAM('enum', + validator=enum('boo', 'faar', z=9), readonly=False, default=1), + 'vector': PARAM('vector of int, float and str', + validator=vector(int, float, str), readonly=False, default=(1, 2.3, 'a')), + 'array': PARAM('array: 2..3 times oneof(0,1)', + validator=array(oneof(2, 3), oneof(0, 1)), readonly=False, default=[1, 0, 1]), + 'nonnegative': PARAM('nonnegative', + validator=nonnegative, readonly=False, default=0), + 'positive': PARAM('positive', + validator=positive, readonly=False, default=1), + 'intrange': PARAM('intrange', + validator=intrange(2, 9), readonly=False, default=4), + 'floatrange': PARAM('floatrange', + validator=floatrange(-1, 1), readonly=False, default=0, + ), } diff --git a/secop/devices/test.py b/secop/devices/test.py index 4e5179c..c9f8561 100644 --- a/secop/devices/test.py +++ b/secop/devices/test.py @@ -18,7 +18,6 @@ # Module authors: # Enrico Faulhaber # ***************************************************************************** - """testing devices""" import random @@ -46,7 +45,8 @@ class Heater(Driveable): """ PARAMS = { 'maxheaterpower': PARAM('maximum allowed heater power', - validator=floatrange(0, 100), unit='W'), + validator=floatrange(0, 100), unit='W', + ), } def read_value(self, maxage=0): @@ -64,9 +64,11 @@ class Temp(Driveable): """ PARAMS = { 'sensor': PARAM("Sensor number or calibration id", - validator=str, readonly=True), - 'target': PARAM("Target temperature", default=300.0, - validator=positive, readonly=False, unit='K'), + validator=str, readonly=True, + ), + 'target': PARAM("Target temperature", + default=300.0, validator=positive, readonly=False, unit='K', + ), } def read_value(self, maxage=0): diff --git a/secop/gui/mainwindow.py b/secop/gui/mainwindow.py index 7e4a338..424075d 100644 --- a/secop/gui/mainwindow.py +++ b/secop/gui/mainwindow.py @@ -21,7 +21,7 @@ # # ***************************************************************************** -from PyQt4.QtGui import QMainWindow, QInputDialog, QTreeWidgetItem +from PyQt4.QtGui import QMainWindow, QInputDialog, QTreeWidgetItem, QMessageBox from PyQt4.QtCore import pyqtSignature as qtsig, QObject, pyqtSignal from secop.gui.util import loadUi @@ -30,6 +30,8 @@ from secop.gui.modulectrl import ModuleCtrl from secop.gui.paramview import ParameterView from secop.client.baseclient import Client as SECNode +import sys + ITEM_TYPE_NODE = QTreeWidgetItem.UserType + 1 ITEM_TYPE_MODULE = QTreeWidgetItem.UserType + 2 ITEM_TYPE_PARAMETER = QTreeWidgetItem.UserType + 3 @@ -61,21 +63,34 @@ class QSECNode(SECNode, QObject): class MainWindow(QMainWindow): - def __init__(self, parent=None): super(MainWindow, self).__init__(parent) loadUi(self, 'mainwindow.ui') + self.toolBar.hide() + self.lineEdit.hide() + self.splitter.setStretchFactor(0, 1) self.splitter.setStretchFactor(1, 70) + self.splitter.setSizes([50, 500]) self._nodes = {} self._nodeCtrls = {} + self._topItems = {} self._currentWidget = self.splitter.widget(1).layout().takeAt(0) - # add localhost if available - self._addNode('localhost') + # add localhost (if available) and SEC nodes given as arguments + args = sys.argv[1:] + if '-d' in args: + args.remove('-d') + if not args: + args = ['localhost'] + for host in args: + try: + self._addNode(host) + except Exception as e: + print e @qtsig('') def on_actionAdd_SEC_node_triggered(self): @@ -85,7 +100,11 @@ class MainWindow(QMainWindow): if not ok: return - self._addNode(host) + try: + self._addNode(host) + except Exception as e: + QMessageBox.critical(self.parent(), + 'Connecting to %s failed!' % host, str(e)) def on_treeWidget_currentItemChanged(self, current, previous): if current.type() == ITEM_TYPE_NODE: @@ -94,8 +113,18 @@ class MainWindow(QMainWindow): self._displayModule(current.parent().text(0), current.text(0)) elif current.type() == ITEM_TYPE_PARAMETER: self._displayParameter(current.parent().parent().text(0), - current.parent().text(0), - current.text(0)) + current.parent().text(0), current.text(0)) + + def _removeSubTree(self, toplevelItem): + #.... + pass + + def _nodeDisconnected_callback(self, host): + node = self._nodes[host] + topItem = self._topItems[node] + self._removeSubTree(topItem) + node.quit() + QMessageBox(self.parent(), repr(host)) def _addNode(self, host): @@ -109,6 +138,7 @@ class MainWindow(QMainWindow): host = '%s (%s)' % (node.equipment_id, host) self._nodes[host] = node + node.register_shutdown_callback(self._nodeDisconnected_callback, host) # fill tree nodeItem = QTreeWidgetItem(None, [host], ITEM_TYPE_NODE) @@ -120,6 +150,7 @@ class MainWindow(QMainWindow): ITEM_TYPE_PARAMETER) self.treeWidget.addTopLevelItem(nodeItem) + self._topItems[node] = nodeItem def _displayNode(self, node): @@ -135,10 +166,7 @@ class MainWindow(QMainWindow): def _displayParameter(self, node, module, parameter): self._replaceCtrlWidget( - ParameterView( - self._nodes[node], - module, - parameter)) + ParameterView(self._nodes[node], module, parameter)) def _replaceCtrlWidget(self, new): old = self.splitter.widget(1).layout().takeAt(0) diff --git a/secop/gui/modulectrl.py b/secop/gui/modulectrl.py index 3cc9c28..525d6db 100644 --- a/secop/gui/modulectrl.py +++ b/secop/gui/modulectrl.py @@ -21,7 +21,7 @@ # # ***************************************************************************** -from PyQt4.QtGui import QWidget, QLabel +from PyQt4.QtGui import QWidget, QLabel, QMessageBox from PyQt4.QtCore import pyqtSignature as qtsig, Qt, pyqtSignal from secop.gui.util import loadUi @@ -30,8 +30,12 @@ from secop.gui.util import loadUi class ParameterButtons(QWidget): setRequested = pyqtSignal(str, str, str) # module, parameter, target - def __init__(self, module, parameter, initval='', - readonly=True, parent=None): + def __init__(self, + module, + parameter, + initval='', + readonly=True, + parent=None): super(ParameterButtons, self).__init__(parent) loadUi(self, 'parambuttons.ui') @@ -42,6 +46,9 @@ class ParameterButtons(QWidget): if readonly: self.setPushButton.setEnabled(False) self.setLineEdit.setEnabled(False) + else: + self.setLineEdit.returnPressed.connect( + self.on_setPushButton_clicked) def on_setPushButton_clicked(self): self.setRequested.emit(self._module, self._parameter, @@ -49,12 +56,12 @@ class ParameterButtons(QWidget): class ModuleCtrl(QWidget): - def __init__(self, node, module, parent=None): super(ModuleCtrl, self).__init__(parent) loadUi(self, 'modulectrl.ui') self._node = node self._module = module + self._lastclick = None self._paramWidgets = {} # widget cache do avoid garbage collection @@ -71,7 +78,16 @@ class ModuleCtrl(QWidget): font.setBold(True) for param in sorted(self._node.getParameters(self._module)): - label = QLabel(param + ':') + labelstr = param + ':' + unit = self._node.getProperties(self._module, param).get('unit', + '') + descr = self._node.getProperties(self._module, + param).get('description', '') + + if unit: + labelstr = "%s (%s):" % (param, unit) + + label = QLabel(labelstr) label.setFont(font) props = self._node.getProperties(self._module, param) @@ -80,7 +96,11 @@ class ModuleCtrl(QWidget): initValues[param].value, props['readonly']) - buttons.setRequested.connect(self._node.setParameter) + # buttons.setRequested.connect(self._node.setParameter) + buttons.setRequested.connect(self._set_Button_pressed) + + if descr: + buttons.setToolTip(descr) self.paramGroupBox.layout().addWidget(label, row, 0) self.paramGroupBox.layout().addWidget(buttons, row, 1) @@ -89,6 +109,16 @@ class ModuleCtrl(QWidget): row += 1 + def _set_Button_pressed(self, module, parameter, target): + sig = (module, parameter, target) + if self._lastclick == sig: + return + self._lastclick = sig + try: + self._node.setParameter(module, parameter, target) + except Exception as e: + QMessageBox.warning(self.parent(), 'Operation failed', str(e)) + def _updateValue(self, module, parameter, value): if module != self._module: return diff --git a/secop/gui/nodectrl.py b/secop/gui/nodectrl.py index 529ef58..22c32fe 100644 --- a/secop/gui/nodectrl.py +++ b/secop/gui/nodectrl.py @@ -22,6 +22,7 @@ # ***************************************************************************** import pprint +import json from PyQt4.QtGui import QWidget, QTextCursor, QFont, QFontMetrics from PyQt4.QtCore import pyqtSignature as qtsig, Qt @@ -31,7 +32,6 @@ from secop.protocol.errors import SECOPError class NodeCtrl(QWidget): - def __init__(self, node, parent=None): super(NodeCtrl, self).__init__(parent) loadUi(self, 'nodectrl.ui') @@ -50,14 +50,26 @@ class NodeCtrl(QWidget): if not msg: return - self._addLogEntry('Request: ' - '%s:' % msg, raw=True) -# msg = msg.split(' ', 2) + self._addLogEntry( + 'Request: ' + '%s:' % msg, + raw=True) + # msg = msg.split(' ', 2) try: reply = self._node.syncCommunicate(*self._node.decode_message(msg)) - self._addLogEntry(reply, newline=True, pretty=True) + if msg == 'describe': + _, eid, stuff = self._node.decode_message(reply) + reply = '%s %s %s' % (_, eid, json.dumps( + stuff, indent=2, separators=(',', ':'), sort_keys=True)) + self._addLogEntry(reply, newline=True, pretty=False) + else: + self._addLogEntry(reply, newline=True, pretty=True) except SECOPError as e: - self._addLogEntry(e, newline=True, pretty=True, error=True) + self._addLogEntry( + 'error %s %s' % (e.name, json.dumps(e.args)), + newline=True, + pretty=True, + error=True) @qtsig('') def on_clearPushButton_clicked(self): @@ -70,19 +82,23 @@ class NodeCtrl(QWidget): self._addLogEntry('=========================') self._addLogEntry('', newline=True) - def _addLogEntry(self, msg, newline=False, - pretty=False, raw=False, error=False): - + def _addLogEntry(self, + msg, + newline=False, + pretty=False, + raw=False, + error=False): if pretty: msg = pprint.pformat(msg, width=self._getLogWidth()) + msg = msg[1:-1] if not raw: if error: msg = '
%s
' % Qt.escape( str(msg)).replace('\n', '
') else: - msg = '
%s
' % Qt.escape(str(msg) - ).replace('\n', '
') + msg = '
%s
' % Qt.escape(str(msg)).replace('\n', + '
') content = '' if self.logTextBrowser.toPlainText(): diff --git a/secop/gui/paramview.py b/secop/gui/paramview.py index 5a8b306..6db8df8 100644 --- a/secop/gui/paramview.py +++ b/secop/gui/paramview.py @@ -29,7 +29,6 @@ from secop.validators import validator_to_str class ParameterView(QWidget): - def __init__(self, node, module, parameter, parent=None): super(ParameterView, self).__init__(parent) loadUi(self, 'paramview.ui') @@ -51,8 +50,8 @@ class ParameterView(QWidget): font = self.font() font.setBold(True) - props = self._node._getDescribingParameterData( - self._module, self._parameter) + props = self._node._getDescribingParameterData(self._module, + self._parameter) for prop in sorted(props): label = QLabel(prop + ':') label.setFont(font) diff --git a/secop/gui/util.py b/secop/gui/util.py index 15015e2..1d86c9b 100644 --- a/secop/gui/util.py +++ b/secop/gui/util.py @@ -25,7 +25,6 @@ from os import path from PyQt4 import uic - uipath = path.dirname(__file__) diff --git a/secop/lib/__init__.py b/secop/lib/__init__.py index a5cbb89..40188f8 100644 --- a/secop/lib/__init__.py +++ b/secop/lib/__init__.py @@ -19,7 +19,6 @@ # Enrico Faulhaber # # ***************************************************************************** - """Define helpers""" import threading @@ -50,13 +49,16 @@ def get_class(spec): modname, classname = spec.rsplit('.', 1) import importlib module = importlib.import_module('secop.' + modname) -# module = __import__(spec) + # module = __import__(spec) return getattr(module, classname) def mkthread(func, *args, **kwds): - t = threading.Thread(name='%s:%s' % (func.__module__, func.__name__), - target=func, args=args, kwargs=kwds) + t = threading.Thread( + name='%s:%s' % (func.__module__, func.__name__), + target=func, + args=args, + kwargs=kwds) t.daemon = True t.start() return t diff --git a/secop/lib/parsing.py b/secop/lib/parsing.py index fb23c52..3b257cc 100644 --- a/secop/lib/parsing.py +++ b/secop/lib/parsing.py @@ -19,7 +19,6 @@ # Enrico Faulhaber # # ***************************************************************************** - """Define parsing helpers""" import re @@ -58,13 +57,13 @@ class LocalTimezone(tzinfo): return time.tzname[self._isdst(dt)] def _isdst(self, dt): - tt = (dt.year, dt.month, dt.day, - dt.hour, dt.minute, dt.second, + tt = (dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.weekday(), 0, 0) stamp = time.mktime(tt) tt = time.localtime(stamp) return tt.tm_isdst > 0 + LocalTimezone = LocalTimezone() @@ -81,7 +80,6 @@ def format_time(timestamp=None): class Timezone(tzinfo): - def __init__(self, offset, name='unknown timezone'): self.offset = offset self.name = name @@ -94,12 +92,13 @@ class Timezone(tzinfo): def dst(self, dt): return timedelta(0) + + datetime_re = re.compile( r'(?P\d{4})-(?P\d{1,2})-(?P\d{1,2})' r'[T ](?P\d{1,2}):(?P\d{1,2})' r'(?::(?P\d{1,2})(?:\.(?P\d{1,6})\d*)?)?' - r'(?PZ|[+-]\d{2}(?::?\d{2})?)?$' -) + r'(?PZ|[+-]\d{2}(?::?\d{2})?)?$') def _parse_isostring(isostring): @@ -125,9 +124,8 @@ def _parse_isostring(isostring): kw = {k: int(v) for k, v in kw.items() if v is not None} kw['tzinfo'] = _tzinfo return datetime(**kw) - raise ValueError( - "%s is not a valid ISO8601 string I can parse!" % - isostring) + raise ValueError("%s is not a valid ISO8601 string I can parse!" % + isostring) def parse_time(isostring): @@ -137,9 +135,9 @@ def parse_time(isostring): dt = _parse_isostring(isostring) return time.mktime(dt.timetuple()) + dt.microsecond * 1e-6 - # possibly unusable stuff below! + def format_args(args): if isinstance(args, list): return ','.join(format_args(arg) for arg in args).join('[]') @@ -168,7 +166,8 @@ class ArgsParser(object): DIGITS_CHARS = [c for c in '0123456789'] NAME_CHARS = [ - c for c in '_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'] + c for c in '_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' + ] NAME_CHARS2 = NAME_CHARS + DIGITS_CHARS def __init__(self, string=''): @@ -393,7 +392,7 @@ if __name__ == '__main__': print "time_formatting:", t = time.time() s = format_time(t) - assert(abs(t - parse_time(s)) < 1e-6) + assert (abs(t - parse_time(s)) < 1e-6) print "OK" print "ArgsParser:" @@ -408,6 +407,6 @@ if __name__ == '__main__': s = format_args(obj) p = a.parse(s) print p, - assert(parse_args(format_args(obj)) == obj) + assert (parse_args(format_args(obj)) == obj) print "OK" print "OK" diff --git a/secop/lib/pidfile.py b/secop/lib/pidfile.py index f5ebb6a..88267a1 100644 --- a/secop/lib/pidfile.py +++ b/secop/lib/pidfile.py @@ -19,7 +19,6 @@ # Enrico Faulhaber # # ***************************************************************************** - """Define pidfile helpers""" import os import atexit diff --git a/secop/loggers/__init__.py b/secop/loggers/__init__.py index 93f4905..bdbead1 100644 --- a/secop/loggers/__init__.py +++ b/secop/loggers/__init__.py @@ -20,7 +20,6 @@ # # ***************************************************************************** - import os import sys import time @@ -34,7 +33,6 @@ from logging import Logger, Formatter, Handler, DEBUG, INFO, WARNING, ERROR, \ from . import colors - LOGFMT = '%(asctime)s : %(levelname)-7s : %(name)-15s: %(message)s' DATEFMT = '%H:%M:%S' DATESTAMP_FMT = '%Y-%m-%d' @@ -43,7 +41,6 @@ SECONDS_PER_DAY = 60 * 60 * 24 LOGLEVELS = {'debug': DEBUG, 'info': INFO, 'warning': WARNING, 'error': ERROR} INVLOGLEVELS = {value: key for key, value in LOGLEVELS.items()} - log = None @@ -142,16 +139,16 @@ class ConsoleFormatter(Formatter): elif levelno <= INFO: fmtstr = '%s%%(message)s' % namefmt elif levelno <= WARNING: - fmtstr = self.colorize('fuchsia', '%s%%(levelname)s: %%(message)s' - % namefmt) + fmtstr = self.colorize('fuchsia', + '%s%%(levelname)s: %%(message)s' % namefmt) else: # Add exception type to error (if caused by exception) msgPrefix = '' if record.exc_info: msgPrefix = '%s: ' % record.exc_info[0].__name__ - fmtstr = self.colorize('red', '%s%%(levelname)s: %s%%(message)s' - % (namefmt, msgPrefix)) + fmtstr = self.colorize('red', '%s%%(levelname)s: %s%%(message)s' % + (namefmt, msgPrefix)) fmtstr = datefmt + fmtstr if not getattr(record, 'nonl', False): fmtstr += '\n' @@ -209,8 +206,8 @@ class LogfileFormatter(Formatter): if self.extended_traceback: s = format_extended_traceback(*ei) else: - s = ''.join(traceback.format_exception(ei[0], ei[1], ei[2], - sys.maxsize)) + s = ''.join( + traceback.format_exception(ei[0], ei[1], ei[2], sys.maxsize)) if s.endswith('\n'): s = s[:-1] return s @@ -268,8 +265,8 @@ class LogfileHandler(StreamHandler): StreamHandler.__init__(self) # determine time of first midnight from now on t = time.localtime() - self.rollover_at = time.mktime((t[0], t[1], t[2], 0, 0, 0, - t[6], t[7], t[8])) + SECONDS_PER_DAY + self.rollover_at = time.mktime( + (t[0], t[1], t[2], 0, 0, 0, t[6], t[7], t[8])) + SECONDS_PER_DAY self.setFormatter(LogfileFormatter(LOGFMT, DATEFMT)) self.disabled = False @@ -338,8 +335,9 @@ class ColoredConsoleHandler(StreamHandler): def __init__(self): StreamHandler.__init__(self, sys.stdout) - self.setFormatter(ConsoleFormatter(datefmt=DATEFMT, - colorize=colors.colorize)) + self.setFormatter( + ConsoleFormatter( + datefmt=DATEFMT, colorize=colors.colorize)) def emit(self, record): msg = self.format(record) diff --git a/secop/loggers/colors.py b/secop/loggers/colors.py index 5f1955c..03cad23 100644 --- a/secop/loggers/colors.py +++ b/secop/loggers/colors.py @@ -20,10 +20,8 @@ # Georg Brandl # # ***************************************************************************** - """Console coloring handlers.""" - _codes = {} _attrs = { diff --git a/secop/paths.py b/secop/paths.py index 4c4f3aa..22de09c 100644 --- a/secop/paths.py +++ b/secop/paths.py @@ -19,13 +19,11 @@ # Enrico Faulhaber # # ***************************************************************************** - """Pathes. how to find what and where...""" import sys from os import path - basepath = path.abspath(path.join(sys.path[0], '..')) etc_path = path.join(basepath, 'etc') pid_path = path.join(basepath, 'pid') diff --git a/secop/protocol/device.py b/secop/protocol/device.py index d7a9b80..3173827 100644 --- a/secop/protocol/device.py +++ b/secop/protocol/device.py @@ -19,7 +19,6 @@ # Enrico Faulhaber # # ***************************************************************************** - """Define SECoP Device classes """ diff --git a/secop/protocol/dispatcher.py b/secop/protocol/dispatcher.py index f12cfa9..9dfd8dc 100644 --- a/secop/protocol/dispatcher.py +++ b/secop/protocol/dispatcher.py @@ -19,7 +19,6 @@ # Enrico Faulhaber # # ***************************************************************************** - """Dispatcher for SECoP Messages Interface to the service offering part: @@ -47,7 +46,6 @@ from secop.lib.parsing import format_time class Dispatcher(object): - def __init__(self, logger, options): self.equipment_id = options.pop('equipment_id') self.log = logger @@ -85,15 +83,18 @@ class Dispatcher(object): reply = handler(conn, msg) except SECOPError as err: self.log.exception(err) - reply = msg.get_error(errorclass=err.__class__.__name__, - errorinfo=[repr(err), str(msg)]) + reply = msg.get_error( + errorclass=err.__class__.__name__, + errorinfo=[repr(err), str(msg)]) except (ValueError, TypeError) as err: - reply = msg.get_error(errorclass='BadValue', - errorinfo=[repr(err), str(msg)]) + reply = msg.get_error( + errorclass='BadValue', + errorinfo=[repr(err), str(msg)]) except Exception as err: self.log.exception(err) - reply = msg.get_error(errorclass='InternalError', - errorinfo=[repr(err), str(msg)]) + reply = msg.get_error( + errorclass='InternalError', + errorinfo=[repr(err), str(msg)]) else: self.log.debug('Can not handle msg %r' % msg) reply = self.unhandled(conn, msg) @@ -106,8 +107,8 @@ class Dispatcher(object): listeners = self._connections else: if getattr(msg, 'command', None) is None: - eventname = '%s:%s' % ( - msg.module, msg.parameter if msg.parameter else 'value') + eventname = '%s:%s' % (msg.module, msg.parameter + if msg.parameter else 'value') else: eventname = '%s:%s()' % (msg.module, msg.command) listeners = self._subscriptions.get(eventname, []) @@ -198,8 +199,7 @@ class Dispatcher(object): res = {} for cmdname, cmdobj in self.get_module(modulename).CMDS.items(): res[cmdname] = cmdobj.as_dict() - self.log.debug('list cmds for module %s -> %r' % - (modulename, res)) + self.log.debug('list cmds for module %s -> %r' % (modulename, res)) return res self.log.debug('-> module is not to be exported!') return {} @@ -210,12 +210,13 @@ class Dispatcher(object): for modulename in self._export: module = self.get_module(modulename) # some of these need rework ! - dd = {'class': module.__class__.__name__, - 'bases': [b.__name__ for b in module.__class__.__bases__], - 'parameters': self.list_module_params(modulename), - 'commands': self.list_module_cmds(modulename), - 'baseclass': 'Readable', - } + dd = { + 'class': module.__class__.__name__, + 'bases': [b.__name__ for b in module.__class__.__bases__], + 'parameters': self.list_module_params(modulename), + 'commands': self.list_module_cmds(modulename), + 'interfaceclass': 'Readable', + } result['modules'][modulename] = dd result['equipment_id'] = self.equipment_id result['firmware'] = 'The SECoP playground' @@ -244,8 +245,11 @@ class Dispatcher(object): # note: exceptions are handled in handle_request, not here! func = getattr(moduleobj, 'do' + command) res = func(*arguments) - res = CommandReply(module=modulename, command=command, - result=res, qualifiers=dict(t=time.time())) + res = CommandReply( + module=modulename, + command=command, + result=res, + qualifiers=dict(t=time.time())) # res = Value(modulename, command=command, value=func(*arguments), t=time.time()) return res @@ -268,15 +272,11 @@ class Dispatcher(object): setattr(moduleobj, pname, value) if pobj.timestamp: return WriteReply( - module=modulename, parameter=pname, value=[ - pobj.value, dict( - t=format_time(pobj.timestamp))]) + module=modulename, + parameter=pname, + value=[pobj.value, dict(t=format_time(pobj.timestamp))]) return WriteReply( - module=modulename, - parameter=pname, - value=[ - pobj.value, - {}]) + module=modulename, parameter=pname, value=[pobj.value, {}]) def _getParamValue(self, modulename, pname): moduleobj = self.get_module(modulename) @@ -370,9 +370,12 @@ class Dispatcher(object): except SECOPError as e: self.log.error('decide what to do here!') self.log.exception(e) - res = Value(module=modulename, parameter=pname, - value=pobj.value, t=pobj.timestamp, - unit=pobj.unit) + res = Value( + module=modulename, + parameter=pname, + value=pobj.value, + t=pobj.timestamp, + unit=pobj.unit) if res.value != Ellipsis: # means we do not have a value at all so skip this self.broadcast_event(res) conn.queue_async_reply(ActivateReply(**msg.as_dict())) @@ -392,5 +395,5 @@ class Dispatcher(object): (no handle_ method was defined) """ self.log.error('IGN: got unhandled request %s' % msg) - return msg.get_error(errorclass="InternalError", - errorinfo="Unhandled Request") + return msg.get_error( + errorclass="InternalError", errorinfo="Unhandled Request") diff --git a/secop/protocol/encoding/__init__.py b/secop/protocol/encoding/__init__.py index def1536..5c1ee47 100644 --- a/secop/protocol/encoding/__init__.py +++ b/secop/protocol/encoding/__init__.py @@ -19,7 +19,6 @@ # Enrico Faulhaber # # ***************************************************************************** - """Encoding/decoding Messages""" # implement as class as they may need some internal 'state' later on @@ -39,6 +38,7 @@ class MessageEncoder(object): """decodes the given frame to a message object""" raise NotImplemented + from demo_v2 import DemoEncoder as DemoEncoderV2 from demo_v3 import DemoEncoder as DemoEncoderV3 from demo_v4 import DemoEncoder as DemoEncoderV4 diff --git a/secop/protocol/encoding/demo_v2.py b/secop/protocol/encoding/demo_v2.py index 7839e6a..b7d7af1 100644 --- a/secop/protocol/encoding/demo_v2.py +++ b/secop/protocol/encoding/demo_v2.py @@ -19,7 +19,6 @@ # Enrico Faulhaber # # ***************************************************************************** - """Encoding/decoding Messages""" # implement as class as they may need some internal 'state' later on @@ -32,11 +31,11 @@ from secop.lib.parsing import * import re DEMO_RE = re.compile( - r'^([!+-])?(\*|[a-z_][a-z_0-9]*)?(?:\:(\*|[a-z_][a-z_0-9]*))?(?:\:(\*|[a-z_][a-z_0-9]*))?(?:\=(.*))?') + r'^([!+-])?(\*|[a-z_][a-z_0-9]*)?(?:\:(\*|[a-z_][a-z_0-9]*))?(?:\:(\*|[a-z_][a-z_0-9]*))?(?:\=(.*))?' +) class DemoEncoder(MessageEncoder): - def decode(sef, encoded): # match [!][*|devicename][: *|paramname [: *|propname]] [=value] match = DEMO_RE.match(encoded) @@ -46,8 +45,8 @@ class DemoEncoder(MessageEncoder): print "parsing", assign, assign = parse_args(assign) print "->", assign - return messages.DemoRequest( - novalue, devname, pname, propname, assign) + return messages.DemoRequest(novalue, devname, pname, propname, + assign) return messages.HelpRequest() def encode(self, msg): @@ -66,8 +65,13 @@ class DemoEncoder(MessageEncoder): return '~InternalError~' return result - def _encode_AsyncDataUnit(self, devname, pname, value, timestamp, - error=None, unit=''): + def _encode_AsyncDataUnit(self, + devname, + pname, + value, + timestamp, + error=None, + unit=''): return '#%s:%s=%s;t=%.3f' % (devname, pname, value, timestamp) def _encode_Error(self, error): @@ -98,5 +102,7 @@ class DemoEncoder(MessageEncoder): return '~InvalidValueForParamError~ %s:%s=%r' % (device, param, value) def _encode_HelpReply(self): - return ['Help not yet implemented!', - 'ask Markus Zolliker about the protocol'] + return [ + 'Help not yet implemented!', + 'ask Markus Zolliker about the protocol' + ] diff --git a/secop/protocol/encoding/demo_v3.py b/secop/protocol/encoding/demo_v3.py index b8e8daf..5e373e2 100644 --- a/secop/protocol/encoding/demo_v3.py +++ b/secop/protocol/encoding/demo_v3.py @@ -19,7 +19,6 @@ # Enrico Faulhaber # # ***************************************************************************** - """Encoding/decoding Messages""" # implement as class as they may need some internal 'state' later on @@ -93,7 +92,6 @@ DEMO_RE_OTHER = re.compile( class DemoEncoder(MessageEncoder): - def __init__(self, *args, **kwds): MessageEncoder.__init__(self, *args, **kwds) self.result = [] # for decoding @@ -112,8 +110,8 @@ class DemoEncoder(MessageEncoder): r.append("help ... more to come") return '\n'.join(r) - if isinstance(msg, (ListMessage, SubscribeMessage, - UnsubscribeMessage, TriggerMessage)): + if isinstance(msg, (ListMessage, SubscribeMessage, UnsubscribeMessage, + TriggerMessage)): msgtype = msg.MSGTYPE if msg.result: if msg.devs: @@ -145,9 +143,7 @@ class DemoEncoder(MessageEncoder): # encode 1..N replies result.append( encode_value( - val, - 'write', - targetvalue=msg.target)) + val, 'write', targetvalue=msg.target)) if not msg.result: # encode a request (no results -> reply, else an error would # have been sent) @@ -164,28 +160,17 @@ class DemoEncoder(MessageEncoder): encode_value( val, 'command', - cmd='%s(%s)' % - (msg.cmd, - ','.join( - msg.args)))) + cmd='%s(%s)' % (msg.cmd, ','.join(msg.args)))) if not msg.result: # encode a request (no results -> reply, else an error would # have been sent) - result.append( - '%s:%s(%s)' % - (devspec( - msg, 'command'), msg.cmd, ','.join( - msg.args))) + result.append('%s:%s(%s)' % (devspec(msg, 'command'), msg.cmd, + ','.join(msg.args))) return '\n'.join(result) if isinstance(msg, ErrorMessage): - return ( - '%s %s' % - (devspec( - msg, - 'error %s' % - msg.errortype), - msg.errorstring)).strip() + return ('%s %s' % (devspec(msg, 'error %s' % msg.errortype), + msg.errorstring)).strip() return 'Can not handle object %r!' % msg @@ -246,6 +231,7 @@ class DemoEncoder(MessageEncoder): if sep in stuff: return stuff.split(sep) return [stuff] + devs = helper(mgroups.pop('devs')) pars = helper(mgroups.pop('pars')) props = helper(mgroups.pop('props')) @@ -272,12 +258,8 @@ class DemoEncoder(MessageEncoder): # reformat qualifiers print mgroups quals = dict( - qual.split( - '=', - 1) for qual in helper( - mgroups.pop( - 'qualifiers', - ';'))) + qual.split('=', 1) + for qual in helper(mgroups.pop('qualifiers', ';'))) # reformat value result = [] @@ -296,48 +278,49 @@ class DemoEncoder(MessageEncoder): # construct messageobj if msgtype in MESSAGE: - return MESSAGE[msgtype]( - devs=devs, - pars=pars, - props=props, - result=result, - **mgroups) + return MESSAGE[msgtype](devs=devs, + pars=pars, + props=props, + result=result, + **mgroups) - return ErrorMessage(errortype="SyntaxError", - errorstring="Can't handle %r" % encoded) + return ErrorMessage( + errortype="SyntaxError", errorstring="Can't handle %r" % encoded) def tests(self): - testmsg = ['list', - 'list *', - 'list device', - 'list device:param1,param2', - 'list *:*', - 'list *=ts,tcoil,mf,lhe,ln2;', - 'read blub=12;t=3', - 'command ts:stop()', - 'command mf:quench(1,"now")', - 'error GibbetNich query x:y:z=9 "blubub blah"', - '#3', - 'read blub:a=12;t=3', - 'read blub:b=13;t=3.1', - 'read blub:c=14;t=3.3', - ] + testmsg = [ + 'list', + 'list *', + 'list device', + 'list device:param1,param2', + 'list *:*', + 'list *=ts,tcoil,mf,lhe,ln2;', + 'read blub=12;t=3', + 'command ts:stop()', + 'command mf:quench(1,"now")', + 'error GibbetNich query x:y:z=9 "blubub blah"', + '#3', + 'read blub:a=12;t=3', + 'read blub:b=13;t=3.1', + 'read blub:c=14;t=3.3', + ] for m in testmsg: print repr(m) print self.decode(m) print -DEMO_RE_MZ = re.compile(r"""^(?P[a-z]+)? # request type word (read/write/list/...) +DEMO_RE_MZ = re.compile( + r"""^(?P[a-z]+)? # request type word (read/write/list/...) \ ? # optional space (?P[a-z][a-z0-9_]*)? # optional devicename (?:\:(?P[a-z0-9_]*) # optional ':'+paramname (?:\:(?P[a-z0-9_]*))?)? # optinal ':' + propname - (?:(?P[=\?])(?P[^;]+)(?:\;(?P.*))?)?$""", re.X) + (?:(?P[=\?])(?P[^;]+)(?:\;(?P.*))?)?$""", + re.X) class DemoEncoder_MZ(MessageEncoder): - def decode(sef, encoded): m = DEMO_RE_MZ.match(encoded) if m: diff --git a/secop/protocol/encoding/demo_v4.py b/secop/protocol/encoding/demo_v4.py index 4034f74..5f38772 100644 --- a/secop/protocol/encoding/demo_v4.py +++ b/secop/protocol/encoding/demo_v4.py @@ -19,7 +19,6 @@ # Enrico Faulhaber # # ***************************************************************************** - """Encoding/decoding Messages""" # implement as class as they may need some internal 'state' later on @@ -47,7 +46,8 @@ DEMO_RE = re.compile( IDENTREQUEST = '*IDN?' # literal # literal! first part is fixed! #IDENTREPLY = 'SECoP, SECoPTCP, V2016-11-30, rc1' -IDENTREPLY = 'SINE2020&ISSE,SECoP,V2016-11-30,rc1' +#IDENTREPLY = 'SINE2020&ISSE,SECoP,V2016-11-30,rc1' +IDENTREPLY = 'SINE2020&ISSE,SECoP,V2017-01-25,rc1' DESCRIPTIONSREQUEST = 'describe' # literal DESCRIPTIONREPLY = 'describing' # + +json ENABLEEVENTSREQUEST = 'activate' # literal @@ -69,10 +69,22 @@ HEARTBEATREPLY = 'pong' # +nonce_without_space ERRORREPLY = 'error' # +errorclass +json_extended_info HELPREQUEST = 'help' # literal HELPREPLY = 'helping' # +line number +json_text -ERRORCLASSES = ['NoSuchDevice', 'NoSuchParameter', 'NoSuchCommand', - 'CommandFailed', 'ReadOnly', 'BadValue', 'CommunicationFailed', - 'IsBusy', 'IsError', 'ProtocolError', 'InternalError', - 'CommandRunning', 'Disabled', ] +ERRORCLASSES = [ + 'NoSuchDevice', + 'NoSuchParameter', + 'NoSuchCommand', + 'CommandFailed', + 'ReadOnly', + 'BadValue', + 'CommunicationFailed', + 'IsBusy', + 'IsError', + 'ProtocolError', + 'InternalError', + 'CommandRunning', + 'Disabled', +] + # note: above strings need to be unique in the sense, that none is/or # starts with another @@ -93,33 +105,61 @@ def encode_value_data(vobj): def encode_error_msg(emsg): # note: result is JSON-ified.... - return [emsg.origin, dict((k, getattr(emsg, k)) - for k in emsg.ARGS if k != 'origin')] + return [ + emsg.origin, dict((k, getattr(emsg, k)) for k in emsg.ARGS + if k != 'origin') + ] class DemoEncoder(MessageEncoder): # map of msg to msgtype string as defined above. ENCODEMAP = { - IdentifyRequest: (IDENTREQUEST,), - IdentifyReply: (IDENTREPLY,), - DescribeRequest: (DESCRIPTIONSREQUEST,), - DescribeReply: (DESCRIPTIONREPLY, 'equipment_id', 'description',), - ActivateRequest: (ENABLEEVENTSREQUEST,), - ActivateReply: (ENABLEEVENTSREPLY,), - DeactivateRequest: (DISABLEEVENTSREQUEST,), - DeactivateReply: (DISABLEEVENTSREPLY,), - CommandRequest: (COMMANDREQUEST, lambda msg: "%s:%s" % (msg.module, msg.command), 'arguments',), - CommandReply: (COMMANDREPLY, lambda msg: "%s:%s" % (msg.module, msg.command), encode_cmd_result,), - WriteRequest: (WRITEREQUEST, lambda msg: "%s:%s" % (msg.module, msg.parameter) if msg.parameter else msg.module, 'value',), - WriteReply: (WRITEREPLY, lambda msg: "%s:%s" % (msg.module, msg.parameter) if msg.parameter else msg.module, 'value', ), - PollRequest: (TRIGGERREQUEST, lambda msg: "%s:%s" % (msg.module, msg.parameter) if msg.parameter else msg.module, ), - HeartbeatRequest: (HEARTBEATREQUEST, 'nonce',), - HeartbeatReply: (HEARTBEATREPLY, 'nonce',), + IdentifyRequest: (IDENTREQUEST, ), + IdentifyReply: (IDENTREPLY, ), + DescribeRequest: (DESCRIPTIONSREQUEST, ), + DescribeReply: ( + DESCRIPTIONREPLY, + 'equipment_id', + 'description', ), + ActivateRequest: (ENABLEEVENTSREQUEST, ), + ActivateReply: (ENABLEEVENTSREPLY, ), + DeactivateRequest: (DISABLEEVENTSREQUEST, ), + DeactivateReply: (DISABLEEVENTSREPLY, ), + CommandRequest: ( + COMMANDREQUEST, + lambda msg: "%s:%s" % (msg.module, msg.command), + 'arguments', ), + CommandReply: ( + COMMANDREPLY, + lambda msg: "%s:%s" % (msg.module, msg.command), + encode_cmd_result, ), + WriteRequest: ( + WRITEREQUEST, + lambda msg: "%s:%s" % (msg.module, msg.parameter) if msg.parameter else msg.module, + 'value', ), + WriteReply: ( + WRITEREPLY, + lambda msg: "%s:%s" % (msg.module, msg.parameter) if msg.parameter else msg.module, + 'value', ), + PollRequest: ( + TRIGGERREQUEST, + lambda msg: "%s:%s" % (msg.module, msg.parameter) if msg.parameter else msg.module, + ), + HeartbeatRequest: ( + HEARTBEATREQUEST, + 'nonce', ), + HeartbeatReply: ( + HEARTBEATREPLY, + 'nonce', ), HelpMessage: (HELPREQUEST, ), - ErrorMessage: (ERRORREPLY, "errorclass", encode_error_msg,), - Value: (EVENT, lambda msg: "%s:%s" % (msg.module, msg.parameter or (msg.command + '()')) - if msg.parameter or msg.command else msg.module, - encode_value_data,), + ErrorMessage: ( + ERRORREPLY, + "errorclass", + encode_error_msg, ), + Value: ( + EVENT, + lambda msg: "%s:%s" % (msg.module, msg.parameter or (msg.command + '()')) if msg.parameter or msg.command else msg.module, + encode_value_data, ), } DECODEMAP = { IDENTREQUEST: lambda spec, data: IdentifyRequest(), @@ -177,8 +217,10 @@ class DemoEncoder(MessageEncoder): for msgcls, parts in self.ENCODEMAP.items(): if isinstance(msg, msgcls): # resolve lambdas - parts = [parts[0]] + [p(msg) if callable(p) - else getattr(msg, p) for p in parts[1:]] + parts = [parts[0]] + [ + p(msg) if callable(p) else getattr(msg, p) + for p in parts[1:] + ] if len(parts) > 1: parts[1] = str(parts[1]) if len(parts) == 3: @@ -199,10 +241,11 @@ class DemoEncoder(MessageEncoder): # is_request=True) msgtype, msgspec, data = match.groups() if msgspec is None and data: - return ErrorMessage(errorclass='Internal', - errorinfo='Regex matched json, but not spec!', - is_request=True, - origin=encoded) + return ErrorMessage( + errorclass='Internal', + errorinfo='Regex matched json, but not spec!', + is_request=True, + origin=encoded) if msgtype in self.DECODEMAP: if msgspec and ':' in msgspec: @@ -213,16 +256,16 @@ class DemoEncoder(MessageEncoder): try: data = json.loads(data) except ValueError as err: - return ErrorMessage(errorclass='BadValue', - errorinfo=[repr(err), str(encoded)], - origin=encoded) + return ErrorMessage( + errorclass='BadValue', + errorinfo=[repr(err), str(encoded)], + origin=encoded) msg = self.DECODEMAP[msgtype](msgspec, data) msg.setvalue("origin", encoded) return msg return ErrorMessage( errorclass='Protocol', - errorinfo='%r: No Such Messagetype defined!' % - encoded, + errorinfo='%r: No Such Messagetype defined!' % encoded, is_request=True, origin=encoded) diff --git a/secop/protocol/encoding/pickle.py b/secop/protocol/encoding/pickle.py index 1d87e1d..9c66e27 100644 --- a/secop/protocol/encoding/pickle.py +++ b/secop/protocol/encoding/pickle.py @@ -19,7 +19,6 @@ # Enrico Faulhaber # # ***************************************************************************** - """Encoding/decoding Messages""" # implement as class as they may need some internal 'state' later on @@ -36,7 +35,6 @@ except ImportError: class PickleEncoder(MessageEncoder): - def encode(self, messageobj): """msg object -> transport layer message""" return pickle.dumps(messageobj) diff --git a/secop/protocol/encoding/simplecomm.py b/secop/protocol/encoding/simplecomm.py index d4ddcf6..51ddbe2 100644 --- a/secop/protocol/encoding/simplecomm.py +++ b/secop/protocol/encoding/simplecomm.py @@ -19,7 +19,6 @@ # Enrico Faulhaber # # ***************************************************************************** - """Encoding/decoding Messages""" # implement as class as they may need some internal 'state' later on @@ -32,13 +31,12 @@ from secop.lib.parsing import * import re import ast - SCPMESSAGE = re.compile( - r'^(?:(?P[0-9@])\ )?(?P[a-zA-Z0-9_\*]*)(?:/(?P[a-zA-Z0-9_\*]*))+(?P[-+=\?\ ])?(?P.*)') + r'^(?:(?P[0-9@])\ )?(?P[a-zA-Z0-9_\*]*)(?:/(?P[a-zA-Z0-9_\*]*))+(?P[-+=\?\ ])?(?P.*)' +) class SCPEncoder(MessageEncoder): - def encode(self, msg): """msg object -> transport layer message""" # fun for Humans @@ -48,20 +46,24 @@ class SCPEncoder(MessageEncoder): r.append("'/version?' to query the current version") r.append("'/modules?' to query the list of modules") r.append( - "'/parameters?' to query the list of params of a module") + "'/parameters?' to query the list of params of a module" + ) r.append("'/value?' to query the value of a module") r.append("'/status?' to query the status of a module") r.append("'/target=' to move a module") - r.append("replies copy the request and are prefixed with an errorcode:") r.append( - "0=OK,3=NoSuchCommand,4=NosuchDevice,5=NoSuchParam,6=SyntaxError,7=BadValue,8=Readonly,9=Forbidden,@=Async") + "replies copy the request and are prefixed with an errorcode:") + r.append( + "0=OK,3=NoSuchCommand,4=NosuchDevice,5=NoSuchParam,6=SyntaxError,7=BadValue,8=Readonly,9=Forbidden,@=Async" + ) r.append("extensions: @-prefix as error-code,") r.append("'/+' subscribe all params of module") r.append("'/+' subscribe a param of a module") r.append("use '-' instead of '+' to unsubscribe") r.append("'/commands?' list of commands") r.append( - "'/@[possible args] execute command (ex. 'stop@')") + "'/@[possible args] execute command (ex. 'stop@')" + ) return '\n'.join(r) return { @@ -117,7 +119,7 @@ class SCPEncoder(MessageEncoder): def decode(self, encoded): """transport layer message -> msg object""" match = SCPMESSAGE.match(encoded) - if not(match): + if not (match): return HelpRequest() err, dev, par, op, val = match.groups() if val is not None: diff --git a/secop/protocol/encoding/text.py b/secop/protocol/encoding/text.py index db81222..c5e9d4a 100644 --- a/secop/protocol/encoding/text.py +++ b/secop/protocol/encoding/text.py @@ -19,7 +19,6 @@ # Enrico Faulhaber # # ***************************************************************************** - """Encoding/decoding Messages""" # implement as class as they may need some internal 'state' later on @@ -31,7 +30,6 @@ from secop.lib.parsing import * class TextEncoder(MessageEncoder): - def __init__(self): # build safe namespace ns = dict() diff --git a/secop/protocol/errors.py b/secop/protocol/errors.py index d0df948..2d4accc 100644 --- a/secop/protocol/errors.py +++ b/secop/protocol/errors.py @@ -19,12 +19,10 @@ # Enrico Faulhaber # # ***************************************************************************** - """Define (internal) SECoP Errors""" class SECOPError(RuntimeError): - def __init__(self, *args, **kwds): self.args = args for k, v in kwds.items(): @@ -91,8 +89,7 @@ EXCEPTIONS = dict( BadValue=BadValueError, Readonly=ReadonlyError, CommandFailed=CommandFailedError, - InvalidParam=InvalidParamValueError, -) + InvalidParam=InvalidParamValueError, ) if __name__ == '__main__': print("Minimal testing of errors....") diff --git a/secop/protocol/framing/__init__.py b/secop/protocol/framing/__init__.py index eb8e9d6..a79f531 100644 --- a/secop/protocol/framing/__init__.py +++ b/secop/protocol/framing/__init__.py @@ -19,7 +19,6 @@ # Enrico Faulhaber # # ***************************************************************************** - """Encoding/decoding Frames""" @@ -45,7 +44,6 @@ class Framer(object): """resets the de/encoding stage (clears internal information)""" raise NotImplemented - # now some Implementations from null import NullFramer from eol import EOLFramer diff --git a/secop/protocol/framing/demo.py b/secop/protocol/framing/demo.py index 1272245..acff8fd 100644 --- a/secop/protocol/framing/demo.py +++ b/secop/protocol/framing/demo.py @@ -19,7 +19,6 @@ # Enrico Faulhaber # # ***************************************************************************** - """Encoding/decoding Frames""" from secop.protocol.framing import Framer diff --git a/secop/protocol/framing/eol.py b/secop/protocol/framing/eol.py index 1b6b0a3..324e64b 100644 --- a/secop/protocol/framing/eol.py +++ b/secop/protocol/framing/eol.py @@ -19,7 +19,6 @@ # Enrico Faulhaber # # ***************************************************************************** - """Encoding/decoding Frames""" from secop.protocol.framing import Framer diff --git a/secop/protocol/framing/null.py b/secop/protocol/framing/null.py index 26f2b2d..9987711 100644 --- a/secop/protocol/framing/null.py +++ b/secop/protocol/framing/null.py @@ -19,7 +19,6 @@ # Enrico Faulhaber # # ***************************************************************************** - """Encoding/decoding Frames""" from secop.protocol.framing import Framer diff --git a/secop/protocol/framing/rle.py b/secop/protocol/framing/rle.py index f68894e..352361f 100644 --- a/secop/protocol/framing/rle.py +++ b/secop/protocol/framing/rle.py @@ -19,7 +19,6 @@ # Enrico Faulhaber # # ***************************************************************************** - """Encoding/decoding Frames""" from secop.protocol.framing import Framer diff --git a/secop/protocol/interface/__init__.py b/secop/protocol/interface/__init__.py index 828da54..23d8756 100644 --- a/secop/protocol/interface/__init__.py +++ b/secop/protocol/interface/__init__.py @@ -23,9 +23,7 @@ from tcp import TCPServer -INTERFACES = { - 'tcp': TCPServer, -} +INTERFACES = {'tcp': TCPServer, } # for 'from protocol.interface import *' to only import the dict __ALL__ = ['INTERFACES'] diff --git a/secop/protocol/interface/tcp.py b/secop/protocol/interface/tcp.py index 9afef2f..72f189e 100644 --- a/secop/protocol/interface/tcp.py +++ b/secop/protocol/interface/tcp.py @@ -19,7 +19,6 @@ # Enrico Faulhaber # # ***************************************************************************** - """provides tcp interface to the SECoP Server""" import os @@ -36,7 +35,6 @@ from secop.protocol.messages import HelpMessage class TCPRequestHandler(SocketServer.BaseRequestHandler): - def setup(self): self.log = self.server.log self._queue = collections.deque(maxlen=100) @@ -49,13 +47,13 @@ class TCPRequestHandler(SocketServer.BaseRequestHandler): mysocket = self.request clientaddr = self.client_address serverobj = self.server - self.log.debug("handling new connection from %s" % repr(clientaddr)) + self.log.info("handling new connection from %s:%d" % clientaddr) # notify dispatcher of us serverobj.dispatcher.add_connection(self) mysocket.settimeout(.3) -# mysocket.setblocking(False) + # mysocket.setblocking(False) # start serving while True: # send replys fist, then listen for requests, timing out after 0.1s @@ -66,7 +64,10 @@ class TCPRequestHandler(SocketServer.BaseRequestHandler): outmsg = self._queue.popleft() outframes = self.encoding.encode(outmsg) outdata = self.framing.encode(outframes) - mysocket.sendall(outdata) + try: + mysocket.sendall(outdata) + except Exception: + return # XXX: improve: use polling/select here? try: @@ -101,6 +102,7 @@ class TCPRequestHandler(SocketServer.BaseRequestHandler): def finish(self): """called when handle() terminates, i.e. the socket closed""" + self.log.info('closing connection from %s:%d' % self.client_address) # notify dispatcher self.server.dispatcher.remove_connection(self) # close socket @@ -132,7 +134,6 @@ class TCPServer(SocketServer.ThreadingTCPServer): self.log.debug("TCPServer using framing=%s" % self.framingCLS.__name__) self.log.debug("TCPServer using encoding=%s" % self.encodingCLS.__name__) - SocketServer.ThreadingTCPServer.__init__(self, (bindto, portnum), - TCPRequestHandler, - bind_and_activate=True) + SocketServer.ThreadingTCPServer.__init__( + self, (bindto, portnum), TCPRequestHandler, bind_and_activate=True) self.log.info("TCPServer initiated") diff --git a/secop/protocol/messages.py b/secop/protocol/messages.py index 4e09f8d..666483a 100644 --- a/secop/protocol/messages.py +++ b/secop/protocol/messages.py @@ -19,7 +19,6 @@ # Enrico Faulhaber # # ***************************************************************************** - """Define SECoP Messages""" @@ -51,8 +50,11 @@ class Message(object): class Value(object): - - def __init__(self, module, parameter=None, command=None, value=Ellipsis, + def __init__(self, + module, + parameter=None, + command=None, + value=Ellipsis, **qualifiers): self.module = module self.parameter = parameter @@ -67,9 +69,10 @@ class Value(object): devspec = '%s:%s' % (devspec, self.parameter) elif self.command: devspec = '%s:%s()' % (devspec, self.command) - return '%s:Value(%s)' % (devspec, ', '.join( - [repr(self.value)] + - ['%s=%s' % (k, format_time(v) if k == "timestamp" else repr(v)) for k, v in self.qualifiers.items()])) + return '%s:Value(%s)' % (devspec, ', '.join([repr(self.value)] + [ + '%s=%s' % (k, format_time(v) if k == "timestamp" else repr(v)) + for k, v in self.qualifiers.items() + ])) class Request(Message): @@ -95,8 +98,7 @@ class Request(Message): for k in self.ARGS: m.setvalue(k, self.__dict__[k]) m.setvalue("errorclass", errorclass[:-5] - if errorclass.endswith('rror') - else errorclass) + if errorclass.endswith('rror') else errorclass) m.setvalue("errorinfo", errorinfo) return m diff --git a/secop/protocol/messages_old.py b/secop/protocol/messages_old.py index 60e22b9..277fb37 100644 --- a/secop/protocol/messages_old.py +++ b/secop/protocol/messages_old.py @@ -19,7 +19,6 @@ # Enrico Faulhaber # # ***************************************************************************** - """Define SECoP Messages""" # Request Types @@ -79,8 +78,9 @@ class Message(object): r = 'Device' if self.devs != ['*'] else 'Devices' t = '' - if self.MSGTYPE in [LIST, READ, WRITE, COMMAND, - POLL, SUBSCRIBE, UNSUBSCRIBE, HELP]: + if self.MSGTYPE in [ + LIST, READ, WRITE, COMMAND, POLL, SUBSCRIBE, UNSUBSCRIBE, HELP + ]: t = 'Request' if not self.result else 'Reply' if self.errortype is None: @@ -95,7 +95,6 @@ class Message(object): class Value(object): - def __init__(self, value=Ellipsis, qualifiers=None, **kwds): self.dev = '' self.param = '' @@ -111,9 +110,9 @@ class Value(object): if self.prop: devspec = '%s:%s' % (devspec, self.prop) return '%s:Value(%s)' % ( - devspec, ', '.join( - [repr(self.value)] + - ['%s=%r' % (k, v) for k, v in self.qualifiers.items()])) + devspec, + ', '.join([repr(self.value)] + + ['%s=%r' % (k, v) for k, v in self.qualifiers.items()])) class ListMessage(Message): @@ -167,28 +166,29 @@ class HelpMessage(Message): class NoSuchDeviceError(ErrorMessage): - def __init__(self, *devs): ErrorMessage.__init__( - self, devs=devs, errorstring="Device %r does not exist" % - devs[0], errortype='NoSuchDevice') + self, + devs=devs, + errorstring="Device %r does not exist" % devs[0], + errortype='NoSuchDevice') class NoSuchParamError(ErrorMessage): - def __init__(self, dev, *params): ErrorMessage.__init__( - self, devs=(dev,), - params=params, errorstring="Device %r has no parameter %r" % - (dev, params[0]), + self, + devs=(dev, ), + params=params, + errorstring="Device %r has no parameter %r" % (dev, params[0]), errortype='NoSuchParam') class ParamReadonlyError(ErrorMessage): - def __init__(self, dev, *params): ErrorMessage.__init__( - self, devs=(dev,), + self, + devs=(dev, ), params=params, errorstring="Device %r, parameter %r is not writeable!" % (dev, params[0]), @@ -196,30 +196,28 @@ class ParamReadonlyError(ErrorMessage): class InvalidParamValueError(ErrorMessage): - def __init__(self, dev, param, value, e): ErrorMessage.__init__( - self, devs=(dev,), - params=params, values=(value), + self, + devs=(dev, ), + params=params, + values=(value), errorstring=str(e), errortype='InvalidParamValueError') class InternalError(ErrorMessage): - def __init__(self, err, **kwds): ErrorMessage.__init__( - self, errorstring=str(err), - errortype='InternalError', **kwds) + self, errorstring=str(err), errortype='InternalError', **kwds) -MESSAGE = dict( - (cls.MSGTYPE, cls) - for cls - in - [HelpMessage, ErrorMessage, EventMessage, TriggerMessage, - UnsubscribeMessage, SubscribeMessage, PollMessage, CommandMessage, - WriteMessage, ReadMessage, ListMessage]) +MESSAGE = dict((cls.MSGTYPE, cls) + for cls in [ + HelpMessage, ErrorMessage, EventMessage, TriggerMessage, + UnsubscribeMessage, SubscribeMessage, PollMessage, + CommandMessage, WriteMessage, ReadMessage, ListMessage + ]) if __name__ == '__main__': print("Minimal testing of messages....") diff --git a/secop/protocol/status.py b/secop/protocol/status.py index 10fe54b..4192746 100644 --- a/secop/protocol/status.py +++ b/secop/protocol/status.py @@ -23,9 +23,9 @@ # could also be some objects OK = 100 -BUSY = 200 -WARN = 300 -UNSTABLE = 350 +WARN = 200 +UNSTABLE = 250 +BUSY = 300 ERROR = 400 UNKNOWN = -1 diff --git a/secop/server.py b/secop/server.py index 5663b48..703ba21 100644 --- a/secop/server.py +++ b/secop/server.py @@ -20,7 +20,6 @@ # Alexander Lenz # # ***************************************************************************** - """Define helpers""" import os import time @@ -41,7 +40,6 @@ from secop.errors import ConfigError class Server(object): - def __init__(self, name, workdir, parentLogger=None): self._name = name self._workdir = workdir @@ -65,9 +63,10 @@ class Server(object): if pidfile.is_locked(): self.log.error('Pidfile already exists. Exiting') - with DaemonContext(working_directory=self._workdir, - pidfile=pidfile, - files_preserve=self.log.getLogfileStreams()): + with DaemonContext( + working_directory=self._workdir, + pidfile=pidfile, + files_preserve=self.log.getLogfileStreams()): self.run() def run(self): @@ -85,9 +84,8 @@ class Server(object): time.sleep(1) for t in self._threads: if not t.is_alive(): - self.log.debug( - 'thread %r died (%d still running)' % - (t, len(self._threads))) + self.log.debug('thread %r died (%d still running)' % + (t, len(self._threads))) t.join() self._threads.discard(t) @@ -95,9 +93,11 @@ class Server(object): self.log.debug('Parse config file %s ...' % self._cfgfile) parser = ConfigParser.SafeConfigParser() + parser.optionxform = str + if not parser.read([self._cfgfile]): - self.log.error('Couldn\'t read cfg file !') - raise ConfigError('Couldn\'t read cfg file %r' % self._cfgfile) + self.log.error("Couldn't read cfg file !") + raise ConfigError("Couldn't read cfg file %r" % self._cfgfile) self._interfaces = [] @@ -113,8 +113,8 @@ class Server(object): if 'class' not in devopts: self.log.error('Device %s needs a class option!') raise ConfigError( - 'cfgfile %r: Device %s needs a class option!' - % (self._cfgfile, devname)) + 'cfgfile %r: Device %s needs a class option!' % + (self._cfgfile, devname)) # try to import the class, raise if this fails devopts['class'] = get_class(devopts['class']) # all went well so far @@ -127,16 +127,15 @@ class Server(object): if 'interface' not in ifopts: self.log.error('Interface %s needs an interface option!') raise ConfigError( - 'cfgfile %r: Interface %s needs an interface option!' - % (self._cfgfile, ifname)) + 'cfgfile %r: Interface %s needs an interface option!' % + (self._cfgfile, ifname)) # all went well so far interfaceopts.append([ifname, ifopts]) if parser.has_option('equipment', 'id'): - equipment_id = parser.get('equipment', 'id') + equipment_id = parser.get('equipment', 'id').replace(' ', '_') self._dispatcher = self._buildObject( - 'Dispatcher', Dispatcher, dict( - equipment_id=equipment_id)) + 'Dispatcher', Dispatcher, dict(equipment_id=equipment_id)) self._processInterfaceOptions(interfaceopts) self._processDeviceOptions(deviceopts) @@ -156,8 +155,8 @@ class Server(object): for d in ("'", '"'): if v.startswith(d) and v.endswith(d): devopts[k] = v[1:-1] - devobj = devclass(self.log.getChild(devname), devopts, devname, - self._dispatcher) + devobj = devclass( + self.log.getChild(devname), devopts, devname, self._dispatcher) devs.append([devname, devobj, export]) # connect devices with dispatcher @@ -178,8 +177,8 @@ class Server(object): for ifname, ifopts in interfaceopts: ifclass = ifopts.pop('interface') ifclass = INTERFACES[ifclass] - interface = self._buildObject(ifname, ifclass, - ifopts, self._dispatcher) + interface = self._buildObject(ifname, ifclass, ifopts, + self._dispatcher) self._interfaces.append(interface) def _buildObject(self, name, cls, options, *args): @@ -187,7 +186,6 @@ class Server(object): # cls.__init__ should pop all used args from options! obj = cls(self.log.getChild(name.lower()), options, *args) if options: - raise ConfigError('%s: don\'t know how to handle option(s): %s' % ( - cls.__name__, - ', '.join(options.keys()))) + raise ConfigError('%s: don\'t know how to handle option(s): %s' % + (cls.__name__, ', '.join(options.keys()))) return obj diff --git a/secop/validators.py b/secop/validators.py index fa1b1dc..c0939bd 100644 --- a/secop/validators.py +++ b/secop/validators.py @@ -21,7 +21,6 @@ # ***************************************************************************** """Define validators.""" - # a Validator returns a validated object or raises an ValueError # easy python validators: int(), float(), str() # also validators should have a __repr__ returning a 'python' string @@ -43,29 +42,25 @@ class Validator(object): plist = self.params[:] if len(args) > len(plist): raise ProgrammingError('%s takes %d parameters only (%d given)' % ( - self.__class__.__name__, - len(plist), len(args))) + self.__class__.__name__, len(plist), len(args))) for pval in args: pname, pconv = plist.pop(0) if pname in kwds: raise ProgrammingError('%s: positional parameter %s is given ' - 'as keyword!' % ( - self.__class__.__name__, - pname)) + 'as keyword!' % + (self.__class__.__name__, pname)) self.__dict__[pname] = pconv(pval) for pname, pconv in plist: if pname in kwds: pval = kwds.pop(pname) self.__dict__[pname] = pconv(pval) else: - raise ProgrammingError('%s: param %s left unspecified!' % ( - self.__class__.__name__, - pname)) + raise ProgrammingError('%s: param %s left unspecified!' % + (self.__class__.__name__, pname)) if kwds: raise ProgrammingError('%s got unknown arguments: %s' % ( - self.__class__.__name__, - ', '.join(list(kwds.keys())))) + self.__class__.__name__, ', '.join(list(kwds.keys())))) params = [] for pn, pt in self.params: pv = getattr(self, pn) @@ -89,10 +84,17 @@ class floatrange(Validator): params = [('lower', float), ('upper', float)] def check(self, value): - if self.lower <= value <= self.upper: - return value - raise ValueError('Floatrange: value %r must be within %f and %f' % - (value, self.lower, self.upper)) + try: + value = float(value) + if self.lower <= value <= self.upper: + return value + raise ValueError( + 'Floatrange: value %r must be a float within %f and %f' % + (value, self.lower, self.upper)) + except TypeError: + raise ValueError( + 'Floatrange: value %r must be a float within %f and %f' % + (value, self.lower, self.upper)) class intrange(Validator): @@ -100,10 +102,11 @@ class intrange(Validator): valuetype = int def check(self, value): - if self.lower <= value <= self.upper: + if self.lower <= int(value) <= self.upper: return value - raise ValueError('Intrange: value %r must be within %f and %f' % - (value, self.lower, self.upper)) + raise ValueError( + 'Intrange: value %r must be an integer within %f and %f' % + (value, self.lower, self.upper)) class array(Validator): @@ -112,8 +115,7 @@ class array(Validator): The size of the array can also be described by an validator """ valuetype = list - params = [('size', lambda x: x), - ('datatype', lambda x: x)] + params = [('size', lambda x: x), ('datatype', lambda x: x)] def check(self, values): requested_size = len(values) @@ -121,15 +123,13 @@ class array(Validator): try: allowed_size = self.size(requested_size) except ValueError as e: - raise ValueError( - 'illegal number of elements %d, need %r: (%s)' % - (requested_size, self.size, e)) + raise ValueError('illegal number of elements %d, need %r: (%s)' + % (requested_size, self.size, e)) else: allowed_size = self.size if requested_size != allowed_size: - raise ValueError( - 'need %d elements (got %d)' % - (allowed_size, requested_size)) + raise ValueError('need %d elements (got %d)' % + (allowed_size, requested_size)) # apply data-type validator to all elements and return res = [] for idx, el in enumerate(values): @@ -152,10 +152,13 @@ class vector(Validator): self.argstr = ', '.join([validator_to_str(e) for e in args]) def __call__(self, args): + if type(args) in (str, unicode): + args = eval(args) if len(args) != len(self.validators): raise ValueError('Vector: need exactly %d elementes (got %d)' % len(self.validators), len(args)) - return tuple(v(e) for v, e in zip(self.validators, args)) + res = tuple(v(e) for v, e in zip(self.validators, args)) + return res # XXX: fixme! @@ -197,7 +200,6 @@ class oneof(Validator): class enum(Validator): - def __init__(self, *args, **kwds): self.mapping = {} # use given kwds directly @@ -238,23 +240,27 @@ class enum(Validator): def positive(value=Ellipsis): if value != Ellipsis: if value > 0: - return value + return float(value) raise ValueError('Value %r must be > 0!' % value) return -1e-38 # small number > 0 + + positive.__repr__ = lambda x: validator_to_str(x) def nonnegative(value=Ellipsis): if value != Ellipsis: if value >= 0: - return value + return float(value) raise ValueError('Value %r must be >= 0!' % value) return 0.0 + + nonnegative.__repr__ = lambda x: validator_to_str(x) - # helpers + def validator_to_str(validator): if isinstance(validator, Validator): return validator.to_string() @@ -269,21 +275,28 @@ def validator_to_str(validator): # XXX: better use a mapping here! def validator_from_str(validator_str): + validator_str = validator_str.replace("", "str") + validator_str = validator_str.replace("", "float") return eval(validator_str) + if __name__ == '__main__': print "minimal testing: validators" - for val, good, bad in [(floatrange(3.09, 5.47), 4.13, 9.27), - (intrange(3, 5), 4, 8), - (array(size=3, datatype=int), (1, 2, 3), (1, 2, 3, 4)), - (vector(int, int), (12, 6), (1.23, 'X')), - (oneof('a', 'b', 'c', 1), 'b', 'x'), - #(record(a=int, b=float), dict(a=2,b=3.97), dict(c=9,d='X')), - (positive, 2, 0), - (nonnegative, 0, -1), - (enum(a=1, b=20), 1, 12), - ]: - print validator_to_str(val), repr(validator_from_str(validator_to_str(val))) + for val, good, bad in [ + (floatrange(3.09, 5.47), 4.13, 9.27), + (intrange(3, 5), 4, 8), + (array( + size=3, datatype=int), (1, 2, 3), (1, 2, 3, 4)), + (vector(int, int), (12, 6), (1.23, 'X')), + (oneof('a', 'b', 'c', 1), 'b', 'x'), + #(record(a=int, b=float), dict(a=2,b=3.97), dict(c=9,d='X')), + (positive, 2, 0), + (nonnegative, 0, -1), + (enum( + a=1, b=20), 1, 12), + ]: + print validator_to_str(val), repr( + validator_from_str(validator_to_str(val))) print val(good), 'OK' try: val(bad)