Merge branch 'wip' of gitlab.psi.ch-samenv:samenv/frappy into wip
This commit is contained in:
commit
dee8f8929e
@ -53,6 +53,8 @@ disable=missing-docstring
|
|||||||
,unidiomatic-typecheck
|
,unidiomatic-typecheck
|
||||||
,undefined-loop-variable
|
,undefined-loop-variable
|
||||||
,consider-using-f-string
|
,consider-using-f-string
|
||||||
|
,use-dict-literal
|
||||||
|
|
||||||
[REPORTS]
|
[REPORTS]
|
||||||
|
|
||||||
# Set the output format. Available formats are text, parseable, colorized, msvs
|
# Set the output format. Available formats are text, parseable, colorized, msvs
|
||||||
|
@ -31,6 +31,7 @@ sys.path.insert(0, path.abspath(path.join(path.dirname(__file__), '..')))
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from mlzlog import ColoredConsoleHandler
|
from mlzlog import ColoredConsoleHandler
|
||||||
|
|
||||||
from frappy.gui.qt import QApplication
|
from frappy.gui.qt import QApplication
|
||||||
from frappy.gui.cfg_editor.mainwindow import MainWindow
|
from frappy.gui.cfg_editor.mainwindow import MainWindow
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# This program is free software; you can redistribute it and/or modify it under
|
||||||
@ -23,8 +22,8 @@
|
|||||||
#
|
#
|
||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
|
|
||||||
import sys
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import sys
|
||||||
from os import path
|
from os import path
|
||||||
|
|
||||||
# Add import path for inplace usage
|
# Add import path for inplace usage
|
||||||
@ -61,8 +60,9 @@ def parseArgv(argv):
|
|||||||
action='store',
|
action='store',
|
||||||
help="comma separated list of cfg files,\n"
|
help="comma separated list of cfg files,\n"
|
||||||
"defaults to <name_of_the_instance>.\n"
|
"defaults to <name_of_the_instance>.\n"
|
||||||
"cfgfiles given without '.cfg' extension are searched in the configuration directory, "
|
"cfgfiles given without '.cfg' extension are searched"
|
||||||
"else they are treated as path names",
|
" in the configuration directory,"
|
||||||
|
" else they are treated as path names",
|
||||||
default=None)
|
default=None)
|
||||||
parser.add_argument('-g',
|
parser.add_argument('-g',
|
||||||
'--gencfg',
|
'--gencfg',
|
||||||
@ -96,15 +96,13 @@ def main(argv=None):
|
|||||||
generalConfig.init(args.gencfg)
|
generalConfig.init(args.gencfg)
|
||||||
logger.init(loglevel)
|
logger.init(loglevel)
|
||||||
|
|
||||||
srv = Server(args.name, logger.log, cfgfiles=args.cfgfiles, interface=args.port, testonly=args.test)
|
srv = Server(args.name, logger.log, cfgfiles=args.cfgfiles,
|
||||||
|
interface=args.port, testonly=args.test)
|
||||||
|
|
||||||
if args.daemonize:
|
if args.daemonize:
|
||||||
srv.start()
|
srv.start()
|
||||||
else:
|
else:
|
||||||
try:
|
srv.run()
|
||||||
srv.run()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
@ -43,6 +43,7 @@ Mod('mf',
|
|||||||
'frappy_psi.sea.SeaDrivable', '',
|
'frappy_psi.sea.SeaDrivable', '',
|
||||||
io='sea_main',
|
io='sea_main',
|
||||||
sea_object='mf',
|
sea_object='mf',
|
||||||
|
rel_paths=['.', 'gen', 'ips'],
|
||||||
)
|
)
|
||||||
|
|
||||||
Mod('lev',
|
Mod('lev',
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{"hcp": {"base": "/hcp", "params": [
|
{"hcp": {"base": "/hcp", "params": [
|
||||||
{"path": "", "type": "float", "kids": 10},
|
{"path": "", "type": "float", "readonly": false, "cmd": "hcp set", "kids": 10},
|
||||||
{"path": "send", "type": "text", "readonly": false, "cmd": "hcp send", "visibility": 3},
|
{"path": "send", "type": "text", "readonly": false, "cmd": "hcp send", "visibility": 3},
|
||||||
{"path": "status", "type": "text", "visibility": 3},
|
{"path": "status", "type": "text", "visibility": 3},
|
||||||
{"path": "set", "type": "float", "readonly": false, "cmd": "hcp set"},
|
{"path": "set", "type": "float", "readonly": false, "cmd": "hcp set"},
|
||||||
|
43
cfg/seop_cfg.py
Normal file
43
cfg/seop_cfg.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
description = """
|
||||||
|
3He system in Lab ...
|
||||||
|
"""
|
||||||
|
Node('mlz_seop',
|
||||||
|
description,
|
||||||
|
'tcp://10767',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('cell',
|
||||||
|
'frappy_mlz.seop.Cell',
|
||||||
|
'interface module to the driver',
|
||||||
|
config_directory = '/home/jcns/daemon/config',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('afp',
|
||||||
|
'frappy_mlz.seop.Afp',
|
||||||
|
'controls the afp flip of the cell',
|
||||||
|
cell = 'cell'
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('nmr',
|
||||||
|
'frappy_mlz.seop.Nmr',
|
||||||
|
'controls the ',
|
||||||
|
cell = 'cell'
|
||||||
|
)
|
||||||
|
|
||||||
|
fitparams = [
|
||||||
|
('amplitude', 'V'),
|
||||||
|
('T1', 's'),
|
||||||
|
('T2', 's'),
|
||||||
|
('b', ''),
|
||||||
|
('frequency', 'Hz'),
|
||||||
|
('phase', 'deg'),
|
||||||
|
]
|
||||||
|
for param, unit in fitparams:
|
||||||
|
Mod(f'nmr_{param.lower()}',
|
||||||
|
'frappy_mlz.seop.FitParam',
|
||||||
|
f'fittet parameter {param} of NMR',
|
||||||
|
cell = 'cell',
|
||||||
|
value = Param(unit=unit),
|
||||||
|
sigma = Param(unit=unit),
|
||||||
|
param = param,
|
||||||
|
)
|
@ -18,7 +18,7 @@ Mod('ts',
|
|||||||
)
|
)
|
||||||
|
|
||||||
Mod('hcp',
|
Mod('hcp',
|
||||||
'frappy_psi.sea.SeaReadable', '',
|
'frappy_psi.sea.SeaWritable', '',
|
||||||
io='sea_stick',
|
io='sea_stick',
|
||||||
sea_object='hcp',
|
sea_object='hcp',
|
||||||
)
|
)
|
||||||
|
@ -10,6 +10,22 @@ The needed fields are Equipment id (1st argument), description (this)
|
|||||||
'tcp://10768',
|
'tcp://10768',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Mod('attachtest',
|
||||||
|
'frappy_demo.test.WithAtt',
|
||||||
|
'test attached',
|
||||||
|
att = 'LN2',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('pinata',
|
||||||
|
'frappy_demo.test.Pin',
|
||||||
|
'scan test',
|
||||||
|
)
|
||||||
|
|
||||||
|
Mod('recursive',
|
||||||
|
'frappy_demo.test.RecPin',
|
||||||
|
'scan test',
|
||||||
|
)
|
||||||
|
|
||||||
Mod('LN2',
|
Mod('LN2',
|
||||||
'frappy_demo.test.LN2',
|
'frappy_demo.test.LN2',
|
||||||
'random value between 0..100%',
|
'random value between 0..100%',
|
||||||
|
46
debian/changelog
vendored
46
debian/changelog
vendored
@ -1,3 +1,49 @@
|
|||||||
|
frappy-core (0.17.13) focal; urgency=medium
|
||||||
|
|
||||||
|
[ Alexander Zaft ]
|
||||||
|
* add egg-info to gitignore
|
||||||
|
|
||||||
|
[ Markus Zolliker ]
|
||||||
|
* GUI bugfix: use isChecked instead of checkState in BoolInput
|
||||||
|
* frappy_psi.mercury/triton: add control_off command
|
||||||
|
* frappy_psi.phytron: rename reset_error to clear_errors
|
||||||
|
* frappy.mixins.HasOutputModule
|
||||||
|
* frappy_psi.mercury: proper handling of control_active
|
||||||
|
* add a hook for reads to be done initially
|
||||||
|
* frappy_psi.triton: fix HeaterOutput.limit
|
||||||
|
* frappy_psi.magfield: bug fix
|
||||||
|
* frappy_psi.sea: bug fixes
|
||||||
|
|
||||||
|
[ Alexander Zaft ]
|
||||||
|
* server: fix systemd variable scope
|
||||||
|
|
||||||
|
-- Alexander Zaft <jenkins@frm2.tum.de> Tue, 20 Jun 2023 14:38:00 +0200
|
||||||
|
|
||||||
|
frappy-core (0.17.12) focal; urgency=medium
|
||||||
|
|
||||||
|
[ Alexander Zaft ]
|
||||||
|
* Warn about duplicate module definitions in a file
|
||||||
|
* Add influences property to parameters/commands
|
||||||
|
|
||||||
|
[ Markus Zolliker ]
|
||||||
|
* frappy.client: dummy logger is missing 'exception' method
|
||||||
|
|
||||||
|
[ Alexander Zaft ]
|
||||||
|
* Add SEOP He3-polarization device
|
||||||
|
* Typo in influences description
|
||||||
|
* seop: fix fitparam command
|
||||||
|
|
||||||
|
[ Markus Zolliker ]
|
||||||
|
* silently catches error in systemd.daemon.notify
|
||||||
|
|
||||||
|
[ Alexander Zaft ]
|
||||||
|
* io: add option to retry first ident request
|
||||||
|
* config: fix merge_modules
|
||||||
|
* io: followup fix for retry-first-ident
|
||||||
|
* entangle: fix tango guards for pytango 9.3
|
||||||
|
|
||||||
|
-- Alexander Zaft <jenkins@frm2.tum.de> Tue, 13 Jun 2023 06:51:27 +0200
|
||||||
|
|
||||||
frappy-core (0.17.11) focal; urgency=medium
|
frappy-core (0.17.11) focal; urgency=medium
|
||||||
|
|
||||||
[ Alexander Zaft ]
|
[ Alexander Zaft ]
|
||||||
|
@ -214,18 +214,28 @@ class Module:
|
|||||||
return self.read()
|
return self.read()
|
||||||
self.target = target # this sets self._is_driving
|
self.target = target # this sets self._is_driving
|
||||||
type(self).value.prev = None # show at least one value
|
type(self).value.prev = None # show at least one value
|
||||||
try:
|
|
||||||
|
def loop():
|
||||||
while self._is_driving:
|
while self._is_driving:
|
||||||
self._driving_event.wait()
|
self._driving_event.wait()
|
||||||
self._watch_parameter(self._name, 'value', mininterval=self._secnode.mininterval)
|
self._watch_parameter(self._name, 'value', mininterval=self._secnode.mininterval)
|
||||||
self._watch_parameter(self._name, 'status')
|
self._watch_parameter(self._name, 'status')
|
||||||
self._driving_event.clear()
|
self._driving_event.clear()
|
||||||
except KeyboardInterrupt:
|
try:
|
||||||
self.stop()
|
loop()
|
||||||
|
except KeyboardInterrupt as e:
|
||||||
self._secnode.log.info('-- interrupted --')
|
self._secnode.log.info('-- interrupted --')
|
||||||
self._watch_parameter(self._name, 'status')
|
self.stop()
|
||||||
self._secnode.readParameter(self._name, 'value')
|
try:
|
||||||
self._watch_parameter(self._name, 'value', forced=True)
|
loop() # wait for stopping to be finished
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
# interrupted again while stopping -> definitely quit
|
||||||
|
pass
|
||||||
|
clientenv.raise_with_short_traceback(e)
|
||||||
|
finally:
|
||||||
|
self._watch_parameter(self._name, 'status')
|
||||||
|
self._secnode.readParameter(self._name, 'value')
|
||||||
|
self._watch_parameter(self._name, 'value', forced=True)
|
||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
@ -331,8 +341,8 @@ def watch(*args, **kwds):
|
|||||||
for mobj in modules:
|
for mobj in modules:
|
||||||
mobj._start_watching()
|
mobj._start_watching()
|
||||||
time.sleep(3600)
|
time.sleep(3600)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt as e:
|
||||||
pass
|
clientenv.raise_with_short_traceback(e)
|
||||||
finally:
|
finally:
|
||||||
for mobj in modules:
|
for mobj in modules:
|
||||||
mobj._stop_watching()
|
mobj._stop_watching()
|
||||||
|
41
frappy/dynamic.py
Normal file
41
frappy/dynamic.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# *****************************************************************************
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License as published by the Free Software
|
||||||
|
# Foundation; either version 2 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
# Module authors:
|
||||||
|
# Alexander Zaft <a.zaft@fz-juelich.de>
|
||||||
|
#
|
||||||
|
# *****************************************************************************
|
||||||
|
|
||||||
|
from .core import Module
|
||||||
|
|
||||||
|
class Pinata(Module):
|
||||||
|
"""Base class for scanning conections and adding modules accordingly.
|
||||||
|
|
||||||
|
Like a piñata. You poke it, and modules fall out.
|
||||||
|
|
||||||
|
To use it, subclass it for your connection type and override the function
|
||||||
|
'scanModules'. For each module you want to register, you should yield the
|
||||||
|
modules name and its config options.
|
||||||
|
The connection will then be scanned during server startup.
|
||||||
|
"""
|
||||||
|
export = False
|
||||||
|
|
||||||
|
# POKE
|
||||||
|
def scanModules(self):
|
||||||
|
"""yield (modname, options) for each module the Pinata should create.
|
||||||
|
Options has to include keys for class and the config for the module.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
@ -407,10 +407,15 @@ class UniqueObject:
|
|||||||
def merge_status(*args):
|
def merge_status(*args):
|
||||||
"""merge status
|
"""merge status
|
||||||
|
|
||||||
the status with biggest code wins
|
for combining stati of different mixins
|
||||||
texts matching maximal code are joined with ', '
|
- the status with biggest code wins
|
||||||
|
- texts matching maximal code are joined with ', '
|
||||||
|
- if texts already contain ', ', it is considered as composed by
|
||||||
|
individual texts and duplication is avoided. when commas are used
|
||||||
|
for other purposes, the behaviour might be surprising
|
||||||
"""
|
"""
|
||||||
maxcode = max(a[0] for a in args)
|
maxcode = max(a[0] for a in args)
|
||||||
merged = [a[1] for a in args if a[0] == maxcode and a[1]]
|
merged = [a[1] for a in args if a[0] == maxcode and a[1]]
|
||||||
|
# use dict instead of set for preserving order
|
||||||
merged = {m: True for mm in merged for m in mm.split(', ')}
|
merged = {m: True for mm in merged for m in mm.split(', ')}
|
||||||
return maxcode, ', '.join(merged)
|
return maxcode, ', '.join(merged)
|
||||||
|
@ -329,7 +329,6 @@ class Module(HasAccessibles):
|
|||||||
|
|
||||||
# reference to the dispatcher (used for sending async updates)
|
# reference to the dispatcher (used for sending async updates)
|
||||||
DISPATCHER = None
|
DISPATCHER = None
|
||||||
attachedModules = None
|
|
||||||
pollInfo = None
|
pollInfo = None
|
||||||
triggerPoll = None # trigger event for polls. used on io modules and modules without io
|
triggerPoll = None # trigger event for polls. used on io modules and modules without io
|
||||||
|
|
||||||
@ -347,7 +346,9 @@ class Module(HasAccessibles):
|
|||||||
self.accessLock = threading.RLock() # for read_* / write_* methods
|
self.accessLock = threading.RLock() # for read_* / write_* methods
|
||||||
self.updateLock = threading.RLock() # for announceUpdate
|
self.updateLock = threading.RLock() # for announceUpdate
|
||||||
self.polledModules = [] # modules polled by thread started in self.startModules
|
self.polledModules = [] # modules polled by thread started in self.startModules
|
||||||
|
self.attachedModules = {}
|
||||||
errors = []
|
errors = []
|
||||||
|
self._isinitialized = False
|
||||||
|
|
||||||
# handle module properties
|
# handle module properties
|
||||||
# 1) make local copies of properties
|
# 1) make local copies of properties
|
||||||
@ -639,6 +640,13 @@ class Module(HasAccessibles):
|
|||||||
all parameters are polled once
|
all parameters are polled once
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def shutdownModule(self):
|
||||||
|
"""called when the sever shuts down
|
||||||
|
|
||||||
|
any cleanup-work should be performed here, like closing threads and
|
||||||
|
saving data.
|
||||||
|
"""
|
||||||
|
|
||||||
def doPoll(self):
|
def doPoll(self):
|
||||||
"""polls important parameters like value and status
|
"""polls important parameters like value and status
|
||||||
|
|
||||||
@ -932,10 +940,12 @@ class Attached(Property):
|
|||||||
def __get__(self, obj, owner):
|
def __get__(self, obj, owner):
|
||||||
if obj is None:
|
if obj is None:
|
||||||
return self
|
return self
|
||||||
if obj.attachedModules is None:
|
if self.name not in obj.attachedModules:
|
||||||
# return the name of the module (called from Server on startup)
|
modobj = obj.DISPATCHER.get_module(super().__get__(obj, owner))
|
||||||
return super().__get__(obj, owner)
|
if not isinstance(modobj, self.basecls):
|
||||||
# return the module (called after startup)
|
raise ConfigError(f'attached module {self.name}={modobj.name!r} '\
|
||||||
|
f'must inherit from {self.basecls.__qualname__!r}')
|
||||||
|
obj.attachedModules[self.name] = modobj
|
||||||
return obj.attachedModules.get(self.name) # return None if not given
|
return obj.attachedModules.get(self.name) # return None if not given
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
|
@ -540,7 +540,6 @@ class Limit(Parameter):
|
|||||||
if self.hasDatatype():
|
if self.hasDatatype():
|
||||||
return # the programmer is responsible that a given datatype is correct
|
return # the programmer is responsible that a given datatype is correct
|
||||||
postfix = self.name.rpartition('_')[-1]
|
postfix = self.name.rpartition('_')[-1]
|
||||||
postfix = self.name.rpartition('_')[-1]
|
|
||||||
if postfix == 'limits':
|
if postfix == 'limits':
|
||||||
self.datatype = TupleOf(datatype, datatype)
|
self.datatype = TupleOf(datatype, datatype)
|
||||||
self.default = (datatype.min, datatype.max)
|
self.default = (datatype.min, datatype.max)
|
||||||
@ -562,6 +561,7 @@ PREDEFINED_ACCESSIBLES = {
|
|||||||
'unit': Parameter, # reserved name
|
'unit': Parameter, # reserved name
|
||||||
'loglevel': Parameter, # reserved name
|
'loglevel': Parameter, # reserved name
|
||||||
'mode': Parameter, # reserved name
|
'mode': Parameter, # reserved name
|
||||||
|
'ctrlpars': Parameter, # spec to be confirmed
|
||||||
'stop': Command,
|
'stop': Command,
|
||||||
'reset': Command,
|
'reset': Command,
|
||||||
'go': Command,
|
'go': Command,
|
||||||
|
@ -39,16 +39,18 @@ Interface to the modules:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
|
import traceback
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from time import time as currenttime
|
from time import time as currenttime
|
||||||
|
|
||||||
from frappy.errors import NoSuchCommandError, NoSuchModuleError, \
|
from frappy.errors import NoSuchCommandError, NoSuchModuleError, \
|
||||||
NoSuchParameterError, ProtocolError, ReadOnlyError
|
NoSuchParameterError, ProtocolError, ReadOnlyError, ConfigError
|
||||||
from frappy.params import Parameter
|
from frappy.params import Parameter
|
||||||
from frappy.protocol.messages import COMMANDREPLY, DESCRIPTIONREPLY, \
|
from frappy.protocol.messages import COMMANDREPLY, DESCRIPTIONREPLY, \
|
||||||
DISABLEEVENTSREPLY, ENABLEEVENTSREPLY, ERRORPREFIX, EVENTREPLY, \
|
DISABLEEVENTSREPLY, ENABLEEVENTSREPLY, ERRORPREFIX, EVENTREPLY, \
|
||||||
HEARTBEATREPLY, IDENTREPLY, IDENTREQUEST, READREPLY, WRITEREPLY, \
|
HEARTBEATREPLY, IDENTREPLY, IDENTREQUEST, READREPLY, WRITEREPLY, \
|
||||||
LOGGING_REPLY, LOG_EVENT
|
LOGGING_REPLY, LOG_EVENT
|
||||||
|
from frappy.lib import get_class
|
||||||
|
|
||||||
|
|
||||||
def make_update(modulename, pobj):
|
def make_update(modulename, pobj):
|
||||||
@ -84,6 +86,13 @@ class Dispatcher:
|
|||||||
self.name = name
|
self.name = name
|
||||||
self.restart = srv.restart
|
self.restart = srv.restart
|
||||||
self.shutdown = srv.shutdown
|
self.shutdown = srv.shutdown
|
||||||
|
# handle to server
|
||||||
|
self.srv = srv
|
||||||
|
# set of modules that failed creation
|
||||||
|
self.failed_modules = set()
|
||||||
|
# list of errors that occured during initialization
|
||||||
|
self.errors = []
|
||||||
|
self.traceback_counter = 0
|
||||||
|
|
||||||
def broadcast_event(self, msg, reallyall=False):
|
def broadcast_event(self, msg, reallyall=False):
|
||||||
"""broadcasts a msg to all active connections
|
"""broadcasts a msg to all active connections
|
||||||
@ -147,11 +156,93 @@ class Dispatcher:
|
|||||||
self._export.append(modulename)
|
self._export.append(modulename)
|
||||||
|
|
||||||
def get_module(self, modulename):
|
def get_module(self, modulename):
|
||||||
|
""" Returns a fully initialized module. Or None, if something went
|
||||||
|
wrong during instatiating/initializing the module."""
|
||||||
|
modobj = self.get_module_instance(modulename)
|
||||||
|
if modobj is None:
|
||||||
|
return None
|
||||||
|
if modobj._isinitialized:
|
||||||
|
return modobj
|
||||||
|
|
||||||
|
# also call earlyInit on the modules
|
||||||
|
self.log.debug('initializing module %r', modulename)
|
||||||
|
try:
|
||||||
|
modobj.earlyInit()
|
||||||
|
if not modobj.earlyInitDone:
|
||||||
|
self.errors.append(f'{modobj.earlyInit.__qualname__} was not called, probably missing super call')
|
||||||
|
modobj.initModule()
|
||||||
|
if not modobj.initModuleDone:
|
||||||
|
self.errors.append(f'{modobj.initModule.__qualname__} was not called, probably missing super call')
|
||||||
|
except Exception as e:
|
||||||
|
if self.traceback_counter == 0:
|
||||||
|
self.log.exception(traceback.format_exc())
|
||||||
|
self.traceback_counter += 1
|
||||||
|
self.errors.append(f'error initializing {modulename}: {e!r}')
|
||||||
|
modobj._isinitialized = True
|
||||||
|
self.log.debug('initialized module %r', modulename)
|
||||||
|
return modobj
|
||||||
|
|
||||||
|
def get_module_instance(self, modulename):
|
||||||
|
""" Returns the module in its current initialization state or creates a
|
||||||
|
new uninitialized modle to return.
|
||||||
|
|
||||||
|
When creating a new module, srv.module_config is accessed to get the
|
||||||
|
modules configuration.
|
||||||
|
"""
|
||||||
if modulename in self._modules:
|
if modulename in self._modules:
|
||||||
return self._modules[modulename]
|
return self._modules[modulename]
|
||||||
if modulename in list(self._modules.values()):
|
if modulename in list(self._modules.values()):
|
||||||
|
# it's actually already the module object
|
||||||
return modulename
|
return modulename
|
||||||
raise NoSuchModuleError(f'Module {modulename!r} does not exist on this SEC-Node!')
|
# create module from srv.module_cfg, store and return
|
||||||
|
self.log.debug('attempting to create module %r', modulename)
|
||||||
|
|
||||||
|
opts = self.srv.module_cfg.get(modulename, None)
|
||||||
|
if opts is None:
|
||||||
|
raise NoSuchModuleError(f'Module {modulename!r} does not exist on this SEC-Node!')
|
||||||
|
pymodule = None
|
||||||
|
try: # pylint: disable=no-else-return
|
||||||
|
classname = opts.pop('cls')
|
||||||
|
if isinstance(classname, str):
|
||||||
|
pymodule = classname.rpartition('.')[0]
|
||||||
|
if pymodule in self.failed_modules:
|
||||||
|
# creation has failed already once, do not try again
|
||||||
|
return None
|
||||||
|
cls = get_class(classname)
|
||||||
|
else:
|
||||||
|
pymodule = classname.__module__
|
||||||
|
if pymodule in self.failed_modules:
|
||||||
|
# creation has failed already once, do not try again
|
||||||
|
return None
|
||||||
|
cls = classname
|
||||||
|
except Exception as e:
|
||||||
|
if str(e) == 'no such class':
|
||||||
|
self.errors.append(f'{classname} not found')
|
||||||
|
else:
|
||||||
|
self.failed_modules.add(pymodule)
|
||||||
|
if self.traceback_counter == 0:
|
||||||
|
self.log.exception(traceback.format_exc())
|
||||||
|
self.traceback_counter += 1
|
||||||
|
self.errors.append(f'error importing {classname}')
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
modobj = cls(modulename, self.log.getChild(modulename), opts, self.srv)
|
||||||
|
except ConfigError as e:
|
||||||
|
self.errors.append(f'error creating module {modulename}:')
|
||||||
|
for errtxt in e.args[0] if isinstance(e.args[0], list) else [e.args[0]]:
|
||||||
|
self.errors.append(' ' + errtxt)
|
||||||
|
modobj = None
|
||||||
|
except Exception as e:
|
||||||
|
if self.traceback_counter == 0:
|
||||||
|
self.log.exception(traceback.format_exc())
|
||||||
|
self.traceback_counter += 1
|
||||||
|
self.errors.append(f'error creating {modulename}')
|
||||||
|
modobj = None
|
||||||
|
if modobj:
|
||||||
|
self.register_module(modobj, modulename, modobj.export)
|
||||||
|
self.srv.modules[modulename] = modobj # IS HERE THE CORRECT PLACE?
|
||||||
|
return modobj
|
||||||
|
|
||||||
def remove_module(self, modulename_or_obj):
|
def remove_module(self, modulename_or_obj):
|
||||||
moduleobj = self.get_module(modulename_or_obj)
|
moduleobj = self.get_module(modulename_or_obj)
|
||||||
@ -183,6 +274,7 @@ class Dispatcher:
|
|||||||
|
|
||||||
def get_descriptive_data(self, specifier):
|
def get_descriptive_data(self, specifier):
|
||||||
"""returns a python object which upon serialisation results in the descriptive data"""
|
"""returns a python object which upon serialisation results in the descriptive data"""
|
||||||
|
specifier = specifier or ''
|
||||||
modules = {}
|
modules = {}
|
||||||
result = {'modules': modules}
|
result = {'modules': modules}
|
||||||
for modulename in self._export:
|
for modulename in self._export:
|
||||||
@ -194,7 +286,7 @@ class Dispatcher:
|
|||||||
mod_desc.update(module.exportProperties())
|
mod_desc.update(module.exportProperties())
|
||||||
mod_desc.pop('export', False)
|
mod_desc.pop('export', False)
|
||||||
modules[modulename] = mod_desc
|
modules[modulename] = mod_desc
|
||||||
modname, _, pname = (specifier or '').partition(':')
|
modname, _, pname = specifier.partition(':')
|
||||||
if modname in modules: # extension to SECoP standard: description of a single module
|
if modname in modules: # extension to SECoP standard: description of a single module
|
||||||
result = modules[modname]
|
result = modules[modname]
|
||||||
if pname in result['accessibles']: # extension to SECoP standard: description of a single accessible
|
if pname in result['accessibles']: # extension to SECoP standard: description of a single accessible
|
||||||
|
203
frappy/server.py
203
frappy/server.py
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# This program is free software; you can redistribute it and/or modify it under
|
||||||
@ -24,16 +23,16 @@
|
|||||||
"""Define helpers"""
|
"""Define helpers"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import signal
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from frappy.errors import ConfigError, SECoPError
|
from frappy.config import load_config
|
||||||
from frappy.lib import formatException, get_class, generalConfig
|
from frappy.errors import ConfigError
|
||||||
|
from frappy.dynamic import Pinata
|
||||||
|
from frappy.lib import formatException, generalConfig, get_class, mkthread
|
||||||
from frappy.lib.multievent import MultiEvent
|
from frappy.lib.multievent import MultiEvent
|
||||||
from frappy.params import PREDEFINED_ACCESSIBLES
|
from frappy.params import PREDEFINED_ACCESSIBLES
|
||||||
from frappy.modules import Attached
|
|
||||||
from frappy.config import load_config
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from daemon import DaemonContext
|
from daemon import DaemonContext
|
||||||
@ -106,6 +105,12 @@ class Server:
|
|||||||
|
|
||||||
self._cfgfiles = cfgfiles
|
self._cfgfiles = cfgfiles
|
||||||
self._pidfile = os.path.join(generalConfig.piddir, name + '.pid')
|
self._pidfile = os.path.join(generalConfig.piddir, name + '.pid')
|
||||||
|
signal.signal(signal.SIGINT, self.signal_handler)
|
||||||
|
signal.signal(signal.SIGTERM, self.signal_handler)
|
||||||
|
|
||||||
|
def signal_handler(self, _num, _frame):
|
||||||
|
if hasattr(self, 'interface') and self.interface:
|
||||||
|
self.shutdown()
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
if not DaemonContext:
|
if not DaemonContext:
|
||||||
@ -127,17 +132,18 @@ class Server:
|
|||||||
return f"{cls.__name__} class don't know how to handle option(s): {', '.join(options)}"
|
return f"{cls.__name__} class don't know how to handle option(s): {', '.join(options)}"
|
||||||
|
|
||||||
def restart_hook(self):
|
def restart_hook(self):
|
||||||
pass
|
"""Actions to be done on restart. May be overridden by a subclass."""
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
global systemd # pylint: disable=global-statement
|
||||||
while self._restart:
|
while self._restart:
|
||||||
self._restart = False
|
self._restart = False
|
||||||
try:
|
try:
|
||||||
# TODO: make systemd notifications configurable
|
# TODO: make systemd notifications configurable
|
||||||
if systemd: # pylint: disable=used-before-assignment
|
if systemd:
|
||||||
systemd.daemon.notify("STATUS=initializing")
|
systemd.daemon.notify("STATUS=initializing")
|
||||||
except Exception:
|
except Exception:
|
||||||
systemd = None # pylint: disable=redefined-outer-name
|
systemd = None
|
||||||
try:
|
try:
|
||||||
self._processCfg()
|
self._processCfg()
|
||||||
if self._testonly:
|
if self._testonly:
|
||||||
@ -156,13 +162,27 @@ class Server:
|
|||||||
self.log.info('startup done, handling transport messages')
|
self.log.info('startup done, handling transport messages')
|
||||||
if systemd:
|
if systemd:
|
||||||
systemd.daemon.notify("READY=1\nSTATUS=accepting requests")
|
systemd.daemon.notify("READY=1\nSTATUS=accepting requests")
|
||||||
self.interface.serve_forever()
|
t = mkthread(self.interface.serve_forever)
|
||||||
self.interface.server_close()
|
# we wait here on the thread finishing, which means we got a
|
||||||
|
# signal to shut down or an exception was raised
|
||||||
|
# TODO: get the exception (and re-raise?)
|
||||||
|
t.join()
|
||||||
|
self.interface = None # fine due to the semantics of 'with'
|
||||||
|
# server_close() called by 'with'
|
||||||
|
|
||||||
|
self.log.info(f'stopped listenning, cleaning up'
|
||||||
|
f' {len(self.modules)} modules')
|
||||||
|
# if systemd:
|
||||||
|
# if self._restart:
|
||||||
|
# systemd.daemon.notify('RELOADING=1')
|
||||||
|
# else:
|
||||||
|
# systemd.daemon.notify('STOPPING=1')
|
||||||
|
for name in self._getSortedModules():
|
||||||
|
self.modules[name].shutdownModule()
|
||||||
if self._restart:
|
if self._restart:
|
||||||
self.restart_hook()
|
self.restart_hook()
|
||||||
self.log.info('restart')
|
self.log.info('restarting')
|
||||||
else:
|
self.log.info('shut down')
|
||||||
self.log.info('shut down')
|
|
||||||
|
|
||||||
def restart(self):
|
def restart(self):
|
||||||
if not self._restart:
|
if not self._restart:
|
||||||
@ -174,85 +194,57 @@ class Server:
|
|||||||
self.interface.shutdown()
|
self.interface.shutdown()
|
||||||
|
|
||||||
def _processCfg(self):
|
def _processCfg(self):
|
||||||
|
"""Processes the module configuration.
|
||||||
|
|
||||||
|
All modules specified in the config file and read recursively from
|
||||||
|
Pinata class Modules are instantiated, initialized and started by the
|
||||||
|
end of this function.
|
||||||
|
If there are errors that occur, they will be collected and emitted
|
||||||
|
together in the end.
|
||||||
|
"""
|
||||||
errors = []
|
errors = []
|
||||||
opts = dict(self.node_cfg)
|
opts = dict(self.node_cfg)
|
||||||
cls = get_class(opts.pop('cls'))
|
cls = get_class(opts.pop('cls'))
|
||||||
self.dispatcher = cls(opts.pop('name', self._cfgfiles), self.log.getChild('dispatcher'), opts, self)
|
self.dispatcher = cls(opts.pop('name', self._cfgfiles),
|
||||||
|
self.log.getChild('dispatcher'), opts, self)
|
||||||
|
|
||||||
if opts:
|
if opts:
|
||||||
errors.append(self.unknown_options(cls, opts))
|
self.dispatcher.errors.append(self.unknown_options(cls, opts))
|
||||||
self.modules = OrderedDict()
|
self.modules = OrderedDict()
|
||||||
failure_traceback = None # traceback for the first error
|
|
||||||
failed = set() # python modules failed to load
|
|
||||||
self.lastError = None
|
|
||||||
for modname, options in self.module_cfg.items():
|
|
||||||
opts = dict(options)
|
|
||||||
pymodule = None
|
|
||||||
try:
|
|
||||||
classname = opts.pop('cls')
|
|
||||||
pymodule = classname.rpartition('.')[0]
|
|
||||||
if pymodule in failed:
|
|
||||||
continue
|
|
||||||
cls = get_class(classname)
|
|
||||||
except Exception as e:
|
|
||||||
if str(e) == 'no such class':
|
|
||||||
errors.append(f'{classname} not found')
|
|
||||||
else:
|
|
||||||
failed.add(pymodule)
|
|
||||||
if failure_traceback is None:
|
|
||||||
failure_traceback = traceback.format_exc()
|
|
||||||
errors.append(f'error importing {classname}')
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
modobj = cls(modname, self.log.getChild(modname), opts, self)
|
|
||||||
self.modules[modname] = modobj
|
|
||||||
except ConfigError as e:
|
|
||||||
errors.append(f'error creating module {modname}:')
|
|
||||||
for errtxt in e.args[0] if isinstance(e.args[0], list) else [e.args[0]]:
|
|
||||||
errors.append(' ' + errtxt)
|
|
||||||
except Exception:
|
|
||||||
if failure_traceback is None:
|
|
||||||
failure_traceback = traceback.format_exc()
|
|
||||||
errors.append(f'error creating {modname}')
|
|
||||||
|
|
||||||
missing_super = set()
|
# create and initialize modules
|
||||||
# all objs created, now start them up and interconnect
|
todos = list(self.module_cfg.items())
|
||||||
for modname, modobj in self.modules.items():
|
while todos:
|
||||||
self.log.info('registering module %r', modname)
|
modname, options = todos.pop(0)
|
||||||
self.dispatcher.register_module(modobj, modname, modobj.export)
|
if modname in self.modules:
|
||||||
# also call earlyInit on the modules
|
# already created by Dispatcher (via Attached)
|
||||||
modobj.earlyInit()
|
continue
|
||||||
if not modobj.earlyInitDone:
|
# For Pinata modules: we need to access this in Dispatcher.get_module
|
||||||
missing_super.add(f'{modobj.earlyInit.__qualname__} was not called, probably missing super call')
|
self.module_cfg[modname] = dict(options)
|
||||||
|
modobj = self.dispatcher.get_module_instance(modname) # lazy
|
||||||
|
if modobj is None:
|
||||||
|
self.log.debug('Module %s returned None', modname)
|
||||||
|
continue
|
||||||
|
self.modules[modname] = modobj
|
||||||
|
if isinstance(modobj, Pinata):
|
||||||
|
# scan for dynamic devices
|
||||||
|
pinata = self.dispatcher.get_module(modname)
|
||||||
|
pinata_modules = list(pinata.scanModules())
|
||||||
|
for name, _cfg in pinata_modules:
|
||||||
|
if name in self.module_cfg:
|
||||||
|
self.log.error('Module %s, from pinata %s, already'
|
||||||
|
' exists in config file!', name, modname)
|
||||||
|
self.log.info('Pinata %s found %d modules', modname, len(pinata_modules))
|
||||||
|
todos.extend(pinata_modules)
|
||||||
|
|
||||||
# handle attached modules
|
# initialize all modules by getting them with Dispatcher.get_module,
|
||||||
for modname, modobj in self.modules.items():
|
# which is done in the get_descriptive data
|
||||||
attached_modules = {}
|
# TODO: caching, to not make this extra work
|
||||||
for propname, propobj in modobj.propertyDict.items():
|
self.dispatcher.get_descriptive_data('')
|
||||||
if isinstance(propobj, Attached):
|
# =========== All modules are initialized ===========
|
||||||
try:
|
|
||||||
attname = getattr(modobj, propname)
|
|
||||||
if attname: # attached module specified in cfg file
|
|
||||||
attobj = self.dispatcher.get_module(attname)
|
|
||||||
if isinstance(attobj, propobj.basecls):
|
|
||||||
attached_modules[propname] = attobj
|
|
||||||
else:
|
|
||||||
errors.append(f'attached module {propname}={attname!r} '\
|
|
||||||
f'must inherit from {propobj.basecls.__qualname__!r}')
|
|
||||||
except SECoPError as e:
|
|
||||||
errors.append(f'module {modname}, attached {propname}: {str(e)}')
|
|
||||||
modobj.attachedModules = attached_modules
|
|
||||||
|
|
||||||
# call init on each module after registering all
|
# all errors from initialization process
|
||||||
for modname, modobj in self.modules.items():
|
errors = self.dispatcher.errors
|
||||||
try:
|
|
||||||
modobj.initModule()
|
|
||||||
if not modobj.initModuleDone:
|
|
||||||
missing_super.add(f'{modobj.initModule.__qualname__} was not called, probably missing super call')
|
|
||||||
except Exception as e:
|
|
||||||
if failure_traceback is None:
|
|
||||||
failure_traceback = traceback.format_exc()
|
|
||||||
errors.append(f'error initializing {modname}: {e!r}')
|
|
||||||
|
|
||||||
if not self._testonly:
|
if not self._testonly:
|
||||||
start_events = MultiEvent(default_timeout=30)
|
start_events = MultiEvent(default_timeout=30)
|
||||||
@ -261,8 +253,7 @@ class Server:
|
|||||||
start_events.name = f'module {modname}'
|
start_events.name = f'module {modname}'
|
||||||
modobj.startModule(start_events)
|
modobj.startModule(start_events)
|
||||||
if not modobj.startModuleDone:
|
if not modobj.startModuleDone:
|
||||||
missing_super.add(f'{modobj.startModule.__qualname__} was not called, probably missing super call')
|
errors.append(f'{modobj.startModule.__qualname__} was not called, probably missing super call')
|
||||||
errors.extend(missing_super)
|
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
for errtxt in errors:
|
for errtxt in errors:
|
||||||
@ -271,8 +262,6 @@ class Server:
|
|||||||
# print a list of config errors to stderr
|
# print a list of config errors to stderr
|
||||||
sys.stderr.write('\n'.join(errors))
|
sys.stderr.write('\n'.join(errors))
|
||||||
sys.stderr.write('\n')
|
sys.stderr.write('\n')
|
||||||
if failure_traceback:
|
|
||||||
sys.stderr.write(failure_traceback)
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if self._testonly:
|
if self._testonly:
|
||||||
@ -299,3 +288,41 @@ class Server:
|
|||||||
# history_path = os.environ.get('ALTERNATIVE_HISTORY')
|
# history_path = os.environ.get('ALTERNATIVE_HISTORY')
|
||||||
# if history_path:
|
# if history_path:
|
||||||
# from frappy_<xx>.historywriter import ... etc.
|
# from frappy_<xx>.historywriter import ... etc.
|
||||||
|
|
||||||
|
def _getSortedModules(self):
|
||||||
|
"""Sort modules topologically by inverse dependency.
|
||||||
|
|
||||||
|
Example: if there is an IO device A and module B depends on it, then
|
||||||
|
the result will be [B, A].
|
||||||
|
Right now, if the dependency graph is not a DAG, we give up and return
|
||||||
|
the unvisited nodes to be dismantled at the end.
|
||||||
|
Taken from Introduction to Algorithms [CLRS].
|
||||||
|
"""
|
||||||
|
def go(name):
|
||||||
|
if name in done: # visiting a node
|
||||||
|
return True
|
||||||
|
if name in visited:
|
||||||
|
visited.add(name)
|
||||||
|
return False # cycle in dependencies -> fail
|
||||||
|
visited.add(name)
|
||||||
|
if name in unmarked:
|
||||||
|
unmarked.remove(name)
|
||||||
|
for module in self.modules[name].attachedModules.values():
|
||||||
|
res = go(module.name)
|
||||||
|
if not res:
|
||||||
|
return False
|
||||||
|
visited.remove(name)
|
||||||
|
done.add(name)
|
||||||
|
l.append(name)
|
||||||
|
return True
|
||||||
|
|
||||||
|
unmarked = set(self.modules.keys()) # unvisited nodes
|
||||||
|
visited = set() # visited in DFS, but not completed
|
||||||
|
done = set()
|
||||||
|
l = [] # list of sorted modules
|
||||||
|
|
||||||
|
while unmarked:
|
||||||
|
if not go(unmarked.pop()):
|
||||||
|
self.log.error('cyclical dependency between modules!')
|
||||||
|
return l[::-1] + list(visited) + list(unmarked)
|
||||||
|
return l[::-1]
|
||||||
|
164
frappy/structparam.py
Normal file
164
frappy/structparam.py
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
# *****************************************************************************
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License as published by the Free Software
|
||||||
|
# Foundation; either version 2 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
# Module authors:
|
||||||
|
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||||
|
#
|
||||||
|
# *****************************************************************************
|
||||||
|
"""convenience class to create a struct Parameter together with indivdual params
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
class Controller(Drivable):
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
ctrlpars = StructParam('ctrlpars struct', [
|
||||||
|
('pid_p', 'p', Parameter('control parameter p', FloatRange())),
|
||||||
|
('pid_i', 'i', Parameter('control parameter i', FloatRange())),
|
||||||
|
('pid_d', 'd', Parameter('control parameter d', FloatRange())),
|
||||||
|
], readonly=False)
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
then implement either read_ctrlpars and write_ctrlpars or
|
||||||
|
read_pid_p, read_pid_i, read_pid_d, write_pid_p, write_pid_i and write_pid_d
|
||||||
|
|
||||||
|
the methods not implemented will be created automatically
|
||||||
|
"""
|
||||||
|
|
||||||
|
from frappy.core import Parameter, Property
|
||||||
|
from frappy.datatypes import BoolType, DataType, StructOf, ValueType
|
||||||
|
from frappy.errors import ProgrammingError
|
||||||
|
|
||||||
|
|
||||||
|
class StructParam(Parameter):
|
||||||
|
"""create a struct parameter together with individual parameters
|
||||||
|
|
||||||
|
in addition to normal Parameter arguments:
|
||||||
|
|
||||||
|
:param paramdict: dict <member name> of Parameter(...)
|
||||||
|
:param prefix_or_map: either a prefix for the parameter name to add to the member name
|
||||||
|
or a dict <member name> or <paramerter name>
|
||||||
|
"""
|
||||||
|
# use properties, as simple attributes are not considered on copy()
|
||||||
|
paramdict = Property('dict <parametername> of Parameter(...)', ValueType())
|
||||||
|
hasStructRW = Property('has a read_<struct param> or write_<struct param> method',
|
||||||
|
BoolType(), default=False)
|
||||||
|
|
||||||
|
insideRW = 0 # counter for avoiding multiple superfluous updates
|
||||||
|
|
||||||
|
def __init__(self, description=None, paramdict=None, prefix_or_map='', *, datatype=None, readonly=False, **kwds):
|
||||||
|
if isinstance(paramdict, DataType):
|
||||||
|
raise ProgrammingError('second argument must be a dict of Param')
|
||||||
|
if datatype is None and paramdict is not None: # omit the following on Parameter.copy()
|
||||||
|
if isinstance(prefix_or_map, str):
|
||||||
|
prefix_or_map = {m: prefix_or_map + m for m in paramdict}
|
||||||
|
for membername, param in paramdict.items():
|
||||||
|
param.name = prefix_or_map[membername]
|
||||||
|
datatype = StructOf(**{m: p.datatype for m, p in paramdict.items()})
|
||||||
|
kwds['influences'] = [p.name for p in paramdict.values()]
|
||||||
|
self.updateEnable = {}
|
||||||
|
super().__init__(description, datatype, paramdict=paramdict, readonly=readonly, **kwds)
|
||||||
|
|
||||||
|
def __set_name__(self, owner, name):
|
||||||
|
# names of access methods of structed param (e.g. ctrlpars)
|
||||||
|
struct_read_name = f'read_{name}' # e.g. 'read_ctrlpars'
|
||||||
|
struct_write_name = f'write_{name}' # e.h. 'write_ctrlpars'
|
||||||
|
self.hasStructRW = hasattr(owner, struct_read_name) or hasattr(owner, struct_write_name)
|
||||||
|
|
||||||
|
for membername, param in self.paramdict.items():
|
||||||
|
pname = param.name
|
||||||
|
changes = {
|
||||||
|
'readonly': self.readonly,
|
||||||
|
'influences': set(param.influences) | {name},
|
||||||
|
}
|
||||||
|
param.ownProperties.update(changes)
|
||||||
|
param.init(changes)
|
||||||
|
setattr(owner, pname, param)
|
||||||
|
param.__set_name__(owner, param.name)
|
||||||
|
|
||||||
|
if self.hasStructRW:
|
||||||
|
rname = f'read_{pname}'
|
||||||
|
|
||||||
|
if not hasattr(owner, rname):
|
||||||
|
def rfunc(self, membername=membername, struct_read_name=struct_read_name):
|
||||||
|
return getattr(self, struct_read_name)()[membername]
|
||||||
|
|
||||||
|
rfunc.poll = False # read_<struct param> is polled only
|
||||||
|
setattr(owner, rname, rfunc)
|
||||||
|
|
||||||
|
if not self.readonly:
|
||||||
|
wname = f'write_{pname}'
|
||||||
|
if not hasattr(owner, wname):
|
||||||
|
def wfunc(self, value, membername=membername,
|
||||||
|
name=name, rname=rname, struct_write_name=struct_write_name):
|
||||||
|
valuedict = dict(getattr(self, name))
|
||||||
|
valuedict[membername] = value
|
||||||
|
getattr(self, struct_write_name)(valuedict)
|
||||||
|
return getattr(self, rname)()
|
||||||
|
|
||||||
|
setattr(owner, wname, wfunc)
|
||||||
|
|
||||||
|
if not self.hasStructRW:
|
||||||
|
if not hasattr(owner, struct_read_name):
|
||||||
|
def struct_read_func(self, name=name, flist=tuple(
|
||||||
|
(m, f'read_{p.name}') for m, p in self.paramdict.items())):
|
||||||
|
pobj = self.parameters[name]
|
||||||
|
# disable updates generated from the callbacks of individual params
|
||||||
|
pobj.insideRW += 1 # guarded by self.accessLock
|
||||||
|
try:
|
||||||
|
return {m: getattr(self, f)() for m, f in flist}
|
||||||
|
finally:
|
||||||
|
pobj.insideRW -= 1
|
||||||
|
|
||||||
|
setattr(owner, struct_read_name, struct_read_func)
|
||||||
|
|
||||||
|
if not (self.readonly or hasattr(owner, struct_write_name)):
|
||||||
|
|
||||||
|
def struct_write_func(self, value, name=name, funclist=tuple(
|
||||||
|
(m, f'write_{p.name}') for m, p in self.paramdict.items())):
|
||||||
|
pobj = self.parameters[name]
|
||||||
|
pobj.insideRW += 1 # guarded by self.accessLock
|
||||||
|
try:
|
||||||
|
return {m: getattr(self, f)(value[m]) for m, f in funclist}
|
||||||
|
finally:
|
||||||
|
pobj.insideRW -= 1
|
||||||
|
|
||||||
|
setattr(owner, struct_write_name, struct_write_func)
|
||||||
|
|
||||||
|
super().__set_name__(owner, name)
|
||||||
|
|
||||||
|
def finish(self, modobj=None):
|
||||||
|
"""register callbacks for consistency"""
|
||||||
|
super().finish(modobj)
|
||||||
|
if modobj:
|
||||||
|
|
||||||
|
if self.hasStructRW:
|
||||||
|
def cb(value, modobj=modobj, structparam=self):
|
||||||
|
for membername, param in structparam.paramdict.items():
|
||||||
|
setattr(modobj, param.name, value[membername])
|
||||||
|
|
||||||
|
modobj.valueCallbacks[self.name].append(cb)
|
||||||
|
else:
|
||||||
|
for membername, param in self.paramdict.items():
|
||||||
|
def cb(value, modobj=modobj, structparam=self, membername=membername):
|
||||||
|
if not structparam.insideRW:
|
||||||
|
prev = dict(getattr(modobj, structparam.name))
|
||||||
|
prev[membername] = value
|
||||||
|
setattr(modobj, structparam.name, prev)
|
||||||
|
|
||||||
|
modobj.valueCallbacks[param.name].append(cb)
|
@ -70,12 +70,11 @@ def get_version(abbrev=4):
|
|||||||
if git_version != release_version:
|
if git_version != release_version:
|
||||||
write_release_version(git_version)
|
write_release_version(git_version)
|
||||||
return git_version
|
return git_version
|
||||||
elif release_version:
|
if release_version:
|
||||||
return release_version
|
return release_version
|
||||||
else:
|
raise ValueError('Cannot find a version number - make sure that '
|
||||||
raise ValueError('Cannot find a version number - make sure that '
|
'git is installed or a RELEASE-VERSION file is '
|
||||||
'git is installed or a RELEASE-VERSION file is '
|
'present!')
|
||||||
'present!')
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@ -354,7 +354,7 @@ class Cryostat(CryoBase):
|
|||||||
timestamp = t
|
timestamp = t
|
||||||
self.read_value()
|
self.read_value()
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdownModule(self):
|
||||||
# should be called from server when the server is stopped
|
# should be called from server when the server is stopped
|
||||||
self._stopflag = True
|
self._stopflag = True
|
||||||
if self._thread and self._thread.is_alive():
|
if self._thread and self._thread.is_alive():
|
||||||
|
@ -24,10 +24,33 @@
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
from frappy.datatypes import FloatRange, StringType, ValueType, TupleOf, StructOf, ArrayOf
|
from frappy.datatypes import FloatRange, StringType, ValueType, TupleOf, StructOf, ArrayOf
|
||||||
from frappy.modules import Communicator, Drivable, Parameter, Property, Readable, Module
|
from frappy.modules import Communicator, Drivable, Parameter, Property, Readable, Module, Attached
|
||||||
from frappy.params import Command
|
from frappy.params import Command
|
||||||
|
from frappy.dynamic import Pinata
|
||||||
from frappy.errors import RangeError
|
from frappy.errors import RangeError
|
||||||
|
|
||||||
|
class Pin(Pinata):
|
||||||
|
def scanModules(self):
|
||||||
|
yield ('pin_a', {'cls': LN2, 'description':'hi'})
|
||||||
|
yield ('pin_b', {'cls': LN2, 'description':'hi'})
|
||||||
|
|
||||||
|
class RecPin(Pinata):
|
||||||
|
def scanModules(self):
|
||||||
|
yield ('rec_a', {'cls': RecPinInner, 'description':'hi'})
|
||||||
|
yield ('rec_b', {'cls': RecPinInner, 'description':'hi'})#, 'idx':'_2'})
|
||||||
|
|
||||||
|
class RecPinInner(Pinata):
|
||||||
|
idx = Property('', StringType(), default='')
|
||||||
|
def scanModules(self):
|
||||||
|
yield ('pin_pin_a' + self.idx, {'cls': Mapped, 'description':'recursive!', 'choices':['A', 'B']})
|
||||||
|
yield ('pin_pin_b' + self.idx, {'cls': Mapped, 'description':'recursive!', 'choices':['A', 'B']})
|
||||||
|
|
||||||
|
|
||||||
|
class WithAtt(Readable):
|
||||||
|
att = Attached()
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
return self.att.read_value()
|
||||||
|
|
||||||
class LN2(Readable):
|
class LN2(Readable):
|
||||||
"""Just a readable.
|
"""Just a readable.
|
||||||
|
363
frappy_mlz/plc_zapf.py
Normal file
363
frappy_mlz/plc_zapf.py
Normal file
@ -0,0 +1,363 @@
|
|||||||
|
# *****************************************************************************
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License as published by the Free Software
|
||||||
|
# Foundation; either version 2 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
# Module authors:
|
||||||
|
# Alexander Zaft <a.zaft@fz-juelich.de>
|
||||||
|
#
|
||||||
|
# *****************************************************************************
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
import zapf
|
||||||
|
import zapf.spec as zspec
|
||||||
|
from zapf.io import PlcIO
|
||||||
|
from zapf.scan import Scanner
|
||||||
|
|
||||||
|
from frappy.core import BUSY, DISABLED, ERROR, FINALIZING, IDLE, \
|
||||||
|
INITIALIZING, STARTING, UNKNOWN, WARN, Attached, Command, Communicator, \
|
||||||
|
Drivable, Parameter, Property, Readable
|
||||||
|
from frappy.datatypes import UNLIMITED, ArrayOf, BLOBType, EnumType, \
|
||||||
|
FloatRange, IntRange, StatusType, StringType, ValueType
|
||||||
|
from frappy.dynamic import Pinata
|
||||||
|
from frappy.errors import CommunicationFailedError, ImpossibleError, \
|
||||||
|
IsBusyError, NoSuchParameterError, ReadOnlyError
|
||||||
|
|
||||||
|
# Untested with real hardware, only testplc_2021_09.py
|
||||||
|
|
||||||
|
|
||||||
|
def internalize_name(name):
|
||||||
|
return re.sub(r'[^a-zA-Z0-9_]+', '_', name, re.ASCII)
|
||||||
|
|
||||||
|
|
||||||
|
ERROR_MAP = {
|
||||||
|
# should not happen. but better to have it here anyway
|
||||||
|
5: NoSuchParameterError,
|
||||||
|
# if this occurs, something may have gone wrong with digesting the scanner
|
||||||
|
# data
|
||||||
|
6: ReadOnlyError,
|
||||||
|
# Most likely from devices you cannot poll when busy.
|
||||||
|
7: IsBusyError,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ZapfPinata(Pinata):
|
||||||
|
"""The Pinata device for a PLC that can be accessed according to PILS.
|
||||||
|
|
||||||
|
See https://forge.frm2.tum.de/public/doc/plc/master/html/
|
||||||
|
|
||||||
|
Instantiates the classes with the base mapped class, which will be replaced
|
||||||
|
by initModule, so modules can also be configured manually in the config
|
||||||
|
file.
|
||||||
|
"""
|
||||||
|
iodev = Property('Connection to PLC', StringType())
|
||||||
|
|
||||||
|
def scanModules(self):
|
||||||
|
try:
|
||||||
|
self._plcio = PlcIO(self.iodev, self.log)
|
||||||
|
except zapf.CommError as e:
|
||||||
|
raise CommunicationFailedError('could not connect to plc') from e
|
||||||
|
scanner = Scanner(self._plcio, self.log)
|
||||||
|
for devinfo in scanner.scan_devices():
|
||||||
|
if zspec.LOWLEVEL in devinfo.info.get('flags'):
|
||||||
|
self.log.debug('device %d (%s) is lowlevel, skipping',
|
||||||
|
devinfo.number, devinfo.name)
|
||||||
|
continue
|
||||||
|
device = scanner.get_device(devinfo)
|
||||||
|
if device is None:
|
||||||
|
self.log.info(f'{devinfo.name} unsupported')
|
||||||
|
continue
|
||||||
|
basecls = CLS_MAP.get(device.__class__, None)
|
||||||
|
if basecls is None:
|
||||||
|
self.log.info('No mapping found for %s, (class %s)',
|
||||||
|
devinfo.name, device.__class__.__name__)
|
||||||
|
continue
|
||||||
|
mod_cls = basecls.makeModuleClass(device, devinfo)
|
||||||
|
config = {
|
||||||
|
'cls': mod_cls,
|
||||||
|
'plcio': device,
|
||||||
|
'description': devinfo.info['description'],
|
||||||
|
'plc_name': devinfo.name,
|
||||||
|
'_pinata': self.name,
|
||||||
|
}
|
||||||
|
if devinfo.info['basetype'] != 'enum' \
|
||||||
|
and not issubclass(basecls, PLCCommunicator):
|
||||||
|
config['value'] = {
|
||||||
|
# internal limit here is 2**64, zapf reports 2**128
|
||||||
|
'min': max(devinfo.info['absmin'], -UNLIMITED),
|
||||||
|
'max': min(devinfo.info['absmax'], UNLIMITED),
|
||||||
|
}
|
||||||
|
if devinfo.info['access'] == 'rw':
|
||||||
|
config['target'] = {
|
||||||
|
'min': config['value']['min'],
|
||||||
|
'max': config['value']['max'],
|
||||||
|
}
|
||||||
|
name = internalize_name(devinfo.name)
|
||||||
|
yield (name, config)
|
||||||
|
self._plcio.start_cache()
|
||||||
|
|
||||||
|
def shutdownModule(self):
|
||||||
|
"""Shutdown the module, _plcio might be invalid after this. Needs to be
|
||||||
|
recreated by scanModules."""
|
||||||
|
self._plcio.stop_cache()
|
||||||
|
self._plcio.proto.disconnect()
|
||||||
|
|
||||||
|
|
||||||
|
STATUS_MAP = {
|
||||||
|
zspec.DevStatus.RESET: (INITIALIZING, 'resetting'),
|
||||||
|
zspec.DevStatus.IDLE: (IDLE, 'idle'),
|
||||||
|
zspec.DevStatus.DISABLED: (DISABLED, 'disabled'),
|
||||||
|
zspec.DevStatus.WARN: (WARN, 'warning'),
|
||||||
|
zspec.DevStatus.START: (STARTING, 'starting'),
|
||||||
|
zspec.DevStatus.BUSY: (BUSY, 'busy'),
|
||||||
|
zspec.DevStatus.STOP: (FINALIZING, 'stopping'),
|
||||||
|
zspec.DevStatus.ERROR: (ERROR, 'error (please reset)'),
|
||||||
|
zspec.DevStatus.DIAGNOSTIC_ERROR: (ERROR, 'hard error (please check plc)'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PLCBase:
|
||||||
|
status = Parameter(datatype=StatusType(Drivable, 'INITIALIZING',
|
||||||
|
'DISABLED', 'STARTING'))
|
||||||
|
status_code = Parameter('raw internal status code',
|
||||||
|
IntRange(0, 2**32-1))
|
||||||
|
plcio = Property('plc io device', ValueType())
|
||||||
|
plc_name = Property('plc io device', StringType(), export=True)
|
||||||
|
_pinata = Attached(ZapfPinata) # TODO: make this automatic?
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def makeModuleClass(cls, device, devinfo):
|
||||||
|
# add parameters and commands according to device info
|
||||||
|
add_members = {}
|
||||||
|
# set correct enums for value/target
|
||||||
|
if devinfo.info['basetype'] == 'enum':
|
||||||
|
rmap = {v: k for k, v in devinfo.info['enum_r'].items()}
|
||||||
|
read_enum = EnumType(rmap)
|
||||||
|
add_members['value'] = Parameter(datatype=read_enum)
|
||||||
|
if hasattr(cls, 'target'):
|
||||||
|
#wmap = {k:v for k, v in devinfo.info['enum_w'].items()}
|
||||||
|
#write_enum = EnumType(wmap)
|
||||||
|
write_enum = EnumType(devinfo.info['enum_w'])
|
||||||
|
add_members['target'] = Parameter(datatype=write_enum)
|
||||||
|
|
||||||
|
for parameter in device.list_params():
|
||||||
|
info = devinfo.info['params'][parameter]
|
||||||
|
iname = internalize_name(parameter)
|
||||||
|
readonly = info.get('access', 'ro') != 'rw'
|
||||||
|
dataty = cls._map_datatype(info)
|
||||||
|
if dataty is None:
|
||||||
|
continue
|
||||||
|
param = Parameter(info['description'],
|
||||||
|
dataty,
|
||||||
|
readonly=readonly)
|
||||||
|
|
||||||
|
def read_param(self, parameter=parameter):
|
||||||
|
code, val = self.plcio.get_param_raw(parameter)
|
||||||
|
if code > 4:
|
||||||
|
raise ERROR_MAP[code](f'Error when reading parameter'
|
||||||
|
f'{parameter}: {code}')
|
||||||
|
return val
|
||||||
|
|
||||||
|
def write_param(self, value, parameter=parameter):
|
||||||
|
code, val = self.plcio.set_param_raw(parameter, value)
|
||||||
|
if code > 4:
|
||||||
|
raise ERROR_MAP[code](f'Error when setting parameter'
|
||||||
|
f'{parameter} to {value!r}: {code}')
|
||||||
|
return val
|
||||||
|
|
||||||
|
# enums can have asymmetric read and write variants. this should be
|
||||||
|
# checked
|
||||||
|
if info['basetype'] == 'enum':
|
||||||
|
allowed = frozenset(info['enum_w'].values())
|
||||||
|
#pylint: disable=function-redefined
|
||||||
|
def write_param(self, value, allowed=allowed, parameter=parameter):
|
||||||
|
if value not in allowed:
|
||||||
|
raise ValueError(f'Invalid value for writing'
|
||||||
|
f' {parameter}: {value!r}')
|
||||||
|
|
||||||
|
code, val = self.plcio.set_param_raw(parameter, value)
|
||||||
|
if code > 4:
|
||||||
|
raise ERROR_MAP[code](f'Error when setting parameter'
|
||||||
|
f'{parameter} to {value!r}: {code}')
|
||||||
|
return val
|
||||||
|
|
||||||
|
add_members[iname] = param
|
||||||
|
add_members['read_' + iname] = read_param
|
||||||
|
if readonly:
|
||||||
|
continue
|
||||||
|
add_members['write_' + iname] = write_param
|
||||||
|
|
||||||
|
for command in device.list_funcs():
|
||||||
|
info = devinfo.info['funcs'][command]
|
||||||
|
iname = internalize_name(command)
|
||||||
|
if info['argument']:
|
||||||
|
arg = cls._map_datatype(info['argument'])
|
||||||
|
else:
|
||||||
|
arg = None
|
||||||
|
if info['result']:
|
||||||
|
result = cls._map_datatype(info['result'])
|
||||||
|
else:
|
||||||
|
result = None
|
||||||
|
def exec_command(self, arg=None, command=command):
|
||||||
|
# TODO: commands return <err/succ>, <result>
|
||||||
|
return self.plcio.exec_func(command, arg)
|
||||||
|
decorator = Command(arg,
|
||||||
|
result = result,
|
||||||
|
description=info['description'],
|
||||||
|
)
|
||||||
|
|
||||||
|
func = decorator(exec_command)
|
||||||
|
add_members['call_' + iname] = func
|
||||||
|
if not add_members:
|
||||||
|
return cls
|
||||||
|
new_name = '_' + cls.__name__ + '_' \
|
||||||
|
+ internalize_name("blub")
|
||||||
|
return type(new_name, (cls,), add_members)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _map_datatype(cls, info):
|
||||||
|
dataty = info['basetype']
|
||||||
|
if dataty == 'int':
|
||||||
|
return IntRange(info['min_value'], info['max_value'])
|
||||||
|
if dataty == 'float':
|
||||||
|
return FloatRange(info['min_value'], info['max_value'])
|
||||||
|
if dataty == 'enum':
|
||||||
|
mapping = {v: k for k, v in info['enum_r'].items()}
|
||||||
|
return EnumType(mapping)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
state, reason, aux, err_id = self.plcio.read_status()
|
||||||
|
if state in STATUS_MAP:
|
||||||
|
status, m = STATUS_MAP[state]
|
||||||
|
else:
|
||||||
|
status, m = UNKNOWN, 'unknown state 0x%x' % state
|
||||||
|
msg = [m]
|
||||||
|
reason = zapf.spec.ReasonMap[reason]
|
||||||
|
if reason:
|
||||||
|
msg.append(reason)
|
||||||
|
if aux:
|
||||||
|
msg.append(self.plcio.decode_aux(aux))
|
||||||
|
if err_id:
|
||||||
|
msg.append(self.plcio.decode_errid(err_id))
|
||||||
|
return status, ', '.join(msg)
|
||||||
|
|
||||||
|
def read_status_code(self):
|
||||||
|
state, reason, aux, _ = self.plcio.read_status()
|
||||||
|
return state << 28 | reason << 24 | aux
|
||||||
|
|
||||||
|
@Command()
|
||||||
|
def stop(self):
|
||||||
|
"""Stop the operation of this module.
|
||||||
|
|
||||||
|
:raises:
|
||||||
|
ImpossibleError: if the command is called while the module is
|
||||||
|
not busy
|
||||||
|
"""
|
||||||
|
if not self.plcio.change_status((zapf.DevStatus.BUSY,),
|
||||||
|
zapf.DevStatus.STOP):
|
||||||
|
self.log.info('stop was called when device was not busy')
|
||||||
|
# TODO: off/on?
|
||||||
|
|
||||||
|
@Command()
|
||||||
|
def reset(self):
|
||||||
|
"""Tries to reset this module.
|
||||||
|
|
||||||
|
:raises:
|
||||||
|
ImpossibleError: when called while the module is not in an error
|
||||||
|
state.
|
||||||
|
"""
|
||||||
|
if not self.plcio.reset():
|
||||||
|
raise ImpossibleError('reset called when the device is not in'
|
||||||
|
'an error state!')
|
||||||
|
|
||||||
|
|
||||||
|
class PLCValue(PLCBase):
|
||||||
|
"""Base class for all but Communicator"""
|
||||||
|
def read_value(self):
|
||||||
|
return self.plcio.read_value_raw() # read_value maps enums on zapf side
|
||||||
|
|
||||||
|
def read_target(self):
|
||||||
|
return self.plcio.read_target_raw()
|
||||||
|
|
||||||
|
def write_target(self, value):
|
||||||
|
self.plcio.change_target_raw(value)
|
||||||
|
|
||||||
|
|
||||||
|
class PLCReadable(PLCValue, Readable):
|
||||||
|
"""Readable value, scanned from PLC."""
|
||||||
|
description = Property('the modules description',
|
||||||
|
datatype=StringType(isUTF8=True))
|
||||||
|
|
||||||
|
|
||||||
|
class PLCDrivable(PLCValue, Drivable):
|
||||||
|
"""Drivable, scanned from PLC."""
|
||||||
|
description = Property('the modules description',
|
||||||
|
datatype=StringType(isUTF8=True))
|
||||||
|
|
||||||
|
|
||||||
|
class PLCCommunicator(PLCBase, Communicator):
|
||||||
|
status = Parameter('current status of the module')
|
||||||
|
|
||||||
|
@Command(BLOBType(), result=BLOBType())
|
||||||
|
def communicate(self, command):
|
||||||
|
return self.plcio.communicate(command)
|
||||||
|
|
||||||
|
|
||||||
|
class Sensor(PLCReadable):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AnalogOutput(PLCDrivable):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DiscreteInput(PLCReadable):
|
||||||
|
value = Parameter(datatype=IntRange())
|
||||||
|
|
||||||
|
class DiscreteOutput(PLCDrivable):
|
||||||
|
value = Parameter(datatype=IntRange())
|
||||||
|
target = Parameter(datatype=IntRange())
|
||||||
|
|
||||||
|
|
||||||
|
class VectorInput(PLCReadable):
|
||||||
|
value = Parameter(datatype=ArrayOf(FloatRange()))
|
||||||
|
|
||||||
|
|
||||||
|
class VectorOutput(PLCDrivable):
|
||||||
|
value = Parameter(datatype=ArrayOf(FloatRange()))
|
||||||
|
target = Parameter(datatype=ArrayOf(FloatRange()))
|
||||||
|
|
||||||
|
|
||||||
|
CLS_MAP = {
|
||||||
|
zapf.device.SimpleDiscreteIn: DiscreteInput,
|
||||||
|
zapf.device.SimpleAnalogIn: Sensor,
|
||||||
|
zapf.device.Keyword: DiscreteOutput,
|
||||||
|
zapf.device.RealValue: AnalogOutput,
|
||||||
|
zapf.device.SimpleDiscreteOut: DiscreteOutput,
|
||||||
|
zapf.device.SimpleAnalogOut: PLCDrivable,
|
||||||
|
zapf.device.StatusWord: DiscreteInput,
|
||||||
|
zapf.device.DiscreteIn: DiscreteInput,
|
||||||
|
zapf.device.AnalogIn: Sensor,
|
||||||
|
zapf.device.DiscreteOut: DiscreteOutput,
|
||||||
|
zapf.device.AnalogOut: PLCDrivable,
|
||||||
|
zapf.device.FlatIn: Sensor,
|
||||||
|
zapf.device.FlatOut: AnalogOutput,
|
||||||
|
zapf.device.ParamIn: Sensor,
|
||||||
|
zapf.device.ParamOut: AnalogOutput,
|
||||||
|
zapf.device.VectorIn: VectorInput,
|
||||||
|
zapf.device.VectorOut: VectorOutput,
|
||||||
|
zapf.device.MessageIO: PLCCommunicator,
|
||||||
|
}
|
234
frappy_mlz/seop.py
Normal file
234
frappy_mlz/seop.py
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# *****************************************************************************
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License as published by the Free Software
|
||||||
|
# Foundation; either version 2 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
# Module authors:
|
||||||
|
# Georg Brandl <g.brandl@fz-juelich.de>
|
||||||
|
# Alexander Zaft <a.zaft@fz-juelich.de>
|
||||||
|
#
|
||||||
|
# *****************************************************************************
|
||||||
|
|
||||||
|
"""Adapter to the existing SEOP 3He spin filter system daemon."""
|
||||||
|
|
||||||
|
from os import path
|
||||||
|
|
||||||
|
# eventually he3control
|
||||||
|
from he3d import he3cell # pylint: disable=import-error
|
||||||
|
|
||||||
|
from frappy.core import Attached
|
||||||
|
from frappy.datatypes import ArrayOf, FloatRange, IntRange, StatusType, \
|
||||||
|
StringType, TupleOf
|
||||||
|
from frappy.errors import CommandRunningError
|
||||||
|
from frappy.modules import Command, Drivable, Module, Parameter, Property, \
|
||||||
|
Readable
|
||||||
|
from frappy.rwhandler import CommonReadHandler
|
||||||
|
|
||||||
|
integral = IntRange()
|
||||||
|
floating = FloatRange()
|
||||||
|
string = StringType()
|
||||||
|
|
||||||
|
# Configuration is kept in YAML files to stay compatible to the
|
||||||
|
# traditional 3He daemon, for now.
|
||||||
|
|
||||||
|
|
||||||
|
class Cell(Module):
|
||||||
|
""" Dummy module for creating He3Cell object in order for other modules to talk to the hardware.
|
||||||
|
Only deals with the config, and rotating the paramlog.
|
||||||
|
"""
|
||||||
|
config_directory = Property(
|
||||||
|
'Directory for the YAML config files', datatype=string)
|
||||||
|
|
||||||
|
def initModule(self):
|
||||||
|
super().initModule()
|
||||||
|
self.cell = he3cell.He3_cell(
|
||||||
|
path.join(self.config_directory, 'cell.yml'))
|
||||||
|
|
||||||
|
# Commands
|
||||||
|
@Command(result=string)
|
||||||
|
def raw_config_file(self):
|
||||||
|
"""return unparsed contents of yaml file"""
|
||||||
|
with open(self.cell._He3_cell__cfg_filename, 'r', encoding='utf-8') as f:
|
||||||
|
return str(f.read())
|
||||||
|
|
||||||
|
@Command(string, result=string)
|
||||||
|
def cfg_get(self, identifier):
|
||||||
|
"""Get a configuration value."""
|
||||||
|
return str(self.cell.cfg_get(identifier))
|
||||||
|
|
||||||
|
@Command((string, string), result=string)
|
||||||
|
def cfg_set(self, identifier, value):
|
||||||
|
"""Set a configuration value."""
|
||||||
|
try:
|
||||||
|
value = int(value)
|
||||||
|
except ValueError:
|
||||||
|
try:
|
||||||
|
value = float(value)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
# The type is lost during transmission.
|
||||||
|
# Check type so the value to be set has the same type and
|
||||||
|
# is not eg. a string where an int would be needed in the config key.
|
||||||
|
oldty = type(self.cell.cfg_get(identifier))
|
||||||
|
if oldty is not type(value):
|
||||||
|
raise ValueError('Type of value to be set does not match the '
|
||||||
|
'value in the configuration!')
|
||||||
|
return str(self.cell.cfg_set(identifier, value))
|
||||||
|
|
||||||
|
@Command()
|
||||||
|
def nmr_paramlog_rotate(self):
|
||||||
|
"""resets fitting and switches to a new logfile"""
|
||||||
|
self.cell.nmr_paramlog_rotate()
|
||||||
|
|
||||||
|
|
||||||
|
class Afp(Readable):
|
||||||
|
"""Polarisation state of the SEOP waveplates"""
|
||||||
|
|
||||||
|
value = Parameter('Current polarisation state of the SEOP waveplates', IntRange(0, 1))
|
||||||
|
|
||||||
|
cell = Attached(Cell)
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
return self.cell.cell.afp_state_get()
|
||||||
|
|
||||||
|
# Commands
|
||||||
|
@Command(description='Flip polarization of SEOP waveplates')
|
||||||
|
def afp_flip(self):
|
||||||
|
self.cell.cell.afp_flip_do()
|
||||||
|
self.read_value()
|
||||||
|
|
||||||
|
|
||||||
|
class Nmr(Readable):
|
||||||
|
Status = Drivable.Status
|
||||||
|
status = Parameter(datatype=StatusType(Drivable.Status))
|
||||||
|
value = Parameter('Timestamp of last NMR', string)
|
||||||
|
cell = Attached(Cell)
|
||||||
|
|
||||||
|
def initModule(self):
|
||||||
|
super().initModule()
|
||||||
|
self.interval = 0
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
return str(self.cell.cell.nmr_timestamp_get())
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
cellstate = self.cell.cell.nmr_state_get()
|
||||||
|
|
||||||
|
if self.cell.cell.nmr_background_check():
|
||||||
|
status = self.Status.BUSY, 'running every %d seconds' % self.interval
|
||||||
|
else:
|
||||||
|
status = self.Status.IDLE, 'not running'
|
||||||
|
|
||||||
|
# TODO: what do we do here with None and -1?
|
||||||
|
# -> None basically indicates that the fit for the parameters did not converge
|
||||||
|
if cellstate is None:
|
||||||
|
return self.Status.IDLE, f'returned None, {status[1]}'
|
||||||
|
if cellstate in (0, 1):
|
||||||
|
return status[0], f'nmr cellstate {cellstate}, {status[1]}'
|
||||||
|
if cellstate == -1:
|
||||||
|
return self.Status.WARN, f'got error from cell, {status[1]}'
|
||||||
|
return self.Status.ERROR, 'Unrecognized cellstate!'
|
||||||
|
|
||||||
|
# Commands
|
||||||
|
@Command()
|
||||||
|
def nmr_do(self):
|
||||||
|
"""Triggers the NMR to run"""
|
||||||
|
self.cell.cell.nmr_do()
|
||||||
|
self.read_status()
|
||||||
|
|
||||||
|
@Command()
|
||||||
|
def bgstart(self):
|
||||||
|
"""Start background NMR"""
|
||||||
|
if self.isBusy():
|
||||||
|
raise CommandRunningError('backgroundNMR is already running')
|
||||||
|
interval = self.cell.cell.cfg_get('tasks/nmr/background/interval')
|
||||||
|
self.interval = interval
|
||||||
|
self.cell.cell.nmr_background_start(interval)
|
||||||
|
self.read_status()
|
||||||
|
|
||||||
|
@Command()
|
||||||
|
def bgstop(self):
|
||||||
|
"""Stop background NMR"""
|
||||||
|
self.cell.cell.nmr_background_stop()
|
||||||
|
self.read_status()
|
||||||
|
|
||||||
|
# Commands to get large datasets we do not want directly in the NICOS cache
|
||||||
|
@Command(result=TupleOf(ArrayOf(floating, maxlen=100000),
|
||||||
|
ArrayOf(floating, maxlen=100000)))
|
||||||
|
def get_processed_nmr(self):
|
||||||
|
"""Get data for processed signal."""
|
||||||
|
val= self.cell.cell.nmr_processed_get()
|
||||||
|
return (val['xval'], val['yval'])
|
||||||
|
|
||||||
|
@Command(result=TupleOf(ArrayOf(floating, maxlen=100000),
|
||||||
|
ArrayOf(floating, maxlen=100000)))
|
||||||
|
def get_raw_nmr(self):
|
||||||
|
"""Get raw signal data."""
|
||||||
|
val = self.cell.cell.nmr_raw_get()
|
||||||
|
return (val['xval'], val['yval'])
|
||||||
|
|
||||||
|
@Command(result=TupleOf(ArrayOf(floating, maxlen=100000),
|
||||||
|
ArrayOf(floating, maxlen=100000)))
|
||||||
|
def get_raw_spectrum(self):
|
||||||
|
"""Get the raw spectrum."""
|
||||||
|
val = self.cell.cell.nmr_raw_spectrum_get()
|
||||||
|
y = val['yval'][:len(val['xval'])]
|
||||||
|
return (val['xval'], y)
|
||||||
|
|
||||||
|
@Command(result=TupleOf(ArrayOf(floating, maxlen=100000),
|
||||||
|
ArrayOf(floating, maxlen=100000)))
|
||||||
|
def get_processed_spectrum(self):
|
||||||
|
"""Get the processed spectrum."""
|
||||||
|
val = self.cell.cell.nmr_processed_spectrum_get()
|
||||||
|
x = val['xval'][:len(val['yval'])]
|
||||||
|
return (x, val['yval'])
|
||||||
|
|
||||||
|
@Command(result=TupleOf(ArrayOf(string, maxlen=100),
|
||||||
|
ArrayOf(floating, maxlen=100)))
|
||||||
|
def get_amplitude(self):
|
||||||
|
"""Last 20 amplitude datapoints."""
|
||||||
|
rv = self.cell.cell.nmr_paramlog_get('amplitude', 20)
|
||||||
|
x = [ str(timestamp) for timestamp in rv['xval']]
|
||||||
|
return (x,rv['yval'])
|
||||||
|
|
||||||
|
@Command(result=TupleOf(ArrayOf(string, maxlen=100),
|
||||||
|
ArrayOf(floating, maxlen=100)))
|
||||||
|
def get_phase(self):
|
||||||
|
"""Last 20 phase datapoints."""
|
||||||
|
val = self.cell.cell.nmr_paramlog_get('phase', 20)
|
||||||
|
return ([str(timestamp) for timestamp in val['xval']], val['yval'])
|
||||||
|
|
||||||
|
|
||||||
|
class FitParam(Readable):
|
||||||
|
value = Parameter('fitted value', unit='$', default=0.0)
|
||||||
|
sigma = Parameter('variance of the fitted value', FloatRange(), default=0.0)
|
||||||
|
param = Property('the parameter that should be accesssed',
|
||||||
|
StringType(), export=False)
|
||||||
|
|
||||||
|
cell = Attached(Cell)
|
||||||
|
|
||||||
|
@CommonReadHandler(['value', 'sigma'])
|
||||||
|
def read_amplitude(self):
|
||||||
|
ret = self.cell.cell.nmr_param_get(self.param)
|
||||||
|
self.value = ret['value']
|
||||||
|
self.sigma = ret['sigma']
|
||||||
|
|
||||||
|
# Commands
|
||||||
|
@Command(integral, result=TupleOf(ArrayOf(string),
|
||||||
|
ArrayOf(floating)))
|
||||||
|
def nmr_paramlog_get(self, n):
|
||||||
|
"""returns the log of the last 'n' values for this parameter"""
|
||||||
|
val = self.cell.cell.nmr_paramlog_get(self.param, n)
|
||||||
|
return ([str(timestamp) for timestamp in val['xval']], val['yval'])
|
296
frappy_mlz/zebra.py
Normal file
296
frappy_mlz/zebra.py
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# *****************************************************************************
|
||||||
|
# MLZ library of Tango servers
|
||||||
|
# Copyright (c) 2015-2023 by the authors, see LICENSE
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License as published by the Free Software
|
||||||
|
# Foundation; either version 2 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
# Module authors:
|
||||||
|
# Georg Brandl <g.brandl@fz-juelich.de>
|
||||||
|
# Alexander Zaft <a.zaft@fz-juelich.de>
|
||||||
|
#
|
||||||
|
# *****************************************************************************
|
||||||
|
|
||||||
|
import threading
|
||||||
|
from time import sleep, time
|
||||||
|
|
||||||
|
from frappy.core import Parameter, Command, nopoll, Readable
|
||||||
|
from frappy.io import HasIO, BytesIO
|
||||||
|
from frappy.lib import mkthread
|
||||||
|
from frappy.errors import CommunicationFailedError
|
||||||
|
from frappy.datatypes import IntRange, StringType, StatusType
|
||||||
|
|
||||||
|
# SSI protocol operations
|
||||||
|
CMD_ACK = 0xD0
|
||||||
|
CMD_NAK = 0xD1
|
||||||
|
DECODE_DATA = 0xF3
|
||||||
|
BEEP = 0xE6
|
||||||
|
REQUEST_REVISION = 0xA3
|
||||||
|
REPLY_REVISION = 0xA4
|
||||||
|
SCAN_ENABLE = 0xE9
|
||||||
|
SCAN_DISABLE = 0xEA
|
||||||
|
|
||||||
|
# source byte
|
||||||
|
HOST = 4
|
||||||
|
DECODER = 0
|
||||||
|
|
||||||
|
|
||||||
|
BARCODE_TYPES = {
|
||||||
|
0x2d: 'Aztec',
|
||||||
|
0x2e: 'Aztec Rune',
|
||||||
|
0x16: 'Bookland',
|
||||||
|
0x72: 'C 2 of 5',
|
||||||
|
0x02: 'Codabar',
|
||||||
|
0x0c: 'Code 11',
|
||||||
|
0x03: 'Code 128',
|
||||||
|
0x12: 'Code 16K',
|
||||||
|
0x20: 'Code 32',
|
||||||
|
0x01: 'Code 39',
|
||||||
|
0x13: 'Code 39 ASCII',
|
||||||
|
0x0d: 'Code 49',
|
||||||
|
0x07: 'Code 93',
|
||||||
|
0x17: 'Coupon',
|
||||||
|
0x38: 'Cue CAT',
|
||||||
|
0x04: 'D25',
|
||||||
|
0x1b: 'Data Matrix',
|
||||||
|
0x0f: 'GS1-128',
|
||||||
|
0xc2: 'GS1 QR',
|
||||||
|
0x0b: 'EAN-13',
|
||||||
|
0x4b: 'EAN-13 + 2',
|
||||||
|
0x8b: 'EAN-13 + 5',
|
||||||
|
0x0a: 'EAN-8',
|
||||||
|
0x4a: 'EAN-8 + 2',
|
||||||
|
0x8a: 'EAN-8 + 5',
|
||||||
|
0x2f: 'French Lottery',
|
||||||
|
0x32: 'GS1 DataBar Expanded',
|
||||||
|
0x31: 'GS1 DataBar Limited',
|
||||||
|
0x30: 'GS1 DataBar-14',
|
||||||
|
0xc1: 'GS1 Datamatrix',
|
||||||
|
0xb7: 'Han Xin',
|
||||||
|
0x05: 'IATA',
|
||||||
|
0x19: 'ISBT-128',
|
||||||
|
0x21: 'ISBT-128 Concat',
|
||||||
|
0x36: 'ISSN',
|
||||||
|
0x06: 'ITF',
|
||||||
|
0x73: 'Korean 2 of 5',
|
||||||
|
0x9a: 'Macro Micro PDF',
|
||||||
|
0x28: 'Macro PDF-417',
|
||||||
|
0x29: 'Macro QR',
|
||||||
|
0x39: 'Matrix 2 of 5',
|
||||||
|
0x25: 'Maxicode',
|
||||||
|
0x1a: 'Micro PDF',
|
||||||
|
0x1d: 'Micro PDF CCA',
|
||||||
|
0x2c: 'Micro QR',
|
||||||
|
0x0e: 'MSI',
|
||||||
|
0x99: 'Multipacket Format',
|
||||||
|
0x18: 'NW7',
|
||||||
|
0xa0: 'OCRB',
|
||||||
|
0x33: 'Parameter FNC3',
|
||||||
|
0x11: 'PDF-417',
|
||||||
|
0x1f: 'Planet US',
|
||||||
|
0x23: 'Postal AUS',
|
||||||
|
0x24: 'Postal NL',
|
||||||
|
0x22: 'Postal JAP',
|
||||||
|
0x27: 'Postal UK',
|
||||||
|
0x26: 'Postbar CA',
|
||||||
|
0x1e: 'Postnet US',
|
||||||
|
0x1c: 'QR',
|
||||||
|
0xe0: 'RFID Raw',
|
||||||
|
0xe1: 'RFID URI',
|
||||||
|
0xb4: 'RSS Expanded',
|
||||||
|
0x37: 'Scanlet Webcode',
|
||||||
|
0x69: 'Signature',
|
||||||
|
0x5a: 'TLC-39',
|
||||||
|
0x15: 'Trioptic',
|
||||||
|
0x08: 'UPCA',
|
||||||
|
0x48: 'UPCA + 2',
|
||||||
|
0x88: 'UPCA + 5',
|
||||||
|
0x14: 'UPCD',
|
||||||
|
0x09: 'UPCE',
|
||||||
|
0x49: 'UPCE + 2',
|
||||||
|
0x89: 'UPCE + 5',
|
||||||
|
0x10: 'UPCE1',
|
||||||
|
0x50: 'UPCE1 + 2',
|
||||||
|
0x90: 'UPCE1 + 5',
|
||||||
|
0x34: '4State US',
|
||||||
|
0x35: '4State US4',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def decode_bytes(byte_list):
|
||||||
|
return bytes(byte_list).decode('latin1')
|
||||||
|
|
||||||
|
|
||||||
|
class ZebraIO(BytesIO):
|
||||||
|
default_settings = {'baudrate': 115200}
|
||||||
|
|
||||||
|
def _cksum(self, data):
|
||||||
|
cksum = 0x10000 - sum(data)
|
||||||
|
return [cksum >> 8, cksum & 0xFF]
|
||||||
|
|
||||||
|
def _make_package(self, op, data):
|
||||||
|
msg = [len(data) + 4, op, HOST, 0] + data
|
||||||
|
return msg + self._cksum(msg)
|
||||||
|
|
||||||
|
def _ssi_send(self, op, data):
|
||||||
|
self.communicate(self._make_package(op, data), 0)
|
||||||
|
|
||||||
|
def _ssi_read_n(self, n, timeout, buf):
|
||||||
|
# read N bytes with specified timeout
|
||||||
|
end = time() + timeout
|
||||||
|
delay = 0.00005
|
||||||
|
while n and time() < end:
|
||||||
|
sleep(delay)
|
||||||
|
delay = min(2 * delay, 0.01)
|
||||||
|
newdata = self.readBytes(int(n))
|
||||||
|
n -= len(newdata)
|
||||||
|
buf.extend(newdata)
|
||||||
|
return buf
|
||||||
|
|
||||||
|
def _ssi_recv(self, expected_op, recv_timeout, rest_timeout):
|
||||||
|
# first determine how much data there is to read
|
||||||
|
buf = []
|
||||||
|
if not self._ssi_read_n(1, recv_timeout, buf):
|
||||||
|
return None
|
||||||
|
# now read the rest of the data
|
||||||
|
rest_len = buf[0] + 1
|
||||||
|
self._ssi_read_n(rest_len, rest_timeout, buf)
|
||||||
|
if len(buf) != rest_len + 1:
|
||||||
|
return None
|
||||||
|
if buf[2] != DECODER:
|
||||||
|
raise CommunicationFailedError('invalid reply received')
|
||||||
|
if self._cksum(buf[:-2]) != buf[-2:]:
|
||||||
|
raise CommunicationFailedError('invalid checksum received')
|
||||||
|
if buf[1] != expected_op:
|
||||||
|
raise CommunicationFailedError('got op %r, expected %r' %
|
||||||
|
(buf[0], expected_op))
|
||||||
|
return buf[3:-2]
|
||||||
|
|
||||||
|
def _ssi_comm(self, op, data):
|
||||||
|
self._ssi_send(op, data)
|
||||||
|
if self._ssi_recv(CMD_ACK, 1, 1) is None:
|
||||||
|
raise CommunicationFailedError('ACK not received')
|
||||||
|
|
||||||
|
|
||||||
|
# Not yet tested
|
||||||
|
class ZebraReader(HasIO, Readable):
|
||||||
|
"""Reads scanned barcodes from a Zebra barcode reader, using the USB-CDC
|
||||||
|
interface mode and the SSI protocol.
|
||||||
|
|
||||||
|
TODO: CHANGE this paragraph
|
||||||
|
The underlying IO device must be a BinaryIO since SSI framing and metadata
|
||||||
|
is transferred in binary.
|
||||||
|
|
||||||
|
Since reading barcodes is initiated by the device and not the host, the
|
||||||
|
parameter decoded does not give the last decoded value when polled.
|
||||||
|
Instead, activate updates for this parameter, which are then sent out when
|
||||||
|
the barcode reader decodes a value. Polling will always return an empty
|
||||||
|
string.
|
||||||
|
|
||||||
|
The update for decoded then contains the decoded barcode type as a string,
|
||||||
|
a comma as a separator, and then the barcode data.
|
||||||
|
|
||||||
|
As a special API, there is a Beep command to make the reader emit some
|
||||||
|
audible signal.
|
||||||
|
"""
|
||||||
|
|
||||||
|
ioClass = ZebraIO
|
||||||
|
|
||||||
|
decoded = Parameter('decoded barcode (updates-only)', StringType(),
|
||||||
|
update_unchanged='always', default='')
|
||||||
|
# TODO: Decide, if this is useful, remove otherwise
|
||||||
|
status = Parameter('status of the module',
|
||||||
|
StatusType('IDLE', 'WARN', 'ERROR'))
|
||||||
|
value = Parameter(datatype=StringType(), default='',
|
||||||
|
update_unchanged='never')
|
||||||
|
|
||||||
|
_thread = None
|
||||||
|
_stoprequest = False
|
||||||
|
|
||||||
|
def initModule(self): # or startModule?
|
||||||
|
super().initModule()
|
||||||
|
self.io._ssi_send(REQUEST_REVISION, [])
|
||||||
|
rev = self.io._ssi_recv(REPLY_REVISION, 1, 1)
|
||||||
|
if rev is None:
|
||||||
|
raise CommunicationFailedError('got no revision info from decoder')
|
||||||
|
self.hw_version = decode_bytes(rev).split()[0]
|
||||||
|
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
self._thread = mkthread(self._thread_func)
|
||||||
|
|
||||||
|
def shutdownModule(self):
|
||||||
|
self._stoprequest = True
|
||||||
|
if self._thread and self._thread.is_alive():
|
||||||
|
self._thread.join()
|
||||||
|
|
||||||
|
@nopoll
|
||||||
|
def read_value(self):
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@nopoll
|
||||||
|
def read_decoded(self):
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
return self.Status.IDLE, ''
|
||||||
|
|
||||||
|
def _thread_func(self):
|
||||||
|
while not self._stoprequest:
|
||||||
|
with self._lock:
|
||||||
|
try:
|
||||||
|
code = self.io._ssi_recv(DECODE_DATA, 0.1, 1)
|
||||||
|
if code is not None:
|
||||||
|
self.io._ssi_send(CMD_ACK, [])
|
||||||
|
# TODO: readBytes from BytesIO always uses self.timeout, so the
|
||||||
|
# case where None can be returned after the timeout cannot be
|
||||||
|
# used
|
||||||
|
except TimeoutError:
|
||||||
|
code = None
|
||||||
|
except Exception as e:
|
||||||
|
self.log.exception('while receiving barcode: %s', e)
|
||||||
|
self.status = self.Status.ERROR, f'{e!r}'
|
||||||
|
continue
|
||||||
|
if code is not None:
|
||||||
|
codetype = BARCODE_TYPES.get(code[0], str(code[0]))
|
||||||
|
code = codetype + ',' + decode_bytes(code[1:])
|
||||||
|
|
||||||
|
tstamp = time()
|
||||||
|
self.log.info('decoded barcode %r with timestamp %s',
|
||||||
|
code, tstamp)
|
||||||
|
self.decoded = code
|
||||||
|
self.decoded = '' # clear value of frappy client cache
|
||||||
|
sleep(0.5)
|
||||||
|
|
||||||
|
@Command()
|
||||||
|
def on(self):
|
||||||
|
"""Enable the Scanner"""
|
||||||
|
with self._lock:
|
||||||
|
self.io._ssi_comm(SCAN_ENABLE, [])
|
||||||
|
|
||||||
|
@Command()
|
||||||
|
def off(self):
|
||||||
|
"""Disable the Scanner"""
|
||||||
|
with self._lock:
|
||||||
|
self.io._ssi_comm(SCAN_DISABLE, [])
|
||||||
|
|
||||||
|
@Command(IntRange(0,26))
|
||||||
|
def beep(self, pattern):
|
||||||
|
"""
|
||||||
|
Emit an audible signal from the reader.
|
||||||
|
:param pattern: The beep pattern (range 0 to 26;
|
||||||
|
see the manual for interpretation).
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
self.io._ssi_comm(BEEP, [pattern])
|
@ -192,9 +192,17 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
|
|||||||
self.hw_stop()
|
self.hw_stop()
|
||||||
|
|
||||||
def read_status(self):
|
def read_status(self):
|
||||||
sysstatus = self.communicate(f'{self.address:x}SE')
|
for _ in range(3):
|
||||||
sysstatus = sysstatus[1:4] if self.axis == 'X' else sysstatus[5:8]
|
sysstatus = self.communicate(f'{self.address:x}SE')
|
||||||
status = self.STATUS_MAP.get(sysstatus[1:]) or (ERROR, f'unknown error {sysstatus[1:]}')
|
try:
|
||||||
|
sysstatus = sysstatus[1:4] if self.axis == 'X' else sysstatus[5:8]
|
||||||
|
status = self.STATUS_MAP[sysstatus[1:]]
|
||||||
|
except Exception: # can not interprete the reply, probably communication error
|
||||||
|
self.log.warning('bad status reply %r', sysstatus)
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
status = (ERROR, f'unknown status after 3 tries {sysstatus!r}')
|
||||||
self._running = sysstatus[0] != '1'
|
self._running = sysstatus[0] != '1'
|
||||||
if status[0] == ERROR:
|
if status[0] == ERROR:
|
||||||
self._blocking_error = status[1]
|
self._blocking_error = status[1]
|
||||||
@ -213,7 +221,7 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
|
|||||||
enc = self.read_encoder()
|
enc = self.read_encoder()
|
||||||
else:
|
else:
|
||||||
enc = self.value
|
enc = self.value
|
||||||
if not self._running: # at target
|
if not self._running: # at target (self._running is updated in self.read_status())
|
||||||
return False
|
return False
|
||||||
diff = abs(self.value - self._intermediate_target)
|
diff = abs(self.value - self._intermediate_target)
|
||||||
if diff > self._prev_diff and diff > self.encoder_tolerance:
|
if diff > self._prev_diff and diff > self.encoder_tolerance:
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
# This program is free software; you can redistribute it and/or modify it under
|
||||||
# the terms of the GNU General Public License as published by the Free Software
|
# the terms of the GNU General Public License as published by the Free Software
|
||||||
@ -17,64 +15,13 @@
|
|||||||
#
|
#
|
||||||
# Module authors:
|
# Module authors:
|
||||||
# Oksana Shliakhtun <oksana.shliakhtun@psi.ch>
|
# Oksana Shliakhtun <oksana.shliakhtun@psi.ch>
|
||||||
|
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
""" RUFS Command: Description of Bits
|
"""bath thermostat Thermo Scientific™ ARCTIC A10 Refrigerated Circulators"""
|
||||||
|
|
||||||
====== ======================================================== ==============================================
|
from frappy.core import Command, StringIO, Parameter, HasIO, \
|
||||||
Value Description
|
|
||||||
====== ======================================================== ==============================================
|
|
||||||
V1
|
|
||||||
B6: warning, rtd1 (internal temp. sensor) is shorted
|
|
||||||
B0 --> 1
|
|
||||||
B7: warning, rtd1 is open
|
|
||||||
B1 --> 2
|
|
||||||
V2
|
|
||||||
B0: error, HTC (high temperature cutout) fault B2 --> 4
|
|
||||||
|
|
||||||
B1: error, high RA (refrigeration) temperature fault B3 --> 8
|
|
||||||
|
|
||||||
V3 B4 --> 16
|
|
||||||
B0: warning, low level in the bath
|
|
||||||
B5 --> 32
|
|
||||||
B1: warning, low temperature
|
|
||||||
B6 --> 64
|
|
||||||
B2: warning, high temperature
|
|
||||||
B7 --> 128
|
|
||||||
B3: error, low level in the bath
|
|
||||||
|
|
||||||
B4: error, low temperature fault
|
|
||||||
|
|
||||||
B5: error, high temperature fault
|
|
||||||
|
|
||||||
B6: error, low temperature fixed* fault
|
|
||||||
|
|
||||||
B7: error, high temperature fixed** fault
|
|
||||||
|
|
||||||
V4
|
|
||||||
B3: idle, circulator** is running
|
|
||||||
|
|
||||||
B5: error, circulator** fault
|
|
||||||
|
|
||||||
V5
|
|
||||||
B0: error, pump speed fault
|
|
||||||
|
|
||||||
B1: error, motor overloaded
|
|
||||||
|
|
||||||
B2: error, high pressure cutout
|
|
||||||
|
|
||||||
B3: idle, maximum cooling
|
|
||||||
|
|
||||||
B4: idle, cooling
|
|
||||||
|
|
||||||
B5: idle, maximum heating
|
|
||||||
|
|
||||||
B6: idle, heating
|
|
||||||
====== ======================================================== ==============================================
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from frappy.core import StringIO, Parameter, HasIO, \
|
|
||||||
Drivable, FloatRange, IDLE, BUSY, ERROR, WARN, BoolType
|
Drivable, FloatRange, IDLE, BUSY, ERROR, WARN, BoolType
|
||||||
|
from frappy.structparam import StructParam
|
||||||
from frappy_psi.convergence import HasConvergence
|
from frappy_psi.convergence import HasConvergence
|
||||||
|
|
||||||
|
|
||||||
@ -85,17 +32,17 @@ class ThermFishIO(StringIO):
|
|||||||
|
|
||||||
class TemperatureLoopA10(HasConvergence, HasIO, Drivable):
|
class TemperatureLoopA10(HasConvergence, HasIO, Drivable):
|
||||||
ioClass = ThermFishIO
|
ioClass = ThermFishIO
|
||||||
value = Parameter('internal temperature', unit='degC')
|
|
||||||
value = Parameter('temperature', unit='degC')
|
value = Parameter('temperature', unit='degC')
|
||||||
target = Parameter('setpoint/target', datatype=FloatRange, unit='degC', default=0)
|
target = Parameter('setpoint/target', datatype=FloatRange, unit='degC', default=0)
|
||||||
circ_on = Parameter('is circulation running', BoolType(), readonly=False, default=False)
|
control_active = Parameter('circilation and control is on', BoolType(), default=False)
|
||||||
# pids
|
ctrlpars = StructParam('control parameters struct', dict(
|
||||||
p_heat = Parameter('proportional heat parameter', FloatRange(), readonly=False)
|
p_heat = Parameter('proportional heat parameter', FloatRange()),
|
||||||
i_heat = Parameter('integral heat parameter', FloatRange(), readonly=False)
|
i_heat = Parameter('integral heat parameter', FloatRange()),
|
||||||
d_heat = Parameter('derivative heat parameter', FloatRange(), readonly=False)
|
d_heat = Parameter('derivative heat parameter', FloatRange()),
|
||||||
p_cool = Parameter('proportional cool parameter', FloatRange(), readonly=False)
|
p_cool = Parameter('proportional cool parameter', FloatRange()),
|
||||||
i_cool = Parameter('integral cool parameter', FloatRange(), readonly=False)
|
i_cool = Parameter('integral cool parameter', FloatRange()),
|
||||||
d_cool = Parameter('derivative cool parameter', FloatRange(), readonly=False)
|
d_cool = Parameter('derivative cool parameter', FloatRange()),
|
||||||
|
), readonly=False)
|
||||||
|
|
||||||
status_messages = [
|
status_messages = [
|
||||||
(ERROR, 'high tempr. cutout fault', 2, 0),
|
(ERROR, 'high tempr. cutout fault', 2, 0),
|
||||||
@ -122,20 +69,22 @@ class TemperatureLoopA10(HasConvergence, HasIO, Drivable):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get_par(self, cmd):
|
def get_par(self, cmd):
|
||||||
"""
|
"""get parameter and convert to float
|
||||||
All the reading commands starts with 'R', in the source code all the commands are written without 'R' (except
|
|
||||||
'RUFS').The result of a reading command is a value in the format '20C', without spaces.
|
|
||||||
|
|
||||||
:param cmd: any hardware command
|
:param cmd: hardware command without the leading 'R'
|
||||||
|
|
||||||
:return: 'R'+cmd
|
:return: result converted to float
|
||||||
"""
|
"""
|
||||||
new_cmd = 'R' + cmd
|
new_cmd = 'R' + cmd
|
||||||
reply = self.communicate(new_cmd)
|
reply = self.communicate(new_cmd).strip()
|
||||||
if any(unit.isalpha() for unit in reply):
|
while reply[-1].isalpha():
|
||||||
reply = ''.join(unit for unit in reply if not unit.isalpha())
|
reply = reply[:-1]
|
||||||
return float(reply)
|
return float(reply)
|
||||||
|
|
||||||
|
def set_par(self, cmd, value):
|
||||||
|
self.communicate(f'S{cmd} {value}')
|
||||||
|
return self.get_par(cmd)
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
"""
|
"""
|
||||||
Reading internal temperature sensor value.
|
Reading internal temperature sensor value.
|
||||||
@ -143,6 +92,34 @@ class TemperatureLoopA10(HasConvergence, HasIO, Drivable):
|
|||||||
return self.get_par('T')
|
return self.get_par('T')
|
||||||
|
|
||||||
def read_status(self):
|
def read_status(self):
|
||||||
|
""" convert from RUFS Command: Description of Bits
|
||||||
|
|
||||||
|
====== ======================================================== ===============
|
||||||
|
Value Description
|
||||||
|
====== ======================================================== ===============
|
||||||
|
V1 B6: warning, rtd1 (internal temp. sensor) is shorted B0 --> 1
|
||||||
|
B7: warning, rtd1 is open B1 --> 2
|
||||||
|
V2 B0: error, HTC (high temperature cutout) fault B2 --> 4
|
||||||
|
B1: error, high RA (refrigeration) temperature fault B3 --> 8
|
||||||
|
V3 B0: warning, low level in the bath B5 --> 32
|
||||||
|
B1: warning, low temperature B6 --> 64
|
||||||
|
B2: warning, high temperature B7 --> 128
|
||||||
|
B3: error, low level in the bath
|
||||||
|
B4: error, low temperature fault
|
||||||
|
B5: error, high temperature fault
|
||||||
|
B6: error, low temperature fixed* fault
|
||||||
|
B7: error, high temperature fixed** fault
|
||||||
|
V4 B3: idle, circulator** is running
|
||||||
|
B5: error, circulator** fault
|
||||||
|
V5 B0: error, pump speed fault
|
||||||
|
B1: error, motor overloaded
|
||||||
|
B2: error, high pressure cutout
|
||||||
|
B3: idle, maximum cooling
|
||||||
|
B4: idle, cooling
|
||||||
|
B5: idle, maximum heating
|
||||||
|
B6: idle, heating
|
||||||
|
====== ======================================================== ===============
|
||||||
|
"""
|
||||||
result_str = self.communicate('RUFS') # read unit fault status
|
result_str = self.communicate('RUFS') # read unit fault status
|
||||||
values_str = result_str.strip().split()
|
values_str = result_str.strip().split()
|
||||||
values_int = [int(val) for val in values_str]
|
values_int = [int(val) for val in values_str]
|
||||||
@ -157,72 +134,55 @@ class TemperatureLoopA10(HasConvergence, HasIO, Drivable):
|
|||||||
return status_type, status_msg
|
return status_type, status_msg
|
||||||
return WARN, 'circulation off'
|
return WARN, 'circulation off'
|
||||||
|
|
||||||
def read_circ_on(self):
|
def read_control_active(self):
|
||||||
return self.communicate('RO')
|
return int(self.get_par('O'))
|
||||||
|
|
||||||
def write_circ_on(self, circ_on):
|
@Command
|
||||||
circ_on_str = '1' if circ_on else '0'
|
def control_off(self):
|
||||||
self.communicate(f'SO {circ_on_str}')
|
"""switch control and circulation off"""
|
||||||
return self.read_circ_on()
|
self.control_active = self.set_par('O', 0)
|
||||||
|
|
||||||
def read_target(self):
|
def read_target(self):
|
||||||
return self.get_par('S')
|
return self.get_par('S')
|
||||||
|
|
||||||
def write_target(self, target):
|
def write_target(self, target):
|
||||||
"""
|
self.control_active = self.set_par('O', 1)
|
||||||
:param target: here, it serves as an equivalent to a setpoint.
|
|
||||||
"""
|
|
||||||
self.write_circ_on('1')
|
|
||||||
self.communicate(f'SS {target}')
|
self.communicate(f'SS {target}')
|
||||||
self.convergence_start()
|
self.convergence_start()
|
||||||
return target
|
return target
|
||||||
|
|
||||||
## heat PID
|
|
||||||
def read_p_heat(self):
|
def read_p_heat(self):
|
||||||
p_heat = self.get_par('PH')
|
return self.get_par('PH')
|
||||||
return float(p_heat)
|
|
||||||
|
|
||||||
def write_p_heat(self, p_heat):
|
def write_p_heat(self, value):
|
||||||
self.communicate(f'SPH {p_heat}')
|
return self.set_par('PH', value)
|
||||||
return p_heat
|
|
||||||
|
|
||||||
def read_i_heat(self):
|
def read_i_heat(self):
|
||||||
i_heat = self.get_par('IH')
|
return self.get_par('IH')
|
||||||
return float(i_heat)
|
|
||||||
|
|
||||||
def write_i_heat(self, i_heat):
|
def write_i_heat(self, value):
|
||||||
self.communicate(f'SIH {i_heat}')
|
return self.set_par('IH', value)
|
||||||
return i_heat
|
|
||||||
|
|
||||||
def read_d_heat(self):
|
def read_d_heat(self):
|
||||||
d_heat = self.get_par('DH')
|
return self.get_par('DH')
|
||||||
return float(d_heat)
|
|
||||||
|
|
||||||
def write_d_heat(self, d_heat):
|
def write_d_heat(self, value):
|
||||||
self.communicate(f'SDH {d_heat}')
|
return self.set_par('DH', value)
|
||||||
return d_heat
|
|
||||||
|
|
||||||
## cool PID
|
|
||||||
def read_p_cool(self):
|
def read_p_cool(self):
|
||||||
p_cool = self.get_par('PC')
|
return self.get_par('PC')
|
||||||
return float(p_cool)
|
|
||||||
|
|
||||||
def write_p_cool(self, p_cool):
|
def write_p_cool(self, value):
|
||||||
self.communicate(f'SPC {p_cool}')
|
return self.set_par('PC', value)
|
||||||
return p_cool
|
|
||||||
|
|
||||||
def read_i_cool(self):
|
def read_i_cool(self):
|
||||||
i_cool = self.get_par('IC')
|
return self.get_par('IC')
|
||||||
return float(i_cool)
|
|
||||||
|
|
||||||
def write_i_cool(self, i_cool):
|
def write_i_cool(self, value):
|
||||||
self.communicate(f'SIC {i_cool}')
|
return self.set_par('IC', value)
|
||||||
return i_cool
|
|
||||||
|
|
||||||
def read_d_cool(self):
|
def read_d_cool(self):
|
||||||
d_cool = self.get_par('DC')
|
return self.get_par('DC')
|
||||||
return float(d_cool)
|
|
||||||
|
|
||||||
def write_d_cool(self, d_cool):
|
def write_d_cool(self, value):
|
||||||
self.communicate(f'SDC {d_cool}')
|
return self.set_par('DC', value)
|
||||||
return d_cool
|
|
||||||
|
@ -7,3 +7,5 @@ markdown>=2.6
|
|||||||
pytest
|
pytest
|
||||||
pytest-randomly>=1.1
|
pytest-randomly>=1.1
|
||||||
pytest-cov
|
pytest-cov
|
||||||
|
# frappy mlz
|
||||||
|
zapf >= 0.4.7
|
||||||
|
@ -75,6 +75,4 @@ def test_attach():
|
|||||||
assert m.propertyValues['att'] == 'a'
|
assert m.propertyValues['att'] == 'a'
|
||||||
srv.dispatcher.register_module(a, 'a')
|
srv.dispatcher.register_module(a, 'a')
|
||||||
srv.dispatcher.register_module(m, 'm')
|
srv.dispatcher.register_module(m, 'm')
|
||||||
assert m.att == 'a'
|
|
||||||
m.attachedModules = {'att': a}
|
|
||||||
assert m.att == a
|
assert m.att == a
|
||||||
|
141
test/test_config.py
Normal file
141
test/test_config.py
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
# *****************************************************************************
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License as published by the Free Software
|
||||||
|
# Foundation; either version 2 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
# Module authors:
|
||||||
|
# Alexander Zaft <a.zaft@fz-juelich.de>
|
||||||
|
#
|
||||||
|
# *****************************************************************************
|
||||||
|
|
||||||
|
# false positive with fixtures
|
||||||
|
# pylint: disable=redefined-outer-name
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from frappy.config import Collector, Config, Mod, NodeCollector, load_config, \
|
||||||
|
process_file, to_config_path
|
||||||
|
from frappy.errors import ConfigError
|
||||||
|
from frappy.lib import generalConfig
|
||||||
|
|
||||||
|
|
||||||
|
class LoggerStub:
|
||||||
|
def debug(self, fmt, *args):
|
||||||
|
pass
|
||||||
|
info = warning = exception = error = debug
|
||||||
|
handlers = []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def log():
|
||||||
|
return LoggerStub()
|
||||||
|
|
||||||
|
|
||||||
|
PY_FILE = """Node('foonode', 'fodesc', 'fooface')
|
||||||
|
Mod('foo', 'frappy.modules.Readable', 'description', value=5)
|
||||||
|
Mod('bar', 'frappy.modules.Readable', 'about me', export=False)
|
||||||
|
Mod('baz', 'frappy.modules.Readable', 'things', value=Param(3, unit='BAR'))
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# fixture file system, TODO: make a bit nicer?
|
||||||
|
@pytest.fixture
|
||||||
|
def direc(tmp_path_factory):
|
||||||
|
d = tmp_path_factory.mktemp('cfgdir')
|
||||||
|
a = d / 'a'
|
||||||
|
b = d / 'b'
|
||||||
|
a.mkdir()
|
||||||
|
b.mkdir()
|
||||||
|
f = a / 'config_cfg.py'
|
||||||
|
pyfile = a / 'pyfile_cfg.py'
|
||||||
|
ff = b / 'test_cfg.py'
|
||||||
|
fff = b / 'alsoworks.py'
|
||||||
|
f.touch()
|
||||||
|
ff.touch()
|
||||||
|
fff.touch()
|
||||||
|
pyfile.write_text(PY_FILE)
|
||||||
|
generalConfig.testinit(confdir=f'{a}:{b}', piddir=str(d))
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
files = [('config', 'a/config_cfg.py'),
|
||||||
|
('config_cfg', 'a/config_cfg.py'),
|
||||||
|
('config_cfg.py', 'a/config_cfg.py'),
|
||||||
|
('test', 'b/test_cfg.py'),
|
||||||
|
('test_cfg', 'b/test_cfg.py'),
|
||||||
|
('test_cfg.py', 'b/test_cfg.py'),
|
||||||
|
('alsoworks', 'b/alsoworks.py'),
|
||||||
|
('alsoworks.py', 'b/alsoworks.py'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('file, res', files)
|
||||||
|
def test_to_cfg_path(log, direc, file, res):
|
||||||
|
assert to_config_path(file, log).endswith(res)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cfg_not_existing(direc, log):
|
||||||
|
with pytest.raises(ConfigError):
|
||||||
|
to_config_path('idonotexist', log)
|
||||||
|
|
||||||
|
|
||||||
|
def collector_helper(node, mods):
|
||||||
|
n = NodeCollector()
|
||||||
|
n.add(*node)
|
||||||
|
m = Collector(Mod)
|
||||||
|
m.list = [Mod(module, '', '') for module in mods]
|
||||||
|
return n, m
|
||||||
|
|
||||||
|
|
||||||
|
configs = [
|
||||||
|
(['n1', 'desc', 'iface'], ['foo', 'bar', 'baz'], ['n2', 'foo', 'bar'],
|
||||||
|
['foo', 'more', 'other'], ['n1', 'iface', 5, {'foo'}]),
|
||||||
|
(['n1', 'desc', 'iface'], ['foo', 'bar', 'baz'], ['n2', 'foo', 'bar'],
|
||||||
|
['different', 'more', 'other'], ['n1', 'iface', 6, set()]),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('n1, m1, n2, m2, res', configs)
|
||||||
|
def test_merge(n1, m1, n2, m2, res):
|
||||||
|
name, iface, num_mods, ambig = res
|
||||||
|
c1 = Config(*collector_helper(n1, m1))
|
||||||
|
c2 = Config(*collector_helper(n2, m2))
|
||||||
|
c1.merge_modules(c2)
|
||||||
|
assert c1['node']['equipment_id'] == name
|
||||||
|
assert c1['node']['interface'] == iface
|
||||||
|
assert len(c1.module_names) == num_mods
|
||||||
|
assert c1.ambiguous == ambig
|
||||||
|
|
||||||
|
|
||||||
|
def do_asserts(ret):
|
||||||
|
assert len(ret.module_names) == 3
|
||||||
|
assert set(ret.module_names) == set(['foo', 'bar', 'baz'])
|
||||||
|
assert ret['node']['equipment_id'] == 'foonode'
|
||||||
|
assert ret['node']['interface'] == 'fooface'
|
||||||
|
assert ret['foo'] == {'cls': 'frappy.modules.Readable',
|
||||||
|
'description': 'description', 'value': {'value': 5}}
|
||||||
|
assert ret['bar'] == {'cls': 'frappy.modules.Readable',
|
||||||
|
'description': 'about me', 'export': {'value': False}}
|
||||||
|
assert ret['baz'] == {'cls': 'frappy.modules.Readable',
|
||||||
|
'description': 'things',
|
||||||
|
'value': {'value': 3, 'unit': 'BAR'}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_file(direc, log):
|
||||||
|
ret = process_file(str(direc / 'a' / 'pyfile_cfg.py'), log)
|
||||||
|
do_asserts(ret)
|
||||||
|
|
||||||
|
|
||||||
|
def test_full(direc, log):
|
||||||
|
ret = load_config('pyfile_cfg.py', log)
|
||||||
|
do_asserts(ret)
|
132
test/test_ctrlpars.py
Normal file
132
test/test_ctrlpars.py
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
# *****************************************************************************
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License as published by the Free Software
|
||||||
|
# Foundation; either version 2 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
# Module authors:
|
||||||
|
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||||
|
#
|
||||||
|
# *****************************************************************************
|
||||||
|
"""test frappy.mixins.HasCtrlPars"""
|
||||||
|
|
||||||
|
|
||||||
|
from test.test_modules import LoggerStub, ServerStub
|
||||||
|
from frappy.core import FloatRange, Module, Parameter
|
||||||
|
from frappy.structparam import StructParam
|
||||||
|
|
||||||
|
|
||||||
|
def test_with_read_ctrlpars():
|
||||||
|
class Mod(Module):
|
||||||
|
ctrlpars = StructParam('ctrlpar struct', dict(
|
||||||
|
p = Parameter('control parameter p', FloatRange()),
|
||||||
|
i = Parameter('control parameter i', FloatRange()),
|
||||||
|
d = Parameter('control parameter d', FloatRange()),
|
||||||
|
), 'pid_', readonly=False)
|
||||||
|
|
||||||
|
def read_ctrlpars(self):
|
||||||
|
return self._ctrlpars
|
||||||
|
|
||||||
|
def write_ctrlpars(self, value):
|
||||||
|
self._ctrlpars = value
|
||||||
|
return self.read_ctrlpars()
|
||||||
|
|
||||||
|
logger = LoggerStub()
|
||||||
|
updates = {}
|
||||||
|
srv = ServerStub(updates)
|
||||||
|
|
||||||
|
ms = Mod('ms', logger, {'description':''}, srv)
|
||||||
|
|
||||||
|
value = {'p': 1, 'i': 2, 'd': 3}
|
||||||
|
assert ms.write_ctrlpars(value) == value
|
||||||
|
assert ms.read_ctrlpars() == value
|
||||||
|
assert ms.read_pid_p() == 1
|
||||||
|
assert ms.read_pid_i() == 2
|
||||||
|
assert ms.read_pid_d() == 3
|
||||||
|
assert ms.write_pid_i(5) == 5
|
||||||
|
assert ms.write_pid_d(0) == 0
|
||||||
|
assert ms.read_ctrlpars() == {'p': 1, 'i': 5, 'd': 0}
|
||||||
|
assert set(Mod.ctrlpars.influences) == {'pid_p', 'pid_i', 'pid_d'}
|
||||||
|
assert Mod.pid_p.influences == ('ctrlpars',)
|
||||||
|
assert Mod.pid_i.influences == ('ctrlpars',)
|
||||||
|
assert Mod.pid_d.influences == ('ctrlpars',)
|
||||||
|
|
||||||
|
|
||||||
|
def test_without_read_ctrlpars():
|
||||||
|
class Mod(Module):
|
||||||
|
ctrlpars = StructParam('ctrlpar struct', dict(
|
||||||
|
p = Parameter('control parameter p', FloatRange()),
|
||||||
|
i = Parameter('control parameter i', FloatRange()),
|
||||||
|
d = Parameter('control parameter d', FloatRange()),
|
||||||
|
), readonly=False)
|
||||||
|
|
||||||
|
_pid_p = 0
|
||||||
|
_pid_i = 0
|
||||||
|
|
||||||
|
def read_p(self):
|
||||||
|
return self._pid_p
|
||||||
|
|
||||||
|
def write_p(self, value):
|
||||||
|
self._pid_p = value
|
||||||
|
return self.read_p()
|
||||||
|
|
||||||
|
def read_i(self):
|
||||||
|
return self._pid_i
|
||||||
|
|
||||||
|
def write_i(self, value):
|
||||||
|
self._pid_i = value
|
||||||
|
return self.read_i()
|
||||||
|
|
||||||
|
logger = LoggerStub()
|
||||||
|
updates = {}
|
||||||
|
srv = ServerStub(updates)
|
||||||
|
|
||||||
|
ms = Mod('ms', logger, {'description': ''}, srv)
|
||||||
|
|
||||||
|
value = {'p': 1, 'i': 2, 'd': 3}
|
||||||
|
assert ms.write_ctrlpars(value) == value
|
||||||
|
assert ms.read_ctrlpars() == value
|
||||||
|
assert ms.read_p() == 1
|
||||||
|
assert ms.read_i() == 2
|
||||||
|
assert ms.read_d() == 3
|
||||||
|
assert ms.write_i(5) == 5
|
||||||
|
assert ms.write_d(0) == 0
|
||||||
|
assert ms.read_ctrlpars() == {'p': 1, 'i': 5, 'd': 0}
|
||||||
|
assert set(Mod.ctrlpars.influences) == {'p', 'i', 'd'}
|
||||||
|
assert Mod.p.influences == ('ctrlpars',)
|
||||||
|
assert Mod.i.influences == ('ctrlpars',)
|
||||||
|
assert Mod.d.influences == ('ctrlpars',)
|
||||||
|
|
||||||
|
|
||||||
|
def test_readonly():
|
||||||
|
class Mod(Module):
|
||||||
|
ctrlpars = StructParam('ctrlpar struct', dict(
|
||||||
|
p = Parameter('control parameter p', FloatRange()),
|
||||||
|
i = Parameter('control parameter i', FloatRange()),
|
||||||
|
d = Parameter('control parameter d', FloatRange()),
|
||||||
|
), {'p': 'pp', 'i':'ii', 'd': 'dd'}, readonly=True)
|
||||||
|
|
||||||
|
assert Mod.ctrlpars.readonly is True
|
||||||
|
assert Mod.pp.readonly is True
|
||||||
|
assert Mod.ii.readonly is True
|
||||||
|
assert Mod.dd.readonly is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_order_dependence1():
|
||||||
|
test_without_read_ctrlpars()
|
||||||
|
test_with_read_ctrlpars()
|
||||||
|
|
||||||
|
|
||||||
|
def test_order_dependence2():
|
||||||
|
test_with_read_ctrlpars()
|
||||||
|
test_without_read_ctrlpars()
|
@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from frappy.lib import parse_host_port
|
from frappy.lib import parse_host_port, merge_status
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('hostport, defaultport, result', [
|
@pytest.mark.parametrize('hostport, defaultport, result', [
|
||||||
@ -46,3 +46,19 @@ def test_parse_host(hostport, defaultport, result):
|
|||||||
parse_host_port(hostport, defaultport)
|
parse_host_port(hostport, defaultport)
|
||||||
else:
|
else:
|
||||||
assert result == parse_host_port(hostport, defaultport)
|
assert result == parse_host_port(hostport, defaultport)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('args, result', [
|
||||||
|
([(100, 'idle'), (200, 'warning')],
|
||||||
|
(200, 'warning')),
|
||||||
|
([(300, 'ramping'), (300, 'within tolerance')],
|
||||||
|
(300, 'ramping, within tolerance')),
|
||||||
|
([(300, 'ramping, within tolerance'), (300, 'within tolerance, slow'), (200, 'warning')],
|
||||||
|
(300, 'ramping, within tolerance, slow')),
|
||||||
|
# when a comma is used for other purposes than separating individual status texts,
|
||||||
|
# the behaviour might not be as desired. However, this case is somewhat constructed.
|
||||||
|
([(100, 'blue, yellow is my favorite'), (100, 'white, blue, red is a bad color mix')],
|
||||||
|
(100, 'blue, yellow is my favorite, white, red is a bad color mix')),
|
||||||
|
])
|
||||||
|
def test_merge_status(args, result):
|
||||||
|
assert merge_status(*args) == result
|
||||||
|
55
test/test_server.py
Normal file
55
test/test_server.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# *****************************************************************************
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License as published by the Free Software
|
||||||
|
# Foundation; either version 2 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
# Module authors:
|
||||||
|
# Alexander Zaft <a.zaft@fz-juelich.de>
|
||||||
|
#
|
||||||
|
# *****************************************************************************
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
# pylint: disable=redefined-outer-name
|
||||||
|
|
||||||
|
from frappy.server import Server
|
||||||
|
|
||||||
|
from .test_config import direc # pylint: disable=unused-import
|
||||||
|
|
||||||
|
|
||||||
|
class LoggerStub:
|
||||||
|
def debug(self, fmt, *args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def getChild(self, *args):
|
||||||
|
return self
|
||||||
|
|
||||||
|
info = warning = exception = error = debug
|
||||||
|
handlers = []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def log():
|
||||||
|
return LoggerStub()
|
||||||
|
|
||||||
|
|
||||||
|
def test_name_only(direc, log):
|
||||||
|
"""only see that this does not throw. get config from name."""
|
||||||
|
s = Server('pyfile', log)
|
||||||
|
s._processCfg()
|
||||||
|
|
||||||
|
|
||||||
|
def test_file(direc, log):
|
||||||
|
"""only see that this does not throw. get config from cfgfiles."""
|
||||||
|
s = Server('foo', log, cfgfiles='pyfile_cfg.py')
|
||||||
|
s._processCfg()
|
Loading…
x
Reference in New Issue
Block a user