Merge branch 'wip' of gitlab.psi.ch-samenv:samenv/frappy into wip

This commit is contained in:
zolliker 2022-04-20 14:52:17 +02:00
commit 1da7657483
88 changed files with 4562 additions and 2058 deletions

View File

@ -162,7 +162,7 @@ max-line-length=132
no-space-check=trailing-comma,dict-separator
# Maximum number of lines in a module
max-module-lines=1200
max-module-lines=1000
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).

View File

@ -59,4 +59,4 @@ release:
build-pkg:
debocker build --image jenkinsng.admin.frm2:5000/mlzbase/buster
debocker build --image docker.ictrl.frm2.tum.de:5443/mlzbase/buster

View File

@ -15,9 +15,8 @@ branches:
- work: current working version, usually in use on /home/l_samenv/frappy (and on neutron instruments)
this should be a copy of an earlier state of the wip branch
- wip: current test version, usually in use on /home/l_samenv/frappy_wip
IMPORTANT: make commits containing either only files to be pushed to Gerrit or only
PSI internal files, not mixed. Mark local commits with '[PSI]' in the commit message.
IMPORTANT: make commits containing either only files to be pushed to Gerrit or only
PSI internal files, not mixed. Mark local commits with '[PSI]' in the commit message.
master --> mlz # these branches match after a sync step, but they might have a different history
@ -35,7 +34,7 @@ where commits may be cherry picked for input to Gerrit. As generally in the revi
changes are done, eventually a sync step should happen:
1) ideally, this is done when work and wip match
2) make sure branches mlz, master, wip and work are in synv with remote, push/pull otherwise
2) make sure branches mlz, master, wip and work are in syns with remote, push/pull otherwise
3) cherry-pick commits from mlz to master
4) make sure master and mlz branches match (git diff --name-only master..wip should only return README.md)
5) create branch new_work from master
@ -43,11 +42,12 @@ changes are done, eventually a sync step should happen:
- core commits already pushed through gerrit are skipped
- all other commits are to be cherry-picked
7) when arrived at the point where the new working version should be,
copy new_wip branch to work with 'git checkout work;git checkout new_wip .'
(note the dot!) and then commit this.
8) continue with (6) if wip and work should differ
9) do like (7), but for wip branch
10) delete new_wip branch, push master, wip and work branches
copy new_wip branch to work with 'git checkout -B work'.
Not sure if this works, as work is to be pushed to git.psi.ch.
We might first remove the remote branch with 'git push origin --delete work'.
And then create again (git push origin work)?
8) continue with (6) if wip and work should differ, and do like (7) for wip branch
9) delete new_wip branch, push master, wip and work branches
## Procedure to update PPMS

View File

@ -27,12 +27,11 @@ import sys
import argparse
from os import path
import mlzlog
# Add import path for inplace usage
sys.path.insert(0, path.abspath(path.join(path.dirname(__file__), '..')))
from secop.lib import getGeneralConfig
from secop.lib import generalConfig
from secop.logging import logger
from secop.server import Server
@ -60,15 +59,26 @@ def parseArgv(argv):
parser.add_argument('-c',
'--cfgfiles',
action='store',
help="comma separated list of cfg files\n"
"defaults to <name_of_the_instance>\n"
"cfgfiles given without '.cfg' extension are searched in the configuration directory,"
help="comma separated list of cfg files,\n"
"defaults to <name_of_the_instance>.\n"
"cfgfiles given without '.cfg' extension are searched in the configuration directory, "
"else they are treated as path names",
default=None)
parser.add_argument('-g',
'--gencfg',
action='store',
help="full path of general config file,\n"
"defaults to env. variable FRAPPY_CONFIG_FILE\n",
default=None)
parser.add_argument('-t',
'--test',
action='store_true',
help='Check cfg files only',
help='check cfg files only',
default=False)
parser.add_argument('-r',
'--relaxed',
action='store_true',
help='no checking of problematic behaviour',
default=False)
return parser.parse_args(argv)
@ -80,9 +90,12 @@ def main(argv=None):
args = parseArgv(argv[1:])
loglevel = 'debug' if args.verbose else ('error' if args.quiet else 'info')
mlzlog.initLogging('secop', loglevel, getGeneralConfig()['logdir'])
generalConfig.defaults = {k: args.relaxed for k in (
'lazy_number_validation', 'disable_value_range_check', 'legacy_hasiodev', 'tolerate_poll_property')}
generalConfig.init(args.gencfg)
logger.init(loglevel)
srv = Server(args.name, mlzlog.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:
srv.start()

31
cfg/cryosim.cfg Normal file
View File

@ -0,0 +1,31 @@
[NODE]
id = cyrosim.psi.ch
description = cryo simulation (similar ppms simulation)
[INTERFACE]
uri = tcp://5000
[tt]
class = secop_psi.ppms.Temp
description = main temperature
io = ppms
[lev]
class = secop_psi.ppms.Level
description = helium level
io = ppms
[ts]
class = secop_psi.ppms.UserChannel
description = sample temperature
enabled = 1
value.unit = K
io = ppms
[ppms]
class = secop_psi.ppms.Main
description = the main and poller module
class_id = QD.MULTIVU.PPMS.1
visibility = 3
pollinterval = 2
export = False

5
cfg/generalConfig.cfg Normal file
View File

@ -0,0 +1,5 @@
[FRAPPY]
# general config for running in git repo
logdir = ./log
piddir = ./pid
confdir = ./cfg

View File

@ -1,24 +1,23 @@
[node LscSIM.psi.ch]
[NODE]
id = LscSIM.psi.ch
description = Lsc Simulation at PSI
[interface tcp]
type = tcp
bindto = 0.0.0.0
bindport = 5000
[INTERFACE]
uri = tcp://5000
[module res]
[res]
class = secop_psi.ls370res.ResChannel
.channel = 3
.description = resistivity
.main = lsmain
.iodev = lscom
channel = 3
description = resistivity
main = lsmain
io = lscom
[module lsmain]
[lsmain]
class = secop_psi.ls370res.Main
.description = main control of Lsc controller
.iodev = lscom
description = main control of Lsc controller
io = lscom
[module lscom]
[lscom]
class = secop_psi.ls370sim.Ls370Sim
.description = simulated serial communicator to a LS 370
.visibility = 3
description = simulated serial communicator to a LS 370
visibility = 3

View File

@ -1,24 +1,20 @@
[NODE]
id = ls370res.psi.ch
[node LscSIM.psi.ch]
description = Lsc370 Test
[INTERFACE]
uri = tcp://5000
[interface tcp]
type = tcp
bindto = 0.0.0.0
bindport = 5000
[lsmain_iodev]
description = the communication device
class = secop_psi.ls370res.StringIO
uri = localhost:4567
[lsmain]
[module lsmain]
class = secop_psi.ls370res.Main
description = main control of Lsc controller
iodev = lsmain_iodev
uri = localhost:4567
[res]
[module res]
class = secop_psi.ls370res.ResChannel
iexc = '1mA'
channel = 5
vexc = '2mV'
channel = 3
description = resistivity
main = lsmain
# the auto created iodev from lsmain:

38
cfg/magsim.cfg Normal file
View File

@ -0,0 +1,38 @@
[NODE]
id = magsim.psi.ch
description = cryo magnet simulation (similar to ppms simulation)
[INTERFACE]
uri = tcp://5000
[tt]
class = secop_psi.ppms.Temp
description = main temperature
io = ppms
[mf]
class = secop_psi.ppms.Field
target.min = -9
target.max = 9
description = magnetic field
io = ppms
[lev]
class = secop_psi.ppms.Level
description = helium level
io = ppms
[ts]
class = secop_psi.ppms.UserChannel
description = sample temperature
enabled = 1
value.unit = K
io = ppms
[ppms]
class = secop_psi.ppms.Main
description = the main and poller module
class_id = QD.MULTIVU.PPMS.1
visibility = 3
pollinterval = 2
export = False

View File

@ -8,117 +8,117 @@ uri = tcp://5000
[tt]
class = secop_psi.ppms.Temp
description = main temperature
iodev = ppms
io = ppms
[mf]
class = secop_psi.ppms.Field
target.min = -9
target.max = 9
.description = magnetic field
.iodev = ppms
description = magnetic field
io = ppms
[pos]
class = secop_psi.ppms.Position
.description = sample rotator
.iodev = ppms
description = sample rotator
io = ppms
[lev]
class = secop_psi.ppms.Level
.description = helium level
.iodev = ppms
description = helium level
io = ppms
[chamber]
class = secop_psi.ppms.Chamber
.description = chamber state
.iodev = ppms
description = chamber state
io = ppms
[r1]
class = secop_psi.ppms.BridgeChannel
.description = resistivity channel 1
.no = 1
description = resistivity channel 1
no = 1
value.unit = Ohm
.iodev = ppms
io = ppms
[r2]
class = secop_psi.ppms.BridgeChannel
.description = resistivity channel 2
.no = 2
description = resistivity channel 2
no = 2
value.unit = Ohm
.iodev = ppms
io = ppms
[r3]
class = secop_psi.ppms.BridgeChannel
.description = resistivity channel 3
.no = 3
description = resistivity channel 3
no = 3
value.unit = Ohm
.iodev = ppms
io = ppms
[r4]
class = secop_psi.ppms.BridgeChannel
.description = resistivity channel 4
.no = 4
description = resistivity channel 4
no = 4
value.unit = Ohm
.iodev = ppms
io = ppms
[i1]
class = secop_psi.ppms.Channel
.description = current channel 1
.no = 1
description = current channel 1
no = 1
value.unit = uA
.iodev = ppms
io = ppms
[i2]
class = secop_psi.ppms.Channel
.description = current channel 2
.no = 2
description = current channel 2
no = 2
value.unit = uA
.iodev = ppms
io = ppms
[i3]
class = secop_psi.ppms.Channel
.description = current channel 3
.no = 3
description = current channel 3
no = 3
value.unit = uA
.iodev = ppms
io = ppms
[i4]
class = secop_psi.ppms.Channel
.description = current channel 4
.no = 4
description = current channel 4
no = 4
value.unit = uA
.iodev = ppms
io = ppms
[v1]
class = secop_psi.ppms.DriverChannel
.description = voltage channel 1
.no = 1
description = voltage channel 1
no = 1
value.unit = V
.iodev = ppms
io = ppms
[v2]
class = secop_psi.ppms.DriverChannel
.description = voltage channel 2
.no = 2
description = voltage channel 2
no = 2
value.unit = V
.iodev = ppms
io = ppms
[tv]
class = secop_psi.ppms.UserChannel
.description = VTI temperature
description = VTI temperature
enabled = 1
value.unit = K
.iodev = ppms
io = ppms
[ts]
class = secop_psi.ppms.UserChannel
.description = sample temperature
description = sample temperature
enabled = 1
value.unit = K
.iodev = ppms
io = ppms
[ppms]
class = secop_psi.ppms.Main
.description = the main and poller module
.class_id = QD.MULTIVU.PPMS.1
.visibility = 3
description = the main and poller module
class_id = QD.MULTIVU.PPMS.1
visibility = 3
pollinterval = 2

View File

@ -5,26 +5,29 @@ description = [sim] uniaxial pressure device
[INTERFACE]
uri=tcp://5000
[drv]
class = secop.simulation.SimDrivable
extra_params = speed, safe_current, safe_step, maxcurrent
description = simulated motor
value.default = 0
speed.readonly = False
speed.default = 10
interval = 0.11
[transducer]
class = secop_psi.simdpm.DPM3
description = simulated force
motor = drv
[force]
class = secop_psi.uniax.Uniax
description = uniax driver
motor = drv
transducer = transducer
[drv]
class = secop.simulation.SimDrivable
extra_params = speed, safe_current, move_limit, maxcurrent, tolerance
description = simulated motor
value.default = 0
speed.readonly = False
speed.default = 40
interval = 0.11
value.unit = deg
tolerance.default = 0.9
[transducer]
class = secop_psi.simdpm.DPM3
description = simulated force
motor = drv
value.unit = 'N'
[res]
class = secop.simulation.SimReadable
description = raw temperature sensor on the stick
@ -37,5 +40,5 @@ value.datatype = {"type":"double", "unit":"Ohm"}
class=secop_psi.softcal.Sensor
description=temperature sensor, soft calibration
rawsensor=res
calib = X132254.340
calib = X132254
value.unit = "K"

View File

@ -49,5 +49,5 @@ channel = A
[T]
class = secop_psi.softcal.Sensor
rawsensor = res
calib = /home/l_samenv/frappy/secop_psi/calcurves/X132254.340
calib = X132254
value.unit = K

40
ci/Jenkinsfile vendored
View File

@ -30,6 +30,8 @@ def changedFiles = '';
def run_pylint(pyver) {
stage ('pylint-' + pyver) {
def cpylint = "RUNNING"
gerritPostCheck(["jenkins:pylint_${pyver}": cpylint])
def status = 'OK'
changedFiles = sh returnStdout: true, script: '''\
#!/bin/bash
@ -56,8 +58,8 @@ fi
withCredentials([string(credentialsId: 'GERRITHTTP',
variable: 'GERRITHTTP')]) {
sh """\
#!/bin/bash
if [ -f pylint_results.txt ] ; then
#!/bin/bash
if [ -f pylint_results.txt ] ; then
/home/jenkins/tools2/bin/pylint2gerrit
mv pylint_results.txt pylint-${pyver}.txt
else
@ -68,18 +70,15 @@ fi
echo "pylint result: $res"
this.verifyresult.put('pylint'+pyver, 1)
cpylint = "SUCCESSFUL"
if ( res != 0 ) {
currentBuild.result='FAILURE'
this.verifyresult.put('pylint'+ pyver, -1)
status = 'FAILURE'
cpylint = "FAILED"
}
gerritverificationpublisher([
verifyStatusValue: this.verifyresult['pylint'+pyver],
verifyStatusCategory: 'pylint ',
verifyStatusName: 'pylint-'+pyver,
verifyStatusReporter: 'jenkins',
verifyStatusRerun: '!recheck'])
gerritPostCheck(["jenkins:pylint_${pyver}": cpylint])
archiveArtifacts([allowEmptyArchive: true,
artifacts: 'pylint-*.txt'])
recordIssues([enabledForFailure: true,
@ -99,7 +98,9 @@ fi
def run_tests(pyver) {
stage('Test:' + pyver) {
writeFile file: 'setup.cfg', text: '''
def cpytest = "RUNNING"
gerritPostCheck(["jenkins:pytest_${pyver}":"RUNNING"])
writeFile file: 'setup.cfg', text: '''
[tool:pytest]
addopts = --junit-xml=pytest.xml --junit-prefix=''' + pyver
@ -116,18 +117,15 @@ python3 setup.py develop
make test
'''
verifyresult.put(pyver, 1)
cpytest = "SUCCESSFUL"
}
} catch (all) {
currentBuild.result = 'FAILURE'
status = 'FAILURE'
cpytest= "FAILED"
verifyresult.put(pyver, -1)
}
gerritverificationpublisher([
verifyStatusValue: verifyresult[pyver],
verifyStatusCategory: 'test ',
verifyStatusName: 'pytest-'+pyver,
verifyStatusReporter: 'jenkins',
verifyStatusRerun: '!recheck'])
gerritPostCheck(["jenkins:pytest_${pyver}":cpytest])
step([$class: 'JUnitResultArchiver', allowEmptyResults: true,
keepLongStdio: true, testResults: 'pytest.xml'])
@ -138,6 +136,8 @@ make test
}
def run_docs() {
def cdocs = "RUNNING"
gerritPostCheck(["jenkins:docs":cdocs])
stage('prepare') {
sh '''
. /home/jenkins/secopvenv/bin/activate
@ -185,15 +185,9 @@ def run_docs() {
stage('store html doc for build') {
publishHTML([allowMissing: false, alwaysLinkToLastBuild: false, keepAll: true, reportDir: 'doc/_build/html', reportFiles: 'index.html', reportName: 'Built documentation', reportTitles: ''])
gerritverificationpublisher([
verifyStatusValue: 1,
verifyStatusCategory: 'test ',
verifyStatusName: 'doc',
verifyStatusReporter: 'jenkins',
verifyStatusRerun: '@recheck'
])
cdocs = "SUCCESSFUL"
}
gerritPostCheck(["jenkins:docs":cdocs])
}

170
debian/changelog vendored
View File

@ -1,3 +1,111 @@
secop-core (0.12.4) focal; urgency=medium
* fix command inheritance
-- Markus Zolliker <jenkins@jenkins01.admin.frm2.tum.de> Thu, 11 Nov 2021 16:21:19 +0100
secop-core (0.12.3) focal; urgency=medium
[ Georg Brandl ]
* Makefile: fix docker image
[ Markus Zolliker ]
* various fixes
* remove irrelevant comments
* introduce BytesIO
* GUI fixes
* persistent params / trinamic motor
* fix Parameter/Command copy method
* show first instead of last traceback on multiple errors
* fix parameter inheritance
* fix property inheritance
* fix python 3.5 compatibility
* omit updates of unchanged values within short time
* improve simulation
* automatically register subclasses of AsynConn
* fix feature for removing commands
-- Georg Brandl <jenkins@jenkins01.admin.frm2.tum.de> Wed, 10 Nov 2021 16:33:19 +0100
secop-core (0.12.2) focal; urgency=medium
[ Markus Zolliker ]
* fix issue with new syntax in simulation
* treat specifier of describe message
* allow to remove accessibles
[ Enrico Faulhaber ]
* secop_mlz: small fixes
-- Markus Zolliker <jenkins@jenkins01.admin.frm2.tum.de> Tue, 18 May 2021 10:29:17 +0200
secop-core (0.12.1) focal; urgency=medium
* remove secop-console from debian *.install file
-- Enrico Faulhaber <jenkins@jenkins02.admin.frm2.tum.de> Tue, 04 May 2021 09:42:53 +0200
secop-core (0.12.0) focal; urgency=medium
[ Markus Zolliker ]
* make datatypes immutable
* customizable general config
* support for multiple secop servers
* secop.asynconn without pyserial
* change cfg file format
* fix bug in secop.gui.valuewidgets
* fix deadlock when reconnecting client
* allow class instead of class name in proxy_class
* fix pylint command in Makefile
* change arguments of stringio-server
* router bug fix
* introduce update callbacks
* improve error handling on client connections
* ppms: improve status and temperature
* rework tcp server
* cosmetics on datatypes.TextType
* improve error handling in SecopClient
* improve HasIodev
* HasIodev bug fix
* fix handling of StructOf datatype
* more flexible end_of_line in stringio
* improvements on PPMS and LS370
* add readbytes method to AsynConn
* Param(..., initwrite=True) works only with poll=True
* fix initwrite behaviour
* make order of accessibles work again
* main module of LS370 is now drivable
* improve softcal
* make arguments of Parameter and Override consistent
* new syntax for parameter/commands/properties
* enhance documentation
* removed old style syntax
* after running isort
* try to follow PEP8
* fix inheritance order
* remove obsolete code
* lookup cfg files in a list of directories
* added hook for optional history writer
* fixed errors during migration
* move historywriter to secop_psi
* fix autoscan behaviour in ls370res
[ l_samenv ]
* improve tutorial_helevel
* fixed bugs from syntax migration
[ Markus Zolliker ]
* user friendly reporting of config errors
[ Bjoern Pedersen ]
* Jenkisfile: verification
* Fixes to Jenkinsfile
* No pull for images, they are recreated in the job
* Another Jenkisfile error
* Correct checks enum
-- Markus Zolliker <jenkins@jenkins02.admin.frm2.tum.de> Tue, 04 May 2021 08:49:57 +0200
secop-core (0.11.6) unstable; urgency=medium
* fix secop-generator
@ -133,7 +241,7 @@ secop-core (0.10.5) unstable; urgency=low
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Tue, 29 Oct 2019 16:33:18 +0100
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Tue, 29 Oct 2019 16:33:18 +0100
secop-core (0.10.3) unstable; urgency=low
@ -142,7 +250,7 @@ secop-core (0.10.3) unstable; urgency=low
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Fri, 11 Oct 2019 10:49:43 +0200
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Fri, 11 Oct 2019 10:49:43 +0200
secop-core (0.10.2) unstable; urgency=low
@ -153,7 +261,7 @@ secop-core (0.10.2) unstable; urgency=low
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Fri, 11 Oct 2019 10:42:58 +0200
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Fri, 11 Oct 2019 10:42:58 +0200
secop-core (0.10.1) unstable; urgency=low
@ -162,7 +270,7 @@ secop-core (0.10.1) unstable; urgency=low
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 26 Sep 2019 16:41:10 +0200
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 26 Sep 2019 16:41:10 +0200
secop-core (0.10.0) unstable; urgency=low
@ -171,7 +279,7 @@ secop-core (0.10.0) unstable; urgency=low
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 26 Sep 2019 16:31:14 +0200
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 26 Sep 2019 16:31:14 +0200
secop-core (0.9.0) unstable; urgency=low
@ -198,7 +306,7 @@ secop-core (0.9.0) unstable; urgency=low
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 26 Sep 2019 16:26:07 +0200
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 26 Sep 2019 16:26:07 +0200
secop-core (0.8.1) unstable; urgency=low
@ -207,7 +315,7 @@ secop-core (0.8.1) unstable; urgency=low
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Wed, 25 Sep 2019 15:40:44 +0200
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Wed, 25 Sep 2019 15:40:44 +0200
secop-core (0.8.0) unstable; urgency=low
@ -275,7 +383,7 @@ secop-core (0.8.0) unstable; urgency=low
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Wed, 25 Sep 2019 10:27:51 +0200
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Wed, 25 Sep 2019 10:27:51 +0200
secop-core (0.7.0) unstable; urgency=low
@ -311,7 +419,7 @@ secop-core (0.7.0) unstable; urgency=low
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 28 Mar 2019 13:46:08 +0100
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 28 Mar 2019 13:46:08 +0100
secop-core (0.6.4) unstable; urgency=low
@ -376,7 +484,7 @@ secop-core (0.6.4) unstable; urgency=low
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 20 Dec 2018 16:44:03 +0100
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 20 Dec 2018 16:44:03 +0100
secop-core (0.6.3) unstable; urgency=low
@ -390,7 +498,7 @@ secop-core (0.6.3) unstable; urgency=low
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Fri, 27 Jul 2018 09:31:59 +0200
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Fri, 27 Jul 2018 09:31:59 +0200
secop-core (0.6.2) unstable; urgency=low
@ -429,7 +537,7 @@ secop-core (0.6.2) unstable; urgency=low
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Wed, 18 Jul 2018 12:06:57 +0200
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Wed, 18 Jul 2018 12:06:57 +0200
secop-core (0.6.1) unstable; urgency=low
@ -438,7 +546,7 @@ secop-core (0.6.1) unstable; urgency=low
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 19 Apr 2018 10:24:44 +0200
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 19 Apr 2018 10:24:44 +0200
secop-core (0.6.0) unstable; urgency=low
@ -458,7 +566,7 @@ secop-core (0.6.0) unstable; urgency=low
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Tue, 17 Apr 2018 17:38:52 +0200
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Tue, 17 Apr 2018 17:38:52 +0200
secop-core (0.5.0) unstable; urgency=low
@ -521,7 +629,7 @@ secop-core (0.5.0) unstable; urgency=low
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Tue, 17 Apr 2018 12:45:58 +0200
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Tue, 17 Apr 2018 12:45:58 +0200
secop-core (0.4.4) unstable; urgency=low
@ -530,7 +638,7 @@ secop-core (0.4.4) unstable; urgency=low
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Sun, 24 Sep 2017 22:25:01 +0200
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Sun, 24 Sep 2017 22:25:01 +0200
secop-core (0.4.3) unstable; urgency=low
@ -539,7 +647,7 @@ secop-core (0.4.3) unstable; urgency=low
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Fri, 22 Sep 2017 17:29:46 +0200
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Fri, 22 Sep 2017 17:29:46 +0200
secop-core (0.4.2) unstable; urgency=low
@ -548,7 +656,7 @@ secop-core (0.4.2) unstable; urgency=low
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Fri, 22 Sep 2017 16:37:59 +0200
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Fri, 22 Sep 2017 16:37:59 +0200
secop-core (0.4.1) unstable; urgency=low
@ -557,7 +665,7 @@ secop-core (0.4.1) unstable; urgency=low
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Fri, 22 Sep 2017 13:25:28 +0200
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Fri, 22 Sep 2017 13:25:28 +0200
secop-core (0.4.0) unstable; urgency=low
@ -567,7 +675,7 @@ secop-core (0.4.0) unstable; urgency=low
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Fri, 22 Sep 2017 10:33:04 +0200
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Fri, 22 Sep 2017 10:33:04 +0200
secop-core (0.3.0) unstable; urgency=low
@ -633,7 +741,7 @@ secop-core (0.3.0) unstable; urgency=low
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Mon, 18 Sep 2017 14:18:36 +0200
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Mon, 18 Sep 2017 14:18:36 +0200
secop-core (0.2.0) unstable; urgency=low
@ -642,7 +750,7 @@ secop-core (0.2.0) unstable; urgency=low
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 07 Sep 2017 14:55:41 +0200
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 07 Sep 2017 14:55:41 +0200
secop-core (0.1.1) unstable; urgency=low
@ -651,7 +759,7 @@ secop-core (0.1.1) unstable; urgency=low
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 07 Sep 2017 11:02:19 +0200
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 07 Sep 2017 11:02:19 +0200
secop-core (0.1.0) unstable; urgency=low
@ -660,7 +768,7 @@ secop-core (0.1.0) unstable; urgency=low
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 07 Sep 2017 10:50:24 +0200
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 07 Sep 2017 10:50:24 +0200
secop-core (0.0.8) unstable; urgency=low
@ -669,7 +777,7 @@ secop-core (0.0.8) unstable; urgency=low
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Tue, 01 Aug 2017 14:13:11 +0200
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Tue, 01 Aug 2017 14:13:11 +0200
secop-core (0.0.7) unstable; urgency=low
@ -678,7 +786,7 @@ secop-core (0.0.7) unstable; urgency=low
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Tue, 01 Aug 2017 13:52:15 +0200
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Tue, 01 Aug 2017 13:52:15 +0200
secop-core (0.0.6) unstable; urgency=low
@ -688,7 +796,7 @@ secop-core (0.0.6) unstable; urgency=low
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Tue, 01 Aug 2017 13:39:07 +0200
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Tue, 01 Aug 2017 13:39:07 +0200
secop-core (0.0.5) unstable; urgency=low
@ -697,7 +805,7 @@ secop-core (0.0.5) unstable; urgency=low
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Tue, 01 Aug 2017 13:11:43 +0200
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Tue, 01 Aug 2017 13:11:43 +0200
secop-core (0.0.4) unstable; urgency=low
@ -706,7 +814,7 @@ secop-core (0.0.4) unstable; urgency=low
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 27 Jul 2017 11:39:42 +0200
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 27 Jul 2017 11:39:42 +0200
secop-core (0.0.3) unstable; urgency=low
@ -716,7 +824,7 @@ secop-core (0.0.3) unstable; urgency=low
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 27 Jul 2017 11:27:28 +0200
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 27 Jul 2017 11:27:28 +0200
secop-core (0.0.2) unstable; urgency=medium
@ -794,4 +902,4 @@ secop-core (0.0.2) unstable; urgency=medium
[ Jenkins ]
-- Jenkins <jenkins@debuild.taco.frm2> Wed, 19 Jul 2017 11:44:13 +0200
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Wed, 19 Jul 2017 11:44:13 +0200

View File

@ -1,5 +1,4 @@
usr/bin/secop-server
usr/bin/secop-console
usr/lib/python3.*/dist-packages/secop/*.py
usr/lib/python3.*/dist-packages/secop/lib
usr/lib/python3.*/dist-packages/secop/client

View File

@ -4,8 +4,10 @@ Reference
Module Base Classes
...................
.. autodata:: secop.modules.Done
.. autoclass:: secop.modules.Module
:members: earlyInit, initModule, startModule, pollerClass
:members: earlyInit, initModule, startModule
.. autoclass:: secop.modules.Readable
:members: Status
@ -49,11 +51,15 @@ Communication
:show-inheritance:
:members: communicate
.. autoclass:: secop.stringio.StringIO
.. autoclass:: secop.io.StringIO
:show-inheritance:
:members: communicate, multicomm
.. autoclass:: secop.stringio.HasIodev
.. autoclass:: secop.io.BytesIO
:show-inheritance:
:members: communicate, multicomm
.. autoclass:: secop.io.HasIO
:show-inheritance:
.. autoclass:: secop.iohandler.IOHandlerBase

View File

@ -22,7 +22,7 @@ CCU4 luckily has a very simple and logical protocol:
.. code:: python
# the most common Frappy classes can be imported from secop.core
from secop.core import Readable, Parameter, FloatRange, BoolType, StringIO, HasIodev
from secop.core import Readable, Parameter, FloatRange, BoolType, StringIO, HasIO
class CCU4IO(StringIO):
@ -34,14 +34,13 @@ CCU4 luckily has a very simple and logical protocol:
identification = [('cid', r'CCU4.*')]
# inheriting the HasIodev mixin creates us a private attribute *_iodev*
# for talking with the hardware
# inheriting HasIO allows us to use the communicate method for talking with the hardware
# Readable as a base class defines the value and status parameters
class HeLevel(HasIodev, Readable):
class HeLevel(HasIO, Readable):
"""He Level channel of CCU4"""
# define the communication class to create the IO module
iodevClass = CCU4IO
ioClass = CCU4IO
# define or alter the parameters
# as Readable.value exists already, we give only the modified property 'unit'
@ -49,7 +48,7 @@ CCU4 luckily has a very simple and logical protocol:
def read_value(self):
# method for reading the main value
reply = self._iodev.communicate('h') # send 'h\n' and get the reply 'h=<value>\n'
reply = self.communicate('h') # send 'h\n' and get the reply 'h=<value>\n'
name, txtvalue = reply.split('=')
assert name == 'h' # check that we got a reply to our command
return txtvalue # the framework will automatically convert the string to a float
@ -115,17 +114,17 @@ the status codes from the hardware to the standard SECoP status codes.
}
def read_status(self):
name, txtvalue = self._iodev.communicate('hsf').split('=')
name, txtvalue = self.communicate('hsf').split('=')
assert name == 'hsf'
return self.STATUS_MAP(int(txtvalue))
def read_empty_length(self):
name, txtvalue = self._iodev.communicate('hem').split('=')
name, txtvalue = self.communicate('hem').split('=')
assert name == 'hem'
return txtvalue
def write_empty_length(self, value):
name, txtvalue = self._iodev.communicate('hem=%g' % value).split('=')
name, txtvalue = self.communicate('hem=%g' % value).split('=')
assert name == 'hem'
return txtvalue
@ -152,7 +151,7 @@ which means it might be worth to create a *query* method, and then the
for changing a parameter
:returns: the (new) value of the parameter
"""
name, txtvalue = self._iodev.communicate(cmd).split('=')
name, txtvalue = self.communicate(cmd).split('=')
assert name == cmd.split('=')[0] # check that we got a reply to our command
return txtvalue # Frappy will automatically convert the string to the needed data type

View File

@ -1,6 +1,7 @@
# doc
sphinx_rtd_theme
Sphinx>=1.2.1
# for generating docu
markdown>=2.6
# test suite
pytest

View File

@ -8,7 +8,7 @@ python-daemon >=2.0
# for zmq interface
#pyzmq>=13.1.0
#for ppms on windows
# don't forget to run
# don't forget to run
# 'python Scripts/pywin32_postinstall.py -install'
# from elevated prompt after install
#pywin32

View File

@ -31,10 +31,16 @@ from secop.datatypes import ArrayOf, BLOBType, BoolType, EnumType, \
from secop.iohandler import IOHandler, IOHandlerBase
from secop.lib.enum import Enum
from secop.modules import Attached, Communicator, \
Done, Drivable, Module, Readable, Writable
Done, Drivable, Module, Readable, Writable, HasAccessibles
from secop.params import Command, Parameter
from secop.poller import AUTO, DYNAMIC, REGULAR, SLOW
from secop.properties import Property
from secop.proxy import Proxy, SecNode, proxy_class
from secop.io import HasIodev, StringIO, BytesIO
from secop.io import HasIO, StringIO, BytesIO, HasIodev # TODO: remove HasIodev (legacy stuff)
from secop.persistent import PersistentMixin, PersistentParam
from secop.rwhandler import ReadHandler, WriteHandler, CommonReadHandler, \
CommonWriteHandler, nopoll
ERROR = Drivable.Status.ERROR
WARN = Drivable.Status.WARN
BUSY = Drivable.Status.BUSY
IDLE = Drivable.Status.IDLE

View File

@ -22,7 +22,7 @@
# *****************************************************************************
"""Define validated data types."""
# pylint: disable=abstract-method
# pylint: disable=abstract-method, too-many-lines
import sys
@ -30,20 +30,12 @@ from base64 import b64decode, b64encode
from secop.errors import BadValueError, \
ConfigError, ProgrammingError, ProtocolError
from secop.lib import clamp
from secop.lib import clamp, generalConfig
from secop.lib.enum import Enum
from secop.parse import Parser
from secop.properties import HasProperties, Property
# Only export these classes for 'from secop.datatypes import *'
__all__ = [
'DataType', 'get_datatype',
'FloatRange', 'IntRange', 'ScaledInteger',
'BoolType', 'EnumType',
'BLOBType', 'StringType', 'TextType',
'TupleOf', 'ArrayOf', 'StructOf',
'CommandType', 'StatusType',
]
generalConfig.set_default('lazy_number_validation', False)
# *DEFAULT* limits for IntRange/ScaledIntegers transport serialisation
DEFAULT_MIN_INT = -16777216
@ -53,6 +45,11 @@ UNLIMITED = 1 << 64 # internal limit for integers, is probably high enough for
Parser = Parser()
class DiscouragedConversion(BadValueError):
"""the discouraged conversion string - > float happened"""
log_message = True
# base class for all DataTypes
class DataType(HasProperties):
"""base class for all data types"""
@ -63,7 +60,7 @@ class DataType(HasProperties):
def __call__(self, value):
"""check if given value (a python obj) is valid for this datatype
returns the value or raises an appropriate exception"""
returns the (possibly converted) value or raises an appropriate exception"""
raise NotImplementedError
def from_string(self, text):
@ -192,9 +189,15 @@ class FloatRange(DataType):
def __call__(self, value):
try:
value = float(value)
value += 0.0 # do not accept strings here
except Exception:
raise BadValueError('Can not convert %r to float' % value) from None
try:
value = float(value)
except Exception:
raise BadValueError('Can not convert %r to float' % value) from None
if not generalConfig.lazy_number_validation:
raise DiscouragedConversion('automatic string to float conversion no longer supported') from None
# map +/-infty to +/-max possible number
value = clamp(-sys.float_info.max, value, sys.float_info.max)
@ -232,6 +235,18 @@ class FloatRange(DataType):
return ' '.join([self.fmtstr % value, unit])
return self.fmtstr % value
def problematic_range(self, target_type):
"""check problematic range
returns True when self.min or self.max is given, not 0 and equal to the same limit on target_type.
"""
value_info = self.get_info()
target_info = target_type.get_info()
minval = value_info.get('min') # None when -infinite
maxval = value_info.get('max') # None when +infinite
return ((minval and minval == target_info.get('min')) or
(maxval and maxval == target_info.get('max')))
def compatible(self, other):
if not isinstance(other, (FloatRange, ScaledInteger)):
raise BadValueError('incompatible datatypes')
@ -265,10 +280,16 @@ class IntRange(DataType):
def __call__(self, value):
try:
fvalue = float(value)
fvalue = value + 0.0 # do not accept strings here
value = int(value)
except Exception:
raise BadValueError('Can not convert %r to int' % value) from None
try:
fvalue = float(value)
value = int(value)
except Exception:
raise BadValueError('Can not convert %r to int' % value) from None
if not generalConfig.lazy_number_validation:
raise DiscouragedConversion('automatic string to float conversion no longer supported') from None
if not self.min <= value <= self.max or round(fvalue) != fvalue:
raise BadValueError('%r should be an int between %d and %d' %
(value, self.min, self.max))
@ -298,13 +319,15 @@ class IntRange(DataType):
return '%d' % value
def compatible(self, other):
if isinstance(other, IntRange):
if isinstance(other, (IntRange, FloatRange, ScaledInteger)):
other(self.min)
other(self.max)
return
# this will accept some EnumType, BoolType
for i in range(self.min, self.max + 1):
other(i)
if isinstance(other, (EnumType, BoolType)):
# the following loop will not cycle more than the number of Enum elements
for i in range(self.min, self.max + 1):
other(i)
raise BadValueError('incompatible datatypes')
class ScaledInteger(DataType):
@ -369,9 +392,14 @@ class ScaledInteger(DataType):
def __call__(self, value):
try:
value = float(value)
value += 0.0 # do not accept strings here
except Exception:
raise BadValueError('Can not convert %r to float' % value) from None
try:
value = float(value)
except Exception:
raise BadValueError('Can not convert %r to float' % value) from None
if not generalConfig.lazy_number_validation:
raise DiscouragedConversion('automatic string to float conversion no longer supported') from None
prec = max(self.scale, abs(value * self.relative_resolution),
self.absolute_resolution)
if self.min - prec <= value <= self.max + prec:
@ -427,7 +455,10 @@ class EnumType(DataType):
super().__init__()
if members is not None:
kwds.update(members)
self._enum = Enum(enum_or_name, **kwds)
if isinstance(enum_or_name, str):
self._enum = Enum(enum_or_name, kwds) # allow 'self' as name
else:
self._enum = Enum(enum_or_name, **kwds)
self.default = self._enum[self._enum.members[0]]
def copy(self):
@ -852,6 +883,8 @@ class StructOf(DataType):
:param optional: a list of optional members
:param members: names as keys and types as values for all members
"""
# Remark: assignment of parameters containing partial structs in their datatype
# are (and can) not be handled here! This has to be done manually in the write method
def __init__(self, optional=None, **members):
super().__init__()
self.members = members
@ -955,10 +988,9 @@ class CommandType(DataType):
return props
def __repr__(self):
argstr = repr(self.argument) if self.argument else ''
if self.result is None:
return 'CommandType(%s)' % argstr
return 'CommandType(%s, %s)' % (argstr, repr(self.result))
return 'CommandType(%s)' % (repr(self.argument) if self.argument else '')
return 'CommandType(%s, %s)' % (repr(self.argument), repr(self.result))
def __call__(self, value):
"""return the validated argument value or raise"""
@ -1119,37 +1151,26 @@ def floatargs(kwds):
DATATYPES = dict(
bool = lambda **kwds:
BoolType(),
int = lambda min, max, **kwds:
IntRange(minval=min, maxval=max),
scaled = lambda scale, min, max, **kwds:
ScaledInteger(scale=scale, minval=min*scale, maxval=max*scale, **floatargs(kwds)),
double = lambda min=None, max=None, **kwds:
FloatRange(minval=min, maxval=max, **floatargs(kwds)),
blob = lambda maxbytes, minbytes=0, **kwds:
BLOBType(minbytes=minbytes, maxbytes=maxbytes),
string = lambda minchars=0, maxchars=None, isUTF8=False, **kwds:
StringType(minchars=minchars, maxchars=maxchars, isUTF8=isUTF8),
array = lambda maxlen, members, minlen=0, pname='', **kwds:
ArrayOf(get_datatype(members, pname), minlen=minlen, maxlen=maxlen),
tuple = lambda members, pname='', **kwds:
TupleOf(*tuple((get_datatype(t, pname) for t in members))),
enum = lambda members, pname='', **kwds:
EnumType(pname, members=members),
struct = lambda members, optional=None, pname='', **kwds:
StructOf(optional, **dict((n, get_datatype(t, pname)) for n, t in list(members.items()))),
command = lambda argument=None, result=None, pname='', **kwds:
CommandType(get_datatype(argument, pname), get_datatype(result)),
limit = lambda members, pname='', **kwds:
LimitsType(get_datatype(members, pname)),
)

View File

@ -25,7 +25,7 @@
class SECoPError(RuntimeError):
def __init__(self, *args, **kwds):
RuntimeError.__init__(self)
super().__init__()
self.args = args
for k, v in list(kwds.items()):
setattr(self, k, v)
@ -151,7 +151,8 @@ EXCEPTIONS = dict(
IsError=IsErrorError,
Disabled=DisabledError,
SyntaxError=ProtocolError,
NotImplementedError=NotImplementedError,
NotImplemented=NotImplementedError,
ProtocolError=ProtocolError,
InternalError=InternalError,
# internal short versions (candidates for spec)
Protocol=ProtocolError,

View File

@ -39,7 +39,7 @@ COMMENT = 'comment'
class MainWindow(QMainWindow):
def __init__(self, file_path=None, parent=None):
QMainWindow.__init__(self, parent)
super().__init__(parent)
loadUi(self, 'mainwindow.ui')
self.tabWidget.currentChanged.connect(self.tab_relevant_btns_disable)
if file_path is None:

View File

@ -26,7 +26,7 @@ from secop.gui.qt import QHBoxLayout, QSizePolicy, QSpacerItem, Qt, QWidget
class NodeDisplay(QWidget):
def __init__(self, file_path=None, parent=None):
QWidget.__init__(self, parent)
super().__init__(parent)
loadUi(self, 'node_display.ui')
self.saved = bool(file_path)
self.created = self.tree_widget.set_file(file_path)

View File

@ -44,7 +44,7 @@ class TreeWidgetItem(QTreeWidgetItem):
the datatype passed onto ValueWidget should be on of secop.datatypes"""
# TODO: like stated in docstring the datatype for parameters and
# properties must be found out through their object
QTreeWidgetItem.__init__(self, parent)
super().__init__(parent)
self.kind = kind
self.name = name
self.class_object = class_object
@ -129,7 +129,7 @@ class ValueWidget(QWidget):
def __init__(self, name='', value='', datatype=None, kind='', parent=None):
# TODO: implement: change module/interface class
QWidget.__init__(self, parent)
super().__init__(parent)
self.datatype = datatype
self.layout = QVBoxLayout()
self.name_label = QLabel(name)
@ -205,7 +205,7 @@ class ValueWidget(QWidget):
class ChangeNameDialog(QDialog):
def __init__(self, current_name='', invalid_names=None, parent=None):
QWidget.__init__(self, parent)
super().__init__(parent)
loadUi(self, 'change_name_dialog.ui')
self.invalid_names = invalid_names
self.name.setText(current_name)

View File

@ -29,7 +29,7 @@ from secop.modules import Module
from secop.params import Parameter
from secop.properties import Property
from secop.protocol.interface.tcp import TCPServer
from secop.server import getGeneralConfig
from secop.server import generalConfig
uipath = path.dirname(__file__)
@ -106,7 +106,7 @@ def get_file_paths(widget, open_file=True):
def get_modules():
modules = {}
base_path = getGeneralConfig()['basedir']
base_path = generalConfig.basedir
# pylint: disable=too-many-nested-blocks
for dirname in listdir(base_path):
if dirname.startswith('secop_'):
@ -156,7 +156,7 @@ def get_interface_class_from_name(name):
def get_interfaces():
# TODO class must be found out like for modules
interfaces = []
interface_path = path.join(getGeneralConfig()['basedir'], 'secop',
interface_path = path.join(generalConfig.basedir, 'secop',
'protocol', 'interface')
for filename in listdir(interface_path):
if path.isfile(path.join(interface_path, filename)) and \

View File

@ -31,7 +31,7 @@ from secop.gui.cfg_editor.utils import get_all_items, \
get_props, loadUi, set_name_edit_style, setActionIcon
from secop.gui.qt import QComboBox, QDialog, QDialogButtonBox, QLabel, \
QLineEdit, QMenu, QPoint, QSize, QStandardItem, QStandardItemModel, \
Qt, QTabBar, QTextEdit, QTreeView, QTreeWidget, QWidget, pyqtSignal
Qt, QTabBar, QTextEdit, QTreeView, QTreeWidget, pyqtSignal
NODE = 'node'
MODULE = 'module'
@ -47,7 +47,7 @@ class TreeWidget(QTreeWidget):
add_canceled = pyqtSignal()
def __init__(self, parent=None):
QTreeWidget.__init__(self, parent)
super().__init__(parent)
self.file_path = None
self.setIconSize(QSize(24, 24))
self.setSelectionMode(QTreeWidget.SingleSelection)
@ -335,7 +335,7 @@ class AddDialog(QDialog):
"""Notes:
self.get_value: is mapped to the specific method for getting
the value from self.value"""
QWidget.__init__(self, parent)
super().__init__(parent)
loadUi(self, 'add_dialog.ui')
self.setWindowTitle('add %s' % kind)
self.kind = kind
@ -402,7 +402,7 @@ class AddDialog(QDialog):
class TabBar(QTabBar):
def __init__(self, parent=None):
QTabBar.__init__(self, parent)
super().__init__(parent)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.context_pos = QPoint(0, 0)
self.menu = QMenu()
@ -436,7 +436,7 @@ class TabBar(QTabBar):
class TreeComboBox(QComboBox):
def __init__(self, value_dict, parent=None):
QComboBox.__init__(self, parent)
super().__init__(parent)
self.tree_view = QTreeView()
self.tree_view.setHeaderHidden(True)
self.tree_view.expanded.connect(self.resize_length)

View File

@ -44,7 +44,7 @@ class QSECNode(QObject):
logEntry = pyqtSignal(str)
def __init__(self, uri, parent=None):
QObject.__init__(self, parent)
super().__init__(parent)
self.conn = conn = secop.client.SecopClient(uri)
conn.validate_data = True
self.log = conn.log
@ -83,10 +83,7 @@ class QSECNode(QObject):
return self.conn.getParameter(module, parameter, True)
def execCommand(self, module, command, argument):
try:
return self.conn.execCommand(module, command, argument)
except Exception as e:
return 'ERROR: %r' % e, {}
return self.conn.execCommand(module, command, argument)
def queryCache(self, module):
return {k: Value(*self.conn.cache[(module, k)])
@ -115,7 +112,7 @@ class QSECNode(QObject):
class MainWindow(QMainWindow):
def __init__(self, hosts, parent=None):
super(MainWindow, self).__init__(parent)
super().__init__(parent)
loadUi(self, 'mainwindow.ui')

View File

@ -160,7 +160,7 @@ class MiniPlotFitCurve(MiniPlotCurve):
return float('-inf')
def __init__(self, formula, params):
super(MiniPlotFitCurve, self).__init__()
super().__init__()
self.formula = formula
self.params = params
@ -193,7 +193,7 @@ class MiniPlot(QWidget):
autoticky = True
def __init__(self, parent=None):
QWidget.__init__(self, parent)
super().__init__(parent)
self.xmin = self.xmax = None
self.ymin = self.ymax = None
self.curves = []

View File

@ -32,7 +32,7 @@ from secop.gui.valuewidgets import get_widget
class CommandDialog(QDialog):
def __init__(self, cmdname, argument, parent=None):
super(CommandDialog, self).__init__(parent)
super().__init__(parent)
loadUi(self, 'cmddialog.ui')
self.setWindowTitle('Arguments for %s' % cmdname)
@ -58,7 +58,7 @@ class CommandDialog(QDialog):
return True, self.widgets[0].get_value()
def exec_(self):
if super(CommandDialog, self).exec_():
if super().exec_():
return self.get_value()
return None
@ -71,16 +71,17 @@ def showCommandResultDialog(command, args, result, extras=''):
m.exec_()
def showErrorDialog(error):
def showErrorDialog(command, args, error):
m = QMessageBox()
m.setText('Error %r' % error)
args = '' if args is None else repr(args)
m.setText('calling: %s(%s)\nraised %r' % (command, args, error))
m.exec_()
class ParameterGroup(QWidget):
def __init__(self, groupname, parent=None):
super(ParameterGroup, self).__init__(parent)
super().__init__(parent)
loadUi(self, 'paramgroup.ui')
self._groupname = groupname
@ -112,7 +113,7 @@ class ParameterGroup(QWidget):
class CommandButton(QPushButton):
def __init__(self, cmdname, cmdinfo, cb, parent=None):
super(CommandButton, self).__init__(parent)
super().__init__(parent)
self._cmdname = cmdname
self._argintype = cmdinfo['datatype'].argument # single datatype
@ -140,7 +141,7 @@ class CommandButton(QPushButton):
class ModuleCtrl(QWidget):
def __init__(self, node, module, parent=None):
super(ModuleCtrl, self).__init__(parent)
super().__init__(parent)
loadUi(self, 'modulectrl.ui')
self._node = node
self._module = module
@ -161,10 +162,9 @@ class ModuleCtrl(QWidget):
try:
result, qualifiers = self._node.execCommand(
self._module, command, args)
except TypeError:
result = None
qualifiers = {}
# XXX: flag missing data report as error
except Exception as e:
showErrorDialog(command, args, e)
return
if result is not None:
showCommandResultDialog(command, args, result, qualifiers)

View File

@ -39,7 +39,7 @@ class ParameterWidget(QWidget):
initvalue=None,
readonly=True,
parent=None):
super(ParameterWidget, self).__init__(parent)
super().__init__(parent)
self._module = module
self._paramcmd = paramcmd
self._datatype = datatype
@ -82,7 +82,6 @@ class GenericParameterWidget(ParameterWidget):
else:
value = fmtstr % (value.value,)
self.currentLineEdit.setText(value)
# self.currentLineEdit.setText(str(value))
class EnumParameterWidget(GenericParameterWidget):

215
secop/historywriter.py Normal file
View File

@ -0,0 +1,215 @@
# -*- 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:
# Markus Zolliker <markus.zolliker@psi.ch>
# *****************************************************************************
import time
from os.path import join
from frappyhistory.writer import Writer # pylint: disable=import-error
from secop.lib import clamp, formatExtendedTraceback
from secop.datatypes import IntRange, FloatRange, ScaledInteger,\
EnumType, BoolType, StringType, TupleOf, StructOf, ArrayOf, TextType
def make_cvt_list(dt, tail):
"""create conversion list
list of tuple (<conversion function>, <tail>, <curve options>)
tail is a postfix to be appended in case of tuples and structs
"""
if isinstance(dt, (IntRange, BoolType)):
return [(int, {'key': tail})]
if isinstance(dt, EnumType):
return [(int, {'key': tail, 'enum': dt.export_datatype()['members']})]
if isinstance(dt, (FloatRange, ScaledInteger)):
opts = {'key': tail}
if dt.unit:
opts['unit'] = dt.unit
opts['stepped'] = True
return [(dt.import_value, opts)]
if isinstance(dt, StringType):
opts = {'key': tail, 'kind': 'STR'}
if isinstance(dt, TextType):
opts['category'] = 'no'
else:
opts['category'] = 'string'
return [(lambda x: x, opts)]
if isinstance(dt, TupleOf):
result = []
for index, elmtype in enumerate(dt.members):
for fun, opts in make_cvt_list(elmtype, '%s.%s' % (tail, index)):
def conv(value, key=index, func=fun):
return func(value[key])
result.append((conv, opts))
return result
if isinstance(dt, ArrayOf):
result = []
for index in range(dt.maxlen):
for fun, opts in make_cvt_list(dt.members, '%s.%s' % (tail, index)):
opts['category'] = 'no'
def conv(value, key=index, func=fun):
return func(value[key])
result.append((conv, opts))
return result
if isinstance(dt, StructOf):
result = []
for subkey, elmtype in dt.members.items():
for fun, opts in make_cvt_list(elmtype, '%s.%s' % (tail, subkey)):
def conv(value, key=subkey, func=fun):
return func(value.get(key)) # None for missing struct key, should not be needed
result.append((conv, opts))
return result
return [] # other types (BlobType) are ignored: too much data, probably not used
class FrappyAbstractHistoryWriter:
"""abstract writer
doc only
"""
def put_def(self, key, kind='NUM', category='minor', **opts):
"""define or overwrite a new curve named <key> with options from dict <opts>
:param key: the key for the curve
:param kind: 'NUM' (default) for numeric values, 'STR' for strings
:param category: 'major' or 'minor': importance of curve
:param opts: a dict containing some of the following options
- label: a label for the curve in the chart
- unit: the physical unit
- group: grouping of the curves in charts (unit by default)
- stepped: lines in charts should be drawn as stepped line. Only applicable when kind='NUM'
True by default.
"""
raise NotImplementedError()
def put(self, timestamp, key, value):
"""add a data point
:param timestamp: the timestamp. must not decrease!
:param key: the curve name
:param value: the value to be stored (number or string), None indicates un undefined value
"""
raise NotImplementedError()
def close(self, timestamp):
"""close the writer
:param timestamp:
indicate to the writer that all values are getting undefined after <timestamp>
"""
raise NotImplementedError()
class FrappyHistory(Writer):
def __init__(self, history_path, modules, logger):
minsteps = {} # generalConfig.mintimesteps
super().__init__(history_path, logger, minsteps=minsteps)
self.__init_time = time.time()
self.__last_time = self.__init_time
# bad_timestep = set()
for modname, modobj in modules.items():
# if isinstance(modobj, HasComlog):
# modobj.enableComlog(join(history_path, 'comlog'))
for pname, pobj in modobj.parameters.items():
key = '%s:%s' % (modname, pname)
dt = pobj.datatype
cvt_list = make_cvt_list(dt, key)
given_opts = pobj.history
if isinstance(given_opts, dict):
given_opts = [given_opts] * len(cvt_list)
if pname == 'value':
for _, opts in cvt_list:
opts['key'] = opts['key'].replace(':value', '')
if pname == 'status':
# default labels '<modname>:status' and '<modname>:status_text'
for lbl, (_, opts) in zip([key, key + '_text'], cvt_list):
opts['label'] = lbl
label_set = set()
cvt_filtered = []
for given, (cvt_func, opts) in zip(given_opts, cvt_list):
result = dict(opts, **given)
stepped = result.pop('stepped', None)
if opts.get('stepped'): # True on floats
if pobj.readonly or stepped is False:
result['stepped'] = False
cat = given.get('category')
if cat is None:
if not pobj.export:
continue
cat = result.get('category')
if cat is None:
if pname in ('value', 'target'):
result['category'] = 'major'
elif pobj.readonly:
result['category'] = 'minor'
else:
result['category'] = 'param'
if cat == 'no':
continue
label = result.pop('label', None)
if label and label not in label_set:
result['label'] = label
label_set.add(label)
cvt_filtered.append((cvt_func, result))
# if result.get('timestep', 1) < minstep:
# bad_timestep.add('%s:%s' % (modname, pname))
self.put_def(**result)
if cvt_filtered:
def callback(value, p=pobj, history=self, cvt=cvt_filtered):
if self.__init_time:
t = self.__init_time # on initialisation, use the same timestamp for all
else:
t = p.timestamp
if t:
# make sure time stamp is not decreasing, as a potentially decreasing
# value might bring the reader software into trouble
t = clamp(self.__last_time, t, time.time())
else:
t = time.time()
history.__last_time = t
if pobj.readerror: # error update
for _, opts in cvt:
self.put(t, opts['key'], None)
else:
for fun, opts in cvt:
self.put(t, opts['key'], fun(value))
modobj.valueCallbacks[pname].append(callback)
modobj.errorCallbacks[pname].append(callback)
# if bad_timestep:
# if minstep < 1:
# logger.error('timestep < generalConfig.mintimestep')
# else:
# logger.error('timestep < 1, generalConfig.mintimestep not given?')
# logger.error('parameters: %s', ', '.join(bad_timestep))
#
self.__init_time = None
def close(self):
super().close(max(self.__last_time, time.time()))

View File

@ -29,50 +29,78 @@ import time
import threading
from secop.lib.asynconn import AsynConn, ConnectionClosed
from secop.datatypes import ArrayOf, BLOBType, BoolType, FloatRange, IntRange, StringType, TupleOf, ValueType
from secop.errors import CommunicationFailedError, CommunicationSilentError, ConfigError
from secop.datatypes import ArrayOf, BLOBType, BoolType, FloatRange, IntRange, \
StringType, TupleOf, ValueType
from secop.errors import CommunicationFailedError, CommunicationSilentError, \
ConfigError, ProgrammingError
from secop.modules import Attached, Command, \
Communicator, Done, Module, Parameter, Property
from secop.poller import REGULAR
from secop.lib import generalConfig
generalConfig.set_default('legacy_hasiodev', False)
HEX_CODE = re.compile(r'[0-9a-fA-F][0-9a-fA-F]$')
class HasIodev(Module):
class HasIO(Module):
"""Mixin for modules using a communicator"""
iodev = Attached()
io = Attached()
uri = Property('uri for automatic creation of the attached communication module',
StringType(), default='')
iodevDict = {}
ioDict = {}
ioClass = None
def __init__(self, name, logger, opts, srv):
iodev = opts.get('iodev')
Module.__init__(self, name, logger, opts, srv)
io = opts.get('io')
super().__init__(name, logger, opts, srv)
if self.uri:
opts = {'uri': self.uri, 'description': 'communication device for %s' % name,
'export': False}
ioname = self.iodevDict.get(self.uri)
ioname = self.ioDict.get(self.uri)
if not ioname:
ioname = iodev or name + '_iodev'
iodev = self.iodevClass(ioname, srv.log.getChild(ioname), opts, srv)
srv.modules[ioname] = iodev
self.iodevDict[self.uri] = ioname
self.iodev = ioname
elif not self.iodev:
raise ConfigError("Module %s needs a value for either 'uri' or 'iodev'" % name)
ioname = io or name + '_io'
io = self.ioClass(ioname, srv.log.getChild(ioname), opts, srv) # pylint: disable=not-callable
io.callingModule = []
srv.modules[ioname] = io
self.ioDict[self.uri] = ioname
self.io = ioname
elif not io:
raise ConfigError("Module %s needs a value for either 'uri' or 'io'" % name)
def initModule(self):
try:
self._iodev.read_is_connected()
self.io.read_is_connected()
except (CommunicationFailedError, AttributeError):
# AttributeError: for missing _iodev?
# AttributeError: read_is_connected is not required for an io object
pass
super().initModule()
def sendRecv(self, command):
return self._iodev.communicate(command)
def communicate(self, *args):
return self.io.communicate(*args)
def multicomm(self, *args):
return self.io.multicomm(*args)
class HasIodev(HasIO):
# TODO: remove this legacy mixin
iodevClass = None
@property
def _iodev(self):
return self.io
def __init__(self, name, logger, opts, srv):
self.ioClass = self.iodevClass
super().__init__(name, logger, opts, srv)
if generalConfig.legacy_hasiodev:
self.log.warn('using the HasIodev mixin is deprecated - use HasIO instead')
else:
self.log.error('legacy HasIodev no longer supported')
self.log.error('you may suppress this error message by running the server with --relaxed')
raise ProgrammingError('legacy HasIodev no longer supported')
self.sendRecv = self.communicate
class IOBase(Communicator):
@ -80,7 +108,7 @@ class IOBase(Communicator):
uri = Property('hostname:portnumber', datatype=StringType())
timeout = Parameter('timeout', datatype=FloatRange(0), default=2)
wait_before = Parameter('wait time before sending', datatype=FloatRange(), default=0)
is_connected = Parameter('connection state', datatype=BoolType(), readonly=False, poll=REGULAR)
is_connected = Parameter('connection state', datatype=BoolType(), readonly=False, default=False)
pollinterval = Parameter('reconnect interval', datatype=FloatRange(0), readonly=False, default=10)
_reconnectCallbacks = None
@ -89,6 +117,8 @@ class IOBase(Communicator):
_lock = None
def earlyInit(self):
super().earlyInit()
self._reconnectCallbacks = {}
self._lock = threading.RLock()
def connectStart(self):
@ -103,6 +133,9 @@ class IOBase(Communicator):
self._conn = None
self.is_connected = False
def doPoll(self):
self.read_is_connected()
def read_is_connected(self):
"""try to reconnect, when not connected
@ -139,10 +172,7 @@ class IOBase(Communicator):
if the callback fails or returns False, it is cleared
"""
if self._reconnectCallbacks is None:
self._reconnectCallbacks = {name: func}
else:
self._reconnectCallbacks[name] = func
self._reconnectCallbacks[name] = func
def callCallbacks(self):
for key, cb in list(self._reconnectCallbacks.items()):
@ -154,6 +184,9 @@ class IOBase(Communicator):
if removeme:
self._reconnectCallbacks.pop(key)
def communicate(self, command):
return NotImplementedError
class StringIO(IOBase):
"""line oriented communicator
@ -218,6 +251,7 @@ class StringIO(IOBase):
if not self.is_connected:
self.read_is_connected() # try to reconnect
if not self._conn:
self.log.debug('can not connect to %r' % self.uri)
raise CommunicationSilentError('can not connect to %r' % self.uri)
try:
with self._lock:
@ -234,15 +268,15 @@ class StringIO(IOBase):
if garbage is None: # read garbage only once
garbage = self._conn.flush_recv()
if garbage:
self.log.debug('garbage: %r', garbage)
self.comLog('garbage: %r', garbage)
self._conn.send(cmd + self._eol_write)
self.log.debug('send: %s', cmd + self._eol_write)
self.comLog('> %s', cmd.decode(self.encoding))
reply = self._conn.readline(self.timeout)
except ConnectionClosed as e:
self.closeConnection()
raise CommunicationFailedError('disconnected') from None
reply = reply.decode(self.encoding)
self.log.debug('recv: %s', reply)
self.comLog('< %s', reply)
return reply
except Exception as e:
if str(e) == self._last_error:
@ -291,6 +325,10 @@ def make_bytes(string):
return bytes([int(c, 16) if HEX_CODE.match(c) else ord(c) for c in string.split()])
def hexify(bytes_):
return ' '.join('%02x' % r for r in bytes_)
class BytesIO(IOBase):
identification = Property(
"""identification
@ -330,14 +368,14 @@ class BytesIO(IOBase):
time.sleep(self.wait_before)
garbage = self._conn.flush_recv()
if garbage:
self.log.debug('garbage: %r', garbage)
self.comLog('garbage: %r', garbage)
self._conn.send(request)
self.log.debug('send: %r', request)
self.comLog('> %s', hexify(request))
reply = self._conn.readbytes(replylen, self.timeout)
except ConnectionClosed as e:
self.closeConnection()
raise CommunicationFailedError('disconnected') from None
self.log.debug('recv: %r', reply)
self.comLog('< %s', hexify(reply))
return self.getFullReply(request, reply)
except Exception as e:
if str(e) == self._last_error:
@ -346,6 +384,15 @@ class BytesIO(IOBase):
self.log.error(self._last_error)
raise
@Command((ArrayOf(TupleOf(BLOBType(), IntRange(0)))), result=ArrayOf(BLOBType()))
def multicomm(self, requests):
"""communicate multiple request/replies in one row"""
replies = []
with self._lock:
for request in requests:
replies.append(self.communicate(*request))
return replies
def readBytes(self, nbytes):
"""read bytes
@ -362,7 +409,7 @@ class BytesIO(IOBase):
:return: the full reply (replyheader + additional bytes)
When the reply length is variable, :meth:`communicate` should be called
with the `replylen` argument set to minimum expected length of the reply.
with the `replylen` argument set to the minimum expected length of the reply.
Typically this method determines then the length of additional bytes from
the already received bytes (replyheader) and/or the request and calls
:meth:`readBytes` to get the remaining bytes.

View File

@ -126,7 +126,7 @@ class CmdParser:
try:
argformat % ((0,) * len(casts)) # validate argformat
except ValueError as e:
raise ValueError("%s in %r" % (e, argformat))
raise ValueError("%s in %r" % (e, argformat)) from None
def format(self, *values):
return self.fmt % values
@ -242,7 +242,7 @@ class IOHandler(IOHandlerBase):
contain the command separator at the end.
"""
querycmd = self.make_query(module)
reply = module.sendRecv(changecmd + querycmd)
reply = module.communicate(changecmd + querycmd)
return self.parse_reply(reply)
def send_change(self, module, *values):
@ -253,7 +253,7 @@ class IOHandler(IOHandlerBase):
"""
changecmd = self.make_change(module, *values)
if self.CMDSEPARATOR is None:
module.sendRecv(changecmd) # ignore result
module.communicate(changecmd) # ignore result
return self.send_command(module)
return self.send_command(module, changecmd + self.CMDSEPARATOR)

View File

@ -27,40 +27,126 @@ import socket
import sys
import threading
import traceback
from configparser import ConfigParser
from os import environ, path
repodir = path.abspath(path.join(path.dirname(__file__), '..', '..'))
class GeneralConfig:
"""generalConfig holds server configuration items
if path.splitext(sys.executable)[1] == ".exe" and not path.basename(sys.executable).startswith('python'):
CONFIG = {
'piddir': './',
'logdir': './log',
'confdir': './',
}
elif not path.exists(path.join(repodir, '.git')):
CONFIG = {
'piddir': '/var/run/secop',
'logdir': '/var/log',
'confdir': '/etc/secop',
}
else:
CONFIG = {
'piddir': path.join(repodir, 'pid'),
'logdir': path.join(repodir, 'log'),
'confdir': path.join(repodir, 'cfg'),
}
# overwrite with env variables SECOP_LOGDIR, SECOP_PIDDIR, SECOP_CONFDIR, if present
for dirname in CONFIG:
CONFIG[dirname] = environ.get('SECOP_%s' % dirname.upper(), CONFIG[dirname])
generalConfig.init is to be called before starting the server.
Accessing generalConfig.<key> raises an error, when generalConfig.init is
not yet called, except when a default for <key> is set.
For tests and for imports from client code, a module may access generalConfig
without calling generalConfig.init before. For this, it should call
generalConfig.set_default on import to define defaults for the needed keys.
"""
# this is not customizable
CONFIG['basedir'] = repodir
def __init__(self):
self._config = None
self.defaults = {} #: default values. may be set before or after :meth:`init`
# TODO: if ever more general options are need, we should think about a general config file
def init(self, configfile=None):
"""init default server configuration
:param configfile: if present, keys and values from the [FRAPPY] section are read
if configfile is not given, it tries to guess the location of the configfile
or determine 'piddir', 'logdir', 'confdir' and 'basedir' from the environment.
"""
cfg = {}
mandatory = 'piddir', 'logdir', 'confdir'
repodir = path.abspath(path.join(path.dirname(__file__), '..', '..'))
# create default paths
if path.splitext(sys.executable)[1] == ".exe" and not path.basename(sys.executable).startswith('python'):
# special MS windows environment
cfg.update(piddir='./', logdir='./log', confdir='./')
elif path.exists(path.join(repodir, '.git')):
# running from git repo
cfg['confdir'] = path.join(repodir, 'cfg')
# take logdir and piddir from <repodir>/cfg/generalConfig.cfg
else:
# running on installed system (typically with systemd)
cfg.update(piddir='/var/run/frappy', logdir='/var/log', confdir='/etc/frappy')
if configfile is None:
configfile = environ.get('FRAPPY_CONFIG_FILE',
path.join(cfg['confdir'], 'generalConfig.cfg'))
if configfile and path.exists(configfile):
parser = ConfigParser()
parser.optionxform = str
parser.read([configfile])
# mandatory in a general config file:
cfg['logdir'] = cfg['piddir'] = None
cfg['confdir'] = path.dirname(configfile)
# only the FRAPPY section is relevant, other sections might be used by others
for key, value in parser['FRAPPY'].items():
if value.startswith('./'):
cfg[key] = path.abspath(path.join(repodir, value))
else:
# expand ~ to username, also in path lists separated with ':'
cfg[key] = ':'.join(path.expanduser(v) for v in value.split(':'))
else:
for key in mandatory:
cfg[key] = environ.get('FRAPPY_%s' % key.upper(), cfg[key])
missing_keys = [key for key in mandatory if cfg[key] is None]
if missing_keys:
if path.exists(configfile):
raise KeyError('missing value for %s in %s' % (' and '.join(missing_keys), configfile))
raise FileNotFoundError(configfile)
# this is not customizable
cfg['basedir'] = repodir
self._config = cfg
def __getitem__(self, key):
"""access for keys known to exist
:param key: the key (raises an error when key is not available)
:return: the value
"""
try:
return self._config[key]
except KeyError:
return self.defaults[key]
except TypeError:
if key in self.defaults:
# accept retrieving defaults before init
# e.g. 'lazy_number_validation' in secop.datatypes
return self.defaults[key]
raise TypeError('generalConfig.init() has to be called first') from None
def get(self, key, default=None):
"""access for keys not known to exist"""
try:
return self.__getitem__(key)
except KeyError:
return default
def getint(self, key, default=None):
"""access and convert to int"""
try:
return int(self.__getitem__(key))
except KeyError:
return default
def __getattr__(self, key):
"""goodie: use generalConfig.<key> instead of generalConfig.get('<key>')"""
return self.get(key)
@property
def initialized(self):
return bool(self._config)
def set_default(self, key, value):
"""set a default value, in case not set already"""
if key not in self.defaults:
self.defaults[key] = value
def testinit(self, **kwds):
"""for test purposes"""
self._config = kwds
unset_value = object()
generalConfig = GeneralConfig()
class lazy_property:
@ -253,10 +339,6 @@ def getfqdn(name=''):
return socket.getfqdn(name)
def getGeneralConfig():
return CONFIG
def formatStatusBits(sword, labels, start=0):
"""Return a list of labels according to bit state in `sword` starting
with bit `start` and the first label in `labels`.
@ -266,3 +348,11 @@ def formatStatusBits(sword, labels, start=0):
if sword & (1 << i) and lbl:
result.append(lbl)
return result
class UniqueObject:
def __init__(self, name):
self.name = name
def __repr__(self):
return self.name

View File

@ -48,6 +48,7 @@ class ConnectionClosed(ConnectionError):
class AsynConn:
timeout = 1 # inter byte timeout
scheme = None
SCHEME_MAP = {}
connection = None # is not None, if connected
defaultport = None
@ -62,11 +63,11 @@ class AsynConn:
except (ValueError, TypeError, AssertionError):
if 'COM' in uri:
raise ValueError("the correct uri for a COM port is: "
"'serial://COM<i>[?<option>=<value>[+<option>=value ...]]'")
"'serial://COM<i>[?<option>=<value>[+<option>=value ...]]'") from None
if '/dev' in uri:
raise ValueError("the correct uri for a serial port is: "
"'serial:///dev/<tty>[?<option>=<value>[+<option>=value ...]]'")
raise ValueError('invalid uri: %s' % uri)
"'serial:///dev/<tty>[?<option>=<value>[+<option>=value ...]]'") from None
raise ValueError('invalid uri: %s' % uri) from None
iocls = cls.SCHEME_MAP['tcp']
uri = 'tcp://%s:%d' % host_port
return object.__new__(iocls)
@ -80,7 +81,9 @@ class AsynConn:
@classmethod
def __init_subclass__(cls):
cls.SCHEME_MAP[cls.scheme] = cls
"""register subclass to scheme, if available"""
if cls.scheme:
cls.SCHEME_MAP[cls.scheme] = cls
def disconnect(self):
raise NotImplementedError
@ -166,7 +169,7 @@ class AsynTcp(AsynConn):
self.connection = tcpSocket(uri, self.defaultport, self.timeout)
except (ConnectionRefusedError, socket.gaierror) as e:
# indicate that retrying might make sense
raise CommunicationFailedError(str(e))
raise CommunicationFailedError(str(e)) from None
def disconnect(self):
if self.connection:
@ -237,8 +240,8 @@ class AsynSerial(AsynConn):
options = dict((kv.split('=') for kv in uri[1].split('+')))
except IndexError: # no uri[1], no options
options = {}
except ValueError:
raise ConfigError('illegal serial options')
except ValueError as e:
raise ConfigError('illegal serial options') from e
parity = options.pop('parity', None) # only parity is to be treated as text
for k, v in options.items():
try:
@ -251,14 +254,12 @@ class AsynSerial(AsynConn):
if not fullname.startswith(name):
raise ConfigError('illegal parity: %s' % parity)
options['parity'] = name[0]
if 'timeout' in options:
options['timeout'] = float(self.timeout)
else:
if 'timeout' not in options:
options['timeout'] = self.timeout
try:
self.connection = Serial(dev, **options)
except ValueError as e:
raise ConfigError(e)
raise ConfigError(e) from None
# TODO: turn exceptions into ConnectionFailedError, where a retry makes sense
def disconnect(self):

View File

@ -74,29 +74,29 @@ SIMPLETYPES = {
}
def short_doc(datatype):
def short_doc(datatype, internal=False):
# pylint: disable=possibly-unused-variable
def doc_EnumType(dt):
return 'one of %s' % str(tuple(dt._enum.keys()))
def doc_ArrayOf(dt):
return 'array of %s' % short_doc(dt.members)
return 'array of %s' % short_doc(dt.members, True)
def doc_TupleOf(dt):
return 'tuple of (%s)' % ', '.join(short_doc(m) for m in dt.members)
return 'tuple of (%s)' % ', '.join(short_doc(m, True) for m in dt.members)
def doc_CommandType(dt):
argument = short_doc(dt.argument) if dt.argument else ''
result = ' -> %s' % short_doc(dt.result) if dt.result else ''
argument = short_doc(dt.argument, True) if dt.argument else ''
result = ' -> %s' % short_doc(dt.result, True) if dt.result else ''
return '(%s)%s' % (argument, result) # return argument list only
def doc_NoneOr(dt):
other = short_doc(dt.other)
other = short_doc(dt.other, True)
return '%s or None' % other if other else None
def doc_OrType(dt):
types = [short_doc(t) for t in dt.types]
types = [short_doc(t, True) for t in dt.types]
if None in types: # type is anyway broad: no doc
return None
return ' or '.join(types)
@ -104,14 +104,17 @@ def short_doc(datatype):
def doc_Stub(dt):
return dt.name.replace('Type', '').replace('Range', '').lower()
clsname = datatype.__class__.__name__
def doc_BLOBType(dt):
return 'byte array'
clsname = type(datatype).__name__
result = SIMPLETYPES.get(clsname)
if result:
return result
fun = locals().get('doc_' + clsname)
if fun:
return fun(datatype)
return None # broad type like ValueType: no doc
return clsname if internal else None # broad types like ValueType: no doc
def append_to_doc(cls, lines, itemcls, name, attrname, fmtfunc):

View File

@ -21,41 +21,51 @@
# *****************************************************************************
import threading
import time
ETERNITY = 1e99
class _SingleEvent:
"""Single Event
remark: :meth:`wait` is not implemented on purpose
"""
def __init__(self, multievent, timeout, name=None):
self.multievent = multievent
self.multievent.clear_(self)
self.name = name
if timeout is None:
self.deadline = ETERNITY
else:
self.deadline = time.monotonic() + timeout
def clear(self):
self.multievent.clear_(self)
def set(self):
self.multievent.set_(self)
def is_set(self):
return self in self.multievent.events
class MultiEvent(threading.Event):
"""Class implementing multi event objects.
"""Class implementing multi event objects."""
meth:`new` creates Event like objects
meth:'wait` waits for all of them being set
"""
class SingleEvent:
"""Single Event
remark: :meth:`wait` is not implemented on purpose
"""
def __init__(self, multievent):
self.multievent = multievent
self.multievent._clear(self)
def clear(self):
self.multievent._clear(self)
def set(self):
self.multievent._set(self)
def is_set(self):
return self in self.multievent.events
def __init__(self):
def __init__(self, default_timeout=None):
self.events = set()
self._lock = threading.Lock()
self.default_timeout = default_timeout or None # treat 0 as None
self.name = None # default event name
self._actions = [] # actions to be executed on trigger
super().__init__()
def new(self):
"""create a new SingleEvent"""
return self.SingleEvent(self)
def new(self, timeout=None, name=None):
"""create a single event like object"""
return _SingleEvent(self, timeout or self.default_timeout,
name or self.name or '<unnamed>')
def set(self):
raise ValueError('a multievent must not be set directly')
@ -63,21 +73,69 @@ class MultiEvent(threading.Event):
def clear(self):
raise ValueError('a multievent must not be cleared directly')
def _set(self, event):
def is_set(self):
return not self.events
def set_(self, event):
"""internal: remove event from the event list"""
with self._lock:
self.events.discard(event)
if self.events:
return
try:
for action in self._actions:
action()
except Exception:
pass # we silently ignore errors here
self._actions = []
super().set()
def _clear(self, event):
def clear_(self, event):
"""internal: add event to the event list"""
with self._lock:
self.events.add(event)
super().clear()
def deadline(self):
deadline = 0
for event in self.events:
deadline = max(event.deadline, deadline)
return None if deadline == ETERNITY else deadline
def wait(self, timeout=None):
"""wait for all events being set or timed out"""
if not self.events: # do not wait if events are empty
return
super().wait(timeout)
return True
deadline = self.deadline()
if deadline is not None:
deadline -= time.monotonic()
timeout = deadline if timeout is None else min(deadline, timeout)
if timeout <= 0:
return False
return super().wait(timeout)
def waiting_for(self):
return set(event.name for event in self.events)
def get_trigger(self, timeout=None, name=None):
"""create a new single event and return its set method
as a convenience method
"""
return self.new(timeout, name).set
def queue(self, action):
"""add an action to the queue of actions to be executed at end
:param action: a function, to be executed after the last event is triggered,
and before the multievent is set
- if no events are waiting, the actions are executed immediately
- if an action raises an exception, it is silently ignore and further
actions in the queue are skipped
- if this is not desired, the action should handle errors by itself
"""
with self._lock:
self._actions.append(action)
if self.is_set():
self.set_(None)

58
secop/lib/py35compat.py Normal file
View File

@ -0,0 +1,58 @@
# -*- 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:
# Markus Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
"""workaround for python versions older than 3.6
``Object`` must be inherited for classes needing support for
__init_subclass__ and __set_name__
"""
if hasattr(object, '__init_subclass__'):
class Object:
pass
else:
class PEP487Metaclass(type):
# support for __set_name__ and __init_subclass__ for older python versions
# slightly modified from PEP487 doc
def __new__(cls, *args, **kwargs):
if len(args) != 3:
return super().__new__(cls, *args)
name, bases, ns = args
init = ns.get('__init_subclass__')
if callable(init):
ns['__init_subclass__'] = classmethod(init)
newtype = super().__new__(cls, name, bases, ns)
for k, v in newtype.__dict__.items():
func = getattr(v, '__set_name__', None)
if func is not None:
func(newtype, k)
if bases:
super(newtype, newtype).__init_subclass__(**kwargs) # pylint: disable=bad-super-call
return newtype
def __init__(cls, name, bases, ns, **kwargs):
super().__init__(name, bases, ns)
class Object(metaclass=PEP487Metaclass):
@classmethod
def __init_subclass__(cls, *args, **kwargs):
pass

View File

@ -137,8 +137,8 @@ class SequencerMixin:
if self._seq_fault_on_stop:
return self.Status.ERROR, self._seq_stopped
return self.Status.WARN, self._seq_stopped
if hasattr(self, 'read_hw_status'):
return self.read_hw_status()
if hasattr(self, 'readHwStatus'):
return self.readHwStatus()
return self.Status.IDLE, ''
def stop(self):
@ -153,7 +153,7 @@ class SequencerMixin:
self._seq_error = str(e)
finally:
self._seq_thread = None
self.pollParams(0)
self.doPoll()
def _seq_thread_inner(self, seq, store_init):
store = Namespace()

320
secop/lib/statemachine.py Normal file
View File

@ -0,0 +1,320 @@
# -*- 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:
# Markus Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
"""a simple, but powerful state machine
Mechanism
---------
The code for the state machine is NOT to be implemented as a subclass
of StateMachine, but usually as functions or methods of an other object.
The created state object may hold variables needed for the state.
A state function may return either:
- a function for the next state to transition to
- Retry(<delay>) to keep the state and call the
- or `None` for finishing
Initialisation Code
-------------------
For code to be called only after a state transition, use stateobj.init.
def state_x(stateobj):
if stateobj.init:
... code to be execute only after entering state x ...
... further code ...
Cleanup Function
----------------
cleanup=<cleanup function> as argument in StateMachine.__init__ or .start
defines a cleanup function to be called whenever the machine is stopped or
an error is raised in a state function. A cleanup function may return
either None for finishing or a further state function for continuing.
In case of stop or restart, this return value is ignored.
State Specific Cleanup Code
---------------------------
To execute state specific cleanup, the cleanup may examine the current state
(stateobj.state) in order to decide what to be done.
If a need arises, a future extension to this library may support specific
cleanup functions by means of a decorator adding the specific cleanup function
as an attribute to the state function.
Threaded Use
------------
On start, a thread is started, which is waiting for a trigger event when the
machine is not active. For test purposes or special needs, the thread creation
may be disabled. :meth:`cycle` must be called periodically in this case.
"""
import time
import threading
import queue
from logging import getLogger
from secop.lib import mkthread, UniqueObject
Stop = UniqueObject('Stop')
Restart = UniqueObject('Restart')
class Retry:
def __init__(self, delay=None):
self.delay = delay
class StateMachine:
"""a simple, but powerful state machine"""
# class attributes are not allowed to be overriden by kwds of __init__ or :meth:`start`
start_time = None # the time of last start
transition_time = None # the last time when the state changed
state = None # the current state
now = None
init = True
stopped = False
last_error = None # last exception raised or Stop or Restart
_last_time = 0
def __init__(self, state=None, logger=None, threaded=True, **kwds):
"""initialize state machine
:param state: if given, this is the first state
:param logger: an optional logger
:param threaded: whether a thread should be started (default: True)
:param kwds: any attributes for the state object
"""
self.default_delay = 0.25 # default delay when returning None
self.now = time.time() # avoid calling time.time several times per state
self.cleanup = self.default_cleanup # default cleanup: finish on error
self.log = logger or getLogger('dummy')
self._update_attributes(kwds)
self._lock = threading.RLock()
self._threaded = threaded
if threaded:
self._thread_queue = queue.Queue()
self._idle_event = threading.Event()
self._thread = None
self._restart = None
if state:
self.start(state)
@staticmethod
def default_cleanup(state):
"""default cleanup
:param self: the state object
:return: None (for custom cleanup functions this might be a new state)
"""
if state.stopped: # stop or restart
state.log.debug('%sed in state %r', repr(state.stopped).lower(), state.status_string)
else:
state.log.warning('%r raised in state %r', state.last_error, state.status_string)
def _update_attributes(self, kwds):
"""update allowed attributes"""
cls = type(self)
for key, value in kwds.items():
if hasattr(cls, key):
raise AttributeError('can not set %s.%s' % (cls.__name__, key))
setattr(self, key, value)
@property
def is_active(self):
return bool(self.state)
@property
def status_string(self):
if self.state is None:
return ''
doc = self.state.__doc__
return doc.split('\n', 1)[0] if doc else self.state.__name__
@property
def state_time(self):
"""the time spent already in this state"""
return self.now - self.transition_time
@property
def run_time(self):
"""time since last (re-)start"""
return self.now - self.start_time
def _new_state(self, state):
self.state = state
self.init = True
self.now = time.time()
self.transition_time = self.now
self.log.debug('state: %s', self.status_string)
def cycle(self):
"""do one cycle in the thread loop
:return: a delay or None when idle
"""
if self.state is None:
return None
with self._lock:
for _ in range(999):
self.now = time.time()
try:
ret = self.state(self)
self.init = False
if self.stopped:
self.last_error = self.stopped
self.cleanup(self)
self.stopped = False
ret = None
except Exception as e:
self.last_error = e
ret = self.cleanup(self)
self.log.debug('called %r %sexc=%r', self.cleanup,
'ret=%r ' % ret if ret else '', e)
if ret is None:
self.log.debug('state: None')
self.state = None
self._idle_event.set()
return None
if callable(ret):
self._new_state(ret)
continue
if isinstance(ret, Retry):
if ret.delay == 0:
continue
if ret.delay is None:
return self.default_delay
return ret.delay
self.last_error = RuntimeError('return value must be callable, Retry(...) or finish')
break
else:
self.last_error = RuntimeError('too many states chained - probably infinite loop')
self.cleanup(self)
self.state = None
return None
def trigger(self, delay=0):
if self._threaded:
self._thread_queue.put(delay)
def _run(self, delay):
"""thread loop
:param delay: delay before first state is called
"""
while True:
try:
ret = self._thread_queue.get(timeout=delay)
if ret is not None:
delay = ret
continue
except queue.Empty:
pass
delay = self.cycle()
def _start(self, state, first_delay, **kwds):
self._restart = None
self._idle_event.clear()
self.last_error = None
self.stopped = False
self._update_attributes(kwds)
self._new_state(state)
self.start_time = self.now
self._last_time = self.now
if self._threaded:
if self._thread is None or not self._thread.is_alive():
# restart thread if dead (may happen when cleanup failed)
self._thread = mkthread(self._run, first_delay)
else:
self.trigger(first_delay)
def start(self, state, **kwds):
"""start with a new state
and interrupt the current state
the cleanup function will be called with state.stopped=Restart
:param state: the first state
:param kwds: items to put as attributes on the state machine
"""
self.log.debug('start %r', kwds)
if self.state:
self.stopped = Restart
with self._lock: # wait for running cycle finished
if self.stopped: # cleanup is not yet done
self.last_error = self.stopped
self.cleanup(self) # ignore return state on restart
self.stopped = False
delay = self.cycle()
self._start(state, delay, **kwds)
else:
delay = self.cycle() # important: call once (e.g. set status to busy)
self._start(state, delay, **kwds)
def stop(self):
"""stop machine, go to idle state
the cleanup function will be called with state.stopped=Stop
"""
self.log.debug('stop')
self.stopped = Stop
with self._lock:
if self.stopped: # cleanup is not yet done
self.last_error = self.stopped
self.cleanup(self) # ignore return state on restart
self.stopped = False
self.state = None
def wait(self, timeout=None):
"""wait for state machine being idle"""
self._idle_event.wait(timeout)
def delta(self, mindelta=0):
"""helper method for time dependent control
:param mindelta: minimum time since last call
:return: time delta or None when less than min delta time has passed
to be called from within an state
Usage:
def state_x(self, state):
delta = state.delta(5)
if delta is None:
return # less than 5 seconds have passed, we wait for the next cycle
# delta is >= 5, and the zero time for delta is set
# now we can use delta for control calculations
remark: in the first step after start, state.delta(0) returns nearly 0
"""
delta = self.now - self._last_time
if delta < mindelta:
return None
self._last_time = self.now
return delta

169
secop/logging.py Normal file
View File

@ -0,0 +1,169 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# *****************************************************************************
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Markus Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
import os
from os.path import dirname, join
from logging import DEBUG, INFO, addLevelName
import mlzlog
from secop.lib import generalConfig
from secop.datatypes import BoolType
from secop.properties import Property
OFF = 99
COMLOG = 15
addLevelName(COMLOG, 'COMLOG')
assert DEBUG < COMLOG < INFO
LOG_LEVELS = dict(mlzlog.LOGLEVELS, off=OFF, comlog=COMLOG)
LEVEL_NAMES = {v: k for k, v in LOG_LEVELS.items()}
def check_level(level):
try:
if isinstance(level, str):
return LOG_LEVELS[level.lower()]
if level in LEVEL_NAMES:
return level
except KeyError:
pass
raise ValueError('%r is not a valid level' % level)
class RemoteLogHandler(mlzlog.Handler):
"""handler for remote logging"""
def __init__(self):
super().__init__()
self.subscriptions = {} # dict[modname] of tuple(mobobj, dict [conn] of level)
def emit(self, record):
"""unused"""
def handle(self, record):
modname = record.name.split('.')[-1]
try:
modobj, subscriptions = self.subscriptions[modname]
except KeyError:
return
for conn, lev in subscriptions.items():
if record.levelno >= lev:
modobj.DISPATCHER.send_log_msg(
conn, modobj.name, LEVEL_NAMES[record.levelno],
record.getMessage())
def set_conn_level(self, modobj, conn, level):
level = check_level(level)
modobj, subscriptions = self.subscriptions.setdefault(modobj.name, (modobj, {}))
if level == OFF:
subscriptions.pop(conn, None)
else:
subscriptions[conn] = level
def __repr__(self):
return 'RemoteLogHandler()'
class LogfileHandler(mlzlog.LogfileHandler):
def __init__(self, logdir, rootname, max_days=0):
self.logdir = logdir
self.rootname = rootname
self.max_days = max_days
super().__init__(logdir, rootname)
def emit(self, record):
if record.levelno != COMLOG:
super().emit(record)
def doRollover(self):
super().doRollover()
if self.max_days:
# keep only the last max_days files
with os.scandir(dirname(self.baseFilename)) as it:
files = sorted(entry.path for entry in it if entry.name != 'current')
for filepath in files[-self.max_days:]:
os.remove(filepath)
class ComLogfileHandler(LogfileHandler):
"""handler for logging communication"""
def format(self, record):
return '%s %s' % (self.formatter.formatTime(record), record.getMessage())
class HasComlog:
"""mixin for modules with comlog"""
comlog = Property('whether communication is logged ', BoolType(),
default=True, export=False)
_comLog = None
def earlyInit(self):
super().earlyInit()
if self.comlog and generalConfig.initialized and generalConfig.comlog:
self._comLog = mlzlog.Logger('COMLOG.%s' % self.name)
self._comLog.handlers[:] = []
directory = join(logger.logdir, logger.rootname, 'comlog', self.DISPATCHER.name)
self._comLog.addHandler(ComLogfileHandler(
directory, self.name, max_days=generalConfig.getint('comlog_days', 7)))
return
def comLog(self, msg, *args, **kwds):
self.log.log(COMLOG, msg, *args, **kwds)
if self._comLog:
self._comLog.info(msg, *args)
class MainLogger:
def __init__(self):
self.log = None
self.logdir = None
self.rootname = None
self.console_handler = None
def init(self, console_level='info'):
self.rootname = generalConfig.get('logger_root', 'frappy')
# set log level to minimum on the logger, effective levels on the handlers
# needed also for RemoteLogHandler
# modified from mlzlog.initLogging
mlzlog.setLoggerClass(mlzlog.MLZLogger)
assert self.log is None
self.log = mlzlog.log = mlzlog.MLZLogger(self.rootname)
self.log.setLevel(DEBUG)
self.log.addHandler(mlzlog.ColoredConsoleHandler())
self.logdir = generalConfig.get('logdir', '/tmp/log')
if self.logdir:
logfile_days = generalConfig.getint('logfile_days')
logfile_handler = LogfileHandler(self.logdir, self.rootname, max_days=logfile_days)
if generalConfig.logfile_days:
logfile_handler.max_days = int(generalConfig.logfile_days)
logfile_handler.setLevel(LOG_LEVELS[generalConfig.get('logfile_level', 'info')])
self.log.addHandler(logfile_handler)
self.log.addHandler(RemoteLogHandler())
self.log.handlers[0].setLevel(LOG_LEVELS[console_level])
logger = MainLogger()

View File

@ -23,21 +23,27 @@
"""Define base classes for real Modules implemented in the server"""
import sys
import time
import threading
from collections import OrderedDict
from functools import wraps
from secop.datatypes import ArrayOf, BoolType, EnumType, FloatRange, \
IntRange, StatusType, StringType, TextType, TupleOf
from secop.errors import BadValueError, ConfigError, InternalError, \
IntRange, StatusType, StringType, TextType, TupleOf, DiscouragedConversion
from secop.errors import BadValueError, ConfigError, \
ProgrammingError, SECoPError, SilentError, secop_error
from secop.lib import formatException, mkthread
from secop.lib import formatException, mkthread, UniqueObject, generalConfig
from secop.lib.enum import Enum
from secop.params import Accessible, Command, Parameter
from secop.poller import BasicPoller, Poller
from secop.properties import HasProperties, Property
from secop.logging import RemoteLogHandler, HasComlog
Done = object() #: a special return value for a read/write function indicating that the setter is triggered already
generalConfig.set_default('disable_value_range_check', False) # check for problematic value range by default
Done = UniqueObject('Done')
"""a special return value for a read/write function
indicating that the setter is triggered already"""
class HasAccessibles(HasProperties):
@ -57,6 +63,7 @@ class HasAccessibles(HasProperties):
merged_properties = {} # dict of dict of merged properties
new_names = [] # list of names of new accessibles
override_values = {} # bare values overriding a parameter and methods overriding a command
for base in reversed(cls.__mro__):
for key, value in base.__dict__.items():
if isinstance(value, Accessible):
@ -66,28 +73,33 @@ class HasAccessibles(HasProperties):
accessibles[key] = value
override_values.pop(key, None)
elif key in accessibles:
# either a bare value overriding a parameter
# or a method overriding a command
override_values[key] = value
for aname, aobj in accessibles.items():
for aname, aobj in list(accessibles.items()):
if aname in override_values:
aobj = aobj.copy()
value = override_values[aname]
if value is None:
accessibles.pop(aname)
continue
aobj.merge(merged_properties[aname])
aobj.override(override_values[aname])
aobj.override(value)
# replace the bare value by the created accessible
setattr(cls, aname, aobj)
else:
aobj.merge(merged_properties[aname])
accessibles[aname] = aobj
# rebuild order: (1) inherited items, (2) items from paramOrder, (3) new accessibles
# move (2) to the end
for aname in list(cls.__dict__.get('paramOrder', ())):
paramOrder = cls.__dict__.get('paramOrder', ())
for aname in paramOrder:
if aname in accessibles:
accessibles.move_to_end(aname)
# ignore unknown names
# move (3) to the end
for aname in new_names:
accessibles.move_to_end(aname)
if aname not in paramOrder:
accessibles.move_to_end(aname)
# note: for python < 3.6 the order of inherited items is not ensured between
# declarations within the same class
cls.accessibles = accessibles
@ -100,12 +112,14 @@ class HasAccessibles(HasProperties):
# XXX: create getters for the units of params ??
# wrap of reading/writing funcs
if isinstance(pobj, Command):
# nothing to do for now
if not isinstance(pobj, Parameter):
# nothing to do for Commands
continue
rfunc = getattr(cls, 'read_' + pname, None)
# TODO: remove handler stuff here
rfunc_handler = pobj.handler.get_read_func(cls, pname) if pobj.handler else None
wrapped = hasattr(rfunc, '__wrapped__')
wrapped = getattr(rfunc, 'wrapped', False) # meaning: wrapped or auto generated
if rfunc_handler:
if 'read_' + pname in cls.__dict__:
if pname in cls.__dict__:
@ -119,65 +133,86 @@ class HasAccessibles(HasProperties):
# create wrapper except when read function is already wrapped
if not wrapped:
def wrapped_rfunc(self, pname=pname, rfunc=rfunc):
if rfunc:
self.log.debug("calling %r" % rfunc)
try:
value = rfunc(self)
self.log.debug("rfunc(%s) returned %r" % (pname, value))
if value is Done: # the setter is already triggered
return getattr(self, pname)
except Exception as e:
self.log.debug("rfunc(%s) failed %r" % (pname, e))
self.announceUpdate(pname, None, e)
raise
else:
# return cached value
self.log.debug("rfunc(%s): return cached value" % pname)
value = self.accessibles[pname].value
setattr(self, pname, value) # important! trigger the setter
return value
if rfunc:
wrapped_rfunc.__doc__ = rfunc.__doc__
setattr(cls, 'read_' + pname, wrapped_rfunc)
wrapped_rfunc.__wrapped__ = True
if not pobj.readonly:
wfunc = getattr(cls, 'write_' + pname, None)
wrapped = hasattr(wfunc, '__wrapped__')
@wraps(rfunc) # handles __wrapped__ and __doc__
def new_rfunc(self, pname=pname, rfunc=rfunc):
with self.accessLock:
try:
value = rfunc(self)
self.log.debug("read_%s returned %r", pname, value)
except Exception as e:
self.log.debug("read_%s failed with %r", pname, e)
self.announceUpdate(pname, None, e)
raise
if value is Done:
return getattr(self, pname)
setattr(self, pname, value) # important! trigger the setter
return value
new_rfunc.poll = getattr(rfunc, 'poll', True)
else:
def new_rfunc(self, pname=pname):
return getattr(self, pname)
new_rfunc.poll = False
new_rfunc.__doc__ = 'auto generated read method for ' + pname
new_rfunc.wrapped = True # indicate to subclasses that no more wrapping is needed
setattr(cls, 'read_' + pname, new_rfunc)
wfunc = getattr(cls, 'write_' + pname, None)
if not pobj.readonly or wfunc: # allow write_ method even when pobj is not readonly
wrapped = getattr(wfunc, 'wrapped', False) # meaning: wrapped or auto generated
if (wfunc is None or wrapped) and pobj.handler:
# ignore the handler, if a write function is present
# TODO: remove handler stuff here
wfunc = pobj.handler.get_write_func(pname)
wrapped = False
# create wrapper except when write function is already wrapped
if not wrapped:
def wrapped_wfunc(self, value, pname=pname, wfunc=wfunc):
self.log.debug("check validity of %s = %r" % (pname, value))
pobj = self.accessibles[pname]
value = pobj.datatype(value)
if wfunc:
self.log.debug('calling %s %r(%r)' % (wfunc.__name__, wfunc, value))
returned_value = wfunc(self, value)
if returned_value is Done: # the setter is already triggered
return getattr(self, pname)
if returned_value is not None: # goodie: accept missing return value
value = returned_value
setattr(self, pname, value)
return value
if wfunc:
wrapped_wfunc.__doc__ = wfunc.__doc__
setattr(cls, 'write_' + pname, wrapped_wfunc)
wrapped_wfunc.__wrapped__ = True
# check information about Command's
for attrname in cls.__dict__:
if attrname.startswith('do_'):
@wraps(wfunc) # handles __wrapped__ and __doc__
def new_wfunc(self, value, pname=pname, wfunc=wfunc):
with self.accessLock:
pobj = self.accessibles[pname]
self.log.debug('validate %r for %r', value, pname)
# we do not need to handle errors here, we do not
# want to make a parameter invalid, when a write failed
new_value = pobj.datatype(value)
new_value = wfunc(self, new_value)
self.log.debug('write_%s(%r) returned %r', pname, value, new_value)
if new_value is Done:
# setattr(self, pname, getattr(self, pname))
return getattr(self, pname)
setattr(self, pname, new_value) # important! trigger the setter
return new_value
else:
def new_wfunc(self, value, pname=pname):
setattr(self, pname, value)
return value
new_wfunc.__doc__ = 'auto generated write method for ' + pname
new_wfunc.wrapped = True # indicate to subclasses that no more wrapping is needed
setattr(cls, 'write_' + pname, new_wfunc)
# check for programming errors
for attrname in dir(cls):
prefix, _, pname = attrname.partition('_')
if not pname:
continue
if prefix == 'do':
raise ProgrammingError('%r: old style command %r not supported anymore'
% (cls.__name__, attrname))
if prefix in ('read', 'write') and not getattr(getattr(cls, attrname), 'wrapped', False):
raise ProgrammingError('%s.%s defined, but %r is no parameter'
% (cls.__name__, attrname, pname))
res = {}
# collect info about properties
@ -193,6 +228,26 @@ class HasAccessibles(HasProperties):
cls.configurables = res
class PollInfo:
def __init__(self, pollinterval, trigger_event):
self.interval = pollinterval
self.last_main = 0
self.last_slow = 0
self.last_error = None
self.polled_parameters = []
self.fast_flag = False
self.trigger_event = trigger_event
def trigger(self):
"""trigger a recalculation of poll due times"""
self.trigger_event.set()
def update_interval(self, pollinterval):
if not self.fast_flag:
self.interval = pollinterval
self.trigger()
class Module(HasAccessibles):
"""basic module
@ -237,6 +292,9 @@ class Module(HasAccessibles):
extname='implementation')
interface_classes = Property('offical highest interface-class of the module', ArrayOf(StringType()),
extname='interface_classes')
pollinterval = Property('poll interval for parameters handled by doPoll', FloatRange(0.1, 120), default=5)
slowinterval = Property('poll interval for other parameters', FloatRange(0.1, 120), default=15)
enablePoll = True
# properties, parameters and commands are auto-merged upon subclassing
parameters = {}
@ -244,16 +302,24 @@ class Module(HasAccessibles):
# reference to the dispatcher (used for sending async updates)
DISPATCHER = None
pollerClass = Poller #: default poller used
attachedModules = None
pollInfo = None
triggerPoll = None # trigger event for polls. used on io modules and modules without io
def __init__(self, name, logger, cfgdict, srv):
# remember the dispatcher object (for the async callbacks)
self.DISPATCHER = srv.dispatcher
self.omit_unchanged_within = getattr(self.DISPATCHER, 'omit_unchanged_within', 0.1)
self.log = logger
self.name = name
self.valueCallbacks = {}
self.errorCallbacks = {}
self.earlyInitDone = False
self.initModuleDone = False
self.startModuleDone = False
self.remoteLogHandler = None
self.accessLock = threading.RLock()
self.polledModules = [] # modules polled by thread started in self.startModules
errors = []
# handle module properties
@ -298,13 +364,6 @@ class Module(HasAccessibles):
for aname, aobj in self.accessibles.items():
# make a copy of the Parameter/Command object
aobj = aobj.copy()
if isinstance(aobj, Parameter):
# fix default properties poll and needscfg
if aobj.poll is None:
aobj.poll = bool(aobj.handler)
if aobj.needscfg is None:
aobj.needscfg = not aobj.poll
if not self.export: # do not export parameters of a module not exported
aobj.export = False
if aobj.export:
@ -319,26 +378,23 @@ class Module(HasAccessibles):
# 2) check and apply parameter_properties
# specified as '<paramname>.<propertyname> = <propertyvalue>'
# this may also be done on commands: e.g. 'stop.visibility = advanced'
for k, v in list(cfgdict.items()): # keep list() as dict may change during iter
if '.' in k[1:]:
paramname, propname = k.split('.', 1)
aname, propname = k.split('.', 1)
propvalue = cfgdict.pop(k)
paramobj = self.accessibles.get(paramname, None)
# paramobj might also be a command (not sure if this is needed)
if paramobj:
# no longer needed, this conversion is done by DataTypeType.__call__:
# if propname == 'datatype':
# propvalue = get_datatype(propvalue, k)
aobj = self.accessibles.get(aname, None)
if aobj:
try:
paramobj.setProperty(propname, propvalue)
aobj.setProperty(propname, propvalue)
except KeyError:
errors.append("'%s.%s' does not exist" %
(paramname, propname))
(aname, propname))
except BadValueError as e:
errors.append('%s.%s: %s' %
(paramname, propname, str(e)))
(aname, propname, str(e)))
else:
errors.append('%r not found' % paramname)
errors.append('%r not found' % aname)
# 3) check config for problems:
# only accept remaining config items specified in parameters
@ -361,9 +417,8 @@ class Module(HasAccessibles):
continue
if pname in cfgdict:
if not pobj.readonly and pobj.initwrite is not False:
if pobj.initwrite is not False and hasattr(self, 'write_' + pname):
# parameters given in cfgdict have to call write_<pname>
# TODO: not sure about readonly (why not a parameter which can only be written from config?)
try:
pobj.value = pobj.datatype(cfgdict[pname])
self.writeDict[pname] = pobj.value
@ -376,7 +431,7 @@ class Module(HasAccessibles):
'value and was not given in config!' % pname)
# we do not want to call the setter for this parameter for now,
# this should happen on the first read
pobj.readerror = ConfigError('not initialized')
pobj.readerror = ConfigError('parameter %r not initialized' % pname)
# above error will be triggered on activate after startup,
# when not all hardware parameters are read because of startup timeout
pobj.value = pobj.datatype(pobj.datatype.default)
@ -386,10 +441,8 @@ class Module(HasAccessibles):
except BadValueError as e:
# this should not happen, as the default is already checked in Parameter
raise ProgrammingError('bad default for %s:%s: %s' % (name, pname, e)) from None
if pobj.initwrite and not pobj.readonly:
if pobj.initwrite and hasattr(self, 'write_' + pname):
# we will need to call write_<pname>
# if this is not desired, the default must not be given
# TODO: not sure about readonly (why not a parameter which can only be written from config?)
pobj.value = value
self.writeDict[pname] = value
else:
@ -424,11 +477,11 @@ class Module(HasAccessibles):
self.checkProperties()
except ConfigError as e:
errors.append(str(e))
for pname, p in self.parameters.items():
for aname, aobj in self.accessibles.items():
try:
p.checkProperties()
except ConfigError as e:
errors.append('%s: %s' % (pname, e))
aobj.checkProperties()
except (ConfigError, ProgrammingError) as e:
errors.append('%s: %s' % (aname, e))
if errors:
raise ConfigError(errors)
@ -441,42 +494,47 @@ class Module(HasAccessibles):
def announceUpdate(self, pname, value=None, err=None, timestamp=None):
"""announce a changed value or readerror"""
pobj = self.parameters[pname]
timestamp = timestamp or time.time()
changed = pobj.value != value
if value is not None:
pobj.value = value # store the value even in case of error
if err:
if not isinstance(err, SECoPError):
err = InternalError(err)
if str(err) == str(pobj.readerror):
return # do call updates for repeated errors
else:
with self.accessLock:
# TODO: remove readerror 'property' and replace value with exception
pobj = self.parameters[pname]
timestamp = timestamp or time.time()
changed = pobj.value != value
try:
# store the value even in case of error
pobj.value = pobj.datatype(value)
except Exception as e:
err = secop_error(e)
if not changed and timestamp < ((pobj.timestamp or 0)
+ self.DISPATCHER.OMIT_UNCHANGED_WITHIN):
if isinstance(e, DiscouragedConversion):
if DiscouragedConversion.log_message:
self.log.error(str(e))
self.log.error('you may disable this behaviour by running the server with --relaxed')
DiscouragedConversion.log_message = False
if not err: # do not overwrite given error
err = e
if err:
err = secop_error(err)
if str(err) == str(pobj.readerror):
return # no updates for repeated errors
elif not changed and timestamp < (pobj.timestamp or 0) + self.omit_unchanged_within:
# no change within short time -> omit
return
pobj.timestamp = timestamp
pobj.readerror = err
if pobj.export:
self.DISPATCHER.announce_update(self.name, pname, pobj)
if err:
callbacks = self.errorCallbacks
arg = err
else:
callbacks = self.valueCallbacks
arg = value
cblist = callbacks[pname]
for cb in cblist:
try:
cb(arg)
except Exception:
# print(formatExtendedTraceback())
pass
pobj.timestamp = timestamp or time.time()
pobj.readerror = err
if pobj.export:
self.DISPATCHER.announce_update(self.name, pname, pobj)
if err:
callbacks = self.errorCallbacks
arg = err
else:
callbacks = self.valueCallbacks
arg = value
cblist = callbacks[pname]
for cb in cblist:
try:
cb(arg)
except Exception:
# print(formatExtendedTraceback())
pass
def registerCallbacks(self, modobj, autoupdate=()):
"""register callbacks to another module <modobj>
@ -495,15 +553,15 @@ class Module(HasAccessibles):
for pname in self.parameters:
errfunc = getattr(modobj, 'error_update_' + pname, None)
if errfunc:
def errcb(err, p=pname, m=modobj, efunc=errfunc):
def errcb(err, p=pname, efunc=errfunc):
try:
efunc(err)
except Exception as e:
m.announceUpdate(p, err=e)
modobj.announceUpdate(p, err=e)
self.errorCallbacks[pname].append(errcb)
else:
def errcb(err, p=pname, m=modobj):
m.announceUpdate(p, err=err)
def errcb(err, p=pname):
modobj.announceUpdate(p, err=err)
if pname in autoupdate:
self.errorCallbacks[pname].append(errcb)
@ -516,8 +574,8 @@ class Module(HasAccessibles):
efunc(e)
self.valueCallbacks[pname].append(cb)
elif pname in autoupdate:
def cb(value, p=pname, m=modobj):
m.announceUpdate(p, value)
def cb(value, p=pname):
modobj.announceUpdate(p, value)
self.valueCallbacks[pname].append(cb)
def isBusy(self, status=None):
@ -526,16 +584,54 @@ class Module(HasAccessibles):
return False
def earlyInit(self):
# may be overriden in derived classes to init stuff
self.log.debug('empty %s.earlyInit()' % self.__class__.__name__)
"""initialise module with stuff to be done before all modules are created"""
self.earlyInitDone = True
def initModule(self):
self.log.debug('empty %s.initModule()' % self.__class__.__name__)
"""initialise module with stuff to be done after all modules are created"""
self.initModuleDone = True
if self.enablePoll or self.writeDict:
# enablePoll == False: we still need the poll thread for writing values from writeDict
if hasattr(self, 'io'):
self.io.polledModules.append(self)
else:
self.triggerPoll = threading.Event()
self.polledModules = [self]
def pollOneParam(self, pname):
"""poll parameter <pname> with proper error handling"""
def startModule(self, start_events):
"""runs after init of all modules
when a thread is started, a trigger function may signal that it
has finished its initial work
start_events.get_trigger(<timeout>) creates such a trigger and
registers it in the server for waiting
<timeout> defaults to 30 seconds
"""
if self.polledModules:
mkthread(self.__pollThread, self.polledModules, start_events.get_trigger())
self.startModuleDone = True
def doPoll(self):
"""polls important parameters like value and status
all other parameters are polled automatically
"""
def setFastPoll(self, flag, fast_interval=0.25):
"""change poll interval
:param flag: enable/disable fast poll mode
:param fast_interval: fast poll interval
"""
if self.pollInfo:
self.pollInfo.fast_flag = flag
self.pollInfo.interval = fast_interval if flag else self.pollinterval
self.pollInfo.trigger()
def callPollFunc(self, rfunc):
"""call read method with proper error handling"""
try:
getattr(self, 'read_' + pname)()
rfunc()
except SilentError:
pass
except SECoPError as e:
@ -543,6 +639,93 @@ class Module(HasAccessibles):
except Exception:
self.log.error(formatException())
def __pollThread(self, modules, started_callback):
"""poll thread body
:param modules: list of modules to be handled by this thread
:param started_callback: to be called after all polls are done once
before polling, parameters which need hardware initialisation are written
"""
for mobj in modules:
mobj.writeInitParams()
modules = [m for m in modules if m.enablePoll]
if not modules: # no polls needed - exit thread
started_callback()
return
if hasattr(self, 'registerReconnectCallback'):
# self is a communicator supporting reconnections
def trigger_all(trg=self.triggerPoll, polled_modules=modules):
for m in polled_modules:
m.pollInfo.last_main = 0
m.pollInfo.last_slow = 0
trg.set()
self.registerReconnectCallback('trigger_polls', trigger_all)
# collect and call all read functions a first time
for mobj in modules:
pinfo = mobj.pollInfo = PollInfo(mobj.pollinterval, self.triggerPoll)
# trigger a poll interval change when self.pollinterval changes.
if 'pollinterval' in mobj.valueCallbacks:
mobj.valueCallbacks['pollinterval'].append(pinfo.update_interval)
for pname, pobj in mobj.parameters.items():
rfunc = getattr(mobj, 'read_' + pname)
if rfunc.poll:
pinfo.polled_parameters.append((mobj, rfunc, pobj))
mobj.callPollFunc(rfunc)
started_callback()
to_poll = ()
while True:
now = time.time()
wait_time = 999
for mobj in modules:
pinfo = mobj.pollInfo
wait_time = min(pinfo.last_main + pinfo.interval - now, wait_time,
pinfo.last_slow + mobj.slowinterval - now)
if wait_time > 0:
self.triggerPoll.wait(wait_time)
self.triggerPoll.clear()
continue
# call doPoll of all modules where due
for mobj in modules:
pinfo = mobj.pollInfo
if now > pinfo.last_main + pinfo.interval:
pinfo.last_main = (now // pinfo.interval) * pinfo.interval
try:
mobj.doPoll()
pinfo.last_error = None
except Exception as e:
if str(e) != str(pinfo.last_error) and not isinstance(e, SilentError):
mobj.log.error('doPoll: %r', e)
pinfo.last_error = e
now = time.time()
# find ONE due slow poll and call it
loop = True
while loop: # loops max. 2 times, when to_poll is at end
for mobj, rfunc, pobj in to_poll:
if now > pobj.timestamp + mobj.slowinterval * 0.5:
try:
prev_err = pobj.readerror
rfunc()
except Exception as e:
if not isinstance(e, SilentError) and str(pobj.readerror) != str(prev_err):
mobj.log.error('%s: %r', pobj.name, e)
loop = False # one poll done
break
else:
to_poll = []
# collect due slow polls
for mobj in modules:
pinfo = mobj.pollInfo
if now > pinfo.last_slow + mobj.slowinterval:
to_poll.extend(pinfo.polled_parameters)
pinfo.last_slow = (now // mobj.slowinterval) * mobj.slowinterval
if to_poll:
to_poll = iter(to_poll)
else:
loop = False # no slow polls ready
def writeInitParams(self, started_callback=None):
"""write values for parameters with configured values
@ -565,14 +748,15 @@ class Module(HasAccessibles):
if started_callback:
started_callback()
def startModule(self, started_callback):
"""runs after init of all modules
started_callback to be called when the thread spawned by startModule
has finished its initial work
might return a timeout value, if different from default
"""
mkthread(self.writeInitParams, started_callback)
def setRemoteLogging(self, conn, level):
if self.remoteLogHandler is None:
for handler in self.log.handlers:
if isinstance(handler, RemoteLogHandler):
self.remoteLogHandler = handler
break
else:
raise ValueError('remote handler not found')
self.remoteLogHandler.set_conn_level(self, conn, level)
class Readable(Module):
@ -587,63 +771,43 @@ class Readable(Module):
UNKNOWN=401,
) #: status codes
value = Parameter('current value of the module', FloatRange(), poll=True)
value = Parameter('current value of the module', FloatRange())
status = Parameter('current status of the module', TupleOf(EnumType(Status), StringType()),
default=(Status.IDLE, ''), poll=True)
pollinterval = Parameter('sleeptime between polls', FloatRange(0.1, 120),
default=5, readonly=False)
default=(Status.IDLE, ''))
pollinterval = Parameter('default poll interval', FloatRange(0.1, 120),
default=5, readonly=False, export=True)
def startModule(self, started_callback):
"""start basic polling thread"""
if self.pollerClass and issubclass(self.pollerClass, BasicPoller):
# use basic poller for legacy code
mkthread(self.__pollThread, started_callback)
else:
super().startModule(started_callback)
def __pollThread(self, started_callback):
while True:
try:
self.__pollThread_inner(started_callback)
except Exception as e:
self.log.exception(e)
self.status = (self.Status.ERROR, 'polling thread could not start')
started_callback()
print(formatException(0, sys.exc_info(), verbose=True))
time.sleep(10)
def __pollThread_inner(self, started_callback):
"""super simple and super stupid per-module polling thread"""
self.writeInitParams()
i = 0
fastpoll = self.pollParams(i)
started_callback()
while True:
i += 1
try:
time.sleep(self.pollinterval * (0.1 if fastpoll else 1))
except TypeError:
time.sleep(min(self.pollinterval)
if fastpoll else max(self.pollinterval))
fastpoll = self.pollParams(i)
def pollParams(self, nr=0):
# Just poll all parameters regularly where polling is enabled
for pname, pobj in self.parameters.items():
if not pobj.poll:
continue
if nr % abs(int(pobj.poll)) == 0:
# pollParams every 'pobj.pollParams' iteration
self.pollOneParam(pname)
return False
def doPoll(self):
self.read_value()
self.read_status()
class Writable(Readable):
"""basic writable module"""
disable_value_range_check = Property('disable value range check', BoolType(), default=False)
target = Parameter('target value of the module',
default=0, readonly=False, datatype=FloatRange(unit='$'))
def __init__(self, name, logger, cfgdict, srv):
super().__init__(name, logger, cfgdict, srv)
value_dt = self.parameters['value'].datatype
target_dt = self.parameters['target'].datatype
try:
# this handles also the cases where the limits on the value are more
# restrictive than on the target
target_dt.compatible(value_dt)
except Exception:
if type(value_dt) == type(target_dt):
raise ConfigError('the target range extends beyond the value range') from None
raise ProgrammingError('the datatypes of target and value are not compatible') from None
if isinstance(value_dt, FloatRange):
if (not self.disable_value_range_check and not generalConfig.disable_value_range_check
and value_dt.problematic_range(target_dt)):
self.log.error('the value range must be bigger than the target range!')
self.log.error('you may disable this error message by running the server with --relaxed')
self.log.error('or by setting the disable_value_range_check property of the module to True')
raise ConfigError('the value range must be bigger than the target range')
class Drivable(Writable):
"""basic drivable module"""
@ -666,30 +830,12 @@ class Drivable(Writable):
"""
return 300 <= (status or self.status)[0] < 390
# improved polling: may poll faster if module is BUSY
def pollParams(self, nr=0):
# poll status first
self.read_status()
fastpoll = self.isBusy()
for pname, pobj in self.parameters.items():
if not pobj.poll:
continue
if pname == 'status':
# status was already polled above
continue
if ((int(pobj.poll) < 0) and fastpoll) or (
nr % abs(int(pobj.poll))) == 0:
# poll always if pobj.poll is negative and fastpoll (i.e. Module is busy)
# otherwise poll every 'pobj.poll' iteration
self.pollOneParam(pname)
return fastpoll
@Command(None, result=None)
def stop(self):
"""cease driving, go to IDLE state"""
class Communicator(Module):
class Communicator(HasComlog, Module):
"""basic abstract communication module"""
@Command(StringType(), result=StringType())
@ -703,19 +849,20 @@ class Communicator(Module):
class Attached(Property):
"""a special property, defining an attached modle
"""a special property, defining an attached module
assign a module name to this property in the cfg file,
and the server will create an attribute with this module
:param attrname: the name of the to be created attribute. if not given
the attribute name is the property name prepended by an underscore.
"""
# we can not put this to properties.py, as it needs datatypes
def __init__(self, attrname=None):
self.attrname = attrname
# we can not make it mandatory, as the check in Module.__init__ will be before auto-assign in HasIodev
super().__init__('attached module', StringType(), mandatory=False)
def __init__(self, basecls=Module, description='attached module', mandatory=True):
self.basecls = basecls
super().__init__(description, StringType(), mandatory=mandatory)
def __repr__(self):
return 'Attached(%s)' % (repr(self.attrname) if self.attrname else '')
def __get__(self, obj, owner):
if obj is None:
return self
if obj.attachedModules is None:
# return the name of the module (called from Server on startup)
return super().__get__(obj, owner)
# return the module (called after startup)
return obj.attachedModules.get(self.name) # return None if not given

View File

@ -26,12 +26,13 @@
import inspect
from secop.datatypes import BoolType, CommandType, DataType, \
DataTypeType, EnumType, IntRange, NoneOr, OrType, \
DataTypeType, EnumType, NoneOr, OrType, \
StringType, StructOf, TextType, TupleOf, ValueType
from secop.errors import BadValueError, ProgrammingError
from secop.properties import HasProperties, Property
from secop.lib import generalConfig
UNSET = object() # an argument not given, not even None
generalConfig.set_default('tolerate_poll_property', False)
class Accessible(HasProperties):
@ -134,24 +135,9 @@ class Parameter(Accessible):
* True: exported, name automatic.
* a string: exported with custom name''', OrType(BoolType(), StringType()),
export=False, default=True)
poll = Property(
'''[internal] polling indicator
may be:
* None (omitted): will be converted to True/False if handler is/is not None
* False or 0 (never poll this parameter)
* True or 1 (AUTO), converted to SLOW (readonly=False)
DYNAMIC (*status* and *value*) or REGULAR (else)
* 2 (SLOW), polled with lower priority and a multiple of pollinterval
* 3 (REGULAR), polled with pollperiod
* 4 (DYNAMIC), if BUSY, with a fraction of pollinterval,
else polled with pollperiod
''', NoneOr(IntRange()),
export=False, default=None)
needscfg = Property(
'[internal] needs value in config', NoneOr(BoolType()),
export=False, default=None)
export=False, default=False)
optional = Property(
'[internal] is this parameter optional?', BoolType(),
export=False, settable=False, default=False)
@ -171,6 +157,8 @@ class Parameter(Accessible):
def __init__(self, description=None, datatype=None, inherit=True, **kwds):
super().__init__()
if 'poll' in kwds and generalConfig.tolerate_poll_property:
kwds.pop('poll')
if datatype is None:
# collect datatype properties. these are not applied, as we have no datatype
self.ownProperties = {k: kwds.pop(k) for k in list(kwds) if k not in self.propertyDict}
@ -198,7 +186,6 @@ class Parameter(Accessible):
self.ownProperties = {k: getattr(self, k) for k in self.propertyDict}
def __get__(self, instance, owner):
# not used yet
if instance is None:
return self
return instance.parameters[self.name].value
@ -219,6 +206,9 @@ class Parameter(Accessible):
self.export = '_' + self.name
else:
raise ProgrammingError('can not use %r as name of a Parameter' % self.name)
if 'export' in self.ownProperties:
# avoid export=True overrides export=<name>
self.ownProperties['export'] = self.export
def copy(self):
"""return a (deep) copy of ourselfs"""
@ -346,6 +336,9 @@ class Command(Accessible):
def __init__(self, argument=False, *, result=None, inherit=True, **kwds):
super().__init__()
if 'datatype' in kwds:
# self.init will complain about invalid keywords except 'datatype', as this is a property
raise ProgrammingError("Command() got an invalid keyword 'datatype'")
self.init(kwds)
if result or kwds or isinstance(argument, DataType) or not callable(argument):
# normal case
@ -362,8 +355,9 @@ class Command(Accessible):
self.func = argument # this is the wrapped method!
if argument.__doc__:
self.description = inspect.cleandoc(argument.__doc__)
self.name = self.func.__name__
self.name = self.func.__name__ # this is probably not needed
self._inherit = inherit # save for __set_name__
self.ownProperties = self.propertyValues.copy()
def __set_name__(self, owner, name):
self.name = name
@ -372,7 +366,6 @@ class Command(Accessible):
(owner.__name__, name))
self.datatype = CommandType(self.argument, self.result)
self.ownProperties = self.propertyValues.copy()
if self.export is True:
predefined_cls = PREDEFINED_ACCESSIBLES.get(name, None)
if predefined_cls is Command:
@ -381,6 +374,9 @@ class Command(Accessible):
self.export = '_' + name
else:
raise ProgrammingError('can not use %r as name of a Command' % name) from None
if 'export' in self.ownProperties:
# avoid export=True overrides export=<name>
self.ownProperties['export'] = self.export
if not self._inherit:
for key, pobj in self.properties.items():
if key not in self.propertyValues:
@ -397,6 +393,7 @@ class Command(Accessible):
"""called when used as decorator"""
if 'description' not in self.propertyValues and func.__doc__:
self.description = inspect.cleandoc(func.__doc__)
self.ownProperties['description'] = self.description
self.func = func
return self

View File

@ -1,32 +0,0 @@
# -*- coding: utf-8 -*-
# *****************************************************************************
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
#
# *****************************************************************************
"""Pathes. how to find what and where..."""
import sys
from os import path
basepath = path.abspath(path.join(sys.path[0], '..'))
etc_path = path.join(basepath, 'etc')
pid_path = path.join(basepath, 'pid')
log_path = path.join(basepath, 'log')
sys.path[0] = path.join(basepath, 'src')

View File

@ -55,9 +55,9 @@ class MyClass(PersistentMixin, ...):
import os
import json
from secop.lib import getGeneralConfig
from secop.lib import generalConfig
from secop.datatypes import EnumType
from secop.params import Parameter, Property, BoolType, Command
from secop.params import Parameter, Property, Command
from secop.modules import HasAccessibles
@ -69,13 +69,13 @@ class PersistentParam(Parameter):
class PersistentMixin(HasAccessibles):
def __init__(self, *args, **kwds):
super().__init__(*args, **kwds)
persistentdir = os.path.join(getGeneralConfig()['logdir'], 'persistent')
persistentdir = os.path.join(generalConfig.logdir, 'persistent')
os.makedirs(persistentdir, exist_ok=True)
self.persistentFile = os.path.join(persistentdir, '%s.%s.json' % (self.DISPATCHER.equipment_id, self.name))
self.initData = {}
for pname in self.parameters:
pobj = self.parameters[pname]
if not pobj.readonly and getattr(pobj, 'persistent', 0):
if hasattr(self, 'write_' + pname) and getattr(pobj, 'persistent', 0):
self.initData[pname] = pobj.value
if pobj.persistent == 'auto':
def cb(value, m=self):
@ -103,6 +103,7 @@ class PersistentMixin(HasAccessibles):
try:
value = pobj.datatype.import_value(self.persistentData[pname])
pobj.value = value
pobj.readerror = None
if not pobj.readonly:
writeDict[pname] = value
except Exception as e:
@ -144,5 +145,6 @@ class PersistentMixin(HasAccessibles):
@Command()
def factory_reset(self):
"""reset to values from config / default values"""
self.writeDict.update(self.initData)
self.writeInitParams()

View File

@ -1,278 +0,0 @@
# -*- coding: utf-8 -*-
# *****************************************************************************
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Markus Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
"""general, advanced frappy poller
Usage examples:
any Module which want to be polled with a specific Poller must define
the pollerClass class variable:
class MyModule(Readable):
...
pollerClass = poller.Poller
...
modules having a parameter 'iodev' with the same value will share the same poller
"""
import time
from heapq import heapify, heapreplace
from threading import Event
from secop.errors import ProgrammingError
from secop.lib import mkthread
# poll types:
AUTO = 1 #: equivalent to True, converted to REGULAR, SLOW or DYNAMIC
SLOW = 2 #: polling with low priority and increased poll interval (used by default when readonly=False)
REGULAR = 3 #: polling with standard interval (used by default for read only parameters except status and value)
DYNAMIC = 4 #: polling with shorter poll interval when BUSY (used by default for status and value)
class PollerBase:
startup_timeout = 30 # default timeout for startup
name = 'unknown' # to be overridden in implementors __init__ method
@classmethod
def add_to_table(cls, table, module):
"""sort module into poller table
table is a dict, with (<pollerClass>, <name>) as the key, and the
poller as value.
<name> is module.iodev or module.name, if iodev is not present
"""
# for modules with the same iodev, a common poller is used,
# modules without iodev all get their own poller
name = getattr(module, 'iodev', module.name)
poller = table.get((cls, name), None)
if poller is None:
poller = cls(name)
table[(cls, name)] = poller
poller.add_to_poller(module)
def start(self, started_callback):
"""start poller thread
started_callback to be called after all poll items were read at least once
"""
mkthread(self.run, started_callback)
return self.startup_timeout
def run(self, started_callback):
"""poller thread function
started_callback to be called after all poll items were read at least once
"""
raise NotImplementedError
def stop(self):
"""stop polling"""
raise NotImplementedError
def __bool__(self):
"""is there any poll item?"""
raise NotImplementedError
def __repr__(self):
return '%s(%r)' % (self.__class__.__name__, self.name)
class Poller(PollerBase):
"""a standard poller
parameters may have the following polltypes:
- REGULAR: by default used for readonly parameters with poll=True
- SLOW: by default used for readonly=False parameters with poll=True.
slow polls happen with lower priority, but at least one parameter
is polled with regular priority within self.module.pollinterval.
Scheduled to poll every slowfactor * module.pollinterval
- DYNAMIC: by default used for 'value' and 'status'
When busy, scheduled to poll every fastfactor * module.pollinterval
"""
DEFAULT_FACTORS = {SLOW: 4, DYNAMIC: 0.25, REGULAR: 1}
def __init__(self, name):
"""create a poller"""
self.queues = {polltype: [] for polltype in self.DEFAULT_FACTORS}
self._event = Event()
self._stopped = False
self.maxwait = 3600
self.name = name
self.modules = [] # used for writeInitParams only
def add_to_poller(self, module):
self.modules.append(module)
factors = self.DEFAULT_FACTORS.copy()
try:
factors[DYNAMIC] = module.fast_pollfactor
except AttributeError:
pass
try:
factors[SLOW] = module.slow_pollfactor
except AttributeError:
pass
self.maxwait = min(self.maxwait, getattr(module, 'max_polltestperiod', 10))
try:
self.startup_timeout = max(self.startup_timeout, module.startup_timeout)
except AttributeError:
pass
handlers = set()
# at the beginning, queues are simple lists
# later, they will be converted to heaps
for pname, pobj in module.parameters.items():
polltype = pobj.poll
if not polltype:
continue
if not hasattr(module, 'pollinterval'):
raise ProgrammingError("module %s must have a pollinterval"
% module.name)
if pname == 'is_connected':
if hasattr(module, 'registerReconnectCallback'):
module.registerReconnectCallback(self.name, self.trigger_all)
else:
module.log.warning("%r has 'is_connected' but no 'registerReconnectCallback'" % module)
if polltype == AUTO: # covers also pobj.poll == True
if pname in ('value', 'status'):
polltype = DYNAMIC
elif pobj.readonly:
polltype = REGULAR
else:
polltype = SLOW
if polltype not in factors:
raise ProgrammingError("unknown poll type %r for parameter '%s'"
% (polltype, pname))
if pobj.handler:
if pobj.handler in handlers:
continue # only one poller per handler
handlers.add(pobj.handler)
# placeholders 0 are used for due, lastdue and idx
self.queues[polltype].append(
(0, 0, (0, module, pobj, pname, factors[polltype])))
def poll_next(self, polltype):
"""try to poll next item
advance in queue until
- an item is found which is really due to poll. return 0 in this case
- or until the next item is not yet due. return next due time in this case
"""
queue = self.queues[polltype]
if not queue:
return float('inf') # queue is empty
now = time.time()
done = False
while not done:
due, lastdue, pollitem = queue[0]
if now < due:
return due
_, module, pobj, pname, factor = pollitem
if polltype == DYNAMIC and not module.isBusy():
interval = module.pollinterval # effective interval
mininterval = interval * factor # interval for calculating next due
else:
interval = module.pollinterval * factor
mininterval = interval
if due == 0:
due = now # do not look at timestamp after trigger_all
else:
due = max(lastdue + interval, pobj.timestamp + interval * 0.5)
if now >= due:
module.pollOneParam(pname)
done = True
lastdue = due
due = max(lastdue + mininterval, now + min(self.maxwait, mininterval * 0.5))
# replace due, lastdue with new values and sort in
heapreplace(queue, (due, lastdue, pollitem))
return 0
def trigger_all(self):
for _, queue in sorted(self.queues.items()):
for idx, (_, lastdue, pollitem) in enumerate(queue):
queue[idx] = (0, lastdue, pollitem)
self._event.set()
return True
def run(self, started_callback):
"""start poll loop
To be called as a thread. After all parameters are polled once first,
started_callback is called. To be called in Module.start_module.
poll strategy:
Slow polls are performed with lower priority than regular and dynamic polls.
If more polls are scheduled than time permits, at least every second poll is a
dynamic poll. After every n regular polls, one slow poll is done, if due
(where n is the number of regular parameters).
"""
if not self:
# nothing to do (else time.sleep(float('inf')) might be called below
started_callback()
return
# if writeInitParams is not yet done, we do it here
for module in self.modules:
module.writeInitParams()
# do all polls once and, at the same time, insert due info
for _, queue in sorted(self.queues.items()): # do SLOW polls first
for idx, (_, _, (_, module, pobj, pname, factor)) in enumerate(queue):
lastdue = time.time()
module.pollOneParam(pname)
due = lastdue + min(self.maxwait, module.pollinterval * factor)
# in python 3 comparing tuples need some care, as not all objects
# are comparable. Inserting a unique idx solves the problem.
queue[idx] = (due, lastdue, (idx, module, pobj, pname, factor))
heapify(queue)
started_callback() # signal end of startup
nregular = len(self.queues[REGULAR])
while not self._stopped:
due = float('inf')
for _ in range(nregular):
due = min(self.poll_next(DYNAMIC), self.poll_next(REGULAR))
if due:
break # no dynamic or regular polls due
due = min(due, self.poll_next(DYNAMIC), self.poll_next(SLOW))
delay = due - time.time()
if delay > 0:
self._event.wait(delay)
self._event.clear()
def stop(self):
self._event.set()
self._stopped = True
def __bool__(self):
"""is there any poll item?"""
return any(self.queues.values())
class BasicPoller(PollerBase):
"""basic poller
this is just a dummy, the poller thread is started in Readable.startModule
"""
# pylint: disable=abstract-method
@classmethod
def add_to_table(cls, table, module):
pass

View File

@ -24,24 +24,15 @@
import inspect
import sys
from secop.errors import BadValueError, ConfigError, ProgrammingError
from secop.lib import UniqueObject
from secop.lib.py35compat import Object
UNSET = UniqueObject('undefined value') #: an unset value, not even None
class HasDescriptorMeta(type):
def __new__(cls, name, bases, attrs):
newtype = type.__new__(cls, name, bases, attrs)
if sys.version_info < (3, 6):
# support older python versions
for key, attr in attrs.items():
if hasattr(attr, '__set_name__'):
attr.__set_name__(newtype, key)
newtype.__init_subclass__()
return newtype
class HasDescriptors(metaclass=HasDescriptorMeta):
class HasDescriptors(Object):
@classmethod
def __init_subclass__(cls):
# when migrating old style declarations, sometimes the trailing comma is not removed
@ -51,9 +42,6 @@ class HasDescriptors(metaclass=HasDescriptorMeta):
raise ProgrammingError('misplaced trailing comma after %s.%s' % (cls.__name__, '/'.join(bad)))
UNSET = object() # an unset value, not even None
# storage for 'properties of a property'
class Property:
"""base class holding info about a property
@ -142,31 +130,27 @@ class HasProperties(HasDescriptors):
@classmethod
def __init_subclass__(cls):
super().__init_subclass__()
# raise an error when an attribute is a tuple with one single descriptor as element
# when migrating old style declarations, sometimes the trailing comma is not removed
bad = [k for k, v in cls.__dict__.items()
if isinstance(v, tuple) and len(v) == 1 and hasattr(v[0], '__set_name__')]
if bad:
raise ProgrammingError('misplaced trailing comma after %s.%s' % (cls.__name__, '/'.join(bad)))
properties = {}
# using cls.__bases__ and base.propertyDict for this would fail on some multiple inheritance cases
for base in reversed(cls.__mro__):
properties.update({k: v for k, v in base.__dict__.items() if isinstance(v, Property)})
cls.propertyDict = properties
# treat overriding properties with bare values
for pn, po in properties.items():
for pn, po in list(properties.items()):
value = getattr(cls, pn, po)
if not isinstance(value, Property): # attribute is a bare value
if isinstance(value, HasProperties): # value is a Parameter, allow override
properties.pop(pn)
elif not isinstance(value, Property): # attribute may be a bare value
po = po.copy()
try:
# try to apply bare value to Property
po.value = po.datatype(value)
except BadValueError:
if pn in properties:
if callable(value):
raise ProgrammingError('method %s.%s collides with property of %s' %
(cls.__name__, pn, base.__name__)) from None
raise ProgrammingError('can not set property %s.%s to %r' %
(cls.__name__, pn, value)) from None
if callable(value):
raise ProgrammingError('method %s.%s collides with property of %s' %
(cls.__name__, pn, base.__name__)) from None
raise ProgrammingError('can not set property %s.%s to %r' %
(cls.__name__, pn, value)) from None
cls.propertyDict[pn] = po
def checkProperties(self):

View File

@ -19,4 +19,4 @@
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
#
# *****************************************************************************
"""SECoP protocl specific stuff"""
"""SECoP protocol specific stuff"""

View File

@ -47,7 +47,8 @@ from secop.errors import NoSuchCommandError, NoSuchModuleError, \
from secop.params import Parameter
from secop.protocol.messages import COMMANDREPLY, DESCRIPTIONREPLY, \
DISABLEEVENTSREPLY, ENABLEEVENTSREPLY, ERRORPREFIX, EVENTREPLY, \
HEARTBEATREPLY, IDENTREPLY, IDENTREQUEST, READREPLY, WRITEREPLY
HEARTBEATREPLY, IDENTREPLY, IDENTREQUEST, READREPLY, WRITEREPLY, \
LOGGING_REPLY, LOG_EVENT
def make_update(modulename, pobj):
@ -60,12 +61,11 @@ def make_update(modulename, pobj):
class Dispatcher:
OMIT_UNCHANGED_WITHIN = 1 # do not send unchanged updates within 1 sec
def __init__(self, name, logger, options, srv):
# to avoid errors, we want to eat all options here
self.equipment_id = options.pop('id', name)
# time interval for omitting updates of unchanged values
self.omit_unchanged_within = options.pop('omit_unchanged_within', 0.1)
self.nodeprops = {}
for k in list(options):
self.nodeprops[k] = options.pop(k)
@ -83,6 +83,7 @@ class Dispatcher:
# eventname is <modulename> or <modulename>:<parametername>
self._subscriptions = {}
self._lock = threading.RLock()
self.name = name
self.restart = srv.restart
self.shutdown = srv.shutdown
@ -124,13 +125,21 @@ class Dispatcher:
"""registers new connection"""
self._connections.append(conn)
def reset_connection(self, conn):
"""remove all subscriptions for a connection
to be called on the identification message
"""
for _evt, conns in list(self._subscriptions.items()):
conns.discard(conn)
self.set_all_log_levels(conn, 'off')
self._active_connections.discard(conn)
def remove_connection(self, conn):
"""removes now longer functional connection"""
if conn in self._connections:
self._connections.remove(conn)
for _evt, conns in list(self._subscriptions.items()):
conns.discard(conn)
self._active_connections.discard(conn)
self.reset_connection(conn)
def register_module(self, moduleobj, modulename, export=True):
self.log.debug('registering module %r as %s (export=%r)' %
@ -214,9 +223,14 @@ class Dispatcher:
if cobj is None:
raise NoSuchCommandError('Module %r has no command %r' % (modulename, cname or exportedname))
if cobj.argument:
argument = cobj.argument.import_value(argument)
# now call func
# note: exceptions are handled in handle_request, not here!
return cobj.do(moduleobj, argument), dict(t=currenttime())
result = cobj.do(moduleobj, argument)
if cobj.result:
result = cobj.result.export_value(result)
return result, dict(t=currenttime())
def _setParameterValue(self, modulename, exportedname, value):
moduleobj = self.get_module(modulename)
@ -236,13 +250,9 @@ class Dispatcher:
# validate!
value = pobj.datatype(value)
writefunc = getattr(moduleobj, 'write_%s' % pname, None)
# note: exceptions are handled in handle_request, not here!
if writefunc:
# return value is ignored here, as it is automatically set on the pobj and broadcast
writefunc(value)
else:
setattr(moduleobj, pname, value)
getattr(moduleobj, 'write_' + pname)(value)
# return value is ignored here, as already handled
return pobj.export_value(), dict(t=pobj.timestamp) if pobj.timestamp else {}
def _getParameterValue(self, modulename, exportedname):
@ -259,11 +269,9 @@ class Dispatcher:
# raise ReadOnlyError('This parameter is constant and can not be accessed remotely.')
return pobj.datatype.export_value(pobj.constant)
readfunc = getattr(moduleobj, 'read_%s' % pname, None)
if readfunc:
# should also update the pobj (via the setter from the metaclass)
# note: exceptions are handled in handle_request, not here!
readfunc()
# note: exceptions are handled in handle_request, not here!
getattr(moduleobj, 'read_' + pname)()
# return value is ignored here, as already handled
return pobj.export_value(), dict(t=pobj.timestamp) if pobj.timestamp else {}
#
@ -298,6 +306,10 @@ class Dispatcher:
self.log.error('should have been handled in the interface!')
def handle__ident(self, conn, specifier, data):
# Remark: the following line is needed due to issue 66.
self.reset_connection(conn)
# The other stuff in issue 66 ('error_closed' message), has to be implemented
# if and when frappy will support serial server connections
return (IDENTREPLY, None, None)
def handle_describe(self, conn, specifier, data):
@ -373,3 +385,19 @@ class Dispatcher:
self._active_connections.discard(conn)
# XXX: also check all entries in self._subscriptions?
return (DISABLEEVENTSREPLY, None, None)
def send_log_msg(self, conn, modname, level, msg):
"""send log message """
conn.send_reply((LOG_EVENT, '%s:%s' % (modname, level), msg))
def set_all_log_levels(self, conn, level):
for modobj in self._modules.values():
modobj.setRemoteLogging(conn, level)
def handle_logging(self, conn, specifier, level):
if specifier and specifier != '.':
modobj = self._modules[specifier]
modobj.setRemoteLogging(conn, level)
else:
self.set_all_log_levels(conn, level)
return LOGGING_REPLY, specifier, level

View File

@ -202,3 +202,11 @@ class TCPServer(socketserver.ThreadingTCPServer):
if ntry:
self.log.warning('tried again %d times after "Address already in use"' % ntry)
self.log.info("TCPServer initiated")
# py35 compatibility
if not hasattr(socketserver.ThreadingTCPServer, '__exit__'):
def __enter__(self):
return self
def __exit__(self, *args):
self.server_close()

View File

@ -65,6 +65,13 @@ ERRORPREFIX = 'error_' # + specifier + json_extended_info(error_report)
HELPREQUEST = 'help' # literal
HELPREPLY = 'helping' # +line number +json_text
LOGGING_REQUEST = 'logging'
LOGGING_REPLY = 'logging'
# + [module] + json string (loglevel)
LOG_EVENT = 'log'
# + [module:level] + json_string (message)
# helper mapping to find the REPLY for a REQUEST
# do not put IDENTREQUEST/IDENTREPLY here, as this needs anyway extra treatment
REQUEST2REPLY = {
@ -77,6 +84,7 @@ REQUEST2REPLY = {
READREQUEST: READREPLY,
HEARTBEATREQUEST: HEARTBEATREPLY,
HELPREQUEST: HELPREPLY,
LOGGING_REQUEST: LOGGING_REPLY,
}
@ -89,6 +97,8 @@ HelpMessage = """Try one of the following:
'%s <nonce>' to request a heartbeat response
'%s' to activate async updates
'%s' to deactivate updates
'%s [<module>] <loglevel>' to activate logging events
""" % (IDENTREQUEST, DESCRIPTIONREQUEST, READREQUEST,
WRITEREQUEST, COMMANDREQUEST, HEARTBEATREQUEST,
ENABLEEVENTSREQUEST, DISABLEEVENTSREQUEST)
ENABLEEVENTSREQUEST, DISABLEEVENTSREQUEST,
LOGGING_REQUEST)

View File

@ -23,23 +23,22 @@
from secop.client import SecopClient, decode_msg, encode_msg_frame
from secop.datatypes import StringType
from secop.errors import BadValueError, \
CommunicationFailedError, ConfigError, make_secop_error
from secop.errors import BadValueError, CommunicationFailedError, ConfigError
from secop.lib import get_class
from secop.modules import Drivable, Module, Readable, Writable
from secop.params import Command, Parameter
from secop.properties import Property
from secop.io import HasIodev
from secop.io import HasIO
class ProxyModule(HasIodev, Module):
class ProxyModule(HasIO, Module):
module = Property('remote module name', datatype=StringType(), default='')
pollerClass = None
_consistency_check_done = False
_secnode = None
enablePoll = False
def iodevClass(self, name, logger, opts, srv):
def ioClass(self, name, logger, opts, srv):
opts['description'] = 'secnode %s on %s' % (opts.get('module', name), opts['uri'])
return SecNode(name, logger, opts, srv)
@ -47,14 +46,12 @@ class ProxyModule(HasIodev, Module):
if parameter not in self.parameters:
return # ignore unknown parameters
# should be done here: deal with clock differences
if readerror:
readerror = make_secop_error(*readerror)
self.announceUpdate(parameter, value, readerror, timestamp)
def initModule(self):
if not self.module:
self.module = self.name
self._secnode = self._iodev.secnode
self._secnode = self.io.secnode
self._secnode.register_callback(self.module, self.updateEvent,
self.descriptiveDataChange, self.nodeStateChange)
super().initModule()
@ -123,7 +120,8 @@ class ProxyModule(HasIodev, Module):
self.announceUpdate('status', newstatus)
def checkProperties(self):
pass # skip
pass # skip
class ProxyReadable(ProxyModule, Readable):
pass
@ -144,10 +142,12 @@ class SecNode(Module):
uri = Property('uri of a SEC node', datatype=StringType())
def earlyInit(self):
super().earlyInit()
self.secnode = SecopClient(self.uri, self.log)
def startModule(self, started_callback):
self.secnode.spawn_connect(started_callback)
def startModule(self, start_events):
super().startModule(start_events)
self.secnode.spawn_connect(start_events.get_trigger())
@Command(StringType(), result=StringType())
def request(self, msg):
@ -182,7 +182,7 @@ def proxy_class(remote_class, name=None):
for aname, aobj in rcls.accessibles.items():
if isinstance(aobj, Parameter):
pobj = aobj.override(poll=False, handler=None, needscfg=False)
pobj = aobj.merge(dict(handler=None, needscfg=False))
attrs[aname] = pobj
def rfunc(self, pname=aname):
@ -198,7 +198,7 @@ def proxy_class(remote_class, name=None):
def wfunc(self, value, pname=aname):
value, _, readerror = self._secnode.setParameter(self.name, pname, value)
if readerror:
raise make_secop_error(*readerror)
raise readerror
return value
attrs['write_' + aname] = wfunc
@ -225,5 +225,5 @@ def Proxy(name, logger, cfgdict, srv):
remote_class = cfgdict.pop('remote_class')
if 'description' not in cfgdict:
cfgdict['description'] = 'remote module %s on %s' % (
cfgdict.get('module', name), cfgdict.get('iodev', '?'))
cfgdict.get('module', name), cfgdict.get('io', '?'))
return proxy_class(remote_class)(name, logger, cfgdict, srv)

221
secop/rwhandler.py Normal file
View File

@ -0,0 +1,221 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# *****************************************************************************
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Markus Zolliker <markus.zolliker@psi.ch>
# *****************************************************************************
"""decorator class for common read_/write_ methods
Usage:
Example 1: combined read/write for multiple parameters
PID_PARAMS = ['p', 'i', 'd']
@CommonReadHandler(PID_PARAMS)
def read_pid(self):
self.p, self.i, self.d = self.get_pid_from_hw()
# no return value
@CommonWriteHandler(PID_PARAMS)
def write_pid(self, values):
# values is a dict[pname] of value, we convert it to a tuple here
self.put_pid_to_hw(values.as_tuple('p', 'i', 'd'')) # or .as_tuple(*PID_PARAMS)
self.read_pid()
# no return value
Example 2: addressable HW parameters
HW_ADDR = {'p': 25, 'i': 26, 'd': 27}
@ReadHandler(HW_ADDR)
def read_addressed(self, pname):
return self.get_hw_register(HW_ADDR[pname])
@WriteHandler(HW_ADDR)
def write_addressed(self, pname, value):
self.put_hw_register(HW_ADDR[pname], value)
return self.get_hw_register(HW_ADDR[pname])
"""
import functools
from secop.modules import Done
from secop.errors import ProgrammingError
def wraps(func):
"""decorator to copy function attributes of wrapped function"""
# we modify the default here:
# copy __doc__ , __module___ and attributes from __dict__
# but not __name__ and __qualname__
return functools.wraps(func, assigned=('__doc__', '__module__'))
class Handler:
func = None
method_names = set() # this is shared among all instances of handlers!
wrapped = True # allow to use read_* or write_* as name of the decorated method
prefix = None # 'read_' or 'write_'
poll = None
def __init__(self, keys):
"""initialize the decorator
:param keys: parameter names (an iterable)
"""
self.keys = set(keys)
def __call__(self, func):
"""decorator call"""
self.func = func
if func.__qualname__ in self.method_names:
raise ProgrammingError('duplicate method %r' % func.__qualname__)
func.wrapped = False
# __qualname__ used here (avoid conflicts between different modules)
self.method_names.add(func.__qualname__)
return self
def __get__(self, obj, owner=None):
"""allow access to the common method"""
if obj is None:
return self
return self.func.__get__(obj, owner)
def __set_name__(self, owner, name):
"""create the wrapped read_* or write_* methods"""
self.method_names.discard(self.func.__qualname__)
for key in self.keys:
wrapped = self.wrap(key)
method_name = self.prefix + key
wrapped.wrapped = True
if self.poll is not None:
# wrapped.poll is False when the nopoll decorator is applied either to self.func or to self
wrapped.poll = getattr(wrapped, 'poll', self.poll)
func = getattr(owner, method_name, None)
if func and not func.wrapped:
raise ProgrammingError('superfluous method %s.%s (overwritten by %s)'
% (owner.__name__, method_name, self.__class__.__name__))
setattr(owner, method_name, wrapped)
def wrap(self, key):
"""create wrapped method from self.func
with name self.prefix + key"""
raise NotImplementedError
class ReadHandler(Handler):
"""decorator for read handler methods"""
prefix = 'read_'
poll = True
def wrap(self, key):
def method(module, pname=key, func=self.func):
with module.accessLock:
value = func(module, pname)
if value is Done:
return getattr(module, pname)
setattr(module, pname, value)
return value
return wraps(self.func)(method)
class CommonReadHandler(ReadHandler):
"""decorator for a handler reading several parameters in one go"""
def __init__(self, keys):
"""initialize the decorator
:param keys: parameter names (an iterable)
"""
super().__init__(keys)
self.first_key = next(iter(keys))
def wrap(self, key):
def method(module, pname=key, func=self.func):
with module.accessLock:
ret = func(module)
if ret not in (None, Done):
raise ProgrammingError('a method wrapped with CommonReadHandler must not return any value')
return getattr(module, pname)
method = wraps(self.func)(method)
method.poll = self.poll and getattr(method, 'poll', True) if key == self.first_key else False
return method
class WriteHandler(Handler):
"""decorator for write handler methods"""
prefix = 'write_'
def wrap(self, key):
@wraps(self.func)
def method(module, value, pname=key, func=self.func):
with module.accessLock:
value = func(module, pname, value)
if value is not Done:
setattr(module, pname, value)
return value
return method
class WriteParameters(dict):
def __init__(self, modobj):
super().__init__()
self.obj = modobj
def __missing__(self, key):
try:
return self.obj.writeDict.pop(key)
except KeyError:
return getattr(self.obj, key)
def as_tuple(self, *keys):
"""return values of given keys as a tuple"""
return tuple(self[k] for k in keys)
class CommonWriteHandler(WriteHandler):
"""decorator for common write handler
calls the wrapped write method function with values as an argument.
- values[pname] returns the to be written value
- values['key'] returns a value taken from writeDict
or, if not available return obj.key
- values.as_tuple() returns a tuple with the items in the same order as keys
"""
def wrap(self, key):
@wraps(self.func)
def method(module, value, pname=key, func=self.func):
with module.accessLock:
values = WriteParameters(module)
values[pname] = value
ret = func(module, values)
if ret not in (None, Done):
raise ProgrammingError('a method wrapped with CommonWriteHandler must not return any value')
# remove pname from writeDict. this was not removed in WriteParameters, as it was not missing
module.writeDict.pop(pname, None)
return method
def nopoll(func):
"""decorator to indicate that a read method is not to be polled"""
func.poll = False
return func

View File

@ -27,15 +27,14 @@ import ast
import configparser
import os
import sys
import threading
import time
import traceback
from collections import OrderedDict
from secop.errors import ConfigError, SECoPError
from secop.lib import formatException, get_class, getGeneralConfig
from secop.modules import Attached
from secop.lib import formatException, get_class, generalConfig
from secop.lib.multievent import MultiEvent
from secop.params import PREDEFINED_ACCESSIBLES
from secop.modules import Attached
try:
from daemon import DaemonContext
@ -89,7 +88,6 @@ class Server:
...
"""
self._testonly = testonly
cfg = getGeneralConfig()
self.log = parent_logger.getChild(name, True)
if not cfgfiles:
@ -114,22 +112,21 @@ class Server:
if ambiguous_sections:
self.log.warning('ambiguous sections in %s: %r' % (cfgfiles, tuple(ambiguous_sections)))
self._cfgfiles = cfgfiles
self._pidfile = os.path.join(cfg['piddir'], name + '.pid')
self._pidfile = os.path.join(generalConfig.piddir, name + '.pid')
def loadCfgFile(self, cfgfile):
if not cfgfile.endswith('.cfg'):
cfgfile += '.cfg'
cfg = getGeneralConfig()
if os.sep in cfgfile: # specified as full path
filename = cfgfile if os.path.exists(cfgfile) else None
else:
for filename in [os.path.join(d, cfgfile) for d in cfg['confdir'].split(os.pathsep)]:
for filename in [os.path.join(d, cfgfile) for d in generalConfig.confdir.split(os.pathsep)]:
if os.path.exists(filename):
break
else:
filename = None
if filename is None:
raise ConfigError("Couldn't find cfg file %r in %s" % (cfgfile, cfg['confdir']))
raise ConfigError("Couldn't find cfg file %r in %s" % (cfgfile, generalConfig.confdir))
self.log.debug('Parse config file %s ...' % filename)
result = OrderedDict()
parser = configparser.ConfigParser()
@ -268,36 +265,58 @@ class Server:
failure_traceback = traceback.format_exc()
errors.append('error creating %s' % modname)
poll_table = dict()
missing_super = set()
# all objs created, now start them up and interconnect
for modname, modobj in self.modules.items():
self.log.info('registering module %r' % modname)
self.dispatcher.register_module(modobj, modname, modobj.export)
if modobj.pollerClass is not None:
# a module might be explicitly excluded from polling by setting pollerClass to None
modobj.pollerClass.add_to_table(poll_table, modobj)
# also call earlyInit on the modules
modobj.earlyInit()
if not modobj.earlyInitDone:
missing_super.add('%s was not called, probably missing super call'
% modobj.earlyInit.__qualname__)
# handle attached modules
for modname, modobj in self.modules.items():
attached_modules = {}
for propname, propobj in modobj.propertyDict.items():
if isinstance(propobj, Attached):
try:
setattr(modobj, propobj.attrname or '_' + propname,
self.dispatcher.get_module(getattr(modobj, propname)))
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('attached module %s=%r must inherit from %r'
% (propname, attname, propobj.basecls.__qualname__))
except SECoPError as e:
errors.append('module %s, attached %s: %s' % (modname, propname, str(e)))
modobj.attachedModules = attached_modules
# call init on each module after registering all
for modname, modobj in self.modules.items():
try:
modobj.initModule()
if not modobj.initModuleDone:
missing_super.add('%s was not called, probably missing super call'
% modobj.initModule.__qualname__)
except Exception as e:
if failure_traceback is None:
failure_traceback = traceback.format_exc()
errors.append('error initializing %s: %r' % (modname, e))
if not self._testonly:
start_events = MultiEvent(default_timeout=30)
for modname, modobj in self.modules.items():
# startModule must return either a timeout value or None (default 30 sec)
start_events.name = 'module %s' % modname
modobj.startModule(start_events)
if not modobj.startModuleDone:
missing_super.add('%s was not called, probably missing super call'
% modobj.startModule.__qualname__)
errors.extend(missing_super)
if errors:
for errtxt in errors:
for line in errtxt.split('\n'):
@ -311,22 +330,13 @@ class Server:
if self._testonly:
return
start_events = []
for modname, modobj in self.modules.items():
event = threading.Event()
# startModule must return either a timeout value or None (default 30 sec)
timeout = modobj.startModule(started_callback=event.set) or 30
start_events.append((time.time() + timeout, 'module %s' % modname, event))
for poller in poll_table.values():
event = threading.Event()
# poller.start must return either a timeout value or None (default 30 sec)
timeout = poller.start(started_callback=event.set) or 30
start_events.append((time.time() + timeout, repr(poller), event))
self.log.info('waiting for modules and pollers being started')
for deadline, name, event in sorted(start_events):
if not event.wait(timeout=max(0, deadline - time.time())):
self.log.info('WARNING: timeout when starting %s' % name)
self.log.info('all modules and pollers started')
self.log.info('waiting for modules being started')
start_events.name = None
if not start_events.wait():
# some timeout happened
for name in start_events.waiting_for():
self.log.warning('timeout when starting %s' % name)
self.log.info('all modules started')
history_path = os.environ.get('FRAPPY_HISTORY')
if history_path:
from secop_psi.historywriter import FrappyHistoryWriter # pylint: disable=import-outside-toplevel

View File

@ -27,13 +27,10 @@ from time import sleep
from secop.datatypes import FloatRange
from secop.lib import mkthread
from secop.modules import BasicPoller, Drivable, \
Module, Parameter, Readable, Writable, Command
from secop.modules import Drivable, Module, Parameter, Readable, Writable, Command
class SimBase:
pollerClass = BasicPoller
def __new__(cls, devname, logger, cfgdict, dispatcher):
extra_params = cfgdict.pop('extra_params', '') or cfgdict.pop('.extra_params', '')
attrs = {}
@ -60,6 +57,7 @@ class SimBase:
return object.__new__(type('SimBase_%s' % devname, (cls,), attrs))
def initModule(self):
super().initModule()
self._sim_thread = mkthread(self._sim)
def _sim(self):
@ -119,7 +117,7 @@ class SimDrivable(SimReadable, Drivable):
self._value = self.target
speed *= self.interval
try:
self.pollParams(0)
self.doPoll()
except Exception:
pass
@ -132,7 +130,7 @@ class SimDrivable(SimReadable, Drivable):
self._value = self.target
sleep(self.interval)
try:
self.pollParams(0)
self.doPoll()
except Exception:
pass
self.status = self.Status.IDLE, ''

View File

@ -111,6 +111,7 @@ class Cryostat(CryoBase):
group='stability')
def initModule(self):
super().initModule()
self._stopflag = False
self._thread = mkthread(self.thread)

View File

@ -133,6 +133,7 @@ class MagneticField(Drivable):
status = Parameter(datatype=TupleOf(EnumType(Status), StringType()))
def initModule(self):
super().initModule()
self._state = Enum('state', idle=1, switch_on=2, switch_off=3, ramp=4).idle
self._heatswitch = self.DISPATCHER.get_module(self.heatswitch)
_thread = threading.Thread(target=self._thread)
@ -235,6 +236,7 @@ class SampleTemp(Drivable):
)
def initModule(self):
super().initModule()
_thread = threading.Thread(target=self._thread)
_thread.daemon = True
_thread.start()

View File

@ -31,7 +31,7 @@ import math
from secop.datatypes import ArrayOf, FloatRange, StringType, StructOf, TupleOf
from secop.errors import ConfigError, DisabledError
from secop.lib.sequence import SequencerMixin, Step
from secop.modules import BasicPoller, Drivable, Parameter
from secop.modules import Drivable, Parameter
class GarfieldMagnet(SequencerMixin, Drivable):
@ -47,9 +47,6 @@ class GarfieldMagnet(SequencerMixin, Drivable):
the symmetry setting selects which.
"""
pollerClass = BasicPoller
# parameters
subdev_currentsource = Parameter('(bipolar) Powersupply', datatype=StringType(), readonly=True, export=False)
subdev_enable = Parameter('Switch to set for on/off', datatype=StringType(), readonly=True, export=False)
@ -57,10 +54,10 @@ class GarfieldMagnet(SequencerMixin, Drivable):
subdev_symmetry = Parameter('Switch to read for symmetry', datatype=StringType(), readonly=True, export=False)
userlimits = Parameter('User defined limits of device value',
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')),
default=(float('-Inf'), float('+Inf')), readonly=False, poll=10)
default=(float('-Inf'), float('+Inf')), readonly=False)
abslimits = Parameter('Absolute limits of device value',
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')),
default=(-0.5, 0.5), poll=True,
default=(-0.5, 0.5),
)
precision = Parameter('Precision of the device value (allowed deviation '
'of stable values from target)',
@ -71,7 +68,7 @@ class GarfieldMagnet(SequencerMixin, Drivable):
calibration = Parameter('Coefficients for calibration '
'function: [c0, c1, c2, c3, c4] calculates '
'B(I) = c0*I + c1*erf(c2*I) + c3*atan(c4*I)'
' in T', poll=1,
' in T',
datatype=ArrayOf(FloatRange(), 5, 5),
default=(1.0, 0.0, 0.0, 0.0, 0.0))
calibrationtable = Parameter('Map of Coefficients for calibration per symmetry setting',
@ -137,7 +134,7 @@ class GarfieldMagnet(SequencerMixin, Drivable):
'_current2field polynome not monotonic!')
def initModule(self):
super(GarfieldMagnet, self).initModule()
super().initModule()
self._enable = self.DISPATCHER.get_module(self.subdev_enable)
self._symmetry = self.DISPATCHER.get_module(self.subdev_symmetry)
self._polswitch = self.DISPATCHER.get_module(self.subdev_polswitch)
@ -220,7 +217,7 @@ class GarfieldMagnet(SequencerMixin, Drivable):
self._currentsource.read_value() *
self._get_field_polarity())
def read_hw_status(self):
def readHwStatus(self):
# called from SequencerMixin.read_status if no sequence is running
if self._enable.value == 'Off':
return self.Status.WARN, 'Disabled'

View File

@ -39,7 +39,7 @@ from secop.datatypes import ArrayOf, EnumType, FloatRange, \
from secop.errors import CommunicationFailedError, \
ConfigError, HardwareError, ProgrammingError
from secop.lib import lazy_property
from secop.modules import BasicPoller, Command, \
from secop.modules import Command, \
Drivable, Module, Parameter, Readable
#####
@ -157,8 +157,6 @@ class PyTangoDevice(Module):
execution and attribute operations with logging and exception mapping.
"""
pollerClass = BasicPoller
# parameters
comtries = Parameter('Maximum retries for communication',
datatype=IntRange(1, 100), default=3, readonly=False,
@ -210,7 +208,7 @@ class PyTangoDevice(Module):
# exception mapping is enabled).
self._createPyTangoDevice = self._applyGuardToFunc(
self._createPyTangoDevice, 'constructor')
super(PyTangoDevice, self).earlyInit()
super().earlyInit()
@lazy_property
def _dev(self):
@ -249,10 +247,10 @@ class PyTangoDevice(Module):
# otherwise would lead to attribute errors later
try:
device.State
except AttributeError:
except AttributeError as e:
raise CommunicationFailedError(
self, 'connection to Tango server failed, '
'is the server running?')
'is the server running?') from e
return self._applyGuardsToPyTangoDevice(device)
def _applyGuardsToPyTangoDevice(self, dev):
@ -376,14 +374,17 @@ class AnalogInput(PyTangoDevice, Readable):
The AnalogInput handles all devices only delivering an analogue value.
"""
def startModule(self, started_callback):
super(AnalogInput, self).startModule(started_callback)
# query unit from tango and update value property
attrInfo = self._dev.attribute_query('value')
# prefer configured unit if nothing is set on the Tango device, else
# update
if attrInfo.unit != 'No unit':
self.accessibles['value'].datatype.setProperty('unit', attrInfo.unit)
def startModule(self, start_events):
super().startModule(start_events)
try:
# query unit from tango and update value property
attrInfo = self._dev.attribute_query('value')
# prefer configured unit if nothing is set on the Tango device, else
# update
if attrInfo.unit != 'No unit':
self.accessibles['value'].datatype.setProperty('unit', attrInfo.unit)
except Exception as e:
self.log.error(e)
def read_value(self):
return self._dev.value
@ -422,7 +423,7 @@ class AnalogOutput(PyTangoDevice, Drivable):
userlimits = Parameter('User defined limits of device value',
datatype=LimitsType(FloatRange(unit='$')),
default=(float('-Inf'), float('+Inf')),
readonly=False, poll=10,
readonly=False,
)
abslimits = Parameter('Absolute limits of device value',
datatype=LimitsType(FloatRange(unit='$')),
@ -446,13 +447,13 @@ class AnalogOutput(PyTangoDevice, Drivable):
_moving = False
def initModule(self):
super(AnalogOutput, self).initModule()
super().initModule()
# init history
self._history = [] # will keep (timestamp, value) tuple
self._timeout = None # keeps the time at which we will timeout, or None
def startModule(self, started_callback):
super(AnalogOutput, self).startModule(started_callback)
def startModule(self, start_events):
super().startModule(start_events)
# query unit from tango and update value property
attrInfo = self._dev.attribute_query('value')
# prefer configured unit if nothing is set on the Tango device, else
@ -460,8 +461,8 @@ class AnalogOutput(PyTangoDevice, Drivable):
if attrInfo.unit != 'No unit':
self.accessibles['value'].datatype.setProperty('unit', attrInfo.unit)
def pollParams(self, nr=0):
super(AnalogOutput, self).pollParams(nr)
def doPoll(self):
super().doPoll()
while len(self._history) > 2:
# if history would be too short, break
if self._history[-1][0] - self._history[1][0] <= self.window:
@ -489,8 +490,11 @@ class AnalogOutput(PyTangoDevice, Drivable):
hist = self._history[:]
window_start = currenttime() - self.window
hist_in_window = [v for (t, v) in hist if t >= window_start]
if len(hist) == len(hist_in_window):
return False # no data point before window
if not hist_in_window:
return False # no relevant history -> no knowledge
# window is too small -> use last point only
hist_in_window = [self.value]
max_in_hist = max(hist_in_window)
min_in_hist = min(hist_in_window)
@ -503,13 +507,14 @@ class AnalogOutput(PyTangoDevice, Drivable):
if self._isAtTarget():
self._timeout = None
self._moving = False
return super(AnalogOutput, self).read_status()
if self._timeout:
if self._timeout < currenttime():
return self.Status.UNSTABLE, 'timeout after waiting for stable value'
if self._moving:
return (self.Status.BUSY, 'moving')
return (self.Status.IDLE, 'stable')
status = super().read_status()
else:
if self._timeout and self._timeout < currenttime():
status = self.Status.UNSTABLE, 'timeout after waiting for stable value'
else:
status = (self.Status.BUSY, 'moving') if self._moving else (self.Status.IDLE, 'stable')
self.setFastPoll(self.isBusy(status))
return status
@property
def absmin(self):
@ -571,11 +576,14 @@ class AnalogOutput(PyTangoDevice, Drivable):
if not self.timeout:
self._timeout = None
self._moving = True
self._history = [] # clear history
self.read_status() # poll our status to keep it updated
# do not clear the history here:
# - if the target is not changed by more than precision, there is no need to wait
# self._history = []
self.read_status() # poll our status to keep it updated (this will also set fast poll)
return self.read_target()
def _hw_wait(self):
while super(AnalogOutput, self).read_status()[0] == self.Status.BUSY:
while super().read_status()[0] == self.Status.BUSY:
sleep(0.3)
def stop(self):
@ -597,8 +605,7 @@ class Actuator(AnalogOutput):
readonly=False, datatype=FloatRange(0, unit='$/s'),
)
ramp = Parameter('The speed of changing the value',
readonly=False, datatype=FloatRange(0, unit='$/s'),
poll=30,
readonly=False, datatype=FloatRange(0, unit='$/min'),
)
def read_speed(self):
@ -677,17 +684,22 @@ class TemperatureController(Actuator):
)
pid = Parameter('pid control Parameters',
datatype=TupleOf(FloatRange(), FloatRange(), FloatRange()),
readonly=False, group='pid', poll=30,
readonly=False, group='pid',
)
setpoint = Parameter('Current setpoint', datatype=FloatRange(unit='$'), poll=1,
setpoint = Parameter('Current setpoint', datatype=FloatRange(unit='$'),
)
heateroutput = Parameter('Heater output', datatype=FloatRange(), poll=1,
heateroutput = Parameter('Heater output', datatype=FloatRange(),
)
# overrides
precision = Parameter(default=0.1)
ramp = Parameter(description='Temperature ramp')
def doPoll(self):
super().doPoll()
self.read_setpoint()
self.read_heateroutput()
def read_ramp(self):
return self._dev.ramp
@ -730,6 +742,10 @@ class TemperatureController(Actuator):
def read_heateroutput(self):
return self._dev.heaterOutput
# remove UserCommand setposition from Actuator
# (makes no sense for a TemperatureController)
setposition = None
class PowerSupply(Actuator):
"""A power supply (voltage and current) device.
@ -737,13 +753,19 @@ class PowerSupply(Actuator):
# parameters
voltage = Parameter('Actual voltage',
datatype=FloatRange(unit='V'), poll=-5)
datatype=FloatRange(unit='V'))
current = Parameter('Actual current',
datatype=FloatRange(unit='A'), poll=-5)
datatype=FloatRange(unit='A'))
# overrides
ramp = Parameter(description='Current/voltage ramp')
def doPoll(self):
super().doPoll()
# TODO: poll voltage and current faster when busy
self.read_voltage()
self.read_current()
def read_ramp(self):
return self._dev.ramp
@ -777,16 +799,18 @@ class NamedDigitalInput(DigitalInput):
datatype=StringType(), export=False) # XXX:!!!
def initModule(self):
super(NamedDigitalInput, self).initModule()
super().initModule()
try:
# pylint: disable=eval-used
mapping = eval(self.mapping.replace('\n', ' '))
mapping = self.mapping
if isinstance(mapping, str):
# pylint: disable=eval-used
mapping = eval(self.mapping.replace('\n', ' '))
if isinstance(mapping, str):
# pylint: disable=eval-used
mapping = eval(mapping)
self.accessibles['value'].setProperty('datatype', EnumType('value', **mapping))
except Exception as e:
raise ValueError('Illegal Value for mapping: %r' % e)
raise ValueError('Illegal Value for mapping: %r' % self.mapping) from e
def read_value(self):
value = self._dev.value
@ -805,7 +829,7 @@ class PartialDigitalInput(NamedDigitalInput):
datatype=IntRange(0), default=1)
def initModule(self):
super(PartialDigitalInput, self).initModule()
super().initModule()
self._mask = (1 << self.bitwidth) - 1
# self.accessibles['value'].datatype = IntRange(0, self._mask)
@ -827,9 +851,16 @@ class DigitalOutput(PyTangoDevice, Drivable):
def read_value(self):
return self._dev.value # mapping is done by datatype upon export()
def read_status(self):
status = self.read_status()
self.setFastPoll(self.isBusy(status))
return status
def write_target(self, value):
self._dev.value = value
self.read_value()
self.read_status() # this will also set fast poll
return self.read_target()
def read_target(self):
attrObj = self._dev.read_attribute('value')
@ -845,22 +876,25 @@ class NamedDigitalOutput(DigitalOutput):
datatype=StringType(), export=False)
def initModule(self):
super(NamedDigitalOutput, self).initModule()
super().initModule()
try:
# pylint: disable=eval-used
mapping = eval(self.mapping.replace('\n', ' '))
mapping = self.mapping
if isinstance(mapping, str):
# pylint: disable=eval-used
mapping = eval(self.mapping.replace('\n', ' '))
if isinstance(mapping, str):
# pylint: disable=eval-used
mapping = eval(mapping)
self.accessibles['value'].setProperty('datatype', EnumType('value', **mapping))
self.accessibles['target'].setProperty('datatype', EnumType('target', **mapping))
except Exception as e:
raise ValueError('Illegal Value for mapping: %r' % e)
raise ValueError('Illegal Value for mapping: %r' % self.mapping) from e
def write_target(self, value):
# map from enum-str to integer value
self._dev.value = int(value)
self.read_value()
return self.read_target()
class PartialDigitalOutput(NamedDigitalOutput):
@ -875,7 +909,7 @@ class PartialDigitalOutput(NamedDigitalOutput):
datatype=IntRange(0), default=1)
def initModule(self):
super(PartialDigitalOutput, self).initModule()
super().initModule()
self._mask = (1 << self.bitwidth) - 1
# self.accessibles['value'].datatype = IntRange(0, self._mask)
# self.accessibles['target'].datatype = IntRange(0, self._mask)
@ -891,6 +925,7 @@ class PartialDigitalOutput(NamedDigitalOutput):
(value << self.startbit)
self._dev.value = newvalue
self.read_value()
return self.read_target()
class StringIO(PyTangoDevice, Module):

View File

@ -20,7 +20,7 @@
# *****************************************************************************
"""Andeen Hagerling capacitance bridge"""
from secop.core import Done, FloatRange, HasIodev, Parameter, Readable, StringIO
from secop.core import Done, FloatRange, HasIO, Parameter, Readable, StringIO, nopoll
class Ah2700IO(StringIO):
@ -28,19 +28,19 @@ class Ah2700IO(StringIO):
timeout = 5
class Capacitance(HasIodev, Readable):
class Capacitance(HasIO, Readable):
value = Parameter('capacitance', FloatRange(unit='pF'), poll=True)
value = Parameter('capacitance', FloatRange(unit='pF'))
freq = Parameter('frequency', FloatRange(unit='Hz'), readonly=False, default=0)
voltage = Parameter('voltage', FloatRange(unit='V'), readonly=False, default=0)
loss = Parameter('loss', FloatRange(unit='deg'), default=0)
iodevClass = Ah2700IO
ioClass = Ah2700IO
def parse_reply(self, reply):
if reply.startswith('SI'): # this is an echo
self.sendRecv('SERIAL ECHO OFF')
reply = self.sendRecv('SI')
self.communicate('SERIAL ECHO OFF')
reply = self.communicate('SI')
if not reply.startswith('F='): # this is probably an error message like "LOSS TOO HIGH"
self.status = [self.Status.ERROR, reply]
return
@ -59,32 +59,35 @@ class Capacitance(HasIodev, Readable):
if lossunit == 'DS':
self.loss = loss
else: # the unit was wrong, we want DS = tan(delta), not NS = nanoSiemens
reply = self.sendRecv('UN DS').split() # UN DS returns a reply similar to SI
reply = self.communicate('UN DS').split() # UN DS returns a reply similar to SI
try:
self.loss = reply[7]
except IndexError:
pass # don't worry, loss will be updated next time
def read_value(self):
self.parse_reply(self.sendRecv('SI')) # SI = single trigger
self.parse_reply(self.communicate('SI')) # SI = single trigger
return Done
@nopoll
def read_freq(self):
self.read_value()
return Done
@nopoll
def read_loss(self):
self.read_value()
return Done
def read_volt(self):
@nopoll
def read_voltage(self):
self.read_value()
return Done
def write_freq(self, value):
self.parse_reply(self.sendRecv('FR %g;SI' % value))
self.parse_reply(self.communicate('FR %g;SI' % value))
return Done
def write_volt(self, value):
self.parse_reply(self.sendRecv('V %g;SI' % value))
def write_voltage(self, value):
self.parse_reply(self.communicate('V %g;SI' % value))
return Done

View File

@ -23,7 +23,7 @@
"""drivers for CCU4, the cryostat control unit at SINQ"""
# the most common Frappy classes can be imported from secop.core
from secop.core import EnumType, FloatRange, \
HasIodev, Parameter, Readable, StringIO
HasIO, Parameter, Readable, StringIO
class CCU4IO(StringIO):
@ -34,14 +34,13 @@ class CCU4IO(StringIO):
identification = [('cid', r'CCU4.*')]
# inheriting the HasIodev mixin creates us a private attribute *_iodev*
# for talking with the hardware
# inheriting HasIO allows us to use the communicate method for talking with the hardware
# Readable as a base class defines the value and status parameters
class HeLevel(HasIodev, Readable):
class HeLevel(HasIO, Readable):
"""He Level channel of CCU4"""
# define the communication class to create the IO module
iodevClass = CCU4IO
ioClass = CCU4IO
# define or alter the parameters
# as Readable.value exists already, we give only the modified property 'unit'
@ -71,9 +70,9 @@ class HeLevel(HasIodev, Readable):
for changing a parameter
:returns: the (new) value of the parameter
"""
name, txtvalue = self._iodev.communicate(cmd).split('=')
name, txtvalue = self.communicate(cmd).split('=')
assert name == cmd.split('=')[0] # check that we got a reply to our command
return txtvalue # Frappy will automatically convert the string to the needed data type
return float(txtvalue)
def read_value(self):
return self.query('h')

159
secop_psi/convergence.py Normal file
View File

@ -0,0 +1,159 @@
# -*- 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:
# Markus Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
from secop.core import Parameter, FloatRange, BUSY, IDLE, WARN
from secop.lib.statemachine import StateMachine, Retry, Stop
class HasConvergence:
"""mixin for convergence checks
Implementation based on tolerance, settling time and timeout.
The algorithm does its best to support changes of these parameters on the
fly. However, the full history is not considered, which means for example
that the spent time inside tolerance stored already is not altered when
changing tolerance.
"""
tolerance = Parameter('absolute tolerance', FloatRange(0, unit='$'), readonly=False, default=0)
settling_time = Parameter(
'''settling time
total amount of time the value has to be within tolerance before switching to idle.
''', FloatRange(0, unit='sec'), readonly=False, default=60)
timeout = Parameter(
'''timeout
timeout = 0: disabled, else:
A timeout event happens, when the difference abs(<target> - <value>) drags behind
the expected difference for longer than <timeout>. The expected difference is determined
by parameters 'workingramp' or 'ramp'. If ramp is not available, an exponential decay of
the difference with <tolerance> as time constant is expected.
As soon as the value is the first time within tolerance, the timeout criterium is changed:
then the timeout event happens after this time + <settling_time> + <timeout>.
''', FloatRange(0, unit='sec'), readonly=False, default=3600)
status = Parameter('status determined from convergence checks', default=(IDLE, ''))
convergence_state = None
def earlyInit(self):
super().earlyInit()
self.convergence_state = StateMachine(threaded=False, logger=self.log,
cleanup=self.cleanup, spent_inside=0)
def cleanup(self, state):
state.default_cleanup(state)
if self.stopped:
if self.stopped is Stop: # and not Restart
self.status = WARN, 'stopped'
else:
self.status = WARN, repr(state.last_error)
def doPoll(self):
super().doPoll()
state = self.convergence_state
state.cycle()
def get_min_slope(self, dif):
slope = getattr(self, 'workingramp', 0) or getattr(self, 'ramp', 0)
if slope or not self.timeout:
return slope
return dif / self.timeout # assume exponential decay of dif, with time constant <tolerance>
def get_dif_tol(self):
self.read_value()
tol = self.tolerance
if not tol:
tol = 0.01 * max(abs(self.target), abs(self.value))
dif = abs(self.target - self.value)
return dif, tol
def start_state(self):
"""to be called from write_target"""
self.convergence_state.start(self.state_approach)
def state_approach(self, state):
"""approaching, checking progress (busy)"""
dif, tol = self.get_dif_tol()
if dif < tol:
state.timeout_base = state.now
return self.state_inside
if not self.timeout:
return Retry()
if state.init:
state.timeout_base = state.now
state.dif_crit = dif # criterium for resetting timeout base
self.status = BUSY, 'approaching'
state.spent_inside = 0
state.dif_crit -= self.get_min_slope(dif) * state.delta()
if dif < state.dif_crit: # progress is good: reset timeout base
state.timeout_base = state.now
elif state.now > state.timeout_base + self.timeout:
self.status = WARN, 'convergence timeout'
return self.state_instable
return Retry()
def state_inside(self, state):
"""inside tolerance, still busy"""
dif, tol = self.get_dif_tol()
if dif > tol:
return self.state_outside
state.spent_inside += state.delta()
if state.spent_inside > self.settling_time:
self.status = IDLE, 'reached target'
return self.state_stable
if state.init:
self.status = BUSY, 'inside tolerance'
return Retry()
def state_outside(self, state):
"""temporarely outside tolerance, busy"""
dif, tol = self.get_dif_tol()
if dif < tol:
return self.state_inside
if state.now > state.timeout_base + self.settling_time + self.timeout:
self.status = WARN, 'settling timeout'
return self.state_instable
if state.init:
self.status = BUSY, 'outside tolerance'
# do not reset the settling time on occasional outliers, count backwards instead
state.spent_inside = max(0.0, state.spent_inside - state.delta())
return Retry()
def state_stable(self, state):
"""stable, after settling_time spent within tolerance, idle"""
dif, tol = self.get_dif_tol()
if dif <= tol:
return Retry()
self.status = WARN, 'instable'
state.spent_inside = max(self.settling_time, state.spent_inside)
return self.state_instable
def state_instable(self, state):
"""went outside tolerance from stable, warning"""
dif, tol = self.get_dif_tol()
if dif <= tol:
state.spent_inside += state.delta()
if state.spent_inside > self.settling_time:
self.status = IDLE, 'stable' # = recovered from instable
return self.state_stable
else:
state.spent_inside = max(0, state.spent_inside - state.delta())
return Retry()

View File

@ -45,12 +45,7 @@ def make_cvt_list(dt, tail=''):
result = []
for subkey, elmtype in items:
for fun, tail_, opts in make_cvt_list(elmtype, '%s.%s' % (tail, subkey)):
def conv(value, key=subkey, func=fun):
try:
return value[key]
except KeyError: # can not use value.get() because value might be a list
return None
result.append((conv, tail_, opts))
result.append((lambda v, k=subkey, f=fun: f(v[k]), tail_, opts))
return result
@ -70,11 +65,11 @@ class FrappyHistoryWriter(frappyhistory.FrappyWriter):
- period:
the typical 'lifetime' of a value.
The intention is, that points in a chart may be connected by a straight line
when the distance is lower than twice this value. If not, the line should be
drawn horizontally from the last point to a point <period> before the next value.
when the distance is lower than this value. If not, the line should be drawn
horizontally from the last point to a point <period> before the next value.
For example a setpoint should have period 0, which will lead to a stepped
line, whereas for a measured value like a temperature, period should be
equal to the poll interval. In order to make full use of this,
slightly bigger than the poll interval. In order to make full use of this,
we would need some additional parameter property.
- show: True/False, whether this curve should be shown or not by default in
a summary chart

View File

@ -18,12 +18,18 @@
# Module authors:
# Markus Zolliker <markus.zolliker@psi.ch>
# *****************************************************************************
"""Keithley 2601B source meter
"""Keithley 2601B 4 quadrant source meter
not tested yet"""
not tested yet
from secop.core import Attached, BoolType, EnumType, FloatRange, \
HasIodev, Module, Parameter, StringIO, Writable, Done
* switching between voltage and current happens by setting their target
* switching output off by setting the active parameter of the controlling
module to False.
* setting the active parameter to True raises an error
"""
from secop.core import Attached, BoolType, Done, EnumType, FloatRange, \
HasIO, Module, Parameter, Readable, StringIO, Writable
class K2601bIO(StringIO):
@ -32,128 +38,162 @@ class K2601bIO(StringIO):
SOURCECMDS = {
0: 'reset()'
' smua.source.output = 0 print("ok")',
1: 'reset()'
' smua.source.func = smua.OUTPUT_DCAMPS'
' display.smua.measure.func = display.MEASURE_VOLTS'
' smua.source.autorangei = 1'
' smua.source.output = %d print("ok"")',
1: 'reset()'
' smua.source.output = 1 print("ok")',
2: 'reset()'
' smua.source.func = smua.OUTPUT_DCVOLTS'
' display.smua.measure.func = display.MEASURE_DCAMPS'
' smua.source.autorangev = 1'
' smua.source.output = %d print("ok"")',
' smua.source.output = 1 print("ok")',
}
class SourceMeter(HasIodev, Module):
class SourceMeter(HasIO, Module):
export = False # export for tests only
mode = Parameter('measurement mode', EnumType(off=0, current=1, voltage=2),
readonly=False, export=False)
ilimit = Parameter('current limit', FloatRange(0, 2.0, unit='A'), default=2)
vlimit = Parameter('voltage limit', FloatRange(0, 2.0, unit='V'), default=2)
resistivity = Parameter('readback resistivity', FloatRange(unit='Ohm'), poll=True)
power = Parameter('readback power', FloatRange(unit='W'), poll=True)
mode = Parameter('measurement mode', EnumType(current_off=0, voltage_off=1, current_on=2, voltage_on=3),
readonly=False, poll=True)
iodevClass = K2601bIO
def read_resistivity(self):
return self.sendRecv('print(smua.measure.r())')
def read_power(self):
return self.sendRecv('print(smua.measure.p())')
ioClass = K2601bIO
def read_mode(self):
return float(self.sendRecv('print(smua.source.func+2*smua.source.output)'))
return float(self.communicate('print((smua.source.func+1)*smua.source.output)'))
def write_mode(self, value):
assert 'ok' == self.sendRecv(SOURCECMDS[value % 2] % (value >= 2))
if value == 'current':
self.write_vlimit(self.vlimit)
elif value == 'voltage':
self.write_ilimit(self.ilimit)
assert self.communicate(SOURCECMDS[value]) == 'ok'
return self.read_mode()
def read_ilimit(self):
if self.mode == 'current':
return self.ilimit
return float(self.communicate('print(smua.source.limiti)'))
class Current(HasIodev, Writable):
sourcemeter = Attached()
def write_ilimit(self, value):
if self.mode == 'current':
return self.ilimit
return float(self.communicate('smua.source.limiti = %g print(smua.source.limiti)' % value))
value = Parameter('measured current', FloatRange(unit='A'), poll=True)
target = Parameter('set current', FloatRange(unit='A'), poll=True)
active = Parameter('current is controlled', BoolType(), default=False) # polled by SourceMeter
limit = Parameter('current limit', FloatRange(0, 2.0, unit='A'), default=2, poll=True)
def read_vlimit(self):
if self.mode == 'voltage':
return self.ilimit
return float(self.communicate('print(smua.source.limitv)'))
def initModule(self):
self._sourcemeter.registerCallbacks(self)
def write_vlimit(self, value):
if self.mode == 'voltage':
return self.ilimit
return float(self.communicate('smua.source.limitv = %g print(smua.source.limitv)' % value))
class Power(HasIO, Readable):
value = Parameter('readback power', FloatRange(unit='W'))
ioClass = K2601bIO
def read_value(self):
return self.sendRecv('print(smua.measure.i())')
return float(self.communicate('print(smua.measure.p())'))
class Resistivity(HasIO, Readable):
value = Parameter('readback resistivity', FloatRange(unit='Ohm'))
ioClass = K2601bIO
def read_value(self):
return float(self.communicate('print(smua.measure.r())'))
class Current(HasIO, Writable):
sourcemeter = Attached()
value = Parameter('measured current', FloatRange(unit='A'))
target = Parameter('set current', FloatRange(unit='A'))
active = Parameter('current is controlled', BoolType(), default=False)
limit = Parameter('current limit', FloatRange(0, 2.0, unit='A'), default=2)
def initModule(self):
self.sourcemeter.registerCallbacks(self)
def read_value(self):
return float(self.communicate('print(smua.measure.i())'))
def read_target(self):
return self.sendRecv('print(smua.source.leveli)')
return float(self.communicate('print(smua.source.leveli)'))
def write_target(self, value):
if not self.active:
raise ValueError('current source is disabled')
if value > self.limit:
if value > self.sourcemeter.ilimit:
raise ValueError('current exceeds limit')
return self.sendRecv('smua.source.leveli = %g print(smua.source.leveli)' % value)
value = float(self.communicate('smua.source.leveli = %g print(smua.source.leveli)' % value))
if not self.active:
self.sourcemeter.write_mode('current') # triggers update_mode -> set active to True
return value
def read_limit(self):
if self.active:
return self.limit
return self.sendRecv('print(smua.source.limiti)')
return self.sourcemeter.read_ilimit()
def write_limit(self, value):
if self.active:
return value
return self.sendRecv('smua.source.limiti = %g print(smua.source.limiti)' % value)
def write_active(self, value):
if value:
self._sourcemeter.write_mode('current_on')
elif self._sourcemeter.mode == 'current_on':
self._sourcemeter.write_mode('current_off')
return self.active
return self.sourcemeter.write_ilimit(value)
def update_mode(self, mode):
# will be called whenever the attached sourcemeters mode changes
self.active = mode == 'current_on'
self.active = mode == 'current'
def write_active(self, value):
self.sourcemeter.read_mode()
if value == self.value:
return Done
if value:
raise ValueError('activate only by setting target')
self.sourcemeter.write_mode('off') # triggers update_mode -> set active to False
return Done
class Voltage(HasIodev, Writable):
class Voltage(HasIO, Writable):
sourcemeter = Attached()
value = Parameter('measured voltage', FloatRange(unit='V'), poll=True)
target = Parameter('set voltage', FloatRange(unit='V'), poll=True)
active = Parameter('voltage is controlled', BoolType(), default=False) # polled by SourceMeter
limit = Parameter('current limit', FloatRange(0, 2.0, unit='V'), default=2, poll=True)
value = Parameter('measured voltage', FloatRange(unit='V'))
target = Parameter('set voltage', FloatRange(unit='V'))
active = Parameter('voltage is controlled', BoolType())
limit = Parameter('voltage limit', FloatRange(0, 2.0, unit='V'), default=2)
def initModule(self):
self._sourcemeter.registerCallbacks(self)
self.sourcemeter.registerCallbacks(self)
def read_value(self):
return self.sendRecv('print(smua.measure.v())')
return float(self.communicate('print(smua.measure.v())'))
def read_target(self):
return self.sendRecv('print(smua.source.levelv)')
return float(self.communicate('print(smua.source.levelv)'))
def write_target(self, value):
if not self.active:
raise ValueError('voltage source is disabled')
if value > self.limit:
if value > self.sourcemeter.vlimit:
raise ValueError('voltage exceeds limit')
return self.sendRecv('smua.source.levelv = %g print(smua.source.levelv)' % value)
value = float(self.communicate('smua.source.levelv = %g print(smua.source.levelv)' % value))
if not self.active:
self.sourcemeter.write_mode('voltage') # triggers update_mode -> set active to True
return value
def read_limit(self):
if self.active:
return self.limit
return self.sendRecv('print(smua.source.limitv)')
return self.sourcemeter.read_vlimit()
def write_limit(self, value):
if self.active:
return value
return self.sendRecv('smua.source.limitv = %g print(smua.source.limitv)' % value)
def write_active(self, value):
if value:
self._sourcemeter.write_mode('voltage_on')
elif self._sourcemeter.mode == 'voltage_on':
self._sourcemeter.write_mode('voltage_off')
return self.active
return self.sourcemeter.write_vlimit(value)
def update_mode(self, mode):
# will be called whenever the attached sourcemeters mode changes
self.active = mode == 'voltage_on'
self.active = mode == 'voltage'
def write_active(self, value):
self.sourcemeter.read_mode()
if value == self.value:
return Done
if value:
raise ValueError('activate only by setting target')
self.sourcemeter.write_mode('off') # triggers update_mode -> set active to False
return Done

View File

@ -27,8 +27,7 @@ from secop.datatypes import BoolType, EnumType, FloatRange, IntRange
from secop.lib import formatStatusBits
from secop.modules import Attached, Done, \
Drivable, Parameter, Property, Readable
from secop.poller import REGULAR, Poller
from secop.io import HasIodev
from secop.io import HasIO
Status = Drivable.Status
@ -58,29 +57,29 @@ class StringIO(secop.io.StringIO):
wait_before = 0.05
class Main(HasIodev, Drivable):
class Main(HasIO, Drivable):
value = Parameter('the current channel', poll=REGULAR, datatype=IntRange(0, 17))
value = Parameter('the current channel', datatype=IntRange(0, 17))
target = Parameter('channel to select', datatype=IntRange(0, 17))
autoscan = Parameter('whether to scan automatically', datatype=BoolType(), readonly=False, default=False)
pollinterval = Parameter(default=1, export=False)
pollerClass = Poller
iodevClass = StringIO
ioClass = StringIO
_channel_changed = 0 # time of last channel change
_channels = None # dict <channel no> of <module object>
def earlyInit(self):
super().earlyInit()
self._channels = {}
def register_channel(self, modobj):
self._channels[modobj.channel] = modobj
def startModule(self, started_callback):
started_callback()
def startModule(self, start_events):
super().startModule(start_events)
for ch in range(1, 16):
if ch not in self._channels:
self.sendRecv('INSET %d,0,0,0,0,0;INSET?%d' % (ch, ch))
self.communicate('INSET %d,0,0,0,0,0;INSET?%d' % (ch, ch))
def read_value(self):
channel, auto = scan.send_command(self)
@ -113,7 +112,7 @@ class Main(HasIodev, Drivable):
def write_target(self, channel):
scan.send_change(self, channel, self.autoscan)
# self.sendRecv('SCAN %d,%d;SCAN?' % (channel, self.autoscan))
# self.communicate('SCAN %d,%d;SCAN?' % (channel, self.autoscan))
if channel != self.value:
self.value = 0
self._channel_changed = time.time()
@ -122,11 +121,11 @@ class Main(HasIodev, Drivable):
def write_autoscan(self, value):
scan.send_change(self, self.value, value)
# self.sendRecv('SCAN %d,%d;SCAN?' % (channel, self.autoscan))
# self.communicate('SCAN %d,%d;SCAN?' % (channel, self.autoscan))
return value
class ResChannel(HasIodev, Readable):
class ResChannel(HasIO, Readable):
"""temperature channel on Lakeshore 336"""
RES_RANGE = {key: i+1 for i, key in list(
@ -140,8 +139,7 @@ class ResChannel(HasIodev, Readable):
enumerate(mag % val for mag in ['%guV', '%gmV']
for val in [2, 6.32, 20, 63.2, 200, 632]))}
pollerClass = Poller
iodevClass = StringIO
ioClass = StringIO
_main = None # main module
_last_range_change = 0 # time of last range change
@ -166,6 +164,7 @@ class ResChannel(HasIodev, Readable):
_trigger_read = False
def initModule(self):
super().initModule()
self._main = self.DISPATCHER.get_module(self.main)
self._main.register_channel(self)
@ -181,7 +180,7 @@ class ResChannel(HasIodev, Readable):
return Done
# we got here, when we missed the idle state of self._main
self._trigger_read = False
result = self.sendRecv('RDGR?%d' % self.channel)
result = self.communicate('RDGR?%d' % self.channel)
result = float(result)
if self.autorange == 'soft':
now = time.time()
@ -214,9 +213,9 @@ class ResChannel(HasIodev, Readable):
def read_status(self):
if not self.enabled:
return [self.Status.DISABLED, 'disabled']
if self.channel != self._main.value:
if self.channel != self.main.value:
return Done
result = int(self.sendRecv('RDGST?%d' % self.channel))
result = int(self.communicate('RDGST?%d' % self.channel))
result &= 0x37 # mask T_OVER and T_UNDER (change this when implementing temperatures instead of resistivities)
statustext = ' '.join(formatStatusBits(result, STATUS_BIT_LABELS))
if statustext:
@ -228,7 +227,7 @@ class ResChannel(HasIodev, Readable):
if autorange:
result['autorange'] = 'hard'
# else: do not change autorange
# self.log.info('%s range %r %r %r' % (self.name, rng, autorange, self.autorange))
self.log.debug('%s range %r %r %r' % (self.name, rng, autorange, self.autorange))
if excoff:
result.update(iexc=0, vexc=0)
elif iscur:
@ -281,5 +280,5 @@ class ResChannel(HasIodev, Readable):
def write_enabled(self, value):
inset.write(self, 'enabled', value)
if value:
self._main.write_target(self.channel)
self.main.write_target(self.channel)
return Done

View File

@ -28,22 +28,23 @@ class Ls370Sim(Communicator):
('RDGR?%d', '1.0'),
('RDGST?%d', '0'),
('RDGRNG?%d', '0,5,5,0,0'),
('INSET?%d', '1,3,3,0,0'),
('FILTER?%d', '1,1,80'),
('INSET?%d', '1,5,5,0,0'),
('FILTER?%d', '1,5,80'),
]
OTHER_COMMANDS = [
('*IDN?', 'LSCI,MODEL370,370184,05302003'),
('SCAN?', '3,0'),
('SCAN?', '3,1'),
]
def earlyInit(self):
super().earlyInit()
self._data = dict(self.OTHER_COMMANDS)
for fmt, v in self.CHANNEL_COMMANDS:
for chan in range(1,17):
self._data[fmt % chan] = v
# mkthread(self.run)
def communicate(self, command):
self.comLog('> %s' % command)
# simulation part, time independent
for channel in range(1,17):
_, _, _, _, excoff = self._data['RDGRNG?%d' % channel].split(',')
@ -68,6 +69,6 @@ class Ls370Sim(Communicator):
if qcmd in self._data:
self._data[qcmd] = arg
break
#if command.startswith('R'):
# print('> %s\t< %s' % (command, reply))
return ';'.join(reply)
reply = ';'.join(reply)
self.comLog('< %s' % reply)
return reply

View File

@ -27,8 +27,9 @@ import time
from secop.core import Drivable, HasIodev, \
Parameter, Property, Readable, StringIO
from secop.datatypes import EnumType, FloatRange, StringType
from secop.datatypes import EnumType, FloatRange, StringType, StructOf
from secop.errors import HardwareError
from secop.lib.statemachine import StateMachine
class MercuryIO(StringIO):
@ -120,7 +121,7 @@ class HasProgressCheck:
changing tolerance.
"""
tolerance = Parameter('absolute tolerance', FloatRange(0), readonly=False, default=0)
relative_tolerance = Parameter('_', FloatRange(0, 1), readonly=False, default=0)
min_slope = Parameter('minimal abs(slope)', FloatRange(0), readonly=False, default=0)
settling_time = Parameter(
'''settling time
@ -130,75 +131,76 @@ class HasProgressCheck:
'''timeout
timeout = 0: disabled, else:
A timeout happens, when the difference value - target is not improved by more than
a factor 2 within timeout.
More precisely, we expect a convergence curve which decreases the difference
by a factor 2 within timeout/2.
If this expected progress is delayed by more than timeout/2, a timeout happens.
If the convergence is better than above, the expected curve is adjusted continuously.
In case the tolerance is reached once, a timeout happens when the time after this is
exceeded by more than settling_time + timeout.
A timeout event happens, when the difference (target - value) is not improved by
at least min_slope * timeout over any interval (t, t + timeout).
As soon as the value is the first time within tolerance, the criterium is changed:
then the timeout event happens after this time + settling_time + timeout.
''', FloatRange(0, unit='sec'), readonly=False, default=3600)
status = Parameter('status determined from progress check')
value = Parameter()
target = Parameter()
_settling_start = None # supposed start of settling time (0 when outside)
_first_inside = None # first time within tolerance
_spent_inside = 0 # accumulated settling time
# the upper limit for t0, for the curve timeout_dif * 2 ** -(t - t0)/timeout not touching abs(value(t) - target)
_timeout_base = 0
_timeout_dif = 1
def earlyInit(self):
super().earlyInit()
self.__state = StateMachine()
def check_progress(self, value, target):
"""called from read_status
def prepare_state(self, state):
tol = self.tolerance
if not tol:
tol = 0.01 * max(abs(self.target), abs(self.value))
dif = abs(self.target - self.value)
return dif, tol, state.now, state.delta(0)
intended to be also be used for alternative implementations of read_status
"""
base = max(abs(target), abs(value))
tol = base * self.relative_tolerance + self.tolerance
if tol == 0:
tol = max(0.01, base * 0.01)
now = time.time()
dif = abs(value - target)
if self._settling_start: # we were inside tol
self._spent_inside = now - self._settling_start
if dif > tol: # transition inside -> outside
self._settling_start = None
else: # we were outside tol
if dif <= tol: # transition outside -> inside
if not self._first_inside:
self._first_inside = now
self._settling_start = now - self._spent_inside
if self._spent_inside > self.settling_time:
return 'IDLE', ''
result = 'BUSY', ('inside tolerance' if self._settling_start else 'outside tolerance')
if self.timeout:
if self._first_inside:
if now > self._first_inside + self.settling_time + self.timeout:
return 'WARNING', 'settling timeout'
return result
tmo2 = self.timeout / 2
def state_approaching(self, state):
if self.init():
self.status = 'BUSY', 'approaching'
dif, tol, now, delta = self.prepare_state(state)
if dif < tol:
state.timeout_base = now
state.next_step(self.state_inside)
return
if not self.timeout:
return
if state.init():
state.timeout_base = now
state.dif_crit = dif
return
min_slope = getattr(self, 'ramp', 0) or getattr('min_slope', 0)
state.dif_crit -= min_slope * delta
if dif < state.dif_crit:
state.timeout_base = now
elif now > state.timeout_base:
self.status = 'WARNING', 'convergence timeout'
state.next_action(self.state_idle)
def exponential_convergence(t):
return self._timeout_dif * 2 ** -(t - self._timeout_base) / tmo2
def state_inside(self, state):
if state.init():
self.status = 'BUSY', 'inside tolerance'
dif, tol, now, delta = self.prepare_state(state)
if dif > tol:
state.next_action(self.state_outside)
state.spent_inside += delta
if state.spent_inside > self.settling_time:
self.status = 'IDLE', 'reached target'
state.next_action(self.state_idle)
if dif < exponential_convergence(now):
# convergence is better than estimated, update expected curve
self._timeout_dif = dif
self._timeout_base = now
elif dif > exponential_convergence(now - tmo2):
return 'WARNING', 'convergence timeout'
return result
def state_outside(self, state, now, dif, tol, delta):
if state.init():
self.status = 'BUSY', 'outside tolerance'
dif, tol, now, delta = self.prepare_state(state)
if dif < tol:
state.next_action(self.state_inside)
elif now > self.timeout_base + self.settling_time + self.timeout:
self.status = 'WARNING', 'settling timeout'
state.next_action(self.state_idle)
def reset_progress(self, value, target):
def start_state(self):
"""must be called from write_target, whenever the target changes"""
self._settling_start = None
self._first_inside = None
self._spent_inside = 0
self._timeout_base = time.time()
self._timeout_dif = abs(value - target)
self.__state.start(self.state_approach)
def poll(self):
super().poll()
self.__state.poll()
def read_status(self):
if self.status[0] == 'IDLE':
@ -213,13 +215,15 @@ class HasProgressCheck:
class Loop(HasProgressCheck, MercuryChannel):
"""common base class for loops"""
mode = Parameter('control mode', EnumType(manual=0, pid=1), readonly=False)
prop = Parameter('pid proportional band', FloatRange(), readonly=False)
integ = Parameter('pid integral parameter', FloatRange(unit='min'), readonly=False)
deriv = Parameter('pid differential parameter', FloatRange(unit='min'), readonly=False)
ctrlpars = Parameter(
'pid (proportional nad, integral time, differential time',
StructOf(p=FloatRange(0, unit='$'), i=FloatRange(0, unit='min'), d=FloatRange(0, unit='min')),
readonly=False, poll=True
)
"""pid = Parameter('control parameters', StructOf(p=FloatRange(), i=FloatRange(), d=FloatRange()),readonly=False)"""
pid_table_mode = Parameter('', EnumType(off=0, on=1), readonly=False)
def read_prop(self):
def read_ctrlpars(self):
return self.query('0:LOOP:P')
def read_integ(self):

View File

@ -148,6 +148,7 @@ class Motor(PersistentMixin, HasIodev, Drivable):
@Command
def reset(self):
"""reset error, set position to encoder"""
self.read_value()
if self.status[0] == self.Status.ERROR:
enc = self.encoder - self.zero

View File

@ -33,17 +33,17 @@ Polling of value and status is done commonly for all modules. For each registere
import threading
import time
from ast import literal_eval # convert string as comma separated numbers into tuple
import secop.iohandler
from secop.datatypes import BoolType, EnumType, \
FloatRange, IntRange, StatusType, StringType
from secop.errors import HardwareError
from secop.lib import clamp
from secop.lib.enum import Enum
from secop.modules import Attached, Communicator, Done, \
from secop.modules import Communicator, Done, \
Drivable, Parameter, Property, Readable
from secop.poller import Poller
from secop.io import HasIodev
from secop.io import HasIO
from secop.rwhandler import CommonReadHandler, CommonWriteHandler
try:
import secop_psi.ppmswindows as ppmshw
@ -52,28 +52,11 @@ except ImportError:
import secop_psi.ppmssim as ppmshw
class IOHandler(secop.iohandler.IOHandler):
"""IO handler for PPMS commands
deals with typical format:
- query command: ``<command>?``
- reply: ``<value1>,<value2>, ..``
- change command: ``<command> <value1>,<value2>,...``
"""
CMDARGS = ['no'] # the channel number is needed in channel commands
CMDSEPARATOR = None # no command chaining
def __init__(self, name, querycmd, replyfmt):
changecmd = querycmd.split('?')[0] + ' '
super().__init__(name, querycmd, replyfmt, changecmd)
class Main(Communicator):
"""ppms communicator module"""
pollinterval = Parameter('poll interval', FloatRange(), readonly=False, default=2)
data = Parameter('internal', StringType(), poll=True, export=True, # export for test only
data = Parameter('internal', StringType(), export=True, # export for test only
default="", readonly=True)
class_id = Property('Quantum Design class id', StringType(), export=False)
@ -86,9 +69,8 @@ class Main(Communicator):
_channel_to_index = dict(((channel, i) for i, channel in enumerate(_channel_names)))
_status_bitpos = {'temp': 0, 'field': 4, 'chamber': 8, 'position': 12}
pollerClass = Poller
def earlyInit(self):
super().earlyInit()
self.modules = {}
self._ppms_device = ppmshw.QDevice(self.class_id)
self.lock = threading.Lock()
@ -99,10 +81,14 @@ class Main(Communicator):
def communicate(self, command):
"""GPIB command"""
with self.lock:
self.comLog('> %s' % command)
reply = self._ppms_device.send(command)
self.log.debug("%s|%s", command, reply)
self.comLog("< %s", reply)
return reply
def doPoll(self):
self.read_data()
def read_data(self):
mask = 1 # always get packed_status
for channelname, channel in self.modules.items():
@ -128,37 +114,27 @@ class Main(Communicator):
return data # return data as string
class PpmsMixin:
class PpmsBase(HasIO, Readable):
"""common base for all ppms modules"""
value = Parameter(needscfg=False)
status = Parameter(needscfg=False)
iodev = Attached()
pollerClass = Poller
enabled = True # default, if no parameter enable is defined
_last_settings = None # used by several modules
slow_pollfactor = 1
# as this pollinterval affects only the polling of settings
# it would be confusing to export it.
pollinterval = Parameter('', FloatRange(), needscfg=False, export=False)
pollinterval = Parameter(export=False)
def initModule(self):
self._iodev.register(self)
super().initModule()
self.io.register(self)
def startModule(self, started_callback):
# no polls except on main module
started_callback()
def read_value(self):
def doPoll(self):
# polling is done by the main module
# and PPMS does not deliver really more fresh values when polled more often
return Done
def read_status(self):
# polling is done by the main module
# and PPMS does not deliver really fresh status values anyway: the status is not
# changed immediately after a target change!
return Done
pass
def update_value_status(self, value, packed_status):
# update value and status
@ -172,12 +148,18 @@ class PpmsMixin:
self.value = value
self.status = (self.Status.IDLE, '')
def comm_write(self, command):
"""write command and check if result is OK"""
reply = self.communicate(command)
if reply != 'OK':
raise HardwareError('bad reply %r to command %r' % (reply, command))
class Channel(PpmsMixin, HasIodev, Readable):
class Channel(PpmsBase):
"""channel base class"""
value = Parameter('main value of channels', poll=True)
enabled = Parameter('is this channel used?', readonly=False, poll=False,
value = Parameter('main value of channels')
enabled = Parameter('is this channel used?', readonly=False,
datatype=BoolType(), default=False)
channel = Property('channel name',
@ -186,26 +168,21 @@ class Channel(PpmsMixin, HasIodev, Readable):
datatype=IntRange(1, 4), export=False)
def earlyInit(self):
Readable.earlyInit(self)
super().earlyInit()
if not self.channel:
self.channel = self.name
def get_settings(self, pname):
return ''
class UserChannel(Channel):
"""user channel"""
# pollinterval = Parameter(visibility=3)
no = Property('channel number',
datatype=IntRange(0, 0), export=False, default=0)
linkenable = Property('name of linked channel for enabling',
datatype=StringType(), export=False, default='')
def write_enabled(self, enabled):
other = self._iodev.modules.get(self.linkenable, None)
other = self.io.modules.get(self.linkenable, None)
if other:
other.enabled = enabled
return enabled
@ -214,201 +191,172 @@ class UserChannel(Channel):
class DriverChannel(Channel):
"""driver channel"""
drvout = IOHandler('drvout', 'DRVOUT? %(no)d', '%d,%g,%g')
current = Parameter('driver current', readonly=False, handler=drvout,
current = Parameter('driver current', readonly=False,
datatype=FloatRange(0., 5000., unit='uA'))
powerlimit = Parameter('power limit', readonly=False, handler=drvout,
powerlimit = Parameter('power limit', readonly=False,
datatype=FloatRange(0., 1000., unit='uW'))
# pollinterval = Parameter(visibility=3)
def analyze_drvout(self, no, current, powerlimit):
param_names = 'current', 'powerlimit'
@CommonReadHandler(param_names)
def read_params(self):
no, self.current, self.powerlimit = literal_eval(
self.communicate('DRVOUT? %d' % self.no))
if self.no != no:
raise HardwareError('DRVOUT command: channel number in reply does not match')
return dict(current=current, powerlimit=powerlimit)
def change_drvout(self, change):
change.readValues()
return change.current, change.powerlimit
@CommonWriteHandler(param_names)
def write_params(self, values):
"""write parameters
:param values: a dict like object containing the parameters to be written
"""
self.read_params() # make sure parameters are up to date
self.comm_write('DRVOUT %(no)d,%(current)g,%(powerlimit)g' % values)
self.read_params() # read back
class BridgeChannel(Channel):
"""bridge channel"""
bridge = IOHandler('bridge', 'BRIDGE? %(no)d', '%d,%g,%g,%d,%d,%g')
# pylint: disable=invalid-name
ReadingMode = Enum('ReadingMode', standard=0, fast=1, highres=2)
enabled = Parameter(handler=bridge)
excitation = Parameter('excitation current', readonly=False, handler=bridge,
excitation = Parameter('excitation current', readonly=False,
datatype=FloatRange(0.01, 5000., unit='uA'))
powerlimit = Parameter('power limit', readonly=False, handler=bridge,
powerlimit = Parameter('power limit', readonly=False,
datatype=FloatRange(0.001, 1000., unit='uW'))
dcflag = Parameter('True when excitation is DC (else AC)', readonly=False, handler=bridge,
dcflag = Parameter('True when excitation is DC (else AC)', readonly=False,
datatype=BoolType())
readingmode = Parameter('reading mode', readonly=False, handler=bridge,
datatype=EnumType(ReadingMode))
voltagelimit = Parameter('voltage limit', readonly=False, handler=bridge,
readingmode = Parameter('reading mode', readonly=False,
datatype=EnumType(standard=0, fast=1, highres=2))
voltagelimit = Parameter('voltage limit', readonly=False,
datatype=FloatRange(0.0001, 100., unit='mV'))
# pollinterval = Parameter(visibility=3)
def analyze_bridge(self, no, excitation, powerlimit, dcflag, readingmode, voltagelimit):
param_names = 'enabled', 'enabled', 'powerlimit', 'dcflag', 'readingmode', 'voltagelimit'
@CommonReadHandler(param_names)
def read_params(self):
no, excitation, powerlimit, self.dcflag, self.readingmode, voltagelimit = literal_eval(
self.communicate('BRIDGE? %d' % self.no))
if self.no != no:
raise HardwareError('DRVOUT command: channel number in reply does not match')
return dict(
enabled=excitation != 0 and powerlimit != 0 and voltagelimit != 0,
excitation=excitation or self.excitation,
powerlimit=powerlimit or self.powerlimit,
dcflag=dcflag,
readingmode=readingmode,
voltagelimit=voltagelimit or self.voltagelimit,
)
self.enabled = excitation != 0 and powerlimit != 0 and voltagelimit != 0
if excitation:
self.excitation = excitation
if powerlimit:
self.powerlimit = powerlimit
if voltagelimit:
self.voltagelimit = voltagelimit
def change_bridge(self, change):
change.readValues()
if change.enabled:
return self.no, change.excitation, change.powerlimit, change.dcflag, change.readingmode, change.voltagelimit
return self.no, 0, 0, change.dcflag, change.readingmode, 0
@CommonWriteHandler(param_names)
def write_params(self, values):
"""write parameters
:param values: a dict like object containing the parameters to be written
"""
self.read_params() # make sure parameters are up to date
if not values['enabled']:
values['excitation'] = 0
values['powerlimit'] = 0
values['voltagelimit'] = 0
self.comm_write('BRIDGE %(no)d,%(enabled)g,%(powerlimit)g,%(dcflag)d,'
'%(readingmode)d,%(voltagelimit)g' % values)
self.read_params() # read back
class Level(PpmsMixin, HasIodev, Readable):
class Level(PpmsBase):
"""helium level"""
level = IOHandler('level', 'LEVEL?', '%g,%d')
value = Parameter(datatype=FloatRange(unit='%'), handler=level)
status = Parameter(handler=level)
# pollinterval = Parameter(visibility=3)
value = Parameter(datatype=FloatRange(unit='%'))
channel = 'level'
def doPoll(self):
self.read_value()
def update_value_status(self, value, packed_status):
pass
# must be a no-op
# when called from Main.read_data, value is always None
# value and status is polled via settings
def analyze_level(self, level, status):
def read_value(self):
# ignore 'old reading' state of the flag, as this happens only for a short time
# during measuring
return dict(value=level, status=(self.Status.IDLE, ''))
return literal_eval(self.communicate('LEVEL?'))[0]
class Chamber(PpmsMixin, HasIodev, Drivable):
class Chamber(PpmsBase, Drivable):
"""sample chamber handling
value is an Enum, which is redundant with the status text
"""
chamber = IOHandler('chamber', 'CHAMBER?', '%d')
Status = Drivable.Status
# pylint: disable=invalid-name
Operation = Enum(
'Operation',
seal_immediately=0,
purge_and_seal=1,
vent_and_seal=2,
pump_continuously=3,
vent_continuously=4,
hi_vacuum=5,
noop=10,
)
StatusCode = Enum(
'StatusCode',
unknown=0,
purged_and_sealed=1,
vented_and_sealed=2,
sealed_unknown=3,
purge_and_seal=4,
vent_and_seal=5,
pumping_down=6,
at_hi_vacuum=7,
pumping_continuously=8,
venting_continuously=9,
general_failure=15,
)
value = Parameter(description='chamber state', handler=chamber,
datatype=EnumType(StatusCode))
target = Parameter(description='chamber command', handler=chamber,
datatype=EnumType(Operation))
# pollinterval = Parameter(visibility=3)
STATUS_MAP = {
StatusCode.purged_and_sealed: (Status.IDLE, 'purged and sealed'),
StatusCode.vented_and_sealed: (Status.IDLE, 'vented and sealed'),
StatusCode.sealed_unknown: (Status.WARN, 'sealed unknown'),
StatusCode.purge_and_seal: (Status.BUSY, 'purge and seal'),
StatusCode.vent_and_seal: (Status.BUSY, 'vent and seal'),
StatusCode.pumping_down: (Status.BUSY, 'pumping down'),
StatusCode.at_hi_vacuum: (Status.IDLE, 'at hi vacuum'),
StatusCode.pumping_continuously: (Status.IDLE, 'pumping continuously'),
StatusCode.venting_continuously: (Status.IDLE, 'venting continuously'),
StatusCode.general_failure: (Status.ERROR, 'general failure'),
}
code_table = [
# valuecode, status, statusname, opcode, targetname
(0, Status.IDLE, 'unknown', 10, 'noop'),
(1, Status.IDLE, 'purged_and_sealed', 1, 'purge_and_seal'),
(2, Status.IDLE, 'vented_and_sealed', 2, 'vent_and_seal'),
(3, Status.WARN, 'sealed_unknown', 0, 'seal_immediately'),
(4, Status.BUSY, 'purge_and_seal', None, None),
(5, Status.BUSY, 'vent_and_seal', None, None),
(6, Status.BUSY, 'pumping_down', None, None),
(8, Status.IDLE, 'pumping_continuously', 3, 'pump_continuously'),
(9, Status.IDLE, 'venting_continuously', 4, 'vent_continuously'),
(15, Status.ERROR, 'general_failure', None, None),
]
value_codes = {k: v for v, _, k, _, _ in code_table}
target_codes = {k: v for v, _, _, _, k in code_table if k}
name2opcode = {k: v for _, _, _, v, k in code_table if k}
opcode2name = {v: k for _, _, _, v, k in code_table if k}
status_map = {v: (c, k.replace('_', ' ')) for v, c, k, _, _ in code_table}
value = Parameter(description='chamber state', datatype=EnumType(**value_codes), default=0)
target = Parameter(description='chamber command', datatype=EnumType(**target_codes), default='noop')
channel = 'chamber'
def update_value_status(self, value, packed_status):
status_code = (packed_status >> 8) & 0xf
if status_code in self.STATUS_MAP:
if status_code in self.status_map:
self.value = status_code
self.status = self.STATUS_MAP[status_code]
self.status = self.status_map[status_code]
else:
self.value = self.StatusCode.unknown
self.value = self.value_map['unknown']
self.status = (self.Status.ERROR, 'unknown status code %d' % status_code)
def analyze_chamber(self, target):
return dict(target=target)
def read_target(self):
opcode = int(self.communicate('CHAMBER?'))
return self.opcode2name[opcode]
def change_chamber(self, change):
# write settings, combining <pname>=<value> and current attributes
# and request updated settings
if change.target == self.Operation.noop:
return None
return (change.target,)
def write_target(self, value):
if value == self.target.noop:
return self.target.noop
opcode = self.name2opcode[self.target.enum(value).name]
assert self.communicate('CHAMBER %d' % opcode) == 'OK'
return self.read_target()
class Temp(PpmsMixin, HasIodev, Drivable):
class Temp(PpmsBase, Drivable):
"""temperature"""
temp = IOHandler('temp', 'TEMP?', '%g,%g,%d')
Status = Enum(
Drivable.Status,
RAMPING=370,
STABILIZING=380,
)
# pylint: disable=invalid-name
ApproachMode = Enum('ApproachMode', fast_settle=0, no_overshoot=1)
value = Parameter(datatype=FloatRange(unit='K'), poll=True)
status = Parameter(datatype=StatusType(Status), poll=True)
target = Parameter(datatype=FloatRange(1.7, 402.0, unit='K'), poll=False, needscfg=False)
value = Parameter(datatype=FloatRange(unit='K'))
status = Parameter(datatype=StatusType(Status))
target = Parameter(datatype=FloatRange(1.7, 402.0, unit='K'), needscfg=False)
setpoint = Parameter('intermediate set point',
datatype=FloatRange(1.7, 402.0, unit='K'), handler=temp)
datatype=FloatRange(1.7, 402.0, unit='K'))
ramp = Parameter('ramping speed', readonly=False, default=0,
datatype=FloatRange(0, 20, unit='K/min'))
workingramp = Parameter('intermediate ramp value',
datatype=FloatRange(0, 20, unit='K/min'), handler=temp)
approachmode = Parameter('how to approach target!', readonly=False, handler=temp,
datatype=EnumType(ApproachMode))
# pollinterval = Parameter(visibility=3)
datatype=FloatRange(0, 20, unit='K/min'), default=0)
approachmode = Parameter('how to approach target!', readonly=False,
datatype=EnumType(fast_settle=0, no_overshoot=1), default=0)
timeout = Parameter('drive timeout, in addition to ramp time', readonly=False,
datatype=FloatRange(0, unit='sec'), default=3600)
# pylint: disable=invalid-name
TempStatus = Enum(
'TempStatus',
stable_at_target=1,
changing=2,
within_tolerance=5,
outside_tolerance=6,
filling_emptying_reservoir=7,
standby=10,
control_disabled=13,
can_not_complete=14,
general_failure=15,
)
general_stop = Property('respect general stop', datatype=BoolType(),
default=True, value=False)
STATUS_MAP = {
1: (Status.IDLE, 'stable at target'),
2: (Status.RAMPING, 'ramping'),
@ -420,8 +368,6 @@ class Temp(PpmsMixin, HasIodev, Drivable):
14: (Status.ERROR, 'can not complete'),
15: (Status.ERROR, 'general failure'),
}
general_stop = Property('respect general stop', datatype=BoolType(),
default=True, value=False)
channel = 'temp'
_stopped = False
@ -432,6 +378,42 @@ class Temp(PpmsMixin, HasIodev, Drivable):
_wait_at10 = False
_ramp_at_limit = False
param_names = 'setpoint', 'workingramp', 'approachmode'
@CommonReadHandler(param_names)
def read_params(self):
settings = literal_eval(self.communicate('TEMP?'))
if settings == self._last_settings:
# update parameters only on change, as 'ramp' and 'approachmode' are
# not always sent to the hardware
return
self.setpoint, self.workingramp, self.approachmode = self._last_settings = settings
if self.setpoint != 10 or not self._wait_at10:
self.log.debug('read back target %g %r' % (self.setpoint, self._wait_at10))
self.target = self.setpoint
if self.workingramp != 2 or not self._ramp_at_limit:
self.log.debug('read back ramp %g %r' % (self.workingramp, self._ramp_at_limit))
self.ramp = self.workingramp
def _write_params(self, setpoint, ramp, approachmode):
wait_at10 = False
ramp_at_limit = False
if self.value > 11:
if setpoint <= 10:
wait_at10 = True
setpoint = 10
elif self.value > setpoint:
if ramp >= 2:
ramp = 2
ramp_at_limit = True
self._wait_at10 = wait_at10
self._ramp_at_limit = ramp_at_limit
self.calc_expected(setpoint, ramp)
self.log.debug(
'change_temp v %r s %r r %r w %r l %r' % (self.value, setpoint, ramp, wait_at10, ramp_at_limit))
self.comm_write('TEMP %g,%g,%d' % (setpoint, ramp, approachmode))
self.read_params()
def update_value_status(self, value, packed_status):
if value is None:
self.status = (self.Status.ERROR, 'invalid value')
@ -449,7 +431,7 @@ class Temp(PpmsMixin, HasIodev, Drivable):
if now > self._cool_deadline:
self._wait_at10 = False
self._last_change = now
self.temp.write(self, 'setpoint', self.target)
self._write_params(self.target, self.ramp, self.approachmode)
status = (self.Status.STABILIZING, 'waiting at 10 K')
if self._last_change: # there was a change, which is not yet confirmed by hw
if now > self._last_change + 5:
@ -478,41 +460,6 @@ class Temp(PpmsMixin, HasIodev, Drivable):
self._expected_target_time = 0
self.status = status
def analyze_temp(self, setpoint, workingramp, approachmode):
if (setpoint, workingramp, approachmode) == self._last_settings:
# update parameters only on change, as 'ramp' and 'approachmode' are
# not always sent to the hardware
return {}
self._last_settings = setpoint, workingramp, approachmode
if setpoint != 10 or not self._wait_at10:
self.log.debug('read back target %g %r' % (setpoint, self._wait_at10))
self.target = setpoint
if workingramp != 2 or not self._ramp_at_limit:
self.log.debug('read back ramp %g %r' % (workingramp, self._ramp_at_limit))
self.ramp = workingramp
result = dict(setpoint=setpoint, workingramp=workingramp)
self.log.debug('analyze_temp %r %r' % (result, (self.target, self.ramp)))
return result
def change_temp(self, change):
ramp = change.ramp
setpoint = change.setpoint
wait_at10 = False
ramp_at_limit = False
if self.value > 11:
if setpoint <= 10:
wait_at10 = True
setpoint = 10
elif self.value > setpoint:
if ramp >= 2:
ramp = 2
ramp_at_limit = True
self._wait_at10 = wait_at10
self._ramp_at_limit = ramp_at_limit
self.calc_expected(setpoint, ramp)
self.log.debug('change_temp v %r s %r r %r w %r l %r' % (self.value, setpoint, ramp, wait_at10, ramp_at_limit))
return setpoint, ramp, change.approachmode
def write_target(self, target):
self._stopped = False
if abs(self.target - self.value) <= 2e-5 * target and target == self.target:
@ -520,23 +467,23 @@ class Temp(PpmsMixin, HasIodev, Drivable):
self._status_before_change = self.status
self.status = (self.Status.BUSY, 'changed target')
self._last_change = time.time()
self.temp.write(self, 'setpoint', target)
self._write_params(target, self.ramp, self.approachmode)
self.log.debug('write_target %s' % repr((self.setpoint, target, self._wait_at10)))
return target
def write_approachmode(self, value):
if self.isDriving():
self.temp.write(self, 'approachmode', value)
self._write_params(self.setpoint, self.ramp, value)
return Done
self.approachmode = value
return None # do not execute TEMP command, as this would trigger an unnecessary T change
return Done # do not execute TEMP command, as this would trigger an unnecessary T change
def write_ramp(self, value):
if self.isDriving():
self.temp.write(self, 'ramp', value)
self._write_params(self.setpoint, value, self.approachmode)
return Done
# self.ramp = value
return None # do not execute TEMP command, as this would trigger an unnecessary T change
self.ramp = value
return Done # do not execute TEMP command, as this would trigger an unnecessary T change
def calc_expected(self, target, ramp):
self._expected_target_time = time.time() + abs(target - self.value) * 60.0 / max(0.1, ramp)
@ -554,10 +501,9 @@ class Temp(PpmsMixin, HasIodev, Drivable):
self._stopped = True
class Field(PpmsMixin, HasIodev, Drivable):
class Field(PpmsBase, Drivable):
"""magnetic field"""
field = IOHandler('field', 'FIELD?', '%g,%g,%d,%d')
Status = Enum(
Drivable.Status,
PREPARED=150,
@ -566,20 +512,15 @@ class Field(PpmsMixin, HasIodev, Drivable):
STABILIZING=380,
FINALIZING=390,
)
# pylint: disable=invalid-name
PersistentMode = Enum('PersistentMode', persistent=0, driven=1)
ApproachMode = Enum('ApproachMode', linear=0, no_overshoot=1, oscillate=2)
value = Parameter(datatype=FloatRange(unit='T'), poll=True)
status = Parameter(datatype=StatusType(Status), poll=True)
target = Parameter(datatype=FloatRange(-15, 15, unit='T'), handler=field)
ramp = Parameter('ramping speed', readonly=False, handler=field,
datatype=FloatRange(0.064, 1.19, unit='T/min'))
approachmode = Parameter('how to approach target', readonly=False, handler=field,
datatype=EnumType(ApproachMode))
persistentmode = Parameter('what to do after changing field', readonly=False, handler=field,
datatype=EnumType(PersistentMode))
# pollinterval = Parameter(visibility=3)
value = Parameter(datatype=FloatRange(unit='T'))
status = Parameter(datatype=StatusType(Status))
target = Parameter(datatype=FloatRange(-15, 15, unit='T')) # poll only one parameter
ramp = Parameter('ramping speed', readonly=False,
datatype=FloatRange(0.064, 1.19, unit='T/min'), default=0.19)
approachmode = Parameter('how to approach target', readonly=False,
datatype=EnumType(linear=0, no_overshoot=1, oscillate=2), default=0)
persistentmode = Parameter('what to do after changing field', readonly=False,
datatype=EnumType(persistent=0, driven=1), default=0)
STATUS_MAP = {
1: (Status.IDLE, 'persistent mode'),
@ -599,6 +540,25 @@ class Field(PpmsMixin, HasIodev, Drivable):
_last_target = None # last reached target
_last_change = 0 # means no target change is pending
param_names = 'target', 'ramp', 'approachmode', 'persistentmode'
@CommonReadHandler(param_names)
def read_params(self):
settings = literal_eval(self.communicate('FIELD?'))
# print('last_settings tt %s' % repr(self._last_settings))
if settings == self._last_settings:
# we update parameters only on change, as 'ramp' and 'approachmode' are
# not always sent to the hardware
return
target, ramp, self.approachmode, self.persistentmode = self._last_settings = settings
self.target = round(target * 1e-4, 7)
self.ramp = ramp * 6e-3
def _write_params(self, target, ramp, approachmode, persistentmode):
self.comm_write('FIELD %g,%g,%d,%d' % (
target * 1e+4, ramp / 6e-3, approachmode, persistentmode))
self.read_params()
def update_value_status(self, value, packed_status):
if value is None:
self.status = (self.Status.ERROR, 'invalid value')
@ -633,19 +593,6 @@ class Field(PpmsMixin, HasIodev, Drivable):
status = (status[0], 'stopping (%s)' % status[1])
self.status = status
def analyze_field(self, target, ramp, approachmode, persistentmode):
# print('last_settings tt %s' % repr(self._last_settings))
if (target, ramp, approachmode, persistentmode) == self._last_settings:
# we update parameters only on change, as 'ramp' and 'approachmode' are
# not always sent to the hardware
return {}
self._last_settings = target, ramp, approachmode, persistentmode
return dict(target=round(target * 1e-4, 7), ramp=ramp * 6e-3, approachmode=approachmode,
persistentmode=persistentmode)
def change_field(self, change):
return change.target * 1e+4, change.ramp / 6e-3, change.approachmode, change.persistentmode
def write_target(self, target):
if abs(self.target - self.value) <= 2e-5 and target == self.target:
self.target = target
@ -654,7 +601,7 @@ class Field(PpmsMixin, HasIodev, Drivable):
self._stopped = False
self._last_change = time.time()
self.status = (self.Status.BUSY, 'changed target')
self.field.write(self, 'target', target)
self._write_params(target, self.ramp, self.approachmode, self.persistentmode)
return Done
def write_persistentmode(self, mode):
@ -665,19 +612,19 @@ class Field(PpmsMixin, HasIodev, Drivable):
self._status_before_change = self.status
self._stopped = False
self.status = (self.Status.BUSY, 'changed persistent mode')
self.field.write(self, 'persistentmode', mode)
self._write_params(self.target, self.ramp, self.approachmode, mode)
return Done
def write_ramp(self, value):
self.ramp = value
if self.isDriving():
self.field.write(self, 'ramp', value)
self._write_params(self.target, value, self.approachmode, self.persistentmode)
return Done
return None # do not execute FIELD command, as this would trigger a ramp up of leads current
def write_approachmode(self, value):
if self.isDriving():
self.field.write(self, 'approachmode', value)
self._write_params(self.target, self.ramp, value, self.persistentmode)
return Done
return None # do not execute FIELD command, as this would trigger a ramp up of leads current
@ -692,20 +639,17 @@ class Field(PpmsMixin, HasIodev, Drivable):
self._stopped = True
class Position(PpmsMixin, HasIodev, Drivable):
class Position(PpmsBase, Drivable):
"""rotator position"""
move = IOHandler('move', 'MOVE?', '%g,%g,%g')
Status = Drivable.Status
value = Parameter(datatype=FloatRange(unit='deg'), poll=True)
target = Parameter(datatype=FloatRange(-720., 720., unit='deg'), handler=move)
enabled = Parameter('is this channel used?', readonly=False, poll=False,
value = Parameter(datatype=FloatRange(unit='deg'))
target = Parameter(datatype=FloatRange(-720., 720., unit='deg'))
enabled = Parameter('is this channel used?', readonly=False,
datatype=BoolType(), default=True)
speed = Parameter('motor speed', readonly=False, handler=move,
speed = Parameter('motor speed', readonly=False, default=12,
datatype=FloatRange(0.8, 12, unit='deg/sec'))
# pollinterval = Parameter(visibility=3)
STATUS_MAP = {
1: (Status.IDLE, 'at target'),
5: (Status.BUSY, 'moving'),
@ -720,6 +664,23 @@ class Position(PpmsMixin, HasIodev, Drivable):
_last_change = 0
_within_target = 0 # time since we are within target
param_names = 'target', 'speed'
@CommonReadHandler(param_names)
def read_params(self):
settings = literal_eval(self.communicate('MOVE?'))
if settings == self._last_settings:
# we update parameters only on change, as 'speed' is
# not always sent to the hardware
return
self.target, _, speed = self._last_settings = settings
self.speed = (15 - speed) * 0.8
def _write_params(self, target, speed):
speed = int(round(min(14, max(0, 15 - speed / 0.8)), 0))
self.comm_write('MOVE %g,%d,%d' % (target, 0, speed))
return self.read_params()
def update_value_status(self, value, packed_status):
if not self.enabled:
self.status = (self.Status.DISABLED, 'disabled')
@ -757,29 +718,17 @@ class Position(PpmsMixin, HasIodev, Drivable):
status = (status[0], 'stopping (%s)' % status[1])
self.status = status
def analyze_move(self, target, mode, speed):
if (target, speed) == self._last_settings:
# we update parameters only on change, as 'speed' is
# not always sent to the hardware
return {}
self._last_settings = target, speed
return dict(target=target, speed=(15 - speed) * 0.8)
def change_move(self, change):
speed = int(round(min(14, max(0, 15 - change.speed / 0.8)), 0))
return change.target, 0, speed
def write_target(self, target):
self._stopped = False
self._last_change = 0
self._status_before_change = self.status
self.status = (self.Status.BUSY, 'changed target')
self.move.write(self, 'target', target)
self._write_params(target, self.speed)
return Done
def write_speed(self, value):
if self.isDriving():
self.move.write(self, 'speed', value)
self._write_params(self.target, value)
return Done
self.speed = value
return None # do not execute MOVE command, as this would trigger an unnecessary move

View File

@ -26,6 +26,7 @@ import time
def num(string):
return json.loads(string)
class NamedList:
def __init__(self, keys, *args, **kwargs):
self.__keys__ = keys.split()
@ -49,8 +50,10 @@ class NamedList:
def __repr__(self):
return ",".join("%.7g" % val for val in self.aslist())
class PpmsSim:
CHANNELS = 'st t mf pos r1 i1 r2 i2'.split()
def __init__(self):
self.status = NamedList('t mf ch pos', 1, 1, 1, 1)
self.st = 0x1111
@ -176,7 +179,6 @@ class PpmsSim:
if abs(self.t - self.temp.target) < 1:
self.status.t = 6 # outside tolerance
if abs(self.pos - self.move.target) < 0.01:
self.status.pos = 1
else:
@ -188,7 +190,6 @@ class PpmsSim:
self.r2 = 1000 / self.t
self.i2 = math.log(self.t)
self.level.value = 100 - (self.time - self.start) * 0.01 % 100
# print('PROGRESS T=%.7g B=%.7g x=%.7g' % (self.t, self.mf, self.pos))
def getdat(self, mask):
mask = int(mask) & 0xff # all channels up to i2
@ -198,6 +199,7 @@ class PpmsSim:
output.append("%.7g" % getattr(self, chan))
return ",".join(output)
class QDevice:
def __init__(self, classid):
self.sim = PpmsSim()
@ -225,5 +227,6 @@ class QDevice:
result = "OK"
return result
def shutdown():
pass

View File

@ -41,7 +41,7 @@ from secop.client import ProxyClient
from secop.datatypes import ArrayOf, BoolType, \
EnumType, FloatRange, IntRange, StringType
from secop.errors import ConfigError, HardwareError, secop_error, NoSuchModuleError
from secop.lib import getGeneralConfig, mkthread
from secop.lib import getGeneralConfig, mkthread, formatExtendedStack
from secop.lib.asynconn import AsynConn, ConnectionClosed
from secop.modules import Attached, Command, Done, Drivable, \
Module, Parameter, Property, Readable, Writable
@ -124,6 +124,7 @@ class SeaClient(ProxyClient, Module):
if config:
self.default_json_file[name] = config.split()[0] + '.json'
self.io = None
self.asyncio = None
ProxyClient.__init__(self)
Module.__init__(self, name, log, opts, srv)
@ -153,7 +154,7 @@ class SeaClient(ProxyClient, Module):
"""send a request and wait for reply"""
with self._write_lock:
if not self.io or not self.io.connection:
if not self.asyncio.connection:
if not self.asyncio or not self.asyncio.connection:
self._connect(None)
self.io = AsynConn(self.uri)
assert self.io.readline() == b'OK'
@ -314,12 +315,14 @@ class SeaConfigCreator(SeaClient):
stripped, _, ext = filename.rpartition('.')
service = SERVICE_NAMES[ext]
seaconn = 'sea_' + service
with open(join(seaconfdir, stripped + '.cfg'), 'w') as fp:
cfgfile = join(seaconfdir, stripped + '.cfg')
with open(cfgfile, 'w') as fp:
fp.write(CFG_HEADER % dict(config=filename, seaconn=seaconn, service=service,
nodedescr=description.get(filename, filename)))
for obj in descr:
fp.write(CFG_MODULE % dict(modcls=modcls[obj], module=obj, seaconn=seaconn))
content = json.dumps(descr).replace('}, {', '},\n{').replace('[{', '[\n{').replace('}]}, ', '}]},\n\n')
result.append('%s\n' % cfgfile)
with open(join(seaconfdir, filename + '.json'), 'w') as fp:
fp.write(content + '\n')
result.append('%s: %s' % (filename, ','.join(n for n in descr)))
@ -495,7 +498,8 @@ class SeaModule(Module):
if key in cls.accessibles:
if key == 'target':
kwds['readonly'] = False
pobj = cls.accessibles[key].override(**kwds)
pobj = cls.accessibles[key]
pobj.init(kwds)
datatype = kwds.get('datatype', cls.accessibles[key].datatype)
else:
pobj = Parameter(**kwds)
@ -542,12 +546,17 @@ class SeaModule(Module):
# create standard parameters like value and status, if not yet there
for pname, pobj in cls.accessibles.items():
if pname == 'pollinterval':
attributes[pname] = pobj.override(export=False)
pobj.export = False
attributes[pname] = pobj
pobj.__set_name__(cls, pname)
elif pname not in attributes and isinstance(pobj, Parameter):
attributes[pname] = pobj.override(poll=False, needscfg=False)
pobj.poll = False
pobj.needscfg = False
attributes[pname] = pobj
pobj.__set_name__(cls, pname)
classname = '%s_%s' % (cls.__name__, sea_object)
# print(name, attributes)
attributes['pollerClass'] = None
newcls = type(classname, (cls,), attributes)
return Module.__new__(newcls)
@ -640,5 +649,11 @@ class SeaDrivable(SeaModule, Drivable):
if value is not None:
self.target = value
@Command()
def stop(self):
"""propagate to SEA
- on stdsct drivables this will call the halt script
- on EaseDriv this will set the stopped state
"""
self._iodev.query('%s is_running 0' % self.sea_object)

View File

@ -22,22 +22,28 @@
"""simulated transducer DPM3 read out"""
import random
import math
from secop.core import Readable, Parameter, FloatRange, Attached
from secop.lib import clamp
class DPM3(Readable):
motor = Attached()
jitter = Parameter('simulated jitter', FloatRange(unit='N'), default=1, readonly=False)
jitter = Parameter('simulated jitter', FloatRange(unit='N'), default=0.1, readonly=False)
hysteresis = Parameter('simulated hysteresis', FloatRange(unit='deg'), default=100, readonly=False)
friction = Parameter('friction', FloatRange(unit='N/deg'), default=1, readonly=False)
slope = Parameter('slope', FloatRange(unit='N/deg'), default=10, readonly=False)
friction = Parameter('friction', FloatRange(unit='N/deg'), default=2.5, readonly=False)
slope = Parameter('slope', FloatRange(unit='N/deg'), default=0.5, readonly=False)
offset = Parameter('offset', FloatRange(unit='N'), default=0, readonly=False)
_pos = 0 # effective piston position, main hysteresis taken into account
_pos = 0
def read_value(self):
mot = self._motor
self._pos = clamp(self._pos, mot.value - self.hysteresis * 0.5, mot.value + self.hysteresis * 0.5)
return clamp(0, 1, mot.value - self._pos) * self.friction \
+ self._pos * self.slope + self.jitter * (random.random() - random.random())
d = self.friction * self.slope
self._pos = clamp(self._pos, mot.value - d, mot.value + d)
f = (mot.value - self._pos) / self.slope
if mot.value > 0:
f = max(f, (mot.value - self.hysteresis) / self.slope)
else:
f = min(f, (mot.value + self.hysteresis) / self.slope)
return f + self.jitter * (random.random() - random.random()) * 0.5

View File

@ -22,7 +22,7 @@
import math
import os
from os.path import basename, exists, join
from os.path import basename, dirname, exists, join
import numpy as np
from scipy.interpolate import splev, splrep # pylint: disable=import-error
@ -109,7 +109,9 @@ class CalCurve:
calibname = sensopt.pop(0)
_, dot, ext = basename(calibname).rpartition('.')
kind = None
for path in os.environ.get('FRAPPY_CALIB_PATH', '').split(','):
pathlist = os.environ.get('FRAPPY_CALIB_PATH', '').split(',')
pathlist.append(join(dirname(__file__), 'calcurves'))
for path in pathlist:
# first try without adding kind
filename = join(path.strip(), calibname)
if exists(filename):
@ -145,7 +147,7 @@ class CalCurve:
for line in f:
parser.parse(line)
except Exception as e:
raise ValueError('calib curve %s: %s' % (calibspec, e))
raise ValueError('calib curve %s: %s' % (calibspec, e)) from e
self.convert_x = nplog if parser.logx else linear
self.convert_y = npexp if parser.logy else linear
x = np.asarray(parser.xdata)
@ -157,8 +159,8 @@ class CalCurve:
raise ValueError('calib curve %s is not monotonic' % calibspec)
try:
self.spline = splrep(x, y, s=0, k=min(3, len(x) - 1))
except (ValueError, TypeError):
raise ValueError('invalid calib curve %s' % calibspec)
except (ValueError, TypeError) as e:
raise ValueError('invalid calib curve %s' % calibspec) from e
def __call__(self, value):
"""convert value
@ -178,8 +180,9 @@ class Sensor(Readable):
pollinterval = Parameter(export=False)
status = Parameter(default=(Readable.Status.ERROR, 'unintialized'))
pollerClass = None
description = 'a calibrated sensor value'
_value_error = None
enablePoll = False
def checkProperties(self):
if 'description' not in self.propertyValues:
@ -187,6 +190,7 @@ class Sensor(Readable):
super().checkProperties()
def initModule(self):
super().initModule()
self._rawsensor.registerCallbacks(self, ['status']) # auto update status
self._calib = CalCurve(self.calib)
if self.description == '_':

View File

@ -24,13 +24,12 @@
import time
import struct
from math import log10
from secop.core import BoolType, Command, EnumType, FloatRange, IntRange, \
HasIodev, Parameter, Property, Drivable, PersistentMixin, PersistentParam
HasIO, Parameter, Property, Drivable, PersistentMixin, PersistentParam, Done
from secop.io import BytesIO
from secop.errors import CommunicationFailedError, HardwareError, BadValueError, IsBusyError
from secop.rwhandler import ReadHandler, WriteHandler
MOTOR_STOP = 3
MOVE = 4
@ -50,84 +49,78 @@ MAX_SPEED = 2047 * SPEED_SCALE
ACCEL_SCALE = 1E12 / 2 ** 31 * ANGLE_SCALE
MAX_ACCEL = 2047 * ACCEL_SCALE
CURRENT_SCALE = 2.8/250
ENCODER_RESOLUTION = 0.4 # 365 / 1024, rounded up
ENCODER_RESOLUTION = 360 / 1024
HW_ARGS = {
# <parameter name>: (address, scale factor)
'encoder_tolerance': (212, ANGLE_SCALE),
'speed': (4, SPEED_SCALE),
'minspeed': (130, SPEED_SCALE),
'currentspeed': (3, SPEED_SCALE),
'maxcurrent': (6, CURRENT_SCALE),
'standby_current': (7, CURRENT_SCALE,),
'acceleration': (5, ACCEL_SCALE),
'target_reached': (8, 1),
'move_status': (207, 1),
'error_bits': (208, 1),
'free_wheeling': (204, 0.01),
'power_down_delay': (214, 0.01),
}
# special handling (adjust zero):
ENCODER_ADR = 209
STEPPOS_ADR = 1
class HwParam(PersistentParam):
adr = Property('parameter address', IntRange(0, 255), export=False)
scale = Property('scale factor (physical value / unit)', FloatRange(), export=False)
def __init__(self, description, datatype, adr, scale=1, poll=True,
readonly=True, persistent=None, **kwds):
"""hardware parameter"""
if persistent is None:
persistent = not readonly
if isinstance(datatype, FloatRange) and datatype.fmtstr == '%g':
datatype.fmtstr = '%%.%df' % max(0, 1 - int(log10(scale) + 0.01))
super().__init__(description, datatype, poll=poll, adr=adr, scale=scale,
persistent=persistent, readonly=readonly, **kwds)
def copy(self):
res = HwParam(self.description, self.datatype.copy(), self.adr)
res.name = self.name
res.init(self.propertyValues)
return res
def writable(*args, **kwds):
"""convenience function to create writable hardware parameters"""
return PersistentParam(*args, readonly=False, initwrite=True, **kwds)
class Motor(PersistentMixin, HasIodev, Drivable):
class Motor(PersistentMixin, HasIO, Drivable):
address = Property('module address', IntRange(0, 255), default=1)
value = Parameter('motor position', FloatRange(unit='deg', fmtstr='%.3f'), poll=False, default=0) # polling by read_status
value = Parameter('motor position', FloatRange(unit='deg', fmtstr='%.3f'))
zero = PersistentParam('zero point', FloatRange(unit='$'), readonly=False, default=0)
encoder = HwParam('encoder reading', FloatRange(unit='$', fmtstr='%.1f'),
209, ANGLE_SCALE, readonly=True, initwrite=False, persistent=True)
steppos = HwParam('position from motor steps', FloatRange(unit='$'),
1, ANGLE_SCALE, readonly=True, initwrite=False)
target = Parameter('_', FloatRange(unit='$'), default=0)
encoder = PersistentParam('encoder reading', FloatRange(unit='$', fmtstr='%.1f'),
readonly=True, initwrite=False)
steppos = PersistentParam('position from motor steps', FloatRange(unit='$', fmtstr='%.3f'),
readonly=True, initwrite=False)
target = Parameter('', FloatRange(unit='$'), default=0)
move_limit = Parameter('max. angle to drive in one go when current above safe_current', FloatRange(unit='$'),
readonly=False, default=5, group='more')
safe_current = Parameter('motor current allowed for big steps', FloatRange(unit='A'),
readonly=False, default=0.2, group='more')
move_limit = Parameter('max. angle to drive in one go', FloatRange(unit='$'),
readonly=False, default=360, group='more')
tolerance = Parameter('positioning tolerance', FloatRange(unit='$'),
readonly=False, default=0.9)
encoder_tolerance = HwParam('the allowed deviation between steppos and encoder\n\nmust be > tolerance',
FloatRange(0, 360., unit='$'),
212, ANGLE_SCALE, readonly=False, group='more')
speed = HwParam('max. speed', FloatRange(0, MAX_SPEED, unit='$/sec'),
4, SPEED_SCALE, readonly=False, group='motorparam')
minspeed = HwParam('min. speed', FloatRange(0, MAX_SPEED, unit='$/sec'),
130, SPEED_SCALE, readonly=False, default=SPEED_SCALE, group='motorparam')
currentspeed = HwParam('current speed', FloatRange(-MAX_SPEED, MAX_SPEED, unit='$/sec'),
3, SPEED_SCALE, readonly=True, group='motorparam')
maxcurrent = HwParam('_', FloatRange(0, 2.8, unit='A'),
6, CURRENT_SCALE, readonly=False, group='motorparam')
standby_current = HwParam('_', FloatRange(0, 2.8, unit='A'),
7, CURRENT_SCALE, readonly=False, group='motorparam')
acceleration = HwParam('_', FloatRange(4.6 * ACCEL_SCALE, MAX_ACCEL, unit='deg/s^2'),
5, ACCEL_SCALE, readonly=False, group='motorparam')
target_reached = HwParam('_', BoolType(), 8, group='hwstatus')
move_status = HwParam('_', IntRange(0, 3),
207, readonly=True, group='hwstatus')
error_bits = HwParam('_', IntRange(0, 255),
208, readonly=True, group='hwstatus')
# the doc says msec, but I believe the scale is 10 msec
free_wheeling = HwParam('_', FloatRange(0, 60., unit='sec'),
204, 0.01, default=0.1, readonly=False, group='motorparam')
power_down_delay = HwParam('_', FloatRange(0, 60., unit='sec'),
214, 0.01, default=0.1, readonly=False, group='motorparam')
baudrate = Parameter('_', EnumType({'%d' % v: i for i, v in enumerate(BAUDRATES)}),
readonly=False, default=0, poll=True, visibility=3, group='more')
encoder_tolerance = writable('the allowed deviation between steppos and encoder\n\nmust be > tolerance',
FloatRange(0, 360., unit='$', fmtstr='%.3f'), group='more')
speed = writable('max. speed', FloatRange(0, MAX_SPEED, unit='$/sec', fmtstr='%.1f'), default=40)
minspeed = writable('min. speed', FloatRange(0, MAX_SPEED, unit='$/sec', fmtstr='%.1f'),
default=SPEED_SCALE, group='motorparam')
currentspeed = Parameter('current speed', FloatRange(-MAX_SPEED, MAX_SPEED, unit='$/sec', fmtstr='%.1f'),
group='motorparam')
maxcurrent = writable('', FloatRange(0, 2.8, unit='A', fmtstr='%.2f'),
default=1.4, group='motorparam')
standby_current = writable('', FloatRange(0, 2.8, unit='A', fmtstr='%.2f'),
default=0.1, group='motorparam')
acceleration = writable('', FloatRange(4.6 * ACCEL_SCALE, MAX_ACCEL, unit='deg/s^2', fmtstr='%.1f'),
default=150., group='motorparam')
target_reached = Parameter('', BoolType(), group='hwstatus')
move_status = Parameter('', IntRange(0, 3), group='hwstatus')
error_bits = Parameter('', IntRange(0, 255), group='hwstatus')
free_wheeling = writable('', FloatRange(0, 60., unit='sec', fmtstr='%.2f'),
default=0.1, group='motorparam')
power_down_delay = writable('', FloatRange(0, 60., unit='sec', fmtstr='%.2f'),
default=0.1, group='motorparam')
baudrate = Parameter('', EnumType({'%d' % v: i for i, v in enumerate(BAUDRATES)}),
readonly=False, default=0, visibility=3, group='more')
pollinterval = Parameter(group='more')
iodevClass = BytesIO
# fast_pollfactor = 0.001 # poll as fast as possible when busy
fast_pollfactor = 0.05
ioClass = BytesIO
fast_pollfactor = 0.001 # not used any more, TODO: use a statemachine for running
_started = 0
_calc_timeout = True
_calcTimeout = True
_need_reset = None
_last_change = 0
def comm(self, cmd, adr, value=0, bank=0):
"""set or get a parameter
@ -138,21 +131,23 @@ class Motor(PersistentMixin, HasIodev, Drivable):
:param value: if given, the parameter is written, else it is returned
:return: the returned value
"""
if self._calc_timeout:
self._calc_timeout = False
baudrate = getattr(self._iodev._conn.connection, 'baudrate', None)
if self._calcTimeout and self.io._conn:
self._calcTimeout = False
baudrate = getattr(self.io._conn.connection, 'baudrate', None)
if baudrate:
if baudrate not in BAUDRATES:
raise CommunicationFailedError('unsupported baud rate: %d' % baudrate)
self._iodev.timeout = 0.03 + 200 / baudrate
self.io.timeout = 0.03 + 200 / baudrate
exc = None
byt = struct.pack('>BBBBi', self.address, cmd, adr, bank, round(value))
byt += bytes([sum(byt) & 0xff])
for itry in range(3,0,-1):
byt = struct.pack('>BBBBi', self.address, cmd, adr, bank, round(value))
try:
reply = self._iodev.communicate(byt + bytes([sum(byt) & 0xff]), 9)
reply = self.communicate(byt, 9)
if sum(reply[:-1]) & 0xff != reply[-1]:
raise CommunicationFailedError('checksum error')
# will try again
except Exception as e:
if itry == 1:
raise
@ -168,36 +163,18 @@ class Motor(PersistentMixin, HasIodev, Drivable):
raise CommunicationFailedError('bad reply %r to command %s %d' % (reply, cmd, adr))
return result
def get(self, pname, **kwds):
"""get parameter"""
pobj = self.parameters[pname]
value = self.comm(GET_AXIS_PAR, pobj.adr, **kwds)
# do not apply scale when 1 (datatype might not be float)
return value if pobj.scale == 1 else value * pobj.scale
def startModule(self, start_events):
super().startModule(start_events)
def set(self, pname, value, check=True, **kwds):
"""set parameter and check result"""
pobj = self.parameters[pname]
scale = pobj.scale
rawvalue = round(value / scale)
for itry in range(2):
self.comm(SET_AXIS_PAR, pobj.adr, rawvalue, **kwds)
if check:
result = self.comm(GET_AXIS_PAR, pobj.adr, **kwds)
if result != rawvalue:
self.log.warning('result for %s does not match %d != %d, try again', pname, result, rawvalue)
continue
value = result * scale
return value
else:
raise HardwareError('result for %s does not match %d != %d' % (pname, result, rawvalue))
def fix_encoder(self=self):
try:
# get encoder value from motor. at this stage self.encoder contains the persistent value
encoder = self._read_axispar(ENCODER_ADR, ANGLE_SCALE) + self.zero
self.fix_encoder(encoder)
except Exception as e:
self.log.error('fix_encoder failed with %r', e)
def startModule(self, started_callback):
# get encoder value from motor. at this stage self.encoder contains the persistent value
encoder = self.get('encoder')
encoder += self.zero
self.fix_encoder(encoder)
super().startModule(started_callback)
start_events.queue(fix_encoder)
def fix_encoder(self, encoder_from_hw):
"""fix encoder value
@ -211,14 +188,52 @@ class Motor(PersistentMixin, HasIodev, Drivable):
# calculate nearest, most probable value
adjusted_encoder = encoder_from_hw + round((self.encoder - encoder_from_hw) / 360.) * 360
if abs(self.encoder - adjusted_encoder) >= self.encoder_tolerance:
# encoder module0 360 has changed
# encoder modulo 360 has changed
self.log.error('saved encoder value (%.2f) does not match reading (%.2f %.2f)',
self.encoder, encoder_from_hw, adjusted_encoder)
if adjusted_encoder != encoder_from_hw:
self.log.info('take next closest encoder value (%.2f)' % adjusted_encoder)
self._need_reset = True
self.status = self.Status.ERROR, 'saved encoder value does not match reading'
self.set('encoder', adjusted_encoder - self.zero, check=False)
self._write_axispar(adjusted_encoder - self.zero, ENCODER_ADR, ANGLE_SCALE, readback=False)
def _read_axispar(self, adr, scale=1):
value = self.comm(GET_AXIS_PAR, adr)
# do not apply scale when 1 (datatype might not be float)
return value if scale == 1 else value * scale
def _write_axispar(self, value, adr, scale=1, readback=True):
rawvalue = round(value / scale)
self.comm(SET_AXIS_PAR, adr, rawvalue)
if readback:
result = self.comm(GET_AXIS_PAR, adr)
if result != rawvalue:
raise HardwareError('result for adr=%d scale=%g does not match %g != %g'
% (adr, scale, result * scale, value))
return result * scale
return rawvalue * scale
@ReadHandler(HW_ARGS)
def read_hwparam(self, pname):
"""handle read for HwParam"""
args = HW_ARGS[pname]
reply = self._read_axispar(*args)
try:
value = getattr(self, pname)
except Exception:
return reply
if reply != value:
if not self.parameters[pname].readonly:
# this should not happen
self.log.warning('hw parameter %s has changed from %r to %r, write again', pname, value, reply)
self._write_axispar(value, *args, readback=False)
reply = self._read_axispar(*args)
return reply
@WriteHandler(HW_ARGS)
def write_hwparam(self, pname, value):
"""handler write for HwParam"""
return self._write_axispar(value, *HW_ARGS[pname])
def read_value(self):
encoder = self.read_encoder()
@ -240,7 +255,7 @@ class Motor(PersistentMixin, HasIodev, Drivable):
self._need_reset = True
self.status = self.Status.ERROR, 'power loss'
# or should we just fix instead of error status?
# self.set('steppos', self.steppos - self.zero, check=False)
# self._write_axispar(self.steppos - self.zero, readback=False)
self.comm(SET_GLOB_PAR, 255, 1, bank=2) # set initialized flag
self._started = 0
@ -256,13 +271,10 @@ class Motor(PersistentMixin, HasIodev, Drivable):
self.log.error('encoder (%.2f) does not match internal pos (%.2f)', self.encoder, self.steppos)
return self.Status.ERROR, 'encoder does not match internal pos'
return self.status
now = self.parameters['steppos'].timestamp
if self.steppos != oldpos:
self._last_change = now
return self.Status.BUSY, 'moving'
if now < self._last_change + 0.2 and not (self.read_target_reached() or self.read_move_status()
or self.read_error_bits()):
if oldpos != self.steppos or not (self.read_target_reached() or self.read_move_status()
or self.read_error_bits()):
return self.Status.BUSY, 'moving'
# TODO: handle the different errors from move_status and error_bits
diff = self.target - self.encoder
if abs(diff) <= self.tolerance:
self._started = 0
@ -273,9 +285,8 @@ class Motor(PersistentMixin, HasIodev, Drivable):
def write_target(self, target):
self.read_value() # make sure encoder and steppos are fresh
if self.maxcurrent >= self.safe_current + CURRENT_SCALE and (
abs(target - self.encoder) > self.move_limit + self.tolerance):
raise BadValueError('can not move more than %s deg %g %g' % (self.move_limit, self.encoder, target))
if abs(target - self.encoder) > self.move_limit:
raise BadValueError('can not move more than %s deg' % self.move_limit)
diff = self.encoder - self.steppos
if self._need_reset:
raise HardwareError('need reset (%s)' % self.status[1])
@ -284,90 +295,28 @@ class Motor(PersistentMixin, HasIodev, Drivable):
self._need_reset = True
self.status = self.Status.ERROR, 'encoder does not match internal pos'
raise HardwareError('need reset (encoder does not match internal pos)')
self.set('steppos', self.encoder - self.zero, check=False)
self._write_axispar(self.encoder - self.zero, STEPPOS_ADR, ANGLE_SCALE)
self._started = time.time()
self.log.debug('move to %.1f', target)
self.log.info('move to %.1f', target)
self.comm(MOVE, 0, (target - self.zero) / ANGLE_SCALE)
self.status = self.Status.BUSY, 'changed target'
return target
def write_zero(self, value):
diff = value - self.zero
self.encoder += diff
self.steppos += diff
self.value += diff
return value
self.zero = value
self.read_value() # apply zero to encoder, steppos and value
return Done
def read_encoder(self):
return self.get('encoder') + self.zero
return self._read_axispar(ENCODER_ADR, ANGLE_SCALE) + self.zero
def read_steppos(self):
return self.get('steppos') + self.zero
def read_encoder_tolerance(self):
return self.get('encoder_tolerance')
def write_encoder_tolerance(self, value):
return self.set('encoder_tolerance', value)
def read_target_reached(self):
return self.get('target_reached')
def read_speed(self):
return self.get('speed')
def write_speed(self, value):
return self.set('speed', value)
def read_minspeed(self):
return self.get('minspeed')
def write_minspeed(self, value):
return self.set('minspeed', value)
def read_currentspeed(self):
return self.get('currentspeed')
def read_acceleration(self):
return self.get('acceleration')
def write_acceleration(self, value):
return self.set('acceleration', value)
def read_maxcurrent(self):
return self.get('maxcurrent')
def write_maxcurrent(self, value):
return self.set('maxcurrent', value)
def read_standby_current(self):
return self.get('standby_current')
def write_standby_current(self, value):
return self.set('standby_current', value)
def read_free_wheeling(self):
return self.get('free_wheeling')
def write_free_wheeling(self, value):
return self.set('free_wheeling', value)
def read_power_down_delay(self):
return self.get('power_down_delay')
def write_power_down_delay(self, value):
return self.set('power_down_delay', value)
def read_move_status(self):
return self.get('move_status')
def read_error_bits(self):
return self.get('error_bits')
return self._read_axispar(STEPPOS_ADR, ANGLE_SCALE) + self.zero
@Command(FloatRange())
def set_zero(self, value):
raw = self.read_value() - self.zero
self.write_zero(value - raw)
"""adjust zero"""
self.write_zero(value - self.read_value())
def read_baudrate(self):
return self.comm(GET_GLOB_PAR, 65)
@ -380,14 +329,14 @@ class Motor(PersistentMixin, HasIodev, Drivable):
"""set steppos to encoder value, if not within tolerance"""
if self._started:
raise IsBusyError('can not reset while moving')
tol = ENCODER_RESOLUTION
tol = ENCODER_RESOLUTION * 1.1
for itry in range(10):
diff = self.read_encoder() - self.read_steppos()
if abs(diff) <= tol:
self._need_reset = False
self.status = self.Status.IDLE, 'ok'
return
self.set('steppos', self.encoder - self.zero, check=False)
self._write_axispar(self.encoder - self.zero, STEPPOS_ADR, ANGLE_SCALE, readback=False)
self.comm(MOVE, 0, (self.encoder - self.zero) / ANGLE_SCALE)
time.sleep(0.1)
if itry > 5:
@ -397,23 +346,33 @@ class Motor(PersistentMixin, HasIodev, Drivable):
@Command()
def stop(self):
"""stop motor immediately"""
self.comm(MOTOR_STOP, 0)
self.status = self.Status.IDLE, 'stopped'
self.target = self.value # indicate to customers that this was stopped
self._started = 0
#@Command()
#def step(self):
# self.comm(MOVE, 1, FULL_STEP / ANGLE_SCALE)
@Command()
def step_forward(self):
"""move one full step forwards
#@Command()
#def back(self):
# self.comm(MOVE, 1, - FULL_STEP / ANGLE_SCALE)
for quick tests
"""
self.comm(MOVE, 1, FULL_STEP / ANGLE_SCALE)
#@Command(IntRange(), result=IntRange())
#def get_axis_par(self, adr):
# return self.comm(GET_AXIS_PAR, adr)
@Command()
def step_back(self):
"""move one full step backwards
#@Command((IntRange(), FloatRange()), result=IntRange())
#def set_axis_par(self, adr, value):
# return self.comm(SET_AXIS_PAR, adr, value)
for quick tests
"""
self.comm(MOVE, 1, - FULL_STEP / ANGLE_SCALE)
@Command(IntRange(), result=IntRange())
def get_axis_par(self, adr):
"""get arbitrary motor parameter"""
return self.comm(GET_AXIS_PAR, adr)
@Command((IntRange(), IntRange()), result=IntRange())
def set_axis_par(self, adr, value):
"""set arbitrary motor parameter"""
return self.comm(SET_AXIS_PAR, adr, value)

View File

@ -107,7 +107,7 @@ class Uniax(PersistentMixin, Drivable):
return True
return False
def next_action(self, action, do_now=True):
def next_action(self, action):
"""call next action
:param action: function to be called next time
@ -116,8 +116,6 @@ class Uniax(PersistentMixin, Drivable):
self._action = action
self._init_action = True
self.log.info('action %r', action.__name__)
if do_now:
self._next_cycle = False
def init_action(self):
"""return true when called the first time after next_action"""
@ -126,13 +124,6 @@ class Uniax(PersistentMixin, Drivable):
return True
return False
def execute_action(self):
for _ in range(5): # limit number of subsequent actions in one cycle
self._next_cycle = True
self._action(self.value, self.target)
if self._next_cycle:
break
def zero_pos(self, value,):
"""get high_pos or low_pos, depending on sign of value
@ -173,11 +164,11 @@ class Uniax(PersistentMixin, Drivable):
self.log.info('motor stopped - substantial force detected: %g', force)
self._motor.stop()
elif self.init_action():
self.next_action(self.adjust, True)
self.next_action(self.adjust)
return
if abs(force) > self.hysteresis:
self.set_zero_pos(force, self._motor.read_value())
self.next_action(self.adjust, True)
self.next_action(self.adjust)
return
if force * sign < -self.hysteresis:
self._previous_force = force
@ -333,7 +324,7 @@ class Uniax(PersistentMixin, Drivable):
return Done
if self.zero_pos(force) is None and abs(force) > self.hysteresis and self._filtered:
self.set_zero_pos(force, self._motor.read_value())
self.execute_action()
self._action(self.value, self.target)
return Done
def write_target(self, target):
@ -351,7 +342,10 @@ class Uniax(PersistentMixin, Drivable):
self._cnt_rderr = 0
self._cnt_wrerr = 0
self.status = 'BUSY', 'changed target'
self.next_action(self.find, False)
if self.value * math.copysign(1, target) > self.hysteresis:
self.next_action(self.adjust)
else:
self.next_action(self.find)
return target
@Command()

80
test/test_attach.py Normal file
View File

@ -0,0 +1,80 @@
# -*- 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:
# Markus Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
from secop.modules import Module, Attached
from secop.protocol.dispatcher import Dispatcher
# class DispatcherStub:
# # omit_unchanged_within = 0
#
# # def __init__(self, updates):
# # self.updates = updates
# #
# # def announce_update(self, modulename, pname, pobj):
# # self.updates.setdefault(modulename, {})
# # if pobj.readerror:
# # self.updates[modulename]['error', pname] = str(pobj.readerror)
# # else:
# # self.updates[modulename][pname] = pobj.value
#
# def __init__(self):
# self.modules = {}
#
# def get_module(self, name):
# return self.modules[name]
#
# def register_module(self, name, module):
# self.modules[name] = module
class LoggerStub:
def debug(self, fmt, *args):
print(fmt % args)
info = warning = exception = debug
handlers = []
logger = LoggerStub()
class ServerStub:
restart = None
shutdown = None
def __init__(self):
self.dispatcher = Dispatcher('dispatcher', logger, {}, self)
def test_attach():
class Mod(Module):
att = Attached()
srv = ServerStub()
a = Module('a', logger, {'description': ''}, srv)
m = Mod('m', logger, {'description': '', 'att': 'a'}, srv)
assert m.propertyValues['att'] == 'a'
srv.dispatcher.register_module(a, 'a')
srv.dispatcher.register_module(m, 'm')
assert m.att == 'a'
m.attachedModules = {'att': a}
assert m.att == a

View File

@ -28,7 +28,9 @@ import pytest
from secop.datatypes import ArrayOf, BLOBType, BoolType, \
CommandType, ConfigError, DataType, Enum, EnumType, FloatRange, \
IntRange, ProgrammingError, ScaledInteger, StatusType, \
StringType, StructOf, TextType, TupleOf, get_datatype
StringType, StructOf, TextType, TupleOf, get_datatype, \
DiscouragedConversion
from secop.lib import generalConfig
def copytest(dt):
@ -36,6 +38,7 @@ def copytest(dt):
assert dt.export_datatype() == dt.copy().export_datatype()
assert dt != dt.copy()
def test_DataType():
dt = DataType()
with pytest.raises(NotImplementedError):
@ -116,7 +119,6 @@ def test_IntRange():
dt('1.3')
dt(1)
dt(0)
dt('1')
with pytest.raises(ProgrammingError):
IntRange('xc', 'Yx')
@ -132,6 +134,7 @@ def test_IntRange():
with pytest.raises(ConfigError):
dt.checkProperties()
def test_ScaledInteger():
dt = ScaledInteger(0.01, -3, 3)
copytest(dt)
@ -407,6 +410,7 @@ def test_ArrayOf():
dt = ArrayOf(EnumType('myenum', single=0), 5)
copytest(dt)
def test_TupleOf():
# test constructor catching illegal arguments
with pytest.raises(ValueError):
@ -641,6 +645,7 @@ def test_oneway_compatible(dt, contained_in):
with pytest.raises(ValueError):
contained_in.compatible(dt)
@pytest.mark.parametrize('dt1, dt2', [
(FloatRange(-5.5, 5.5), ScaledInteger(10, -5.5, 5.5)),
(IntRange(0,1), BoolType()),
@ -650,6 +655,7 @@ def test_twoway_compatible(dt1, dt2):
dt1.compatible(dt1)
dt2.compatible(dt2)
@pytest.mark.parametrize('dt1, dt2', [
(StringType(), FloatRange()),
(IntRange(-10, 10), StringType()),
@ -665,3 +671,12 @@ def test_incompatible(dt1, dt2):
dt1.compatible(dt2)
with pytest.raises(ValueError):
dt2.compatible(dt1)
@pytest.mark.parametrize('dt', [FloatRange(), IntRange(), ScaledInteger(1)])
def test_lazy_validation(dt):
generalConfig.defaults['lazy_number_validation'] = True
dt('0')
generalConfig.defaults['lazy_number_validation'] = False
with pytest.raises(DiscouragedConversion):
dt('0')

234
test/test_handler.py Normal file
View 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:
# Markus Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
from secop.rwhandler import ReadHandler, WriteHandler, \
CommonReadHandler, CommonWriteHandler, nopoll
from secop.core import Module, Parameter, FloatRange, Done
class DispatcherStub:
# the first update from the poller comes a very short time after the
# initial value from the timestamp. However, in the test below
# the second update happens after the updates dict is cleared
# -> we have to inhibit the 'omit unchanged update' feature
omit_unchanged_within = 0
def __init__(self, updates):
self.updates = updates
def announce_update(self, modulename, pname, pobj):
self.updates.setdefault(modulename, {})
if pobj.readerror:
self.updates[modulename]['error', pname] = str(pobj.readerror)
else:
self.updates[modulename][pname] = pobj.value
class LoggerStub:
def debug(self, fmt, *args):
print(fmt % args)
info = warning = exception = error = debug
handlers = []
logger = LoggerStub()
class ServerStub:
def __init__(self, updates):
self.dispatcher = DispatcherStub(updates)
class ModuleTest(Module):
def __init__(self, updates=None, **opts):
opts['description'] = ''
super().__init__('mod', logger, opts, ServerStub(updates or {}))
def test_handler():
data = []
class Mod(ModuleTest):
a = Parameter('', FloatRange(), readonly=False)
b = Parameter('', FloatRange(), readonly=False)
@ReadHandler(['a', 'b'])
def read_hdl(self, pname):
value = data.pop()
data.append(pname)
return value
@WriteHandler(['a', 'b'])
def write_hdl(self, pname, value):
data.append(pname)
return value
assert Mod.read_a.poll is True
assert Mod.read_b.poll is True
m = Mod()
data.append(1.2)
assert m.read_a() == 1.2
assert data.pop() == 'a'
data.append(1.3)
assert m.read_b() == 1.3
assert data.pop() == 'b'
assert m.write_a(1.5) == 1.5
assert m.a == 1.5
assert data.pop() == 'a'
assert m.write_b(7) == 7
assert m.b == 7
assert data.pop() == 'b'
data.append(Done)
assert m.read_b() == 7
assert data.pop() == 'b'
assert data == []
def test_common_handler():
data = []
class Mod(ModuleTest):
a = Parameter('', FloatRange(), readonly=False)
b = Parameter('', FloatRange(), readonly=False)
@CommonReadHandler(['a', 'b'])
def read_hdl(self):
self.a, self.b = data.pop()
data.append('read_hdl')
@CommonWriteHandler(['a', 'b'])
def write_hdl(self, values):
self.a = values['a']
self.b = values['b']
data.append('write_hdl')
assert set([Mod.read_a.poll, Mod.read_b.poll]) == {True, False}
m = Mod(a=1, b=2)
assert m.writeDict == {'a': 1, 'b': 2}
m.write_a(3)
assert m.a == 3
assert m.b == 2
assert data.pop() == 'write_hdl'
assert m.writeDict == {}
m.write_b(4)
assert m.a == 3
assert m.b == 4
assert data.pop() == 'write_hdl'
data.append((3, 4))
assert m.read_a() == 3
assert m.a == 3
assert m.b == 4
assert data.pop() == 'read_hdl'
data.append((5, 6))
assert m.read_b() == 6
assert data.pop() == 'read_hdl'
data.append((1.1, 2.2))
assert m.read_b() == 2.2
assert m.a == 1.1
assert m.b == 2.2
assert data.pop() == 'read_hdl'
assert data == []
def test_nopoll():
class Mod1(ModuleTest):
a = Parameter('', FloatRange(), readonly=False)
b = Parameter('', FloatRange(), readonly=False)
@ReadHandler(['a', 'b'])
def read_hdl(self):
pass
assert Mod1.read_a.poll is True
assert Mod1.read_b.poll is True
class Mod2(ModuleTest):
a = Parameter('', FloatRange(), readonly=False)
b = Parameter('', FloatRange(), readonly=False)
@CommonReadHandler(['a', 'b'])
def read_hdl(self):
pass
assert Mod2.read_a.poll is True
assert Mod2.read_b.poll is False
class Mod3(ModuleTest):
a = Parameter('', FloatRange(), readonly=False)
b = Parameter('', FloatRange(), readonly=False)
@ReadHandler(['a', 'b'])
@nopoll
def read_hdl(self):
pass
assert Mod3.read_a.poll is False
assert Mod3.read_b.poll is False
class Mod4(ModuleTest):
a = Parameter('', FloatRange(), readonly=False)
b = Parameter('', FloatRange(), readonly=False)
@nopoll
@ReadHandler(['a', 'b'])
def read_hdl(self):
pass
assert Mod4.read_a.poll is False
assert Mod4.read_b.poll is False
class Mod5(ModuleTest):
a = Parameter('', FloatRange(), readonly=False)
b = Parameter('', FloatRange(), readonly=False)
@CommonReadHandler(['a', 'b'])
@nopoll
def read_hdl(self):
pass
assert Mod5.read_a.poll is False
assert Mod5.read_b.poll is False
class Mod6(ModuleTest):
a = Parameter('', FloatRange(), readonly=False)
b = Parameter('', FloatRange(), readonly=False)
@nopoll
@CommonReadHandler(['a', 'b'])
def read_hdl(self):
pass
assert Mod6.read_a.poll is False
assert Mod6.read_b.poll is False

View File

@ -71,7 +71,11 @@ class Data:
class DispatcherStub:
OMIT_UNCHANGED_WITHIN = 0
# the first update from the poller comes a very short time after the
# initial value from the timestamp. However, in the test below
# the second update happens after the updates dict is cleared
# -> we have to inhibit the 'omit unchanged update' feature
omit_unchanged_within = 0
def __init__(self, updates):
self.updates = updates
@ -108,6 +112,7 @@ def test_IOHandler():
group1 = Hdl('group1', 'SIMPLE?', '%g')
group2 = Hdl('group2', 'CMD?%(channel)d', '%g,%s,%d')
class Module1(Module):
channel = Property('the channel', IntRange(), default=3)
loop = Property('the loop', IntRange(), default=2)
@ -115,7 +120,7 @@ def test_IOHandler():
real = Parameter('a float value', FloatRange(), default=12.3, handler=group2, readonly=False)
text = Parameter('a string value', StringType(), default='x', handler=group2, readonly=False)
def sendRecv(self, command):
def communicate(self, command):
assert data.pop('command') == command
return data.pop('reply')
@ -141,7 +146,7 @@ def test_IOHandler():
print(updates)
updates.clear() # get rid of updates from initialisation
# for sendRecv
# for communicate
data.push('command', 'SIMPLE?')
data.push('reply', '4.51')
# for analyze_group1
@ -154,7 +159,7 @@ def test_IOHandler():
assert updates.pop('simple') == 45.1
assert not updates
# for sendRecv
# for communicate
data.push('command', 'CMD?3')
data.push('reply', '1.23,text,5')
# for analyze_group2
@ -167,7 +172,7 @@ def test_IOHandler():
assert data.empty()
assert not updates
# for sendRecv
# for communicate
data.push('command', 'CMD?3')
data.push('reply', '1.23,text,5')
# for analyze_group2
@ -178,7 +183,7 @@ def test_IOHandler():
data.push('self', 12.3, 'string')
data.push('new', 12.3, 'FOO')
data.push('changed', 1.23, 'foo', 9)
# for sendRecv
# for communicate
data.push('command', 'CMD 3,1.23,foo,9|CMD?3')
data.push('reply', '1.23,foo,9')
# for analyze_group2

View File

@ -64,6 +64,7 @@ def test_EnumMember():
assert a != 3
assert a == 1
def test_Enum():
e1 = Enum('e1')
e2 = Enum('e2', e1, a=1, b=3)
@ -75,3 +76,4 @@ def test_Enum():
assert e2.b > e3.a
assert e3.c >= e2.a
assert e3.b <= e2.b
assert Enum({'self': 0, 'other': 1})('self') == 0

274
test/test_logging.py Normal file
View File

@ -0,0 +1,274 @@
# -*- 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:
# Markus Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
import pytest
import mlzlog
from secop.modules import Module
from secop.protocol.dispatcher import Dispatcher
from secop.protocol.interface import encode_msg_frame, decode_msg
import secop.logging
from secop.logging import logger, generalConfig, HasComlog
class ServerStub:
restart = None
shutdown = None
def __init__(self):
self.dispatcher = Dispatcher('', logger.log.getChild('dispatcher'), {}, self)
class Connection:
def __init__(self, name, dispatcher, result):
self.result = result
self.dispatcher = dispatcher
self.name = name
dispatcher.add_connection(self)
def send_reply(self, msg):
self.result.append(encode_msg_frame(*msg).strip().decode())
def send(self, msg):
request = decode_msg(msg.encode())
assert self.dispatcher.handle_request(self, request) == request
@pytest.fixture(name='init')
def init_(monkeypatch):
logger.__init__()
class Playground:
def __init__(self, console_level='debug', comlog=True, com_module=True):
self.result_dict = result_dict = dict(
console=[], comlog=[], conn1=[], conn2=[])
class ConsoleHandler(mlzlog.Handler):
def __init__(self, *args, **kwds):
super().__init__()
self.result = result_dict['console']
def emit(self, record):
if record.name != 'frappy.dispatcher':
self.result.append('%s %s %s' % (record.name, record.levelname, record.getMessage()))
class ComLogHandler(mlzlog.Handler):
def __init__(self, *args, **kwds):
super().__init__()
self.result = result_dict['comlog']
def emit(self, record):
self.result.append('%s %s' % (record.name.split('.')[1], record.getMessage()))
class LogfileHandler(mlzlog.Handler):
def __init__(self, *args, **kwds):
super().__init__()
def noop(self, *args):
pass
close = flush = emit = noop
monkeypatch.setattr(mlzlog, 'ColoredConsoleHandler', ConsoleHandler)
monkeypatch.setattr(secop.logging, 'ComLogfileHandler', ComLogHandler)
monkeypatch.setattr(secop.logging, 'LogfileHandler', LogfileHandler)
class Mod(Module):
result = []
def __init__(self, name, srv, **kwds):
kwds['description'] = ''
super().__init__(name or 'mod', logger.log.getChild(name), kwds, srv)
srv.dispatcher.register_module(self, name, name)
self.result[:] = []
def earlyInit(self):
pass
class Com(HasComlog, Mod):
def __init__(self, name, srv, **kwds):
super().__init__(name, srv, **kwds)
self.earlyInit()
self.log.handlers[-1].result = result_dict['comlog']
def communicate(self, request):
self.comLog('> %s', request)
generalConfig.testinit(logger_root='frappy', comlog=comlog)
logger.init(console_level)
self.srv = ServerStub()
self.conn1 = Connection('conn1', self.srv.dispatcher, self.result_dict['conn1'])
self.conn2 = Connection('conn2', self.srv.dispatcher, self.result_dict['conn2'])
self.mod = Mod('mod', self.srv)
self.com = Com('com', self.srv, comlog=com_module)
for item in self.result_dict.values():
assert item == []
def check(self, both=None, **expected):
if both:
expected['conn1'] = expected['conn2'] = both
assert self.result_dict['console'] == expected.get('console', [])
assert self.result_dict['comlog'] == expected.get('comlog', [])
assert self.result_dict['conn1'] == expected.get('conn1', [])
assert self.result_dict['conn2'] == expected.get('conn2', [])
for item in self.result_dict.values():
item[:] = []
def comlog(self, flag):
logger.comlog = flag
yield Playground
# revert settings
generalConfig.testinit()
logger.__init__()
def test_mod_info(init):
p = init()
p.mod.log.info('i')
p.check(console=['frappy.mod INFO i'])
p.conn1.send('logging mod "debug"')
p.conn2.send('logging mod "info"')
p.mod.log.info('i')
p.check(console=['frappy.mod INFO i'], both=['log mod:info "i"'])
def test_mod_debug(init):
p = init()
p.mod.log.debug('d')
p.check(console=['frappy.mod DEBUG d'])
p.conn1.send('logging mod "debug"')
p.conn2.send('logging mod "info"')
p.mod.log.debug('d')
p.check(console=['frappy.mod DEBUG d'], conn1=['log mod:debug "d"'])
def test_com_info(init):
p = init()
p.com.log.info('i')
p.check(console=['frappy.com INFO i'])
p.conn1.send('logging com "info"')
p.conn2.send('logging com "debug"')
p.com.log.info('i')
p.check(console=['frappy.com INFO i'], both=['log com:info "i"'])
def test_com_debug(init):
p = init()
p.com.log.debug('d')
p.check(console=['frappy.com DEBUG d'])
p.conn2.send('logging com "debug"')
p.com.log.debug('d')
p.check(console=['frappy.com DEBUG d'], conn2=['log com:debug "d"'])
def test_com_com(init):
p = init()
p.com.communicate('x')
p.check(console=['frappy.com COMLOG > x'], comlog=['com > x'])
p.conn1.send('logging mod "debug"')
p.conn2.send('logging mod "info"')
p.conn2.send('logging com "debug"')
p.com.communicate('x')
p.check(console=['frappy.com COMLOG > x'], comlog=['com > x'], conn2=['log com:comlog "> x"'])
def test_main_info(init):
p = init(console_level='info')
p.mod.log.debug('d')
p.com.communicate('x')
p.check(comlog=['com > x'])
p.conn1.send('logging mod "debug"')
p.conn2.send('logging mod "info"')
p.conn2.send('logging com "debug"')
p.com.communicate('x')
p.check(comlog=['com > x'], conn2=['log com:comlog "> x"'])
def test_comlog_off(init):
p = init(console_level='info', comlog=False)
p.mod.log.debug('d')
p.com.communicate('x')
p.check()
def test_comlog_module_off(init):
p = init(console_level='info', com_module=False)
p.mod.log.debug('d')
p.com.communicate('x')
p.check()
def test_remote_all_off(init):
p = init()
p.conn1.send('logging mod "debug"')
p.conn2.send('logging mod "info"')
p.conn2.send('logging com "debug"')
p.mod.log.debug('d')
p.com.communicate('x')
p.mod.log.info('i')
checks = dict(
console=['frappy.mod DEBUG d', 'frappy.com COMLOG > x', 'frappy.mod INFO i'],
comlog=['com > x'],
conn1=['log mod:debug "d"', 'log mod:info "i"'],
conn2=['log com:comlog "> x"', 'log mod:info "i"'])
p.check(**checks)
p.conn1.send('logging "off"')
p.mod.log.debug('d')
p.com.communicate('x')
p.mod.log.info('i')
checks.pop('conn1')
p.check(**checks)
p.conn2.send('logging . "off"')
p.mod.log.debug('d')
p.com.communicate('x')
p.mod.log.info('i')
checks.pop('conn2')
p.check(**checks)
def test_remote_single_off(init):
p = init()
p.conn1.send('logging mod "debug"')
p.conn2.send('logging mod "info"')
p.conn2.send('logging com "debug"')
p.mod.log.debug('d')
p.com.communicate('x')
p.mod.log.info('i')
checks = dict(
console=['frappy.mod DEBUG d', 'frappy.com COMLOG > x', 'frappy.mod INFO i'],
comlog=['com > x'],
conn1=['log mod:debug "d"', 'log mod:info "i"'],
conn2=['log com:comlog "> x"', 'log mod:info "i"'])
p.check(**checks)
p.conn2.send('logging com "off"')
p.mod.log.debug('d')
p.com.communicate('x')
p.mod.log.info('i')
checks['conn2'] = ['log mod:info "i"']
p.check(**checks)
p.conn2.send('logging mod "off"')
p.mod.log.debug('d')
p.com.communicate('x')
p.mod.log.info('i')
checks['conn2'] = []
p.check(**checks)

View File

@ -22,19 +22,24 @@
# *****************************************************************************
"""test data types."""
import sys
import threading
import pytest
from secop.datatypes import BoolType, FloatRange, StringType, IntRange, CommandType
from secop.datatypes import BoolType, FloatRange, StringType, IntRange, ScaledInteger
from secop.errors import ProgrammingError, ConfigError
from secop.modules import Communicator, Drivable, Readable, Module
from secop.params import Command, Parameter
from secop.poller import BasicPoller
from secop.rwhandler import ReadHandler, WriteHandler, nopoll
from secop.lib import generalConfig
class DispatcherStub:
OMIT_UNCHANGED_WITHIN = 0
# the first update from the poller comes a very short time after the
# initial value from the timestamp. However, in the test below
# the second update happens after the updates dict is cleared
# -> we have to inhibit the 'omit unchanged update' feature
omit_unchanged_within = 0
def __init__(self, updates):
self.updates = updates
@ -48,9 +53,10 @@ class DispatcherStub:
class LoggerStub:
def debug(self, *args):
print(*args)
info = warning = exception = debug
def debug(self, fmt, *args):
print(fmt % args)
info = warning = exception = error = debug
handlers = []
logger = LoggerStub()
@ -61,13 +67,23 @@ class ServerStub:
self.dispatcher = DispatcherStub(updates)
class DummyMultiEvent(threading.Event):
def get_trigger(self):
def trigger(event=self):
event.set()
sys.exit()
return trigger
def test_Communicator():
o = Communicator('communicator', LoggerStub(), {'.description':''}, ServerStub({}))
o = Communicator('communicator', LoggerStub(), {'.description': ''}, ServerStub({}))
o.earlyInit()
o.initModule()
event = threading.Event()
o.startModule(event.set)
assert event.is_set() # event should be set immediately
event = DummyMultiEvent()
o.initModule()
o.startModule(event)
assert event.wait(timeout=0.1)
def test_ModuleMagic():
@ -83,14 +99,13 @@ def test_ModuleMagic():
a1 = Parameter('a1', datatype=BoolType(), default=False)
a2 = Parameter('a2', datatype=BoolType(), default=True)
value = Parameter(datatype=StringType(), default='first')
target = Parameter(datatype=StringType(), default='')
@Command(argument=BoolType(), result=BoolType())
def cmd2(self, arg):
"""another stuff"""
return not arg
pollerClass = BasicPoller
def read_param1(self):
return True
@ -100,12 +115,16 @@ def test_ModuleMagic():
def read_a1(self):
return True
@nopoll
def read_a2(self):
return True
def read_value(self):
return 'second'
def read_status(self):
return 'IDLE', 'ok'
with pytest.raises(ProgrammingError):
class Mod1(Module): # pylint: disable=unused-variable
def do_this(self): # old style command
@ -128,15 +147,19 @@ def test_ModuleMagic():
return arg
value = Parameter(datatype=FloatRange(unit='deg'))
target = Parameter(datatype=FloatRange(), default=0)
a1 = Parameter(datatype=FloatRange(unit='$/s'), readonly=False)
b2 = Parameter('<b2>', datatype=BoolType(), default=True,
poll=True, readonly=False, initwrite=True)
# remark: it might be a programming error to override the datatype
# and not overriding the read_* method. This is not checked!
b2 = Parameter('<b2>', datatype=StringType(), default='empty',
readonly=False, initwrite=True)
def write_a1(self, value):
self._a1_written = value
return value
def write_b2(self, value):
value = value.upper()
self._b2_written = value
return value
@ -166,33 +189,38 @@ def test_ModuleMagic():
# check for inital updates working properly
o1 = Newclass1('o1', logger, {'.description':''}, srv)
expectedBeforeStart = {'target': 0.0, 'status': (Drivable.Status.IDLE, ''),
expectedBeforeStart = {'target': '', 'status': (Drivable.Status.IDLE, ''),
'param1': False, 'param2': 1.0, 'a1': 0.0, 'a2': True, 'pollinterval': 5.0,
'value': 'first'}
assert updates.pop('o1') == expectedBeforeStart
o1.earlyInit()
event = threading.Event()
o1.startModule(event.set)
event.wait()
event = DummyMultiEvent()
o1.initModule()
o1.startModule(event)
assert event.wait(timeout=0.1)
# should contain polled values
expectedAfterStart = {'status': (Drivable.Status.IDLE, ''),
'value': 'second'}
expectedAfterStart = {
'status': (Drivable.Status.IDLE, 'ok'), 'value': 'second',
'param1': True, 'param2': 0.0, 'a1': True}
assert updates.pop('o1') == expectedAfterStart
# check in addition if parameters are written
o2 = Newclass2('o2', logger, {'.description':'', 'a1': 2.7}, srv)
# no update for b2, as this has to be written
expectedBeforeStart['a1'] = 2.7
expectedBeforeStart['target'] = 0.0
assert updates.pop('o2') == expectedBeforeStart
o2.earlyInit()
event = threading.Event()
o2.startModule(event.set)
event.wait()
event = DummyMultiEvent()
o2.initModule()
o2.startModule(event)
assert event.wait(timeout=0.1)
# value has changed type, b2 and a1 are written
expectedAfterStart.update(value=0, b2=True, a1=2.7)
expectedAfterStart.update(value=0, b2='EMPTY', a1=True)
# ramerk: a1=True: this behaviour is a Porgamming error
assert updates.pop('o2') == expectedAfterStart
assert o2._a1_written == 2.7
assert o2._b2_written is True
assert o2._b2_written == 'EMPTY'
assert not updates
@ -206,13 +234,15 @@ def test_ModuleMagic():
# check '$' in unit works properly
assert o2.parameters['a1'].datatype.unit == 'mm/s'
cfg = Newclass2.configurables
assert set(cfg.keys()) == {'export', 'group', 'description',
assert set(cfg.keys()) == {
'export', 'group', 'description', 'disable_value_range_check',
'meaning', 'visibility', 'implementation', 'interface_classes', 'target', 'stop',
'status', 'param1', 'param2', 'cmd', 'a2', 'pollinterval', 'b2', 'cmd2', 'value',
'a1'}
assert set(cfg['value'].keys()) == {'group', 'export', 'relative_resolution',
'status', 'param1', 'param2', 'cmd', 'a2', 'pollinterval', 'slowinterval', 'b2',
'cmd2', 'value', 'a1'}
assert set(cfg['value'].keys()) == {
'group', 'export', 'relative_resolution',
'visibility', 'unit', 'default', 'datatype', 'fmtstr',
'absolute_resolution', 'poll', 'max', 'min', 'readonly', 'constant',
'absolute_resolution', 'max', 'min', 'readonly', 'constant',
'description', 'needscfg'}
# check on the level of classes
@ -260,9 +290,101 @@ def test_param_inheritance():
Base('o', logger, {'description': ''}, srv)
def test_mixin():
# srv = ServerStub({})
def test_command_inheritance():
class Base(Module):
@Command(BoolType(), visibility=2)
def cmd(self, arg):
"""base"""
class Sub1(Base):
@Command(group='grp')
def cmd(self, arg):
"""first"""
class Sub2(Sub1):
@Command(None, result=BoolType())
def cmd(self): # pylint: disable=arguments-differ
"""second"""
class Sub3(Base):
# when either argument or result is given, the other one is assumed to be None
# i.e. here we override the argument with None
@Command(result=FloatRange())
def cmd(self, arg):
"""third"""
assert Sub1.accessibles['cmd'].for_export() == {
'description': 'first', 'group': 'grp', 'visibility': 2,
'datainfo': {'type': 'command', 'argument': {'type': 'bool'}}
}
assert Sub2.accessibles['cmd'].for_export() == {
'description': 'second', 'group': 'grp', 'visibility': 2,
'datainfo': {'type': 'command', 'result': {'type': 'bool'}}
}
assert Sub3.accessibles['cmd'].for_export() == {
'description': 'third', 'visibility': 2,
'datainfo': {'type': 'command', 'result': {'type': 'double'}}
}
for cls in locals().values():
if hasattr(cls, 'accessibles'):
for p in cls.accessibles.values():
assert isinstance(p.ownProperties, dict)
assert p.copy().ownProperties == {}
def test_command_check():
srv = ServerStub({})
class Good(Module):
@Command(description='available')
def with_description(self):
pass
@Command()
def with_docstring(self):
"""docstring"""
Good('o', logger, {'description': ''}, srv)
class Bad1(Module):
@Command
def without_description(self):
pass
class Bad2(Module):
@Command()
def without_description(self):
pass
for cls in Bad1, Bad2:
with pytest.raises(ConfigError) as e_info:
cls('o', logger, {'description': ''}, srv)
assert 'description' in repr(e_info.value)
class BadDatatype(Module):
@Command(FloatRange(0.1, 0.9), result=FloatRange())
def cmd(self):
"""valid command"""
BadDatatype('o', logger, {'description': ''}, srv)
# test for command property checking
with pytest.raises(ProgrammingError):
BadDatatype('o', logger, {
'description': '',
'cmd.argument': {'type': 'double', 'min': 1, 'max': 0},
}, srv)
with pytest.raises(ProgrammingError):
BadDatatype('o', logger, {
'description': '',
'cmd.visibility': 'invalid',
}, srv)
def test_mixin():
class Mixin: # no need to inherit from Module or HasAccessible
value = Parameter(unit='K') # missing datatype and description acceptable in mixins
param1 = Parameter('no datatype yet', fmtstr='%.5f')
@ -276,7 +398,7 @@ def test_mixin():
param1 = Parameter(datatype=FloatRange())
with pytest.raises(ProgrammingError):
class MixedModule(Mixin):
class MixedModule(Mixin): # pylint: disable=unused-variable
param1 = Parameter('', FloatRange(), fmtstr=0) # fmtstr must be a string
assert repr(MixedDrivable.status.datatype) == repr(Drivable.status.datatype)
@ -305,10 +427,29 @@ def test_mixin():
}, srv)
def test_override():
class Mod(Drivable):
value = 5 # overriding the default value
def stop(self):
"""no decorator needed"""
assert Mod.value.default == 5
assert Mod.stop.description == "no decorator needed"
class Mod2(Drivable):
@Command()
def stop(self):
pass
assert Mod2.stop.description == Drivable.stop.description
def test_command_config():
class Mod(Module):
@Command(IntRange(0, 1), result=IntRange(0, 1))
def convert(self, value):
"""dummy conversion"""
return value
srv = ServerStub({})
@ -332,3 +473,189 @@ def test_command_config():
'result': {'type': 'bool'},
}
def test_command_none():
srv = ServerStub({})
class Mod(Drivable):
pass
class Mod2(Drivable):
stop = None
assert 'stop' in Mod('o', logger, {'description': ''}, srv).accessibles
assert 'stop' not in Mod2('o', logger, {'description': ''}, srv).accessibles
def test_bad_method():
class Mod0(Drivable): # pylint: disable=unused-variable
def write_target(self, value):
pass
with pytest.raises(ProgrammingError):
class Mod1(Drivable): # pylint: disable=unused-variable
def write_taget(self, value):
pass
class Mod2(Drivable): # pylint: disable=unused-variable
def read_value(self, value):
pass
with pytest.raises(ProgrammingError):
class Mod3(Drivable): # pylint: disable=unused-variable
def read_valu(self, value):
pass
def test_generic_access():
class Mod(Module):
param = Parameter('handled param', StringType(), readonly=False)
unhandled = Parameter('unhandled param', StringType(), default='', readonly=False)
data = {'param': ''}
@ReadHandler(['param'])
def read_handler(self, pname):
value = self.data[pname]
setattr(self, pname, value)
return value
@WriteHandler(['param'])
def write_handler(self, pname, value):
value = value.lower()
self.data[pname] = value
setattr(self, pname, value)
return value
updates = {}
srv = ServerStub(updates)
obj = Mod('obj', logger, {'description': '', 'param': 'initial value'}, srv)
assert obj.param == 'initial value'
assert obj.write_param('Cheese') == 'cheese'
assert obj.write_unhandled('Cheese') == 'Cheese'
assert updates == {'obj': {'param': 'cheese', 'unhandled': 'Cheese'}}
updates.clear()
assert obj.write_param('Potato') == 'potato'
assert updates == {'obj': {'param': 'potato'}}
updates.clear()
assert obj.read_param() == 'potato'
assert obj.read_unhandled()
assert updates == {'obj': {'param': 'potato'}}
updates.clear()
assert updates == {}
def test_duplicate_handler_name():
with pytest.raises(ProgrammingError):
class Mod(Module): # pylint: disable=unused-variable
param = Parameter('handled param', StringType(), readonly=False)
@ReadHandler(['param'])
def handler(self, pname):
pass
@WriteHandler(['param'])
def handler(self, pname, value): # pylint: disable=function-redefined
pass
def test_handler_overwrites_method():
with pytest.raises(RuntimeError):
class Mod1(Module): # pylint: disable=unused-variable
param = Parameter('handled param', StringType(), readonly=False)
@ReadHandler(['param'])
def read_handler(self, pname):
pass
def read_param(self):
pass
with pytest.raises(RuntimeError):
class Mod2(Module): # pylint: disable=unused-variable
param = Parameter('handled param', StringType(), readonly=False)
@WriteHandler(['param'])
def write_handler(self, pname, value):
pass
def write_param(self, value):
pass
def test_no_read_write():
class Mod(Module):
param = Parameter('test param', StringType(), readonly=False)
updates = {}
srv = ServerStub(updates)
obj = Mod('obj', logger, {'description': '', 'param': 'cheese'}, srv)
assert obj.param == 'cheese'
assert obj.read_param() == 'cheese'
assert updates == {'obj': {'param': 'cheese'}}
assert obj.write_param('egg') == 'egg'
assert obj.param == 'egg'
assert updates == {'obj': {'param': 'egg'}}
def test_incompatible_value_target():
class Mod1(Drivable):
value = Parameter('', FloatRange(0, 10), default=0)
target = Parameter('', FloatRange(0, 11), default=0)
class Mod2(Drivable):
value = Parameter('', FloatRange(), default=0)
target = Parameter('', StringType(), default='')
class Mod3(Drivable):
value = Parameter('', FloatRange(), default=0)
target = Parameter('', ScaledInteger(1, 0, 10), default=0)
srv = ServerStub({})
with pytest.raises(ConfigError):
obj = Mod1('obj', logger, {'description': ''}, srv) # pylint: disable=unused-variable
with pytest.raises(ProgrammingError):
obj = Mod2('obj', logger, {'description': ''}, srv)
obj = Mod3('obj', logger, {'description': ''}, srv)
def test_problematic_value_range():
class Mod(Drivable):
value = Parameter('', FloatRange(0, 10), default=0)
target = Parameter('', FloatRange(0, 10), default=0)
srv = ServerStub({})
obj = Mod('obj', logger, {'description': '', 'value.max': 10.1}, srv) # pylint: disable=unused-variable
with pytest.raises(ConfigError):
obj = Mod('obj', logger, {'description': ''}, srv)
class Mod2(Drivable):
value = Parameter('', FloatRange(), default=0)
target = Parameter('', FloatRange(), default=0)
obj = Mod2('obj', logger, {'description': ''}, srv)
obj = Mod2('obj', logger, {'description': '', 'target.min': 0, 'target.max': 10}, srv)
with pytest.raises(ConfigError):
obj = Mod('obj', logger, {
'value.min': 0, 'value.max': 10,
'target.min': 0, 'target.max': 10, 'description': ''}, srv)
obj = Mod('obj', logger, {'disable_value_range_check': True,
'value.min': 0, 'value.max': 10,
'target.min': 0, 'target.max': 10, 'description': ''}, srv)
generalConfig.defaults['disable_value_range_check'] = True
class Mod4(Drivable):
value = Parameter('', FloatRange(0, 10), default=0)
target = Parameter('', FloatRange(0, 10), default=0)
obj = Mod4('obj', logger, {
'value.min': 0, 'value.max': 10,
'target.min': 0, 'target.max': 10, 'description': ''}, srv)

60
test/test_multievent.py Normal file
View File

@ -0,0 +1,60 @@
# -*- 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:
# Markus Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
import time
from secop.lib.multievent import MultiEvent
def test_without_timeout():
m = MultiEvent()
s1 = m.get_trigger(name='s1')
s2 = m.get_trigger(name='s2')
assert not m.wait(0)
assert m.deadline() is None
assert m.waiting_for() == {'s1', 's2'}
s2()
assert m.waiting_for() == {'s1'}
assert not m.wait(0)
s1()
assert not m.waiting_for()
assert m.wait(0)
def test_with_timeout(monkeypatch):
current_time = 1000
monkeypatch.setattr(time, 'monotonic', lambda: current_time)
m = MultiEvent()
assert m.deadline() == 0
m.name = 's1'
s1 = m.get_trigger(10)
assert m.deadline() == 1010
m.name = 's2'
s2 = m.get_trigger(20)
assert m.deadline() == 1020
current_time += 21
assert not m.wait(0)
assert m.waiting_for() == {'s1', 's2'}
s1()
assert m.waiting_for() == {'s2'}
s2()
assert not m.waiting_for()
assert m.wait(0)

View File

@ -93,11 +93,17 @@ def test_Override():
assert id(Mod.p3) == id(Base.p3)
assert repr(Mod.p2) == repr(Base.p2) # must be a clone
assert repr(Mod.p3) == repr(Base.p3) # must be a clone
assert Mod.p1.default == True
assert Mod.p1.default is True
# manipulating default makes Base.p1 and Mod.p1 match
Mod.p1.default = False
assert repr(Mod.p1) == repr(Base.p1)
for cls in locals().values():
if hasattr(cls, 'accessibles'):
for p in cls.accessibles.values():
assert isinstance(p.ownProperties, dict)
assert p.copy().ownProperties == {}
def test_Export():
class Mod(HasAccessibles):

View File

@ -21,227 +21,133 @@
# *****************************************************************************
"""test poller."""
import sys
import threading
from time import time as current_time
import time
from collections import OrderedDict
import logging
import pytest
from secop.modules import Drivable
from secop.poller import DYNAMIC, REGULAR, SLOW, Poller
from secop.core import Module, Parameter, FloatRange, Readable, ReadHandler, nopoll
from secop.lib.multievent import MultiEvent
Status = Drivable.Status
class Time:
STARTTIME = 1000 # artificial time zero
"""artificial time, forwarded on sleep instead of waiting"""
def __init__(self):
self.reset()
self.finish = float('inf')
self.stop = lambda : None
self.commtime = 0.05 # time needed for 1 poll
def reset(self, lifetime=10):
self.seconds = self.STARTTIME
self.idletime = 0.0
self.busytime = 0.0
self.finish = self.STARTTIME + lifetime
self.offset = 0
def time(self):
if self.seconds > self.finish:
self.finish = float('inf')
self.stop()
return self.seconds
return current_time() + self.offset
def sleep(self, seconds):
assert 0 <= seconds <= 24*3600
self.idletime += seconds
self.seconds += seconds
def busy(self, seconds):
assert seconds >= 0
self.seconds += seconds
self.busytime += seconds
artime = Time() # artificial test time
@pytest.fixture(autouse=True)
def patch_time(monkeypatch):
monkeypatch.setattr(time, 'time', artime.time)
self.offset += seconds
class Event:
def __init__(self):
self.flag = False
def wait(self, timeout):
artime.sleep(max(0,timeout))
def set(self):
self.flag = True
def clear(self):
self.flag = False
def is_set(self):
return self.flag
artime = Time() # artificial test time
class Parameter:
def __init__(self, name, readonly, poll, polltype, interval):
self.poll = poll
self.polltype = polltype # used for check only
self.export = name
self.readonly = readonly
self.interval = interval
self.timestamp = 0
self.handler = None
self.reset()
class DispatcherStub:
maxcycles = 10
def reset(self):
self.cnt = 0
self.span = 0
self.maxspan = 0
def rfunc(self):
artime.busy(artime.commtime)
def announce_update(self, modulename, pname, pobj):
now = artime.time()
self.span = now - self.timestamp
self.maxspan = max(self.maxspan, self.span)
self.timestamp = now
self.cnt += 1
return True
def __repr__(self):
return 'Parameter(%s)' % ", ".join("%s=%r" % item for item in self.__dict__.items())
class Module:
properties = {}
pollerClass = Poller
iodev = 'common_iodev'
def __init__(self, name, pollinterval=5, fastfactor=0.25, slowfactor=4, busy=False,
counts=(), auto=None):
'''create a dummy module
nauto, ndynamic, nregular, nslow are the number of parameters of each polltype
'''
self.pollinterval = pollinterval
self.fast_pollfactor = fastfactor
self.slow_pollfactor = slowfactor
self.parameters = OrderedDict()
self.name = name
self.is_busy = busy
if auto is not None:
self.pvalue = self.addPar('value', True, auto or DYNAMIC, DYNAMIC)
# readonly = False should not matter:
self.pstatus = self.addPar('status', False, auto or DYNAMIC, DYNAMIC)
self.pregular = self.addPar('regular', True, auto or REGULAR, REGULAR)
self.pslow = self.addPar('slow', False, auto or SLOW, SLOW)
self.addPar('notpolled', True, False, 0)
self.counts = 'auto'
if hasattr(pobj, 'stat'):
pobj.stat.append(now)
else:
ndynamic, nregular, nslow = counts
for i in range(ndynamic):
self.addPar('%s:d%d' % (name, i), True, DYNAMIC, DYNAMIC)
for i in range(nregular):
self.addPar('%s:r%d' % (name, i), True, REGULAR, REGULAR)
for i in range(nslow):
self.addPar('%s:s%d' % (name, i), False, SLOW, SLOW)
self.counts = counts
pobj.stat = [now]
self.maxcycles -= 1
if self.maxcycles <= 0:
self.finish_event.set()
sys.exit() # stop thread
def addPar(self, name, readonly, poll, expected_polltype):
# self.count[polltype] += 1
expected_interval = self.pollinterval
if expected_polltype == SLOW:
expected_interval *= self.slow_pollfactor
elif expected_polltype == DYNAMIC and self.is_busy:
expected_interval *= self.fast_pollfactor
pobj = Parameter(name, readonly, poll, expected_polltype, expected_interval)
setattr(self, 'read_' + pobj.export, pobj.rfunc)
self.parameters[pobj.export] = pobj
return pobj
def isBusy(self):
return self.is_busy
class ServerStub:
def __init__(self):
self.dispatcher = DispatcherStub()
def pollOneParam(self, pname):
getattr(self, 'read_' + pname)()
def writeInitParams(self):
pass
class Base(Module):
def __init__(self):
srv = ServerStub()
super().__init__('mod', logging.getLogger('dummy'), dict(description=''), srv)
self.dispatcher = srv.dispatcher
def __repr__(self):
rdict = self.__dict__.copy()
rdict.pop('parameters')
return 'Module(%r, counts=%r, f=%r, pollinterval=%g, is_busy=%r)' % (self.name,
self.counts, (self.fast_pollfactor, self.slow_pollfactor, 1),
self.pollinterval, self.is_busy)
def run(self, maxcycles):
self.dispatcher.maxcycles = maxcycles
self.dispatcher.finish_event = threading.Event()
self.initModule()
module_list = [
[Module('x', 3.0, 0.125, 10, False, auto=True),
Module('y', 3.0, 0.125, 10, False, auto=False)],
[Module('a', 1.0, 0.25, 4, True, (5, 5, 10)),
Module('b', 2.0, 0.25, 4, True, (5, 5, 50))],
[Module('c', 1.0, 0.25, 4, False, (5, 0, 0))],
[Module('d', 1.0, 0.25, 4, True, (0, 9, 0))],
[Module('e', 1.0, 0.25, 4, True, (0, 0, 9))],
[Module('f', 1.0, 0.25, 4, True, (0, 0, 0))],
]
@pytest.mark.parametrize('modules', module_list)
def test_Poller(modules):
# check for proper timing
def wait(timeout=None, base=self.triggerPoll):
"""simplified simulation
for overloaded in False, True:
artime.reset()
count = {DYNAMIC: 0, REGULAR: 0, SLOW: 0}
maxspan = {DYNAMIC: 0, REGULAR: 0, SLOW: 0}
pollTable = dict()
for module in modules:
Poller.add_to_table(pollTable, module)
for pobj in module.parameters.values():
if pobj.poll:
maxspan[pobj.polltype] = max(maxspan[pobj.polltype], pobj.interval)
count[pobj.polltype] += 1
pobj.reset()
assert len(pollTable) == 1
poller = pollTable[(Poller, 'common_iodev')]
artime.stop = poller.stop
poller._event = Event() # patch Event.wait
when an event is already set return True, else forward artificial time
"""
if base.is_set():
return True
artime.sleep(max(0.0, 99.9 if timeout is None else timeout))
return base.is_set()
assert (sum(count.values()) > 0) == bool(poller)
self.triggerPoll.wait = wait
self.startModule(MultiEvent())
assert self.dispatcher.finish_event.wait(1)
def started_callback(modules=modules):
for module in modules:
for pobj in module.parameters.values():
assert pobj.cnt == bool(pobj.poll) # all parameters have to be polled once
pobj.reset() # set maxspan and cnt to 0
if overloaded:
# overloaded scenario
artime.commtime = 1.0
ncycles = 10
if count[SLOW] > 0:
cycletime = (count[REGULAR] + 1) * count[SLOW] * 2
else:
cycletime = max(count[REGULAR], count[DYNAMIC]) * 2
artime.reset(cycletime * ncycles * 1.01) # poller will quit given time
poller.run(started_callback)
total = artime.time() - artime.STARTTIME
for module in modules:
for pobj in module.parameters.values():
if pobj.poll:
# average_span = total / (pobj.cnt + 1)
assert total / (pobj.cnt + 1) <= max(cycletime, pobj.interval * 1.1)
else:
# normal scenario
artime.commtime = 0.001
artime.reset(max(maxspan.values()) * 5) # poller will quit given time
poller.run(started_callback)
total = artime.time() - artime.STARTTIME
for module in modules:
for pobj in module.parameters.values():
if pobj.poll:
assert pobj.cnt > 0
assert pobj.maxspan <= maxspan[pobj.polltype] * 1.1
assert (pobj.cnt + 1) * pobj.interval >= total * 0.99
assert abs(pobj.span - pobj.interval) < 0.01
pobj.reset()
class Mod1(Base, Readable):
param1 = Parameter('', FloatRange())
param2 = Parameter('', FloatRange())
param3 = Parameter('', FloatRange())
param4 = Parameter('', FloatRange())
@ReadHandler(('param1', 'param2', 'param3'))
def read_param(self, name):
artime.sleep(1.0)
return 0
@nopoll
def read_param4(self):
return 0
def read_status(self):
artime.sleep(1.0)
return 0
def read_value(self):
artime.sleep(1.0)
return 0
@pytest.mark.parametrize(
'ncycles, pollinterval, slowinterval, mspan, pspan',
[ # normal case:
( 60, 5, 15, (4.9, 5.1), (14, 16)),
# pollinterval faster then reading: mspan max ~ 3 s (polls of value, status and ONE other parameter)
( 60, 1, 5, (0.9, 3.1), (5, 17)),
])
def test_poll(ncycles, pollinterval, slowinterval, mspan, pspan, monkeypatch):
monkeypatch.setattr(time, 'time', artime.time)
m = Mod1()
m.pollinterval = pollinterval
m.slowInterval = slowinterval
m.run(ncycles)
assert not hasattr(m.parameters['param4'], 'stat')
for pname in ['value', 'status']:
pobj = m.parameters[pname]
lowcnt = 0
print(pname, [t2 - t1 for t1, t2 in zip(pobj.stat[1:], pobj.stat[2:-1])])
for t1, t2 in zip(pobj.stat[1:], pobj.stat[2:-1]):
if t2 - t1 < mspan[0]:
lowcnt += 1
assert t2 - t1 <= mspan[1]
assert lowcnt <= 2
for pname in ['param1', 'param2', 'param3']:
pobj = m.parameters[pname]
lowcnt = 0
print(pname, [t2 - t1 for t1, t2 in zip(pobj.stat[1:], pobj.stat[2:-1])])
for t1, t2 in zip(pobj.stat[1:], pobj.stat[2:-1]):
if t2 - t1 < pspan[0]:
lowcnt += 1
assert t2 - t1 <= pspan[1]
assert lowcnt <= 2

View File

@ -26,6 +26,7 @@ import pytest
from secop.datatypes import FloatRange, IntRange, StringType, ValueType
from secop.errors import BadValueError, ConfigError, ProgrammingError
from secop.properties import HasProperties, Property
from secop.core import Parameter
def Prop(*args, name=None, **kwds):
@ -38,10 +39,10 @@ V_test_Property = [
[Prop(StringType(), 'default', extname='extname', mandatory=False),
dict(default='default', extname='extname', export=True, mandatory=False)
],
[Prop(IntRange(), '42', export=True, name='custom', mandatory=True),
[Prop(IntRange(), 42, export=True, name='custom', mandatory=True),
dict(default=42, extname='_custom', export=True, mandatory=True),
],
[Prop(IntRange(), '42', export=True, name='name'),
[Prop(IntRange(), 42, export=True, name='name'),
dict(default=42, extname='_name', export=True, mandatory=False)
],
[Prop(IntRange(), 42, '_extname', mandatory=True),
@ -85,12 +86,12 @@ def test_Property_basic():
Property('')
with pytest.raises(ValueError):
Property('', 1)
Property('', IntRange(), '42', 'extname', False, False)
Property('', IntRange(), 42, 'extname', False, False)
def test_Properties():
class Cls(HasProperties):
aa = Property('', IntRange(0, 99), '42', export=True)
aa = Property('', IntRange(0, 99), 42, export=True)
bb = Property('', IntRange(), 0, export=False)
assert Cls.aa.default == 42
@ -149,36 +150,44 @@ def test_Property_override():
assert o2.a == 3
with pytest.raises(ProgrammingError) as e:
class cx(c): # pylint: disable=unused-variable
class cx(c): # pylint: disable=unused-variable
def a(self):
pass
assert 'collides with' in str(e.value)
with pytest.raises(ProgrammingError) as e:
class cz(c): # pylint: disable=unused-variable
class cy(c): # pylint: disable=unused-variable
a = 's'
assert 'can not set' in str(e.value)
with pytest.raises(ProgrammingError) as e:
class cz(c): # pylint: disable=unused-variable
a = 's'
class cp(c): # pylint: disable=unused-variable
# overriding a Property with a Parameter is allowed
a = Parameter('x', IntRange())
def test_Properties_mro():
class A(HasProperties):
p = Property('base', StringType(), 'base', export='always')
class Base(HasProperties):
prop = Property('base', StringType(), 'base', export='always')
class B(A):
class SubA(Base):
pass
class C(A):
p = Property('sub', FloatRange(), extname='p')
class SubB(Base):
prop = Property('sub', FloatRange(), extname='prop')
class D(C, B):
p = 1
class FinalBA(SubB, SubA):
prop = 1
class E(B, C):
p = 2
class FinalAB(SubA, SubB):
prop = 2
assert B().exportProperties() == {'_p': 'base'}
assert D().exportProperties() == {'p': 1.0}
# in an older implementation the following would fail, as B.p is constructed first
# and then B.p overrides C.p
assert E().exportProperties() == {'p': 2.0}
assert SubA().exportProperties() == {'_prop': 'base'}
assert FinalBA().exportProperties() == {'prop': 1.0}
# in an older implementation the following would fail, as SubA.p is constructed first
# and then SubA.p overrides SubB.p
assert FinalAB().exportProperties() == {'prop': 2.0}

148
test/test_statemachine.py Normal file
View File

@ -0,0 +1,148 @@
# -*- 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:
# Markus Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
from secop.lib.statemachine import StateMachine, Stop, Retry
def rise(state):
state.step += 1
print('rise', state.step)
if state.init:
state.status = 'rise'
state.level += 1
if state.level > 3:
return turn
return Retry()
def turn(state):
state.step += 1
if state.init:
state.status = 'turn'
state.direction += 1
if state.direction > 3:
return fall
return Retry()
def fall(state):
state.step += 1
if state.init:
state.status = 'fall'
state.level -= 1
if state.level < 0:
raise ValueError('crash')
return Retry(0) # retry until crash!
def error_handler(state):
state.last_error_name = type(state.last_error).__name__
class LoggerStub:
def debug(self, fmt, *args):
print(fmt % args)
info = warning = exception = error = debug
handlers = []
class DummyThread:
def is_alive(self):
return True
def test_fun():
s = StateMachine(step=0, status='', threaded=False, logger=LoggerStub())
assert s.step == 0
assert s.status == ''
s.cycle() # do nothing
assert s.step == 0
s.start(rise, level=0, direction=0)
s.cycle()
for i in range(1, 4):
assert s.status == 'rise'
assert s.step == i
assert s.level == i
assert s.direction == 0
s.cycle()
for i in range(5, 8):
assert s.status == 'turn'
assert s.step == i
assert s.level == 4
assert s.direction == i - 4
s.cycle()
s.cycle() # -> crash
assert isinstance(s.last_error, ValueError)
assert str(s.last_error) == 'crash'
assert s.state is None
def test_max_chain():
s = StateMachine(step=0, status='', threaded=False, logger=LoggerStub())
s.start(fall, level=999+1, direction=0)
s.cycle()
assert isinstance(s.last_error, RuntimeError)
assert s.state is None
def test_stop():
s = StateMachine(step=0, status='', threaded=False, logger=LoggerStub())
s.start(rise, level=0, direction=0)
for _ in range(1, 3):
s.cycle()
s.stop()
s.cycle()
assert s.last_error is Stop
assert s.state is None
def test_std_error_handling():
s = StateMachine(step=0, status='', threaded=False, logger=LoggerStub())
s.start(rise, level=0, direction=0)
s.cycle()
s.level = None # -> TypeError on next step
s.cycle()
assert s.state is None # default error handler: stop machine
assert isinstance(s.last_error, TypeError)
assert not hasattr(s, 'last_error_name')
def test_default_error_handling():
s = StateMachine(step=0, status='', cleanup=error_handler, threaded=False, logger=LoggerStub())
s.start(rise, level=0, direction=0)
s.cycle()
s.level = None
s.cycle()
assert s.state is None
assert s.last_error_name == 'TypeError'
assert isinstance(s.last_error, TypeError)
def test_cleanup_on_restart():
s = StateMachine(step=0, status='', threaded=False, logger=LoggerStub())
s.start(rise, level=0, direction=0)
s.cycle()
s.start(turn)
s.cycle()
assert s.state is turn
assert s.last_error is None