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 no-space-check=trailing-comma,dict-separator
# Maximum number of lines in a module # 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 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab). # tab).

View File

@ -59,4 +59,4 @@ release:
build-pkg: 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,7 +15,6 @@ branches:
- work: current working version, usually in use on /home/l_samenv/frappy (and on neutron instruments) - 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 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 - 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 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. PSI internal files, not mixed. Mark local commits with '[PSI]' in the commit message.
@ -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: changes are done, eventually a sync step should happen:
1) ideally, this is done when work and wip match 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 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) 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 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 - core commits already pushed through gerrit are skipped
- all other commits are to be cherry-picked - all other commits are to be cherry-picked
7) when arrived at the point where the new working version should be, 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 .' copy new_wip branch to work with 'git checkout -B work'.
(note the dot!) and then commit this. Not sure if this works, as work is to be pushed to git.psi.ch.
8) continue with (6) if wip and work should differ We might first remove the remote branch with 'git push origin --delete work'.
9) do like (7), but for wip branch And then create again (git push origin work)?
10) delete new_wip branch, push master, wip and work branches 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 ## Procedure to update PPMS

View File

@ -27,12 +27,11 @@ import sys
import argparse import argparse
from os import path from os import path
import mlzlog
# Add import path for inplace usage # Add import path for inplace usage
sys.path.insert(0, path.abspath(path.join(path.dirname(__file__), '..'))) 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 from secop.server import Server
@ -60,15 +59,26 @@ def parseArgv(argv):
parser.add_argument('-c', parser.add_argument('-c',
'--cfgfiles', '--cfgfiles',
action='store', action='store',
help="comma separated list of cfg files\n" help="comma separated list of cfg files,\n"
"defaults to <name_of_the_instance>\n" "defaults to <name_of_the_instance>.\n"
"cfgfiles given without '.cfg' extension are searched in the configuration directory, " "cfgfiles given without '.cfg' extension are searched in the configuration directory, "
"else they are treated as path names", "else they are treated as path names",
default=None) 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', parser.add_argument('-t',
'--test', '--test',
action='store_true', 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) default=False)
return parser.parse_args(argv) return parser.parse_args(argv)
@ -80,9 +90,12 @@ def main(argv=None):
args = parseArgv(argv[1:]) args = parseArgv(argv[1:])
loglevel = 'debug' if args.verbose else ('error' if args.quiet else 'info') 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: if args.daemonize:
srv.start() 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 description = Lsc Simulation at PSI
[interface tcp] [INTERFACE]
type = tcp uri = tcp://5000
bindto = 0.0.0.0
bindport = 5000
[module res] [res]
class = secop_psi.ls370res.ResChannel class = secop_psi.ls370res.ResChannel
.channel = 3 channel = 3
.description = resistivity description = resistivity
.main = lsmain main = lsmain
.iodev = lscom io = lscom
[module lsmain] [lsmain]
class = secop_psi.ls370res.Main class = secop_psi.ls370res.Main
.description = main control of Lsc controller description = main control of Lsc controller
.iodev = lscom io = lscom
[module lscom] [lscom]
class = secop_psi.ls370sim.Ls370Sim class = secop_psi.ls370sim.Ls370Sim
.description = simulated serial communicator to a LS 370 description = simulated serial communicator to a LS 370
.visibility = 3 visibility = 3

View File

@ -1,24 +1,20 @@
[NODE] [node LscSIM.psi.ch]
id = ls370res.psi.ch
description = Lsc370 Test description = Lsc370 Test
[INTERFACE] [interface tcp]
uri = tcp://5000 type = tcp
bindto = 0.0.0.0
bindport = 5000
[lsmain_iodev] [module lsmain]
description = the communication device
class = secop_psi.ls370res.StringIO
uri = localhost:4567
[lsmain]
class = secop_psi.ls370res.Main class = secop_psi.ls370res.Main
description = main control of Lsc controller description = main control of Lsc controller
iodev = lsmain_iodev uri = localhost:4567
[res] [module res]
class = secop_psi.ls370res.ResChannel class = secop_psi.ls370res.ResChannel
iexc = '1mA' vexc = '2mV'
channel = 5 channel = 3
description = resistivity description = resistivity
main = lsmain main = lsmain
# the auto created iodev from 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] [tt]
class = secop_psi.ppms.Temp class = secop_psi.ppms.Temp
description = main temperature description = main temperature
iodev = ppms io = ppms
[mf] [mf]
class = secop_psi.ppms.Field class = secop_psi.ppms.Field
target.min = -9 target.min = -9
target.max = 9 target.max = 9
.description = magnetic field description = magnetic field
.iodev = ppms io = ppms
[pos] [pos]
class = secop_psi.ppms.Position class = secop_psi.ppms.Position
.description = sample rotator description = sample rotator
.iodev = ppms io = ppms
[lev] [lev]
class = secop_psi.ppms.Level class = secop_psi.ppms.Level
.description = helium level description = helium level
.iodev = ppms io = ppms
[chamber] [chamber]
class = secop_psi.ppms.Chamber class = secop_psi.ppms.Chamber
.description = chamber state description = chamber state
.iodev = ppms io = ppms
[r1] [r1]
class = secop_psi.ppms.BridgeChannel class = secop_psi.ppms.BridgeChannel
.description = resistivity channel 1 description = resistivity channel 1
.no = 1 no = 1
value.unit = Ohm value.unit = Ohm
.iodev = ppms io = ppms
[r2] [r2]
class = secop_psi.ppms.BridgeChannel class = secop_psi.ppms.BridgeChannel
.description = resistivity channel 2 description = resistivity channel 2
.no = 2 no = 2
value.unit = Ohm value.unit = Ohm
.iodev = ppms io = ppms
[r3] [r3]
class = secop_psi.ppms.BridgeChannel class = secop_psi.ppms.BridgeChannel
.description = resistivity channel 3 description = resistivity channel 3
.no = 3 no = 3
value.unit = Ohm value.unit = Ohm
.iodev = ppms io = ppms
[r4] [r4]
class = secop_psi.ppms.BridgeChannel class = secop_psi.ppms.BridgeChannel
.description = resistivity channel 4 description = resistivity channel 4
.no = 4 no = 4
value.unit = Ohm value.unit = Ohm
.iodev = ppms io = ppms
[i1] [i1]
class = secop_psi.ppms.Channel class = secop_psi.ppms.Channel
.description = current channel 1 description = current channel 1
.no = 1 no = 1
value.unit = uA value.unit = uA
.iodev = ppms io = ppms
[i2] [i2]
class = secop_psi.ppms.Channel class = secop_psi.ppms.Channel
.description = current channel 2 description = current channel 2
.no = 2 no = 2
value.unit = uA value.unit = uA
.iodev = ppms io = ppms
[i3] [i3]
class = secop_psi.ppms.Channel class = secop_psi.ppms.Channel
.description = current channel 3 description = current channel 3
.no = 3 no = 3
value.unit = uA value.unit = uA
.iodev = ppms io = ppms
[i4] [i4]
class = secop_psi.ppms.Channel class = secop_psi.ppms.Channel
.description = current channel 4 description = current channel 4
.no = 4 no = 4
value.unit = uA value.unit = uA
.iodev = ppms io = ppms
[v1] [v1]
class = secop_psi.ppms.DriverChannel class = secop_psi.ppms.DriverChannel
.description = voltage channel 1 description = voltage channel 1
.no = 1 no = 1
value.unit = V value.unit = V
.iodev = ppms io = ppms
[v2] [v2]
class = secop_psi.ppms.DriverChannel class = secop_psi.ppms.DriverChannel
.description = voltage channel 2 description = voltage channel 2
.no = 2 no = 2
value.unit = V value.unit = V
.iodev = ppms io = ppms
[tv] [tv]
class = secop_psi.ppms.UserChannel class = secop_psi.ppms.UserChannel
.description = VTI temperature description = VTI temperature
enabled = 1 enabled = 1
value.unit = K value.unit = K
.iodev = ppms io = ppms
[ts] [ts]
class = secop_psi.ppms.UserChannel class = secop_psi.ppms.UserChannel
.description = sample temperature description = sample temperature
enabled = 1 enabled = 1
value.unit = K value.unit = K
.iodev = ppms io = ppms
[ppms] [ppms]
class = secop_psi.ppms.Main class = secop_psi.ppms.Main
.description = the main and poller module description = the main and poller module
.class_id = QD.MULTIVU.PPMS.1 class_id = QD.MULTIVU.PPMS.1
.visibility = 3 visibility = 3
pollinterval = 2 pollinterval = 2

View File

@ -5,26 +5,29 @@ description = [sim] uniaxial pressure device
[INTERFACE] [INTERFACE]
uri=tcp://5000 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] [force]
class = secop_psi.uniax.Uniax class = secop_psi.uniax.Uniax
description = uniax driver description = uniax driver
motor = drv motor = drv
transducer = transducer 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] [res]
class = secop.simulation.SimReadable class = secop.simulation.SimReadable
description = raw temperature sensor on the stick description = raw temperature sensor on the stick
@ -37,5 +40,5 @@ value.datatype = {"type":"double", "unit":"Ohm"}
class=secop_psi.softcal.Sensor class=secop_psi.softcal.Sensor
description=temperature sensor, soft calibration description=temperature sensor, soft calibration
rawsensor=res rawsensor=res
calib = X132254.340 calib = X132254
value.unit = "K" value.unit = "K"

View File

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

34
ci/Jenkinsfile vendored
View File

@ -30,6 +30,8 @@ def changedFiles = '';
def run_pylint(pyver) { def run_pylint(pyver) {
stage ('pylint-' + pyver) { stage ('pylint-' + pyver) {
def cpylint = "RUNNING"
gerritPostCheck(["jenkins:pylint_${pyver}": cpylint])
def status = 'OK' def status = 'OK'
changedFiles = sh returnStdout: true, script: '''\ changedFiles = sh returnStdout: true, script: '''\
#!/bin/bash #!/bin/bash
@ -68,18 +70,15 @@ fi
echo "pylint result: $res" echo "pylint result: $res"
this.verifyresult.put('pylint'+pyver, 1) this.verifyresult.put('pylint'+pyver, 1)
cpylint = "SUCCESSFUL"
if ( res != 0 ) { if ( res != 0 ) {
currentBuild.result='FAILURE' currentBuild.result='FAILURE'
this.verifyresult.put('pylint'+ pyver, -1) this.verifyresult.put('pylint'+ pyver, -1)
status = 'FAILURE' status = 'FAILURE'
cpylint = "FAILED"
} }
gerritverificationpublisher([ gerritPostCheck(["jenkins:pylint_${pyver}": cpylint])
verifyStatusValue: this.verifyresult['pylint'+pyver],
verifyStatusCategory: 'pylint ',
verifyStatusName: 'pylint-'+pyver,
verifyStatusReporter: 'jenkins',
verifyStatusRerun: '!recheck'])
archiveArtifacts([allowEmptyArchive: true, archiveArtifacts([allowEmptyArchive: true,
artifacts: 'pylint-*.txt']) artifacts: 'pylint-*.txt'])
recordIssues([enabledForFailure: true, recordIssues([enabledForFailure: true,
@ -99,6 +98,8 @@ fi
def run_tests(pyver) { def run_tests(pyver) {
stage('Test:' + pyver) { stage('Test:' + pyver) {
def cpytest = "RUNNING"
gerritPostCheck(["jenkins:pytest_${pyver}":"RUNNING"])
writeFile file: 'setup.cfg', text: ''' writeFile file: 'setup.cfg', text: '''
[tool:pytest] [tool:pytest]
addopts = --junit-xml=pytest.xml --junit-prefix=''' + pyver addopts = --junit-xml=pytest.xml --junit-prefix=''' + pyver
@ -116,18 +117,15 @@ python3 setup.py develop
make test make test
''' '''
verifyresult.put(pyver, 1) verifyresult.put(pyver, 1)
cpytest = "SUCCESSFUL"
} }
} catch (all) { } catch (all) {
currentBuild.result = 'FAILURE' currentBuild.result = 'FAILURE'
status = 'FAILURE' status = 'FAILURE'
cpytest= "FAILED"
verifyresult.put(pyver, -1) verifyresult.put(pyver, -1)
} }
gerritverificationpublisher([ gerritPostCheck(["jenkins:pytest_${pyver}":cpytest])
verifyStatusValue: verifyresult[pyver],
verifyStatusCategory: 'test ',
verifyStatusName: 'pytest-'+pyver,
verifyStatusReporter: 'jenkins',
verifyStatusRerun: '!recheck'])
step([$class: 'JUnitResultArchiver', allowEmptyResults: true, step([$class: 'JUnitResultArchiver', allowEmptyResults: true,
keepLongStdio: true, testResults: 'pytest.xml']) keepLongStdio: true, testResults: 'pytest.xml'])
@ -138,6 +136,8 @@ make test
} }
def run_docs() { def run_docs() {
def cdocs = "RUNNING"
gerritPostCheck(["jenkins:docs":cdocs])
stage('prepare') { stage('prepare') {
sh ''' sh '''
. /home/jenkins/secopvenv/bin/activate . /home/jenkins/secopvenv/bin/activate
@ -185,15 +185,9 @@ def run_docs() {
stage('store html doc for build') { stage('store html doc for build') {
publishHTML([allowMissing: false, alwaysLinkToLastBuild: false, keepAll: true, reportDir: 'doc/_build/html', reportFiles: 'index.html', reportName: 'Built documentation', reportTitles: '']) publishHTML([allowMissing: false, alwaysLinkToLastBuild: false, keepAll: true, reportDir: 'doc/_build/html', reportFiles: 'index.html', reportName: 'Built documentation', reportTitles: ''])
gerritverificationpublisher([ cdocs = "SUCCESSFUL"
verifyStatusValue: 1,
verifyStatusCategory: 'test ',
verifyStatusName: 'doc',
verifyStatusReporter: 'jenkins',
verifyStatusRerun: '@recheck'
])
} }
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 secop-core (0.11.6) unstable; urgency=medium
* fix secop-generator * fix secop-generator
@ -133,7 +241,7 @@ secop-core (0.10.5) unstable; urgency=low
[ Jenkins ] [ 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 secop-core (0.10.3) unstable; urgency=low
@ -142,7 +250,7 @@ secop-core (0.10.3) unstable; urgency=low
[ Jenkins ] [ 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 secop-core (0.10.2) unstable; urgency=low
@ -153,7 +261,7 @@ secop-core (0.10.2) unstable; urgency=low
[ Jenkins ] [ 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 secop-core (0.10.1) unstable; urgency=low
@ -162,7 +270,7 @@ secop-core (0.10.1) unstable; urgency=low
[ Jenkins ] [ 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 secop-core (0.10.0) unstable; urgency=low
@ -171,7 +279,7 @@ secop-core (0.10.0) unstable; urgency=low
[ Jenkins ] [ 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 secop-core (0.9.0) unstable; urgency=low
@ -198,7 +306,7 @@ secop-core (0.9.0) unstable; urgency=low
[ Jenkins ] [ 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 secop-core (0.8.1) unstable; urgency=low
@ -207,7 +315,7 @@ secop-core (0.8.1) unstable; urgency=low
[ Jenkins ] [ 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 secop-core (0.8.0) unstable; urgency=low
@ -275,7 +383,7 @@ secop-core (0.8.0) unstable; urgency=low
[ Jenkins ] [ 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 secop-core (0.7.0) unstable; urgency=low
@ -311,7 +419,7 @@ secop-core (0.7.0) unstable; urgency=low
[ Jenkins ] [ 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 secop-core (0.6.4) unstable; urgency=low
@ -376,7 +484,7 @@ secop-core (0.6.4) unstable; urgency=low
[ Jenkins ] [ 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 secop-core (0.6.3) unstable; urgency=low
@ -390,7 +498,7 @@ secop-core (0.6.3) unstable; urgency=low
[ Jenkins ] [ 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 secop-core (0.6.2) unstable; urgency=low
@ -429,7 +537,7 @@ secop-core (0.6.2) unstable; urgency=low
[ Jenkins ] [ 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 secop-core (0.6.1) unstable; urgency=low
@ -438,7 +546,7 @@ secop-core (0.6.1) unstable; urgency=low
[ Jenkins ] [ 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 secop-core (0.6.0) unstable; urgency=low
@ -458,7 +566,7 @@ secop-core (0.6.0) unstable; urgency=low
[ Jenkins ] [ 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 secop-core (0.5.0) unstable; urgency=low
@ -521,7 +629,7 @@ secop-core (0.5.0) unstable; urgency=low
[ Jenkins ] [ 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 secop-core (0.4.4) unstable; urgency=low
@ -530,7 +638,7 @@ secop-core (0.4.4) unstable; urgency=low
[ Jenkins ] [ 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 secop-core (0.4.3) unstable; urgency=low
@ -539,7 +647,7 @@ secop-core (0.4.3) unstable; urgency=low
[ Jenkins ] [ 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 secop-core (0.4.2) unstable; urgency=low
@ -548,7 +656,7 @@ secop-core (0.4.2) unstable; urgency=low
[ Jenkins ] [ 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 secop-core (0.4.1) unstable; urgency=low
@ -557,7 +665,7 @@ secop-core (0.4.1) unstable; urgency=low
[ Jenkins ] [ 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 secop-core (0.4.0) unstable; urgency=low
@ -567,7 +675,7 @@ secop-core (0.4.0) unstable; urgency=low
[ Jenkins ] [ 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 secop-core (0.3.0) unstable; urgency=low
@ -633,7 +741,7 @@ secop-core (0.3.0) unstable; urgency=low
[ Jenkins ] [ 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 secop-core (0.2.0) unstable; urgency=low
@ -642,7 +750,7 @@ secop-core (0.2.0) unstable; urgency=low
[ Jenkins ] [ 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 secop-core (0.1.1) unstable; urgency=low
@ -651,7 +759,7 @@ secop-core (0.1.1) unstable; urgency=low
[ Jenkins ] [ 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 secop-core (0.1.0) unstable; urgency=low
@ -660,7 +768,7 @@ secop-core (0.1.0) unstable; urgency=low
[ Jenkins ] [ 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 secop-core (0.0.8) unstable; urgency=low
@ -669,7 +777,7 @@ secop-core (0.0.8) unstable; urgency=low
[ Jenkins ] [ 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 secop-core (0.0.7) unstable; urgency=low
@ -678,7 +786,7 @@ secop-core (0.0.7) unstable; urgency=low
[ Jenkins ] [ 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 secop-core (0.0.6) unstable; urgency=low
@ -688,7 +796,7 @@ secop-core (0.0.6) unstable; urgency=low
[ Jenkins ] [ 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 secop-core (0.0.5) unstable; urgency=low
@ -697,7 +805,7 @@ secop-core (0.0.5) unstable; urgency=low
[ Jenkins ] [ 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 secop-core (0.0.4) unstable; urgency=low
@ -706,7 +814,7 @@ secop-core (0.0.4) unstable; urgency=low
[ Jenkins ] [ 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 secop-core (0.0.3) unstable; urgency=low
@ -716,7 +824,7 @@ secop-core (0.0.3) unstable; urgency=low
[ Jenkins ] [ 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 secop-core (0.0.2) unstable; urgency=medium
@ -794,4 +902,4 @@ secop-core (0.0.2) unstable; urgency=medium
[ Jenkins ] [ 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-server
usr/bin/secop-console
usr/lib/python3.*/dist-packages/secop/*.py usr/lib/python3.*/dist-packages/secop/*.py
usr/lib/python3.*/dist-packages/secop/lib usr/lib/python3.*/dist-packages/secop/lib
usr/lib/python3.*/dist-packages/secop/client usr/lib/python3.*/dist-packages/secop/client

View File

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

View File

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

View File

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

View File

@ -31,10 +31,16 @@ from secop.datatypes import ArrayOf, BLOBType, BoolType, EnumType, \
from secop.iohandler import IOHandler, IOHandlerBase from secop.iohandler import IOHandler, IOHandlerBase
from secop.lib.enum import Enum from secop.lib.enum import Enum
from secop.modules import Attached, Communicator, \ 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.params import Command, Parameter
from secop.poller import AUTO, DYNAMIC, REGULAR, SLOW
from secop.properties import Property from secop.properties import Property
from secop.proxy import Proxy, SecNode, proxy_class 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.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.""" """Define validated data types."""
# pylint: disable=abstract-method # pylint: disable=abstract-method, too-many-lines
import sys import sys
@ -30,20 +30,12 @@ from base64 import b64decode, b64encode
from secop.errors import BadValueError, \ from secop.errors import BadValueError, \
ConfigError, ProgrammingError, ProtocolError ConfigError, ProgrammingError, ProtocolError
from secop.lib import clamp from secop.lib import clamp, generalConfig
from secop.lib.enum import Enum from secop.lib.enum import Enum
from secop.parse import Parser from secop.parse import Parser
from secop.properties import HasProperties, Property from secop.properties import HasProperties, Property
# Only export these classes for 'from secop.datatypes import *' generalConfig.set_default('lazy_number_validation', False)
__all__ = [
'DataType', 'get_datatype',
'FloatRange', 'IntRange', 'ScaledInteger',
'BoolType', 'EnumType',
'BLOBType', 'StringType', 'TextType',
'TupleOf', 'ArrayOf', 'StructOf',
'CommandType', 'StatusType',
]
# *DEFAULT* limits for IntRange/ScaledIntegers transport serialisation # *DEFAULT* limits for IntRange/ScaledIntegers transport serialisation
DEFAULT_MIN_INT = -16777216 DEFAULT_MIN_INT = -16777216
@ -53,6 +45,11 @@ UNLIMITED = 1 << 64 # internal limit for integers, is probably high enough for
Parser = Parser() Parser = Parser()
class DiscouragedConversion(BadValueError):
"""the discouraged conversion string - > float happened"""
log_message = True
# base class for all DataTypes # base class for all DataTypes
class DataType(HasProperties): class DataType(HasProperties):
"""base class for all data types""" """base class for all data types"""
@ -63,7 +60,7 @@ class DataType(HasProperties):
def __call__(self, value): def __call__(self, value):
"""check if given value (a python obj) is valid for this datatype """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 raise NotImplementedError
def from_string(self, text): def from_string(self, text):
@ -191,10 +188,16 @@ class FloatRange(DataType):
return self.get_info(type='double') return self.get_info(type='double')
def __call__(self, value): def __call__(self, value):
try:
value += 0.0 # do not accept strings here
except Exception:
try: try:
value = float(value) value = float(value)
except Exception: except Exception:
raise BadValueError('Can not convert %r to float' % value) from None 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 # map +/-infty to +/-max possible number
value = clamp(-sys.float_info.max, value, sys.float_info.max) 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 ' '.join([self.fmtstr % value, unit])
return self.fmtstr % value 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): def compatible(self, other):
if not isinstance(other, (FloatRange, ScaledInteger)): if not isinstance(other, (FloatRange, ScaledInteger)):
raise BadValueError('incompatible datatypes') raise BadValueError('incompatible datatypes')
@ -264,11 +279,17 @@ class IntRange(DataType):
return self.get_info(type='int') return self.get_info(type='int')
def __call__(self, value): def __call__(self, value):
try:
fvalue = value + 0.0 # do not accept strings here
value = int(value)
except Exception:
try: try:
fvalue = float(value) fvalue = float(value)
value = int(value) value = int(value)
except Exception: except Exception:
raise BadValueError('Can not convert %r to int' % value) from None 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: if not self.min <= value <= self.max or round(fvalue) != fvalue:
raise BadValueError('%r should be an int between %d and %d' % raise BadValueError('%r should be an int between %d and %d' %
(value, self.min, self.max)) (value, self.min, self.max))
@ -298,13 +319,15 @@ class IntRange(DataType):
return '%d' % value return '%d' % value
def compatible(self, other): def compatible(self, other):
if isinstance(other, IntRange): if isinstance(other, (IntRange, FloatRange, ScaledInteger)):
other(self.min) other(self.min)
other(self.max) other(self.max)
return return
# this will accept some EnumType, BoolType 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): for i in range(self.min, self.max + 1):
other(i) other(i)
raise BadValueError('incompatible datatypes')
class ScaledInteger(DataType): class ScaledInteger(DataType):
@ -368,10 +391,15 @@ class ScaledInteger(DataType):
max=int((self.max + self.scale * 0.5) // self.scale)) max=int((self.max + self.scale * 0.5) // self.scale))
def __call__(self, value): def __call__(self, value):
try:
value += 0.0 # do not accept strings here
except Exception:
try: try:
value = float(value) value = float(value)
except Exception: except Exception:
raise BadValueError('Can not convert %r to float' % value) from None 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), prec = max(self.scale, abs(value * self.relative_resolution),
self.absolute_resolution) self.absolute_resolution)
if self.min - prec <= value <= self.max + prec: if self.min - prec <= value <= self.max + prec:
@ -427,6 +455,9 @@ class EnumType(DataType):
super().__init__() super().__init__()
if members is not None: if members is not None:
kwds.update(members) kwds.update(members)
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._enum = Enum(enum_or_name, **kwds)
self.default = self._enum[self._enum.members[0]] self.default = self._enum[self._enum.members[0]]
@ -852,6 +883,8 @@ class StructOf(DataType):
:param optional: a list of optional members :param optional: a list of optional members
:param members: names as keys and types as values for all 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): def __init__(self, optional=None, **members):
super().__init__() super().__init__()
self.members = members self.members = members
@ -955,10 +988,9 @@ class CommandType(DataType):
return props return props
def __repr__(self): def __repr__(self):
argstr = repr(self.argument) if self.argument else ''
if self.result is None: if self.result is None:
return 'CommandType(%s)' % argstr return 'CommandType(%s)' % (repr(self.argument) if self.argument else '')
return 'CommandType(%s, %s)' % (argstr, repr(self.result)) return 'CommandType(%s, %s)' % (repr(self.argument), repr(self.result))
def __call__(self, value): def __call__(self, value):
"""return the validated argument value or raise""" """return the validated argument value or raise"""
@ -1119,37 +1151,26 @@ def floatargs(kwds):
DATATYPES = dict( DATATYPES = dict(
bool = lambda **kwds: bool = lambda **kwds:
BoolType(), BoolType(),
int = lambda min, max, **kwds: int = lambda min, max, **kwds:
IntRange(minval=min, maxval=max), IntRange(minval=min, maxval=max),
scaled = lambda scale, min, max, **kwds: scaled = lambda scale, min, max, **kwds:
ScaledInteger(scale=scale, minval=min*scale, maxval=max*scale, **floatargs(kwds)), ScaledInteger(scale=scale, minval=min*scale, maxval=max*scale, **floatargs(kwds)),
double = lambda min=None, max=None, **kwds: double = lambda min=None, max=None, **kwds:
FloatRange(minval=min, maxval=max, **floatargs(kwds)), FloatRange(minval=min, maxval=max, **floatargs(kwds)),
blob = lambda maxbytes, minbytes=0, **kwds: blob = lambda maxbytes, minbytes=0, **kwds:
BLOBType(minbytes=minbytes, maxbytes=maxbytes), BLOBType(minbytes=minbytes, maxbytes=maxbytes),
string = lambda minchars=0, maxchars=None, isUTF8=False, **kwds: string = lambda minchars=0, maxchars=None, isUTF8=False, **kwds:
StringType(minchars=minchars, maxchars=maxchars, isUTF8=isUTF8), StringType(minchars=minchars, maxchars=maxchars, isUTF8=isUTF8),
array = lambda maxlen, members, minlen=0, pname='', **kwds: array = lambda maxlen, members, minlen=0, pname='', **kwds:
ArrayOf(get_datatype(members, pname), minlen=minlen, maxlen=maxlen), ArrayOf(get_datatype(members, pname), minlen=minlen, maxlen=maxlen),
tuple = lambda members, pname='', **kwds: tuple = lambda members, pname='', **kwds:
TupleOf(*tuple((get_datatype(t, pname) for t in members))), TupleOf(*tuple((get_datatype(t, pname) for t in members))),
enum = lambda members, pname='', **kwds: enum = lambda members, pname='', **kwds:
EnumType(pname, members=members), EnumType(pname, members=members),
struct = lambda members, optional=None, pname='', **kwds: struct = lambda members, optional=None, pname='', **kwds:
StructOf(optional, **dict((n, get_datatype(t, pname)) for n, t in list(members.items()))), StructOf(optional, **dict((n, get_datatype(t, pname)) for n, t in list(members.items()))),
command = lambda argument=None, result=None, pname='', **kwds: command = lambda argument=None, result=None, pname='', **kwds:
CommandType(get_datatype(argument, pname), get_datatype(result)), CommandType(get_datatype(argument, pname), get_datatype(result)),
limit = lambda members, pname='', **kwds: limit = lambda members, pname='', **kwds:
LimitsType(get_datatype(members, pname)), LimitsType(get_datatype(members, pname)),
) )

View File

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

View File

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

View File

@ -26,7 +26,7 @@ from secop.gui.qt import QHBoxLayout, QSizePolicy, QSpacerItem, Qt, QWidget
class NodeDisplay(QWidget): class NodeDisplay(QWidget):
def __init__(self, file_path=None, parent=None): def __init__(self, file_path=None, parent=None):
QWidget.__init__(self, parent) super().__init__(parent)
loadUi(self, 'node_display.ui') loadUi(self, 'node_display.ui')
self.saved = bool(file_path) self.saved = bool(file_path)
self.created = self.tree_widget.set_file(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""" the datatype passed onto ValueWidget should be on of secop.datatypes"""
# TODO: like stated in docstring the datatype for parameters and # TODO: like stated in docstring the datatype for parameters and
# properties must be found out through their object # properties must be found out through their object
QTreeWidgetItem.__init__(self, parent) super().__init__(parent)
self.kind = kind self.kind = kind
self.name = name self.name = name
self.class_object = class_object self.class_object = class_object
@ -129,7 +129,7 @@ class ValueWidget(QWidget):
def __init__(self, name='', value='', datatype=None, kind='', parent=None): def __init__(self, name='', value='', datatype=None, kind='', parent=None):
# TODO: implement: change module/interface class # TODO: implement: change module/interface class
QWidget.__init__(self, parent) super().__init__(parent)
self.datatype = datatype self.datatype = datatype
self.layout = QVBoxLayout() self.layout = QVBoxLayout()
self.name_label = QLabel(name) self.name_label = QLabel(name)
@ -205,7 +205,7 @@ class ValueWidget(QWidget):
class ChangeNameDialog(QDialog): class ChangeNameDialog(QDialog):
def __init__(self, current_name='', invalid_names=None, parent=None): def __init__(self, current_name='', invalid_names=None, parent=None):
QWidget.__init__(self, parent) super().__init__(parent)
loadUi(self, 'change_name_dialog.ui') loadUi(self, 'change_name_dialog.ui')
self.invalid_names = invalid_names self.invalid_names = invalid_names
self.name.setText(current_name) self.name.setText(current_name)

View File

@ -29,7 +29,7 @@ from secop.modules import Module
from secop.params import Parameter from secop.params import Parameter
from secop.properties import Property from secop.properties import Property
from secop.protocol.interface.tcp import TCPServer from secop.protocol.interface.tcp import TCPServer
from secop.server import getGeneralConfig from secop.server import generalConfig
uipath = path.dirname(__file__) uipath = path.dirname(__file__)
@ -106,7 +106,7 @@ def get_file_paths(widget, open_file=True):
def get_modules(): def get_modules():
modules = {} modules = {}
base_path = getGeneralConfig()['basedir'] base_path = generalConfig.basedir
# pylint: disable=too-many-nested-blocks # pylint: disable=too-many-nested-blocks
for dirname in listdir(base_path): for dirname in listdir(base_path):
if dirname.startswith('secop_'): if dirname.startswith('secop_'):
@ -156,7 +156,7 @@ def get_interface_class_from_name(name):
def get_interfaces(): def get_interfaces():
# TODO class must be found out like for modules # TODO class must be found out like for modules
interfaces = [] interfaces = []
interface_path = path.join(getGeneralConfig()['basedir'], 'secop', interface_path = path.join(generalConfig.basedir, 'secop',
'protocol', 'interface') 'protocol', 'interface')
for filename in listdir(interface_path): for filename in listdir(interface_path):
if path.isfile(path.join(interface_path, filename)) and \ 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 get_props, loadUi, set_name_edit_style, setActionIcon
from secop.gui.qt import QComboBox, QDialog, QDialogButtonBox, QLabel, \ from secop.gui.qt import QComboBox, QDialog, QDialogButtonBox, QLabel, \
QLineEdit, QMenu, QPoint, QSize, QStandardItem, QStandardItemModel, \ QLineEdit, QMenu, QPoint, QSize, QStandardItem, QStandardItemModel, \
Qt, QTabBar, QTextEdit, QTreeView, QTreeWidget, QWidget, pyqtSignal Qt, QTabBar, QTextEdit, QTreeView, QTreeWidget, pyqtSignal
NODE = 'node' NODE = 'node'
MODULE = 'module' MODULE = 'module'
@ -47,7 +47,7 @@ class TreeWidget(QTreeWidget):
add_canceled = pyqtSignal() add_canceled = pyqtSignal()
def __init__(self, parent=None): def __init__(self, parent=None):
QTreeWidget.__init__(self, parent) super().__init__(parent)
self.file_path = None self.file_path = None
self.setIconSize(QSize(24, 24)) self.setIconSize(QSize(24, 24))
self.setSelectionMode(QTreeWidget.SingleSelection) self.setSelectionMode(QTreeWidget.SingleSelection)
@ -335,7 +335,7 @@ class AddDialog(QDialog):
"""Notes: """Notes:
self.get_value: is mapped to the specific method for getting self.get_value: is mapped to the specific method for getting
the value from self.value""" the value from self.value"""
QWidget.__init__(self, parent) super().__init__(parent)
loadUi(self, 'add_dialog.ui') loadUi(self, 'add_dialog.ui')
self.setWindowTitle('add %s' % kind) self.setWindowTitle('add %s' % kind)
self.kind = kind self.kind = kind
@ -402,7 +402,7 @@ class AddDialog(QDialog):
class TabBar(QTabBar): class TabBar(QTabBar):
def __init__(self, parent=None): def __init__(self, parent=None):
QTabBar.__init__(self, parent) super().__init__(parent)
self.setContextMenuPolicy(Qt.CustomContextMenu) self.setContextMenuPolicy(Qt.CustomContextMenu)
self.context_pos = QPoint(0, 0) self.context_pos = QPoint(0, 0)
self.menu = QMenu() self.menu = QMenu()
@ -436,7 +436,7 @@ class TabBar(QTabBar):
class TreeComboBox(QComboBox): class TreeComboBox(QComboBox):
def __init__(self, value_dict, parent=None): def __init__(self, value_dict, parent=None):
QComboBox.__init__(self, parent) super().__init__(parent)
self.tree_view = QTreeView() self.tree_view = QTreeView()
self.tree_view.setHeaderHidden(True) self.tree_view.setHeaderHidden(True)
self.tree_view.expanded.connect(self.resize_length) self.tree_view.expanded.connect(self.resize_length)

View File

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

View File

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

View File

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

View File

@ -39,7 +39,7 @@ class ParameterWidget(QWidget):
initvalue=None, initvalue=None,
readonly=True, readonly=True,
parent=None): parent=None):
super(ParameterWidget, self).__init__(parent) super().__init__(parent)
self._module = module self._module = module
self._paramcmd = paramcmd self._paramcmd = paramcmd
self._datatype = datatype self._datatype = datatype
@ -82,7 +82,6 @@ class GenericParameterWidget(ParameterWidget):
else: else:
value = fmtstr % (value.value,) value = fmtstr % (value.value,)
self.currentLineEdit.setText(value) self.currentLineEdit.setText(value)
# self.currentLineEdit.setText(str(value))
class EnumParameterWidget(GenericParameterWidget): 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 import threading
from secop.lib.asynconn import AsynConn, ConnectionClosed from secop.lib.asynconn import AsynConn, ConnectionClosed
from secop.datatypes import ArrayOf, BLOBType, BoolType, FloatRange, IntRange, StringType, TupleOf, ValueType from secop.datatypes import ArrayOf, BLOBType, BoolType, FloatRange, IntRange, \
from secop.errors import CommunicationFailedError, CommunicationSilentError, ConfigError StringType, TupleOf, ValueType
from secop.errors import CommunicationFailedError, CommunicationSilentError, \
ConfigError, ProgrammingError
from secop.modules import Attached, Command, \ from secop.modules import Attached, Command, \
Communicator, Done, Module, Parameter, Property 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]$') 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""" """Mixin for modules using a communicator"""
iodev = Attached() io = Attached()
uri = Property('uri for automatic creation of the attached communication module', uri = Property('uri for automatic creation of the attached communication module',
StringType(), default='') StringType(), default='')
iodevDict = {} ioDict = {}
ioClass = None
def __init__(self, name, logger, opts, srv): def __init__(self, name, logger, opts, srv):
iodev = opts.get('iodev') io = opts.get('io')
Module.__init__(self, name, logger, opts, srv) super().__init__(name, logger, opts, srv)
if self.uri: if self.uri:
opts = {'uri': self.uri, 'description': 'communication device for %s' % name, opts = {'uri': self.uri, 'description': 'communication device for %s' % name,
'export': False} 'export': False}
ioname = self.iodevDict.get(self.uri) ioname = self.ioDict.get(self.uri)
if not ioname: if not ioname:
ioname = iodev or name + '_iodev' ioname = io or name + '_io'
iodev = self.iodevClass(ioname, srv.log.getChild(ioname), opts, srv) io = self.ioClass(ioname, srv.log.getChild(ioname), opts, srv) # pylint: disable=not-callable
srv.modules[ioname] = iodev io.callingModule = []
self.iodevDict[self.uri] = ioname srv.modules[ioname] = io
self.iodev = ioname self.ioDict[self.uri] = ioname
elif not self.iodev: self.io = ioname
raise ConfigError("Module %s needs a value for either 'uri' or 'iodev'" % name) elif not io:
raise ConfigError("Module %s needs a value for either 'uri' or 'io'" % name)
def initModule(self): def initModule(self):
try: try:
self._iodev.read_is_connected() self.io.read_is_connected()
except (CommunicationFailedError, AttributeError): except (CommunicationFailedError, AttributeError):
# AttributeError: for missing _iodev? # AttributeError: read_is_connected is not required for an io object
pass pass
super().initModule() super().initModule()
def sendRecv(self, command): def communicate(self, *args):
return self._iodev.communicate(command) 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): class IOBase(Communicator):
@ -80,7 +108,7 @@ class IOBase(Communicator):
uri = Property('hostname:portnumber', datatype=StringType()) uri = Property('hostname:portnumber', datatype=StringType())
timeout = Parameter('timeout', datatype=FloatRange(0), default=2) timeout = Parameter('timeout', datatype=FloatRange(0), default=2)
wait_before = Parameter('wait time before sending', datatype=FloatRange(), default=0) 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) pollinterval = Parameter('reconnect interval', datatype=FloatRange(0), readonly=False, default=10)
_reconnectCallbacks = None _reconnectCallbacks = None
@ -89,6 +117,8 @@ class IOBase(Communicator):
_lock = None _lock = None
def earlyInit(self): def earlyInit(self):
super().earlyInit()
self._reconnectCallbacks = {}
self._lock = threading.RLock() self._lock = threading.RLock()
def connectStart(self): def connectStart(self):
@ -103,6 +133,9 @@ class IOBase(Communicator):
self._conn = None self._conn = None
self.is_connected = False self.is_connected = False
def doPoll(self):
self.read_is_connected()
def read_is_connected(self): def read_is_connected(self):
"""try to reconnect, when not connected """try to reconnect, when not connected
@ -139,9 +172,6 @@ class IOBase(Communicator):
if the callback fails or returns False, it is cleared 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): def callCallbacks(self):
@ -154,6 +184,9 @@ class IOBase(Communicator):
if removeme: if removeme:
self._reconnectCallbacks.pop(key) self._reconnectCallbacks.pop(key)
def communicate(self, command):
return NotImplementedError
class StringIO(IOBase): class StringIO(IOBase):
"""line oriented communicator """line oriented communicator
@ -218,6 +251,7 @@ class StringIO(IOBase):
if not self.is_connected: if not self.is_connected:
self.read_is_connected() # try to reconnect self.read_is_connected() # try to reconnect
if not self._conn: if not self._conn:
self.log.debug('can not connect to %r' % self.uri)
raise CommunicationSilentError('can not connect to %r' % self.uri) raise CommunicationSilentError('can not connect to %r' % self.uri)
try: try:
with self._lock: with self._lock:
@ -234,15 +268,15 @@ class StringIO(IOBase):
if garbage is None: # read garbage only once if garbage is None: # read garbage only once
garbage = self._conn.flush_recv() garbage = self._conn.flush_recv()
if garbage: if garbage:
self.log.debug('garbage: %r', garbage) self.comLog('garbage: %r', garbage)
self._conn.send(cmd + self._eol_write) 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) reply = self._conn.readline(self.timeout)
except ConnectionClosed as e: except ConnectionClosed as e:
self.closeConnection() self.closeConnection()
raise CommunicationFailedError('disconnected') from None raise CommunicationFailedError('disconnected') from None
reply = reply.decode(self.encoding) reply = reply.decode(self.encoding)
self.log.debug('recv: %s', reply) self.comLog('< %s', reply)
return reply return reply
except Exception as e: except Exception as e:
if str(e) == self._last_error: 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()]) 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): class BytesIO(IOBase):
identification = Property( identification = Property(
"""identification """identification
@ -330,14 +368,14 @@ class BytesIO(IOBase):
time.sleep(self.wait_before) time.sleep(self.wait_before)
garbage = self._conn.flush_recv() garbage = self._conn.flush_recv()
if garbage: if garbage:
self.log.debug('garbage: %r', garbage) self.comLog('garbage: %r', garbage)
self._conn.send(request) self._conn.send(request)
self.log.debug('send: %r', request) self.comLog('> %s', hexify(request))
reply = self._conn.readbytes(replylen, self.timeout) reply = self._conn.readbytes(replylen, self.timeout)
except ConnectionClosed as e: except ConnectionClosed as e:
self.closeConnection() self.closeConnection()
raise CommunicationFailedError('disconnected') from None raise CommunicationFailedError('disconnected') from None
self.log.debug('recv: %r', reply) self.comLog('< %s', hexify(reply))
return self.getFullReply(request, reply) return self.getFullReply(request, reply)
except Exception as e: except Exception as e:
if str(e) == self._last_error: if str(e) == self._last_error:
@ -346,6 +384,15 @@ class BytesIO(IOBase):
self.log.error(self._last_error) self.log.error(self._last_error)
raise 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): def readBytes(self, nbytes):
"""read bytes """read bytes
@ -362,7 +409,7 @@ class BytesIO(IOBase):
:return: the full reply (replyheader + additional bytes) :return: the full reply (replyheader + additional bytes)
When the reply length is variable, :meth:`communicate` should be called 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 Typically this method determines then the length of additional bytes from
the already received bytes (replyheader) and/or the request and calls the already received bytes (replyheader) and/or the request and calls
:meth:`readBytes` to get the remaining bytes. :meth:`readBytes` to get the remaining bytes.

View File

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

View File

@ -27,40 +27,126 @@ import socket
import sys import sys
import threading import threading
import traceback import traceback
from configparser import ConfigParser
from os import environ, path from os import environ, path
class GeneralConfig:
"""generalConfig holds server configuration items
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.
"""
def __init__(self):
self._config = None
self.defaults = {} #: default values. may be set before or after :meth:`init`
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__), '..', '..')) 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'): if path.splitext(sys.executable)[1] == ".exe" and not path.basename(sys.executable).startswith('python'):
CONFIG = { # special MS windows environment
'piddir': './', cfg.update(piddir='./', logdir='./log', confdir='./')
'logdir': './log', elif path.exists(path.join(repodir, '.git')):
'confdir': './', # running from git repo
} cfg['confdir'] = path.join(repodir, 'cfg')
elif not path.exists(path.join(repodir, '.git')): # take logdir and piddir from <repodir>/cfg/generalConfig.cfg
CONFIG = {
'piddir': '/var/run/secop',
'logdir': '/var/log',
'confdir': '/etc/secop',
}
else: else:
CONFIG = { # running on installed system (typically with systemd)
'piddir': path.join(repodir, 'pid'), cfg.update(piddir='/var/run/frappy', logdir='/var/log', confdir='/etc/frappy')
'logdir': path.join(repodir, 'log'), if configfile is None:
'confdir': path.join(repodir, 'cfg'), configfile = environ.get('FRAPPY_CONFIG_FILE',
} path.join(cfg['confdir'], 'generalConfig.cfg'))
# overwrite with env variables SECOP_LOGDIR, SECOP_PIDDIR, SECOP_CONFDIR, if present if configfile and path.exists(configfile):
for dirname in CONFIG: parser = ConfigParser()
CONFIG[dirname] = environ.get('SECOP_%s' % dirname.upper(), CONFIG[dirname]) 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 # this is not customizable
CONFIG['basedir'] = repodir cfg['basedir'] = repodir
self._config = cfg
# TODO: if ever more general options are need, we should think about a general config file 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: class lazy_property:
@ -253,10 +339,6 @@ def getfqdn(name=''):
return socket.getfqdn(name) return socket.getfqdn(name)
def getGeneralConfig():
return CONFIG
def formatStatusBits(sword, labels, start=0): def formatStatusBits(sword, labels, start=0):
"""Return a list of labels according to bit state in `sword` starting """Return a list of labels according to bit state in `sword` starting
with bit `start` and the first label in `labels`. 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: if sword & (1 << i) and lbl:
result.append(lbl) result.append(lbl)
return result 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: class AsynConn:
timeout = 1 # inter byte timeout timeout = 1 # inter byte timeout
scheme = None
SCHEME_MAP = {} SCHEME_MAP = {}
connection = None # is not None, if connected connection = None # is not None, if connected
defaultport = None defaultport = None
@ -62,11 +63,11 @@ class AsynConn:
except (ValueError, TypeError, AssertionError): except (ValueError, TypeError, AssertionError):
if 'COM' in uri: if 'COM' in uri:
raise ValueError("the correct uri for a COM port is: " 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: if '/dev' in uri:
raise ValueError("the correct uri for a serial port is: " raise ValueError("the correct uri for a serial port is: "
"'serial:///dev/<tty>[?<option>=<value>[+<option>=value ...]]'") "'serial:///dev/<tty>[?<option>=<value>[+<option>=value ...]]'") from None
raise ValueError('invalid uri: %s' % uri) raise ValueError('invalid uri: %s' % uri) from None
iocls = cls.SCHEME_MAP['tcp'] iocls = cls.SCHEME_MAP['tcp']
uri = 'tcp://%s:%d' % host_port uri = 'tcp://%s:%d' % host_port
return object.__new__(iocls) return object.__new__(iocls)
@ -80,6 +81,8 @@ class AsynConn:
@classmethod @classmethod
def __init_subclass__(cls): def __init_subclass__(cls):
"""register subclass to scheme, if available"""
if cls.scheme:
cls.SCHEME_MAP[cls.scheme] = cls cls.SCHEME_MAP[cls.scheme] = cls
def disconnect(self): def disconnect(self):
@ -166,7 +169,7 @@ class AsynTcp(AsynConn):
self.connection = tcpSocket(uri, self.defaultport, self.timeout) self.connection = tcpSocket(uri, self.defaultport, self.timeout)
except (ConnectionRefusedError, socket.gaierror) as e: except (ConnectionRefusedError, socket.gaierror) as e:
# indicate that retrying might make sense # indicate that retrying might make sense
raise CommunicationFailedError(str(e)) raise CommunicationFailedError(str(e)) from None
def disconnect(self): def disconnect(self):
if self.connection: if self.connection:
@ -237,8 +240,8 @@ class AsynSerial(AsynConn):
options = dict((kv.split('=') for kv in uri[1].split('+'))) options = dict((kv.split('=') for kv in uri[1].split('+')))
except IndexError: # no uri[1], no options except IndexError: # no uri[1], no options
options = {} options = {}
except ValueError: except ValueError as e:
raise ConfigError('illegal serial options') raise ConfigError('illegal serial options') from e
parity = options.pop('parity', None) # only parity is to be treated as text parity = options.pop('parity', None) # only parity is to be treated as text
for k, v in options.items(): for k, v in options.items():
try: try:
@ -251,14 +254,12 @@ class AsynSerial(AsynConn):
if not fullname.startswith(name): if not fullname.startswith(name):
raise ConfigError('illegal parity: %s' % parity) raise ConfigError('illegal parity: %s' % parity)
options['parity'] = name[0] options['parity'] = name[0]
if 'timeout' in options: if 'timeout' not in options:
options['timeout'] = float(self.timeout)
else:
options['timeout'] = self.timeout options['timeout'] = self.timeout
try: try:
self.connection = Serial(dev, **options) self.connection = Serial(dev, **options)
except ValueError as e: except ValueError as e:
raise ConfigError(e) raise ConfigError(e) from None
# TODO: turn exceptions into ConnectionFailedError, where a retry makes sense # TODO: turn exceptions into ConnectionFailedError, where a retry makes sense
def disconnect(self): 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 # pylint: disable=possibly-unused-variable
def doc_EnumType(dt): def doc_EnumType(dt):
return 'one of %s' % str(tuple(dt._enum.keys())) return 'one of %s' % str(tuple(dt._enum.keys()))
def doc_ArrayOf(dt): 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): 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): def doc_CommandType(dt):
argument = short_doc(dt.argument) if dt.argument else '' argument = short_doc(dt.argument, True) if dt.argument else ''
result = ' -> %s' % short_doc(dt.result) if dt.result else '' result = ' -> %s' % short_doc(dt.result, True) if dt.result else ''
return '(%s)%s' % (argument, result) # return argument list only return '(%s)%s' % (argument, result) # return argument list only
def doc_NoneOr(dt): def doc_NoneOr(dt):
other = short_doc(dt.other) other = short_doc(dt.other, True)
return '%s or None' % other if other else None return '%s or None' % other if other else None
def doc_OrType(dt): 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 if None in types: # type is anyway broad: no doc
return None return None
return ' or '.join(types) return ' or '.join(types)
@ -104,14 +104,17 @@ def short_doc(datatype):
def doc_Stub(dt): def doc_Stub(dt):
return dt.name.replace('Type', '').replace('Range', '').lower() 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) result = SIMPLETYPES.get(clsname)
if result: if result:
return result return result
fun = locals().get('doc_' + clsname) fun = locals().get('doc_' + clsname)
if fun: if fun:
return fun(datatype) 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): def append_to_doc(cls, lines, itemcls, name, attrname, fmtfunc):

View File

@ -21,41 +21,51 @@
# ***************************************************************************** # *****************************************************************************
import threading import threading
import time
class MultiEvent(threading.Event): ETERNITY = 1e99
"""Class implementing multi event objects.
meth:`new` creates Event like objects
meth:'wait` waits for all of them being set
"""
class SingleEvent: class _SingleEvent:
"""Single Event """Single Event
remark: :meth:`wait` is not implemented on purpose remark: :meth:`wait` is not implemented on purpose
""" """
def __init__(self, multievent): def __init__(self, multievent, timeout, name=None):
self.multievent = multievent self.multievent = multievent
self.multievent._clear(self) self.multievent.clear_(self)
self.name = name
if timeout is None:
self.deadline = ETERNITY
else:
self.deadline = time.monotonic() + timeout
def clear(self): def clear(self):
self.multievent._clear(self) self.multievent.clear_(self)
def set(self): def set(self):
self.multievent._set(self) self.multievent.set_(self)
def is_set(self): def is_set(self):
return self in self.multievent.events return self in self.multievent.events
def __init__(self):
class MultiEvent(threading.Event):
"""Class implementing multi event objects."""
def __init__(self, default_timeout=None):
self.events = set() self.events = set()
self._lock = threading.Lock() 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__() super().__init__()
def new(self): def new(self, timeout=None, name=None):
"""create a new SingleEvent""" """create a single event like object"""
return self.SingleEvent(self) return _SingleEvent(self, timeout or self.default_timeout,
name or self.name or '<unnamed>')
def set(self): def set(self):
raise ValueError('a multievent must not be set directly') raise ValueError('a multievent must not be set directly')
@ -63,21 +73,69 @@ class MultiEvent(threading.Event):
def clear(self): def clear(self):
raise ValueError('a multievent must not be cleared directly') 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""" """internal: remove event from the event list"""
with self._lock: with self._lock:
self.events.discard(event) self.events.discard(event)
if self.events: if self.events:
return return
try:
for action in self._actions:
action()
except Exception:
pass # we silently ignore errors here
self._actions = []
super().set() super().set()
def _clear(self, event): def clear_(self, event):
"""internal: add event to the event list""" """internal: add event to the event list"""
with self._lock: with self._lock:
self.events.add(event) self.events.add(event)
super().clear() 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): 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 if not self.events: # do not wait if events are empty
return return True
super().wait(timeout) 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: if self._seq_fault_on_stop:
return self.Status.ERROR, self._seq_stopped return self.Status.ERROR, self._seq_stopped
return self.Status.WARN, self._seq_stopped return self.Status.WARN, self._seq_stopped
if hasattr(self, 'read_hw_status'): if hasattr(self, 'readHwStatus'):
return self.read_hw_status() return self.readHwStatus()
return self.Status.IDLE, '' return self.Status.IDLE, ''
def stop(self): def stop(self):
@ -153,7 +153,7 @@ class SequencerMixin:
self._seq_error = str(e) self._seq_error = str(e)
finally: finally:
self._seq_thread = None self._seq_thread = None
self.pollParams(0) self.doPoll()
def _seq_thread_inner(self, seq, store_init): def _seq_thread_inner(self, seq, store_init):
store = Namespace() 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""" """Define base classes for real Modules implemented in the server"""
import sys
import time import time
import threading
from collections import OrderedDict from collections import OrderedDict
from functools import wraps
from secop.datatypes import ArrayOf, BoolType, EnumType, FloatRange, \ from secop.datatypes import ArrayOf, BoolType, EnumType, FloatRange, \
IntRange, StatusType, StringType, TextType, TupleOf IntRange, StatusType, StringType, TextType, TupleOf, DiscouragedConversion
from secop.errors import BadValueError, ConfigError, InternalError, \ from secop.errors import BadValueError, ConfigError, \
ProgrammingError, SECoPError, SilentError, secop_error 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.lib.enum import Enum
from secop.params import Accessible, Command, Parameter from secop.params import Accessible, Command, Parameter
from secop.poller import BasicPoller, Poller
from secop.properties import HasProperties, Property 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): class HasAccessibles(HasProperties):
@ -57,6 +63,7 @@ class HasAccessibles(HasProperties):
merged_properties = {} # dict of dict of merged properties merged_properties = {} # dict of dict of merged properties
new_names = [] # list of names of new accessibles new_names = [] # list of names of new accessibles
override_values = {} # bare values overriding a parameter and methods overriding a command override_values = {} # bare values overriding a parameter and methods overriding a command
for base in reversed(cls.__mro__): for base in reversed(cls.__mro__):
for key, value in base.__dict__.items(): for key, value in base.__dict__.items():
if isinstance(value, Accessible): if isinstance(value, Accessible):
@ -66,27 +73,32 @@ class HasAccessibles(HasProperties):
accessibles[key] = value accessibles[key] = value
override_values.pop(key, None) override_values.pop(key, None)
elif key in accessibles: elif key in accessibles:
# either a bare value overriding a parameter
# or a method overriding a command
override_values[key] = value override_values[key] = value
for aname, aobj in accessibles.items(): for aname, aobj in list(accessibles.items()):
if aname in override_values: if aname in override_values:
aobj = aobj.copy() aobj = aobj.copy()
value = override_values[aname]
if value is None:
accessibles.pop(aname)
continue
aobj.merge(merged_properties[aname]) aobj.merge(merged_properties[aname])
aobj.override(override_values[aname]) aobj.override(value)
# replace the bare value by the created accessible # replace the bare value by the created accessible
setattr(cls, aname, aobj) setattr(cls, aname, aobj)
else: else:
aobj.merge(merged_properties[aname]) aobj.merge(merged_properties[aname])
accessibles[aname] = aobj accessibles[aname] = aobj
# rebuild order: (1) inherited items, (2) items from paramOrder, (3) new accessibles # rebuild order: (1) inherited items, (2) items from paramOrder, (3) new accessibles
# move (2) to the end # 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: if aname in accessibles:
accessibles.move_to_end(aname) accessibles.move_to_end(aname)
# ignore unknown names # ignore unknown names
# move (3) to the end # move (3) to the end
for aname in new_names: for aname in new_names:
if aname not in paramOrder:
accessibles.move_to_end(aname) accessibles.move_to_end(aname)
# note: for python < 3.6 the order of inherited items is not ensured between # note: for python < 3.6 the order of inherited items is not ensured between
# declarations within the same class # declarations within the same class
@ -100,12 +112,14 @@ class HasAccessibles(HasProperties):
# XXX: create getters for the units of params ?? # XXX: create getters for the units of params ??
# wrap of reading/writing funcs # wrap of reading/writing funcs
if isinstance(pobj, Command): if not isinstance(pobj, Parameter):
# nothing to do for now # nothing to do for Commands
continue continue
rfunc = getattr(cls, 'read_' + pname, None) 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 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 rfunc_handler:
if 'read_' + pname in cls.__dict__: if 'read_' + pname in cls.__dict__:
if 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 # create wrapper except when read function is already wrapped
if not wrapped: if not wrapped:
def wrapped_rfunc(self, pname=pname, rfunc=rfunc):
if rfunc: if rfunc:
self.log.debug("calling %r" % rfunc)
@wraps(rfunc) # handles __wrapped__ and __doc__
def new_rfunc(self, pname=pname, rfunc=rfunc):
with self.accessLock:
try: try:
value = rfunc(self) value = rfunc(self)
self.log.debug("rfunc(%s) returned %r" % (pname, value)) self.log.debug("read_%s returned %r", pname, value)
if value is Done: # the setter is already triggered
return getattr(self, pname)
except Exception as e: except Exception as e:
self.log.debug("rfunc(%s) failed %r" % (pname, e)) self.log.debug("read_%s failed with %r", pname, e)
self.announceUpdate(pname, None, e) self.announceUpdate(pname, None, e)
raise raise
else: if value is Done:
# return cached value return getattr(self, pname)
self.log.debug("rfunc(%s): return cached value" % pname)
value = self.accessibles[pname].value
setattr(self, pname, value) # important! trigger the setter setattr(self, pname, value) # important! trigger the setter
return value return value
if rfunc: new_rfunc.poll = getattr(rfunc, 'poll', True)
wrapped_rfunc.__doc__ = rfunc.__doc__ else:
setattr(cls, 'read_' + pname, wrapped_rfunc)
wrapped_rfunc.__wrapped__ = True 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)
if not pobj.readonly:
wfunc = getattr(cls, 'write_' + pname, None) wfunc = getattr(cls, 'write_' + pname, None)
wrapped = hasattr(wfunc, '__wrapped__') 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: if (wfunc is None or wrapped) and pobj.handler:
# ignore the handler, if a write function is present # ignore the handler, if a write function is present
# TODO: remove handler stuff here
wfunc = pobj.handler.get_write_func(pname) wfunc = pobj.handler.get_write_func(pname)
wrapped = False wrapped = False
# create wrapper except when write function is already wrapped # create wrapper except when write function is already wrapped
if not 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: if wfunc:
self.log.debug('calling %s %r(%r)' % (wfunc.__name__, wfunc, value))
returned_value = wfunc(self, value) @wraps(wfunc) # handles __wrapped__ and __doc__
if returned_value is Done: # the setter is already triggered 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) return getattr(self, pname)
if returned_value is not None: # goodie: accept missing return value setattr(self, pname, new_value) # important! trigger the setter
value = returned_value return new_value
else:
def new_wfunc(self, value, pname=pname):
setattr(self, pname, value) setattr(self, pname, value)
return value return value
if wfunc: new_wfunc.__doc__ = 'auto generated write method for ' + pname
wrapped_wfunc.__doc__ = wfunc.__doc__
setattr(cls, 'write_' + pname, wrapped_wfunc)
wrapped_wfunc.__wrapped__ = True
# check information about Command's new_wfunc.wrapped = True # indicate to subclasses that no more wrapping is needed
for attrname in cls.__dict__: setattr(cls, 'write_' + pname, new_wfunc)
if attrname.startswith('do_'):
# 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' raise ProgrammingError('%r: old style command %r not supported anymore'
% (cls.__name__, attrname)) % (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 = {} res = {}
# collect info about properties # collect info about properties
@ -193,6 +228,26 @@ class HasAccessibles(HasProperties):
cls.configurables = res 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): class Module(HasAccessibles):
"""basic module """basic module
@ -237,6 +292,9 @@ class Module(HasAccessibles):
extname='implementation') extname='implementation')
interface_classes = Property('offical highest interface-class of the module', ArrayOf(StringType()), interface_classes = Property('offical highest interface-class of the module', ArrayOf(StringType()),
extname='interface_classes') 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 # properties, parameters and commands are auto-merged upon subclassing
parameters = {} parameters = {}
@ -244,16 +302,24 @@ class Module(HasAccessibles):
# reference to the dispatcher (used for sending async updates) # reference to the dispatcher (used for sending async updates)
DISPATCHER = None DISPATCHER = None
attachedModules = None
pollerClass = Poller #: default poller used pollInfo = None
triggerPoll = None # trigger event for polls. used on io modules and modules without io
def __init__(self, name, logger, cfgdict, srv): def __init__(self, name, logger, cfgdict, srv):
# remember the dispatcher object (for the async callbacks) # remember the dispatcher object (for the async callbacks)
self.DISPATCHER = srv.dispatcher self.DISPATCHER = srv.dispatcher
self.omit_unchanged_within = getattr(self.DISPATCHER, 'omit_unchanged_within', 0.1)
self.log = logger self.log = logger
self.name = name self.name = name
self.valueCallbacks = {} self.valueCallbacks = {}
self.errorCallbacks = {} 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 = [] errors = []
# handle module properties # handle module properties
@ -298,13 +364,6 @@ class Module(HasAccessibles):
for aname, aobj in self.accessibles.items(): for aname, aobj in self.accessibles.items():
# make a copy of the Parameter/Command object # make a copy of the Parameter/Command object
aobj = aobj.copy() 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 if not self.export: # do not export parameters of a module not exported
aobj.export = False aobj.export = False
if aobj.export: if aobj.export:
@ -319,26 +378,23 @@ class Module(HasAccessibles):
# 2) check and apply parameter_properties # 2) check and apply parameter_properties
# specified as '<paramname>.<propertyname> = <propertyvalue>' # 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 for k, v in list(cfgdict.items()): # keep list() as dict may change during iter
if '.' in k[1:]: if '.' in k[1:]:
paramname, propname = k.split('.', 1) aname, propname = k.split('.', 1)
propvalue = cfgdict.pop(k) propvalue = cfgdict.pop(k)
paramobj = self.accessibles.get(paramname, None) aobj = self.accessibles.get(aname, None)
# paramobj might also be a command (not sure if this is needed) if aobj:
if paramobj:
# no longer needed, this conversion is done by DataTypeType.__call__:
# if propname == 'datatype':
# propvalue = get_datatype(propvalue, k)
try: try:
paramobj.setProperty(propname, propvalue) aobj.setProperty(propname, propvalue)
except KeyError: except KeyError:
errors.append("'%s.%s' does not exist" % errors.append("'%s.%s' does not exist" %
(paramname, propname)) (aname, propname))
except BadValueError as e: except BadValueError as e:
errors.append('%s.%s: %s' % errors.append('%s.%s: %s' %
(paramname, propname, str(e))) (aname, propname, str(e)))
else: else:
errors.append('%r not found' % paramname) errors.append('%r not found' % aname)
# 3) check config for problems: # 3) check config for problems:
# only accept remaining config items specified in parameters # only accept remaining config items specified in parameters
@ -361,9 +417,8 @@ class Module(HasAccessibles):
continue continue
if pname in cfgdict: 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> # 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: try:
pobj.value = pobj.datatype(cfgdict[pname]) pobj.value = pobj.datatype(cfgdict[pname])
self.writeDict[pname] = pobj.value self.writeDict[pname] = pobj.value
@ -376,7 +431,7 @@ class Module(HasAccessibles):
'value and was not given in config!' % pname) 'value and was not given in config!' % pname)
# we do not want to call the setter for this parameter for now, # we do not want to call the setter for this parameter for now,
# this should happen on the first read # 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, # above error will be triggered on activate after startup,
# when not all hardware parameters are read because of startup timeout # when not all hardware parameters are read because of startup timeout
pobj.value = pobj.datatype(pobj.datatype.default) pobj.value = pobj.datatype(pobj.datatype.default)
@ -386,10 +441,8 @@ class Module(HasAccessibles):
except BadValueError as e: except BadValueError as e:
# this should not happen, as the default is already checked in Parameter # 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 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> # 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 pobj.value = value
self.writeDict[pname] = value self.writeDict[pname] = value
else: else:
@ -424,11 +477,11 @@ class Module(HasAccessibles):
self.checkProperties() self.checkProperties()
except ConfigError as e: except ConfigError as e:
errors.append(str(e)) errors.append(str(e))
for pname, p in self.parameters.items(): for aname, aobj in self.accessibles.items():
try: try:
p.checkProperties() aobj.checkProperties()
except ConfigError as e: except (ConfigError, ProgrammingError) as e:
errors.append('%s: %s' % (pname, e)) errors.append('%s: %s' % (aname, e))
if errors: if errors:
raise ConfigError(errors) raise ConfigError(errors)
@ -441,26 +494,31 @@ class Module(HasAccessibles):
def announceUpdate(self, pname, value=None, err=None, timestamp=None): def announceUpdate(self, pname, value=None, err=None, timestamp=None):
"""announce a changed value or readerror""" """announce a changed value or readerror"""
with self.accessLock:
# TODO: remove readerror 'property' and replace value with exception
pobj = self.parameters[pname] pobj = self.parameters[pname]
timestamp = timestamp or time.time() timestamp = timestamp or time.time()
changed = pobj.value != value 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:
try: try:
# store the value even in case of error
pobj.value = pobj.datatype(value) pobj.value = pobj.datatype(value)
except Exception as e: except Exception as e:
err = secop_error(e) if isinstance(e, DiscouragedConversion):
if not changed and timestamp < ((pobj.timestamp or 0) if DiscouragedConversion.log_message:
+ self.DISPATCHER.OMIT_UNCHANGED_WITHIN): 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 # no change within short time -> omit
return return
pobj.timestamp = timestamp pobj.timestamp = timestamp or time.time()
pobj.readerror = err pobj.readerror = err
if pobj.export: if pobj.export:
self.DISPATCHER.announce_update(self.name, pname, pobj) self.DISPATCHER.announce_update(self.name, pname, pobj)
@ -495,15 +553,15 @@ class Module(HasAccessibles):
for pname in self.parameters: for pname in self.parameters:
errfunc = getattr(modobj, 'error_update_' + pname, None) errfunc = getattr(modobj, 'error_update_' + pname, None)
if errfunc: if errfunc:
def errcb(err, p=pname, m=modobj, efunc=errfunc): def errcb(err, p=pname, efunc=errfunc):
try: try:
efunc(err) efunc(err)
except Exception as e: except Exception as e:
m.announceUpdate(p, err=e) modobj.announceUpdate(p, err=e)
self.errorCallbacks[pname].append(errcb) self.errorCallbacks[pname].append(errcb)
else: else:
def errcb(err, p=pname, m=modobj): def errcb(err, p=pname):
m.announceUpdate(p, err=err) modobj.announceUpdate(p, err=err)
if pname in autoupdate: if pname in autoupdate:
self.errorCallbacks[pname].append(errcb) self.errorCallbacks[pname].append(errcb)
@ -516,8 +574,8 @@ class Module(HasAccessibles):
efunc(e) efunc(e)
self.valueCallbacks[pname].append(cb) self.valueCallbacks[pname].append(cb)
elif pname in autoupdate: elif pname in autoupdate:
def cb(value, p=pname, m=modobj): def cb(value, p=pname):
m.announceUpdate(p, value) modobj.announceUpdate(p, value)
self.valueCallbacks[pname].append(cb) self.valueCallbacks[pname].append(cb)
def isBusy(self, status=None): def isBusy(self, status=None):
@ -526,16 +584,54 @@ class Module(HasAccessibles):
return False return False
def earlyInit(self): def earlyInit(self):
# may be overriden in derived classes to init stuff """initialise module with stuff to be done before all modules are created"""
self.log.debug('empty %s.earlyInit()' % self.__class__.__name__) self.earlyInitDone = True
def initModule(self): 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): def startModule(self, start_events):
"""poll parameter <pname> with proper error handling""" """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: try:
getattr(self, 'read_' + pname)() rfunc()
except SilentError: except SilentError:
pass pass
except SECoPError as e: except SECoPError as e:
@ -543,6 +639,93 @@ class Module(HasAccessibles):
except Exception: except Exception:
self.log.error(formatException()) 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): def writeInitParams(self, started_callback=None):
"""write values for parameters with configured values """write values for parameters with configured values
@ -565,14 +748,15 @@ class Module(HasAccessibles):
if started_callback: if started_callback:
started_callback() started_callback()
def startModule(self, started_callback): def setRemoteLogging(self, conn, level):
"""runs after init of all modules if self.remoteLogHandler is None:
for handler in self.log.handlers:
started_callback to be called when the thread spawned by startModule if isinstance(handler, RemoteLogHandler):
has finished its initial work self.remoteLogHandler = handler
might return a timeout value, if different from default break
""" else:
mkthread(self.writeInitParams, started_callback) raise ValueError('remote handler not found')
self.remoteLogHandler.set_conn_level(self, conn, level)
class Readable(Module): class Readable(Module):
@ -587,63 +771,43 @@ class Readable(Module):
UNKNOWN=401, UNKNOWN=401,
) #: status codes ) #: 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()), status = Parameter('current status of the module', TupleOf(EnumType(Status), StringType()),
default=(Status.IDLE, ''), poll=True) default=(Status.IDLE, ''))
pollinterval = Parameter('sleeptime between polls', FloatRange(0.1, 120), pollinterval = Parameter('default poll interval', FloatRange(0.1, 120),
default=5, readonly=False) default=5, readonly=False, export=True)
def startModule(self, started_callback): def doPoll(self):
"""start basic polling thread""" self.read_value()
if self.pollerClass and issubclass(self.pollerClass, BasicPoller): self.read_status()
# 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
class Writable(Readable): class Writable(Readable):
"""basic writable module""" """basic writable module"""
disable_value_range_check = Property('disable value range check', BoolType(), default=False)
target = Parameter('target value of the module', target = Parameter('target value of the module',
default=0, readonly=False, datatype=FloatRange(unit='$')) 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): class Drivable(Writable):
"""basic drivable module""" """basic drivable module"""
@ -666,30 +830,12 @@ class Drivable(Writable):
""" """
return 300 <= (status or self.status)[0] < 390 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) @Command(None, result=None)
def stop(self): def stop(self):
"""cease driving, go to IDLE state""" """cease driving, go to IDLE state"""
class Communicator(Module): class Communicator(HasComlog, Module):
"""basic abstract communication module""" """basic abstract communication module"""
@Command(StringType(), result=StringType()) @Command(StringType(), result=StringType())
@ -703,19 +849,20 @@ class Communicator(Module):
class Attached(Property): 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, assign a module name to this property in the cfg file,
and the server will create an attribute with this module 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, basecls=Module, description='attached module', mandatory=True):
def __init__(self, attrname=None): self.basecls = basecls
self.attrname = attrname super().__init__(description, StringType(), mandatory=mandatory)
# 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 __repr__(self): def __get__(self, obj, owner):
return 'Attached(%s)' % (repr(self.attrname) if self.attrname else '') 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 import inspect
from secop.datatypes import BoolType, CommandType, DataType, \ from secop.datatypes import BoolType, CommandType, DataType, \
DataTypeType, EnumType, IntRange, NoneOr, OrType, \ DataTypeType, EnumType, NoneOr, OrType, \
StringType, StructOf, TextType, TupleOf, ValueType StringType, StructOf, TextType, TupleOf, ValueType
from secop.errors import BadValueError, ProgrammingError from secop.errors import BadValueError, ProgrammingError
from secop.properties import HasProperties, Property 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): class Accessible(HasProperties):
@ -134,24 +135,9 @@ class Parameter(Accessible):
* True: exported, name automatic. * True: exported, name automatic.
* a string: exported with custom name''', OrType(BoolType(), StringType()), * a string: exported with custom name''', OrType(BoolType(), StringType()),
export=False, default=True) 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( needscfg = Property(
'[internal] needs value in config', NoneOr(BoolType()), '[internal] needs value in config', NoneOr(BoolType()),
export=False, default=None) export=False, default=False)
optional = Property( optional = Property(
'[internal] is this parameter optional?', BoolType(), '[internal] is this parameter optional?', BoolType(),
export=False, settable=False, default=False) export=False, settable=False, default=False)
@ -171,6 +157,8 @@ class Parameter(Accessible):
def __init__(self, description=None, datatype=None, inherit=True, **kwds): def __init__(self, description=None, datatype=None, inherit=True, **kwds):
super().__init__() super().__init__()
if 'poll' in kwds and generalConfig.tolerate_poll_property:
kwds.pop('poll')
if datatype is None: if datatype is None:
# collect datatype properties. these are not applied, as we have no datatype # 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} 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} self.ownProperties = {k: getattr(self, k) for k in self.propertyDict}
def __get__(self, instance, owner): def __get__(self, instance, owner):
# not used yet
if instance is None: if instance is None:
return self return self
return instance.parameters[self.name].value return instance.parameters[self.name].value
@ -219,6 +206,9 @@ class Parameter(Accessible):
self.export = '_' + self.name self.export = '_' + self.name
else: else:
raise ProgrammingError('can not use %r as name of a Parameter' % self.name) 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): def copy(self):
"""return a (deep) copy of ourselfs""" """return a (deep) copy of ourselfs"""
@ -346,6 +336,9 @@ class Command(Accessible):
def __init__(self, argument=False, *, result=None, inherit=True, **kwds): def __init__(self, argument=False, *, result=None, inherit=True, **kwds):
super().__init__() 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) self.init(kwds)
if result or kwds or isinstance(argument, DataType) or not callable(argument): if result or kwds or isinstance(argument, DataType) or not callable(argument):
# normal case # normal case
@ -362,8 +355,9 @@ class Command(Accessible):
self.func = argument # this is the wrapped method! self.func = argument # this is the wrapped method!
if argument.__doc__: if argument.__doc__:
self.description = inspect.cleandoc(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._inherit = inherit # save for __set_name__
self.ownProperties = self.propertyValues.copy()
def __set_name__(self, owner, name): def __set_name__(self, owner, name):
self.name = name self.name = name
@ -372,7 +366,6 @@ class Command(Accessible):
(owner.__name__, name)) (owner.__name__, name))
self.datatype = CommandType(self.argument, self.result) self.datatype = CommandType(self.argument, self.result)
self.ownProperties = self.propertyValues.copy()
if self.export is True: if self.export is True:
predefined_cls = PREDEFINED_ACCESSIBLES.get(name, None) predefined_cls = PREDEFINED_ACCESSIBLES.get(name, None)
if predefined_cls is Command: if predefined_cls is Command:
@ -381,6 +374,9 @@ class Command(Accessible):
self.export = '_' + name self.export = '_' + name
else: else:
raise ProgrammingError('can not use %r as name of a Command' % name) from None 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: if not self._inherit:
for key, pobj in self.properties.items(): for key, pobj in self.properties.items():
if key not in self.propertyValues: if key not in self.propertyValues:
@ -397,6 +393,7 @@ class Command(Accessible):
"""called when used as decorator""" """called when used as decorator"""
if 'description' not in self.propertyValues and func.__doc__: if 'description' not in self.propertyValues and func.__doc__:
self.description = inspect.cleandoc(func.__doc__) self.description = inspect.cleandoc(func.__doc__)
self.ownProperties['description'] = self.description
self.func = func self.func = func
return self 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 os
import json import json
from secop.lib import getGeneralConfig from secop.lib import generalConfig
from secop.datatypes import EnumType 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 from secop.modules import HasAccessibles
@ -69,13 +69,13 @@ class PersistentParam(Parameter):
class PersistentMixin(HasAccessibles): class PersistentMixin(HasAccessibles):
def __init__(self, *args, **kwds): def __init__(self, *args, **kwds):
super().__init__(*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) os.makedirs(persistentdir, exist_ok=True)
self.persistentFile = os.path.join(persistentdir, '%s.%s.json' % (self.DISPATCHER.equipment_id, self.name)) self.persistentFile = os.path.join(persistentdir, '%s.%s.json' % (self.DISPATCHER.equipment_id, self.name))
self.initData = {} self.initData = {}
for pname in self.parameters: for pname in self.parameters:
pobj = self.parameters[pname] 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 self.initData[pname] = pobj.value
if pobj.persistent == 'auto': if pobj.persistent == 'auto':
def cb(value, m=self): def cb(value, m=self):
@ -103,6 +103,7 @@ class PersistentMixin(HasAccessibles):
try: try:
value = pobj.datatype.import_value(self.persistentData[pname]) value = pobj.datatype.import_value(self.persistentData[pname])
pobj.value = value pobj.value = value
pobj.readerror = None
if not pobj.readonly: if not pobj.readonly:
writeDict[pname] = value writeDict[pname] = value
except Exception as e: except Exception as e:
@ -144,5 +145,6 @@ class PersistentMixin(HasAccessibles):
@Command() @Command()
def factory_reset(self): def factory_reset(self):
"""reset to values from config / default values"""
self.writeDict.update(self.initData) self.writeDict.update(self.initData)
self.writeInitParams() 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 inspect
import sys
from secop.errors import BadValueError, ConfigError, ProgrammingError 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): class HasDescriptors(Object):
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):
@classmethod @classmethod
def __init_subclass__(cls): def __init_subclass__(cls):
# when migrating old style declarations, sometimes the trailing comma is not removed # 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))) 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' # storage for 'properties of a property'
class Property: class Property:
"""base class holding info about a property """base class holding info about a property
@ -142,26 +130,22 @@ class HasProperties(HasDescriptors):
@classmethod @classmethod
def __init_subclass__(cls): def __init_subclass__(cls):
super().__init_subclass__() 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 = {} properties = {}
# using cls.__bases__ and base.propertyDict for this would fail on some multiple inheritance cases # using cls.__bases__ and base.propertyDict for this would fail on some multiple inheritance cases
for base in reversed(cls.__mro__): for base in reversed(cls.__mro__):
properties.update({k: v for k, v in base.__dict__.items() if isinstance(v, Property)}) properties.update({k: v for k, v in base.__dict__.items() if isinstance(v, Property)})
cls.propertyDict = properties cls.propertyDict = properties
# treat overriding properties with bare values # treat overriding properties with bare values
for pn, po in properties.items(): for pn, po in list(properties.items()):
value = getattr(cls, pn, po) 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() po = po.copy()
try: try:
# try to apply bare value to Property
po.value = po.datatype(value) po.value = po.datatype(value)
except BadValueError: except BadValueError:
if pn in properties:
if callable(value): if callable(value):
raise ProgrammingError('method %s.%s collides with property of %s' % raise ProgrammingError('method %s.%s collides with property of %s' %
(cls.__name__, pn, base.__name__)) from None (cls.__name__, pn, base.__name__)) from None

View File

@ -19,4 +19,4 @@
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de> # 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.params import Parameter
from secop.protocol.messages import COMMANDREPLY, DESCRIPTIONREPLY, \ from secop.protocol.messages import COMMANDREPLY, DESCRIPTIONREPLY, \
DISABLEEVENTSREPLY, ENABLEEVENTSREPLY, ERRORPREFIX, EVENTREPLY, \ DISABLEEVENTSREPLY, ENABLEEVENTSREPLY, ERRORPREFIX, EVENTREPLY, \
HEARTBEATREPLY, IDENTREPLY, IDENTREQUEST, READREPLY, WRITEREPLY HEARTBEATREPLY, IDENTREPLY, IDENTREQUEST, READREPLY, WRITEREPLY, \
LOGGING_REPLY, LOG_EVENT
def make_update(modulename, pobj): def make_update(modulename, pobj):
@ -60,12 +61,11 @@ def make_update(modulename, pobj):
class Dispatcher: class Dispatcher:
OMIT_UNCHANGED_WITHIN = 1 # do not send unchanged updates within 1 sec
def __init__(self, name, logger, options, srv): def __init__(self, name, logger, options, srv):
# to avoid errors, we want to eat all options here # to avoid errors, we want to eat all options here
self.equipment_id = options.pop('id', name) 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 = {} self.nodeprops = {}
for k in list(options): for k in list(options):
self.nodeprops[k] = options.pop(k) self.nodeprops[k] = options.pop(k)
@ -83,6 +83,7 @@ class Dispatcher:
# eventname is <modulename> or <modulename>:<parametername> # eventname is <modulename> or <modulename>:<parametername>
self._subscriptions = {} self._subscriptions = {}
self._lock = threading.RLock() self._lock = threading.RLock()
self.name = name
self.restart = srv.restart self.restart = srv.restart
self.shutdown = srv.shutdown self.shutdown = srv.shutdown
@ -124,13 +125,21 @@ class Dispatcher:
"""registers new connection""" """registers new connection"""
self._connections.append(conn) 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): def remove_connection(self, conn):
"""removes now longer functional connection""" """removes now longer functional connection"""
if conn in self._connections: if conn in self._connections:
self._connections.remove(conn) self._connections.remove(conn)
for _evt, conns in list(self._subscriptions.items()): self.reset_connection(conn)
conns.discard(conn)
self._active_connections.discard(conn)
def register_module(self, moduleobj, modulename, export=True): def register_module(self, moduleobj, modulename, export=True):
self.log.debug('registering module %r as %s (export=%r)' % self.log.debug('registering module %r as %s (export=%r)' %
@ -214,9 +223,14 @@ class Dispatcher:
if cobj is None: if cobj is None:
raise NoSuchCommandError('Module %r has no command %r' % (modulename, cname or exportedname)) raise NoSuchCommandError('Module %r has no command %r' % (modulename, cname or exportedname))
if cobj.argument:
argument = cobj.argument.import_value(argument)
# now call func # now call func
# note: exceptions are handled in handle_request, not here! # 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): def _setParameterValue(self, modulename, exportedname, value):
moduleobj = self.get_module(modulename) moduleobj = self.get_module(modulename)
@ -236,13 +250,9 @@ class Dispatcher:
# validate! # validate!
value = pobj.datatype(value) value = pobj.datatype(value)
writefunc = getattr(moduleobj, 'write_%s' % pname, None)
# note: exceptions are handled in handle_request, not here! # note: exceptions are handled in handle_request, not here!
if writefunc: getattr(moduleobj, 'write_' + pname)(value)
# return value is ignored here, as it is automatically set on the pobj and broadcast # return value is ignored here, as already handled
writefunc(value)
else:
setattr(moduleobj, pname, value)
return pobj.export_value(), dict(t=pobj.timestamp) if pobj.timestamp else {} return pobj.export_value(), dict(t=pobj.timestamp) if pobj.timestamp else {}
def _getParameterValue(self, modulename, exportedname): def _getParameterValue(self, modulename, exportedname):
@ -259,11 +269,9 @@ class Dispatcher:
# raise ReadOnlyError('This parameter is constant and can not be accessed remotely.') # raise ReadOnlyError('This parameter is constant and can not be accessed remotely.')
return pobj.datatype.export_value(pobj.constant) 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! # note: exceptions are handled in handle_request, not here!
readfunc() getattr(moduleobj, 'read_' + pname)()
# return value is ignored here, as already handled
return pobj.export_value(), dict(t=pobj.timestamp) if pobj.timestamp else {} 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!') self.log.error('should have been handled in the interface!')
def handle__ident(self, conn, specifier, data): 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) return (IDENTREPLY, None, None)
def handle_describe(self, conn, specifier, data): def handle_describe(self, conn, specifier, data):
@ -373,3 +385,19 @@ class Dispatcher:
self._active_connections.discard(conn) self._active_connections.discard(conn)
# XXX: also check all entries in self._subscriptions? # XXX: also check all entries in self._subscriptions?
return (DISABLEEVENTSREPLY, None, None) 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: if ntry:
self.log.warning('tried again %d times after "Address already in use"' % ntry) self.log.warning('tried again %d times after "Address already in use"' % ntry)
self.log.info("TCPServer initiated") 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 HELPREQUEST = 'help' # literal
HELPREPLY = 'helping' # +line number +json_text 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 # helper mapping to find the REPLY for a REQUEST
# do not put IDENTREQUEST/IDENTREPLY here, as this needs anyway extra treatment # do not put IDENTREQUEST/IDENTREPLY here, as this needs anyway extra treatment
REQUEST2REPLY = { REQUEST2REPLY = {
@ -77,6 +84,7 @@ REQUEST2REPLY = {
READREQUEST: READREPLY, READREQUEST: READREPLY,
HEARTBEATREQUEST: HEARTBEATREPLY, HEARTBEATREQUEST: HEARTBEATREPLY,
HELPREQUEST: HELPREPLY, HELPREQUEST: HELPREPLY,
LOGGING_REQUEST: LOGGING_REPLY,
} }
@ -89,6 +97,8 @@ HelpMessage = """Try one of the following:
'%s <nonce>' to request a heartbeat response '%s <nonce>' to request a heartbeat response
'%s' to activate async updates '%s' to activate async updates
'%s' to deactivate updates '%s' to deactivate updates
'%s [<module>] <loglevel>' to activate logging events
""" % (IDENTREQUEST, DESCRIPTIONREQUEST, READREQUEST, """ % (IDENTREQUEST, DESCRIPTIONREQUEST, READREQUEST,
WRITEREQUEST, COMMANDREQUEST, HEARTBEATREQUEST, 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.client import SecopClient, decode_msg, encode_msg_frame
from secop.datatypes import StringType from secop.datatypes import StringType
from secop.errors import BadValueError, \ from secop.errors import BadValueError, CommunicationFailedError, ConfigError
CommunicationFailedError, ConfigError, make_secop_error
from secop.lib import get_class from secop.lib import get_class
from secop.modules import Drivable, Module, Readable, Writable from secop.modules import Drivable, Module, Readable, Writable
from secop.params import Command, Parameter from secop.params import Command, Parameter
from secop.properties import Property 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='') module = Property('remote module name', datatype=StringType(), default='')
pollerClass = None
_consistency_check_done = False _consistency_check_done = False
_secnode = None _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']) opts['description'] = 'secnode %s on %s' % (opts.get('module', name), opts['uri'])
return SecNode(name, logger, opts, srv) return SecNode(name, logger, opts, srv)
@ -47,14 +46,12 @@ class ProxyModule(HasIodev, Module):
if parameter not in self.parameters: if parameter not in self.parameters:
return # ignore unknown parameters return # ignore unknown parameters
# should be done here: deal with clock differences # should be done here: deal with clock differences
if readerror:
readerror = make_secop_error(*readerror)
self.announceUpdate(parameter, value, readerror, timestamp) self.announceUpdate(parameter, value, readerror, timestamp)
def initModule(self): def initModule(self):
if not self.module: if not self.module:
self.module = self.name self.module = self.name
self._secnode = self._iodev.secnode self._secnode = self.io.secnode
self._secnode.register_callback(self.module, self.updateEvent, self._secnode.register_callback(self.module, self.updateEvent,
self.descriptiveDataChange, self.nodeStateChange) self.descriptiveDataChange, self.nodeStateChange)
super().initModule() super().initModule()
@ -125,6 +122,7 @@ class ProxyModule(HasIodev, Module):
def checkProperties(self): def checkProperties(self):
pass # skip pass # skip
class ProxyReadable(ProxyModule, Readable): class ProxyReadable(ProxyModule, Readable):
pass pass
@ -144,10 +142,12 @@ class SecNode(Module):
uri = Property('uri of a SEC node', datatype=StringType()) uri = Property('uri of a SEC node', datatype=StringType())
def earlyInit(self): def earlyInit(self):
super().earlyInit()
self.secnode = SecopClient(self.uri, self.log) self.secnode = SecopClient(self.uri, self.log)
def startModule(self, started_callback): def startModule(self, start_events):
self.secnode.spawn_connect(started_callback) super().startModule(start_events)
self.secnode.spawn_connect(start_events.get_trigger())
@Command(StringType(), result=StringType()) @Command(StringType(), result=StringType())
def request(self, msg): def request(self, msg):
@ -182,7 +182,7 @@ def proxy_class(remote_class, name=None):
for aname, aobj in rcls.accessibles.items(): for aname, aobj in rcls.accessibles.items():
if isinstance(aobj, Parameter): if isinstance(aobj, Parameter):
pobj = aobj.override(poll=False, handler=None, needscfg=False) pobj = aobj.merge(dict(handler=None, needscfg=False))
attrs[aname] = pobj attrs[aname] = pobj
def rfunc(self, pname=aname): def rfunc(self, pname=aname):
@ -198,7 +198,7 @@ def proxy_class(remote_class, name=None):
def wfunc(self, value, pname=aname): def wfunc(self, value, pname=aname):
value, _, readerror = self._secnode.setParameter(self.name, pname, value) value, _, readerror = self._secnode.setParameter(self.name, pname, value)
if readerror: if readerror:
raise make_secop_error(*readerror) raise readerror
return value return value
attrs['write_' + aname] = wfunc attrs['write_' + aname] = wfunc
@ -225,5 +225,5 @@ def Proxy(name, logger, cfgdict, srv):
remote_class = cfgdict.pop('remote_class') remote_class = cfgdict.pop('remote_class')
if 'description' not in cfgdict: if 'description' not in cfgdict:
cfgdict['description'] = 'remote module %s on %s' % ( 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) 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 configparser
import os import os
import sys import sys
import threading
import time
import traceback import traceback
from collections import OrderedDict from collections import OrderedDict
from secop.errors import ConfigError, SECoPError from secop.errors import ConfigError, SECoPError
from secop.lib import formatException, get_class, getGeneralConfig from secop.lib import formatException, get_class, generalConfig
from secop.modules import Attached from secop.lib.multievent import MultiEvent
from secop.params import PREDEFINED_ACCESSIBLES from secop.params import PREDEFINED_ACCESSIBLES
from secop.modules import Attached
try: try:
from daemon import DaemonContext from daemon import DaemonContext
@ -89,7 +88,6 @@ class Server:
... ...
""" """
self._testonly = testonly self._testonly = testonly
cfg = getGeneralConfig()
self.log = parent_logger.getChild(name, True) self.log = parent_logger.getChild(name, True)
if not cfgfiles: if not cfgfiles:
@ -114,22 +112,21 @@ class Server:
if ambiguous_sections: if ambiguous_sections:
self.log.warning('ambiguous sections in %s: %r' % (cfgfiles, tuple(ambiguous_sections))) self.log.warning('ambiguous sections in %s: %r' % (cfgfiles, tuple(ambiguous_sections)))
self._cfgfiles = cfgfiles 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): def loadCfgFile(self, cfgfile):
if not cfgfile.endswith('.cfg'): if not cfgfile.endswith('.cfg'):
cfgfile += '.cfg' cfgfile += '.cfg'
cfg = getGeneralConfig()
if os.sep in cfgfile: # specified as full path if os.sep in cfgfile: # specified as full path
filename = cfgfile if os.path.exists(cfgfile) else None filename = cfgfile if os.path.exists(cfgfile) else None
else: 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): if os.path.exists(filename):
break break
else: else:
filename = None filename = None
if filename is 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) self.log.debug('Parse config file %s ...' % filename)
result = OrderedDict() result = OrderedDict()
parser = configparser.ConfigParser() parser = configparser.ConfigParser()
@ -268,36 +265,58 @@ class Server:
failure_traceback = traceback.format_exc() failure_traceback = traceback.format_exc()
errors.append('error creating %s' % modname) errors.append('error creating %s' % modname)
poll_table = dict() missing_super = set()
# all objs created, now start them up and interconnect # all objs created, now start them up and interconnect
for modname, modobj in self.modules.items(): for modname, modobj in self.modules.items():
self.log.info('registering module %r' % modname) self.log.info('registering module %r' % modname)
self.dispatcher.register_module(modobj, modname, modobj.export) 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 # also call earlyInit on the modules
modobj.earlyInit() modobj.earlyInit()
if not modobj.earlyInitDone:
missing_super.add('%s was not called, probably missing super call'
% modobj.earlyInit.__qualname__)
# handle attached modules # handle attached modules
for modname, modobj in self.modules.items(): for modname, modobj in self.modules.items():
attached_modules = {}
for propname, propobj in modobj.propertyDict.items(): for propname, propobj in modobj.propertyDict.items():
if isinstance(propobj, Attached): if isinstance(propobj, Attached):
try: try:
setattr(modobj, propobj.attrname or '_' + propname, attname = getattr(modobj, propname)
self.dispatcher.get_module(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: except SECoPError as e:
errors.append('module %s, attached %s: %s' % (modname, propname, str(e))) errors.append('module %s, attached %s: %s' % (modname, propname, str(e)))
modobj.attachedModules = attached_modules
# call init on each module after registering all # call init on each module after registering all
for modname, modobj in self.modules.items(): for modname, modobj in self.modules.items():
try: try:
modobj.initModule() modobj.initModule()
if not modobj.initModuleDone:
missing_super.add('%s was not called, probably missing super call'
% modobj.initModule.__qualname__)
except Exception as e: except Exception as e:
if failure_traceback is None: if failure_traceback is None:
failure_traceback = traceback.format_exc() failure_traceback = traceback.format_exc()
errors.append('error initializing %s: %r' % (modname, e)) 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: if errors:
for errtxt in errors: for errtxt in errors:
for line in errtxt.split('\n'): for line in errtxt.split('\n'):
@ -311,22 +330,13 @@ class Server:
if self._testonly: if self._testonly:
return return
start_events = [] self.log.info('waiting for modules being started')
for modname, modobj in self.modules.items(): start_events.name = None
event = threading.Event() if not start_events.wait():
# startModule must return either a timeout value or None (default 30 sec) # some timeout happened
timeout = modobj.startModule(started_callback=event.set) or 30 for name in start_events.waiting_for():
start_events.append((time.time() + timeout, 'module %s' % modname, event)) self.log.warning('timeout when starting %s' % name)
for poller in poll_table.values(): self.log.info('all modules started')
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')
history_path = os.environ.get('FRAPPY_HISTORY') history_path = os.environ.get('FRAPPY_HISTORY')
if history_path: if history_path:
from secop_psi.historywriter import FrappyHistoryWriter # pylint: disable=import-outside-toplevel 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.datatypes import FloatRange
from secop.lib import mkthread from secop.lib import mkthread
from secop.modules import BasicPoller, Drivable, \ from secop.modules import Drivable, Module, Parameter, Readable, Writable, Command
Module, Parameter, Readable, Writable, Command
class SimBase: class SimBase:
pollerClass = BasicPoller
def __new__(cls, devname, logger, cfgdict, dispatcher): def __new__(cls, devname, logger, cfgdict, dispatcher):
extra_params = cfgdict.pop('extra_params', '') or cfgdict.pop('.extra_params', '') extra_params = cfgdict.pop('extra_params', '') or cfgdict.pop('.extra_params', '')
attrs = {} attrs = {}
@ -60,6 +57,7 @@ class SimBase:
return object.__new__(type('SimBase_%s' % devname, (cls,), attrs)) return object.__new__(type('SimBase_%s' % devname, (cls,), attrs))
def initModule(self): def initModule(self):
super().initModule()
self._sim_thread = mkthread(self._sim) self._sim_thread = mkthread(self._sim)
def _sim(self): def _sim(self):
@ -119,7 +117,7 @@ class SimDrivable(SimReadable, Drivable):
self._value = self.target self._value = self.target
speed *= self.interval speed *= self.interval
try: try:
self.pollParams(0) self.doPoll()
except Exception: except Exception:
pass pass
@ -132,7 +130,7 @@ class SimDrivable(SimReadable, Drivable):
self._value = self.target self._value = self.target
sleep(self.interval) sleep(self.interval)
try: try:
self.pollParams(0) self.doPoll()
except Exception: except Exception:
pass pass
self.status = self.Status.IDLE, '' self.status = self.Status.IDLE, ''

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,7 +23,7 @@
"""drivers for CCU4, the cryostat control unit at SINQ""" """drivers for CCU4, the cryostat control unit at SINQ"""
# the most common Frappy classes can be imported from secop.core # the most common Frappy classes can be imported from secop.core
from secop.core import EnumType, FloatRange, \ from secop.core import EnumType, FloatRange, \
HasIodev, Parameter, Readable, StringIO HasIO, Parameter, Readable, StringIO
class CCU4IO(StringIO): class CCU4IO(StringIO):
@ -34,14 +34,13 @@ class CCU4IO(StringIO):
identification = [('cid', r'CCU4.*')] identification = [('cid', r'CCU4.*')]
# inheriting the HasIodev mixin creates us a private attribute *_iodev* # inheriting HasIO allows us to use the communicate method for talking with the hardware
# for talking with the hardware
# Readable as a base class defines the value and status parameters # Readable as a base class defines the value and status parameters
class HeLevel(HasIodev, Readable): class HeLevel(HasIO, Readable):
"""He Level channel of CCU4""" """He Level channel of CCU4"""
# define the communication class to create the IO module # define the communication class to create the IO module
iodevClass = CCU4IO ioClass = CCU4IO
# define or alter the parameters # define or alter the parameters
# as Readable.value exists already, we give only the modified property 'unit' # as Readable.value exists already, we give only the modified property 'unit'
@ -71,9 +70,9 @@ class HeLevel(HasIodev, Readable):
for changing a parameter for changing a parameter
:returns: the (new) value of the 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 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): def read_value(self):
return self.query('h') 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 = [] result = []
for subkey, elmtype in items: for subkey, elmtype in items:
for fun, tail_, opts in make_cvt_list(elmtype, '%s.%s' % (tail, subkey)): for fun, tail_, opts in make_cvt_list(elmtype, '%s.%s' % (tail, subkey)):
def conv(value, key=subkey, func=fun): result.append((lambda v, k=subkey, f=fun: f(v[k]), tail_, opts))
try:
return value[key]
except KeyError: # can not use value.get() because value might be a list
return None
result.append((conv, tail_, opts))
return result return result
@ -70,11 +65,11 @@ class FrappyHistoryWriter(frappyhistory.FrappyWriter):
- period: - period:
the typical 'lifetime' of a value. the typical 'lifetime' of a value.
The intention is, that points in a chart may be connected by a straight line 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 when the distance is lower than this value. If not, the line should be drawn
drawn horizontally from the last point to a point <period> before the next value. 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 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 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. we would need some additional parameter property.
- show: True/False, whether this curve should be shown or not by default in - show: True/False, whether this curve should be shown or not by default in
a summary chart a summary chart

View File

@ -18,12 +18,18 @@
# Module authors: # Module authors:
# Markus Zolliker <markus.zolliker@psi.ch> # 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, \ * switching between voltage and current happens by setting their target
HasIodev, Module, Parameter, StringIO, Writable, Done * 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): class K2601bIO(StringIO):
@ -32,128 +38,162 @@ class K2601bIO(StringIO):
SOURCECMDS = { SOURCECMDS = {
0: 'reset()' 0: 'reset()'
' smua.source.output = 0 print("ok")',
1: 'reset()'
' smua.source.func = smua.OUTPUT_DCAMPS' ' smua.source.func = smua.OUTPUT_DCAMPS'
' display.smua.measure.func = display.MEASURE_VOLTS' ' display.smua.measure.func = display.MEASURE_VOLTS'
' smua.source.autorangei = 1' ' smua.source.autorangei = 1'
' smua.source.output = %d print("ok"")', ' smua.source.output = 1 print("ok")',
1: 'reset()' 2: 'reset()'
' smua.source.func = smua.OUTPUT_DCVOLTS' ' smua.source.func = smua.OUTPUT_DCVOLTS'
' display.smua.measure.func = display.MEASURE_DCAMPS' ' display.smua.measure.func = display.MEASURE_DCAMPS'
' smua.source.autorangev = 1' ' 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) ioClass = K2601bIO
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())')
def read_mode(self): 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): 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() 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): def write_ilimit(self, value):
sourcemeter = Attached() 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) def read_vlimit(self):
target = Parameter('set current', FloatRange(unit='A'), poll=True) if self.mode == 'voltage':
active = Parameter('current is controlled', BoolType(), default=False) # polled by SourceMeter return self.ilimit
limit = Parameter('current limit', FloatRange(0, 2.0, unit='A'), default=2, poll=True) return float(self.communicate('print(smua.source.limitv)'))
def initModule(self): def write_vlimit(self, value):
self._sourcemeter.registerCallbacks(self) 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): 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): def read_target(self):
return self.sendRecv('print(smua.source.leveli)') return float(self.communicate('print(smua.source.leveli)'))
def write_target(self, value): def write_target(self, value):
if not self.active: if value > self.sourcemeter.ilimit:
raise ValueError('current source is disabled')
if value > self.limit:
raise ValueError('current exceeds limit') 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): def read_limit(self):
if self.active: return self.sourcemeter.read_ilimit()
return self.limit
return self.sendRecv('print(smua.source.limiti)')
def write_limit(self, value): def write_limit(self, value):
if self.active: return self.sourcemeter.write_ilimit(value)
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
def update_mode(self, mode): def update_mode(self, mode):
# will be called whenever the attached sourcemeters mode changes # 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() sourcemeter = Attached()
value = Parameter('measured voltage', FloatRange(unit='V'), poll=True) value = Parameter('measured voltage', FloatRange(unit='V'))
target = Parameter('set voltage', FloatRange(unit='V'), poll=True) target = Parameter('set voltage', FloatRange(unit='V'))
active = Parameter('voltage is controlled', BoolType(), default=False) # polled by SourceMeter active = Parameter('voltage is controlled', BoolType())
limit = Parameter('current limit', FloatRange(0, 2.0, unit='V'), default=2, poll=True) limit = Parameter('voltage limit', FloatRange(0, 2.0, unit='V'), default=2)
def initModule(self): def initModule(self):
self._sourcemeter.registerCallbacks(self) self.sourcemeter.registerCallbacks(self)
def read_value(self): def read_value(self):
return self.sendRecv('print(smua.measure.v())') return float(self.communicate('print(smua.measure.v())'))
def read_target(self): def read_target(self):
return self.sendRecv('print(smua.source.levelv)') return float(self.communicate('print(smua.source.levelv)'))
def write_target(self, value): def write_target(self, value):
if not self.active: if value > self.sourcemeter.vlimit:
raise ValueError('voltage source is disabled')
if value > self.limit:
raise ValueError('voltage exceeds limit') 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): def read_limit(self):
if self.active: return self.sourcemeter.read_vlimit()
return self.limit
return self.sendRecv('print(smua.source.limitv)')
def write_limit(self, value): def write_limit(self, value):
if self.active: return self.sourcemeter.write_vlimit(value)
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
def update_mode(self, mode): def update_mode(self, mode):
# will be called whenever the attached sourcemeters mode changes # 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.lib import formatStatusBits
from secop.modules import Attached, Done, \ from secop.modules import Attached, Done, \
Drivable, Parameter, Property, Readable Drivable, Parameter, Property, Readable
from secop.poller import REGULAR, Poller from secop.io import HasIO
from secop.io import HasIodev
Status = Drivable.Status Status = Drivable.Status
@ -58,29 +57,29 @@ class StringIO(secop.io.StringIO):
wait_before = 0.05 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)) target = Parameter('channel to select', datatype=IntRange(0, 17))
autoscan = Parameter('whether to scan automatically', datatype=BoolType(), readonly=False, default=False) autoscan = Parameter('whether to scan automatically', datatype=BoolType(), readonly=False, default=False)
pollinterval = Parameter(default=1, export=False) pollinterval = Parameter(default=1, export=False)
pollerClass = Poller ioClass = StringIO
iodevClass = StringIO
_channel_changed = 0 # time of last channel change _channel_changed = 0 # time of last channel change
_channels = None # dict <channel no> of <module object> _channels = None # dict <channel no> of <module object>
def earlyInit(self): def earlyInit(self):
super().earlyInit()
self._channels = {} self._channels = {}
def register_channel(self, modobj): def register_channel(self, modobj):
self._channels[modobj.channel] = modobj self._channels[modobj.channel] = modobj
def startModule(self, started_callback): def startModule(self, start_events):
started_callback() super().startModule(start_events)
for ch in range(1, 16): for ch in range(1, 16):
if ch not in self._channels: 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): def read_value(self):
channel, auto = scan.send_command(self) channel, auto = scan.send_command(self)
@ -113,7 +112,7 @@ class Main(HasIodev, Drivable):
def write_target(self, channel): def write_target(self, channel):
scan.send_change(self, channel, self.autoscan) 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: if channel != self.value:
self.value = 0 self.value = 0
self._channel_changed = time.time() self._channel_changed = time.time()
@ -122,11 +121,11 @@ class Main(HasIodev, Drivable):
def write_autoscan(self, value): def write_autoscan(self, value):
scan.send_change(self, self.value, 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 return value
class ResChannel(HasIodev, Readable): class ResChannel(HasIO, Readable):
"""temperature channel on Lakeshore 336""" """temperature channel on Lakeshore 336"""
RES_RANGE = {key: i+1 for i, key in list( 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'] enumerate(mag % val for mag in ['%guV', '%gmV']
for val in [2, 6.32, 20, 63.2, 200, 632]))} for val in [2, 6.32, 20, 63.2, 200, 632]))}
pollerClass = Poller ioClass = StringIO
iodevClass = StringIO
_main = None # main module _main = None # main module
_last_range_change = 0 # time of last range change _last_range_change = 0 # time of last range change
@ -166,6 +164,7 @@ class ResChannel(HasIodev, Readable):
_trigger_read = False _trigger_read = False
def initModule(self): def initModule(self):
super().initModule()
self._main = self.DISPATCHER.get_module(self.main) self._main = self.DISPATCHER.get_module(self.main)
self._main.register_channel(self) self._main.register_channel(self)
@ -181,7 +180,7 @@ class ResChannel(HasIodev, Readable):
return Done return Done
# we got here, when we missed the idle state of self._main # we got here, when we missed the idle state of self._main
self._trigger_read = False self._trigger_read = False
result = self.sendRecv('RDGR?%d' % self.channel) result = self.communicate('RDGR?%d' % self.channel)
result = float(result) result = float(result)
if self.autorange == 'soft': if self.autorange == 'soft':
now = time.time() now = time.time()
@ -214,9 +213,9 @@ class ResChannel(HasIodev, Readable):
def read_status(self): def read_status(self):
if not self.enabled: if not self.enabled:
return [self.Status.DISABLED, 'disabled'] return [self.Status.DISABLED, 'disabled']
if self.channel != self._main.value: if self.channel != self.main.value:
return Done 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) result &= 0x37 # mask T_OVER and T_UNDER (change this when implementing temperatures instead of resistivities)
statustext = ' '.join(formatStatusBits(result, STATUS_BIT_LABELS)) statustext = ' '.join(formatStatusBits(result, STATUS_BIT_LABELS))
if statustext: if statustext:
@ -228,7 +227,7 @@ class ResChannel(HasIodev, Readable):
if autorange: if autorange:
result['autorange'] = 'hard' result['autorange'] = 'hard'
# else: do not change autorange # 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: if excoff:
result.update(iexc=0, vexc=0) result.update(iexc=0, vexc=0)
elif iscur: elif iscur:
@ -281,5 +280,5 @@ class ResChannel(HasIodev, Readable):
def write_enabled(self, value): def write_enabled(self, value):
inset.write(self, 'enabled', value) inset.write(self, 'enabled', value)
if value: if value:
self._main.write_target(self.channel) self.main.write_target(self.channel)
return Done return Done

View File

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

View File

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

View File

@ -148,6 +148,7 @@ class Motor(PersistentMixin, HasIodev, Drivable):
@Command @Command
def reset(self): def reset(self):
"""reset error, set position to encoder"""
self.read_value() self.read_value()
if self.status[0] == self.Status.ERROR: if self.status[0] == self.Status.ERROR:
enc = self.encoder - self.zero 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 threading
import time import time
from ast import literal_eval # convert string as comma separated numbers into tuple
import secop.iohandler
from secop.datatypes import BoolType, EnumType, \ from secop.datatypes import BoolType, EnumType, \
FloatRange, IntRange, StatusType, StringType FloatRange, IntRange, StatusType, StringType
from secop.errors import HardwareError from secop.errors import HardwareError
from secop.lib import clamp from secop.lib import clamp
from secop.lib.enum import Enum from secop.lib.enum import Enum
from secop.modules import Attached, Communicator, Done, \ from secop.modules import Communicator, Done, \
Drivable, Parameter, Property, Readable Drivable, Parameter, Property, Readable
from secop.poller import Poller from secop.io import HasIO
from secop.io import HasIodev from secop.rwhandler import CommonReadHandler, CommonWriteHandler
try: try:
import secop_psi.ppmswindows as ppmshw import secop_psi.ppmswindows as ppmshw
@ -52,28 +52,11 @@ except ImportError:
import secop_psi.ppmssim as ppmshw 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): class Main(Communicator):
"""ppms communicator module""" """ppms communicator module"""
pollinterval = Parameter('poll interval', FloatRange(), readonly=False, default=2) 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) default="", readonly=True)
class_id = Property('Quantum Design class id', StringType(), export=False) 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))) _channel_to_index = dict(((channel, i) for i, channel in enumerate(_channel_names)))
_status_bitpos = {'temp': 0, 'field': 4, 'chamber': 8, 'position': 12} _status_bitpos = {'temp': 0, 'field': 4, 'chamber': 8, 'position': 12}
pollerClass = Poller
def earlyInit(self): def earlyInit(self):
super().earlyInit()
self.modules = {} self.modules = {}
self._ppms_device = ppmshw.QDevice(self.class_id) self._ppms_device = ppmshw.QDevice(self.class_id)
self.lock = threading.Lock() self.lock = threading.Lock()
@ -99,10 +81,14 @@ class Main(Communicator):
def communicate(self, command): def communicate(self, command):
"""GPIB command""" """GPIB command"""
with self.lock: with self.lock:
self.comLog('> %s' % command)
reply = self._ppms_device.send(command) reply = self._ppms_device.send(command)
self.log.debug("%s|%s", command, reply) self.comLog("< %s", reply)
return reply return reply
def doPoll(self):
self.read_data()
def read_data(self): def read_data(self):
mask = 1 # always get packed_status mask = 1 # always get packed_status
for channelname, channel in self.modules.items(): for channelname, channel in self.modules.items():
@ -128,37 +114,27 @@ class Main(Communicator):
return data # return data as string return data # return data as string
class PpmsMixin: class PpmsBase(HasIO, Readable):
"""common base for all ppms modules""" """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 enabled = True # default, if no parameter enable is defined
_last_settings = None # used by several modules _last_settings = None # used by several modules
slow_pollfactor = 1 slow_pollfactor = 1
# as this pollinterval affects only the polling of settings # as this pollinterval affects only the polling of settings
# it would be confusing to export it. # it would be confusing to export it.
pollinterval = Parameter('', FloatRange(), needscfg=False, export=False) pollinterval = Parameter(export=False)
def initModule(self): def initModule(self):
self._iodev.register(self) super().initModule()
self.io.register(self)
def startModule(self, started_callback): def doPoll(self):
# no polls except on main module
started_callback()
def read_value(self):
# polling is done by the main module # polling is done by the main module
# and PPMS does not deliver really more fresh values when polled more often # and PPMS does not deliver really more fresh values when polled more often
return Done pass
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
def update_value_status(self, value, packed_status): def update_value_status(self, value, packed_status):
# update value and status # update value and status
@ -172,12 +148,18 @@ class PpmsMixin:
self.value = value self.value = value
self.status = (self.Status.IDLE, '') 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""" """channel base class"""
value = Parameter('main value of channels', poll=True) value = Parameter('main value of channels')
enabled = Parameter('is this channel used?', readonly=False, poll=False, enabled = Parameter('is this channel used?', readonly=False,
datatype=BoolType(), default=False) datatype=BoolType(), default=False)
channel = Property('channel name', channel = Property('channel name',
@ -186,26 +168,21 @@ class Channel(PpmsMixin, HasIodev, Readable):
datatype=IntRange(1, 4), export=False) datatype=IntRange(1, 4), export=False)
def earlyInit(self): def earlyInit(self):
Readable.earlyInit(self) super().earlyInit()
if not self.channel: if not self.channel:
self.channel = self.name self.channel = self.name
def get_settings(self, pname):
return ''
class UserChannel(Channel): class UserChannel(Channel):
"""user channel""" """user channel"""
# pollinterval = Parameter(visibility=3)
no = Property('channel number', no = Property('channel number',
datatype=IntRange(0, 0), export=False, default=0) datatype=IntRange(0, 0), export=False, default=0)
linkenable = Property('name of linked channel for enabling', linkenable = Property('name of linked channel for enabling',
datatype=StringType(), export=False, default='') datatype=StringType(), export=False, default='')
def write_enabled(self, enabled): def write_enabled(self, enabled):
other = self._iodev.modules.get(self.linkenable, None) other = self.io.modules.get(self.linkenable, None)
if other: if other:
other.enabled = enabled other.enabled = enabled
return enabled return enabled
@ -214,201 +191,172 @@ class UserChannel(Channel):
class DriverChannel(Channel): class DriverChannel(Channel):
"""driver channel""" """driver channel"""
drvout = IOHandler('drvout', 'DRVOUT? %(no)d', '%d,%g,%g') current = Parameter('driver current', readonly=False,
current = Parameter('driver current', readonly=False, handler=drvout,
datatype=FloatRange(0., 5000., unit='uA')) 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')) 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: if self.no != no:
raise HardwareError('DRVOUT command: channel number in reply does not match') raise HardwareError('DRVOUT command: channel number in reply does not match')
return dict(current=current, powerlimit=powerlimit)
def change_drvout(self, change): @CommonWriteHandler(param_names)
change.readValues() def write_params(self, values):
return change.current, change.powerlimit """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): class BridgeChannel(Channel):
"""bridge channel""" """bridge channel"""
bridge = IOHandler('bridge', 'BRIDGE? %(no)d', '%d,%g,%g,%d,%d,%g') excitation = Parameter('excitation current', readonly=False,
# 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,
datatype=FloatRange(0.01, 5000., unit='uA')) 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')) 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()) datatype=BoolType())
readingmode = Parameter('reading mode', readonly=False, handler=bridge, readingmode = Parameter('reading mode', readonly=False,
datatype=EnumType(ReadingMode)) datatype=EnumType(standard=0, fast=1, highres=2))
voltagelimit = Parameter('voltage limit', readonly=False, handler=bridge, voltagelimit = Parameter('voltage limit', readonly=False,
datatype=FloatRange(0.0001, 100., unit='mV')) 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: if self.no != no:
raise HardwareError('DRVOUT command: channel number in reply does not match') raise HardwareError('DRVOUT command: channel number in reply does not match')
return dict( self.enabled = excitation != 0 and powerlimit != 0 and voltagelimit != 0
enabled=excitation != 0 and powerlimit != 0 and voltagelimit != 0, if excitation:
excitation=excitation or self.excitation, self.excitation = excitation
powerlimit=powerlimit or self.powerlimit, if powerlimit:
dcflag=dcflag, self.powerlimit = powerlimit
readingmode=readingmode, if voltagelimit:
voltagelimit=voltagelimit or self.voltagelimit, self.voltagelimit = voltagelimit
)
def change_bridge(self, change): @CommonWriteHandler(param_names)
change.readValues() def write_params(self, values):
if change.enabled: """write parameters
return self.no, change.excitation, change.powerlimit, change.dcflag, change.readingmode, change.voltagelimit
return self.no, 0, 0, change.dcflag, change.readingmode, 0 :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""" """helium level"""
level = IOHandler('level', 'LEVEL?', '%g,%d') value = Parameter(datatype=FloatRange(unit='%'))
value = Parameter(datatype=FloatRange(unit='%'), handler=level)
status = Parameter(handler=level)
# pollinterval = Parameter(visibility=3)
channel = 'level' channel = 'level'
def doPoll(self):
self.read_value()
def update_value_status(self, value, packed_status): def update_value_status(self, value, packed_status):
pass pass
# must be a no-op # must be a no-op
# when called from Main.read_data, value is always None # when called from Main.read_data, value is always None
# value and status is polled via settings # 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 # ignore 'old reading' state of the flag, as this happens only for a short time
# during measuring return literal_eval(self.communicate('LEVEL?'))[0]
return dict(value=level, status=(self.Status.IDLE, ''))
class Chamber(PpmsMixin, HasIodev, Drivable): class Chamber(PpmsBase, Drivable):
"""sample chamber handling """sample chamber handling
value is an Enum, which is redundant with the status text value is an Enum, which is redundant with the status text
""" """
chamber = IOHandler('chamber', 'CHAMBER?', '%d')
Status = Drivable.Status Status = Drivable.Status
# pylint: disable=invalid-name code_table = [
Operation = Enum( # valuecode, status, statusname, opcode, targetname
'Operation', (0, Status.IDLE, 'unknown', 10, 'noop'),
seal_immediately=0, (1, Status.IDLE, 'purged_and_sealed', 1, 'purge_and_seal'),
purge_and_seal=1, (2, Status.IDLE, 'vented_and_sealed', 2, 'vent_and_seal'),
vent_and_seal=2, (3, Status.WARN, 'sealed_unknown', 0, 'seal_immediately'),
pump_continuously=3, (4, Status.BUSY, 'purge_and_seal', None, None),
vent_continuously=4, (5, Status.BUSY, 'vent_and_seal', None, None),
hi_vacuum=5, (6, Status.BUSY, 'pumping_down', None, None),
noop=10, (8, Status.IDLE, 'pumping_continuously', 3, 'pump_continuously'),
) (9, Status.IDLE, 'venting_continuously', 4, 'vent_continuously'),
StatusCode = Enum( (15, Status.ERROR, 'general_failure', None, None),
'StatusCode', ]
unknown=0, value_codes = {k: v for v, _, k, _, _ in code_table}
purged_and_sealed=1, target_codes = {k: v for v, _, _, _, k in code_table if k}
vented_and_sealed=2, name2opcode = {k: v for _, _, _, v, k in code_table if k}
sealed_unknown=3, opcode2name = {v: k for _, _, _, v, k in code_table if k}
purge_and_seal=4, status_map = {v: (c, k.replace('_', ' ')) for v, c, k, _, _ in code_table}
vent_and_seal=5, value = Parameter(description='chamber state', datatype=EnumType(**value_codes), default=0)
pumping_down=6, target = Parameter(description='chamber command', datatype=EnumType(**target_codes), default='noop')
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'),
}
channel = 'chamber' channel = 'chamber'
def update_value_status(self, value, packed_status): def update_value_status(self, value, packed_status):
status_code = (packed_status >> 8) & 0xf 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.value = status_code
self.status = self.STATUS_MAP[status_code] self.status = self.status_map[status_code]
else: else:
self.value = self.StatusCode.unknown self.value = self.value_map['unknown']
self.status = (self.Status.ERROR, 'unknown status code %d' % status_code) self.status = (self.Status.ERROR, 'unknown status code %d' % status_code)
def analyze_chamber(self, target): def read_target(self):
return dict(target=target) opcode = int(self.communicate('CHAMBER?'))
return self.opcode2name[opcode]
def change_chamber(self, change): def write_target(self, value):
# write settings, combining <pname>=<value> and current attributes if value == self.target.noop:
# and request updated settings return self.target.noop
if change.target == self.Operation.noop: opcode = self.name2opcode[self.target.enum(value).name]
return None assert self.communicate('CHAMBER %d' % opcode) == 'OK'
return (change.target,) return self.read_target()
class Temp(PpmsMixin, HasIodev, Drivable): class Temp(PpmsBase, Drivable):
"""temperature""" """temperature"""
temp = IOHandler('temp', 'TEMP?', '%g,%g,%d')
Status = Enum( Status = Enum(
Drivable.Status, Drivable.Status,
RAMPING=370, RAMPING=370,
STABILIZING=380, STABILIZING=380,
) )
# pylint: disable=invalid-name value = Parameter(datatype=FloatRange(unit='K'))
ApproachMode = Enum('ApproachMode', fast_settle=0, no_overshoot=1) status = Parameter(datatype=StatusType(Status))
target = Parameter(datatype=FloatRange(1.7, 402.0, unit='K'), needscfg=False)
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)
setpoint = Parameter('intermediate set point', 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, ramp = Parameter('ramping speed', readonly=False, default=0,
datatype=FloatRange(0, 20, unit='K/min')) datatype=FloatRange(0, 20, unit='K/min'))
workingramp = Parameter('intermediate ramp value', workingramp = Parameter('intermediate ramp value',
datatype=FloatRange(0, 20, unit='K/min'), handler=temp) datatype=FloatRange(0, 20, unit='K/min'), default=0)
approachmode = Parameter('how to approach target!', readonly=False, handler=temp, approachmode = Parameter('how to approach target!', readonly=False,
datatype=EnumType(ApproachMode)) datatype=EnumType(fast_settle=0, no_overshoot=1), default=0)
# pollinterval = Parameter(visibility=3)
timeout = Parameter('drive timeout, in addition to ramp time', readonly=False, timeout = Parameter('drive timeout, in addition to ramp time', readonly=False,
datatype=FloatRange(0, unit='sec'), default=3600) datatype=FloatRange(0, unit='sec'), default=3600)
general_stop = Property('respect general stop', datatype=BoolType(),
# pylint: disable=invalid-name default=True, value=False)
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,
)
STATUS_MAP = { STATUS_MAP = {
1: (Status.IDLE, 'stable at target'), 1: (Status.IDLE, 'stable at target'),
2: (Status.RAMPING, 'ramping'), 2: (Status.RAMPING, 'ramping'),
@ -420,8 +368,6 @@ class Temp(PpmsMixin, HasIodev, Drivable):
14: (Status.ERROR, 'can not complete'), 14: (Status.ERROR, 'can not complete'),
15: (Status.ERROR, 'general failure'), 15: (Status.ERROR, 'general failure'),
} }
general_stop = Property('respect general stop', datatype=BoolType(),
default=True, value=False)
channel = 'temp' channel = 'temp'
_stopped = False _stopped = False
@ -432,6 +378,42 @@ class Temp(PpmsMixin, HasIodev, Drivable):
_wait_at10 = False _wait_at10 = False
_ramp_at_limit = 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): def update_value_status(self, value, packed_status):
if value is None: if value is None:
self.status = (self.Status.ERROR, 'invalid value') self.status = (self.Status.ERROR, 'invalid value')
@ -449,7 +431,7 @@ class Temp(PpmsMixin, HasIodev, Drivable):
if now > self._cool_deadline: if now > self._cool_deadline:
self._wait_at10 = False self._wait_at10 = False
self._last_change = now 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') status = (self.Status.STABILIZING, 'waiting at 10 K')
if self._last_change: # there was a change, which is not yet confirmed by hw if self._last_change: # there was a change, which is not yet confirmed by hw
if now > self._last_change + 5: if now > self._last_change + 5:
@ -478,41 +460,6 @@ class Temp(PpmsMixin, HasIodev, Drivable):
self._expected_target_time = 0 self._expected_target_time = 0
self.status = status 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): def write_target(self, target):
self._stopped = False self._stopped = False
if abs(self.target - self.value) <= 2e-5 * target and target == self.target: 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_before_change = self.status
self.status = (self.Status.BUSY, 'changed target') self.status = (self.Status.BUSY, 'changed target')
self._last_change = time.time() 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))) self.log.debug('write_target %s' % repr((self.setpoint, target, self._wait_at10)))
return target return target
def write_approachmode(self, value): def write_approachmode(self, value):
if self.isDriving(): if self.isDriving():
self.temp.write(self, 'approachmode', value) self._write_params(self.setpoint, self.ramp, value)
return Done return Done
self.approachmode = value 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): def write_ramp(self, value):
if self.isDriving(): if self.isDriving():
self.temp.write(self, 'ramp', value) self._write_params(self.setpoint, value, self.approachmode)
return Done return Done
# self.ramp = value self.ramp = 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 calc_expected(self, target, ramp): def calc_expected(self, target, ramp):
self._expected_target_time = time.time() + abs(target - self.value) * 60.0 / max(0.1, 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 self._stopped = True
class Field(PpmsMixin, HasIodev, Drivable): class Field(PpmsBase, Drivable):
"""magnetic field""" """magnetic field"""
field = IOHandler('field', 'FIELD?', '%g,%g,%d,%d')
Status = Enum( Status = Enum(
Drivable.Status, Drivable.Status,
PREPARED=150, PREPARED=150,
@ -566,20 +512,15 @@ class Field(PpmsMixin, HasIodev, Drivable):
STABILIZING=380, STABILIZING=380,
FINALIZING=390, FINALIZING=390,
) )
# pylint: disable=invalid-name value = Parameter(datatype=FloatRange(unit='T'))
PersistentMode = Enum('PersistentMode', persistent=0, driven=1) status = Parameter(datatype=StatusType(Status))
ApproachMode = Enum('ApproachMode', linear=0, no_overshoot=1, oscillate=2) target = Parameter(datatype=FloatRange(-15, 15, unit='T')) # poll only one parameter
ramp = Parameter('ramping speed', readonly=False,
value = Parameter(datatype=FloatRange(unit='T'), poll=True) datatype=FloatRange(0.064, 1.19, unit='T/min'), default=0.19)
status = Parameter(datatype=StatusType(Status), poll=True) approachmode = Parameter('how to approach target', readonly=False,
target = Parameter(datatype=FloatRange(-15, 15, unit='T'), handler=field) datatype=EnumType(linear=0, no_overshoot=1, oscillate=2), default=0)
ramp = Parameter('ramping speed', readonly=False, handler=field, persistentmode = Parameter('what to do after changing field', readonly=False,
datatype=FloatRange(0.064, 1.19, unit='T/min')) datatype=EnumType(persistent=0, driven=1), default=0)
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)
STATUS_MAP = { STATUS_MAP = {
1: (Status.IDLE, 'persistent mode'), 1: (Status.IDLE, 'persistent mode'),
@ -599,6 +540,25 @@ class Field(PpmsMixin, HasIodev, Drivable):
_last_target = None # last reached target _last_target = None # last reached target
_last_change = 0 # means no target change is pending _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): def update_value_status(self, value, packed_status):
if value is None: if value is None:
self.status = (self.Status.ERROR, 'invalid value') self.status = (self.Status.ERROR, 'invalid value')
@ -633,19 +593,6 @@ class Field(PpmsMixin, HasIodev, Drivable):
status = (status[0], 'stopping (%s)' % status[1]) status = (status[0], 'stopping (%s)' % status[1])
self.status = status 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): def write_target(self, target):
if abs(self.target - self.value) <= 2e-5 and target == self.target: if abs(self.target - self.value) <= 2e-5 and target == self.target:
self.target = target self.target = target
@ -654,7 +601,7 @@ class Field(PpmsMixin, HasIodev, Drivable):
self._stopped = False self._stopped = False
self._last_change = time.time() self._last_change = time.time()
self.status = (self.Status.BUSY, 'changed target') 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 return Done
def write_persistentmode(self, mode): def write_persistentmode(self, mode):
@ -665,19 +612,19 @@ class Field(PpmsMixin, HasIodev, Drivable):
self._status_before_change = self.status self._status_before_change = self.status
self._stopped = False self._stopped = False
self.status = (self.Status.BUSY, 'changed persistent mode') 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 return Done
def write_ramp(self, value): def write_ramp(self, value):
self.ramp = value self.ramp = value
if self.isDriving(): if self.isDriving():
self.field.write(self, 'ramp', value) self._write_params(self.target, value, self.approachmode, self.persistentmode)
return Done return Done
return None # do not execute FIELD command, as this would trigger a ramp up of leads current return None # do not execute FIELD command, as this would trigger a ramp up of leads current
def write_approachmode(self, value): def write_approachmode(self, value):
if self.isDriving(): if self.isDriving():
self.field.write(self, 'approachmode', value) self._write_params(self.target, self.ramp, value, self.persistentmode)
return Done return Done
return None # do not execute FIELD command, as this would trigger a ramp up of leads current 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 self._stopped = True
class Position(PpmsMixin, HasIodev, Drivable): class Position(PpmsBase, Drivable):
"""rotator position""" """rotator position"""
move = IOHandler('move', 'MOVE?', '%g,%g,%g')
Status = Drivable.Status Status = Drivable.Status
value = Parameter(datatype=FloatRange(unit='deg'), poll=True) value = Parameter(datatype=FloatRange(unit='deg'))
target = Parameter(datatype=FloatRange(-720., 720., unit='deg'), handler=move) target = Parameter(datatype=FloatRange(-720., 720., unit='deg'))
enabled = Parameter('is this channel used?', readonly=False, poll=False, enabled = Parameter('is this channel used?', readonly=False,
datatype=BoolType(), default=True) 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')) datatype=FloatRange(0.8, 12, unit='deg/sec'))
# pollinterval = Parameter(visibility=3)
STATUS_MAP = { STATUS_MAP = {
1: (Status.IDLE, 'at target'), 1: (Status.IDLE, 'at target'),
5: (Status.BUSY, 'moving'), 5: (Status.BUSY, 'moving'),
@ -720,6 +664,23 @@ class Position(PpmsMixin, HasIodev, Drivable):
_last_change = 0 _last_change = 0
_within_target = 0 # time since we are within target _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): def update_value_status(self, value, packed_status):
if not self.enabled: if not self.enabled:
self.status = (self.Status.DISABLED, 'disabled') self.status = (self.Status.DISABLED, 'disabled')
@ -757,29 +718,17 @@ class Position(PpmsMixin, HasIodev, Drivable):
status = (status[0], 'stopping (%s)' % status[1]) status = (status[0], 'stopping (%s)' % status[1])
self.status = status 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): def write_target(self, target):
self._stopped = False self._stopped = False
self._last_change = 0 self._last_change = 0
self._status_before_change = self.status self._status_before_change = self.status
self.status = (self.Status.BUSY, 'changed target') self.status = (self.Status.BUSY, 'changed target')
self.move.write(self, 'target', target) self._write_params(target, self.speed)
return Done return Done
def write_speed(self, value): def write_speed(self, value):
if self.isDriving(): if self.isDriving():
self.move.write(self, 'speed', value) self._write_params(self.target, value)
return Done return Done
self.speed = value self.speed = value
return None # do not execute MOVE command, as this would trigger an unnecessary move 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): def num(string):
return json.loads(string) return json.loads(string)
class NamedList: class NamedList:
def __init__(self, keys, *args, **kwargs): def __init__(self, keys, *args, **kwargs):
self.__keys__ = keys.split() self.__keys__ = keys.split()
@ -49,8 +50,10 @@ class NamedList:
def __repr__(self): def __repr__(self):
return ",".join("%.7g" % val for val in self.aslist()) return ",".join("%.7g" % val for val in self.aslist())
class PpmsSim: class PpmsSim:
CHANNELS = 'st t mf pos r1 i1 r2 i2'.split() CHANNELS = 'st t mf pos r1 i1 r2 i2'.split()
def __init__(self): def __init__(self):
self.status = NamedList('t mf ch pos', 1, 1, 1, 1) self.status = NamedList('t mf ch pos', 1, 1, 1, 1)
self.st = 0x1111 self.st = 0x1111
@ -176,7 +179,6 @@ class PpmsSim:
if abs(self.t - self.temp.target) < 1: if abs(self.t - self.temp.target) < 1:
self.status.t = 6 # outside tolerance self.status.t = 6 # outside tolerance
if abs(self.pos - self.move.target) < 0.01: if abs(self.pos - self.move.target) < 0.01:
self.status.pos = 1 self.status.pos = 1
else: else:
@ -188,7 +190,6 @@ class PpmsSim:
self.r2 = 1000 / self.t self.r2 = 1000 / self.t
self.i2 = math.log(self.t) self.i2 = math.log(self.t)
self.level.value = 100 - (self.time - self.start) * 0.01 % 100 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): def getdat(self, mask):
mask = int(mask) & 0xff # all channels up to i2 mask = int(mask) & 0xff # all channels up to i2
@ -198,6 +199,7 @@ class PpmsSim:
output.append("%.7g" % getattr(self, chan)) output.append("%.7g" % getattr(self, chan))
return ",".join(output) return ",".join(output)
class QDevice: class QDevice:
def __init__(self, classid): def __init__(self, classid):
self.sim = PpmsSim() self.sim = PpmsSim()
@ -225,5 +227,6 @@ class QDevice:
result = "OK" result = "OK"
return result return result
def shutdown(): def shutdown():
pass pass

View File

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

View File

@ -22,22 +22,28 @@
"""simulated transducer DPM3 read out""" """simulated transducer DPM3 read out"""
import random import random
import math
from secop.core import Readable, Parameter, FloatRange, Attached from secop.core import Readable, Parameter, FloatRange, Attached
from secop.lib import clamp from secop.lib import clamp
class DPM3(Readable): class DPM3(Readable):
motor = Attached() 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) hysteresis = Parameter('simulated hysteresis', FloatRange(unit='deg'), default=100, readonly=False)
friction = Parameter('friction', FloatRange(unit='N/deg'), default=1, readonly=False) friction = Parameter('friction', FloatRange(unit='N/deg'), default=2.5, readonly=False)
slope = Parameter('slope', FloatRange(unit='N/deg'), default=10, readonly=False) slope = Parameter('slope', FloatRange(unit='N/deg'), default=0.5, readonly=False)
offset = Parameter('offset', FloatRange(unit='N'), default=0, 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): def read_value(self):
mot = self._motor mot = self._motor
self._pos = clamp(self._pos, mot.value - self.hysteresis * 0.5, mot.value + self.hysteresis * 0.5) d = self.friction * self.slope
return clamp(0, 1, mot.value - self._pos) * self.friction \ self._pos = clamp(self._pos, mot.value - d, mot.value + d)
+ self._pos * self.slope + self.jitter * (random.random() - random.random()) 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 math
import os import os
from os.path import basename, exists, join from os.path import basename, dirname, exists, join
import numpy as np import numpy as np
from scipy.interpolate import splev, splrep # pylint: disable=import-error from scipy.interpolate import splev, splrep # pylint: disable=import-error
@ -109,7 +109,9 @@ class CalCurve:
calibname = sensopt.pop(0) calibname = sensopt.pop(0)
_, dot, ext = basename(calibname).rpartition('.') _, dot, ext = basename(calibname).rpartition('.')
kind = None 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 # first try without adding kind
filename = join(path.strip(), calibname) filename = join(path.strip(), calibname)
if exists(filename): if exists(filename):
@ -145,7 +147,7 @@ class CalCurve:
for line in f: for line in f:
parser.parse(line) parser.parse(line)
except Exception as e: 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_x = nplog if parser.logx else linear
self.convert_y = npexp if parser.logy else linear self.convert_y = npexp if parser.logy else linear
x = np.asarray(parser.xdata) x = np.asarray(parser.xdata)
@ -157,8 +159,8 @@ class CalCurve:
raise ValueError('calib curve %s is not monotonic' % calibspec) raise ValueError('calib curve %s is not monotonic' % calibspec)
try: try:
self.spline = splrep(x, y, s=0, k=min(3, len(x) - 1)) self.spline = splrep(x, y, s=0, k=min(3, len(x) - 1))
except (ValueError, TypeError): except (ValueError, TypeError) as e:
raise ValueError('invalid calib curve %s' % calibspec) raise ValueError('invalid calib curve %s' % calibspec) from e
def __call__(self, value): def __call__(self, value):
"""convert value """convert value
@ -178,8 +180,9 @@ class Sensor(Readable):
pollinterval = Parameter(export=False) pollinterval = Parameter(export=False)
status = Parameter(default=(Readable.Status.ERROR, 'unintialized')) status = Parameter(default=(Readable.Status.ERROR, 'unintialized'))
pollerClass = None description = 'a calibrated sensor value'
_value_error = None _value_error = None
enablePoll = False
def checkProperties(self): def checkProperties(self):
if 'description' not in self.propertyValues: if 'description' not in self.propertyValues:
@ -187,6 +190,7 @@ class Sensor(Readable):
super().checkProperties() super().checkProperties()
def initModule(self): def initModule(self):
super().initModule()
self._rawsensor.registerCallbacks(self, ['status']) # auto update status self._rawsensor.registerCallbacks(self, ['status']) # auto update status
self._calib = CalCurve(self.calib) self._calib = CalCurve(self.calib)
if self.description == '_': if self.description == '_':

View File

@ -24,13 +24,12 @@
import time import time
import struct import struct
from math import log10
from secop.core import BoolType, Command, EnumType, FloatRange, IntRange, \ 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.io import BytesIO
from secop.errors import CommunicationFailedError, HardwareError, BadValueError, IsBusyError from secop.errors import CommunicationFailedError, HardwareError, BadValueError, IsBusyError
from secop.rwhandler import ReadHandler, WriteHandler
MOTOR_STOP = 3 MOTOR_STOP = 3
MOVE = 4 MOVE = 4
@ -50,84 +49,78 @@ MAX_SPEED = 2047 * SPEED_SCALE
ACCEL_SCALE = 1E12 / 2 ** 31 * ANGLE_SCALE ACCEL_SCALE = 1E12 / 2 ** 31 * ANGLE_SCALE
MAX_ACCEL = 2047 * ACCEL_SCALE MAX_ACCEL = 2047 * ACCEL_SCALE
CURRENT_SCALE = 2.8/250 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): def writable(*args, **kwds):
adr = Property('parameter address', IntRange(0, 255), export=False) """convenience function to create writable hardware parameters"""
scale = Property('scale factor (physical value / unit)', FloatRange(), export=False) return PersistentParam(*args, readonly=False, initwrite=True, **kwds)
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
class Motor(PersistentMixin, HasIodev, Drivable): class Motor(PersistentMixin, HasIO, Drivable):
address = Property('module address', IntRange(0, 255), default=1) 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) zero = PersistentParam('zero point', FloatRange(unit='$'), readonly=False, default=0)
encoder = HwParam('encoder reading', FloatRange(unit='$', fmtstr='%.1f'), encoder = PersistentParam('encoder reading', FloatRange(unit='$', fmtstr='%.1f'),
209, ANGLE_SCALE, readonly=True, initwrite=False, persistent=True) readonly=True, initwrite=False)
steppos = HwParam('position from motor steps', FloatRange(unit='$'), steppos = PersistentParam('position from motor steps', FloatRange(unit='$', fmtstr='%.3f'),
1, ANGLE_SCALE, readonly=True, initwrite=False) readonly=True, initwrite=False)
target = Parameter('_', FloatRange(unit='$'), default=0) target = Parameter('', FloatRange(unit='$'), default=0)
move_limit = Parameter('max. angle to drive in one go when current above safe_current', FloatRange(unit='$'), move_limit = Parameter('max. angle to drive in one go', FloatRange(unit='$'),
readonly=False, default=5, group='more') readonly=False, default=360, group='more')
safe_current = Parameter('motor current allowed for big steps', FloatRange(unit='A'),
readonly=False, default=0.2, group='more')
tolerance = Parameter('positioning tolerance', FloatRange(unit='$'), tolerance = Parameter('positioning tolerance', FloatRange(unit='$'),
readonly=False, default=0.9) readonly=False, default=0.9)
encoder_tolerance = HwParam('the allowed deviation between steppos and encoder\n\nmust be > tolerance', encoder_tolerance = writable('the allowed deviation between steppos and encoder\n\nmust be > tolerance',
FloatRange(0, 360., unit='$'), FloatRange(0, 360., unit='$', fmtstr='%.3f'), group='more')
212, ANGLE_SCALE, readonly=False, group='more') speed = writable('max. speed', FloatRange(0, MAX_SPEED, unit='$/sec', fmtstr='%.1f'), default=40)
speed = HwParam('max. speed', FloatRange(0, MAX_SPEED, unit='$/sec'), minspeed = writable('min. speed', FloatRange(0, MAX_SPEED, unit='$/sec', fmtstr='%.1f'),
4, SPEED_SCALE, readonly=False, group='motorparam') default=SPEED_SCALE, group='motorparam')
minspeed = HwParam('min. speed', FloatRange(0, MAX_SPEED, unit='$/sec'), currentspeed = Parameter('current speed', FloatRange(-MAX_SPEED, MAX_SPEED, unit='$/sec', fmtstr='%.1f'),
130, SPEED_SCALE, readonly=False, default=SPEED_SCALE, group='motorparam') group='motorparam')
currentspeed = HwParam('current speed', FloatRange(-MAX_SPEED, MAX_SPEED, unit='$/sec'), maxcurrent = writable('', FloatRange(0, 2.8, unit='A', fmtstr='%.2f'),
3, SPEED_SCALE, readonly=True, group='motorparam') default=1.4, group='motorparam')
maxcurrent = HwParam('_', FloatRange(0, 2.8, unit='A'), standby_current = writable('', FloatRange(0, 2.8, unit='A', fmtstr='%.2f'),
6, CURRENT_SCALE, readonly=False, group='motorparam') default=0.1, group='motorparam')
standby_current = HwParam('_', FloatRange(0, 2.8, unit='A'), acceleration = writable('', FloatRange(4.6 * ACCEL_SCALE, MAX_ACCEL, unit='deg/s^2', fmtstr='%.1f'),
7, CURRENT_SCALE, readonly=False, group='motorparam') default=150., group='motorparam')
acceleration = HwParam('_', FloatRange(4.6 * ACCEL_SCALE, MAX_ACCEL, unit='deg/s^2'), target_reached = Parameter('', BoolType(), group='hwstatus')
5, ACCEL_SCALE, readonly=False, group='motorparam') move_status = Parameter('', IntRange(0, 3), group='hwstatus')
target_reached = HwParam('_', BoolType(), 8, group='hwstatus') error_bits = Parameter('', IntRange(0, 255), group='hwstatus')
move_status = HwParam('_', IntRange(0, 3), free_wheeling = writable('', FloatRange(0, 60., unit='sec', fmtstr='%.2f'),
207, readonly=True, group='hwstatus') default=0.1, group='motorparam')
error_bits = HwParam('_', IntRange(0, 255), power_down_delay = writable('', FloatRange(0, 60., unit='sec', fmtstr='%.2f'),
208, readonly=True, group='hwstatus') default=0.1, group='motorparam')
# the doc says msec, but I believe the scale is 10 msec baudrate = Parameter('', EnumType({'%d' % v: i for i, v in enumerate(BAUDRATES)}),
free_wheeling = HwParam('_', FloatRange(0, 60., unit='sec'), readonly=False, default=0, visibility=3, group='more')
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')
pollinterval = Parameter(group='more') pollinterval = Parameter(group='more')
ioClass = BytesIO
iodevClass = BytesIO fast_pollfactor = 0.001 # not used any more, TODO: use a statemachine for running
# fast_pollfactor = 0.001 # poll as fast as possible when busy
fast_pollfactor = 0.05
_started = 0 _started = 0
_calc_timeout = True _calcTimeout = True
_need_reset = None _need_reset = None
_last_change = 0
def comm(self, cmd, adr, value=0, bank=0): def comm(self, cmd, adr, value=0, bank=0):
"""set or get a parameter """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 :param value: if given, the parameter is written, else it is returned
:return: the returned value :return: the returned value
""" """
if self._calc_timeout: if self._calcTimeout and self.io._conn:
self._calc_timeout = False self._calcTimeout = False
baudrate = getattr(self._iodev._conn.connection, 'baudrate', None) baudrate = getattr(self.io._conn.connection, 'baudrate', None)
if baudrate: if baudrate:
if baudrate not in BAUDRATES: if baudrate not in BAUDRATES:
raise CommunicationFailedError('unsupported baud rate: %d' % baudrate) raise CommunicationFailedError('unsupported baud rate: %d' % baudrate)
self._iodev.timeout = 0.03 + 200 / baudrate self.io.timeout = 0.03 + 200 / baudrate
exc = None exc = None
for itry in range(3,0,-1):
byt = struct.pack('>BBBBi', self.address, cmd, adr, bank, round(value)) byt = struct.pack('>BBBBi', self.address, cmd, adr, bank, round(value))
byt += bytes([sum(byt) & 0xff])
for itry in range(3,0,-1):
try: try:
reply = self._iodev.communicate(byt + bytes([sum(byt) & 0xff]), 9) reply = self.communicate(byt, 9)
if sum(reply[:-1]) & 0xff != reply[-1]: if sum(reply[:-1]) & 0xff != reply[-1]:
raise CommunicationFailedError('checksum error') raise CommunicationFailedError('checksum error')
# will try again
except Exception as e: except Exception as e:
if itry == 1: if itry == 1:
raise raise
@ -168,36 +163,18 @@ class Motor(PersistentMixin, HasIodev, Drivable):
raise CommunicationFailedError('bad reply %r to command %s %d' % (reply, cmd, adr)) raise CommunicationFailedError('bad reply %r to command %s %d' % (reply, cmd, adr))
return result return result
def get(self, pname, **kwds): def startModule(self, start_events):
"""get parameter""" super().startModule(start_events)
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 set(self, pname, value, check=True, **kwds): def fix_encoder(self=self):
"""set parameter and check result""" try:
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 startModule(self, started_callback):
# get encoder value from motor. at this stage self.encoder contains the persistent value # get encoder value from motor. at this stage self.encoder contains the persistent value
encoder = self.get('encoder') encoder = self._read_axispar(ENCODER_ADR, ANGLE_SCALE) + self.zero
encoder += self.zero
self.fix_encoder(encoder) self.fix_encoder(encoder)
super().startModule(started_callback) except Exception as e:
self.log.error('fix_encoder failed with %r', e)
start_events.queue(fix_encoder)
def fix_encoder(self, encoder_from_hw): def fix_encoder(self, encoder_from_hw):
"""fix encoder value """fix encoder value
@ -211,14 +188,52 @@ class Motor(PersistentMixin, HasIodev, Drivable):
# calculate nearest, most probable value # calculate nearest, most probable value
adjusted_encoder = encoder_from_hw + round((self.encoder - encoder_from_hw) / 360.) * 360 adjusted_encoder = encoder_from_hw + round((self.encoder - encoder_from_hw) / 360.) * 360
if abs(self.encoder - adjusted_encoder) >= self.encoder_tolerance: 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.log.error('saved encoder value (%.2f) does not match reading (%.2f %.2f)',
self.encoder, encoder_from_hw, adjusted_encoder) self.encoder, encoder_from_hw, adjusted_encoder)
if adjusted_encoder != encoder_from_hw: if adjusted_encoder != encoder_from_hw:
self.log.info('take next closest encoder value (%.2f)' % adjusted_encoder) self.log.info('take next closest encoder value (%.2f)' % adjusted_encoder)
self._need_reset = True self._need_reset = True
self.status = self.Status.ERROR, 'saved encoder value does not match reading' 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): def read_value(self):
encoder = self.read_encoder() encoder = self.read_encoder()
@ -240,7 +255,7 @@ class Motor(PersistentMixin, HasIodev, Drivable):
self._need_reset = True self._need_reset = True
self.status = self.Status.ERROR, 'power loss' self.status = self.Status.ERROR, 'power loss'
# or should we just fix instead of error status? # 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.comm(SET_GLOB_PAR, 255, 1, bank=2) # set initialized flag
self._started = 0 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) 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.ERROR, 'encoder does not match internal pos'
return self.status return self.status
now = self.parameters['steppos'].timestamp if oldpos != self.steppos or not (self.read_target_reached() or self.read_move_status()
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()): or self.read_error_bits()):
return self.Status.BUSY, 'moving' return self.Status.BUSY, 'moving'
# TODO: handle the different errors from move_status and error_bits
diff = self.target - self.encoder diff = self.target - self.encoder
if abs(diff) <= self.tolerance: if abs(diff) <= self.tolerance:
self._started = 0 self._started = 0
@ -273,9 +285,8 @@ class Motor(PersistentMixin, HasIodev, Drivable):
def write_target(self, target): def write_target(self, target):
self.read_value() # make sure encoder and steppos are fresh self.read_value() # make sure encoder and steppos are fresh
if self.maxcurrent >= self.safe_current + CURRENT_SCALE and ( if abs(target - self.encoder) > self.move_limit:
abs(target - self.encoder) > self.move_limit + self.tolerance): raise BadValueError('can not move more than %s deg' % self.move_limit)
raise BadValueError('can not move more than %s deg %g %g' % (self.move_limit, self.encoder, target))
diff = self.encoder - self.steppos diff = self.encoder - self.steppos
if self._need_reset: if self._need_reset:
raise HardwareError('need reset (%s)' % self.status[1]) raise HardwareError('need reset (%s)' % self.status[1])
@ -284,90 +295,28 @@ class Motor(PersistentMixin, HasIodev, Drivable):
self._need_reset = True self._need_reset = True
self.status = self.Status.ERROR, 'encoder does not match internal pos' self.status = self.Status.ERROR, 'encoder does not match internal pos'
raise HardwareError('need reset (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._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.comm(MOVE, 0, (target - self.zero) / ANGLE_SCALE)
self.status = self.Status.BUSY, 'changed target' self.status = self.Status.BUSY, 'changed target'
return target return target
def write_zero(self, value): def write_zero(self, value):
diff = value - self.zero self.zero = value
self.encoder += diff self.read_value() # apply zero to encoder, steppos and value
self.steppos += diff return Done
self.value += diff
return value
def read_encoder(self): def read_encoder(self):
return self.get('encoder') + self.zero return self._read_axispar(ENCODER_ADR, ANGLE_SCALE) + self.zero
def read_steppos(self): def read_steppos(self):
return self.get('steppos') + self.zero return self._read_axispar(STEPPOS_ADR, ANGLE_SCALE) + 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')
@Command(FloatRange()) @Command(FloatRange())
def set_zero(self, value): def set_zero(self, value):
raw = self.read_value() - self.zero """adjust zero"""
self.write_zero(value - raw) self.write_zero(value - self.read_value())
def read_baudrate(self): def read_baudrate(self):
return self.comm(GET_GLOB_PAR, 65) 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""" """set steppos to encoder value, if not within tolerance"""
if self._started: if self._started:
raise IsBusyError('can not reset while moving') raise IsBusyError('can not reset while moving')
tol = ENCODER_RESOLUTION tol = ENCODER_RESOLUTION * 1.1
for itry in range(10): for itry in range(10):
diff = self.read_encoder() - self.read_steppos() diff = self.read_encoder() - self.read_steppos()
if abs(diff) <= tol: if abs(diff) <= tol:
self._need_reset = False self._need_reset = False
self.status = self.Status.IDLE, 'ok' self.status = self.Status.IDLE, 'ok'
return 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) self.comm(MOVE, 0, (self.encoder - self.zero) / ANGLE_SCALE)
time.sleep(0.1) time.sleep(0.1)
if itry > 5: if itry > 5:
@ -397,23 +346,33 @@ class Motor(PersistentMixin, HasIodev, Drivable):
@Command() @Command()
def stop(self): def stop(self):
"""stop motor immediately"""
self.comm(MOTOR_STOP, 0) self.comm(MOTOR_STOP, 0)
self.status = self.Status.IDLE, 'stopped' self.status = self.Status.IDLE, 'stopped'
self.target = self.value # indicate to customers that this was stopped
self._started = 0 self._started = 0
#@Command() @Command()
#def step(self): def step_forward(self):
# self.comm(MOVE, 1, FULL_STEP / ANGLE_SCALE) """move one full step forwards
#@Command() for quick tests
#def back(self): """
# self.comm(MOVE, 1, - FULL_STEP / ANGLE_SCALE) self.comm(MOVE, 1, FULL_STEP / ANGLE_SCALE)
#@Command(IntRange(), result=IntRange()) @Command()
#def get_axis_par(self, adr): def step_back(self):
# return self.comm(GET_AXIS_PAR, adr) """move one full step backwards
#@Command((IntRange(), FloatRange()), result=IntRange()) for quick tests
#def set_axis_par(self, adr, value): """
# return self.comm(SET_AXIS_PAR, adr, value) 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 True
return False return False
def next_action(self, action, do_now=True): def next_action(self, action):
"""call next action """call next action
:param action: function to be called next time :param action: function to be called next time
@ -116,8 +116,6 @@ class Uniax(PersistentMixin, Drivable):
self._action = action self._action = action
self._init_action = True self._init_action = True
self.log.info('action %r', action.__name__) self.log.info('action %r', action.__name__)
if do_now:
self._next_cycle = False
def init_action(self): def init_action(self):
"""return true when called the first time after next_action""" """return true when called the first time after next_action"""
@ -126,13 +124,6 @@ class Uniax(PersistentMixin, Drivable):
return True return True
return False 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,): def zero_pos(self, value,):
"""get high_pos or low_pos, depending on sign of 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.log.info('motor stopped - substantial force detected: %g', force)
self._motor.stop() self._motor.stop()
elif self.init_action(): elif self.init_action():
self.next_action(self.adjust, True) self.next_action(self.adjust)
return return
if abs(force) > self.hysteresis: if abs(force) > self.hysteresis:
self.set_zero_pos(force, self._motor.read_value()) self.set_zero_pos(force, self._motor.read_value())
self.next_action(self.adjust, True) self.next_action(self.adjust)
return return
if force * sign < -self.hysteresis: if force * sign < -self.hysteresis:
self._previous_force = force self._previous_force = force
@ -333,7 +324,7 @@ class Uniax(PersistentMixin, Drivable):
return Done return Done
if self.zero_pos(force) is None and abs(force) > self.hysteresis and self._filtered: 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.set_zero_pos(force, self._motor.read_value())
self.execute_action() self._action(self.value, self.target)
return Done return Done
def write_target(self, target): def write_target(self, target):
@ -351,7 +342,10 @@ class Uniax(PersistentMixin, Drivable):
self._cnt_rderr = 0 self._cnt_rderr = 0
self._cnt_wrerr = 0 self._cnt_wrerr = 0
self.status = 'BUSY', 'changed target' 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 return target
@Command() @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, \ from secop.datatypes import ArrayOf, BLOBType, BoolType, \
CommandType, ConfigError, DataType, Enum, EnumType, FloatRange, \ CommandType, ConfigError, DataType, Enum, EnumType, FloatRange, \
IntRange, ProgrammingError, ScaledInteger, StatusType, \ 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): def copytest(dt):
@ -36,6 +38,7 @@ def copytest(dt):
assert dt.export_datatype() == dt.copy().export_datatype() assert dt.export_datatype() == dt.copy().export_datatype()
assert dt != dt.copy() assert dt != dt.copy()
def test_DataType(): def test_DataType():
dt = DataType() dt = DataType()
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
@ -116,7 +119,6 @@ def test_IntRange():
dt('1.3') dt('1.3')
dt(1) dt(1)
dt(0) dt(0)
dt('1')
with pytest.raises(ProgrammingError): with pytest.raises(ProgrammingError):
IntRange('xc', 'Yx') IntRange('xc', 'Yx')
@ -132,6 +134,7 @@ def test_IntRange():
with pytest.raises(ConfigError): with pytest.raises(ConfigError):
dt.checkProperties() dt.checkProperties()
def test_ScaledInteger(): def test_ScaledInteger():
dt = ScaledInteger(0.01, -3, 3) dt = ScaledInteger(0.01, -3, 3)
copytest(dt) copytest(dt)
@ -407,6 +410,7 @@ def test_ArrayOf():
dt = ArrayOf(EnumType('myenum', single=0), 5) dt = ArrayOf(EnumType('myenum', single=0), 5)
copytest(dt) copytest(dt)
def test_TupleOf(): def test_TupleOf():
# test constructor catching illegal arguments # test constructor catching illegal arguments
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -641,6 +645,7 @@ def test_oneway_compatible(dt, contained_in):
with pytest.raises(ValueError): with pytest.raises(ValueError):
contained_in.compatible(dt) contained_in.compatible(dt)
@pytest.mark.parametrize('dt1, dt2', [ @pytest.mark.parametrize('dt1, dt2', [
(FloatRange(-5.5, 5.5), ScaledInteger(10, -5.5, 5.5)), (FloatRange(-5.5, 5.5), ScaledInteger(10, -5.5, 5.5)),
(IntRange(0,1), BoolType()), (IntRange(0,1), BoolType()),
@ -650,6 +655,7 @@ def test_twoway_compatible(dt1, dt2):
dt1.compatible(dt1) dt1.compatible(dt1)
dt2.compatible(dt2) dt2.compatible(dt2)
@pytest.mark.parametrize('dt1, dt2', [ @pytest.mark.parametrize('dt1, dt2', [
(StringType(), FloatRange()), (StringType(), FloatRange()),
(IntRange(-10, 10), StringType()), (IntRange(-10, 10), StringType()),
@ -665,3 +671,12 @@ def test_incompatible(dt1, dt2):
dt1.compatible(dt2) dt1.compatible(dt2)
with pytest.raises(ValueError): with pytest.raises(ValueError):
dt2.compatible(dt1) 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: 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): def __init__(self, updates):
self.updates = updates self.updates = updates
@ -108,6 +112,7 @@ def test_IOHandler():
group1 = Hdl('group1', 'SIMPLE?', '%g') group1 = Hdl('group1', 'SIMPLE?', '%g')
group2 = Hdl('group2', 'CMD?%(channel)d', '%g,%s,%d') group2 = Hdl('group2', 'CMD?%(channel)d', '%g,%s,%d')
class Module1(Module): class Module1(Module):
channel = Property('the channel', IntRange(), default=3) channel = Property('the channel', IntRange(), default=3)
loop = Property('the loop', IntRange(), default=2) 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) 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) 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 assert data.pop('command') == command
return data.pop('reply') return data.pop('reply')
@ -141,7 +146,7 @@ def test_IOHandler():
print(updates) print(updates)
updates.clear() # get rid of updates from initialisation updates.clear() # get rid of updates from initialisation
# for sendRecv # for communicate
data.push('command', 'SIMPLE?') data.push('command', 'SIMPLE?')
data.push('reply', '4.51') data.push('reply', '4.51')
# for analyze_group1 # for analyze_group1
@ -154,7 +159,7 @@ def test_IOHandler():
assert updates.pop('simple') == 45.1 assert updates.pop('simple') == 45.1
assert not updates assert not updates
# for sendRecv # for communicate
data.push('command', 'CMD?3') data.push('command', 'CMD?3')
data.push('reply', '1.23,text,5') data.push('reply', '1.23,text,5')
# for analyze_group2 # for analyze_group2
@ -167,7 +172,7 @@ def test_IOHandler():
assert data.empty() assert data.empty()
assert not updates assert not updates
# for sendRecv # for communicate
data.push('command', 'CMD?3') data.push('command', 'CMD?3')
data.push('reply', '1.23,text,5') data.push('reply', '1.23,text,5')
# for analyze_group2 # for analyze_group2
@ -178,7 +183,7 @@ def test_IOHandler():
data.push('self', 12.3, 'string') data.push('self', 12.3, 'string')
data.push('new', 12.3, 'FOO') data.push('new', 12.3, 'FOO')
data.push('changed', 1.23, 'foo', 9) data.push('changed', 1.23, 'foo', 9)
# for sendRecv # for communicate
data.push('command', 'CMD 3,1.23,foo,9|CMD?3') data.push('command', 'CMD 3,1.23,foo,9|CMD?3')
data.push('reply', '1.23,foo,9') data.push('reply', '1.23,foo,9')
# for analyze_group2 # for analyze_group2

View File

@ -64,6 +64,7 @@ def test_EnumMember():
assert a != 3 assert a != 3
assert a == 1 assert a == 1
def test_Enum(): def test_Enum():
e1 = Enum('e1') e1 = Enum('e1')
e2 = Enum('e2', e1, a=1, b=3) e2 = Enum('e2', e1, a=1, b=3)
@ -75,3 +76,4 @@ def test_Enum():
assert e2.b > e3.a assert e2.b > e3.a
assert e3.c >= e2.a assert e3.c >= e2.a
assert e3.b <= e2.b 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.""" """test data types."""
import sys
import threading import threading
import pytest 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.errors import ProgrammingError, ConfigError
from secop.modules import Communicator, Drivable, Readable, Module from secop.modules import Communicator, Drivable, Readable, Module
from secop.params import Command, Parameter 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: 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): def __init__(self, updates):
self.updates = updates self.updates = updates
@ -48,9 +53,10 @@ class DispatcherStub:
class LoggerStub: class LoggerStub:
def debug(self, *args): def debug(self, fmt, *args):
print(*args) print(fmt % args)
info = warning = exception = debug info = warning = exception = error = debug
handlers = []
logger = LoggerStub() logger = LoggerStub()
@ -61,13 +67,23 @@ class ServerStub:
self.dispatcher = DispatcherStub(updates) 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(): def test_Communicator():
o = Communicator('communicator', LoggerStub(), {'.description': ''}, ServerStub({})) o = Communicator('communicator', LoggerStub(), {'.description': ''}, ServerStub({}))
o.earlyInit() o.earlyInit()
o.initModule() o.initModule()
event = threading.Event() event = DummyMultiEvent()
o.startModule(event.set) o.initModule()
assert event.is_set() # event should be set immediately o.startModule(event)
assert event.wait(timeout=0.1)
def test_ModuleMagic(): def test_ModuleMagic():
@ -83,14 +99,13 @@ def test_ModuleMagic():
a1 = Parameter('a1', datatype=BoolType(), default=False) a1 = Parameter('a1', datatype=BoolType(), default=False)
a2 = Parameter('a2', datatype=BoolType(), default=True) a2 = Parameter('a2', datatype=BoolType(), default=True)
value = Parameter(datatype=StringType(), default='first') value = Parameter(datatype=StringType(), default='first')
target = Parameter(datatype=StringType(), default='')
@Command(argument=BoolType(), result=BoolType()) @Command(argument=BoolType(), result=BoolType())
def cmd2(self, arg): def cmd2(self, arg):
"""another stuff""" """another stuff"""
return not arg return not arg
pollerClass = BasicPoller
def read_param1(self): def read_param1(self):
return True return True
@ -100,12 +115,16 @@ def test_ModuleMagic():
def read_a1(self): def read_a1(self):
return True return True
@nopoll
def read_a2(self): def read_a2(self):
return True return True
def read_value(self): def read_value(self):
return 'second' return 'second'
def read_status(self):
return 'IDLE', 'ok'
with pytest.raises(ProgrammingError): with pytest.raises(ProgrammingError):
class Mod1(Module): # pylint: disable=unused-variable class Mod1(Module): # pylint: disable=unused-variable
def do_this(self): # old style command def do_this(self): # old style command
@ -128,15 +147,19 @@ def test_ModuleMagic():
return arg return arg
value = Parameter(datatype=FloatRange(unit='deg')) value = Parameter(datatype=FloatRange(unit='deg'))
target = Parameter(datatype=FloatRange(), default=0)
a1 = Parameter(datatype=FloatRange(unit='$/s'), readonly=False) a1 = Parameter(datatype=FloatRange(unit='$/s'), readonly=False)
b2 = Parameter('<b2>', datatype=BoolType(), default=True, # remark: it might be a programming error to override the datatype
poll=True, readonly=False, initwrite=True) # 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): def write_a1(self, value):
self._a1_written = value self._a1_written = value
return value return value
def write_b2(self, value): def write_b2(self, value):
value = value.upper()
self._b2_written = value self._b2_written = value
return value return value
@ -166,33 +189,38 @@ def test_ModuleMagic():
# check for inital updates working properly # check for inital updates working properly
o1 = Newclass1('o1', logger, {'.description':''}, srv) 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, 'param1': False, 'param2': 1.0, 'a1': 0.0, 'a2': True, 'pollinterval': 5.0,
'value': 'first'} 'value': 'first'}
assert updates.pop('o1') == expectedBeforeStart assert updates.pop('o1') == expectedBeforeStart
o1.earlyInit() o1.earlyInit()
event = threading.Event() event = DummyMultiEvent()
o1.startModule(event.set) o1.initModule()
event.wait() o1.startModule(event)
assert event.wait(timeout=0.1)
# should contain polled values # should contain polled values
expectedAfterStart = {'status': (Drivable.Status.IDLE, ''), expectedAfterStart = {
'value': 'second'} 'status': (Drivable.Status.IDLE, 'ok'), 'value': 'second',
'param1': True, 'param2': 0.0, 'a1': True}
assert updates.pop('o1') == expectedAfterStart assert updates.pop('o1') == expectedAfterStart
# check in addition if parameters are written # check in addition if parameters are written
o2 = Newclass2('o2', logger, {'.description':'', 'a1': 2.7}, srv) o2 = Newclass2('o2', logger, {'.description':'', 'a1': 2.7}, srv)
# no update for b2, as this has to be written # no update for b2, as this has to be written
expectedBeforeStart['a1'] = 2.7 expectedBeforeStart['a1'] = 2.7
expectedBeforeStart['target'] = 0.0
assert updates.pop('o2') == expectedBeforeStart assert updates.pop('o2') == expectedBeforeStart
o2.earlyInit() o2.earlyInit()
event = threading.Event() event = DummyMultiEvent()
o2.startModule(event.set) o2.initModule()
event.wait() o2.startModule(event)
assert event.wait(timeout=0.1)
# value has changed type, b2 and a1 are written # 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 updates.pop('o2') == expectedAfterStart
assert o2._a1_written == 2.7 assert o2._a1_written == 2.7
assert o2._b2_written is True assert o2._b2_written == 'EMPTY'
assert not updates assert not updates
@ -206,13 +234,15 @@ def test_ModuleMagic():
# check '$' in unit works properly # check '$' in unit works properly
assert o2.parameters['a1'].datatype.unit == 'mm/s' assert o2.parameters['a1'].datatype.unit == 'mm/s'
cfg = Newclass2.configurables 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', 'meaning', 'visibility', 'implementation', 'interface_classes', 'target', 'stop',
'status', 'param1', 'param2', 'cmd', 'a2', 'pollinterval', 'b2', 'cmd2', 'value', 'status', 'param1', 'param2', 'cmd', 'a2', 'pollinterval', 'slowinterval', 'b2',
'a1'} 'cmd2', 'value', 'a1'}
assert set(cfg['value'].keys()) == {'group', 'export', 'relative_resolution', assert set(cfg['value'].keys()) == {
'group', 'export', 'relative_resolution',
'visibility', 'unit', 'default', 'datatype', 'fmtstr', 'visibility', 'unit', 'default', 'datatype', 'fmtstr',
'absolute_resolution', 'poll', 'max', 'min', 'readonly', 'constant', 'absolute_resolution', 'max', 'min', 'readonly', 'constant',
'description', 'needscfg'} 'description', 'needscfg'}
# check on the level of classes # check on the level of classes
@ -260,9 +290,101 @@ def test_param_inheritance():
Base('o', logger, {'description': ''}, srv) Base('o', logger, {'description': ''}, srv)
def test_mixin(): def test_command_inheritance():
# srv = ServerStub({}) 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 class Mixin: # no need to inherit from Module or HasAccessible
value = Parameter(unit='K') # missing datatype and description acceptable in mixins value = Parameter(unit='K') # missing datatype and description acceptable in mixins
param1 = Parameter('no datatype yet', fmtstr='%.5f') param1 = Parameter('no datatype yet', fmtstr='%.5f')
@ -276,7 +398,7 @@ def test_mixin():
param1 = Parameter(datatype=FloatRange()) param1 = Parameter(datatype=FloatRange())
with pytest.raises(ProgrammingError): with pytest.raises(ProgrammingError):
class MixedModule(Mixin): class MixedModule(Mixin): # pylint: disable=unused-variable
param1 = Parameter('', FloatRange(), fmtstr=0) # fmtstr must be a string param1 = Parameter('', FloatRange(), fmtstr=0) # fmtstr must be a string
assert repr(MixedDrivable.status.datatype) == repr(Drivable.status.datatype) assert repr(MixedDrivable.status.datatype) == repr(Drivable.status.datatype)
@ -305,10 +427,29 @@ def test_mixin():
}, srv) }, 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(): def test_command_config():
class Mod(Module): class Mod(Module):
@Command(IntRange(0, 1), result=IntRange(0, 1)) @Command(IntRange(0, 1), result=IntRange(0, 1))
def convert(self, value): def convert(self, value):
"""dummy conversion"""
return value return value
srv = ServerStub({}) srv = ServerStub({})
@ -332,3 +473,189 @@ def test_command_config():
'result': {'type': 'bool'}, '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 id(Mod.p3) == id(Base.p3)
assert repr(Mod.p2) == repr(Base.p2) # must be a clone assert repr(Mod.p2) == repr(Base.p2) # must be a clone
assert repr(Mod.p3) == repr(Base.p3) # 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 # manipulating default makes Base.p1 and Mod.p1 match
Mod.p1.default = False Mod.p1.default = False
assert repr(Mod.p1) == repr(Base.p1) 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(): def test_Export():
class Mod(HasAccessibles): class Mod(HasAccessibles):

View File

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

View File

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