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
|
no-space-check=trailing-comma,dict-separator
|
||||||
|
|
||||||
# Maximum number of lines in a module
|
# Maximum number of lines in a module
|
||||||
max-module-lines=1200
|
max-module-lines=1000
|
||||||
|
|
||||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||||
# tab).
|
# tab).
|
||||||
|
2
Makefile
2
Makefile
@ -59,4 +59,4 @@ release:
|
|||||||
|
|
||||||
|
|
||||||
build-pkg:
|
build-pkg:
|
||||||
debocker build --image jenkinsng.admin.frm2:5000/mlzbase/buster
|
debocker build --image docker.ictrl.frm2.tum.de:5443/mlzbase/buster
|
||||||
|
14
README.md
14
README.md
@ -15,7 +15,6 @@ branches:
|
|||||||
- work: current working version, usually in use on /home/l_samenv/frappy (and on neutron instruments)
|
- work: current working version, usually in use on /home/l_samenv/frappy (and on neutron instruments)
|
||||||
this should be a copy of an earlier state of the wip branch
|
this should be a copy of an earlier state of the wip branch
|
||||||
- wip: current test version, usually in use on /home/l_samenv/frappy_wip
|
- wip: current test version, usually in use on /home/l_samenv/frappy_wip
|
||||||
|
|
||||||
IMPORTANT: make commits containing either only files to be pushed to Gerrit or only
|
IMPORTANT: make commits containing either only files to be pushed to Gerrit or only
|
||||||
PSI internal files, not mixed. Mark local commits with '[PSI]' in the commit message.
|
PSI internal files, not mixed. Mark local commits with '[PSI]' in the commit message.
|
||||||
|
|
||||||
@ -35,7 +34,7 @@ where commits may be cherry picked for input to Gerrit. As generally in the revi
|
|||||||
changes are done, eventually a sync step should happen:
|
changes are done, eventually a sync step should happen:
|
||||||
|
|
||||||
1) ideally, this is done when work and wip match
|
1) ideally, this is done when work and wip match
|
||||||
2) make sure branches mlz, master, wip and work are in synv with remote, push/pull otherwise
|
2) make sure branches mlz, master, wip and work are in syns with remote, push/pull otherwise
|
||||||
3) cherry-pick commits from mlz to master
|
3) cherry-pick commits from mlz to master
|
||||||
4) make sure master and mlz branches match (git diff --name-only master..wip should only return README.md)
|
4) make sure master and mlz branches match (git diff --name-only master..wip should only return README.md)
|
||||||
5) create branch new_work from master
|
5) create branch new_work from master
|
||||||
@ -43,11 +42,12 @@ changes are done, eventually a sync step should happen:
|
|||||||
- core commits already pushed through gerrit are skipped
|
- core commits already pushed through gerrit are skipped
|
||||||
- all other commits are to be cherry-picked
|
- all other commits are to be cherry-picked
|
||||||
7) when arrived at the point where the new working version should be,
|
7) when arrived at the point where the new working version should be,
|
||||||
copy new_wip branch to work with 'git checkout work;git checkout new_wip .'
|
copy new_wip branch to work with 'git checkout -B work'.
|
||||||
(note the dot!) and then commit this.
|
Not sure if this works, as work is to be pushed to git.psi.ch.
|
||||||
8) continue with (6) if wip and work should differ
|
We might first remove the remote branch with 'git push origin --delete work'.
|
||||||
9) do like (7), but for wip branch
|
And then create again (git push origin work)?
|
||||||
10) delete new_wip branch, push master, wip and work branches
|
8) continue with (6) if wip and work should differ, and do like (7) for wip branch
|
||||||
|
9) delete new_wip branch, push master, wip and work branches
|
||||||
|
|
||||||
|
|
||||||
## Procedure to update PPMS
|
## Procedure to update PPMS
|
||||||
|
@ -27,12 +27,11 @@ import sys
|
|||||||
import argparse
|
import argparse
|
||||||
from os import path
|
from os import path
|
||||||
|
|
||||||
import mlzlog
|
|
||||||
|
|
||||||
# Add import path for inplace usage
|
# Add import path for inplace usage
|
||||||
sys.path.insert(0, path.abspath(path.join(path.dirname(__file__), '..')))
|
sys.path.insert(0, path.abspath(path.join(path.dirname(__file__), '..')))
|
||||||
|
|
||||||
from secop.lib import getGeneralConfig
|
from secop.lib import generalConfig
|
||||||
|
from secop.logging import logger
|
||||||
from secop.server import Server
|
from secop.server import Server
|
||||||
|
|
||||||
|
|
||||||
@ -60,15 +59,26 @@ def parseArgv(argv):
|
|||||||
parser.add_argument('-c',
|
parser.add_argument('-c',
|
||||||
'--cfgfiles',
|
'--cfgfiles',
|
||||||
action='store',
|
action='store',
|
||||||
help="comma separated list of cfg files\n"
|
help="comma separated list of cfg files,\n"
|
||||||
"defaults to <name_of_the_instance>\n"
|
"defaults to <name_of_the_instance>.\n"
|
||||||
"cfgfiles given without '.cfg' extension are searched in the configuration directory, "
|
"cfgfiles given without '.cfg' extension are searched in the configuration directory, "
|
||||||
"else they are treated as path names",
|
"else they are treated as path names",
|
||||||
default=None)
|
default=None)
|
||||||
|
parser.add_argument('-g',
|
||||||
|
'--gencfg',
|
||||||
|
action='store',
|
||||||
|
help="full path of general config file,\n"
|
||||||
|
"defaults to env. variable FRAPPY_CONFIG_FILE\n",
|
||||||
|
default=None)
|
||||||
parser.add_argument('-t',
|
parser.add_argument('-t',
|
||||||
'--test',
|
'--test',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
help='Check cfg files only',
|
help='check cfg files only',
|
||||||
|
default=False)
|
||||||
|
parser.add_argument('-r',
|
||||||
|
'--relaxed',
|
||||||
|
action='store_true',
|
||||||
|
help='no checking of problematic behaviour',
|
||||||
default=False)
|
default=False)
|
||||||
return parser.parse_args(argv)
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
@ -80,9 +90,12 @@ def main(argv=None):
|
|||||||
args = parseArgv(argv[1:])
|
args = parseArgv(argv[1:])
|
||||||
|
|
||||||
loglevel = 'debug' if args.verbose else ('error' if args.quiet else 'info')
|
loglevel = 'debug' if args.verbose else ('error' if args.quiet else 'info')
|
||||||
mlzlog.initLogging('secop', loglevel, getGeneralConfig()['logdir'])
|
generalConfig.defaults = {k: args.relaxed for k in (
|
||||||
|
'lazy_number_validation', 'disable_value_range_check', 'legacy_hasiodev', 'tolerate_poll_property')}
|
||||||
|
generalConfig.init(args.gencfg)
|
||||||
|
logger.init(loglevel)
|
||||||
|
|
||||||
srv = Server(args.name, mlzlog.log, cfgfiles=args.cfgfiles, interface=args.port, testonly=args.test)
|
srv = Server(args.name, logger.log, cfgfiles=args.cfgfiles, interface=args.port, testonly=args.test)
|
||||||
|
|
||||||
if args.daemonize:
|
if args.daemonize:
|
||||||
srv.start()
|
srv.start()
|
||||||
|
31
cfg/cryosim.cfg
Normal file
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
|
description = Lsc Simulation at PSI
|
||||||
|
|
||||||
[interface tcp]
|
[INTERFACE]
|
||||||
type = tcp
|
uri = tcp://5000
|
||||||
bindto = 0.0.0.0
|
|
||||||
bindport = 5000
|
|
||||||
|
|
||||||
[module res]
|
[res]
|
||||||
class = secop_psi.ls370res.ResChannel
|
class = secop_psi.ls370res.ResChannel
|
||||||
.channel = 3
|
channel = 3
|
||||||
.description = resistivity
|
description = resistivity
|
||||||
.main = lsmain
|
main = lsmain
|
||||||
.iodev = lscom
|
io = lscom
|
||||||
|
|
||||||
[module lsmain]
|
[lsmain]
|
||||||
class = secop_psi.ls370res.Main
|
class = secop_psi.ls370res.Main
|
||||||
.description = main control of Lsc controller
|
description = main control of Lsc controller
|
||||||
.iodev = lscom
|
io = lscom
|
||||||
|
|
||||||
[module lscom]
|
[lscom]
|
||||||
class = secop_psi.ls370sim.Ls370Sim
|
class = secop_psi.ls370sim.Ls370Sim
|
||||||
.description = simulated serial communicator to a LS 370
|
description = simulated serial communicator to a LS 370
|
||||||
.visibility = 3
|
visibility = 3
|
||||||
|
@ -1,24 +1,20 @@
|
|||||||
[NODE]
|
[node LscSIM.psi.ch]
|
||||||
id = ls370res.psi.ch
|
|
||||||
description = Lsc370 Test
|
description = Lsc370 Test
|
||||||
|
|
||||||
[INTERFACE]
|
[interface tcp]
|
||||||
uri = tcp://5000
|
type = tcp
|
||||||
|
bindto = 0.0.0.0
|
||||||
|
bindport = 5000
|
||||||
|
|
||||||
[lsmain_iodev]
|
[module lsmain]
|
||||||
description = the communication device
|
|
||||||
class = secop_psi.ls370res.StringIO
|
|
||||||
uri = localhost:4567
|
|
||||||
|
|
||||||
[lsmain]
|
|
||||||
class = secop_psi.ls370res.Main
|
class = secop_psi.ls370res.Main
|
||||||
description = main control of Lsc controller
|
description = main control of Lsc controller
|
||||||
iodev = lsmain_iodev
|
uri = localhost:4567
|
||||||
|
|
||||||
[res]
|
[module res]
|
||||||
class = secop_psi.ls370res.ResChannel
|
class = secop_psi.ls370res.ResChannel
|
||||||
iexc = '1mA'
|
vexc = '2mV'
|
||||||
channel = 5
|
channel = 3
|
||||||
description = resistivity
|
description = resistivity
|
||||||
main = lsmain
|
main = lsmain
|
||||||
# the auto created iodev from lsmain:
|
# the auto created iodev from lsmain:
|
||||||
|
38
cfg/magsim.cfg
Normal file
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]
|
[tt]
|
||||||
class = secop_psi.ppms.Temp
|
class = secop_psi.ppms.Temp
|
||||||
description = main temperature
|
description = main temperature
|
||||||
iodev = ppms
|
io = ppms
|
||||||
|
|
||||||
[mf]
|
[mf]
|
||||||
class = secop_psi.ppms.Field
|
class = secop_psi.ppms.Field
|
||||||
target.min = -9
|
target.min = -9
|
||||||
target.max = 9
|
target.max = 9
|
||||||
.description = magnetic field
|
description = magnetic field
|
||||||
.iodev = ppms
|
io = ppms
|
||||||
|
|
||||||
[pos]
|
[pos]
|
||||||
class = secop_psi.ppms.Position
|
class = secop_psi.ppms.Position
|
||||||
.description = sample rotator
|
description = sample rotator
|
||||||
.iodev = ppms
|
io = ppms
|
||||||
|
|
||||||
[lev]
|
[lev]
|
||||||
class = secop_psi.ppms.Level
|
class = secop_psi.ppms.Level
|
||||||
.description = helium level
|
description = helium level
|
||||||
.iodev = ppms
|
io = ppms
|
||||||
|
|
||||||
[chamber]
|
[chamber]
|
||||||
class = secop_psi.ppms.Chamber
|
class = secop_psi.ppms.Chamber
|
||||||
.description = chamber state
|
description = chamber state
|
||||||
.iodev = ppms
|
io = ppms
|
||||||
|
|
||||||
[r1]
|
[r1]
|
||||||
class = secop_psi.ppms.BridgeChannel
|
class = secop_psi.ppms.BridgeChannel
|
||||||
.description = resistivity channel 1
|
description = resistivity channel 1
|
||||||
.no = 1
|
no = 1
|
||||||
value.unit = Ohm
|
value.unit = Ohm
|
||||||
.iodev = ppms
|
io = ppms
|
||||||
|
|
||||||
[r2]
|
[r2]
|
||||||
class = secop_psi.ppms.BridgeChannel
|
class = secop_psi.ppms.BridgeChannel
|
||||||
.description = resistivity channel 2
|
description = resistivity channel 2
|
||||||
.no = 2
|
no = 2
|
||||||
value.unit = Ohm
|
value.unit = Ohm
|
||||||
.iodev = ppms
|
io = ppms
|
||||||
|
|
||||||
[r3]
|
[r3]
|
||||||
class = secop_psi.ppms.BridgeChannel
|
class = secop_psi.ppms.BridgeChannel
|
||||||
.description = resistivity channel 3
|
description = resistivity channel 3
|
||||||
.no = 3
|
no = 3
|
||||||
value.unit = Ohm
|
value.unit = Ohm
|
||||||
.iodev = ppms
|
io = ppms
|
||||||
|
|
||||||
[r4]
|
[r4]
|
||||||
class = secop_psi.ppms.BridgeChannel
|
class = secop_psi.ppms.BridgeChannel
|
||||||
.description = resistivity channel 4
|
description = resistivity channel 4
|
||||||
.no = 4
|
no = 4
|
||||||
value.unit = Ohm
|
value.unit = Ohm
|
||||||
.iodev = ppms
|
io = ppms
|
||||||
|
|
||||||
[i1]
|
[i1]
|
||||||
class = secop_psi.ppms.Channel
|
class = secop_psi.ppms.Channel
|
||||||
.description = current channel 1
|
description = current channel 1
|
||||||
.no = 1
|
no = 1
|
||||||
value.unit = uA
|
value.unit = uA
|
||||||
.iodev = ppms
|
io = ppms
|
||||||
|
|
||||||
[i2]
|
[i2]
|
||||||
class = secop_psi.ppms.Channel
|
class = secop_psi.ppms.Channel
|
||||||
.description = current channel 2
|
description = current channel 2
|
||||||
.no = 2
|
no = 2
|
||||||
value.unit = uA
|
value.unit = uA
|
||||||
.iodev = ppms
|
io = ppms
|
||||||
|
|
||||||
[i3]
|
[i3]
|
||||||
class = secop_psi.ppms.Channel
|
class = secop_psi.ppms.Channel
|
||||||
.description = current channel 3
|
description = current channel 3
|
||||||
.no = 3
|
no = 3
|
||||||
value.unit = uA
|
value.unit = uA
|
||||||
.iodev = ppms
|
io = ppms
|
||||||
|
|
||||||
[i4]
|
[i4]
|
||||||
class = secop_psi.ppms.Channel
|
class = secop_psi.ppms.Channel
|
||||||
.description = current channel 4
|
description = current channel 4
|
||||||
.no = 4
|
no = 4
|
||||||
value.unit = uA
|
value.unit = uA
|
||||||
.iodev = ppms
|
io = ppms
|
||||||
|
|
||||||
[v1]
|
[v1]
|
||||||
class = secop_psi.ppms.DriverChannel
|
class = secop_psi.ppms.DriverChannel
|
||||||
.description = voltage channel 1
|
description = voltage channel 1
|
||||||
.no = 1
|
no = 1
|
||||||
value.unit = V
|
value.unit = V
|
||||||
.iodev = ppms
|
io = ppms
|
||||||
|
|
||||||
[v2]
|
[v2]
|
||||||
class = secop_psi.ppms.DriverChannel
|
class = secop_psi.ppms.DriverChannel
|
||||||
.description = voltage channel 2
|
description = voltage channel 2
|
||||||
.no = 2
|
no = 2
|
||||||
value.unit = V
|
value.unit = V
|
||||||
.iodev = ppms
|
io = ppms
|
||||||
|
|
||||||
[tv]
|
[tv]
|
||||||
class = secop_psi.ppms.UserChannel
|
class = secop_psi.ppms.UserChannel
|
||||||
.description = VTI temperature
|
description = VTI temperature
|
||||||
enabled = 1
|
enabled = 1
|
||||||
value.unit = K
|
value.unit = K
|
||||||
.iodev = ppms
|
io = ppms
|
||||||
|
|
||||||
[ts]
|
[ts]
|
||||||
class = secop_psi.ppms.UserChannel
|
class = secop_psi.ppms.UserChannel
|
||||||
.description = sample temperature
|
description = sample temperature
|
||||||
enabled = 1
|
enabled = 1
|
||||||
value.unit = K
|
value.unit = K
|
||||||
.iodev = ppms
|
io = ppms
|
||||||
|
|
||||||
[ppms]
|
[ppms]
|
||||||
class = secop_psi.ppms.Main
|
class = secop_psi.ppms.Main
|
||||||
.description = the main and poller module
|
description = the main and poller module
|
||||||
.class_id = QD.MULTIVU.PPMS.1
|
class_id = QD.MULTIVU.PPMS.1
|
||||||
.visibility = 3
|
visibility = 3
|
||||||
pollinterval = 2
|
pollinterval = 2
|
||||||
|
@ -5,26 +5,29 @@ description = [sim] uniaxial pressure device
|
|||||||
[INTERFACE]
|
[INTERFACE]
|
||||||
uri=tcp://5000
|
uri=tcp://5000
|
||||||
|
|
||||||
[drv]
|
|
||||||
class = secop.simulation.SimDrivable
|
|
||||||
extra_params = speed, safe_current, safe_step, maxcurrent
|
|
||||||
description = simulated motor
|
|
||||||
value.default = 0
|
|
||||||
speed.readonly = False
|
|
||||||
speed.default = 10
|
|
||||||
interval = 0.11
|
|
||||||
|
|
||||||
[transducer]
|
|
||||||
class = secop_psi.simdpm.DPM3
|
|
||||||
description = simulated force
|
|
||||||
motor = drv
|
|
||||||
|
|
||||||
[force]
|
[force]
|
||||||
class = secop_psi.uniax.Uniax
|
class = secop_psi.uniax.Uniax
|
||||||
description = uniax driver
|
description = uniax driver
|
||||||
motor = drv
|
motor = drv
|
||||||
transducer = transducer
|
transducer = transducer
|
||||||
|
|
||||||
|
[drv]
|
||||||
|
class = secop.simulation.SimDrivable
|
||||||
|
extra_params = speed, safe_current, move_limit, maxcurrent, tolerance
|
||||||
|
description = simulated motor
|
||||||
|
value.default = 0
|
||||||
|
speed.readonly = False
|
||||||
|
speed.default = 40
|
||||||
|
interval = 0.11
|
||||||
|
value.unit = deg
|
||||||
|
tolerance.default = 0.9
|
||||||
|
|
||||||
|
[transducer]
|
||||||
|
class = secop_psi.simdpm.DPM3
|
||||||
|
description = simulated force
|
||||||
|
motor = drv
|
||||||
|
value.unit = 'N'
|
||||||
|
|
||||||
[res]
|
[res]
|
||||||
class = secop.simulation.SimReadable
|
class = secop.simulation.SimReadable
|
||||||
description = raw temperature sensor on the stick
|
description = raw temperature sensor on the stick
|
||||||
@ -37,5 +40,5 @@ value.datatype = {"type":"double", "unit":"Ohm"}
|
|||||||
class=secop_psi.softcal.Sensor
|
class=secop_psi.softcal.Sensor
|
||||||
description=temperature sensor, soft calibration
|
description=temperature sensor, soft calibration
|
||||||
rawsensor=res
|
rawsensor=res
|
||||||
calib = X132254.340
|
calib = X132254
|
||||||
value.unit = "K"
|
value.unit = "K"
|
||||||
|
@ -49,5 +49,5 @@ channel = A
|
|||||||
[T]
|
[T]
|
||||||
class = secop_psi.softcal.Sensor
|
class = secop_psi.softcal.Sensor
|
||||||
rawsensor = res
|
rawsensor = res
|
||||||
calib = /home/l_samenv/frappy/secop_psi/calcurves/X132254.340
|
calib = X132254
|
||||||
value.unit = K
|
value.unit = K
|
||||||
|
34
ci/Jenkinsfile
vendored
34
ci/Jenkinsfile
vendored
@ -30,6 +30,8 @@ def changedFiles = '';
|
|||||||
|
|
||||||
def run_pylint(pyver) {
|
def run_pylint(pyver) {
|
||||||
stage ('pylint-' + pyver) {
|
stage ('pylint-' + pyver) {
|
||||||
|
def cpylint = "RUNNING"
|
||||||
|
gerritPostCheck(["jenkins:pylint_${pyver}": cpylint])
|
||||||
def status = 'OK'
|
def status = 'OK'
|
||||||
changedFiles = sh returnStdout: true, script: '''\
|
changedFiles = sh returnStdout: true, script: '''\
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
@ -68,18 +70,15 @@ fi
|
|||||||
|
|
||||||
echo "pylint result: $res"
|
echo "pylint result: $res"
|
||||||
this.verifyresult.put('pylint'+pyver, 1)
|
this.verifyresult.put('pylint'+pyver, 1)
|
||||||
|
cpylint = "SUCCESSFUL"
|
||||||
if ( res != 0 ) {
|
if ( res != 0 ) {
|
||||||
currentBuild.result='FAILURE'
|
currentBuild.result='FAILURE'
|
||||||
this.verifyresult.put('pylint'+ pyver, -1)
|
this.verifyresult.put('pylint'+ pyver, -1)
|
||||||
status = 'FAILURE'
|
status = 'FAILURE'
|
||||||
|
cpylint = "FAILED"
|
||||||
}
|
}
|
||||||
|
|
||||||
gerritverificationpublisher([
|
gerritPostCheck(["jenkins:pylint_${pyver}": cpylint])
|
||||||
verifyStatusValue: this.verifyresult['pylint'+pyver],
|
|
||||||
verifyStatusCategory: 'pylint ',
|
|
||||||
verifyStatusName: 'pylint-'+pyver,
|
|
||||||
verifyStatusReporter: 'jenkins',
|
|
||||||
verifyStatusRerun: '!recheck'])
|
|
||||||
archiveArtifacts([allowEmptyArchive: true,
|
archiveArtifacts([allowEmptyArchive: true,
|
||||||
artifacts: 'pylint-*.txt'])
|
artifacts: 'pylint-*.txt'])
|
||||||
recordIssues([enabledForFailure: true,
|
recordIssues([enabledForFailure: true,
|
||||||
@ -99,6 +98,8 @@ fi
|
|||||||
|
|
||||||
def run_tests(pyver) {
|
def run_tests(pyver) {
|
||||||
stage('Test:' + pyver) {
|
stage('Test:' + pyver) {
|
||||||
|
def cpytest = "RUNNING"
|
||||||
|
gerritPostCheck(["jenkins:pytest_${pyver}":"RUNNING"])
|
||||||
writeFile file: 'setup.cfg', text: '''
|
writeFile file: 'setup.cfg', text: '''
|
||||||
[tool:pytest]
|
[tool:pytest]
|
||||||
addopts = --junit-xml=pytest.xml --junit-prefix=''' + pyver
|
addopts = --junit-xml=pytest.xml --junit-prefix=''' + pyver
|
||||||
@ -116,18 +117,15 @@ python3 setup.py develop
|
|||||||
make test
|
make test
|
||||||
'''
|
'''
|
||||||
verifyresult.put(pyver, 1)
|
verifyresult.put(pyver, 1)
|
||||||
|
cpytest = "SUCCESSFUL"
|
||||||
}
|
}
|
||||||
} catch (all) {
|
} catch (all) {
|
||||||
currentBuild.result = 'FAILURE'
|
currentBuild.result = 'FAILURE'
|
||||||
status = 'FAILURE'
|
status = 'FAILURE'
|
||||||
|
cpytest= "FAILED"
|
||||||
verifyresult.put(pyver, -1)
|
verifyresult.put(pyver, -1)
|
||||||
}
|
}
|
||||||
gerritverificationpublisher([
|
gerritPostCheck(["jenkins:pytest_${pyver}":cpytest])
|
||||||
verifyStatusValue: verifyresult[pyver],
|
|
||||||
verifyStatusCategory: 'test ',
|
|
||||||
verifyStatusName: 'pytest-'+pyver,
|
|
||||||
verifyStatusReporter: 'jenkins',
|
|
||||||
verifyStatusRerun: '!recheck'])
|
|
||||||
|
|
||||||
step([$class: 'JUnitResultArchiver', allowEmptyResults: true,
|
step([$class: 'JUnitResultArchiver', allowEmptyResults: true,
|
||||||
keepLongStdio: true, testResults: 'pytest.xml'])
|
keepLongStdio: true, testResults: 'pytest.xml'])
|
||||||
@ -138,6 +136,8 @@ make test
|
|||||||
}
|
}
|
||||||
|
|
||||||
def run_docs() {
|
def run_docs() {
|
||||||
|
def cdocs = "RUNNING"
|
||||||
|
gerritPostCheck(["jenkins:docs":cdocs])
|
||||||
stage('prepare') {
|
stage('prepare') {
|
||||||
sh '''
|
sh '''
|
||||||
. /home/jenkins/secopvenv/bin/activate
|
. /home/jenkins/secopvenv/bin/activate
|
||||||
@ -185,15 +185,9 @@ def run_docs() {
|
|||||||
|
|
||||||
stage('store html doc for build') {
|
stage('store html doc for build') {
|
||||||
publishHTML([allowMissing: false, alwaysLinkToLastBuild: false, keepAll: true, reportDir: 'doc/_build/html', reportFiles: 'index.html', reportName: 'Built documentation', reportTitles: ''])
|
publishHTML([allowMissing: false, alwaysLinkToLastBuild: false, keepAll: true, reportDir: 'doc/_build/html', reportFiles: 'index.html', reportName: 'Built documentation', reportTitles: ''])
|
||||||
gerritverificationpublisher([
|
cdocs = "SUCCESSFUL"
|
||||||
verifyStatusValue: 1,
|
|
||||||
verifyStatusCategory: 'test ',
|
|
||||||
verifyStatusName: 'doc',
|
|
||||||
verifyStatusReporter: 'jenkins',
|
|
||||||
verifyStatusRerun: '@recheck'
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
gerritPostCheck(["jenkins:docs":cdocs])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
170
debian/changelog
vendored
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
|
secop-core (0.11.6) unstable; urgency=medium
|
||||||
|
|
||||||
* fix secop-generator
|
* fix secop-generator
|
||||||
@ -133,7 +241,7 @@ secop-core (0.10.5) unstable; urgency=low
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Tue, 29 Oct 2019 16:33:18 +0100
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Tue, 29 Oct 2019 16:33:18 +0100
|
||||||
|
|
||||||
secop-core (0.10.3) unstable; urgency=low
|
secop-core (0.10.3) unstable; urgency=low
|
||||||
|
|
||||||
@ -142,7 +250,7 @@ secop-core (0.10.3) unstable; urgency=low
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Fri, 11 Oct 2019 10:49:43 +0200
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Fri, 11 Oct 2019 10:49:43 +0200
|
||||||
|
|
||||||
secop-core (0.10.2) unstable; urgency=low
|
secop-core (0.10.2) unstable; urgency=low
|
||||||
|
|
||||||
@ -153,7 +261,7 @@ secop-core (0.10.2) unstable; urgency=low
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Fri, 11 Oct 2019 10:42:58 +0200
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Fri, 11 Oct 2019 10:42:58 +0200
|
||||||
|
|
||||||
secop-core (0.10.1) unstable; urgency=low
|
secop-core (0.10.1) unstable; urgency=low
|
||||||
|
|
||||||
@ -162,7 +270,7 @@ secop-core (0.10.1) unstable; urgency=low
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 26 Sep 2019 16:41:10 +0200
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 26 Sep 2019 16:41:10 +0200
|
||||||
|
|
||||||
secop-core (0.10.0) unstable; urgency=low
|
secop-core (0.10.0) unstable; urgency=low
|
||||||
|
|
||||||
@ -171,7 +279,7 @@ secop-core (0.10.0) unstable; urgency=low
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 26 Sep 2019 16:31:14 +0200
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 26 Sep 2019 16:31:14 +0200
|
||||||
|
|
||||||
secop-core (0.9.0) unstable; urgency=low
|
secop-core (0.9.0) unstable; urgency=low
|
||||||
|
|
||||||
@ -198,7 +306,7 @@ secop-core (0.9.0) unstable; urgency=low
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 26 Sep 2019 16:26:07 +0200
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 26 Sep 2019 16:26:07 +0200
|
||||||
|
|
||||||
secop-core (0.8.1) unstable; urgency=low
|
secop-core (0.8.1) unstable; urgency=low
|
||||||
|
|
||||||
@ -207,7 +315,7 @@ secop-core (0.8.1) unstable; urgency=low
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Wed, 25 Sep 2019 15:40:44 +0200
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Wed, 25 Sep 2019 15:40:44 +0200
|
||||||
|
|
||||||
secop-core (0.8.0) unstable; urgency=low
|
secop-core (0.8.0) unstable; urgency=low
|
||||||
|
|
||||||
@ -275,7 +383,7 @@ secop-core (0.8.0) unstable; urgency=low
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Wed, 25 Sep 2019 10:27:51 +0200
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Wed, 25 Sep 2019 10:27:51 +0200
|
||||||
|
|
||||||
secop-core (0.7.0) unstable; urgency=low
|
secop-core (0.7.0) unstable; urgency=low
|
||||||
|
|
||||||
@ -311,7 +419,7 @@ secop-core (0.7.0) unstable; urgency=low
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 28 Mar 2019 13:46:08 +0100
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 28 Mar 2019 13:46:08 +0100
|
||||||
|
|
||||||
secop-core (0.6.4) unstable; urgency=low
|
secop-core (0.6.4) unstable; urgency=low
|
||||||
|
|
||||||
@ -376,7 +484,7 @@ secop-core (0.6.4) unstable; urgency=low
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 20 Dec 2018 16:44:03 +0100
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 20 Dec 2018 16:44:03 +0100
|
||||||
|
|
||||||
secop-core (0.6.3) unstable; urgency=low
|
secop-core (0.6.3) unstable; urgency=low
|
||||||
|
|
||||||
@ -390,7 +498,7 @@ secop-core (0.6.3) unstable; urgency=low
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Fri, 27 Jul 2018 09:31:59 +0200
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Fri, 27 Jul 2018 09:31:59 +0200
|
||||||
|
|
||||||
secop-core (0.6.2) unstable; urgency=low
|
secop-core (0.6.2) unstable; urgency=low
|
||||||
|
|
||||||
@ -429,7 +537,7 @@ secop-core (0.6.2) unstable; urgency=low
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Wed, 18 Jul 2018 12:06:57 +0200
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Wed, 18 Jul 2018 12:06:57 +0200
|
||||||
|
|
||||||
secop-core (0.6.1) unstable; urgency=low
|
secop-core (0.6.1) unstable; urgency=low
|
||||||
|
|
||||||
@ -438,7 +546,7 @@ secop-core (0.6.1) unstable; urgency=low
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 19 Apr 2018 10:24:44 +0200
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 19 Apr 2018 10:24:44 +0200
|
||||||
|
|
||||||
secop-core (0.6.0) unstable; urgency=low
|
secop-core (0.6.0) unstable; urgency=low
|
||||||
|
|
||||||
@ -458,7 +566,7 @@ secop-core (0.6.0) unstable; urgency=low
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Tue, 17 Apr 2018 17:38:52 +0200
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Tue, 17 Apr 2018 17:38:52 +0200
|
||||||
|
|
||||||
secop-core (0.5.0) unstable; urgency=low
|
secop-core (0.5.0) unstable; urgency=low
|
||||||
|
|
||||||
@ -521,7 +629,7 @@ secop-core (0.5.0) unstable; urgency=low
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Tue, 17 Apr 2018 12:45:58 +0200
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Tue, 17 Apr 2018 12:45:58 +0200
|
||||||
|
|
||||||
secop-core (0.4.4) unstable; urgency=low
|
secop-core (0.4.4) unstable; urgency=low
|
||||||
|
|
||||||
@ -530,7 +638,7 @@ secop-core (0.4.4) unstable; urgency=low
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Sun, 24 Sep 2017 22:25:01 +0200
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Sun, 24 Sep 2017 22:25:01 +0200
|
||||||
|
|
||||||
secop-core (0.4.3) unstable; urgency=low
|
secop-core (0.4.3) unstable; urgency=low
|
||||||
|
|
||||||
@ -539,7 +647,7 @@ secop-core (0.4.3) unstable; urgency=low
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Fri, 22 Sep 2017 17:29:46 +0200
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Fri, 22 Sep 2017 17:29:46 +0200
|
||||||
|
|
||||||
secop-core (0.4.2) unstable; urgency=low
|
secop-core (0.4.2) unstable; urgency=low
|
||||||
|
|
||||||
@ -548,7 +656,7 @@ secop-core (0.4.2) unstable; urgency=low
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Fri, 22 Sep 2017 16:37:59 +0200
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Fri, 22 Sep 2017 16:37:59 +0200
|
||||||
|
|
||||||
secop-core (0.4.1) unstable; urgency=low
|
secop-core (0.4.1) unstable; urgency=low
|
||||||
|
|
||||||
@ -557,7 +665,7 @@ secop-core (0.4.1) unstable; urgency=low
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Fri, 22 Sep 2017 13:25:28 +0200
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Fri, 22 Sep 2017 13:25:28 +0200
|
||||||
|
|
||||||
secop-core (0.4.0) unstable; urgency=low
|
secop-core (0.4.0) unstable; urgency=low
|
||||||
|
|
||||||
@ -567,7 +675,7 @@ secop-core (0.4.0) unstable; urgency=low
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Fri, 22 Sep 2017 10:33:04 +0200
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Fri, 22 Sep 2017 10:33:04 +0200
|
||||||
|
|
||||||
secop-core (0.3.0) unstable; urgency=low
|
secop-core (0.3.0) unstable; urgency=low
|
||||||
|
|
||||||
@ -633,7 +741,7 @@ secop-core (0.3.0) unstable; urgency=low
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Mon, 18 Sep 2017 14:18:36 +0200
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Mon, 18 Sep 2017 14:18:36 +0200
|
||||||
|
|
||||||
secop-core (0.2.0) unstable; urgency=low
|
secop-core (0.2.0) unstable; urgency=low
|
||||||
|
|
||||||
@ -642,7 +750,7 @@ secop-core (0.2.0) unstable; urgency=low
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 07 Sep 2017 14:55:41 +0200
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 07 Sep 2017 14:55:41 +0200
|
||||||
|
|
||||||
secop-core (0.1.1) unstable; urgency=low
|
secop-core (0.1.1) unstable; urgency=low
|
||||||
|
|
||||||
@ -651,7 +759,7 @@ secop-core (0.1.1) unstable; urgency=low
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 07 Sep 2017 11:02:19 +0200
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 07 Sep 2017 11:02:19 +0200
|
||||||
|
|
||||||
secop-core (0.1.0) unstable; urgency=low
|
secop-core (0.1.0) unstable; urgency=low
|
||||||
|
|
||||||
@ -660,7 +768,7 @@ secop-core (0.1.0) unstable; urgency=low
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 07 Sep 2017 10:50:24 +0200
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 07 Sep 2017 10:50:24 +0200
|
||||||
|
|
||||||
secop-core (0.0.8) unstable; urgency=low
|
secop-core (0.0.8) unstable; urgency=low
|
||||||
|
|
||||||
@ -669,7 +777,7 @@ secop-core (0.0.8) unstable; urgency=low
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Tue, 01 Aug 2017 14:13:11 +0200
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Tue, 01 Aug 2017 14:13:11 +0200
|
||||||
|
|
||||||
secop-core (0.0.7) unstable; urgency=low
|
secop-core (0.0.7) unstable; urgency=low
|
||||||
|
|
||||||
@ -678,7 +786,7 @@ secop-core (0.0.7) unstable; urgency=low
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Tue, 01 Aug 2017 13:52:15 +0200
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Tue, 01 Aug 2017 13:52:15 +0200
|
||||||
|
|
||||||
secop-core (0.0.6) unstable; urgency=low
|
secop-core (0.0.6) unstable; urgency=low
|
||||||
|
|
||||||
@ -688,7 +796,7 @@ secop-core (0.0.6) unstable; urgency=low
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Tue, 01 Aug 2017 13:39:07 +0200
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Tue, 01 Aug 2017 13:39:07 +0200
|
||||||
|
|
||||||
secop-core (0.0.5) unstable; urgency=low
|
secop-core (0.0.5) unstable; urgency=low
|
||||||
|
|
||||||
@ -697,7 +805,7 @@ secop-core (0.0.5) unstable; urgency=low
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Tue, 01 Aug 2017 13:11:43 +0200
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Tue, 01 Aug 2017 13:11:43 +0200
|
||||||
|
|
||||||
secop-core (0.0.4) unstable; urgency=low
|
secop-core (0.0.4) unstable; urgency=low
|
||||||
|
|
||||||
@ -706,7 +814,7 @@ secop-core (0.0.4) unstable; urgency=low
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 27 Jul 2017 11:39:42 +0200
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 27 Jul 2017 11:39:42 +0200
|
||||||
|
|
||||||
secop-core (0.0.3) unstable; urgency=low
|
secop-core (0.0.3) unstable; urgency=low
|
||||||
|
|
||||||
@ -716,7 +824,7 @@ secop-core (0.0.3) unstable; urgency=low
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Thu, 27 Jul 2017 11:27:28 +0200
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Thu, 27 Jul 2017 11:27:28 +0200
|
||||||
|
|
||||||
secop-core (0.0.2) unstable; urgency=medium
|
secop-core (0.0.2) unstable; urgency=medium
|
||||||
|
|
||||||
@ -794,4 +902,4 @@ secop-core (0.0.2) unstable; urgency=medium
|
|||||||
|
|
||||||
[ Jenkins ]
|
[ Jenkins ]
|
||||||
|
|
||||||
-- Jenkins <jenkins@debuild.taco.frm2> Wed, 19 Jul 2017 11:44:13 +0200
|
-- Jenkins <jenkins@debuild.taco.frm2.tum.de> Wed, 19 Jul 2017 11:44:13 +0200
|
||||||
|
1
debian/secop-core.install
vendored
1
debian/secop-core.install
vendored
@ -1,5 +1,4 @@
|
|||||||
usr/bin/secop-server
|
usr/bin/secop-server
|
||||||
usr/bin/secop-console
|
|
||||||
usr/lib/python3.*/dist-packages/secop/*.py
|
usr/lib/python3.*/dist-packages/secop/*.py
|
||||||
usr/lib/python3.*/dist-packages/secop/lib
|
usr/lib/python3.*/dist-packages/secop/lib
|
||||||
usr/lib/python3.*/dist-packages/secop/client
|
usr/lib/python3.*/dist-packages/secop/client
|
||||||
|
@ -4,8 +4,10 @@ Reference
|
|||||||
Module Base Classes
|
Module Base Classes
|
||||||
...................
|
...................
|
||||||
|
|
||||||
|
.. autodata:: secop.modules.Done
|
||||||
|
|
||||||
.. autoclass:: secop.modules.Module
|
.. autoclass:: secop.modules.Module
|
||||||
:members: earlyInit, initModule, startModule, pollerClass
|
:members: earlyInit, initModule, startModule
|
||||||
|
|
||||||
.. autoclass:: secop.modules.Readable
|
.. autoclass:: secop.modules.Readable
|
||||||
:members: Status
|
:members: Status
|
||||||
@ -49,11 +51,15 @@ Communication
|
|||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
:members: communicate
|
:members: communicate
|
||||||
|
|
||||||
.. autoclass:: secop.stringio.StringIO
|
.. autoclass:: secop.io.StringIO
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
:members: communicate, multicomm
|
:members: communicate, multicomm
|
||||||
|
|
||||||
.. autoclass:: secop.stringio.HasIodev
|
.. autoclass:: secop.io.BytesIO
|
||||||
|
:show-inheritance:
|
||||||
|
:members: communicate, multicomm
|
||||||
|
|
||||||
|
.. autoclass:: secop.io.HasIO
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
.. autoclass:: secop.iohandler.IOHandlerBase
|
.. autoclass:: secop.iohandler.IOHandlerBase
|
||||||
|
@ -22,7 +22,7 @@ CCU4 luckily has a very simple and logical protocol:
|
|||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
# the most common Frappy classes can be imported from secop.core
|
# the most common Frappy classes can be imported from secop.core
|
||||||
from secop.core import Readable, Parameter, FloatRange, BoolType, StringIO, HasIodev
|
from secop.core import Readable, Parameter, FloatRange, BoolType, StringIO, HasIO
|
||||||
|
|
||||||
|
|
||||||
class CCU4IO(StringIO):
|
class CCU4IO(StringIO):
|
||||||
@ -34,14 +34,13 @@ CCU4 luckily has a very simple and logical protocol:
|
|||||||
identification = [('cid', r'CCU4.*')]
|
identification = [('cid', r'CCU4.*')]
|
||||||
|
|
||||||
|
|
||||||
# inheriting the HasIodev mixin creates us a private attribute *_iodev*
|
# inheriting HasIO allows us to use the communicate method for talking with the hardware
|
||||||
# for talking with the hardware
|
|
||||||
# Readable as a base class defines the value and status parameters
|
# Readable as a base class defines the value and status parameters
|
||||||
class HeLevel(HasIodev, Readable):
|
class HeLevel(HasIO, Readable):
|
||||||
"""He Level channel of CCU4"""
|
"""He Level channel of CCU4"""
|
||||||
|
|
||||||
# define the communication class to create the IO module
|
# define the communication class to create the IO module
|
||||||
iodevClass = CCU4IO
|
ioClass = CCU4IO
|
||||||
|
|
||||||
# define or alter the parameters
|
# define or alter the parameters
|
||||||
# as Readable.value exists already, we give only the modified property 'unit'
|
# as Readable.value exists already, we give only the modified property 'unit'
|
||||||
@ -49,7 +48,7 @@ CCU4 luckily has a very simple and logical protocol:
|
|||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
# method for reading the main value
|
# method for reading the main value
|
||||||
reply = self._iodev.communicate('h') # send 'h\n' and get the reply 'h=<value>\n'
|
reply = self.communicate('h') # send 'h\n' and get the reply 'h=<value>\n'
|
||||||
name, txtvalue = reply.split('=')
|
name, txtvalue = reply.split('=')
|
||||||
assert name == 'h' # check that we got a reply to our command
|
assert name == 'h' # check that we got a reply to our command
|
||||||
return txtvalue # the framework will automatically convert the string to a float
|
return txtvalue # the framework will automatically convert the string to a float
|
||||||
@ -115,17 +114,17 @@ the status codes from the hardware to the standard SECoP status codes.
|
|||||||
}
|
}
|
||||||
|
|
||||||
def read_status(self):
|
def read_status(self):
|
||||||
name, txtvalue = self._iodev.communicate('hsf').split('=')
|
name, txtvalue = self.communicate('hsf').split('=')
|
||||||
assert name == 'hsf'
|
assert name == 'hsf'
|
||||||
return self.STATUS_MAP(int(txtvalue))
|
return self.STATUS_MAP(int(txtvalue))
|
||||||
|
|
||||||
def read_empty_length(self):
|
def read_empty_length(self):
|
||||||
name, txtvalue = self._iodev.communicate('hem').split('=')
|
name, txtvalue = self.communicate('hem').split('=')
|
||||||
assert name == 'hem'
|
assert name == 'hem'
|
||||||
return txtvalue
|
return txtvalue
|
||||||
|
|
||||||
def write_empty_length(self, value):
|
def write_empty_length(self, value):
|
||||||
name, txtvalue = self._iodev.communicate('hem=%g' % value).split('=')
|
name, txtvalue = self.communicate('hem=%g' % value).split('=')
|
||||||
assert name == 'hem'
|
assert name == 'hem'
|
||||||
return txtvalue
|
return txtvalue
|
||||||
|
|
||||||
@ -152,7 +151,7 @@ which means it might be worth to create a *query* method, and then the
|
|||||||
for changing a parameter
|
for changing a parameter
|
||||||
:returns: the (new) value of the parameter
|
:returns: the (new) value of the parameter
|
||||||
"""
|
"""
|
||||||
name, txtvalue = self._iodev.communicate(cmd).split('=')
|
name, txtvalue = self.communicate(cmd).split('=')
|
||||||
assert name == cmd.split('=')[0] # check that we got a reply to our command
|
assert name == cmd.split('=')[0] # check that we got a reply to our command
|
||||||
return txtvalue # Frappy will automatically convert the string to the needed data type
|
return txtvalue # Frappy will automatically convert the string to the needed data type
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# doc
|
# doc
|
||||||
sphinx_rtd_theme
|
sphinx_rtd_theme
|
||||||
Sphinx>=1.2.1
|
Sphinx>=1.2.1
|
||||||
|
# for generating docu
|
||||||
markdown>=2.6
|
markdown>=2.6
|
||||||
# test suite
|
# test suite
|
||||||
pytest
|
pytest
|
||||||
|
@ -31,10 +31,16 @@ from secop.datatypes import ArrayOf, BLOBType, BoolType, EnumType, \
|
|||||||
from secop.iohandler import IOHandler, IOHandlerBase
|
from secop.iohandler import IOHandler, IOHandlerBase
|
||||||
from secop.lib.enum import Enum
|
from secop.lib.enum import Enum
|
||||||
from secop.modules import Attached, Communicator, \
|
from secop.modules import Attached, Communicator, \
|
||||||
Done, Drivable, Module, Readable, Writable
|
Done, Drivable, Module, Readable, Writable, HasAccessibles
|
||||||
from secop.params import Command, Parameter
|
from secop.params import Command, Parameter
|
||||||
from secop.poller import AUTO, DYNAMIC, REGULAR, SLOW
|
|
||||||
from secop.properties import Property
|
from secop.properties import Property
|
||||||
from secop.proxy import Proxy, SecNode, proxy_class
|
from secop.proxy import Proxy, SecNode, proxy_class
|
||||||
from secop.io import HasIodev, StringIO, BytesIO
|
from secop.io import HasIO, StringIO, BytesIO, HasIodev # TODO: remove HasIodev (legacy stuff)
|
||||||
from secop.persistent import PersistentMixin, PersistentParam
|
from secop.persistent import PersistentMixin, PersistentParam
|
||||||
|
from secop.rwhandler import ReadHandler, WriteHandler, CommonReadHandler, \
|
||||||
|
CommonWriteHandler, nopoll
|
||||||
|
|
||||||
|
ERROR = Drivable.Status.ERROR
|
||||||
|
WARN = Drivable.Status.WARN
|
||||||
|
BUSY = Drivable.Status.BUSY
|
||||||
|
IDLE = Drivable.Status.IDLE
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
"""Define validated data types."""
|
"""Define validated data types."""
|
||||||
|
|
||||||
# pylint: disable=abstract-method
|
# pylint: disable=abstract-method, too-many-lines
|
||||||
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
@ -30,20 +30,12 @@ from base64 import b64decode, b64encode
|
|||||||
|
|
||||||
from secop.errors import BadValueError, \
|
from secop.errors import BadValueError, \
|
||||||
ConfigError, ProgrammingError, ProtocolError
|
ConfigError, ProgrammingError, ProtocolError
|
||||||
from secop.lib import clamp
|
from secop.lib import clamp, generalConfig
|
||||||
from secop.lib.enum import Enum
|
from secop.lib.enum import Enum
|
||||||
from secop.parse import Parser
|
from secop.parse import Parser
|
||||||
from secop.properties import HasProperties, Property
|
from secop.properties import HasProperties, Property
|
||||||
|
|
||||||
# Only export these classes for 'from secop.datatypes import *'
|
generalConfig.set_default('lazy_number_validation', False)
|
||||||
__all__ = [
|
|
||||||
'DataType', 'get_datatype',
|
|
||||||
'FloatRange', 'IntRange', 'ScaledInteger',
|
|
||||||
'BoolType', 'EnumType',
|
|
||||||
'BLOBType', 'StringType', 'TextType',
|
|
||||||
'TupleOf', 'ArrayOf', 'StructOf',
|
|
||||||
'CommandType', 'StatusType',
|
|
||||||
]
|
|
||||||
|
|
||||||
# *DEFAULT* limits for IntRange/ScaledIntegers transport serialisation
|
# *DEFAULT* limits for IntRange/ScaledIntegers transport serialisation
|
||||||
DEFAULT_MIN_INT = -16777216
|
DEFAULT_MIN_INT = -16777216
|
||||||
@ -53,6 +45,11 @@ UNLIMITED = 1 << 64 # internal limit for integers, is probably high enough for
|
|||||||
Parser = Parser()
|
Parser = Parser()
|
||||||
|
|
||||||
|
|
||||||
|
class DiscouragedConversion(BadValueError):
|
||||||
|
"""the discouraged conversion string - > float happened"""
|
||||||
|
log_message = True
|
||||||
|
|
||||||
|
|
||||||
# base class for all DataTypes
|
# base class for all DataTypes
|
||||||
class DataType(HasProperties):
|
class DataType(HasProperties):
|
||||||
"""base class for all data types"""
|
"""base class for all data types"""
|
||||||
@ -63,7 +60,7 @@ class DataType(HasProperties):
|
|||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
"""check if given value (a python obj) is valid for this datatype
|
"""check if given value (a python obj) is valid for this datatype
|
||||||
|
|
||||||
returns the value or raises an appropriate exception"""
|
returns the (possibly converted) value or raises an appropriate exception"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def from_string(self, text):
|
def from_string(self, text):
|
||||||
@ -191,10 +188,16 @@ class FloatRange(DataType):
|
|||||||
return self.get_info(type='double')
|
return self.get_info(type='double')
|
||||||
|
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
|
try:
|
||||||
|
value += 0.0 # do not accept strings here
|
||||||
|
except Exception:
|
||||||
try:
|
try:
|
||||||
value = float(value)
|
value = float(value)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise BadValueError('Can not convert %r to float' % value) from None
|
raise BadValueError('Can not convert %r to float' % value) from None
|
||||||
|
if not generalConfig.lazy_number_validation:
|
||||||
|
raise DiscouragedConversion('automatic string to float conversion no longer supported') from None
|
||||||
|
|
||||||
# map +/-infty to +/-max possible number
|
# map +/-infty to +/-max possible number
|
||||||
value = clamp(-sys.float_info.max, value, sys.float_info.max)
|
value = clamp(-sys.float_info.max, value, sys.float_info.max)
|
||||||
|
|
||||||
@ -232,6 +235,18 @@ class FloatRange(DataType):
|
|||||||
return ' '.join([self.fmtstr % value, unit])
|
return ' '.join([self.fmtstr % value, unit])
|
||||||
return self.fmtstr % value
|
return self.fmtstr % value
|
||||||
|
|
||||||
|
def problematic_range(self, target_type):
|
||||||
|
"""check problematic range
|
||||||
|
|
||||||
|
returns True when self.min or self.max is given, not 0 and equal to the same limit on target_type.
|
||||||
|
"""
|
||||||
|
value_info = self.get_info()
|
||||||
|
target_info = target_type.get_info()
|
||||||
|
minval = value_info.get('min') # None when -infinite
|
||||||
|
maxval = value_info.get('max') # None when +infinite
|
||||||
|
return ((minval and minval == target_info.get('min')) or
|
||||||
|
(maxval and maxval == target_info.get('max')))
|
||||||
|
|
||||||
def compatible(self, other):
|
def compatible(self, other):
|
||||||
if not isinstance(other, (FloatRange, ScaledInteger)):
|
if not isinstance(other, (FloatRange, ScaledInteger)):
|
||||||
raise BadValueError('incompatible datatypes')
|
raise BadValueError('incompatible datatypes')
|
||||||
@ -264,11 +279,17 @@ class IntRange(DataType):
|
|||||||
return self.get_info(type='int')
|
return self.get_info(type='int')
|
||||||
|
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
|
try:
|
||||||
|
fvalue = value + 0.0 # do not accept strings here
|
||||||
|
value = int(value)
|
||||||
|
except Exception:
|
||||||
try:
|
try:
|
||||||
fvalue = float(value)
|
fvalue = float(value)
|
||||||
value = int(value)
|
value = int(value)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise BadValueError('Can not convert %r to int' % value) from None
|
raise BadValueError('Can not convert %r to int' % value) from None
|
||||||
|
if not generalConfig.lazy_number_validation:
|
||||||
|
raise DiscouragedConversion('automatic string to float conversion no longer supported') from None
|
||||||
if not self.min <= value <= self.max or round(fvalue) != fvalue:
|
if not self.min <= value <= self.max or round(fvalue) != fvalue:
|
||||||
raise BadValueError('%r should be an int between %d and %d' %
|
raise BadValueError('%r should be an int between %d and %d' %
|
||||||
(value, self.min, self.max))
|
(value, self.min, self.max))
|
||||||
@ -298,13 +319,15 @@ class IntRange(DataType):
|
|||||||
return '%d' % value
|
return '%d' % value
|
||||||
|
|
||||||
def compatible(self, other):
|
def compatible(self, other):
|
||||||
if isinstance(other, IntRange):
|
if isinstance(other, (IntRange, FloatRange, ScaledInteger)):
|
||||||
other(self.min)
|
other(self.min)
|
||||||
other(self.max)
|
other(self.max)
|
||||||
return
|
return
|
||||||
# this will accept some EnumType, BoolType
|
if isinstance(other, (EnumType, BoolType)):
|
||||||
|
# the following loop will not cycle more than the number of Enum elements
|
||||||
for i in range(self.min, self.max + 1):
|
for i in range(self.min, self.max + 1):
|
||||||
other(i)
|
other(i)
|
||||||
|
raise BadValueError('incompatible datatypes')
|
||||||
|
|
||||||
|
|
||||||
class ScaledInteger(DataType):
|
class ScaledInteger(DataType):
|
||||||
@ -368,10 +391,15 @@ class ScaledInteger(DataType):
|
|||||||
max=int((self.max + self.scale * 0.5) // self.scale))
|
max=int((self.max + self.scale * 0.5) // self.scale))
|
||||||
|
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
|
try:
|
||||||
|
value += 0.0 # do not accept strings here
|
||||||
|
except Exception:
|
||||||
try:
|
try:
|
||||||
value = float(value)
|
value = float(value)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise BadValueError('Can not convert %r to float' % value) from None
|
raise BadValueError('Can not convert %r to float' % value) from None
|
||||||
|
if not generalConfig.lazy_number_validation:
|
||||||
|
raise DiscouragedConversion('automatic string to float conversion no longer supported') from None
|
||||||
prec = max(self.scale, abs(value * self.relative_resolution),
|
prec = max(self.scale, abs(value * self.relative_resolution),
|
||||||
self.absolute_resolution)
|
self.absolute_resolution)
|
||||||
if self.min - prec <= value <= self.max + prec:
|
if self.min - prec <= value <= self.max + prec:
|
||||||
@ -427,6 +455,9 @@ class EnumType(DataType):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
if members is not None:
|
if members is not None:
|
||||||
kwds.update(members)
|
kwds.update(members)
|
||||||
|
if isinstance(enum_or_name, str):
|
||||||
|
self._enum = Enum(enum_or_name, kwds) # allow 'self' as name
|
||||||
|
else:
|
||||||
self._enum = Enum(enum_or_name, **kwds)
|
self._enum = Enum(enum_or_name, **kwds)
|
||||||
self.default = self._enum[self._enum.members[0]]
|
self.default = self._enum[self._enum.members[0]]
|
||||||
|
|
||||||
@ -852,6 +883,8 @@ class StructOf(DataType):
|
|||||||
:param optional: a list of optional members
|
:param optional: a list of optional members
|
||||||
:param members: names as keys and types as values for all members
|
:param members: names as keys and types as values for all members
|
||||||
"""
|
"""
|
||||||
|
# Remark: assignment of parameters containing partial structs in their datatype
|
||||||
|
# are (and can) not be handled here! This has to be done manually in the write method
|
||||||
def __init__(self, optional=None, **members):
|
def __init__(self, optional=None, **members):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.members = members
|
self.members = members
|
||||||
@ -955,10 +988,9 @@ class CommandType(DataType):
|
|||||||
return props
|
return props
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
argstr = repr(self.argument) if self.argument else ''
|
|
||||||
if self.result is None:
|
if self.result is None:
|
||||||
return 'CommandType(%s)' % argstr
|
return 'CommandType(%s)' % (repr(self.argument) if self.argument else '')
|
||||||
return 'CommandType(%s, %s)' % (argstr, repr(self.result))
|
return 'CommandType(%s, %s)' % (repr(self.argument), repr(self.result))
|
||||||
|
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
"""return the validated argument value or raise"""
|
"""return the validated argument value or raise"""
|
||||||
@ -1119,37 +1151,26 @@ def floatargs(kwds):
|
|||||||
DATATYPES = dict(
|
DATATYPES = dict(
|
||||||
bool = lambda **kwds:
|
bool = lambda **kwds:
|
||||||
BoolType(),
|
BoolType(),
|
||||||
|
|
||||||
int = lambda min, max, **kwds:
|
int = lambda min, max, **kwds:
|
||||||
IntRange(minval=min, maxval=max),
|
IntRange(minval=min, maxval=max),
|
||||||
|
|
||||||
scaled = lambda scale, min, max, **kwds:
|
scaled = lambda scale, min, max, **kwds:
|
||||||
ScaledInteger(scale=scale, minval=min*scale, maxval=max*scale, **floatargs(kwds)),
|
ScaledInteger(scale=scale, minval=min*scale, maxval=max*scale, **floatargs(kwds)),
|
||||||
|
|
||||||
double = lambda min=None, max=None, **kwds:
|
double = lambda min=None, max=None, **kwds:
|
||||||
FloatRange(minval=min, maxval=max, **floatargs(kwds)),
|
FloatRange(minval=min, maxval=max, **floatargs(kwds)),
|
||||||
|
|
||||||
blob = lambda maxbytes, minbytes=0, **kwds:
|
blob = lambda maxbytes, minbytes=0, **kwds:
|
||||||
BLOBType(minbytes=minbytes, maxbytes=maxbytes),
|
BLOBType(minbytes=minbytes, maxbytes=maxbytes),
|
||||||
|
|
||||||
string = lambda minchars=0, maxchars=None, isUTF8=False, **kwds:
|
string = lambda minchars=0, maxchars=None, isUTF8=False, **kwds:
|
||||||
StringType(minchars=minchars, maxchars=maxchars, isUTF8=isUTF8),
|
StringType(minchars=minchars, maxchars=maxchars, isUTF8=isUTF8),
|
||||||
|
|
||||||
array = lambda maxlen, members, minlen=0, pname='', **kwds:
|
array = lambda maxlen, members, minlen=0, pname='', **kwds:
|
||||||
ArrayOf(get_datatype(members, pname), minlen=minlen, maxlen=maxlen),
|
ArrayOf(get_datatype(members, pname), minlen=minlen, maxlen=maxlen),
|
||||||
|
|
||||||
tuple = lambda members, pname='', **kwds:
|
tuple = lambda members, pname='', **kwds:
|
||||||
TupleOf(*tuple((get_datatype(t, pname) for t in members))),
|
TupleOf(*tuple((get_datatype(t, pname) for t in members))),
|
||||||
|
|
||||||
enum = lambda members, pname='', **kwds:
|
enum = lambda members, pname='', **kwds:
|
||||||
EnumType(pname, members=members),
|
EnumType(pname, members=members),
|
||||||
|
|
||||||
struct = lambda members, optional=None, pname='', **kwds:
|
struct = lambda members, optional=None, pname='', **kwds:
|
||||||
StructOf(optional, **dict((n, get_datatype(t, pname)) for n, t in list(members.items()))),
|
StructOf(optional, **dict((n, get_datatype(t, pname)) for n, t in list(members.items()))),
|
||||||
|
|
||||||
command = lambda argument=None, result=None, pname='', **kwds:
|
command = lambda argument=None, result=None, pname='', **kwds:
|
||||||
CommandType(get_datatype(argument, pname), get_datatype(result)),
|
CommandType(get_datatype(argument, pname), get_datatype(result)),
|
||||||
|
|
||||||
limit = lambda members, pname='', **kwds:
|
limit = lambda members, pname='', **kwds:
|
||||||
LimitsType(get_datatype(members, pname)),
|
LimitsType(get_datatype(members, pname)),
|
||||||
)
|
)
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
class SECoPError(RuntimeError):
|
class SECoPError(RuntimeError):
|
||||||
|
|
||||||
def __init__(self, *args, **kwds):
|
def __init__(self, *args, **kwds):
|
||||||
RuntimeError.__init__(self)
|
super().__init__()
|
||||||
self.args = args
|
self.args = args
|
||||||
for k, v in list(kwds.items()):
|
for k, v in list(kwds.items()):
|
||||||
setattr(self, k, v)
|
setattr(self, k, v)
|
||||||
@ -151,7 +151,8 @@ EXCEPTIONS = dict(
|
|||||||
IsError=IsErrorError,
|
IsError=IsErrorError,
|
||||||
Disabled=DisabledError,
|
Disabled=DisabledError,
|
||||||
SyntaxError=ProtocolError,
|
SyntaxError=ProtocolError,
|
||||||
NotImplementedError=NotImplementedError,
|
NotImplemented=NotImplementedError,
|
||||||
|
ProtocolError=ProtocolError,
|
||||||
InternalError=InternalError,
|
InternalError=InternalError,
|
||||||
# internal short versions (candidates for spec)
|
# internal short versions (candidates for spec)
|
||||||
Protocol=ProtocolError,
|
Protocol=ProtocolError,
|
||||||
|
@ -39,7 +39,7 @@ COMMENT = 'comment'
|
|||||||
class MainWindow(QMainWindow):
|
class MainWindow(QMainWindow):
|
||||||
|
|
||||||
def __init__(self, file_path=None, parent=None):
|
def __init__(self, file_path=None, parent=None):
|
||||||
QMainWindow.__init__(self, parent)
|
super().__init__(parent)
|
||||||
loadUi(self, 'mainwindow.ui')
|
loadUi(self, 'mainwindow.ui')
|
||||||
self.tabWidget.currentChanged.connect(self.tab_relevant_btns_disable)
|
self.tabWidget.currentChanged.connect(self.tab_relevant_btns_disable)
|
||||||
if file_path is None:
|
if file_path is None:
|
||||||
|
@ -26,7 +26,7 @@ from secop.gui.qt import QHBoxLayout, QSizePolicy, QSpacerItem, Qt, QWidget
|
|||||||
|
|
||||||
class NodeDisplay(QWidget):
|
class NodeDisplay(QWidget):
|
||||||
def __init__(self, file_path=None, parent=None):
|
def __init__(self, file_path=None, parent=None):
|
||||||
QWidget.__init__(self, parent)
|
super().__init__(parent)
|
||||||
loadUi(self, 'node_display.ui')
|
loadUi(self, 'node_display.ui')
|
||||||
self.saved = bool(file_path)
|
self.saved = bool(file_path)
|
||||||
self.created = self.tree_widget.set_file(file_path)
|
self.created = self.tree_widget.set_file(file_path)
|
||||||
|
@ -44,7 +44,7 @@ class TreeWidgetItem(QTreeWidgetItem):
|
|||||||
the datatype passed onto ValueWidget should be on of secop.datatypes"""
|
the datatype passed onto ValueWidget should be on of secop.datatypes"""
|
||||||
# TODO: like stated in docstring the datatype for parameters and
|
# TODO: like stated in docstring the datatype for parameters and
|
||||||
# properties must be found out through their object
|
# properties must be found out through their object
|
||||||
QTreeWidgetItem.__init__(self, parent)
|
super().__init__(parent)
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
self.name = name
|
self.name = name
|
||||||
self.class_object = class_object
|
self.class_object = class_object
|
||||||
@ -129,7 +129,7 @@ class ValueWidget(QWidget):
|
|||||||
|
|
||||||
def __init__(self, name='', value='', datatype=None, kind='', parent=None):
|
def __init__(self, name='', value='', datatype=None, kind='', parent=None):
|
||||||
# TODO: implement: change module/interface class
|
# TODO: implement: change module/interface class
|
||||||
QWidget.__init__(self, parent)
|
super().__init__(parent)
|
||||||
self.datatype = datatype
|
self.datatype = datatype
|
||||||
self.layout = QVBoxLayout()
|
self.layout = QVBoxLayout()
|
||||||
self.name_label = QLabel(name)
|
self.name_label = QLabel(name)
|
||||||
@ -205,7 +205,7 @@ class ValueWidget(QWidget):
|
|||||||
|
|
||||||
class ChangeNameDialog(QDialog):
|
class ChangeNameDialog(QDialog):
|
||||||
def __init__(self, current_name='', invalid_names=None, parent=None):
|
def __init__(self, current_name='', invalid_names=None, parent=None):
|
||||||
QWidget.__init__(self, parent)
|
super().__init__(parent)
|
||||||
loadUi(self, 'change_name_dialog.ui')
|
loadUi(self, 'change_name_dialog.ui')
|
||||||
self.invalid_names = invalid_names
|
self.invalid_names = invalid_names
|
||||||
self.name.setText(current_name)
|
self.name.setText(current_name)
|
||||||
|
@ -29,7 +29,7 @@ from secop.modules import Module
|
|||||||
from secop.params import Parameter
|
from secop.params import Parameter
|
||||||
from secop.properties import Property
|
from secop.properties import Property
|
||||||
from secop.protocol.interface.tcp import TCPServer
|
from secop.protocol.interface.tcp import TCPServer
|
||||||
from secop.server import getGeneralConfig
|
from secop.server import generalConfig
|
||||||
|
|
||||||
uipath = path.dirname(__file__)
|
uipath = path.dirname(__file__)
|
||||||
|
|
||||||
@ -106,7 +106,7 @@ def get_file_paths(widget, open_file=True):
|
|||||||
|
|
||||||
def get_modules():
|
def get_modules():
|
||||||
modules = {}
|
modules = {}
|
||||||
base_path = getGeneralConfig()['basedir']
|
base_path = generalConfig.basedir
|
||||||
# pylint: disable=too-many-nested-blocks
|
# pylint: disable=too-many-nested-blocks
|
||||||
for dirname in listdir(base_path):
|
for dirname in listdir(base_path):
|
||||||
if dirname.startswith('secop_'):
|
if dirname.startswith('secop_'):
|
||||||
@ -156,7 +156,7 @@ def get_interface_class_from_name(name):
|
|||||||
def get_interfaces():
|
def get_interfaces():
|
||||||
# TODO class must be found out like for modules
|
# TODO class must be found out like for modules
|
||||||
interfaces = []
|
interfaces = []
|
||||||
interface_path = path.join(getGeneralConfig()['basedir'], 'secop',
|
interface_path = path.join(generalConfig.basedir, 'secop',
|
||||||
'protocol', 'interface')
|
'protocol', 'interface')
|
||||||
for filename in listdir(interface_path):
|
for filename in listdir(interface_path):
|
||||||
if path.isfile(path.join(interface_path, filename)) and \
|
if path.isfile(path.join(interface_path, filename)) and \
|
||||||
|
@ -31,7 +31,7 @@ from secop.gui.cfg_editor.utils import get_all_items, \
|
|||||||
get_props, loadUi, set_name_edit_style, setActionIcon
|
get_props, loadUi, set_name_edit_style, setActionIcon
|
||||||
from secop.gui.qt import QComboBox, QDialog, QDialogButtonBox, QLabel, \
|
from secop.gui.qt import QComboBox, QDialog, QDialogButtonBox, QLabel, \
|
||||||
QLineEdit, QMenu, QPoint, QSize, QStandardItem, QStandardItemModel, \
|
QLineEdit, QMenu, QPoint, QSize, QStandardItem, QStandardItemModel, \
|
||||||
Qt, QTabBar, QTextEdit, QTreeView, QTreeWidget, QWidget, pyqtSignal
|
Qt, QTabBar, QTextEdit, QTreeView, QTreeWidget, pyqtSignal
|
||||||
|
|
||||||
NODE = 'node'
|
NODE = 'node'
|
||||||
MODULE = 'module'
|
MODULE = 'module'
|
||||||
@ -47,7 +47,7 @@ class TreeWidget(QTreeWidget):
|
|||||||
add_canceled = pyqtSignal()
|
add_canceled = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
QTreeWidget.__init__(self, parent)
|
super().__init__(parent)
|
||||||
self.file_path = None
|
self.file_path = None
|
||||||
self.setIconSize(QSize(24, 24))
|
self.setIconSize(QSize(24, 24))
|
||||||
self.setSelectionMode(QTreeWidget.SingleSelection)
|
self.setSelectionMode(QTreeWidget.SingleSelection)
|
||||||
@ -335,7 +335,7 @@ class AddDialog(QDialog):
|
|||||||
"""Notes:
|
"""Notes:
|
||||||
self.get_value: is mapped to the specific method for getting
|
self.get_value: is mapped to the specific method for getting
|
||||||
the value from self.value"""
|
the value from self.value"""
|
||||||
QWidget.__init__(self, parent)
|
super().__init__(parent)
|
||||||
loadUi(self, 'add_dialog.ui')
|
loadUi(self, 'add_dialog.ui')
|
||||||
self.setWindowTitle('add %s' % kind)
|
self.setWindowTitle('add %s' % kind)
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
@ -402,7 +402,7 @@ class AddDialog(QDialog):
|
|||||||
|
|
||||||
class TabBar(QTabBar):
|
class TabBar(QTabBar):
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
QTabBar.__init__(self, parent)
|
super().__init__(parent)
|
||||||
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||||
self.context_pos = QPoint(0, 0)
|
self.context_pos = QPoint(0, 0)
|
||||||
self.menu = QMenu()
|
self.menu = QMenu()
|
||||||
@ -436,7 +436,7 @@ class TabBar(QTabBar):
|
|||||||
|
|
||||||
class TreeComboBox(QComboBox):
|
class TreeComboBox(QComboBox):
|
||||||
def __init__(self, value_dict, parent=None):
|
def __init__(self, value_dict, parent=None):
|
||||||
QComboBox.__init__(self, parent)
|
super().__init__(parent)
|
||||||
self.tree_view = QTreeView()
|
self.tree_view = QTreeView()
|
||||||
self.tree_view.setHeaderHidden(True)
|
self.tree_view.setHeaderHidden(True)
|
||||||
self.tree_view.expanded.connect(self.resize_length)
|
self.tree_view.expanded.connect(self.resize_length)
|
||||||
|
@ -44,7 +44,7 @@ class QSECNode(QObject):
|
|||||||
logEntry = pyqtSignal(str)
|
logEntry = pyqtSignal(str)
|
||||||
|
|
||||||
def __init__(self, uri, parent=None):
|
def __init__(self, uri, parent=None):
|
||||||
QObject.__init__(self, parent)
|
super().__init__(parent)
|
||||||
self.conn = conn = secop.client.SecopClient(uri)
|
self.conn = conn = secop.client.SecopClient(uri)
|
||||||
conn.validate_data = True
|
conn.validate_data = True
|
||||||
self.log = conn.log
|
self.log = conn.log
|
||||||
@ -83,10 +83,7 @@ class QSECNode(QObject):
|
|||||||
return self.conn.getParameter(module, parameter, True)
|
return self.conn.getParameter(module, parameter, True)
|
||||||
|
|
||||||
def execCommand(self, module, command, argument):
|
def execCommand(self, module, command, argument):
|
||||||
try:
|
|
||||||
return self.conn.execCommand(module, command, argument)
|
return self.conn.execCommand(module, command, argument)
|
||||||
except Exception as e:
|
|
||||||
return 'ERROR: %r' % e, {}
|
|
||||||
|
|
||||||
def queryCache(self, module):
|
def queryCache(self, module):
|
||||||
return {k: Value(*self.conn.cache[(module, k)])
|
return {k: Value(*self.conn.cache[(module, k)])
|
||||||
@ -115,7 +112,7 @@ class QSECNode(QObject):
|
|||||||
|
|
||||||
class MainWindow(QMainWindow):
|
class MainWindow(QMainWindow):
|
||||||
def __init__(self, hosts, parent=None):
|
def __init__(self, hosts, parent=None):
|
||||||
super(MainWindow, self).__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
loadUi(self, 'mainwindow.ui')
|
loadUi(self, 'mainwindow.ui')
|
||||||
|
|
||||||
|
@ -160,7 +160,7 @@ class MiniPlotFitCurve(MiniPlotCurve):
|
|||||||
return float('-inf')
|
return float('-inf')
|
||||||
|
|
||||||
def __init__(self, formula, params):
|
def __init__(self, formula, params):
|
||||||
super(MiniPlotFitCurve, self).__init__()
|
super().__init__()
|
||||||
self.formula = formula
|
self.formula = formula
|
||||||
self.params = params
|
self.params = params
|
||||||
|
|
||||||
@ -193,7 +193,7 @@ class MiniPlot(QWidget):
|
|||||||
autoticky = True
|
autoticky = True
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
QWidget.__init__(self, parent)
|
super().__init__(parent)
|
||||||
self.xmin = self.xmax = None
|
self.xmin = self.xmax = None
|
||||||
self.ymin = self.ymax = None
|
self.ymin = self.ymax = None
|
||||||
self.curves = []
|
self.curves = []
|
||||||
|
@ -32,7 +32,7 @@ from secop.gui.valuewidgets import get_widget
|
|||||||
|
|
||||||
class CommandDialog(QDialog):
|
class CommandDialog(QDialog):
|
||||||
def __init__(self, cmdname, argument, parent=None):
|
def __init__(self, cmdname, argument, parent=None):
|
||||||
super(CommandDialog, self).__init__(parent)
|
super().__init__(parent)
|
||||||
loadUi(self, 'cmddialog.ui')
|
loadUi(self, 'cmddialog.ui')
|
||||||
|
|
||||||
self.setWindowTitle('Arguments for %s' % cmdname)
|
self.setWindowTitle('Arguments for %s' % cmdname)
|
||||||
@ -58,7 +58,7 @@ class CommandDialog(QDialog):
|
|||||||
return True, self.widgets[0].get_value()
|
return True, self.widgets[0].get_value()
|
||||||
|
|
||||||
def exec_(self):
|
def exec_(self):
|
||||||
if super(CommandDialog, self).exec_():
|
if super().exec_():
|
||||||
return self.get_value()
|
return self.get_value()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -71,16 +71,17 @@ def showCommandResultDialog(command, args, result, extras=''):
|
|||||||
m.exec_()
|
m.exec_()
|
||||||
|
|
||||||
|
|
||||||
def showErrorDialog(error):
|
def showErrorDialog(command, args, error):
|
||||||
m = QMessageBox()
|
m = QMessageBox()
|
||||||
m.setText('Error %r' % error)
|
args = '' if args is None else repr(args)
|
||||||
|
m.setText('calling: %s(%s)\nraised %r' % (command, args, error))
|
||||||
m.exec_()
|
m.exec_()
|
||||||
|
|
||||||
|
|
||||||
class ParameterGroup(QWidget):
|
class ParameterGroup(QWidget):
|
||||||
|
|
||||||
def __init__(self, groupname, parent=None):
|
def __init__(self, groupname, parent=None):
|
||||||
super(ParameterGroup, self).__init__(parent)
|
super().__init__(parent)
|
||||||
loadUi(self, 'paramgroup.ui')
|
loadUi(self, 'paramgroup.ui')
|
||||||
|
|
||||||
self._groupname = groupname
|
self._groupname = groupname
|
||||||
@ -112,7 +113,7 @@ class ParameterGroup(QWidget):
|
|||||||
class CommandButton(QPushButton):
|
class CommandButton(QPushButton):
|
||||||
|
|
||||||
def __init__(self, cmdname, cmdinfo, cb, parent=None):
|
def __init__(self, cmdname, cmdinfo, cb, parent=None):
|
||||||
super(CommandButton, self).__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
self._cmdname = cmdname
|
self._cmdname = cmdname
|
||||||
self._argintype = cmdinfo['datatype'].argument # single datatype
|
self._argintype = cmdinfo['datatype'].argument # single datatype
|
||||||
@ -140,7 +141,7 @@ class CommandButton(QPushButton):
|
|||||||
class ModuleCtrl(QWidget):
|
class ModuleCtrl(QWidget):
|
||||||
|
|
||||||
def __init__(self, node, module, parent=None):
|
def __init__(self, node, module, parent=None):
|
||||||
super(ModuleCtrl, self).__init__(parent)
|
super().__init__(parent)
|
||||||
loadUi(self, 'modulectrl.ui')
|
loadUi(self, 'modulectrl.ui')
|
||||||
self._node = node
|
self._node = node
|
||||||
self._module = module
|
self._module = module
|
||||||
@ -161,10 +162,9 @@ class ModuleCtrl(QWidget):
|
|||||||
try:
|
try:
|
||||||
result, qualifiers = self._node.execCommand(
|
result, qualifiers = self._node.execCommand(
|
||||||
self._module, command, args)
|
self._module, command, args)
|
||||||
except TypeError:
|
except Exception as e:
|
||||||
result = None
|
showErrorDialog(command, args, e)
|
||||||
qualifiers = {}
|
return
|
||||||
# XXX: flag missing data report as error
|
|
||||||
if result is not None:
|
if result is not None:
|
||||||
showCommandResultDialog(command, args, result, qualifiers)
|
showCommandResultDialog(command, args, result, qualifiers)
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ class ParameterWidget(QWidget):
|
|||||||
initvalue=None,
|
initvalue=None,
|
||||||
readonly=True,
|
readonly=True,
|
||||||
parent=None):
|
parent=None):
|
||||||
super(ParameterWidget, self).__init__(parent)
|
super().__init__(parent)
|
||||||
self._module = module
|
self._module = module
|
||||||
self._paramcmd = paramcmd
|
self._paramcmd = paramcmd
|
||||||
self._datatype = datatype
|
self._datatype = datatype
|
||||||
@ -82,7 +82,6 @@ class GenericParameterWidget(ParameterWidget):
|
|||||||
else:
|
else:
|
||||||
value = fmtstr % (value.value,)
|
value = fmtstr % (value.value,)
|
||||||
self.currentLineEdit.setText(value)
|
self.currentLineEdit.setText(value)
|
||||||
# self.currentLineEdit.setText(str(value))
|
|
||||||
|
|
||||||
|
|
||||||
class EnumParameterWidget(GenericParameterWidget):
|
class EnumParameterWidget(GenericParameterWidget):
|
||||||
|
215
secop/historywriter.py
Normal file
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()))
|
109
secop/io.py
109
secop/io.py
@ -29,50 +29,78 @@ import time
|
|||||||
import threading
|
import threading
|
||||||
|
|
||||||
from secop.lib.asynconn import AsynConn, ConnectionClosed
|
from secop.lib.asynconn import AsynConn, ConnectionClosed
|
||||||
from secop.datatypes import ArrayOf, BLOBType, BoolType, FloatRange, IntRange, StringType, TupleOf, ValueType
|
from secop.datatypes import ArrayOf, BLOBType, BoolType, FloatRange, IntRange, \
|
||||||
from secop.errors import CommunicationFailedError, CommunicationSilentError, ConfigError
|
StringType, TupleOf, ValueType
|
||||||
|
from secop.errors import CommunicationFailedError, CommunicationSilentError, \
|
||||||
|
ConfigError, ProgrammingError
|
||||||
from secop.modules import Attached, Command, \
|
from secop.modules import Attached, Command, \
|
||||||
Communicator, Done, Module, Parameter, Property
|
Communicator, Done, Module, Parameter, Property
|
||||||
from secop.poller import REGULAR
|
from secop.lib import generalConfig
|
||||||
|
|
||||||
|
generalConfig.set_default('legacy_hasiodev', False)
|
||||||
|
|
||||||
HEX_CODE = re.compile(r'[0-9a-fA-F][0-9a-fA-F]$')
|
HEX_CODE = re.compile(r'[0-9a-fA-F][0-9a-fA-F]$')
|
||||||
|
|
||||||
|
|
||||||
class HasIodev(Module):
|
class HasIO(Module):
|
||||||
"""Mixin for modules using a communicator"""
|
"""Mixin for modules using a communicator"""
|
||||||
iodev = Attached()
|
io = Attached()
|
||||||
uri = Property('uri for automatic creation of the attached communication module',
|
uri = Property('uri for automatic creation of the attached communication module',
|
||||||
StringType(), default='')
|
StringType(), default='')
|
||||||
|
|
||||||
iodevDict = {}
|
ioDict = {}
|
||||||
|
ioClass = None
|
||||||
|
|
||||||
def __init__(self, name, logger, opts, srv):
|
def __init__(self, name, logger, opts, srv):
|
||||||
iodev = opts.get('iodev')
|
io = opts.get('io')
|
||||||
Module.__init__(self, name, logger, opts, srv)
|
super().__init__(name, logger, opts, srv)
|
||||||
if self.uri:
|
if self.uri:
|
||||||
opts = {'uri': self.uri, 'description': 'communication device for %s' % name,
|
opts = {'uri': self.uri, 'description': 'communication device for %s' % name,
|
||||||
'export': False}
|
'export': False}
|
||||||
ioname = self.iodevDict.get(self.uri)
|
ioname = self.ioDict.get(self.uri)
|
||||||
if not ioname:
|
if not ioname:
|
||||||
ioname = iodev or name + '_iodev'
|
ioname = io or name + '_io'
|
||||||
iodev = self.iodevClass(ioname, srv.log.getChild(ioname), opts, srv)
|
io = self.ioClass(ioname, srv.log.getChild(ioname), opts, srv) # pylint: disable=not-callable
|
||||||
srv.modules[ioname] = iodev
|
io.callingModule = []
|
||||||
self.iodevDict[self.uri] = ioname
|
srv.modules[ioname] = io
|
||||||
self.iodev = ioname
|
self.ioDict[self.uri] = ioname
|
||||||
elif not self.iodev:
|
self.io = ioname
|
||||||
raise ConfigError("Module %s needs a value for either 'uri' or 'iodev'" % name)
|
elif not io:
|
||||||
|
raise ConfigError("Module %s needs a value for either 'uri' or 'io'" % name)
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
try:
|
try:
|
||||||
self._iodev.read_is_connected()
|
self.io.read_is_connected()
|
||||||
except (CommunicationFailedError, AttributeError):
|
except (CommunicationFailedError, AttributeError):
|
||||||
# AttributeError: for missing _iodev?
|
# AttributeError: read_is_connected is not required for an io object
|
||||||
pass
|
pass
|
||||||
super().initModule()
|
super().initModule()
|
||||||
|
|
||||||
def sendRecv(self, command):
|
def communicate(self, *args):
|
||||||
return self._iodev.communicate(command)
|
return self.io.communicate(*args)
|
||||||
|
|
||||||
|
def multicomm(self, *args):
|
||||||
|
return self.io.multicomm(*args)
|
||||||
|
|
||||||
|
|
||||||
|
class HasIodev(HasIO):
|
||||||
|
# TODO: remove this legacy mixin
|
||||||
|
iodevClass = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _iodev(self):
|
||||||
|
return self.io
|
||||||
|
|
||||||
|
def __init__(self, name, logger, opts, srv):
|
||||||
|
self.ioClass = self.iodevClass
|
||||||
|
super().__init__(name, logger, opts, srv)
|
||||||
|
if generalConfig.legacy_hasiodev:
|
||||||
|
self.log.warn('using the HasIodev mixin is deprecated - use HasIO instead')
|
||||||
|
else:
|
||||||
|
self.log.error('legacy HasIodev no longer supported')
|
||||||
|
self.log.error('you may suppress this error message by running the server with --relaxed')
|
||||||
|
raise ProgrammingError('legacy HasIodev no longer supported')
|
||||||
|
self.sendRecv = self.communicate
|
||||||
|
|
||||||
|
|
||||||
class IOBase(Communicator):
|
class IOBase(Communicator):
|
||||||
@ -80,7 +108,7 @@ class IOBase(Communicator):
|
|||||||
uri = Property('hostname:portnumber', datatype=StringType())
|
uri = Property('hostname:portnumber', datatype=StringType())
|
||||||
timeout = Parameter('timeout', datatype=FloatRange(0), default=2)
|
timeout = Parameter('timeout', datatype=FloatRange(0), default=2)
|
||||||
wait_before = Parameter('wait time before sending', datatype=FloatRange(), default=0)
|
wait_before = Parameter('wait time before sending', datatype=FloatRange(), default=0)
|
||||||
is_connected = Parameter('connection state', datatype=BoolType(), readonly=False, poll=REGULAR)
|
is_connected = Parameter('connection state', datatype=BoolType(), readonly=False, default=False)
|
||||||
pollinterval = Parameter('reconnect interval', datatype=FloatRange(0), readonly=False, default=10)
|
pollinterval = Parameter('reconnect interval', datatype=FloatRange(0), readonly=False, default=10)
|
||||||
|
|
||||||
_reconnectCallbacks = None
|
_reconnectCallbacks = None
|
||||||
@ -89,6 +117,8 @@ class IOBase(Communicator):
|
|||||||
_lock = None
|
_lock = None
|
||||||
|
|
||||||
def earlyInit(self):
|
def earlyInit(self):
|
||||||
|
super().earlyInit()
|
||||||
|
self._reconnectCallbacks = {}
|
||||||
self._lock = threading.RLock()
|
self._lock = threading.RLock()
|
||||||
|
|
||||||
def connectStart(self):
|
def connectStart(self):
|
||||||
@ -103,6 +133,9 @@ class IOBase(Communicator):
|
|||||||
self._conn = None
|
self._conn = None
|
||||||
self.is_connected = False
|
self.is_connected = False
|
||||||
|
|
||||||
|
def doPoll(self):
|
||||||
|
self.read_is_connected()
|
||||||
|
|
||||||
def read_is_connected(self):
|
def read_is_connected(self):
|
||||||
"""try to reconnect, when not connected
|
"""try to reconnect, when not connected
|
||||||
|
|
||||||
@ -139,9 +172,6 @@ class IOBase(Communicator):
|
|||||||
|
|
||||||
if the callback fails or returns False, it is cleared
|
if the callback fails or returns False, it is cleared
|
||||||
"""
|
"""
|
||||||
if self._reconnectCallbacks is None:
|
|
||||||
self._reconnectCallbacks = {name: func}
|
|
||||||
else:
|
|
||||||
self._reconnectCallbacks[name] = func
|
self._reconnectCallbacks[name] = func
|
||||||
|
|
||||||
def callCallbacks(self):
|
def callCallbacks(self):
|
||||||
@ -154,6 +184,9 @@ class IOBase(Communicator):
|
|||||||
if removeme:
|
if removeme:
|
||||||
self._reconnectCallbacks.pop(key)
|
self._reconnectCallbacks.pop(key)
|
||||||
|
|
||||||
|
def communicate(self, command):
|
||||||
|
return NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class StringIO(IOBase):
|
class StringIO(IOBase):
|
||||||
"""line oriented communicator
|
"""line oriented communicator
|
||||||
@ -218,6 +251,7 @@ class StringIO(IOBase):
|
|||||||
if not self.is_connected:
|
if not self.is_connected:
|
||||||
self.read_is_connected() # try to reconnect
|
self.read_is_connected() # try to reconnect
|
||||||
if not self._conn:
|
if not self._conn:
|
||||||
|
self.log.debug('can not connect to %r' % self.uri)
|
||||||
raise CommunicationSilentError('can not connect to %r' % self.uri)
|
raise CommunicationSilentError('can not connect to %r' % self.uri)
|
||||||
try:
|
try:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
@ -234,15 +268,15 @@ class StringIO(IOBase):
|
|||||||
if garbage is None: # read garbage only once
|
if garbage is None: # read garbage only once
|
||||||
garbage = self._conn.flush_recv()
|
garbage = self._conn.flush_recv()
|
||||||
if garbage:
|
if garbage:
|
||||||
self.log.debug('garbage: %r', garbage)
|
self.comLog('garbage: %r', garbage)
|
||||||
self._conn.send(cmd + self._eol_write)
|
self._conn.send(cmd + self._eol_write)
|
||||||
self.log.debug('send: %s', cmd + self._eol_write)
|
self.comLog('> %s', cmd.decode(self.encoding))
|
||||||
reply = self._conn.readline(self.timeout)
|
reply = self._conn.readline(self.timeout)
|
||||||
except ConnectionClosed as e:
|
except ConnectionClosed as e:
|
||||||
self.closeConnection()
|
self.closeConnection()
|
||||||
raise CommunicationFailedError('disconnected') from None
|
raise CommunicationFailedError('disconnected') from None
|
||||||
reply = reply.decode(self.encoding)
|
reply = reply.decode(self.encoding)
|
||||||
self.log.debug('recv: %s', reply)
|
self.comLog('< %s', reply)
|
||||||
return reply
|
return reply
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if str(e) == self._last_error:
|
if str(e) == self._last_error:
|
||||||
@ -291,6 +325,10 @@ def make_bytes(string):
|
|||||||
return bytes([int(c, 16) if HEX_CODE.match(c) else ord(c) for c in string.split()])
|
return bytes([int(c, 16) if HEX_CODE.match(c) else ord(c) for c in string.split()])
|
||||||
|
|
||||||
|
|
||||||
|
def hexify(bytes_):
|
||||||
|
return ' '.join('%02x' % r for r in bytes_)
|
||||||
|
|
||||||
|
|
||||||
class BytesIO(IOBase):
|
class BytesIO(IOBase):
|
||||||
identification = Property(
|
identification = Property(
|
||||||
"""identification
|
"""identification
|
||||||
@ -330,14 +368,14 @@ class BytesIO(IOBase):
|
|||||||
time.sleep(self.wait_before)
|
time.sleep(self.wait_before)
|
||||||
garbage = self._conn.flush_recv()
|
garbage = self._conn.flush_recv()
|
||||||
if garbage:
|
if garbage:
|
||||||
self.log.debug('garbage: %r', garbage)
|
self.comLog('garbage: %r', garbage)
|
||||||
self._conn.send(request)
|
self._conn.send(request)
|
||||||
self.log.debug('send: %r', request)
|
self.comLog('> %s', hexify(request))
|
||||||
reply = self._conn.readbytes(replylen, self.timeout)
|
reply = self._conn.readbytes(replylen, self.timeout)
|
||||||
except ConnectionClosed as e:
|
except ConnectionClosed as e:
|
||||||
self.closeConnection()
|
self.closeConnection()
|
||||||
raise CommunicationFailedError('disconnected') from None
|
raise CommunicationFailedError('disconnected') from None
|
||||||
self.log.debug('recv: %r', reply)
|
self.comLog('< %s', hexify(reply))
|
||||||
return self.getFullReply(request, reply)
|
return self.getFullReply(request, reply)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if str(e) == self._last_error:
|
if str(e) == self._last_error:
|
||||||
@ -346,6 +384,15 @@ class BytesIO(IOBase):
|
|||||||
self.log.error(self._last_error)
|
self.log.error(self._last_error)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
@Command((ArrayOf(TupleOf(BLOBType(), IntRange(0)))), result=ArrayOf(BLOBType()))
|
||||||
|
def multicomm(self, requests):
|
||||||
|
"""communicate multiple request/replies in one row"""
|
||||||
|
replies = []
|
||||||
|
with self._lock:
|
||||||
|
for request in requests:
|
||||||
|
replies.append(self.communicate(*request))
|
||||||
|
return replies
|
||||||
|
|
||||||
def readBytes(self, nbytes):
|
def readBytes(self, nbytes):
|
||||||
"""read bytes
|
"""read bytes
|
||||||
|
|
||||||
@ -362,7 +409,7 @@ class BytesIO(IOBase):
|
|||||||
:return: the full reply (replyheader + additional bytes)
|
:return: the full reply (replyheader + additional bytes)
|
||||||
|
|
||||||
When the reply length is variable, :meth:`communicate` should be called
|
When the reply length is variable, :meth:`communicate` should be called
|
||||||
with the `replylen` argument set to minimum expected length of the reply.
|
with the `replylen` argument set to the minimum expected length of the reply.
|
||||||
Typically this method determines then the length of additional bytes from
|
Typically this method determines then the length of additional bytes from
|
||||||
the already received bytes (replyheader) and/or the request and calls
|
the already received bytes (replyheader) and/or the request and calls
|
||||||
:meth:`readBytes` to get the remaining bytes.
|
:meth:`readBytes` to get the remaining bytes.
|
||||||
|
@ -126,7 +126,7 @@ class CmdParser:
|
|||||||
try:
|
try:
|
||||||
argformat % ((0,) * len(casts)) # validate argformat
|
argformat % ((0,) * len(casts)) # validate argformat
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise ValueError("%s in %r" % (e, argformat))
|
raise ValueError("%s in %r" % (e, argformat)) from None
|
||||||
|
|
||||||
def format(self, *values):
|
def format(self, *values):
|
||||||
return self.fmt % values
|
return self.fmt % values
|
||||||
@ -242,7 +242,7 @@ class IOHandler(IOHandlerBase):
|
|||||||
contain the command separator at the end.
|
contain the command separator at the end.
|
||||||
"""
|
"""
|
||||||
querycmd = self.make_query(module)
|
querycmd = self.make_query(module)
|
||||||
reply = module.sendRecv(changecmd + querycmd)
|
reply = module.communicate(changecmd + querycmd)
|
||||||
return self.parse_reply(reply)
|
return self.parse_reply(reply)
|
||||||
|
|
||||||
def send_change(self, module, *values):
|
def send_change(self, module, *values):
|
||||||
@ -253,7 +253,7 @@ class IOHandler(IOHandlerBase):
|
|||||||
"""
|
"""
|
||||||
changecmd = self.make_change(module, *values)
|
changecmd = self.make_change(module, *values)
|
||||||
if self.CMDSEPARATOR is None:
|
if self.CMDSEPARATOR is None:
|
||||||
module.sendRecv(changecmd) # ignore result
|
module.communicate(changecmd) # ignore result
|
||||||
return self.send_command(module)
|
return self.send_command(module)
|
||||||
return self.send_command(module, changecmd + self.CMDSEPARATOR)
|
return self.send_command(module, changecmd + self.CMDSEPARATOR)
|
||||||
|
|
||||||
|
@ -27,40 +27,126 @@ import socket
|
|||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
|
from configparser import ConfigParser
|
||||||
from os import environ, path
|
from os import environ, path
|
||||||
|
|
||||||
|
|
||||||
|
class GeneralConfig:
|
||||||
|
"""generalConfig holds server configuration items
|
||||||
|
|
||||||
|
generalConfig.init is to be called before starting the server.
|
||||||
|
Accessing generalConfig.<key> raises an error, when generalConfig.init is
|
||||||
|
not yet called, except when a default for <key> is set.
|
||||||
|
For tests and for imports from client code, a module may access generalConfig
|
||||||
|
without calling generalConfig.init before. For this, it should call
|
||||||
|
generalConfig.set_default on import to define defaults for the needed keys.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._config = None
|
||||||
|
self.defaults = {} #: default values. may be set before or after :meth:`init`
|
||||||
|
|
||||||
|
def init(self, configfile=None):
|
||||||
|
"""init default server configuration
|
||||||
|
|
||||||
|
:param configfile: if present, keys and values from the [FRAPPY] section are read
|
||||||
|
|
||||||
|
if configfile is not given, it tries to guess the location of the configfile
|
||||||
|
or determine 'piddir', 'logdir', 'confdir' and 'basedir' from the environment.
|
||||||
|
"""
|
||||||
|
cfg = {}
|
||||||
|
mandatory = 'piddir', 'logdir', 'confdir'
|
||||||
repodir = path.abspath(path.join(path.dirname(__file__), '..', '..'))
|
repodir = path.abspath(path.join(path.dirname(__file__), '..', '..'))
|
||||||
|
# create default paths
|
||||||
if path.splitext(sys.executable)[1] == ".exe" and not path.basename(sys.executable).startswith('python'):
|
if path.splitext(sys.executable)[1] == ".exe" and not path.basename(sys.executable).startswith('python'):
|
||||||
CONFIG = {
|
# special MS windows environment
|
||||||
'piddir': './',
|
cfg.update(piddir='./', logdir='./log', confdir='./')
|
||||||
'logdir': './log',
|
elif path.exists(path.join(repodir, '.git')):
|
||||||
'confdir': './',
|
# running from git repo
|
||||||
}
|
cfg['confdir'] = path.join(repodir, 'cfg')
|
||||||
elif not path.exists(path.join(repodir, '.git')):
|
# take logdir and piddir from <repodir>/cfg/generalConfig.cfg
|
||||||
CONFIG = {
|
|
||||||
'piddir': '/var/run/secop',
|
|
||||||
'logdir': '/var/log',
|
|
||||||
'confdir': '/etc/secop',
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
CONFIG = {
|
# running on installed system (typically with systemd)
|
||||||
'piddir': path.join(repodir, 'pid'),
|
cfg.update(piddir='/var/run/frappy', logdir='/var/log', confdir='/etc/frappy')
|
||||||
'logdir': path.join(repodir, 'log'),
|
if configfile is None:
|
||||||
'confdir': path.join(repodir, 'cfg'),
|
configfile = environ.get('FRAPPY_CONFIG_FILE',
|
||||||
}
|
path.join(cfg['confdir'], 'generalConfig.cfg'))
|
||||||
# overwrite with env variables SECOP_LOGDIR, SECOP_PIDDIR, SECOP_CONFDIR, if present
|
if configfile and path.exists(configfile):
|
||||||
for dirname in CONFIG:
|
parser = ConfigParser()
|
||||||
CONFIG[dirname] = environ.get('SECOP_%s' % dirname.upper(), CONFIG[dirname])
|
parser.optionxform = str
|
||||||
|
parser.read([configfile])
|
||||||
|
# mandatory in a general config file:
|
||||||
|
cfg['logdir'] = cfg['piddir'] = None
|
||||||
|
cfg['confdir'] = path.dirname(configfile)
|
||||||
|
# only the FRAPPY section is relevant, other sections might be used by others
|
||||||
|
for key, value in parser['FRAPPY'].items():
|
||||||
|
if value.startswith('./'):
|
||||||
|
cfg[key] = path.abspath(path.join(repodir, value))
|
||||||
|
else:
|
||||||
|
# expand ~ to username, also in path lists separated with ':'
|
||||||
|
cfg[key] = ':'.join(path.expanduser(v) for v in value.split(':'))
|
||||||
|
else:
|
||||||
|
for key in mandatory:
|
||||||
|
cfg[key] = environ.get('FRAPPY_%s' % key.upper(), cfg[key])
|
||||||
|
missing_keys = [key for key in mandatory if cfg[key] is None]
|
||||||
|
if missing_keys:
|
||||||
|
if path.exists(configfile):
|
||||||
|
raise KeyError('missing value for %s in %s' % (' and '.join(missing_keys), configfile))
|
||||||
|
raise FileNotFoundError(configfile)
|
||||||
# this is not customizable
|
# this is not customizable
|
||||||
CONFIG['basedir'] = repodir
|
cfg['basedir'] = repodir
|
||||||
|
self._config = cfg
|
||||||
|
|
||||||
# TODO: if ever more general options are need, we should think about a general config file
|
def __getitem__(self, key):
|
||||||
|
"""access for keys known to exist
|
||||||
|
|
||||||
|
:param key: the key (raises an error when key is not available)
|
||||||
|
:return: the value
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return self._config[key]
|
||||||
|
except KeyError:
|
||||||
|
return self.defaults[key]
|
||||||
|
except TypeError:
|
||||||
|
if key in self.defaults:
|
||||||
|
# accept retrieving defaults before init
|
||||||
|
# e.g. 'lazy_number_validation' in secop.datatypes
|
||||||
|
return self.defaults[key]
|
||||||
|
raise TypeError('generalConfig.init() has to be called first') from None
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
"""access for keys not known to exist"""
|
||||||
|
try:
|
||||||
|
return self.__getitem__(key)
|
||||||
|
except KeyError:
|
||||||
|
return default
|
||||||
|
|
||||||
|
def getint(self, key, default=None):
|
||||||
|
"""access and convert to int"""
|
||||||
|
try:
|
||||||
|
return int(self.__getitem__(key))
|
||||||
|
except KeyError:
|
||||||
|
return default
|
||||||
|
|
||||||
|
def __getattr__(self, key):
|
||||||
|
"""goodie: use generalConfig.<key> instead of generalConfig.get('<key>')"""
|
||||||
|
return self.get(key)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def initialized(self):
|
||||||
|
return bool(self._config)
|
||||||
|
|
||||||
|
def set_default(self, key, value):
|
||||||
|
"""set a default value, in case not set already"""
|
||||||
|
if key not in self.defaults:
|
||||||
|
self.defaults[key] = value
|
||||||
|
|
||||||
|
def testinit(self, **kwds):
|
||||||
|
"""for test purposes"""
|
||||||
|
self._config = kwds
|
||||||
|
|
||||||
|
|
||||||
unset_value = object()
|
generalConfig = GeneralConfig()
|
||||||
|
|
||||||
|
|
||||||
class lazy_property:
|
class lazy_property:
|
||||||
@ -253,10 +339,6 @@ def getfqdn(name=''):
|
|||||||
return socket.getfqdn(name)
|
return socket.getfqdn(name)
|
||||||
|
|
||||||
|
|
||||||
def getGeneralConfig():
|
|
||||||
return CONFIG
|
|
||||||
|
|
||||||
|
|
||||||
def formatStatusBits(sword, labels, start=0):
|
def formatStatusBits(sword, labels, start=0):
|
||||||
"""Return a list of labels according to bit state in `sword` starting
|
"""Return a list of labels according to bit state in `sword` starting
|
||||||
with bit `start` and the first label in `labels`.
|
with bit `start` and the first label in `labels`.
|
||||||
@ -266,3 +348,11 @@ def formatStatusBits(sword, labels, start=0):
|
|||||||
if sword & (1 << i) and lbl:
|
if sword & (1 << i) and lbl:
|
||||||
result.append(lbl)
|
result.append(lbl)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class UniqueObject:
|
||||||
|
def __init__(self, name):
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.name
|
||||||
|
@ -48,6 +48,7 @@ class ConnectionClosed(ConnectionError):
|
|||||||
|
|
||||||
class AsynConn:
|
class AsynConn:
|
||||||
timeout = 1 # inter byte timeout
|
timeout = 1 # inter byte timeout
|
||||||
|
scheme = None
|
||||||
SCHEME_MAP = {}
|
SCHEME_MAP = {}
|
||||||
connection = None # is not None, if connected
|
connection = None # is not None, if connected
|
||||||
defaultport = None
|
defaultport = None
|
||||||
@ -62,11 +63,11 @@ class AsynConn:
|
|||||||
except (ValueError, TypeError, AssertionError):
|
except (ValueError, TypeError, AssertionError):
|
||||||
if 'COM' in uri:
|
if 'COM' in uri:
|
||||||
raise ValueError("the correct uri for a COM port is: "
|
raise ValueError("the correct uri for a COM port is: "
|
||||||
"'serial://COM<i>[?<option>=<value>[+<option>=value ...]]'")
|
"'serial://COM<i>[?<option>=<value>[+<option>=value ...]]'") from None
|
||||||
if '/dev' in uri:
|
if '/dev' in uri:
|
||||||
raise ValueError("the correct uri for a serial port is: "
|
raise ValueError("the correct uri for a serial port is: "
|
||||||
"'serial:///dev/<tty>[?<option>=<value>[+<option>=value ...]]'")
|
"'serial:///dev/<tty>[?<option>=<value>[+<option>=value ...]]'") from None
|
||||||
raise ValueError('invalid uri: %s' % uri)
|
raise ValueError('invalid uri: %s' % uri) from None
|
||||||
iocls = cls.SCHEME_MAP['tcp']
|
iocls = cls.SCHEME_MAP['tcp']
|
||||||
uri = 'tcp://%s:%d' % host_port
|
uri = 'tcp://%s:%d' % host_port
|
||||||
return object.__new__(iocls)
|
return object.__new__(iocls)
|
||||||
@ -80,6 +81,8 @@ class AsynConn:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __init_subclass__(cls):
|
def __init_subclass__(cls):
|
||||||
|
"""register subclass to scheme, if available"""
|
||||||
|
if cls.scheme:
|
||||||
cls.SCHEME_MAP[cls.scheme] = cls
|
cls.SCHEME_MAP[cls.scheme] = cls
|
||||||
|
|
||||||
def disconnect(self):
|
def disconnect(self):
|
||||||
@ -166,7 +169,7 @@ class AsynTcp(AsynConn):
|
|||||||
self.connection = tcpSocket(uri, self.defaultport, self.timeout)
|
self.connection = tcpSocket(uri, self.defaultport, self.timeout)
|
||||||
except (ConnectionRefusedError, socket.gaierror) as e:
|
except (ConnectionRefusedError, socket.gaierror) as e:
|
||||||
# indicate that retrying might make sense
|
# indicate that retrying might make sense
|
||||||
raise CommunicationFailedError(str(e))
|
raise CommunicationFailedError(str(e)) from None
|
||||||
|
|
||||||
def disconnect(self):
|
def disconnect(self):
|
||||||
if self.connection:
|
if self.connection:
|
||||||
@ -237,8 +240,8 @@ class AsynSerial(AsynConn):
|
|||||||
options = dict((kv.split('=') for kv in uri[1].split('+')))
|
options = dict((kv.split('=') for kv in uri[1].split('+')))
|
||||||
except IndexError: # no uri[1], no options
|
except IndexError: # no uri[1], no options
|
||||||
options = {}
|
options = {}
|
||||||
except ValueError:
|
except ValueError as e:
|
||||||
raise ConfigError('illegal serial options')
|
raise ConfigError('illegal serial options') from e
|
||||||
parity = options.pop('parity', None) # only parity is to be treated as text
|
parity = options.pop('parity', None) # only parity is to be treated as text
|
||||||
for k, v in options.items():
|
for k, v in options.items():
|
||||||
try:
|
try:
|
||||||
@ -251,14 +254,12 @@ class AsynSerial(AsynConn):
|
|||||||
if not fullname.startswith(name):
|
if not fullname.startswith(name):
|
||||||
raise ConfigError('illegal parity: %s' % parity)
|
raise ConfigError('illegal parity: %s' % parity)
|
||||||
options['parity'] = name[0]
|
options['parity'] = name[0]
|
||||||
if 'timeout' in options:
|
if 'timeout' not in options:
|
||||||
options['timeout'] = float(self.timeout)
|
|
||||||
else:
|
|
||||||
options['timeout'] = self.timeout
|
options['timeout'] = self.timeout
|
||||||
try:
|
try:
|
||||||
self.connection = Serial(dev, **options)
|
self.connection = Serial(dev, **options)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise ConfigError(e)
|
raise ConfigError(e) from None
|
||||||
# TODO: turn exceptions into ConnectionFailedError, where a retry makes sense
|
# TODO: turn exceptions into ConnectionFailedError, where a retry makes sense
|
||||||
|
|
||||||
def disconnect(self):
|
def disconnect(self):
|
||||||
|
@ -74,29 +74,29 @@ SIMPLETYPES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def short_doc(datatype):
|
def short_doc(datatype, internal=False):
|
||||||
# pylint: disable=possibly-unused-variable
|
# pylint: disable=possibly-unused-variable
|
||||||
|
|
||||||
def doc_EnumType(dt):
|
def doc_EnumType(dt):
|
||||||
return 'one of %s' % str(tuple(dt._enum.keys()))
|
return 'one of %s' % str(tuple(dt._enum.keys()))
|
||||||
|
|
||||||
def doc_ArrayOf(dt):
|
def doc_ArrayOf(dt):
|
||||||
return 'array of %s' % short_doc(dt.members)
|
return 'array of %s' % short_doc(dt.members, True)
|
||||||
|
|
||||||
def doc_TupleOf(dt):
|
def doc_TupleOf(dt):
|
||||||
return 'tuple of (%s)' % ', '.join(short_doc(m) for m in dt.members)
|
return 'tuple of (%s)' % ', '.join(short_doc(m, True) for m in dt.members)
|
||||||
|
|
||||||
def doc_CommandType(dt):
|
def doc_CommandType(dt):
|
||||||
argument = short_doc(dt.argument) if dt.argument else ''
|
argument = short_doc(dt.argument, True) if dt.argument else ''
|
||||||
result = ' -> %s' % short_doc(dt.result) if dt.result else ''
|
result = ' -> %s' % short_doc(dt.result, True) if dt.result else ''
|
||||||
return '(%s)%s' % (argument, result) # return argument list only
|
return '(%s)%s' % (argument, result) # return argument list only
|
||||||
|
|
||||||
def doc_NoneOr(dt):
|
def doc_NoneOr(dt):
|
||||||
other = short_doc(dt.other)
|
other = short_doc(dt.other, True)
|
||||||
return '%s or None' % other if other else None
|
return '%s or None' % other if other else None
|
||||||
|
|
||||||
def doc_OrType(dt):
|
def doc_OrType(dt):
|
||||||
types = [short_doc(t) for t in dt.types]
|
types = [short_doc(t, True) for t in dt.types]
|
||||||
if None in types: # type is anyway broad: no doc
|
if None in types: # type is anyway broad: no doc
|
||||||
return None
|
return None
|
||||||
return ' or '.join(types)
|
return ' or '.join(types)
|
||||||
@ -104,14 +104,17 @@ def short_doc(datatype):
|
|||||||
def doc_Stub(dt):
|
def doc_Stub(dt):
|
||||||
return dt.name.replace('Type', '').replace('Range', '').lower()
|
return dt.name.replace('Type', '').replace('Range', '').lower()
|
||||||
|
|
||||||
clsname = datatype.__class__.__name__
|
def doc_BLOBType(dt):
|
||||||
|
return 'byte array'
|
||||||
|
|
||||||
|
clsname = type(datatype).__name__
|
||||||
result = SIMPLETYPES.get(clsname)
|
result = SIMPLETYPES.get(clsname)
|
||||||
if result:
|
if result:
|
||||||
return result
|
return result
|
||||||
fun = locals().get('doc_' + clsname)
|
fun = locals().get('doc_' + clsname)
|
||||||
if fun:
|
if fun:
|
||||||
return fun(datatype)
|
return fun(datatype)
|
||||||
return None # broad type like ValueType: no doc
|
return clsname if internal else None # broad types like ValueType: no doc
|
||||||
|
|
||||||
|
|
||||||
def append_to_doc(cls, lines, itemcls, name, attrname, fmtfunc):
|
def append_to_doc(cls, lines, itemcls, name, attrname, fmtfunc):
|
||||||
|
@ -21,41 +21,51 @@
|
|||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
class MultiEvent(threading.Event):
|
ETERNITY = 1e99
|
||||||
"""Class implementing multi event objects.
|
|
||||||
|
|
||||||
meth:`new` creates Event like objects
|
|
||||||
meth:'wait` waits for all of them being set
|
|
||||||
"""
|
|
||||||
|
|
||||||
class SingleEvent:
|
class _SingleEvent:
|
||||||
"""Single Event
|
"""Single Event
|
||||||
|
|
||||||
remark: :meth:`wait` is not implemented on purpose
|
remark: :meth:`wait` is not implemented on purpose
|
||||||
"""
|
"""
|
||||||
def __init__(self, multievent):
|
def __init__(self, multievent, timeout, name=None):
|
||||||
self.multievent = multievent
|
self.multievent = multievent
|
||||||
self.multievent._clear(self)
|
self.multievent.clear_(self)
|
||||||
|
self.name = name
|
||||||
|
if timeout is None:
|
||||||
|
self.deadline = ETERNITY
|
||||||
|
else:
|
||||||
|
self.deadline = time.monotonic() + timeout
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
self.multievent._clear(self)
|
self.multievent.clear_(self)
|
||||||
|
|
||||||
def set(self):
|
def set(self):
|
||||||
self.multievent._set(self)
|
self.multievent.set_(self)
|
||||||
|
|
||||||
def is_set(self):
|
def is_set(self):
|
||||||
return self in self.multievent.events
|
return self in self.multievent.events
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
|
class MultiEvent(threading.Event):
|
||||||
|
"""Class implementing multi event objects."""
|
||||||
|
|
||||||
|
def __init__(self, default_timeout=None):
|
||||||
self.events = set()
|
self.events = set()
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
|
self.default_timeout = default_timeout or None # treat 0 as None
|
||||||
|
self.name = None # default event name
|
||||||
|
self._actions = [] # actions to be executed on trigger
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def new(self):
|
def new(self, timeout=None, name=None):
|
||||||
"""create a new SingleEvent"""
|
"""create a single event like object"""
|
||||||
return self.SingleEvent(self)
|
return _SingleEvent(self, timeout or self.default_timeout,
|
||||||
|
name or self.name or '<unnamed>')
|
||||||
|
|
||||||
def set(self):
|
def set(self):
|
||||||
raise ValueError('a multievent must not be set directly')
|
raise ValueError('a multievent must not be set directly')
|
||||||
@ -63,21 +73,69 @@ class MultiEvent(threading.Event):
|
|||||||
def clear(self):
|
def clear(self):
|
||||||
raise ValueError('a multievent must not be cleared directly')
|
raise ValueError('a multievent must not be cleared directly')
|
||||||
|
|
||||||
def _set(self, event):
|
def is_set(self):
|
||||||
|
return not self.events
|
||||||
|
|
||||||
|
def set_(self, event):
|
||||||
"""internal: remove event from the event list"""
|
"""internal: remove event from the event list"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self.events.discard(event)
|
self.events.discard(event)
|
||||||
if self.events:
|
if self.events:
|
||||||
return
|
return
|
||||||
|
try:
|
||||||
|
for action in self._actions:
|
||||||
|
action()
|
||||||
|
except Exception:
|
||||||
|
pass # we silently ignore errors here
|
||||||
|
self._actions = []
|
||||||
super().set()
|
super().set()
|
||||||
|
|
||||||
def _clear(self, event):
|
def clear_(self, event):
|
||||||
"""internal: add event to the event list"""
|
"""internal: add event to the event list"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self.events.add(event)
|
self.events.add(event)
|
||||||
super().clear()
|
super().clear()
|
||||||
|
|
||||||
|
def deadline(self):
|
||||||
|
deadline = 0
|
||||||
|
for event in self.events:
|
||||||
|
deadline = max(event.deadline, deadline)
|
||||||
|
return None if deadline == ETERNITY else deadline
|
||||||
|
|
||||||
def wait(self, timeout=None):
|
def wait(self, timeout=None):
|
||||||
|
"""wait for all events being set or timed out"""
|
||||||
if not self.events: # do not wait if events are empty
|
if not self.events: # do not wait if events are empty
|
||||||
return
|
return True
|
||||||
super().wait(timeout)
|
deadline = self.deadline()
|
||||||
|
if deadline is not None:
|
||||||
|
deadline -= time.monotonic()
|
||||||
|
timeout = deadline if timeout is None else min(deadline, timeout)
|
||||||
|
if timeout <= 0:
|
||||||
|
return False
|
||||||
|
return super().wait(timeout)
|
||||||
|
|
||||||
|
def waiting_for(self):
|
||||||
|
return set(event.name for event in self.events)
|
||||||
|
|
||||||
|
def get_trigger(self, timeout=None, name=None):
|
||||||
|
"""create a new single event and return its set method
|
||||||
|
|
||||||
|
as a convenience method
|
||||||
|
"""
|
||||||
|
return self.new(timeout, name).set
|
||||||
|
|
||||||
|
def queue(self, action):
|
||||||
|
"""add an action to the queue of actions to be executed at end
|
||||||
|
|
||||||
|
:param action: a function, to be executed after the last event is triggered,
|
||||||
|
and before the multievent is set
|
||||||
|
|
||||||
|
- if no events are waiting, the actions are executed immediately
|
||||||
|
- if an action raises an exception, it is silently ignore and further
|
||||||
|
actions in the queue are skipped
|
||||||
|
- if this is not desired, the action should handle errors by itself
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
self._actions.append(action)
|
||||||
|
if self.is_set():
|
||||||
|
self.set_(None)
|
||||||
|
58
secop/lib/py35compat.py
Normal file
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:
|
if self._seq_fault_on_stop:
|
||||||
return self.Status.ERROR, self._seq_stopped
|
return self.Status.ERROR, self._seq_stopped
|
||||||
return self.Status.WARN, self._seq_stopped
|
return self.Status.WARN, self._seq_stopped
|
||||||
if hasattr(self, 'read_hw_status'):
|
if hasattr(self, 'readHwStatus'):
|
||||||
return self.read_hw_status()
|
return self.readHwStatus()
|
||||||
return self.Status.IDLE, ''
|
return self.Status.IDLE, ''
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
@ -153,7 +153,7 @@ class SequencerMixin:
|
|||||||
self._seq_error = str(e)
|
self._seq_error = str(e)
|
||||||
finally:
|
finally:
|
||||||
self._seq_thread = None
|
self._seq_thread = None
|
||||||
self.pollParams(0)
|
self.doPoll()
|
||||||
|
|
||||||
def _seq_thread_inner(self, seq, store_init):
|
def _seq_thread_inner(self, seq, store_init):
|
||||||
store = Namespace()
|
store = Namespace()
|
||||||
|
320
secop/lib/statemachine.py
Normal file
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()
|
519
secop/modules.py
519
secop/modules.py
@ -23,21 +23,27 @@
|
|||||||
"""Define base classes for real Modules implemented in the server"""
|
"""Define base classes for real Modules implemented in the server"""
|
||||||
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import time
|
import time
|
||||||
|
import threading
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
from secop.datatypes import ArrayOf, BoolType, EnumType, FloatRange, \
|
from secop.datatypes import ArrayOf, BoolType, EnumType, FloatRange, \
|
||||||
IntRange, StatusType, StringType, TextType, TupleOf
|
IntRange, StatusType, StringType, TextType, TupleOf, DiscouragedConversion
|
||||||
from secop.errors import BadValueError, ConfigError, InternalError, \
|
from secop.errors import BadValueError, ConfigError, \
|
||||||
ProgrammingError, SECoPError, SilentError, secop_error
|
ProgrammingError, SECoPError, SilentError, secop_error
|
||||||
from secop.lib import formatException, mkthread
|
from secop.lib import formatException, mkthread, UniqueObject, generalConfig
|
||||||
from secop.lib.enum import Enum
|
from secop.lib.enum import Enum
|
||||||
from secop.params import Accessible, Command, Parameter
|
from secop.params import Accessible, Command, Parameter
|
||||||
from secop.poller import BasicPoller, Poller
|
|
||||||
from secop.properties import HasProperties, Property
|
from secop.properties import HasProperties, Property
|
||||||
|
from secop.logging import RemoteLogHandler, HasComlog
|
||||||
|
|
||||||
Done = object() #: a special return value for a read/write function indicating that the setter is triggered already
|
generalConfig.set_default('disable_value_range_check', False) # check for problematic value range by default
|
||||||
|
|
||||||
|
Done = UniqueObject('Done')
|
||||||
|
"""a special return value for a read/write function
|
||||||
|
|
||||||
|
indicating that the setter is triggered already"""
|
||||||
|
|
||||||
|
|
||||||
class HasAccessibles(HasProperties):
|
class HasAccessibles(HasProperties):
|
||||||
@ -57,6 +63,7 @@ class HasAccessibles(HasProperties):
|
|||||||
merged_properties = {} # dict of dict of merged properties
|
merged_properties = {} # dict of dict of merged properties
|
||||||
new_names = [] # list of names of new accessibles
|
new_names = [] # list of names of new accessibles
|
||||||
override_values = {} # bare values overriding a parameter and methods overriding a command
|
override_values = {} # bare values overriding a parameter and methods overriding a command
|
||||||
|
|
||||||
for base in reversed(cls.__mro__):
|
for base in reversed(cls.__mro__):
|
||||||
for key, value in base.__dict__.items():
|
for key, value in base.__dict__.items():
|
||||||
if isinstance(value, Accessible):
|
if isinstance(value, Accessible):
|
||||||
@ -66,27 +73,32 @@ class HasAccessibles(HasProperties):
|
|||||||
accessibles[key] = value
|
accessibles[key] = value
|
||||||
override_values.pop(key, None)
|
override_values.pop(key, None)
|
||||||
elif key in accessibles:
|
elif key in accessibles:
|
||||||
# either a bare value overriding a parameter
|
|
||||||
# or a method overriding a command
|
|
||||||
override_values[key] = value
|
override_values[key] = value
|
||||||
for aname, aobj in accessibles.items():
|
for aname, aobj in list(accessibles.items()):
|
||||||
if aname in override_values:
|
if aname in override_values:
|
||||||
aobj = aobj.copy()
|
aobj = aobj.copy()
|
||||||
|
value = override_values[aname]
|
||||||
|
if value is None:
|
||||||
|
accessibles.pop(aname)
|
||||||
|
continue
|
||||||
aobj.merge(merged_properties[aname])
|
aobj.merge(merged_properties[aname])
|
||||||
aobj.override(override_values[aname])
|
aobj.override(value)
|
||||||
# replace the bare value by the created accessible
|
# replace the bare value by the created accessible
|
||||||
setattr(cls, aname, aobj)
|
setattr(cls, aname, aobj)
|
||||||
else:
|
else:
|
||||||
aobj.merge(merged_properties[aname])
|
aobj.merge(merged_properties[aname])
|
||||||
accessibles[aname] = aobj
|
accessibles[aname] = aobj
|
||||||
|
|
||||||
# rebuild order: (1) inherited items, (2) items from paramOrder, (3) new accessibles
|
# rebuild order: (1) inherited items, (2) items from paramOrder, (3) new accessibles
|
||||||
# move (2) to the end
|
# move (2) to the end
|
||||||
for aname in list(cls.__dict__.get('paramOrder', ())):
|
paramOrder = cls.__dict__.get('paramOrder', ())
|
||||||
|
for aname in paramOrder:
|
||||||
if aname in accessibles:
|
if aname in accessibles:
|
||||||
accessibles.move_to_end(aname)
|
accessibles.move_to_end(aname)
|
||||||
# ignore unknown names
|
# ignore unknown names
|
||||||
# move (3) to the end
|
# move (3) to the end
|
||||||
for aname in new_names:
|
for aname in new_names:
|
||||||
|
if aname not in paramOrder:
|
||||||
accessibles.move_to_end(aname)
|
accessibles.move_to_end(aname)
|
||||||
# note: for python < 3.6 the order of inherited items is not ensured between
|
# note: for python < 3.6 the order of inherited items is not ensured between
|
||||||
# declarations within the same class
|
# declarations within the same class
|
||||||
@ -100,12 +112,14 @@ class HasAccessibles(HasProperties):
|
|||||||
# XXX: create getters for the units of params ??
|
# XXX: create getters for the units of params ??
|
||||||
|
|
||||||
# wrap of reading/writing funcs
|
# wrap of reading/writing funcs
|
||||||
if isinstance(pobj, Command):
|
if not isinstance(pobj, Parameter):
|
||||||
# nothing to do for now
|
# nothing to do for Commands
|
||||||
continue
|
continue
|
||||||
|
|
||||||
rfunc = getattr(cls, 'read_' + pname, None)
|
rfunc = getattr(cls, 'read_' + pname, None)
|
||||||
|
# TODO: remove handler stuff here
|
||||||
rfunc_handler = pobj.handler.get_read_func(cls, pname) if pobj.handler else None
|
rfunc_handler = pobj.handler.get_read_func(cls, pname) if pobj.handler else None
|
||||||
wrapped = hasattr(rfunc, '__wrapped__')
|
wrapped = getattr(rfunc, 'wrapped', False) # meaning: wrapped or auto generated
|
||||||
if rfunc_handler:
|
if rfunc_handler:
|
||||||
if 'read_' + pname in cls.__dict__:
|
if 'read_' + pname in cls.__dict__:
|
||||||
if pname in cls.__dict__:
|
if pname in cls.__dict__:
|
||||||
@ -119,65 +133,86 @@ class HasAccessibles(HasProperties):
|
|||||||
# create wrapper except when read function is already wrapped
|
# create wrapper except when read function is already wrapped
|
||||||
if not wrapped:
|
if not wrapped:
|
||||||
|
|
||||||
def wrapped_rfunc(self, pname=pname, rfunc=rfunc):
|
|
||||||
if rfunc:
|
if rfunc:
|
||||||
self.log.debug("calling %r" % rfunc)
|
|
||||||
|
@wraps(rfunc) # handles __wrapped__ and __doc__
|
||||||
|
def new_rfunc(self, pname=pname, rfunc=rfunc):
|
||||||
|
with self.accessLock:
|
||||||
try:
|
try:
|
||||||
value = rfunc(self)
|
value = rfunc(self)
|
||||||
self.log.debug("rfunc(%s) returned %r" % (pname, value))
|
self.log.debug("read_%s returned %r", pname, value)
|
||||||
if value is Done: # the setter is already triggered
|
|
||||||
return getattr(self, pname)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.debug("rfunc(%s) failed %r" % (pname, e))
|
self.log.debug("read_%s failed with %r", pname, e)
|
||||||
self.announceUpdate(pname, None, e)
|
self.announceUpdate(pname, None, e)
|
||||||
raise
|
raise
|
||||||
else:
|
if value is Done:
|
||||||
# return cached value
|
return getattr(self, pname)
|
||||||
self.log.debug("rfunc(%s): return cached value" % pname)
|
|
||||||
value = self.accessibles[pname].value
|
|
||||||
setattr(self, pname, value) # important! trigger the setter
|
setattr(self, pname, value) # important! trigger the setter
|
||||||
return value
|
return value
|
||||||
|
|
||||||
if rfunc:
|
new_rfunc.poll = getattr(rfunc, 'poll', True)
|
||||||
wrapped_rfunc.__doc__ = rfunc.__doc__
|
else:
|
||||||
setattr(cls, 'read_' + pname, wrapped_rfunc)
|
|
||||||
wrapped_rfunc.__wrapped__ = True
|
def new_rfunc(self, pname=pname):
|
||||||
|
return getattr(self, pname)
|
||||||
|
|
||||||
|
new_rfunc.poll = False
|
||||||
|
new_rfunc.__doc__ = 'auto generated read method for ' + pname
|
||||||
|
|
||||||
|
new_rfunc.wrapped = True # indicate to subclasses that no more wrapping is needed
|
||||||
|
setattr(cls, 'read_' + pname, new_rfunc)
|
||||||
|
|
||||||
if not pobj.readonly:
|
|
||||||
wfunc = getattr(cls, 'write_' + pname, None)
|
wfunc = getattr(cls, 'write_' + pname, None)
|
||||||
wrapped = hasattr(wfunc, '__wrapped__')
|
if not pobj.readonly or wfunc: # allow write_ method even when pobj is not readonly
|
||||||
|
wrapped = getattr(wfunc, 'wrapped', False) # meaning: wrapped or auto generated
|
||||||
if (wfunc is None or wrapped) and pobj.handler:
|
if (wfunc is None or wrapped) and pobj.handler:
|
||||||
# ignore the handler, if a write function is present
|
# ignore the handler, if a write function is present
|
||||||
|
# TODO: remove handler stuff here
|
||||||
wfunc = pobj.handler.get_write_func(pname)
|
wfunc = pobj.handler.get_write_func(pname)
|
||||||
wrapped = False
|
wrapped = False
|
||||||
|
|
||||||
# create wrapper except when write function is already wrapped
|
# create wrapper except when write function is already wrapped
|
||||||
if not wrapped:
|
if not wrapped:
|
||||||
|
|
||||||
def wrapped_wfunc(self, value, pname=pname, wfunc=wfunc):
|
|
||||||
self.log.debug("check validity of %s = %r" % (pname, value))
|
|
||||||
pobj = self.accessibles[pname]
|
|
||||||
value = pobj.datatype(value)
|
|
||||||
if wfunc:
|
if wfunc:
|
||||||
self.log.debug('calling %s %r(%r)' % (wfunc.__name__, wfunc, value))
|
|
||||||
returned_value = wfunc(self, value)
|
@wraps(wfunc) # handles __wrapped__ and __doc__
|
||||||
if returned_value is Done: # the setter is already triggered
|
def new_wfunc(self, value, pname=pname, wfunc=wfunc):
|
||||||
|
with self.accessLock:
|
||||||
|
pobj = self.accessibles[pname]
|
||||||
|
self.log.debug('validate %r for %r', value, pname)
|
||||||
|
# we do not need to handle errors here, we do not
|
||||||
|
# want to make a parameter invalid, when a write failed
|
||||||
|
new_value = pobj.datatype(value)
|
||||||
|
new_value = wfunc(self, new_value)
|
||||||
|
self.log.debug('write_%s(%r) returned %r', pname, value, new_value)
|
||||||
|
if new_value is Done:
|
||||||
|
# setattr(self, pname, getattr(self, pname))
|
||||||
return getattr(self, pname)
|
return getattr(self, pname)
|
||||||
if returned_value is not None: # goodie: accept missing return value
|
setattr(self, pname, new_value) # important! trigger the setter
|
||||||
value = returned_value
|
return new_value
|
||||||
|
else:
|
||||||
|
|
||||||
|
def new_wfunc(self, value, pname=pname):
|
||||||
setattr(self, pname, value)
|
setattr(self, pname, value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
if wfunc:
|
new_wfunc.__doc__ = 'auto generated write method for ' + pname
|
||||||
wrapped_wfunc.__doc__ = wfunc.__doc__
|
|
||||||
setattr(cls, 'write_' + pname, wrapped_wfunc)
|
|
||||||
wrapped_wfunc.__wrapped__ = True
|
|
||||||
|
|
||||||
# check information about Command's
|
new_wfunc.wrapped = True # indicate to subclasses that no more wrapping is needed
|
||||||
for attrname in cls.__dict__:
|
setattr(cls, 'write_' + pname, new_wfunc)
|
||||||
if attrname.startswith('do_'):
|
|
||||||
|
# check for programming errors
|
||||||
|
for attrname in dir(cls):
|
||||||
|
prefix, _, pname = attrname.partition('_')
|
||||||
|
if not pname:
|
||||||
|
continue
|
||||||
|
if prefix == 'do':
|
||||||
raise ProgrammingError('%r: old style command %r not supported anymore'
|
raise ProgrammingError('%r: old style command %r not supported anymore'
|
||||||
% (cls.__name__, attrname))
|
% (cls.__name__, attrname))
|
||||||
|
if prefix in ('read', 'write') and not getattr(getattr(cls, attrname), 'wrapped', False):
|
||||||
|
raise ProgrammingError('%s.%s defined, but %r is no parameter'
|
||||||
|
% (cls.__name__, attrname, pname))
|
||||||
|
|
||||||
res = {}
|
res = {}
|
||||||
# collect info about properties
|
# collect info about properties
|
||||||
@ -193,6 +228,26 @@ class HasAccessibles(HasProperties):
|
|||||||
cls.configurables = res
|
cls.configurables = res
|
||||||
|
|
||||||
|
|
||||||
|
class PollInfo:
|
||||||
|
def __init__(self, pollinterval, trigger_event):
|
||||||
|
self.interval = pollinterval
|
||||||
|
self.last_main = 0
|
||||||
|
self.last_slow = 0
|
||||||
|
self.last_error = None
|
||||||
|
self.polled_parameters = []
|
||||||
|
self.fast_flag = False
|
||||||
|
self.trigger_event = trigger_event
|
||||||
|
|
||||||
|
def trigger(self):
|
||||||
|
"""trigger a recalculation of poll due times"""
|
||||||
|
self.trigger_event.set()
|
||||||
|
|
||||||
|
def update_interval(self, pollinterval):
|
||||||
|
if not self.fast_flag:
|
||||||
|
self.interval = pollinterval
|
||||||
|
self.trigger()
|
||||||
|
|
||||||
|
|
||||||
class Module(HasAccessibles):
|
class Module(HasAccessibles):
|
||||||
"""basic module
|
"""basic module
|
||||||
|
|
||||||
@ -237,6 +292,9 @@ class Module(HasAccessibles):
|
|||||||
extname='implementation')
|
extname='implementation')
|
||||||
interface_classes = Property('offical highest interface-class of the module', ArrayOf(StringType()),
|
interface_classes = Property('offical highest interface-class of the module', ArrayOf(StringType()),
|
||||||
extname='interface_classes')
|
extname='interface_classes')
|
||||||
|
pollinterval = Property('poll interval for parameters handled by doPoll', FloatRange(0.1, 120), default=5)
|
||||||
|
slowinterval = Property('poll interval for other parameters', FloatRange(0.1, 120), default=15)
|
||||||
|
enablePoll = True
|
||||||
|
|
||||||
# properties, parameters and commands are auto-merged upon subclassing
|
# properties, parameters and commands are auto-merged upon subclassing
|
||||||
parameters = {}
|
parameters = {}
|
||||||
@ -244,16 +302,24 @@ class Module(HasAccessibles):
|
|||||||
|
|
||||||
# reference to the dispatcher (used for sending async updates)
|
# reference to the dispatcher (used for sending async updates)
|
||||||
DISPATCHER = None
|
DISPATCHER = None
|
||||||
|
attachedModules = None
|
||||||
pollerClass = Poller #: default poller used
|
pollInfo = None
|
||||||
|
triggerPoll = None # trigger event for polls. used on io modules and modules without io
|
||||||
|
|
||||||
def __init__(self, name, logger, cfgdict, srv):
|
def __init__(self, name, logger, cfgdict, srv):
|
||||||
# remember the dispatcher object (for the async callbacks)
|
# remember the dispatcher object (for the async callbacks)
|
||||||
self.DISPATCHER = srv.dispatcher
|
self.DISPATCHER = srv.dispatcher
|
||||||
|
self.omit_unchanged_within = getattr(self.DISPATCHER, 'omit_unchanged_within', 0.1)
|
||||||
self.log = logger
|
self.log = logger
|
||||||
self.name = name
|
self.name = name
|
||||||
self.valueCallbacks = {}
|
self.valueCallbacks = {}
|
||||||
self.errorCallbacks = {}
|
self.errorCallbacks = {}
|
||||||
|
self.earlyInitDone = False
|
||||||
|
self.initModuleDone = False
|
||||||
|
self.startModuleDone = False
|
||||||
|
self.remoteLogHandler = None
|
||||||
|
self.accessLock = threading.RLock()
|
||||||
|
self.polledModules = [] # modules polled by thread started in self.startModules
|
||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
# handle module properties
|
# handle module properties
|
||||||
@ -298,13 +364,6 @@ class Module(HasAccessibles):
|
|||||||
for aname, aobj in self.accessibles.items():
|
for aname, aobj in self.accessibles.items():
|
||||||
# make a copy of the Parameter/Command object
|
# make a copy of the Parameter/Command object
|
||||||
aobj = aobj.copy()
|
aobj = aobj.copy()
|
||||||
if isinstance(aobj, Parameter):
|
|
||||||
# fix default properties poll and needscfg
|
|
||||||
if aobj.poll is None:
|
|
||||||
aobj.poll = bool(aobj.handler)
|
|
||||||
if aobj.needscfg is None:
|
|
||||||
aobj.needscfg = not aobj.poll
|
|
||||||
|
|
||||||
if not self.export: # do not export parameters of a module not exported
|
if not self.export: # do not export parameters of a module not exported
|
||||||
aobj.export = False
|
aobj.export = False
|
||||||
if aobj.export:
|
if aobj.export:
|
||||||
@ -319,26 +378,23 @@ class Module(HasAccessibles):
|
|||||||
|
|
||||||
# 2) check and apply parameter_properties
|
# 2) check and apply parameter_properties
|
||||||
# specified as '<paramname>.<propertyname> = <propertyvalue>'
|
# specified as '<paramname>.<propertyname> = <propertyvalue>'
|
||||||
|
# this may also be done on commands: e.g. 'stop.visibility = advanced'
|
||||||
for k, v in list(cfgdict.items()): # keep list() as dict may change during iter
|
for k, v in list(cfgdict.items()): # keep list() as dict may change during iter
|
||||||
if '.' in k[1:]:
|
if '.' in k[1:]:
|
||||||
paramname, propname = k.split('.', 1)
|
aname, propname = k.split('.', 1)
|
||||||
propvalue = cfgdict.pop(k)
|
propvalue = cfgdict.pop(k)
|
||||||
paramobj = self.accessibles.get(paramname, None)
|
aobj = self.accessibles.get(aname, None)
|
||||||
# paramobj might also be a command (not sure if this is needed)
|
if aobj:
|
||||||
if paramobj:
|
|
||||||
# no longer needed, this conversion is done by DataTypeType.__call__:
|
|
||||||
# if propname == 'datatype':
|
|
||||||
# propvalue = get_datatype(propvalue, k)
|
|
||||||
try:
|
try:
|
||||||
paramobj.setProperty(propname, propvalue)
|
aobj.setProperty(propname, propvalue)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
errors.append("'%s.%s' does not exist" %
|
errors.append("'%s.%s' does not exist" %
|
||||||
(paramname, propname))
|
(aname, propname))
|
||||||
except BadValueError as e:
|
except BadValueError as e:
|
||||||
errors.append('%s.%s: %s' %
|
errors.append('%s.%s: %s' %
|
||||||
(paramname, propname, str(e)))
|
(aname, propname, str(e)))
|
||||||
else:
|
else:
|
||||||
errors.append('%r not found' % paramname)
|
errors.append('%r not found' % aname)
|
||||||
|
|
||||||
# 3) check config for problems:
|
# 3) check config for problems:
|
||||||
# only accept remaining config items specified in parameters
|
# only accept remaining config items specified in parameters
|
||||||
@ -361,9 +417,8 @@ class Module(HasAccessibles):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if pname in cfgdict:
|
if pname in cfgdict:
|
||||||
if not pobj.readonly and pobj.initwrite is not False:
|
if pobj.initwrite is not False and hasattr(self, 'write_' + pname):
|
||||||
# parameters given in cfgdict have to call write_<pname>
|
# parameters given in cfgdict have to call write_<pname>
|
||||||
# TODO: not sure about readonly (why not a parameter which can only be written from config?)
|
|
||||||
try:
|
try:
|
||||||
pobj.value = pobj.datatype(cfgdict[pname])
|
pobj.value = pobj.datatype(cfgdict[pname])
|
||||||
self.writeDict[pname] = pobj.value
|
self.writeDict[pname] = pobj.value
|
||||||
@ -376,7 +431,7 @@ class Module(HasAccessibles):
|
|||||||
'value and was not given in config!' % pname)
|
'value and was not given in config!' % pname)
|
||||||
# we do not want to call the setter for this parameter for now,
|
# we do not want to call the setter for this parameter for now,
|
||||||
# this should happen on the first read
|
# this should happen on the first read
|
||||||
pobj.readerror = ConfigError('not initialized')
|
pobj.readerror = ConfigError('parameter %r not initialized' % pname)
|
||||||
# above error will be triggered on activate after startup,
|
# above error will be triggered on activate after startup,
|
||||||
# when not all hardware parameters are read because of startup timeout
|
# when not all hardware parameters are read because of startup timeout
|
||||||
pobj.value = pobj.datatype(pobj.datatype.default)
|
pobj.value = pobj.datatype(pobj.datatype.default)
|
||||||
@ -386,10 +441,8 @@ class Module(HasAccessibles):
|
|||||||
except BadValueError as e:
|
except BadValueError as e:
|
||||||
# this should not happen, as the default is already checked in Parameter
|
# this should not happen, as the default is already checked in Parameter
|
||||||
raise ProgrammingError('bad default for %s:%s: %s' % (name, pname, e)) from None
|
raise ProgrammingError('bad default for %s:%s: %s' % (name, pname, e)) from None
|
||||||
if pobj.initwrite and not pobj.readonly:
|
if pobj.initwrite and hasattr(self, 'write_' + pname):
|
||||||
# we will need to call write_<pname>
|
# we will need to call write_<pname>
|
||||||
# if this is not desired, the default must not be given
|
|
||||||
# TODO: not sure about readonly (why not a parameter which can only be written from config?)
|
|
||||||
pobj.value = value
|
pobj.value = value
|
||||||
self.writeDict[pname] = value
|
self.writeDict[pname] = value
|
||||||
else:
|
else:
|
||||||
@ -424,11 +477,11 @@ class Module(HasAccessibles):
|
|||||||
self.checkProperties()
|
self.checkProperties()
|
||||||
except ConfigError as e:
|
except ConfigError as e:
|
||||||
errors.append(str(e))
|
errors.append(str(e))
|
||||||
for pname, p in self.parameters.items():
|
for aname, aobj in self.accessibles.items():
|
||||||
try:
|
try:
|
||||||
p.checkProperties()
|
aobj.checkProperties()
|
||||||
except ConfigError as e:
|
except (ConfigError, ProgrammingError) as e:
|
||||||
errors.append('%s: %s' % (pname, e))
|
errors.append('%s: %s' % (aname, e))
|
||||||
if errors:
|
if errors:
|
||||||
raise ConfigError(errors)
|
raise ConfigError(errors)
|
||||||
|
|
||||||
@ -441,26 +494,31 @@ class Module(HasAccessibles):
|
|||||||
|
|
||||||
def announceUpdate(self, pname, value=None, err=None, timestamp=None):
|
def announceUpdate(self, pname, value=None, err=None, timestamp=None):
|
||||||
"""announce a changed value or readerror"""
|
"""announce a changed value or readerror"""
|
||||||
|
|
||||||
|
with self.accessLock:
|
||||||
|
# TODO: remove readerror 'property' and replace value with exception
|
||||||
pobj = self.parameters[pname]
|
pobj = self.parameters[pname]
|
||||||
timestamp = timestamp or time.time()
|
timestamp = timestamp or time.time()
|
||||||
changed = pobj.value != value
|
changed = pobj.value != value
|
||||||
if value is not None:
|
|
||||||
pobj.value = value # store the value even in case of error
|
|
||||||
if err:
|
|
||||||
if not isinstance(err, SECoPError):
|
|
||||||
err = InternalError(err)
|
|
||||||
if str(err) == str(pobj.readerror):
|
|
||||||
return # do call updates for repeated errors
|
|
||||||
else:
|
|
||||||
try:
|
try:
|
||||||
|
# store the value even in case of error
|
||||||
pobj.value = pobj.datatype(value)
|
pobj.value = pobj.datatype(value)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
err = secop_error(e)
|
if isinstance(e, DiscouragedConversion):
|
||||||
if not changed and timestamp < ((pobj.timestamp or 0)
|
if DiscouragedConversion.log_message:
|
||||||
+ self.DISPATCHER.OMIT_UNCHANGED_WITHIN):
|
self.log.error(str(e))
|
||||||
|
self.log.error('you may disable this behaviour by running the server with --relaxed')
|
||||||
|
DiscouragedConversion.log_message = False
|
||||||
|
if not err: # do not overwrite given error
|
||||||
|
err = e
|
||||||
|
if err:
|
||||||
|
err = secop_error(err)
|
||||||
|
if str(err) == str(pobj.readerror):
|
||||||
|
return # no updates for repeated errors
|
||||||
|
elif not changed and timestamp < (pobj.timestamp or 0) + self.omit_unchanged_within:
|
||||||
# no change within short time -> omit
|
# no change within short time -> omit
|
||||||
return
|
return
|
||||||
pobj.timestamp = timestamp
|
pobj.timestamp = timestamp or time.time()
|
||||||
pobj.readerror = err
|
pobj.readerror = err
|
||||||
if pobj.export:
|
if pobj.export:
|
||||||
self.DISPATCHER.announce_update(self.name, pname, pobj)
|
self.DISPATCHER.announce_update(self.name, pname, pobj)
|
||||||
@ -495,15 +553,15 @@ class Module(HasAccessibles):
|
|||||||
for pname in self.parameters:
|
for pname in self.parameters:
|
||||||
errfunc = getattr(modobj, 'error_update_' + pname, None)
|
errfunc = getattr(modobj, 'error_update_' + pname, None)
|
||||||
if errfunc:
|
if errfunc:
|
||||||
def errcb(err, p=pname, m=modobj, efunc=errfunc):
|
def errcb(err, p=pname, efunc=errfunc):
|
||||||
try:
|
try:
|
||||||
efunc(err)
|
efunc(err)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
m.announceUpdate(p, err=e)
|
modobj.announceUpdate(p, err=e)
|
||||||
self.errorCallbacks[pname].append(errcb)
|
self.errorCallbacks[pname].append(errcb)
|
||||||
else:
|
else:
|
||||||
def errcb(err, p=pname, m=modobj):
|
def errcb(err, p=pname):
|
||||||
m.announceUpdate(p, err=err)
|
modobj.announceUpdate(p, err=err)
|
||||||
if pname in autoupdate:
|
if pname in autoupdate:
|
||||||
self.errorCallbacks[pname].append(errcb)
|
self.errorCallbacks[pname].append(errcb)
|
||||||
|
|
||||||
@ -516,8 +574,8 @@ class Module(HasAccessibles):
|
|||||||
efunc(e)
|
efunc(e)
|
||||||
self.valueCallbacks[pname].append(cb)
|
self.valueCallbacks[pname].append(cb)
|
||||||
elif pname in autoupdate:
|
elif pname in autoupdate:
|
||||||
def cb(value, p=pname, m=modobj):
|
def cb(value, p=pname):
|
||||||
m.announceUpdate(p, value)
|
modobj.announceUpdate(p, value)
|
||||||
self.valueCallbacks[pname].append(cb)
|
self.valueCallbacks[pname].append(cb)
|
||||||
|
|
||||||
def isBusy(self, status=None):
|
def isBusy(self, status=None):
|
||||||
@ -526,16 +584,54 @@ class Module(HasAccessibles):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def earlyInit(self):
|
def earlyInit(self):
|
||||||
# may be overriden in derived classes to init stuff
|
"""initialise module with stuff to be done before all modules are created"""
|
||||||
self.log.debug('empty %s.earlyInit()' % self.__class__.__name__)
|
self.earlyInitDone = True
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
self.log.debug('empty %s.initModule()' % self.__class__.__name__)
|
"""initialise module with stuff to be done after all modules are created"""
|
||||||
|
self.initModuleDone = True
|
||||||
|
if self.enablePoll or self.writeDict:
|
||||||
|
# enablePoll == False: we still need the poll thread for writing values from writeDict
|
||||||
|
if hasattr(self, 'io'):
|
||||||
|
self.io.polledModules.append(self)
|
||||||
|
else:
|
||||||
|
self.triggerPoll = threading.Event()
|
||||||
|
self.polledModules = [self]
|
||||||
|
|
||||||
def pollOneParam(self, pname):
|
def startModule(self, start_events):
|
||||||
"""poll parameter <pname> with proper error handling"""
|
"""runs after init of all modules
|
||||||
|
|
||||||
|
when a thread is started, a trigger function may signal that it
|
||||||
|
has finished its initial work
|
||||||
|
start_events.get_trigger(<timeout>) creates such a trigger and
|
||||||
|
registers it in the server for waiting
|
||||||
|
<timeout> defaults to 30 seconds
|
||||||
|
"""
|
||||||
|
if self.polledModules:
|
||||||
|
mkthread(self.__pollThread, self.polledModules, start_events.get_trigger())
|
||||||
|
self.startModuleDone = True
|
||||||
|
|
||||||
|
def doPoll(self):
|
||||||
|
"""polls important parameters like value and status
|
||||||
|
|
||||||
|
all other parameters are polled automatically
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setFastPoll(self, flag, fast_interval=0.25):
|
||||||
|
"""change poll interval
|
||||||
|
|
||||||
|
:param flag: enable/disable fast poll mode
|
||||||
|
:param fast_interval: fast poll interval
|
||||||
|
"""
|
||||||
|
if self.pollInfo:
|
||||||
|
self.pollInfo.fast_flag = flag
|
||||||
|
self.pollInfo.interval = fast_interval if flag else self.pollinterval
|
||||||
|
self.pollInfo.trigger()
|
||||||
|
|
||||||
|
def callPollFunc(self, rfunc):
|
||||||
|
"""call read method with proper error handling"""
|
||||||
try:
|
try:
|
||||||
getattr(self, 'read_' + pname)()
|
rfunc()
|
||||||
except SilentError:
|
except SilentError:
|
||||||
pass
|
pass
|
||||||
except SECoPError as e:
|
except SECoPError as e:
|
||||||
@ -543,6 +639,93 @@ class Module(HasAccessibles):
|
|||||||
except Exception:
|
except Exception:
|
||||||
self.log.error(formatException())
|
self.log.error(formatException())
|
||||||
|
|
||||||
|
def __pollThread(self, modules, started_callback):
|
||||||
|
"""poll thread body
|
||||||
|
|
||||||
|
:param modules: list of modules to be handled by this thread
|
||||||
|
:param started_callback: to be called after all polls are done once
|
||||||
|
|
||||||
|
before polling, parameters which need hardware initialisation are written
|
||||||
|
"""
|
||||||
|
for mobj in modules:
|
||||||
|
mobj.writeInitParams()
|
||||||
|
modules = [m for m in modules if m.enablePoll]
|
||||||
|
if not modules: # no polls needed - exit thread
|
||||||
|
started_callback()
|
||||||
|
return
|
||||||
|
if hasattr(self, 'registerReconnectCallback'):
|
||||||
|
# self is a communicator supporting reconnections
|
||||||
|
def trigger_all(trg=self.triggerPoll, polled_modules=modules):
|
||||||
|
for m in polled_modules:
|
||||||
|
m.pollInfo.last_main = 0
|
||||||
|
m.pollInfo.last_slow = 0
|
||||||
|
trg.set()
|
||||||
|
self.registerReconnectCallback('trigger_polls', trigger_all)
|
||||||
|
|
||||||
|
# collect and call all read functions a first time
|
||||||
|
for mobj in modules:
|
||||||
|
pinfo = mobj.pollInfo = PollInfo(mobj.pollinterval, self.triggerPoll)
|
||||||
|
# trigger a poll interval change when self.pollinterval changes.
|
||||||
|
if 'pollinterval' in mobj.valueCallbacks:
|
||||||
|
mobj.valueCallbacks['pollinterval'].append(pinfo.update_interval)
|
||||||
|
|
||||||
|
for pname, pobj in mobj.parameters.items():
|
||||||
|
rfunc = getattr(mobj, 'read_' + pname)
|
||||||
|
if rfunc.poll:
|
||||||
|
pinfo.polled_parameters.append((mobj, rfunc, pobj))
|
||||||
|
mobj.callPollFunc(rfunc)
|
||||||
|
started_callback()
|
||||||
|
to_poll = ()
|
||||||
|
while True:
|
||||||
|
now = time.time()
|
||||||
|
wait_time = 999
|
||||||
|
for mobj in modules:
|
||||||
|
pinfo = mobj.pollInfo
|
||||||
|
wait_time = min(pinfo.last_main + pinfo.interval - now, wait_time,
|
||||||
|
pinfo.last_slow + mobj.slowinterval - now)
|
||||||
|
if wait_time > 0:
|
||||||
|
self.triggerPoll.wait(wait_time)
|
||||||
|
self.triggerPoll.clear()
|
||||||
|
continue
|
||||||
|
# call doPoll of all modules where due
|
||||||
|
for mobj in modules:
|
||||||
|
pinfo = mobj.pollInfo
|
||||||
|
if now > pinfo.last_main + pinfo.interval:
|
||||||
|
pinfo.last_main = (now // pinfo.interval) * pinfo.interval
|
||||||
|
try:
|
||||||
|
mobj.doPoll()
|
||||||
|
pinfo.last_error = None
|
||||||
|
except Exception as e:
|
||||||
|
if str(e) != str(pinfo.last_error) and not isinstance(e, SilentError):
|
||||||
|
mobj.log.error('doPoll: %r', e)
|
||||||
|
pinfo.last_error = e
|
||||||
|
now = time.time()
|
||||||
|
# find ONE due slow poll and call it
|
||||||
|
loop = True
|
||||||
|
while loop: # loops max. 2 times, when to_poll is at end
|
||||||
|
for mobj, rfunc, pobj in to_poll:
|
||||||
|
if now > pobj.timestamp + mobj.slowinterval * 0.5:
|
||||||
|
try:
|
||||||
|
prev_err = pobj.readerror
|
||||||
|
rfunc()
|
||||||
|
except Exception as e:
|
||||||
|
if not isinstance(e, SilentError) and str(pobj.readerror) != str(prev_err):
|
||||||
|
mobj.log.error('%s: %r', pobj.name, e)
|
||||||
|
loop = False # one poll done
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
to_poll = []
|
||||||
|
# collect due slow polls
|
||||||
|
for mobj in modules:
|
||||||
|
pinfo = mobj.pollInfo
|
||||||
|
if now > pinfo.last_slow + mobj.slowinterval:
|
||||||
|
to_poll.extend(pinfo.polled_parameters)
|
||||||
|
pinfo.last_slow = (now // mobj.slowinterval) * mobj.slowinterval
|
||||||
|
if to_poll:
|
||||||
|
to_poll = iter(to_poll)
|
||||||
|
else:
|
||||||
|
loop = False # no slow polls ready
|
||||||
|
|
||||||
def writeInitParams(self, started_callback=None):
|
def writeInitParams(self, started_callback=None):
|
||||||
"""write values for parameters with configured values
|
"""write values for parameters with configured values
|
||||||
|
|
||||||
@ -565,14 +748,15 @@ class Module(HasAccessibles):
|
|||||||
if started_callback:
|
if started_callback:
|
||||||
started_callback()
|
started_callback()
|
||||||
|
|
||||||
def startModule(self, started_callback):
|
def setRemoteLogging(self, conn, level):
|
||||||
"""runs after init of all modules
|
if self.remoteLogHandler is None:
|
||||||
|
for handler in self.log.handlers:
|
||||||
started_callback to be called when the thread spawned by startModule
|
if isinstance(handler, RemoteLogHandler):
|
||||||
has finished its initial work
|
self.remoteLogHandler = handler
|
||||||
might return a timeout value, if different from default
|
break
|
||||||
"""
|
else:
|
||||||
mkthread(self.writeInitParams, started_callback)
|
raise ValueError('remote handler not found')
|
||||||
|
self.remoteLogHandler.set_conn_level(self, conn, level)
|
||||||
|
|
||||||
|
|
||||||
class Readable(Module):
|
class Readable(Module):
|
||||||
@ -587,63 +771,43 @@ class Readable(Module):
|
|||||||
UNKNOWN=401,
|
UNKNOWN=401,
|
||||||
) #: status codes
|
) #: status codes
|
||||||
|
|
||||||
value = Parameter('current value of the module', FloatRange(), poll=True)
|
value = Parameter('current value of the module', FloatRange())
|
||||||
status = Parameter('current status of the module', TupleOf(EnumType(Status), StringType()),
|
status = Parameter('current status of the module', TupleOf(EnumType(Status), StringType()),
|
||||||
default=(Status.IDLE, ''), poll=True)
|
default=(Status.IDLE, ''))
|
||||||
pollinterval = Parameter('sleeptime between polls', FloatRange(0.1, 120),
|
pollinterval = Parameter('default poll interval', FloatRange(0.1, 120),
|
||||||
default=5, readonly=False)
|
default=5, readonly=False, export=True)
|
||||||
|
|
||||||
def startModule(self, started_callback):
|
def doPoll(self):
|
||||||
"""start basic polling thread"""
|
self.read_value()
|
||||||
if self.pollerClass and issubclass(self.pollerClass, BasicPoller):
|
self.read_status()
|
||||||
# use basic poller for legacy code
|
|
||||||
mkthread(self.__pollThread, started_callback)
|
|
||||||
else:
|
|
||||||
super().startModule(started_callback)
|
|
||||||
|
|
||||||
def __pollThread(self, started_callback):
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
self.__pollThread_inner(started_callback)
|
|
||||||
except Exception as e:
|
|
||||||
self.log.exception(e)
|
|
||||||
self.status = (self.Status.ERROR, 'polling thread could not start')
|
|
||||||
started_callback()
|
|
||||||
print(formatException(0, sys.exc_info(), verbose=True))
|
|
||||||
time.sleep(10)
|
|
||||||
|
|
||||||
def __pollThread_inner(self, started_callback):
|
|
||||||
"""super simple and super stupid per-module polling thread"""
|
|
||||||
self.writeInitParams()
|
|
||||||
i = 0
|
|
||||||
fastpoll = self.pollParams(i)
|
|
||||||
started_callback()
|
|
||||||
while True:
|
|
||||||
i += 1
|
|
||||||
try:
|
|
||||||
time.sleep(self.pollinterval * (0.1 if fastpoll else 1))
|
|
||||||
except TypeError:
|
|
||||||
time.sleep(min(self.pollinterval)
|
|
||||||
if fastpoll else max(self.pollinterval))
|
|
||||||
fastpoll = self.pollParams(i)
|
|
||||||
|
|
||||||
def pollParams(self, nr=0):
|
|
||||||
# Just poll all parameters regularly where polling is enabled
|
|
||||||
for pname, pobj in self.parameters.items():
|
|
||||||
if not pobj.poll:
|
|
||||||
continue
|
|
||||||
if nr % abs(int(pobj.poll)) == 0:
|
|
||||||
# pollParams every 'pobj.pollParams' iteration
|
|
||||||
self.pollOneParam(pname)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class Writable(Readable):
|
class Writable(Readable):
|
||||||
"""basic writable module"""
|
"""basic writable module"""
|
||||||
|
disable_value_range_check = Property('disable value range check', BoolType(), default=False)
|
||||||
target = Parameter('target value of the module',
|
target = Parameter('target value of the module',
|
||||||
default=0, readonly=False, datatype=FloatRange(unit='$'))
|
default=0, readonly=False, datatype=FloatRange(unit='$'))
|
||||||
|
|
||||||
|
def __init__(self, name, logger, cfgdict, srv):
|
||||||
|
super().__init__(name, logger, cfgdict, srv)
|
||||||
|
value_dt = self.parameters['value'].datatype
|
||||||
|
target_dt = self.parameters['target'].datatype
|
||||||
|
try:
|
||||||
|
# this handles also the cases where the limits on the value are more
|
||||||
|
# restrictive than on the target
|
||||||
|
target_dt.compatible(value_dt)
|
||||||
|
except Exception:
|
||||||
|
if type(value_dt) == type(target_dt):
|
||||||
|
raise ConfigError('the target range extends beyond the value range') from None
|
||||||
|
raise ProgrammingError('the datatypes of target and value are not compatible') from None
|
||||||
|
if isinstance(value_dt, FloatRange):
|
||||||
|
if (not self.disable_value_range_check and not generalConfig.disable_value_range_check
|
||||||
|
and value_dt.problematic_range(target_dt)):
|
||||||
|
self.log.error('the value range must be bigger than the target range!')
|
||||||
|
self.log.error('you may disable this error message by running the server with --relaxed')
|
||||||
|
self.log.error('or by setting the disable_value_range_check property of the module to True')
|
||||||
|
raise ConfigError('the value range must be bigger than the target range')
|
||||||
|
|
||||||
|
|
||||||
class Drivable(Writable):
|
class Drivable(Writable):
|
||||||
"""basic drivable module"""
|
"""basic drivable module"""
|
||||||
@ -666,30 +830,12 @@ class Drivable(Writable):
|
|||||||
"""
|
"""
|
||||||
return 300 <= (status or self.status)[0] < 390
|
return 300 <= (status or self.status)[0] < 390
|
||||||
|
|
||||||
# improved polling: may poll faster if module is BUSY
|
|
||||||
def pollParams(self, nr=0):
|
|
||||||
# poll status first
|
|
||||||
self.read_status()
|
|
||||||
fastpoll = self.isBusy()
|
|
||||||
for pname, pobj in self.parameters.items():
|
|
||||||
if not pobj.poll:
|
|
||||||
continue
|
|
||||||
if pname == 'status':
|
|
||||||
# status was already polled above
|
|
||||||
continue
|
|
||||||
if ((int(pobj.poll) < 0) and fastpoll) or (
|
|
||||||
nr % abs(int(pobj.poll))) == 0:
|
|
||||||
# poll always if pobj.poll is negative and fastpoll (i.e. Module is busy)
|
|
||||||
# otherwise poll every 'pobj.poll' iteration
|
|
||||||
self.pollOneParam(pname)
|
|
||||||
return fastpoll
|
|
||||||
|
|
||||||
@Command(None, result=None)
|
@Command(None, result=None)
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""cease driving, go to IDLE state"""
|
"""cease driving, go to IDLE state"""
|
||||||
|
|
||||||
|
|
||||||
class Communicator(Module):
|
class Communicator(HasComlog, Module):
|
||||||
"""basic abstract communication module"""
|
"""basic abstract communication module"""
|
||||||
|
|
||||||
@Command(StringType(), result=StringType())
|
@Command(StringType(), result=StringType())
|
||||||
@ -703,19 +849,20 @@ class Communicator(Module):
|
|||||||
|
|
||||||
|
|
||||||
class Attached(Property):
|
class Attached(Property):
|
||||||
"""a special property, defining an attached modle
|
"""a special property, defining an attached module
|
||||||
|
|
||||||
assign a module name to this property in the cfg file,
|
assign a module name to this property in the cfg file,
|
||||||
and the server will create an attribute with this module
|
and the server will create an attribute with this module
|
||||||
|
|
||||||
:param attrname: the name of the to be created attribute. if not given
|
|
||||||
the attribute name is the property name prepended by an underscore.
|
|
||||||
"""
|
"""
|
||||||
# we can not put this to properties.py, as it needs datatypes
|
def __init__(self, basecls=Module, description='attached module', mandatory=True):
|
||||||
def __init__(self, attrname=None):
|
self.basecls = basecls
|
||||||
self.attrname = attrname
|
super().__init__(description, StringType(), mandatory=mandatory)
|
||||||
# we can not make it mandatory, as the check in Module.__init__ will be before auto-assign in HasIodev
|
|
||||||
super().__init__('attached module', StringType(), mandatory=False)
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __get__(self, obj, owner):
|
||||||
return 'Attached(%s)' % (repr(self.attrname) if self.attrname else '')
|
if obj is None:
|
||||||
|
return self
|
||||||
|
if obj.attachedModules is None:
|
||||||
|
# return the name of the module (called from Server on startup)
|
||||||
|
return super().__get__(obj, owner)
|
||||||
|
# return the module (called after startup)
|
||||||
|
return obj.attachedModules.get(self.name) # return None if not given
|
||||||
|
@ -26,12 +26,13 @@
|
|||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
from secop.datatypes import BoolType, CommandType, DataType, \
|
from secop.datatypes import BoolType, CommandType, DataType, \
|
||||||
DataTypeType, EnumType, IntRange, NoneOr, OrType, \
|
DataTypeType, EnumType, NoneOr, OrType, \
|
||||||
StringType, StructOf, TextType, TupleOf, ValueType
|
StringType, StructOf, TextType, TupleOf, ValueType
|
||||||
from secop.errors import BadValueError, ProgrammingError
|
from secop.errors import BadValueError, ProgrammingError
|
||||||
from secop.properties import HasProperties, Property
|
from secop.properties import HasProperties, Property
|
||||||
|
from secop.lib import generalConfig
|
||||||
|
|
||||||
UNSET = object() # an argument not given, not even None
|
generalConfig.set_default('tolerate_poll_property', False)
|
||||||
|
|
||||||
|
|
||||||
class Accessible(HasProperties):
|
class Accessible(HasProperties):
|
||||||
@ -134,24 +135,9 @@ class Parameter(Accessible):
|
|||||||
* True: exported, name automatic.
|
* True: exported, name automatic.
|
||||||
* a string: exported with custom name''', OrType(BoolType(), StringType()),
|
* a string: exported with custom name''', OrType(BoolType(), StringType()),
|
||||||
export=False, default=True)
|
export=False, default=True)
|
||||||
poll = Property(
|
|
||||||
'''[internal] polling indicator
|
|
||||||
|
|
||||||
may be:
|
|
||||||
|
|
||||||
* None (omitted): will be converted to True/False if handler is/is not None
|
|
||||||
* False or 0 (never poll this parameter)
|
|
||||||
* True or 1 (AUTO), converted to SLOW (readonly=False)
|
|
||||||
DYNAMIC (*status* and *value*) or REGULAR (else)
|
|
||||||
* 2 (SLOW), polled with lower priority and a multiple of pollinterval
|
|
||||||
* 3 (REGULAR), polled with pollperiod
|
|
||||||
* 4 (DYNAMIC), if BUSY, with a fraction of pollinterval,
|
|
||||||
else polled with pollperiod
|
|
||||||
''', NoneOr(IntRange()),
|
|
||||||
export=False, default=None)
|
|
||||||
needscfg = Property(
|
needscfg = Property(
|
||||||
'[internal] needs value in config', NoneOr(BoolType()),
|
'[internal] needs value in config', NoneOr(BoolType()),
|
||||||
export=False, default=None)
|
export=False, default=False)
|
||||||
optional = Property(
|
optional = Property(
|
||||||
'[internal] is this parameter optional?', BoolType(),
|
'[internal] is this parameter optional?', BoolType(),
|
||||||
export=False, settable=False, default=False)
|
export=False, settable=False, default=False)
|
||||||
@ -171,6 +157,8 @@ class Parameter(Accessible):
|
|||||||
|
|
||||||
def __init__(self, description=None, datatype=None, inherit=True, **kwds):
|
def __init__(self, description=None, datatype=None, inherit=True, **kwds):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
if 'poll' in kwds and generalConfig.tolerate_poll_property:
|
||||||
|
kwds.pop('poll')
|
||||||
if datatype is None:
|
if datatype is None:
|
||||||
# collect datatype properties. these are not applied, as we have no datatype
|
# collect datatype properties. these are not applied, as we have no datatype
|
||||||
self.ownProperties = {k: kwds.pop(k) for k in list(kwds) if k not in self.propertyDict}
|
self.ownProperties = {k: kwds.pop(k) for k in list(kwds) if k not in self.propertyDict}
|
||||||
@ -198,7 +186,6 @@ class Parameter(Accessible):
|
|||||||
self.ownProperties = {k: getattr(self, k) for k in self.propertyDict}
|
self.ownProperties = {k: getattr(self, k) for k in self.propertyDict}
|
||||||
|
|
||||||
def __get__(self, instance, owner):
|
def __get__(self, instance, owner):
|
||||||
# not used yet
|
|
||||||
if instance is None:
|
if instance is None:
|
||||||
return self
|
return self
|
||||||
return instance.parameters[self.name].value
|
return instance.parameters[self.name].value
|
||||||
@ -219,6 +206,9 @@ class Parameter(Accessible):
|
|||||||
self.export = '_' + self.name
|
self.export = '_' + self.name
|
||||||
else:
|
else:
|
||||||
raise ProgrammingError('can not use %r as name of a Parameter' % self.name)
|
raise ProgrammingError('can not use %r as name of a Parameter' % self.name)
|
||||||
|
if 'export' in self.ownProperties:
|
||||||
|
# avoid export=True overrides export=<name>
|
||||||
|
self.ownProperties['export'] = self.export
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
"""return a (deep) copy of ourselfs"""
|
"""return a (deep) copy of ourselfs"""
|
||||||
@ -346,6 +336,9 @@ class Command(Accessible):
|
|||||||
|
|
||||||
def __init__(self, argument=False, *, result=None, inherit=True, **kwds):
|
def __init__(self, argument=False, *, result=None, inherit=True, **kwds):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
if 'datatype' in kwds:
|
||||||
|
# self.init will complain about invalid keywords except 'datatype', as this is a property
|
||||||
|
raise ProgrammingError("Command() got an invalid keyword 'datatype'")
|
||||||
self.init(kwds)
|
self.init(kwds)
|
||||||
if result or kwds or isinstance(argument, DataType) or not callable(argument):
|
if result or kwds or isinstance(argument, DataType) or not callable(argument):
|
||||||
# normal case
|
# normal case
|
||||||
@ -362,8 +355,9 @@ class Command(Accessible):
|
|||||||
self.func = argument # this is the wrapped method!
|
self.func = argument # this is the wrapped method!
|
||||||
if argument.__doc__:
|
if argument.__doc__:
|
||||||
self.description = inspect.cleandoc(argument.__doc__)
|
self.description = inspect.cleandoc(argument.__doc__)
|
||||||
self.name = self.func.__name__
|
self.name = self.func.__name__ # this is probably not needed
|
||||||
self._inherit = inherit # save for __set_name__
|
self._inherit = inherit # save for __set_name__
|
||||||
|
self.ownProperties = self.propertyValues.copy()
|
||||||
|
|
||||||
def __set_name__(self, owner, name):
|
def __set_name__(self, owner, name):
|
||||||
self.name = name
|
self.name = name
|
||||||
@ -372,7 +366,6 @@ class Command(Accessible):
|
|||||||
(owner.__name__, name))
|
(owner.__name__, name))
|
||||||
|
|
||||||
self.datatype = CommandType(self.argument, self.result)
|
self.datatype = CommandType(self.argument, self.result)
|
||||||
self.ownProperties = self.propertyValues.copy()
|
|
||||||
if self.export is True:
|
if self.export is True:
|
||||||
predefined_cls = PREDEFINED_ACCESSIBLES.get(name, None)
|
predefined_cls = PREDEFINED_ACCESSIBLES.get(name, None)
|
||||||
if predefined_cls is Command:
|
if predefined_cls is Command:
|
||||||
@ -381,6 +374,9 @@ class Command(Accessible):
|
|||||||
self.export = '_' + name
|
self.export = '_' + name
|
||||||
else:
|
else:
|
||||||
raise ProgrammingError('can not use %r as name of a Command' % name) from None
|
raise ProgrammingError('can not use %r as name of a Command' % name) from None
|
||||||
|
if 'export' in self.ownProperties:
|
||||||
|
# avoid export=True overrides export=<name>
|
||||||
|
self.ownProperties['export'] = self.export
|
||||||
if not self._inherit:
|
if not self._inherit:
|
||||||
for key, pobj in self.properties.items():
|
for key, pobj in self.properties.items():
|
||||||
if key not in self.propertyValues:
|
if key not in self.propertyValues:
|
||||||
@ -397,6 +393,7 @@ class Command(Accessible):
|
|||||||
"""called when used as decorator"""
|
"""called when used as decorator"""
|
||||||
if 'description' not in self.propertyValues and func.__doc__:
|
if 'description' not in self.propertyValues and func.__doc__:
|
||||||
self.description = inspect.cleandoc(func.__doc__)
|
self.description = inspect.cleandoc(func.__doc__)
|
||||||
|
self.ownProperties['description'] = self.description
|
||||||
self.func = func
|
self.func = func
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@ -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 os
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from secop.lib import getGeneralConfig
|
from secop.lib import generalConfig
|
||||||
from secop.datatypes import EnumType
|
from secop.datatypes import EnumType
|
||||||
from secop.params import Parameter, Property, BoolType, Command
|
from secop.params import Parameter, Property, Command
|
||||||
from secop.modules import HasAccessibles
|
from secop.modules import HasAccessibles
|
||||||
|
|
||||||
|
|
||||||
@ -69,13 +69,13 @@ class PersistentParam(Parameter):
|
|||||||
class PersistentMixin(HasAccessibles):
|
class PersistentMixin(HasAccessibles):
|
||||||
def __init__(self, *args, **kwds):
|
def __init__(self, *args, **kwds):
|
||||||
super().__init__(*args, **kwds)
|
super().__init__(*args, **kwds)
|
||||||
persistentdir = os.path.join(getGeneralConfig()['logdir'], 'persistent')
|
persistentdir = os.path.join(generalConfig.logdir, 'persistent')
|
||||||
os.makedirs(persistentdir, exist_ok=True)
|
os.makedirs(persistentdir, exist_ok=True)
|
||||||
self.persistentFile = os.path.join(persistentdir, '%s.%s.json' % (self.DISPATCHER.equipment_id, self.name))
|
self.persistentFile = os.path.join(persistentdir, '%s.%s.json' % (self.DISPATCHER.equipment_id, self.name))
|
||||||
self.initData = {}
|
self.initData = {}
|
||||||
for pname in self.parameters:
|
for pname in self.parameters:
|
||||||
pobj = self.parameters[pname]
|
pobj = self.parameters[pname]
|
||||||
if not pobj.readonly and getattr(pobj, 'persistent', 0):
|
if hasattr(self, 'write_' + pname) and getattr(pobj, 'persistent', 0):
|
||||||
self.initData[pname] = pobj.value
|
self.initData[pname] = pobj.value
|
||||||
if pobj.persistent == 'auto':
|
if pobj.persistent == 'auto':
|
||||||
def cb(value, m=self):
|
def cb(value, m=self):
|
||||||
@ -103,6 +103,7 @@ class PersistentMixin(HasAccessibles):
|
|||||||
try:
|
try:
|
||||||
value = pobj.datatype.import_value(self.persistentData[pname])
|
value = pobj.datatype.import_value(self.persistentData[pname])
|
||||||
pobj.value = value
|
pobj.value = value
|
||||||
|
pobj.readerror = None
|
||||||
if not pobj.readonly:
|
if not pobj.readonly:
|
||||||
writeDict[pname] = value
|
writeDict[pname] = value
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -144,5 +145,6 @@ class PersistentMixin(HasAccessibles):
|
|||||||
|
|
||||||
@Command()
|
@Command()
|
||||||
def factory_reset(self):
|
def factory_reset(self):
|
||||||
|
"""reset to values from config / default values"""
|
||||||
self.writeDict.update(self.initData)
|
self.writeDict.update(self.initData)
|
||||||
self.writeInitParams()
|
self.writeInitParams()
|
||||||
|
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 inspect
|
||||||
import sys
|
|
||||||
|
|
||||||
from secop.errors import BadValueError, ConfigError, ProgrammingError
|
from secop.errors import BadValueError, ConfigError, ProgrammingError
|
||||||
|
from secop.lib import UniqueObject
|
||||||
|
from secop.lib.py35compat import Object
|
||||||
|
|
||||||
|
UNSET = UniqueObject('undefined value') #: an unset value, not even None
|
||||||
|
|
||||||
|
|
||||||
class HasDescriptorMeta(type):
|
class HasDescriptors(Object):
|
||||||
def __new__(cls, name, bases, attrs):
|
|
||||||
newtype = type.__new__(cls, name, bases, attrs)
|
|
||||||
if sys.version_info < (3, 6):
|
|
||||||
# support older python versions
|
|
||||||
for key, attr in attrs.items():
|
|
||||||
if hasattr(attr, '__set_name__'):
|
|
||||||
attr.__set_name__(newtype, key)
|
|
||||||
newtype.__init_subclass__()
|
|
||||||
return newtype
|
|
||||||
|
|
||||||
|
|
||||||
class HasDescriptors(metaclass=HasDescriptorMeta):
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __init_subclass__(cls):
|
def __init_subclass__(cls):
|
||||||
# when migrating old style declarations, sometimes the trailing comma is not removed
|
# when migrating old style declarations, sometimes the trailing comma is not removed
|
||||||
@ -51,9 +42,6 @@ class HasDescriptors(metaclass=HasDescriptorMeta):
|
|||||||
raise ProgrammingError('misplaced trailing comma after %s.%s' % (cls.__name__, '/'.join(bad)))
|
raise ProgrammingError('misplaced trailing comma after %s.%s' % (cls.__name__, '/'.join(bad)))
|
||||||
|
|
||||||
|
|
||||||
UNSET = object() # an unset value, not even None
|
|
||||||
|
|
||||||
|
|
||||||
# storage for 'properties of a property'
|
# storage for 'properties of a property'
|
||||||
class Property:
|
class Property:
|
||||||
"""base class holding info about a property
|
"""base class holding info about a property
|
||||||
@ -142,26 +130,22 @@ class HasProperties(HasDescriptors):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def __init_subclass__(cls):
|
def __init_subclass__(cls):
|
||||||
super().__init_subclass__()
|
super().__init_subclass__()
|
||||||
# raise an error when an attribute is a tuple with one single descriptor as element
|
|
||||||
# when migrating old style declarations, sometimes the trailing comma is not removed
|
|
||||||
bad = [k for k, v in cls.__dict__.items()
|
|
||||||
if isinstance(v, tuple) and len(v) == 1 and hasattr(v[0], '__set_name__')]
|
|
||||||
if bad:
|
|
||||||
raise ProgrammingError('misplaced trailing comma after %s.%s' % (cls.__name__, '/'.join(bad)))
|
|
||||||
properties = {}
|
properties = {}
|
||||||
# using cls.__bases__ and base.propertyDict for this would fail on some multiple inheritance cases
|
# using cls.__bases__ and base.propertyDict for this would fail on some multiple inheritance cases
|
||||||
for base in reversed(cls.__mro__):
|
for base in reversed(cls.__mro__):
|
||||||
properties.update({k: v for k, v in base.__dict__.items() if isinstance(v, Property)})
|
properties.update({k: v for k, v in base.__dict__.items() if isinstance(v, Property)})
|
||||||
cls.propertyDict = properties
|
cls.propertyDict = properties
|
||||||
# treat overriding properties with bare values
|
# treat overriding properties with bare values
|
||||||
for pn, po in properties.items():
|
for pn, po in list(properties.items()):
|
||||||
value = getattr(cls, pn, po)
|
value = getattr(cls, pn, po)
|
||||||
if not isinstance(value, Property): # attribute is a bare value
|
if isinstance(value, HasProperties): # value is a Parameter, allow override
|
||||||
|
properties.pop(pn)
|
||||||
|
elif not isinstance(value, Property): # attribute may be a bare value
|
||||||
po = po.copy()
|
po = po.copy()
|
||||||
try:
|
try:
|
||||||
|
# try to apply bare value to Property
|
||||||
po.value = po.datatype(value)
|
po.value = po.datatype(value)
|
||||||
except BadValueError:
|
except BadValueError:
|
||||||
if pn in properties:
|
|
||||||
if callable(value):
|
if callable(value):
|
||||||
raise ProgrammingError('method %s.%s collides with property of %s' %
|
raise ProgrammingError('method %s.%s collides with property of %s' %
|
||||||
(cls.__name__, pn, base.__name__)) from None
|
(cls.__name__, pn, base.__name__)) from None
|
||||||
|
@ -19,4 +19,4 @@
|
|||||||
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
||||||
#
|
#
|
||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
"""SECoP protocl specific stuff"""
|
"""SECoP protocol specific stuff"""
|
||||||
|
@ -47,7 +47,8 @@ from secop.errors import NoSuchCommandError, NoSuchModuleError, \
|
|||||||
from secop.params import Parameter
|
from secop.params import Parameter
|
||||||
from secop.protocol.messages import COMMANDREPLY, DESCRIPTIONREPLY, \
|
from secop.protocol.messages import COMMANDREPLY, DESCRIPTIONREPLY, \
|
||||||
DISABLEEVENTSREPLY, ENABLEEVENTSREPLY, ERRORPREFIX, EVENTREPLY, \
|
DISABLEEVENTSREPLY, ENABLEEVENTSREPLY, ERRORPREFIX, EVENTREPLY, \
|
||||||
HEARTBEATREPLY, IDENTREPLY, IDENTREQUEST, READREPLY, WRITEREPLY
|
HEARTBEATREPLY, IDENTREPLY, IDENTREQUEST, READREPLY, WRITEREPLY, \
|
||||||
|
LOGGING_REPLY, LOG_EVENT
|
||||||
|
|
||||||
|
|
||||||
def make_update(modulename, pobj):
|
def make_update(modulename, pobj):
|
||||||
@ -60,12 +61,11 @@ def make_update(modulename, pobj):
|
|||||||
|
|
||||||
|
|
||||||
class Dispatcher:
|
class Dispatcher:
|
||||||
|
|
||||||
OMIT_UNCHANGED_WITHIN = 1 # do not send unchanged updates within 1 sec
|
|
||||||
|
|
||||||
def __init__(self, name, logger, options, srv):
|
def __init__(self, name, logger, options, srv):
|
||||||
# to avoid errors, we want to eat all options here
|
# to avoid errors, we want to eat all options here
|
||||||
self.equipment_id = options.pop('id', name)
|
self.equipment_id = options.pop('id', name)
|
||||||
|
# time interval for omitting updates of unchanged values
|
||||||
|
self.omit_unchanged_within = options.pop('omit_unchanged_within', 0.1)
|
||||||
self.nodeprops = {}
|
self.nodeprops = {}
|
||||||
for k in list(options):
|
for k in list(options):
|
||||||
self.nodeprops[k] = options.pop(k)
|
self.nodeprops[k] = options.pop(k)
|
||||||
@ -83,6 +83,7 @@ class Dispatcher:
|
|||||||
# eventname is <modulename> or <modulename>:<parametername>
|
# eventname is <modulename> or <modulename>:<parametername>
|
||||||
self._subscriptions = {}
|
self._subscriptions = {}
|
||||||
self._lock = threading.RLock()
|
self._lock = threading.RLock()
|
||||||
|
self.name = name
|
||||||
self.restart = srv.restart
|
self.restart = srv.restart
|
||||||
self.shutdown = srv.shutdown
|
self.shutdown = srv.shutdown
|
||||||
|
|
||||||
@ -124,13 +125,21 @@ class Dispatcher:
|
|||||||
"""registers new connection"""
|
"""registers new connection"""
|
||||||
self._connections.append(conn)
|
self._connections.append(conn)
|
||||||
|
|
||||||
|
def reset_connection(self, conn):
|
||||||
|
"""remove all subscriptions for a connection
|
||||||
|
|
||||||
|
to be called on the identification message
|
||||||
|
"""
|
||||||
|
for _evt, conns in list(self._subscriptions.items()):
|
||||||
|
conns.discard(conn)
|
||||||
|
self.set_all_log_levels(conn, 'off')
|
||||||
|
self._active_connections.discard(conn)
|
||||||
|
|
||||||
def remove_connection(self, conn):
|
def remove_connection(self, conn):
|
||||||
"""removes now longer functional connection"""
|
"""removes now longer functional connection"""
|
||||||
if conn in self._connections:
|
if conn in self._connections:
|
||||||
self._connections.remove(conn)
|
self._connections.remove(conn)
|
||||||
for _evt, conns in list(self._subscriptions.items()):
|
self.reset_connection(conn)
|
||||||
conns.discard(conn)
|
|
||||||
self._active_connections.discard(conn)
|
|
||||||
|
|
||||||
def register_module(self, moduleobj, modulename, export=True):
|
def register_module(self, moduleobj, modulename, export=True):
|
||||||
self.log.debug('registering module %r as %s (export=%r)' %
|
self.log.debug('registering module %r as %s (export=%r)' %
|
||||||
@ -214,9 +223,14 @@ class Dispatcher:
|
|||||||
if cobj is None:
|
if cobj is None:
|
||||||
raise NoSuchCommandError('Module %r has no command %r' % (modulename, cname or exportedname))
|
raise NoSuchCommandError('Module %r has no command %r' % (modulename, cname or exportedname))
|
||||||
|
|
||||||
|
if cobj.argument:
|
||||||
|
argument = cobj.argument.import_value(argument)
|
||||||
# now call func
|
# now call func
|
||||||
# note: exceptions are handled in handle_request, not here!
|
# note: exceptions are handled in handle_request, not here!
|
||||||
return cobj.do(moduleobj, argument), dict(t=currenttime())
|
result = cobj.do(moduleobj, argument)
|
||||||
|
if cobj.result:
|
||||||
|
result = cobj.result.export_value(result)
|
||||||
|
return result, dict(t=currenttime())
|
||||||
|
|
||||||
def _setParameterValue(self, modulename, exportedname, value):
|
def _setParameterValue(self, modulename, exportedname, value):
|
||||||
moduleobj = self.get_module(modulename)
|
moduleobj = self.get_module(modulename)
|
||||||
@ -236,13 +250,9 @@ class Dispatcher:
|
|||||||
|
|
||||||
# validate!
|
# validate!
|
||||||
value = pobj.datatype(value)
|
value = pobj.datatype(value)
|
||||||
writefunc = getattr(moduleobj, 'write_%s' % pname, None)
|
|
||||||
# note: exceptions are handled in handle_request, not here!
|
# note: exceptions are handled in handle_request, not here!
|
||||||
if writefunc:
|
getattr(moduleobj, 'write_' + pname)(value)
|
||||||
# return value is ignored here, as it is automatically set on the pobj and broadcast
|
# return value is ignored here, as already handled
|
||||||
writefunc(value)
|
|
||||||
else:
|
|
||||||
setattr(moduleobj, pname, value)
|
|
||||||
return pobj.export_value(), dict(t=pobj.timestamp) if pobj.timestamp else {}
|
return pobj.export_value(), dict(t=pobj.timestamp) if pobj.timestamp else {}
|
||||||
|
|
||||||
def _getParameterValue(self, modulename, exportedname):
|
def _getParameterValue(self, modulename, exportedname):
|
||||||
@ -259,11 +269,9 @@ class Dispatcher:
|
|||||||
# raise ReadOnlyError('This parameter is constant and can not be accessed remotely.')
|
# raise ReadOnlyError('This parameter is constant and can not be accessed remotely.')
|
||||||
return pobj.datatype.export_value(pobj.constant)
|
return pobj.datatype.export_value(pobj.constant)
|
||||||
|
|
||||||
readfunc = getattr(moduleobj, 'read_%s' % pname, None)
|
|
||||||
if readfunc:
|
|
||||||
# should also update the pobj (via the setter from the metaclass)
|
|
||||||
# note: exceptions are handled in handle_request, not here!
|
# note: exceptions are handled in handle_request, not here!
|
||||||
readfunc()
|
getattr(moduleobj, 'read_' + pname)()
|
||||||
|
# return value is ignored here, as already handled
|
||||||
return pobj.export_value(), dict(t=pobj.timestamp) if pobj.timestamp else {}
|
return pobj.export_value(), dict(t=pobj.timestamp) if pobj.timestamp else {}
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -298,6 +306,10 @@ class Dispatcher:
|
|||||||
self.log.error('should have been handled in the interface!')
|
self.log.error('should have been handled in the interface!')
|
||||||
|
|
||||||
def handle__ident(self, conn, specifier, data):
|
def handle__ident(self, conn, specifier, data):
|
||||||
|
# Remark: the following line is needed due to issue 66.
|
||||||
|
self.reset_connection(conn)
|
||||||
|
# The other stuff in issue 66 ('error_closed' message), has to be implemented
|
||||||
|
# if and when frappy will support serial server connections
|
||||||
return (IDENTREPLY, None, None)
|
return (IDENTREPLY, None, None)
|
||||||
|
|
||||||
def handle_describe(self, conn, specifier, data):
|
def handle_describe(self, conn, specifier, data):
|
||||||
@ -373,3 +385,19 @@ class Dispatcher:
|
|||||||
self._active_connections.discard(conn)
|
self._active_connections.discard(conn)
|
||||||
# XXX: also check all entries in self._subscriptions?
|
# XXX: also check all entries in self._subscriptions?
|
||||||
return (DISABLEEVENTSREPLY, None, None)
|
return (DISABLEEVENTSREPLY, None, None)
|
||||||
|
|
||||||
|
def send_log_msg(self, conn, modname, level, msg):
|
||||||
|
"""send log message """
|
||||||
|
conn.send_reply((LOG_EVENT, '%s:%s' % (modname, level), msg))
|
||||||
|
|
||||||
|
def set_all_log_levels(self, conn, level):
|
||||||
|
for modobj in self._modules.values():
|
||||||
|
modobj.setRemoteLogging(conn, level)
|
||||||
|
|
||||||
|
def handle_logging(self, conn, specifier, level):
|
||||||
|
if specifier and specifier != '.':
|
||||||
|
modobj = self._modules[specifier]
|
||||||
|
modobj.setRemoteLogging(conn, level)
|
||||||
|
else:
|
||||||
|
self.set_all_log_levels(conn, level)
|
||||||
|
return LOGGING_REPLY, specifier, level
|
||||||
|
@ -202,3 +202,11 @@ class TCPServer(socketserver.ThreadingTCPServer):
|
|||||||
if ntry:
|
if ntry:
|
||||||
self.log.warning('tried again %d times after "Address already in use"' % ntry)
|
self.log.warning('tried again %d times after "Address already in use"' % ntry)
|
||||||
self.log.info("TCPServer initiated")
|
self.log.info("TCPServer initiated")
|
||||||
|
|
||||||
|
# py35 compatibility
|
||||||
|
if not hasattr(socketserver.ThreadingTCPServer, '__exit__'):
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *args):
|
||||||
|
self.server_close()
|
||||||
|
@ -65,6 +65,13 @@ ERRORPREFIX = 'error_' # + specifier + json_extended_info(error_report)
|
|||||||
HELPREQUEST = 'help' # literal
|
HELPREQUEST = 'help' # literal
|
||||||
HELPREPLY = 'helping' # +line number +json_text
|
HELPREPLY = 'helping' # +line number +json_text
|
||||||
|
|
||||||
|
LOGGING_REQUEST = 'logging'
|
||||||
|
LOGGING_REPLY = 'logging'
|
||||||
|
# + [module] + json string (loglevel)
|
||||||
|
|
||||||
|
LOG_EVENT = 'log'
|
||||||
|
# + [module:level] + json_string (message)
|
||||||
|
|
||||||
# helper mapping to find the REPLY for a REQUEST
|
# helper mapping to find the REPLY for a REQUEST
|
||||||
# do not put IDENTREQUEST/IDENTREPLY here, as this needs anyway extra treatment
|
# do not put IDENTREQUEST/IDENTREPLY here, as this needs anyway extra treatment
|
||||||
REQUEST2REPLY = {
|
REQUEST2REPLY = {
|
||||||
@ -77,6 +84,7 @@ REQUEST2REPLY = {
|
|||||||
READREQUEST: READREPLY,
|
READREQUEST: READREPLY,
|
||||||
HEARTBEATREQUEST: HEARTBEATREPLY,
|
HEARTBEATREQUEST: HEARTBEATREPLY,
|
||||||
HELPREQUEST: HELPREPLY,
|
HELPREQUEST: HELPREPLY,
|
||||||
|
LOGGING_REQUEST: LOGGING_REPLY,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -89,6 +97,8 @@ HelpMessage = """Try one of the following:
|
|||||||
'%s <nonce>' to request a heartbeat response
|
'%s <nonce>' to request a heartbeat response
|
||||||
'%s' to activate async updates
|
'%s' to activate async updates
|
||||||
'%s' to deactivate updates
|
'%s' to deactivate updates
|
||||||
|
'%s [<module>] <loglevel>' to activate logging events
|
||||||
""" % (IDENTREQUEST, DESCRIPTIONREQUEST, READREQUEST,
|
""" % (IDENTREQUEST, DESCRIPTIONREQUEST, READREQUEST,
|
||||||
WRITEREQUEST, COMMANDREQUEST, HEARTBEATREQUEST,
|
WRITEREQUEST, COMMANDREQUEST, HEARTBEATREQUEST,
|
||||||
ENABLEEVENTSREQUEST, DISABLEEVENTSREQUEST)
|
ENABLEEVENTSREQUEST, DISABLEEVENTSREQUEST,
|
||||||
|
LOGGING_REQUEST)
|
||||||
|
@ -23,23 +23,22 @@
|
|||||||
|
|
||||||
from secop.client import SecopClient, decode_msg, encode_msg_frame
|
from secop.client import SecopClient, decode_msg, encode_msg_frame
|
||||||
from secop.datatypes import StringType
|
from secop.datatypes import StringType
|
||||||
from secop.errors import BadValueError, \
|
from secop.errors import BadValueError, CommunicationFailedError, ConfigError
|
||||||
CommunicationFailedError, ConfigError, make_secop_error
|
|
||||||
from secop.lib import get_class
|
from secop.lib import get_class
|
||||||
from secop.modules import Drivable, Module, Readable, Writable
|
from secop.modules import Drivable, Module, Readable, Writable
|
||||||
from secop.params import Command, Parameter
|
from secop.params import Command, Parameter
|
||||||
from secop.properties import Property
|
from secop.properties import Property
|
||||||
from secop.io import HasIodev
|
from secop.io import HasIO
|
||||||
|
|
||||||
|
|
||||||
class ProxyModule(HasIodev, Module):
|
class ProxyModule(HasIO, Module):
|
||||||
module = Property('remote module name', datatype=StringType(), default='')
|
module = Property('remote module name', datatype=StringType(), default='')
|
||||||
|
|
||||||
pollerClass = None
|
|
||||||
_consistency_check_done = False
|
_consistency_check_done = False
|
||||||
_secnode = None
|
_secnode = None
|
||||||
|
enablePoll = False
|
||||||
|
|
||||||
def iodevClass(self, name, logger, opts, srv):
|
def ioClass(self, name, logger, opts, srv):
|
||||||
opts['description'] = 'secnode %s on %s' % (opts.get('module', name), opts['uri'])
|
opts['description'] = 'secnode %s on %s' % (opts.get('module', name), opts['uri'])
|
||||||
return SecNode(name, logger, opts, srv)
|
return SecNode(name, logger, opts, srv)
|
||||||
|
|
||||||
@ -47,14 +46,12 @@ class ProxyModule(HasIodev, Module):
|
|||||||
if parameter not in self.parameters:
|
if parameter not in self.parameters:
|
||||||
return # ignore unknown parameters
|
return # ignore unknown parameters
|
||||||
# should be done here: deal with clock differences
|
# should be done here: deal with clock differences
|
||||||
if readerror:
|
|
||||||
readerror = make_secop_error(*readerror)
|
|
||||||
self.announceUpdate(parameter, value, readerror, timestamp)
|
self.announceUpdate(parameter, value, readerror, timestamp)
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
if not self.module:
|
if not self.module:
|
||||||
self.module = self.name
|
self.module = self.name
|
||||||
self._secnode = self._iodev.secnode
|
self._secnode = self.io.secnode
|
||||||
self._secnode.register_callback(self.module, self.updateEvent,
|
self._secnode.register_callback(self.module, self.updateEvent,
|
||||||
self.descriptiveDataChange, self.nodeStateChange)
|
self.descriptiveDataChange, self.nodeStateChange)
|
||||||
super().initModule()
|
super().initModule()
|
||||||
@ -125,6 +122,7 @@ class ProxyModule(HasIodev, Module):
|
|||||||
def checkProperties(self):
|
def checkProperties(self):
|
||||||
pass # skip
|
pass # skip
|
||||||
|
|
||||||
|
|
||||||
class ProxyReadable(ProxyModule, Readable):
|
class ProxyReadable(ProxyModule, Readable):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -144,10 +142,12 @@ class SecNode(Module):
|
|||||||
uri = Property('uri of a SEC node', datatype=StringType())
|
uri = Property('uri of a SEC node', datatype=StringType())
|
||||||
|
|
||||||
def earlyInit(self):
|
def earlyInit(self):
|
||||||
|
super().earlyInit()
|
||||||
self.secnode = SecopClient(self.uri, self.log)
|
self.secnode = SecopClient(self.uri, self.log)
|
||||||
|
|
||||||
def startModule(self, started_callback):
|
def startModule(self, start_events):
|
||||||
self.secnode.spawn_connect(started_callback)
|
super().startModule(start_events)
|
||||||
|
self.secnode.spawn_connect(start_events.get_trigger())
|
||||||
|
|
||||||
@Command(StringType(), result=StringType())
|
@Command(StringType(), result=StringType())
|
||||||
def request(self, msg):
|
def request(self, msg):
|
||||||
@ -182,7 +182,7 @@ def proxy_class(remote_class, name=None):
|
|||||||
|
|
||||||
for aname, aobj in rcls.accessibles.items():
|
for aname, aobj in rcls.accessibles.items():
|
||||||
if isinstance(aobj, Parameter):
|
if isinstance(aobj, Parameter):
|
||||||
pobj = aobj.override(poll=False, handler=None, needscfg=False)
|
pobj = aobj.merge(dict(handler=None, needscfg=False))
|
||||||
attrs[aname] = pobj
|
attrs[aname] = pobj
|
||||||
|
|
||||||
def rfunc(self, pname=aname):
|
def rfunc(self, pname=aname):
|
||||||
@ -198,7 +198,7 @@ def proxy_class(remote_class, name=None):
|
|||||||
def wfunc(self, value, pname=aname):
|
def wfunc(self, value, pname=aname):
|
||||||
value, _, readerror = self._secnode.setParameter(self.name, pname, value)
|
value, _, readerror = self._secnode.setParameter(self.name, pname, value)
|
||||||
if readerror:
|
if readerror:
|
||||||
raise make_secop_error(*readerror)
|
raise readerror
|
||||||
return value
|
return value
|
||||||
|
|
||||||
attrs['write_' + aname] = wfunc
|
attrs['write_' + aname] = wfunc
|
||||||
@ -225,5 +225,5 @@ def Proxy(name, logger, cfgdict, srv):
|
|||||||
remote_class = cfgdict.pop('remote_class')
|
remote_class = cfgdict.pop('remote_class')
|
||||||
if 'description' not in cfgdict:
|
if 'description' not in cfgdict:
|
||||||
cfgdict['description'] = 'remote module %s on %s' % (
|
cfgdict['description'] = 'remote module %s on %s' % (
|
||||||
cfgdict.get('module', name), cfgdict.get('iodev', '?'))
|
cfgdict.get('module', name), cfgdict.get('io', '?'))
|
||||||
return proxy_class(remote_class)(name, logger, cfgdict, srv)
|
return proxy_class(remote_class)(name, logger, cfgdict, srv)
|
||||||
|
221
secop/rwhandler.py
Normal file
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 configparser
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
import traceback
|
import traceback
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from secop.errors import ConfigError, SECoPError
|
from secop.errors import ConfigError, SECoPError
|
||||||
from secop.lib import formatException, get_class, getGeneralConfig
|
from secop.lib import formatException, get_class, generalConfig
|
||||||
from secop.modules import Attached
|
from secop.lib.multievent import MultiEvent
|
||||||
from secop.params import PREDEFINED_ACCESSIBLES
|
from secop.params import PREDEFINED_ACCESSIBLES
|
||||||
|
from secop.modules import Attached
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from daemon import DaemonContext
|
from daemon import DaemonContext
|
||||||
@ -89,7 +88,6 @@ class Server:
|
|||||||
...
|
...
|
||||||
"""
|
"""
|
||||||
self._testonly = testonly
|
self._testonly = testonly
|
||||||
cfg = getGeneralConfig()
|
|
||||||
|
|
||||||
self.log = parent_logger.getChild(name, True)
|
self.log = parent_logger.getChild(name, True)
|
||||||
if not cfgfiles:
|
if not cfgfiles:
|
||||||
@ -114,22 +112,21 @@ class Server:
|
|||||||
if ambiguous_sections:
|
if ambiguous_sections:
|
||||||
self.log.warning('ambiguous sections in %s: %r' % (cfgfiles, tuple(ambiguous_sections)))
|
self.log.warning('ambiguous sections in %s: %r' % (cfgfiles, tuple(ambiguous_sections)))
|
||||||
self._cfgfiles = cfgfiles
|
self._cfgfiles = cfgfiles
|
||||||
self._pidfile = os.path.join(cfg['piddir'], name + '.pid')
|
self._pidfile = os.path.join(generalConfig.piddir, name + '.pid')
|
||||||
|
|
||||||
def loadCfgFile(self, cfgfile):
|
def loadCfgFile(self, cfgfile):
|
||||||
if not cfgfile.endswith('.cfg'):
|
if not cfgfile.endswith('.cfg'):
|
||||||
cfgfile += '.cfg'
|
cfgfile += '.cfg'
|
||||||
cfg = getGeneralConfig()
|
|
||||||
if os.sep in cfgfile: # specified as full path
|
if os.sep in cfgfile: # specified as full path
|
||||||
filename = cfgfile if os.path.exists(cfgfile) else None
|
filename = cfgfile if os.path.exists(cfgfile) else None
|
||||||
else:
|
else:
|
||||||
for filename in [os.path.join(d, cfgfile) for d in cfg['confdir'].split(os.pathsep)]:
|
for filename in [os.path.join(d, cfgfile) for d in generalConfig.confdir.split(os.pathsep)]:
|
||||||
if os.path.exists(filename):
|
if os.path.exists(filename):
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
filename = None
|
filename = None
|
||||||
if filename is None:
|
if filename is None:
|
||||||
raise ConfigError("Couldn't find cfg file %r in %s" % (cfgfile, cfg['confdir']))
|
raise ConfigError("Couldn't find cfg file %r in %s" % (cfgfile, generalConfig.confdir))
|
||||||
self.log.debug('Parse config file %s ...' % filename)
|
self.log.debug('Parse config file %s ...' % filename)
|
||||||
result = OrderedDict()
|
result = OrderedDict()
|
||||||
parser = configparser.ConfigParser()
|
parser = configparser.ConfigParser()
|
||||||
@ -268,36 +265,58 @@ class Server:
|
|||||||
failure_traceback = traceback.format_exc()
|
failure_traceback = traceback.format_exc()
|
||||||
errors.append('error creating %s' % modname)
|
errors.append('error creating %s' % modname)
|
||||||
|
|
||||||
poll_table = dict()
|
missing_super = set()
|
||||||
# all objs created, now start them up and interconnect
|
# all objs created, now start them up and interconnect
|
||||||
for modname, modobj in self.modules.items():
|
for modname, modobj in self.modules.items():
|
||||||
self.log.info('registering module %r' % modname)
|
self.log.info('registering module %r' % modname)
|
||||||
self.dispatcher.register_module(modobj, modname, modobj.export)
|
self.dispatcher.register_module(modobj, modname, modobj.export)
|
||||||
if modobj.pollerClass is not None:
|
|
||||||
# a module might be explicitly excluded from polling by setting pollerClass to None
|
|
||||||
modobj.pollerClass.add_to_table(poll_table, modobj)
|
|
||||||
# also call earlyInit on the modules
|
# also call earlyInit on the modules
|
||||||
modobj.earlyInit()
|
modobj.earlyInit()
|
||||||
|
if not modobj.earlyInitDone:
|
||||||
|
missing_super.add('%s was not called, probably missing super call'
|
||||||
|
% modobj.earlyInit.__qualname__)
|
||||||
|
|
||||||
# handle attached modules
|
# handle attached modules
|
||||||
for modname, modobj in self.modules.items():
|
for modname, modobj in self.modules.items():
|
||||||
|
attached_modules = {}
|
||||||
for propname, propobj in modobj.propertyDict.items():
|
for propname, propobj in modobj.propertyDict.items():
|
||||||
if isinstance(propobj, Attached):
|
if isinstance(propobj, Attached):
|
||||||
try:
|
try:
|
||||||
setattr(modobj, propobj.attrname or '_' + propname,
|
attname = getattr(modobj, propname)
|
||||||
self.dispatcher.get_module(getattr(modobj, propname)))
|
if attname: # attached module specified in cfg file
|
||||||
|
attobj = self.dispatcher.get_module(attname)
|
||||||
|
if isinstance(attobj, propobj.basecls):
|
||||||
|
attached_modules[propname] = attobj
|
||||||
|
else:
|
||||||
|
errors.append('attached module %s=%r must inherit from %r'
|
||||||
|
% (propname, attname, propobj.basecls.__qualname__))
|
||||||
except SECoPError as e:
|
except SECoPError as e:
|
||||||
errors.append('module %s, attached %s: %s' % (modname, propname, str(e)))
|
errors.append('module %s, attached %s: %s' % (modname, propname, str(e)))
|
||||||
|
modobj.attachedModules = attached_modules
|
||||||
|
|
||||||
# call init on each module after registering all
|
# call init on each module after registering all
|
||||||
for modname, modobj in self.modules.items():
|
for modname, modobj in self.modules.items():
|
||||||
try:
|
try:
|
||||||
modobj.initModule()
|
modobj.initModule()
|
||||||
|
if not modobj.initModuleDone:
|
||||||
|
missing_super.add('%s was not called, probably missing super call'
|
||||||
|
% modobj.initModule.__qualname__)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if failure_traceback is None:
|
if failure_traceback is None:
|
||||||
failure_traceback = traceback.format_exc()
|
failure_traceback = traceback.format_exc()
|
||||||
errors.append('error initializing %s: %r' % (modname, e))
|
errors.append('error initializing %s: %r' % (modname, e))
|
||||||
|
|
||||||
|
if not self._testonly:
|
||||||
|
start_events = MultiEvent(default_timeout=30)
|
||||||
|
for modname, modobj in self.modules.items():
|
||||||
|
# startModule must return either a timeout value or None (default 30 sec)
|
||||||
|
start_events.name = 'module %s' % modname
|
||||||
|
modobj.startModule(start_events)
|
||||||
|
if not modobj.startModuleDone:
|
||||||
|
missing_super.add('%s was not called, probably missing super call'
|
||||||
|
% modobj.startModule.__qualname__)
|
||||||
|
errors.extend(missing_super)
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
for errtxt in errors:
|
for errtxt in errors:
|
||||||
for line in errtxt.split('\n'):
|
for line in errtxt.split('\n'):
|
||||||
@ -311,22 +330,13 @@ class Server:
|
|||||||
|
|
||||||
if self._testonly:
|
if self._testonly:
|
||||||
return
|
return
|
||||||
start_events = []
|
self.log.info('waiting for modules being started')
|
||||||
for modname, modobj in self.modules.items():
|
start_events.name = None
|
||||||
event = threading.Event()
|
if not start_events.wait():
|
||||||
# startModule must return either a timeout value or None (default 30 sec)
|
# some timeout happened
|
||||||
timeout = modobj.startModule(started_callback=event.set) or 30
|
for name in start_events.waiting_for():
|
||||||
start_events.append((time.time() + timeout, 'module %s' % modname, event))
|
self.log.warning('timeout when starting %s' % name)
|
||||||
for poller in poll_table.values():
|
self.log.info('all modules started')
|
||||||
event = threading.Event()
|
|
||||||
# poller.start must return either a timeout value or None (default 30 sec)
|
|
||||||
timeout = poller.start(started_callback=event.set) or 30
|
|
||||||
start_events.append((time.time() + timeout, repr(poller), event))
|
|
||||||
self.log.info('waiting for modules and pollers being started')
|
|
||||||
for deadline, name, event in sorted(start_events):
|
|
||||||
if not event.wait(timeout=max(0, deadline - time.time())):
|
|
||||||
self.log.info('WARNING: timeout when starting %s' % name)
|
|
||||||
self.log.info('all modules and pollers started')
|
|
||||||
history_path = os.environ.get('FRAPPY_HISTORY')
|
history_path = os.environ.get('FRAPPY_HISTORY')
|
||||||
if history_path:
|
if history_path:
|
||||||
from secop_psi.historywriter import FrappyHistoryWriter # pylint: disable=import-outside-toplevel
|
from secop_psi.historywriter import FrappyHistoryWriter # pylint: disable=import-outside-toplevel
|
||||||
|
@ -27,13 +27,10 @@ from time import sleep
|
|||||||
|
|
||||||
from secop.datatypes import FloatRange
|
from secop.datatypes import FloatRange
|
||||||
from secop.lib import mkthread
|
from secop.lib import mkthread
|
||||||
from secop.modules import BasicPoller, Drivable, \
|
from secop.modules import Drivable, Module, Parameter, Readable, Writable, Command
|
||||||
Module, Parameter, Readable, Writable, Command
|
|
||||||
|
|
||||||
|
|
||||||
class SimBase:
|
class SimBase:
|
||||||
pollerClass = BasicPoller
|
|
||||||
|
|
||||||
def __new__(cls, devname, logger, cfgdict, dispatcher):
|
def __new__(cls, devname, logger, cfgdict, dispatcher):
|
||||||
extra_params = cfgdict.pop('extra_params', '') or cfgdict.pop('.extra_params', '')
|
extra_params = cfgdict.pop('extra_params', '') or cfgdict.pop('.extra_params', '')
|
||||||
attrs = {}
|
attrs = {}
|
||||||
@ -60,6 +57,7 @@ class SimBase:
|
|||||||
return object.__new__(type('SimBase_%s' % devname, (cls,), attrs))
|
return object.__new__(type('SimBase_%s' % devname, (cls,), attrs))
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
|
super().initModule()
|
||||||
self._sim_thread = mkthread(self._sim)
|
self._sim_thread = mkthread(self._sim)
|
||||||
|
|
||||||
def _sim(self):
|
def _sim(self):
|
||||||
@ -119,7 +117,7 @@ class SimDrivable(SimReadable, Drivable):
|
|||||||
self._value = self.target
|
self._value = self.target
|
||||||
speed *= self.interval
|
speed *= self.interval
|
||||||
try:
|
try:
|
||||||
self.pollParams(0)
|
self.doPoll()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -132,7 +130,7 @@ class SimDrivable(SimReadable, Drivable):
|
|||||||
self._value = self.target
|
self._value = self.target
|
||||||
sleep(self.interval)
|
sleep(self.interval)
|
||||||
try:
|
try:
|
||||||
self.pollParams(0)
|
self.doPoll()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
self.status = self.Status.IDLE, ''
|
self.status = self.Status.IDLE, ''
|
||||||
|
@ -111,6 +111,7 @@ class Cryostat(CryoBase):
|
|||||||
group='stability')
|
group='stability')
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
|
super().initModule()
|
||||||
self._stopflag = False
|
self._stopflag = False
|
||||||
self._thread = mkthread(self.thread)
|
self._thread = mkthread(self.thread)
|
||||||
|
|
||||||
|
@ -133,6 +133,7 @@ class MagneticField(Drivable):
|
|||||||
status = Parameter(datatype=TupleOf(EnumType(Status), StringType()))
|
status = Parameter(datatype=TupleOf(EnumType(Status), StringType()))
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
|
super().initModule()
|
||||||
self._state = Enum('state', idle=1, switch_on=2, switch_off=3, ramp=4).idle
|
self._state = Enum('state', idle=1, switch_on=2, switch_off=3, ramp=4).idle
|
||||||
self._heatswitch = self.DISPATCHER.get_module(self.heatswitch)
|
self._heatswitch = self.DISPATCHER.get_module(self.heatswitch)
|
||||||
_thread = threading.Thread(target=self._thread)
|
_thread = threading.Thread(target=self._thread)
|
||||||
@ -235,6 +236,7 @@ class SampleTemp(Drivable):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
|
super().initModule()
|
||||||
_thread = threading.Thread(target=self._thread)
|
_thread = threading.Thread(target=self._thread)
|
||||||
_thread.daemon = True
|
_thread.daemon = True
|
||||||
_thread.start()
|
_thread.start()
|
||||||
|
@ -31,7 +31,7 @@ import math
|
|||||||
from secop.datatypes import ArrayOf, FloatRange, StringType, StructOf, TupleOf
|
from secop.datatypes import ArrayOf, FloatRange, StringType, StructOf, TupleOf
|
||||||
from secop.errors import ConfigError, DisabledError
|
from secop.errors import ConfigError, DisabledError
|
||||||
from secop.lib.sequence import SequencerMixin, Step
|
from secop.lib.sequence import SequencerMixin, Step
|
||||||
from secop.modules import BasicPoller, Drivable, Parameter
|
from secop.modules import Drivable, Parameter
|
||||||
|
|
||||||
|
|
||||||
class GarfieldMagnet(SequencerMixin, Drivable):
|
class GarfieldMagnet(SequencerMixin, Drivable):
|
||||||
@ -47,9 +47,6 @@ class GarfieldMagnet(SequencerMixin, Drivable):
|
|||||||
the symmetry setting selects which.
|
the symmetry setting selects which.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pollerClass = BasicPoller
|
|
||||||
|
|
||||||
|
|
||||||
# parameters
|
# parameters
|
||||||
subdev_currentsource = Parameter('(bipolar) Powersupply', datatype=StringType(), readonly=True, export=False)
|
subdev_currentsource = Parameter('(bipolar) Powersupply', datatype=StringType(), readonly=True, export=False)
|
||||||
subdev_enable = Parameter('Switch to set for on/off', datatype=StringType(), readonly=True, export=False)
|
subdev_enable = Parameter('Switch to set for on/off', datatype=StringType(), readonly=True, export=False)
|
||||||
@ -57,10 +54,10 @@ class GarfieldMagnet(SequencerMixin, Drivable):
|
|||||||
subdev_symmetry = Parameter('Switch to read for symmetry', datatype=StringType(), readonly=True, export=False)
|
subdev_symmetry = Parameter('Switch to read for symmetry', datatype=StringType(), readonly=True, export=False)
|
||||||
userlimits = Parameter('User defined limits of device value',
|
userlimits = Parameter('User defined limits of device value',
|
||||||
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')),
|
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')),
|
||||||
default=(float('-Inf'), float('+Inf')), readonly=False, poll=10)
|
default=(float('-Inf'), float('+Inf')), readonly=False)
|
||||||
abslimits = Parameter('Absolute limits of device value',
|
abslimits = Parameter('Absolute limits of device value',
|
||||||
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')),
|
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')),
|
||||||
default=(-0.5, 0.5), poll=True,
|
default=(-0.5, 0.5),
|
||||||
)
|
)
|
||||||
precision = Parameter('Precision of the device value (allowed deviation '
|
precision = Parameter('Precision of the device value (allowed deviation '
|
||||||
'of stable values from target)',
|
'of stable values from target)',
|
||||||
@ -71,7 +68,7 @@ class GarfieldMagnet(SequencerMixin, Drivable):
|
|||||||
calibration = Parameter('Coefficients for calibration '
|
calibration = Parameter('Coefficients for calibration '
|
||||||
'function: [c0, c1, c2, c3, c4] calculates '
|
'function: [c0, c1, c2, c3, c4] calculates '
|
||||||
'B(I) = c0*I + c1*erf(c2*I) + c3*atan(c4*I)'
|
'B(I) = c0*I + c1*erf(c2*I) + c3*atan(c4*I)'
|
||||||
' in T', poll=1,
|
' in T',
|
||||||
datatype=ArrayOf(FloatRange(), 5, 5),
|
datatype=ArrayOf(FloatRange(), 5, 5),
|
||||||
default=(1.0, 0.0, 0.0, 0.0, 0.0))
|
default=(1.0, 0.0, 0.0, 0.0, 0.0))
|
||||||
calibrationtable = Parameter('Map of Coefficients for calibration per symmetry setting',
|
calibrationtable = Parameter('Map of Coefficients for calibration per symmetry setting',
|
||||||
@ -137,7 +134,7 @@ class GarfieldMagnet(SequencerMixin, Drivable):
|
|||||||
'_current2field polynome not monotonic!')
|
'_current2field polynome not monotonic!')
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
super(GarfieldMagnet, self).initModule()
|
super().initModule()
|
||||||
self._enable = self.DISPATCHER.get_module(self.subdev_enable)
|
self._enable = self.DISPATCHER.get_module(self.subdev_enable)
|
||||||
self._symmetry = self.DISPATCHER.get_module(self.subdev_symmetry)
|
self._symmetry = self.DISPATCHER.get_module(self.subdev_symmetry)
|
||||||
self._polswitch = self.DISPATCHER.get_module(self.subdev_polswitch)
|
self._polswitch = self.DISPATCHER.get_module(self.subdev_polswitch)
|
||||||
@ -220,7 +217,7 @@ class GarfieldMagnet(SequencerMixin, Drivable):
|
|||||||
self._currentsource.read_value() *
|
self._currentsource.read_value() *
|
||||||
self._get_field_polarity())
|
self._get_field_polarity())
|
||||||
|
|
||||||
def read_hw_status(self):
|
def readHwStatus(self):
|
||||||
# called from SequencerMixin.read_status if no sequence is running
|
# called from SequencerMixin.read_status if no sequence is running
|
||||||
if self._enable.value == 'Off':
|
if self._enable.value == 'Off':
|
||||||
return self.Status.WARN, 'Disabled'
|
return self.Status.WARN, 'Disabled'
|
||||||
|
@ -39,7 +39,7 @@ from secop.datatypes import ArrayOf, EnumType, FloatRange, \
|
|||||||
from secop.errors import CommunicationFailedError, \
|
from secop.errors import CommunicationFailedError, \
|
||||||
ConfigError, HardwareError, ProgrammingError
|
ConfigError, HardwareError, ProgrammingError
|
||||||
from secop.lib import lazy_property
|
from secop.lib import lazy_property
|
||||||
from secop.modules import BasicPoller, Command, \
|
from secop.modules import Command, \
|
||||||
Drivable, Module, Parameter, Readable
|
Drivable, Module, Parameter, Readable
|
||||||
|
|
||||||
#####
|
#####
|
||||||
@ -157,8 +157,6 @@ class PyTangoDevice(Module):
|
|||||||
execution and attribute operations with logging and exception mapping.
|
execution and attribute operations with logging and exception mapping.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pollerClass = BasicPoller
|
|
||||||
|
|
||||||
# parameters
|
# parameters
|
||||||
comtries = Parameter('Maximum retries for communication',
|
comtries = Parameter('Maximum retries for communication',
|
||||||
datatype=IntRange(1, 100), default=3, readonly=False,
|
datatype=IntRange(1, 100), default=3, readonly=False,
|
||||||
@ -210,7 +208,7 @@ class PyTangoDevice(Module):
|
|||||||
# exception mapping is enabled).
|
# exception mapping is enabled).
|
||||||
self._createPyTangoDevice = self._applyGuardToFunc(
|
self._createPyTangoDevice = self._applyGuardToFunc(
|
||||||
self._createPyTangoDevice, 'constructor')
|
self._createPyTangoDevice, 'constructor')
|
||||||
super(PyTangoDevice, self).earlyInit()
|
super().earlyInit()
|
||||||
|
|
||||||
@lazy_property
|
@lazy_property
|
||||||
def _dev(self):
|
def _dev(self):
|
||||||
@ -249,10 +247,10 @@ class PyTangoDevice(Module):
|
|||||||
# otherwise would lead to attribute errors later
|
# otherwise would lead to attribute errors later
|
||||||
try:
|
try:
|
||||||
device.State
|
device.State
|
||||||
except AttributeError:
|
except AttributeError as e:
|
||||||
raise CommunicationFailedError(
|
raise CommunicationFailedError(
|
||||||
self, 'connection to Tango server failed, '
|
self, 'connection to Tango server failed, '
|
||||||
'is the server running?')
|
'is the server running?') from e
|
||||||
return self._applyGuardsToPyTangoDevice(device)
|
return self._applyGuardsToPyTangoDevice(device)
|
||||||
|
|
||||||
def _applyGuardsToPyTangoDevice(self, dev):
|
def _applyGuardsToPyTangoDevice(self, dev):
|
||||||
@ -376,14 +374,17 @@ class AnalogInput(PyTangoDevice, Readable):
|
|||||||
The AnalogInput handles all devices only delivering an analogue value.
|
The AnalogInput handles all devices only delivering an analogue value.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def startModule(self, started_callback):
|
def startModule(self, start_events):
|
||||||
super(AnalogInput, self).startModule(started_callback)
|
super().startModule(start_events)
|
||||||
|
try:
|
||||||
# query unit from tango and update value property
|
# query unit from tango and update value property
|
||||||
attrInfo = self._dev.attribute_query('value')
|
attrInfo = self._dev.attribute_query('value')
|
||||||
# prefer configured unit if nothing is set on the Tango device, else
|
# prefer configured unit if nothing is set on the Tango device, else
|
||||||
# update
|
# update
|
||||||
if attrInfo.unit != 'No unit':
|
if attrInfo.unit != 'No unit':
|
||||||
self.accessibles['value'].datatype.setProperty('unit', attrInfo.unit)
|
self.accessibles['value'].datatype.setProperty('unit', attrInfo.unit)
|
||||||
|
except Exception as e:
|
||||||
|
self.log.error(e)
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
return self._dev.value
|
return self._dev.value
|
||||||
@ -422,7 +423,7 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
|||||||
userlimits = Parameter('User defined limits of device value',
|
userlimits = Parameter('User defined limits of device value',
|
||||||
datatype=LimitsType(FloatRange(unit='$')),
|
datatype=LimitsType(FloatRange(unit='$')),
|
||||||
default=(float('-Inf'), float('+Inf')),
|
default=(float('-Inf'), float('+Inf')),
|
||||||
readonly=False, poll=10,
|
readonly=False,
|
||||||
)
|
)
|
||||||
abslimits = Parameter('Absolute limits of device value',
|
abslimits = Parameter('Absolute limits of device value',
|
||||||
datatype=LimitsType(FloatRange(unit='$')),
|
datatype=LimitsType(FloatRange(unit='$')),
|
||||||
@ -446,13 +447,13 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
|||||||
_moving = False
|
_moving = False
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
super(AnalogOutput, self).initModule()
|
super().initModule()
|
||||||
# init history
|
# init history
|
||||||
self._history = [] # will keep (timestamp, value) tuple
|
self._history = [] # will keep (timestamp, value) tuple
|
||||||
self._timeout = None # keeps the time at which we will timeout, or None
|
self._timeout = None # keeps the time at which we will timeout, or None
|
||||||
|
|
||||||
def startModule(self, started_callback):
|
def startModule(self, start_events):
|
||||||
super(AnalogOutput, self).startModule(started_callback)
|
super().startModule(start_events)
|
||||||
# query unit from tango and update value property
|
# query unit from tango and update value property
|
||||||
attrInfo = self._dev.attribute_query('value')
|
attrInfo = self._dev.attribute_query('value')
|
||||||
# prefer configured unit if nothing is set on the Tango device, else
|
# prefer configured unit if nothing is set on the Tango device, else
|
||||||
@ -460,8 +461,8 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
|||||||
if attrInfo.unit != 'No unit':
|
if attrInfo.unit != 'No unit':
|
||||||
self.accessibles['value'].datatype.setProperty('unit', attrInfo.unit)
|
self.accessibles['value'].datatype.setProperty('unit', attrInfo.unit)
|
||||||
|
|
||||||
def pollParams(self, nr=0):
|
def doPoll(self):
|
||||||
super(AnalogOutput, self).pollParams(nr)
|
super().doPoll()
|
||||||
while len(self._history) > 2:
|
while len(self._history) > 2:
|
||||||
# if history would be too short, break
|
# if history would be too short, break
|
||||||
if self._history[-1][0] - self._history[1][0] <= self.window:
|
if self._history[-1][0] - self._history[1][0] <= self.window:
|
||||||
@ -489,8 +490,11 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
|||||||
hist = self._history[:]
|
hist = self._history[:]
|
||||||
window_start = currenttime() - self.window
|
window_start = currenttime() - self.window
|
||||||
hist_in_window = [v for (t, v) in hist if t >= window_start]
|
hist_in_window = [v for (t, v) in hist if t >= window_start]
|
||||||
|
if len(hist) == len(hist_in_window):
|
||||||
|
return False # no data point before window
|
||||||
if not hist_in_window:
|
if not hist_in_window:
|
||||||
return False # no relevant history -> no knowledge
|
# window is too small -> use last point only
|
||||||
|
hist_in_window = [self.value]
|
||||||
|
|
||||||
max_in_hist = max(hist_in_window)
|
max_in_hist = max(hist_in_window)
|
||||||
min_in_hist = min(hist_in_window)
|
min_in_hist = min(hist_in_window)
|
||||||
@ -503,13 +507,14 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
|||||||
if self._isAtTarget():
|
if self._isAtTarget():
|
||||||
self._timeout = None
|
self._timeout = None
|
||||||
self._moving = False
|
self._moving = False
|
||||||
return super(AnalogOutput, self).read_status()
|
status = super().read_status()
|
||||||
if self._timeout:
|
else:
|
||||||
if self._timeout < currenttime():
|
if self._timeout and self._timeout < currenttime():
|
||||||
return self.Status.UNSTABLE, 'timeout after waiting for stable value'
|
status = self.Status.UNSTABLE, 'timeout after waiting for stable value'
|
||||||
if self._moving:
|
else:
|
||||||
return (self.Status.BUSY, 'moving')
|
status = (self.Status.BUSY, 'moving') if self._moving else (self.Status.IDLE, 'stable')
|
||||||
return (self.Status.IDLE, 'stable')
|
self.setFastPoll(self.isBusy(status))
|
||||||
|
return status
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def absmin(self):
|
def absmin(self):
|
||||||
@ -571,11 +576,14 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
|||||||
if not self.timeout:
|
if not self.timeout:
|
||||||
self._timeout = None
|
self._timeout = None
|
||||||
self._moving = True
|
self._moving = True
|
||||||
self._history = [] # clear history
|
# do not clear the history here:
|
||||||
self.read_status() # poll our status to keep it updated
|
# - if the target is not changed by more than precision, there is no need to wait
|
||||||
|
# self._history = []
|
||||||
|
self.read_status() # poll our status to keep it updated (this will also set fast poll)
|
||||||
|
return self.read_target()
|
||||||
|
|
||||||
def _hw_wait(self):
|
def _hw_wait(self):
|
||||||
while super(AnalogOutput, self).read_status()[0] == self.Status.BUSY:
|
while super().read_status()[0] == self.Status.BUSY:
|
||||||
sleep(0.3)
|
sleep(0.3)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
@ -597,8 +605,7 @@ class Actuator(AnalogOutput):
|
|||||||
readonly=False, datatype=FloatRange(0, unit='$/s'),
|
readonly=False, datatype=FloatRange(0, unit='$/s'),
|
||||||
)
|
)
|
||||||
ramp = Parameter('The speed of changing the value',
|
ramp = Parameter('The speed of changing the value',
|
||||||
readonly=False, datatype=FloatRange(0, unit='$/s'),
|
readonly=False, datatype=FloatRange(0, unit='$/min'),
|
||||||
poll=30,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def read_speed(self):
|
def read_speed(self):
|
||||||
@ -677,17 +684,22 @@ class TemperatureController(Actuator):
|
|||||||
)
|
)
|
||||||
pid = Parameter('pid control Parameters',
|
pid = Parameter('pid control Parameters',
|
||||||
datatype=TupleOf(FloatRange(), FloatRange(), FloatRange()),
|
datatype=TupleOf(FloatRange(), FloatRange(), FloatRange()),
|
||||||
readonly=False, group='pid', poll=30,
|
readonly=False, group='pid',
|
||||||
)
|
)
|
||||||
setpoint = Parameter('Current setpoint', datatype=FloatRange(unit='$'), poll=1,
|
setpoint = Parameter('Current setpoint', datatype=FloatRange(unit='$'),
|
||||||
)
|
)
|
||||||
heateroutput = Parameter('Heater output', datatype=FloatRange(), poll=1,
|
heateroutput = Parameter('Heater output', datatype=FloatRange(),
|
||||||
)
|
)
|
||||||
|
|
||||||
# overrides
|
# overrides
|
||||||
precision = Parameter(default=0.1)
|
precision = Parameter(default=0.1)
|
||||||
ramp = Parameter(description='Temperature ramp')
|
ramp = Parameter(description='Temperature ramp')
|
||||||
|
|
||||||
|
def doPoll(self):
|
||||||
|
super().doPoll()
|
||||||
|
self.read_setpoint()
|
||||||
|
self.read_heateroutput()
|
||||||
|
|
||||||
def read_ramp(self):
|
def read_ramp(self):
|
||||||
return self._dev.ramp
|
return self._dev.ramp
|
||||||
|
|
||||||
@ -730,6 +742,10 @@ class TemperatureController(Actuator):
|
|||||||
def read_heateroutput(self):
|
def read_heateroutput(self):
|
||||||
return self._dev.heaterOutput
|
return self._dev.heaterOutput
|
||||||
|
|
||||||
|
# remove UserCommand setposition from Actuator
|
||||||
|
# (makes no sense for a TemperatureController)
|
||||||
|
setposition = None
|
||||||
|
|
||||||
|
|
||||||
class PowerSupply(Actuator):
|
class PowerSupply(Actuator):
|
||||||
"""A power supply (voltage and current) device.
|
"""A power supply (voltage and current) device.
|
||||||
@ -737,13 +753,19 @@ class PowerSupply(Actuator):
|
|||||||
|
|
||||||
# parameters
|
# parameters
|
||||||
voltage = Parameter('Actual voltage',
|
voltage = Parameter('Actual voltage',
|
||||||
datatype=FloatRange(unit='V'), poll=-5)
|
datatype=FloatRange(unit='V'))
|
||||||
current = Parameter('Actual current',
|
current = Parameter('Actual current',
|
||||||
datatype=FloatRange(unit='A'), poll=-5)
|
datatype=FloatRange(unit='A'))
|
||||||
|
|
||||||
# overrides
|
# overrides
|
||||||
ramp = Parameter(description='Current/voltage ramp')
|
ramp = Parameter(description='Current/voltage ramp')
|
||||||
|
|
||||||
|
def doPoll(self):
|
||||||
|
super().doPoll()
|
||||||
|
# TODO: poll voltage and current faster when busy
|
||||||
|
self.read_voltage()
|
||||||
|
self.read_current()
|
||||||
|
|
||||||
def read_ramp(self):
|
def read_ramp(self):
|
||||||
return self._dev.ramp
|
return self._dev.ramp
|
||||||
|
|
||||||
@ -777,8 +799,10 @@ class NamedDigitalInput(DigitalInput):
|
|||||||
datatype=StringType(), export=False) # XXX:!!!
|
datatype=StringType(), export=False) # XXX:!!!
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
super(NamedDigitalInput, self).initModule()
|
super().initModule()
|
||||||
try:
|
try:
|
||||||
|
mapping = self.mapping
|
||||||
|
if isinstance(mapping, str):
|
||||||
# pylint: disable=eval-used
|
# pylint: disable=eval-used
|
||||||
mapping = eval(self.mapping.replace('\n', ' '))
|
mapping = eval(self.mapping.replace('\n', ' '))
|
||||||
if isinstance(mapping, str):
|
if isinstance(mapping, str):
|
||||||
@ -786,7 +810,7 @@ class NamedDigitalInput(DigitalInput):
|
|||||||
mapping = eval(mapping)
|
mapping = eval(mapping)
|
||||||
self.accessibles['value'].setProperty('datatype', EnumType('value', **mapping))
|
self.accessibles['value'].setProperty('datatype', EnumType('value', **mapping))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError('Illegal Value for mapping: %r' % e)
|
raise ValueError('Illegal Value for mapping: %r' % self.mapping) from e
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
value = self._dev.value
|
value = self._dev.value
|
||||||
@ -805,7 +829,7 @@ class PartialDigitalInput(NamedDigitalInput):
|
|||||||
datatype=IntRange(0), default=1)
|
datatype=IntRange(0), default=1)
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
super(PartialDigitalInput, self).initModule()
|
super().initModule()
|
||||||
self._mask = (1 << self.bitwidth) - 1
|
self._mask = (1 << self.bitwidth) - 1
|
||||||
# self.accessibles['value'].datatype = IntRange(0, self._mask)
|
# self.accessibles['value'].datatype = IntRange(0, self._mask)
|
||||||
|
|
||||||
@ -827,9 +851,16 @@ class DigitalOutput(PyTangoDevice, Drivable):
|
|||||||
def read_value(self):
|
def read_value(self):
|
||||||
return self._dev.value # mapping is done by datatype upon export()
|
return self._dev.value # mapping is done by datatype upon export()
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
status = self.read_status()
|
||||||
|
self.setFastPoll(self.isBusy(status))
|
||||||
|
return status
|
||||||
|
|
||||||
def write_target(self, value):
|
def write_target(self, value):
|
||||||
self._dev.value = value
|
self._dev.value = value
|
||||||
self.read_value()
|
self.read_value()
|
||||||
|
self.read_status() # this will also set fast poll
|
||||||
|
return self.read_target()
|
||||||
|
|
||||||
def read_target(self):
|
def read_target(self):
|
||||||
attrObj = self._dev.read_attribute('value')
|
attrObj = self._dev.read_attribute('value')
|
||||||
@ -845,8 +876,10 @@ class NamedDigitalOutput(DigitalOutput):
|
|||||||
datatype=StringType(), export=False)
|
datatype=StringType(), export=False)
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
super(NamedDigitalOutput, self).initModule()
|
super().initModule()
|
||||||
try:
|
try:
|
||||||
|
mapping = self.mapping
|
||||||
|
if isinstance(mapping, str):
|
||||||
# pylint: disable=eval-used
|
# pylint: disable=eval-used
|
||||||
mapping = eval(self.mapping.replace('\n', ' '))
|
mapping = eval(self.mapping.replace('\n', ' '))
|
||||||
if isinstance(mapping, str):
|
if isinstance(mapping, str):
|
||||||
@ -855,12 +888,13 @@ class NamedDigitalOutput(DigitalOutput):
|
|||||||
self.accessibles['value'].setProperty('datatype', EnumType('value', **mapping))
|
self.accessibles['value'].setProperty('datatype', EnumType('value', **mapping))
|
||||||
self.accessibles['target'].setProperty('datatype', EnumType('target', **mapping))
|
self.accessibles['target'].setProperty('datatype', EnumType('target', **mapping))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError('Illegal Value for mapping: %r' % e)
|
raise ValueError('Illegal Value for mapping: %r' % self.mapping) from e
|
||||||
|
|
||||||
def write_target(self, value):
|
def write_target(self, value):
|
||||||
# map from enum-str to integer value
|
# map from enum-str to integer value
|
||||||
self._dev.value = int(value)
|
self._dev.value = int(value)
|
||||||
self.read_value()
|
self.read_value()
|
||||||
|
return self.read_target()
|
||||||
|
|
||||||
|
|
||||||
class PartialDigitalOutput(NamedDigitalOutput):
|
class PartialDigitalOutput(NamedDigitalOutput):
|
||||||
@ -875,7 +909,7 @@ class PartialDigitalOutput(NamedDigitalOutput):
|
|||||||
datatype=IntRange(0), default=1)
|
datatype=IntRange(0), default=1)
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
super(PartialDigitalOutput, self).initModule()
|
super().initModule()
|
||||||
self._mask = (1 << self.bitwidth) - 1
|
self._mask = (1 << self.bitwidth) - 1
|
||||||
# self.accessibles['value'].datatype = IntRange(0, self._mask)
|
# self.accessibles['value'].datatype = IntRange(0, self._mask)
|
||||||
# self.accessibles['target'].datatype = IntRange(0, self._mask)
|
# self.accessibles['target'].datatype = IntRange(0, self._mask)
|
||||||
@ -891,6 +925,7 @@ class PartialDigitalOutput(NamedDigitalOutput):
|
|||||||
(value << self.startbit)
|
(value << self.startbit)
|
||||||
self._dev.value = newvalue
|
self._dev.value = newvalue
|
||||||
self.read_value()
|
self.read_value()
|
||||||
|
return self.read_target()
|
||||||
|
|
||||||
|
|
||||||
class StringIO(PyTangoDevice, Module):
|
class StringIO(PyTangoDevice, Module):
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
"""Andeen Hagerling capacitance bridge"""
|
"""Andeen Hagerling capacitance bridge"""
|
||||||
|
|
||||||
from secop.core import Done, FloatRange, HasIodev, Parameter, Readable, StringIO
|
from secop.core import Done, FloatRange, HasIO, Parameter, Readable, StringIO, nopoll
|
||||||
|
|
||||||
|
|
||||||
class Ah2700IO(StringIO):
|
class Ah2700IO(StringIO):
|
||||||
@ -28,19 +28,19 @@ class Ah2700IO(StringIO):
|
|||||||
timeout = 5
|
timeout = 5
|
||||||
|
|
||||||
|
|
||||||
class Capacitance(HasIodev, Readable):
|
class Capacitance(HasIO, Readable):
|
||||||
|
|
||||||
value = Parameter('capacitance', FloatRange(unit='pF'), poll=True)
|
value = Parameter('capacitance', FloatRange(unit='pF'))
|
||||||
freq = Parameter('frequency', FloatRange(unit='Hz'), readonly=False, default=0)
|
freq = Parameter('frequency', FloatRange(unit='Hz'), readonly=False, default=0)
|
||||||
voltage = Parameter('voltage', FloatRange(unit='V'), readonly=False, default=0)
|
voltage = Parameter('voltage', FloatRange(unit='V'), readonly=False, default=0)
|
||||||
loss = Parameter('loss', FloatRange(unit='deg'), default=0)
|
loss = Parameter('loss', FloatRange(unit='deg'), default=0)
|
||||||
|
|
||||||
iodevClass = Ah2700IO
|
ioClass = Ah2700IO
|
||||||
|
|
||||||
def parse_reply(self, reply):
|
def parse_reply(self, reply):
|
||||||
if reply.startswith('SI'): # this is an echo
|
if reply.startswith('SI'): # this is an echo
|
||||||
self.sendRecv('SERIAL ECHO OFF')
|
self.communicate('SERIAL ECHO OFF')
|
||||||
reply = self.sendRecv('SI')
|
reply = self.communicate('SI')
|
||||||
if not reply.startswith('F='): # this is probably an error message like "LOSS TOO HIGH"
|
if not reply.startswith('F='): # this is probably an error message like "LOSS TOO HIGH"
|
||||||
self.status = [self.Status.ERROR, reply]
|
self.status = [self.Status.ERROR, reply]
|
||||||
return
|
return
|
||||||
@ -59,32 +59,35 @@ class Capacitance(HasIodev, Readable):
|
|||||||
if lossunit == 'DS':
|
if lossunit == 'DS':
|
||||||
self.loss = loss
|
self.loss = loss
|
||||||
else: # the unit was wrong, we want DS = tan(delta), not NS = nanoSiemens
|
else: # the unit was wrong, we want DS = tan(delta), not NS = nanoSiemens
|
||||||
reply = self.sendRecv('UN DS').split() # UN DS returns a reply similar to SI
|
reply = self.communicate('UN DS').split() # UN DS returns a reply similar to SI
|
||||||
try:
|
try:
|
||||||
self.loss = reply[7]
|
self.loss = reply[7]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
pass # don't worry, loss will be updated next time
|
pass # don't worry, loss will be updated next time
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
self.parse_reply(self.sendRecv('SI')) # SI = single trigger
|
self.parse_reply(self.communicate('SI')) # SI = single trigger
|
||||||
return Done
|
return Done
|
||||||
|
|
||||||
|
@nopoll
|
||||||
def read_freq(self):
|
def read_freq(self):
|
||||||
self.read_value()
|
self.read_value()
|
||||||
return Done
|
return Done
|
||||||
|
|
||||||
|
@nopoll
|
||||||
def read_loss(self):
|
def read_loss(self):
|
||||||
self.read_value()
|
self.read_value()
|
||||||
return Done
|
return Done
|
||||||
|
|
||||||
def read_volt(self):
|
@nopoll
|
||||||
|
def read_voltage(self):
|
||||||
self.read_value()
|
self.read_value()
|
||||||
return Done
|
return Done
|
||||||
|
|
||||||
def write_freq(self, value):
|
def write_freq(self, value):
|
||||||
self.parse_reply(self.sendRecv('FR %g;SI' % value))
|
self.parse_reply(self.communicate('FR %g;SI' % value))
|
||||||
return Done
|
return Done
|
||||||
|
|
||||||
def write_volt(self, value):
|
def write_voltage(self, value):
|
||||||
self.parse_reply(self.sendRecv('V %g;SI' % value))
|
self.parse_reply(self.communicate('V %g;SI' % value))
|
||||||
return Done
|
return Done
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
"""drivers for CCU4, the cryostat control unit at SINQ"""
|
"""drivers for CCU4, the cryostat control unit at SINQ"""
|
||||||
# the most common Frappy classes can be imported from secop.core
|
# the most common Frappy classes can be imported from secop.core
|
||||||
from secop.core import EnumType, FloatRange, \
|
from secop.core import EnumType, FloatRange, \
|
||||||
HasIodev, Parameter, Readable, StringIO
|
HasIO, Parameter, Readable, StringIO
|
||||||
|
|
||||||
|
|
||||||
class CCU4IO(StringIO):
|
class CCU4IO(StringIO):
|
||||||
@ -34,14 +34,13 @@ class CCU4IO(StringIO):
|
|||||||
identification = [('cid', r'CCU4.*')]
|
identification = [('cid', r'CCU4.*')]
|
||||||
|
|
||||||
|
|
||||||
# inheriting the HasIodev mixin creates us a private attribute *_iodev*
|
# inheriting HasIO allows us to use the communicate method for talking with the hardware
|
||||||
# for talking with the hardware
|
|
||||||
# Readable as a base class defines the value and status parameters
|
# Readable as a base class defines the value and status parameters
|
||||||
class HeLevel(HasIodev, Readable):
|
class HeLevel(HasIO, Readable):
|
||||||
"""He Level channel of CCU4"""
|
"""He Level channel of CCU4"""
|
||||||
|
|
||||||
# define the communication class to create the IO module
|
# define the communication class to create the IO module
|
||||||
iodevClass = CCU4IO
|
ioClass = CCU4IO
|
||||||
|
|
||||||
# define or alter the parameters
|
# define or alter the parameters
|
||||||
# as Readable.value exists already, we give only the modified property 'unit'
|
# as Readable.value exists already, we give only the modified property 'unit'
|
||||||
@ -71,9 +70,9 @@ class HeLevel(HasIodev, Readable):
|
|||||||
for changing a parameter
|
for changing a parameter
|
||||||
:returns: the (new) value of the parameter
|
:returns: the (new) value of the parameter
|
||||||
"""
|
"""
|
||||||
name, txtvalue = self._iodev.communicate(cmd).split('=')
|
name, txtvalue = self.communicate(cmd).split('=')
|
||||||
assert name == cmd.split('=')[0] # check that we got a reply to our command
|
assert name == cmd.split('=')[0] # check that we got a reply to our command
|
||||||
return txtvalue # Frappy will automatically convert the string to the needed data type
|
return float(txtvalue)
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
return self.query('h')
|
return self.query('h')
|
||||||
|
159
secop_psi/convergence.py
Normal file
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 = []
|
result = []
|
||||||
for subkey, elmtype in items:
|
for subkey, elmtype in items:
|
||||||
for fun, tail_, opts in make_cvt_list(elmtype, '%s.%s' % (tail, subkey)):
|
for fun, tail_, opts in make_cvt_list(elmtype, '%s.%s' % (tail, subkey)):
|
||||||
def conv(value, key=subkey, func=fun):
|
result.append((lambda v, k=subkey, f=fun: f(v[k]), tail_, opts))
|
||||||
try:
|
|
||||||
return value[key]
|
|
||||||
except KeyError: # can not use value.get() because value might be a list
|
|
||||||
return None
|
|
||||||
result.append((conv, tail_, opts))
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@ -70,11 +65,11 @@ class FrappyHistoryWriter(frappyhistory.FrappyWriter):
|
|||||||
- period:
|
- period:
|
||||||
the typical 'lifetime' of a value.
|
the typical 'lifetime' of a value.
|
||||||
The intention is, that points in a chart may be connected by a straight line
|
The intention is, that points in a chart may be connected by a straight line
|
||||||
when the distance is lower than twice this value. If not, the line should be
|
when the distance is lower than this value. If not, the line should be drawn
|
||||||
drawn horizontally from the last point to a point <period> before the next value.
|
horizontally from the last point to a point <period> before the next value.
|
||||||
For example a setpoint should have period 0, which will lead to a stepped
|
For example a setpoint should have period 0, which will lead to a stepped
|
||||||
line, whereas for a measured value like a temperature, period should be
|
line, whereas for a measured value like a temperature, period should be
|
||||||
equal to the poll interval. In order to make full use of this,
|
slightly bigger than the poll interval. In order to make full use of this,
|
||||||
we would need some additional parameter property.
|
we would need some additional parameter property.
|
||||||
- show: True/False, whether this curve should be shown or not by default in
|
- show: True/False, whether this curve should be shown or not by default in
|
||||||
a summary chart
|
a summary chart
|
||||||
|
@ -18,12 +18,18 @@
|
|||||||
# Module authors:
|
# Module authors:
|
||||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
"""Keithley 2601B source meter
|
"""Keithley 2601B 4 quadrant source meter
|
||||||
|
|
||||||
not tested yet"""
|
not tested yet
|
||||||
|
|
||||||
from secop.core import Attached, BoolType, EnumType, FloatRange, \
|
* switching between voltage and current happens by setting their target
|
||||||
HasIodev, Module, Parameter, StringIO, Writable, Done
|
* switching output off by setting the active parameter of the controlling
|
||||||
|
module to False.
|
||||||
|
* setting the active parameter to True raises an error
|
||||||
|
"""
|
||||||
|
|
||||||
|
from secop.core import Attached, BoolType, Done, EnumType, FloatRange, \
|
||||||
|
HasIO, Module, Parameter, Readable, StringIO, Writable
|
||||||
|
|
||||||
|
|
||||||
class K2601bIO(StringIO):
|
class K2601bIO(StringIO):
|
||||||
@ -32,128 +38,162 @@ class K2601bIO(StringIO):
|
|||||||
|
|
||||||
SOURCECMDS = {
|
SOURCECMDS = {
|
||||||
0: 'reset()'
|
0: 'reset()'
|
||||||
|
' smua.source.output = 0 print("ok")',
|
||||||
|
1: 'reset()'
|
||||||
' smua.source.func = smua.OUTPUT_DCAMPS'
|
' smua.source.func = smua.OUTPUT_DCAMPS'
|
||||||
' display.smua.measure.func = display.MEASURE_VOLTS'
|
' display.smua.measure.func = display.MEASURE_VOLTS'
|
||||||
' smua.source.autorangei = 1'
|
' smua.source.autorangei = 1'
|
||||||
' smua.source.output = %d print("ok"")',
|
' smua.source.output = 1 print("ok")',
|
||||||
1: 'reset()'
|
2: 'reset()'
|
||||||
' smua.source.func = smua.OUTPUT_DCVOLTS'
|
' smua.source.func = smua.OUTPUT_DCVOLTS'
|
||||||
' display.smua.measure.func = display.MEASURE_DCAMPS'
|
' display.smua.measure.func = display.MEASURE_DCAMPS'
|
||||||
' smua.source.autorangev = 1'
|
' smua.source.autorangev = 1'
|
||||||
' smua.source.output = %d print("ok"")',
|
' smua.source.output = 1 print("ok")',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class SourceMeter(HasIodev, Module):
|
class SourceMeter(HasIO, Module):
|
||||||
|
export = False # export for tests only
|
||||||
|
mode = Parameter('measurement mode', EnumType(off=0, current=1, voltage=2),
|
||||||
|
readonly=False, export=False)
|
||||||
|
ilimit = Parameter('current limit', FloatRange(0, 2.0, unit='A'), default=2)
|
||||||
|
vlimit = Parameter('voltage limit', FloatRange(0, 2.0, unit='V'), default=2)
|
||||||
|
|
||||||
resistivity = Parameter('readback resistivity', FloatRange(unit='Ohm'), poll=True)
|
ioClass = K2601bIO
|
||||||
power = Parameter('readback power', FloatRange(unit='W'), poll=True)
|
|
||||||
mode = Parameter('measurement mode', EnumType(current_off=0, voltage_off=1, current_on=2, voltage_on=3),
|
|
||||||
readonly=False, poll=True)
|
|
||||||
|
|
||||||
iodevClass = K2601bIO
|
|
||||||
|
|
||||||
def read_resistivity(self):
|
|
||||||
return self.sendRecv('print(smua.measure.r())')
|
|
||||||
|
|
||||||
def read_power(self):
|
|
||||||
return self.sendRecv('print(smua.measure.p())')
|
|
||||||
|
|
||||||
def read_mode(self):
|
def read_mode(self):
|
||||||
return float(self.sendRecv('print(smua.source.func+2*smua.source.output)'))
|
return float(self.communicate('print((smua.source.func+1)*smua.source.output)'))
|
||||||
|
|
||||||
def write_mode(self, value):
|
def write_mode(self, value):
|
||||||
assert 'ok' == self.sendRecv(SOURCECMDS[value % 2] % (value >= 2))
|
if value == 'current':
|
||||||
|
self.write_vlimit(self.vlimit)
|
||||||
|
elif value == 'voltage':
|
||||||
|
self.write_ilimit(self.ilimit)
|
||||||
|
assert self.communicate(SOURCECMDS[value]) == 'ok'
|
||||||
return self.read_mode()
|
return self.read_mode()
|
||||||
|
|
||||||
|
def read_ilimit(self):
|
||||||
|
if self.mode == 'current':
|
||||||
|
return self.ilimit
|
||||||
|
return float(self.communicate('print(smua.source.limiti)'))
|
||||||
|
|
||||||
class Current(HasIodev, Writable):
|
def write_ilimit(self, value):
|
||||||
sourcemeter = Attached()
|
if self.mode == 'current':
|
||||||
|
return self.ilimit
|
||||||
|
return float(self.communicate('smua.source.limiti = %g print(smua.source.limiti)' % value))
|
||||||
|
|
||||||
value = Parameter('measured current', FloatRange(unit='A'), poll=True)
|
def read_vlimit(self):
|
||||||
target = Parameter('set current', FloatRange(unit='A'), poll=True)
|
if self.mode == 'voltage':
|
||||||
active = Parameter('current is controlled', BoolType(), default=False) # polled by SourceMeter
|
return self.ilimit
|
||||||
limit = Parameter('current limit', FloatRange(0, 2.0, unit='A'), default=2, poll=True)
|
return float(self.communicate('print(smua.source.limitv)'))
|
||||||
|
|
||||||
def initModule(self):
|
def write_vlimit(self, value):
|
||||||
self._sourcemeter.registerCallbacks(self)
|
if self.mode == 'voltage':
|
||||||
|
return self.ilimit
|
||||||
|
return float(self.communicate('smua.source.limitv = %g print(smua.source.limitv)' % value))
|
||||||
|
|
||||||
|
|
||||||
|
class Power(HasIO, Readable):
|
||||||
|
value = Parameter('readback power', FloatRange(unit='W'))
|
||||||
|
ioClass = K2601bIO
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
return self.sendRecv('print(smua.measure.i())')
|
return float(self.communicate('print(smua.measure.p())'))
|
||||||
|
|
||||||
|
|
||||||
|
class Resistivity(HasIO, Readable):
|
||||||
|
value = Parameter('readback resistivity', FloatRange(unit='Ohm'))
|
||||||
|
ioClass = K2601bIO
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
return float(self.communicate('print(smua.measure.r())'))
|
||||||
|
|
||||||
|
|
||||||
|
class Current(HasIO, Writable):
|
||||||
|
sourcemeter = Attached()
|
||||||
|
|
||||||
|
value = Parameter('measured current', FloatRange(unit='A'))
|
||||||
|
target = Parameter('set current', FloatRange(unit='A'))
|
||||||
|
active = Parameter('current is controlled', BoolType(), default=False)
|
||||||
|
limit = Parameter('current limit', FloatRange(0, 2.0, unit='A'), default=2)
|
||||||
|
|
||||||
|
def initModule(self):
|
||||||
|
self.sourcemeter.registerCallbacks(self)
|
||||||
|
|
||||||
|
def read_value(self):
|
||||||
|
return float(self.communicate('print(smua.measure.i())'))
|
||||||
|
|
||||||
def read_target(self):
|
def read_target(self):
|
||||||
return self.sendRecv('print(smua.source.leveli)')
|
return float(self.communicate('print(smua.source.leveli)'))
|
||||||
|
|
||||||
def write_target(self, value):
|
def write_target(self, value):
|
||||||
if not self.active:
|
if value > self.sourcemeter.ilimit:
|
||||||
raise ValueError('current source is disabled')
|
|
||||||
if value > self.limit:
|
|
||||||
raise ValueError('current exceeds limit')
|
raise ValueError('current exceeds limit')
|
||||||
return self.sendRecv('smua.source.leveli = %g print(smua.source.leveli)' % value)
|
value = float(self.communicate('smua.source.leveli = %g print(smua.source.leveli)' % value))
|
||||||
|
if not self.active:
|
||||||
|
self.sourcemeter.write_mode('current') # triggers update_mode -> set active to True
|
||||||
|
return value
|
||||||
|
|
||||||
def read_limit(self):
|
def read_limit(self):
|
||||||
if self.active:
|
return self.sourcemeter.read_ilimit()
|
||||||
return self.limit
|
|
||||||
return self.sendRecv('print(smua.source.limiti)')
|
|
||||||
|
|
||||||
def write_limit(self, value):
|
def write_limit(self, value):
|
||||||
if self.active:
|
return self.sourcemeter.write_ilimit(value)
|
||||||
return value
|
|
||||||
return self.sendRecv('smua.source.limiti = %g print(smua.source.limiti)' % value)
|
|
||||||
|
|
||||||
def write_active(self, value):
|
|
||||||
if value:
|
|
||||||
self._sourcemeter.write_mode('current_on')
|
|
||||||
elif self._sourcemeter.mode == 'current_on':
|
|
||||||
self._sourcemeter.write_mode('current_off')
|
|
||||||
return self.active
|
|
||||||
|
|
||||||
def update_mode(self, mode):
|
def update_mode(self, mode):
|
||||||
# will be called whenever the attached sourcemeters mode changes
|
# will be called whenever the attached sourcemeters mode changes
|
||||||
self.active = mode == 'current_on'
|
self.active = mode == 'current'
|
||||||
|
|
||||||
|
def write_active(self, value):
|
||||||
|
self.sourcemeter.read_mode()
|
||||||
|
if value == self.value:
|
||||||
|
return Done
|
||||||
|
if value:
|
||||||
|
raise ValueError('activate only by setting target')
|
||||||
|
self.sourcemeter.write_mode('off') # triggers update_mode -> set active to False
|
||||||
|
return Done
|
||||||
|
|
||||||
|
|
||||||
class Voltage(HasIodev, Writable):
|
class Voltage(HasIO, Writable):
|
||||||
sourcemeter = Attached()
|
sourcemeter = Attached()
|
||||||
|
|
||||||
value = Parameter('measured voltage', FloatRange(unit='V'), poll=True)
|
value = Parameter('measured voltage', FloatRange(unit='V'))
|
||||||
target = Parameter('set voltage', FloatRange(unit='V'), poll=True)
|
target = Parameter('set voltage', FloatRange(unit='V'))
|
||||||
active = Parameter('voltage is controlled', BoolType(), default=False) # polled by SourceMeter
|
active = Parameter('voltage is controlled', BoolType())
|
||||||
limit = Parameter('current limit', FloatRange(0, 2.0, unit='V'), default=2, poll=True)
|
limit = Parameter('voltage limit', FloatRange(0, 2.0, unit='V'), default=2)
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
self._sourcemeter.registerCallbacks(self)
|
self.sourcemeter.registerCallbacks(self)
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
return self.sendRecv('print(smua.measure.v())')
|
return float(self.communicate('print(smua.measure.v())'))
|
||||||
|
|
||||||
def read_target(self):
|
def read_target(self):
|
||||||
return self.sendRecv('print(smua.source.levelv)')
|
return float(self.communicate('print(smua.source.levelv)'))
|
||||||
|
|
||||||
def write_target(self, value):
|
def write_target(self, value):
|
||||||
if not self.active:
|
if value > self.sourcemeter.vlimit:
|
||||||
raise ValueError('voltage source is disabled')
|
|
||||||
if value > self.limit:
|
|
||||||
raise ValueError('voltage exceeds limit')
|
raise ValueError('voltage exceeds limit')
|
||||||
return self.sendRecv('smua.source.levelv = %g print(smua.source.levelv)' % value)
|
value = float(self.communicate('smua.source.levelv = %g print(smua.source.levelv)' % value))
|
||||||
|
if not self.active:
|
||||||
|
self.sourcemeter.write_mode('voltage') # triggers update_mode -> set active to True
|
||||||
|
return value
|
||||||
|
|
||||||
def read_limit(self):
|
def read_limit(self):
|
||||||
if self.active:
|
return self.sourcemeter.read_vlimit()
|
||||||
return self.limit
|
|
||||||
return self.sendRecv('print(smua.source.limitv)')
|
|
||||||
|
|
||||||
def write_limit(self, value):
|
def write_limit(self, value):
|
||||||
if self.active:
|
return self.sourcemeter.write_vlimit(value)
|
||||||
return value
|
|
||||||
return self.sendRecv('smua.source.limitv = %g print(smua.source.limitv)' % value)
|
|
||||||
|
|
||||||
def write_active(self, value):
|
|
||||||
if value:
|
|
||||||
self._sourcemeter.write_mode('voltage_on')
|
|
||||||
elif self._sourcemeter.mode == 'voltage_on':
|
|
||||||
self._sourcemeter.write_mode('voltage_off')
|
|
||||||
return self.active
|
|
||||||
|
|
||||||
def update_mode(self, mode):
|
def update_mode(self, mode):
|
||||||
# will be called whenever the attached sourcemeters mode changes
|
# will be called whenever the attached sourcemeters mode changes
|
||||||
self.active = mode == 'voltage_on'
|
self.active = mode == 'voltage'
|
||||||
|
|
||||||
|
def write_active(self, value):
|
||||||
|
self.sourcemeter.read_mode()
|
||||||
|
if value == self.value:
|
||||||
|
return Done
|
||||||
|
if value:
|
||||||
|
raise ValueError('activate only by setting target')
|
||||||
|
self.sourcemeter.write_mode('off') # triggers update_mode -> set active to False
|
||||||
|
return Done
|
||||||
|
@ -27,8 +27,7 @@ from secop.datatypes import BoolType, EnumType, FloatRange, IntRange
|
|||||||
from secop.lib import formatStatusBits
|
from secop.lib import formatStatusBits
|
||||||
from secop.modules import Attached, Done, \
|
from secop.modules import Attached, Done, \
|
||||||
Drivable, Parameter, Property, Readable
|
Drivable, Parameter, Property, Readable
|
||||||
from secop.poller import REGULAR, Poller
|
from secop.io import HasIO
|
||||||
from secop.io import HasIodev
|
|
||||||
|
|
||||||
Status = Drivable.Status
|
Status = Drivable.Status
|
||||||
|
|
||||||
@ -58,29 +57,29 @@ class StringIO(secop.io.StringIO):
|
|||||||
wait_before = 0.05
|
wait_before = 0.05
|
||||||
|
|
||||||
|
|
||||||
class Main(HasIodev, Drivable):
|
class Main(HasIO, Drivable):
|
||||||
|
|
||||||
value = Parameter('the current channel', poll=REGULAR, datatype=IntRange(0, 17))
|
value = Parameter('the current channel', datatype=IntRange(0, 17))
|
||||||
target = Parameter('channel to select', datatype=IntRange(0, 17))
|
target = Parameter('channel to select', datatype=IntRange(0, 17))
|
||||||
autoscan = Parameter('whether to scan automatically', datatype=BoolType(), readonly=False, default=False)
|
autoscan = Parameter('whether to scan automatically', datatype=BoolType(), readonly=False, default=False)
|
||||||
pollinterval = Parameter(default=1, export=False)
|
pollinterval = Parameter(default=1, export=False)
|
||||||
|
|
||||||
pollerClass = Poller
|
ioClass = StringIO
|
||||||
iodevClass = StringIO
|
|
||||||
_channel_changed = 0 # time of last channel change
|
_channel_changed = 0 # time of last channel change
|
||||||
_channels = None # dict <channel no> of <module object>
|
_channels = None # dict <channel no> of <module object>
|
||||||
|
|
||||||
def earlyInit(self):
|
def earlyInit(self):
|
||||||
|
super().earlyInit()
|
||||||
self._channels = {}
|
self._channels = {}
|
||||||
|
|
||||||
def register_channel(self, modobj):
|
def register_channel(self, modobj):
|
||||||
self._channels[modobj.channel] = modobj
|
self._channels[modobj.channel] = modobj
|
||||||
|
|
||||||
def startModule(self, started_callback):
|
def startModule(self, start_events):
|
||||||
started_callback()
|
super().startModule(start_events)
|
||||||
for ch in range(1, 16):
|
for ch in range(1, 16):
|
||||||
if ch not in self._channels:
|
if ch not in self._channels:
|
||||||
self.sendRecv('INSET %d,0,0,0,0,0;INSET?%d' % (ch, ch))
|
self.communicate('INSET %d,0,0,0,0,0;INSET?%d' % (ch, ch))
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
channel, auto = scan.send_command(self)
|
channel, auto = scan.send_command(self)
|
||||||
@ -113,7 +112,7 @@ class Main(HasIodev, Drivable):
|
|||||||
|
|
||||||
def write_target(self, channel):
|
def write_target(self, channel):
|
||||||
scan.send_change(self, channel, self.autoscan)
|
scan.send_change(self, channel, self.autoscan)
|
||||||
# self.sendRecv('SCAN %d,%d;SCAN?' % (channel, self.autoscan))
|
# self.communicate('SCAN %d,%d;SCAN?' % (channel, self.autoscan))
|
||||||
if channel != self.value:
|
if channel != self.value:
|
||||||
self.value = 0
|
self.value = 0
|
||||||
self._channel_changed = time.time()
|
self._channel_changed = time.time()
|
||||||
@ -122,11 +121,11 @@ class Main(HasIodev, Drivable):
|
|||||||
|
|
||||||
def write_autoscan(self, value):
|
def write_autoscan(self, value):
|
||||||
scan.send_change(self, self.value, value)
|
scan.send_change(self, self.value, value)
|
||||||
# self.sendRecv('SCAN %d,%d;SCAN?' % (channel, self.autoscan))
|
# self.communicate('SCAN %d,%d;SCAN?' % (channel, self.autoscan))
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
class ResChannel(HasIodev, Readable):
|
class ResChannel(HasIO, Readable):
|
||||||
"""temperature channel on Lakeshore 336"""
|
"""temperature channel on Lakeshore 336"""
|
||||||
|
|
||||||
RES_RANGE = {key: i+1 for i, key in list(
|
RES_RANGE = {key: i+1 for i, key in list(
|
||||||
@ -140,8 +139,7 @@ class ResChannel(HasIodev, Readable):
|
|||||||
enumerate(mag % val for mag in ['%guV', '%gmV']
|
enumerate(mag % val for mag in ['%guV', '%gmV']
|
||||||
for val in [2, 6.32, 20, 63.2, 200, 632]))}
|
for val in [2, 6.32, 20, 63.2, 200, 632]))}
|
||||||
|
|
||||||
pollerClass = Poller
|
ioClass = StringIO
|
||||||
iodevClass = StringIO
|
|
||||||
_main = None # main module
|
_main = None # main module
|
||||||
_last_range_change = 0 # time of last range change
|
_last_range_change = 0 # time of last range change
|
||||||
|
|
||||||
@ -166,6 +164,7 @@ class ResChannel(HasIodev, Readable):
|
|||||||
_trigger_read = False
|
_trigger_read = False
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
|
super().initModule()
|
||||||
self._main = self.DISPATCHER.get_module(self.main)
|
self._main = self.DISPATCHER.get_module(self.main)
|
||||||
self._main.register_channel(self)
|
self._main.register_channel(self)
|
||||||
|
|
||||||
@ -181,7 +180,7 @@ class ResChannel(HasIodev, Readable):
|
|||||||
return Done
|
return Done
|
||||||
# we got here, when we missed the idle state of self._main
|
# we got here, when we missed the idle state of self._main
|
||||||
self._trigger_read = False
|
self._trigger_read = False
|
||||||
result = self.sendRecv('RDGR?%d' % self.channel)
|
result = self.communicate('RDGR?%d' % self.channel)
|
||||||
result = float(result)
|
result = float(result)
|
||||||
if self.autorange == 'soft':
|
if self.autorange == 'soft':
|
||||||
now = time.time()
|
now = time.time()
|
||||||
@ -214,9 +213,9 @@ class ResChannel(HasIodev, Readable):
|
|||||||
def read_status(self):
|
def read_status(self):
|
||||||
if not self.enabled:
|
if not self.enabled:
|
||||||
return [self.Status.DISABLED, 'disabled']
|
return [self.Status.DISABLED, 'disabled']
|
||||||
if self.channel != self._main.value:
|
if self.channel != self.main.value:
|
||||||
return Done
|
return Done
|
||||||
result = int(self.sendRecv('RDGST?%d' % self.channel))
|
result = int(self.communicate('RDGST?%d' % self.channel))
|
||||||
result &= 0x37 # mask T_OVER and T_UNDER (change this when implementing temperatures instead of resistivities)
|
result &= 0x37 # mask T_OVER and T_UNDER (change this when implementing temperatures instead of resistivities)
|
||||||
statustext = ' '.join(formatStatusBits(result, STATUS_BIT_LABELS))
|
statustext = ' '.join(formatStatusBits(result, STATUS_BIT_LABELS))
|
||||||
if statustext:
|
if statustext:
|
||||||
@ -228,7 +227,7 @@ class ResChannel(HasIodev, Readable):
|
|||||||
if autorange:
|
if autorange:
|
||||||
result['autorange'] = 'hard'
|
result['autorange'] = 'hard'
|
||||||
# else: do not change autorange
|
# else: do not change autorange
|
||||||
# self.log.info('%s range %r %r %r' % (self.name, rng, autorange, self.autorange))
|
self.log.debug('%s range %r %r %r' % (self.name, rng, autorange, self.autorange))
|
||||||
if excoff:
|
if excoff:
|
||||||
result.update(iexc=0, vexc=0)
|
result.update(iexc=0, vexc=0)
|
||||||
elif iscur:
|
elif iscur:
|
||||||
@ -281,5 +280,5 @@ class ResChannel(HasIodev, Readable):
|
|||||||
def write_enabled(self, value):
|
def write_enabled(self, value):
|
||||||
inset.write(self, 'enabled', value)
|
inset.write(self, 'enabled', value)
|
||||||
if value:
|
if value:
|
||||||
self._main.write_target(self.channel)
|
self.main.write_target(self.channel)
|
||||||
return Done
|
return Done
|
||||||
|
@ -28,22 +28,23 @@ class Ls370Sim(Communicator):
|
|||||||
('RDGR?%d', '1.0'),
|
('RDGR?%d', '1.0'),
|
||||||
('RDGST?%d', '0'),
|
('RDGST?%d', '0'),
|
||||||
('RDGRNG?%d', '0,5,5,0,0'),
|
('RDGRNG?%d', '0,5,5,0,0'),
|
||||||
('INSET?%d', '1,3,3,0,0'),
|
('INSET?%d', '1,5,5,0,0'),
|
||||||
('FILTER?%d', '1,1,80'),
|
('FILTER?%d', '1,5,80'),
|
||||||
]
|
]
|
||||||
OTHER_COMMANDS = [
|
OTHER_COMMANDS = [
|
||||||
('*IDN?', 'LSCI,MODEL370,370184,05302003'),
|
('*IDN?', 'LSCI,MODEL370,370184,05302003'),
|
||||||
('SCAN?', '3,0'),
|
('SCAN?', '3,1'),
|
||||||
]
|
]
|
||||||
|
|
||||||
def earlyInit(self):
|
def earlyInit(self):
|
||||||
|
super().earlyInit()
|
||||||
self._data = dict(self.OTHER_COMMANDS)
|
self._data = dict(self.OTHER_COMMANDS)
|
||||||
for fmt, v in self.CHANNEL_COMMANDS:
|
for fmt, v in self.CHANNEL_COMMANDS:
|
||||||
for chan in range(1,17):
|
for chan in range(1,17):
|
||||||
self._data[fmt % chan] = v
|
self._data[fmt % chan] = v
|
||||||
# mkthread(self.run)
|
|
||||||
|
|
||||||
def communicate(self, command):
|
def communicate(self, command):
|
||||||
|
self.comLog('> %s' % command)
|
||||||
# simulation part, time independent
|
# simulation part, time independent
|
||||||
for channel in range(1,17):
|
for channel in range(1,17):
|
||||||
_, _, _, _, excoff = self._data['RDGRNG?%d' % channel].split(',')
|
_, _, _, _, excoff = self._data['RDGRNG?%d' % channel].split(',')
|
||||||
@ -68,6 +69,6 @@ class Ls370Sim(Communicator):
|
|||||||
if qcmd in self._data:
|
if qcmd in self._data:
|
||||||
self._data[qcmd] = arg
|
self._data[qcmd] = arg
|
||||||
break
|
break
|
||||||
#if command.startswith('R'):
|
reply = ';'.join(reply)
|
||||||
# print('> %s\t< %s' % (command, reply))
|
self.comLog('< %s' % reply)
|
||||||
return ';'.join(reply)
|
return reply
|
||||||
|
@ -27,8 +27,9 @@ import time
|
|||||||
|
|
||||||
from secop.core import Drivable, HasIodev, \
|
from secop.core import Drivable, HasIodev, \
|
||||||
Parameter, Property, Readable, StringIO
|
Parameter, Property, Readable, StringIO
|
||||||
from secop.datatypes import EnumType, FloatRange, StringType
|
from secop.datatypes import EnumType, FloatRange, StringType, StructOf
|
||||||
from secop.errors import HardwareError
|
from secop.errors import HardwareError
|
||||||
|
from secop.lib.statemachine import StateMachine
|
||||||
|
|
||||||
|
|
||||||
class MercuryIO(StringIO):
|
class MercuryIO(StringIO):
|
||||||
@ -120,7 +121,7 @@ class HasProgressCheck:
|
|||||||
changing tolerance.
|
changing tolerance.
|
||||||
"""
|
"""
|
||||||
tolerance = Parameter('absolute tolerance', FloatRange(0), readonly=False, default=0)
|
tolerance = Parameter('absolute tolerance', FloatRange(0), readonly=False, default=0)
|
||||||
relative_tolerance = Parameter('_', FloatRange(0, 1), readonly=False, default=0)
|
min_slope = Parameter('minimal abs(slope)', FloatRange(0), readonly=False, default=0)
|
||||||
settling_time = Parameter(
|
settling_time = Parameter(
|
||||||
'''settling time
|
'''settling time
|
||||||
|
|
||||||
@ -130,75 +131,76 @@ class HasProgressCheck:
|
|||||||
'''timeout
|
'''timeout
|
||||||
|
|
||||||
timeout = 0: disabled, else:
|
timeout = 0: disabled, else:
|
||||||
A timeout happens, when the difference value - target is not improved by more than
|
A timeout event happens, when the difference (target - value) is not improved by
|
||||||
a factor 2 within timeout.
|
at least min_slope * timeout over any interval (t, t + timeout).
|
||||||
|
As soon as the value is the first time within tolerance, the criterium is changed:
|
||||||
More precisely, we expect a convergence curve which decreases the difference
|
then the timeout event happens after this time + settling_time + timeout.
|
||||||
by a factor 2 within timeout/2.
|
|
||||||
If this expected progress is delayed by more than timeout/2, a timeout happens.
|
|
||||||
If the convergence is better than above, the expected curve is adjusted continuously.
|
|
||||||
In case the tolerance is reached once, a timeout happens when the time after this is
|
|
||||||
exceeded by more than settling_time + timeout.
|
|
||||||
''', FloatRange(0, unit='sec'), readonly=False, default=3600)
|
''', FloatRange(0, unit='sec'), readonly=False, default=3600)
|
||||||
status = Parameter('status determined from progress check')
|
status = Parameter('status determined from progress check')
|
||||||
value = Parameter()
|
value = Parameter()
|
||||||
target = Parameter()
|
target = Parameter()
|
||||||
|
|
||||||
_settling_start = None # supposed start of settling time (0 when outside)
|
def earlyInit(self):
|
||||||
_first_inside = None # first time within tolerance
|
super().earlyInit()
|
||||||
_spent_inside = 0 # accumulated settling time
|
self.__state = StateMachine()
|
||||||
# the upper limit for t0, for the curve timeout_dif * 2 ** -(t - t0)/timeout not touching abs(value(t) - target)
|
|
||||||
_timeout_base = 0
|
|
||||||
_timeout_dif = 1
|
|
||||||
|
|
||||||
def check_progress(self, value, target):
|
def prepare_state(self, state):
|
||||||
"""called from read_status
|
tol = self.tolerance
|
||||||
|
if not tol:
|
||||||
|
tol = 0.01 * max(abs(self.target), abs(self.value))
|
||||||
|
dif = abs(self.target - self.value)
|
||||||
|
return dif, tol, state.now, state.delta(0)
|
||||||
|
|
||||||
intended to be also be used for alternative implementations of read_status
|
def state_approaching(self, state):
|
||||||
"""
|
if self.init():
|
||||||
base = max(abs(target), abs(value))
|
self.status = 'BUSY', 'approaching'
|
||||||
tol = base * self.relative_tolerance + self.tolerance
|
dif, tol, now, delta = self.prepare_state(state)
|
||||||
if tol == 0:
|
if dif < tol:
|
||||||
tol = max(0.01, base * 0.01)
|
state.timeout_base = now
|
||||||
now = time.time()
|
state.next_step(self.state_inside)
|
||||||
dif = abs(value - target)
|
return
|
||||||
if self._settling_start: # we were inside tol
|
if not self.timeout:
|
||||||
self._spent_inside = now - self._settling_start
|
return
|
||||||
if dif > tol: # transition inside -> outside
|
if state.init():
|
||||||
self._settling_start = None
|
state.timeout_base = now
|
||||||
else: # we were outside tol
|
state.dif_crit = dif
|
||||||
if dif <= tol: # transition outside -> inside
|
return
|
||||||
if not self._first_inside:
|
min_slope = getattr(self, 'ramp', 0) or getattr('min_slope', 0)
|
||||||
self._first_inside = now
|
state.dif_crit -= min_slope * delta
|
||||||
self._settling_start = now - self._spent_inside
|
if dif < state.dif_crit:
|
||||||
if self._spent_inside > self.settling_time:
|
state.timeout_base = now
|
||||||
return 'IDLE', ''
|
elif now > state.timeout_base:
|
||||||
result = 'BUSY', ('inside tolerance' if self._settling_start else 'outside tolerance')
|
self.status = 'WARNING', 'convergence timeout'
|
||||||
if self.timeout:
|
state.next_action(self.state_idle)
|
||||||
if self._first_inside:
|
|
||||||
if now > self._first_inside + self.settling_time + self.timeout:
|
|
||||||
return 'WARNING', 'settling timeout'
|
|
||||||
return result
|
|
||||||
tmo2 = self.timeout / 2
|
|
||||||
|
|
||||||
def exponential_convergence(t):
|
def state_inside(self, state):
|
||||||
return self._timeout_dif * 2 ** -(t - self._timeout_base) / tmo2
|
if state.init():
|
||||||
|
self.status = 'BUSY', 'inside tolerance'
|
||||||
|
dif, tol, now, delta = self.prepare_state(state)
|
||||||
|
if dif > tol:
|
||||||
|
state.next_action(self.state_outside)
|
||||||
|
state.spent_inside += delta
|
||||||
|
if state.spent_inside > self.settling_time:
|
||||||
|
self.status = 'IDLE', 'reached target'
|
||||||
|
state.next_action(self.state_idle)
|
||||||
|
|
||||||
if dif < exponential_convergence(now):
|
def state_outside(self, state, now, dif, tol, delta):
|
||||||
# convergence is better than estimated, update expected curve
|
if state.init():
|
||||||
self._timeout_dif = dif
|
self.status = 'BUSY', 'outside tolerance'
|
||||||
self._timeout_base = now
|
dif, tol, now, delta = self.prepare_state(state)
|
||||||
elif dif > exponential_convergence(now - tmo2):
|
if dif < tol:
|
||||||
return 'WARNING', 'convergence timeout'
|
state.next_action(self.state_inside)
|
||||||
return result
|
elif now > self.timeout_base + self.settling_time + self.timeout:
|
||||||
|
self.status = 'WARNING', 'settling timeout'
|
||||||
|
state.next_action(self.state_idle)
|
||||||
|
|
||||||
def reset_progress(self, value, target):
|
def start_state(self):
|
||||||
"""must be called from write_target, whenever the target changes"""
|
"""must be called from write_target, whenever the target changes"""
|
||||||
self._settling_start = None
|
self.__state.start(self.state_approach)
|
||||||
self._first_inside = None
|
|
||||||
self._spent_inside = 0
|
def poll(self):
|
||||||
self._timeout_base = time.time()
|
super().poll()
|
||||||
self._timeout_dif = abs(value - target)
|
self.__state.poll()
|
||||||
|
|
||||||
def read_status(self):
|
def read_status(self):
|
||||||
if self.status[0] == 'IDLE':
|
if self.status[0] == 'IDLE':
|
||||||
@ -213,13 +215,15 @@ class HasProgressCheck:
|
|||||||
class Loop(HasProgressCheck, MercuryChannel):
|
class Loop(HasProgressCheck, MercuryChannel):
|
||||||
"""common base class for loops"""
|
"""common base class for loops"""
|
||||||
mode = Parameter('control mode', EnumType(manual=0, pid=1), readonly=False)
|
mode = Parameter('control mode', EnumType(manual=0, pid=1), readonly=False)
|
||||||
prop = Parameter('pid proportional band', FloatRange(), readonly=False)
|
ctrlpars = Parameter(
|
||||||
integ = Parameter('pid integral parameter', FloatRange(unit='min'), readonly=False)
|
'pid (proportional nad, integral time, differential time',
|
||||||
deriv = Parameter('pid differential parameter', FloatRange(unit='min'), readonly=False)
|
StructOf(p=FloatRange(0, unit='$'), i=FloatRange(0, unit='min'), d=FloatRange(0, unit='min')),
|
||||||
|
readonly=False, poll=True
|
||||||
|
)
|
||||||
"""pid = Parameter('control parameters', StructOf(p=FloatRange(), i=FloatRange(), d=FloatRange()),readonly=False)"""
|
"""pid = Parameter('control parameters', StructOf(p=FloatRange(), i=FloatRange(), d=FloatRange()),readonly=False)"""
|
||||||
pid_table_mode = Parameter('', EnumType(off=0, on=1), readonly=False)
|
pid_table_mode = Parameter('', EnumType(off=0, on=1), readonly=False)
|
||||||
|
|
||||||
def read_prop(self):
|
def read_ctrlpars(self):
|
||||||
return self.query('0:LOOP:P')
|
return self.query('0:LOOP:P')
|
||||||
|
|
||||||
def read_integ(self):
|
def read_integ(self):
|
||||||
|
@ -148,6 +148,7 @@ class Motor(PersistentMixin, HasIodev, Drivable):
|
|||||||
|
|
||||||
@Command
|
@Command
|
||||||
def reset(self):
|
def reset(self):
|
||||||
|
"""reset error, set position to encoder"""
|
||||||
self.read_value()
|
self.read_value()
|
||||||
if self.status[0] == self.Status.ERROR:
|
if self.status[0] == self.Status.ERROR:
|
||||||
enc = self.encoder - self.zero
|
enc = self.encoder - self.zero
|
||||||
|
@ -33,17 +33,17 @@ Polling of value and status is done commonly for all modules. For each registere
|
|||||||
|
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
from ast import literal_eval # convert string as comma separated numbers into tuple
|
||||||
|
|
||||||
import secop.iohandler
|
|
||||||
from secop.datatypes import BoolType, EnumType, \
|
from secop.datatypes import BoolType, EnumType, \
|
||||||
FloatRange, IntRange, StatusType, StringType
|
FloatRange, IntRange, StatusType, StringType
|
||||||
from secop.errors import HardwareError
|
from secop.errors import HardwareError
|
||||||
from secop.lib import clamp
|
from secop.lib import clamp
|
||||||
from secop.lib.enum import Enum
|
from secop.lib.enum import Enum
|
||||||
from secop.modules import Attached, Communicator, Done, \
|
from secop.modules import Communicator, Done, \
|
||||||
Drivable, Parameter, Property, Readable
|
Drivable, Parameter, Property, Readable
|
||||||
from secop.poller import Poller
|
from secop.io import HasIO
|
||||||
from secop.io import HasIodev
|
from secop.rwhandler import CommonReadHandler, CommonWriteHandler
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import secop_psi.ppmswindows as ppmshw
|
import secop_psi.ppmswindows as ppmshw
|
||||||
@ -52,28 +52,11 @@ except ImportError:
|
|||||||
import secop_psi.ppmssim as ppmshw
|
import secop_psi.ppmssim as ppmshw
|
||||||
|
|
||||||
|
|
||||||
class IOHandler(secop.iohandler.IOHandler):
|
|
||||||
"""IO handler for PPMS commands
|
|
||||||
|
|
||||||
deals with typical format:
|
|
||||||
|
|
||||||
- query command: ``<command>?``
|
|
||||||
- reply: ``<value1>,<value2>, ..``
|
|
||||||
- change command: ``<command> <value1>,<value2>,...``
|
|
||||||
"""
|
|
||||||
CMDARGS = ['no'] # the channel number is needed in channel commands
|
|
||||||
CMDSEPARATOR = None # no command chaining
|
|
||||||
|
|
||||||
def __init__(self, name, querycmd, replyfmt):
|
|
||||||
changecmd = querycmd.split('?')[0] + ' '
|
|
||||||
super().__init__(name, querycmd, replyfmt, changecmd)
|
|
||||||
|
|
||||||
|
|
||||||
class Main(Communicator):
|
class Main(Communicator):
|
||||||
"""ppms communicator module"""
|
"""ppms communicator module"""
|
||||||
|
|
||||||
pollinterval = Parameter('poll interval', FloatRange(), readonly=False, default=2)
|
pollinterval = Parameter('poll interval', FloatRange(), readonly=False, default=2)
|
||||||
data = Parameter('internal', StringType(), poll=True, export=True, # export for test only
|
data = Parameter('internal', StringType(), export=True, # export for test only
|
||||||
default="", readonly=True)
|
default="", readonly=True)
|
||||||
|
|
||||||
class_id = Property('Quantum Design class id', StringType(), export=False)
|
class_id = Property('Quantum Design class id', StringType(), export=False)
|
||||||
@ -86,9 +69,8 @@ class Main(Communicator):
|
|||||||
_channel_to_index = dict(((channel, i) for i, channel in enumerate(_channel_names)))
|
_channel_to_index = dict(((channel, i) for i, channel in enumerate(_channel_names)))
|
||||||
_status_bitpos = {'temp': 0, 'field': 4, 'chamber': 8, 'position': 12}
|
_status_bitpos = {'temp': 0, 'field': 4, 'chamber': 8, 'position': 12}
|
||||||
|
|
||||||
pollerClass = Poller
|
|
||||||
|
|
||||||
def earlyInit(self):
|
def earlyInit(self):
|
||||||
|
super().earlyInit()
|
||||||
self.modules = {}
|
self.modules = {}
|
||||||
self._ppms_device = ppmshw.QDevice(self.class_id)
|
self._ppms_device = ppmshw.QDevice(self.class_id)
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
@ -99,10 +81,14 @@ class Main(Communicator):
|
|||||||
def communicate(self, command):
|
def communicate(self, command):
|
||||||
"""GPIB command"""
|
"""GPIB command"""
|
||||||
with self.lock:
|
with self.lock:
|
||||||
|
self.comLog('> %s' % command)
|
||||||
reply = self._ppms_device.send(command)
|
reply = self._ppms_device.send(command)
|
||||||
self.log.debug("%s|%s", command, reply)
|
self.comLog("< %s", reply)
|
||||||
return reply
|
return reply
|
||||||
|
|
||||||
|
def doPoll(self):
|
||||||
|
self.read_data()
|
||||||
|
|
||||||
def read_data(self):
|
def read_data(self):
|
||||||
mask = 1 # always get packed_status
|
mask = 1 # always get packed_status
|
||||||
for channelname, channel in self.modules.items():
|
for channelname, channel in self.modules.items():
|
||||||
@ -128,37 +114,27 @@ class Main(Communicator):
|
|||||||
return data # return data as string
|
return data # return data as string
|
||||||
|
|
||||||
|
|
||||||
class PpmsMixin:
|
class PpmsBase(HasIO, Readable):
|
||||||
"""common base for all ppms modules"""
|
"""common base for all ppms modules"""
|
||||||
|
value = Parameter(needscfg=False)
|
||||||
|
status = Parameter(needscfg=False)
|
||||||
|
|
||||||
iodev = Attached()
|
|
||||||
|
|
||||||
pollerClass = Poller
|
|
||||||
enabled = True # default, if no parameter enable is defined
|
enabled = True # default, if no parameter enable is defined
|
||||||
_last_settings = None # used by several modules
|
_last_settings = None # used by several modules
|
||||||
slow_pollfactor = 1
|
slow_pollfactor = 1
|
||||||
|
|
||||||
# as this pollinterval affects only the polling of settings
|
# as this pollinterval affects only the polling of settings
|
||||||
# it would be confusing to export it.
|
# it would be confusing to export it.
|
||||||
pollinterval = Parameter('', FloatRange(), needscfg=False, export=False)
|
pollinterval = Parameter(export=False)
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
self._iodev.register(self)
|
super().initModule()
|
||||||
|
self.io.register(self)
|
||||||
|
|
||||||
def startModule(self, started_callback):
|
def doPoll(self):
|
||||||
# no polls except on main module
|
|
||||||
started_callback()
|
|
||||||
|
|
||||||
def read_value(self):
|
|
||||||
# polling is done by the main module
|
# polling is done by the main module
|
||||||
# and PPMS does not deliver really more fresh values when polled more often
|
# and PPMS does not deliver really more fresh values when polled more often
|
||||||
return Done
|
pass
|
||||||
|
|
||||||
def read_status(self):
|
|
||||||
# polling is done by the main module
|
|
||||||
# and PPMS does not deliver really fresh status values anyway: the status is not
|
|
||||||
# changed immediately after a target change!
|
|
||||||
return Done
|
|
||||||
|
|
||||||
def update_value_status(self, value, packed_status):
|
def update_value_status(self, value, packed_status):
|
||||||
# update value and status
|
# update value and status
|
||||||
@ -172,12 +148,18 @@ class PpmsMixin:
|
|||||||
self.value = value
|
self.value = value
|
||||||
self.status = (self.Status.IDLE, '')
|
self.status = (self.Status.IDLE, '')
|
||||||
|
|
||||||
|
def comm_write(self, command):
|
||||||
|
"""write command and check if result is OK"""
|
||||||
|
reply = self.communicate(command)
|
||||||
|
if reply != 'OK':
|
||||||
|
raise HardwareError('bad reply %r to command %r' % (reply, command))
|
||||||
|
|
||||||
class Channel(PpmsMixin, HasIodev, Readable):
|
|
||||||
|
class Channel(PpmsBase):
|
||||||
"""channel base class"""
|
"""channel base class"""
|
||||||
|
|
||||||
value = Parameter('main value of channels', poll=True)
|
value = Parameter('main value of channels')
|
||||||
enabled = Parameter('is this channel used?', readonly=False, poll=False,
|
enabled = Parameter('is this channel used?', readonly=False,
|
||||||
datatype=BoolType(), default=False)
|
datatype=BoolType(), default=False)
|
||||||
|
|
||||||
channel = Property('channel name',
|
channel = Property('channel name',
|
||||||
@ -186,26 +168,21 @@ class Channel(PpmsMixin, HasIodev, Readable):
|
|||||||
datatype=IntRange(1, 4), export=False)
|
datatype=IntRange(1, 4), export=False)
|
||||||
|
|
||||||
def earlyInit(self):
|
def earlyInit(self):
|
||||||
Readable.earlyInit(self)
|
super().earlyInit()
|
||||||
if not self.channel:
|
if not self.channel:
|
||||||
self.channel = self.name
|
self.channel = self.name
|
||||||
|
|
||||||
def get_settings(self, pname):
|
|
||||||
return ''
|
|
||||||
|
|
||||||
|
|
||||||
class UserChannel(Channel):
|
class UserChannel(Channel):
|
||||||
"""user channel"""
|
"""user channel"""
|
||||||
|
|
||||||
# pollinterval = Parameter(visibility=3)
|
|
||||||
|
|
||||||
no = Property('channel number',
|
no = Property('channel number',
|
||||||
datatype=IntRange(0, 0), export=False, default=0)
|
datatype=IntRange(0, 0), export=False, default=0)
|
||||||
linkenable = Property('name of linked channel for enabling',
|
linkenable = Property('name of linked channel for enabling',
|
||||||
datatype=StringType(), export=False, default='')
|
datatype=StringType(), export=False, default='')
|
||||||
|
|
||||||
def write_enabled(self, enabled):
|
def write_enabled(self, enabled):
|
||||||
other = self._iodev.modules.get(self.linkenable, None)
|
other = self.io.modules.get(self.linkenable, None)
|
||||||
if other:
|
if other:
|
||||||
other.enabled = enabled
|
other.enabled = enabled
|
||||||
return enabled
|
return enabled
|
||||||
@ -214,201 +191,172 @@ class UserChannel(Channel):
|
|||||||
class DriverChannel(Channel):
|
class DriverChannel(Channel):
|
||||||
"""driver channel"""
|
"""driver channel"""
|
||||||
|
|
||||||
drvout = IOHandler('drvout', 'DRVOUT? %(no)d', '%d,%g,%g')
|
current = Parameter('driver current', readonly=False,
|
||||||
|
|
||||||
current = Parameter('driver current', readonly=False, handler=drvout,
|
|
||||||
datatype=FloatRange(0., 5000., unit='uA'))
|
datatype=FloatRange(0., 5000., unit='uA'))
|
||||||
powerlimit = Parameter('power limit', readonly=False, handler=drvout,
|
powerlimit = Parameter('power limit', readonly=False,
|
||||||
datatype=FloatRange(0., 1000., unit='uW'))
|
datatype=FloatRange(0., 1000., unit='uW'))
|
||||||
# pollinterval = Parameter(visibility=3)
|
|
||||||
|
|
||||||
def analyze_drvout(self, no, current, powerlimit):
|
param_names = 'current', 'powerlimit'
|
||||||
|
|
||||||
|
@CommonReadHandler(param_names)
|
||||||
|
def read_params(self):
|
||||||
|
no, self.current, self.powerlimit = literal_eval(
|
||||||
|
self.communicate('DRVOUT? %d' % self.no))
|
||||||
if self.no != no:
|
if self.no != no:
|
||||||
raise HardwareError('DRVOUT command: channel number in reply does not match')
|
raise HardwareError('DRVOUT command: channel number in reply does not match')
|
||||||
return dict(current=current, powerlimit=powerlimit)
|
|
||||||
|
|
||||||
def change_drvout(self, change):
|
@CommonWriteHandler(param_names)
|
||||||
change.readValues()
|
def write_params(self, values):
|
||||||
return change.current, change.powerlimit
|
"""write parameters
|
||||||
|
|
||||||
|
:param values: a dict like object containing the parameters to be written
|
||||||
|
"""
|
||||||
|
self.read_params() # make sure parameters are up to date
|
||||||
|
self.comm_write('DRVOUT %(no)d,%(current)g,%(powerlimit)g' % values)
|
||||||
|
self.read_params() # read back
|
||||||
|
|
||||||
|
|
||||||
class BridgeChannel(Channel):
|
class BridgeChannel(Channel):
|
||||||
"""bridge channel"""
|
"""bridge channel"""
|
||||||
|
|
||||||
bridge = IOHandler('bridge', 'BRIDGE? %(no)d', '%d,%g,%g,%d,%d,%g')
|
excitation = Parameter('excitation current', readonly=False,
|
||||||
# pylint: disable=invalid-name
|
|
||||||
ReadingMode = Enum('ReadingMode', standard=0, fast=1, highres=2)
|
|
||||||
|
|
||||||
enabled = Parameter(handler=bridge)
|
|
||||||
excitation = Parameter('excitation current', readonly=False, handler=bridge,
|
|
||||||
datatype=FloatRange(0.01, 5000., unit='uA'))
|
datatype=FloatRange(0.01, 5000., unit='uA'))
|
||||||
powerlimit = Parameter('power limit', readonly=False, handler=bridge,
|
powerlimit = Parameter('power limit', readonly=False,
|
||||||
datatype=FloatRange(0.001, 1000., unit='uW'))
|
datatype=FloatRange(0.001, 1000., unit='uW'))
|
||||||
dcflag = Parameter('True when excitation is DC (else AC)', readonly=False, handler=bridge,
|
dcflag = Parameter('True when excitation is DC (else AC)', readonly=False,
|
||||||
datatype=BoolType())
|
datatype=BoolType())
|
||||||
readingmode = Parameter('reading mode', readonly=False, handler=bridge,
|
readingmode = Parameter('reading mode', readonly=False,
|
||||||
datatype=EnumType(ReadingMode))
|
datatype=EnumType(standard=0, fast=1, highres=2))
|
||||||
voltagelimit = Parameter('voltage limit', readonly=False, handler=bridge,
|
voltagelimit = Parameter('voltage limit', readonly=False,
|
||||||
datatype=FloatRange(0.0001, 100., unit='mV'))
|
datatype=FloatRange(0.0001, 100., unit='mV'))
|
||||||
# pollinterval = Parameter(visibility=3)
|
|
||||||
|
|
||||||
def analyze_bridge(self, no, excitation, powerlimit, dcflag, readingmode, voltagelimit):
|
param_names = 'enabled', 'enabled', 'powerlimit', 'dcflag', 'readingmode', 'voltagelimit'
|
||||||
|
|
||||||
|
@CommonReadHandler(param_names)
|
||||||
|
def read_params(self):
|
||||||
|
no, excitation, powerlimit, self.dcflag, self.readingmode, voltagelimit = literal_eval(
|
||||||
|
self.communicate('BRIDGE? %d' % self.no))
|
||||||
if self.no != no:
|
if self.no != no:
|
||||||
raise HardwareError('DRVOUT command: channel number in reply does not match')
|
raise HardwareError('DRVOUT command: channel number in reply does not match')
|
||||||
return dict(
|
self.enabled = excitation != 0 and powerlimit != 0 and voltagelimit != 0
|
||||||
enabled=excitation != 0 and powerlimit != 0 and voltagelimit != 0,
|
if excitation:
|
||||||
excitation=excitation or self.excitation,
|
self.excitation = excitation
|
||||||
powerlimit=powerlimit or self.powerlimit,
|
if powerlimit:
|
||||||
dcflag=dcflag,
|
self.powerlimit = powerlimit
|
||||||
readingmode=readingmode,
|
if voltagelimit:
|
||||||
voltagelimit=voltagelimit or self.voltagelimit,
|
self.voltagelimit = voltagelimit
|
||||||
)
|
|
||||||
|
|
||||||
def change_bridge(self, change):
|
@CommonWriteHandler(param_names)
|
||||||
change.readValues()
|
def write_params(self, values):
|
||||||
if change.enabled:
|
"""write parameters
|
||||||
return self.no, change.excitation, change.powerlimit, change.dcflag, change.readingmode, change.voltagelimit
|
|
||||||
return self.no, 0, 0, change.dcflag, change.readingmode, 0
|
:param values: a dict like object containing the parameters to be written
|
||||||
|
"""
|
||||||
|
self.read_params() # make sure parameters are up to date
|
||||||
|
if not values['enabled']:
|
||||||
|
values['excitation'] = 0
|
||||||
|
values['powerlimit'] = 0
|
||||||
|
values['voltagelimit'] = 0
|
||||||
|
self.comm_write('BRIDGE %(no)d,%(enabled)g,%(powerlimit)g,%(dcflag)d,'
|
||||||
|
'%(readingmode)d,%(voltagelimit)g' % values)
|
||||||
|
self.read_params() # read back
|
||||||
|
|
||||||
|
|
||||||
class Level(PpmsMixin, HasIodev, Readable):
|
class Level(PpmsBase):
|
||||||
"""helium level"""
|
"""helium level"""
|
||||||
|
|
||||||
level = IOHandler('level', 'LEVEL?', '%g,%d')
|
value = Parameter(datatype=FloatRange(unit='%'))
|
||||||
|
|
||||||
value = Parameter(datatype=FloatRange(unit='%'), handler=level)
|
|
||||||
status = Parameter(handler=level)
|
|
||||||
# pollinterval = Parameter(visibility=3)
|
|
||||||
|
|
||||||
channel = 'level'
|
channel = 'level'
|
||||||
|
|
||||||
|
def doPoll(self):
|
||||||
|
self.read_value()
|
||||||
|
|
||||||
def update_value_status(self, value, packed_status):
|
def update_value_status(self, value, packed_status):
|
||||||
pass
|
pass
|
||||||
# must be a no-op
|
# must be a no-op
|
||||||
# when called from Main.read_data, value is always None
|
# when called from Main.read_data, value is always None
|
||||||
# value and status is polled via settings
|
# value and status is polled via settings
|
||||||
|
|
||||||
def analyze_level(self, level, status):
|
def read_value(self):
|
||||||
# ignore 'old reading' state of the flag, as this happens only for a short time
|
# ignore 'old reading' state of the flag, as this happens only for a short time
|
||||||
# during measuring
|
return literal_eval(self.communicate('LEVEL?'))[0]
|
||||||
return dict(value=level, status=(self.Status.IDLE, ''))
|
|
||||||
|
|
||||||
|
|
||||||
class Chamber(PpmsMixin, HasIodev, Drivable):
|
class Chamber(PpmsBase, Drivable):
|
||||||
"""sample chamber handling
|
"""sample chamber handling
|
||||||
|
|
||||||
value is an Enum, which is redundant with the status text
|
value is an Enum, which is redundant with the status text
|
||||||
"""
|
"""
|
||||||
|
|
||||||
chamber = IOHandler('chamber', 'CHAMBER?', '%d')
|
|
||||||
Status = Drivable.Status
|
Status = Drivable.Status
|
||||||
# pylint: disable=invalid-name
|
code_table = [
|
||||||
Operation = Enum(
|
# valuecode, status, statusname, opcode, targetname
|
||||||
'Operation',
|
(0, Status.IDLE, 'unknown', 10, 'noop'),
|
||||||
seal_immediately=0,
|
(1, Status.IDLE, 'purged_and_sealed', 1, 'purge_and_seal'),
|
||||||
purge_and_seal=1,
|
(2, Status.IDLE, 'vented_and_sealed', 2, 'vent_and_seal'),
|
||||||
vent_and_seal=2,
|
(3, Status.WARN, 'sealed_unknown', 0, 'seal_immediately'),
|
||||||
pump_continuously=3,
|
(4, Status.BUSY, 'purge_and_seal', None, None),
|
||||||
vent_continuously=4,
|
(5, Status.BUSY, 'vent_and_seal', None, None),
|
||||||
hi_vacuum=5,
|
(6, Status.BUSY, 'pumping_down', None, None),
|
||||||
noop=10,
|
(8, Status.IDLE, 'pumping_continuously', 3, 'pump_continuously'),
|
||||||
)
|
(9, Status.IDLE, 'venting_continuously', 4, 'vent_continuously'),
|
||||||
StatusCode = Enum(
|
(15, Status.ERROR, 'general_failure', None, None),
|
||||||
'StatusCode',
|
]
|
||||||
unknown=0,
|
value_codes = {k: v for v, _, k, _, _ in code_table}
|
||||||
purged_and_sealed=1,
|
target_codes = {k: v for v, _, _, _, k in code_table if k}
|
||||||
vented_and_sealed=2,
|
name2opcode = {k: v for _, _, _, v, k in code_table if k}
|
||||||
sealed_unknown=3,
|
opcode2name = {v: k for _, _, _, v, k in code_table if k}
|
||||||
purge_and_seal=4,
|
status_map = {v: (c, k.replace('_', ' ')) for v, c, k, _, _ in code_table}
|
||||||
vent_and_seal=5,
|
value = Parameter(description='chamber state', datatype=EnumType(**value_codes), default=0)
|
||||||
pumping_down=6,
|
target = Parameter(description='chamber command', datatype=EnumType(**target_codes), default='noop')
|
||||||
at_hi_vacuum=7,
|
|
||||||
pumping_continuously=8,
|
|
||||||
venting_continuously=9,
|
|
||||||
general_failure=15,
|
|
||||||
)
|
|
||||||
|
|
||||||
value = Parameter(description='chamber state', handler=chamber,
|
|
||||||
datatype=EnumType(StatusCode))
|
|
||||||
target = Parameter(description='chamber command', handler=chamber,
|
|
||||||
datatype=EnumType(Operation))
|
|
||||||
# pollinterval = Parameter(visibility=3)
|
|
||||||
|
|
||||||
STATUS_MAP = {
|
|
||||||
StatusCode.purged_and_sealed: (Status.IDLE, 'purged and sealed'),
|
|
||||||
StatusCode.vented_and_sealed: (Status.IDLE, 'vented and sealed'),
|
|
||||||
StatusCode.sealed_unknown: (Status.WARN, 'sealed unknown'),
|
|
||||||
StatusCode.purge_and_seal: (Status.BUSY, 'purge and seal'),
|
|
||||||
StatusCode.vent_and_seal: (Status.BUSY, 'vent and seal'),
|
|
||||||
StatusCode.pumping_down: (Status.BUSY, 'pumping down'),
|
|
||||||
StatusCode.at_hi_vacuum: (Status.IDLE, 'at hi vacuum'),
|
|
||||||
StatusCode.pumping_continuously: (Status.IDLE, 'pumping continuously'),
|
|
||||||
StatusCode.venting_continuously: (Status.IDLE, 'venting continuously'),
|
|
||||||
StatusCode.general_failure: (Status.ERROR, 'general failure'),
|
|
||||||
}
|
|
||||||
|
|
||||||
channel = 'chamber'
|
channel = 'chamber'
|
||||||
|
|
||||||
def update_value_status(self, value, packed_status):
|
def update_value_status(self, value, packed_status):
|
||||||
status_code = (packed_status >> 8) & 0xf
|
status_code = (packed_status >> 8) & 0xf
|
||||||
if status_code in self.STATUS_MAP:
|
if status_code in self.status_map:
|
||||||
self.value = status_code
|
self.value = status_code
|
||||||
self.status = self.STATUS_MAP[status_code]
|
self.status = self.status_map[status_code]
|
||||||
else:
|
else:
|
||||||
self.value = self.StatusCode.unknown
|
self.value = self.value_map['unknown']
|
||||||
self.status = (self.Status.ERROR, 'unknown status code %d' % status_code)
|
self.status = (self.Status.ERROR, 'unknown status code %d' % status_code)
|
||||||
|
|
||||||
def analyze_chamber(self, target):
|
def read_target(self):
|
||||||
return dict(target=target)
|
opcode = int(self.communicate('CHAMBER?'))
|
||||||
|
return self.opcode2name[opcode]
|
||||||
|
|
||||||
def change_chamber(self, change):
|
def write_target(self, value):
|
||||||
# write settings, combining <pname>=<value> and current attributes
|
if value == self.target.noop:
|
||||||
# and request updated settings
|
return self.target.noop
|
||||||
if change.target == self.Operation.noop:
|
opcode = self.name2opcode[self.target.enum(value).name]
|
||||||
return None
|
assert self.communicate('CHAMBER %d' % opcode) == 'OK'
|
||||||
return (change.target,)
|
return self.read_target()
|
||||||
|
|
||||||
|
|
||||||
class Temp(PpmsMixin, HasIodev, Drivable):
|
class Temp(PpmsBase, Drivable):
|
||||||
"""temperature"""
|
"""temperature"""
|
||||||
|
|
||||||
temp = IOHandler('temp', 'TEMP?', '%g,%g,%d')
|
|
||||||
Status = Enum(
|
Status = Enum(
|
||||||
Drivable.Status,
|
Drivable.Status,
|
||||||
RAMPING=370,
|
RAMPING=370,
|
||||||
STABILIZING=380,
|
STABILIZING=380,
|
||||||
)
|
)
|
||||||
# pylint: disable=invalid-name
|
value = Parameter(datatype=FloatRange(unit='K'))
|
||||||
ApproachMode = Enum('ApproachMode', fast_settle=0, no_overshoot=1)
|
status = Parameter(datatype=StatusType(Status))
|
||||||
|
target = Parameter(datatype=FloatRange(1.7, 402.0, unit='K'), needscfg=False)
|
||||||
value = Parameter(datatype=FloatRange(unit='K'), poll=True)
|
|
||||||
status = Parameter(datatype=StatusType(Status), poll=True)
|
|
||||||
target = Parameter(datatype=FloatRange(1.7, 402.0, unit='K'), poll=False, needscfg=False)
|
|
||||||
setpoint = Parameter('intermediate set point',
|
setpoint = Parameter('intermediate set point',
|
||||||
datatype=FloatRange(1.7, 402.0, unit='K'), handler=temp)
|
datatype=FloatRange(1.7, 402.0, unit='K'))
|
||||||
ramp = Parameter('ramping speed', readonly=False, default=0,
|
ramp = Parameter('ramping speed', readonly=False, default=0,
|
||||||
datatype=FloatRange(0, 20, unit='K/min'))
|
datatype=FloatRange(0, 20, unit='K/min'))
|
||||||
workingramp = Parameter('intermediate ramp value',
|
workingramp = Parameter('intermediate ramp value',
|
||||||
datatype=FloatRange(0, 20, unit='K/min'), handler=temp)
|
datatype=FloatRange(0, 20, unit='K/min'), default=0)
|
||||||
approachmode = Parameter('how to approach target!', readonly=False, handler=temp,
|
approachmode = Parameter('how to approach target!', readonly=False,
|
||||||
datatype=EnumType(ApproachMode))
|
datatype=EnumType(fast_settle=0, no_overshoot=1), default=0)
|
||||||
# pollinterval = Parameter(visibility=3)
|
|
||||||
timeout = Parameter('drive timeout, in addition to ramp time', readonly=False,
|
timeout = Parameter('drive timeout, in addition to ramp time', readonly=False,
|
||||||
datatype=FloatRange(0, unit='sec'), default=3600)
|
datatype=FloatRange(0, unit='sec'), default=3600)
|
||||||
|
general_stop = Property('respect general stop', datatype=BoolType(),
|
||||||
# pylint: disable=invalid-name
|
default=True, value=False)
|
||||||
TempStatus = Enum(
|
|
||||||
'TempStatus',
|
|
||||||
stable_at_target=1,
|
|
||||||
changing=2,
|
|
||||||
within_tolerance=5,
|
|
||||||
outside_tolerance=6,
|
|
||||||
filling_emptying_reservoir=7,
|
|
||||||
standby=10,
|
|
||||||
control_disabled=13,
|
|
||||||
can_not_complete=14,
|
|
||||||
general_failure=15,
|
|
||||||
)
|
|
||||||
STATUS_MAP = {
|
STATUS_MAP = {
|
||||||
1: (Status.IDLE, 'stable at target'),
|
1: (Status.IDLE, 'stable at target'),
|
||||||
2: (Status.RAMPING, 'ramping'),
|
2: (Status.RAMPING, 'ramping'),
|
||||||
@ -420,8 +368,6 @@ class Temp(PpmsMixin, HasIodev, Drivable):
|
|||||||
14: (Status.ERROR, 'can not complete'),
|
14: (Status.ERROR, 'can not complete'),
|
||||||
15: (Status.ERROR, 'general failure'),
|
15: (Status.ERROR, 'general failure'),
|
||||||
}
|
}
|
||||||
general_stop = Property('respect general stop', datatype=BoolType(),
|
|
||||||
default=True, value=False)
|
|
||||||
|
|
||||||
channel = 'temp'
|
channel = 'temp'
|
||||||
_stopped = False
|
_stopped = False
|
||||||
@ -432,6 +378,42 @@ class Temp(PpmsMixin, HasIodev, Drivable):
|
|||||||
_wait_at10 = False
|
_wait_at10 = False
|
||||||
_ramp_at_limit = False
|
_ramp_at_limit = False
|
||||||
|
|
||||||
|
param_names = 'setpoint', 'workingramp', 'approachmode'
|
||||||
|
|
||||||
|
@CommonReadHandler(param_names)
|
||||||
|
def read_params(self):
|
||||||
|
settings = literal_eval(self.communicate('TEMP?'))
|
||||||
|
if settings == self._last_settings:
|
||||||
|
# update parameters only on change, as 'ramp' and 'approachmode' are
|
||||||
|
# not always sent to the hardware
|
||||||
|
return
|
||||||
|
self.setpoint, self.workingramp, self.approachmode = self._last_settings = settings
|
||||||
|
if self.setpoint != 10 or not self._wait_at10:
|
||||||
|
self.log.debug('read back target %g %r' % (self.setpoint, self._wait_at10))
|
||||||
|
self.target = self.setpoint
|
||||||
|
if self.workingramp != 2 or not self._ramp_at_limit:
|
||||||
|
self.log.debug('read back ramp %g %r' % (self.workingramp, self._ramp_at_limit))
|
||||||
|
self.ramp = self.workingramp
|
||||||
|
|
||||||
|
def _write_params(self, setpoint, ramp, approachmode):
|
||||||
|
wait_at10 = False
|
||||||
|
ramp_at_limit = False
|
||||||
|
if self.value > 11:
|
||||||
|
if setpoint <= 10:
|
||||||
|
wait_at10 = True
|
||||||
|
setpoint = 10
|
||||||
|
elif self.value > setpoint:
|
||||||
|
if ramp >= 2:
|
||||||
|
ramp = 2
|
||||||
|
ramp_at_limit = True
|
||||||
|
self._wait_at10 = wait_at10
|
||||||
|
self._ramp_at_limit = ramp_at_limit
|
||||||
|
self.calc_expected(setpoint, ramp)
|
||||||
|
self.log.debug(
|
||||||
|
'change_temp v %r s %r r %r w %r l %r' % (self.value, setpoint, ramp, wait_at10, ramp_at_limit))
|
||||||
|
self.comm_write('TEMP %g,%g,%d' % (setpoint, ramp, approachmode))
|
||||||
|
self.read_params()
|
||||||
|
|
||||||
def update_value_status(self, value, packed_status):
|
def update_value_status(self, value, packed_status):
|
||||||
if value is None:
|
if value is None:
|
||||||
self.status = (self.Status.ERROR, 'invalid value')
|
self.status = (self.Status.ERROR, 'invalid value')
|
||||||
@ -449,7 +431,7 @@ class Temp(PpmsMixin, HasIodev, Drivable):
|
|||||||
if now > self._cool_deadline:
|
if now > self._cool_deadline:
|
||||||
self._wait_at10 = False
|
self._wait_at10 = False
|
||||||
self._last_change = now
|
self._last_change = now
|
||||||
self.temp.write(self, 'setpoint', self.target)
|
self._write_params(self.target, self.ramp, self.approachmode)
|
||||||
status = (self.Status.STABILIZING, 'waiting at 10 K')
|
status = (self.Status.STABILIZING, 'waiting at 10 K')
|
||||||
if self._last_change: # there was a change, which is not yet confirmed by hw
|
if self._last_change: # there was a change, which is not yet confirmed by hw
|
||||||
if now > self._last_change + 5:
|
if now > self._last_change + 5:
|
||||||
@ -478,41 +460,6 @@ class Temp(PpmsMixin, HasIodev, Drivable):
|
|||||||
self._expected_target_time = 0
|
self._expected_target_time = 0
|
||||||
self.status = status
|
self.status = status
|
||||||
|
|
||||||
def analyze_temp(self, setpoint, workingramp, approachmode):
|
|
||||||
if (setpoint, workingramp, approachmode) == self._last_settings:
|
|
||||||
# update parameters only on change, as 'ramp' and 'approachmode' are
|
|
||||||
# not always sent to the hardware
|
|
||||||
return {}
|
|
||||||
self._last_settings = setpoint, workingramp, approachmode
|
|
||||||
if setpoint != 10 or not self._wait_at10:
|
|
||||||
self.log.debug('read back target %g %r' % (setpoint, self._wait_at10))
|
|
||||||
self.target = setpoint
|
|
||||||
if workingramp != 2 or not self._ramp_at_limit:
|
|
||||||
self.log.debug('read back ramp %g %r' % (workingramp, self._ramp_at_limit))
|
|
||||||
self.ramp = workingramp
|
|
||||||
result = dict(setpoint=setpoint, workingramp=workingramp)
|
|
||||||
self.log.debug('analyze_temp %r %r' % (result, (self.target, self.ramp)))
|
|
||||||
return result
|
|
||||||
|
|
||||||
def change_temp(self, change):
|
|
||||||
ramp = change.ramp
|
|
||||||
setpoint = change.setpoint
|
|
||||||
wait_at10 = False
|
|
||||||
ramp_at_limit = False
|
|
||||||
if self.value > 11:
|
|
||||||
if setpoint <= 10:
|
|
||||||
wait_at10 = True
|
|
||||||
setpoint = 10
|
|
||||||
elif self.value > setpoint:
|
|
||||||
if ramp >= 2:
|
|
||||||
ramp = 2
|
|
||||||
ramp_at_limit = True
|
|
||||||
self._wait_at10 = wait_at10
|
|
||||||
self._ramp_at_limit = ramp_at_limit
|
|
||||||
self.calc_expected(setpoint, ramp)
|
|
||||||
self.log.debug('change_temp v %r s %r r %r w %r l %r' % (self.value, setpoint, ramp, wait_at10, ramp_at_limit))
|
|
||||||
return setpoint, ramp, change.approachmode
|
|
||||||
|
|
||||||
def write_target(self, target):
|
def write_target(self, target):
|
||||||
self._stopped = False
|
self._stopped = False
|
||||||
if abs(self.target - self.value) <= 2e-5 * target and target == self.target:
|
if abs(self.target - self.value) <= 2e-5 * target and target == self.target:
|
||||||
@ -520,23 +467,23 @@ class Temp(PpmsMixin, HasIodev, Drivable):
|
|||||||
self._status_before_change = self.status
|
self._status_before_change = self.status
|
||||||
self.status = (self.Status.BUSY, 'changed target')
|
self.status = (self.Status.BUSY, 'changed target')
|
||||||
self._last_change = time.time()
|
self._last_change = time.time()
|
||||||
self.temp.write(self, 'setpoint', target)
|
self._write_params(target, self.ramp, self.approachmode)
|
||||||
self.log.debug('write_target %s' % repr((self.setpoint, target, self._wait_at10)))
|
self.log.debug('write_target %s' % repr((self.setpoint, target, self._wait_at10)))
|
||||||
return target
|
return target
|
||||||
|
|
||||||
def write_approachmode(self, value):
|
def write_approachmode(self, value):
|
||||||
if self.isDriving():
|
if self.isDriving():
|
||||||
self.temp.write(self, 'approachmode', value)
|
self._write_params(self.setpoint, self.ramp, value)
|
||||||
return Done
|
return Done
|
||||||
self.approachmode = value
|
self.approachmode = value
|
||||||
return None # do not execute TEMP command, as this would trigger an unnecessary T change
|
return Done # do not execute TEMP command, as this would trigger an unnecessary T change
|
||||||
|
|
||||||
def write_ramp(self, value):
|
def write_ramp(self, value):
|
||||||
if self.isDriving():
|
if self.isDriving():
|
||||||
self.temp.write(self, 'ramp', value)
|
self._write_params(self.setpoint, value, self.approachmode)
|
||||||
return Done
|
return Done
|
||||||
# self.ramp = value
|
self.ramp = value
|
||||||
return None # do not execute TEMP command, as this would trigger an unnecessary T change
|
return Done # do not execute TEMP command, as this would trigger an unnecessary T change
|
||||||
|
|
||||||
def calc_expected(self, target, ramp):
|
def calc_expected(self, target, ramp):
|
||||||
self._expected_target_time = time.time() + abs(target - self.value) * 60.0 / max(0.1, ramp)
|
self._expected_target_time = time.time() + abs(target - self.value) * 60.0 / max(0.1, ramp)
|
||||||
@ -554,10 +501,9 @@ class Temp(PpmsMixin, HasIodev, Drivable):
|
|||||||
self._stopped = True
|
self._stopped = True
|
||||||
|
|
||||||
|
|
||||||
class Field(PpmsMixin, HasIodev, Drivable):
|
class Field(PpmsBase, Drivable):
|
||||||
"""magnetic field"""
|
"""magnetic field"""
|
||||||
|
|
||||||
field = IOHandler('field', 'FIELD?', '%g,%g,%d,%d')
|
|
||||||
Status = Enum(
|
Status = Enum(
|
||||||
Drivable.Status,
|
Drivable.Status,
|
||||||
PREPARED=150,
|
PREPARED=150,
|
||||||
@ -566,20 +512,15 @@ class Field(PpmsMixin, HasIodev, Drivable):
|
|||||||
STABILIZING=380,
|
STABILIZING=380,
|
||||||
FINALIZING=390,
|
FINALIZING=390,
|
||||||
)
|
)
|
||||||
# pylint: disable=invalid-name
|
value = Parameter(datatype=FloatRange(unit='T'))
|
||||||
PersistentMode = Enum('PersistentMode', persistent=0, driven=1)
|
status = Parameter(datatype=StatusType(Status))
|
||||||
ApproachMode = Enum('ApproachMode', linear=0, no_overshoot=1, oscillate=2)
|
target = Parameter(datatype=FloatRange(-15, 15, unit='T')) # poll only one parameter
|
||||||
|
ramp = Parameter('ramping speed', readonly=False,
|
||||||
value = Parameter(datatype=FloatRange(unit='T'), poll=True)
|
datatype=FloatRange(0.064, 1.19, unit='T/min'), default=0.19)
|
||||||
status = Parameter(datatype=StatusType(Status), poll=True)
|
approachmode = Parameter('how to approach target', readonly=False,
|
||||||
target = Parameter(datatype=FloatRange(-15, 15, unit='T'), handler=field)
|
datatype=EnumType(linear=0, no_overshoot=1, oscillate=2), default=0)
|
||||||
ramp = Parameter('ramping speed', readonly=False, handler=field,
|
persistentmode = Parameter('what to do after changing field', readonly=False,
|
||||||
datatype=FloatRange(0.064, 1.19, unit='T/min'))
|
datatype=EnumType(persistent=0, driven=1), default=0)
|
||||||
approachmode = Parameter('how to approach target', readonly=False, handler=field,
|
|
||||||
datatype=EnumType(ApproachMode))
|
|
||||||
persistentmode = Parameter('what to do after changing field', readonly=False, handler=field,
|
|
||||||
datatype=EnumType(PersistentMode))
|
|
||||||
# pollinterval = Parameter(visibility=3)
|
|
||||||
|
|
||||||
STATUS_MAP = {
|
STATUS_MAP = {
|
||||||
1: (Status.IDLE, 'persistent mode'),
|
1: (Status.IDLE, 'persistent mode'),
|
||||||
@ -599,6 +540,25 @@ class Field(PpmsMixin, HasIodev, Drivable):
|
|||||||
_last_target = None # last reached target
|
_last_target = None # last reached target
|
||||||
_last_change = 0 # means no target change is pending
|
_last_change = 0 # means no target change is pending
|
||||||
|
|
||||||
|
param_names = 'target', 'ramp', 'approachmode', 'persistentmode'
|
||||||
|
|
||||||
|
@CommonReadHandler(param_names)
|
||||||
|
def read_params(self):
|
||||||
|
settings = literal_eval(self.communicate('FIELD?'))
|
||||||
|
# print('last_settings tt %s' % repr(self._last_settings))
|
||||||
|
if settings == self._last_settings:
|
||||||
|
# we update parameters only on change, as 'ramp' and 'approachmode' are
|
||||||
|
# not always sent to the hardware
|
||||||
|
return
|
||||||
|
target, ramp, self.approachmode, self.persistentmode = self._last_settings = settings
|
||||||
|
self.target = round(target * 1e-4, 7)
|
||||||
|
self.ramp = ramp * 6e-3
|
||||||
|
|
||||||
|
def _write_params(self, target, ramp, approachmode, persistentmode):
|
||||||
|
self.comm_write('FIELD %g,%g,%d,%d' % (
|
||||||
|
target * 1e+4, ramp / 6e-3, approachmode, persistentmode))
|
||||||
|
self.read_params()
|
||||||
|
|
||||||
def update_value_status(self, value, packed_status):
|
def update_value_status(self, value, packed_status):
|
||||||
if value is None:
|
if value is None:
|
||||||
self.status = (self.Status.ERROR, 'invalid value')
|
self.status = (self.Status.ERROR, 'invalid value')
|
||||||
@ -633,19 +593,6 @@ class Field(PpmsMixin, HasIodev, Drivable):
|
|||||||
status = (status[0], 'stopping (%s)' % status[1])
|
status = (status[0], 'stopping (%s)' % status[1])
|
||||||
self.status = status
|
self.status = status
|
||||||
|
|
||||||
def analyze_field(self, target, ramp, approachmode, persistentmode):
|
|
||||||
# print('last_settings tt %s' % repr(self._last_settings))
|
|
||||||
if (target, ramp, approachmode, persistentmode) == self._last_settings:
|
|
||||||
# we update parameters only on change, as 'ramp' and 'approachmode' are
|
|
||||||
# not always sent to the hardware
|
|
||||||
return {}
|
|
||||||
self._last_settings = target, ramp, approachmode, persistentmode
|
|
||||||
return dict(target=round(target * 1e-4, 7), ramp=ramp * 6e-3, approachmode=approachmode,
|
|
||||||
persistentmode=persistentmode)
|
|
||||||
|
|
||||||
def change_field(self, change):
|
|
||||||
return change.target * 1e+4, change.ramp / 6e-3, change.approachmode, change.persistentmode
|
|
||||||
|
|
||||||
def write_target(self, target):
|
def write_target(self, target):
|
||||||
if abs(self.target - self.value) <= 2e-5 and target == self.target:
|
if abs(self.target - self.value) <= 2e-5 and target == self.target:
|
||||||
self.target = target
|
self.target = target
|
||||||
@ -654,7 +601,7 @@ class Field(PpmsMixin, HasIodev, Drivable):
|
|||||||
self._stopped = False
|
self._stopped = False
|
||||||
self._last_change = time.time()
|
self._last_change = time.time()
|
||||||
self.status = (self.Status.BUSY, 'changed target')
|
self.status = (self.Status.BUSY, 'changed target')
|
||||||
self.field.write(self, 'target', target)
|
self._write_params(target, self.ramp, self.approachmode, self.persistentmode)
|
||||||
return Done
|
return Done
|
||||||
|
|
||||||
def write_persistentmode(self, mode):
|
def write_persistentmode(self, mode):
|
||||||
@ -665,19 +612,19 @@ class Field(PpmsMixin, HasIodev, Drivable):
|
|||||||
self._status_before_change = self.status
|
self._status_before_change = self.status
|
||||||
self._stopped = False
|
self._stopped = False
|
||||||
self.status = (self.Status.BUSY, 'changed persistent mode')
|
self.status = (self.Status.BUSY, 'changed persistent mode')
|
||||||
self.field.write(self, 'persistentmode', mode)
|
self._write_params(self.target, self.ramp, self.approachmode, mode)
|
||||||
return Done
|
return Done
|
||||||
|
|
||||||
def write_ramp(self, value):
|
def write_ramp(self, value):
|
||||||
self.ramp = value
|
self.ramp = value
|
||||||
if self.isDriving():
|
if self.isDriving():
|
||||||
self.field.write(self, 'ramp', value)
|
self._write_params(self.target, value, self.approachmode, self.persistentmode)
|
||||||
return Done
|
return Done
|
||||||
return None # do not execute FIELD command, as this would trigger a ramp up of leads current
|
return None # do not execute FIELD command, as this would trigger a ramp up of leads current
|
||||||
|
|
||||||
def write_approachmode(self, value):
|
def write_approachmode(self, value):
|
||||||
if self.isDriving():
|
if self.isDriving():
|
||||||
self.field.write(self, 'approachmode', value)
|
self._write_params(self.target, self.ramp, value, self.persistentmode)
|
||||||
return Done
|
return Done
|
||||||
return None # do not execute FIELD command, as this would trigger a ramp up of leads current
|
return None # do not execute FIELD command, as this would trigger a ramp up of leads current
|
||||||
|
|
||||||
@ -692,20 +639,17 @@ class Field(PpmsMixin, HasIodev, Drivable):
|
|||||||
self._stopped = True
|
self._stopped = True
|
||||||
|
|
||||||
|
|
||||||
class Position(PpmsMixin, HasIodev, Drivable):
|
class Position(PpmsBase, Drivable):
|
||||||
"""rotator position"""
|
"""rotator position"""
|
||||||
|
|
||||||
move = IOHandler('move', 'MOVE?', '%g,%g,%g')
|
|
||||||
Status = Drivable.Status
|
Status = Drivable.Status
|
||||||
|
|
||||||
value = Parameter(datatype=FloatRange(unit='deg'), poll=True)
|
value = Parameter(datatype=FloatRange(unit='deg'))
|
||||||
target = Parameter(datatype=FloatRange(-720., 720., unit='deg'), handler=move)
|
target = Parameter(datatype=FloatRange(-720., 720., unit='deg'))
|
||||||
enabled = Parameter('is this channel used?', readonly=False, poll=False,
|
enabled = Parameter('is this channel used?', readonly=False,
|
||||||
datatype=BoolType(), default=True)
|
datatype=BoolType(), default=True)
|
||||||
speed = Parameter('motor speed', readonly=False, handler=move,
|
speed = Parameter('motor speed', readonly=False, default=12,
|
||||||
datatype=FloatRange(0.8, 12, unit='deg/sec'))
|
datatype=FloatRange(0.8, 12, unit='deg/sec'))
|
||||||
# pollinterval = Parameter(visibility=3)
|
|
||||||
|
|
||||||
STATUS_MAP = {
|
STATUS_MAP = {
|
||||||
1: (Status.IDLE, 'at target'),
|
1: (Status.IDLE, 'at target'),
|
||||||
5: (Status.BUSY, 'moving'),
|
5: (Status.BUSY, 'moving'),
|
||||||
@ -720,6 +664,23 @@ class Position(PpmsMixin, HasIodev, Drivable):
|
|||||||
_last_change = 0
|
_last_change = 0
|
||||||
_within_target = 0 # time since we are within target
|
_within_target = 0 # time since we are within target
|
||||||
|
|
||||||
|
param_names = 'target', 'speed'
|
||||||
|
|
||||||
|
@CommonReadHandler(param_names)
|
||||||
|
def read_params(self):
|
||||||
|
settings = literal_eval(self.communicate('MOVE?'))
|
||||||
|
if settings == self._last_settings:
|
||||||
|
# we update parameters only on change, as 'speed' is
|
||||||
|
# not always sent to the hardware
|
||||||
|
return
|
||||||
|
self.target, _, speed = self._last_settings = settings
|
||||||
|
self.speed = (15 - speed) * 0.8
|
||||||
|
|
||||||
|
def _write_params(self, target, speed):
|
||||||
|
speed = int(round(min(14, max(0, 15 - speed / 0.8)), 0))
|
||||||
|
self.comm_write('MOVE %g,%d,%d' % (target, 0, speed))
|
||||||
|
return self.read_params()
|
||||||
|
|
||||||
def update_value_status(self, value, packed_status):
|
def update_value_status(self, value, packed_status):
|
||||||
if not self.enabled:
|
if not self.enabled:
|
||||||
self.status = (self.Status.DISABLED, 'disabled')
|
self.status = (self.Status.DISABLED, 'disabled')
|
||||||
@ -757,29 +718,17 @@ class Position(PpmsMixin, HasIodev, Drivable):
|
|||||||
status = (status[0], 'stopping (%s)' % status[1])
|
status = (status[0], 'stopping (%s)' % status[1])
|
||||||
self.status = status
|
self.status = status
|
||||||
|
|
||||||
def analyze_move(self, target, mode, speed):
|
|
||||||
if (target, speed) == self._last_settings:
|
|
||||||
# we update parameters only on change, as 'speed' is
|
|
||||||
# not always sent to the hardware
|
|
||||||
return {}
|
|
||||||
self._last_settings = target, speed
|
|
||||||
return dict(target=target, speed=(15 - speed) * 0.8)
|
|
||||||
|
|
||||||
def change_move(self, change):
|
|
||||||
speed = int(round(min(14, max(0, 15 - change.speed / 0.8)), 0))
|
|
||||||
return change.target, 0, speed
|
|
||||||
|
|
||||||
def write_target(self, target):
|
def write_target(self, target):
|
||||||
self._stopped = False
|
self._stopped = False
|
||||||
self._last_change = 0
|
self._last_change = 0
|
||||||
self._status_before_change = self.status
|
self._status_before_change = self.status
|
||||||
self.status = (self.Status.BUSY, 'changed target')
|
self.status = (self.Status.BUSY, 'changed target')
|
||||||
self.move.write(self, 'target', target)
|
self._write_params(target, self.speed)
|
||||||
return Done
|
return Done
|
||||||
|
|
||||||
def write_speed(self, value):
|
def write_speed(self, value):
|
||||||
if self.isDriving():
|
if self.isDriving():
|
||||||
self.move.write(self, 'speed', value)
|
self._write_params(self.target, value)
|
||||||
return Done
|
return Done
|
||||||
self.speed = value
|
self.speed = value
|
||||||
return None # do not execute MOVE command, as this would trigger an unnecessary move
|
return None # do not execute MOVE command, as this would trigger an unnecessary move
|
||||||
|
@ -26,6 +26,7 @@ import time
|
|||||||
def num(string):
|
def num(string):
|
||||||
return json.loads(string)
|
return json.loads(string)
|
||||||
|
|
||||||
|
|
||||||
class NamedList:
|
class NamedList:
|
||||||
def __init__(self, keys, *args, **kwargs):
|
def __init__(self, keys, *args, **kwargs):
|
||||||
self.__keys__ = keys.split()
|
self.__keys__ = keys.split()
|
||||||
@ -49,8 +50,10 @@ class NamedList:
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return ",".join("%.7g" % val for val in self.aslist())
|
return ",".join("%.7g" % val for val in self.aslist())
|
||||||
|
|
||||||
|
|
||||||
class PpmsSim:
|
class PpmsSim:
|
||||||
CHANNELS = 'st t mf pos r1 i1 r2 i2'.split()
|
CHANNELS = 'st t mf pos r1 i1 r2 i2'.split()
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.status = NamedList('t mf ch pos', 1, 1, 1, 1)
|
self.status = NamedList('t mf ch pos', 1, 1, 1, 1)
|
||||||
self.st = 0x1111
|
self.st = 0x1111
|
||||||
@ -176,7 +179,6 @@ class PpmsSim:
|
|||||||
if abs(self.t - self.temp.target) < 1:
|
if abs(self.t - self.temp.target) < 1:
|
||||||
self.status.t = 6 # outside tolerance
|
self.status.t = 6 # outside tolerance
|
||||||
|
|
||||||
|
|
||||||
if abs(self.pos - self.move.target) < 0.01:
|
if abs(self.pos - self.move.target) < 0.01:
|
||||||
self.status.pos = 1
|
self.status.pos = 1
|
||||||
else:
|
else:
|
||||||
@ -188,7 +190,6 @@ class PpmsSim:
|
|||||||
self.r2 = 1000 / self.t
|
self.r2 = 1000 / self.t
|
||||||
self.i2 = math.log(self.t)
|
self.i2 = math.log(self.t)
|
||||||
self.level.value = 100 - (self.time - self.start) * 0.01 % 100
|
self.level.value = 100 - (self.time - self.start) * 0.01 % 100
|
||||||
# print('PROGRESS T=%.7g B=%.7g x=%.7g' % (self.t, self.mf, self.pos))
|
|
||||||
|
|
||||||
def getdat(self, mask):
|
def getdat(self, mask):
|
||||||
mask = int(mask) & 0xff # all channels up to i2
|
mask = int(mask) & 0xff # all channels up to i2
|
||||||
@ -198,6 +199,7 @@ class PpmsSim:
|
|||||||
output.append("%.7g" % getattr(self, chan))
|
output.append("%.7g" % getattr(self, chan))
|
||||||
return ",".join(output)
|
return ",".join(output)
|
||||||
|
|
||||||
|
|
||||||
class QDevice:
|
class QDevice:
|
||||||
def __init__(self, classid):
|
def __init__(self, classid):
|
||||||
self.sim = PpmsSim()
|
self.sim = PpmsSim()
|
||||||
@ -225,5 +227,6 @@ class QDevice:
|
|||||||
result = "OK"
|
result = "OK"
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def shutdown():
|
def shutdown():
|
||||||
pass
|
pass
|
||||||
|
@ -41,7 +41,7 @@ from secop.client import ProxyClient
|
|||||||
from secop.datatypes import ArrayOf, BoolType, \
|
from secop.datatypes import ArrayOf, BoolType, \
|
||||||
EnumType, FloatRange, IntRange, StringType
|
EnumType, FloatRange, IntRange, StringType
|
||||||
from secop.errors import ConfigError, HardwareError, secop_error, NoSuchModuleError
|
from secop.errors import ConfigError, HardwareError, secop_error, NoSuchModuleError
|
||||||
from secop.lib import getGeneralConfig, mkthread
|
from secop.lib import getGeneralConfig, mkthread, formatExtendedStack
|
||||||
from secop.lib.asynconn import AsynConn, ConnectionClosed
|
from secop.lib.asynconn import AsynConn, ConnectionClosed
|
||||||
from secop.modules import Attached, Command, Done, Drivable, \
|
from secop.modules import Attached, Command, Done, Drivable, \
|
||||||
Module, Parameter, Property, Readable, Writable
|
Module, Parameter, Property, Readable, Writable
|
||||||
@ -124,6 +124,7 @@ class SeaClient(ProxyClient, Module):
|
|||||||
if config:
|
if config:
|
||||||
self.default_json_file[name] = config.split()[0] + '.json'
|
self.default_json_file[name] = config.split()[0] + '.json'
|
||||||
self.io = None
|
self.io = None
|
||||||
|
self.asyncio = None
|
||||||
ProxyClient.__init__(self)
|
ProxyClient.__init__(self)
|
||||||
Module.__init__(self, name, log, opts, srv)
|
Module.__init__(self, name, log, opts, srv)
|
||||||
|
|
||||||
@ -153,7 +154,7 @@ class SeaClient(ProxyClient, Module):
|
|||||||
"""send a request and wait for reply"""
|
"""send a request and wait for reply"""
|
||||||
with self._write_lock:
|
with self._write_lock:
|
||||||
if not self.io or not self.io.connection:
|
if not self.io or not self.io.connection:
|
||||||
if not self.asyncio.connection:
|
if not self.asyncio or not self.asyncio.connection:
|
||||||
self._connect(None)
|
self._connect(None)
|
||||||
self.io = AsynConn(self.uri)
|
self.io = AsynConn(self.uri)
|
||||||
assert self.io.readline() == b'OK'
|
assert self.io.readline() == b'OK'
|
||||||
@ -314,12 +315,14 @@ class SeaConfigCreator(SeaClient):
|
|||||||
stripped, _, ext = filename.rpartition('.')
|
stripped, _, ext = filename.rpartition('.')
|
||||||
service = SERVICE_NAMES[ext]
|
service = SERVICE_NAMES[ext]
|
||||||
seaconn = 'sea_' + service
|
seaconn = 'sea_' + service
|
||||||
with open(join(seaconfdir, stripped + '.cfg'), 'w') as fp:
|
cfgfile = join(seaconfdir, stripped + '.cfg')
|
||||||
|
with open(cfgfile, 'w') as fp:
|
||||||
fp.write(CFG_HEADER % dict(config=filename, seaconn=seaconn, service=service,
|
fp.write(CFG_HEADER % dict(config=filename, seaconn=seaconn, service=service,
|
||||||
nodedescr=description.get(filename, filename)))
|
nodedescr=description.get(filename, filename)))
|
||||||
for obj in descr:
|
for obj in descr:
|
||||||
fp.write(CFG_MODULE % dict(modcls=modcls[obj], module=obj, seaconn=seaconn))
|
fp.write(CFG_MODULE % dict(modcls=modcls[obj], module=obj, seaconn=seaconn))
|
||||||
content = json.dumps(descr).replace('}, {', '},\n{').replace('[{', '[\n{').replace('}]}, ', '}]},\n\n')
|
content = json.dumps(descr).replace('}, {', '},\n{').replace('[{', '[\n{').replace('}]}, ', '}]},\n\n')
|
||||||
|
result.append('%s\n' % cfgfile)
|
||||||
with open(join(seaconfdir, filename + '.json'), 'w') as fp:
|
with open(join(seaconfdir, filename + '.json'), 'w') as fp:
|
||||||
fp.write(content + '\n')
|
fp.write(content + '\n')
|
||||||
result.append('%s: %s' % (filename, ','.join(n for n in descr)))
|
result.append('%s: %s' % (filename, ','.join(n for n in descr)))
|
||||||
@ -495,7 +498,8 @@ class SeaModule(Module):
|
|||||||
if key in cls.accessibles:
|
if key in cls.accessibles:
|
||||||
if key == 'target':
|
if key == 'target':
|
||||||
kwds['readonly'] = False
|
kwds['readonly'] = False
|
||||||
pobj = cls.accessibles[key].override(**kwds)
|
pobj = cls.accessibles[key]
|
||||||
|
pobj.init(kwds)
|
||||||
datatype = kwds.get('datatype', cls.accessibles[key].datatype)
|
datatype = kwds.get('datatype', cls.accessibles[key].datatype)
|
||||||
else:
|
else:
|
||||||
pobj = Parameter(**kwds)
|
pobj = Parameter(**kwds)
|
||||||
@ -542,12 +546,17 @@ class SeaModule(Module):
|
|||||||
# create standard parameters like value and status, if not yet there
|
# create standard parameters like value and status, if not yet there
|
||||||
for pname, pobj in cls.accessibles.items():
|
for pname, pobj in cls.accessibles.items():
|
||||||
if pname == 'pollinterval':
|
if pname == 'pollinterval':
|
||||||
attributes[pname] = pobj.override(export=False)
|
pobj.export = False
|
||||||
|
attributes[pname] = pobj
|
||||||
|
pobj.__set_name__(cls, pname)
|
||||||
elif pname not in attributes and isinstance(pobj, Parameter):
|
elif pname not in attributes and isinstance(pobj, Parameter):
|
||||||
attributes[pname] = pobj.override(poll=False, needscfg=False)
|
pobj.poll = False
|
||||||
|
pobj.needscfg = False
|
||||||
|
attributes[pname] = pobj
|
||||||
|
pobj.__set_name__(cls, pname)
|
||||||
|
|
||||||
classname = '%s_%s' % (cls.__name__, sea_object)
|
classname = '%s_%s' % (cls.__name__, sea_object)
|
||||||
# print(name, attributes)
|
attributes['pollerClass'] = None
|
||||||
newcls = type(classname, (cls,), attributes)
|
newcls = type(classname, (cls,), attributes)
|
||||||
return Module.__new__(newcls)
|
return Module.__new__(newcls)
|
||||||
|
|
||||||
@ -640,5 +649,11 @@ class SeaDrivable(SeaModule, Drivable):
|
|||||||
if value is not None:
|
if value is not None:
|
||||||
self.target = value
|
self.target = value
|
||||||
|
|
||||||
|
@Command()
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
"""propagate to SEA
|
||||||
|
|
||||||
|
- on stdsct drivables this will call the halt script
|
||||||
|
- on EaseDriv this will set the stopped state
|
||||||
|
"""
|
||||||
self._iodev.query('%s is_running 0' % self.sea_object)
|
self._iodev.query('%s is_running 0' % self.sea_object)
|
||||||
|
@ -22,22 +22,28 @@
|
|||||||
"""simulated transducer DPM3 read out"""
|
"""simulated transducer DPM3 read out"""
|
||||||
|
|
||||||
import random
|
import random
|
||||||
|
import math
|
||||||
from secop.core import Readable, Parameter, FloatRange, Attached
|
from secop.core import Readable, Parameter, FloatRange, Attached
|
||||||
from secop.lib import clamp
|
from secop.lib import clamp
|
||||||
|
|
||||||
|
|
||||||
class DPM3(Readable):
|
class DPM3(Readable):
|
||||||
motor = Attached()
|
motor = Attached()
|
||||||
jitter = Parameter('simulated jitter', FloatRange(unit='N'), default=1, readonly=False)
|
jitter = Parameter('simulated jitter', FloatRange(unit='N'), default=0.1, readonly=False)
|
||||||
hysteresis = Parameter('simulated hysteresis', FloatRange(unit='deg'), default=100, readonly=False)
|
hysteresis = Parameter('simulated hysteresis', FloatRange(unit='deg'), default=100, readonly=False)
|
||||||
friction = Parameter('friction', FloatRange(unit='N/deg'), default=1, readonly=False)
|
friction = Parameter('friction', FloatRange(unit='N/deg'), default=2.5, readonly=False)
|
||||||
slope = Parameter('slope', FloatRange(unit='N/deg'), default=10, readonly=False)
|
slope = Parameter('slope', FloatRange(unit='N/deg'), default=0.5, readonly=False)
|
||||||
offset = Parameter('offset', FloatRange(unit='N'), default=0, readonly=False)
|
offset = Parameter('offset', FloatRange(unit='N'), default=0, readonly=False)
|
||||||
|
|
||||||
_pos = 0 # effective piston position, main hysteresis taken into account
|
_pos = 0
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
mot = self._motor
|
mot = self._motor
|
||||||
self._pos = clamp(self._pos, mot.value - self.hysteresis * 0.5, mot.value + self.hysteresis * 0.5)
|
d = self.friction * self.slope
|
||||||
return clamp(0, 1, mot.value - self._pos) * self.friction \
|
self._pos = clamp(self._pos, mot.value - d, mot.value + d)
|
||||||
+ self._pos * self.slope + self.jitter * (random.random() - random.random())
|
f = (mot.value - self._pos) / self.slope
|
||||||
|
if mot.value > 0:
|
||||||
|
f = max(f, (mot.value - self.hysteresis) / self.slope)
|
||||||
|
else:
|
||||||
|
f = min(f, (mot.value + self.hysteresis) / self.slope)
|
||||||
|
return f + self.jitter * (random.random() - random.random()) * 0.5
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
from os.path import basename, exists, join
|
from os.path import basename, dirname, exists, join
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from scipy.interpolate import splev, splrep # pylint: disable=import-error
|
from scipy.interpolate import splev, splrep # pylint: disable=import-error
|
||||||
@ -109,7 +109,9 @@ class CalCurve:
|
|||||||
calibname = sensopt.pop(0)
|
calibname = sensopt.pop(0)
|
||||||
_, dot, ext = basename(calibname).rpartition('.')
|
_, dot, ext = basename(calibname).rpartition('.')
|
||||||
kind = None
|
kind = None
|
||||||
for path in os.environ.get('FRAPPY_CALIB_PATH', '').split(','):
|
pathlist = os.environ.get('FRAPPY_CALIB_PATH', '').split(',')
|
||||||
|
pathlist.append(join(dirname(__file__), 'calcurves'))
|
||||||
|
for path in pathlist:
|
||||||
# first try without adding kind
|
# first try without adding kind
|
||||||
filename = join(path.strip(), calibname)
|
filename = join(path.strip(), calibname)
|
||||||
if exists(filename):
|
if exists(filename):
|
||||||
@ -145,7 +147,7 @@ class CalCurve:
|
|||||||
for line in f:
|
for line in f:
|
||||||
parser.parse(line)
|
parser.parse(line)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError('calib curve %s: %s' % (calibspec, e))
|
raise ValueError('calib curve %s: %s' % (calibspec, e)) from e
|
||||||
self.convert_x = nplog if parser.logx else linear
|
self.convert_x = nplog if parser.logx else linear
|
||||||
self.convert_y = npexp if parser.logy else linear
|
self.convert_y = npexp if parser.logy else linear
|
||||||
x = np.asarray(parser.xdata)
|
x = np.asarray(parser.xdata)
|
||||||
@ -157,8 +159,8 @@ class CalCurve:
|
|||||||
raise ValueError('calib curve %s is not monotonic' % calibspec)
|
raise ValueError('calib curve %s is not monotonic' % calibspec)
|
||||||
try:
|
try:
|
||||||
self.spline = splrep(x, y, s=0, k=min(3, len(x) - 1))
|
self.spline = splrep(x, y, s=0, k=min(3, len(x) - 1))
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError) as e:
|
||||||
raise ValueError('invalid calib curve %s' % calibspec)
|
raise ValueError('invalid calib curve %s' % calibspec) from e
|
||||||
|
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
"""convert value
|
"""convert value
|
||||||
@ -178,8 +180,9 @@ class Sensor(Readable):
|
|||||||
pollinterval = Parameter(export=False)
|
pollinterval = Parameter(export=False)
|
||||||
status = Parameter(default=(Readable.Status.ERROR, 'unintialized'))
|
status = Parameter(default=(Readable.Status.ERROR, 'unintialized'))
|
||||||
|
|
||||||
pollerClass = None
|
description = 'a calibrated sensor value'
|
||||||
_value_error = None
|
_value_error = None
|
||||||
|
enablePoll = False
|
||||||
|
|
||||||
def checkProperties(self):
|
def checkProperties(self):
|
||||||
if 'description' not in self.propertyValues:
|
if 'description' not in self.propertyValues:
|
||||||
@ -187,6 +190,7 @@ class Sensor(Readable):
|
|||||||
super().checkProperties()
|
super().checkProperties()
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
|
super().initModule()
|
||||||
self._rawsensor.registerCallbacks(self, ['status']) # auto update status
|
self._rawsensor.registerCallbacks(self, ['status']) # auto update status
|
||||||
self._calib = CalCurve(self.calib)
|
self._calib = CalCurve(self.calib)
|
||||||
if self.description == '_':
|
if self.description == '_':
|
||||||
|
@ -24,13 +24,12 @@
|
|||||||
|
|
||||||
import time
|
import time
|
||||||
import struct
|
import struct
|
||||||
from math import log10
|
|
||||||
|
|
||||||
from secop.core import BoolType, Command, EnumType, FloatRange, IntRange, \
|
from secop.core import BoolType, Command, EnumType, FloatRange, IntRange, \
|
||||||
HasIodev, Parameter, Property, Drivable, PersistentMixin, PersistentParam
|
HasIO, Parameter, Property, Drivable, PersistentMixin, PersistentParam, Done
|
||||||
from secop.io import BytesIO
|
from secop.io import BytesIO
|
||||||
from secop.errors import CommunicationFailedError, HardwareError, BadValueError, IsBusyError
|
from secop.errors import CommunicationFailedError, HardwareError, BadValueError, IsBusyError
|
||||||
|
from secop.rwhandler import ReadHandler, WriteHandler
|
||||||
|
|
||||||
MOTOR_STOP = 3
|
MOTOR_STOP = 3
|
||||||
MOVE = 4
|
MOVE = 4
|
||||||
@ -50,84 +49,78 @@ MAX_SPEED = 2047 * SPEED_SCALE
|
|||||||
ACCEL_SCALE = 1E12 / 2 ** 31 * ANGLE_SCALE
|
ACCEL_SCALE = 1E12 / 2 ** 31 * ANGLE_SCALE
|
||||||
MAX_ACCEL = 2047 * ACCEL_SCALE
|
MAX_ACCEL = 2047 * ACCEL_SCALE
|
||||||
CURRENT_SCALE = 2.8/250
|
CURRENT_SCALE = 2.8/250
|
||||||
ENCODER_RESOLUTION = 0.4 # 365 / 1024, rounded up
|
ENCODER_RESOLUTION = 360 / 1024
|
||||||
|
|
||||||
|
HW_ARGS = {
|
||||||
|
# <parameter name>: (address, scale factor)
|
||||||
|
'encoder_tolerance': (212, ANGLE_SCALE),
|
||||||
|
'speed': (4, SPEED_SCALE),
|
||||||
|
'minspeed': (130, SPEED_SCALE),
|
||||||
|
'currentspeed': (3, SPEED_SCALE),
|
||||||
|
'maxcurrent': (6, CURRENT_SCALE),
|
||||||
|
'standby_current': (7, CURRENT_SCALE,),
|
||||||
|
'acceleration': (5, ACCEL_SCALE),
|
||||||
|
'target_reached': (8, 1),
|
||||||
|
'move_status': (207, 1),
|
||||||
|
'error_bits': (208, 1),
|
||||||
|
'free_wheeling': (204, 0.01),
|
||||||
|
'power_down_delay': (214, 0.01),
|
||||||
|
}
|
||||||
|
|
||||||
|
# special handling (adjust zero):
|
||||||
|
ENCODER_ADR = 209
|
||||||
|
STEPPOS_ADR = 1
|
||||||
|
|
||||||
|
|
||||||
class HwParam(PersistentParam):
|
def writable(*args, **kwds):
|
||||||
adr = Property('parameter address', IntRange(0, 255), export=False)
|
"""convenience function to create writable hardware parameters"""
|
||||||
scale = Property('scale factor (physical value / unit)', FloatRange(), export=False)
|
return PersistentParam(*args, readonly=False, initwrite=True, **kwds)
|
||||||
|
|
||||||
def __init__(self, description, datatype, adr, scale=1, poll=True,
|
|
||||||
readonly=True, persistent=None, **kwds):
|
|
||||||
"""hardware parameter"""
|
|
||||||
if persistent is None:
|
|
||||||
persistent = not readonly
|
|
||||||
if isinstance(datatype, FloatRange) and datatype.fmtstr == '%g':
|
|
||||||
datatype.fmtstr = '%%.%df' % max(0, 1 - int(log10(scale) + 0.01))
|
|
||||||
super().__init__(description, datatype, poll=poll, adr=adr, scale=scale,
|
|
||||||
persistent=persistent, readonly=readonly, **kwds)
|
|
||||||
|
|
||||||
def copy(self):
|
|
||||||
res = HwParam(self.description, self.datatype.copy(), self.adr)
|
|
||||||
res.name = self.name
|
|
||||||
res.init(self.propertyValues)
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
class Motor(PersistentMixin, HasIodev, Drivable):
|
class Motor(PersistentMixin, HasIO, Drivable):
|
||||||
address = Property('module address', IntRange(0, 255), default=1)
|
address = Property('module address', IntRange(0, 255), default=1)
|
||||||
|
|
||||||
value = Parameter('motor position', FloatRange(unit='deg', fmtstr='%.3f'), poll=False, default=0) # polling by read_status
|
value = Parameter('motor position', FloatRange(unit='deg', fmtstr='%.3f'))
|
||||||
zero = PersistentParam('zero point', FloatRange(unit='$'), readonly=False, default=0)
|
zero = PersistentParam('zero point', FloatRange(unit='$'), readonly=False, default=0)
|
||||||
encoder = HwParam('encoder reading', FloatRange(unit='$', fmtstr='%.1f'),
|
encoder = PersistentParam('encoder reading', FloatRange(unit='$', fmtstr='%.1f'),
|
||||||
209, ANGLE_SCALE, readonly=True, initwrite=False, persistent=True)
|
readonly=True, initwrite=False)
|
||||||
steppos = HwParam('position from motor steps', FloatRange(unit='$'),
|
steppos = PersistentParam('position from motor steps', FloatRange(unit='$', fmtstr='%.3f'),
|
||||||
1, ANGLE_SCALE, readonly=True, initwrite=False)
|
readonly=True, initwrite=False)
|
||||||
target = Parameter('_', FloatRange(unit='$'), default=0)
|
target = Parameter('', FloatRange(unit='$'), default=0)
|
||||||
|
|
||||||
move_limit = Parameter('max. angle to drive in one go when current above safe_current', FloatRange(unit='$'),
|
move_limit = Parameter('max. angle to drive in one go', FloatRange(unit='$'),
|
||||||
readonly=False, default=5, group='more')
|
readonly=False, default=360, group='more')
|
||||||
safe_current = Parameter('motor current allowed for big steps', FloatRange(unit='A'),
|
|
||||||
readonly=False, default=0.2, group='more')
|
|
||||||
tolerance = Parameter('positioning tolerance', FloatRange(unit='$'),
|
tolerance = Parameter('positioning tolerance', FloatRange(unit='$'),
|
||||||
readonly=False, default=0.9)
|
readonly=False, default=0.9)
|
||||||
encoder_tolerance = HwParam('the allowed deviation between steppos and encoder\n\nmust be > tolerance',
|
encoder_tolerance = writable('the allowed deviation between steppos and encoder\n\nmust be > tolerance',
|
||||||
FloatRange(0, 360., unit='$'),
|
FloatRange(0, 360., unit='$', fmtstr='%.3f'), group='more')
|
||||||
212, ANGLE_SCALE, readonly=False, group='more')
|
speed = writable('max. speed', FloatRange(0, MAX_SPEED, unit='$/sec', fmtstr='%.1f'), default=40)
|
||||||
speed = HwParam('max. speed', FloatRange(0, MAX_SPEED, unit='$/sec'),
|
minspeed = writable('min. speed', FloatRange(0, MAX_SPEED, unit='$/sec', fmtstr='%.1f'),
|
||||||
4, SPEED_SCALE, readonly=False, group='motorparam')
|
default=SPEED_SCALE, group='motorparam')
|
||||||
minspeed = HwParam('min. speed', FloatRange(0, MAX_SPEED, unit='$/sec'),
|
currentspeed = Parameter('current speed', FloatRange(-MAX_SPEED, MAX_SPEED, unit='$/sec', fmtstr='%.1f'),
|
||||||
130, SPEED_SCALE, readonly=False, default=SPEED_SCALE, group='motorparam')
|
group='motorparam')
|
||||||
currentspeed = HwParam('current speed', FloatRange(-MAX_SPEED, MAX_SPEED, unit='$/sec'),
|
maxcurrent = writable('', FloatRange(0, 2.8, unit='A', fmtstr='%.2f'),
|
||||||
3, SPEED_SCALE, readonly=True, group='motorparam')
|
default=1.4, group='motorparam')
|
||||||
maxcurrent = HwParam('_', FloatRange(0, 2.8, unit='A'),
|
standby_current = writable('', FloatRange(0, 2.8, unit='A', fmtstr='%.2f'),
|
||||||
6, CURRENT_SCALE, readonly=False, group='motorparam')
|
default=0.1, group='motorparam')
|
||||||
standby_current = HwParam('_', FloatRange(0, 2.8, unit='A'),
|
acceleration = writable('', FloatRange(4.6 * ACCEL_SCALE, MAX_ACCEL, unit='deg/s^2', fmtstr='%.1f'),
|
||||||
7, CURRENT_SCALE, readonly=False, group='motorparam')
|
default=150., group='motorparam')
|
||||||
acceleration = HwParam('_', FloatRange(4.6 * ACCEL_SCALE, MAX_ACCEL, unit='deg/s^2'),
|
target_reached = Parameter('', BoolType(), group='hwstatus')
|
||||||
5, ACCEL_SCALE, readonly=False, group='motorparam')
|
move_status = Parameter('', IntRange(0, 3), group='hwstatus')
|
||||||
target_reached = HwParam('_', BoolType(), 8, group='hwstatus')
|
error_bits = Parameter('', IntRange(0, 255), group='hwstatus')
|
||||||
move_status = HwParam('_', IntRange(0, 3),
|
free_wheeling = writable('', FloatRange(0, 60., unit='sec', fmtstr='%.2f'),
|
||||||
207, readonly=True, group='hwstatus')
|
default=0.1, group='motorparam')
|
||||||
error_bits = HwParam('_', IntRange(0, 255),
|
power_down_delay = writable('', FloatRange(0, 60., unit='sec', fmtstr='%.2f'),
|
||||||
208, readonly=True, group='hwstatus')
|
default=0.1, group='motorparam')
|
||||||
# the doc says msec, but I believe the scale is 10 msec
|
baudrate = Parameter('', EnumType({'%d' % v: i for i, v in enumerate(BAUDRATES)}),
|
||||||
free_wheeling = HwParam('_', FloatRange(0, 60., unit='sec'),
|
readonly=False, default=0, visibility=3, group='more')
|
||||||
204, 0.01, default=0.1, readonly=False, group='motorparam')
|
|
||||||
power_down_delay = HwParam('_', FloatRange(0, 60., unit='sec'),
|
|
||||||
214, 0.01, default=0.1, readonly=False, group='motorparam')
|
|
||||||
baudrate = Parameter('_', EnumType({'%d' % v: i for i, v in enumerate(BAUDRATES)}),
|
|
||||||
readonly=False, default=0, poll=True, visibility=3, group='more')
|
|
||||||
pollinterval = Parameter(group='more')
|
pollinterval = Parameter(group='more')
|
||||||
|
|
||||||
|
ioClass = BytesIO
|
||||||
iodevClass = BytesIO
|
fast_pollfactor = 0.001 # not used any more, TODO: use a statemachine for running
|
||||||
# fast_pollfactor = 0.001 # poll as fast as possible when busy
|
|
||||||
fast_pollfactor = 0.05
|
|
||||||
_started = 0
|
_started = 0
|
||||||
_calc_timeout = True
|
_calcTimeout = True
|
||||||
_need_reset = None
|
_need_reset = None
|
||||||
_last_change = 0
|
|
||||||
|
|
||||||
def comm(self, cmd, adr, value=0, bank=0):
|
def comm(self, cmd, adr, value=0, bank=0):
|
||||||
"""set or get a parameter
|
"""set or get a parameter
|
||||||
@ -138,21 +131,23 @@ class Motor(PersistentMixin, HasIodev, Drivable):
|
|||||||
:param value: if given, the parameter is written, else it is returned
|
:param value: if given, the parameter is written, else it is returned
|
||||||
:return: the returned value
|
:return: the returned value
|
||||||
"""
|
"""
|
||||||
if self._calc_timeout:
|
if self._calcTimeout and self.io._conn:
|
||||||
self._calc_timeout = False
|
self._calcTimeout = False
|
||||||
baudrate = getattr(self._iodev._conn.connection, 'baudrate', None)
|
baudrate = getattr(self.io._conn.connection, 'baudrate', None)
|
||||||
if baudrate:
|
if baudrate:
|
||||||
if baudrate not in BAUDRATES:
|
if baudrate not in BAUDRATES:
|
||||||
raise CommunicationFailedError('unsupported baud rate: %d' % baudrate)
|
raise CommunicationFailedError('unsupported baud rate: %d' % baudrate)
|
||||||
self._iodev.timeout = 0.03 + 200 / baudrate
|
self.io.timeout = 0.03 + 200 / baudrate
|
||||||
|
|
||||||
exc = None
|
exc = None
|
||||||
for itry in range(3,0,-1):
|
|
||||||
byt = struct.pack('>BBBBi', self.address, cmd, adr, bank, round(value))
|
byt = struct.pack('>BBBBi', self.address, cmd, adr, bank, round(value))
|
||||||
|
byt += bytes([sum(byt) & 0xff])
|
||||||
|
for itry in range(3,0,-1):
|
||||||
try:
|
try:
|
||||||
reply = self._iodev.communicate(byt + bytes([sum(byt) & 0xff]), 9)
|
reply = self.communicate(byt, 9)
|
||||||
if sum(reply[:-1]) & 0xff != reply[-1]:
|
if sum(reply[:-1]) & 0xff != reply[-1]:
|
||||||
raise CommunicationFailedError('checksum error')
|
raise CommunicationFailedError('checksum error')
|
||||||
|
# will try again
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if itry == 1:
|
if itry == 1:
|
||||||
raise
|
raise
|
||||||
@ -168,36 +163,18 @@ class Motor(PersistentMixin, HasIodev, Drivable):
|
|||||||
raise CommunicationFailedError('bad reply %r to command %s %d' % (reply, cmd, adr))
|
raise CommunicationFailedError('bad reply %r to command %s %d' % (reply, cmd, adr))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get(self, pname, **kwds):
|
def startModule(self, start_events):
|
||||||
"""get parameter"""
|
super().startModule(start_events)
|
||||||
pobj = self.parameters[pname]
|
|
||||||
value = self.comm(GET_AXIS_PAR, pobj.adr, **kwds)
|
|
||||||
# do not apply scale when 1 (datatype might not be float)
|
|
||||||
return value if pobj.scale == 1 else value * pobj.scale
|
|
||||||
|
|
||||||
def set(self, pname, value, check=True, **kwds):
|
def fix_encoder(self=self):
|
||||||
"""set parameter and check result"""
|
try:
|
||||||
pobj = self.parameters[pname]
|
|
||||||
scale = pobj.scale
|
|
||||||
rawvalue = round(value / scale)
|
|
||||||
for itry in range(2):
|
|
||||||
self.comm(SET_AXIS_PAR, pobj.adr, rawvalue, **kwds)
|
|
||||||
if check:
|
|
||||||
result = self.comm(GET_AXIS_PAR, pobj.adr, **kwds)
|
|
||||||
if result != rawvalue:
|
|
||||||
self.log.warning('result for %s does not match %d != %d, try again', pname, result, rawvalue)
|
|
||||||
continue
|
|
||||||
value = result * scale
|
|
||||||
return value
|
|
||||||
else:
|
|
||||||
raise HardwareError('result for %s does not match %d != %d' % (pname, result, rawvalue))
|
|
||||||
|
|
||||||
def startModule(self, started_callback):
|
|
||||||
# get encoder value from motor. at this stage self.encoder contains the persistent value
|
# get encoder value from motor. at this stage self.encoder contains the persistent value
|
||||||
encoder = self.get('encoder')
|
encoder = self._read_axispar(ENCODER_ADR, ANGLE_SCALE) + self.zero
|
||||||
encoder += self.zero
|
|
||||||
self.fix_encoder(encoder)
|
self.fix_encoder(encoder)
|
||||||
super().startModule(started_callback)
|
except Exception as e:
|
||||||
|
self.log.error('fix_encoder failed with %r', e)
|
||||||
|
|
||||||
|
start_events.queue(fix_encoder)
|
||||||
|
|
||||||
def fix_encoder(self, encoder_from_hw):
|
def fix_encoder(self, encoder_from_hw):
|
||||||
"""fix encoder value
|
"""fix encoder value
|
||||||
@ -211,14 +188,52 @@ class Motor(PersistentMixin, HasIodev, Drivable):
|
|||||||
# calculate nearest, most probable value
|
# calculate nearest, most probable value
|
||||||
adjusted_encoder = encoder_from_hw + round((self.encoder - encoder_from_hw) / 360.) * 360
|
adjusted_encoder = encoder_from_hw + round((self.encoder - encoder_from_hw) / 360.) * 360
|
||||||
if abs(self.encoder - adjusted_encoder) >= self.encoder_tolerance:
|
if abs(self.encoder - adjusted_encoder) >= self.encoder_tolerance:
|
||||||
# encoder module0 360 has changed
|
# encoder modulo 360 has changed
|
||||||
self.log.error('saved encoder value (%.2f) does not match reading (%.2f %.2f)',
|
self.log.error('saved encoder value (%.2f) does not match reading (%.2f %.2f)',
|
||||||
self.encoder, encoder_from_hw, adjusted_encoder)
|
self.encoder, encoder_from_hw, adjusted_encoder)
|
||||||
if adjusted_encoder != encoder_from_hw:
|
if adjusted_encoder != encoder_from_hw:
|
||||||
self.log.info('take next closest encoder value (%.2f)' % adjusted_encoder)
|
self.log.info('take next closest encoder value (%.2f)' % adjusted_encoder)
|
||||||
self._need_reset = True
|
self._need_reset = True
|
||||||
self.status = self.Status.ERROR, 'saved encoder value does not match reading'
|
self.status = self.Status.ERROR, 'saved encoder value does not match reading'
|
||||||
self.set('encoder', adjusted_encoder - self.zero, check=False)
|
self._write_axispar(adjusted_encoder - self.zero, ENCODER_ADR, ANGLE_SCALE, readback=False)
|
||||||
|
|
||||||
|
def _read_axispar(self, adr, scale=1):
|
||||||
|
value = self.comm(GET_AXIS_PAR, adr)
|
||||||
|
# do not apply scale when 1 (datatype might not be float)
|
||||||
|
return value if scale == 1 else value * scale
|
||||||
|
|
||||||
|
def _write_axispar(self, value, adr, scale=1, readback=True):
|
||||||
|
rawvalue = round(value / scale)
|
||||||
|
self.comm(SET_AXIS_PAR, adr, rawvalue)
|
||||||
|
if readback:
|
||||||
|
result = self.comm(GET_AXIS_PAR, adr)
|
||||||
|
if result != rawvalue:
|
||||||
|
raise HardwareError('result for adr=%d scale=%g does not match %g != %g'
|
||||||
|
% (adr, scale, result * scale, value))
|
||||||
|
return result * scale
|
||||||
|
return rawvalue * scale
|
||||||
|
|
||||||
|
@ReadHandler(HW_ARGS)
|
||||||
|
def read_hwparam(self, pname):
|
||||||
|
"""handle read for HwParam"""
|
||||||
|
args = HW_ARGS[pname]
|
||||||
|
reply = self._read_axispar(*args)
|
||||||
|
try:
|
||||||
|
value = getattr(self, pname)
|
||||||
|
except Exception:
|
||||||
|
return reply
|
||||||
|
if reply != value:
|
||||||
|
if not self.parameters[pname].readonly:
|
||||||
|
# this should not happen
|
||||||
|
self.log.warning('hw parameter %s has changed from %r to %r, write again', pname, value, reply)
|
||||||
|
self._write_axispar(value, *args, readback=False)
|
||||||
|
reply = self._read_axispar(*args)
|
||||||
|
return reply
|
||||||
|
|
||||||
|
@WriteHandler(HW_ARGS)
|
||||||
|
def write_hwparam(self, pname, value):
|
||||||
|
"""handler write for HwParam"""
|
||||||
|
return self._write_axispar(value, *HW_ARGS[pname])
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
encoder = self.read_encoder()
|
encoder = self.read_encoder()
|
||||||
@ -240,7 +255,7 @@ class Motor(PersistentMixin, HasIodev, Drivable):
|
|||||||
self._need_reset = True
|
self._need_reset = True
|
||||||
self.status = self.Status.ERROR, 'power loss'
|
self.status = self.Status.ERROR, 'power loss'
|
||||||
# or should we just fix instead of error status?
|
# or should we just fix instead of error status?
|
||||||
# self.set('steppos', self.steppos - self.zero, check=False)
|
# self._write_axispar(self.steppos - self.zero, readback=False)
|
||||||
self.comm(SET_GLOB_PAR, 255, 1, bank=2) # set initialized flag
|
self.comm(SET_GLOB_PAR, 255, 1, bank=2) # set initialized flag
|
||||||
self._started = 0
|
self._started = 0
|
||||||
|
|
||||||
@ -256,13 +271,10 @@ class Motor(PersistentMixin, HasIodev, Drivable):
|
|||||||
self.log.error('encoder (%.2f) does not match internal pos (%.2f)', self.encoder, self.steppos)
|
self.log.error('encoder (%.2f) does not match internal pos (%.2f)', self.encoder, self.steppos)
|
||||||
return self.Status.ERROR, 'encoder does not match internal pos'
|
return self.Status.ERROR, 'encoder does not match internal pos'
|
||||||
return self.status
|
return self.status
|
||||||
now = self.parameters['steppos'].timestamp
|
if oldpos != self.steppos or not (self.read_target_reached() or self.read_move_status()
|
||||||
if self.steppos != oldpos:
|
|
||||||
self._last_change = now
|
|
||||||
return self.Status.BUSY, 'moving'
|
|
||||||
if now < self._last_change + 0.2 and not (self.read_target_reached() or self.read_move_status()
|
|
||||||
or self.read_error_bits()):
|
or self.read_error_bits()):
|
||||||
return self.Status.BUSY, 'moving'
|
return self.Status.BUSY, 'moving'
|
||||||
|
# TODO: handle the different errors from move_status and error_bits
|
||||||
diff = self.target - self.encoder
|
diff = self.target - self.encoder
|
||||||
if abs(diff) <= self.tolerance:
|
if abs(diff) <= self.tolerance:
|
||||||
self._started = 0
|
self._started = 0
|
||||||
@ -273,9 +285,8 @@ class Motor(PersistentMixin, HasIodev, Drivable):
|
|||||||
|
|
||||||
def write_target(self, target):
|
def write_target(self, target):
|
||||||
self.read_value() # make sure encoder and steppos are fresh
|
self.read_value() # make sure encoder and steppos are fresh
|
||||||
if self.maxcurrent >= self.safe_current + CURRENT_SCALE and (
|
if abs(target - self.encoder) > self.move_limit:
|
||||||
abs(target - self.encoder) > self.move_limit + self.tolerance):
|
raise BadValueError('can not move more than %s deg' % self.move_limit)
|
||||||
raise BadValueError('can not move more than %s deg %g %g' % (self.move_limit, self.encoder, target))
|
|
||||||
diff = self.encoder - self.steppos
|
diff = self.encoder - self.steppos
|
||||||
if self._need_reset:
|
if self._need_reset:
|
||||||
raise HardwareError('need reset (%s)' % self.status[1])
|
raise HardwareError('need reset (%s)' % self.status[1])
|
||||||
@ -284,90 +295,28 @@ class Motor(PersistentMixin, HasIodev, Drivable):
|
|||||||
self._need_reset = True
|
self._need_reset = True
|
||||||
self.status = self.Status.ERROR, 'encoder does not match internal pos'
|
self.status = self.Status.ERROR, 'encoder does not match internal pos'
|
||||||
raise HardwareError('need reset (encoder does not match internal pos)')
|
raise HardwareError('need reset (encoder does not match internal pos)')
|
||||||
self.set('steppos', self.encoder - self.zero, check=False)
|
self._write_axispar(self.encoder - self.zero, STEPPOS_ADR, ANGLE_SCALE)
|
||||||
self._started = time.time()
|
self._started = time.time()
|
||||||
self.log.debug('move to %.1f', target)
|
self.log.info('move to %.1f', target)
|
||||||
self.comm(MOVE, 0, (target - self.zero) / ANGLE_SCALE)
|
self.comm(MOVE, 0, (target - self.zero) / ANGLE_SCALE)
|
||||||
self.status = self.Status.BUSY, 'changed target'
|
self.status = self.Status.BUSY, 'changed target'
|
||||||
return target
|
return target
|
||||||
|
|
||||||
def write_zero(self, value):
|
def write_zero(self, value):
|
||||||
diff = value - self.zero
|
self.zero = value
|
||||||
self.encoder += diff
|
self.read_value() # apply zero to encoder, steppos and value
|
||||||
self.steppos += diff
|
return Done
|
||||||
self.value += diff
|
|
||||||
return value
|
|
||||||
|
|
||||||
def read_encoder(self):
|
def read_encoder(self):
|
||||||
return self.get('encoder') + self.zero
|
return self._read_axispar(ENCODER_ADR, ANGLE_SCALE) + self.zero
|
||||||
|
|
||||||
def read_steppos(self):
|
def read_steppos(self):
|
||||||
return self.get('steppos') + self.zero
|
return self._read_axispar(STEPPOS_ADR, ANGLE_SCALE) + self.zero
|
||||||
|
|
||||||
def read_encoder_tolerance(self):
|
|
||||||
return self.get('encoder_tolerance')
|
|
||||||
|
|
||||||
def write_encoder_tolerance(self, value):
|
|
||||||
return self.set('encoder_tolerance', value)
|
|
||||||
|
|
||||||
def read_target_reached(self):
|
|
||||||
return self.get('target_reached')
|
|
||||||
|
|
||||||
def read_speed(self):
|
|
||||||
return self.get('speed')
|
|
||||||
|
|
||||||
def write_speed(self, value):
|
|
||||||
return self.set('speed', value)
|
|
||||||
|
|
||||||
def read_minspeed(self):
|
|
||||||
return self.get('minspeed')
|
|
||||||
|
|
||||||
def write_minspeed(self, value):
|
|
||||||
return self.set('minspeed', value)
|
|
||||||
|
|
||||||
def read_currentspeed(self):
|
|
||||||
return self.get('currentspeed')
|
|
||||||
|
|
||||||
def read_acceleration(self):
|
|
||||||
return self.get('acceleration')
|
|
||||||
|
|
||||||
def write_acceleration(self, value):
|
|
||||||
return self.set('acceleration', value)
|
|
||||||
|
|
||||||
def read_maxcurrent(self):
|
|
||||||
return self.get('maxcurrent')
|
|
||||||
|
|
||||||
def write_maxcurrent(self, value):
|
|
||||||
return self.set('maxcurrent', value)
|
|
||||||
|
|
||||||
def read_standby_current(self):
|
|
||||||
return self.get('standby_current')
|
|
||||||
|
|
||||||
def write_standby_current(self, value):
|
|
||||||
return self.set('standby_current', value)
|
|
||||||
|
|
||||||
def read_free_wheeling(self):
|
|
||||||
return self.get('free_wheeling')
|
|
||||||
|
|
||||||
def write_free_wheeling(self, value):
|
|
||||||
return self.set('free_wheeling', value)
|
|
||||||
|
|
||||||
def read_power_down_delay(self):
|
|
||||||
return self.get('power_down_delay')
|
|
||||||
|
|
||||||
def write_power_down_delay(self, value):
|
|
||||||
return self.set('power_down_delay', value)
|
|
||||||
|
|
||||||
def read_move_status(self):
|
|
||||||
return self.get('move_status')
|
|
||||||
|
|
||||||
def read_error_bits(self):
|
|
||||||
return self.get('error_bits')
|
|
||||||
|
|
||||||
@Command(FloatRange())
|
@Command(FloatRange())
|
||||||
def set_zero(self, value):
|
def set_zero(self, value):
|
||||||
raw = self.read_value() - self.zero
|
"""adjust zero"""
|
||||||
self.write_zero(value - raw)
|
self.write_zero(value - self.read_value())
|
||||||
|
|
||||||
def read_baudrate(self):
|
def read_baudrate(self):
|
||||||
return self.comm(GET_GLOB_PAR, 65)
|
return self.comm(GET_GLOB_PAR, 65)
|
||||||
@ -380,14 +329,14 @@ class Motor(PersistentMixin, HasIodev, Drivable):
|
|||||||
"""set steppos to encoder value, if not within tolerance"""
|
"""set steppos to encoder value, if not within tolerance"""
|
||||||
if self._started:
|
if self._started:
|
||||||
raise IsBusyError('can not reset while moving')
|
raise IsBusyError('can not reset while moving')
|
||||||
tol = ENCODER_RESOLUTION
|
tol = ENCODER_RESOLUTION * 1.1
|
||||||
for itry in range(10):
|
for itry in range(10):
|
||||||
diff = self.read_encoder() - self.read_steppos()
|
diff = self.read_encoder() - self.read_steppos()
|
||||||
if abs(diff) <= tol:
|
if abs(diff) <= tol:
|
||||||
self._need_reset = False
|
self._need_reset = False
|
||||||
self.status = self.Status.IDLE, 'ok'
|
self.status = self.Status.IDLE, 'ok'
|
||||||
return
|
return
|
||||||
self.set('steppos', self.encoder - self.zero, check=False)
|
self._write_axispar(self.encoder - self.zero, STEPPOS_ADR, ANGLE_SCALE, readback=False)
|
||||||
self.comm(MOVE, 0, (self.encoder - self.zero) / ANGLE_SCALE)
|
self.comm(MOVE, 0, (self.encoder - self.zero) / ANGLE_SCALE)
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
if itry > 5:
|
if itry > 5:
|
||||||
@ -397,23 +346,33 @@ class Motor(PersistentMixin, HasIodev, Drivable):
|
|||||||
|
|
||||||
@Command()
|
@Command()
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
"""stop motor immediately"""
|
||||||
self.comm(MOTOR_STOP, 0)
|
self.comm(MOTOR_STOP, 0)
|
||||||
self.status = self.Status.IDLE, 'stopped'
|
self.status = self.Status.IDLE, 'stopped'
|
||||||
self.target = self.value # indicate to customers that this was stopped
|
|
||||||
self._started = 0
|
self._started = 0
|
||||||
|
|
||||||
#@Command()
|
@Command()
|
||||||
#def step(self):
|
def step_forward(self):
|
||||||
# self.comm(MOVE, 1, FULL_STEP / ANGLE_SCALE)
|
"""move one full step forwards
|
||||||
|
|
||||||
#@Command()
|
for quick tests
|
||||||
#def back(self):
|
"""
|
||||||
# self.comm(MOVE, 1, - FULL_STEP / ANGLE_SCALE)
|
self.comm(MOVE, 1, FULL_STEP / ANGLE_SCALE)
|
||||||
|
|
||||||
#@Command(IntRange(), result=IntRange())
|
@Command()
|
||||||
#def get_axis_par(self, adr):
|
def step_back(self):
|
||||||
# return self.comm(GET_AXIS_PAR, adr)
|
"""move one full step backwards
|
||||||
|
|
||||||
#@Command((IntRange(), FloatRange()), result=IntRange())
|
for quick tests
|
||||||
#def set_axis_par(self, adr, value):
|
"""
|
||||||
# return self.comm(SET_AXIS_PAR, adr, value)
|
self.comm(MOVE, 1, - FULL_STEP / ANGLE_SCALE)
|
||||||
|
|
||||||
|
@Command(IntRange(), result=IntRange())
|
||||||
|
def get_axis_par(self, adr):
|
||||||
|
"""get arbitrary motor parameter"""
|
||||||
|
return self.comm(GET_AXIS_PAR, adr)
|
||||||
|
|
||||||
|
@Command((IntRange(), IntRange()), result=IntRange())
|
||||||
|
def set_axis_par(self, adr, value):
|
||||||
|
"""set arbitrary motor parameter"""
|
||||||
|
return self.comm(SET_AXIS_PAR, adr, value)
|
||||||
|
@ -107,7 +107,7 @@ class Uniax(PersistentMixin, Drivable):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def next_action(self, action, do_now=True):
|
def next_action(self, action):
|
||||||
"""call next action
|
"""call next action
|
||||||
|
|
||||||
:param action: function to be called next time
|
:param action: function to be called next time
|
||||||
@ -116,8 +116,6 @@ class Uniax(PersistentMixin, Drivable):
|
|||||||
self._action = action
|
self._action = action
|
||||||
self._init_action = True
|
self._init_action = True
|
||||||
self.log.info('action %r', action.__name__)
|
self.log.info('action %r', action.__name__)
|
||||||
if do_now:
|
|
||||||
self._next_cycle = False
|
|
||||||
|
|
||||||
def init_action(self):
|
def init_action(self):
|
||||||
"""return true when called the first time after next_action"""
|
"""return true when called the first time after next_action"""
|
||||||
@ -126,13 +124,6 @@ class Uniax(PersistentMixin, Drivable):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def execute_action(self):
|
|
||||||
for _ in range(5): # limit number of subsequent actions in one cycle
|
|
||||||
self._next_cycle = True
|
|
||||||
self._action(self.value, self.target)
|
|
||||||
if self._next_cycle:
|
|
||||||
break
|
|
||||||
|
|
||||||
def zero_pos(self, value,):
|
def zero_pos(self, value,):
|
||||||
"""get high_pos or low_pos, depending on sign of value
|
"""get high_pos or low_pos, depending on sign of value
|
||||||
|
|
||||||
@ -173,11 +164,11 @@ class Uniax(PersistentMixin, Drivable):
|
|||||||
self.log.info('motor stopped - substantial force detected: %g', force)
|
self.log.info('motor stopped - substantial force detected: %g', force)
|
||||||
self._motor.stop()
|
self._motor.stop()
|
||||||
elif self.init_action():
|
elif self.init_action():
|
||||||
self.next_action(self.adjust, True)
|
self.next_action(self.adjust)
|
||||||
return
|
return
|
||||||
if abs(force) > self.hysteresis:
|
if abs(force) > self.hysteresis:
|
||||||
self.set_zero_pos(force, self._motor.read_value())
|
self.set_zero_pos(force, self._motor.read_value())
|
||||||
self.next_action(self.adjust, True)
|
self.next_action(self.adjust)
|
||||||
return
|
return
|
||||||
if force * sign < -self.hysteresis:
|
if force * sign < -self.hysteresis:
|
||||||
self._previous_force = force
|
self._previous_force = force
|
||||||
@ -333,7 +324,7 @@ class Uniax(PersistentMixin, Drivable):
|
|||||||
return Done
|
return Done
|
||||||
if self.zero_pos(force) is None and abs(force) > self.hysteresis and self._filtered:
|
if self.zero_pos(force) is None and abs(force) > self.hysteresis and self._filtered:
|
||||||
self.set_zero_pos(force, self._motor.read_value())
|
self.set_zero_pos(force, self._motor.read_value())
|
||||||
self.execute_action()
|
self._action(self.value, self.target)
|
||||||
return Done
|
return Done
|
||||||
|
|
||||||
def write_target(self, target):
|
def write_target(self, target):
|
||||||
@ -351,7 +342,10 @@ class Uniax(PersistentMixin, Drivable):
|
|||||||
self._cnt_rderr = 0
|
self._cnt_rderr = 0
|
||||||
self._cnt_wrerr = 0
|
self._cnt_wrerr = 0
|
||||||
self.status = 'BUSY', 'changed target'
|
self.status = 'BUSY', 'changed target'
|
||||||
self.next_action(self.find, False)
|
if self.value * math.copysign(1, target) > self.hysteresis:
|
||||||
|
self.next_action(self.adjust)
|
||||||
|
else:
|
||||||
|
self.next_action(self.find)
|
||||||
return target
|
return target
|
||||||
|
|
||||||
@Command()
|
@Command()
|
||||||
|
80
test/test_attach.py
Normal file
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, \
|
from secop.datatypes import ArrayOf, BLOBType, BoolType, \
|
||||||
CommandType, ConfigError, DataType, Enum, EnumType, FloatRange, \
|
CommandType, ConfigError, DataType, Enum, EnumType, FloatRange, \
|
||||||
IntRange, ProgrammingError, ScaledInteger, StatusType, \
|
IntRange, ProgrammingError, ScaledInteger, StatusType, \
|
||||||
StringType, StructOf, TextType, TupleOf, get_datatype
|
StringType, StructOf, TextType, TupleOf, get_datatype, \
|
||||||
|
DiscouragedConversion
|
||||||
|
from secop.lib import generalConfig
|
||||||
|
|
||||||
|
|
||||||
def copytest(dt):
|
def copytest(dt):
|
||||||
@ -36,6 +38,7 @@ def copytest(dt):
|
|||||||
assert dt.export_datatype() == dt.copy().export_datatype()
|
assert dt.export_datatype() == dt.copy().export_datatype()
|
||||||
assert dt != dt.copy()
|
assert dt != dt.copy()
|
||||||
|
|
||||||
|
|
||||||
def test_DataType():
|
def test_DataType():
|
||||||
dt = DataType()
|
dt = DataType()
|
||||||
with pytest.raises(NotImplementedError):
|
with pytest.raises(NotImplementedError):
|
||||||
@ -116,7 +119,6 @@ def test_IntRange():
|
|||||||
dt('1.3')
|
dt('1.3')
|
||||||
dt(1)
|
dt(1)
|
||||||
dt(0)
|
dt(0)
|
||||||
dt('1')
|
|
||||||
with pytest.raises(ProgrammingError):
|
with pytest.raises(ProgrammingError):
|
||||||
IntRange('xc', 'Yx')
|
IntRange('xc', 'Yx')
|
||||||
|
|
||||||
@ -132,6 +134,7 @@ def test_IntRange():
|
|||||||
with pytest.raises(ConfigError):
|
with pytest.raises(ConfigError):
|
||||||
dt.checkProperties()
|
dt.checkProperties()
|
||||||
|
|
||||||
|
|
||||||
def test_ScaledInteger():
|
def test_ScaledInteger():
|
||||||
dt = ScaledInteger(0.01, -3, 3)
|
dt = ScaledInteger(0.01, -3, 3)
|
||||||
copytest(dt)
|
copytest(dt)
|
||||||
@ -407,6 +410,7 @@ def test_ArrayOf():
|
|||||||
dt = ArrayOf(EnumType('myenum', single=0), 5)
|
dt = ArrayOf(EnumType('myenum', single=0), 5)
|
||||||
copytest(dt)
|
copytest(dt)
|
||||||
|
|
||||||
|
|
||||||
def test_TupleOf():
|
def test_TupleOf():
|
||||||
# test constructor catching illegal arguments
|
# test constructor catching illegal arguments
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
@ -641,6 +645,7 @@ def test_oneway_compatible(dt, contained_in):
|
|||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
contained_in.compatible(dt)
|
contained_in.compatible(dt)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('dt1, dt2', [
|
@pytest.mark.parametrize('dt1, dt2', [
|
||||||
(FloatRange(-5.5, 5.5), ScaledInteger(10, -5.5, 5.5)),
|
(FloatRange(-5.5, 5.5), ScaledInteger(10, -5.5, 5.5)),
|
||||||
(IntRange(0,1), BoolType()),
|
(IntRange(0,1), BoolType()),
|
||||||
@ -650,6 +655,7 @@ def test_twoway_compatible(dt1, dt2):
|
|||||||
dt1.compatible(dt1)
|
dt1.compatible(dt1)
|
||||||
dt2.compatible(dt2)
|
dt2.compatible(dt2)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('dt1, dt2', [
|
@pytest.mark.parametrize('dt1, dt2', [
|
||||||
(StringType(), FloatRange()),
|
(StringType(), FloatRange()),
|
||||||
(IntRange(-10, 10), StringType()),
|
(IntRange(-10, 10), StringType()),
|
||||||
@ -665,3 +671,12 @@ def test_incompatible(dt1, dt2):
|
|||||||
dt1.compatible(dt2)
|
dt1.compatible(dt2)
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
dt2.compatible(dt1)
|
dt2.compatible(dt1)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('dt', [FloatRange(), IntRange(), ScaledInteger(1)])
|
||||||
|
def test_lazy_validation(dt):
|
||||||
|
generalConfig.defaults['lazy_number_validation'] = True
|
||||||
|
dt('0')
|
||||||
|
generalConfig.defaults['lazy_number_validation'] = False
|
||||||
|
with pytest.raises(DiscouragedConversion):
|
||||||
|
dt('0')
|
||||||
|
234
test/test_handler.py
Normal file
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:
|
class DispatcherStub:
|
||||||
OMIT_UNCHANGED_WITHIN = 0
|
# the first update from the poller comes a very short time after the
|
||||||
|
# initial value from the timestamp. However, in the test below
|
||||||
|
# the second update happens after the updates dict is cleared
|
||||||
|
# -> we have to inhibit the 'omit unchanged update' feature
|
||||||
|
omit_unchanged_within = 0
|
||||||
|
|
||||||
def __init__(self, updates):
|
def __init__(self, updates):
|
||||||
self.updates = updates
|
self.updates = updates
|
||||||
@ -108,6 +112,7 @@ def test_IOHandler():
|
|||||||
group1 = Hdl('group1', 'SIMPLE?', '%g')
|
group1 = Hdl('group1', 'SIMPLE?', '%g')
|
||||||
group2 = Hdl('group2', 'CMD?%(channel)d', '%g,%s,%d')
|
group2 = Hdl('group2', 'CMD?%(channel)d', '%g,%s,%d')
|
||||||
|
|
||||||
|
|
||||||
class Module1(Module):
|
class Module1(Module):
|
||||||
channel = Property('the channel', IntRange(), default=3)
|
channel = Property('the channel', IntRange(), default=3)
|
||||||
loop = Property('the loop', IntRange(), default=2)
|
loop = Property('the loop', IntRange(), default=2)
|
||||||
@ -115,7 +120,7 @@ def test_IOHandler():
|
|||||||
real = Parameter('a float value', FloatRange(), default=12.3, handler=group2, readonly=False)
|
real = Parameter('a float value', FloatRange(), default=12.3, handler=group2, readonly=False)
|
||||||
text = Parameter('a string value', StringType(), default='x', handler=group2, readonly=False)
|
text = Parameter('a string value', StringType(), default='x', handler=group2, readonly=False)
|
||||||
|
|
||||||
def sendRecv(self, command):
|
def communicate(self, command):
|
||||||
assert data.pop('command') == command
|
assert data.pop('command') == command
|
||||||
return data.pop('reply')
|
return data.pop('reply')
|
||||||
|
|
||||||
@ -141,7 +146,7 @@ def test_IOHandler():
|
|||||||
print(updates)
|
print(updates)
|
||||||
updates.clear() # get rid of updates from initialisation
|
updates.clear() # get rid of updates from initialisation
|
||||||
|
|
||||||
# for sendRecv
|
# for communicate
|
||||||
data.push('command', 'SIMPLE?')
|
data.push('command', 'SIMPLE?')
|
||||||
data.push('reply', '4.51')
|
data.push('reply', '4.51')
|
||||||
# for analyze_group1
|
# for analyze_group1
|
||||||
@ -154,7 +159,7 @@ def test_IOHandler():
|
|||||||
assert updates.pop('simple') == 45.1
|
assert updates.pop('simple') == 45.1
|
||||||
assert not updates
|
assert not updates
|
||||||
|
|
||||||
# for sendRecv
|
# for communicate
|
||||||
data.push('command', 'CMD?3')
|
data.push('command', 'CMD?3')
|
||||||
data.push('reply', '1.23,text,5')
|
data.push('reply', '1.23,text,5')
|
||||||
# for analyze_group2
|
# for analyze_group2
|
||||||
@ -167,7 +172,7 @@ def test_IOHandler():
|
|||||||
assert data.empty()
|
assert data.empty()
|
||||||
assert not updates
|
assert not updates
|
||||||
|
|
||||||
# for sendRecv
|
# for communicate
|
||||||
data.push('command', 'CMD?3')
|
data.push('command', 'CMD?3')
|
||||||
data.push('reply', '1.23,text,5')
|
data.push('reply', '1.23,text,5')
|
||||||
# for analyze_group2
|
# for analyze_group2
|
||||||
@ -178,7 +183,7 @@ def test_IOHandler():
|
|||||||
data.push('self', 12.3, 'string')
|
data.push('self', 12.3, 'string')
|
||||||
data.push('new', 12.3, 'FOO')
|
data.push('new', 12.3, 'FOO')
|
||||||
data.push('changed', 1.23, 'foo', 9)
|
data.push('changed', 1.23, 'foo', 9)
|
||||||
# for sendRecv
|
# for communicate
|
||||||
data.push('command', 'CMD 3,1.23,foo,9|CMD?3')
|
data.push('command', 'CMD 3,1.23,foo,9|CMD?3')
|
||||||
data.push('reply', '1.23,foo,9')
|
data.push('reply', '1.23,foo,9')
|
||||||
# for analyze_group2
|
# for analyze_group2
|
||||||
|
@ -64,6 +64,7 @@ def test_EnumMember():
|
|||||||
assert a != 3
|
assert a != 3
|
||||||
assert a == 1
|
assert a == 1
|
||||||
|
|
||||||
|
|
||||||
def test_Enum():
|
def test_Enum():
|
||||||
e1 = Enum('e1')
|
e1 = Enum('e1')
|
||||||
e2 = Enum('e2', e1, a=1, b=3)
|
e2 = Enum('e2', e1, a=1, b=3)
|
||||||
@ -75,3 +76,4 @@ def test_Enum():
|
|||||||
assert e2.b > e3.a
|
assert e2.b > e3.a
|
||||||
assert e3.c >= e2.a
|
assert e3.c >= e2.a
|
||||||
assert e3.b <= e2.b
|
assert e3.b <= e2.b
|
||||||
|
assert Enum({'self': 0, 'other': 1})('self') == 0
|
||||||
|
274
test/test_logging.py
Normal file
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."""
|
"""test data types."""
|
||||||
|
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from secop.datatypes import BoolType, FloatRange, StringType, IntRange, CommandType
|
from secop.datatypes import BoolType, FloatRange, StringType, IntRange, ScaledInteger
|
||||||
from secop.errors import ProgrammingError, ConfigError
|
from secop.errors import ProgrammingError, ConfigError
|
||||||
from secop.modules import Communicator, Drivable, Readable, Module
|
from secop.modules import Communicator, Drivable, Readable, Module
|
||||||
from secop.params import Command, Parameter
|
from secop.params import Command, Parameter
|
||||||
from secop.poller import BasicPoller
|
from secop.rwhandler import ReadHandler, WriteHandler, nopoll
|
||||||
|
from secop.lib import generalConfig
|
||||||
|
|
||||||
|
|
||||||
class DispatcherStub:
|
class DispatcherStub:
|
||||||
OMIT_UNCHANGED_WITHIN = 0
|
# the first update from the poller comes a very short time after the
|
||||||
|
# initial value from the timestamp. However, in the test below
|
||||||
|
# the second update happens after the updates dict is cleared
|
||||||
|
# -> we have to inhibit the 'omit unchanged update' feature
|
||||||
|
omit_unchanged_within = 0
|
||||||
|
|
||||||
def __init__(self, updates):
|
def __init__(self, updates):
|
||||||
self.updates = updates
|
self.updates = updates
|
||||||
@ -48,9 +53,10 @@ class DispatcherStub:
|
|||||||
|
|
||||||
|
|
||||||
class LoggerStub:
|
class LoggerStub:
|
||||||
def debug(self, *args):
|
def debug(self, fmt, *args):
|
||||||
print(*args)
|
print(fmt % args)
|
||||||
info = warning = exception = debug
|
info = warning = exception = error = debug
|
||||||
|
handlers = []
|
||||||
|
|
||||||
|
|
||||||
logger = LoggerStub()
|
logger = LoggerStub()
|
||||||
@ -61,13 +67,23 @@ class ServerStub:
|
|||||||
self.dispatcher = DispatcherStub(updates)
|
self.dispatcher = DispatcherStub(updates)
|
||||||
|
|
||||||
|
|
||||||
|
class DummyMultiEvent(threading.Event):
|
||||||
|
def get_trigger(self):
|
||||||
|
|
||||||
|
def trigger(event=self):
|
||||||
|
event.set()
|
||||||
|
sys.exit()
|
||||||
|
return trigger
|
||||||
|
|
||||||
|
|
||||||
def test_Communicator():
|
def test_Communicator():
|
||||||
o = Communicator('communicator', LoggerStub(), {'.description': ''}, ServerStub({}))
|
o = Communicator('communicator', LoggerStub(), {'.description': ''}, ServerStub({}))
|
||||||
o.earlyInit()
|
o.earlyInit()
|
||||||
o.initModule()
|
o.initModule()
|
||||||
event = threading.Event()
|
event = DummyMultiEvent()
|
||||||
o.startModule(event.set)
|
o.initModule()
|
||||||
assert event.is_set() # event should be set immediately
|
o.startModule(event)
|
||||||
|
assert event.wait(timeout=0.1)
|
||||||
|
|
||||||
|
|
||||||
def test_ModuleMagic():
|
def test_ModuleMagic():
|
||||||
@ -83,14 +99,13 @@ def test_ModuleMagic():
|
|||||||
a1 = Parameter('a1', datatype=BoolType(), default=False)
|
a1 = Parameter('a1', datatype=BoolType(), default=False)
|
||||||
a2 = Parameter('a2', datatype=BoolType(), default=True)
|
a2 = Parameter('a2', datatype=BoolType(), default=True)
|
||||||
value = Parameter(datatype=StringType(), default='first')
|
value = Parameter(datatype=StringType(), default='first')
|
||||||
|
target = Parameter(datatype=StringType(), default='')
|
||||||
|
|
||||||
@Command(argument=BoolType(), result=BoolType())
|
@Command(argument=BoolType(), result=BoolType())
|
||||||
def cmd2(self, arg):
|
def cmd2(self, arg):
|
||||||
"""another stuff"""
|
"""another stuff"""
|
||||||
return not arg
|
return not arg
|
||||||
|
|
||||||
pollerClass = BasicPoller
|
|
||||||
|
|
||||||
def read_param1(self):
|
def read_param1(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -100,12 +115,16 @@ def test_ModuleMagic():
|
|||||||
def read_a1(self):
|
def read_a1(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@nopoll
|
||||||
def read_a2(self):
|
def read_a2(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
return 'second'
|
return 'second'
|
||||||
|
|
||||||
|
def read_status(self):
|
||||||
|
return 'IDLE', 'ok'
|
||||||
|
|
||||||
with pytest.raises(ProgrammingError):
|
with pytest.raises(ProgrammingError):
|
||||||
class Mod1(Module): # pylint: disable=unused-variable
|
class Mod1(Module): # pylint: disable=unused-variable
|
||||||
def do_this(self): # old style command
|
def do_this(self): # old style command
|
||||||
@ -128,15 +147,19 @@ def test_ModuleMagic():
|
|||||||
return arg
|
return arg
|
||||||
|
|
||||||
value = Parameter(datatype=FloatRange(unit='deg'))
|
value = Parameter(datatype=FloatRange(unit='deg'))
|
||||||
|
target = Parameter(datatype=FloatRange(), default=0)
|
||||||
a1 = Parameter(datatype=FloatRange(unit='$/s'), readonly=False)
|
a1 = Parameter(datatype=FloatRange(unit='$/s'), readonly=False)
|
||||||
b2 = Parameter('<b2>', datatype=BoolType(), default=True,
|
# remark: it might be a programming error to override the datatype
|
||||||
poll=True, readonly=False, initwrite=True)
|
# and not overriding the read_* method. This is not checked!
|
||||||
|
b2 = Parameter('<b2>', datatype=StringType(), default='empty',
|
||||||
|
readonly=False, initwrite=True)
|
||||||
|
|
||||||
def write_a1(self, value):
|
def write_a1(self, value):
|
||||||
self._a1_written = value
|
self._a1_written = value
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def write_b2(self, value):
|
def write_b2(self, value):
|
||||||
|
value = value.upper()
|
||||||
self._b2_written = value
|
self._b2_written = value
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@ -166,33 +189,38 @@ def test_ModuleMagic():
|
|||||||
|
|
||||||
# check for inital updates working properly
|
# check for inital updates working properly
|
||||||
o1 = Newclass1('o1', logger, {'.description':''}, srv)
|
o1 = Newclass1('o1', logger, {'.description':''}, srv)
|
||||||
expectedBeforeStart = {'target': 0.0, 'status': (Drivable.Status.IDLE, ''),
|
expectedBeforeStart = {'target': '', 'status': (Drivable.Status.IDLE, ''),
|
||||||
'param1': False, 'param2': 1.0, 'a1': 0.0, 'a2': True, 'pollinterval': 5.0,
|
'param1': False, 'param2': 1.0, 'a1': 0.0, 'a2': True, 'pollinterval': 5.0,
|
||||||
'value': 'first'}
|
'value': 'first'}
|
||||||
assert updates.pop('o1') == expectedBeforeStart
|
assert updates.pop('o1') == expectedBeforeStart
|
||||||
o1.earlyInit()
|
o1.earlyInit()
|
||||||
event = threading.Event()
|
event = DummyMultiEvent()
|
||||||
o1.startModule(event.set)
|
o1.initModule()
|
||||||
event.wait()
|
o1.startModule(event)
|
||||||
|
assert event.wait(timeout=0.1)
|
||||||
# should contain polled values
|
# should contain polled values
|
||||||
expectedAfterStart = {'status': (Drivable.Status.IDLE, ''),
|
expectedAfterStart = {
|
||||||
'value': 'second'}
|
'status': (Drivable.Status.IDLE, 'ok'), 'value': 'second',
|
||||||
|
'param1': True, 'param2': 0.0, 'a1': True}
|
||||||
assert updates.pop('o1') == expectedAfterStart
|
assert updates.pop('o1') == expectedAfterStart
|
||||||
|
|
||||||
# check in addition if parameters are written
|
# check in addition if parameters are written
|
||||||
o2 = Newclass2('o2', logger, {'.description':'', 'a1': 2.7}, srv)
|
o2 = Newclass2('o2', logger, {'.description':'', 'a1': 2.7}, srv)
|
||||||
# no update for b2, as this has to be written
|
# no update for b2, as this has to be written
|
||||||
expectedBeforeStart['a1'] = 2.7
|
expectedBeforeStart['a1'] = 2.7
|
||||||
|
expectedBeforeStart['target'] = 0.0
|
||||||
assert updates.pop('o2') == expectedBeforeStart
|
assert updates.pop('o2') == expectedBeforeStart
|
||||||
o2.earlyInit()
|
o2.earlyInit()
|
||||||
event = threading.Event()
|
event = DummyMultiEvent()
|
||||||
o2.startModule(event.set)
|
o2.initModule()
|
||||||
event.wait()
|
o2.startModule(event)
|
||||||
|
assert event.wait(timeout=0.1)
|
||||||
# value has changed type, b2 and a1 are written
|
# value has changed type, b2 and a1 are written
|
||||||
expectedAfterStart.update(value=0, b2=True, a1=2.7)
|
expectedAfterStart.update(value=0, b2='EMPTY', a1=True)
|
||||||
|
# ramerk: a1=True: this behaviour is a Porgamming error
|
||||||
assert updates.pop('o2') == expectedAfterStart
|
assert updates.pop('o2') == expectedAfterStart
|
||||||
assert o2._a1_written == 2.7
|
assert o2._a1_written == 2.7
|
||||||
assert o2._b2_written is True
|
assert o2._b2_written == 'EMPTY'
|
||||||
|
|
||||||
assert not updates
|
assert not updates
|
||||||
|
|
||||||
@ -206,13 +234,15 @@ def test_ModuleMagic():
|
|||||||
# check '$' in unit works properly
|
# check '$' in unit works properly
|
||||||
assert o2.parameters['a1'].datatype.unit == 'mm/s'
|
assert o2.parameters['a1'].datatype.unit == 'mm/s'
|
||||||
cfg = Newclass2.configurables
|
cfg = Newclass2.configurables
|
||||||
assert set(cfg.keys()) == {'export', 'group', 'description',
|
assert set(cfg.keys()) == {
|
||||||
|
'export', 'group', 'description', 'disable_value_range_check',
|
||||||
'meaning', 'visibility', 'implementation', 'interface_classes', 'target', 'stop',
|
'meaning', 'visibility', 'implementation', 'interface_classes', 'target', 'stop',
|
||||||
'status', 'param1', 'param2', 'cmd', 'a2', 'pollinterval', 'b2', 'cmd2', 'value',
|
'status', 'param1', 'param2', 'cmd', 'a2', 'pollinterval', 'slowinterval', 'b2',
|
||||||
'a1'}
|
'cmd2', 'value', 'a1'}
|
||||||
assert set(cfg['value'].keys()) == {'group', 'export', 'relative_resolution',
|
assert set(cfg['value'].keys()) == {
|
||||||
|
'group', 'export', 'relative_resolution',
|
||||||
'visibility', 'unit', 'default', 'datatype', 'fmtstr',
|
'visibility', 'unit', 'default', 'datatype', 'fmtstr',
|
||||||
'absolute_resolution', 'poll', 'max', 'min', 'readonly', 'constant',
|
'absolute_resolution', 'max', 'min', 'readonly', 'constant',
|
||||||
'description', 'needscfg'}
|
'description', 'needscfg'}
|
||||||
|
|
||||||
# check on the level of classes
|
# check on the level of classes
|
||||||
@ -260,9 +290,101 @@ def test_param_inheritance():
|
|||||||
Base('o', logger, {'description': ''}, srv)
|
Base('o', logger, {'description': ''}, srv)
|
||||||
|
|
||||||
|
|
||||||
def test_mixin():
|
def test_command_inheritance():
|
||||||
# srv = ServerStub({})
|
class Base(Module):
|
||||||
|
@Command(BoolType(), visibility=2)
|
||||||
|
def cmd(self, arg):
|
||||||
|
"""base"""
|
||||||
|
|
||||||
|
class Sub1(Base):
|
||||||
|
@Command(group='grp')
|
||||||
|
def cmd(self, arg):
|
||||||
|
"""first"""
|
||||||
|
|
||||||
|
class Sub2(Sub1):
|
||||||
|
@Command(None, result=BoolType())
|
||||||
|
def cmd(self): # pylint: disable=arguments-differ
|
||||||
|
"""second"""
|
||||||
|
|
||||||
|
class Sub3(Base):
|
||||||
|
# when either argument or result is given, the other one is assumed to be None
|
||||||
|
# i.e. here we override the argument with None
|
||||||
|
@Command(result=FloatRange())
|
||||||
|
def cmd(self, arg):
|
||||||
|
"""third"""
|
||||||
|
|
||||||
|
assert Sub1.accessibles['cmd'].for_export() == {
|
||||||
|
'description': 'first', 'group': 'grp', 'visibility': 2,
|
||||||
|
'datainfo': {'type': 'command', 'argument': {'type': 'bool'}}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert Sub2.accessibles['cmd'].for_export() == {
|
||||||
|
'description': 'second', 'group': 'grp', 'visibility': 2,
|
||||||
|
'datainfo': {'type': 'command', 'result': {'type': 'bool'}}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert Sub3.accessibles['cmd'].for_export() == {
|
||||||
|
'description': 'third', 'visibility': 2,
|
||||||
|
'datainfo': {'type': 'command', 'result': {'type': 'double'}}
|
||||||
|
}
|
||||||
|
|
||||||
|
for cls in locals().values():
|
||||||
|
if hasattr(cls, 'accessibles'):
|
||||||
|
for p in cls.accessibles.values():
|
||||||
|
assert isinstance(p.ownProperties, dict)
|
||||||
|
assert p.copy().ownProperties == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_command_check():
|
||||||
|
srv = ServerStub({})
|
||||||
|
|
||||||
|
class Good(Module):
|
||||||
|
@Command(description='available')
|
||||||
|
def with_description(self):
|
||||||
|
pass
|
||||||
|
@Command()
|
||||||
|
def with_docstring(self):
|
||||||
|
"""docstring"""
|
||||||
|
|
||||||
|
Good('o', logger, {'description': ''}, srv)
|
||||||
|
|
||||||
|
class Bad1(Module):
|
||||||
|
@Command
|
||||||
|
def without_description(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Bad2(Module):
|
||||||
|
@Command()
|
||||||
|
def without_description(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
for cls in Bad1, Bad2:
|
||||||
|
with pytest.raises(ConfigError) as e_info:
|
||||||
|
cls('o', logger, {'description': ''}, srv)
|
||||||
|
assert 'description' in repr(e_info.value)
|
||||||
|
|
||||||
|
class BadDatatype(Module):
|
||||||
|
@Command(FloatRange(0.1, 0.9), result=FloatRange())
|
||||||
|
def cmd(self):
|
||||||
|
"""valid command"""
|
||||||
|
|
||||||
|
BadDatatype('o', logger, {'description': ''}, srv)
|
||||||
|
|
||||||
|
# test for command property checking
|
||||||
|
with pytest.raises(ProgrammingError):
|
||||||
|
BadDatatype('o', logger, {
|
||||||
|
'description': '',
|
||||||
|
'cmd.argument': {'type': 'double', 'min': 1, 'max': 0},
|
||||||
|
}, srv)
|
||||||
|
|
||||||
|
with pytest.raises(ProgrammingError):
|
||||||
|
BadDatatype('o', logger, {
|
||||||
|
'description': '',
|
||||||
|
'cmd.visibility': 'invalid',
|
||||||
|
}, srv)
|
||||||
|
|
||||||
|
|
||||||
|
def test_mixin():
|
||||||
class Mixin: # no need to inherit from Module or HasAccessible
|
class Mixin: # no need to inherit from Module or HasAccessible
|
||||||
value = Parameter(unit='K') # missing datatype and description acceptable in mixins
|
value = Parameter(unit='K') # missing datatype and description acceptable in mixins
|
||||||
param1 = Parameter('no datatype yet', fmtstr='%.5f')
|
param1 = Parameter('no datatype yet', fmtstr='%.5f')
|
||||||
@ -276,7 +398,7 @@ def test_mixin():
|
|||||||
param1 = Parameter(datatype=FloatRange())
|
param1 = Parameter(datatype=FloatRange())
|
||||||
|
|
||||||
with pytest.raises(ProgrammingError):
|
with pytest.raises(ProgrammingError):
|
||||||
class MixedModule(Mixin):
|
class MixedModule(Mixin): # pylint: disable=unused-variable
|
||||||
param1 = Parameter('', FloatRange(), fmtstr=0) # fmtstr must be a string
|
param1 = Parameter('', FloatRange(), fmtstr=0) # fmtstr must be a string
|
||||||
|
|
||||||
assert repr(MixedDrivable.status.datatype) == repr(Drivable.status.datatype)
|
assert repr(MixedDrivable.status.datatype) == repr(Drivable.status.datatype)
|
||||||
@ -305,10 +427,29 @@ def test_mixin():
|
|||||||
}, srv)
|
}, srv)
|
||||||
|
|
||||||
|
|
||||||
|
def test_override():
|
||||||
|
class Mod(Drivable):
|
||||||
|
value = 5 # overriding the default value
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""no decorator needed"""
|
||||||
|
|
||||||
|
assert Mod.value.default == 5
|
||||||
|
assert Mod.stop.description == "no decorator needed"
|
||||||
|
|
||||||
|
class Mod2(Drivable):
|
||||||
|
@Command()
|
||||||
|
def stop(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert Mod2.stop.description == Drivable.stop.description
|
||||||
|
|
||||||
|
|
||||||
def test_command_config():
|
def test_command_config():
|
||||||
class Mod(Module):
|
class Mod(Module):
|
||||||
@Command(IntRange(0, 1), result=IntRange(0, 1))
|
@Command(IntRange(0, 1), result=IntRange(0, 1))
|
||||||
def convert(self, value):
|
def convert(self, value):
|
||||||
|
"""dummy conversion"""
|
||||||
return value
|
return value
|
||||||
|
|
||||||
srv = ServerStub({})
|
srv = ServerStub({})
|
||||||
@ -332,3 +473,189 @@ def test_command_config():
|
|||||||
'result': {'type': 'bool'},
|
'result': {'type': 'bool'},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_command_none():
|
||||||
|
srv = ServerStub({})
|
||||||
|
|
||||||
|
class Mod(Drivable):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Mod2(Drivable):
|
||||||
|
stop = None
|
||||||
|
|
||||||
|
assert 'stop' in Mod('o', logger, {'description': ''}, srv).accessibles
|
||||||
|
assert 'stop' not in Mod2('o', logger, {'description': ''}, srv).accessibles
|
||||||
|
|
||||||
|
|
||||||
|
def test_bad_method():
|
||||||
|
class Mod0(Drivable): # pylint: disable=unused-variable
|
||||||
|
def write_target(self, value):
|
||||||
|
pass
|
||||||
|
|
||||||
|
with pytest.raises(ProgrammingError):
|
||||||
|
class Mod1(Drivable): # pylint: disable=unused-variable
|
||||||
|
def write_taget(self, value):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Mod2(Drivable): # pylint: disable=unused-variable
|
||||||
|
def read_value(self, value):
|
||||||
|
pass
|
||||||
|
|
||||||
|
with pytest.raises(ProgrammingError):
|
||||||
|
class Mod3(Drivable): # pylint: disable=unused-variable
|
||||||
|
def read_valu(self, value):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_generic_access():
|
||||||
|
class Mod(Module):
|
||||||
|
param = Parameter('handled param', StringType(), readonly=False)
|
||||||
|
unhandled = Parameter('unhandled param', StringType(), default='', readonly=False)
|
||||||
|
data = {'param': ''}
|
||||||
|
|
||||||
|
@ReadHandler(['param'])
|
||||||
|
def read_handler(self, pname):
|
||||||
|
value = self.data[pname]
|
||||||
|
setattr(self, pname, value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
@WriteHandler(['param'])
|
||||||
|
def write_handler(self, pname, value):
|
||||||
|
value = value.lower()
|
||||||
|
self.data[pname] = value
|
||||||
|
setattr(self, pname, value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
updates = {}
|
||||||
|
srv = ServerStub(updates)
|
||||||
|
|
||||||
|
obj = Mod('obj', logger, {'description': '', 'param': 'initial value'}, srv)
|
||||||
|
assert obj.param == 'initial value'
|
||||||
|
assert obj.write_param('Cheese') == 'cheese'
|
||||||
|
assert obj.write_unhandled('Cheese') == 'Cheese'
|
||||||
|
assert updates == {'obj': {'param': 'cheese', 'unhandled': 'Cheese'}}
|
||||||
|
updates.clear()
|
||||||
|
assert obj.write_param('Potato') == 'potato'
|
||||||
|
assert updates == {'obj': {'param': 'potato'}}
|
||||||
|
updates.clear()
|
||||||
|
assert obj.read_param() == 'potato'
|
||||||
|
assert obj.read_unhandled()
|
||||||
|
assert updates == {'obj': {'param': 'potato'}}
|
||||||
|
updates.clear()
|
||||||
|
assert updates == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_duplicate_handler_name():
|
||||||
|
with pytest.raises(ProgrammingError):
|
||||||
|
class Mod(Module): # pylint: disable=unused-variable
|
||||||
|
param = Parameter('handled param', StringType(), readonly=False)
|
||||||
|
|
||||||
|
@ReadHandler(['param'])
|
||||||
|
def handler(self, pname):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@WriteHandler(['param'])
|
||||||
|
def handler(self, pname, value): # pylint: disable=function-redefined
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_handler_overwrites_method():
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
class Mod1(Module): # pylint: disable=unused-variable
|
||||||
|
param = Parameter('handled param', StringType(), readonly=False)
|
||||||
|
|
||||||
|
@ReadHandler(['param'])
|
||||||
|
def read_handler(self, pname):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def read_param(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
class Mod2(Module): # pylint: disable=unused-variable
|
||||||
|
param = Parameter('handled param', StringType(), readonly=False)
|
||||||
|
|
||||||
|
@WriteHandler(['param'])
|
||||||
|
def write_handler(self, pname, value):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def write_param(self, value):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_read_write():
|
||||||
|
class Mod(Module):
|
||||||
|
param = Parameter('test param', StringType(), readonly=False)
|
||||||
|
|
||||||
|
updates = {}
|
||||||
|
srv = ServerStub(updates)
|
||||||
|
|
||||||
|
obj = Mod('obj', logger, {'description': '', 'param': 'cheese'}, srv)
|
||||||
|
assert obj.param == 'cheese'
|
||||||
|
assert obj.read_param() == 'cheese'
|
||||||
|
assert updates == {'obj': {'param': 'cheese'}}
|
||||||
|
assert obj.write_param('egg') == 'egg'
|
||||||
|
assert obj.param == 'egg'
|
||||||
|
assert updates == {'obj': {'param': 'egg'}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_incompatible_value_target():
|
||||||
|
class Mod1(Drivable):
|
||||||
|
value = Parameter('', FloatRange(0, 10), default=0)
|
||||||
|
target = Parameter('', FloatRange(0, 11), default=0)
|
||||||
|
|
||||||
|
class Mod2(Drivable):
|
||||||
|
value = Parameter('', FloatRange(), default=0)
|
||||||
|
target = Parameter('', StringType(), default='')
|
||||||
|
|
||||||
|
class Mod3(Drivable):
|
||||||
|
value = Parameter('', FloatRange(), default=0)
|
||||||
|
target = Parameter('', ScaledInteger(1, 0, 10), default=0)
|
||||||
|
|
||||||
|
srv = ServerStub({})
|
||||||
|
|
||||||
|
with pytest.raises(ConfigError):
|
||||||
|
obj = Mod1('obj', logger, {'description': ''}, srv) # pylint: disable=unused-variable
|
||||||
|
|
||||||
|
with pytest.raises(ProgrammingError):
|
||||||
|
obj = Mod2('obj', logger, {'description': ''}, srv)
|
||||||
|
|
||||||
|
obj = Mod3('obj', logger, {'description': ''}, srv)
|
||||||
|
|
||||||
|
|
||||||
|
def test_problematic_value_range():
|
||||||
|
class Mod(Drivable):
|
||||||
|
value = Parameter('', FloatRange(0, 10), default=0)
|
||||||
|
target = Parameter('', FloatRange(0, 10), default=0)
|
||||||
|
|
||||||
|
srv = ServerStub({})
|
||||||
|
|
||||||
|
obj = Mod('obj', logger, {'description': '', 'value.max': 10.1}, srv) # pylint: disable=unused-variable
|
||||||
|
|
||||||
|
with pytest.raises(ConfigError):
|
||||||
|
obj = Mod('obj', logger, {'description': ''}, srv)
|
||||||
|
|
||||||
|
class Mod2(Drivable):
|
||||||
|
value = Parameter('', FloatRange(), default=0)
|
||||||
|
target = Parameter('', FloatRange(), default=0)
|
||||||
|
|
||||||
|
obj = Mod2('obj', logger, {'description': ''}, srv)
|
||||||
|
obj = Mod2('obj', logger, {'description': '', 'target.min': 0, 'target.max': 10}, srv)
|
||||||
|
|
||||||
|
with pytest.raises(ConfigError):
|
||||||
|
obj = Mod('obj', logger, {
|
||||||
|
'value.min': 0, 'value.max': 10,
|
||||||
|
'target.min': 0, 'target.max': 10, 'description': ''}, srv)
|
||||||
|
|
||||||
|
obj = Mod('obj', logger, {'disable_value_range_check': True,
|
||||||
|
'value.min': 0, 'value.max': 10,
|
||||||
|
'target.min': 0, 'target.max': 10, 'description': ''}, srv)
|
||||||
|
|
||||||
|
generalConfig.defaults['disable_value_range_check'] = True
|
||||||
|
|
||||||
|
class Mod4(Drivable):
|
||||||
|
value = Parameter('', FloatRange(0, 10), default=0)
|
||||||
|
target = Parameter('', FloatRange(0, 10), default=0)
|
||||||
|
obj = Mod4('obj', logger, {
|
||||||
|
'value.min': 0, 'value.max': 10,
|
||||||
|
'target.min': 0, 'target.max': 10, 'description': ''}, srv)
|
||||||
|
60
test/test_multievent.py
Normal file
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 id(Mod.p3) == id(Base.p3)
|
||||||
assert repr(Mod.p2) == repr(Base.p2) # must be a clone
|
assert repr(Mod.p2) == repr(Base.p2) # must be a clone
|
||||||
assert repr(Mod.p3) == repr(Base.p3) # must be a clone
|
assert repr(Mod.p3) == repr(Base.p3) # must be a clone
|
||||||
assert Mod.p1.default == True
|
assert Mod.p1.default is True
|
||||||
# manipulating default makes Base.p1 and Mod.p1 match
|
# manipulating default makes Base.p1 and Mod.p1 match
|
||||||
Mod.p1.default = False
|
Mod.p1.default = False
|
||||||
assert repr(Mod.p1) == repr(Base.p1)
|
assert repr(Mod.p1) == repr(Base.p1)
|
||||||
|
|
||||||
|
for cls in locals().values():
|
||||||
|
if hasattr(cls, 'accessibles'):
|
||||||
|
for p in cls.accessibles.values():
|
||||||
|
assert isinstance(p.ownProperties, dict)
|
||||||
|
assert p.copy().ownProperties == {}
|
||||||
|
|
||||||
|
|
||||||
def test_Export():
|
def test_Export():
|
||||||
class Mod(HasAccessibles):
|
class Mod(HasAccessibles):
|
||||||
|
@ -21,227 +21,133 @@
|
|||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
"""test poller."""
|
"""test poller."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
from time import time as current_time
|
||||||
import time
|
import time
|
||||||
from collections import OrderedDict
|
import logging
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from secop.modules import Drivable
|
from secop.core import Module, Parameter, FloatRange, Readable, ReadHandler, nopoll
|
||||||
from secop.poller import DYNAMIC, REGULAR, SLOW, Poller
|
from secop.lib.multievent import MultiEvent
|
||||||
|
|
||||||
Status = Drivable.Status
|
|
||||||
|
|
||||||
class Time:
|
class Time:
|
||||||
STARTTIME = 1000 # artificial time zero
|
"""artificial time, forwarded on sleep instead of waiting"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.reset()
|
self.offset = 0
|
||||||
self.finish = float('inf')
|
|
||||||
self.stop = lambda : None
|
|
||||||
self.commtime = 0.05 # time needed for 1 poll
|
|
||||||
|
|
||||||
def reset(self, lifetime=10):
|
|
||||||
self.seconds = self.STARTTIME
|
|
||||||
self.idletime = 0.0
|
|
||||||
self.busytime = 0.0
|
|
||||||
self.finish = self.STARTTIME + lifetime
|
|
||||||
|
|
||||||
def time(self):
|
def time(self):
|
||||||
if self.seconds > self.finish:
|
return current_time() + self.offset
|
||||||
self.finish = float('inf')
|
|
||||||
self.stop()
|
|
||||||
return self.seconds
|
|
||||||
|
|
||||||
def sleep(self, seconds):
|
def sleep(self, seconds):
|
||||||
assert 0 <= seconds <= 24*3600
|
assert 0 <= seconds <= 24*3600
|
||||||
self.idletime += seconds
|
self.offset += seconds
|
||||||
self.seconds += seconds
|
|
||||||
|
|
||||||
def busy(self, seconds):
|
|
||||||
assert seconds >= 0
|
|
||||||
self.seconds += seconds
|
|
||||||
self.busytime += seconds
|
|
||||||
|
|
||||||
artime = Time() # artificial test time
|
artime = Time() # artificial test time
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
|
||||||
def patch_time(monkeypatch):
|
|
||||||
monkeypatch.setattr(time, 'time', artime.time)
|
|
||||||
|
|
||||||
|
class DispatcherStub:
|
||||||
|
maxcycles = 10
|
||||||
|
|
||||||
class Event:
|
def announce_update(self, modulename, pname, pobj):
|
||||||
def __init__(self):
|
|
||||||
self.flag = False
|
|
||||||
|
|
||||||
def wait(self, timeout):
|
|
||||||
artime.sleep(max(0,timeout))
|
|
||||||
|
|
||||||
def set(self):
|
|
||||||
self.flag = True
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
self.flag = False
|
|
||||||
|
|
||||||
def is_set(self):
|
|
||||||
return self.flag
|
|
||||||
|
|
||||||
|
|
||||||
class Parameter:
|
|
||||||
def __init__(self, name, readonly, poll, polltype, interval):
|
|
||||||
self.poll = poll
|
|
||||||
self.polltype = polltype # used for check only
|
|
||||||
self.export = name
|
|
||||||
self.readonly = readonly
|
|
||||||
self.interval = interval
|
|
||||||
self.timestamp = 0
|
|
||||||
self.handler = None
|
|
||||||
self.reset()
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
self.cnt = 0
|
|
||||||
self.span = 0
|
|
||||||
self.maxspan = 0
|
|
||||||
|
|
||||||
def rfunc(self):
|
|
||||||
artime.busy(artime.commtime)
|
|
||||||
now = artime.time()
|
now = artime.time()
|
||||||
self.span = now - self.timestamp
|
if hasattr(pobj, 'stat'):
|
||||||
self.maxspan = max(self.maxspan, self.span)
|
pobj.stat.append(now)
|
||||||
self.timestamp = now
|
else:
|
||||||
self.cnt += 1
|
pobj.stat = [now]
|
||||||
|
self.maxcycles -= 1
|
||||||
|
if self.maxcycles <= 0:
|
||||||
|
self.finish_event.set()
|
||||||
|
sys.exit() # stop thread
|
||||||
|
|
||||||
|
|
||||||
|
class ServerStub:
|
||||||
|
def __init__(self):
|
||||||
|
self.dispatcher = DispatcherStub()
|
||||||
|
|
||||||
|
|
||||||
|
class Base(Module):
|
||||||
|
def __init__(self):
|
||||||
|
srv = ServerStub()
|
||||||
|
super().__init__('mod', logging.getLogger('dummy'), dict(description=''), srv)
|
||||||
|
self.dispatcher = srv.dispatcher
|
||||||
|
|
||||||
|
def run(self, maxcycles):
|
||||||
|
self.dispatcher.maxcycles = maxcycles
|
||||||
|
self.dispatcher.finish_event = threading.Event()
|
||||||
|
self.initModule()
|
||||||
|
|
||||||
|
def wait(timeout=None, base=self.triggerPoll):
|
||||||
|
"""simplified simulation
|
||||||
|
|
||||||
|
when an event is already set return True, else forward artificial time
|
||||||
|
"""
|
||||||
|
if base.is_set():
|
||||||
return True
|
return True
|
||||||
|
artime.sleep(max(0.0, 99.9 if timeout is None else timeout))
|
||||||
|
return base.is_set()
|
||||||
|
|
||||||
def __repr__(self):
|
self.triggerPoll.wait = wait
|
||||||
return 'Parameter(%s)' % ", ".join("%s=%r" % item for item in self.__dict__.items())
|
self.startModule(MultiEvent())
|
||||||
|
assert self.dispatcher.finish_event.wait(1)
|
||||||
|
|
||||||
|
|
||||||
class Module:
|
class Mod1(Base, Readable):
|
||||||
properties = {}
|
param1 = Parameter('', FloatRange())
|
||||||
pollerClass = Poller
|
param2 = Parameter('', FloatRange())
|
||||||
iodev = 'common_iodev'
|
param3 = Parameter('', FloatRange())
|
||||||
def __init__(self, name, pollinterval=5, fastfactor=0.25, slowfactor=4, busy=False,
|
param4 = Parameter('', FloatRange())
|
||||||
counts=(), auto=None):
|
|
||||||
'''create a dummy module
|
|
||||||
|
|
||||||
nauto, ndynamic, nregular, nslow are the number of parameters of each polltype
|
@ReadHandler(('param1', 'param2', 'param3'))
|
||||||
'''
|
def read_param(self, name):
|
||||||
self.pollinterval = pollinterval
|
artime.sleep(1.0)
|
||||||
self.fast_pollfactor = fastfactor
|
return 0
|
||||||
self.slow_pollfactor = slowfactor
|
|
||||||
self.parameters = OrderedDict()
|
|
||||||
self.name = name
|
|
||||||
self.is_busy = busy
|
|
||||||
if auto is not None:
|
|
||||||
self.pvalue = self.addPar('value', True, auto or DYNAMIC, DYNAMIC)
|
|
||||||
# readonly = False should not matter:
|
|
||||||
self.pstatus = self.addPar('status', False, auto or DYNAMIC, DYNAMIC)
|
|
||||||
self.pregular = self.addPar('regular', True, auto or REGULAR, REGULAR)
|
|
||||||
self.pslow = self.addPar('slow', False, auto or SLOW, SLOW)
|
|
||||||
self.addPar('notpolled', True, False, 0)
|
|
||||||
self.counts = 'auto'
|
|
||||||
else:
|
|
||||||
ndynamic, nregular, nslow = counts
|
|
||||||
for i in range(ndynamic):
|
|
||||||
self.addPar('%s:d%d' % (name, i), True, DYNAMIC, DYNAMIC)
|
|
||||||
for i in range(nregular):
|
|
||||||
self.addPar('%s:r%d' % (name, i), True, REGULAR, REGULAR)
|
|
||||||
for i in range(nslow):
|
|
||||||
self.addPar('%s:s%d' % (name, i), False, SLOW, SLOW)
|
|
||||||
self.counts = counts
|
|
||||||
|
|
||||||
def addPar(self, name, readonly, poll, expected_polltype):
|
@nopoll
|
||||||
# self.count[polltype] += 1
|
def read_param4(self):
|
||||||
expected_interval = self.pollinterval
|
return 0
|
||||||
if expected_polltype == SLOW:
|
|
||||||
expected_interval *= self.slow_pollfactor
|
|
||||||
elif expected_polltype == DYNAMIC and self.is_busy:
|
|
||||||
expected_interval *= self.fast_pollfactor
|
|
||||||
pobj = Parameter(name, readonly, poll, expected_polltype, expected_interval)
|
|
||||||
setattr(self, 'read_' + pobj.export, pobj.rfunc)
|
|
||||||
self.parameters[pobj.export] = pobj
|
|
||||||
return pobj
|
|
||||||
|
|
||||||
def isBusy(self):
|
def read_status(self):
|
||||||
return self.is_busy
|
artime.sleep(1.0)
|
||||||
|
return 0
|
||||||
|
|
||||||
def pollOneParam(self, pname):
|
def read_value(self):
|
||||||
getattr(self, 'read_' + pname)()
|
artime.sleep(1.0)
|
||||||
|
return 0
|
||||||
|
|
||||||
def writeInitParams(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __repr__(self):
|
@pytest.mark.parametrize(
|
||||||
rdict = self.__dict__.copy()
|
'ncycles, pollinterval, slowinterval, mspan, pspan',
|
||||||
rdict.pop('parameters')
|
[ # normal case:
|
||||||
return 'Module(%r, counts=%r, f=%r, pollinterval=%g, is_busy=%r)' % (self.name,
|
( 60, 5, 15, (4.9, 5.1), (14, 16)),
|
||||||
self.counts, (self.fast_pollfactor, self.slow_pollfactor, 1),
|
# pollinterval faster then reading: mspan max ~ 3 s (polls of value, status and ONE other parameter)
|
||||||
self.pollinterval, self.is_busy)
|
( 60, 1, 5, (0.9, 3.1), (5, 17)),
|
||||||
|
])
|
||||||
module_list = [
|
def test_poll(ncycles, pollinterval, slowinterval, mspan, pspan, monkeypatch):
|
||||||
[Module('x', 3.0, 0.125, 10, False, auto=True),
|
monkeypatch.setattr(time, 'time', artime.time)
|
||||||
Module('y', 3.0, 0.125, 10, False, auto=False)],
|
m = Mod1()
|
||||||
[Module('a', 1.0, 0.25, 4, True, (5, 5, 10)),
|
m.pollinterval = pollinterval
|
||||||
Module('b', 2.0, 0.25, 4, True, (5, 5, 50))],
|
m.slowInterval = slowinterval
|
||||||
[Module('c', 1.0, 0.25, 4, False, (5, 0, 0))],
|
m.run(ncycles)
|
||||||
[Module('d', 1.0, 0.25, 4, True, (0, 9, 0))],
|
assert not hasattr(m.parameters['param4'], 'stat')
|
||||||
[Module('e', 1.0, 0.25, 4, True, (0, 0, 9))],
|
for pname in ['value', 'status']:
|
||||||
[Module('f', 1.0, 0.25, 4, True, (0, 0, 0))],
|
pobj = m.parameters[pname]
|
||||||
]
|
lowcnt = 0
|
||||||
@pytest.mark.parametrize('modules', module_list)
|
print(pname, [t2 - t1 for t1, t2 in zip(pobj.stat[1:], pobj.stat[2:-1])])
|
||||||
def test_Poller(modules):
|
for t1, t2 in zip(pobj.stat[1:], pobj.stat[2:-1]):
|
||||||
# check for proper timing
|
if t2 - t1 < mspan[0]:
|
||||||
|
lowcnt += 1
|
||||||
for overloaded in False, True:
|
assert t2 - t1 <= mspan[1]
|
||||||
artime.reset()
|
assert lowcnt <= 2
|
||||||
count = {DYNAMIC: 0, REGULAR: 0, SLOW: 0}
|
for pname in ['param1', 'param2', 'param3']:
|
||||||
maxspan = {DYNAMIC: 0, REGULAR: 0, SLOW: 0}
|
pobj = m.parameters[pname]
|
||||||
pollTable = dict()
|
lowcnt = 0
|
||||||
for module in modules:
|
print(pname, [t2 - t1 for t1, t2 in zip(pobj.stat[1:], pobj.stat[2:-1])])
|
||||||
Poller.add_to_table(pollTable, module)
|
for t1, t2 in zip(pobj.stat[1:], pobj.stat[2:-1]):
|
||||||
for pobj in module.parameters.values():
|
if t2 - t1 < pspan[0]:
|
||||||
if pobj.poll:
|
lowcnt += 1
|
||||||
maxspan[pobj.polltype] = max(maxspan[pobj.polltype], pobj.interval)
|
assert t2 - t1 <= pspan[1]
|
||||||
count[pobj.polltype] += 1
|
assert lowcnt <= 2
|
||||||
pobj.reset()
|
|
||||||
assert len(pollTable) == 1
|
|
||||||
poller = pollTable[(Poller, 'common_iodev')]
|
|
||||||
artime.stop = poller.stop
|
|
||||||
poller._event = Event() # patch Event.wait
|
|
||||||
|
|
||||||
assert (sum(count.values()) > 0) == bool(poller)
|
|
||||||
|
|
||||||
def started_callback(modules=modules):
|
|
||||||
for module in modules:
|
|
||||||
for pobj in module.parameters.values():
|
|
||||||
assert pobj.cnt == bool(pobj.poll) # all parameters have to be polled once
|
|
||||||
pobj.reset() # set maxspan and cnt to 0
|
|
||||||
|
|
||||||
if overloaded:
|
|
||||||
# overloaded scenario
|
|
||||||
artime.commtime = 1.0
|
|
||||||
ncycles = 10
|
|
||||||
if count[SLOW] > 0:
|
|
||||||
cycletime = (count[REGULAR] + 1) * count[SLOW] * 2
|
|
||||||
else:
|
|
||||||
cycletime = max(count[REGULAR], count[DYNAMIC]) * 2
|
|
||||||
artime.reset(cycletime * ncycles * 1.01) # poller will quit given time
|
|
||||||
poller.run(started_callback)
|
|
||||||
total = artime.time() - artime.STARTTIME
|
|
||||||
for module in modules:
|
|
||||||
for pobj in module.parameters.values():
|
|
||||||
if pobj.poll:
|
|
||||||
# average_span = total / (pobj.cnt + 1)
|
|
||||||
assert total / (pobj.cnt + 1) <= max(cycletime, pobj.interval * 1.1)
|
|
||||||
else:
|
|
||||||
# normal scenario
|
|
||||||
artime.commtime = 0.001
|
|
||||||
artime.reset(max(maxspan.values()) * 5) # poller will quit given time
|
|
||||||
poller.run(started_callback)
|
|
||||||
total = artime.time() - artime.STARTTIME
|
|
||||||
for module in modules:
|
|
||||||
for pobj in module.parameters.values():
|
|
||||||
if pobj.poll:
|
|
||||||
assert pobj.cnt > 0
|
|
||||||
assert pobj.maxspan <= maxspan[pobj.polltype] * 1.1
|
|
||||||
assert (pobj.cnt + 1) * pobj.interval >= total * 0.99
|
|
||||||
assert abs(pobj.span - pobj.interval) < 0.01
|
|
||||||
pobj.reset()
|
|
||||||
|
@ -26,6 +26,7 @@ import pytest
|
|||||||
from secop.datatypes import FloatRange, IntRange, StringType, ValueType
|
from secop.datatypes import FloatRange, IntRange, StringType, ValueType
|
||||||
from secop.errors import BadValueError, ConfigError, ProgrammingError
|
from secop.errors import BadValueError, ConfigError, ProgrammingError
|
||||||
from secop.properties import HasProperties, Property
|
from secop.properties import HasProperties, Property
|
||||||
|
from secop.core import Parameter
|
||||||
|
|
||||||
|
|
||||||
def Prop(*args, name=None, **kwds):
|
def Prop(*args, name=None, **kwds):
|
||||||
@ -38,10 +39,10 @@ V_test_Property = [
|
|||||||
[Prop(StringType(), 'default', extname='extname', mandatory=False),
|
[Prop(StringType(), 'default', extname='extname', mandatory=False),
|
||||||
dict(default='default', extname='extname', export=True, mandatory=False)
|
dict(default='default', extname='extname', export=True, mandatory=False)
|
||||||
],
|
],
|
||||||
[Prop(IntRange(), '42', export=True, name='custom', mandatory=True),
|
[Prop(IntRange(), 42, export=True, name='custom', mandatory=True),
|
||||||
dict(default=42, extname='_custom', export=True, mandatory=True),
|
dict(default=42, extname='_custom', export=True, mandatory=True),
|
||||||
],
|
],
|
||||||
[Prop(IntRange(), '42', export=True, name='name'),
|
[Prop(IntRange(), 42, export=True, name='name'),
|
||||||
dict(default=42, extname='_name', export=True, mandatory=False)
|
dict(default=42, extname='_name', export=True, mandatory=False)
|
||||||
],
|
],
|
||||||
[Prop(IntRange(), 42, '_extname', mandatory=True),
|
[Prop(IntRange(), 42, '_extname', mandatory=True),
|
||||||
@ -85,12 +86,12 @@ def test_Property_basic():
|
|||||||
Property('')
|
Property('')
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
Property('', 1)
|
Property('', 1)
|
||||||
Property('', IntRange(), '42', 'extname', False, False)
|
Property('', IntRange(), 42, 'extname', False, False)
|
||||||
|
|
||||||
|
|
||||||
def test_Properties():
|
def test_Properties():
|
||||||
class Cls(HasProperties):
|
class Cls(HasProperties):
|
||||||
aa = Property('', IntRange(0, 99), '42', export=True)
|
aa = Property('', IntRange(0, 99), 42, export=True)
|
||||||
bb = Property('', IntRange(), 0, export=False)
|
bb = Property('', IntRange(), 0, export=False)
|
||||||
|
|
||||||
assert Cls.aa.default == 42
|
assert Cls.aa.default == 42
|
||||||
@ -155,30 +156,38 @@ def test_Property_override():
|
|||||||
assert 'collides with' in str(e.value)
|
assert 'collides with' in str(e.value)
|
||||||
|
|
||||||
with pytest.raises(ProgrammingError) as e:
|
with pytest.raises(ProgrammingError) as e:
|
||||||
class cz(c): # pylint: disable=unused-variable
|
class cy(c): # pylint: disable=unused-variable
|
||||||
a = 's'
|
a = 's'
|
||||||
|
|
||||||
assert 'can not set' in str(e.value)
|
assert 'can not set' in str(e.value)
|
||||||
|
|
||||||
|
with pytest.raises(ProgrammingError) as e:
|
||||||
|
class cz(c): # pylint: disable=unused-variable
|
||||||
|
a = 's'
|
||||||
|
|
||||||
|
class cp(c): # pylint: disable=unused-variable
|
||||||
|
# overriding a Property with a Parameter is allowed
|
||||||
|
a = Parameter('x', IntRange())
|
||||||
|
|
||||||
|
|
||||||
def test_Properties_mro():
|
def test_Properties_mro():
|
||||||
class A(HasProperties):
|
class Base(HasProperties):
|
||||||
p = Property('base', StringType(), 'base', export='always')
|
prop = Property('base', StringType(), 'base', export='always')
|
||||||
|
|
||||||
class B(A):
|
class SubA(Base):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class C(A):
|
class SubB(Base):
|
||||||
p = Property('sub', FloatRange(), extname='p')
|
prop = Property('sub', FloatRange(), extname='prop')
|
||||||
|
|
||||||
class D(C, B):
|
class FinalBA(SubB, SubA):
|
||||||
p = 1
|
prop = 1
|
||||||
|
|
||||||
class E(B, C):
|
class FinalAB(SubA, SubB):
|
||||||
p = 2
|
prop = 2
|
||||||
|
|
||||||
assert B().exportProperties() == {'_p': 'base'}
|
assert SubA().exportProperties() == {'_prop': 'base'}
|
||||||
assert D().exportProperties() == {'p': 1.0}
|
assert FinalBA().exportProperties() == {'prop': 1.0}
|
||||||
# in an older implementation the following would fail, as B.p is constructed first
|
# in an older implementation the following would fail, as SubA.p is constructed first
|
||||||
# and then B.p overrides C.p
|
# and then SubA.p overrides SubB.p
|
||||||
assert E().exportProperties() == {'p': 2.0}
|
assert FinalAB().exportProperties() == {'prop': 2.0}
|
||||||
|
148
test/test_statemachine.py
Normal file
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