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

This commit is contained in:
l_samenv 2022-12-02 09:00:12 +01:00
commit b1608c4d7f
19 changed files with 1693 additions and 355 deletions

52
cfg/main/flamp.cfg Normal file
View File

@ -0,0 +1,52 @@
[NODE]
description = lamp oven control (from manuel knecht)
id = flamp.config.sea.psi.ch
[sea_main]
class = secop_psi.sea.SeaClient
description = main sea connection for flamp.config
config = flamp.config
service = main
[tt]
class = secop_psi.sea.SeaDrivable
io = sea_main
sea_object = tt
rel_paths = . t1
[t2]
class = secop_psi.sea.SeaReadable
io = sea_main
sea_object = tt
rel_paths = t2
[current]
class = secop_psi.sea.SeaReadable
io = sea_main
sea_object = current
extra_modules = i1, i2, i3, i4
[i1]
class = secop_psi.sea.SeaReadable
io = sea_main
single_module = current.i1
[i2]
class = secop_psi.sea.SeaReadable
io = sea_main
single_module = current.i2
[i3]
class = secop_psi.sea.SeaReadable
io = sea_main
single_module = current.i3
[i4]
class = secop_psi.sea.SeaReadable
io = sea_main
single_module = current.i4
[pv]
class = secop_psi.sea.SeaReadable
io = sea_main
sea_object = pv

21
cfg/main/fs.cfg Normal file
View File

@ -0,0 +1,21 @@
[NODE]
description = small furnace
id = fs.config.sea.psi.ch
[sea_main]
class = secop_psi.sea.SeaClient
description = main sea connection for fs.config
config = fs.config
service = main
[tt]
class = secop_psi.sea.SeaDrivable
io = sea_main
sea_object = tt
rel_paths = . tm
[ts]
class = secop_psi.sea.SeaDrivable
io = sea_main
sea_object = tt
rel_paths = ts

22
cfg/main/ft.cfg Normal file
View File

@ -0,0 +1,22 @@
[NODE]
description = FT tantalum furnace (1400 K)
id = ft.config.sea.psi.ch
[sea_main]
class = secop_psi.sea.SeaClient
description = main sea connection for fw.config
config = ft.config
service = main
[ts]
class = secop_psi.sea.SeaDrivable
io = sea_main
sea_object = tt
rel_paths = . ts
[t2]
class = secop_psi.sea.SeaReadable
io = sea_main
sea_object = tt
rel_paths = tm

View File

@ -68,5 +68,5 @@ description = stick rotation, typically used for omega
class = secop_psi.phytron.Motor class = secop_psi.phytron.Motor
io = om_io io = om_io
sign = -1 sign = -1
encoder_mode = CHECK encoder_mode = READ

80
cfg/sea/flamp.config.json Normal file
View File

@ -0,0 +1,80 @@
{"tt": {"base": "/tt", "params": [
{"path": "", "type": "float", "readonly": false, "cmd": "run tt", "description": "tt", "kids": 40},
{"path": "send", "type": "text", "readonly": false, "cmd": "tt send", "visibility": 3},
{"path": "status", "type": "text", "readonly": false, "cmd": "run tt", "visibility": 3},
{"path": "is_running", "type": "int", "readonly": false, "cmd": "tt is_running", "visibility": 3},
{"path": "set", "type": "float", "readonly": false, "cmd": "tt set"},
{"path": "target", "type": "float", "readonly": false, "cmd": "run tt"},
{"path": "running", "type": "int", "readonly": false, "cmd": "run tt"},
{"path": "tolerance", "type": "float", "readonly": false, "cmd": "tt tolerance"},
{"path": "maxwait", "type": "float", "readonly": false, "cmd": "tt maxwait"},
{"path": "settle", "type": "float", "readonly": false, "cmd": "tt settle"},
{"path": "log", "type": "text", "readonly": false, "cmd": "tt log", "visibility": 3, "kids": 4},
{"path": "log/mean", "type": "float", "readonly": false, "cmd": "run tt", "visibility": 3},
{"path": "log/m2", "type": "float", "readonly": false, "cmd": "run tt", "visibility": 3},
{"path": "log/stddev", "type": "float", "readonly": false, "cmd": "run tt", "visibility": 3},
{"path": "log/n", "type": "float", "readonly": false, "cmd": "run tt", "visibility": 3},
{"path": "limit", "type": "float", "readonly": false, "cmd": "tt limit"},
{"path": "t1", "type": "float", "readonly": false, "cmd": "run tt", "kids": 3},
{"path": "t1/raw", "type": "float", "readonly": false, "cmd": "run tt"},
{"path": "t1/curve", "type": "text", "readonly": false, "cmd": "tt t1/curve", "kids": 1},
{"path": "t1/curve/points", "type": "floatvarar", "readonly": false, "cmd": "tt t1/curve/points"},
{"path": "t1/valid", "type": "bool", "readonly": false, "cmd": "run tt"},
{"path": "t2", "type": "float", "readonly": false, "cmd": "run tt", "kids": 3},
{"path": "t2/raw", "type": "float", "readonly": false, "cmd": "run tt"},
{"path": "t2/curve", "type": "text", "readonly": false, "cmd": "tt t2/curve", "kids": 1},
{"path": "t2/curve/points", "type": "floatvarar", "readonly": false, "cmd": "tt t2/curve/points"},
{"path": "t2/valid", "type": "bool", "readonly": false, "cmd": "run tt"},
{"path": "t3", "type": "float", "readonly": false, "cmd": "run tt", "kids": 3},
{"path": "t3/raw", "type": "float", "readonly": false, "cmd": "run tt"},
{"path": "t3/curve", "type": "text", "readonly": false, "cmd": "tt t3/curve", "kids": 1},
{"path": "t3/curve/points", "type": "floatvarar", "readonly": false, "cmd": "tt t3/curve/points"},
{"path": "t3/valid", "type": "bool", "readonly": false, "cmd": "run tt"},
{"path": "t4", "type": "float", "readonly": false, "cmd": "run tt", "kids": 3},
{"path": "t4/raw", "type": "float", "readonly": false, "cmd": "run tt"},
{"path": "t4/curve", "type": "text", "readonly": false, "cmd": "tt t4/curve", "kids": 1},
{"path": "t4/curve/points", "type": "floatvarar", "readonly": false, "cmd": "tt t4/curve/points"},
{"path": "t4/valid", "type": "bool", "readonly": false, "cmd": "run tt"},
{"path": "tref", "type": "float", "readonly": false, "cmd": "run tt"},
{"path": "tout", "type": "float", "readonly": false, "cmd": "run tt"},
{"path": "toutmax", "type": "float", "readonly": false, "cmd": "tt toutmax"},
{"path": "toutmin", "type": "float", "readonly": false, "cmd": "tt toutmin"},
{"path": "ctrlmode", "type": "enum", "enum": {"ok": 0, "off": 1, "illegal_channel": 2, "no_sensor": 3, "no_waterflow": 4, "bad_vacuum": 5, "tc_overflow": 6, "wall_T_overflow": 7}, "readonly": false, "cmd": "tt ctrlmode"},
{"path": "ramp", "type": "float", "readonly": false, "cmd": "tt ramp"},
{"path": "smooth", "type": "float", "readonly": false, "cmd": "tt smooth"},
{"path": "prop", "type": "float", "readonly": false, "cmd": "tt prop", "description": "proportional gain for T slope control"},
{"path": "int", "type": "float", "readonly": false, "cmd": "tt int", "description": "time constant for T slope control"},
{"path": "powerset", "type": "float", "readonly": false, "cmd": "tt powerset"},
{"path": "power", "type": "float", "readonly": false, "cmd": "run tt"},
{"path": "resist", "type": "float", "readonly": false, "cmd": "run tt"},
{"path": "maxpower", "type": "float", "readonly": false, "cmd": "tt maxpower"},
{"path": "maxheater", "type": "float", "readonly": false, "cmd": "tt maxheater"},
{"path": "output", "type": "float", "readonly": false, "cmd": "tt output"},
{"path": "manualpower", "type": "bool", "readonly": false, "cmd": "tt manualpower"},
{"path": "ctrlchan", "type": "int", "readonly": false, "cmd": "tt ctrlchan"},
{"path": "interlock_state", "type": "enum", "enum": {"ok": 0, "no_waterflow": 1, "bad_vacuum": 2, "no_waterflow_bad_vacuum": 3}, "readonly": false, "cmd": "run tt"},
{"path": "interlock_mask", "type": "enum", "enum": {"no_check": 0, "check_water_only": 1, "check_vacuum_only": 2, "check_all": 3}, "readonly": false, "cmd": "tt interlock_mask"},
{"path": "sramp", "type": "float", "readonly": false, "cmd": "run tt"},
{"path": "slope", "type": "float", "readonly": false, "cmd": "run tt"},
{"path": "v_htr", "type": "float", "readonly": false, "cmd": "run tt"},
{"path": "i_htr", "type": "float", "readonly": false, "cmd": "run tt"},
{"path": "htr", "type": "float", "readonly": false, "cmd": "run tt"},
{"path": "powerprop", "type": "float", "readonly": false, "cmd": "run tt"}]},
"current": {"base": "/current", "params": [
{"path": "", "type": "float", "kids": 7},
{"path": "send", "type": "text", "readonly": false, "cmd": "current send", "visibility": 3},
{"path": "status", "type": "text", "visibility": 3},
{"path": "i1", "type": "float"},
{"path": "i2", "type": "float"},
{"path": "i3", "type": "float"},
{"path": "i4", "type": "float"},
{"path": "ib", "type": "float"}]},
"pv": {"base": "/pv", "params": [
{"path": "", "type": "float", "kids": 5},
{"path": "send", "type": "text", "readonly": false, "cmd": "pv send", "visibility": 3},
{"path": "status", "type": "text", "visibility": 3},
{"path": "sp1", "type": "text"},
{"path": "sp2", "type": "text"},
{"path": "sps", "type": "text"}]}}

160
cfg/sea/fs.config.json Normal file
View File

@ -0,0 +1,160 @@
{"tt": {"base": "/tt", "params": [
{"path": "", "type": "float", "readonly": false, "cmd": "run tt", "description": "tt", "kids": 18},
{"path": "send", "type": "text", "readonly": false, "cmd": "tt send", "visibility": 3},
{"path": "status", "type": "text", "visibility": 3},
{"path": "is_running", "type": "int", "readonly": false, "cmd": "tt is_running", "visibility": 3},
{"path": "mainloop", "type": "text", "readonly": false, "cmd": "tt mainloop", "visibility": 3},
{"path": "target", "type": "float"},
{"path": "running", "type": "int"},
{"path": "tolerance", "type": "float", "readonly": false, "cmd": "tt tolerance"},
{"path": "maxwait", "type": "float", "readonly": false, "cmd": "tt maxwait"},
{"path": "settle", "type": "float", "readonly": false, "cmd": "tt settle"},
{"path": "log", "type": "text", "readonly": false, "cmd": "tt log", "visibility": 3, "kids": 4},
{"path": "log/mean", "type": "float", "visibility": 3},
{"path": "log/m2", "type": "float", "visibility": 3},
{"path": "log/stddev", "type": "float", "visibility": 3},
{"path": "log/n", "type": "float", "visibility": 3},
{"path": "dblctrl", "type": "bool", "readonly": false, "cmd": "tt dblctrl", "kids": 9},
{"path": "dblctrl/tshift", "type": "float", "readonly": false, "cmd": "tt dblctrl/tshift"},
{"path": "dblctrl/mode", "type": "enum", "enum": {"disabled": -1, "inactive": 0, "stable": 1, "up": 2, "down": 3}, "readonly": false, "cmd": "tt dblctrl/mode"},
{"path": "dblctrl/shift_up", "type": "float"},
{"path": "dblctrl/shift_lo", "type": "float"},
{"path": "dblctrl/t_min", "type": "float"},
{"path": "dblctrl/t_max", "type": "float"},
{"path": "dblctrl/int2", "type": "float", "readonly": false, "cmd": "tt dblctrl/int2"},
{"path": "dblctrl/prop_up", "type": "float", "readonly": false, "cmd": "tt dblctrl/prop_up"},
{"path": "dblctrl/prop_lo", "type": "float", "readonly": false, "cmd": "tt dblctrl/prop_lo"},
{"path": "ts", "type": "float", "kids": 4},
{"path": "ts/curve", "type": "text", "readonly": false, "cmd": "tt ts/curve", "kids": 1},
{"path": "ts/curve/points", "type": "floatvarar", "readonly": false, "cmd": "tt ts/curve/points", "visibility": 3},
{"path": "ts/alarm", "type": "float", "readonly": false, "cmd": "tt ts/alarm"},
{"path": "ts/stddev", "type": "float"},
{"path": "ts/raw", "type": "float"},
{"path": "tm", "type": "float", "kids": 4},
{"path": "tm/curve", "type": "text", "readonly": false, "cmd": "tt tm/curve", "kids": 1},
{"path": "tm/curve/points", "type": "floatvarar", "readonly": false, "cmd": "tt tm/curve/points", "visibility": 3},
{"path": "tm/alarm", "type": "float", "readonly": false, "cmd": "tt tm/alarm"},
{"path": "tm/stddev", "type": "float"},
{"path": "tm/raw", "type": "float"},
{"path": "set", "type": "float", "readonly": false, "cmd": "tt set", "kids": 18},
{"path": "set/mode", "type": "enum", "enum": {"disabled": -1, "off": 0, "controlling": 1, "manual": 2}, "readonly": false, "cmd": "tt set/mode"},
{"path": "set/reg", "type": "float"},
{"path": "set/ramp", "type": "float", "readonly": false, "cmd": "tt set/ramp", "description": "maximum ramp in K/min (0: ramp off)"},
{"path": "set/wramp", "type": "float", "readonly": false, "cmd": "tt set/wramp"},
{"path": "set/smooth", "type": "float", "readonly": false, "cmd": "tt set/smooth", "description": "smooth time (minutes)"},
{"path": "set/channel", "type": "text", "readonly": false, "cmd": "tt set/channel"},
{"path": "set/limit", "type": "float", "readonly": false, "cmd": "tt set/limit"},
{"path": "set/resist", "type": "float", "readonly": false, "cmd": "tt set/resist"},
{"path": "set/maxheater", "type": "text", "readonly": false, "cmd": "tt set/maxheater", "description": "maximum heater limit, units should be given without space: W, mW, A, mA"},
{"path": "set/linearpower", "type": "float", "readonly": false, "cmd": "tt set/linearpower", "description": "when not 0, it is the maximum effective power, and the power is linear to the heater output"},
{"path": "set/maxpowerlim", "type": "float", "description": "the maximum power limit (before any booster or converter)"},
{"path": "set/maxpower", "type": "float", "readonly": false, "cmd": "tt set/maxpower", "description": "maximum power [W]"},
{"path": "set/maxcurrent", "type": "float", "description": "the maximum current before any booster or converter"},
{"path": "set/manualpower", "type": "float", "readonly": false, "cmd": "tt set/manualpower"},
{"path": "set/power", "type": "float"},
{"path": "set/prop", "type": "float", "readonly": false, "cmd": "tt set/prop", "description": "bigger means more gain"},
{"path": "set/integ", "type": "float", "readonly": false, "cmd": "tt set/integ", "description": "bigger means faster"},
{"path": "set/deriv", "type": "float", "readonly": false, "cmd": "tt set/deriv"},
{"path": "display", "type": "text", "readonly": false, "cmd": "tt display"},
{"path": "dout", "type": "int", "readonly": false, "cmd": "tt dout"},
{"path": "dinp", "type": "int"},
{"path": "remote", "type": "bool"}]},
"cc": {"base": "/cc", "params": [
{"path": "", "type": "bool", "kids": 96},
{"path": "send", "type": "text", "readonly": false, "cmd": "cc send", "visibility": 3},
{"path": "status", "type": "text", "visibility": 3},
{"path": "autodevice", "type": "bool", "readonly": false, "cmd": "cc autodevice"},
{"path": "fav", "type": "bool", "readonly": false, "cmd": "cc fav"},
{"path": "f", "type": "float", "visibility": 3},
{"path": "fs", "type": "enum", "enum": {"ok": 0, "no_sens": 1}, "readonly": false, "cmd": "cc fs", "visibility": 3},
{"path": "mav", "type": "bool", "readonly": false, "cmd": "cc mav"},
{"path": "fm", "type": "enum", "enum": {"idle": 0, "opening": 1, "closing": 2, "opened": 3, "closed": 4, "no_motor": 5}, "visibility": 3},
{"path": "fa", "type": "enum", "enum": {"fixed": 0, "controlled": 1, "automatic": 2, "offline": 3}, "readonly": false, "cmd": "cc fa", "visibility": 3},
{"path": "mp", "type": "float", "readonly": false, "cmd": "cc mp", "visibility": 3},
{"path": "msp", "type": "float", "visibility": 3},
{"path": "mmp", "type": "float", "visibility": 3},
{"path": "mc", "type": "float", "readonly": false, "cmd": "cc mc", "visibility": 3},
{"path": "mfc", "type": "float", "readonly": false, "cmd": "cc mfc", "visibility": 3},
{"path": "moc", "type": "float", "readonly": false, "cmd": "cc moc", "visibility": 3},
{"path": "mtc", "type": "float", "readonly": false, "cmd": "cc mtc", "visibility": 3},
{"path": "mtl", "type": "float", "visibility": 3},
{"path": "mft", "type": "float", "readonly": false, "cmd": "cc mft", "visibility": 3},
{"path": "mt", "type": "float", "visibility": 3},
{"path": "mo", "type": "float", "visibility": 3},
{"path": "mcr", "type": "float", "visibility": 3},
{"path": "mot", "type": "float", "visibility": 3},
{"path": "mw", "type": "float", "readonly": false, "cmd": "cc mw", "description": "correction pulse after automatic open", "visibility": 3},
{"path": "hav", "type": "bool", "readonly": false, "cmd": "cc hav"},
{"path": "h", "type": "float", "visibility": 3},
{"path": "hr", "type": "float", "visibility": 3},
{"path": "hc", "type": "float", "visibility": 3},
{"path": "hu", "type": "float", "visibility": 3},
{"path": "hh", "type": "float", "readonly": false, "cmd": "cc hh", "visibility": 3},
{"path": "hl", "type": "float", "readonly": false, "cmd": "cc hl", "visibility": 3},
{"path": "htf", "type": "float", "readonly": false, "cmd": "cc htf", "description": "meas. period in fast mode", "visibility": 3},
{"path": "hts", "type": "float", "readonly": false, "cmd": "cc hts", "description": "meas. period in slow mode", "visibility": 3},
{"path": "hd", "type": "float", "readonly": false, "cmd": "cc hd", "visibility": 3},
{"path": "hwr", "type": "float", "readonly": false, "cmd": "cc hwr", "visibility": 3},
{"path": "hem", "type": "float", "readonly": false, "cmd": "cc hem", "description": "sensor length in mm from top to empty pos.", "visibility": 3},
{"path": "hfu", "type": "float", "readonly": false, "cmd": "cc hfu", "description": "sensor length in mm from top to full pos.", "visibility": 3},
{"path": "hcd", "type": "enum", "enum": {"stop": 0, "fill": 1, "off": 2, "auto": 3, "manual": 7}, "readonly": false, "cmd": "cc hcd", "visibility": 3},
{"path": "hv", "type": "enum", "enum": {"fill_valve_off": 0, "filling": 1, "no_fill_valve": 2, "timeout": 3, "timeout1": 4}, "visibility": 3},
{"path": "hsf", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}, "visibility": 3},
{"path": "ha", "type": "bool", "readonly": false, "cmd": "cc ha", "visibility": 3},
{"path": "hm", "type": "bool", "visibility": 3},
{"path": "hf", "type": "enum", "enum": {"slow": 0, "fast": 1}, "readonly": false, "cmd": "cc hf", "visibility": 3},
{"path": "hbe", "type": "bool", "readonly": false, "cmd": "cc hbe", "visibility": 3},
{"path": "hmf", "type": "float", "visibility": 3},
{"path": "hms", "type": "float", "visibility": 3},
{"path": "hit", "type": "float", "readonly": false, "cmd": "cc hit", "visibility": 3},
{"path": "hft", "type": "int", "readonly": false, "cmd": "cc hft", "visibility": 3},
{"path": "hea", "type": "enum", "enum": {"0": 0, "1": 1, "6": 6}, "readonly": false, "cmd": "cc hea"},
{"path": "hch", "type": "int", "readonly": false, "cmd": "cc hch", "visibility": 3},
{"path": "hwr0", "type": "float", "readonly": false, "cmd": "cc hwr0", "visibility": 3},
{"path": "hem0", "type": "float", "readonly": false, "cmd": "cc hem0", "description": "sensor length in mm from top to empty pos.", "visibility": 3},
{"path": "hfu0", "type": "float", "readonly": false, "cmd": "cc hfu0", "description": "sensor length in mm from top to full pos.", "visibility": 3},
{"path": "hd0", "type": "float", "readonly": false, "cmd": "cc hd0", "description": "external sensor drive current (mA)", "visibility": 3},
{"path": "h0", "type": "float", "visibility": 3},
{"path": "hs0", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}, "visibility": 3},
{"path": "h1", "type": "float", "visibility": 3},
{"path": "hs1", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}, "visibility": 3},
{"path": "h2", "type": "float", "visibility": 3},
{"path": "hs2", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}, "visibility": 3},
{"path": "h3", "type": "float", "visibility": 3},
{"path": "hs3", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}, "visibility": 3},
{"path": "h4", "type": "float", "visibility": 3},
{"path": "hs4", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}, "visibility": 3},
{"path": "h5", "type": "float", "visibility": 3},
{"path": "hs5", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}, "visibility": 3},
{"path": "hfb", "type": "float", "visibility": 3},
{"path": "nav", "type": "bool", "readonly": false, "cmd": "cc nav"},
{"path": "nu", "type": "float", "visibility": 3},
{"path": "nl", "type": "float", "visibility": 3},
{"path": "nth", "type": "float", "readonly": false, "cmd": "cc nth", "visibility": 3},
{"path": "ntc", "type": "float", "readonly": false, "cmd": "cc ntc", "visibility": 3},
{"path": "ntm", "type": "float", "readonly": false, "cmd": "cc ntm", "visibility": 3},
{"path": "ns", "type": "enum", "enum": {"sens_ok": 0, "no_sens": 1, "short_circuit": 2, "upside_down": 3, "sens_warm": 4, "empty": 5}, "visibility": 3},
{"path": "na", "type": "bool", "readonly": false, "cmd": "cc na", "visibility": 3},
{"path": "nv", "type": "enum", "enum": {"fill_valve_off": 0, "filling": 1, "no_fill_valve": 2, "timeout": 3, "timeout1": 4, "boost": 5}, "visibility": 3},
{"path": "nc", "type": "enum", "enum": {"stop": 0, "fill": 1, "off": 2, "auto": 3}, "readonly": false, "cmd": "cc nc", "visibility": 3},
{"path": "nfb", "type": "float", "visibility": 3},
{"path": "cda", "type": "float"},
{"path": "cdb", "type": "float"},
{"path": "cba", "type": "float"},
{"path": "cbb", "type": "float"},
{"path": "cvs", "type": "int"},
{"path": "csp", "type": "int"},
{"path": "cdv", "type": "text", "readonly": false, "cmd": "cc cdv"},
{"path": "cic", "type": "text", "readonly": false, "cmd": "cc cic"},
{"path": "cin", "type": "text"},
{"path": "cds", "type": "enum", "enum": {"local": 0, "remote": 1, "loading": 2, "by_code": 3, "by_touch": 4}, "readonly": false, "cmd": "cc cds"},
{"path": "timing", "type": "bool", "readonly": false, "cmd": "cc timing"},
{"path": "tc", "type": "float", "visibility": 3},
{"path": "tn", "type": "float", "visibility": 3},
{"path": "th", "type": "float", "visibility": 3},
{"path": "tf", "type": "float", "visibility": 3},
{"path": "tm", "type": "float", "visibility": 3},
{"path": "tv", "type": "float", "visibility": 3},
{"path": "tq", "type": "float", "visibility": 3},
{"path": "bdl", "type": "float", "readonly": false, "cmd": "cc bdl"}]}}

183
cfg/sea/ft.config.json Normal file
View File

@ -0,0 +1,183 @@
{"tt": {"base": "/tt", "params": [
{"path": "", "type": "float", "readonly": false, "cmd": "run tt", "description": "tt", "kids": 20},
{"path": "send", "type": "text", "readonly": false, "cmd": "tt send", "visibility": 3},
{"path": "status", "type": "text", "visibility": 3},
{"path": "is_running", "type": "int", "readonly": false, "cmd": "tt is_running", "visibility": 3},
{"path": "mainloop", "type": "text", "readonly": false, "cmd": "tt mainloop", "visibility": 3},
{"path": "target", "type": "float"},
{"path": "running", "type": "int"},
{"path": "tolerance", "type": "float", "readonly": false, "cmd": "tt tolerance"},
{"path": "maxwait", "type": "float", "readonly": false, "cmd": "tt maxwait"},
{"path": "settle", "type": "float", "readonly": false, "cmd": "tt settle"},
{"path": "log", "type": "text", "readonly": false, "cmd": "tt log", "visibility": 3, "kids": 4},
{"path": "log/mean", "type": "float", "visibility": 3},
{"path": "log/m2", "type": "float", "visibility": 3},
{"path": "log/stddev", "type": "float", "visibility": 3},
{"path": "log/n", "type": "float", "visibility": 3},
{"path": "dblctrl", "type": "bool", "readonly": false, "cmd": "tt dblctrl", "visibility": 3, "kids": 9},
{"path": "dblctrl/tshift", "type": "float", "readonly": false, "cmd": "tt dblctrl/tshift", "visibility": 3},
{"path": "dblctrl/mode", "type": "enum", "enum": {"disabled": -1, "inactive": 0, "stable": 1, "up": 2, "down": 3}, "readonly": false, "cmd": "tt dblctrl/mode", "visibility": 3},
{"path": "dblctrl/shift_up", "type": "float", "visibility": 3},
{"path": "dblctrl/shift_lo", "type": "float", "visibility": 3},
{"path": "dblctrl/t_min", "type": "float", "visibility": 3},
{"path": "dblctrl/t_max", "type": "float", "visibility": 3},
{"path": "dblctrl/int2", "type": "float", "readonly": false, "cmd": "tt dblctrl/int2", "visibility": 3},
{"path": "dblctrl/prop_up", "type": "float", "readonly": false, "cmd": "tt dblctrl/prop_up", "visibility": 3},
{"path": "dblctrl/prop_lo", "type": "float", "readonly": false, "cmd": "tt dblctrl/prop_lo", "visibility": 3},
{"path": "ts", "type": "float", "kids": 4},
{"path": "ts/curve", "type": "text", "readonly": false, "cmd": "tt ts/curve", "kids": 1},
{"path": "ts/curve/points", "type": "floatvarar", "readonly": false, "cmd": "tt ts/curve/points", "visibility": 3},
{"path": "ts/alarm", "type": "float", "readonly": false, "cmd": "tt ts/alarm"},
{"path": "ts/stddev", "type": "float"},
{"path": "ts/raw", "type": "float"},
{"path": "tm", "type": "float", "kids": 4},
{"path": "tm/curve", "type": "text", "readonly": false, "cmd": "tt tm/curve", "kids": 1},
{"path": "tm/curve/points", "type": "floatvarar", "readonly": false, "cmd": "tt tm/curve/points", "visibility": 3},
{"path": "tm/alarm", "type": "float", "readonly": false, "cmd": "tt tm/alarm"},
{"path": "tm/stddev", "type": "float"},
{"path": "tm/raw", "type": "float"},
{"path": "te", "type": "float", "kids": 4},
{"path": "te/curve", "type": "text", "readonly": false, "cmd": "tt te/curve", "kids": 1},
{"path": "te/curve/points", "type": "floatvarar", "readonly": false, "cmd": "tt te/curve/points", "visibility": 3},
{"path": "te/alarm", "type": "float", "readonly": false, "cmd": "tt te/alarm"},
{"path": "te/stddev", "type": "float"},
{"path": "te/raw", "type": "float"},
{"path": "p", "type": "float", "kids": 4},
{"path": "p/curve", "type": "text", "readonly": false, "cmd": "tt p/curve", "kids": 1},
{"path": "p/curve/points", "type": "floatvarar", "readonly": false, "cmd": "tt p/curve/points", "visibility": 3},
{"path": "p/alarm", "type": "float", "readonly": false, "cmd": "tt p/alarm"},
{"path": "p/stddev", "type": "float"},
{"path": "p/raw", "type": "float"},
{"path": "set", "type": "float", "readonly": false, "cmd": "tt set", "kids": 18},
{"path": "set/mode", "type": "enum", "enum": {"disabled": -1, "off": 0, "controlling": 1, "manual": 2}, "readonly": false, "cmd": "tt set/mode"},
{"path": "set/reg", "type": "float"},
{"path": "set/ramp", "type": "float", "readonly": false, "cmd": "tt set/ramp", "description": "maximum ramp in K/min (0: ramp off)"},
{"path": "set/wramp", "type": "float", "readonly": false, "cmd": "tt set/wramp"},
{"path": "set/smooth", "type": "float", "readonly": false, "cmd": "tt set/smooth", "description": "smooth time (minutes)"},
{"path": "set/channel", "type": "text", "readonly": false, "cmd": "tt set/channel"},
{"path": "set/limit", "type": "float", "readonly": false, "cmd": "tt set/limit"},
{"path": "set/resist", "type": "float", "readonly": false, "cmd": "tt set/resist"},
{"path": "set/maxheater", "type": "text", "readonly": false, "cmd": "tt set/maxheater", "description": "maximum heater limit, units should be given without space: W, mW, A, mA"},
{"path": "set/linearpower", "type": "float", "readonly": false, "cmd": "tt set/linearpower", "description": "when not 0, it is the maximum effective power, and the power is linear to the heater output"},
{"path": "set/maxpowerlim", "type": "float", "description": "the maximum power limit (before any booster or converter)"},
{"path": "set/maxpower", "type": "float", "readonly": false, "cmd": "tt set/maxpower", "description": "maximum power [W]"},
{"path": "set/maxcurrent", "type": "float", "description": "the maximum current before any booster or converter"},
{"path": "set/manualpower", "type": "float", "readonly": false, "cmd": "tt set/manualpower"},
{"path": "set/power", "type": "float"},
{"path": "set/prop", "type": "float", "readonly": false, "cmd": "tt set/prop", "description": "bigger means more gain"},
{"path": "set/integ", "type": "float", "readonly": false, "cmd": "tt set/integ", "description": "bigger means faster"},
{"path": "set/deriv", "type": "float", "readonly": false, "cmd": "tt set/deriv"},
{"path": "display", "type": "text", "readonly": false, "cmd": "tt display"},
{"path": "dout", "type": "int", "readonly": false, "cmd": "tt dout"},
{"path": "dinp", "type": "int"},
{"path": "remote", "type": "bool"}]},
"table": {"base": "/table", "params": [
{"path": "", "type": "none", "kids": 8},
{"path": "send", "type": "text", "readonly": false, "cmd": "table send", "visibility": 3},
{"path": "status", "type": "text", "visibility": 3},
{"path": "fix_tt_set_prop", "type": "bool", "readonly": false, "cmd": "table fix_tt_set_prop"},
{"path": "val_tt_set_prop", "type": "float"},
{"path": "tbl_tt_set_prop", "type": "text", "readonly": false, "cmd": "table tbl_tt_set_prop", "description": "enter value pair separated with colon T1:par1 T2:par2 ..."},
{"path": "fix_tt_set_integ", "type": "bool", "readonly": false, "cmd": "table fix_tt_set_integ"},
{"path": "val_tt_set_integ", "type": "float"},
{"path": "tbl_tt_set_integ", "type": "text", "readonly": false, "cmd": "table tbl_tt_set_integ", "description": "enter value pair separated with colon T1:par1 T2:par2 ..."}]},
"cc": {"base": "/cc", "params": [
{"path": "", "type": "bool", "kids": 96},
{"path": "send", "type": "text", "readonly": false, "cmd": "cc send", "visibility": 3},
{"path": "status", "type": "text", "visibility": 3},
{"path": "autodevice", "type": "bool", "readonly": false, "cmd": "cc autodevice"},
{"path": "fav", "type": "bool", "readonly": false, "cmd": "cc fav"},
{"path": "f", "type": "float", "visibility": 3},
{"path": "fs", "type": "enum", "enum": {"ok": 0, "no_sens": 1}, "readonly": false, "cmd": "cc fs", "visibility": 3},
{"path": "mav", "type": "bool", "readonly": false, "cmd": "cc mav"},
{"path": "fm", "type": "enum", "enum": {"idle": 0, "opening": 1, "closing": 2, "opened": 3, "closed": 4, "no_motor": 5}, "visibility": 3},
{"path": "fa", "type": "enum", "enum": {"fixed": 0, "controlled": 1, "automatic": 2, "offline": 3}, "readonly": false, "cmd": "cc fa", "visibility": 3},
{"path": "mp", "type": "float", "readonly": false, "cmd": "cc mp", "visibility": 3},
{"path": "msp", "type": "float", "visibility": 3},
{"path": "mmp", "type": "float", "visibility": 3},
{"path": "mc", "type": "float", "readonly": false, "cmd": "cc mc", "visibility": 3},
{"path": "mfc", "type": "float", "readonly": false, "cmd": "cc mfc", "visibility": 3},
{"path": "moc", "type": "float", "readonly": false, "cmd": "cc moc", "visibility": 3},
{"path": "mtc", "type": "float", "readonly": false, "cmd": "cc mtc", "visibility": 3},
{"path": "mtl", "type": "float", "visibility": 3},
{"path": "mft", "type": "float", "readonly": false, "cmd": "cc mft", "visibility": 3},
{"path": "mt", "type": "float", "visibility": 3},
{"path": "mo", "type": "float", "visibility": 3},
{"path": "mcr", "type": "float", "visibility": 3},
{"path": "mot", "type": "float", "visibility": 3},
{"path": "mw", "type": "float", "readonly": false, "cmd": "cc mw", "description": "correction pulse after automatic open", "visibility": 3},
{"path": "hav", "type": "bool", "readonly": false, "cmd": "cc hav"},
{"path": "h", "type": "float", "visibility": 3},
{"path": "hr", "type": "float", "visibility": 3},
{"path": "hc", "type": "float", "visibility": 3},
{"path": "hu", "type": "float", "visibility": 3},
{"path": "hh", "type": "float", "readonly": false, "cmd": "cc hh", "visibility": 3},
{"path": "hl", "type": "float", "readonly": false, "cmd": "cc hl", "visibility": 3},
{"path": "htf", "type": "float", "readonly": false, "cmd": "cc htf", "description": "meas. period in fast mode", "visibility": 3},
{"path": "hts", "type": "float", "readonly": false, "cmd": "cc hts", "description": "meas. period in slow mode", "visibility": 3},
{"path": "hd", "type": "float", "readonly": false, "cmd": "cc hd", "visibility": 3},
{"path": "hwr", "type": "float", "readonly": false, "cmd": "cc hwr", "visibility": 3},
{"path": "hem", "type": "float", "readonly": false, "cmd": "cc hem", "description": "sensor length in mm from top to empty pos.", "visibility": 3},
{"path": "hfu", "type": "float", "readonly": false, "cmd": "cc hfu", "description": "sensor length in mm from top to full pos.", "visibility": 3},
{"path": "hcd", "type": "enum", "enum": {"stop": 0, "fill": 1, "off": 2, "auto": 3, "manual": 7}, "readonly": false, "cmd": "cc hcd", "visibility": 3},
{"path": "hv", "type": "enum", "enum": {"fill_valve_off": 0, "filling": 1, "no_fill_valve": 2, "timeout": 3, "timeout1": 4}, "visibility": 3},
{"path": "hsf", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}, "visibility": 3},
{"path": "ha", "type": "bool", "readonly": false, "cmd": "cc ha", "visibility": 3},
{"path": "hm", "type": "bool", "visibility": 3},
{"path": "hf", "type": "enum", "enum": {"slow": 0, "fast": 1}, "readonly": false, "cmd": "cc hf", "visibility": 3},
{"path": "hbe", "type": "bool", "readonly": false, "cmd": "cc hbe", "visibility": 3},
{"path": "hmf", "type": "float", "visibility": 3},
{"path": "hms", "type": "float", "visibility": 3},
{"path": "hit", "type": "float", "readonly": false, "cmd": "cc hit", "visibility": 3},
{"path": "hft", "type": "int", "readonly": false, "cmd": "cc hft", "visibility": 3},
{"path": "hea", "type": "enum", "enum": {"0": 0, "1": 1, "6": 2}, "readonly": false, "cmd": "cc hea"},
{"path": "hch", "type": "int", "readonly": false, "cmd": "cc hch", "visibility": 3},
{"path": "hwr0", "type": "float", "readonly": false, "cmd": "cc hwr0", "visibility": 3},
{"path": "hem0", "type": "float", "readonly": false, "cmd": "cc hem0", "description": "sensor length in mm from top to empty pos.", "visibility": 3},
{"path": "hfu0", "type": "float", "readonly": false, "cmd": "cc hfu0", "description": "sensor length in mm from top to full pos.", "visibility": 3},
{"path": "hd0", "type": "float", "readonly": false, "cmd": "cc hd0", "description": "external sensor drive current (mA)", "visibility": 3},
{"path": "h0", "type": "float", "visibility": 3},
{"path": "hs0", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}, "visibility": 3},
{"path": "h1", "type": "float", "visibility": 3},
{"path": "hs1", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}, "visibility": 3},
{"path": "h2", "type": "float", "visibility": 3},
{"path": "hs2", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}, "visibility": 3},
{"path": "h3", "type": "float", "visibility": 3},
{"path": "hs3", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}, "visibility": 3},
{"path": "h4", "type": "float", "visibility": 3},
{"path": "hs4", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}, "visibility": 3},
{"path": "h5", "type": "float", "visibility": 3},
{"path": "hs5", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}, "visibility": 3},
{"path": "hfb", "type": "float", "visibility": 3},
{"path": "nav", "type": "bool", "readonly": false, "cmd": "cc nav"},
{"path": "nu", "type": "float", "visibility": 3},
{"path": "nl", "type": "float", "visibility": 3},
{"path": "nth", "type": "float", "readonly": false, "cmd": "cc nth", "visibility": 3},
{"path": "ntc", "type": "float", "readonly": false, "cmd": "cc ntc", "visibility": 3},
{"path": "ntm", "type": "float", "readonly": false, "cmd": "cc ntm", "visibility": 3},
{"path": "ns", "type": "enum", "enum": {"sens_ok": 0, "no_sens": 1, "short_circuit": 2, "upside_down": 3, "sens_warm": 4, "empty": 5}, "visibility": 3},
{"path": "na", "type": "bool", "readonly": false, "cmd": "cc na", "visibility": 3},
{"path": "nv", "type": "enum", "enum": {"fill_valve_off": 0, "filling": 1, "no_fill_valve": 2, "timeout": 3, "timeout1": 4, "boost": 5}, "visibility": 3},
{"path": "nc", "type": "enum", "enum": {"stop": 0, "fill": 1, "off": 2, "auto": 3}, "readonly": false, "cmd": "cc nc", "visibility": 3},
{"path": "nfb", "type": "float", "visibility": 3},
{"path": "cda", "type": "float"},
{"path": "cdb", "type": "float"},
{"path": "cba", "type": "float"},
{"path": "cbb", "type": "float"},
{"path": "cvs", "type": "int"},
{"path": "csp", "type": "int"},
{"path": "cdv", "type": "text", "readonly": false, "cmd": "cc cdv"},
{"path": "cic", "type": "text", "readonly": false, "cmd": "cc cic"},
{"path": "cin", "type": "text"},
{"path": "cds", "type": "enum", "enum": {"local": 0, "remote": 1, "loading": 2, "by_code": 3, "by_touch": 4}, "readonly": false, "cmd": "cc cds"},
{"path": "timing", "type": "bool", "readonly": false, "cmd": "cc timing"},
{"path": "tc", "type": "float", "visibility": 3},
{"path": "tn", "type": "float", "visibility": 3},
{"path": "th", "type": "float", "visibility": 3},
{"path": "tf", "type": "float", "visibility": 3},
{"path": "tm", "type": "float", "visibility": 3},
{"path": "tv", "type": "float", "visibility": 3},
{"path": "tq", "type": "float", "visibility": 3},
{"path": "bdl", "type": "float", "readonly": false, "cmd": "cc bdl"}]}}

View File

@ -13,7 +13,26 @@ class = secop_psi.sea.SeaDrivable
io = sea_stick io = sea_stick
sea_object = ts sea_object = ts
[th] [T_sorb]
class = secop_psi.sea.SeaDrivable class = secop_psi.sea.SeaReadable
io = sea_stick io = sea_stick
sea_object = th sea_object = th
rel_paths = sorb
[T_plate]
class = secop_psi.sea.SeaReadable
io = sea_stick
sea_object = th
rel_paths = plate
[T_low]
class = secop_psi.sea.SeaReadable
io = sea_stick
sea_object = th
rel_paths = low
[T_pot]
class = secop_psi.sea.SeaReadable
io = sea_stick
sea_object = th
rel_paths = pot

View File

@ -1,16 +1,19 @@
[NODE] # DO NOT USE
description = MB11 standard sample stick # use 'mb11std' instead of 'mb11', 'mb11stick'
id = mb11.stick.sea.psi.ch # as the communication proxy for itc does not work yet
[NODE]
description = MB11 standard sample stick (do not use)
id = mb11.stick.sea.psi.ch
[INTERFACE] [INTERFACE]
uri = tcp://5000 uri = tcp://5000
[itc] #[itc]
class = secop.proxy.Proxy #class = secop.proxy.Proxy
remote_class = secop_psi.mercury.IO #remote_class = secop_psi.mercury.IO
description = connection to MB11 mercury #description = connection to MB11 mercury
module = itc1 #module = itc1
#uri = mb11-ts:3001 #uri = mb11-ts:3001
#timeout = 5 #timeout = 5
@ -25,19 +28,17 @@ module = itc1
#calib = /home/l_samenv/sea/tcl/calcurves/X70197.340 #calib = /home/l_samenv/sea/tcl/calcurves/X70197.340
#svalue.unit = K #svalue.unit = K
#[ts]
#class = secop.proxy.Proxy
#remote_class = secop_psi.mercury.TemperatureLoop
#description = sample temperature
#module = T_sample
#io = itc1
#[htr_ts]
#class = secop.proxy.Proxy
#remote_class = secop_psi.mercury.HeaterOutput
#description = sample stick heater power
#module = htr_sample
#io = itc1
[ts]
class = secop_psi.mercury.TemperatureLoop
description = sample temperature
output_module = htr_ts
slot = MB1.T1
io = itc
tolerance = 1
[htr_ts]
class = secop_psi.mercury.HeaterOutput
description = sample stick heater power
slot = MB0.H1
io = itc

View File

@ -0,0 +1,246 @@
# -*- 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 to keep the state and call the state function again
- or Finish for finishing
Initialisation Code
-------------------
For code to be called only after a state transition, use statemachine.init.
def state_x(stateobj):
if stateobj.init:
... code to be execute only after entering state x ...
... further code ...
Restart
-------
To restart the statemachine, call statemachine.start. The current task is interrupted,
the cleanup sequence is called, and after this the machien is restarted with the
arguments of the start method.
Stop
----
To stop the statemachine, call statemachine.stop. The current task is interrupted,
the cleanup sequence is called, and the machine finishes.
Cleaning Up
-----------
A cleanup function might be added as arguments to StateMachine.start.
On error, stop or restart, the cleanup sequence will be executed.
The cleanup itself is not be interrupted:
- if a further exeception is raised, the machine is interrupted immediately
- if start or stop is called again, a previous start or stop is ignored
"""
import time
import threading
from logging import getLogger
from secop.lib import UniqueObject
Retry = UniqueObject('Retry')
Finish = UniqueObject('Finish')
class Start:
def __init__(self, newstate, kwds):
self.newstate = newstate
self.kwds = kwds # statemachine attributes
class Stop:
pass
class StateMachine:
"""a simple, but powerful state machine"""
# class attributes are not allowed to be overriden by kwds of __init__ or :meth:`start`
statefunc = None # the current statefunc
now = None # the current time (avoid mutiple calls within a state)
init = True # True only in the first call of a state after a transition
_last_time = 0 # for delta method
next_task = None # None or an instance of Start or Stop
cleanup_reason = None # None or an instance of Exception, Start or Stop
def __init__(self, statefunc=None, logger=None, **kwds):
"""initialize state machine
:param statefunc: if given, this is the first statefunc
:param logger: an optional logger
:param kwds: any attributes for the state object
"""
self.cleanup = None
self.transition = None
self.maxloops = 10 # the maximum number of statefunc functions called in sequence without Retry
self.now = time.time() # avoid calling time.time several times per statefunc
self.log = logger or getLogger('dummy')
self._lock = threading.Lock()
self._update_attributes(kwds)
if statefunc:
self.start(statefunc)
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)
def _cleanup(self, reason):
if isinstance(reason, Exception):
self.log.warning('%s: raised %r', self.statefunc.__name__, reason)
elif isinstance(reason, Stop):
self.log.debug('stopped in %s', self.statefunc.__name__)
else: # must be Start
self.log.debug('restart %s during %s', reason.newstate.__name__, self.statefunc.__name__)
if self.cleanup_reason is None:
self.cleanup_reason = reason
if not self.cleanup:
return None # no cleanup needed or cleanup already handled
with self._lock:
cleanup, self.cleanup = self.cleanup, None
ret = None
try:
ret = cleanup(self)
if not (ret is None or callable(ret)):
self.log.error('%s: return value must be callable or None, not %r',
self.statefunc.__name__, ret)
ret = None
except Exception as e:
self.log.exception('%r raised in cleanup', e)
return ret
@property
def is_active(self):
return bool(self.statefunc)
def _new_state(self, statefunc):
if self.transition:
self.transition(self, statefunc)
self.init = True
self.statefunc = statefunc
self._last_time = self.now
def cycle(self):
"""do one cycle
call state functions until Retry is returned
"""
for _ in range(2):
if self.statefunc:
for _ in range(self.maxloops):
self.now = time.time()
if self.next_task and not self.cleanup_reason:
# interrupt only when not cleaning up
ret = self._cleanup(self.next_task)
else:
try:
ret = self.statefunc(self)
self.init = False
if ret is Retry:
return
if ret is Finish:
break
if not callable(ret):
ret = self._cleanup(RuntimeError(
'%s: return value must be callable, Retry or Finish, not %r'
% (self.statefunc.__name__, ret)))
except Exception as e:
ret = self._cleanup(e)
if ret is None:
break
self._new_state(ret)
else:
ret = self._cleanup(RuntimeError(
'%s: too many states chained - probably infinite loop' % self.statefunc.__name__))
if ret:
self._new_state(ret)
continue
if self.cleanup_reason is None:
self.log.debug('finish in state %r', self.statefunc.__name__)
self._new_state(None)
if self.next_task:
with self._lock:
action, self.next_task = self.next_task, None
self.cleanup_reason = None
if isinstance(action, Start):
self._new_state(action.newstate)
self._update_attributes(action.kwds)
def start(self, statefunc, **kwds):
"""start with a new state
:param statefunc: the first state
:param kwds: items to put as attributes on the state machine
"""
kwds.setdefault('cleanup', None) # cleanup must be given on each restart
with self._lock:
self.next_task = Start(statefunc, kwds)
def stop(self):
"""stop machine, go to idle state"""
with self._lock:
self.next_task = Stop()
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 statefunc
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

View File

@ -246,7 +246,7 @@ class PollInfo:
self.fast_flag = False self.fast_flag = False
self.trigger_event = trigger_event self.trigger_event = trigger_event
def trigger(self): def trigger(self, immediate=False):
"""trigger a recalculation of poll due times""" """trigger a recalculation of poll due times"""
self.trigger_event.set() self.trigger_event.set()

156
secop/states.py Normal file
View File

@ -0,0 +1,156 @@
#!/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>
#
# *****************************************************************************
"""state machine mixin
handles status depending on statemachine state
"""
from secop.core import BUSY, IDLE, ERROR, Parameter, Command, Done
from secop.errors import ProgrammingError
from secop.lib.newstatemachine import StateMachine, Retry, Finish, Start, Stop
class status_code:
"""decorator for state methods"""
def __init__(self, code, text=None):
self.code = code
self.text = text
def __set_name__(self, owner, name):
if not issubclass(owner, HasStates):
raise ProgrammingError('when using decorator "status_code", %s must inherit HasStates' % owner.__name__)
self.cls = owner
self.name = name
if 'statusMap' not in owner.__dict__:
# we need a copy on each inheritance level
owner.statusMap = owner.statusMap.copy()
owner.statusMap[name] = self.code, name.replace('_', ' ') if self.text is None else self.text
setattr(owner, name, self.func) # replace with original method
def __call__(self, func):
self.func = func
return self
class HasStates:
status = Parameter() # make sure this is a parameter
all_status_changes = False # when true, send also updates for status changes within a cycle
_state_machine = None
_status = IDLE, ''
statusMap = {}
def init_state_machine(self, **kwds):
self._state_machine = StateMachine(
logger=self.log,
idle_status=(IDLE, ''),
transition=self.state_transition,
reset_fast_poll=False,
status=(IDLE, ''),
**kwds)
def initModule(self):
super().initModule()
self.init_state_machine()
def state_transition(self, sm, newstate):
"""handle status updates"""
status = self.get_status(newstate)
if sm.next_task:
if isinstance(sm.next_task, Stop):
if newstate and status is not None:
status = status[0], 'stopping (%s)' % status[1]
elif newstate:
# restart case
if status is not None:
status = sm.status[0], 'restarting (%s)' % status[1]
else:
# start case
status = self.get_status(sm.next_task.newstate, BUSY)
if status:
sm.status = status
if self.all_status_changes:
self.read_status()
def get_status(self, statefunc, default_code=None):
if statefunc is None:
status = self._state_machine.idle_status or (ERROR, 'Finish was returned without final status')
else:
name = statefunc.__name__
status = self.statusMap.get(name)
if status is None and default_code is not None:
status = default_code, name.replace('_', ' ')
return status
def read_status(self):
sm = self._state_machine
if sm.status == self.status:
return Done
return sm.status
def doPoll(self):
super().doPoll()
sm = self._state_machine
sm.cycle()
if sm.statefunc is None:
if sm.reset_fast_poll:
sm.reset_fast_poll = False
self.setFastPoll(False)
self.read_status()
def start_machine(self, statefunc, fast_poll=True, **kwds):
sm = self._state_machine
sm.status = self.get_status(statefunc, BUSY)
if sm.statefunc:
sm.status = sm.status[0], 'restarting'
sm.start(statefunc, **kwds)
self.read_status()
if fast_poll:
sm.reset_fast_poll = True
self.setFastPoll(True)
self.pollInfo.trigger(True) # trigger poller
def stop_machine(self, stopped_status=(IDLE, 'stopped')):
sm = self._state_machine
if sm.is_active:
sm.idle_status = stopped_status
sm.stop()
sm.status = self.get_status(sm.statefunc, sm.status[0])[0], 'stopping'
self.read_status()
self.pollInfo.trigger(True) # trigger poller
@Command
def stop(self):
self.stop_machine()
def final_status(self, code=IDLE, text=''):
"""final status
Usage:
return self.final_status('IDLE', 'machine idle')
"""
sm = self._state_machine
sm.idle_status = code, text
sm.cleanup = None
return Finish

97
secop_psi/dilsc.py Normal file
View File

@ -0,0 +1,97 @@
# -*- 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>
# *****************************************************************************
"""vector field"""
from secop.core import Drivable, Done, BUSY, IDLE, WARN, ERROR
from secop.errors import BadValueError
from secop_psi.vector import Vector
DECREASE = 1
INCREASE = 2
class VectorField(Vector, Drivable):
_state = None
def doPoll(self):
"""periodically called method"""
try:
if self._starting:
# first decrease components
driving = False
for target, component in zip(self.target, self.components):
if target * component.value < 0:
# change sign: drive to zero first
target = 0
if abs(target) < abs(component.target):
if target != component.target:
component.write_target(target)
if component.isDriving():
driving = True
if driving:
return
# now we can go to the final targets
for target, component in zip(self.target, self.components):
component.write_target(target)
self._starting = False
else:
for component in self.components:
if component.isDriving():
return
self.setFastPoll(False)
finally:
super().doPoll()
def merge_status(self):
names = [c.name for c in self.components if c.status[0] >= ERROR]
if names:
return ERROR, 'error in %s' % ', '.join(names)
names = [c.name for c in self.components if c.isDriving()]
if self._state:
# self.log.info('merge %r', [c.status for c in self.components])
if names:
direction = 'down ' if self._state == DECREASE else ''
return BUSY, 'ramping %s%s' % (direction, ', '.join(names))
if self.status[0] == BUSY:
return self.status
return BUSY, 'driving'
if names:
return WARN, 'moving %s directly' % ', '.join(names)
names = [c.name for c in self.components if c.status[0] >= WARN]
if names:
return WARN, 'warnings in %s' % ', '.join(names)
return IDLE, ''
def write_target(self, value):
"""initiate target change"""
# first make sure target is valid
for target, component in zip(self.target, self.components):
# check against limits if individual components
component.check_limits(target)
if sum(v * v for v in value) > 1:
raise BadValueError('norm of vector too high')
self.log.info('decrease')
self.setFastPoll(True)
self.target = value
self._state = DECREASE
self.doPoll()
self.log.info('done write_target %r', value)
return Done

View File

@ -21,10 +21,10 @@
"""oxford instruments mercury IPS power supply""" """oxford instruments mercury IPS power supply"""
import time import time
from secop.core import Parameter, EnumType, FloatRange, BoolType from secop.core import Parameter, EnumType, FloatRange, BoolType, IntRange, StringType, Property, BUSY
from secop.lib.enum import Enum from secop.lib.enum import Enum
from secop.errors import BadValueError, HardwareError from secop.errors import BadValueError, HardwareError
from secop_psi.magfield import Magfield from secop_psi.magfield import Magfield, SimpleMagfield, Status
from secop_psi.mercury import MercuryChannel, off_on, Mapped from secop_psi.mercury import MercuryChannel, off_on, Mapped
from secop.lib.statemachine import Retry from secop.lib.statemachine import Retry
@ -34,54 +34,39 @@ hold_rtoz_rtos_clmp = Mapped(HOLD=Action.hold, RTOS=Action.run_to_set,
CURRENT_CHECK_SIZE = 2 CURRENT_CHECK_SIZE = 2
class Field(MercuryChannel, Magfield): class SimpleField(MercuryChannel, SimpleMagfield):
nunits = Property('number of IPS subunits', IntRange(1, 6), default=1)
action = Parameter('action', EnumType(Action), readonly=False) action = Parameter('action', EnumType(Action), readonly=False)
setpoint = Parameter('field setpoint', FloatRange(unit='T'), default=0) setpoint = Parameter('field setpoint', FloatRange(unit='T'), default=0)
voltage = Parameter('leads voltage', FloatRange(unit='V'), default=0) voltage = Parameter('leads voltage', FloatRange(unit='V'), default=0)
atob = Parameter('field to amp', FloatRange(0, unit='A/T'), default=0) atob = Parameter('field to amp', FloatRange(0, unit='A/T'), default=0)
I1 = Parameter('master current', FloatRange(unit='A'), default=0) working_ramp = Parameter('effective ramp', FloatRange(0, unit='T/min'), default=0)
I2 = Parameter('slave 2 current', FloatRange(unit='A'), default=0)
I3 = Parameter('slave 3 current', FloatRange(unit='A'), default=0)
V1 = Parameter('master voltage', FloatRange(unit='V'), default=0)
V2 = Parameter('slave 2 voltage', FloatRange(unit='V'), default=0)
V3 = Parameter('slave 3 voltage', FloatRange(unit='V'), default=0)
forced_persistent_field = Parameter(
'manual indication that persistent field is bad', BoolType(), readonly=False, default=False)
channel_type = 'PSU' channel_type = 'PSU'
_field_mismatch = None
nslaves = 3
slave_currents = None slave_currents = None
__init = True classdict = {}
def doPoll(self): def __new__(cls, name, logger, cfgdict, srv):
super().doPoll() base = cls.__bases__[1]
self.read_current() nunits = cfgdict.get('nunits', 1)
if nunits == 1:
obj = object.__new__(cls)
return obj
classname = cls.__name__ + str(nunits)
newclass = cls.classdict.get(classname)
if not newclass:
# create individual current and voltage parameters dynamically
attrs = {}
for i in range(1, nunits + 1):
attrs['I%d' % i] = Parameter('slave %s current' % i, FloatRange(unit='A'), default=0)
attrs['V%d' % i] = Parameter('slave %s voltage' % i, FloatRange(unit='V'), default=0)
newclass = type(classname, (cls,), attrs)
cls.classdict[classname] = newclass
obj = object.__new__(newclass)
return obj
def read_value(self): def read_value(self):
self.current = self.query('PSU:SIG:FLD') return self.query('PSU:SIG:FLD')
pf = self.query('PSU:SIG:PFLD')
if self.__init:
self.__init = False
self.persistent_field = pf
if self.switch_heater == self.switch_heater.on or self._field_mismatch is None:
self.forced_persistent_field = False
self._field_mismatch = False
return self.current
self._field_mismatch = abs(self.persistent_field - pf) > self.tolerance
return pf
def write_persistent_field(self, value):
if self.forced_persistent_field:
self._field_mismatch = False
return value
raise BadValueError('changing persistent field needs forced_persistent_field=True')
def write_target(self, target):
if self._field_mismatch:
self.forced_persistent_field = True
raise BadValueError('persistent field does not match - set persistent field to guessed value first')
return super().write_target(target)
def read_ramp(self): def read_ramp(self):
return self.query('PSU:SIG:RFST') return self.query('PSU:SIG:RFST')
@ -95,86 +80,224 @@ class Field(MercuryChannel, Magfield):
def write_action(self, value): def write_action(self, value):
return self.change('PSU:ACTN', value, hold_rtoz_rtos_clmp) return self.change('PSU:ACTN', value, hold_rtoz_rtos_clmp)
def read_switch_heater(self):
value = self.query('PSU:SIG:SWHT', off_on)
now = time.time()
if value != self.switch_heater:
if now < (self.switch_time[self.switch_heater] or 0) + 10:
# probably switch heater was changed, but IPS reply is not yet updated
return self.switch_heater
return value
def write_switch_heater(self, value):
return self.change('PSU:SIG:SWHT', value, off_on)
def read_atob(self): def read_atob(self):
return self.query('PSU:ATOB') return self.query('PSU:ATOB')
def read_voltage(self): def read_voltage(self):
return self.query('PSU:SIG:VOLT') return self.query('PSU:SIG:VOLT')
def read_working_ramp(self):
return self.query('PSU:SIG:RFLD')
def read_setpoint(self): def read_setpoint(self):
return self.query('PSU:SIG:FSET') return self.query('PSU:SIG:FSET')
def set_and_go(self, value):
self.setpoint = self.change('PSU:SIG:FSET', value)
assert self.write_action('hold') == 'hold'
assert self.write_action('run_to_set') == 'run_to_set'
def start_ramp_to_target(self, sm):
# if self.action != 'hold':
# assert self.write_action('hold') == 'hold'
# return Retry
self.set_and_go(sm.target)
sm.try_cnt = 5
return self.ramp_to_target
def ramp_to_target(self, sm):
try:
return super().ramp_to_target(sm)
except HardwareError:
sm.try_cnt -= 1
if sm.try_cnt < 0:
raise
self.set_and_go(sm.target)
return Retry
def final_status(self, *args, **kwds):
print('FINAL-hold')
self.write_action('hold')
return super().final_status(*args, **kwds)
def on_restart(self, sm):
print('ON_RESTART-hold', sm.sm)
self.write_action('hold')
return super().on_restart(sm)
class Field(SimpleField, Magfield):
wait_switch_on = Parameter(
'wait time to ensure switch is on', FloatRange(0, unit='s'), readonly=True, default=60)
wait_switch_off = Parameter(
'wait time to ensure switch is off', FloatRange(0, unit='s'), readonly=True, default=60)
forced_persistent_field = Parameter(
'manual indication that persistent field is bad', BoolType(), readonly=False, default=False)
_field_mismatch = None
__init = True
__switch_heater_fix = 0
def doPoll(self):
super().doPoll()
self.read_current()
def startModule(self, start_events):
# on restart, assume switch is changed long time ago, if not, the mercury
# will complain and this will be handled in start_ramp_to_field
self.switch_on_time = 0
self.switch_off_time = 0
self.switch_heater = self.query('PSU:SIG:SWHT', off_on)
super().startModule(start_events)
def read_value(self):
current = self.query('PSU:SIG:FLD')
pf = self.query('PSU:SIG:PFLD')
if self.__init:
self.__init = False
self.persistent_field = pf
if self.switch_heater == self.switch_heater.on or self._field_mismatch is None:
self.forced_persistent_field = False
self._field_mismatch = False
return current
self._field_mismatch = abs(self.persistent_field - pf) > self.tolerance
return pf
def read_current(self): def read_current(self):
if self.slave_currents is None: if self.slave_currents is None:
self.slave_currents = [[] for _ in range(self.nslaves + 1)] self.slave_currents = [[] for _ in range(self.nunits + 1)]
current = self.query('PSU:SIG:CURR') if self.nunits > 1:
for i in range(self.nslaves + 1): for i in range(1, self.nunits + 1):
if i:
curri = self.query('DEV:PSU.M%d:PSU:SIG:CURR' % i) curri = self.query('DEV:PSU.M%d:PSU:SIG:CURR' % i)
volti = self.query('DEV:PSU.M%d:PSU:SIG:VOLT' % i) volti = self.query('DEV:PSU.M%d:PSU:SIG:VOLT' % i)
setattr(self, 'I%d' % i, curri) setattr(self, 'I%d' % i, curri)
setattr(self, 'V%d' % i, volti) setattr(self, 'V%d' % i, volti)
self.slave_currents[i].append(curri) self.slave_currents[i].append(curri)
else: current = self.query('PSU:SIG:CURR')
self.slave_currents[i].append(current) self.slave_currents[0].append(current)
min_ = min(self.slave_currents[0]) / self.nunits
max_ = max(self.slave_currents[0]) / self.nunits
# keep one element more for the total current (first and last measurement is a total)
self.slave_currents[0] = self.slave_currents[0][-CURRENT_CHECK_SIZE-1:]
for i in range(1, self.nunits + 1):
min_i = min(self.slave_currents[i]) min_i = min(self.slave_currents[i])
max_i = max(self.slave_currents[i]) max_i = max(self.slave_currents[i])
min_ = min(self.slave_currents[0]) / self.nslaves
max_ = max(self.slave_currents[0]) / self.nslaves
if len(self.slave_currents[i]) > CURRENT_CHECK_SIZE: if len(self.slave_currents[i]) > CURRENT_CHECK_SIZE:
self.slave_currents[i] = self.slave_currents[i][-CURRENT_CHECK_SIZE:] self.slave_currents[i] = self.slave_currents[i][-CURRENT_CHECK_SIZE:]
if i and (min_i -1 > max_ or min_ > max_i + 1): if min_i - 0.1 > max_ or min_ > max_i + 0.1: # use an arbitrary 0.1 A tolerance
self.log.warning('individual currents mismatch %r', self.slave_currents) self.log.warning('individual currents mismatch %r', self.slave_currents)
else:
current = self.query('PSU:SIG:CURR')
if self.atob: if self.atob:
return current / self.atob return current / self.atob
return 0 return 0
def set_and_go(self, value): def write_persistent_field(self, value):
self.change('PSU:SIG:FSET', value) if self.forced_persistent_field:
assert self.write_action('hold') == 'hold' self._field_mismatch = False
assert self.write_action('run_to_set') == 'run_to_set' return value
raise BadValueError('changing persistent field needs forced_persistent_field=True')
def start_ramp_to_field(self, state): def write_target(self, target):
if self._field_mismatch:
self.forced_persistent_field = True
raise BadValueError('persistent field does not match - set persistent field to guessed value first')
return super().write_target(target)
def read_switch_heater(self):
value = self.query('PSU:SIG:SWHT', off_on)
now = time.time()
if value != self.switch_heater:
if now < self.__switch_heater_fix:
# probably switch heater was changed, but IPS reply is not yet updated
if self.switch_heater:
self.switch_on_time = time.time()
else:
self.switch_off_time = time.time()
return self.switch_heater
return value
def read_wait_switch_on(self):
return self.query('PSU:SWONT') * 0.001
def read_wait_switch_off(self):
return self.query('PSU:SWOFT') * 0.001
def write_switch_heater(self, value):
if value == self.read_switch_heater():
self.log.info('switch heater already %r', value)
# we do not want to restart the timer
return value
self.__switch_heater_fix = time.time() + 10
return self.change('PSU:SIG:SWHT', value, off_on)
def start_ramp_to_field(self, sm):
if abs(self.current - self.persistent_field) <= self.tolerance:
self.log.info('leads %g are already at %g', self.current, self.persistent_field)
return self.ramp_to_field
try: try:
self.set_and_go(self.persistent_field) self.set_and_go(self.persistent_field)
except (HardwareError, AssertionError): except (HardwareError, AssertionError) as e:
state.switch_undef = self.switch_time[self.switch_heater.on] or state.now if self.switch_heater:
self.log.warn('switch is already on!')
return self.ramp_to_field
self.log.warn('wait first for switch off current=%g pf=%g', self.current, self.persistent_field)
return Retry
self.status = Status.PREPARING, 'wait for switch off'
sm.after_wait = self.ramp_to_field
return self.wait_for_switch return self.wait_for_switch
return self.ramp_to_field return self.ramp_to_field
def ramp_to_field(self, state): def start_ramp_to_target(self, sm):
if self.action != 'run_to_set': sm.try_cnt = 5
self.status = Status.PREPARING, 'restart ramp to field' try:
return self.start_ramp_to_field self.set_and_go(sm.target)
return super().ramp_to_field(state) except (HardwareError, AssertionError) as e:
self.log.warn('switch not yet ready %r', e)
def wait_for_switch(self, state): self.status = Status.PREPARING, 'wait for switch on'
if state.now - state.switch_undef < self.wait_switch_on: sm.after_wait = self.ramp_to_target
return Retry() return self.wait_for_switch
self.set_and_go(self.persistent_field)
return self.ramp_to_field
def start_ramp_to_target(self, state):
self.set_and_go(self.target)
return self.ramp_to_target return self.ramp_to_target
def start_ramp_to_zero(self, state): def ramp_to_field(self, sm):
try:
return super().ramp_to_field(sm)
except HardwareError:
sm.try_cnt -= 1
if sm.try_cnt < 0:
raise
self.set_and_go(sm.persistent_field)
return Retry
def wait_for_switch(self, sm):
if not self.delay(10):
return Retry
try:
self.log.warn('try again')
# try again
self.set_and_go(self.persistent_field)
except (HardwareError, AssertionError) as e:
return Retry
return sm.after_wait
def start_ramp_to_zero(self, sm):
try:
assert self.write_action('hold') == 'hold' assert self.write_action('hold') == 'hold'
assert self.write_action('run_to_zero') == 'run_to_zero' assert self.write_action('run_to_zero') == 'run_to_zero'
except (HardwareError, AssertionError) as e:
self.log.warn('switch not yet ready %r', e)
self.status = Status.PREPARING, 'wait for switch off'
sm.after_wait = self.ramp_to_zero
return self.wait_for_switch
return self.ramp_to_zero return self.ramp_to_zero
def finish_state(self, state): def ramp_to_zero(self, sm):
self.write_action('hold') try:
super().finish_state(state) return super().ramp_to_zero(sm)
except HardwareError:
sm.try_cnt -= 1
if sm.try_cnt < 0:
raise
assert self.write_action('hold') == 'hold'
assert self.write_action('run_to_zero') == 'run_to_zero'
return Retry

View File

@ -20,12 +20,12 @@
"""generic persistent magnet driver""" """generic persistent magnet driver"""
import time import time
from secop.core import Drivable, Parameter, Done from secop.core import Drivable, Parameter, Done, IDLE, BUSY, ERROR
from secop.datatypes import FloatRange, EnumType, ArrayOf, TupleOf, StatusType from secop.datatypes import FloatRange, EnumType, ArrayOf, TupleOf, StatusType
from secop.features import HasLimits from secop.features import HasLimits
from secop.errors import ConfigError, ProgrammingError from secop.errors import ConfigError, ProgrammingError, HardwareError
from secop.lib.enum import Enum from secop.lib.enum import Enum
from secop.lib.statemachine import Retry, StateMachine from secop.states import Retry, HasStates, status_code
UNLIMITED = FloatRange() UNLIMITED = FloatRange()
@ -48,52 +48,23 @@ OFF = 0
ON = 1 ON = 1
class Magfield(HasLimits, Drivable): class SimpleMagfield(HasStates, HasLimits, Drivable):
value = Parameter('magnetic field', datatype=FloatRange(unit='T')) value = Parameter('magnetic field', datatype=FloatRange(unit='T'))
status = Parameter(datatype=StatusType(Status)) ramp = Parameter(
mode = Parameter( 'wanted ramp rate for field', FloatRange(unit='$/min'), readonly=False)
'persistent mode', EnumType(Mode), readonly=False, default=Mode.PERSISTENT) # export only when different from ramp:
workingramp = Parameter(
'effective ramp rate for field', FloatRange(unit='$/min'), export=False)
tolerance = Parameter( tolerance = Parameter(
'tolerance', FloatRange(0, unit='$'), readonly=False, default=0.0002) 'tolerance', FloatRange(0, unit='$'), readonly=False, default=0.0002)
switch_heater = Parameter('switch heater', EnumType(off=OFF, on=ON),
readonly=False, default=0)
persistent_field = Parameter(
'persistent field', FloatRange(unit='$'), readonly=False)
current = Parameter(
'leads current (in units of field)', FloatRange(unit='$'))
ramp = Parameter(
'ramp rate for field', FloatRange(unit='$/min'), readonly=False)
trained = Parameter( trained = Parameter(
'trained field (positive)', 'trained field (positive)',
TupleOf(FloatRange(-99, 0, unit='$'), FloatRange(0, unit='$')), TupleOf(FloatRange(-99, 0, unit='$'), FloatRange(0, unit='$')),
readonly=False, default=(0, 0)) readonly=False, default=(0, 0))
# TODO: time_to_target
# profile = Parameter(
# 'ramp limit table', ArrayOf(TupleOf(FloatRange(unit='$'), FloatRange(unit='$/min'))),
# readonly=False)
# profile_training = Parameter(
# 'ramp limit table when in training',
# ArrayOf(TupleOf(FloatRange(unit='$'), FloatRange(unit='$/min'))), readonly=False)
# TODO: the following parameters should be changed into properties after tests
wait_switch_on = Parameter(
'wait time to ensure switch is on', FloatRange(0, unit='s'), readonly=False, default=60)
wait_switch_off = Parameter(
'wait time to ensure switch is off', FloatRange(0, unit='s'), readonly=False, default=60)
wait_stable_leads = Parameter(
'wait time to ensure current is stable', FloatRange(0, unit='s'), readonly=False, default=6)
wait_stable_field = Parameter( wait_stable_field = Parameter(
'wait time to ensure field is stable', FloatRange(0, unit='s'), readonly=False, default=30) 'wait time to ensure field is stable', FloatRange(0, unit='s'), readonly=False, default=31)
persistent_limit = Parameter(
'above this limit, lead currents are not driven to 0',
FloatRange(0, unit='$'), readonly=False, default=99)
_state = None
_last_target = None _last_target = None
switch_time = None, None
def doPoll(self):
self.read_value()
self._state.cycle()
def checkProperties(self): def checkProperties(self):
dt = self.parameters['target'].datatype dt = self.parameters['target'].datatype
@ -104,216 +75,285 @@ class Magfield(HasLimits, Drivable):
dt.min = -max_ dt.min = -max_
super().checkProperties() super().checkProperties()
def initModule(self):
super().initModule()
self.registerCallbacks(self) # for update_switch_heater
self._state = StateMachine(logger=self.log, threaded=False, cleanup=self.cleanup_state)
def startModule(self, start_events):
start_events.queue(self.startupCheck)
super().startModule(start_events)
def startupCheck(self):
if self.read_switch_heater() and self.mode == Mode.PERSISTENT:
self.read_value() # check for persistent field mismatch
# switch off heater from previous live or manual intervention
self.write_mode(self.mode)
self.write_target(self.persistent_field)
else:
self._last_target = self.persistent_field
def write_target(self, target):
self.check_limits(target)
self.target = target
if not self._state.is_active:
# as long as the state machine is still running, it takes care of changing targets
self._state.start(self.start_field_change)
self.doPoll()
return Done
def write_mode(self, value):
self.mode = value
if not self._state.is_active:
self._state.start(self.start_field_change)
self.doPoll()
return Done
def cleanup_state(self, state):
self.status = Status.ERROR, repr(state.last_error)
self.log.error('in state %s: %r', state.state.__name__, state.last_error)
self.setFastPoll(False)
if self.switch_heater != 0:
self.persistent_field = self.read_value()
if self.mode != Mode.DRIVEN:
self.log.warning('turn switch heater off')
self.write_switch_heater(0)
def stop(self): def stop(self):
"""keep field at current value""" """keep field at current value"""
# let the state machine do the needed steps to finish # let the state machine do the needed steps to finish
self.write_target(self.value) self.write_target(self.value)
def start_field_change(self, state): def write_target(self, target):
self.check_limits(target)
self.start_machine(self.start_field_change, target=target)
return target
def init_progress(self, sm, value):
sm.prev_point = sm.now, value
def get_progress(self, sm, value):
"""return the time passed for at least one tolerance step"""
t, v = sm.prev_point
dif = abs(v - value)
tdif = sm.now - t
if dif > self.tolerance:
sm.prev_point = sm.now, value
return tdif
@status_code(BUSY, 'start ramp to target')
def start_field_change(self, sm):
self.setFastPoll(True, 1.0) self.setFastPoll(True, 1.0)
self.status = Status.PREPARING, 'changed target field' return self.start_ramp_to_target
if (self.target == self._last_target and
abs(self.target - self.persistent_field) <= self.tolerance): # short cut @status_code(BUSY, 'ramping field')
def ramp_to_target(self, sm):
if sm.init:
self.init_progress(sm, self.value)
# Remarks: assume there is a ramp limiting feature
if abs(self.value - sm.target) > self.tolerance:
if self.get_progress(sm, self.value):
return Retry
raise HardwareError('no progress')
sm.stabilize_start = time.time()
return self.stabilize_field
@status_code(BUSY, 'stabilizing field')
def stabilize_field(self, sm):
if sm.now - sm.stabilize_start < self.wait_stable_field:
return Retry
return self.final_status()
def read_workingramp(self):
return self.ramp
class Magfield(SimpleMagfield):
status = Parameter(datatype=StatusType(Status))
mode = Parameter(
'persistent mode', EnumType(Mode), readonly=False, default=Mode.PERSISTENT)
switch_heater = Parameter('switch heater', EnumType(off=OFF, on=ON),
readonly=False, default=0)
persistent_field = Parameter(
'persistent field', FloatRange(unit='$'), readonly=False)
current = Parameter(
'leads current (in units of field)', FloatRange(unit='$'))
# TODO: time_to_target
# profile = Parameter(
# 'ramp limit table', ArrayOf(TupleOf(FloatRange(unit='$'), FloatRange(unit='$/min'))),
# readonly=False)
# profile_training = Parameter(
# 'ramp limit table when in training',
# ArrayOf(TupleOf(FloatRange(unit='$'), FloatRange(unit='$/min'))), readonly=False)
# TODO: the following parameters should be changed into properties after tests
wait_switch_on = Parameter(
'wait time to ensure switch is on', FloatRange(0, unit='s'), readonly=False, default=61)
wait_switch_off = Parameter(
'wait time to ensure switch is off', FloatRange(0, unit='s'), readonly=False, default=61)
wait_stable_leads = Parameter(
'wait time to ensure current is stable', FloatRange(0, unit='s'), readonly=False, default=6)
persistent_limit = Parameter(
'above this limit, lead currents are not driven to 0',
FloatRange(0, unit='$'), readonly=False, default=99)
leads_ramp_tmo = Parameter(
'timeout for leads ramp progress',
FloatRange(0, unit='s'), readonly=False, default=30)
ramp_tmo = Parameter(
'timeout for field ramp progress',
FloatRange(0, unit='s'), readonly=False, default=30)
__init = True
switch_on_time = None
switch_off_time = None
def doPoll(self):
if self.__init:
self.__init = False
if self.read_switch_heater() and self.mode == Mode.PERSISTENT:
self.read_value() # check for persistent field mismatch
# switch off heater from previous live or manual intervention
self.write_target(self.persistent_field)
else:
self._last_target = self.persistent_field
else:
super().doPoll()
def initModule(self):
super().initModule()
self.registerCallbacks(self) # for update_switch_heater
def write_mode(self, value):
self.start_machine(self.start_field_change, cleanup=self.cleanup, target=self.target, mode=value)
return value
def write_target(self, target):
self.check_limits(target)
self.start_machine(self.start_field_change, cleanup=self.cleanup, target=target, mode=self.mode)
return target
def cleanup(self, sm): # sm is short for statemachine
if self.switch_heater != 0:
self.persistent_field = self.read_value()
if sm.mode != Mode.DRIVEN:
self.log.warning('turn switch heater off')
self.write_switch_heater(0)
@status_code('PREPARING')
def start_field_change(self, sm):
self.setFastPoll(True, 1.0)
if sm.target == self.persistent_field or (
sm.target == self._last_target and
abs(sm.target - self.persistent_field) <= self.tolerance): # short cut
return self.check_switch_off return self.check_switch_off
if self.switch_heater:
return self.start_switch_on
return self.start_ramp_to_field return self.start_ramp_to_field
def start_ramp_to_field(self, state): @status_code('PREPARING')
def start_ramp_to_field(self, sm):
"""start ramping current to persistent field """start ramping current to persistent field
should return ramp_to_field initiate ramp to persistent field (with corresponding ramp rate)
the implementation should return ramp_to_field
""" """
raise NotImplementedError raise NotImplementedError
def ramp_to_field(self, state): @status_code('PREPARING', 'ramp leads to match field')
"""ramping, wait for current at persistent field""" def ramp_to_field(self, sm):
if (self.target == self._last_target and if sm.init:
abs(self.target - self.persistent_field) <= self.tolerance): # short cut sm.stabilize_start = 0 # in case current is already at field
return self.check_switch_off self.init_progress(sm, self.current)
if abs(self.current - self.persistent_field) > self.tolerance: dif = abs(self.current - self.persistent_field)
if state.init: if dif > self.tolerance:
self.status = Status.PREPARING, 'ramping leads current to field' tdif = self.get_progress(sm, self.current)
return Retry() if tdif > self.leads_ramp_tmo:
state.stabilize_start = time.time() raise HardwareError('no progress')
sm.stabilize_start = None # force reset
return Retry
if sm.stabilize_start is None:
sm.stabilize_start = sm.now
return self.stabilize_current return self.stabilize_current
def stabilize_current(self, state): @status_code('PREPARING')
"""wait for stable current at persistent field""" def stabilize_current(self, sm):
if state.now - state.stabilize_start < self.wait_stable_leads: if sm.now - sm.stabilize_start < self.wait_stable_leads:
if state.init: return Retry
self.status = Status.PREPARING, 'stabilizing leads current'
return Retry()
return self.start_switch_on return self.start_switch_on
def update_switch_heater(self, value): def update_switch_heater(self, value):
"""is called whenever switch heater was changed""" """is called whenever switch heater was changed"""
switch_time = self.switch_time[value] print('SW', value)
if switch_time is None: if value == 0:
switch_time = time.time() if self.switch_off_time is None:
self.switch_time = [None, None] self.log.info('restart switch_off_time')
self.switch_time[value] = switch_time self.switch_off_time = time.time()
self.switch_on_time = None
else:
if self.switch_on_time is None:
self.log.info('restart switch_on_time')
self.switch_on_time = time.time()
self.switch_off_time = None
def start_switch_on(self, state): @status_code('PREPARING')
"""switch heater on""" def start_switch_on(self, sm):
if self.switch_heater == 0: if self.read_switch_heater() == 0:
self.status = Status.PREPARING, 'turn switch heater on' self.status = Status.PREPARING, 'turn switch heater on'
try: try:
self.write_switch_heater(True) self.write_switch_heater(True)
except Exception as e: except Exception as e:
self.log.warning('write_switch_heater %r', e) self.log.warning('write_switch_heater %r', e)
return Retry() return Retry
else: else:
self.status = Status.PREPARING, 'wait for heater on' self.status = Status.PREPARING, 'wait for heater on'
return self.switch_on return self.wait_for_switch_on
def switch_on(self, state): @status_code('PREPARING')
"""wait for switch heater open""" def wait_for_switch_on(self, sm):
if (self.target == self._last_target and if (sm.target == self._last_target and
abs(self.target - self.persistent_field) <= self.tolerance): # short cut abs(sm.target - self.persistent_field) <= self.tolerance): # short cut
return self.check_switch_off return self.check_switch_off
self.read_switch_heater() self.read_switch_heater() # trigger switch_on/off_time
if self.switch_time[ON] is None: if self.switch_heater == 0:
self.log.warning('switch turned off manually?') self.log.warning('switch turned off manually?')
return self.start_switch_on return self.start_switch_on
if state.now - self.switch_time[ON] < self.wait_switch_on: if sm.now - self.switch_on_time < self.wait_switch_on:
return Retry() if sm.delta(10):
self._last_target = self.target self.log.info('waited for %g sec', sm.now - self.switch_on_time)
return Retry
self._last_target = sm.target
return self.start_ramp_to_target return self.start_ramp_to_target
def start_ramp_to_target(self, state): @status_code('RAMPING')
"""start ramping current to target def start_ramp_to_target(self, sm):
"""start ramping current to target field
should return ramp_to_target initiate ramp to target
the implementation should return ramp_to_target
""" """
raise NotImplementedError raise NotImplementedError
def ramp_to_target(self, state): @status_code('RAMPING')
"""ramp field to target""" def ramp_to_target(self, sm):
if self.target != self._last_target: # target was changed
self._last_target = self.target
return self.start_ramp_to_target
self.persistent_field = self.value self.persistent_field = self.value
# Remarks: assume there is a ramp limiting feature dif = abs(self.value - sm.target)
if abs(self.value - self.target) > self.tolerance: if sm.init:
if state.init: sm.stabilize_start = 0 # in case current is already at target
self.status = Status.RAMPING, 'ramping field' self.init_progress(sm, self.value)
return Retry() if dif > self.tolerance:
state.stabilize_start = time.time() sm.stabilize_start = sm.now
tdif = self.get_progress(sm, self.value)
if tdif > self.workingramp / self.tolerance * 60 + self.ramp_tmo:
raise HardwareError('no progress')
sm.stabilize_start = None
return Retry
if sm.stabilize_start is None:
sm.stabilize_start = sm.now
return self.stabilize_field return self.stabilize_field
def stabilize_field(self, state): @status_code('STABILIZING')
"""stabilize field""" def stabilize_field(self, sm):
if self.target != self._last_target: # target was changed
self._last_target = self.target
return self.start_ramp_to_target
self.persistent_field = self.value self.persistent_field = self.value
if state.now - state.stabilize_start < self.wait_stable_field: if sm.now > sm.stablize_start + self.wait_stable_field:
if state.init: return Retry
self.status = Status.STABILIZING, 'stabilizing field'
return Retry()
return self.check_switch_off return self.check_switch_off
def check_switch_off(self, state): def check_switch_off(self, sm):
if self.mode == Mode.DRIVEN: if sm.mode == Mode.DRIVEN:
self.status = Status.PREPARED, 'driven' return self.final_status(Status.PREPARED, 'driven')
return self.finish_state
return self.start_switch_off return self.start_switch_off
def start_switch_off(self, state): @status_code('FINALIZING')
"""turn off switch heater""" def start_switch_off(self, sm):
if self.switch_heater == 1: if self.switch_heater == 1:
self.status = Status.FINALIZING, 'turn switch heater off'
self.write_switch_heater(False) self.write_switch_heater(False)
else: return self.wait_for_switch_off
self.status = Status.FINALIZING, 'wait for heater off'
return self.switch_off
def switch_off(self, state): @status_code('FINALIZING')
"""wait for switch heater closed""" def wait_for_switch_off(self, sm):
if self.target != self._last_target or self.mode == Mode.DRIVEN:
# target or mode has changed -> redo
self._last_target = None
return self.start_switch_on
self.persistent_field = self.value self.persistent_field = self.value
self.read_switch_heater() self.read_switch_heater()
if self.switch_time[OFF] is None: if self.switch_off_time is None:
self.log.warning('switch turned on manually?') self.log.warning('switch turned on manually?')
return self.start_switch_off return self.start_switch_off
if state.now - self.switch_time[OFF] < self.wait_switch_off: if sm.now - self.switch_off_time < self.wait_switch_off:
return Retry() return Retry
if abs(self.value) > self.persistent_limit: if abs(self.value) > self.persistent_limit:
self.status = Status.IDLE, 'leads current at field, switch off' return self.final_status(Status.IDLE, 'leads current at field, switch off')
return self.finish_state
return self.start_ramp_to_zero return self.start_ramp_to_zero
def start_ramp_to_zero(self, state): @status_code('FINALIZING')
"""start ramping current to target def start_ramp_to_zero(self, sm):
"""start ramping current to zero
initiate ramp to zero (with corresponding ramp rate) initiate ramp to zero (with corresponding ramp rate)
should return ramp_to_zero the implementation should return ramp_to_zero
""" """
raise NotImplementedError raise NotImplementedError
def ramp_to_zero(self, state): @status_code('FINALIZING')
def ramp_to_zero(self, sm):
"""ramp field to zero""" """ramp field to zero"""
if self.target != self._last_target or self.mode == Mode.DRIVEN: if sm.init:
# target or mode has changed -> redo self.init_progress(sm, self.current)
self._last_target = None
return self.start_field_change
if abs(self.current) > self.tolerance: if abs(self.current) > self.tolerance:
if state.init: if self.get_progress(sm, self.current, self.ramp) > self.leads_ramp_tmo:
self.status = Status.FINALIZING, 'ramp leads to zero' raise HardwareError('no progress')
return Retry() return Retry
if self.mode == Mode.DISABLED and self.persistent_field == 0: if sm.mode == Mode.DISABLED and self.persistent_field == 0:
self.status = Status.DISABLED, 'disabled' return self.final_status(Status.DISABLED, 'disabled')
else: return self.final_status(Status.IDLE, 'persistent mode')
self.status = Status.IDLE, 'persistent mode'
return self.finish_state
def finish_state(self, state):
"""finish"""
self.setFastPoll(False)
return None

View File

@ -38,6 +38,7 @@ SELF = 0
def as_float(value): def as_float(value):
"""converts string (with unit) to float and float to string"""
if isinstance(value, str): if isinstance(value, str):
return float(VALUE_UNIT.match(value).group(1)) return float(VALUE_UNIT.match(value).group(1))
return '%g' % value return '%g' % value
@ -82,7 +83,7 @@ class MercuryChannel(HasIO):
return 'DEV:%s:%s%s%s' % (slot, head, sep, tail) return 'DEV:%s:%s%s%s' % (slot, head, sep, tail)
return adr return adr
def multiquery(self, adr, names=(), convert=as_float): def multiquery(self, adr, names=(), convert=as_float, debug=None):
"""get parameter(s) in mercury syntax """get parameter(s) in mercury syntax
:param adr: the 'address part' of the SCPI command :param adr: the 'address part' of the SCPI command
@ -99,9 +100,16 @@ class MercuryChannel(HasIO):
self.slot='DB5.P1,DB3.G1' # -> take second slot self.slot='DB5.P1,DB3.G1' # -> take second slot
-> query command will be READ:DEV:DB3.G1:PRES:SIG:PERC -> query command will be READ:DEV:DB3.G1:PRES:SIG:PERC
""" """
# TODO: if the need arises: allow convert to be a list
adr = self._complete_adr(adr) adr = self._complete_adr(adr)
cmd = 'READ:%s:%s' % (adr, ':'.join(names)) cmd = 'READ:%s:%s' % (adr, ':'.join(names))
msg = ''
for _ in range(3):
if msg:
self.log.warning('%s', msg)
reply = self.communicate(cmd) reply = self.communicate(cmd)
if debug is not None:
debug.append(reply)
head = 'STAT:%s:' % adr head = 'STAT:%s:' % adr
try: try:
assert reply.startswith(head) assert reply.startswith(head)
@ -111,14 +119,17 @@ class MercuryChannel(HasIO):
return tuple(convert(r) for r in result) return tuple(convert(r) for r in result)
except (AssertionError, AttributeError, ValueError): except (AssertionError, AttributeError, ValueError):
time.sleep(0.1) # in case this was the answer of a previous command time.sleep(0.1) # in case this was the answer of a previous command
raise HardwareError('invalid reply %r to cmd %r' % (reply, cmd)) from None msg = 'invalid reply %r to cmd %r' % (reply, cmd)
else:
raise HardwareError(msg) from None
def multichange(self, adr, values, convert=as_float): def multichange(self, adr, values, convert=as_float, tolerance=0):
"""set parameter(s) in mercury syntax """set parameter(s) in mercury syntax
:param adr: as in see multiquery method :param adr: as in see multiquery method
:param values: [(name1, value1), (name2, value2) ...] :param values: [(name1, value1), (name2, value2) ...]
:param convert: a converter function (converts given value to string and replied string to value) :param convert: a converter function (converts given value to string and replied string to value)
:param tolerance: tolerance for readback check
:return: the values as tuple :return: the values as tuple
Example: Example:
@ -128,22 +139,40 @@ class MercuryChannel(HasIO):
self.slot='DB6.T1,DB1.H1' # and take first slot self.slot='DB6.T1,DB1.H1' # and take first slot
-> change command will be SET:DEV:DB6.T1:TEMP:LOOP:P:5:I:2:D:0 -> change command will be SET:DEV:DB6.T1:TEMP:LOOP:P:5:I:2:D:0
""" """
# TODO: if the need arises: allow convert and or tolerance to be a list
adr = self._complete_adr(adr) adr = self._complete_adr(adr)
params = ['%s:%s' % (k, convert(v)) for k, v in values] params = ['%s:%s' % (k, convert(v)) for k, v in values]
cmd = 'SET:%s:%s' % (adr, ':'.join(params)) cmd = 'SET:%s:%s' % (adr, ':'.join(params))
for _ in range(3): # try 3 times or until readback result matches
t = time.time()
reply = self.communicate(cmd) reply = self.communicate(cmd)
head = 'STAT:SET:%s:' % adr head = 'STAT:SET:%s:' % adr
try: try:
assert reply.startswith(head) assert reply.startswith(head)
replyiter = iter(reply[len(head):].split(':')) replyiter = iter(reply[len(head):].split(':'))
# reshuffle reply=(k1, r1, v1, k2, r2, v1) --> keys = (k1, k2), result = (r1, r2), valid = (v1, v2)
keys, result, valid = zip(*zip(replyiter, replyiter, replyiter)) keys, result, valid = zip(*zip(replyiter, replyiter, replyiter))
assert keys == tuple(k for k, _ in values) assert keys == tuple(k for k, _ in values)
assert any(v == 'VALID' for v in valid) assert any(v == 'VALID' for v in valid)
return tuple(convert(r) for r in result) result = tuple(convert(r) for r in result)
except (AssertionError, AttributeError, ValueError) as e: except (AssertionError, AttributeError, ValueError) as e:
time.sleep(0.1) # in case of missed replies this might help to skip garbage time.sleep(0.1) # in case of missed replies this might help to skip garbage
raise HardwareError('invalid reply %r to cmd %r' % (reply, cmd)) from e raise HardwareError('invalid reply %r to cmd %r' % (reply, cmd)) from e
keys = [v[0] for v in values]
debug = []
readback = self.multiquery(adr, keys, convert, debug)
for k, r, b in zip(keys, result, readback):
if convert == as_float:
tol = max(abs(r) * 1e-3, abs(b) * 1e-3, tolerance)
if abs(r - b) > tol:
break
elif r != b:
break
else:
return readback
self.log.warning('sent: %s', cmd)
self.log.warning('got: %s', debug[0])
return readback
def query(self, adr, convert=as_float): def query(self, adr, convert=as_float):
"""query a single parameter """query a single parameter
@ -153,9 +182,9 @@ class MercuryChannel(HasIO):
adr, _, name = adr.rpartition(':') adr, _, name = adr.rpartition(':')
return self.multiquery(adr, [name], convert)[0] return self.multiquery(adr, [name], convert)[0]
def change(self, adr, value, convert=as_float): def change(self, adr, value, convert=as_float, tolerance=0):
adr, _, name = adr.rpartition(':') adr, _, name = adr.rpartition(':')
return self.multichange(adr, [(name, value)], convert)[0] return self.multichange(adr, [(name, value)], convert, tolerance)[0]
class TemperatureSensor(MercuryChannel, Readable): class TemperatureSensor(MercuryChannel, Readable):

View File

@ -533,11 +533,14 @@ class SeaModule(Module):
if key == 'target': if key == 'target':
kwds['readonly'] = False kwds['readonly'] = False
prev = cls.accessibles[key] prev = cls.accessibles[key]
if key == 'status':
# special case: status from sea is a string, not the status tuple
pobj = prev.copy()
else:
pobj = Parameter(**kwds) pobj = Parameter(**kwds)
merged_properties = prev.propertyValues.copy() merged_properties = prev.propertyValues.copy()
pobj.updateProperties(merged_properties) pobj.updateProperties(merged_properties)
pobj.merge(merged_properties) pobj.merge(merged_properties)
datatype = kwds.get('datatype', cls.accessibles[key].datatype)
else: else:
pobj = Parameter(**kwds) pobj = Parameter(**kwds)
datatype = pobj.datatype datatype = pobj.datatype

View File

@ -20,11 +20,13 @@
# ***************************************************************************** # *****************************************************************************
"""oxford instruments triton (kelvinoxjt dil)""" """oxford instruments triton (kelvinoxjt dil)"""
from math import sqrt from math import sqrt, log10
from secop.core import Writable, Parameter, Readable, Drivable, IDLE, WARN, BUSY, ERROR, Done from secop.core import Writable, Parameter, Readable, Drivable, IDLE, WARN, BUSY, ERROR, \
from secop.datatypes import EnumType, FloatRange Done, Property
from secop.datatypes import EnumType, FloatRange, StringType
from secop.lib.enum import Enum from secop.lib.enum import Enum
from secop_psi.mercury import MercuryChannel, Mapped, off_on, HasInput, SELF from secop_psi.mercury import MercuryChannel, Mapped, off_on, HasInput, SELF
from secop.lib import clamp
import secop_psi.mercury as mercury import secop_psi.mercury as mercury
actions = Enum(none=0, condense=1, circulate=2, collect=3) actions = Enum(none=0, condense=1, circulate=2, collect=3)
@ -35,6 +37,7 @@ actions_map.mapping['NONE'] = actions.none # when writing, STOP is used instead
class Action(MercuryChannel, Writable): class Action(MercuryChannel, Writable):
channel_type = 'ACTN' channel_type = 'ACTN'
cooldown_channel = Property('cool down channel', StringType(), 'T5')
value = Parameter('running action', EnumType(actions)) value = Parameter('running action', EnumType(actions))
target = Parameter('action to do', EnumType(none=0, condense=1, collect=3), readonly=False) target = Parameter('action to do', EnumType(none=0, condense=1, collect=3), readonly=False)
_target = 0 _target = 0
@ -47,6 +50,7 @@ class Action(MercuryChannel, Writable):
def write_target(self, value): def write_target(self, value):
self._target = value self._target = value
self.change('SYS:DR:CHAN:COOL', self.cooldown_channel, str)
return self.change('SYS:DR:ACTN', value, actions_map) return self.change('SYS:DR:ACTN', value, actions_map)
# actions: # actions:
@ -245,10 +249,14 @@ class TemperatureLoop(ScannerChannel, mercury.TemperatureLoop):
ENABLE = 'TEMP:LOOP:MODE' ENABLE = 'TEMP:LOOP:MODE'
ENABLE_RAMP = 'TEMP:LOOP:RAMP:ENAB' ENABLE_RAMP = 'TEMP:LOOP:RAMP:ENAB'
RAMP_RATE = 'TEMP:LOOP:RAMP:RATE' RAMP_RATE = 'TEMP:LOOP:RAMP:RATE'
enable_pid_table = None # remove, does not work on triton enable_pid_table = None # remove, does not work on triton
ctrlpars = Parameter('pid (gain, integral (inv. time), differential time')
system_channel = Property('system channel name', StringType(), 'MC')
def write_control_active(self, value): def write_control_active(self, value):
self.change('SYS:DR:CHAN:MC', 'T5', str) if self.system_channel:
self.change('SYS:DR:CHAN:%s' % self.system_channel, self.slot.split(',')[0], str)
if value: if value:
self.change('TEMP:LOOP:FILT:ENAB', 'ON', str) self.change('TEMP:LOOP:FILT:ENAB', 'ON', str)
if self.output_module: if self.output_module:
@ -260,7 +268,7 @@ class TemperatureLoop(ScannerChannel, mercury.TemperatureLoop):
class HeaterOutput(HasInput, MercuryChannel, Writable): class HeaterOutput(HasInput, MercuryChannel, Writable):
"""heater output""" """heater output"""
channel_type = 'HTR' channel_type = 'HTR'
value = Parameter('heater output', FloatRange(unit='W')) value = Parameter('heater output', FloatRange(unit='uW'))
target = Parameter('heater output', FloatRange(0, unit='$'), readonly=False) target = Parameter('heater output', FloatRange(0, unit='$'), readonly=False)
resistivity = Parameter('heater resistivity', FloatRange(unit='Ohm')) resistivity = Parameter('heater resistivity', FloatRange(unit='Ohm'))
@ -268,7 +276,7 @@ class HeaterOutput(HasInput, MercuryChannel, Writable):
return self.query('HTR:RES') return self.query('HTR:RES')
def read_value(self): def read_value(self):
return self.query('HTR:SIG:POWR') * 1e-6 return round(self.query('HTR:SIG:POWR'), 3)
def read_target(self): def read_target(self):
if self.controlled_by != 0: if self.controlled_by != 0:
@ -277,24 +285,33 @@ class HeaterOutput(HasInput, MercuryChannel, Writable):
def write_target(self, value): def write_target(self, value):
self.write_controlled_by(SELF) self.write_controlled_by(SELF)
return self.change('HTR:SIG:POWR', value * 1e6) if self.resistivity:
# round to the next voltage step
value = round(sqrt(value * self.resistivity)) ** 2 / self.resistivity
return round(self.change('HTR:SIG:POWR', value), 3)
class HeaterOutputWithRange(HeaterOutput): class HeaterOutputWithRange(HeaterOutput):
"""heater output with heater range""" """heater output with heater range"""
channel_type = 'HTR,TEMP' channel_type = 'HTR,TEMP'
limit = Parameter('max. heater power', FloatRange(unit='W'), readonly=False) limit = Parameter('max. heater power', FloatRange(unit='uW'), readonly=False)
def read_limit(self): def read_limit(self):
maxcur = self.query('TEMP:LOOP:RANGE') * 0.001 # mA -> A maxcur = self.query('TEMP:LOOP:RANGE') # mA
return self.read_resistivity() * maxcur ** 2 return self.read_resistivity() * maxcur ** 2 # uW
def write_limit(self, value): def write_limit(self, value):
if value is None: if value is None:
maxcur = 0.1 # max. allowed current 100mA maxcur = 100 # max. allowed current 100mA
else: else:
maxcur = sqrt(value / self.read_resistivity()) maxcur = sqrt(value / self.read_resistivity())
self.change('TEMP:LOOP:RANGE', maxcur * 1000) for cur in 0.0316, 0.1, 0.316, 1, 3.16, 10, 31.6, 100:
if cur > maxcur * 0.999:
maxcur = cur
break
else:
maxcur = cur
self.change('TEMP:LOOP:RANGE', maxcur)
return self.read_limit() return self.read_limit()

89
secop_psi/vector.py Normal file
View File

@ -0,0 +1,89 @@
# -*- 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>
# *****************************************************************************
"""generic 3D vector"""
from secop.core import Attached, Drivable, Readable, Parameter, Done
from secop.datatypes import FloatRange, TupleOf, StatusType, Enum
class VectorRd(Readable):
"""generic readable vector"""
value = Parameter(datatype=TupleOf(FloatRange(), FloatRange(), FloatRange()))
x = Attached()
y = Attached()
z = Attached()
pollFuncs = None
components = None
def initModule(self):
super().initModule()
members = []
status_codes = {} # collect all possible status codes
components = []
for name in 'xyz':
component = getattr(self, name)
members.append(component.parameters['value'].datatype.copy())
components.append(component)
for code in component.status[0].enum.members:
status_codes[int(code)] = code.name
self.parameters['value'].datatype = TupleOf(*members)
self.parameters['status'].datatype = StatusType(Enum(
'status', **{k: v for v, k in status_codes.items()}))
self.components = components
def doPoll(self):
for component in self.components:
component.doPoll()
# update
component.pollInfo.last_main = self.pollInfo.last_main
self.value = self.merge_value()
self.status = self.merge_status()
def merge_value(self):
return [c.value for c in self.components]
def merge_status(self):
status = -1, ''
for c in self.components:
if c.status[0] > status[0]:
status = c.status
return status
def read_value(self):
return tuple((c.read_value() for c in self.components))
def read_status(self):
[c.read_status() for c in self.components]
return self.merge_status()
class Vector(Drivable, VectorRd):
"""generic drivable vector"""
target = Parameter(datatype=TupleOf(FloatRange(), FloatRange(), FloatRange()))
def initModule(self):
super().initModule()
members = []
for component in self.components:
members.append(component.parameters['target'].datatype.copy())
self.parameters['target'].datatype = TupleOf(*members)
def write_target(self, value):
return tuple((c.write_target(v) for v, c in zip(value, self.components)))