Merge branch 'wip' of gitlab.psi.ch-samenv:samenv/frappy into wip
This commit is contained in:
commit
1da7657483
@ -162,7 +162,7 @@ max-line-length=132
|
||||
no-space-check=trailing-comma,dict-separator
|
||||
|
||||
# Maximum number of lines in a module
|
||||
max-module-lines=1200
|
||||
max-module-lines=1000
|
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||
# tab).
|
||||
|
2
Makefile
2
Makefile
@ -59,4 +59,4 @@ release:
|
||||
|
||||
|
||||
build-pkg:
|
||||
debocker build --image jenkinsng.admin.frm2:5000/mlzbase/buster
|
||||
debocker build --image docker.ictrl.frm2.tum.de:5443/mlzbase/buster
|
||||
|
18
README.md
18
README.md
@ -15,9 +15,8 @@ branches:
|
||||
- work: current working version, usually in use on /home/l_samenv/frappy (and on neutron instruments)
|
||||
this should be a copy of an earlier state of the wip branch
|
||||
- wip: current test version, usually in use on /home/l_samenv/frappy_wip
|
||||
|
||||
IMPORTANT: make commits containing either only files to be pushed to Gerrit or only
|
||||
PSI internal files, not mixed. Mark local commits with '[PSI]' in the commit message.
|
||||
IMPORTANT: make commits containing either only files to be pushed to Gerrit or only
|
||||
PSI internal files, not mixed. Mark local commits with '[PSI]' in the commit message.
|
||||
|
||||
|
||||
master --> mlz # these branches match after a sync step, but they might have a different history
|
||||
@ -35,7 +34,7 @@ where commits may be cherry picked for input to Gerrit. As generally in the revi
|
||||
changes are done, eventually a sync step should happen:
|
||||
|
||||
1) ideally, this is done when work and wip match
|
||||
2) make sure branches mlz, master, wip and work are in synv with remote, push/pull otherwise
|
||||
2) make sure branches mlz, master, wip and work are in syns with remote, push/pull otherwise
|
||||
3) cherry-pick commits from mlz to master
|
||||
4) make sure master and mlz branches match (git diff --name-only master..wip should only return README.md)
|
||||
5) create branch new_work from master
|
||||
@ -43,11 +42,12 @@ changes are done, eventually a sync step should happen:
|
||||
- core commits already pushed through gerrit are skipped
|
||||
- all other commits are to be cherry-picked
|
||||
7) when arrived at the point where the new working version should be,
|
||||
copy new_wip branch to work with 'git checkout work;git checkout new_wip .'
|
||||
(note the dot!) and then commit this.
|
||||
8) continue with (6) if wip and work should differ
|
||||
9) do like (7), but for wip branch
|
||||
10) delete new_wip branch, push master, wip and work branches
|
||||
copy new_wip branch to work with 'git checkout -B work'.
|
||||
Not sure if this works, as work is to be pushed to git.psi.ch.
|
||||
We might first remove the remote branch with 'git push origin --delete work'.
|
||||
And then create again (git push origin work)?
|
||||
8) continue with (6) if wip and work should differ, and do like (7) for wip branch
|
||||
9) delete new_wip branch, push master, wip and work branches
|
||||
|
||||
|
||||
## Procedure to update PPMS
|
||||
|
@ -27,12 +27,11 @@ import sys
|
||||
import argparse
|
||||
from os import path
|
||||
|
||||
import mlzlog
|
||||
|
||||
# Add import path for inplace usage
|
||||
sys.path.insert(0, path.abspath(path.join(path.dirname(__file__), '..')))
|
||||
|
||||
from secop.lib import getGeneralConfig
|
||||
from secop.lib import generalConfig
|
||||
from secop.logging import logger
|
||||
from secop.server import Server
|
||||
|
||||
|
||||
@ -60,15 +59,26 @@ def parseArgv(argv):
|
||||
parser.add_argument('-c',
|
||||
'--cfgfiles',
|
||||
action='store',
|
||||
help="comma separated list of cfg files\n"
|
||||
"defaults to <name_of_the_instance>\n"
|
||||
"cfgfiles given without '.cfg' extension are searched in the configuration directory,"
|
||||
help="comma separated list of cfg files,\n"
|
||||
"defaults to <name_of_the_instance>.\n"
|
||||
"cfgfiles given without '.cfg' extension are searched in the configuration directory, "
|
||||
"else they are treated as path names",
|
||||
default=None)
|
||||
parser.add_argument('-g',
|
||||
'--gencfg',
|
||||
action='store',
|
||||
help="full path of general config file,\n"
|
||||
"defaults to env. variable FRAPPY_CONFIG_FILE\n",
|
||||
default=None)
|
||||
parser.add_argument('-t',
|
||||
'--test',
|
||||
action='store_true',
|
||||
help='Check cfg files only',
|
||||
help='check cfg files only',
|
||||
default=False)
|
||||
parser.add_argument('-r',
|
||||
'--relaxed',
|
||||
action='store_true',
|
||||
help='no checking of problematic behaviour',
|
||||
default=False)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
@ -80,9 +90,12 @@ def main(argv=None):
|
||||
args = parseArgv(argv[1:])
|
||||
|
||||
loglevel = 'debug' if args.verbose else ('error' if args.quiet else 'info')
|
||||
mlzlog.initLogging('secop', loglevel, getGeneralConfig()['logdir'])
|
||||
generalConfig.defaults = {k: args.relaxed for k in (
|
||||
'lazy_number_validation', 'disable_value_range_check', 'legacy_hasiodev', 'tolerate_poll_property')}
|
||||
generalConfig.init(args.gencfg)
|
||||
logger.init(loglevel)
|
||||
|
||||
srv = Server(args.name, mlzlog.log, cfgfiles=args.cfgfiles, interface=args.port, testonly=args.test)
|
||||
srv = Server(args.name, logger.log, cfgfiles=args.cfgfiles, interface=args.port, testonly=args.test)
|
||||
|
||||
if args.daemonize:
|
||||
srv.start()
|
||||
|
31
cfg/cryosim.cfg
Normal file
31
cfg/cryosim.cfg
Normal 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
5
cfg/generalConfig.cfg
Normal file
@ -0,0 +1,5 @@
|
||||
[FRAPPY]
|
||||
# general config for running in git repo
|
||||
logdir = ./log
|
||||
piddir = ./pid
|
||||
confdir = ./cfg
|
@ -1,24 +1,23 @@
|
||||
[node LscSIM.psi.ch]
|
||||
[NODE]
|
||||
id = LscSIM.psi.ch
|
||||
description = Lsc Simulation at PSI
|
||||
|
||||
[interface tcp]
|
||||
type = tcp
|
||||
bindto = 0.0.0.0
|
||||
bindport = 5000
|
||||
[INTERFACE]
|
||||
uri = tcp://5000
|
||||
|
||||
[module res]
|
||||
[res]
|
||||
class = secop_psi.ls370res.ResChannel
|
||||
.channel = 3
|
||||
.description = resistivity
|
||||
.main = lsmain
|
||||
.iodev = lscom
|
||||
channel = 3
|
||||
description = resistivity
|
||||
main = lsmain
|
||||
io = lscom
|
||||
|
||||
[module lsmain]
|
||||
[lsmain]
|
||||
class = secop_psi.ls370res.Main
|
||||
.description = main control of Lsc controller
|
||||
.iodev = lscom
|
||||
description = main control of Lsc controller
|
||||
io = lscom
|
||||
|
||||
[module lscom]
|
||||
[lscom]
|
||||
class = secop_psi.ls370sim.Ls370Sim
|
||||
.description = simulated serial communicator to a LS 370
|
||||
.visibility = 3
|
||||
description = simulated serial communicator to a LS 370
|
||||
visibility = 3
|
||||
|
@ -1,24 +1,20 @@
|
||||
[NODE]
|
||||
id = ls370res.psi.ch
|
||||
[node LscSIM.psi.ch]
|
||||
description = Lsc370 Test
|
||||
|
||||
[INTERFACE]
|
||||
uri = tcp://5000
|
||||
[interface tcp]
|
||||
type = tcp
|
||||
bindto = 0.0.0.0
|
||||
bindport = 5000
|
||||
|
||||
[lsmain_iodev]
|
||||
description = the communication device
|
||||
class = secop_psi.ls370res.StringIO
|
||||
uri = localhost:4567
|
||||
|
||||
[lsmain]
|
||||
[module lsmain]
|
||||
class = secop_psi.ls370res.Main
|
||||
description = main control of Lsc controller
|
||||
iodev = lsmain_iodev
|
||||
uri = localhost:4567
|
||||
|
||||
[res]
|
||||
[module res]
|
||||
class = secop_psi.ls370res.ResChannel
|
||||
iexc = '1mA'
|
||||
channel = 5
|
||||
vexc = '2mV'
|
||||
channel = 3
|
||||
description = resistivity
|
||||
main = lsmain
|
||||
# the auto created iodev from lsmain:
|
||||
|
38
cfg/magsim.cfg
Normal file
38
cfg/magsim.cfg
Normal 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
|
92
cfg/ppms.cfg
92
cfg/ppms.cfg
@ -8,117 +8,117 @@ uri = tcp://5000
|
||||
[tt]
|
||||
class = secop_psi.ppms.Temp
|
||||
description = main temperature
|
||||
iodev = ppms
|
||||
io = ppms
|
||||
|
||||
[mf]
|
||||
class = secop_psi.ppms.Field
|
||||
target.min = -9
|
||||
target.max = 9
|
||||
.description = magnetic field
|
||||
.iodev = ppms
|
||||
description = magnetic field
|
||||
io = ppms
|
||||
|
||||
[pos]
|
||||
class = secop_psi.ppms.Position
|
||||
.description = sample rotator
|
||||
.iodev = ppms
|
||||
description = sample rotator
|
||||
io = ppms
|
||||
|
||||
[lev]
|
||||
class = secop_psi.ppms.Level
|
||||
.description = helium level
|
||||
.iodev = ppms
|
||||
description = helium level
|
||||
io = ppms
|
||||
|
||||
[chamber]
|
||||
class = secop_psi.ppms.Chamber
|
||||
.description = chamber state
|
||||
.iodev = ppms
|
||||
description = chamber state
|
||||
io = ppms
|
||||
|
||||
[r1]
|
||||
class = secop_psi.ppms.BridgeChannel
|
||||
.description = resistivity channel 1
|
||||
.no = 1
|
||||
description = resistivity channel 1
|
||||
no = 1
|
||||
value.unit = Ohm
|
||||
.iodev = ppms
|
||||
io = ppms
|
||||
|
||||
[r2]
|
||||
class = secop_psi.ppms.BridgeChannel
|
||||
.description = resistivity channel 2
|
||||
.no = 2
|
||||
description = resistivity channel 2
|
||||
no = 2
|
||||
value.unit = Ohm
|
||||
.iodev = ppms
|
||||
io = ppms
|
||||
|
||||
[r3]
|
||||
class = secop_psi.ppms.BridgeChannel
|
||||
.description = resistivity channel 3
|
||||
.no = 3
|
||||
description = resistivity channel 3
|
||||
no = 3
|
||||
value.unit = Ohm
|
||||
.iodev = ppms
|
||||
io = ppms
|
||||
|
||||
[r4]
|
||||
class = secop_psi.ppms.BridgeChannel
|
||||
.description = resistivity channel 4
|
||||
.no = 4
|
||||
description = resistivity channel 4
|
||||
no = 4
|
||||
value.unit = Ohm
|
||||
.iodev = ppms
|
||||
io = ppms
|
||||
|
||||
[i1]
|
||||
class = secop_psi.ppms.Channel
|
||||
.description = current channel 1
|
||||
.no = 1
|
||||
description = current channel 1
|
||||
no = 1
|
||||
value.unit = uA
|
||||
.iodev = ppms
|
||||
io = ppms
|
||||
|
||||
[i2]
|
||||
class = secop_psi.ppms.Channel
|
||||
.description = current channel 2
|
||||
.no = 2
|
||||
description = current channel 2
|
||||
no = 2
|
||||
value.unit = uA
|
||||
.iodev = ppms
|
||||
io = ppms
|
||||
|
||||
[i3]
|
||||
class = secop_psi.ppms.Channel
|
||||
.description = current channel 3
|
||||
.no = 3
|
||||
description = current channel 3
|
||||
no = 3
|
||||
value.unit = uA
|
||||
.iodev = ppms
|
||||
io = ppms
|
||||
|
||||
[i4]
|
||||
class = secop_psi.ppms.Channel
|
||||
.description = current channel 4
|
||||
.no = 4
|
||||
description = current channel 4
|
||||
no = 4
|
||||
value.unit = uA
|
||||
.iodev = ppms
|
||||
io = ppms
|
||||
|
||||
[v1]
|
||||
class = secop_psi.ppms.DriverChannel
|
||||
.description = voltage channel 1
|
||||
.no = 1
|
||||
description = voltage channel 1
|
||||
no = 1
|
||||
value.unit = V
|
||||
.iodev = ppms
|
||||
io = ppms
|
||||
|
||||
[v2]
|
||||
class = secop_psi.ppms.DriverChannel
|
||||
.description = voltage channel 2
|
||||
.no = 2
|
||||
description = voltage channel 2
|
||||
no = 2
|
||||
value.unit = V
|
||||
.iodev = ppms
|
||||
io = ppms
|
||||
|
||||
[tv]
|
||||
class = secop_psi.ppms.UserChannel
|
||||
.description = VTI temperature
|
||||
description = VTI temperature
|
||||
enabled = 1
|
||||
value.unit = K
|
||||
.iodev = ppms
|
||||
io = ppms
|
||||
|
||||
[ts]
|
||||
class = secop_psi.ppms.UserChannel
|
||||
.description = sample temperature
|
||||
description = sample temperature
|
||||
enabled = 1
|
||||
value.unit = K
|
||||
.iodev = ppms
|
||||
io = ppms
|
||||
|
||||
[ppms]
|
||||
class = secop_psi.ppms.Main
|
||||
.description = the main and poller module
|
||||
.class_id = QD.MULTIVU.PPMS.1
|
||||
.visibility = 3
|
||||
description = the main and poller module
|
||||
class_id = QD.MULTIVU.PPMS.1
|
||||
visibility = 3
|
||||
pollinterval = 2
|
||||
|
@ -5,26 +5,29 @@ description = [sim] uniaxial pressure device
|
||||
[INTERFACE]
|
||||
uri=tcp://5000
|
||||
|
||||
[drv]
|
||||
class = secop.simulation.SimDrivable
|
||||
extra_params = speed, safe_current, safe_step, maxcurrent
|
||||
description = simulated motor
|
||||
value.default = 0
|
||||
speed.readonly = False
|
||||
speed.default = 10
|
||||
interval = 0.11
|
||||
|
||||
[transducer]
|
||||
class = secop_psi.simdpm.DPM3
|
||||
description = simulated force
|
||||
motor = drv
|
||||
|
||||
[force]
|
||||
class = secop_psi.uniax.Uniax
|
||||
description = uniax driver
|
||||
motor = drv
|
||||
transducer = transducer
|
||||
|
||||
[drv]
|
||||
class = secop.simulation.SimDrivable
|
||||
extra_params = speed, safe_current, move_limit, maxcurrent, tolerance
|
||||
description = simulated motor
|
||||
value.default = 0
|
||||
speed.readonly = False
|
||||
speed.default = 40
|
||||
interval = 0.11
|
||||
value.unit = deg
|
||||
tolerance.default = 0.9
|
||||
|
||||
[transducer]
|
||||
class = secop_psi.simdpm.DPM3
|
||||
description = simulated force
|
||||
motor = drv
|
||||
value.unit = 'N'
|
||||
|
||||
[res]
|
||||
class = secop.simulation.SimReadable
|
||||
description = raw temperature sensor on the stick
|
||||
@ -37,5 +40,5 @@ value.datatype = {"type":"double", "unit":"Ohm"}
|
||||
class=secop_psi.softcal.Sensor
|
||||
description=temperature sensor, soft calibration
|
||||
rawsensor=res
|
||||
calib = X132254.340
|
||||
calib = X132254
|
||||
value.unit = "K"
|
||||
|
@ -49,5 +49,5 @@ channel = A
|
||||
[T]
|
||||
class = secop_psi.softcal.Sensor
|
||||
rawsensor = res
|
||||
calib = /home/l_samenv/frappy/secop_psi/calcurves/X132254.340
|
||||
calib = X132254
|
||||
value.unit = K
|
||||
|
40
ci/Jenkinsfile
vendored
40
ci/Jenkinsfile
vendored
@ -30,6 +30,8 @@ def changedFiles = '';
|
||||
|
||||
def run_pylint(pyver) {
|
||||
stage ('pylint-' + pyver) {
|
||||
def cpylint = "RUNNING"
|
||||
gerritPostCheck(["jenkins:pylint_${pyver}": cpylint])
|
||||
def status = 'OK'
|
||||
changedFiles = sh returnStdout: true, script: '''\
|
||||
#!/bin/bash
|
||||
@ -56,8 +58,8 @@ fi
|
||||
withCredentials([string(credentialsId: 'GERRITHTTP',
|
||||
variable: 'GERRITHTTP')]) {
|
||||
sh """\
|
||||
#!/bin/bash
|
||||
if [ -f pylint_results.txt ] ; then
|
||||
#!/bin/bash
|
||||
if [ -f pylint_results.txt ] ; then
|
||||
/home/jenkins/tools2/bin/pylint2gerrit
|
||||
mv pylint_results.txt pylint-${pyver}.txt
|
||||
else
|
||||
@ -68,18 +70,15 @@ fi
|
||||
|
||||
echo "pylint result: $res"
|
||||
this.verifyresult.put('pylint'+pyver, 1)
|
||||
cpylint = "SUCCESSFUL"
|
||||
if ( res != 0 ) {
|
||||
currentBuild.result='FAILURE'
|
||||
this.verifyresult.put('pylint'+ pyver, -1)
|
||||
status = 'FAILURE'
|
||||
cpylint = "FAILED"
|
||||
}
|
||||
|
||||
gerritverificationpublisher([
|
||||
verifyStatusValue: this.verifyresult['pylint'+pyver],
|
||||
verifyStatusCategory: 'pylint ',
|
||||
verifyStatusName: 'pylint-'+pyver,
|
||||
verifyStatusReporter: 'jenkins',
|
||||
verifyStatusRerun: '!recheck'])
|
||||
gerritPostCheck(["jenkins:pylint_${pyver}": cpylint])
|
||||
archiveArtifacts([allowEmptyArchive: true,
|
||||
artifacts: 'pylint-*.txt'])
|
||||
recordIssues([enabledForFailure: true,
|
||||
@ -99,7 +98,9 @@ fi
|
||||
|
||||
def run_tests(pyver) {
|
||||
stage('Test:' + pyver) {
|
||||
writeFile file: 'setup.cfg', text: '''
|
||||
def cpytest = "RUNNING"
|
||||
gerritPostCheck(["jenkins:pytest_${pyver}":"RUNNING"])
|
||||
writeFile file: 'setup.cfg', text: '''
|
||||
[tool:pytest]
|
||||
addopts = --junit-xml=pytest.xml --junit-prefix=''' + pyver
|
||||
|
||||
@ -116,18 +117,15 @@ python3 setup.py develop
|
||||
make test
|
||||
'''
|
||||
verifyresult.put(pyver, 1)
|
||||
cpytest = "SUCCESSFUL"
|
||||
}
|
||||
} catch (all) {
|
||||
currentBuild.result = 'FAILURE'
|
||||
status = 'FAILURE'
|
||||
cpytest= "FAILED"
|
||||
verifyresult.put(pyver, -1)
|
||||
}
|
||||
gerritverificationpublisher([
|
||||
verifyStatusValue: verifyresult[pyver],
|
||||
verifyStatusCategory: 'test ',
|
||||
verifyStatusName: 'pytest-'+pyver,
|
||||
verifyStatusReporter: 'jenkins',
|
||||
verifyStatusRerun: '!recheck'])
|
||||
gerritPostCheck(["jenkins:pytest_${pyver}":cpytest])
|
||||
|
||||
step([$class: 'JUnitResultArchiver', allowEmptyResults: true,
|
||||
keepLongStdio: true, testResults: 'pytest.xml'])
|
||||
@ -138,6 +136,8 @@ make test
|
||||
}
|
||||
|
||||
def run_docs() {
|
||||
def cdocs = "RUNNING"
|
||||
gerritPostCheck(["jenkins:docs":cdocs])
|
||||
stage('prepare') {
|
||||
sh '''
|
||||
. /home/jenkins/secopvenv/bin/activate
|
||||
@ -185,15 +185,9 @@ def run_docs() {
|
||||
|
||||
stage('store html doc for build') {
|
||||
publishHTML([allowMissing: false, alwaysLinkToLastBuild: false, keepAll: true, reportDir: 'doc/_build/html', reportFiles: 'index.html', reportName: 'Built documentation', reportTitles: ''])
|
||||
gerritverificationpublisher([
|
||||
verifyStatusValue: 1,
|
||||
verifyStatusCategory: 'test ',
|
||||
verifyStatusName: 'doc',
|
||||
verifyStatusReporter: 'jenkins',
|
||||
verifyStatusRerun: '@recheck'
|
||||
])
|
||||
cdocs = "SUCCESSFUL"
|
||||
}
|
||||
|
||||
gerritPostCheck(["jenkins:docs":cdocs])
|
||||
}
|
||||
|
||||
|
||||
|
170
debian/changelog
vendored
170
debian/changelog
vendored
@ -1,3 +1,111 @@
|
||||
secop-core (0.12.4) focal; urgency=medium
|
||||
|
||||
* fix command inheritance
|
||||
|
||||
-- Markus Zolliker <jenkins@jenkins01.admin.frm2.tum.de> Thu, 11 Nov 2021 16:21:19 +0100
|
||||
|
||||
secop-core (0.12.3) focal; urgency=medium
|
||||
|
||||
[ Georg Brandl ]
|
||||
* Makefile: fix docker image
|
||||
|
||||
[ Markus Zolliker ]
|
||||
* various fixes
|
||||
* remove irrelevant comments
|
||||
* introduce BytesIO
|
||||
* GUI fixes
|
||||
* persistent params / trinamic motor
|
||||
* fix Parameter/Command copy method
|
||||
* show first instead of last traceback on multiple errors
|
||||
* fix parameter inheritance
|
||||
* fix property inheritance
|
||||
* fix python 3.5 compatibility
|
||||
* omit updates of unchanged values within short time
|
||||
* improve simulation
|
||||
* automatically register subclasses of AsynConn
|
||||
* fix feature for removing commands
|
||||
|
||||
-- Georg Brandl <jenkins@jenkins01.admin.frm2.tum.de> Wed, 10 Nov 2021 16:33:19 +0100
|
||||
|
||||
secop-core (0.12.2) focal; urgency=medium
|
||||
|
||||
[ Markus Zolliker ]
|
||||
* fix issue with new syntax in simulation
|
||||
* treat specifier of describe message
|
||||
* allow to remove accessibles
|
||||
|
||||
[ Enrico Faulhaber ]
|
||||
* secop_mlz: small fixes
|
||||
|
||||
-- Markus Zolliker <jenkins@jenkins01.admin.frm2.tum.de> Tue, 18 May 2021 10:29:17 +0200
|
||||
|
||||
secop-core (0.12.1) focal; urgency=medium
|
||||
|
||||
* remove secop-console from debian *.install file
|
||||
|
||||
-- Enrico Faulhaber <jenkins@jenkins02.admin.frm2.tum.de> Tue, 04 May 2021 09:42:53 +0200
|
||||
|
||||
secop-core (0.12.0) focal; urgency=medium
|
||||
|
||||
[ Markus Zolliker ]
|
||||
* make datatypes immutable
|
||||
* customizable general config
|
||||
* support for multiple secop servers
|
||||
* secop.asynconn without pyserial
|
||||
* change cfg file format
|
||||
* fix bug in secop.gui.valuewidgets
|
||||
* fix deadlock when reconnecting client
|
||||
* allow class instead of class name in proxy_class
|
||||
* fix pylint command in Makefile
|
||||
* change arguments of stringio-server
|
||||
* router bug fix
|
||||
* introduce update callbacks
|
||||
* improve error handling on client connections
|
||||
* ppms: improve status and temperature
|
||||
* rework tcp server
|
||||
* cosmetics on datatypes.TextType
|
||||
* improve error handling in SecopClient
|
||||
* improve HasIodev
|
||||
* HasIodev bug fix
|
||||
* fix handling of StructOf datatype
|
||||
* more flexible end_of_line in stringio
|
||||
* improvements on PPMS and LS370
|
||||
* add readbytes method to AsynConn
|
||||
* Param(..., initwrite=True) works only with poll=True
|
||||
* fix initwrite behaviour
|
||||
* make order of accessibles work again
|
||||
* main module of LS370 is now drivable
|
||||
* improve softcal
|
||||
* make arguments of Parameter and Override consistent
|
||||
* new syntax for parameter/commands/properties
|
||||
* enhance documentation
|
||||
* removed old style syntax
|
||||
* after running isort
|
||||
* try to follow PEP8
|
||||
* fix inheritance order
|
||||
* remove obsolete code
|
||||
* lookup cfg files in a list of directories
|
||||
* added hook for optional history writer
|
||||
* fixed errors during migration
|
||||
* move historywriter to secop_psi
|
||||
* fix autoscan behaviour in ls370res
|
||||
|
||||
[ l_samenv ]
|
||||
* improve tutorial_helevel
|
||||
* fixed bugs from syntax migration
|
||||
|
||||
[ Markus Zolliker ]
|
||||
* user friendly reporting of config errors
|
||||
|
||||
[ Bjoern Pedersen ]
|
||||
* Jenkisfile: verification
|
||||
* Fixes to Jenkinsfile
|
||||
* No pull for images, they are recreated in the job
|
||||
* Another Jenkisfile error
|
||||
* Correct checks enum
|
||||
|
||||
-- Markus Zolliker <jenkins@jenkins02.admin.frm2.tum.de> Tue, 04 May 2021 08:49:57 +0200
|
||||
|
||||
secop-core (0.11.6) unstable; urgency=medium
|
||||
|
||||
* fix secop-generator
|
||||
@ -133,7 +241,7 @@ secop-core (0.10.5) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Tue, 29 Oct 2019 16:33:18 +0100
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Tue, 29 Oct 2019 16:33:18 +0100
|
||||
|
||||
secop-core (0.10.3) unstable; urgency=low
|
||||
|
||||
@ -142,7 +250,7 @@ secop-core (0.10.3) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Fri, 11 Oct 2019 10:49:43 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Fri, 11 Oct 2019 10:49:43 +0200
|
||||
|
||||
secop-core (0.10.2) unstable; urgency=low
|
||||
|
||||
@ -153,7 +261,7 @@ secop-core (0.10.2) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Fri, 11 Oct 2019 10:42:58 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Fri, 11 Oct 2019 10:42:58 +0200
|
||||
|
||||
secop-core (0.10.1) unstable; urgency=low
|
||||
|
||||
@ -162,7 +270,7 @@ secop-core (0.10.1) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 26 Sep 2019 16:41:10 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 26 Sep 2019 16:41:10 +0200
|
||||
|
||||
secop-core (0.10.0) unstable; urgency=low
|
||||
|
||||
@ -171,7 +279,7 @@ secop-core (0.10.0) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 26 Sep 2019 16:31:14 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 26 Sep 2019 16:31:14 +0200
|
||||
|
||||
secop-core (0.9.0) unstable; urgency=low
|
||||
|
||||
@ -198,7 +306,7 @@ secop-core (0.9.0) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 26 Sep 2019 16:26:07 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 26 Sep 2019 16:26:07 +0200
|
||||
|
||||
secop-core (0.8.1) unstable; urgency=low
|
||||
|
||||
@ -207,7 +315,7 @@ secop-core (0.8.1) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Wed, 25 Sep 2019 15:40:44 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Wed, 25 Sep 2019 15:40:44 +0200
|
||||
|
||||
secop-core (0.8.0) unstable; urgency=low
|
||||
|
||||
@ -275,7 +383,7 @@ secop-core (0.8.0) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Wed, 25 Sep 2019 10:27:51 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Wed, 25 Sep 2019 10:27:51 +0200
|
||||
|
||||
secop-core (0.7.0) unstable; urgency=low
|
||||
|
||||
@ -311,7 +419,7 @@ secop-core (0.7.0) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 28 Mar 2019 13:46:08 +0100
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 28 Mar 2019 13:46:08 +0100
|
||||
|
||||
secop-core (0.6.4) unstable; urgency=low
|
||||
|
||||
@ -376,7 +484,7 @@ secop-core (0.6.4) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 20 Dec 2018 16:44:03 +0100
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 20 Dec 2018 16:44:03 +0100
|
||||
|
||||
secop-core (0.6.3) unstable; urgency=low
|
||||
|
||||
@ -390,7 +498,7 @@ secop-core (0.6.3) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Fri, 27 Jul 2018 09:31:59 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Fri, 27 Jul 2018 09:31:59 +0200
|
||||
|
||||
secop-core (0.6.2) unstable; urgency=low
|
||||
|
||||
@ -429,7 +537,7 @@ secop-core (0.6.2) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Wed, 18 Jul 2018 12:06:57 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Wed, 18 Jul 2018 12:06:57 +0200
|
||||
|
||||
secop-core (0.6.1) unstable; urgency=low
|
||||
|
||||
@ -438,7 +546,7 @@ secop-core (0.6.1) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 19 Apr 2018 10:24:44 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 19 Apr 2018 10:24:44 +0200
|
||||
|
||||
secop-core (0.6.0) unstable; urgency=low
|
||||
|
||||
@ -458,7 +566,7 @@ secop-core (0.6.0) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Tue, 17 Apr 2018 17:38:52 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Tue, 17 Apr 2018 17:38:52 +0200
|
||||
|
||||
secop-core (0.5.0) unstable; urgency=low
|
||||
|
||||
@ -521,7 +629,7 @@ secop-core (0.5.0) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Tue, 17 Apr 2018 12:45:58 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Tue, 17 Apr 2018 12:45:58 +0200
|
||||
|
||||
secop-core (0.4.4) unstable; urgency=low
|
||||
|
||||
@ -530,7 +638,7 @@ secop-core (0.4.4) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Sun, 24 Sep 2017 22:25:01 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Sun, 24 Sep 2017 22:25:01 +0200
|
||||
|
||||
secop-core (0.4.3) unstable; urgency=low
|
||||
|
||||
@ -539,7 +647,7 @@ secop-core (0.4.3) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Fri, 22 Sep 2017 17:29:46 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Fri, 22 Sep 2017 17:29:46 +0200
|
||||
|
||||
secop-core (0.4.2) unstable; urgency=low
|
||||
|
||||
@ -548,7 +656,7 @@ secop-core (0.4.2) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Fri, 22 Sep 2017 16:37:59 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Fri, 22 Sep 2017 16:37:59 +0200
|
||||
|
||||
secop-core (0.4.1) unstable; urgency=low
|
||||
|
||||
@ -557,7 +665,7 @@ secop-core (0.4.1) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Fri, 22 Sep 2017 13:25:28 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Fri, 22 Sep 2017 13:25:28 +0200
|
||||
|
||||
secop-core (0.4.0) unstable; urgency=low
|
||||
|
||||
@ -567,7 +675,7 @@ secop-core (0.4.0) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Fri, 22 Sep 2017 10:33:04 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Fri, 22 Sep 2017 10:33:04 +0200
|
||||
|
||||
secop-core (0.3.0) unstable; urgency=low
|
||||
|
||||
@ -633,7 +741,7 @@ secop-core (0.3.0) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Mon, 18 Sep 2017 14:18:36 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Mon, 18 Sep 2017 14:18:36 +0200
|
||||
|
||||
secop-core (0.2.0) unstable; urgency=low
|
||||
|
||||
@ -642,7 +750,7 @@ secop-core (0.2.0) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 07 Sep 2017 14:55:41 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 07 Sep 2017 14:55:41 +0200
|
||||
|
||||
secop-core (0.1.1) unstable; urgency=low
|
||||
|
||||
@ -651,7 +759,7 @@ secop-core (0.1.1) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 07 Sep 2017 11:02:19 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 07 Sep 2017 11:02:19 +0200
|
||||
|
||||
secop-core (0.1.0) unstable; urgency=low
|
||||
|
||||
@ -660,7 +768,7 @@ secop-core (0.1.0) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 07 Sep 2017 10:50:24 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 07 Sep 2017 10:50:24 +0200
|
||||
|
||||
secop-core (0.0.8) unstable; urgency=low
|
||||
|
||||
@ -669,7 +777,7 @@ secop-core (0.0.8) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Tue, 01 Aug 2017 14:13:11 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Tue, 01 Aug 2017 14:13:11 +0200
|
||||
|
||||
secop-core (0.0.7) unstable; urgency=low
|
||||
|
||||
@ -678,7 +786,7 @@ secop-core (0.0.7) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Tue, 01 Aug 2017 13:52:15 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Tue, 01 Aug 2017 13:52:15 +0200
|
||||
|
||||
secop-core (0.0.6) unstable; urgency=low
|
||||
|
||||
@ -688,7 +796,7 @@ secop-core (0.0.6) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Tue, 01 Aug 2017 13:39:07 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Tue, 01 Aug 2017 13:39:07 +0200
|
||||
|
||||
secop-core (0.0.5) unstable; urgency=low
|
||||
|
||||
@ -697,7 +805,7 @@ secop-core (0.0.5) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Tue, 01 Aug 2017 13:11:43 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Tue, 01 Aug 2017 13:11:43 +0200
|
||||
|
||||
secop-core (0.0.4) unstable; urgency=low
|
||||
|
||||
@ -706,7 +814,7 @@ secop-core (0.0.4) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 27 Jul 2017 11:39:42 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 27 Jul 2017 11:39:42 +0200
|
||||
|
||||
secop-core (0.0.3) unstable; urgency=low
|
||||
|
||||
@ -716,7 +824,7 @@ secop-core (0.0.3) unstable; urgency=low
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 27 Jul 2017 11:27:28 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 27 Jul 2017 11:27:28 +0200
|
||||
|
||||
secop-core (0.0.2) unstable; urgency=medium
|
||||
|
||||
@ -794,4 +902,4 @@ secop-core (0.0.2) unstable; urgency=medium
|
||||
|
||||
[ Jenkins ]
|
||||
|
||||
-- Jenkins <jenkins@debuild.taco.frm2> Wed, 19 Jul 2017 11:44:13 +0200
|
||||
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Wed, 19 Jul 2017 11:44:13 +0200
|
||||
|
1
debian/secop-core.install
vendored
1
debian/secop-core.install
vendored
@ -1,5 +1,4 @@
|
||||
usr/bin/secop-server
|
||||
usr/bin/secop-console
|
||||
usr/lib/python3.*/dist-packages/secop/*.py
|
||||
usr/lib/python3.*/dist-packages/secop/lib
|
||||
usr/lib/python3.*/dist-packages/secop/client
|
||||
|
@ -4,8 +4,10 @@ Reference
|
||||
Module Base Classes
|
||||
...................
|
||||
|
||||
.. autodata:: secop.modules.Done
|
||||
|
||||
.. autoclass:: secop.modules.Module
|
||||
:members: earlyInit, initModule, startModule, pollerClass
|
||||
:members: earlyInit, initModule, startModule
|
||||
|
||||
.. autoclass:: secop.modules.Readable
|
||||
:members: Status
|
||||
@ -49,11 +51,15 @@ Communication
|
||||
:show-inheritance:
|
||||
:members: communicate
|
||||
|
||||
.. autoclass:: secop.stringio.StringIO
|
||||
.. autoclass:: secop.io.StringIO
|
||||
:show-inheritance:
|
||||
:members: communicate, multicomm
|
||||
|
||||
.. autoclass:: secop.stringio.HasIodev
|
||||
.. autoclass:: secop.io.BytesIO
|
||||
:show-inheritance:
|
||||
:members: communicate, multicomm
|
||||
|
||||
.. autoclass:: secop.io.HasIO
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: secop.iohandler.IOHandlerBase
|
||||
|
@ -22,7 +22,7 @@ CCU4 luckily has a very simple and logical protocol:
|
||||
.. code:: python
|
||||
|
||||
# the most common Frappy classes can be imported from secop.core
|
||||
from secop.core import Readable, Parameter, FloatRange, BoolType, StringIO, HasIodev
|
||||
from secop.core import Readable, Parameter, FloatRange, BoolType, StringIO, HasIO
|
||||
|
||||
|
||||
class CCU4IO(StringIO):
|
||||
@ -34,14 +34,13 @@ CCU4 luckily has a very simple and logical protocol:
|
||||
identification = [('cid', r'CCU4.*')]
|
||||
|
||||
|
||||
# inheriting the HasIodev mixin creates us a private attribute *_iodev*
|
||||
# for talking with the hardware
|
||||
# inheriting HasIO allows us to use the communicate method for talking with the hardware
|
||||
# Readable as a base class defines the value and status parameters
|
||||
class HeLevel(HasIodev, Readable):
|
||||
class HeLevel(HasIO, Readable):
|
||||
"""He Level channel of CCU4"""
|
||||
|
||||
# define the communication class to create the IO module
|
||||
iodevClass = CCU4IO
|
||||
ioClass = CCU4IO
|
||||
|
||||
# define or alter the parameters
|
||||
# as Readable.value exists already, we give only the modified property 'unit'
|
||||
@ -49,7 +48,7 @@ CCU4 luckily has a very simple and logical protocol:
|
||||
|
||||
def read_value(self):
|
||||
# method for reading the main value
|
||||
reply = self._iodev.communicate('h') # send 'h\n' and get the reply 'h=<value>\n'
|
||||
reply = self.communicate('h') # send 'h\n' and get the reply 'h=<value>\n'
|
||||
name, txtvalue = reply.split('=')
|
||||
assert name == 'h' # check that we got a reply to our command
|
||||
return txtvalue # the framework will automatically convert the string to a float
|
||||
@ -115,17 +114,17 @@ the status codes from the hardware to the standard SECoP status codes.
|
||||
}
|
||||
|
||||
def read_status(self):
|
||||
name, txtvalue = self._iodev.communicate('hsf').split('=')
|
||||
name, txtvalue = self.communicate('hsf').split('=')
|
||||
assert name == 'hsf'
|
||||
return self.STATUS_MAP(int(txtvalue))
|
||||
|
||||
def read_empty_length(self):
|
||||
name, txtvalue = self._iodev.communicate('hem').split('=')
|
||||
name, txtvalue = self.communicate('hem').split('=')
|
||||
assert name == 'hem'
|
||||
return txtvalue
|
||||
|
||||
def write_empty_length(self, value):
|
||||
name, txtvalue = self._iodev.communicate('hem=%g' % value).split('=')
|
||||
name, txtvalue = self.communicate('hem=%g' % value).split('=')
|
||||
assert name == 'hem'
|
||||
return txtvalue
|
||||
|
||||
@ -152,7 +151,7 @@ which means it might be worth to create a *query* method, and then the
|
||||
for changing a parameter
|
||||
:returns: the (new) value of the parameter
|
||||
"""
|
||||
name, txtvalue = self._iodev.communicate(cmd).split('=')
|
||||
name, txtvalue = self.communicate(cmd).split('=')
|
||||
assert name == cmd.split('=')[0] # check that we got a reply to our command
|
||||
return txtvalue # Frappy will automatically convert the string to the needed data type
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
# doc
|
||||
sphinx_rtd_theme
|
||||
Sphinx>=1.2.1
|
||||
# for generating docu
|
||||
markdown>=2.6
|
||||
# test suite
|
||||
pytest
|
||||
|
@ -8,7 +8,7 @@ python-daemon >=2.0
|
||||
# for zmq interface
|
||||
#pyzmq>=13.1.0
|
||||
#for ppms on windows
|
||||
# don't forget to run
|
||||
# don't forget to run
|
||||
# 'python Scripts/pywin32_postinstall.py -install'
|
||||
# from elevated prompt after install
|
||||
#pywin32
|
||||
|
@ -31,10 +31,16 @@ from secop.datatypes import ArrayOf, BLOBType, BoolType, EnumType, \
|
||||
from secop.iohandler import IOHandler, IOHandlerBase
|
||||
from secop.lib.enum import Enum
|
||||
from secop.modules import Attached, Communicator, \
|
||||
Done, Drivable, Module, Readable, Writable
|
||||
Done, Drivable, Module, Readable, Writable, HasAccessibles
|
||||
from secop.params import Command, Parameter
|
||||
from secop.poller import AUTO, DYNAMIC, REGULAR, SLOW
|
||||
from secop.properties import Property
|
||||
from secop.proxy import Proxy, SecNode, proxy_class
|
||||
from secop.io import HasIodev, StringIO, BytesIO
|
||||
from secop.io import HasIO, StringIO, BytesIO, HasIodev # TODO: remove HasIodev (legacy stuff)
|
||||
from secop.persistent import PersistentMixin, PersistentParam
|
||||
from secop.rwhandler import ReadHandler, WriteHandler, CommonReadHandler, \
|
||||
CommonWriteHandler, nopoll
|
||||
|
||||
ERROR = Drivable.Status.ERROR
|
||||
WARN = Drivable.Status.WARN
|
||||
BUSY = Drivable.Status.BUSY
|
||||
IDLE = Drivable.Status.IDLE
|
||||
|
@ -22,7 +22,7 @@
|
||||
# *****************************************************************************
|
||||
"""Define validated data types."""
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
# pylint: disable=abstract-method, too-many-lines
|
||||
|
||||
|
||||
import sys
|
||||
@ -30,20 +30,12 @@ from base64 import b64decode, b64encode
|
||||
|
||||
from secop.errors import BadValueError, \
|
||||
ConfigError, ProgrammingError, ProtocolError
|
||||
from secop.lib import clamp
|
||||
from secop.lib import clamp, generalConfig
|
||||
from secop.lib.enum import Enum
|
||||
from secop.parse import Parser
|
||||
from secop.properties import HasProperties, Property
|
||||
|
||||
# Only export these classes for 'from secop.datatypes import *'
|
||||
__all__ = [
|
||||
'DataType', 'get_datatype',
|
||||
'FloatRange', 'IntRange', 'ScaledInteger',
|
||||
'BoolType', 'EnumType',
|
||||
'BLOBType', 'StringType', 'TextType',
|
||||
'TupleOf', 'ArrayOf', 'StructOf',
|
||||
'CommandType', 'StatusType',
|
||||
]
|
||||
generalConfig.set_default('lazy_number_validation', False)
|
||||
|
||||
# *DEFAULT* limits for IntRange/ScaledIntegers transport serialisation
|
||||
DEFAULT_MIN_INT = -16777216
|
||||
@ -53,6 +45,11 @@ UNLIMITED = 1 << 64 # internal limit for integers, is probably high enough for
|
||||
Parser = Parser()
|
||||
|
||||
|
||||
class DiscouragedConversion(BadValueError):
|
||||
"""the discouraged conversion string - > float happened"""
|
||||
log_message = True
|
||||
|
||||
|
||||
# base class for all DataTypes
|
||||
class DataType(HasProperties):
|
||||
"""base class for all data types"""
|
||||
@ -63,7 +60,7 @@ class DataType(HasProperties):
|
||||
def __call__(self, value):
|
||||
"""check if given value (a python obj) is valid for this datatype
|
||||
|
||||
returns the value or raises an appropriate exception"""
|
||||
returns the (possibly converted) value or raises an appropriate exception"""
|
||||
raise NotImplementedError
|
||||
|
||||
def from_string(self, text):
|
||||
@ -192,9 +189,15 @@ class FloatRange(DataType):
|
||||
|
||||
def __call__(self, value):
|
||||
try:
|
||||
value = float(value)
|
||||
value += 0.0 # do not accept strings here
|
||||
except Exception:
|
||||
raise BadValueError('Can not convert %r to float' % value) from None
|
||||
try:
|
||||
value = float(value)
|
||||
except Exception:
|
||||
raise BadValueError('Can not convert %r to float' % value) from None
|
||||
if not generalConfig.lazy_number_validation:
|
||||
raise DiscouragedConversion('automatic string to float conversion no longer supported') from None
|
||||
|
||||
# map +/-infty to +/-max possible number
|
||||
value = clamp(-sys.float_info.max, value, sys.float_info.max)
|
||||
|
||||
@ -232,6 +235,18 @@ class FloatRange(DataType):
|
||||
return ' '.join([self.fmtstr % value, unit])
|
||||
return self.fmtstr % value
|
||||
|
||||
def problematic_range(self, target_type):
|
||||
"""check problematic range
|
||||
|
||||
returns True when self.min or self.max is given, not 0 and equal to the same limit on target_type.
|
||||
"""
|
||||
value_info = self.get_info()
|
||||
target_info = target_type.get_info()
|
||||
minval = value_info.get('min') # None when -infinite
|
||||
maxval = value_info.get('max') # None when +infinite
|
||||
return ((minval and minval == target_info.get('min')) or
|
||||
(maxval and maxval == target_info.get('max')))
|
||||
|
||||
def compatible(self, other):
|
||||
if not isinstance(other, (FloatRange, ScaledInteger)):
|
||||
raise BadValueError('incompatible datatypes')
|
||||
@ -265,10 +280,16 @@ class IntRange(DataType):
|
||||
|
||||
def __call__(self, value):
|
||||
try:
|
||||
fvalue = float(value)
|
||||
fvalue = value + 0.0 # do not accept strings here
|
||||
value = int(value)
|
||||
except Exception:
|
||||
raise BadValueError('Can not convert %r to int' % value) from None
|
||||
try:
|
||||
fvalue = float(value)
|
||||
value = int(value)
|
||||
except Exception:
|
||||
raise BadValueError('Can not convert %r to int' % value) from None
|
||||
if not generalConfig.lazy_number_validation:
|
||||
raise DiscouragedConversion('automatic string to float conversion no longer supported') from None
|
||||
if not self.min <= value <= self.max or round(fvalue) != fvalue:
|
||||
raise BadValueError('%r should be an int between %d and %d' %
|
||||
(value, self.min, self.max))
|
||||
@ -298,13 +319,15 @@ class IntRange(DataType):
|
||||
return '%d' % value
|
||||
|
||||
def compatible(self, other):
|
||||
if isinstance(other, IntRange):
|
||||
if isinstance(other, (IntRange, FloatRange, ScaledInteger)):
|
||||
other(self.min)
|
||||
other(self.max)
|
||||
return
|
||||
# this will accept some EnumType, BoolType
|
||||
for i in range(self.min, self.max + 1):
|
||||
other(i)
|
||||
if isinstance(other, (EnumType, BoolType)):
|
||||
# the following loop will not cycle more than the number of Enum elements
|
||||
for i in range(self.min, self.max + 1):
|
||||
other(i)
|
||||
raise BadValueError('incompatible datatypes')
|
||||
|
||||
|
||||
class ScaledInteger(DataType):
|
||||
@ -369,9 +392,14 @@ class ScaledInteger(DataType):
|
||||
|
||||
def __call__(self, value):
|
||||
try:
|
||||
value = float(value)
|
||||
value += 0.0 # do not accept strings here
|
||||
except Exception:
|
||||
raise BadValueError('Can not convert %r to float' % value) from None
|
||||
try:
|
||||
value = float(value)
|
||||
except Exception:
|
||||
raise BadValueError('Can not convert %r to float' % value) from None
|
||||
if not generalConfig.lazy_number_validation:
|
||||
raise DiscouragedConversion('automatic string to float conversion no longer supported') from None
|
||||
prec = max(self.scale, abs(value * self.relative_resolution),
|
||||
self.absolute_resolution)
|
||||
if self.min - prec <= value <= self.max + prec:
|
||||
@ -427,7 +455,10 @@ class EnumType(DataType):
|
||||
super().__init__()
|
||||
if members is not None:
|
||||
kwds.update(members)
|
||||
self._enum = Enum(enum_or_name, **kwds)
|
||||
if isinstance(enum_or_name, str):
|
||||
self._enum = Enum(enum_or_name, kwds) # allow 'self' as name
|
||||
else:
|
||||
self._enum = Enum(enum_or_name, **kwds)
|
||||
self.default = self._enum[self._enum.members[0]]
|
||||
|
||||
def copy(self):
|
||||
@ -852,6 +883,8 @@ class StructOf(DataType):
|
||||
:param optional: a list of optional members
|
||||
:param members: names as keys and types as values for all members
|
||||
"""
|
||||
# Remark: assignment of parameters containing partial structs in their datatype
|
||||
# are (and can) not be handled here! This has to be done manually in the write method
|
||||
def __init__(self, optional=None, **members):
|
||||
super().__init__()
|
||||
self.members = members
|
||||
@ -955,10 +988,9 @@ class CommandType(DataType):
|
||||
return props
|
||||
|
||||
def __repr__(self):
|
||||
argstr = repr(self.argument) if self.argument else ''
|
||||
if self.result is None:
|
||||
return 'CommandType(%s)' % argstr
|
||||
return 'CommandType(%s, %s)' % (argstr, repr(self.result))
|
||||
return 'CommandType(%s)' % (repr(self.argument) if self.argument else '')
|
||||
return 'CommandType(%s, %s)' % (repr(self.argument), repr(self.result))
|
||||
|
||||
def __call__(self, value):
|
||||
"""return the validated argument value or raise"""
|
||||
@ -1119,37 +1151,26 @@ def floatargs(kwds):
|
||||
DATATYPES = dict(
|
||||
bool = lambda **kwds:
|
||||
BoolType(),
|
||||
|
||||
int = lambda min, max, **kwds:
|
||||
IntRange(minval=min, maxval=max),
|
||||
|
||||
scaled = lambda scale, min, max, **kwds:
|
||||
ScaledInteger(scale=scale, minval=min*scale, maxval=max*scale, **floatargs(kwds)),
|
||||
|
||||
double = lambda min=None, max=None, **kwds:
|
||||
FloatRange(minval=min, maxval=max, **floatargs(kwds)),
|
||||
|
||||
blob = lambda maxbytes, minbytes=0, **kwds:
|
||||
BLOBType(minbytes=minbytes, maxbytes=maxbytes),
|
||||
|
||||
string = lambda minchars=0, maxchars=None, isUTF8=False, **kwds:
|
||||
StringType(minchars=minchars, maxchars=maxchars, isUTF8=isUTF8),
|
||||
|
||||
array = lambda maxlen, members, minlen=0, pname='', **kwds:
|
||||
ArrayOf(get_datatype(members, pname), minlen=minlen, maxlen=maxlen),
|
||||
|
||||
tuple = lambda members, pname='', **kwds:
|
||||
TupleOf(*tuple((get_datatype(t, pname) for t in members))),
|
||||
|
||||
enum = lambda members, pname='', **kwds:
|
||||
EnumType(pname, members=members),
|
||||
|
||||
struct = lambda members, optional=None, pname='', **kwds:
|
||||
StructOf(optional, **dict((n, get_datatype(t, pname)) for n, t in list(members.items()))),
|
||||
|
||||
command = lambda argument=None, result=None, pname='', **kwds:
|
||||
CommandType(get_datatype(argument, pname), get_datatype(result)),
|
||||
|
||||
limit = lambda members, pname='', **kwds:
|
||||
LimitsType(get_datatype(members, pname)),
|
||||
)
|
||||
|
@ -25,7 +25,7 @@
|
||||
class SECoPError(RuntimeError):
|
||||
|
||||
def __init__(self, *args, **kwds):
|
||||
RuntimeError.__init__(self)
|
||||
super().__init__()
|
||||
self.args = args
|
||||
for k, v in list(kwds.items()):
|
||||
setattr(self, k, v)
|
||||
@ -151,7 +151,8 @@ EXCEPTIONS = dict(
|
||||
IsError=IsErrorError,
|
||||
Disabled=DisabledError,
|
||||
SyntaxError=ProtocolError,
|
||||
NotImplementedError=NotImplementedError,
|
||||
NotImplemented=NotImplementedError,
|
||||
ProtocolError=ProtocolError,
|
||||
InternalError=InternalError,
|
||||
# internal short versions (candidates for spec)
|
||||
Protocol=ProtocolError,
|
||||
|
@ -39,7 +39,7 @@ COMMENT = 'comment'
|
||||
class MainWindow(QMainWindow):
|
||||
|
||||
def __init__(self, file_path=None, parent=None):
|
||||
QMainWindow.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
loadUi(self, 'mainwindow.ui')
|
||||
self.tabWidget.currentChanged.connect(self.tab_relevant_btns_disable)
|
||||
if file_path is None:
|
||||
|
@ -26,7 +26,7 @@ from secop.gui.qt import QHBoxLayout, QSizePolicy, QSpacerItem, Qt, QWidget
|
||||
|
||||
class NodeDisplay(QWidget):
|
||||
def __init__(self, file_path=None, parent=None):
|
||||
QWidget.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
loadUi(self, 'node_display.ui')
|
||||
self.saved = bool(file_path)
|
||||
self.created = self.tree_widget.set_file(file_path)
|
||||
|
@ -44,7 +44,7 @@ class TreeWidgetItem(QTreeWidgetItem):
|
||||
the datatype passed onto ValueWidget should be on of secop.datatypes"""
|
||||
# TODO: like stated in docstring the datatype for parameters and
|
||||
# properties must be found out through their object
|
||||
QTreeWidgetItem.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
self.kind = kind
|
||||
self.name = name
|
||||
self.class_object = class_object
|
||||
@ -129,7 +129,7 @@ class ValueWidget(QWidget):
|
||||
|
||||
def __init__(self, name='', value='', datatype=None, kind='', parent=None):
|
||||
# TODO: implement: change module/interface class
|
||||
QWidget.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
self.datatype = datatype
|
||||
self.layout = QVBoxLayout()
|
||||
self.name_label = QLabel(name)
|
||||
@ -205,7 +205,7 @@ class ValueWidget(QWidget):
|
||||
|
||||
class ChangeNameDialog(QDialog):
|
||||
def __init__(self, current_name='', invalid_names=None, parent=None):
|
||||
QWidget.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
loadUi(self, 'change_name_dialog.ui')
|
||||
self.invalid_names = invalid_names
|
||||
self.name.setText(current_name)
|
||||
|
@ -29,7 +29,7 @@ from secop.modules import Module
|
||||
from secop.params import Parameter
|
||||
from secop.properties import Property
|
||||
from secop.protocol.interface.tcp import TCPServer
|
||||
from secop.server import getGeneralConfig
|
||||
from secop.server import generalConfig
|
||||
|
||||
uipath = path.dirname(__file__)
|
||||
|
||||
@ -106,7 +106,7 @@ def get_file_paths(widget, open_file=True):
|
||||
|
||||
def get_modules():
|
||||
modules = {}
|
||||
base_path = getGeneralConfig()['basedir']
|
||||
base_path = generalConfig.basedir
|
||||
# pylint: disable=too-many-nested-blocks
|
||||
for dirname in listdir(base_path):
|
||||
if dirname.startswith('secop_'):
|
||||
@ -156,7 +156,7 @@ def get_interface_class_from_name(name):
|
||||
def get_interfaces():
|
||||
# TODO class must be found out like for modules
|
||||
interfaces = []
|
||||
interface_path = path.join(getGeneralConfig()['basedir'], 'secop',
|
||||
interface_path = path.join(generalConfig.basedir, 'secop',
|
||||
'protocol', 'interface')
|
||||
for filename in listdir(interface_path):
|
||||
if path.isfile(path.join(interface_path, filename)) and \
|
||||
|
@ -31,7 +31,7 @@ from secop.gui.cfg_editor.utils import get_all_items, \
|
||||
get_props, loadUi, set_name_edit_style, setActionIcon
|
||||
from secop.gui.qt import QComboBox, QDialog, QDialogButtonBox, QLabel, \
|
||||
QLineEdit, QMenu, QPoint, QSize, QStandardItem, QStandardItemModel, \
|
||||
Qt, QTabBar, QTextEdit, QTreeView, QTreeWidget, QWidget, pyqtSignal
|
||||
Qt, QTabBar, QTextEdit, QTreeView, QTreeWidget, pyqtSignal
|
||||
|
||||
NODE = 'node'
|
||||
MODULE = 'module'
|
||||
@ -47,7 +47,7 @@ class TreeWidget(QTreeWidget):
|
||||
add_canceled = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QTreeWidget.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
self.file_path = None
|
||||
self.setIconSize(QSize(24, 24))
|
||||
self.setSelectionMode(QTreeWidget.SingleSelection)
|
||||
@ -335,7 +335,7 @@ class AddDialog(QDialog):
|
||||
"""Notes:
|
||||
self.get_value: is mapped to the specific method for getting
|
||||
the value from self.value"""
|
||||
QWidget.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
loadUi(self, 'add_dialog.ui')
|
||||
self.setWindowTitle('add %s' % kind)
|
||||
self.kind = kind
|
||||
@ -402,7 +402,7 @@ class AddDialog(QDialog):
|
||||
|
||||
class TabBar(QTabBar):
|
||||
def __init__(self, parent=None):
|
||||
QTabBar.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.context_pos = QPoint(0, 0)
|
||||
self.menu = QMenu()
|
||||
@ -436,7 +436,7 @@ class TabBar(QTabBar):
|
||||
|
||||
class TreeComboBox(QComboBox):
|
||||
def __init__(self, value_dict, parent=None):
|
||||
QComboBox.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
self.tree_view = QTreeView()
|
||||
self.tree_view.setHeaderHidden(True)
|
||||
self.tree_view.expanded.connect(self.resize_length)
|
||||
|
@ -44,7 +44,7 @@ class QSECNode(QObject):
|
||||
logEntry = pyqtSignal(str)
|
||||
|
||||
def __init__(self, uri, parent=None):
|
||||
QObject.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
self.conn = conn = secop.client.SecopClient(uri)
|
||||
conn.validate_data = True
|
||||
self.log = conn.log
|
||||
@ -83,10 +83,7 @@ class QSECNode(QObject):
|
||||
return self.conn.getParameter(module, parameter, True)
|
||||
|
||||
def execCommand(self, module, command, argument):
|
||||
try:
|
||||
return self.conn.execCommand(module, command, argument)
|
||||
except Exception as e:
|
||||
return 'ERROR: %r' % e, {}
|
||||
return self.conn.execCommand(module, command, argument)
|
||||
|
||||
def queryCache(self, module):
|
||||
return {k: Value(*self.conn.cache[(module, k)])
|
||||
@ -115,7 +112,7 @@ class QSECNode(QObject):
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
def __init__(self, hosts, parent=None):
|
||||
super(MainWindow, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
loadUi(self, 'mainwindow.ui')
|
||||
|
||||
|
@ -160,7 +160,7 @@ class MiniPlotFitCurve(MiniPlotCurve):
|
||||
return float('-inf')
|
||||
|
||||
def __init__(self, formula, params):
|
||||
super(MiniPlotFitCurve, self).__init__()
|
||||
super().__init__()
|
||||
self.formula = formula
|
||||
self.params = params
|
||||
|
||||
@ -193,7 +193,7 @@ class MiniPlot(QWidget):
|
||||
autoticky = True
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QWidget.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
self.xmin = self.xmax = None
|
||||
self.ymin = self.ymax = None
|
||||
self.curves = []
|
||||
|
@ -32,7 +32,7 @@ from secop.gui.valuewidgets import get_widget
|
||||
|
||||
class CommandDialog(QDialog):
|
||||
def __init__(self, cmdname, argument, parent=None):
|
||||
super(CommandDialog, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
loadUi(self, 'cmddialog.ui')
|
||||
|
||||
self.setWindowTitle('Arguments for %s' % cmdname)
|
||||
@ -58,7 +58,7 @@ class CommandDialog(QDialog):
|
||||
return True, self.widgets[0].get_value()
|
||||
|
||||
def exec_(self):
|
||||
if super(CommandDialog, self).exec_():
|
||||
if super().exec_():
|
||||
return self.get_value()
|
||||
return None
|
||||
|
||||
@ -71,16 +71,17 @@ def showCommandResultDialog(command, args, result, extras=''):
|
||||
m.exec_()
|
||||
|
||||
|
||||
def showErrorDialog(error):
|
||||
def showErrorDialog(command, args, error):
|
||||
m = QMessageBox()
|
||||
m.setText('Error %r' % error)
|
||||
args = '' if args is None else repr(args)
|
||||
m.setText('calling: %s(%s)\nraised %r' % (command, args, error))
|
||||
m.exec_()
|
||||
|
||||
|
||||
class ParameterGroup(QWidget):
|
||||
|
||||
def __init__(self, groupname, parent=None):
|
||||
super(ParameterGroup, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
loadUi(self, 'paramgroup.ui')
|
||||
|
||||
self._groupname = groupname
|
||||
@ -112,7 +113,7 @@ class ParameterGroup(QWidget):
|
||||
class CommandButton(QPushButton):
|
||||
|
||||
def __init__(self, cmdname, cmdinfo, cb, parent=None):
|
||||
super(CommandButton, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
self._cmdname = cmdname
|
||||
self._argintype = cmdinfo['datatype'].argument # single datatype
|
||||
@ -140,7 +141,7 @@ class CommandButton(QPushButton):
|
||||
class ModuleCtrl(QWidget):
|
||||
|
||||
def __init__(self, node, module, parent=None):
|
||||
super(ModuleCtrl, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
loadUi(self, 'modulectrl.ui')
|
||||
self._node = node
|
||||
self._module = module
|
||||
@ -161,10 +162,9 @@ class ModuleCtrl(QWidget):
|
||||
try:
|
||||
result, qualifiers = self._node.execCommand(
|
||||
self._module, command, args)
|
||||
except TypeError:
|
||||
result = None
|
||||
qualifiers = {}
|
||||
# XXX: flag missing data report as error
|
||||
except Exception as e:
|
||||
showErrorDialog(command, args, e)
|
||||
return
|
||||
if result is not None:
|
||||
showCommandResultDialog(command, args, result, qualifiers)
|
||||
|
||||
|
@ -39,7 +39,7 @@ class ParameterWidget(QWidget):
|
||||
initvalue=None,
|
||||
readonly=True,
|
||||
parent=None):
|
||||
super(ParameterWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
self._module = module
|
||||
self._paramcmd = paramcmd
|
||||
self._datatype = datatype
|
||||
@ -82,7 +82,6 @@ class GenericParameterWidget(ParameterWidget):
|
||||
else:
|
||||
value = fmtstr % (value.value,)
|
||||
self.currentLineEdit.setText(value)
|
||||
# self.currentLineEdit.setText(str(value))
|
||||
|
||||
|
||||
class EnumParameterWidget(GenericParameterWidget):
|
||||
|
215
secop/historywriter.py
Normal file
215
secop/historywriter.py
Normal 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()))
|
111
secop/io.py
111
secop/io.py
@ -29,50 +29,78 @@ import time
|
||||
import threading
|
||||
|
||||
from secop.lib.asynconn import AsynConn, ConnectionClosed
|
||||
from secop.datatypes import ArrayOf, BLOBType, BoolType, FloatRange, IntRange, StringType, TupleOf, ValueType
|
||||
from secop.errors import CommunicationFailedError, CommunicationSilentError, ConfigError
|
||||
from secop.datatypes import ArrayOf, BLOBType, BoolType, FloatRange, IntRange, \
|
||||
StringType, TupleOf, ValueType
|
||||
from secop.errors import CommunicationFailedError, CommunicationSilentError, \
|
||||
ConfigError, ProgrammingError
|
||||
from secop.modules import Attached, Command, \
|
||||
Communicator, Done, Module, Parameter, Property
|
||||
from secop.poller import REGULAR
|
||||
from secop.lib import generalConfig
|
||||
|
||||
generalConfig.set_default('legacy_hasiodev', False)
|
||||
|
||||
HEX_CODE = re.compile(r'[0-9a-fA-F][0-9a-fA-F]$')
|
||||
|
||||
|
||||
class HasIodev(Module):
|
||||
class HasIO(Module):
|
||||
"""Mixin for modules using a communicator"""
|
||||
iodev = Attached()
|
||||
io = Attached()
|
||||
uri = Property('uri for automatic creation of the attached communication module',
|
||||
StringType(), default='')
|
||||
|
||||
iodevDict = {}
|
||||
ioDict = {}
|
||||
ioClass = None
|
||||
|
||||
def __init__(self, name, logger, opts, srv):
|
||||
iodev = opts.get('iodev')
|
||||
Module.__init__(self, name, logger, opts, srv)
|
||||
io = opts.get('io')
|
||||
super().__init__(name, logger, opts, srv)
|
||||
if self.uri:
|
||||
opts = {'uri': self.uri, 'description': 'communication device for %s' % name,
|
||||
'export': False}
|
||||
ioname = self.iodevDict.get(self.uri)
|
||||
ioname = self.ioDict.get(self.uri)
|
||||
if not ioname:
|
||||
ioname = iodev or name + '_iodev'
|
||||
iodev = self.iodevClass(ioname, srv.log.getChild(ioname), opts, srv)
|
||||
srv.modules[ioname] = iodev
|
||||
self.iodevDict[self.uri] = ioname
|
||||
self.iodev = ioname
|
||||
elif not self.iodev:
|
||||
raise ConfigError("Module %s needs a value for either 'uri' or 'iodev'" % name)
|
||||
ioname = io or name + '_io'
|
||||
io = self.ioClass(ioname, srv.log.getChild(ioname), opts, srv) # pylint: disable=not-callable
|
||||
io.callingModule = []
|
||||
srv.modules[ioname] = io
|
||||
self.ioDict[self.uri] = ioname
|
||||
self.io = ioname
|
||||
elif not io:
|
||||
raise ConfigError("Module %s needs a value for either 'uri' or 'io'" % name)
|
||||
|
||||
def initModule(self):
|
||||
try:
|
||||
self._iodev.read_is_connected()
|
||||
self.io.read_is_connected()
|
||||
except (CommunicationFailedError, AttributeError):
|
||||
# AttributeError: for missing _iodev?
|
||||
# AttributeError: read_is_connected is not required for an io object
|
||||
pass
|
||||
super().initModule()
|
||||
|
||||
def sendRecv(self, command):
|
||||
return self._iodev.communicate(command)
|
||||
def communicate(self, *args):
|
||||
return self.io.communicate(*args)
|
||||
|
||||
def multicomm(self, *args):
|
||||
return self.io.multicomm(*args)
|
||||
|
||||
|
||||
class HasIodev(HasIO):
|
||||
# TODO: remove this legacy mixin
|
||||
iodevClass = None
|
||||
|
||||
@property
|
||||
def _iodev(self):
|
||||
return self.io
|
||||
|
||||
def __init__(self, name, logger, opts, srv):
|
||||
self.ioClass = self.iodevClass
|
||||
super().__init__(name, logger, opts, srv)
|
||||
if generalConfig.legacy_hasiodev:
|
||||
self.log.warn('using the HasIodev mixin is deprecated - use HasIO instead')
|
||||
else:
|
||||
self.log.error('legacy HasIodev no longer supported')
|
||||
self.log.error('you may suppress this error message by running the server with --relaxed')
|
||||
raise ProgrammingError('legacy HasIodev no longer supported')
|
||||
self.sendRecv = self.communicate
|
||||
|
||||
|
||||
class IOBase(Communicator):
|
||||
@ -80,7 +108,7 @@ class IOBase(Communicator):
|
||||
uri = Property('hostname:portnumber', datatype=StringType())
|
||||
timeout = Parameter('timeout', datatype=FloatRange(0), default=2)
|
||||
wait_before = Parameter('wait time before sending', datatype=FloatRange(), default=0)
|
||||
is_connected = Parameter('connection state', datatype=BoolType(), readonly=False, poll=REGULAR)
|
||||
is_connected = Parameter('connection state', datatype=BoolType(), readonly=False, default=False)
|
||||
pollinterval = Parameter('reconnect interval', datatype=FloatRange(0), readonly=False, default=10)
|
||||
|
||||
_reconnectCallbacks = None
|
||||
@ -89,6 +117,8 @@ class IOBase(Communicator):
|
||||
_lock = None
|
||||
|
||||
def earlyInit(self):
|
||||
super().earlyInit()
|
||||
self._reconnectCallbacks = {}
|
||||
self._lock = threading.RLock()
|
||||
|
||||
def connectStart(self):
|
||||
@ -103,6 +133,9 @@ class IOBase(Communicator):
|
||||
self._conn = None
|
||||
self.is_connected = False
|
||||
|
||||
def doPoll(self):
|
||||
self.read_is_connected()
|
||||
|
||||
def read_is_connected(self):
|
||||
"""try to reconnect, when not connected
|
||||
|
||||
@ -139,10 +172,7 @@ class IOBase(Communicator):
|
||||
|
||||
if the callback fails or returns False, it is cleared
|
||||
"""
|
||||
if self._reconnectCallbacks is None:
|
||||
self._reconnectCallbacks = {name: func}
|
||||
else:
|
||||
self._reconnectCallbacks[name] = func
|
||||
self._reconnectCallbacks[name] = func
|
||||
|
||||
def callCallbacks(self):
|
||||
for key, cb in list(self._reconnectCallbacks.items()):
|
||||
@ -154,6 +184,9 @@ class IOBase(Communicator):
|
||||
if removeme:
|
||||
self._reconnectCallbacks.pop(key)
|
||||
|
||||
def communicate(self, command):
|
||||
return NotImplementedError
|
||||
|
||||
|
||||
class StringIO(IOBase):
|
||||
"""line oriented communicator
|
||||
@ -218,6 +251,7 @@ class StringIO(IOBase):
|
||||
if not self.is_connected:
|
||||
self.read_is_connected() # try to reconnect
|
||||
if not self._conn:
|
||||
self.log.debug('can not connect to %r' % self.uri)
|
||||
raise CommunicationSilentError('can not connect to %r' % self.uri)
|
||||
try:
|
||||
with self._lock:
|
||||
@ -234,15 +268,15 @@ class StringIO(IOBase):
|
||||
if garbage is None: # read garbage only once
|
||||
garbage = self._conn.flush_recv()
|
||||
if garbage:
|
||||
self.log.debug('garbage: %r', garbage)
|
||||
self.comLog('garbage: %r', garbage)
|
||||
self._conn.send(cmd + self._eol_write)
|
||||
self.log.debug('send: %s', cmd + self._eol_write)
|
||||
self.comLog('> %s', cmd.decode(self.encoding))
|
||||
reply = self._conn.readline(self.timeout)
|
||||
except ConnectionClosed as e:
|
||||
self.closeConnection()
|
||||
raise CommunicationFailedError('disconnected') from None
|
||||
reply = reply.decode(self.encoding)
|
||||
self.log.debug('recv: %s', reply)
|
||||
self.comLog('< %s', reply)
|
||||
return reply
|
||||
except Exception as e:
|
||||
if str(e) == self._last_error:
|
||||
@ -291,6 +325,10 @@ def make_bytes(string):
|
||||
return bytes([int(c, 16) if HEX_CODE.match(c) else ord(c) for c in string.split()])
|
||||
|
||||
|
||||
def hexify(bytes_):
|
||||
return ' '.join('%02x' % r for r in bytes_)
|
||||
|
||||
|
||||
class BytesIO(IOBase):
|
||||
identification = Property(
|
||||
"""identification
|
||||
@ -330,14 +368,14 @@ class BytesIO(IOBase):
|
||||
time.sleep(self.wait_before)
|
||||
garbage = self._conn.flush_recv()
|
||||
if garbage:
|
||||
self.log.debug('garbage: %r', garbage)
|
||||
self.comLog('garbage: %r', garbage)
|
||||
self._conn.send(request)
|
||||
self.log.debug('send: %r', request)
|
||||
self.comLog('> %s', hexify(request))
|
||||
reply = self._conn.readbytes(replylen, self.timeout)
|
||||
except ConnectionClosed as e:
|
||||
self.closeConnection()
|
||||
raise CommunicationFailedError('disconnected') from None
|
||||
self.log.debug('recv: %r', reply)
|
||||
self.comLog('< %s', hexify(reply))
|
||||
return self.getFullReply(request, reply)
|
||||
except Exception as e:
|
||||
if str(e) == self._last_error:
|
||||
@ -346,6 +384,15 @@ class BytesIO(IOBase):
|
||||
self.log.error(self._last_error)
|
||||
raise
|
||||
|
||||
@Command((ArrayOf(TupleOf(BLOBType(), IntRange(0)))), result=ArrayOf(BLOBType()))
|
||||
def multicomm(self, requests):
|
||||
"""communicate multiple request/replies in one row"""
|
||||
replies = []
|
||||
with self._lock:
|
||||
for request in requests:
|
||||
replies.append(self.communicate(*request))
|
||||
return replies
|
||||
|
||||
def readBytes(self, nbytes):
|
||||
"""read bytes
|
||||
|
||||
@ -362,7 +409,7 @@ class BytesIO(IOBase):
|
||||
:return: the full reply (replyheader + additional bytes)
|
||||
|
||||
When the reply length is variable, :meth:`communicate` should be called
|
||||
with the `replylen` argument set to minimum expected length of the reply.
|
||||
with the `replylen` argument set to the minimum expected length of the reply.
|
||||
Typically this method determines then the length of additional bytes from
|
||||
the already received bytes (replyheader) and/or the request and calls
|
||||
:meth:`readBytes` to get the remaining bytes.
|
||||
|
@ -126,7 +126,7 @@ class CmdParser:
|
||||
try:
|
||||
argformat % ((0,) * len(casts)) # validate argformat
|
||||
except ValueError as e:
|
||||
raise ValueError("%s in %r" % (e, argformat))
|
||||
raise ValueError("%s in %r" % (e, argformat)) from None
|
||||
|
||||
def format(self, *values):
|
||||
return self.fmt % values
|
||||
@ -242,7 +242,7 @@ class IOHandler(IOHandlerBase):
|
||||
contain the command separator at the end.
|
||||
"""
|
||||
querycmd = self.make_query(module)
|
||||
reply = module.sendRecv(changecmd + querycmd)
|
||||
reply = module.communicate(changecmd + querycmd)
|
||||
return self.parse_reply(reply)
|
||||
|
||||
def send_change(self, module, *values):
|
||||
@ -253,7 +253,7 @@ class IOHandler(IOHandlerBase):
|
||||
"""
|
||||
changecmd = self.make_change(module, *values)
|
||||
if self.CMDSEPARATOR is None:
|
||||
module.sendRecv(changecmd) # ignore result
|
||||
module.communicate(changecmd) # ignore result
|
||||
return self.send_command(module)
|
||||
return self.send_command(module, changecmd + self.CMDSEPARATOR)
|
||||
|
||||
|
@ -27,40 +27,126 @@ import socket
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
from configparser import ConfigParser
|
||||
from os import environ, path
|
||||
|
||||
|
||||
repodir = path.abspath(path.join(path.dirname(__file__), '..', '..'))
|
||||
class GeneralConfig:
|
||||
"""generalConfig holds server configuration items
|
||||
|
||||
if path.splitext(sys.executable)[1] == ".exe" and not path.basename(sys.executable).startswith('python'):
|
||||
CONFIG = {
|
||||
'piddir': './',
|
||||
'logdir': './log',
|
||||
'confdir': './',
|
||||
}
|
||||
elif not path.exists(path.join(repodir, '.git')):
|
||||
CONFIG = {
|
||||
'piddir': '/var/run/secop',
|
||||
'logdir': '/var/log',
|
||||
'confdir': '/etc/secop',
|
||||
}
|
||||
else:
|
||||
CONFIG = {
|
||||
'piddir': path.join(repodir, 'pid'),
|
||||
'logdir': path.join(repodir, 'log'),
|
||||
'confdir': path.join(repodir, 'cfg'),
|
||||
}
|
||||
# overwrite with env variables SECOP_LOGDIR, SECOP_PIDDIR, SECOP_CONFDIR, if present
|
||||
for dirname in CONFIG:
|
||||
CONFIG[dirname] = environ.get('SECOP_%s' % dirname.upper(), CONFIG[dirname])
|
||||
generalConfig.init is to be called before starting the server.
|
||||
Accessing generalConfig.<key> raises an error, when generalConfig.init is
|
||||
not yet called, except when a default for <key> is set.
|
||||
For tests and for imports from client code, a module may access generalConfig
|
||||
without calling generalConfig.init before. For this, it should call
|
||||
generalConfig.set_default on import to define defaults for the needed keys.
|
||||
"""
|
||||
|
||||
# this is not customizable
|
||||
CONFIG['basedir'] = repodir
|
||||
def __init__(self):
|
||||
self._config = None
|
||||
self.defaults = {} #: default values. may be set before or after :meth:`init`
|
||||
|
||||
# TODO: if ever more general options are need, we should think about a general config file
|
||||
def init(self, configfile=None):
|
||||
"""init default server configuration
|
||||
|
||||
:param configfile: if present, keys and values from the [FRAPPY] section are read
|
||||
|
||||
if configfile is not given, it tries to guess the location of the configfile
|
||||
or determine 'piddir', 'logdir', 'confdir' and 'basedir' from the environment.
|
||||
"""
|
||||
cfg = {}
|
||||
mandatory = 'piddir', 'logdir', 'confdir'
|
||||
repodir = path.abspath(path.join(path.dirname(__file__), '..', '..'))
|
||||
# create default paths
|
||||
if path.splitext(sys.executable)[1] == ".exe" and not path.basename(sys.executable).startswith('python'):
|
||||
# special MS windows environment
|
||||
cfg.update(piddir='./', logdir='./log', confdir='./')
|
||||
elif path.exists(path.join(repodir, '.git')):
|
||||
# running from git repo
|
||||
cfg['confdir'] = path.join(repodir, 'cfg')
|
||||
# take logdir and piddir from <repodir>/cfg/generalConfig.cfg
|
||||
else:
|
||||
# running on installed system (typically with systemd)
|
||||
cfg.update(piddir='/var/run/frappy', logdir='/var/log', confdir='/etc/frappy')
|
||||
if configfile is None:
|
||||
configfile = environ.get('FRAPPY_CONFIG_FILE',
|
||||
path.join(cfg['confdir'], 'generalConfig.cfg'))
|
||||
if configfile and path.exists(configfile):
|
||||
parser = ConfigParser()
|
||||
parser.optionxform = str
|
||||
parser.read([configfile])
|
||||
# mandatory in a general config file:
|
||||
cfg['logdir'] = cfg['piddir'] = None
|
||||
cfg['confdir'] = path.dirname(configfile)
|
||||
# only the FRAPPY section is relevant, other sections might be used by others
|
||||
for key, value in parser['FRAPPY'].items():
|
||||
if value.startswith('./'):
|
||||
cfg[key] = path.abspath(path.join(repodir, value))
|
||||
else:
|
||||
# expand ~ to username, also in path lists separated with ':'
|
||||
cfg[key] = ':'.join(path.expanduser(v) for v in value.split(':'))
|
||||
else:
|
||||
for key in mandatory:
|
||||
cfg[key] = environ.get('FRAPPY_%s' % key.upper(), cfg[key])
|
||||
missing_keys = [key for key in mandatory if cfg[key] is None]
|
||||
if missing_keys:
|
||||
if path.exists(configfile):
|
||||
raise KeyError('missing value for %s in %s' % (' and '.join(missing_keys), configfile))
|
||||
raise FileNotFoundError(configfile)
|
||||
# this is not customizable
|
||||
cfg['basedir'] = repodir
|
||||
self._config = cfg
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""access for keys known to exist
|
||||
|
||||
:param key: the key (raises an error when key is not available)
|
||||
:return: the value
|
||||
"""
|
||||
try:
|
||||
return self._config[key]
|
||||
except KeyError:
|
||||
return self.defaults[key]
|
||||
except TypeError:
|
||||
if key in self.defaults:
|
||||
# accept retrieving defaults before init
|
||||
# e.g. 'lazy_number_validation' in secop.datatypes
|
||||
return self.defaults[key]
|
||||
raise TypeError('generalConfig.init() has to be called first') from None
|
||||
|
||||
def get(self, key, default=None):
|
||||
"""access for keys not known to exist"""
|
||||
try:
|
||||
return self.__getitem__(key)
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def getint(self, key, default=None):
|
||||
"""access and convert to int"""
|
||||
try:
|
||||
return int(self.__getitem__(key))
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def __getattr__(self, key):
|
||||
"""goodie: use generalConfig.<key> instead of generalConfig.get('<key>')"""
|
||||
return self.get(key)
|
||||
|
||||
@property
|
||||
def initialized(self):
|
||||
return bool(self._config)
|
||||
|
||||
def set_default(self, key, value):
|
||||
"""set a default value, in case not set already"""
|
||||
if key not in self.defaults:
|
||||
self.defaults[key] = value
|
||||
|
||||
def testinit(self, **kwds):
|
||||
"""for test purposes"""
|
||||
self._config = kwds
|
||||
|
||||
|
||||
unset_value = object()
|
||||
generalConfig = GeneralConfig()
|
||||
|
||||
|
||||
class lazy_property:
|
||||
@ -253,10 +339,6 @@ def getfqdn(name=''):
|
||||
return socket.getfqdn(name)
|
||||
|
||||
|
||||
def getGeneralConfig():
|
||||
return CONFIG
|
||||
|
||||
|
||||
def formatStatusBits(sword, labels, start=0):
|
||||
"""Return a list of labels according to bit state in `sword` starting
|
||||
with bit `start` and the first label in `labels`.
|
||||
@ -266,3 +348,11 @@ def formatStatusBits(sword, labels, start=0):
|
||||
if sword & (1 << i) and lbl:
|
||||
result.append(lbl)
|
||||
return result
|
||||
|
||||
|
||||
class UniqueObject:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __repr__(self):
|
||||
return self.name
|
||||
|
@ -48,6 +48,7 @@ class ConnectionClosed(ConnectionError):
|
||||
|
||||
class AsynConn:
|
||||
timeout = 1 # inter byte timeout
|
||||
scheme = None
|
||||
SCHEME_MAP = {}
|
||||
connection = None # is not None, if connected
|
||||
defaultport = None
|
||||
@ -62,11 +63,11 @@ class AsynConn:
|
||||
except (ValueError, TypeError, AssertionError):
|
||||
if 'COM' in uri:
|
||||
raise ValueError("the correct uri for a COM port is: "
|
||||
"'serial://COM<i>[?<option>=<value>[+<option>=value ...]]'")
|
||||
"'serial://COM<i>[?<option>=<value>[+<option>=value ...]]'") from None
|
||||
if '/dev' in uri:
|
||||
raise ValueError("the correct uri for a serial port is: "
|
||||
"'serial:///dev/<tty>[?<option>=<value>[+<option>=value ...]]'")
|
||||
raise ValueError('invalid uri: %s' % uri)
|
||||
"'serial:///dev/<tty>[?<option>=<value>[+<option>=value ...]]'") from None
|
||||
raise ValueError('invalid uri: %s' % uri) from None
|
||||
iocls = cls.SCHEME_MAP['tcp']
|
||||
uri = 'tcp://%s:%d' % host_port
|
||||
return object.__new__(iocls)
|
||||
@ -80,7 +81,9 @@ class AsynConn:
|
||||
|
||||
@classmethod
|
||||
def __init_subclass__(cls):
|
||||
cls.SCHEME_MAP[cls.scheme] = cls
|
||||
"""register subclass to scheme, if available"""
|
||||
if cls.scheme:
|
||||
cls.SCHEME_MAP[cls.scheme] = cls
|
||||
|
||||
def disconnect(self):
|
||||
raise NotImplementedError
|
||||
@ -166,7 +169,7 @@ class AsynTcp(AsynConn):
|
||||
self.connection = tcpSocket(uri, self.defaultport, self.timeout)
|
||||
except (ConnectionRefusedError, socket.gaierror) as e:
|
||||
# indicate that retrying might make sense
|
||||
raise CommunicationFailedError(str(e))
|
||||
raise CommunicationFailedError(str(e)) from None
|
||||
|
||||
def disconnect(self):
|
||||
if self.connection:
|
||||
@ -237,8 +240,8 @@ class AsynSerial(AsynConn):
|
||||
options = dict((kv.split('=') for kv in uri[1].split('+')))
|
||||
except IndexError: # no uri[1], no options
|
||||
options = {}
|
||||
except ValueError:
|
||||
raise ConfigError('illegal serial options')
|
||||
except ValueError as e:
|
||||
raise ConfigError('illegal serial options') from e
|
||||
parity = options.pop('parity', None) # only parity is to be treated as text
|
||||
for k, v in options.items():
|
||||
try:
|
||||
@ -251,14 +254,12 @@ class AsynSerial(AsynConn):
|
||||
if not fullname.startswith(name):
|
||||
raise ConfigError('illegal parity: %s' % parity)
|
||||
options['parity'] = name[0]
|
||||
if 'timeout' in options:
|
||||
options['timeout'] = float(self.timeout)
|
||||
else:
|
||||
if 'timeout' not in options:
|
||||
options['timeout'] = self.timeout
|
||||
try:
|
||||
self.connection = Serial(dev, **options)
|
||||
except ValueError as e:
|
||||
raise ConfigError(e)
|
||||
raise ConfigError(e) from None
|
||||
# TODO: turn exceptions into ConnectionFailedError, where a retry makes sense
|
||||
|
||||
def disconnect(self):
|
||||
|
@ -74,29 +74,29 @@ SIMPLETYPES = {
|
||||
}
|
||||
|
||||
|
||||
def short_doc(datatype):
|
||||
def short_doc(datatype, internal=False):
|
||||
# pylint: disable=possibly-unused-variable
|
||||
|
||||
def doc_EnumType(dt):
|
||||
return 'one of %s' % str(tuple(dt._enum.keys()))
|
||||
|
||||
def doc_ArrayOf(dt):
|
||||
return 'array of %s' % short_doc(dt.members)
|
||||
return 'array of %s' % short_doc(dt.members, True)
|
||||
|
||||
def doc_TupleOf(dt):
|
||||
return 'tuple of (%s)' % ', '.join(short_doc(m) for m in dt.members)
|
||||
return 'tuple of (%s)' % ', '.join(short_doc(m, True) for m in dt.members)
|
||||
|
||||
def doc_CommandType(dt):
|
||||
argument = short_doc(dt.argument) if dt.argument else ''
|
||||
result = ' -> %s' % short_doc(dt.result) if dt.result else ''
|
||||
argument = short_doc(dt.argument, True) if dt.argument else ''
|
||||
result = ' -> %s' % short_doc(dt.result, True) if dt.result else ''
|
||||
return '(%s)%s' % (argument, result) # return argument list only
|
||||
|
||||
def doc_NoneOr(dt):
|
||||
other = short_doc(dt.other)
|
||||
other = short_doc(dt.other, True)
|
||||
return '%s or None' % other if other else None
|
||||
|
||||
def doc_OrType(dt):
|
||||
types = [short_doc(t) for t in dt.types]
|
||||
types = [short_doc(t, True) for t in dt.types]
|
||||
if None in types: # type is anyway broad: no doc
|
||||
return None
|
||||
return ' or '.join(types)
|
||||
@ -104,14 +104,17 @@ def short_doc(datatype):
|
||||
def doc_Stub(dt):
|
||||
return dt.name.replace('Type', '').replace('Range', '').lower()
|
||||
|
||||
clsname = datatype.__class__.__name__
|
||||
def doc_BLOBType(dt):
|
||||
return 'byte array'
|
||||
|
||||
clsname = type(datatype).__name__
|
||||
result = SIMPLETYPES.get(clsname)
|
||||
if result:
|
||||
return result
|
||||
fun = locals().get('doc_' + clsname)
|
||||
if fun:
|
||||
return fun(datatype)
|
||||
return None # broad type like ValueType: no doc
|
||||
return clsname if internal else None # broad types like ValueType: no doc
|
||||
|
||||
|
||||
def append_to_doc(cls, lines, itemcls, name, attrname, fmtfunc):
|
||||
|
@ -21,41 +21,51 @@
|
||||
# *****************************************************************************
|
||||
|
||||
import threading
|
||||
import time
|
||||
|
||||
|
||||
ETERNITY = 1e99
|
||||
|
||||
|
||||
class _SingleEvent:
|
||||
"""Single Event
|
||||
|
||||
remark: :meth:`wait` is not implemented on purpose
|
||||
"""
|
||||
def __init__(self, multievent, timeout, name=None):
|
||||
self.multievent = multievent
|
||||
self.multievent.clear_(self)
|
||||
self.name = name
|
||||
if timeout is None:
|
||||
self.deadline = ETERNITY
|
||||
else:
|
||||
self.deadline = time.monotonic() + timeout
|
||||
|
||||
def clear(self):
|
||||
self.multievent.clear_(self)
|
||||
|
||||
def set(self):
|
||||
self.multievent.set_(self)
|
||||
|
||||
def is_set(self):
|
||||
return self in self.multievent.events
|
||||
|
||||
|
||||
class MultiEvent(threading.Event):
|
||||
"""Class implementing multi event objects.
|
||||
"""Class implementing multi event objects."""
|
||||
|
||||
meth:`new` creates Event like objects
|
||||
meth:'wait` waits for all of them being set
|
||||
"""
|
||||
|
||||
class SingleEvent:
|
||||
"""Single Event
|
||||
|
||||
remark: :meth:`wait` is not implemented on purpose
|
||||
"""
|
||||
def __init__(self, multievent):
|
||||
self.multievent = multievent
|
||||
self.multievent._clear(self)
|
||||
|
||||
def clear(self):
|
||||
self.multievent._clear(self)
|
||||
|
||||
def set(self):
|
||||
self.multievent._set(self)
|
||||
|
||||
def is_set(self):
|
||||
return self in self.multievent.events
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, default_timeout=None):
|
||||
self.events = set()
|
||||
self._lock = threading.Lock()
|
||||
self.default_timeout = default_timeout or None # treat 0 as None
|
||||
self.name = None # default event name
|
||||
self._actions = [] # actions to be executed on trigger
|
||||
super().__init__()
|
||||
|
||||
def new(self):
|
||||
"""create a new SingleEvent"""
|
||||
return self.SingleEvent(self)
|
||||
def new(self, timeout=None, name=None):
|
||||
"""create a single event like object"""
|
||||
return _SingleEvent(self, timeout or self.default_timeout,
|
||||
name or self.name or '<unnamed>')
|
||||
|
||||
def set(self):
|
||||
raise ValueError('a multievent must not be set directly')
|
||||
@ -63,21 +73,69 @@ class MultiEvent(threading.Event):
|
||||
def clear(self):
|
||||
raise ValueError('a multievent must not be cleared directly')
|
||||
|
||||
def _set(self, event):
|
||||
def is_set(self):
|
||||
return not self.events
|
||||
|
||||
def set_(self, event):
|
||||
"""internal: remove event from the event list"""
|
||||
with self._lock:
|
||||
self.events.discard(event)
|
||||
if self.events:
|
||||
return
|
||||
try:
|
||||
for action in self._actions:
|
||||
action()
|
||||
except Exception:
|
||||
pass # we silently ignore errors here
|
||||
self._actions = []
|
||||
super().set()
|
||||
|
||||
def _clear(self, event):
|
||||
def clear_(self, event):
|
||||
"""internal: add event to the event list"""
|
||||
with self._lock:
|
||||
self.events.add(event)
|
||||
super().clear()
|
||||
|
||||
def deadline(self):
|
||||
deadline = 0
|
||||
for event in self.events:
|
||||
deadline = max(event.deadline, deadline)
|
||||
return None if deadline == ETERNITY else deadline
|
||||
|
||||
def wait(self, timeout=None):
|
||||
"""wait for all events being set or timed out"""
|
||||
if not self.events: # do not wait if events are empty
|
||||
return
|
||||
super().wait(timeout)
|
||||
return True
|
||||
deadline = self.deadline()
|
||||
if deadline is not None:
|
||||
deadline -= time.monotonic()
|
||||
timeout = deadline if timeout is None else min(deadline, timeout)
|
||||
if timeout <= 0:
|
||||
return False
|
||||
return super().wait(timeout)
|
||||
|
||||
def waiting_for(self):
|
||||
return set(event.name for event in self.events)
|
||||
|
||||
def get_trigger(self, timeout=None, name=None):
|
||||
"""create a new single event and return its set method
|
||||
|
||||
as a convenience method
|
||||
"""
|
||||
return self.new(timeout, name).set
|
||||
|
||||
def queue(self, action):
|
||||
"""add an action to the queue of actions to be executed at end
|
||||
|
||||
:param action: a function, to be executed after the last event is triggered,
|
||||
and before the multievent is set
|
||||
|
||||
- if no events are waiting, the actions are executed immediately
|
||||
- if an action raises an exception, it is silently ignore and further
|
||||
actions in the queue are skipped
|
||||
- if this is not desired, the action should handle errors by itself
|
||||
"""
|
||||
with self._lock:
|
||||
self._actions.append(action)
|
||||
if self.is_set():
|
||||
self.set_(None)
|
||||
|
58
secop/lib/py35compat.py
Normal file
58
secop/lib/py35compat.py
Normal 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
|
@ -137,8 +137,8 @@ class SequencerMixin:
|
||||
if self._seq_fault_on_stop:
|
||||
return self.Status.ERROR, self._seq_stopped
|
||||
return self.Status.WARN, self._seq_stopped
|
||||
if hasattr(self, 'read_hw_status'):
|
||||
return self.read_hw_status()
|
||||
if hasattr(self, 'readHwStatus'):
|
||||
return self.readHwStatus()
|
||||
return self.Status.IDLE, ''
|
||||
|
||||
def stop(self):
|
||||
@ -153,7 +153,7 @@ class SequencerMixin:
|
||||
self._seq_error = str(e)
|
||||
finally:
|
||||
self._seq_thread = None
|
||||
self.pollParams(0)
|
||||
self.doPoll()
|
||||
|
||||
def _seq_thread_inner(self, seq, store_init):
|
||||
store = Namespace()
|
||||
|
320
secop/lib/statemachine.py
Normal file
320
secop/lib/statemachine.py
Normal 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
169
secop/logging.py
Normal 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()
|
585
secop/modules.py
585
secop/modules.py
@ -23,21 +23,27 @@
|
||||
"""Define base classes for real Modules implemented in the server"""
|
||||
|
||||
|
||||
import sys
|
||||
import time
|
||||
import threading
|
||||
from collections import OrderedDict
|
||||
from functools import wraps
|
||||
|
||||
from secop.datatypes import ArrayOf, BoolType, EnumType, FloatRange, \
|
||||
IntRange, StatusType, StringType, TextType, TupleOf
|
||||
from secop.errors import BadValueError, ConfigError, InternalError, \
|
||||
IntRange, StatusType, StringType, TextType, TupleOf, DiscouragedConversion
|
||||
from secop.errors import BadValueError, ConfigError, \
|
||||
ProgrammingError, SECoPError, SilentError, secop_error
|
||||
from secop.lib import formatException, mkthread
|
||||
from secop.lib import formatException, mkthread, UniqueObject, generalConfig
|
||||
from secop.lib.enum import Enum
|
||||
from secop.params import Accessible, Command, Parameter
|
||||
from secop.poller import BasicPoller, Poller
|
||||
from secop.properties import HasProperties, Property
|
||||
from secop.logging import RemoteLogHandler, HasComlog
|
||||
|
||||
Done = object() #: a special return value for a read/write function indicating that the setter is triggered already
|
||||
generalConfig.set_default('disable_value_range_check', False) # check for problematic value range by default
|
||||
|
||||
Done = UniqueObject('Done')
|
||||
"""a special return value for a read/write function
|
||||
|
||||
indicating that the setter is triggered already"""
|
||||
|
||||
|
||||
class HasAccessibles(HasProperties):
|
||||
@ -57,6 +63,7 @@ class HasAccessibles(HasProperties):
|
||||
merged_properties = {} # dict of dict of merged properties
|
||||
new_names = [] # list of names of new accessibles
|
||||
override_values = {} # bare values overriding a parameter and methods overriding a command
|
||||
|
||||
for base in reversed(cls.__mro__):
|
||||
for key, value in base.__dict__.items():
|
||||
if isinstance(value, Accessible):
|
||||
@ -66,28 +73,33 @@ class HasAccessibles(HasProperties):
|
||||
accessibles[key] = value
|
||||
override_values.pop(key, None)
|
||||
elif key in accessibles:
|
||||
# either a bare value overriding a parameter
|
||||
# or a method overriding a command
|
||||
override_values[key] = value
|
||||
for aname, aobj in accessibles.items():
|
||||
for aname, aobj in list(accessibles.items()):
|
||||
if aname in override_values:
|
||||
aobj = aobj.copy()
|
||||
value = override_values[aname]
|
||||
if value is None:
|
||||
accessibles.pop(aname)
|
||||
continue
|
||||
aobj.merge(merged_properties[aname])
|
||||
aobj.override(override_values[aname])
|
||||
aobj.override(value)
|
||||
# replace the bare value by the created accessible
|
||||
setattr(cls, aname, aobj)
|
||||
else:
|
||||
aobj.merge(merged_properties[aname])
|
||||
accessibles[aname] = aobj
|
||||
|
||||
# rebuild order: (1) inherited items, (2) items from paramOrder, (3) new accessibles
|
||||
# move (2) to the end
|
||||
for aname in list(cls.__dict__.get('paramOrder', ())):
|
||||
paramOrder = cls.__dict__.get('paramOrder', ())
|
||||
for aname in paramOrder:
|
||||
if aname in accessibles:
|
||||
accessibles.move_to_end(aname)
|
||||
# ignore unknown names
|
||||
# move (3) to the end
|
||||
for aname in new_names:
|
||||
accessibles.move_to_end(aname)
|
||||
if aname not in paramOrder:
|
||||
accessibles.move_to_end(aname)
|
||||
# note: for python < 3.6 the order of inherited items is not ensured between
|
||||
# declarations within the same class
|
||||
cls.accessibles = accessibles
|
||||
@ -100,12 +112,14 @@ class HasAccessibles(HasProperties):
|
||||
# XXX: create getters for the units of params ??
|
||||
|
||||
# wrap of reading/writing funcs
|
||||
if isinstance(pobj, Command):
|
||||
# nothing to do for now
|
||||
if not isinstance(pobj, Parameter):
|
||||
# nothing to do for Commands
|
||||
continue
|
||||
|
||||
rfunc = getattr(cls, 'read_' + pname, None)
|
||||
# TODO: remove handler stuff here
|
||||
rfunc_handler = pobj.handler.get_read_func(cls, pname) if pobj.handler else None
|
||||
wrapped = hasattr(rfunc, '__wrapped__')
|
||||
wrapped = getattr(rfunc, 'wrapped', False) # meaning: wrapped or auto generated
|
||||
if rfunc_handler:
|
||||
if 'read_' + pname in cls.__dict__:
|
||||
if pname in cls.__dict__:
|
||||
@ -119,65 +133,86 @@ class HasAccessibles(HasProperties):
|
||||
# create wrapper except when read function is already wrapped
|
||||
if not wrapped:
|
||||
|
||||
def wrapped_rfunc(self, pname=pname, rfunc=rfunc):
|
||||
if rfunc:
|
||||
self.log.debug("calling %r" % rfunc)
|
||||
try:
|
||||
value = rfunc(self)
|
||||
self.log.debug("rfunc(%s) returned %r" % (pname, value))
|
||||
if value is Done: # the setter is already triggered
|
||||
return getattr(self, pname)
|
||||
except Exception as e:
|
||||
self.log.debug("rfunc(%s) failed %r" % (pname, e))
|
||||
self.announceUpdate(pname, None, e)
|
||||
raise
|
||||
else:
|
||||
# return cached value
|
||||
self.log.debug("rfunc(%s): return cached value" % pname)
|
||||
value = self.accessibles[pname].value
|
||||
setattr(self, pname, value) # important! trigger the setter
|
||||
return value
|
||||
|
||||
if rfunc:
|
||||
wrapped_rfunc.__doc__ = rfunc.__doc__
|
||||
setattr(cls, 'read_' + pname, wrapped_rfunc)
|
||||
wrapped_rfunc.__wrapped__ = True
|
||||
|
||||
if not pobj.readonly:
|
||||
wfunc = getattr(cls, 'write_' + pname, None)
|
||||
wrapped = hasattr(wfunc, '__wrapped__')
|
||||
@wraps(rfunc) # handles __wrapped__ and __doc__
|
||||
def new_rfunc(self, pname=pname, rfunc=rfunc):
|
||||
with self.accessLock:
|
||||
try:
|
||||
value = rfunc(self)
|
||||
self.log.debug("read_%s returned %r", pname, value)
|
||||
except Exception as e:
|
||||
self.log.debug("read_%s failed with %r", pname, e)
|
||||
self.announceUpdate(pname, None, e)
|
||||
raise
|
||||
if value is Done:
|
||||
return getattr(self, pname)
|
||||
setattr(self, pname, value) # important! trigger the setter
|
||||
return value
|
||||
|
||||
new_rfunc.poll = getattr(rfunc, 'poll', True)
|
||||
else:
|
||||
|
||||
def new_rfunc(self, pname=pname):
|
||||
return getattr(self, pname)
|
||||
|
||||
new_rfunc.poll = False
|
||||
new_rfunc.__doc__ = 'auto generated read method for ' + pname
|
||||
|
||||
new_rfunc.wrapped = True # indicate to subclasses that no more wrapping is needed
|
||||
setattr(cls, 'read_' + pname, new_rfunc)
|
||||
|
||||
wfunc = getattr(cls, 'write_' + pname, None)
|
||||
if not pobj.readonly or wfunc: # allow write_ method even when pobj is not readonly
|
||||
wrapped = getattr(wfunc, 'wrapped', False) # meaning: wrapped or auto generated
|
||||
if (wfunc is None or wrapped) and pobj.handler:
|
||||
# ignore the handler, if a write function is present
|
||||
# TODO: remove handler stuff here
|
||||
wfunc = pobj.handler.get_write_func(pname)
|
||||
wrapped = False
|
||||
|
||||
# create wrapper except when write function is already wrapped
|
||||
if not wrapped:
|
||||
|
||||
def wrapped_wfunc(self, value, pname=pname, wfunc=wfunc):
|
||||
self.log.debug("check validity of %s = %r" % (pname, value))
|
||||
pobj = self.accessibles[pname]
|
||||
value = pobj.datatype(value)
|
||||
if wfunc:
|
||||
self.log.debug('calling %s %r(%r)' % (wfunc.__name__, wfunc, value))
|
||||
returned_value = wfunc(self, value)
|
||||
if returned_value is Done: # the setter is already triggered
|
||||
return getattr(self, pname)
|
||||
if returned_value is not None: # goodie: accept missing return value
|
||||
value = returned_value
|
||||
setattr(self, pname, value)
|
||||
return value
|
||||
|
||||
if wfunc:
|
||||
wrapped_wfunc.__doc__ = wfunc.__doc__
|
||||
setattr(cls, 'write_' + pname, wrapped_wfunc)
|
||||
wrapped_wfunc.__wrapped__ = True
|
||||
|
||||
# check information about Command's
|
||||
for attrname in cls.__dict__:
|
||||
if attrname.startswith('do_'):
|
||||
@wraps(wfunc) # handles __wrapped__ and __doc__
|
||||
def new_wfunc(self, value, pname=pname, wfunc=wfunc):
|
||||
with self.accessLock:
|
||||
pobj = self.accessibles[pname]
|
||||
self.log.debug('validate %r for %r', value, pname)
|
||||
# we do not need to handle errors here, we do not
|
||||
# want to make a parameter invalid, when a write failed
|
||||
new_value = pobj.datatype(value)
|
||||
new_value = wfunc(self, new_value)
|
||||
self.log.debug('write_%s(%r) returned %r', pname, value, new_value)
|
||||
if new_value is Done:
|
||||
# setattr(self, pname, getattr(self, pname))
|
||||
return getattr(self, pname)
|
||||
setattr(self, pname, new_value) # important! trigger the setter
|
||||
return new_value
|
||||
else:
|
||||
|
||||
def new_wfunc(self, value, pname=pname):
|
||||
setattr(self, pname, value)
|
||||
return value
|
||||
|
||||
new_wfunc.__doc__ = 'auto generated write method for ' + pname
|
||||
|
||||
new_wfunc.wrapped = True # indicate to subclasses that no more wrapping is needed
|
||||
setattr(cls, 'write_' + pname, new_wfunc)
|
||||
|
||||
# check for programming errors
|
||||
for attrname in dir(cls):
|
||||
prefix, _, pname = attrname.partition('_')
|
||||
if not pname:
|
||||
continue
|
||||
if prefix == 'do':
|
||||
raise ProgrammingError('%r: old style command %r not supported anymore'
|
||||
% (cls.__name__, attrname))
|
||||
if prefix in ('read', 'write') and not getattr(getattr(cls, attrname), 'wrapped', False):
|
||||
raise ProgrammingError('%s.%s defined, but %r is no parameter'
|
||||
% (cls.__name__, attrname, pname))
|
||||
|
||||
res = {}
|
||||
# collect info about properties
|
||||
@ -193,6 +228,26 @@ class HasAccessibles(HasProperties):
|
||||
cls.configurables = res
|
||||
|
||||
|
||||
class PollInfo:
|
||||
def __init__(self, pollinterval, trigger_event):
|
||||
self.interval = pollinterval
|
||||
self.last_main = 0
|
||||
self.last_slow = 0
|
||||
self.last_error = None
|
||||
self.polled_parameters = []
|
||||
self.fast_flag = False
|
||||
self.trigger_event = trigger_event
|
||||
|
||||
def trigger(self):
|
||||
"""trigger a recalculation of poll due times"""
|
||||
self.trigger_event.set()
|
||||
|
||||
def update_interval(self, pollinterval):
|
||||
if not self.fast_flag:
|
||||
self.interval = pollinterval
|
||||
self.trigger()
|
||||
|
||||
|
||||
class Module(HasAccessibles):
|
||||
"""basic module
|
||||
|
||||
@ -237,6 +292,9 @@ class Module(HasAccessibles):
|
||||
extname='implementation')
|
||||
interface_classes = Property('offical highest interface-class of the module', ArrayOf(StringType()),
|
||||
extname='interface_classes')
|
||||
pollinterval = Property('poll interval for parameters handled by doPoll', FloatRange(0.1, 120), default=5)
|
||||
slowinterval = Property('poll interval for other parameters', FloatRange(0.1, 120), default=15)
|
||||
enablePoll = True
|
||||
|
||||
# properties, parameters and commands are auto-merged upon subclassing
|
||||
parameters = {}
|
||||
@ -244,16 +302,24 @@ class Module(HasAccessibles):
|
||||
|
||||
# reference to the dispatcher (used for sending async updates)
|
||||
DISPATCHER = None
|
||||
|
||||
pollerClass = Poller #: default poller used
|
||||
attachedModules = None
|
||||
pollInfo = None
|
||||
triggerPoll = None # trigger event for polls. used on io modules and modules without io
|
||||
|
||||
def __init__(self, name, logger, cfgdict, srv):
|
||||
# remember the dispatcher object (for the async callbacks)
|
||||
self.DISPATCHER = srv.dispatcher
|
||||
self.omit_unchanged_within = getattr(self.DISPATCHER, 'omit_unchanged_within', 0.1)
|
||||
self.log = logger
|
||||
self.name = name
|
||||
self.valueCallbacks = {}
|
||||
self.errorCallbacks = {}
|
||||
self.earlyInitDone = False
|
||||
self.initModuleDone = False
|
||||
self.startModuleDone = False
|
||||
self.remoteLogHandler = None
|
||||
self.accessLock = threading.RLock()
|
||||
self.polledModules = [] # modules polled by thread started in self.startModules
|
||||
errors = []
|
||||
|
||||
# handle module properties
|
||||
@ -298,13 +364,6 @@ class Module(HasAccessibles):
|
||||
for aname, aobj in self.accessibles.items():
|
||||
# make a copy of the Parameter/Command object
|
||||
aobj = aobj.copy()
|
||||
if isinstance(aobj, Parameter):
|
||||
# fix default properties poll and needscfg
|
||||
if aobj.poll is None:
|
||||
aobj.poll = bool(aobj.handler)
|
||||
if aobj.needscfg is None:
|
||||
aobj.needscfg = not aobj.poll
|
||||
|
||||
if not self.export: # do not export parameters of a module not exported
|
||||
aobj.export = False
|
||||
if aobj.export:
|
||||
@ -319,26 +378,23 @@ class Module(HasAccessibles):
|
||||
|
||||
# 2) check and apply parameter_properties
|
||||
# specified as '<paramname>.<propertyname> = <propertyvalue>'
|
||||
# this may also be done on commands: e.g. 'stop.visibility = advanced'
|
||||
for k, v in list(cfgdict.items()): # keep list() as dict may change during iter
|
||||
if '.' in k[1:]:
|
||||
paramname, propname = k.split('.', 1)
|
||||
aname, propname = k.split('.', 1)
|
||||
propvalue = cfgdict.pop(k)
|
||||
paramobj = self.accessibles.get(paramname, None)
|
||||
# paramobj might also be a command (not sure if this is needed)
|
||||
if paramobj:
|
||||
# no longer needed, this conversion is done by DataTypeType.__call__:
|
||||
# if propname == 'datatype':
|
||||
# propvalue = get_datatype(propvalue, k)
|
||||
aobj = self.accessibles.get(aname, None)
|
||||
if aobj:
|
||||
try:
|
||||
paramobj.setProperty(propname, propvalue)
|
||||
aobj.setProperty(propname, propvalue)
|
||||
except KeyError:
|
||||
errors.append("'%s.%s' does not exist" %
|
||||
(paramname, propname))
|
||||
(aname, propname))
|
||||
except BadValueError as e:
|
||||
errors.append('%s.%s: %s' %
|
||||
(paramname, propname, str(e)))
|
||||
(aname, propname, str(e)))
|
||||
else:
|
||||
errors.append('%r not found' % paramname)
|
||||
errors.append('%r not found' % aname)
|
||||
|
||||
# 3) check config for problems:
|
||||
# only accept remaining config items specified in parameters
|
||||
@ -361,9 +417,8 @@ class Module(HasAccessibles):
|
||||
continue
|
||||
|
||||
if pname in cfgdict:
|
||||
if not pobj.readonly and pobj.initwrite is not False:
|
||||
if pobj.initwrite is not False and hasattr(self, 'write_' + pname):
|
||||
# parameters given in cfgdict have to call write_<pname>
|
||||
# TODO: not sure about readonly (why not a parameter which can only be written from config?)
|
||||
try:
|
||||
pobj.value = pobj.datatype(cfgdict[pname])
|
||||
self.writeDict[pname] = pobj.value
|
||||
@ -376,7 +431,7 @@ class Module(HasAccessibles):
|
||||
'value and was not given in config!' % pname)
|
||||
# we do not want to call the setter for this parameter for now,
|
||||
# this should happen on the first read
|
||||
pobj.readerror = ConfigError('not initialized')
|
||||
pobj.readerror = ConfigError('parameter %r not initialized' % pname)
|
||||
# above error will be triggered on activate after startup,
|
||||
# when not all hardware parameters are read because of startup timeout
|
||||
pobj.value = pobj.datatype(pobj.datatype.default)
|
||||
@ -386,10 +441,8 @@ class Module(HasAccessibles):
|
||||
except BadValueError as e:
|
||||
# this should not happen, as the default is already checked in Parameter
|
||||
raise ProgrammingError('bad default for %s:%s: %s' % (name, pname, e)) from None
|
||||
if pobj.initwrite and not pobj.readonly:
|
||||
if pobj.initwrite and hasattr(self, 'write_' + pname):
|
||||
# we will need to call write_<pname>
|
||||
# if this is not desired, the default must not be given
|
||||
# TODO: not sure about readonly (why not a parameter which can only be written from config?)
|
||||
pobj.value = value
|
||||
self.writeDict[pname] = value
|
||||
else:
|
||||
@ -424,11 +477,11 @@ class Module(HasAccessibles):
|
||||
self.checkProperties()
|
||||
except ConfigError as e:
|
||||
errors.append(str(e))
|
||||
for pname, p in self.parameters.items():
|
||||
for aname, aobj in self.accessibles.items():
|
||||
try:
|
||||
p.checkProperties()
|
||||
except ConfigError as e:
|
||||
errors.append('%s: %s' % (pname, e))
|
||||
aobj.checkProperties()
|
||||
except (ConfigError, ProgrammingError) as e:
|
||||
errors.append('%s: %s' % (aname, e))
|
||||
if errors:
|
||||
raise ConfigError(errors)
|
||||
|
||||
@ -441,42 +494,47 @@ class Module(HasAccessibles):
|
||||
|
||||
def announceUpdate(self, pname, value=None, err=None, timestamp=None):
|
||||
"""announce a changed value or readerror"""
|
||||
pobj = self.parameters[pname]
|
||||
timestamp = timestamp or time.time()
|
||||
changed = pobj.value != value
|
||||
if value is not None:
|
||||
pobj.value = value # store the value even in case of error
|
||||
if err:
|
||||
if not isinstance(err, SECoPError):
|
||||
err = InternalError(err)
|
||||
if str(err) == str(pobj.readerror):
|
||||
return # do call updates for repeated errors
|
||||
else:
|
||||
|
||||
with self.accessLock:
|
||||
# TODO: remove readerror 'property' and replace value with exception
|
||||
pobj = self.parameters[pname]
|
||||
timestamp = timestamp or time.time()
|
||||
changed = pobj.value != value
|
||||
try:
|
||||
# store the value even in case of error
|
||||
pobj.value = pobj.datatype(value)
|
||||
except Exception as e:
|
||||
err = secop_error(e)
|
||||
if not changed and timestamp < ((pobj.timestamp or 0)
|
||||
+ self.DISPATCHER.OMIT_UNCHANGED_WITHIN):
|
||||
if isinstance(e, DiscouragedConversion):
|
||||
if DiscouragedConversion.log_message:
|
||||
self.log.error(str(e))
|
||||
self.log.error('you may disable this behaviour by running the server with --relaxed')
|
||||
DiscouragedConversion.log_message = False
|
||||
if not err: # do not overwrite given error
|
||||
err = e
|
||||
if err:
|
||||
err = secop_error(err)
|
||||
if str(err) == str(pobj.readerror):
|
||||
return # no updates for repeated errors
|
||||
elif not changed and timestamp < (pobj.timestamp or 0) + self.omit_unchanged_within:
|
||||
# no change within short time -> omit
|
||||
return
|
||||
pobj.timestamp = timestamp
|
||||
pobj.readerror = err
|
||||
if pobj.export:
|
||||
self.DISPATCHER.announce_update(self.name, pname, pobj)
|
||||
if err:
|
||||
callbacks = self.errorCallbacks
|
||||
arg = err
|
||||
else:
|
||||
callbacks = self.valueCallbacks
|
||||
arg = value
|
||||
cblist = callbacks[pname]
|
||||
for cb in cblist:
|
||||
try:
|
||||
cb(arg)
|
||||
except Exception:
|
||||
# print(formatExtendedTraceback())
|
||||
pass
|
||||
pobj.timestamp = timestamp or time.time()
|
||||
pobj.readerror = err
|
||||
if pobj.export:
|
||||
self.DISPATCHER.announce_update(self.name, pname, pobj)
|
||||
if err:
|
||||
callbacks = self.errorCallbacks
|
||||
arg = err
|
||||
else:
|
||||
callbacks = self.valueCallbacks
|
||||
arg = value
|
||||
cblist = callbacks[pname]
|
||||
for cb in cblist:
|
||||
try:
|
||||
cb(arg)
|
||||
except Exception:
|
||||
# print(formatExtendedTraceback())
|
||||
pass
|
||||
|
||||
def registerCallbacks(self, modobj, autoupdate=()):
|
||||
"""register callbacks to another module <modobj>
|
||||
@ -495,15 +553,15 @@ class Module(HasAccessibles):
|
||||
for pname in self.parameters:
|
||||
errfunc = getattr(modobj, 'error_update_' + pname, None)
|
||||
if errfunc:
|
||||
def errcb(err, p=pname, m=modobj, efunc=errfunc):
|
||||
def errcb(err, p=pname, efunc=errfunc):
|
||||
try:
|
||||
efunc(err)
|
||||
except Exception as e:
|
||||
m.announceUpdate(p, err=e)
|
||||
modobj.announceUpdate(p, err=e)
|
||||
self.errorCallbacks[pname].append(errcb)
|
||||
else:
|
||||
def errcb(err, p=pname, m=modobj):
|
||||
m.announceUpdate(p, err=err)
|
||||
def errcb(err, p=pname):
|
||||
modobj.announceUpdate(p, err=err)
|
||||
if pname in autoupdate:
|
||||
self.errorCallbacks[pname].append(errcb)
|
||||
|
||||
@ -516,8 +574,8 @@ class Module(HasAccessibles):
|
||||
efunc(e)
|
||||
self.valueCallbacks[pname].append(cb)
|
||||
elif pname in autoupdate:
|
||||
def cb(value, p=pname, m=modobj):
|
||||
m.announceUpdate(p, value)
|
||||
def cb(value, p=pname):
|
||||
modobj.announceUpdate(p, value)
|
||||
self.valueCallbacks[pname].append(cb)
|
||||
|
||||
def isBusy(self, status=None):
|
||||
@ -526,16 +584,54 @@ class Module(HasAccessibles):
|
||||
return False
|
||||
|
||||
def earlyInit(self):
|
||||
# may be overriden in derived classes to init stuff
|
||||
self.log.debug('empty %s.earlyInit()' % self.__class__.__name__)
|
||||
"""initialise module with stuff to be done before all modules are created"""
|
||||
self.earlyInitDone = True
|
||||
|
||||
def initModule(self):
|
||||
self.log.debug('empty %s.initModule()' % self.__class__.__name__)
|
||||
"""initialise module with stuff to be done after all modules are created"""
|
||||
self.initModuleDone = True
|
||||
if self.enablePoll or self.writeDict:
|
||||
# enablePoll == False: we still need the poll thread for writing values from writeDict
|
||||
if hasattr(self, 'io'):
|
||||
self.io.polledModules.append(self)
|
||||
else:
|
||||
self.triggerPoll = threading.Event()
|
||||
self.polledModules = [self]
|
||||
|
||||
def pollOneParam(self, pname):
|
||||
"""poll parameter <pname> with proper error handling"""
|
||||
def startModule(self, start_events):
|
||||
"""runs after init of all modules
|
||||
|
||||
when a thread is started, a trigger function may signal that it
|
||||
has finished its initial work
|
||||
start_events.get_trigger(<timeout>) creates such a trigger and
|
||||
registers it in the server for waiting
|
||||
<timeout> defaults to 30 seconds
|
||||
"""
|
||||
if self.polledModules:
|
||||
mkthread(self.__pollThread, self.polledModules, start_events.get_trigger())
|
||||
self.startModuleDone = True
|
||||
|
||||
def doPoll(self):
|
||||
"""polls important parameters like value and status
|
||||
|
||||
all other parameters are polled automatically
|
||||
"""
|
||||
|
||||
def setFastPoll(self, flag, fast_interval=0.25):
|
||||
"""change poll interval
|
||||
|
||||
:param flag: enable/disable fast poll mode
|
||||
:param fast_interval: fast poll interval
|
||||
"""
|
||||
if self.pollInfo:
|
||||
self.pollInfo.fast_flag = flag
|
||||
self.pollInfo.interval = fast_interval if flag else self.pollinterval
|
||||
self.pollInfo.trigger()
|
||||
|
||||
def callPollFunc(self, rfunc):
|
||||
"""call read method with proper error handling"""
|
||||
try:
|
||||
getattr(self, 'read_' + pname)()
|
||||
rfunc()
|
||||
except SilentError:
|
||||
pass
|
||||
except SECoPError as e:
|
||||
@ -543,6 +639,93 @@ class Module(HasAccessibles):
|
||||
except Exception:
|
||||
self.log.error(formatException())
|
||||
|
||||
def __pollThread(self, modules, started_callback):
|
||||
"""poll thread body
|
||||
|
||||
:param modules: list of modules to be handled by this thread
|
||||
:param started_callback: to be called after all polls are done once
|
||||
|
||||
before polling, parameters which need hardware initialisation are written
|
||||
"""
|
||||
for mobj in modules:
|
||||
mobj.writeInitParams()
|
||||
modules = [m for m in modules if m.enablePoll]
|
||||
if not modules: # no polls needed - exit thread
|
||||
started_callback()
|
||||
return
|
||||
if hasattr(self, 'registerReconnectCallback'):
|
||||
# self is a communicator supporting reconnections
|
||||
def trigger_all(trg=self.triggerPoll, polled_modules=modules):
|
||||
for m in polled_modules:
|
||||
m.pollInfo.last_main = 0
|
||||
m.pollInfo.last_slow = 0
|
||||
trg.set()
|
||||
self.registerReconnectCallback('trigger_polls', trigger_all)
|
||||
|
||||
# collect and call all read functions a first time
|
||||
for mobj in modules:
|
||||
pinfo = mobj.pollInfo = PollInfo(mobj.pollinterval, self.triggerPoll)
|
||||
# trigger a poll interval change when self.pollinterval changes.
|
||||
if 'pollinterval' in mobj.valueCallbacks:
|
||||
mobj.valueCallbacks['pollinterval'].append(pinfo.update_interval)
|
||||
|
||||
for pname, pobj in mobj.parameters.items():
|
||||
rfunc = getattr(mobj, 'read_' + pname)
|
||||
if rfunc.poll:
|
||||
pinfo.polled_parameters.append((mobj, rfunc, pobj))
|
||||
mobj.callPollFunc(rfunc)
|
||||
started_callback()
|
||||
to_poll = ()
|
||||
while True:
|
||||
now = time.time()
|
||||
wait_time = 999
|
||||
for mobj in modules:
|
||||
pinfo = mobj.pollInfo
|
||||
wait_time = min(pinfo.last_main + pinfo.interval - now, wait_time,
|
||||
pinfo.last_slow + mobj.slowinterval - now)
|
||||
if wait_time > 0:
|
||||
self.triggerPoll.wait(wait_time)
|
||||
self.triggerPoll.clear()
|
||||
continue
|
||||
# call doPoll of all modules where due
|
||||
for mobj in modules:
|
||||
pinfo = mobj.pollInfo
|
||||
if now > pinfo.last_main + pinfo.interval:
|
||||
pinfo.last_main = (now // pinfo.interval) * pinfo.interval
|
||||
try:
|
||||
mobj.doPoll()
|
||||
pinfo.last_error = None
|
||||
except Exception as e:
|
||||
if str(e) != str(pinfo.last_error) and not isinstance(e, SilentError):
|
||||
mobj.log.error('doPoll: %r', e)
|
||||
pinfo.last_error = e
|
||||
now = time.time()
|
||||
# find ONE due slow poll and call it
|
||||
loop = True
|
||||
while loop: # loops max. 2 times, when to_poll is at end
|
||||
for mobj, rfunc, pobj in to_poll:
|
||||
if now > pobj.timestamp + mobj.slowinterval * 0.5:
|
||||
try:
|
||||
prev_err = pobj.readerror
|
||||
rfunc()
|
||||
except Exception as e:
|
||||
if not isinstance(e, SilentError) and str(pobj.readerror) != str(prev_err):
|
||||
mobj.log.error('%s: %r', pobj.name, e)
|
||||
loop = False # one poll done
|
||||
break
|
||||
else:
|
||||
to_poll = []
|
||||
# collect due slow polls
|
||||
for mobj in modules:
|
||||
pinfo = mobj.pollInfo
|
||||
if now > pinfo.last_slow + mobj.slowinterval:
|
||||
to_poll.extend(pinfo.polled_parameters)
|
||||
pinfo.last_slow = (now // mobj.slowinterval) * mobj.slowinterval
|
||||
if to_poll:
|
||||
to_poll = iter(to_poll)
|
||||
else:
|
||||
loop = False # no slow polls ready
|
||||
|
||||
def writeInitParams(self, started_callback=None):
|
||||
"""write values for parameters with configured values
|
||||
|
||||
@ -565,14 +748,15 @@ class Module(HasAccessibles):
|
||||
if started_callback:
|
||||
started_callback()
|
||||
|
||||
def startModule(self, started_callback):
|
||||
"""runs after init of all modules
|
||||
|
||||
started_callback to be called when the thread spawned by startModule
|
||||
has finished its initial work
|
||||
might return a timeout value, if different from default
|
||||
"""
|
||||
mkthread(self.writeInitParams, started_callback)
|
||||
def setRemoteLogging(self, conn, level):
|
||||
if self.remoteLogHandler is None:
|
||||
for handler in self.log.handlers:
|
||||
if isinstance(handler, RemoteLogHandler):
|
||||
self.remoteLogHandler = handler
|
||||
break
|
||||
else:
|
||||
raise ValueError('remote handler not found')
|
||||
self.remoteLogHandler.set_conn_level(self, conn, level)
|
||||
|
||||
|
||||
class Readable(Module):
|
||||
@ -587,63 +771,43 @@ class Readable(Module):
|
||||
UNKNOWN=401,
|
||||
) #: status codes
|
||||
|
||||
value = Parameter('current value of the module', FloatRange(), poll=True)
|
||||
value = Parameter('current value of the module', FloatRange())
|
||||
status = Parameter('current status of the module', TupleOf(EnumType(Status), StringType()),
|
||||
default=(Status.IDLE, ''), poll=True)
|
||||
pollinterval = Parameter('sleeptime between polls', FloatRange(0.1, 120),
|
||||
default=5, readonly=False)
|
||||
default=(Status.IDLE, ''))
|
||||
pollinterval = Parameter('default poll interval', FloatRange(0.1, 120),
|
||||
default=5, readonly=False, export=True)
|
||||
|
||||
def startModule(self, started_callback):
|
||||
"""start basic polling thread"""
|
||||
if self.pollerClass and issubclass(self.pollerClass, BasicPoller):
|
||||
# use basic poller for legacy code
|
||||
mkthread(self.__pollThread, started_callback)
|
||||
else:
|
||||
super().startModule(started_callback)
|
||||
|
||||
def __pollThread(self, started_callback):
|
||||
while True:
|
||||
try:
|
||||
self.__pollThread_inner(started_callback)
|
||||
except Exception as e:
|
||||
self.log.exception(e)
|
||||
self.status = (self.Status.ERROR, 'polling thread could not start')
|
||||
started_callback()
|
||||
print(formatException(0, sys.exc_info(), verbose=True))
|
||||
time.sleep(10)
|
||||
|
||||
def __pollThread_inner(self, started_callback):
|
||||
"""super simple and super stupid per-module polling thread"""
|
||||
self.writeInitParams()
|
||||
i = 0
|
||||
fastpoll = self.pollParams(i)
|
||||
started_callback()
|
||||
while True:
|
||||
i += 1
|
||||
try:
|
||||
time.sleep(self.pollinterval * (0.1 if fastpoll else 1))
|
||||
except TypeError:
|
||||
time.sleep(min(self.pollinterval)
|
||||
if fastpoll else max(self.pollinterval))
|
||||
fastpoll = self.pollParams(i)
|
||||
|
||||
def pollParams(self, nr=0):
|
||||
# Just poll all parameters regularly where polling is enabled
|
||||
for pname, pobj in self.parameters.items():
|
||||
if not pobj.poll:
|
||||
continue
|
||||
if nr % abs(int(pobj.poll)) == 0:
|
||||
# pollParams every 'pobj.pollParams' iteration
|
||||
self.pollOneParam(pname)
|
||||
return False
|
||||
def doPoll(self):
|
||||
self.read_value()
|
||||
self.read_status()
|
||||
|
||||
|
||||
class Writable(Readable):
|
||||
"""basic writable module"""
|
||||
|
||||
disable_value_range_check = Property('disable value range check', BoolType(), default=False)
|
||||
target = Parameter('target value of the module',
|
||||
default=0, readonly=False, datatype=FloatRange(unit='$'))
|
||||
|
||||
def __init__(self, name, logger, cfgdict, srv):
|
||||
super().__init__(name, logger, cfgdict, srv)
|
||||
value_dt = self.parameters['value'].datatype
|
||||
target_dt = self.parameters['target'].datatype
|
||||
try:
|
||||
# this handles also the cases where the limits on the value are more
|
||||
# restrictive than on the target
|
||||
target_dt.compatible(value_dt)
|
||||
except Exception:
|
||||
if type(value_dt) == type(target_dt):
|
||||
raise ConfigError('the target range extends beyond the value range') from None
|
||||
raise ProgrammingError('the datatypes of target and value are not compatible') from None
|
||||
if isinstance(value_dt, FloatRange):
|
||||
if (not self.disable_value_range_check and not generalConfig.disable_value_range_check
|
||||
and value_dt.problematic_range(target_dt)):
|
||||
self.log.error('the value range must be bigger than the target range!')
|
||||
self.log.error('you may disable this error message by running the server with --relaxed')
|
||||
self.log.error('or by setting the disable_value_range_check property of the module to True')
|
||||
raise ConfigError('the value range must be bigger than the target range')
|
||||
|
||||
|
||||
class Drivable(Writable):
|
||||
"""basic drivable module"""
|
||||
@ -666,30 +830,12 @@ class Drivable(Writable):
|
||||
"""
|
||||
return 300 <= (status or self.status)[0] < 390
|
||||
|
||||
# improved polling: may poll faster if module is BUSY
|
||||
def pollParams(self, nr=0):
|
||||
# poll status first
|
||||
self.read_status()
|
||||
fastpoll = self.isBusy()
|
||||
for pname, pobj in self.parameters.items():
|
||||
if not pobj.poll:
|
||||
continue
|
||||
if pname == 'status':
|
||||
# status was already polled above
|
||||
continue
|
||||
if ((int(pobj.poll) < 0) and fastpoll) or (
|
||||
nr % abs(int(pobj.poll))) == 0:
|
||||
# poll always if pobj.poll is negative and fastpoll (i.e. Module is busy)
|
||||
# otherwise poll every 'pobj.poll' iteration
|
||||
self.pollOneParam(pname)
|
||||
return fastpoll
|
||||
|
||||
@Command(None, result=None)
|
||||
def stop(self):
|
||||
"""cease driving, go to IDLE state"""
|
||||
|
||||
|
||||
class Communicator(Module):
|
||||
class Communicator(HasComlog, Module):
|
||||
"""basic abstract communication module"""
|
||||
|
||||
@Command(StringType(), result=StringType())
|
||||
@ -703,19 +849,20 @@ class Communicator(Module):
|
||||
|
||||
|
||||
class Attached(Property):
|
||||
"""a special property, defining an attached modle
|
||||
"""a special property, defining an attached module
|
||||
|
||||
assign a module name to this property in the cfg file,
|
||||
and the server will create an attribute with this module
|
||||
|
||||
:param attrname: the name of the to be created attribute. if not given
|
||||
the attribute name is the property name prepended by an underscore.
|
||||
"""
|
||||
# we can not put this to properties.py, as it needs datatypes
|
||||
def __init__(self, attrname=None):
|
||||
self.attrname = attrname
|
||||
# we can not make it mandatory, as the check in Module.__init__ will be before auto-assign in HasIodev
|
||||
super().__init__('attached module', StringType(), mandatory=False)
|
||||
def __init__(self, basecls=Module, description='attached module', mandatory=True):
|
||||
self.basecls = basecls
|
||||
super().__init__(description, StringType(), mandatory=mandatory)
|
||||
|
||||
def __repr__(self):
|
||||
return 'Attached(%s)' % (repr(self.attrname) if self.attrname else '')
|
||||
def __get__(self, obj, owner):
|
||||
if obj is None:
|
||||
return self
|
||||
if obj.attachedModules is None:
|
||||
# return the name of the module (called from Server on startup)
|
||||
return super().__get__(obj, owner)
|
||||
# return the module (called after startup)
|
||||
return obj.attachedModules.get(self.name) # return None if not given
|
||||
|
@ -26,12 +26,13 @@
|
||||
import inspect
|
||||
|
||||
from secop.datatypes import BoolType, CommandType, DataType, \
|
||||
DataTypeType, EnumType, IntRange, NoneOr, OrType, \
|
||||
DataTypeType, EnumType, NoneOr, OrType, \
|
||||
StringType, StructOf, TextType, TupleOf, ValueType
|
||||
from secop.errors import BadValueError, ProgrammingError
|
||||
from secop.properties import HasProperties, Property
|
||||
from secop.lib import generalConfig
|
||||
|
||||
UNSET = object() # an argument not given, not even None
|
||||
generalConfig.set_default('tolerate_poll_property', False)
|
||||
|
||||
|
||||
class Accessible(HasProperties):
|
||||
@ -134,24 +135,9 @@ class Parameter(Accessible):
|
||||
* True: exported, name automatic.
|
||||
* a string: exported with custom name''', OrType(BoolType(), StringType()),
|
||||
export=False, default=True)
|
||||
poll = Property(
|
||||
'''[internal] polling indicator
|
||||
|
||||
may be:
|
||||
|
||||
* None (omitted): will be converted to True/False if handler is/is not None
|
||||
* False or 0 (never poll this parameter)
|
||||
* True or 1 (AUTO), converted to SLOW (readonly=False)
|
||||
DYNAMIC (*status* and *value*) or REGULAR (else)
|
||||
* 2 (SLOW), polled with lower priority and a multiple of pollinterval
|
||||
* 3 (REGULAR), polled with pollperiod
|
||||
* 4 (DYNAMIC), if BUSY, with a fraction of pollinterval,
|
||||
else polled with pollperiod
|
||||
''', NoneOr(IntRange()),
|
||||
export=False, default=None)
|
||||
needscfg = Property(
|
||||
'[internal] needs value in config', NoneOr(BoolType()),
|
||||
export=False, default=None)
|
||||
export=False, default=False)
|
||||
optional = Property(
|
||||
'[internal] is this parameter optional?', BoolType(),
|
||||
export=False, settable=False, default=False)
|
||||
@ -171,6 +157,8 @@ class Parameter(Accessible):
|
||||
|
||||
def __init__(self, description=None, datatype=None, inherit=True, **kwds):
|
||||
super().__init__()
|
||||
if 'poll' in kwds and generalConfig.tolerate_poll_property:
|
||||
kwds.pop('poll')
|
||||
if datatype is None:
|
||||
# collect datatype properties. these are not applied, as we have no datatype
|
||||
self.ownProperties = {k: kwds.pop(k) for k in list(kwds) if k not in self.propertyDict}
|
||||
@ -198,7 +186,6 @@ class Parameter(Accessible):
|
||||
self.ownProperties = {k: getattr(self, k) for k in self.propertyDict}
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
# not used yet
|
||||
if instance is None:
|
||||
return self
|
||||
return instance.parameters[self.name].value
|
||||
@ -219,6 +206,9 @@ class Parameter(Accessible):
|
||||
self.export = '_' + self.name
|
||||
else:
|
||||
raise ProgrammingError('can not use %r as name of a Parameter' % self.name)
|
||||
if 'export' in self.ownProperties:
|
||||
# avoid export=True overrides export=<name>
|
||||
self.ownProperties['export'] = self.export
|
||||
|
||||
def copy(self):
|
||||
"""return a (deep) copy of ourselfs"""
|
||||
@ -346,6 +336,9 @@ class Command(Accessible):
|
||||
|
||||
def __init__(self, argument=False, *, result=None, inherit=True, **kwds):
|
||||
super().__init__()
|
||||
if 'datatype' in kwds:
|
||||
# self.init will complain about invalid keywords except 'datatype', as this is a property
|
||||
raise ProgrammingError("Command() got an invalid keyword 'datatype'")
|
||||
self.init(kwds)
|
||||
if result or kwds or isinstance(argument, DataType) or not callable(argument):
|
||||
# normal case
|
||||
@ -362,8 +355,9 @@ class Command(Accessible):
|
||||
self.func = argument # this is the wrapped method!
|
||||
if argument.__doc__:
|
||||
self.description = inspect.cleandoc(argument.__doc__)
|
||||
self.name = self.func.__name__
|
||||
self.name = self.func.__name__ # this is probably not needed
|
||||
self._inherit = inherit # save for __set_name__
|
||||
self.ownProperties = self.propertyValues.copy()
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
self.name = name
|
||||
@ -372,7 +366,6 @@ class Command(Accessible):
|
||||
(owner.__name__, name))
|
||||
|
||||
self.datatype = CommandType(self.argument, self.result)
|
||||
self.ownProperties = self.propertyValues.copy()
|
||||
if self.export is True:
|
||||
predefined_cls = PREDEFINED_ACCESSIBLES.get(name, None)
|
||||
if predefined_cls is Command:
|
||||
@ -381,6 +374,9 @@ class Command(Accessible):
|
||||
self.export = '_' + name
|
||||
else:
|
||||
raise ProgrammingError('can not use %r as name of a Command' % name) from None
|
||||
if 'export' in self.ownProperties:
|
||||
# avoid export=True overrides export=<name>
|
||||
self.ownProperties['export'] = self.export
|
||||
if not self._inherit:
|
||||
for key, pobj in self.properties.items():
|
||||
if key not in self.propertyValues:
|
||||
@ -397,6 +393,7 @@ class Command(Accessible):
|
||||
"""called when used as decorator"""
|
||||
if 'description' not in self.propertyValues and func.__doc__:
|
||||
self.description = inspect.cleandoc(func.__doc__)
|
||||
self.ownProperties['description'] = self.description
|
||||
self.func = func
|
||||
return self
|
||||
|
||||
|
@ -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')
|
@ -55,9 +55,9 @@ class MyClass(PersistentMixin, ...):
|
||||
import os
|
||||
import json
|
||||
|
||||
from secop.lib import getGeneralConfig
|
||||
from secop.lib import generalConfig
|
||||
from secop.datatypes import EnumType
|
||||
from secop.params import Parameter, Property, BoolType, Command
|
||||
from secop.params import Parameter, Property, Command
|
||||
from secop.modules import HasAccessibles
|
||||
|
||||
|
||||
@ -69,13 +69,13 @@ class PersistentParam(Parameter):
|
||||
class PersistentMixin(HasAccessibles):
|
||||
def __init__(self, *args, **kwds):
|
||||
super().__init__(*args, **kwds)
|
||||
persistentdir = os.path.join(getGeneralConfig()['logdir'], 'persistent')
|
||||
persistentdir = os.path.join(generalConfig.logdir, 'persistent')
|
||||
os.makedirs(persistentdir, exist_ok=True)
|
||||
self.persistentFile = os.path.join(persistentdir, '%s.%s.json' % (self.DISPATCHER.equipment_id, self.name))
|
||||
self.initData = {}
|
||||
for pname in self.parameters:
|
||||
pobj = self.parameters[pname]
|
||||
if not pobj.readonly and getattr(pobj, 'persistent', 0):
|
||||
if hasattr(self, 'write_' + pname) and getattr(pobj, 'persistent', 0):
|
||||
self.initData[pname] = pobj.value
|
||||
if pobj.persistent == 'auto':
|
||||
def cb(value, m=self):
|
||||
@ -103,6 +103,7 @@ class PersistentMixin(HasAccessibles):
|
||||
try:
|
||||
value = pobj.datatype.import_value(self.persistentData[pname])
|
||||
pobj.value = value
|
||||
pobj.readerror = None
|
||||
if not pobj.readonly:
|
||||
writeDict[pname] = value
|
||||
except Exception as e:
|
||||
@ -144,5 +145,6 @@ class PersistentMixin(HasAccessibles):
|
||||
|
||||
@Command()
|
||||
def factory_reset(self):
|
||||
"""reset to values from config / default values"""
|
||||
self.writeDict.update(self.initData)
|
||||
self.writeInitParams()
|
||||
|
278
secop/poller.py
278
secop/poller.py
@ -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
|
@ -24,24 +24,15 @@
|
||||
|
||||
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
from secop.errors import BadValueError, ConfigError, ProgrammingError
|
||||
from secop.lib import UniqueObject
|
||||
from secop.lib.py35compat import Object
|
||||
|
||||
UNSET = UniqueObject('undefined value') #: an unset value, not even None
|
||||
|
||||
|
||||
class HasDescriptorMeta(type):
|
||||
def __new__(cls, name, bases, attrs):
|
||||
newtype = type.__new__(cls, name, bases, attrs)
|
||||
if sys.version_info < (3, 6):
|
||||
# support older python versions
|
||||
for key, attr in attrs.items():
|
||||
if hasattr(attr, '__set_name__'):
|
||||
attr.__set_name__(newtype, key)
|
||||
newtype.__init_subclass__()
|
||||
return newtype
|
||||
|
||||
|
||||
class HasDescriptors(metaclass=HasDescriptorMeta):
|
||||
class HasDescriptors(Object):
|
||||
@classmethod
|
||||
def __init_subclass__(cls):
|
||||
# when migrating old style declarations, sometimes the trailing comma is not removed
|
||||
@ -51,9 +42,6 @@ class HasDescriptors(metaclass=HasDescriptorMeta):
|
||||
raise ProgrammingError('misplaced trailing comma after %s.%s' % (cls.__name__, '/'.join(bad)))
|
||||
|
||||
|
||||
UNSET = object() # an unset value, not even None
|
||||
|
||||
|
||||
# storage for 'properties of a property'
|
||||
class Property:
|
||||
"""base class holding info about a property
|
||||
@ -142,31 +130,27 @@ class HasProperties(HasDescriptors):
|
||||
@classmethod
|
||||
def __init_subclass__(cls):
|
||||
super().__init_subclass__()
|
||||
# raise an error when an attribute is a tuple with one single descriptor as element
|
||||
# when migrating old style declarations, sometimes the trailing comma is not removed
|
||||
bad = [k for k, v in cls.__dict__.items()
|
||||
if isinstance(v, tuple) and len(v) == 1 and hasattr(v[0], '__set_name__')]
|
||||
if bad:
|
||||
raise ProgrammingError('misplaced trailing comma after %s.%s' % (cls.__name__, '/'.join(bad)))
|
||||
properties = {}
|
||||
# using cls.__bases__ and base.propertyDict for this would fail on some multiple inheritance cases
|
||||
for base in reversed(cls.__mro__):
|
||||
properties.update({k: v for k, v in base.__dict__.items() if isinstance(v, Property)})
|
||||
cls.propertyDict = properties
|
||||
# treat overriding properties with bare values
|
||||
for pn, po in properties.items():
|
||||
for pn, po in list(properties.items()):
|
||||
value = getattr(cls, pn, po)
|
||||
if not isinstance(value, Property): # attribute is a bare value
|
||||
if isinstance(value, HasProperties): # value is a Parameter, allow override
|
||||
properties.pop(pn)
|
||||
elif not isinstance(value, Property): # attribute may be a bare value
|
||||
po = po.copy()
|
||||
try:
|
||||
# try to apply bare value to Property
|
||||
po.value = po.datatype(value)
|
||||
except BadValueError:
|
||||
if pn in properties:
|
||||
if callable(value):
|
||||
raise ProgrammingError('method %s.%s collides with property of %s' %
|
||||
(cls.__name__, pn, base.__name__)) from None
|
||||
raise ProgrammingError('can not set property %s.%s to %r' %
|
||||
(cls.__name__, pn, value)) from None
|
||||
if callable(value):
|
||||
raise ProgrammingError('method %s.%s collides with property of %s' %
|
||||
(cls.__name__, pn, base.__name__)) from None
|
||||
raise ProgrammingError('can not set property %s.%s to %r' %
|
||||
(cls.__name__, pn, value)) from None
|
||||
cls.propertyDict[pn] = po
|
||||
|
||||
def checkProperties(self):
|
||||
|
@ -19,4 +19,4 @@
|
||||
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
||||
#
|
||||
# *****************************************************************************
|
||||
"""SECoP protocl specific stuff"""
|
||||
"""SECoP protocol specific stuff"""
|
||||
|
@ -47,7 +47,8 @@ from secop.errors import NoSuchCommandError, NoSuchModuleError, \
|
||||
from secop.params import Parameter
|
||||
from secop.protocol.messages import COMMANDREPLY, DESCRIPTIONREPLY, \
|
||||
DISABLEEVENTSREPLY, ENABLEEVENTSREPLY, ERRORPREFIX, EVENTREPLY, \
|
||||
HEARTBEATREPLY, IDENTREPLY, IDENTREQUEST, READREPLY, WRITEREPLY
|
||||
HEARTBEATREPLY, IDENTREPLY, IDENTREQUEST, READREPLY, WRITEREPLY, \
|
||||
LOGGING_REPLY, LOG_EVENT
|
||||
|
||||
|
||||
def make_update(modulename, pobj):
|
||||
@ -60,12 +61,11 @@ def make_update(modulename, pobj):
|
||||
|
||||
|
||||
class Dispatcher:
|
||||
|
||||
OMIT_UNCHANGED_WITHIN = 1 # do not send unchanged updates within 1 sec
|
||||
|
||||
def __init__(self, name, logger, options, srv):
|
||||
# to avoid errors, we want to eat all options here
|
||||
self.equipment_id = options.pop('id', name)
|
||||
# time interval for omitting updates of unchanged values
|
||||
self.omit_unchanged_within = options.pop('omit_unchanged_within', 0.1)
|
||||
self.nodeprops = {}
|
||||
for k in list(options):
|
||||
self.nodeprops[k] = options.pop(k)
|
||||
@ -83,6 +83,7 @@ class Dispatcher:
|
||||
# eventname is <modulename> or <modulename>:<parametername>
|
||||
self._subscriptions = {}
|
||||
self._lock = threading.RLock()
|
||||
self.name = name
|
||||
self.restart = srv.restart
|
||||
self.shutdown = srv.shutdown
|
||||
|
||||
@ -124,13 +125,21 @@ class Dispatcher:
|
||||
"""registers new connection"""
|
||||
self._connections.append(conn)
|
||||
|
||||
def reset_connection(self, conn):
|
||||
"""remove all subscriptions for a connection
|
||||
|
||||
to be called on the identification message
|
||||
"""
|
||||
for _evt, conns in list(self._subscriptions.items()):
|
||||
conns.discard(conn)
|
||||
self.set_all_log_levels(conn, 'off')
|
||||
self._active_connections.discard(conn)
|
||||
|
||||
def remove_connection(self, conn):
|
||||
"""removes now longer functional connection"""
|
||||
if conn in self._connections:
|
||||
self._connections.remove(conn)
|
||||
for _evt, conns in list(self._subscriptions.items()):
|
||||
conns.discard(conn)
|
||||
self._active_connections.discard(conn)
|
||||
self.reset_connection(conn)
|
||||
|
||||
def register_module(self, moduleobj, modulename, export=True):
|
||||
self.log.debug('registering module %r as %s (export=%r)' %
|
||||
@ -214,9 +223,14 @@ class Dispatcher:
|
||||
if cobj is None:
|
||||
raise NoSuchCommandError('Module %r has no command %r' % (modulename, cname or exportedname))
|
||||
|
||||
if cobj.argument:
|
||||
argument = cobj.argument.import_value(argument)
|
||||
# now call func
|
||||
# note: exceptions are handled in handle_request, not here!
|
||||
return cobj.do(moduleobj, argument), dict(t=currenttime())
|
||||
result = cobj.do(moduleobj, argument)
|
||||
if cobj.result:
|
||||
result = cobj.result.export_value(result)
|
||||
return result, dict(t=currenttime())
|
||||
|
||||
def _setParameterValue(self, modulename, exportedname, value):
|
||||
moduleobj = self.get_module(modulename)
|
||||
@ -236,13 +250,9 @@ class Dispatcher:
|
||||
|
||||
# validate!
|
||||
value = pobj.datatype(value)
|
||||
writefunc = getattr(moduleobj, 'write_%s' % pname, None)
|
||||
# note: exceptions are handled in handle_request, not here!
|
||||
if writefunc:
|
||||
# return value is ignored here, as it is automatically set on the pobj and broadcast
|
||||
writefunc(value)
|
||||
else:
|
||||
setattr(moduleobj, pname, value)
|
||||
getattr(moduleobj, 'write_' + pname)(value)
|
||||
# return value is ignored here, as already handled
|
||||
return pobj.export_value(), dict(t=pobj.timestamp) if pobj.timestamp else {}
|
||||
|
||||
def _getParameterValue(self, modulename, exportedname):
|
||||
@ -259,11 +269,9 @@ class Dispatcher:
|
||||
# raise ReadOnlyError('This parameter is constant and can not be accessed remotely.')
|
||||
return pobj.datatype.export_value(pobj.constant)
|
||||
|
||||
readfunc = getattr(moduleobj, 'read_%s' % pname, None)
|
||||
if readfunc:
|
||||
# should also update the pobj (via the setter from the metaclass)
|
||||
# note: exceptions are handled in handle_request, not here!
|
||||
readfunc()
|
||||
# note: exceptions are handled in handle_request, not here!
|
||||
getattr(moduleobj, 'read_' + pname)()
|
||||
# return value is ignored here, as already handled
|
||||
return pobj.export_value(), dict(t=pobj.timestamp) if pobj.timestamp else {}
|
||||
|
||||
#
|
||||
@ -298,6 +306,10 @@ class Dispatcher:
|
||||
self.log.error('should have been handled in the interface!')
|
||||
|
||||
def handle__ident(self, conn, specifier, data):
|
||||
# Remark: the following line is needed due to issue 66.
|
||||
self.reset_connection(conn)
|
||||
# The other stuff in issue 66 ('error_closed' message), has to be implemented
|
||||
# if and when frappy will support serial server connections
|
||||
return (IDENTREPLY, None, None)
|
||||
|
||||
def handle_describe(self, conn, specifier, data):
|
||||
@ -373,3 +385,19 @@ class Dispatcher:
|
||||
self._active_connections.discard(conn)
|
||||
# XXX: also check all entries in self._subscriptions?
|
||||
return (DISABLEEVENTSREPLY, None, None)
|
||||
|
||||
def send_log_msg(self, conn, modname, level, msg):
|
||||
"""send log message """
|
||||
conn.send_reply((LOG_EVENT, '%s:%s' % (modname, level), msg))
|
||||
|
||||
def set_all_log_levels(self, conn, level):
|
||||
for modobj in self._modules.values():
|
||||
modobj.setRemoteLogging(conn, level)
|
||||
|
||||
def handle_logging(self, conn, specifier, level):
|
||||
if specifier and specifier != '.':
|
||||
modobj = self._modules[specifier]
|
||||
modobj.setRemoteLogging(conn, level)
|
||||
else:
|
||||
self.set_all_log_levels(conn, level)
|
||||
return LOGGING_REPLY, specifier, level
|
||||
|
@ -202,3 +202,11 @@ class TCPServer(socketserver.ThreadingTCPServer):
|
||||
if ntry:
|
||||
self.log.warning('tried again %d times after "Address already in use"' % ntry)
|
||||
self.log.info("TCPServer initiated")
|
||||
|
||||
# py35 compatibility
|
||||
if not hasattr(socketserver.ThreadingTCPServer, '__exit__'):
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.server_close()
|
||||
|
@ -65,6 +65,13 @@ ERRORPREFIX = 'error_' # + specifier + json_extended_info(error_report)
|
||||
HELPREQUEST = 'help' # literal
|
||||
HELPREPLY = 'helping' # +line number +json_text
|
||||
|
||||
LOGGING_REQUEST = 'logging'
|
||||
LOGGING_REPLY = 'logging'
|
||||
# + [module] + json string (loglevel)
|
||||
|
||||
LOG_EVENT = 'log'
|
||||
# + [module:level] + json_string (message)
|
||||
|
||||
# helper mapping to find the REPLY for a REQUEST
|
||||
# do not put IDENTREQUEST/IDENTREPLY here, as this needs anyway extra treatment
|
||||
REQUEST2REPLY = {
|
||||
@ -77,6 +84,7 @@ REQUEST2REPLY = {
|
||||
READREQUEST: READREPLY,
|
||||
HEARTBEATREQUEST: HEARTBEATREPLY,
|
||||
HELPREQUEST: HELPREPLY,
|
||||
LOGGING_REQUEST: LOGGING_REPLY,
|
||||
}
|
||||
|
||||
|
||||
@ -89,6 +97,8 @@ HelpMessage = """Try one of the following:
|
||||
'%s <nonce>' to request a heartbeat response
|
||||
'%s' to activate async updates
|
||||
'%s' to deactivate updates
|
||||
'%s [<module>] <loglevel>' to activate logging events
|
||||
""" % (IDENTREQUEST, DESCRIPTIONREQUEST, READREQUEST,
|
||||
WRITEREQUEST, COMMANDREQUEST, HEARTBEATREQUEST,
|
||||
ENABLEEVENTSREQUEST, DISABLEEVENTSREQUEST)
|
||||
ENABLEEVENTSREQUEST, DISABLEEVENTSREQUEST,
|
||||
LOGGING_REQUEST)
|
||||
|
@ -23,23 +23,22 @@
|
||||
|
||||
from secop.client import SecopClient, decode_msg, encode_msg_frame
|
||||
from secop.datatypes import StringType
|
||||
from secop.errors import BadValueError, \
|
||||
CommunicationFailedError, ConfigError, make_secop_error
|
||||
from secop.errors import BadValueError, CommunicationFailedError, ConfigError
|
||||
from secop.lib import get_class
|
||||
from secop.modules import Drivable, Module, Readable, Writable
|
||||
from secop.params import Command, Parameter
|
||||
from secop.properties import Property
|
||||
from secop.io import HasIodev
|
||||
from secop.io import HasIO
|
||||
|
||||
|
||||
class ProxyModule(HasIodev, Module):
|
||||
class ProxyModule(HasIO, Module):
|
||||
module = Property('remote module name', datatype=StringType(), default='')
|
||||
|
||||
pollerClass = None
|
||||
_consistency_check_done = False
|
||||
_secnode = None
|
||||
enablePoll = False
|
||||
|
||||
def iodevClass(self, name, logger, opts, srv):
|
||||
def ioClass(self, name, logger, opts, srv):
|
||||
opts['description'] = 'secnode %s on %s' % (opts.get('module', name), opts['uri'])
|
||||
return SecNode(name, logger, opts, srv)
|
||||
|
||||
@ -47,14 +46,12 @@ class ProxyModule(HasIodev, Module):
|
||||
if parameter not in self.parameters:
|
||||
return # ignore unknown parameters
|
||||
# should be done here: deal with clock differences
|
||||
if readerror:
|
||||
readerror = make_secop_error(*readerror)
|
||||
self.announceUpdate(parameter, value, readerror, timestamp)
|
||||
|
||||
def initModule(self):
|
||||
if not self.module:
|
||||
self.module = self.name
|
||||
self._secnode = self._iodev.secnode
|
||||
self._secnode = self.io.secnode
|
||||
self._secnode.register_callback(self.module, self.updateEvent,
|
||||
self.descriptiveDataChange, self.nodeStateChange)
|
||||
super().initModule()
|
||||
@ -123,7 +120,8 @@ class ProxyModule(HasIodev, Module):
|
||||
self.announceUpdate('status', newstatus)
|
||||
|
||||
def checkProperties(self):
|
||||
pass # skip
|
||||
pass # skip
|
||||
|
||||
|
||||
class ProxyReadable(ProxyModule, Readable):
|
||||
pass
|
||||
@ -144,10 +142,12 @@ class SecNode(Module):
|
||||
uri = Property('uri of a SEC node', datatype=StringType())
|
||||
|
||||
def earlyInit(self):
|
||||
super().earlyInit()
|
||||
self.secnode = SecopClient(self.uri, self.log)
|
||||
|
||||
def startModule(self, started_callback):
|
||||
self.secnode.spawn_connect(started_callback)
|
||||
def startModule(self, start_events):
|
||||
super().startModule(start_events)
|
||||
self.secnode.spawn_connect(start_events.get_trigger())
|
||||
|
||||
@Command(StringType(), result=StringType())
|
||||
def request(self, msg):
|
||||
@ -182,7 +182,7 @@ def proxy_class(remote_class, name=None):
|
||||
|
||||
for aname, aobj in rcls.accessibles.items():
|
||||
if isinstance(aobj, Parameter):
|
||||
pobj = aobj.override(poll=False, handler=None, needscfg=False)
|
||||
pobj = aobj.merge(dict(handler=None, needscfg=False))
|
||||
attrs[aname] = pobj
|
||||
|
||||
def rfunc(self, pname=aname):
|
||||
@ -198,7 +198,7 @@ def proxy_class(remote_class, name=None):
|
||||
def wfunc(self, value, pname=aname):
|
||||
value, _, readerror = self._secnode.setParameter(self.name, pname, value)
|
||||
if readerror:
|
||||
raise make_secop_error(*readerror)
|
||||
raise readerror
|
||||
return value
|
||||
|
||||
attrs['write_' + aname] = wfunc
|
||||
@ -225,5 +225,5 @@ def Proxy(name, logger, cfgdict, srv):
|
||||
remote_class = cfgdict.pop('remote_class')
|
||||
if 'description' not in cfgdict:
|
||||
cfgdict['description'] = 'remote module %s on %s' % (
|
||||
cfgdict.get('module', name), cfgdict.get('iodev', '?'))
|
||||
cfgdict.get('module', name), cfgdict.get('io', '?'))
|
||||
return proxy_class(remote_class)(name, logger, cfgdict, srv)
|
||||
|
221
secop/rwhandler.py
Normal file
221
secop/rwhandler.py
Normal 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
|
@ -27,15 +27,14 @@ import ast
|
||||
import configparser
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
from collections import OrderedDict
|
||||
|
||||
from secop.errors import ConfigError, SECoPError
|
||||
from secop.lib import formatException, get_class, getGeneralConfig
|
||||
from secop.modules import Attached
|
||||
from secop.lib import formatException, get_class, generalConfig
|
||||
from secop.lib.multievent import MultiEvent
|
||||
from secop.params import PREDEFINED_ACCESSIBLES
|
||||
from secop.modules import Attached
|
||||
|
||||
try:
|
||||
from daemon import DaemonContext
|
||||
@ -89,7 +88,6 @@ class Server:
|
||||
...
|
||||
"""
|
||||
self._testonly = testonly
|
||||
cfg = getGeneralConfig()
|
||||
|
||||
self.log = parent_logger.getChild(name, True)
|
||||
if not cfgfiles:
|
||||
@ -114,22 +112,21 @@ class Server:
|
||||
if ambiguous_sections:
|
||||
self.log.warning('ambiguous sections in %s: %r' % (cfgfiles, tuple(ambiguous_sections)))
|
||||
self._cfgfiles = cfgfiles
|
||||
self._pidfile = os.path.join(cfg['piddir'], name + '.pid')
|
||||
self._pidfile = os.path.join(generalConfig.piddir, name + '.pid')
|
||||
|
||||
def loadCfgFile(self, cfgfile):
|
||||
if not cfgfile.endswith('.cfg'):
|
||||
cfgfile += '.cfg'
|
||||
cfg = getGeneralConfig()
|
||||
if os.sep in cfgfile: # specified as full path
|
||||
filename = cfgfile if os.path.exists(cfgfile) else None
|
||||
else:
|
||||
for filename in [os.path.join(d, cfgfile) for d in cfg['confdir'].split(os.pathsep)]:
|
||||
for filename in [os.path.join(d, cfgfile) for d in generalConfig.confdir.split(os.pathsep)]:
|
||||
if os.path.exists(filename):
|
||||
break
|
||||
else:
|
||||
filename = None
|
||||
if filename is None:
|
||||
raise ConfigError("Couldn't find cfg file %r in %s" % (cfgfile, cfg['confdir']))
|
||||
raise ConfigError("Couldn't find cfg file %r in %s" % (cfgfile, generalConfig.confdir))
|
||||
self.log.debug('Parse config file %s ...' % filename)
|
||||
result = OrderedDict()
|
||||
parser = configparser.ConfigParser()
|
||||
@ -268,36 +265,58 @@ class Server:
|
||||
failure_traceback = traceback.format_exc()
|
||||
errors.append('error creating %s' % modname)
|
||||
|
||||
poll_table = dict()
|
||||
missing_super = set()
|
||||
# all objs created, now start them up and interconnect
|
||||
for modname, modobj in self.modules.items():
|
||||
self.log.info('registering module %r' % modname)
|
||||
self.dispatcher.register_module(modobj, modname, modobj.export)
|
||||
if modobj.pollerClass is not None:
|
||||
# a module might be explicitly excluded from polling by setting pollerClass to None
|
||||
modobj.pollerClass.add_to_table(poll_table, modobj)
|
||||
# also call earlyInit on the modules
|
||||
modobj.earlyInit()
|
||||
if not modobj.earlyInitDone:
|
||||
missing_super.add('%s was not called, probably missing super call'
|
||||
% modobj.earlyInit.__qualname__)
|
||||
|
||||
# handle attached modules
|
||||
for modname, modobj in self.modules.items():
|
||||
attached_modules = {}
|
||||
for propname, propobj in modobj.propertyDict.items():
|
||||
if isinstance(propobj, Attached):
|
||||
try:
|
||||
setattr(modobj, propobj.attrname or '_' + propname,
|
||||
self.dispatcher.get_module(getattr(modobj, propname)))
|
||||
attname = getattr(modobj, propname)
|
||||
if attname: # attached module specified in cfg file
|
||||
attobj = self.dispatcher.get_module(attname)
|
||||
if isinstance(attobj, propobj.basecls):
|
||||
attached_modules[propname] = attobj
|
||||
else:
|
||||
errors.append('attached module %s=%r must inherit from %r'
|
||||
% (propname, attname, propobj.basecls.__qualname__))
|
||||
except SECoPError as e:
|
||||
errors.append('module %s, attached %s: %s' % (modname, propname, str(e)))
|
||||
modobj.attachedModules = attached_modules
|
||||
|
||||
# call init on each module after registering all
|
||||
for modname, modobj in self.modules.items():
|
||||
try:
|
||||
modobj.initModule()
|
||||
if not modobj.initModuleDone:
|
||||
missing_super.add('%s was not called, probably missing super call'
|
||||
% modobj.initModule.__qualname__)
|
||||
except Exception as e:
|
||||
if failure_traceback is None:
|
||||
failure_traceback = traceback.format_exc()
|
||||
errors.append('error initializing %s: %r' % (modname, e))
|
||||
|
||||
if not self._testonly:
|
||||
start_events = MultiEvent(default_timeout=30)
|
||||
for modname, modobj in self.modules.items():
|
||||
# startModule must return either a timeout value or None (default 30 sec)
|
||||
start_events.name = 'module %s' % modname
|
||||
modobj.startModule(start_events)
|
||||
if not modobj.startModuleDone:
|
||||
missing_super.add('%s was not called, probably missing super call'
|
||||
% modobj.startModule.__qualname__)
|
||||
errors.extend(missing_super)
|
||||
|
||||
if errors:
|
||||
for errtxt in errors:
|
||||
for line in errtxt.split('\n'):
|
||||
@ -311,22 +330,13 @@ class Server:
|
||||
|
||||
if self._testonly:
|
||||
return
|
||||
start_events = []
|
||||
for modname, modobj in self.modules.items():
|
||||
event = threading.Event()
|
||||
# startModule must return either a timeout value or None (default 30 sec)
|
||||
timeout = modobj.startModule(started_callback=event.set) or 30
|
||||
start_events.append((time.time() + timeout, 'module %s' % modname, event))
|
||||
for poller in poll_table.values():
|
||||
event = threading.Event()
|
||||
# poller.start must return either a timeout value or None (default 30 sec)
|
||||
timeout = poller.start(started_callback=event.set) or 30
|
||||
start_events.append((time.time() + timeout, repr(poller), event))
|
||||
self.log.info('waiting for modules and pollers being started')
|
||||
for deadline, name, event in sorted(start_events):
|
||||
if not event.wait(timeout=max(0, deadline - time.time())):
|
||||
self.log.info('WARNING: timeout when starting %s' % name)
|
||||
self.log.info('all modules and pollers started')
|
||||
self.log.info('waiting for modules being started')
|
||||
start_events.name = None
|
||||
if not start_events.wait():
|
||||
# some timeout happened
|
||||
for name in start_events.waiting_for():
|
||||
self.log.warning('timeout when starting %s' % name)
|
||||
self.log.info('all modules started')
|
||||
history_path = os.environ.get('FRAPPY_HISTORY')
|
||||
if history_path:
|
||||
from secop_psi.historywriter import FrappyHistoryWriter # pylint: disable=import-outside-toplevel
|
||||
|
@ -27,13 +27,10 @@ from time import sleep
|
||||
|
||||
from secop.datatypes import FloatRange
|
||||
from secop.lib import mkthread
|
||||
from secop.modules import BasicPoller, Drivable, \
|
||||
Module, Parameter, Readable, Writable, Command
|
||||
from secop.modules import Drivable, Module, Parameter, Readable, Writable, Command
|
||||
|
||||
|
||||
class SimBase:
|
||||
pollerClass = BasicPoller
|
||||
|
||||
def __new__(cls, devname, logger, cfgdict, dispatcher):
|
||||
extra_params = cfgdict.pop('extra_params', '') or cfgdict.pop('.extra_params', '')
|
||||
attrs = {}
|
||||
@ -60,6 +57,7 @@ class SimBase:
|
||||
return object.__new__(type('SimBase_%s' % devname, (cls,), attrs))
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
self._sim_thread = mkthread(self._sim)
|
||||
|
||||
def _sim(self):
|
||||
@ -119,7 +117,7 @@ class SimDrivable(SimReadable, Drivable):
|
||||
self._value = self.target
|
||||
speed *= self.interval
|
||||
try:
|
||||
self.pollParams(0)
|
||||
self.doPoll()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@ -132,7 +130,7 @@ class SimDrivable(SimReadable, Drivable):
|
||||
self._value = self.target
|
||||
sleep(self.interval)
|
||||
try:
|
||||
self.pollParams(0)
|
||||
self.doPoll()
|
||||
except Exception:
|
||||
pass
|
||||
self.status = self.Status.IDLE, ''
|
||||
|
@ -111,6 +111,7 @@ class Cryostat(CryoBase):
|
||||
group='stability')
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
self._stopflag = False
|
||||
self._thread = mkthread(self.thread)
|
||||
|
||||
|
@ -133,6 +133,7 @@ class MagneticField(Drivable):
|
||||
status = Parameter(datatype=TupleOf(EnumType(Status), StringType()))
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
self._state = Enum('state', idle=1, switch_on=2, switch_off=3, ramp=4).idle
|
||||
self._heatswitch = self.DISPATCHER.get_module(self.heatswitch)
|
||||
_thread = threading.Thread(target=self._thread)
|
||||
@ -235,6 +236,7 @@ class SampleTemp(Drivable):
|
||||
)
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
_thread = threading.Thread(target=self._thread)
|
||||
_thread.daemon = True
|
||||
_thread.start()
|
||||
|
@ -31,7 +31,7 @@ import math
|
||||
from secop.datatypes import ArrayOf, FloatRange, StringType, StructOf, TupleOf
|
||||
from secop.errors import ConfigError, DisabledError
|
||||
from secop.lib.sequence import SequencerMixin, Step
|
||||
from secop.modules import BasicPoller, Drivable, Parameter
|
||||
from secop.modules import Drivable, Parameter
|
||||
|
||||
|
||||
class GarfieldMagnet(SequencerMixin, Drivable):
|
||||
@ -47,9 +47,6 @@ class GarfieldMagnet(SequencerMixin, Drivable):
|
||||
the symmetry setting selects which.
|
||||
"""
|
||||
|
||||
pollerClass = BasicPoller
|
||||
|
||||
|
||||
# parameters
|
||||
subdev_currentsource = Parameter('(bipolar) Powersupply', datatype=StringType(), readonly=True, export=False)
|
||||
subdev_enable = Parameter('Switch to set for on/off', datatype=StringType(), readonly=True, export=False)
|
||||
@ -57,10 +54,10 @@ class GarfieldMagnet(SequencerMixin, Drivable):
|
||||
subdev_symmetry = Parameter('Switch to read for symmetry', datatype=StringType(), readonly=True, export=False)
|
||||
userlimits = Parameter('User defined limits of device value',
|
||||
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')),
|
||||
default=(float('-Inf'), float('+Inf')), readonly=False, poll=10)
|
||||
default=(float('-Inf'), float('+Inf')), readonly=False)
|
||||
abslimits = Parameter('Absolute limits of device value',
|
||||
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')),
|
||||
default=(-0.5, 0.5), poll=True,
|
||||
default=(-0.5, 0.5),
|
||||
)
|
||||
precision = Parameter('Precision of the device value (allowed deviation '
|
||||
'of stable values from target)',
|
||||
@ -71,7 +68,7 @@ class GarfieldMagnet(SequencerMixin, Drivable):
|
||||
calibration = Parameter('Coefficients for calibration '
|
||||
'function: [c0, c1, c2, c3, c4] calculates '
|
||||
'B(I) = c0*I + c1*erf(c2*I) + c3*atan(c4*I)'
|
||||
' in T', poll=1,
|
||||
' in T',
|
||||
datatype=ArrayOf(FloatRange(), 5, 5),
|
||||
default=(1.0, 0.0, 0.0, 0.0, 0.0))
|
||||
calibrationtable = Parameter('Map of Coefficients for calibration per symmetry setting',
|
||||
@ -137,7 +134,7 @@ class GarfieldMagnet(SequencerMixin, Drivable):
|
||||
'_current2field polynome not monotonic!')
|
||||
|
||||
def initModule(self):
|
||||
super(GarfieldMagnet, self).initModule()
|
||||
super().initModule()
|
||||
self._enable = self.DISPATCHER.get_module(self.subdev_enable)
|
||||
self._symmetry = self.DISPATCHER.get_module(self.subdev_symmetry)
|
||||
self._polswitch = self.DISPATCHER.get_module(self.subdev_polswitch)
|
||||
@ -220,7 +217,7 @@ class GarfieldMagnet(SequencerMixin, Drivable):
|
||||
self._currentsource.read_value() *
|
||||
self._get_field_polarity())
|
||||
|
||||
def read_hw_status(self):
|
||||
def readHwStatus(self):
|
||||
# called from SequencerMixin.read_status if no sequence is running
|
||||
if self._enable.value == 'Off':
|
||||
return self.Status.WARN, 'Disabled'
|
||||
|
@ -39,7 +39,7 @@ from secop.datatypes import ArrayOf, EnumType, FloatRange, \
|
||||
from secop.errors import CommunicationFailedError, \
|
||||
ConfigError, HardwareError, ProgrammingError
|
||||
from secop.lib import lazy_property
|
||||
from secop.modules import BasicPoller, Command, \
|
||||
from secop.modules import Command, \
|
||||
Drivable, Module, Parameter, Readable
|
||||
|
||||
#####
|
||||
@ -157,8 +157,6 @@ class PyTangoDevice(Module):
|
||||
execution and attribute operations with logging and exception mapping.
|
||||
"""
|
||||
|
||||
pollerClass = BasicPoller
|
||||
|
||||
# parameters
|
||||
comtries = Parameter('Maximum retries for communication',
|
||||
datatype=IntRange(1, 100), default=3, readonly=False,
|
||||
@ -210,7 +208,7 @@ class PyTangoDevice(Module):
|
||||
# exception mapping is enabled).
|
||||
self._createPyTangoDevice = self._applyGuardToFunc(
|
||||
self._createPyTangoDevice, 'constructor')
|
||||
super(PyTangoDevice, self).earlyInit()
|
||||
super().earlyInit()
|
||||
|
||||
@lazy_property
|
||||
def _dev(self):
|
||||
@ -249,10 +247,10 @@ class PyTangoDevice(Module):
|
||||
# otherwise would lead to attribute errors later
|
||||
try:
|
||||
device.State
|
||||
except AttributeError:
|
||||
except AttributeError as e:
|
||||
raise CommunicationFailedError(
|
||||
self, 'connection to Tango server failed, '
|
||||
'is the server running?')
|
||||
'is the server running?') from e
|
||||
return self._applyGuardsToPyTangoDevice(device)
|
||||
|
||||
def _applyGuardsToPyTangoDevice(self, dev):
|
||||
@ -376,14 +374,17 @@ class AnalogInput(PyTangoDevice, Readable):
|
||||
The AnalogInput handles all devices only delivering an analogue value.
|
||||
"""
|
||||
|
||||
def startModule(self, started_callback):
|
||||
super(AnalogInput, self).startModule(started_callback)
|
||||
# query unit from tango and update value property
|
||||
attrInfo = self._dev.attribute_query('value')
|
||||
# prefer configured unit if nothing is set on the Tango device, else
|
||||
# update
|
||||
if attrInfo.unit != 'No unit':
|
||||
self.accessibles['value'].datatype.setProperty('unit', attrInfo.unit)
|
||||
def startModule(self, start_events):
|
||||
super().startModule(start_events)
|
||||
try:
|
||||
# query unit from tango and update value property
|
||||
attrInfo = self._dev.attribute_query('value')
|
||||
# prefer configured unit if nothing is set on the Tango device, else
|
||||
# update
|
||||
if attrInfo.unit != 'No unit':
|
||||
self.accessibles['value'].datatype.setProperty('unit', attrInfo.unit)
|
||||
except Exception as e:
|
||||
self.log.error(e)
|
||||
|
||||
def read_value(self):
|
||||
return self._dev.value
|
||||
@ -422,7 +423,7 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
||||
userlimits = Parameter('User defined limits of device value',
|
||||
datatype=LimitsType(FloatRange(unit='$')),
|
||||
default=(float('-Inf'), float('+Inf')),
|
||||
readonly=False, poll=10,
|
||||
readonly=False,
|
||||
)
|
||||
abslimits = Parameter('Absolute limits of device value',
|
||||
datatype=LimitsType(FloatRange(unit='$')),
|
||||
@ -446,13 +447,13 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
||||
_moving = False
|
||||
|
||||
def initModule(self):
|
||||
super(AnalogOutput, self).initModule()
|
||||
super().initModule()
|
||||
# init history
|
||||
self._history = [] # will keep (timestamp, value) tuple
|
||||
self._timeout = None # keeps the time at which we will timeout, or None
|
||||
|
||||
def startModule(self, started_callback):
|
||||
super(AnalogOutput, self).startModule(started_callback)
|
||||
def startModule(self, start_events):
|
||||
super().startModule(start_events)
|
||||
# query unit from tango and update value property
|
||||
attrInfo = self._dev.attribute_query('value')
|
||||
# prefer configured unit if nothing is set on the Tango device, else
|
||||
@ -460,8 +461,8 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
||||
if attrInfo.unit != 'No unit':
|
||||
self.accessibles['value'].datatype.setProperty('unit', attrInfo.unit)
|
||||
|
||||
def pollParams(self, nr=0):
|
||||
super(AnalogOutput, self).pollParams(nr)
|
||||
def doPoll(self):
|
||||
super().doPoll()
|
||||
while len(self._history) > 2:
|
||||
# if history would be too short, break
|
||||
if self._history[-1][0] - self._history[1][0] <= self.window:
|
||||
@ -489,8 +490,11 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
||||
hist = self._history[:]
|
||||
window_start = currenttime() - self.window
|
||||
hist_in_window = [v for (t, v) in hist if t >= window_start]
|
||||
if len(hist) == len(hist_in_window):
|
||||
return False # no data point before window
|
||||
if not hist_in_window:
|
||||
return False # no relevant history -> no knowledge
|
||||
# window is too small -> use last point only
|
||||
hist_in_window = [self.value]
|
||||
|
||||
max_in_hist = max(hist_in_window)
|
||||
min_in_hist = min(hist_in_window)
|
||||
@ -503,13 +507,14 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
||||
if self._isAtTarget():
|
||||
self._timeout = None
|
||||
self._moving = False
|
||||
return super(AnalogOutput, self).read_status()
|
||||
if self._timeout:
|
||||
if self._timeout < currenttime():
|
||||
return self.Status.UNSTABLE, 'timeout after waiting for stable value'
|
||||
if self._moving:
|
||||
return (self.Status.BUSY, 'moving')
|
||||
return (self.Status.IDLE, 'stable')
|
||||
status = super().read_status()
|
||||
else:
|
||||
if self._timeout and self._timeout < currenttime():
|
||||
status = self.Status.UNSTABLE, 'timeout after waiting for stable value'
|
||||
else:
|
||||
status = (self.Status.BUSY, 'moving') if self._moving else (self.Status.IDLE, 'stable')
|
||||
self.setFastPoll(self.isBusy(status))
|
||||
return status
|
||||
|
||||
@property
|
||||
def absmin(self):
|
||||
@ -571,11 +576,14 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
||||
if not self.timeout:
|
||||
self._timeout = None
|
||||
self._moving = True
|
||||
self._history = [] # clear history
|
||||
self.read_status() # poll our status to keep it updated
|
||||
# do not clear the history here:
|
||||
# - if the target is not changed by more than precision, there is no need to wait
|
||||
# self._history = []
|
||||
self.read_status() # poll our status to keep it updated (this will also set fast poll)
|
||||
return self.read_target()
|
||||
|
||||
def _hw_wait(self):
|
||||
while super(AnalogOutput, self).read_status()[0] == self.Status.BUSY:
|
||||
while super().read_status()[0] == self.Status.BUSY:
|
||||
sleep(0.3)
|
||||
|
||||
def stop(self):
|
||||
@ -597,8 +605,7 @@ class Actuator(AnalogOutput):
|
||||
readonly=False, datatype=FloatRange(0, unit='$/s'),
|
||||
)
|
||||
ramp = Parameter('The speed of changing the value',
|
||||
readonly=False, datatype=FloatRange(0, unit='$/s'),
|
||||
poll=30,
|
||||
readonly=False, datatype=FloatRange(0, unit='$/min'),
|
||||
)
|
||||
|
||||
def read_speed(self):
|
||||
@ -677,17 +684,22 @@ class TemperatureController(Actuator):
|
||||
)
|
||||
pid = Parameter('pid control Parameters',
|
||||
datatype=TupleOf(FloatRange(), FloatRange(), FloatRange()),
|
||||
readonly=False, group='pid', poll=30,
|
||||
readonly=False, group='pid',
|
||||
)
|
||||
setpoint = Parameter('Current setpoint', datatype=FloatRange(unit='$'), poll=1,
|
||||
setpoint = Parameter('Current setpoint', datatype=FloatRange(unit='$'),
|
||||
)
|
||||
heateroutput = Parameter('Heater output', datatype=FloatRange(), poll=1,
|
||||
heateroutput = Parameter('Heater output', datatype=FloatRange(),
|
||||
)
|
||||
|
||||
# overrides
|
||||
precision = Parameter(default=0.1)
|
||||
ramp = Parameter(description='Temperature ramp')
|
||||
|
||||
def doPoll(self):
|
||||
super().doPoll()
|
||||
self.read_setpoint()
|
||||
self.read_heateroutput()
|
||||
|
||||
def read_ramp(self):
|
||||
return self._dev.ramp
|
||||
|
||||
@ -730,6 +742,10 @@ class TemperatureController(Actuator):
|
||||
def read_heateroutput(self):
|
||||
return self._dev.heaterOutput
|
||||
|
||||
# remove UserCommand setposition from Actuator
|
||||
# (makes no sense for a TemperatureController)
|
||||
setposition = None
|
||||
|
||||
|
||||
class PowerSupply(Actuator):
|
||||
"""A power supply (voltage and current) device.
|
||||
@ -737,13 +753,19 @@ class PowerSupply(Actuator):
|
||||
|
||||
# parameters
|
||||
voltage = Parameter('Actual voltage',
|
||||
datatype=FloatRange(unit='V'), poll=-5)
|
||||
datatype=FloatRange(unit='V'))
|
||||
current = Parameter('Actual current',
|
||||
datatype=FloatRange(unit='A'), poll=-5)
|
||||
datatype=FloatRange(unit='A'))
|
||||
|
||||
# overrides
|
||||
ramp = Parameter(description='Current/voltage ramp')
|
||||
|
||||
def doPoll(self):
|
||||
super().doPoll()
|
||||
# TODO: poll voltage and current faster when busy
|
||||
self.read_voltage()
|
||||
self.read_current()
|
||||
|
||||
def read_ramp(self):
|
||||
return self._dev.ramp
|
||||
|
||||
@ -777,16 +799,18 @@ class NamedDigitalInput(DigitalInput):
|
||||
datatype=StringType(), export=False) # XXX:!!!
|
||||
|
||||
def initModule(self):
|
||||
super(NamedDigitalInput, self).initModule()
|
||||
super().initModule()
|
||||
try:
|
||||
# pylint: disable=eval-used
|
||||
mapping = eval(self.mapping.replace('\n', ' '))
|
||||
mapping = self.mapping
|
||||
if isinstance(mapping, str):
|
||||
# pylint: disable=eval-used
|
||||
mapping = eval(self.mapping.replace('\n', ' '))
|
||||
if isinstance(mapping, str):
|
||||
# pylint: disable=eval-used
|
||||
mapping = eval(mapping)
|
||||
self.accessibles['value'].setProperty('datatype', EnumType('value', **mapping))
|
||||
except Exception as e:
|
||||
raise ValueError('Illegal Value for mapping: %r' % e)
|
||||
raise ValueError('Illegal Value for mapping: %r' % self.mapping) from e
|
||||
|
||||
def read_value(self):
|
||||
value = self._dev.value
|
||||
@ -805,7 +829,7 @@ class PartialDigitalInput(NamedDigitalInput):
|
||||
datatype=IntRange(0), default=1)
|
||||
|
||||
def initModule(self):
|
||||
super(PartialDigitalInput, self).initModule()
|
||||
super().initModule()
|
||||
self._mask = (1 << self.bitwidth) - 1
|
||||
# self.accessibles['value'].datatype = IntRange(0, self._mask)
|
||||
|
||||
@ -827,9 +851,16 @@ class DigitalOutput(PyTangoDevice, Drivable):
|
||||
def read_value(self):
|
||||
return self._dev.value # mapping is done by datatype upon export()
|
||||
|
||||
def read_status(self):
|
||||
status = self.read_status()
|
||||
self.setFastPoll(self.isBusy(status))
|
||||
return status
|
||||
|
||||
def write_target(self, value):
|
||||
self._dev.value = value
|
||||
self.read_value()
|
||||
self.read_status() # this will also set fast poll
|
||||
return self.read_target()
|
||||
|
||||
def read_target(self):
|
||||
attrObj = self._dev.read_attribute('value')
|
||||
@ -845,22 +876,25 @@ class NamedDigitalOutput(DigitalOutput):
|
||||
datatype=StringType(), export=False)
|
||||
|
||||
def initModule(self):
|
||||
super(NamedDigitalOutput, self).initModule()
|
||||
super().initModule()
|
||||
try:
|
||||
# pylint: disable=eval-used
|
||||
mapping = eval(self.mapping.replace('\n', ' '))
|
||||
mapping = self.mapping
|
||||
if isinstance(mapping, str):
|
||||
# pylint: disable=eval-used
|
||||
mapping = eval(self.mapping.replace('\n', ' '))
|
||||
if isinstance(mapping, str):
|
||||
# pylint: disable=eval-used
|
||||
mapping = eval(mapping)
|
||||
self.accessibles['value'].setProperty('datatype', EnumType('value', **mapping))
|
||||
self.accessibles['target'].setProperty('datatype', EnumType('target', **mapping))
|
||||
except Exception as e:
|
||||
raise ValueError('Illegal Value for mapping: %r' % e)
|
||||
raise ValueError('Illegal Value for mapping: %r' % self.mapping) from e
|
||||
|
||||
def write_target(self, value):
|
||||
# map from enum-str to integer value
|
||||
self._dev.value = int(value)
|
||||
self.read_value()
|
||||
return self.read_target()
|
||||
|
||||
|
||||
class PartialDigitalOutput(NamedDigitalOutput):
|
||||
@ -875,7 +909,7 @@ class PartialDigitalOutput(NamedDigitalOutput):
|
||||
datatype=IntRange(0), default=1)
|
||||
|
||||
def initModule(self):
|
||||
super(PartialDigitalOutput, self).initModule()
|
||||
super().initModule()
|
||||
self._mask = (1 << self.bitwidth) - 1
|
||||
# self.accessibles['value'].datatype = IntRange(0, self._mask)
|
||||
# self.accessibles['target'].datatype = IntRange(0, self._mask)
|
||||
@ -891,6 +925,7 @@ class PartialDigitalOutput(NamedDigitalOutput):
|
||||
(value << self.startbit)
|
||||
self._dev.value = newvalue
|
||||
self.read_value()
|
||||
return self.read_target()
|
||||
|
||||
|
||||
class StringIO(PyTangoDevice, Module):
|
||||
|
@ -20,7 +20,7 @@
|
||||
# *****************************************************************************
|
||||
"""Andeen Hagerling capacitance bridge"""
|
||||
|
||||
from secop.core import Done, FloatRange, HasIodev, Parameter, Readable, StringIO
|
||||
from secop.core import Done, FloatRange, HasIO, Parameter, Readable, StringIO, nopoll
|
||||
|
||||
|
||||
class Ah2700IO(StringIO):
|
||||
@ -28,19 +28,19 @@ class Ah2700IO(StringIO):
|
||||
timeout = 5
|
||||
|
||||
|
||||
class Capacitance(HasIodev, Readable):
|
||||
class Capacitance(HasIO, Readable):
|
||||
|
||||
value = Parameter('capacitance', FloatRange(unit='pF'), poll=True)
|
||||
value = Parameter('capacitance', FloatRange(unit='pF'))
|
||||
freq = Parameter('frequency', FloatRange(unit='Hz'), readonly=False, default=0)
|
||||
voltage = Parameter('voltage', FloatRange(unit='V'), readonly=False, default=0)
|
||||
loss = Parameter('loss', FloatRange(unit='deg'), default=0)
|
||||
|
||||
iodevClass = Ah2700IO
|
||||
ioClass = Ah2700IO
|
||||
|
||||
def parse_reply(self, reply):
|
||||
if reply.startswith('SI'): # this is an echo
|
||||
self.sendRecv('SERIAL ECHO OFF')
|
||||
reply = self.sendRecv('SI')
|
||||
self.communicate('SERIAL ECHO OFF')
|
||||
reply = self.communicate('SI')
|
||||
if not reply.startswith('F='): # this is probably an error message like "LOSS TOO HIGH"
|
||||
self.status = [self.Status.ERROR, reply]
|
||||
return
|
||||
@ -59,32 +59,35 @@ class Capacitance(HasIodev, Readable):
|
||||
if lossunit == 'DS':
|
||||
self.loss = loss
|
||||
else: # the unit was wrong, we want DS = tan(delta), not NS = nanoSiemens
|
||||
reply = self.sendRecv('UN DS').split() # UN DS returns a reply similar to SI
|
||||
reply = self.communicate('UN DS').split() # UN DS returns a reply similar to SI
|
||||
try:
|
||||
self.loss = reply[7]
|
||||
except IndexError:
|
||||
pass # don't worry, loss will be updated next time
|
||||
|
||||
def read_value(self):
|
||||
self.parse_reply(self.sendRecv('SI')) # SI = single trigger
|
||||
self.parse_reply(self.communicate('SI')) # SI = single trigger
|
||||
return Done
|
||||
|
||||
@nopoll
|
||||
def read_freq(self):
|
||||
self.read_value()
|
||||
return Done
|
||||
|
||||
@nopoll
|
||||
def read_loss(self):
|
||||
self.read_value()
|
||||
return Done
|
||||
|
||||
def read_volt(self):
|
||||
@nopoll
|
||||
def read_voltage(self):
|
||||
self.read_value()
|
||||
return Done
|
||||
|
||||
def write_freq(self, value):
|
||||
self.parse_reply(self.sendRecv('FR %g;SI' % value))
|
||||
self.parse_reply(self.communicate('FR %g;SI' % value))
|
||||
return Done
|
||||
|
||||
def write_volt(self, value):
|
||||
self.parse_reply(self.sendRecv('V %g;SI' % value))
|
||||
def write_voltage(self, value):
|
||||
self.parse_reply(self.communicate('V %g;SI' % value))
|
||||
return Done
|
||||
|
@ -23,7 +23,7 @@
|
||||
"""drivers for CCU4, the cryostat control unit at SINQ"""
|
||||
# the most common Frappy classes can be imported from secop.core
|
||||
from secop.core import EnumType, FloatRange, \
|
||||
HasIodev, Parameter, Readable, StringIO
|
||||
HasIO, Parameter, Readable, StringIO
|
||||
|
||||
|
||||
class CCU4IO(StringIO):
|
||||
@ -34,14 +34,13 @@ class CCU4IO(StringIO):
|
||||
identification = [('cid', r'CCU4.*')]
|
||||
|
||||
|
||||
# inheriting the HasIodev mixin creates us a private attribute *_iodev*
|
||||
# for talking with the hardware
|
||||
# inheriting HasIO allows us to use the communicate method for talking with the hardware
|
||||
# Readable as a base class defines the value and status parameters
|
||||
class HeLevel(HasIodev, Readable):
|
||||
class HeLevel(HasIO, Readable):
|
||||
"""He Level channel of CCU4"""
|
||||
|
||||
# define the communication class to create the IO module
|
||||
iodevClass = CCU4IO
|
||||
ioClass = CCU4IO
|
||||
|
||||
# define or alter the parameters
|
||||
# as Readable.value exists already, we give only the modified property 'unit'
|
||||
@ -71,9 +70,9 @@ class HeLevel(HasIodev, Readable):
|
||||
for changing a parameter
|
||||
:returns: the (new) value of the parameter
|
||||
"""
|
||||
name, txtvalue = self._iodev.communicate(cmd).split('=')
|
||||
name, txtvalue = self.communicate(cmd).split('=')
|
||||
assert name == cmd.split('=')[0] # check that we got a reply to our command
|
||||
return txtvalue # Frappy will automatically convert the string to the needed data type
|
||||
return float(txtvalue)
|
||||
|
||||
def read_value(self):
|
||||
return self.query('h')
|
||||
|
159
secop_psi/convergence.py
Normal file
159
secop_psi/convergence.py
Normal 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()
|
@ -45,12 +45,7 @@ def make_cvt_list(dt, tail=''):
|
||||
result = []
|
||||
for subkey, elmtype in items:
|
||||
for fun, tail_, opts in make_cvt_list(elmtype, '%s.%s' % (tail, subkey)):
|
||||
def conv(value, key=subkey, func=fun):
|
||||
try:
|
||||
return value[key]
|
||||
except KeyError: # can not use value.get() because value might be a list
|
||||
return None
|
||||
result.append((conv, tail_, opts))
|
||||
result.append((lambda v, k=subkey, f=fun: f(v[k]), tail_, opts))
|
||||
return result
|
||||
|
||||
|
||||
@ -70,11 +65,11 @@ class FrappyHistoryWriter(frappyhistory.FrappyWriter):
|
||||
- period:
|
||||
the typical 'lifetime' of a value.
|
||||
The intention is, that points in a chart may be connected by a straight line
|
||||
when the distance is lower than twice this value. If not, the line should be
|
||||
drawn horizontally from the last point to a point <period> before the next value.
|
||||
when the distance is lower than this value. If not, the line should be drawn
|
||||
horizontally from the last point to a point <period> before the next value.
|
||||
For example a setpoint should have period 0, which will lead to a stepped
|
||||
line, whereas for a measured value like a temperature, period should be
|
||||
equal to the poll interval. In order to make full use of this,
|
||||
slightly bigger than the poll interval. In order to make full use of this,
|
||||
we would need some additional parameter property.
|
||||
- show: True/False, whether this curve should be shown or not by default in
|
||||
a summary chart
|
||||
|
@ -18,12 +18,18 @@
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
# *****************************************************************************
|
||||
"""Keithley 2601B source meter
|
||||
"""Keithley 2601B 4 quadrant source meter
|
||||
|
||||
not tested yet"""
|
||||
not tested yet
|
||||
|
||||
from secop.core import Attached, BoolType, EnumType, FloatRange, \
|
||||
HasIodev, Module, Parameter, StringIO, Writable, Done
|
||||
* switching between voltage and current happens by setting their target
|
||||
* switching output off by setting the active parameter of the controlling
|
||||
module to False.
|
||||
* setting the active parameter to True raises an error
|
||||
"""
|
||||
|
||||
from secop.core import Attached, BoolType, Done, EnumType, FloatRange, \
|
||||
HasIO, Module, Parameter, Readable, StringIO, Writable
|
||||
|
||||
|
||||
class K2601bIO(StringIO):
|
||||
@ -32,128 +38,162 @@ class K2601bIO(StringIO):
|
||||
|
||||
SOURCECMDS = {
|
||||
0: 'reset()'
|
||||
' smua.source.output = 0 print("ok")',
|
||||
1: 'reset()'
|
||||
' smua.source.func = smua.OUTPUT_DCAMPS'
|
||||
' display.smua.measure.func = display.MEASURE_VOLTS'
|
||||
' smua.source.autorangei = 1'
|
||||
' smua.source.output = %d print("ok"")',
|
||||
1: 'reset()'
|
||||
' smua.source.output = 1 print("ok")',
|
||||
2: 'reset()'
|
||||
' smua.source.func = smua.OUTPUT_DCVOLTS'
|
||||
' display.smua.measure.func = display.MEASURE_DCAMPS'
|
||||
' smua.source.autorangev = 1'
|
||||
' smua.source.output = %d print("ok"")',
|
||||
' smua.source.output = 1 print("ok")',
|
||||
}
|
||||
|
||||
|
||||
class SourceMeter(HasIodev, Module):
|
||||
class SourceMeter(HasIO, Module):
|
||||
export = False # export for tests only
|
||||
mode = Parameter('measurement mode', EnumType(off=0, current=1, voltage=2),
|
||||
readonly=False, export=False)
|
||||
ilimit = Parameter('current limit', FloatRange(0, 2.0, unit='A'), default=2)
|
||||
vlimit = Parameter('voltage limit', FloatRange(0, 2.0, unit='V'), default=2)
|
||||
|
||||
resistivity = Parameter('readback resistivity', FloatRange(unit='Ohm'), poll=True)
|
||||
power = Parameter('readback power', FloatRange(unit='W'), poll=True)
|
||||
mode = Parameter('measurement mode', EnumType(current_off=0, voltage_off=1, current_on=2, voltage_on=3),
|
||||
readonly=False, poll=True)
|
||||
|
||||
iodevClass = K2601bIO
|
||||
|
||||
def read_resistivity(self):
|
||||
return self.sendRecv('print(smua.measure.r())')
|
||||
|
||||
def read_power(self):
|
||||
return self.sendRecv('print(smua.measure.p())')
|
||||
ioClass = K2601bIO
|
||||
|
||||
def read_mode(self):
|
||||
return float(self.sendRecv('print(smua.source.func+2*smua.source.output)'))
|
||||
return float(self.communicate('print((smua.source.func+1)*smua.source.output)'))
|
||||
|
||||
def write_mode(self, value):
|
||||
assert 'ok' == self.sendRecv(SOURCECMDS[value % 2] % (value >= 2))
|
||||
if value == 'current':
|
||||
self.write_vlimit(self.vlimit)
|
||||
elif value == 'voltage':
|
||||
self.write_ilimit(self.ilimit)
|
||||
assert self.communicate(SOURCECMDS[value]) == 'ok'
|
||||
return self.read_mode()
|
||||
|
||||
def read_ilimit(self):
|
||||
if self.mode == 'current':
|
||||
return self.ilimit
|
||||
return float(self.communicate('print(smua.source.limiti)'))
|
||||
|
||||
class Current(HasIodev, Writable):
|
||||
sourcemeter = Attached()
|
||||
def write_ilimit(self, value):
|
||||
if self.mode == 'current':
|
||||
return self.ilimit
|
||||
return float(self.communicate('smua.source.limiti = %g print(smua.source.limiti)' % value))
|
||||
|
||||
value = Parameter('measured current', FloatRange(unit='A'), poll=True)
|
||||
target = Parameter('set current', FloatRange(unit='A'), poll=True)
|
||||
active = Parameter('current is controlled', BoolType(), default=False) # polled by SourceMeter
|
||||
limit = Parameter('current limit', FloatRange(0, 2.0, unit='A'), default=2, poll=True)
|
||||
def read_vlimit(self):
|
||||
if self.mode == 'voltage':
|
||||
return self.ilimit
|
||||
return float(self.communicate('print(smua.source.limitv)'))
|
||||
|
||||
def initModule(self):
|
||||
self._sourcemeter.registerCallbacks(self)
|
||||
def write_vlimit(self, value):
|
||||
if self.mode == 'voltage':
|
||||
return self.ilimit
|
||||
return float(self.communicate('smua.source.limitv = %g print(smua.source.limitv)' % value))
|
||||
|
||||
|
||||
class Power(HasIO, Readable):
|
||||
value = Parameter('readback power', FloatRange(unit='W'))
|
||||
ioClass = K2601bIO
|
||||
|
||||
def read_value(self):
|
||||
return self.sendRecv('print(smua.measure.i())')
|
||||
return float(self.communicate('print(smua.measure.p())'))
|
||||
|
||||
|
||||
class Resistivity(HasIO, Readable):
|
||||
value = Parameter('readback resistivity', FloatRange(unit='Ohm'))
|
||||
ioClass = K2601bIO
|
||||
|
||||
def read_value(self):
|
||||
return float(self.communicate('print(smua.measure.r())'))
|
||||
|
||||
|
||||
class Current(HasIO, Writable):
|
||||
sourcemeter = Attached()
|
||||
|
||||
value = Parameter('measured current', FloatRange(unit='A'))
|
||||
target = Parameter('set current', FloatRange(unit='A'))
|
||||
active = Parameter('current is controlled', BoolType(), default=False)
|
||||
limit = Parameter('current limit', FloatRange(0, 2.0, unit='A'), default=2)
|
||||
|
||||
def initModule(self):
|
||||
self.sourcemeter.registerCallbacks(self)
|
||||
|
||||
def read_value(self):
|
||||
return float(self.communicate('print(smua.measure.i())'))
|
||||
|
||||
def read_target(self):
|
||||
return self.sendRecv('print(smua.source.leveli)')
|
||||
return float(self.communicate('print(smua.source.leveli)'))
|
||||
|
||||
def write_target(self, value):
|
||||
if not self.active:
|
||||
raise ValueError('current source is disabled')
|
||||
if value > self.limit:
|
||||
if value > self.sourcemeter.ilimit:
|
||||
raise ValueError('current exceeds limit')
|
||||
return self.sendRecv('smua.source.leveli = %g print(smua.source.leveli)' % value)
|
||||
value = float(self.communicate('smua.source.leveli = %g print(smua.source.leveli)' % value))
|
||||
if not self.active:
|
||||
self.sourcemeter.write_mode('current') # triggers update_mode -> set active to True
|
||||
return value
|
||||
|
||||
def read_limit(self):
|
||||
if self.active:
|
||||
return self.limit
|
||||
return self.sendRecv('print(smua.source.limiti)')
|
||||
return self.sourcemeter.read_ilimit()
|
||||
|
||||
def write_limit(self, value):
|
||||
if self.active:
|
||||
return value
|
||||
return self.sendRecv('smua.source.limiti = %g print(smua.source.limiti)' % value)
|
||||
|
||||
def write_active(self, value):
|
||||
if value:
|
||||
self._sourcemeter.write_mode('current_on')
|
||||
elif self._sourcemeter.mode == 'current_on':
|
||||
self._sourcemeter.write_mode('current_off')
|
||||
return self.active
|
||||
return self.sourcemeter.write_ilimit(value)
|
||||
|
||||
def update_mode(self, mode):
|
||||
# will be called whenever the attached sourcemeters mode changes
|
||||
self.active = mode == 'current_on'
|
||||
self.active = mode == 'current'
|
||||
|
||||
def write_active(self, value):
|
||||
self.sourcemeter.read_mode()
|
||||
if value == self.value:
|
||||
return Done
|
||||
if value:
|
||||
raise ValueError('activate only by setting target')
|
||||
self.sourcemeter.write_mode('off') # triggers update_mode -> set active to False
|
||||
return Done
|
||||
|
||||
|
||||
class Voltage(HasIodev, Writable):
|
||||
class Voltage(HasIO, Writable):
|
||||
sourcemeter = Attached()
|
||||
|
||||
value = Parameter('measured voltage', FloatRange(unit='V'), poll=True)
|
||||
target = Parameter('set voltage', FloatRange(unit='V'), poll=True)
|
||||
active = Parameter('voltage is controlled', BoolType(), default=False) # polled by SourceMeter
|
||||
limit = Parameter('current limit', FloatRange(0, 2.0, unit='V'), default=2, poll=True)
|
||||
value = Parameter('measured voltage', FloatRange(unit='V'))
|
||||
target = Parameter('set voltage', FloatRange(unit='V'))
|
||||
active = Parameter('voltage is controlled', BoolType())
|
||||
limit = Parameter('voltage limit', FloatRange(0, 2.0, unit='V'), default=2)
|
||||
|
||||
def initModule(self):
|
||||
self._sourcemeter.registerCallbacks(self)
|
||||
self.sourcemeter.registerCallbacks(self)
|
||||
|
||||
def read_value(self):
|
||||
return self.sendRecv('print(smua.measure.v())')
|
||||
return float(self.communicate('print(smua.measure.v())'))
|
||||
|
||||
def read_target(self):
|
||||
return self.sendRecv('print(smua.source.levelv)')
|
||||
return float(self.communicate('print(smua.source.levelv)'))
|
||||
|
||||
def write_target(self, value):
|
||||
if not self.active:
|
||||
raise ValueError('voltage source is disabled')
|
||||
if value > self.limit:
|
||||
if value > self.sourcemeter.vlimit:
|
||||
raise ValueError('voltage exceeds limit')
|
||||
return self.sendRecv('smua.source.levelv = %g print(smua.source.levelv)' % value)
|
||||
value = float(self.communicate('smua.source.levelv = %g print(smua.source.levelv)' % value))
|
||||
if not self.active:
|
||||
self.sourcemeter.write_mode('voltage') # triggers update_mode -> set active to True
|
||||
return value
|
||||
|
||||
def read_limit(self):
|
||||
if self.active:
|
||||
return self.limit
|
||||
return self.sendRecv('print(smua.source.limitv)')
|
||||
return self.sourcemeter.read_vlimit()
|
||||
|
||||
def write_limit(self, value):
|
||||
if self.active:
|
||||
return value
|
||||
return self.sendRecv('smua.source.limitv = %g print(smua.source.limitv)' % value)
|
||||
|
||||
def write_active(self, value):
|
||||
if value:
|
||||
self._sourcemeter.write_mode('voltage_on')
|
||||
elif self._sourcemeter.mode == 'voltage_on':
|
||||
self._sourcemeter.write_mode('voltage_off')
|
||||
return self.active
|
||||
return self.sourcemeter.write_vlimit(value)
|
||||
|
||||
def update_mode(self, mode):
|
||||
# will be called whenever the attached sourcemeters mode changes
|
||||
self.active = mode == 'voltage_on'
|
||||
self.active = mode == 'voltage'
|
||||
|
||||
def write_active(self, value):
|
||||
self.sourcemeter.read_mode()
|
||||
if value == self.value:
|
||||
return Done
|
||||
if value:
|
||||
raise ValueError('activate only by setting target')
|
||||
self.sourcemeter.write_mode('off') # triggers update_mode -> set active to False
|
||||
return Done
|
||||
|
@ -27,8 +27,7 @@ from secop.datatypes import BoolType, EnumType, FloatRange, IntRange
|
||||
from secop.lib import formatStatusBits
|
||||
from secop.modules import Attached, Done, \
|
||||
Drivable, Parameter, Property, Readable
|
||||
from secop.poller import REGULAR, Poller
|
||||
from secop.io import HasIodev
|
||||
from secop.io import HasIO
|
||||
|
||||
Status = Drivable.Status
|
||||
|
||||
@ -58,29 +57,29 @@ class StringIO(secop.io.StringIO):
|
||||
wait_before = 0.05
|
||||
|
||||
|
||||
class Main(HasIodev, Drivable):
|
||||
class Main(HasIO, Drivable):
|
||||
|
||||
value = Parameter('the current channel', poll=REGULAR, datatype=IntRange(0, 17))
|
||||
value = Parameter('the current channel', datatype=IntRange(0, 17))
|
||||
target = Parameter('channel to select', datatype=IntRange(0, 17))
|
||||
autoscan = Parameter('whether to scan automatically', datatype=BoolType(), readonly=False, default=False)
|
||||
pollinterval = Parameter(default=1, export=False)
|
||||
|
||||
pollerClass = Poller
|
||||
iodevClass = StringIO
|
||||
ioClass = StringIO
|
||||
_channel_changed = 0 # time of last channel change
|
||||
_channels = None # dict <channel no> of <module object>
|
||||
|
||||
def earlyInit(self):
|
||||
super().earlyInit()
|
||||
self._channels = {}
|
||||
|
||||
def register_channel(self, modobj):
|
||||
self._channels[modobj.channel] = modobj
|
||||
|
||||
def startModule(self, started_callback):
|
||||
started_callback()
|
||||
def startModule(self, start_events):
|
||||
super().startModule(start_events)
|
||||
for ch in range(1, 16):
|
||||
if ch not in self._channels:
|
||||
self.sendRecv('INSET %d,0,0,0,0,0;INSET?%d' % (ch, ch))
|
||||
self.communicate('INSET %d,0,0,0,0,0;INSET?%d' % (ch, ch))
|
||||
|
||||
def read_value(self):
|
||||
channel, auto = scan.send_command(self)
|
||||
@ -113,7 +112,7 @@ class Main(HasIodev, Drivable):
|
||||
|
||||
def write_target(self, channel):
|
||||
scan.send_change(self, channel, self.autoscan)
|
||||
# self.sendRecv('SCAN %d,%d;SCAN?' % (channel, self.autoscan))
|
||||
# self.communicate('SCAN %d,%d;SCAN?' % (channel, self.autoscan))
|
||||
if channel != self.value:
|
||||
self.value = 0
|
||||
self._channel_changed = time.time()
|
||||
@ -122,11 +121,11 @@ class Main(HasIodev, Drivable):
|
||||
|
||||
def write_autoscan(self, value):
|
||||
scan.send_change(self, self.value, value)
|
||||
# self.sendRecv('SCAN %d,%d;SCAN?' % (channel, self.autoscan))
|
||||
# self.communicate('SCAN %d,%d;SCAN?' % (channel, self.autoscan))
|
||||
return value
|
||||
|
||||
|
||||
class ResChannel(HasIodev, Readable):
|
||||
class ResChannel(HasIO, Readable):
|
||||
"""temperature channel on Lakeshore 336"""
|
||||
|
||||
RES_RANGE = {key: i+1 for i, key in list(
|
||||
@ -140,8 +139,7 @@ class ResChannel(HasIodev, Readable):
|
||||
enumerate(mag % val for mag in ['%guV', '%gmV']
|
||||
for val in [2, 6.32, 20, 63.2, 200, 632]))}
|
||||
|
||||
pollerClass = Poller
|
||||
iodevClass = StringIO
|
||||
ioClass = StringIO
|
||||
_main = None # main module
|
||||
_last_range_change = 0 # time of last range change
|
||||
|
||||
@ -166,6 +164,7 @@ class ResChannel(HasIodev, Readable):
|
||||
_trigger_read = False
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
self._main = self.DISPATCHER.get_module(self.main)
|
||||
self._main.register_channel(self)
|
||||
|
||||
@ -181,7 +180,7 @@ class ResChannel(HasIodev, Readable):
|
||||
return Done
|
||||
# we got here, when we missed the idle state of self._main
|
||||
self._trigger_read = False
|
||||
result = self.sendRecv('RDGR?%d' % self.channel)
|
||||
result = self.communicate('RDGR?%d' % self.channel)
|
||||
result = float(result)
|
||||
if self.autorange == 'soft':
|
||||
now = time.time()
|
||||
@ -214,9 +213,9 @@ class ResChannel(HasIodev, Readable):
|
||||
def read_status(self):
|
||||
if not self.enabled:
|
||||
return [self.Status.DISABLED, 'disabled']
|
||||
if self.channel != self._main.value:
|
||||
if self.channel != self.main.value:
|
||||
return Done
|
||||
result = int(self.sendRecv('RDGST?%d' % self.channel))
|
||||
result = int(self.communicate('RDGST?%d' % self.channel))
|
||||
result &= 0x37 # mask T_OVER and T_UNDER (change this when implementing temperatures instead of resistivities)
|
||||
statustext = ' '.join(formatStatusBits(result, STATUS_BIT_LABELS))
|
||||
if statustext:
|
||||
@ -228,7 +227,7 @@ class ResChannel(HasIodev, Readable):
|
||||
if autorange:
|
||||
result['autorange'] = 'hard'
|
||||
# else: do not change autorange
|
||||
# self.log.info('%s range %r %r %r' % (self.name, rng, autorange, self.autorange))
|
||||
self.log.debug('%s range %r %r %r' % (self.name, rng, autorange, self.autorange))
|
||||
if excoff:
|
||||
result.update(iexc=0, vexc=0)
|
||||
elif iscur:
|
||||
@ -281,5 +280,5 @@ class ResChannel(HasIodev, Readable):
|
||||
def write_enabled(self, value):
|
||||
inset.write(self, 'enabled', value)
|
||||
if value:
|
||||
self._main.write_target(self.channel)
|
||||
self.main.write_target(self.channel)
|
||||
return Done
|
||||
|
@ -28,22 +28,23 @@ class Ls370Sim(Communicator):
|
||||
('RDGR?%d', '1.0'),
|
||||
('RDGST?%d', '0'),
|
||||
('RDGRNG?%d', '0,5,5,0,0'),
|
||||
('INSET?%d', '1,3,3,0,0'),
|
||||
('FILTER?%d', '1,1,80'),
|
||||
('INSET?%d', '1,5,5,0,0'),
|
||||
('FILTER?%d', '1,5,80'),
|
||||
]
|
||||
OTHER_COMMANDS = [
|
||||
('*IDN?', 'LSCI,MODEL370,370184,05302003'),
|
||||
('SCAN?', '3,0'),
|
||||
('SCAN?', '3,1'),
|
||||
]
|
||||
|
||||
def earlyInit(self):
|
||||
super().earlyInit()
|
||||
self._data = dict(self.OTHER_COMMANDS)
|
||||
for fmt, v in self.CHANNEL_COMMANDS:
|
||||
for chan in range(1,17):
|
||||
self._data[fmt % chan] = v
|
||||
# mkthread(self.run)
|
||||
|
||||
def communicate(self, command):
|
||||
self.comLog('> %s' % command)
|
||||
# simulation part, time independent
|
||||
for channel in range(1,17):
|
||||
_, _, _, _, excoff = self._data['RDGRNG?%d' % channel].split(',')
|
||||
@ -68,6 +69,6 @@ class Ls370Sim(Communicator):
|
||||
if qcmd in self._data:
|
||||
self._data[qcmd] = arg
|
||||
break
|
||||
#if command.startswith('R'):
|
||||
# print('> %s\t< %s' % (command, reply))
|
||||
return ';'.join(reply)
|
||||
reply = ';'.join(reply)
|
||||
self.comLog('< %s' % reply)
|
||||
return reply
|
||||
|
@ -27,8 +27,9 @@ import time
|
||||
|
||||
from secop.core import Drivable, HasIodev, \
|
||||
Parameter, Property, Readable, StringIO
|
||||
from secop.datatypes import EnumType, FloatRange, StringType
|
||||
from secop.datatypes import EnumType, FloatRange, StringType, StructOf
|
||||
from secop.errors import HardwareError
|
||||
from secop.lib.statemachine import StateMachine
|
||||
|
||||
|
||||
class MercuryIO(StringIO):
|
||||
@ -120,7 +121,7 @@ class HasProgressCheck:
|
||||
changing tolerance.
|
||||
"""
|
||||
tolerance = Parameter('absolute tolerance', FloatRange(0), readonly=False, default=0)
|
||||
relative_tolerance = Parameter('_', FloatRange(0, 1), readonly=False, default=0)
|
||||
min_slope = Parameter('minimal abs(slope)', FloatRange(0), readonly=False, default=0)
|
||||
settling_time = Parameter(
|
||||
'''settling time
|
||||
|
||||
@ -130,75 +131,76 @@ class HasProgressCheck:
|
||||
'''timeout
|
||||
|
||||
timeout = 0: disabled, else:
|
||||
A timeout happens, when the difference value - target is not improved by more than
|
||||
a factor 2 within timeout.
|
||||
|
||||
More precisely, we expect a convergence curve which decreases the difference
|
||||
by a factor 2 within timeout/2.
|
||||
If this expected progress is delayed by more than timeout/2, a timeout happens.
|
||||
If the convergence is better than above, the expected curve is adjusted continuously.
|
||||
In case the tolerance is reached once, a timeout happens when the time after this is
|
||||
exceeded by more than settling_time + timeout.
|
||||
A timeout event happens, when the difference (target - value) is not improved by
|
||||
at least min_slope * timeout over any interval (t, t + timeout).
|
||||
As soon as the value is the first time within tolerance, the criterium is changed:
|
||||
then the timeout event happens after this time + settling_time + timeout.
|
||||
''', FloatRange(0, unit='sec'), readonly=False, default=3600)
|
||||
status = Parameter('status determined from progress check')
|
||||
value = Parameter()
|
||||
target = Parameter()
|
||||
|
||||
_settling_start = None # supposed start of settling time (0 when outside)
|
||||
_first_inside = None # first time within tolerance
|
||||
_spent_inside = 0 # accumulated settling time
|
||||
# the upper limit for t0, for the curve timeout_dif * 2 ** -(t - t0)/timeout not touching abs(value(t) - target)
|
||||
_timeout_base = 0
|
||||
_timeout_dif = 1
|
||||
def earlyInit(self):
|
||||
super().earlyInit()
|
||||
self.__state = StateMachine()
|
||||
|
||||
def check_progress(self, value, target):
|
||||
"""called from read_status
|
||||
def prepare_state(self, state):
|
||||
tol = self.tolerance
|
||||
if not tol:
|
||||
tol = 0.01 * max(abs(self.target), abs(self.value))
|
||||
dif = abs(self.target - self.value)
|
||||
return dif, tol, state.now, state.delta(0)
|
||||
|
||||
intended to be also be used for alternative implementations of read_status
|
||||
"""
|
||||
base = max(abs(target), abs(value))
|
||||
tol = base * self.relative_tolerance + self.tolerance
|
||||
if tol == 0:
|
||||
tol = max(0.01, base * 0.01)
|
||||
now = time.time()
|
||||
dif = abs(value - target)
|
||||
if self._settling_start: # we were inside tol
|
||||
self._spent_inside = now - self._settling_start
|
||||
if dif > tol: # transition inside -> outside
|
||||
self._settling_start = None
|
||||
else: # we were outside tol
|
||||
if dif <= tol: # transition outside -> inside
|
||||
if not self._first_inside:
|
||||
self._first_inside = now
|
||||
self._settling_start = now - self._spent_inside
|
||||
if self._spent_inside > self.settling_time:
|
||||
return 'IDLE', ''
|
||||
result = 'BUSY', ('inside tolerance' if self._settling_start else 'outside tolerance')
|
||||
if self.timeout:
|
||||
if self._first_inside:
|
||||
if now > self._first_inside + self.settling_time + self.timeout:
|
||||
return 'WARNING', 'settling timeout'
|
||||
return result
|
||||
tmo2 = self.timeout / 2
|
||||
def state_approaching(self, state):
|
||||
if self.init():
|
||||
self.status = 'BUSY', 'approaching'
|
||||
dif, tol, now, delta = self.prepare_state(state)
|
||||
if dif < tol:
|
||||
state.timeout_base = now
|
||||
state.next_step(self.state_inside)
|
||||
return
|
||||
if not self.timeout:
|
||||
return
|
||||
if state.init():
|
||||
state.timeout_base = now
|
||||
state.dif_crit = dif
|
||||
return
|
||||
min_slope = getattr(self, 'ramp', 0) or getattr('min_slope', 0)
|
||||
state.dif_crit -= min_slope * delta
|
||||
if dif < state.dif_crit:
|
||||
state.timeout_base = now
|
||||
elif now > state.timeout_base:
|
||||
self.status = 'WARNING', 'convergence timeout'
|
||||
state.next_action(self.state_idle)
|
||||
|
||||
def exponential_convergence(t):
|
||||
return self._timeout_dif * 2 ** -(t - self._timeout_base) / tmo2
|
||||
def state_inside(self, state):
|
||||
if state.init():
|
||||
self.status = 'BUSY', 'inside tolerance'
|
||||
dif, tol, now, delta = self.prepare_state(state)
|
||||
if dif > tol:
|
||||
state.next_action(self.state_outside)
|
||||
state.spent_inside += delta
|
||||
if state.spent_inside > self.settling_time:
|
||||
self.status = 'IDLE', 'reached target'
|
||||
state.next_action(self.state_idle)
|
||||
|
||||
if dif < exponential_convergence(now):
|
||||
# convergence is better than estimated, update expected curve
|
||||
self._timeout_dif = dif
|
||||
self._timeout_base = now
|
||||
elif dif > exponential_convergence(now - tmo2):
|
||||
return 'WARNING', 'convergence timeout'
|
||||
return result
|
||||
def state_outside(self, state, now, dif, tol, delta):
|
||||
if state.init():
|
||||
self.status = 'BUSY', 'outside tolerance'
|
||||
dif, tol, now, delta = self.prepare_state(state)
|
||||
if dif < tol:
|
||||
state.next_action(self.state_inside)
|
||||
elif now > self.timeout_base + self.settling_time + self.timeout:
|
||||
self.status = 'WARNING', 'settling timeout'
|
||||
state.next_action(self.state_idle)
|
||||
|
||||
def reset_progress(self, value, target):
|
||||
def start_state(self):
|
||||
"""must be called from write_target, whenever the target changes"""
|
||||
self._settling_start = None
|
||||
self._first_inside = None
|
||||
self._spent_inside = 0
|
||||
self._timeout_base = time.time()
|
||||
self._timeout_dif = abs(value - target)
|
||||
self.__state.start(self.state_approach)
|
||||
|
||||
def poll(self):
|
||||
super().poll()
|
||||
self.__state.poll()
|
||||
|
||||
def read_status(self):
|
||||
if self.status[0] == 'IDLE':
|
||||
@ -213,13 +215,15 @@ class HasProgressCheck:
|
||||
class Loop(HasProgressCheck, MercuryChannel):
|
||||
"""common base class for loops"""
|
||||
mode = Parameter('control mode', EnumType(manual=0, pid=1), readonly=False)
|
||||
prop = Parameter('pid proportional band', FloatRange(), readonly=False)
|
||||
integ = Parameter('pid integral parameter', FloatRange(unit='min'), readonly=False)
|
||||
deriv = Parameter('pid differential parameter', FloatRange(unit='min'), readonly=False)
|
||||
ctrlpars = Parameter(
|
||||
'pid (proportional nad, integral time, differential time',
|
||||
StructOf(p=FloatRange(0, unit='$'), i=FloatRange(0, unit='min'), d=FloatRange(0, unit='min')),
|
||||
readonly=False, poll=True
|
||||
)
|
||||
"""pid = Parameter('control parameters', StructOf(p=FloatRange(), i=FloatRange(), d=FloatRange()),readonly=False)"""
|
||||
pid_table_mode = Parameter('', EnumType(off=0, on=1), readonly=False)
|
||||
|
||||
def read_prop(self):
|
||||
def read_ctrlpars(self):
|
||||
return self.query('0:LOOP:P')
|
||||
|
||||
def read_integ(self):
|
||||
|
@ -148,6 +148,7 @@ class Motor(PersistentMixin, HasIodev, Drivable):
|
||||
|
||||
@Command
|
||||
def reset(self):
|
||||
"""reset error, set position to encoder"""
|
||||
self.read_value()
|
||||
if self.status[0] == self.Status.ERROR:
|
||||
enc = self.encoder - self.zero
|
||||
|
@ -33,17 +33,17 @@ Polling of value and status is done commonly for all modules. For each registere
|
||||
|
||||
import threading
|
||||
import time
|
||||
from ast import literal_eval # convert string as comma separated numbers into tuple
|
||||
|
||||
import secop.iohandler
|
||||
from secop.datatypes import BoolType, EnumType, \
|
||||
FloatRange, IntRange, StatusType, StringType
|
||||
from secop.errors import HardwareError
|
||||
from secop.lib import clamp
|
||||
from secop.lib.enum import Enum
|
||||
from secop.modules import Attached, Communicator, Done, \
|
||||
from secop.modules import Communicator, Done, \
|
||||
Drivable, Parameter, Property, Readable
|
||||
from secop.poller import Poller
|
||||
from secop.io import HasIodev
|
||||
from secop.io import HasIO
|
||||
from secop.rwhandler import CommonReadHandler, CommonWriteHandler
|
||||
|
||||
try:
|
||||
import secop_psi.ppmswindows as ppmshw
|
||||
@ -52,28 +52,11 @@ except ImportError:
|
||||
import secop_psi.ppmssim as ppmshw
|
||||
|
||||
|
||||
class IOHandler(secop.iohandler.IOHandler):
|
||||
"""IO handler for PPMS commands
|
||||
|
||||
deals with typical format:
|
||||
|
||||
- query command: ``<command>?``
|
||||
- reply: ``<value1>,<value2>, ..``
|
||||
- change command: ``<command> <value1>,<value2>,...``
|
||||
"""
|
||||
CMDARGS = ['no'] # the channel number is needed in channel commands
|
||||
CMDSEPARATOR = None # no command chaining
|
||||
|
||||
def __init__(self, name, querycmd, replyfmt):
|
||||
changecmd = querycmd.split('?')[0] + ' '
|
||||
super().__init__(name, querycmd, replyfmt, changecmd)
|
||||
|
||||
|
||||
class Main(Communicator):
|
||||
"""ppms communicator module"""
|
||||
|
||||
pollinterval = Parameter('poll interval', FloatRange(), readonly=False, default=2)
|
||||
data = Parameter('internal', StringType(), poll=True, export=True, # export for test only
|
||||
data = Parameter('internal', StringType(), export=True, # export for test only
|
||||
default="", readonly=True)
|
||||
|
||||
class_id = Property('Quantum Design class id', StringType(), export=False)
|
||||
@ -86,9 +69,8 @@ class Main(Communicator):
|
||||
_channel_to_index = dict(((channel, i) for i, channel in enumerate(_channel_names)))
|
||||
_status_bitpos = {'temp': 0, 'field': 4, 'chamber': 8, 'position': 12}
|
||||
|
||||
pollerClass = Poller
|
||||
|
||||
def earlyInit(self):
|
||||
super().earlyInit()
|
||||
self.modules = {}
|
||||
self._ppms_device = ppmshw.QDevice(self.class_id)
|
||||
self.lock = threading.Lock()
|
||||
@ -99,10 +81,14 @@ class Main(Communicator):
|
||||
def communicate(self, command):
|
||||
"""GPIB command"""
|
||||
with self.lock:
|
||||
self.comLog('> %s' % command)
|
||||
reply = self._ppms_device.send(command)
|
||||
self.log.debug("%s|%s", command, reply)
|
||||
self.comLog("< %s", reply)
|
||||
return reply
|
||||
|
||||
def doPoll(self):
|
||||
self.read_data()
|
||||
|
||||
def read_data(self):
|
||||
mask = 1 # always get packed_status
|
||||
for channelname, channel in self.modules.items():
|
||||
@ -128,37 +114,27 @@ class Main(Communicator):
|
||||
return data # return data as string
|
||||
|
||||
|
||||
class PpmsMixin:
|
||||
class PpmsBase(HasIO, Readable):
|
||||
"""common base for all ppms modules"""
|
||||
value = Parameter(needscfg=False)
|
||||
status = Parameter(needscfg=False)
|
||||
|
||||
iodev = Attached()
|
||||
|
||||
pollerClass = Poller
|
||||
enabled = True # default, if no parameter enable is defined
|
||||
_last_settings = None # used by several modules
|
||||
slow_pollfactor = 1
|
||||
|
||||
# as this pollinterval affects only the polling of settings
|
||||
# it would be confusing to export it.
|
||||
pollinterval = Parameter('', FloatRange(), needscfg=False, export=False)
|
||||
pollinterval = Parameter(export=False)
|
||||
|
||||
def initModule(self):
|
||||
self._iodev.register(self)
|
||||
super().initModule()
|
||||
self.io.register(self)
|
||||
|
||||
def startModule(self, started_callback):
|
||||
# no polls except on main module
|
||||
started_callback()
|
||||
|
||||
def read_value(self):
|
||||
def doPoll(self):
|
||||
# polling is done by the main module
|
||||
# and PPMS does not deliver really more fresh values when polled more often
|
||||
return Done
|
||||
|
||||
def read_status(self):
|
||||
# polling is done by the main module
|
||||
# and PPMS does not deliver really fresh status values anyway: the status is not
|
||||
# changed immediately after a target change!
|
||||
return Done
|
||||
pass
|
||||
|
||||
def update_value_status(self, value, packed_status):
|
||||
# update value and status
|
||||
@ -172,12 +148,18 @@ class PpmsMixin:
|
||||
self.value = value
|
||||
self.status = (self.Status.IDLE, '')
|
||||
|
||||
def comm_write(self, command):
|
||||
"""write command and check if result is OK"""
|
||||
reply = self.communicate(command)
|
||||
if reply != 'OK':
|
||||
raise HardwareError('bad reply %r to command %r' % (reply, command))
|
||||
|
||||
class Channel(PpmsMixin, HasIodev, Readable):
|
||||
|
||||
class Channel(PpmsBase):
|
||||
"""channel base class"""
|
||||
|
||||
value = Parameter('main value of channels', poll=True)
|
||||
enabled = Parameter('is this channel used?', readonly=False, poll=False,
|
||||
value = Parameter('main value of channels')
|
||||
enabled = Parameter('is this channel used?', readonly=False,
|
||||
datatype=BoolType(), default=False)
|
||||
|
||||
channel = Property('channel name',
|
||||
@ -186,26 +168,21 @@ class Channel(PpmsMixin, HasIodev, Readable):
|
||||
datatype=IntRange(1, 4), export=False)
|
||||
|
||||
def earlyInit(self):
|
||||
Readable.earlyInit(self)
|
||||
super().earlyInit()
|
||||
if not self.channel:
|
||||
self.channel = self.name
|
||||
|
||||
def get_settings(self, pname):
|
||||
return ''
|
||||
|
||||
|
||||
class UserChannel(Channel):
|
||||
"""user channel"""
|
||||
|
||||
# pollinterval = Parameter(visibility=3)
|
||||
|
||||
no = Property('channel number',
|
||||
datatype=IntRange(0, 0), export=False, default=0)
|
||||
linkenable = Property('name of linked channel for enabling',
|
||||
datatype=StringType(), export=False, default='')
|
||||
|
||||
def write_enabled(self, enabled):
|
||||
other = self._iodev.modules.get(self.linkenable, None)
|
||||
other = self.io.modules.get(self.linkenable, None)
|
||||
if other:
|
||||
other.enabled = enabled
|
||||
return enabled
|
||||
@ -214,201 +191,172 @@ class UserChannel(Channel):
|
||||
class DriverChannel(Channel):
|
||||
"""driver channel"""
|
||||
|
||||
drvout = IOHandler('drvout', 'DRVOUT? %(no)d', '%d,%g,%g')
|
||||
|
||||
current = Parameter('driver current', readonly=False, handler=drvout,
|
||||
current = Parameter('driver current', readonly=False,
|
||||
datatype=FloatRange(0., 5000., unit='uA'))
|
||||
powerlimit = Parameter('power limit', readonly=False, handler=drvout,
|
||||
powerlimit = Parameter('power limit', readonly=False,
|
||||
datatype=FloatRange(0., 1000., unit='uW'))
|
||||
# pollinterval = Parameter(visibility=3)
|
||||
|
||||
def analyze_drvout(self, no, current, powerlimit):
|
||||
param_names = 'current', 'powerlimit'
|
||||
|
||||
@CommonReadHandler(param_names)
|
||||
def read_params(self):
|
||||
no, self.current, self.powerlimit = literal_eval(
|
||||
self.communicate('DRVOUT? %d' % self.no))
|
||||
if self.no != no:
|
||||
raise HardwareError('DRVOUT command: channel number in reply does not match')
|
||||
return dict(current=current, powerlimit=powerlimit)
|
||||
|
||||
def change_drvout(self, change):
|
||||
change.readValues()
|
||||
return change.current, change.powerlimit
|
||||
@CommonWriteHandler(param_names)
|
||||
def write_params(self, values):
|
||||
"""write parameters
|
||||
|
||||
:param values: a dict like object containing the parameters to be written
|
||||
"""
|
||||
self.read_params() # make sure parameters are up to date
|
||||
self.comm_write('DRVOUT %(no)d,%(current)g,%(powerlimit)g' % values)
|
||||
self.read_params() # read back
|
||||
|
||||
|
||||
class BridgeChannel(Channel):
|
||||
"""bridge channel"""
|
||||
|
||||
bridge = IOHandler('bridge', 'BRIDGE? %(no)d', '%d,%g,%g,%d,%d,%g')
|
||||
# pylint: disable=invalid-name
|
||||
ReadingMode = Enum('ReadingMode', standard=0, fast=1, highres=2)
|
||||
|
||||
enabled = Parameter(handler=bridge)
|
||||
excitation = Parameter('excitation current', readonly=False, handler=bridge,
|
||||
excitation = Parameter('excitation current', readonly=False,
|
||||
datatype=FloatRange(0.01, 5000., unit='uA'))
|
||||
powerlimit = Parameter('power limit', readonly=False, handler=bridge,
|
||||
powerlimit = Parameter('power limit', readonly=False,
|
||||
datatype=FloatRange(0.001, 1000., unit='uW'))
|
||||
dcflag = Parameter('True when excitation is DC (else AC)', readonly=False, handler=bridge,
|
||||
dcflag = Parameter('True when excitation is DC (else AC)', readonly=False,
|
||||
datatype=BoolType())
|
||||
readingmode = Parameter('reading mode', readonly=False, handler=bridge,
|
||||
datatype=EnumType(ReadingMode))
|
||||
voltagelimit = Parameter('voltage limit', readonly=False, handler=bridge,
|
||||
readingmode = Parameter('reading mode', readonly=False,
|
||||
datatype=EnumType(standard=0, fast=1, highres=2))
|
||||
voltagelimit = Parameter('voltage limit', readonly=False,
|
||||
datatype=FloatRange(0.0001, 100., unit='mV'))
|
||||
# pollinterval = Parameter(visibility=3)
|
||||
|
||||
def analyze_bridge(self, no, excitation, powerlimit, dcflag, readingmode, voltagelimit):
|
||||
param_names = 'enabled', 'enabled', 'powerlimit', 'dcflag', 'readingmode', 'voltagelimit'
|
||||
|
||||
@CommonReadHandler(param_names)
|
||||
def read_params(self):
|
||||
no, excitation, powerlimit, self.dcflag, self.readingmode, voltagelimit = literal_eval(
|
||||
self.communicate('BRIDGE? %d' % self.no))
|
||||
if self.no != no:
|
||||
raise HardwareError('DRVOUT command: channel number in reply does not match')
|
||||
return dict(
|
||||
enabled=excitation != 0 and powerlimit != 0 and voltagelimit != 0,
|
||||
excitation=excitation or self.excitation,
|
||||
powerlimit=powerlimit or self.powerlimit,
|
||||
dcflag=dcflag,
|
||||
readingmode=readingmode,
|
||||
voltagelimit=voltagelimit or self.voltagelimit,
|
||||
)
|
||||
self.enabled = excitation != 0 and powerlimit != 0 and voltagelimit != 0
|
||||
if excitation:
|
||||
self.excitation = excitation
|
||||
if powerlimit:
|
||||
self.powerlimit = powerlimit
|
||||
if voltagelimit:
|
||||
self.voltagelimit = voltagelimit
|
||||
|
||||
def change_bridge(self, change):
|
||||
change.readValues()
|
||||
if change.enabled:
|
||||
return self.no, change.excitation, change.powerlimit, change.dcflag, change.readingmode, change.voltagelimit
|
||||
return self.no, 0, 0, change.dcflag, change.readingmode, 0
|
||||
@CommonWriteHandler(param_names)
|
||||
def write_params(self, values):
|
||||
"""write parameters
|
||||
|
||||
:param values: a dict like object containing the parameters to be written
|
||||
"""
|
||||
self.read_params() # make sure parameters are up to date
|
||||
if not values['enabled']:
|
||||
values['excitation'] = 0
|
||||
values['powerlimit'] = 0
|
||||
values['voltagelimit'] = 0
|
||||
self.comm_write('BRIDGE %(no)d,%(enabled)g,%(powerlimit)g,%(dcflag)d,'
|
||||
'%(readingmode)d,%(voltagelimit)g' % values)
|
||||
self.read_params() # read back
|
||||
|
||||
|
||||
class Level(PpmsMixin, HasIodev, Readable):
|
||||
class Level(PpmsBase):
|
||||
"""helium level"""
|
||||
|
||||
level = IOHandler('level', 'LEVEL?', '%g,%d')
|
||||
|
||||
value = Parameter(datatype=FloatRange(unit='%'), handler=level)
|
||||
status = Parameter(handler=level)
|
||||
# pollinterval = Parameter(visibility=3)
|
||||
value = Parameter(datatype=FloatRange(unit='%'))
|
||||
|
||||
channel = 'level'
|
||||
|
||||
def doPoll(self):
|
||||
self.read_value()
|
||||
|
||||
def update_value_status(self, value, packed_status):
|
||||
pass
|
||||
# must be a no-op
|
||||
# when called from Main.read_data, value is always None
|
||||
# value and status is polled via settings
|
||||
|
||||
def analyze_level(self, level, status):
|
||||
def read_value(self):
|
||||
# ignore 'old reading' state of the flag, as this happens only for a short time
|
||||
# during measuring
|
||||
return dict(value=level, status=(self.Status.IDLE, ''))
|
||||
return literal_eval(self.communicate('LEVEL?'))[0]
|
||||
|
||||
|
||||
class Chamber(PpmsMixin, HasIodev, Drivable):
|
||||
class Chamber(PpmsBase, Drivable):
|
||||
"""sample chamber handling
|
||||
|
||||
value is an Enum, which is redundant with the status text
|
||||
"""
|
||||
|
||||
chamber = IOHandler('chamber', 'CHAMBER?', '%d')
|
||||
Status = Drivable.Status
|
||||
# pylint: disable=invalid-name
|
||||
Operation = Enum(
|
||||
'Operation',
|
||||
seal_immediately=0,
|
||||
purge_and_seal=1,
|
||||
vent_and_seal=2,
|
||||
pump_continuously=3,
|
||||
vent_continuously=4,
|
||||
hi_vacuum=5,
|
||||
noop=10,
|
||||
)
|
||||
StatusCode = Enum(
|
||||
'StatusCode',
|
||||
unknown=0,
|
||||
purged_and_sealed=1,
|
||||
vented_and_sealed=2,
|
||||
sealed_unknown=3,
|
||||
purge_and_seal=4,
|
||||
vent_and_seal=5,
|
||||
pumping_down=6,
|
||||
at_hi_vacuum=7,
|
||||
pumping_continuously=8,
|
||||
venting_continuously=9,
|
||||
general_failure=15,
|
||||
)
|
||||
|
||||
value = Parameter(description='chamber state', handler=chamber,
|
||||
datatype=EnumType(StatusCode))
|
||||
target = Parameter(description='chamber command', handler=chamber,
|
||||
datatype=EnumType(Operation))
|
||||
# pollinterval = Parameter(visibility=3)
|
||||
|
||||
STATUS_MAP = {
|
||||
StatusCode.purged_and_sealed: (Status.IDLE, 'purged and sealed'),
|
||||
StatusCode.vented_and_sealed: (Status.IDLE, 'vented and sealed'),
|
||||
StatusCode.sealed_unknown: (Status.WARN, 'sealed unknown'),
|
||||
StatusCode.purge_and_seal: (Status.BUSY, 'purge and seal'),
|
||||
StatusCode.vent_and_seal: (Status.BUSY, 'vent and seal'),
|
||||
StatusCode.pumping_down: (Status.BUSY, 'pumping down'),
|
||||
StatusCode.at_hi_vacuum: (Status.IDLE, 'at hi vacuum'),
|
||||
StatusCode.pumping_continuously: (Status.IDLE, 'pumping continuously'),
|
||||
StatusCode.venting_continuously: (Status.IDLE, 'venting continuously'),
|
||||
StatusCode.general_failure: (Status.ERROR, 'general failure'),
|
||||
}
|
||||
code_table = [
|
||||
# valuecode, status, statusname, opcode, targetname
|
||||
(0, Status.IDLE, 'unknown', 10, 'noop'),
|
||||
(1, Status.IDLE, 'purged_and_sealed', 1, 'purge_and_seal'),
|
||||
(2, Status.IDLE, 'vented_and_sealed', 2, 'vent_and_seal'),
|
||||
(3, Status.WARN, 'sealed_unknown', 0, 'seal_immediately'),
|
||||
(4, Status.BUSY, 'purge_and_seal', None, None),
|
||||
(5, Status.BUSY, 'vent_and_seal', None, None),
|
||||
(6, Status.BUSY, 'pumping_down', None, None),
|
||||
(8, Status.IDLE, 'pumping_continuously', 3, 'pump_continuously'),
|
||||
(9, Status.IDLE, 'venting_continuously', 4, 'vent_continuously'),
|
||||
(15, Status.ERROR, 'general_failure', None, None),
|
||||
]
|
||||
value_codes = {k: v for v, _, k, _, _ in code_table}
|
||||
target_codes = {k: v for v, _, _, _, k in code_table if k}
|
||||
name2opcode = {k: v for _, _, _, v, k in code_table if k}
|
||||
opcode2name = {v: k for _, _, _, v, k in code_table if k}
|
||||
status_map = {v: (c, k.replace('_', ' ')) for v, c, k, _, _ in code_table}
|
||||
value = Parameter(description='chamber state', datatype=EnumType(**value_codes), default=0)
|
||||
target = Parameter(description='chamber command', datatype=EnumType(**target_codes), default='noop')
|
||||
|
||||
channel = 'chamber'
|
||||
|
||||
def update_value_status(self, value, packed_status):
|
||||
status_code = (packed_status >> 8) & 0xf
|
||||
if status_code in self.STATUS_MAP:
|
||||
if status_code in self.status_map:
|
||||
self.value = status_code
|
||||
self.status = self.STATUS_MAP[status_code]
|
||||
self.status = self.status_map[status_code]
|
||||
else:
|
||||
self.value = self.StatusCode.unknown
|
||||
self.value = self.value_map['unknown']
|
||||
self.status = (self.Status.ERROR, 'unknown status code %d' % status_code)
|
||||
|
||||
def analyze_chamber(self, target):
|
||||
return dict(target=target)
|
||||
def read_target(self):
|
||||
opcode = int(self.communicate('CHAMBER?'))
|
||||
return self.opcode2name[opcode]
|
||||
|
||||
def change_chamber(self, change):
|
||||
# write settings, combining <pname>=<value> and current attributes
|
||||
# and request updated settings
|
||||
if change.target == self.Operation.noop:
|
||||
return None
|
||||
return (change.target,)
|
||||
def write_target(self, value):
|
||||
if value == self.target.noop:
|
||||
return self.target.noop
|
||||
opcode = self.name2opcode[self.target.enum(value).name]
|
||||
assert self.communicate('CHAMBER %d' % opcode) == 'OK'
|
||||
return self.read_target()
|
||||
|
||||
|
||||
class Temp(PpmsMixin, HasIodev, Drivable):
|
||||
class Temp(PpmsBase, Drivable):
|
||||
"""temperature"""
|
||||
|
||||
temp = IOHandler('temp', 'TEMP?', '%g,%g,%d')
|
||||
Status = Enum(
|
||||
Drivable.Status,
|
||||
RAMPING=370,
|
||||
STABILIZING=380,
|
||||
)
|
||||
# pylint: disable=invalid-name
|
||||
ApproachMode = Enum('ApproachMode', fast_settle=0, no_overshoot=1)
|
||||
|
||||
value = Parameter(datatype=FloatRange(unit='K'), poll=True)
|
||||
status = Parameter(datatype=StatusType(Status), poll=True)
|
||||
target = Parameter(datatype=FloatRange(1.7, 402.0, unit='K'), poll=False, needscfg=False)
|
||||
value = Parameter(datatype=FloatRange(unit='K'))
|
||||
status = Parameter(datatype=StatusType(Status))
|
||||
target = Parameter(datatype=FloatRange(1.7, 402.0, unit='K'), needscfg=False)
|
||||
setpoint = Parameter('intermediate set point',
|
||||
datatype=FloatRange(1.7, 402.0, unit='K'), handler=temp)
|
||||
datatype=FloatRange(1.7, 402.0, unit='K'))
|
||||
ramp = Parameter('ramping speed', readonly=False, default=0,
|
||||
datatype=FloatRange(0, 20, unit='K/min'))
|
||||
workingramp = Parameter('intermediate ramp value',
|
||||
datatype=FloatRange(0, 20, unit='K/min'), handler=temp)
|
||||
approachmode = Parameter('how to approach target!', readonly=False, handler=temp,
|
||||
datatype=EnumType(ApproachMode))
|
||||
# pollinterval = Parameter(visibility=3)
|
||||
datatype=FloatRange(0, 20, unit='K/min'), default=0)
|
||||
approachmode = Parameter('how to approach target!', readonly=False,
|
||||
datatype=EnumType(fast_settle=0, no_overshoot=1), default=0)
|
||||
timeout = Parameter('drive timeout, in addition to ramp time', readonly=False,
|
||||
datatype=FloatRange(0, unit='sec'), default=3600)
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
TempStatus = Enum(
|
||||
'TempStatus',
|
||||
stable_at_target=1,
|
||||
changing=2,
|
||||
within_tolerance=5,
|
||||
outside_tolerance=6,
|
||||
filling_emptying_reservoir=7,
|
||||
standby=10,
|
||||
control_disabled=13,
|
||||
can_not_complete=14,
|
||||
general_failure=15,
|
||||
)
|
||||
general_stop = Property('respect general stop', datatype=BoolType(),
|
||||
default=True, value=False)
|
||||
STATUS_MAP = {
|
||||
1: (Status.IDLE, 'stable at target'),
|
||||
2: (Status.RAMPING, 'ramping'),
|
||||
@ -420,8 +368,6 @@ class Temp(PpmsMixin, HasIodev, Drivable):
|
||||
14: (Status.ERROR, 'can not complete'),
|
||||
15: (Status.ERROR, 'general failure'),
|
||||
}
|
||||
general_stop = Property('respect general stop', datatype=BoolType(),
|
||||
default=True, value=False)
|
||||
|
||||
channel = 'temp'
|
||||
_stopped = False
|
||||
@ -432,6 +378,42 @@ class Temp(PpmsMixin, HasIodev, Drivable):
|
||||
_wait_at10 = False
|
||||
_ramp_at_limit = False
|
||||
|
||||
param_names = 'setpoint', 'workingramp', 'approachmode'
|
||||
|
||||
@CommonReadHandler(param_names)
|
||||
def read_params(self):
|
||||
settings = literal_eval(self.communicate('TEMP?'))
|
||||
if settings == self._last_settings:
|
||||
# update parameters only on change, as 'ramp' and 'approachmode' are
|
||||
# not always sent to the hardware
|
||||
return
|
||||
self.setpoint, self.workingramp, self.approachmode = self._last_settings = settings
|
||||
if self.setpoint != 10 or not self._wait_at10:
|
||||
self.log.debug('read back target %g %r' % (self.setpoint, self._wait_at10))
|
||||
self.target = self.setpoint
|
||||
if self.workingramp != 2 or not self._ramp_at_limit:
|
||||
self.log.debug('read back ramp %g %r' % (self.workingramp, self._ramp_at_limit))
|
||||
self.ramp = self.workingramp
|
||||
|
||||
def _write_params(self, setpoint, ramp, approachmode):
|
||||
wait_at10 = False
|
||||
ramp_at_limit = False
|
||||
if self.value > 11:
|
||||
if setpoint <= 10:
|
||||
wait_at10 = True
|
||||
setpoint = 10
|
||||
elif self.value > setpoint:
|
||||
if ramp >= 2:
|
||||
ramp = 2
|
||||
ramp_at_limit = True
|
||||
self._wait_at10 = wait_at10
|
||||
self._ramp_at_limit = ramp_at_limit
|
||||
self.calc_expected(setpoint, ramp)
|
||||
self.log.debug(
|
||||
'change_temp v %r s %r r %r w %r l %r' % (self.value, setpoint, ramp, wait_at10, ramp_at_limit))
|
||||
self.comm_write('TEMP %g,%g,%d' % (setpoint, ramp, approachmode))
|
||||
self.read_params()
|
||||
|
||||
def update_value_status(self, value, packed_status):
|
||||
if value is None:
|
||||
self.status = (self.Status.ERROR, 'invalid value')
|
||||
@ -449,7 +431,7 @@ class Temp(PpmsMixin, HasIodev, Drivable):
|
||||
if now > self._cool_deadline:
|
||||
self._wait_at10 = False
|
||||
self._last_change = now
|
||||
self.temp.write(self, 'setpoint', self.target)
|
||||
self._write_params(self.target, self.ramp, self.approachmode)
|
||||
status = (self.Status.STABILIZING, 'waiting at 10 K')
|
||||
if self._last_change: # there was a change, which is not yet confirmed by hw
|
||||
if now > self._last_change + 5:
|
||||
@ -478,41 +460,6 @@ class Temp(PpmsMixin, HasIodev, Drivable):
|
||||
self._expected_target_time = 0
|
||||
self.status = status
|
||||
|
||||
def analyze_temp(self, setpoint, workingramp, approachmode):
|
||||
if (setpoint, workingramp, approachmode) == self._last_settings:
|
||||
# update parameters only on change, as 'ramp' and 'approachmode' are
|
||||
# not always sent to the hardware
|
||||
return {}
|
||||
self._last_settings = setpoint, workingramp, approachmode
|
||||
if setpoint != 10 or not self._wait_at10:
|
||||
self.log.debug('read back target %g %r' % (setpoint, self._wait_at10))
|
||||
self.target = setpoint
|
||||
if workingramp != 2 or not self._ramp_at_limit:
|
||||
self.log.debug('read back ramp %g %r' % (workingramp, self._ramp_at_limit))
|
||||
self.ramp = workingramp
|
||||
result = dict(setpoint=setpoint, workingramp=workingramp)
|
||||
self.log.debug('analyze_temp %r %r' % (result, (self.target, self.ramp)))
|
||||
return result
|
||||
|
||||
def change_temp(self, change):
|
||||
ramp = change.ramp
|
||||
setpoint = change.setpoint
|
||||
wait_at10 = False
|
||||
ramp_at_limit = False
|
||||
if self.value > 11:
|
||||
if setpoint <= 10:
|
||||
wait_at10 = True
|
||||
setpoint = 10
|
||||
elif self.value > setpoint:
|
||||
if ramp >= 2:
|
||||
ramp = 2
|
||||
ramp_at_limit = True
|
||||
self._wait_at10 = wait_at10
|
||||
self._ramp_at_limit = ramp_at_limit
|
||||
self.calc_expected(setpoint, ramp)
|
||||
self.log.debug('change_temp v %r s %r r %r w %r l %r' % (self.value, setpoint, ramp, wait_at10, ramp_at_limit))
|
||||
return setpoint, ramp, change.approachmode
|
||||
|
||||
def write_target(self, target):
|
||||
self._stopped = False
|
||||
if abs(self.target - self.value) <= 2e-5 * target and target == self.target:
|
||||
@ -520,23 +467,23 @@ class Temp(PpmsMixin, HasIodev, Drivable):
|
||||
self._status_before_change = self.status
|
||||
self.status = (self.Status.BUSY, 'changed target')
|
||||
self._last_change = time.time()
|
||||
self.temp.write(self, 'setpoint', target)
|
||||
self._write_params(target, self.ramp, self.approachmode)
|
||||
self.log.debug('write_target %s' % repr((self.setpoint, target, self._wait_at10)))
|
||||
return target
|
||||
|
||||
def write_approachmode(self, value):
|
||||
if self.isDriving():
|
||||
self.temp.write(self, 'approachmode', value)
|
||||
self._write_params(self.setpoint, self.ramp, value)
|
||||
return Done
|
||||
self.approachmode = value
|
||||
return None # do not execute TEMP command, as this would trigger an unnecessary T change
|
||||
return Done # do not execute TEMP command, as this would trigger an unnecessary T change
|
||||
|
||||
def write_ramp(self, value):
|
||||
if self.isDriving():
|
||||
self.temp.write(self, 'ramp', value)
|
||||
self._write_params(self.setpoint, value, self.approachmode)
|
||||
return Done
|
||||
# self.ramp = value
|
||||
return None # do not execute TEMP command, as this would trigger an unnecessary T change
|
||||
self.ramp = value
|
||||
return Done # do not execute TEMP command, as this would trigger an unnecessary T change
|
||||
|
||||
def calc_expected(self, target, ramp):
|
||||
self._expected_target_time = time.time() + abs(target - self.value) * 60.0 / max(0.1, ramp)
|
||||
@ -554,10 +501,9 @@ class Temp(PpmsMixin, HasIodev, Drivable):
|
||||
self._stopped = True
|
||||
|
||||
|
||||
class Field(PpmsMixin, HasIodev, Drivable):
|
||||
class Field(PpmsBase, Drivable):
|
||||
"""magnetic field"""
|
||||
|
||||
field = IOHandler('field', 'FIELD?', '%g,%g,%d,%d')
|
||||
Status = Enum(
|
||||
Drivable.Status,
|
||||
PREPARED=150,
|
||||
@ -566,20 +512,15 @@ class Field(PpmsMixin, HasIodev, Drivable):
|
||||
STABILIZING=380,
|
||||
FINALIZING=390,
|
||||
)
|
||||
# pylint: disable=invalid-name
|
||||
PersistentMode = Enum('PersistentMode', persistent=0, driven=1)
|
||||
ApproachMode = Enum('ApproachMode', linear=0, no_overshoot=1, oscillate=2)
|
||||
|
||||
value = Parameter(datatype=FloatRange(unit='T'), poll=True)
|
||||
status = Parameter(datatype=StatusType(Status), poll=True)
|
||||
target = Parameter(datatype=FloatRange(-15, 15, unit='T'), handler=field)
|
||||
ramp = Parameter('ramping speed', readonly=False, handler=field,
|
||||
datatype=FloatRange(0.064, 1.19, unit='T/min'))
|
||||
approachmode = Parameter('how to approach target', readonly=False, handler=field,
|
||||
datatype=EnumType(ApproachMode))
|
||||
persistentmode = Parameter('what to do after changing field', readonly=False, handler=field,
|
||||
datatype=EnumType(PersistentMode))
|
||||
# pollinterval = Parameter(visibility=3)
|
||||
value = Parameter(datatype=FloatRange(unit='T'))
|
||||
status = Parameter(datatype=StatusType(Status))
|
||||
target = Parameter(datatype=FloatRange(-15, 15, unit='T')) # poll only one parameter
|
||||
ramp = Parameter('ramping speed', readonly=False,
|
||||
datatype=FloatRange(0.064, 1.19, unit='T/min'), default=0.19)
|
||||
approachmode = Parameter('how to approach target', readonly=False,
|
||||
datatype=EnumType(linear=0, no_overshoot=1, oscillate=2), default=0)
|
||||
persistentmode = Parameter('what to do after changing field', readonly=False,
|
||||
datatype=EnumType(persistent=0, driven=1), default=0)
|
||||
|
||||
STATUS_MAP = {
|
||||
1: (Status.IDLE, 'persistent mode'),
|
||||
@ -599,6 +540,25 @@ class Field(PpmsMixin, HasIodev, Drivable):
|
||||
_last_target = None # last reached target
|
||||
_last_change = 0 # means no target change is pending
|
||||
|
||||
param_names = 'target', 'ramp', 'approachmode', 'persistentmode'
|
||||
|
||||
@CommonReadHandler(param_names)
|
||||
def read_params(self):
|
||||
settings = literal_eval(self.communicate('FIELD?'))
|
||||
# print('last_settings tt %s' % repr(self._last_settings))
|
||||
if settings == self._last_settings:
|
||||
# we update parameters only on change, as 'ramp' and 'approachmode' are
|
||||
# not always sent to the hardware
|
||||
return
|
||||
target, ramp, self.approachmode, self.persistentmode = self._last_settings = settings
|
||||
self.target = round(target * 1e-4, 7)
|
||||
self.ramp = ramp * 6e-3
|
||||
|
||||
def _write_params(self, target, ramp, approachmode, persistentmode):
|
||||
self.comm_write('FIELD %g,%g,%d,%d' % (
|
||||
target * 1e+4, ramp / 6e-3, approachmode, persistentmode))
|
||||
self.read_params()
|
||||
|
||||
def update_value_status(self, value, packed_status):
|
||||
if value is None:
|
||||
self.status = (self.Status.ERROR, 'invalid value')
|
||||
@ -633,19 +593,6 @@ class Field(PpmsMixin, HasIodev, Drivable):
|
||||
status = (status[0], 'stopping (%s)' % status[1])
|
||||
self.status = status
|
||||
|
||||
def analyze_field(self, target, ramp, approachmode, persistentmode):
|
||||
# print('last_settings tt %s' % repr(self._last_settings))
|
||||
if (target, ramp, approachmode, persistentmode) == self._last_settings:
|
||||
# we update parameters only on change, as 'ramp' and 'approachmode' are
|
||||
# not always sent to the hardware
|
||||
return {}
|
||||
self._last_settings = target, ramp, approachmode, persistentmode
|
||||
return dict(target=round(target * 1e-4, 7), ramp=ramp * 6e-3, approachmode=approachmode,
|
||||
persistentmode=persistentmode)
|
||||
|
||||
def change_field(self, change):
|
||||
return change.target * 1e+4, change.ramp / 6e-3, change.approachmode, change.persistentmode
|
||||
|
||||
def write_target(self, target):
|
||||
if abs(self.target - self.value) <= 2e-5 and target == self.target:
|
||||
self.target = target
|
||||
@ -654,7 +601,7 @@ class Field(PpmsMixin, HasIodev, Drivable):
|
||||
self._stopped = False
|
||||
self._last_change = time.time()
|
||||
self.status = (self.Status.BUSY, 'changed target')
|
||||
self.field.write(self, 'target', target)
|
||||
self._write_params(target, self.ramp, self.approachmode, self.persistentmode)
|
||||
return Done
|
||||
|
||||
def write_persistentmode(self, mode):
|
||||
@ -665,19 +612,19 @@ class Field(PpmsMixin, HasIodev, Drivable):
|
||||
self._status_before_change = self.status
|
||||
self._stopped = False
|
||||
self.status = (self.Status.BUSY, 'changed persistent mode')
|
||||
self.field.write(self, 'persistentmode', mode)
|
||||
self._write_params(self.target, self.ramp, self.approachmode, mode)
|
||||
return Done
|
||||
|
||||
def write_ramp(self, value):
|
||||
self.ramp = value
|
||||
if self.isDriving():
|
||||
self.field.write(self, 'ramp', value)
|
||||
self._write_params(self.target, value, self.approachmode, self.persistentmode)
|
||||
return Done
|
||||
return None # do not execute FIELD command, as this would trigger a ramp up of leads current
|
||||
|
||||
def write_approachmode(self, value):
|
||||
if self.isDriving():
|
||||
self.field.write(self, 'approachmode', value)
|
||||
self._write_params(self.target, self.ramp, value, self.persistentmode)
|
||||
return Done
|
||||
return None # do not execute FIELD command, as this would trigger a ramp up of leads current
|
||||
|
||||
@ -692,20 +639,17 @@ class Field(PpmsMixin, HasIodev, Drivable):
|
||||
self._stopped = True
|
||||
|
||||
|
||||
class Position(PpmsMixin, HasIodev, Drivable):
|
||||
class Position(PpmsBase, Drivable):
|
||||
"""rotator position"""
|
||||
|
||||
move = IOHandler('move', 'MOVE?', '%g,%g,%g')
|
||||
Status = Drivable.Status
|
||||
|
||||
value = Parameter(datatype=FloatRange(unit='deg'), poll=True)
|
||||
target = Parameter(datatype=FloatRange(-720., 720., unit='deg'), handler=move)
|
||||
enabled = Parameter('is this channel used?', readonly=False, poll=False,
|
||||
value = Parameter(datatype=FloatRange(unit='deg'))
|
||||
target = Parameter(datatype=FloatRange(-720., 720., unit='deg'))
|
||||
enabled = Parameter('is this channel used?', readonly=False,
|
||||
datatype=BoolType(), default=True)
|
||||
speed = Parameter('motor speed', readonly=False, handler=move,
|
||||
speed = Parameter('motor speed', readonly=False, default=12,
|
||||
datatype=FloatRange(0.8, 12, unit='deg/sec'))
|
||||
# pollinterval = Parameter(visibility=3)
|
||||
|
||||
STATUS_MAP = {
|
||||
1: (Status.IDLE, 'at target'),
|
||||
5: (Status.BUSY, 'moving'),
|
||||
@ -720,6 +664,23 @@ class Position(PpmsMixin, HasIodev, Drivable):
|
||||
_last_change = 0
|
||||
_within_target = 0 # time since we are within target
|
||||
|
||||
param_names = 'target', 'speed'
|
||||
|
||||
@CommonReadHandler(param_names)
|
||||
def read_params(self):
|
||||
settings = literal_eval(self.communicate('MOVE?'))
|
||||
if settings == self._last_settings:
|
||||
# we update parameters only on change, as 'speed' is
|
||||
# not always sent to the hardware
|
||||
return
|
||||
self.target, _, speed = self._last_settings = settings
|
||||
self.speed = (15 - speed) * 0.8
|
||||
|
||||
def _write_params(self, target, speed):
|
||||
speed = int(round(min(14, max(0, 15 - speed / 0.8)), 0))
|
||||
self.comm_write('MOVE %g,%d,%d' % (target, 0, speed))
|
||||
return self.read_params()
|
||||
|
||||
def update_value_status(self, value, packed_status):
|
||||
if not self.enabled:
|
||||
self.status = (self.Status.DISABLED, 'disabled')
|
||||
@ -757,29 +718,17 @@ class Position(PpmsMixin, HasIodev, Drivable):
|
||||
status = (status[0], 'stopping (%s)' % status[1])
|
||||
self.status = status
|
||||
|
||||
def analyze_move(self, target, mode, speed):
|
||||
if (target, speed) == self._last_settings:
|
||||
# we update parameters only on change, as 'speed' is
|
||||
# not always sent to the hardware
|
||||
return {}
|
||||
self._last_settings = target, speed
|
||||
return dict(target=target, speed=(15 - speed) * 0.8)
|
||||
|
||||
def change_move(self, change):
|
||||
speed = int(round(min(14, max(0, 15 - change.speed / 0.8)), 0))
|
||||
return change.target, 0, speed
|
||||
|
||||
def write_target(self, target):
|
||||
self._stopped = False
|
||||
self._last_change = 0
|
||||
self._status_before_change = self.status
|
||||
self.status = (self.Status.BUSY, 'changed target')
|
||||
self.move.write(self, 'target', target)
|
||||
self._write_params(target, self.speed)
|
||||
return Done
|
||||
|
||||
def write_speed(self, value):
|
||||
if self.isDriving():
|
||||
self.move.write(self, 'speed', value)
|
||||
self._write_params(self.target, value)
|
||||
return Done
|
||||
self.speed = value
|
||||
return None # do not execute MOVE command, as this would trigger an unnecessary move
|
||||
|
@ -26,6 +26,7 @@ import time
|
||||
def num(string):
|
||||
return json.loads(string)
|
||||
|
||||
|
||||
class NamedList:
|
||||
def __init__(self, keys, *args, **kwargs):
|
||||
self.__keys__ = keys.split()
|
||||
@ -49,8 +50,10 @@ class NamedList:
|
||||
def __repr__(self):
|
||||
return ",".join("%.7g" % val for val in self.aslist())
|
||||
|
||||
|
||||
class PpmsSim:
|
||||
CHANNELS = 'st t mf pos r1 i1 r2 i2'.split()
|
||||
|
||||
def __init__(self):
|
||||
self.status = NamedList('t mf ch pos', 1, 1, 1, 1)
|
||||
self.st = 0x1111
|
||||
@ -176,7 +179,6 @@ class PpmsSim:
|
||||
if abs(self.t - self.temp.target) < 1:
|
||||
self.status.t = 6 # outside tolerance
|
||||
|
||||
|
||||
if abs(self.pos - self.move.target) < 0.01:
|
||||
self.status.pos = 1
|
||||
else:
|
||||
@ -188,7 +190,6 @@ class PpmsSim:
|
||||
self.r2 = 1000 / self.t
|
||||
self.i2 = math.log(self.t)
|
||||
self.level.value = 100 - (self.time - self.start) * 0.01 % 100
|
||||
# print('PROGRESS T=%.7g B=%.7g x=%.7g' % (self.t, self.mf, self.pos))
|
||||
|
||||
def getdat(self, mask):
|
||||
mask = int(mask) & 0xff # all channels up to i2
|
||||
@ -198,6 +199,7 @@ class PpmsSim:
|
||||
output.append("%.7g" % getattr(self, chan))
|
||||
return ",".join(output)
|
||||
|
||||
|
||||
class QDevice:
|
||||
def __init__(self, classid):
|
||||
self.sim = PpmsSim()
|
||||
@ -225,5 +227,6 @@ class QDevice:
|
||||
result = "OK"
|
||||
return result
|
||||
|
||||
|
||||
def shutdown():
|
||||
pass
|
||||
|
@ -41,7 +41,7 @@ from secop.client import ProxyClient
|
||||
from secop.datatypes import ArrayOf, BoolType, \
|
||||
EnumType, FloatRange, IntRange, StringType
|
||||
from secop.errors import ConfigError, HardwareError, secop_error, NoSuchModuleError
|
||||
from secop.lib import getGeneralConfig, mkthread
|
||||
from secop.lib import getGeneralConfig, mkthread, formatExtendedStack
|
||||
from secop.lib.asynconn import AsynConn, ConnectionClosed
|
||||
from secop.modules import Attached, Command, Done, Drivable, \
|
||||
Module, Parameter, Property, Readable, Writable
|
||||
@ -124,6 +124,7 @@ class SeaClient(ProxyClient, Module):
|
||||
if config:
|
||||
self.default_json_file[name] = config.split()[0] + '.json'
|
||||
self.io = None
|
||||
self.asyncio = None
|
||||
ProxyClient.__init__(self)
|
||||
Module.__init__(self, name, log, opts, srv)
|
||||
|
||||
@ -153,7 +154,7 @@ class SeaClient(ProxyClient, Module):
|
||||
"""send a request and wait for reply"""
|
||||
with self._write_lock:
|
||||
if not self.io or not self.io.connection:
|
||||
if not self.asyncio.connection:
|
||||
if not self.asyncio or not self.asyncio.connection:
|
||||
self._connect(None)
|
||||
self.io = AsynConn(self.uri)
|
||||
assert self.io.readline() == b'OK'
|
||||
@ -314,12 +315,14 @@ class SeaConfigCreator(SeaClient):
|
||||
stripped, _, ext = filename.rpartition('.')
|
||||
service = SERVICE_NAMES[ext]
|
||||
seaconn = 'sea_' + service
|
||||
with open(join(seaconfdir, stripped + '.cfg'), 'w') as fp:
|
||||
cfgfile = join(seaconfdir, stripped + '.cfg')
|
||||
with open(cfgfile, 'w') as fp:
|
||||
fp.write(CFG_HEADER % dict(config=filename, seaconn=seaconn, service=service,
|
||||
nodedescr=description.get(filename, filename)))
|
||||
for obj in descr:
|
||||
fp.write(CFG_MODULE % dict(modcls=modcls[obj], module=obj, seaconn=seaconn))
|
||||
content = json.dumps(descr).replace('}, {', '},\n{').replace('[{', '[\n{').replace('}]}, ', '}]},\n\n')
|
||||
result.append('%s\n' % cfgfile)
|
||||
with open(join(seaconfdir, filename + '.json'), 'w') as fp:
|
||||
fp.write(content + '\n')
|
||||
result.append('%s: %s' % (filename, ','.join(n for n in descr)))
|
||||
@ -495,7 +498,8 @@ class SeaModule(Module):
|
||||
if key in cls.accessibles:
|
||||
if key == 'target':
|
||||
kwds['readonly'] = False
|
||||
pobj = cls.accessibles[key].override(**kwds)
|
||||
pobj = cls.accessibles[key]
|
||||
pobj.init(kwds)
|
||||
datatype = kwds.get('datatype', cls.accessibles[key].datatype)
|
||||
else:
|
||||
pobj = Parameter(**kwds)
|
||||
@ -542,12 +546,17 @@ class SeaModule(Module):
|
||||
# create standard parameters like value and status, if not yet there
|
||||
for pname, pobj in cls.accessibles.items():
|
||||
if pname == 'pollinterval':
|
||||
attributes[pname] = pobj.override(export=False)
|
||||
pobj.export = False
|
||||
attributes[pname] = pobj
|
||||
pobj.__set_name__(cls, pname)
|
||||
elif pname not in attributes and isinstance(pobj, Parameter):
|
||||
attributes[pname] = pobj.override(poll=False, needscfg=False)
|
||||
pobj.poll = False
|
||||
pobj.needscfg = False
|
||||
attributes[pname] = pobj
|
||||
pobj.__set_name__(cls, pname)
|
||||
|
||||
classname = '%s_%s' % (cls.__name__, sea_object)
|
||||
# print(name, attributes)
|
||||
attributes['pollerClass'] = None
|
||||
newcls = type(classname, (cls,), attributes)
|
||||
return Module.__new__(newcls)
|
||||
|
||||
@ -640,5 +649,11 @@ class SeaDrivable(SeaModule, Drivable):
|
||||
if value is not None:
|
||||
self.target = value
|
||||
|
||||
@Command()
|
||||
def stop(self):
|
||||
"""propagate to SEA
|
||||
|
||||
- on stdsct drivables this will call the halt script
|
||||
- on EaseDriv this will set the stopped state
|
||||
"""
|
||||
self._iodev.query('%s is_running 0' % self.sea_object)
|
||||
|
@ -22,22 +22,28 @@
|
||||
"""simulated transducer DPM3 read out"""
|
||||
|
||||
import random
|
||||
import math
|
||||
from secop.core import Readable, Parameter, FloatRange, Attached
|
||||
from secop.lib import clamp
|
||||
|
||||
|
||||
class DPM3(Readable):
|
||||
motor = Attached()
|
||||
jitter = Parameter('simulated jitter', FloatRange(unit='N'), default=1, readonly=False)
|
||||
jitter = Parameter('simulated jitter', FloatRange(unit='N'), default=0.1, readonly=False)
|
||||
hysteresis = Parameter('simulated hysteresis', FloatRange(unit='deg'), default=100, readonly=False)
|
||||
friction = Parameter('friction', FloatRange(unit='N/deg'), default=1, readonly=False)
|
||||
slope = Parameter('slope', FloatRange(unit='N/deg'), default=10, readonly=False)
|
||||
friction = Parameter('friction', FloatRange(unit='N/deg'), default=2.5, readonly=False)
|
||||
slope = Parameter('slope', FloatRange(unit='N/deg'), default=0.5, readonly=False)
|
||||
offset = Parameter('offset', FloatRange(unit='N'), default=0, readonly=False)
|
||||
|
||||
_pos = 0 # effective piston position, main hysteresis taken into account
|
||||
_pos = 0
|
||||
|
||||
def read_value(self):
|
||||
mot = self._motor
|
||||
self._pos = clamp(self._pos, mot.value - self.hysteresis * 0.5, mot.value + self.hysteresis * 0.5)
|
||||
return clamp(0, 1, mot.value - self._pos) * self.friction \
|
||||
+ self._pos * self.slope + self.jitter * (random.random() - random.random())
|
||||
d = self.friction * self.slope
|
||||
self._pos = clamp(self._pos, mot.value - d, mot.value + d)
|
||||
f = (mot.value - self._pos) / self.slope
|
||||
if mot.value > 0:
|
||||
f = max(f, (mot.value - self.hysteresis) / self.slope)
|
||||
else:
|
||||
f = min(f, (mot.value + self.hysteresis) / self.slope)
|
||||
return f + self.jitter * (random.random() - random.random()) * 0.5
|
||||
|
@ -22,7 +22,7 @@
|
||||
|
||||
import math
|
||||
import os
|
||||
from os.path import basename, exists, join
|
||||
from os.path import basename, dirname, exists, join
|
||||
|
||||
import numpy as np
|
||||
from scipy.interpolate import splev, splrep # pylint: disable=import-error
|
||||
@ -109,7 +109,9 @@ class CalCurve:
|
||||
calibname = sensopt.pop(0)
|
||||
_, dot, ext = basename(calibname).rpartition('.')
|
||||
kind = None
|
||||
for path in os.environ.get('FRAPPY_CALIB_PATH', '').split(','):
|
||||
pathlist = os.environ.get('FRAPPY_CALIB_PATH', '').split(',')
|
||||
pathlist.append(join(dirname(__file__), 'calcurves'))
|
||||
for path in pathlist:
|
||||
# first try without adding kind
|
||||
filename = join(path.strip(), calibname)
|
||||
if exists(filename):
|
||||
@ -145,7 +147,7 @@ class CalCurve:
|
||||
for line in f:
|
||||
parser.parse(line)
|
||||
except Exception as e:
|
||||
raise ValueError('calib curve %s: %s' % (calibspec, e))
|
||||
raise ValueError('calib curve %s: %s' % (calibspec, e)) from e
|
||||
self.convert_x = nplog if parser.logx else linear
|
||||
self.convert_y = npexp if parser.logy else linear
|
||||
x = np.asarray(parser.xdata)
|
||||
@ -157,8 +159,8 @@ class CalCurve:
|
||||
raise ValueError('calib curve %s is not monotonic' % calibspec)
|
||||
try:
|
||||
self.spline = splrep(x, y, s=0, k=min(3, len(x) - 1))
|
||||
except (ValueError, TypeError):
|
||||
raise ValueError('invalid calib curve %s' % calibspec)
|
||||
except (ValueError, TypeError) as e:
|
||||
raise ValueError('invalid calib curve %s' % calibspec) from e
|
||||
|
||||
def __call__(self, value):
|
||||
"""convert value
|
||||
@ -178,8 +180,9 @@ class Sensor(Readable):
|
||||
pollinterval = Parameter(export=False)
|
||||
status = Parameter(default=(Readable.Status.ERROR, 'unintialized'))
|
||||
|
||||
pollerClass = None
|
||||
description = 'a calibrated sensor value'
|
||||
_value_error = None
|
||||
enablePoll = False
|
||||
|
||||
def checkProperties(self):
|
||||
if 'description' not in self.propertyValues:
|
||||
@ -187,6 +190,7 @@ class Sensor(Readable):
|
||||
super().checkProperties()
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
self._rawsensor.registerCallbacks(self, ['status']) # auto update status
|
||||
self._calib = CalCurve(self.calib)
|
||||
if self.description == '_':
|
||||
|
@ -24,13 +24,12 @@
|
||||
|
||||
import time
|
||||
import struct
|
||||
from math import log10
|
||||
|
||||
from secop.core import BoolType, Command, EnumType, FloatRange, IntRange, \
|
||||
HasIodev, Parameter, Property, Drivable, PersistentMixin, PersistentParam
|
||||
HasIO, Parameter, Property, Drivable, PersistentMixin, PersistentParam, Done
|
||||
from secop.io import BytesIO
|
||||
from secop.errors import CommunicationFailedError, HardwareError, BadValueError, IsBusyError
|
||||
|
||||
from secop.rwhandler import ReadHandler, WriteHandler
|
||||
|
||||
MOTOR_STOP = 3
|
||||
MOVE = 4
|
||||
@ -50,84 +49,78 @@ MAX_SPEED = 2047 * SPEED_SCALE
|
||||
ACCEL_SCALE = 1E12 / 2 ** 31 * ANGLE_SCALE
|
||||
MAX_ACCEL = 2047 * ACCEL_SCALE
|
||||
CURRENT_SCALE = 2.8/250
|
||||
ENCODER_RESOLUTION = 0.4 # 365 / 1024, rounded up
|
||||
ENCODER_RESOLUTION = 360 / 1024
|
||||
|
||||
HW_ARGS = {
|
||||
# <parameter name>: (address, scale factor)
|
||||
'encoder_tolerance': (212, ANGLE_SCALE),
|
||||
'speed': (4, SPEED_SCALE),
|
||||
'minspeed': (130, SPEED_SCALE),
|
||||
'currentspeed': (3, SPEED_SCALE),
|
||||
'maxcurrent': (6, CURRENT_SCALE),
|
||||
'standby_current': (7, CURRENT_SCALE,),
|
||||
'acceleration': (5, ACCEL_SCALE),
|
||||
'target_reached': (8, 1),
|
||||
'move_status': (207, 1),
|
||||
'error_bits': (208, 1),
|
||||
'free_wheeling': (204, 0.01),
|
||||
'power_down_delay': (214, 0.01),
|
||||
}
|
||||
|
||||
# special handling (adjust zero):
|
||||
ENCODER_ADR = 209
|
||||
STEPPOS_ADR = 1
|
||||
|
||||
|
||||
class HwParam(PersistentParam):
|
||||
adr = Property('parameter address', IntRange(0, 255), export=False)
|
||||
scale = Property('scale factor (physical value / unit)', FloatRange(), export=False)
|
||||
|
||||
def __init__(self, description, datatype, adr, scale=1, poll=True,
|
||||
readonly=True, persistent=None, **kwds):
|
||||
"""hardware parameter"""
|
||||
if persistent is None:
|
||||
persistent = not readonly
|
||||
if isinstance(datatype, FloatRange) and datatype.fmtstr == '%g':
|
||||
datatype.fmtstr = '%%.%df' % max(0, 1 - int(log10(scale) + 0.01))
|
||||
super().__init__(description, datatype, poll=poll, adr=adr, scale=scale,
|
||||
persistent=persistent, readonly=readonly, **kwds)
|
||||
|
||||
def copy(self):
|
||||
res = HwParam(self.description, self.datatype.copy(), self.adr)
|
||||
res.name = self.name
|
||||
res.init(self.propertyValues)
|
||||
return res
|
||||
def writable(*args, **kwds):
|
||||
"""convenience function to create writable hardware parameters"""
|
||||
return PersistentParam(*args, readonly=False, initwrite=True, **kwds)
|
||||
|
||||
|
||||
class Motor(PersistentMixin, HasIodev, Drivable):
|
||||
class Motor(PersistentMixin, HasIO, Drivable):
|
||||
address = Property('module address', IntRange(0, 255), default=1)
|
||||
|
||||
value = Parameter('motor position', FloatRange(unit='deg', fmtstr='%.3f'), poll=False, default=0) # polling by read_status
|
||||
value = Parameter('motor position', FloatRange(unit='deg', fmtstr='%.3f'))
|
||||
zero = PersistentParam('zero point', FloatRange(unit='$'), readonly=False, default=0)
|
||||
encoder = HwParam('encoder reading', FloatRange(unit='$', fmtstr='%.1f'),
|
||||
209, ANGLE_SCALE, readonly=True, initwrite=False, persistent=True)
|
||||
steppos = HwParam('position from motor steps', FloatRange(unit='$'),
|
||||
1, ANGLE_SCALE, readonly=True, initwrite=False)
|
||||
target = Parameter('_', FloatRange(unit='$'), default=0)
|
||||
encoder = PersistentParam('encoder reading', FloatRange(unit='$', fmtstr='%.1f'),
|
||||
readonly=True, initwrite=False)
|
||||
steppos = PersistentParam('position from motor steps', FloatRange(unit='$', fmtstr='%.3f'),
|
||||
readonly=True, initwrite=False)
|
||||
target = Parameter('', FloatRange(unit='$'), default=0)
|
||||
|
||||
move_limit = Parameter('max. angle to drive in one go when current above safe_current', FloatRange(unit='$'),
|
||||
readonly=False, default=5, group='more')
|
||||
safe_current = Parameter('motor current allowed for big steps', FloatRange(unit='A'),
|
||||
readonly=False, default=0.2, group='more')
|
||||
move_limit = Parameter('max. angle to drive in one go', FloatRange(unit='$'),
|
||||
readonly=False, default=360, group='more')
|
||||
tolerance = Parameter('positioning tolerance', FloatRange(unit='$'),
|
||||
readonly=False, default=0.9)
|
||||
encoder_tolerance = HwParam('the allowed deviation between steppos and encoder\n\nmust be > tolerance',
|
||||
FloatRange(0, 360., unit='$'),
|
||||
212, ANGLE_SCALE, readonly=False, group='more')
|
||||
speed = HwParam('max. speed', FloatRange(0, MAX_SPEED, unit='$/sec'),
|
||||
4, SPEED_SCALE, readonly=False, group='motorparam')
|
||||
minspeed = HwParam('min. speed', FloatRange(0, MAX_SPEED, unit='$/sec'),
|
||||
130, SPEED_SCALE, readonly=False, default=SPEED_SCALE, group='motorparam')
|
||||
currentspeed = HwParam('current speed', FloatRange(-MAX_SPEED, MAX_SPEED, unit='$/sec'),
|
||||
3, SPEED_SCALE, readonly=True, group='motorparam')
|
||||
maxcurrent = HwParam('_', FloatRange(0, 2.8, unit='A'),
|
||||
6, CURRENT_SCALE, readonly=False, group='motorparam')
|
||||
standby_current = HwParam('_', FloatRange(0, 2.8, unit='A'),
|
||||
7, CURRENT_SCALE, readonly=False, group='motorparam')
|
||||
acceleration = HwParam('_', FloatRange(4.6 * ACCEL_SCALE, MAX_ACCEL, unit='deg/s^2'),
|
||||
5, ACCEL_SCALE, readonly=False, group='motorparam')
|
||||
target_reached = HwParam('_', BoolType(), 8, group='hwstatus')
|
||||
move_status = HwParam('_', IntRange(0, 3),
|
||||
207, readonly=True, group='hwstatus')
|
||||
error_bits = HwParam('_', IntRange(0, 255),
|
||||
208, readonly=True, group='hwstatus')
|
||||
# the doc says msec, but I believe the scale is 10 msec
|
||||
free_wheeling = HwParam('_', FloatRange(0, 60., unit='sec'),
|
||||
204, 0.01, default=0.1, readonly=False, group='motorparam')
|
||||
power_down_delay = HwParam('_', FloatRange(0, 60., unit='sec'),
|
||||
214, 0.01, default=0.1, readonly=False, group='motorparam')
|
||||
baudrate = Parameter('_', EnumType({'%d' % v: i for i, v in enumerate(BAUDRATES)}),
|
||||
readonly=False, default=0, poll=True, visibility=3, group='more')
|
||||
encoder_tolerance = writable('the allowed deviation between steppos and encoder\n\nmust be > tolerance',
|
||||
FloatRange(0, 360., unit='$', fmtstr='%.3f'), group='more')
|
||||
speed = writable('max. speed', FloatRange(0, MAX_SPEED, unit='$/sec', fmtstr='%.1f'), default=40)
|
||||
minspeed = writable('min. speed', FloatRange(0, MAX_SPEED, unit='$/sec', fmtstr='%.1f'),
|
||||
default=SPEED_SCALE, group='motorparam')
|
||||
currentspeed = Parameter('current speed', FloatRange(-MAX_SPEED, MAX_SPEED, unit='$/sec', fmtstr='%.1f'),
|
||||
group='motorparam')
|
||||
maxcurrent = writable('', FloatRange(0, 2.8, unit='A', fmtstr='%.2f'),
|
||||
default=1.4, group='motorparam')
|
||||
standby_current = writable('', FloatRange(0, 2.8, unit='A', fmtstr='%.2f'),
|
||||
default=0.1, group='motorparam')
|
||||
acceleration = writable('', FloatRange(4.6 * ACCEL_SCALE, MAX_ACCEL, unit='deg/s^2', fmtstr='%.1f'),
|
||||
default=150., group='motorparam')
|
||||
target_reached = Parameter('', BoolType(), group='hwstatus')
|
||||
move_status = Parameter('', IntRange(0, 3), group='hwstatus')
|
||||
error_bits = Parameter('', IntRange(0, 255), group='hwstatus')
|
||||
free_wheeling = writable('', FloatRange(0, 60., unit='sec', fmtstr='%.2f'),
|
||||
default=0.1, group='motorparam')
|
||||
power_down_delay = writable('', FloatRange(0, 60., unit='sec', fmtstr='%.2f'),
|
||||
default=0.1, group='motorparam')
|
||||
baudrate = Parameter('', EnumType({'%d' % v: i for i, v in enumerate(BAUDRATES)}),
|
||||
readonly=False, default=0, visibility=3, group='more')
|
||||
pollinterval = Parameter(group='more')
|
||||
|
||||
|
||||
iodevClass = BytesIO
|
||||
# fast_pollfactor = 0.001 # poll as fast as possible when busy
|
||||
fast_pollfactor = 0.05
|
||||
ioClass = BytesIO
|
||||
fast_pollfactor = 0.001 # not used any more, TODO: use a statemachine for running
|
||||
_started = 0
|
||||
_calc_timeout = True
|
||||
_calcTimeout = True
|
||||
_need_reset = None
|
||||
_last_change = 0
|
||||
|
||||
def comm(self, cmd, adr, value=0, bank=0):
|
||||
"""set or get a parameter
|
||||
@ -138,21 +131,23 @@ class Motor(PersistentMixin, HasIodev, Drivable):
|
||||
:param value: if given, the parameter is written, else it is returned
|
||||
:return: the returned value
|
||||
"""
|
||||
if self._calc_timeout:
|
||||
self._calc_timeout = False
|
||||
baudrate = getattr(self._iodev._conn.connection, 'baudrate', None)
|
||||
if self._calcTimeout and self.io._conn:
|
||||
self._calcTimeout = False
|
||||
baudrate = getattr(self.io._conn.connection, 'baudrate', None)
|
||||
if baudrate:
|
||||
if baudrate not in BAUDRATES:
|
||||
raise CommunicationFailedError('unsupported baud rate: %d' % baudrate)
|
||||
self._iodev.timeout = 0.03 + 200 / baudrate
|
||||
self.io.timeout = 0.03 + 200 / baudrate
|
||||
|
||||
exc = None
|
||||
byt = struct.pack('>BBBBi', self.address, cmd, adr, bank, round(value))
|
||||
byt += bytes([sum(byt) & 0xff])
|
||||
for itry in range(3,0,-1):
|
||||
byt = struct.pack('>BBBBi', self.address, cmd, adr, bank, round(value))
|
||||
try:
|
||||
reply = self._iodev.communicate(byt + bytes([sum(byt) & 0xff]), 9)
|
||||
reply = self.communicate(byt, 9)
|
||||
if sum(reply[:-1]) & 0xff != reply[-1]:
|
||||
raise CommunicationFailedError('checksum error')
|
||||
# will try again
|
||||
except Exception as e:
|
||||
if itry == 1:
|
||||
raise
|
||||
@ -168,36 +163,18 @@ class Motor(PersistentMixin, HasIodev, Drivable):
|
||||
raise CommunicationFailedError('bad reply %r to command %s %d' % (reply, cmd, adr))
|
||||
return result
|
||||
|
||||
def get(self, pname, **kwds):
|
||||
"""get parameter"""
|
||||
pobj = self.parameters[pname]
|
||||
value = self.comm(GET_AXIS_PAR, pobj.adr, **kwds)
|
||||
# do not apply scale when 1 (datatype might not be float)
|
||||
return value if pobj.scale == 1 else value * pobj.scale
|
||||
def startModule(self, start_events):
|
||||
super().startModule(start_events)
|
||||
|
||||
def set(self, pname, value, check=True, **kwds):
|
||||
"""set parameter and check result"""
|
||||
pobj = self.parameters[pname]
|
||||
scale = pobj.scale
|
||||
rawvalue = round(value / scale)
|
||||
for itry in range(2):
|
||||
self.comm(SET_AXIS_PAR, pobj.adr, rawvalue, **kwds)
|
||||
if check:
|
||||
result = self.comm(GET_AXIS_PAR, pobj.adr, **kwds)
|
||||
if result != rawvalue:
|
||||
self.log.warning('result for %s does not match %d != %d, try again', pname, result, rawvalue)
|
||||
continue
|
||||
value = result * scale
|
||||
return value
|
||||
else:
|
||||
raise HardwareError('result for %s does not match %d != %d' % (pname, result, rawvalue))
|
||||
def fix_encoder(self=self):
|
||||
try:
|
||||
# get encoder value from motor. at this stage self.encoder contains the persistent value
|
||||
encoder = self._read_axispar(ENCODER_ADR, ANGLE_SCALE) + self.zero
|
||||
self.fix_encoder(encoder)
|
||||
except Exception as e:
|
||||
self.log.error('fix_encoder failed with %r', e)
|
||||
|
||||
def startModule(self, started_callback):
|
||||
# get encoder value from motor. at this stage self.encoder contains the persistent value
|
||||
encoder = self.get('encoder')
|
||||
encoder += self.zero
|
||||
self.fix_encoder(encoder)
|
||||
super().startModule(started_callback)
|
||||
start_events.queue(fix_encoder)
|
||||
|
||||
def fix_encoder(self, encoder_from_hw):
|
||||
"""fix encoder value
|
||||
@ -211,14 +188,52 @@ class Motor(PersistentMixin, HasIodev, Drivable):
|
||||
# calculate nearest, most probable value
|
||||
adjusted_encoder = encoder_from_hw + round((self.encoder - encoder_from_hw) / 360.) * 360
|
||||
if abs(self.encoder - adjusted_encoder) >= self.encoder_tolerance:
|
||||
# encoder module0 360 has changed
|
||||
# encoder modulo 360 has changed
|
||||
self.log.error('saved encoder value (%.2f) does not match reading (%.2f %.2f)',
|
||||
self.encoder, encoder_from_hw, adjusted_encoder)
|
||||
if adjusted_encoder != encoder_from_hw:
|
||||
self.log.info('take next closest encoder value (%.2f)' % adjusted_encoder)
|
||||
self._need_reset = True
|
||||
self.status = self.Status.ERROR, 'saved encoder value does not match reading'
|
||||
self.set('encoder', adjusted_encoder - self.zero, check=False)
|
||||
self._write_axispar(adjusted_encoder - self.zero, ENCODER_ADR, ANGLE_SCALE, readback=False)
|
||||
|
||||
def _read_axispar(self, adr, scale=1):
|
||||
value = self.comm(GET_AXIS_PAR, adr)
|
||||
# do not apply scale when 1 (datatype might not be float)
|
||||
return value if scale == 1 else value * scale
|
||||
|
||||
def _write_axispar(self, value, adr, scale=1, readback=True):
|
||||
rawvalue = round(value / scale)
|
||||
self.comm(SET_AXIS_PAR, adr, rawvalue)
|
||||
if readback:
|
||||
result = self.comm(GET_AXIS_PAR, adr)
|
||||
if result != rawvalue:
|
||||
raise HardwareError('result for adr=%d scale=%g does not match %g != %g'
|
||||
% (adr, scale, result * scale, value))
|
||||
return result * scale
|
||||
return rawvalue * scale
|
||||
|
||||
@ReadHandler(HW_ARGS)
|
||||
def read_hwparam(self, pname):
|
||||
"""handle read for HwParam"""
|
||||
args = HW_ARGS[pname]
|
||||
reply = self._read_axispar(*args)
|
||||
try:
|
||||
value = getattr(self, pname)
|
||||
except Exception:
|
||||
return reply
|
||||
if reply != value:
|
||||
if not self.parameters[pname].readonly:
|
||||
# this should not happen
|
||||
self.log.warning('hw parameter %s has changed from %r to %r, write again', pname, value, reply)
|
||||
self._write_axispar(value, *args, readback=False)
|
||||
reply = self._read_axispar(*args)
|
||||
return reply
|
||||
|
||||
@WriteHandler(HW_ARGS)
|
||||
def write_hwparam(self, pname, value):
|
||||
"""handler write for HwParam"""
|
||||
return self._write_axispar(value, *HW_ARGS[pname])
|
||||
|
||||
def read_value(self):
|
||||
encoder = self.read_encoder()
|
||||
@ -240,7 +255,7 @@ class Motor(PersistentMixin, HasIodev, Drivable):
|
||||
self._need_reset = True
|
||||
self.status = self.Status.ERROR, 'power loss'
|
||||
# or should we just fix instead of error status?
|
||||
# self.set('steppos', self.steppos - self.zero, check=False)
|
||||
# self._write_axispar(self.steppos - self.zero, readback=False)
|
||||
self.comm(SET_GLOB_PAR, 255, 1, bank=2) # set initialized flag
|
||||
self._started = 0
|
||||
|
||||
@ -256,13 +271,10 @@ class Motor(PersistentMixin, HasIodev, Drivable):
|
||||
self.log.error('encoder (%.2f) does not match internal pos (%.2f)', self.encoder, self.steppos)
|
||||
return self.Status.ERROR, 'encoder does not match internal pos'
|
||||
return self.status
|
||||
now = self.parameters['steppos'].timestamp
|
||||
if self.steppos != oldpos:
|
||||
self._last_change = now
|
||||
return self.Status.BUSY, 'moving'
|
||||
if now < self._last_change + 0.2 and not (self.read_target_reached() or self.read_move_status()
|
||||
or self.read_error_bits()):
|
||||
if oldpos != self.steppos or not (self.read_target_reached() or self.read_move_status()
|
||||
or self.read_error_bits()):
|
||||
return self.Status.BUSY, 'moving'
|
||||
# TODO: handle the different errors from move_status and error_bits
|
||||
diff = self.target - self.encoder
|
||||
if abs(diff) <= self.tolerance:
|
||||
self._started = 0
|
||||
@ -273,9 +285,8 @@ class Motor(PersistentMixin, HasIodev, Drivable):
|
||||
|
||||
def write_target(self, target):
|
||||
self.read_value() # make sure encoder and steppos are fresh
|
||||
if self.maxcurrent >= self.safe_current + CURRENT_SCALE and (
|
||||
abs(target - self.encoder) > self.move_limit + self.tolerance):
|
||||
raise BadValueError('can not move more than %s deg %g %g' % (self.move_limit, self.encoder, target))
|
||||
if abs(target - self.encoder) > self.move_limit:
|
||||
raise BadValueError('can not move more than %s deg' % self.move_limit)
|
||||
diff = self.encoder - self.steppos
|
||||
if self._need_reset:
|
||||
raise HardwareError('need reset (%s)' % self.status[1])
|
||||
@ -284,90 +295,28 @@ class Motor(PersistentMixin, HasIodev, Drivable):
|
||||
self._need_reset = True
|
||||
self.status = self.Status.ERROR, 'encoder does not match internal pos'
|
||||
raise HardwareError('need reset (encoder does not match internal pos)')
|
||||
self.set('steppos', self.encoder - self.zero, check=False)
|
||||
self._write_axispar(self.encoder - self.zero, STEPPOS_ADR, ANGLE_SCALE)
|
||||
self._started = time.time()
|
||||
self.log.debug('move to %.1f', target)
|
||||
self.log.info('move to %.1f', target)
|
||||
self.comm(MOVE, 0, (target - self.zero) / ANGLE_SCALE)
|
||||
self.status = self.Status.BUSY, 'changed target'
|
||||
return target
|
||||
|
||||
def write_zero(self, value):
|
||||
diff = value - self.zero
|
||||
self.encoder += diff
|
||||
self.steppos += diff
|
||||
self.value += diff
|
||||
return value
|
||||
self.zero = value
|
||||
self.read_value() # apply zero to encoder, steppos and value
|
||||
return Done
|
||||
|
||||
def read_encoder(self):
|
||||
return self.get('encoder') + self.zero
|
||||
return self._read_axispar(ENCODER_ADR, ANGLE_SCALE) + self.zero
|
||||
|
||||
def read_steppos(self):
|
||||
return self.get('steppos') + self.zero
|
||||
|
||||
def read_encoder_tolerance(self):
|
||||
return self.get('encoder_tolerance')
|
||||
|
||||
def write_encoder_tolerance(self, value):
|
||||
return self.set('encoder_tolerance', value)
|
||||
|
||||
def read_target_reached(self):
|
||||
return self.get('target_reached')
|
||||
|
||||
def read_speed(self):
|
||||
return self.get('speed')
|
||||
|
||||
def write_speed(self, value):
|
||||
return self.set('speed', value)
|
||||
|
||||
def read_minspeed(self):
|
||||
return self.get('minspeed')
|
||||
|
||||
def write_minspeed(self, value):
|
||||
return self.set('minspeed', value)
|
||||
|
||||
def read_currentspeed(self):
|
||||
return self.get('currentspeed')
|
||||
|
||||
def read_acceleration(self):
|
||||
return self.get('acceleration')
|
||||
|
||||
def write_acceleration(self, value):
|
||||
return self.set('acceleration', value)
|
||||
|
||||
def read_maxcurrent(self):
|
||||
return self.get('maxcurrent')
|
||||
|
||||
def write_maxcurrent(self, value):
|
||||
return self.set('maxcurrent', value)
|
||||
|
||||
def read_standby_current(self):
|
||||
return self.get('standby_current')
|
||||
|
||||
def write_standby_current(self, value):
|
||||
return self.set('standby_current', value)
|
||||
|
||||
def read_free_wheeling(self):
|
||||
return self.get('free_wheeling')
|
||||
|
||||
def write_free_wheeling(self, value):
|
||||
return self.set('free_wheeling', value)
|
||||
|
||||
def read_power_down_delay(self):
|
||||
return self.get('power_down_delay')
|
||||
|
||||
def write_power_down_delay(self, value):
|
||||
return self.set('power_down_delay', value)
|
||||
|
||||
def read_move_status(self):
|
||||
return self.get('move_status')
|
||||
|
||||
def read_error_bits(self):
|
||||
return self.get('error_bits')
|
||||
return self._read_axispar(STEPPOS_ADR, ANGLE_SCALE) + self.zero
|
||||
|
||||
@Command(FloatRange())
|
||||
def set_zero(self, value):
|
||||
raw = self.read_value() - self.zero
|
||||
self.write_zero(value - raw)
|
||||
"""adjust zero"""
|
||||
self.write_zero(value - self.read_value())
|
||||
|
||||
def read_baudrate(self):
|
||||
return self.comm(GET_GLOB_PAR, 65)
|
||||
@ -380,14 +329,14 @@ class Motor(PersistentMixin, HasIodev, Drivable):
|
||||
"""set steppos to encoder value, if not within tolerance"""
|
||||
if self._started:
|
||||
raise IsBusyError('can not reset while moving')
|
||||
tol = ENCODER_RESOLUTION
|
||||
tol = ENCODER_RESOLUTION * 1.1
|
||||
for itry in range(10):
|
||||
diff = self.read_encoder() - self.read_steppos()
|
||||
if abs(diff) <= tol:
|
||||
self._need_reset = False
|
||||
self.status = self.Status.IDLE, 'ok'
|
||||
return
|
||||
self.set('steppos', self.encoder - self.zero, check=False)
|
||||
self._write_axispar(self.encoder - self.zero, STEPPOS_ADR, ANGLE_SCALE, readback=False)
|
||||
self.comm(MOVE, 0, (self.encoder - self.zero) / ANGLE_SCALE)
|
||||
time.sleep(0.1)
|
||||
if itry > 5:
|
||||
@ -397,23 +346,33 @@ class Motor(PersistentMixin, HasIodev, Drivable):
|
||||
|
||||
@Command()
|
||||
def stop(self):
|
||||
"""stop motor immediately"""
|
||||
self.comm(MOTOR_STOP, 0)
|
||||
self.status = self.Status.IDLE, 'stopped'
|
||||
self.target = self.value # indicate to customers that this was stopped
|
||||
self._started = 0
|
||||
|
||||
#@Command()
|
||||
#def step(self):
|
||||
# self.comm(MOVE, 1, FULL_STEP / ANGLE_SCALE)
|
||||
@Command()
|
||||
def step_forward(self):
|
||||
"""move one full step forwards
|
||||
|
||||
#@Command()
|
||||
#def back(self):
|
||||
# self.comm(MOVE, 1, - FULL_STEP / ANGLE_SCALE)
|
||||
for quick tests
|
||||
"""
|
||||
self.comm(MOVE, 1, FULL_STEP / ANGLE_SCALE)
|
||||
|
||||
#@Command(IntRange(), result=IntRange())
|
||||
#def get_axis_par(self, adr):
|
||||
# return self.comm(GET_AXIS_PAR, adr)
|
||||
@Command()
|
||||
def step_back(self):
|
||||
"""move one full step backwards
|
||||
|
||||
#@Command((IntRange(), FloatRange()), result=IntRange())
|
||||
#def set_axis_par(self, adr, value):
|
||||
# return self.comm(SET_AXIS_PAR, adr, value)
|
||||
for quick tests
|
||||
"""
|
||||
self.comm(MOVE, 1, - FULL_STEP / ANGLE_SCALE)
|
||||
|
||||
@Command(IntRange(), result=IntRange())
|
||||
def get_axis_par(self, adr):
|
||||
"""get arbitrary motor parameter"""
|
||||
return self.comm(GET_AXIS_PAR, adr)
|
||||
|
||||
@Command((IntRange(), IntRange()), result=IntRange())
|
||||
def set_axis_par(self, adr, value):
|
||||
"""set arbitrary motor parameter"""
|
||||
return self.comm(SET_AXIS_PAR, adr, value)
|
||||
|
@ -107,7 +107,7 @@ class Uniax(PersistentMixin, Drivable):
|
||||
return True
|
||||
return False
|
||||
|
||||
def next_action(self, action, do_now=True):
|
||||
def next_action(self, action):
|
||||
"""call next action
|
||||
|
||||
:param action: function to be called next time
|
||||
@ -116,8 +116,6 @@ class Uniax(PersistentMixin, Drivable):
|
||||
self._action = action
|
||||
self._init_action = True
|
||||
self.log.info('action %r', action.__name__)
|
||||
if do_now:
|
||||
self._next_cycle = False
|
||||
|
||||
def init_action(self):
|
||||
"""return true when called the first time after next_action"""
|
||||
@ -126,13 +124,6 @@ class Uniax(PersistentMixin, Drivable):
|
||||
return True
|
||||
return False
|
||||
|
||||
def execute_action(self):
|
||||
for _ in range(5): # limit number of subsequent actions in one cycle
|
||||
self._next_cycle = True
|
||||
self._action(self.value, self.target)
|
||||
if self._next_cycle:
|
||||
break
|
||||
|
||||
def zero_pos(self, value,):
|
||||
"""get high_pos or low_pos, depending on sign of value
|
||||
|
||||
@ -173,11 +164,11 @@ class Uniax(PersistentMixin, Drivable):
|
||||
self.log.info('motor stopped - substantial force detected: %g', force)
|
||||
self._motor.stop()
|
||||
elif self.init_action():
|
||||
self.next_action(self.adjust, True)
|
||||
self.next_action(self.adjust)
|
||||
return
|
||||
if abs(force) > self.hysteresis:
|
||||
self.set_zero_pos(force, self._motor.read_value())
|
||||
self.next_action(self.adjust, True)
|
||||
self.next_action(self.adjust)
|
||||
return
|
||||
if force * sign < -self.hysteresis:
|
||||
self._previous_force = force
|
||||
@ -333,7 +324,7 @@ class Uniax(PersistentMixin, Drivable):
|
||||
return Done
|
||||
if self.zero_pos(force) is None and abs(force) > self.hysteresis and self._filtered:
|
||||
self.set_zero_pos(force, self._motor.read_value())
|
||||
self.execute_action()
|
||||
self._action(self.value, self.target)
|
||||
return Done
|
||||
|
||||
def write_target(self, target):
|
||||
@ -351,7 +342,10 @@ class Uniax(PersistentMixin, Drivable):
|
||||
self._cnt_rderr = 0
|
||||
self._cnt_wrerr = 0
|
||||
self.status = 'BUSY', 'changed target'
|
||||
self.next_action(self.find, False)
|
||||
if self.value * math.copysign(1, target) > self.hysteresis:
|
||||
self.next_action(self.adjust)
|
||||
else:
|
||||
self.next_action(self.find)
|
||||
return target
|
||||
|
||||
@Command()
|
||||
|
80
test/test_attach.py
Normal file
80
test/test_attach.py
Normal 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
|
@ -28,7 +28,9 @@ import pytest
|
||||
from secop.datatypes import ArrayOf, BLOBType, BoolType, \
|
||||
CommandType, ConfigError, DataType, Enum, EnumType, FloatRange, \
|
||||
IntRange, ProgrammingError, ScaledInteger, StatusType, \
|
||||
StringType, StructOf, TextType, TupleOf, get_datatype
|
||||
StringType, StructOf, TextType, TupleOf, get_datatype, \
|
||||
DiscouragedConversion
|
||||
from secop.lib import generalConfig
|
||||
|
||||
|
||||
def copytest(dt):
|
||||
@ -36,6 +38,7 @@ def copytest(dt):
|
||||
assert dt.export_datatype() == dt.copy().export_datatype()
|
||||
assert dt != dt.copy()
|
||||
|
||||
|
||||
def test_DataType():
|
||||
dt = DataType()
|
||||
with pytest.raises(NotImplementedError):
|
||||
@ -116,7 +119,6 @@ def test_IntRange():
|
||||
dt('1.3')
|
||||
dt(1)
|
||||
dt(0)
|
||||
dt('1')
|
||||
with pytest.raises(ProgrammingError):
|
||||
IntRange('xc', 'Yx')
|
||||
|
||||
@ -132,6 +134,7 @@ def test_IntRange():
|
||||
with pytest.raises(ConfigError):
|
||||
dt.checkProperties()
|
||||
|
||||
|
||||
def test_ScaledInteger():
|
||||
dt = ScaledInteger(0.01, -3, 3)
|
||||
copytest(dt)
|
||||
@ -407,6 +410,7 @@ def test_ArrayOf():
|
||||
dt = ArrayOf(EnumType('myenum', single=0), 5)
|
||||
copytest(dt)
|
||||
|
||||
|
||||
def test_TupleOf():
|
||||
# test constructor catching illegal arguments
|
||||
with pytest.raises(ValueError):
|
||||
@ -641,6 +645,7 @@ def test_oneway_compatible(dt, contained_in):
|
||||
with pytest.raises(ValueError):
|
||||
contained_in.compatible(dt)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('dt1, dt2', [
|
||||
(FloatRange(-5.5, 5.5), ScaledInteger(10, -5.5, 5.5)),
|
||||
(IntRange(0,1), BoolType()),
|
||||
@ -650,6 +655,7 @@ def test_twoway_compatible(dt1, dt2):
|
||||
dt1.compatible(dt1)
|
||||
dt2.compatible(dt2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('dt1, dt2', [
|
||||
(StringType(), FloatRange()),
|
||||
(IntRange(-10, 10), StringType()),
|
||||
@ -665,3 +671,12 @@ def test_incompatible(dt1, dt2):
|
||||
dt1.compatible(dt2)
|
||||
with pytest.raises(ValueError):
|
||||
dt2.compatible(dt1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('dt', [FloatRange(), IntRange(), ScaledInteger(1)])
|
||||
def test_lazy_validation(dt):
|
||||
generalConfig.defaults['lazy_number_validation'] = True
|
||||
dt('0')
|
||||
generalConfig.defaults['lazy_number_validation'] = False
|
||||
with pytest.raises(DiscouragedConversion):
|
||||
dt('0')
|
||||
|
234
test/test_handler.py
Normal file
234
test/test_handler.py
Normal file
@ -0,0 +1,234 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# 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
|
@ -71,7 +71,11 @@ class Data:
|
||||
|
||||
|
||||
class DispatcherStub:
|
||||
OMIT_UNCHANGED_WITHIN = 0
|
||||
# the first update from the poller comes a very short time after the
|
||||
# initial value from the timestamp. However, in the test below
|
||||
# the second update happens after the updates dict is cleared
|
||||
# -> we have to inhibit the 'omit unchanged update' feature
|
||||
omit_unchanged_within = 0
|
||||
|
||||
def __init__(self, updates):
|
||||
self.updates = updates
|
||||
@ -108,6 +112,7 @@ def test_IOHandler():
|
||||
group1 = Hdl('group1', 'SIMPLE?', '%g')
|
||||
group2 = Hdl('group2', 'CMD?%(channel)d', '%g,%s,%d')
|
||||
|
||||
|
||||
class Module1(Module):
|
||||
channel = Property('the channel', IntRange(), default=3)
|
||||
loop = Property('the loop', IntRange(), default=2)
|
||||
@ -115,7 +120,7 @@ def test_IOHandler():
|
||||
real = Parameter('a float value', FloatRange(), default=12.3, handler=group2, readonly=False)
|
||||
text = Parameter('a string value', StringType(), default='x', handler=group2, readonly=False)
|
||||
|
||||
def sendRecv(self, command):
|
||||
def communicate(self, command):
|
||||
assert data.pop('command') == command
|
||||
return data.pop('reply')
|
||||
|
||||
@ -141,7 +146,7 @@ def test_IOHandler():
|
||||
print(updates)
|
||||
updates.clear() # get rid of updates from initialisation
|
||||
|
||||
# for sendRecv
|
||||
# for communicate
|
||||
data.push('command', 'SIMPLE?')
|
||||
data.push('reply', '4.51')
|
||||
# for analyze_group1
|
||||
@ -154,7 +159,7 @@ def test_IOHandler():
|
||||
assert updates.pop('simple') == 45.1
|
||||
assert not updates
|
||||
|
||||
# for sendRecv
|
||||
# for communicate
|
||||
data.push('command', 'CMD?3')
|
||||
data.push('reply', '1.23,text,5')
|
||||
# for analyze_group2
|
||||
@ -167,7 +172,7 @@ def test_IOHandler():
|
||||
assert data.empty()
|
||||
assert not updates
|
||||
|
||||
# for sendRecv
|
||||
# for communicate
|
||||
data.push('command', 'CMD?3')
|
||||
data.push('reply', '1.23,text,5')
|
||||
# for analyze_group2
|
||||
@ -178,7 +183,7 @@ def test_IOHandler():
|
||||
data.push('self', 12.3, 'string')
|
||||
data.push('new', 12.3, 'FOO')
|
||||
data.push('changed', 1.23, 'foo', 9)
|
||||
# for sendRecv
|
||||
# for communicate
|
||||
data.push('command', 'CMD 3,1.23,foo,9|CMD?3')
|
||||
data.push('reply', '1.23,foo,9')
|
||||
# for analyze_group2
|
||||
|
@ -64,6 +64,7 @@ def test_EnumMember():
|
||||
assert a != 3
|
||||
assert a == 1
|
||||
|
||||
|
||||
def test_Enum():
|
||||
e1 = Enum('e1')
|
||||
e2 = Enum('e2', e1, a=1, b=3)
|
||||
@ -75,3 +76,4 @@ def test_Enum():
|
||||
assert e2.b > e3.a
|
||||
assert e3.c >= e2.a
|
||||
assert e3.b <= e2.b
|
||||
assert Enum({'self': 0, 'other': 1})('self') == 0
|
||||
|
274
test/test_logging.py
Normal file
274
test/test_logging.py
Normal 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)
|
@ -22,19 +22,24 @@
|
||||
# *****************************************************************************
|
||||
"""test data types."""
|
||||
|
||||
import sys
|
||||
import threading
|
||||
|
||||
import pytest
|
||||
|
||||
from secop.datatypes import BoolType, FloatRange, StringType, IntRange, CommandType
|
||||
from secop.datatypes import BoolType, FloatRange, StringType, IntRange, ScaledInteger
|
||||
from secop.errors import ProgrammingError, ConfigError
|
||||
from secop.modules import Communicator, Drivable, Readable, Module
|
||||
from secop.params import Command, Parameter
|
||||
from secop.poller import BasicPoller
|
||||
from secop.rwhandler import ReadHandler, WriteHandler, nopoll
|
||||
from secop.lib import generalConfig
|
||||
|
||||
|
||||
class DispatcherStub:
|
||||
OMIT_UNCHANGED_WITHIN = 0
|
||||
# the first update from the poller comes a very short time after the
|
||||
# initial value from the timestamp. However, in the test below
|
||||
# the second update happens after the updates dict is cleared
|
||||
# -> we have to inhibit the 'omit unchanged update' feature
|
||||
omit_unchanged_within = 0
|
||||
|
||||
def __init__(self, updates):
|
||||
self.updates = updates
|
||||
@ -48,9 +53,10 @@ class DispatcherStub:
|
||||
|
||||
|
||||
class LoggerStub:
|
||||
def debug(self, *args):
|
||||
print(*args)
|
||||
info = warning = exception = debug
|
||||
def debug(self, fmt, *args):
|
||||
print(fmt % args)
|
||||
info = warning = exception = error = debug
|
||||
handlers = []
|
||||
|
||||
|
||||
logger = LoggerStub()
|
||||
@ -61,13 +67,23 @@ class ServerStub:
|
||||
self.dispatcher = DispatcherStub(updates)
|
||||
|
||||
|
||||
class DummyMultiEvent(threading.Event):
|
||||
def get_trigger(self):
|
||||
|
||||
def trigger(event=self):
|
||||
event.set()
|
||||
sys.exit()
|
||||
return trigger
|
||||
|
||||
|
||||
def test_Communicator():
|
||||
o = Communicator('communicator', LoggerStub(), {'.description':''}, ServerStub({}))
|
||||
o = Communicator('communicator', LoggerStub(), {'.description': ''}, ServerStub({}))
|
||||
o.earlyInit()
|
||||
o.initModule()
|
||||
event = threading.Event()
|
||||
o.startModule(event.set)
|
||||
assert event.is_set() # event should be set immediately
|
||||
event = DummyMultiEvent()
|
||||
o.initModule()
|
||||
o.startModule(event)
|
||||
assert event.wait(timeout=0.1)
|
||||
|
||||
|
||||
def test_ModuleMagic():
|
||||
@ -83,14 +99,13 @@ def test_ModuleMagic():
|
||||
a1 = Parameter('a1', datatype=BoolType(), default=False)
|
||||
a2 = Parameter('a2', datatype=BoolType(), default=True)
|
||||
value = Parameter(datatype=StringType(), default='first')
|
||||
target = Parameter(datatype=StringType(), default='')
|
||||
|
||||
@Command(argument=BoolType(), result=BoolType())
|
||||
def cmd2(self, arg):
|
||||
"""another stuff"""
|
||||
return not arg
|
||||
|
||||
pollerClass = BasicPoller
|
||||
|
||||
def read_param1(self):
|
||||
return True
|
||||
|
||||
@ -100,12 +115,16 @@ def test_ModuleMagic():
|
||||
def read_a1(self):
|
||||
return True
|
||||
|
||||
@nopoll
|
||||
def read_a2(self):
|
||||
return True
|
||||
|
||||
def read_value(self):
|
||||
return 'second'
|
||||
|
||||
def read_status(self):
|
||||
return 'IDLE', 'ok'
|
||||
|
||||
with pytest.raises(ProgrammingError):
|
||||
class Mod1(Module): # pylint: disable=unused-variable
|
||||
def do_this(self): # old style command
|
||||
@ -128,15 +147,19 @@ def test_ModuleMagic():
|
||||
return arg
|
||||
|
||||
value = Parameter(datatype=FloatRange(unit='deg'))
|
||||
target = Parameter(datatype=FloatRange(), default=0)
|
||||
a1 = Parameter(datatype=FloatRange(unit='$/s'), readonly=False)
|
||||
b2 = Parameter('<b2>', datatype=BoolType(), default=True,
|
||||
poll=True, readonly=False, initwrite=True)
|
||||
# remark: it might be a programming error to override the datatype
|
||||
# and not overriding the read_* method. This is not checked!
|
||||
b2 = Parameter('<b2>', datatype=StringType(), default='empty',
|
||||
readonly=False, initwrite=True)
|
||||
|
||||
def write_a1(self, value):
|
||||
self._a1_written = value
|
||||
return value
|
||||
|
||||
def write_b2(self, value):
|
||||
value = value.upper()
|
||||
self._b2_written = value
|
||||
return value
|
||||
|
||||
@ -166,33 +189,38 @@ def test_ModuleMagic():
|
||||
|
||||
# check for inital updates working properly
|
||||
o1 = Newclass1('o1', logger, {'.description':''}, srv)
|
||||
expectedBeforeStart = {'target': 0.0, 'status': (Drivable.Status.IDLE, ''),
|
||||
expectedBeforeStart = {'target': '', 'status': (Drivable.Status.IDLE, ''),
|
||||
'param1': False, 'param2': 1.0, 'a1': 0.0, 'a2': True, 'pollinterval': 5.0,
|
||||
'value': 'first'}
|
||||
assert updates.pop('o1') == expectedBeforeStart
|
||||
o1.earlyInit()
|
||||
event = threading.Event()
|
||||
o1.startModule(event.set)
|
||||
event.wait()
|
||||
event = DummyMultiEvent()
|
||||
o1.initModule()
|
||||
o1.startModule(event)
|
||||
assert event.wait(timeout=0.1)
|
||||
# should contain polled values
|
||||
expectedAfterStart = {'status': (Drivable.Status.IDLE, ''),
|
||||
'value': 'second'}
|
||||
expectedAfterStart = {
|
||||
'status': (Drivable.Status.IDLE, 'ok'), 'value': 'second',
|
||||
'param1': True, 'param2': 0.0, 'a1': True}
|
||||
assert updates.pop('o1') == expectedAfterStart
|
||||
|
||||
# check in addition if parameters are written
|
||||
o2 = Newclass2('o2', logger, {'.description':'', 'a1': 2.7}, srv)
|
||||
# no update for b2, as this has to be written
|
||||
expectedBeforeStart['a1'] = 2.7
|
||||
expectedBeforeStart['target'] = 0.0
|
||||
assert updates.pop('o2') == expectedBeforeStart
|
||||
o2.earlyInit()
|
||||
event = threading.Event()
|
||||
o2.startModule(event.set)
|
||||
event.wait()
|
||||
event = DummyMultiEvent()
|
||||
o2.initModule()
|
||||
o2.startModule(event)
|
||||
assert event.wait(timeout=0.1)
|
||||
# value has changed type, b2 and a1 are written
|
||||
expectedAfterStart.update(value=0, b2=True, a1=2.7)
|
||||
expectedAfterStart.update(value=0, b2='EMPTY', a1=True)
|
||||
# ramerk: a1=True: this behaviour is a Porgamming error
|
||||
assert updates.pop('o2') == expectedAfterStart
|
||||
assert o2._a1_written == 2.7
|
||||
assert o2._b2_written is True
|
||||
assert o2._b2_written == 'EMPTY'
|
||||
|
||||
assert not updates
|
||||
|
||||
@ -206,13 +234,15 @@ def test_ModuleMagic():
|
||||
# check '$' in unit works properly
|
||||
assert o2.parameters['a1'].datatype.unit == 'mm/s'
|
||||
cfg = Newclass2.configurables
|
||||
assert set(cfg.keys()) == {'export', 'group', 'description',
|
||||
assert set(cfg.keys()) == {
|
||||
'export', 'group', 'description', 'disable_value_range_check',
|
||||
'meaning', 'visibility', 'implementation', 'interface_classes', 'target', 'stop',
|
||||
'status', 'param1', 'param2', 'cmd', 'a2', 'pollinterval', 'b2', 'cmd2', 'value',
|
||||
'a1'}
|
||||
assert set(cfg['value'].keys()) == {'group', 'export', 'relative_resolution',
|
||||
'status', 'param1', 'param2', 'cmd', 'a2', 'pollinterval', 'slowinterval', 'b2',
|
||||
'cmd2', 'value', 'a1'}
|
||||
assert set(cfg['value'].keys()) == {
|
||||
'group', 'export', 'relative_resolution',
|
||||
'visibility', 'unit', 'default', 'datatype', 'fmtstr',
|
||||
'absolute_resolution', 'poll', 'max', 'min', 'readonly', 'constant',
|
||||
'absolute_resolution', 'max', 'min', 'readonly', 'constant',
|
||||
'description', 'needscfg'}
|
||||
|
||||
# check on the level of classes
|
||||
@ -260,9 +290,101 @@ def test_param_inheritance():
|
||||
Base('o', logger, {'description': ''}, srv)
|
||||
|
||||
|
||||
def test_mixin():
|
||||
# srv = ServerStub({})
|
||||
def test_command_inheritance():
|
||||
class Base(Module):
|
||||
@Command(BoolType(), visibility=2)
|
||||
def cmd(self, arg):
|
||||
"""base"""
|
||||
|
||||
class Sub1(Base):
|
||||
@Command(group='grp')
|
||||
def cmd(self, arg):
|
||||
"""first"""
|
||||
|
||||
class Sub2(Sub1):
|
||||
@Command(None, result=BoolType())
|
||||
def cmd(self): # pylint: disable=arguments-differ
|
||||
"""second"""
|
||||
|
||||
class Sub3(Base):
|
||||
# when either argument or result is given, the other one is assumed to be None
|
||||
# i.e. here we override the argument with None
|
||||
@Command(result=FloatRange())
|
||||
def cmd(self, arg):
|
||||
"""third"""
|
||||
|
||||
assert Sub1.accessibles['cmd'].for_export() == {
|
||||
'description': 'first', 'group': 'grp', 'visibility': 2,
|
||||
'datainfo': {'type': 'command', 'argument': {'type': 'bool'}}
|
||||
}
|
||||
|
||||
assert Sub2.accessibles['cmd'].for_export() == {
|
||||
'description': 'second', 'group': 'grp', 'visibility': 2,
|
||||
'datainfo': {'type': 'command', 'result': {'type': 'bool'}}
|
||||
}
|
||||
|
||||
assert Sub3.accessibles['cmd'].for_export() == {
|
||||
'description': 'third', 'visibility': 2,
|
||||
'datainfo': {'type': 'command', 'result': {'type': 'double'}}
|
||||
}
|
||||
|
||||
for cls in locals().values():
|
||||
if hasattr(cls, 'accessibles'):
|
||||
for p in cls.accessibles.values():
|
||||
assert isinstance(p.ownProperties, dict)
|
||||
assert p.copy().ownProperties == {}
|
||||
|
||||
|
||||
def test_command_check():
|
||||
srv = ServerStub({})
|
||||
|
||||
class Good(Module):
|
||||
@Command(description='available')
|
||||
def with_description(self):
|
||||
pass
|
||||
@Command()
|
||||
def with_docstring(self):
|
||||
"""docstring"""
|
||||
|
||||
Good('o', logger, {'description': ''}, srv)
|
||||
|
||||
class Bad1(Module):
|
||||
@Command
|
||||
def without_description(self):
|
||||
pass
|
||||
|
||||
class Bad2(Module):
|
||||
@Command()
|
||||
def without_description(self):
|
||||
pass
|
||||
|
||||
for cls in Bad1, Bad2:
|
||||
with pytest.raises(ConfigError) as e_info:
|
||||
cls('o', logger, {'description': ''}, srv)
|
||||
assert 'description' in repr(e_info.value)
|
||||
|
||||
class BadDatatype(Module):
|
||||
@Command(FloatRange(0.1, 0.9), result=FloatRange())
|
||||
def cmd(self):
|
||||
"""valid command"""
|
||||
|
||||
BadDatatype('o', logger, {'description': ''}, srv)
|
||||
|
||||
# test for command property checking
|
||||
with pytest.raises(ProgrammingError):
|
||||
BadDatatype('o', logger, {
|
||||
'description': '',
|
||||
'cmd.argument': {'type': 'double', 'min': 1, 'max': 0},
|
||||
}, srv)
|
||||
|
||||
with pytest.raises(ProgrammingError):
|
||||
BadDatatype('o', logger, {
|
||||
'description': '',
|
||||
'cmd.visibility': 'invalid',
|
||||
}, srv)
|
||||
|
||||
|
||||
def test_mixin():
|
||||
class Mixin: # no need to inherit from Module or HasAccessible
|
||||
value = Parameter(unit='K') # missing datatype and description acceptable in mixins
|
||||
param1 = Parameter('no datatype yet', fmtstr='%.5f')
|
||||
@ -276,7 +398,7 @@ def test_mixin():
|
||||
param1 = Parameter(datatype=FloatRange())
|
||||
|
||||
with pytest.raises(ProgrammingError):
|
||||
class MixedModule(Mixin):
|
||||
class MixedModule(Mixin): # pylint: disable=unused-variable
|
||||
param1 = Parameter('', FloatRange(), fmtstr=0) # fmtstr must be a string
|
||||
|
||||
assert repr(MixedDrivable.status.datatype) == repr(Drivable.status.datatype)
|
||||
@ -305,10 +427,29 @@ def test_mixin():
|
||||
}, srv)
|
||||
|
||||
|
||||
def test_override():
|
||||
class Mod(Drivable):
|
||||
value = 5 # overriding the default value
|
||||
|
||||
def stop(self):
|
||||
"""no decorator needed"""
|
||||
|
||||
assert Mod.value.default == 5
|
||||
assert Mod.stop.description == "no decorator needed"
|
||||
|
||||
class Mod2(Drivable):
|
||||
@Command()
|
||||
def stop(self):
|
||||
pass
|
||||
|
||||
assert Mod2.stop.description == Drivable.stop.description
|
||||
|
||||
|
||||
def test_command_config():
|
||||
class Mod(Module):
|
||||
@Command(IntRange(0, 1), result=IntRange(0, 1))
|
||||
def convert(self, value):
|
||||
"""dummy conversion"""
|
||||
return value
|
||||
|
||||
srv = ServerStub({})
|
||||
@ -332,3 +473,189 @@ def test_command_config():
|
||||
'result': {'type': 'bool'},
|
||||
}
|
||||
|
||||
|
||||
def test_command_none():
|
||||
srv = ServerStub({})
|
||||
|
||||
class Mod(Drivable):
|
||||
pass
|
||||
|
||||
class Mod2(Drivable):
|
||||
stop = None
|
||||
|
||||
assert 'stop' in Mod('o', logger, {'description': ''}, srv).accessibles
|
||||
assert 'stop' not in Mod2('o', logger, {'description': ''}, srv).accessibles
|
||||
|
||||
|
||||
def test_bad_method():
|
||||
class Mod0(Drivable): # pylint: disable=unused-variable
|
||||
def write_target(self, value):
|
||||
pass
|
||||
|
||||
with pytest.raises(ProgrammingError):
|
||||
class Mod1(Drivable): # pylint: disable=unused-variable
|
||||
def write_taget(self, value):
|
||||
pass
|
||||
|
||||
class Mod2(Drivable): # pylint: disable=unused-variable
|
||||
def read_value(self, value):
|
||||
pass
|
||||
|
||||
with pytest.raises(ProgrammingError):
|
||||
class Mod3(Drivable): # pylint: disable=unused-variable
|
||||
def read_valu(self, value):
|
||||
pass
|
||||
|
||||
|
||||
def test_generic_access():
|
||||
class Mod(Module):
|
||||
param = Parameter('handled param', StringType(), readonly=False)
|
||||
unhandled = Parameter('unhandled param', StringType(), default='', readonly=False)
|
||||
data = {'param': ''}
|
||||
|
||||
@ReadHandler(['param'])
|
||||
def read_handler(self, pname):
|
||||
value = self.data[pname]
|
||||
setattr(self, pname, value)
|
||||
return value
|
||||
|
||||
@WriteHandler(['param'])
|
||||
def write_handler(self, pname, value):
|
||||
value = value.lower()
|
||||
self.data[pname] = value
|
||||
setattr(self, pname, value)
|
||||
return value
|
||||
|
||||
updates = {}
|
||||
srv = ServerStub(updates)
|
||||
|
||||
obj = Mod('obj', logger, {'description': '', 'param': 'initial value'}, srv)
|
||||
assert obj.param == 'initial value'
|
||||
assert obj.write_param('Cheese') == 'cheese'
|
||||
assert obj.write_unhandled('Cheese') == 'Cheese'
|
||||
assert updates == {'obj': {'param': 'cheese', 'unhandled': 'Cheese'}}
|
||||
updates.clear()
|
||||
assert obj.write_param('Potato') == 'potato'
|
||||
assert updates == {'obj': {'param': 'potato'}}
|
||||
updates.clear()
|
||||
assert obj.read_param() == 'potato'
|
||||
assert obj.read_unhandled()
|
||||
assert updates == {'obj': {'param': 'potato'}}
|
||||
updates.clear()
|
||||
assert updates == {}
|
||||
|
||||
|
||||
def test_duplicate_handler_name():
|
||||
with pytest.raises(ProgrammingError):
|
||||
class Mod(Module): # pylint: disable=unused-variable
|
||||
param = Parameter('handled param', StringType(), readonly=False)
|
||||
|
||||
@ReadHandler(['param'])
|
||||
def handler(self, pname):
|
||||
pass
|
||||
|
||||
@WriteHandler(['param'])
|
||||
def handler(self, pname, value): # pylint: disable=function-redefined
|
||||
pass
|
||||
|
||||
|
||||
def test_handler_overwrites_method():
|
||||
with pytest.raises(RuntimeError):
|
||||
class Mod1(Module): # pylint: disable=unused-variable
|
||||
param = Parameter('handled param', StringType(), readonly=False)
|
||||
|
||||
@ReadHandler(['param'])
|
||||
def read_handler(self, pname):
|
||||
pass
|
||||
|
||||
def read_param(self):
|
||||
pass
|
||||
|
||||
with pytest.raises(RuntimeError):
|
||||
class Mod2(Module): # pylint: disable=unused-variable
|
||||
param = Parameter('handled param', StringType(), readonly=False)
|
||||
|
||||
@WriteHandler(['param'])
|
||||
def write_handler(self, pname, value):
|
||||
pass
|
||||
|
||||
def write_param(self, value):
|
||||
pass
|
||||
|
||||
|
||||
def test_no_read_write():
|
||||
class Mod(Module):
|
||||
param = Parameter('test param', StringType(), readonly=False)
|
||||
|
||||
updates = {}
|
||||
srv = ServerStub(updates)
|
||||
|
||||
obj = Mod('obj', logger, {'description': '', 'param': 'cheese'}, srv)
|
||||
assert obj.param == 'cheese'
|
||||
assert obj.read_param() == 'cheese'
|
||||
assert updates == {'obj': {'param': 'cheese'}}
|
||||
assert obj.write_param('egg') == 'egg'
|
||||
assert obj.param == 'egg'
|
||||
assert updates == {'obj': {'param': 'egg'}}
|
||||
|
||||
|
||||
def test_incompatible_value_target():
|
||||
class Mod1(Drivable):
|
||||
value = Parameter('', FloatRange(0, 10), default=0)
|
||||
target = Parameter('', FloatRange(0, 11), default=0)
|
||||
|
||||
class Mod2(Drivable):
|
||||
value = Parameter('', FloatRange(), default=0)
|
||||
target = Parameter('', StringType(), default='')
|
||||
|
||||
class Mod3(Drivable):
|
||||
value = Parameter('', FloatRange(), default=0)
|
||||
target = Parameter('', ScaledInteger(1, 0, 10), default=0)
|
||||
|
||||
srv = ServerStub({})
|
||||
|
||||
with pytest.raises(ConfigError):
|
||||
obj = Mod1('obj', logger, {'description': ''}, srv) # pylint: disable=unused-variable
|
||||
|
||||
with pytest.raises(ProgrammingError):
|
||||
obj = Mod2('obj', logger, {'description': ''}, srv)
|
||||
|
||||
obj = Mod3('obj', logger, {'description': ''}, srv)
|
||||
|
||||
|
||||
def test_problematic_value_range():
|
||||
class Mod(Drivable):
|
||||
value = Parameter('', FloatRange(0, 10), default=0)
|
||||
target = Parameter('', FloatRange(0, 10), default=0)
|
||||
|
||||
srv = ServerStub({})
|
||||
|
||||
obj = Mod('obj', logger, {'description': '', 'value.max': 10.1}, srv) # pylint: disable=unused-variable
|
||||
|
||||
with pytest.raises(ConfigError):
|
||||
obj = Mod('obj', logger, {'description': ''}, srv)
|
||||
|
||||
class Mod2(Drivable):
|
||||
value = Parameter('', FloatRange(), default=0)
|
||||
target = Parameter('', FloatRange(), default=0)
|
||||
|
||||
obj = Mod2('obj', logger, {'description': ''}, srv)
|
||||
obj = Mod2('obj', logger, {'description': '', 'target.min': 0, 'target.max': 10}, srv)
|
||||
|
||||
with pytest.raises(ConfigError):
|
||||
obj = Mod('obj', logger, {
|
||||
'value.min': 0, 'value.max': 10,
|
||||
'target.min': 0, 'target.max': 10, 'description': ''}, srv)
|
||||
|
||||
obj = Mod('obj', logger, {'disable_value_range_check': True,
|
||||
'value.min': 0, 'value.max': 10,
|
||||
'target.min': 0, 'target.max': 10, 'description': ''}, srv)
|
||||
|
||||
generalConfig.defaults['disable_value_range_check'] = True
|
||||
|
||||
class Mod4(Drivable):
|
||||
value = Parameter('', FloatRange(0, 10), default=0)
|
||||
target = Parameter('', FloatRange(0, 10), default=0)
|
||||
obj = Mod4('obj', logger, {
|
||||
'value.min': 0, 'value.max': 10,
|
||||
'target.min': 0, 'target.max': 10, 'description': ''}, srv)
|
||||
|
60
test/test_multievent.py
Normal file
60
test/test_multievent.py
Normal 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)
|
@ -93,11 +93,17 @@ def test_Override():
|
||||
assert id(Mod.p3) == id(Base.p3)
|
||||
assert repr(Mod.p2) == repr(Base.p2) # must be a clone
|
||||
assert repr(Mod.p3) == repr(Base.p3) # must be a clone
|
||||
assert Mod.p1.default == True
|
||||
assert Mod.p1.default is True
|
||||
# manipulating default makes Base.p1 and Mod.p1 match
|
||||
Mod.p1.default = False
|
||||
assert repr(Mod.p1) == repr(Base.p1)
|
||||
|
||||
for cls in locals().values():
|
||||
if hasattr(cls, 'accessibles'):
|
||||
for p in cls.accessibles.values():
|
||||
assert isinstance(p.ownProperties, dict)
|
||||
assert p.copy().ownProperties == {}
|
||||
|
||||
|
||||
def test_Export():
|
||||
class Mod(HasAccessibles):
|
||||
|
@ -21,227 +21,133 @@
|
||||
# *****************************************************************************
|
||||
"""test poller."""
|
||||
|
||||
import sys
|
||||
import threading
|
||||
from time import time as current_time
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from secop.modules import Drivable
|
||||
from secop.poller import DYNAMIC, REGULAR, SLOW, Poller
|
||||
from secop.core import Module, Parameter, FloatRange, Readable, ReadHandler, nopoll
|
||||
from secop.lib.multievent import MultiEvent
|
||||
|
||||
Status = Drivable.Status
|
||||
|
||||
class Time:
|
||||
STARTTIME = 1000 # artificial time zero
|
||||
"""artificial time, forwarded on sleep instead of waiting"""
|
||||
def __init__(self):
|
||||
self.reset()
|
||||
self.finish = float('inf')
|
||||
self.stop = lambda : None
|
||||
self.commtime = 0.05 # time needed for 1 poll
|
||||
|
||||
def reset(self, lifetime=10):
|
||||
self.seconds = self.STARTTIME
|
||||
self.idletime = 0.0
|
||||
self.busytime = 0.0
|
||||
self.finish = self.STARTTIME + lifetime
|
||||
self.offset = 0
|
||||
|
||||
def time(self):
|
||||
if self.seconds > self.finish:
|
||||
self.finish = float('inf')
|
||||
self.stop()
|
||||
return self.seconds
|
||||
return current_time() + self.offset
|
||||
|
||||
def sleep(self, seconds):
|
||||
assert 0 <= seconds <= 24*3600
|
||||
self.idletime += seconds
|
||||
self.seconds += seconds
|
||||
|
||||
def busy(self, seconds):
|
||||
assert seconds >= 0
|
||||
self.seconds += seconds
|
||||
self.busytime += seconds
|
||||
|
||||
artime = Time() # artificial test time
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def patch_time(monkeypatch):
|
||||
monkeypatch.setattr(time, 'time', artime.time)
|
||||
self.offset += seconds
|
||||
|
||||
|
||||
class Event:
|
||||
def __init__(self):
|
||||
self.flag = False
|
||||
|
||||
def wait(self, timeout):
|
||||
artime.sleep(max(0,timeout))
|
||||
|
||||
def set(self):
|
||||
self.flag = True
|
||||
|
||||
def clear(self):
|
||||
self.flag = False
|
||||
|
||||
def is_set(self):
|
||||
return self.flag
|
||||
artime = Time() # artificial test time
|
||||
|
||||
|
||||
class Parameter:
|
||||
def __init__(self, name, readonly, poll, polltype, interval):
|
||||
self.poll = poll
|
||||
self.polltype = polltype # used for check only
|
||||
self.export = name
|
||||
self.readonly = readonly
|
||||
self.interval = interval
|
||||
self.timestamp = 0
|
||||
self.handler = None
|
||||
self.reset()
|
||||
class DispatcherStub:
|
||||
maxcycles = 10
|
||||
|
||||
def reset(self):
|
||||
self.cnt = 0
|
||||
self.span = 0
|
||||
self.maxspan = 0
|
||||
|
||||
def rfunc(self):
|
||||
artime.busy(artime.commtime)
|
||||
def announce_update(self, modulename, pname, pobj):
|
||||
now = artime.time()
|
||||
self.span = now - self.timestamp
|
||||
self.maxspan = max(self.maxspan, self.span)
|
||||
self.timestamp = now
|
||||
self.cnt += 1
|
||||
return True
|
||||
|
||||
def __repr__(self):
|
||||
return 'Parameter(%s)' % ", ".join("%s=%r" % item for item in self.__dict__.items())
|
||||
|
||||
|
||||
class Module:
|
||||
properties = {}
|
||||
pollerClass = Poller
|
||||
iodev = 'common_iodev'
|
||||
def __init__(self, name, pollinterval=5, fastfactor=0.25, slowfactor=4, busy=False,
|
||||
counts=(), auto=None):
|
||||
'''create a dummy module
|
||||
|
||||
nauto, ndynamic, nregular, nslow are the number of parameters of each polltype
|
||||
'''
|
||||
self.pollinterval = pollinterval
|
||||
self.fast_pollfactor = fastfactor
|
||||
self.slow_pollfactor = slowfactor
|
||||
self.parameters = OrderedDict()
|
||||
self.name = name
|
||||
self.is_busy = busy
|
||||
if auto is not None:
|
||||
self.pvalue = self.addPar('value', True, auto or DYNAMIC, DYNAMIC)
|
||||
# readonly = False should not matter:
|
||||
self.pstatus = self.addPar('status', False, auto or DYNAMIC, DYNAMIC)
|
||||
self.pregular = self.addPar('regular', True, auto or REGULAR, REGULAR)
|
||||
self.pslow = self.addPar('slow', False, auto or SLOW, SLOW)
|
||||
self.addPar('notpolled', True, False, 0)
|
||||
self.counts = 'auto'
|
||||
if hasattr(pobj, 'stat'):
|
||||
pobj.stat.append(now)
|
||||
else:
|
||||
ndynamic, nregular, nslow = counts
|
||||
for i in range(ndynamic):
|
||||
self.addPar('%s:d%d' % (name, i), True, DYNAMIC, DYNAMIC)
|
||||
for i in range(nregular):
|
||||
self.addPar('%s:r%d' % (name, i), True, REGULAR, REGULAR)
|
||||
for i in range(nslow):
|
||||
self.addPar('%s:s%d' % (name, i), False, SLOW, SLOW)
|
||||
self.counts = counts
|
||||
pobj.stat = [now]
|
||||
self.maxcycles -= 1
|
||||
if self.maxcycles <= 0:
|
||||
self.finish_event.set()
|
||||
sys.exit() # stop thread
|
||||
|
||||
def addPar(self, name, readonly, poll, expected_polltype):
|
||||
# self.count[polltype] += 1
|
||||
expected_interval = self.pollinterval
|
||||
if expected_polltype == SLOW:
|
||||
expected_interval *= self.slow_pollfactor
|
||||
elif expected_polltype == DYNAMIC and self.is_busy:
|
||||
expected_interval *= self.fast_pollfactor
|
||||
pobj = Parameter(name, readonly, poll, expected_polltype, expected_interval)
|
||||
setattr(self, 'read_' + pobj.export, pobj.rfunc)
|
||||
self.parameters[pobj.export] = pobj
|
||||
return pobj
|
||||
|
||||
def isBusy(self):
|
||||
return self.is_busy
|
||||
class ServerStub:
|
||||
def __init__(self):
|
||||
self.dispatcher = DispatcherStub()
|
||||
|
||||
def pollOneParam(self, pname):
|
||||
getattr(self, 'read_' + pname)()
|
||||
|
||||
def writeInitParams(self):
|
||||
pass
|
||||
class Base(Module):
|
||||
def __init__(self):
|
||||
srv = ServerStub()
|
||||
super().__init__('mod', logging.getLogger('dummy'), dict(description=''), srv)
|
||||
self.dispatcher = srv.dispatcher
|
||||
|
||||
def __repr__(self):
|
||||
rdict = self.__dict__.copy()
|
||||
rdict.pop('parameters')
|
||||
return 'Module(%r, counts=%r, f=%r, pollinterval=%g, is_busy=%r)' % (self.name,
|
||||
self.counts, (self.fast_pollfactor, self.slow_pollfactor, 1),
|
||||
self.pollinterval, self.is_busy)
|
||||
def run(self, maxcycles):
|
||||
self.dispatcher.maxcycles = maxcycles
|
||||
self.dispatcher.finish_event = threading.Event()
|
||||
self.initModule()
|
||||
|
||||
module_list = [
|
||||
[Module('x', 3.0, 0.125, 10, False, auto=True),
|
||||
Module('y', 3.0, 0.125, 10, False, auto=False)],
|
||||
[Module('a', 1.0, 0.25, 4, True, (5, 5, 10)),
|
||||
Module('b', 2.0, 0.25, 4, True, (5, 5, 50))],
|
||||
[Module('c', 1.0, 0.25, 4, False, (5, 0, 0))],
|
||||
[Module('d', 1.0, 0.25, 4, True, (0, 9, 0))],
|
||||
[Module('e', 1.0, 0.25, 4, True, (0, 0, 9))],
|
||||
[Module('f', 1.0, 0.25, 4, True, (0, 0, 0))],
|
||||
]
|
||||
@pytest.mark.parametrize('modules', module_list)
|
||||
def test_Poller(modules):
|
||||
# check for proper timing
|
||||
def wait(timeout=None, base=self.triggerPoll):
|
||||
"""simplified simulation
|
||||
|
||||
for overloaded in False, True:
|
||||
artime.reset()
|
||||
count = {DYNAMIC: 0, REGULAR: 0, SLOW: 0}
|
||||
maxspan = {DYNAMIC: 0, REGULAR: 0, SLOW: 0}
|
||||
pollTable = dict()
|
||||
for module in modules:
|
||||
Poller.add_to_table(pollTable, module)
|
||||
for pobj in module.parameters.values():
|
||||
if pobj.poll:
|
||||
maxspan[pobj.polltype] = max(maxspan[pobj.polltype], pobj.interval)
|
||||
count[pobj.polltype] += 1
|
||||
pobj.reset()
|
||||
assert len(pollTable) == 1
|
||||
poller = pollTable[(Poller, 'common_iodev')]
|
||||
artime.stop = poller.stop
|
||||
poller._event = Event() # patch Event.wait
|
||||
when an event is already set return True, else forward artificial time
|
||||
"""
|
||||
if base.is_set():
|
||||
return True
|
||||
artime.sleep(max(0.0, 99.9 if timeout is None else timeout))
|
||||
return base.is_set()
|
||||
|
||||
assert (sum(count.values()) > 0) == bool(poller)
|
||||
self.triggerPoll.wait = wait
|
||||
self.startModule(MultiEvent())
|
||||
assert self.dispatcher.finish_event.wait(1)
|
||||
|
||||
def started_callback(modules=modules):
|
||||
for module in modules:
|
||||
for pobj in module.parameters.values():
|
||||
assert pobj.cnt == bool(pobj.poll) # all parameters have to be polled once
|
||||
pobj.reset() # set maxspan and cnt to 0
|
||||
|
||||
if overloaded:
|
||||
# overloaded scenario
|
||||
artime.commtime = 1.0
|
||||
ncycles = 10
|
||||
if count[SLOW] > 0:
|
||||
cycletime = (count[REGULAR] + 1) * count[SLOW] * 2
|
||||
else:
|
||||
cycletime = max(count[REGULAR], count[DYNAMIC]) * 2
|
||||
artime.reset(cycletime * ncycles * 1.01) # poller will quit given time
|
||||
poller.run(started_callback)
|
||||
total = artime.time() - artime.STARTTIME
|
||||
for module in modules:
|
||||
for pobj in module.parameters.values():
|
||||
if pobj.poll:
|
||||
# average_span = total / (pobj.cnt + 1)
|
||||
assert total / (pobj.cnt + 1) <= max(cycletime, pobj.interval * 1.1)
|
||||
else:
|
||||
# normal scenario
|
||||
artime.commtime = 0.001
|
||||
artime.reset(max(maxspan.values()) * 5) # poller will quit given time
|
||||
poller.run(started_callback)
|
||||
total = artime.time() - artime.STARTTIME
|
||||
for module in modules:
|
||||
for pobj in module.parameters.values():
|
||||
if pobj.poll:
|
||||
assert pobj.cnt > 0
|
||||
assert pobj.maxspan <= maxspan[pobj.polltype] * 1.1
|
||||
assert (pobj.cnt + 1) * pobj.interval >= total * 0.99
|
||||
assert abs(pobj.span - pobj.interval) < 0.01
|
||||
pobj.reset()
|
||||
class Mod1(Base, Readable):
|
||||
param1 = Parameter('', FloatRange())
|
||||
param2 = Parameter('', FloatRange())
|
||||
param3 = Parameter('', FloatRange())
|
||||
param4 = Parameter('', FloatRange())
|
||||
|
||||
@ReadHandler(('param1', 'param2', 'param3'))
|
||||
def read_param(self, name):
|
||||
artime.sleep(1.0)
|
||||
return 0
|
||||
|
||||
@nopoll
|
||||
def read_param4(self):
|
||||
return 0
|
||||
|
||||
def read_status(self):
|
||||
artime.sleep(1.0)
|
||||
return 0
|
||||
|
||||
def read_value(self):
|
||||
artime.sleep(1.0)
|
||||
return 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'ncycles, pollinterval, slowinterval, mspan, pspan',
|
||||
[ # normal case:
|
||||
( 60, 5, 15, (4.9, 5.1), (14, 16)),
|
||||
# pollinterval faster then reading: mspan max ~ 3 s (polls of value, status and ONE other parameter)
|
||||
( 60, 1, 5, (0.9, 3.1), (5, 17)),
|
||||
])
|
||||
def test_poll(ncycles, pollinterval, slowinterval, mspan, pspan, monkeypatch):
|
||||
monkeypatch.setattr(time, 'time', artime.time)
|
||||
m = Mod1()
|
||||
m.pollinterval = pollinterval
|
||||
m.slowInterval = slowinterval
|
||||
m.run(ncycles)
|
||||
assert not hasattr(m.parameters['param4'], 'stat')
|
||||
for pname in ['value', 'status']:
|
||||
pobj = m.parameters[pname]
|
||||
lowcnt = 0
|
||||
print(pname, [t2 - t1 for t1, t2 in zip(pobj.stat[1:], pobj.stat[2:-1])])
|
||||
for t1, t2 in zip(pobj.stat[1:], pobj.stat[2:-1]):
|
||||
if t2 - t1 < mspan[0]:
|
||||
lowcnt += 1
|
||||
assert t2 - t1 <= mspan[1]
|
||||
assert lowcnt <= 2
|
||||
for pname in ['param1', 'param2', 'param3']:
|
||||
pobj = m.parameters[pname]
|
||||
lowcnt = 0
|
||||
print(pname, [t2 - t1 for t1, t2 in zip(pobj.stat[1:], pobj.stat[2:-1])])
|
||||
for t1, t2 in zip(pobj.stat[1:], pobj.stat[2:-1]):
|
||||
if t2 - t1 < pspan[0]:
|
||||
lowcnt += 1
|
||||
assert t2 - t1 <= pspan[1]
|
||||
assert lowcnt <= 2
|
||||
|
@ -26,6 +26,7 @@ import pytest
|
||||
from secop.datatypes import FloatRange, IntRange, StringType, ValueType
|
||||
from secop.errors import BadValueError, ConfigError, ProgrammingError
|
||||
from secop.properties import HasProperties, Property
|
||||
from secop.core import Parameter
|
||||
|
||||
|
||||
def Prop(*args, name=None, **kwds):
|
||||
@ -38,10 +39,10 @@ V_test_Property = [
|
||||
[Prop(StringType(), 'default', extname='extname', mandatory=False),
|
||||
dict(default='default', extname='extname', export=True, mandatory=False)
|
||||
],
|
||||
[Prop(IntRange(), '42', export=True, name='custom', mandatory=True),
|
||||
[Prop(IntRange(), 42, export=True, name='custom', mandatory=True),
|
||||
dict(default=42, extname='_custom', export=True, mandatory=True),
|
||||
],
|
||||
[Prop(IntRange(), '42', export=True, name='name'),
|
||||
[Prop(IntRange(), 42, export=True, name='name'),
|
||||
dict(default=42, extname='_name', export=True, mandatory=False)
|
||||
],
|
||||
[Prop(IntRange(), 42, '_extname', mandatory=True),
|
||||
@ -85,12 +86,12 @@ def test_Property_basic():
|
||||
Property('')
|
||||
with pytest.raises(ValueError):
|
||||
Property('', 1)
|
||||
Property('', IntRange(), '42', 'extname', False, False)
|
||||
Property('', IntRange(), 42, 'extname', False, False)
|
||||
|
||||
|
||||
def test_Properties():
|
||||
class Cls(HasProperties):
|
||||
aa = Property('', IntRange(0, 99), '42', export=True)
|
||||
aa = Property('', IntRange(0, 99), 42, export=True)
|
||||
bb = Property('', IntRange(), 0, export=False)
|
||||
|
||||
assert Cls.aa.default == 42
|
||||
@ -149,36 +150,44 @@ def test_Property_override():
|
||||
assert o2.a == 3
|
||||
|
||||
with pytest.raises(ProgrammingError) as e:
|
||||
class cx(c): # pylint: disable=unused-variable
|
||||
class cx(c): # pylint: disable=unused-variable
|
||||
def a(self):
|
||||
pass
|
||||
assert 'collides with' in str(e.value)
|
||||
|
||||
with pytest.raises(ProgrammingError) as e:
|
||||
class cz(c): # pylint: disable=unused-variable
|
||||
class cy(c): # pylint: disable=unused-variable
|
||||
a = 's'
|
||||
|
||||
assert 'can not set' in str(e.value)
|
||||
|
||||
with pytest.raises(ProgrammingError) as e:
|
||||
class cz(c): # pylint: disable=unused-variable
|
||||
a = 's'
|
||||
|
||||
class cp(c): # pylint: disable=unused-variable
|
||||
# overriding a Property with a Parameter is allowed
|
||||
a = Parameter('x', IntRange())
|
||||
|
||||
|
||||
def test_Properties_mro():
|
||||
class A(HasProperties):
|
||||
p = Property('base', StringType(), 'base', export='always')
|
||||
class Base(HasProperties):
|
||||
prop = Property('base', StringType(), 'base', export='always')
|
||||
|
||||
class B(A):
|
||||
class SubA(Base):
|
||||
pass
|
||||
|
||||
class C(A):
|
||||
p = Property('sub', FloatRange(), extname='p')
|
||||
class SubB(Base):
|
||||
prop = Property('sub', FloatRange(), extname='prop')
|
||||
|
||||
class D(C, B):
|
||||
p = 1
|
||||
class FinalBA(SubB, SubA):
|
||||
prop = 1
|
||||
|
||||
class E(B, C):
|
||||
p = 2
|
||||
class FinalAB(SubA, SubB):
|
||||
prop = 2
|
||||
|
||||
assert B().exportProperties() == {'_p': 'base'}
|
||||
assert D().exportProperties() == {'p': 1.0}
|
||||
# in an older implementation the following would fail, as B.p is constructed first
|
||||
# and then B.p overrides C.p
|
||||
assert E().exportProperties() == {'p': 2.0}
|
||||
assert SubA().exportProperties() == {'_prop': 'base'}
|
||||
assert FinalBA().exportProperties() == {'prop': 1.0}
|
||||
# in an older implementation the following would fail, as SubA.p is constructed first
|
||||
# and then SubA.p overrides SubB.p
|
||||
assert FinalAB().exportProperties() == {'prop': 2.0}
|
||||
|
148
test/test_statemachine.py
Normal file
148
test/test_statemachine.py
Normal 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
|
Loading…
x
Reference in New Issue
Block a user