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

This commit is contained in:
zolliker 2022-10-04 15:13:23 +02:00
commit 971c1dcfee
19 changed files with 607 additions and 357 deletions

View File

@ -12,6 +12,7 @@ service = main
class = secop_psi.sea.SeaDrivable class = secop_psi.sea.SeaDrivable
io = sea_main io = sea_main
sea_object = tt sea_object = tt
rel_paths = . tt
[T_ccr] [T_ccr]
class = secop_psi.sea.SeaReadable class = secop_psi.sea.SeaReadable

View File

@ -63,5 +63,5 @@ uri = ma6-ts.psi.ch:3003
description = stick rotation, typically used for omega description = stick rotation, typically used for omega
class = secop_psi.phytron.Motor class = secop_psi.phytron.Motor
io = om_io io = om_io
encoder_mode = CHECK encoder_mode = NO

View File

@ -1,50 +1,50 @@
{"tt": {"base": "/tt", "params": [ {"tt": {"base": "/tt", "params": [
{"path": "", "type": "float", "readonly": false, "cmd": "run tt", "description": "tt", "kids": 18}, {"path": "", "type": "float", "readonly": false, "cmd": "run tt", "description": "tt", "kids": 18},
{"path": "send", "type": "text", "readonly": false, "cmd": "tt send", "visibility": 3}, {"path": "send", "type": "text", "readonly": false, "cmd": "tt send", "visibility": 3},
{"path": "status", "type": "text", "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": "is_running", "type": "int", "readonly": false, "cmd": "tt is_running", "visibility": 3},
{"path": "mainloop", "type": "text", "readonly": false, "cmd": "tt mainloop", "visibility": 3}, {"path": "mainloop", "type": "text", "readonly": false, "cmd": "tt mainloop", "visibility": 3},
{"path": "target", "type": "float"}, {"path": "target", "type": "float", "readonly": false, "cmd": "run tt"},
{"path": "running", "type": "int"}, {"path": "running", "type": "int", "readonly": false, "cmd": "run tt"},
{"path": "tolerance", "type": "float", "readonly": false, "cmd": "tt tolerance"}, {"path": "tolerance", "type": "float", "readonly": false, "cmd": "tt tolerance"},
{"path": "maxwait", "type": "float", "readonly": false, "cmd": "tt maxwait"}, {"path": "maxwait", "type": "float", "readonly": false, "cmd": "tt maxwait"},
{"path": "settle", "type": "float", "readonly": false, "cmd": "tt settle"}, {"path": "settle", "type": "float", "readonly": false, "cmd": "tt settle"},
{"path": "log", "type": "text", "readonly": false, "cmd": "tt log", "visibility": 3, "kids": 4}, {"path": "log", "type": "text", "readonly": false, "cmd": "tt log", "visibility": 3, "kids": 4},
{"path": "log/mean", "type": "float", "visibility": 3}, {"path": "log/mean", "type": "float", "readonly": false, "cmd": "run tt", "visibility": 3},
{"path": "log/m2", "type": "float", "visibility": 3}, {"path": "log/m2", "type": "float", "readonly": false, "cmd": "run tt", "visibility": 3},
{"path": "log/stddev", "type": "float", "visibility": 3}, {"path": "log/stddev", "type": "float", "readonly": false, "cmd": "run tt", "visibility": 3},
{"path": "log/n", "type": "float", "visibility": 3}, {"path": "log/n", "type": "float", "readonly": false, "cmd": "run tt", "visibility": 3},
{"path": "dblctrl", "type": "bool", "readonly": false, "cmd": "tt dblctrl", "kids": 9}, {"path": "dblctrl", "type": "bool", "readonly": false, "cmd": "tt dblctrl", "kids": 9},
{"path": "dblctrl/tshift", "type": "float", "readonly": false, "cmd": "tt dblctrl/tshift"}, {"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/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_up", "type": "float", "readonly": false, "cmd": "run tt"},
{"path": "dblctrl/shift_lo", "type": "float"}, {"path": "dblctrl/shift_lo", "type": "float", "readonly": false, "cmd": "run tt"},
{"path": "dblctrl/t_min", "type": "float"}, {"path": "dblctrl/t_min", "type": "float", "readonly": false, "cmd": "run tt"},
{"path": "dblctrl/t_max", "type": "float"}, {"path": "dblctrl/t_max", "type": "float", "readonly": false, "cmd": "run tt"},
{"path": "dblctrl/int2", "type": "float", "readonly": false, "cmd": "tt dblctrl/int2"}, {"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_up", "type": "float", "readonly": false, "cmd": "tt dblctrl/prop_up"},
{"path": "dblctrl/prop_lo", "type": "float", "readonly": false, "cmd": "tt dblctrl/prop_lo"}, {"path": "dblctrl/prop_lo", "type": "float", "readonly": false, "cmd": "tt dblctrl/prop_lo"},
{"path": "tm", "type": "float", "kids": 4}, {"path": "tm", "type": "float", "readonly": false, "cmd": "run tt", "kids": 4},
{"path": "tm/curve", "type": "text", "readonly": false, "cmd": "tt tm/curve", "kids": 1}, {"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/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/alarm", "type": "float", "readonly": false, "cmd": "tt tm/alarm"},
{"path": "tm/stddev", "type": "float"}, {"path": "tm/stddev", "type": "float", "readonly": false, "cmd": "run tt"},
{"path": "tm/raw", "type": "float"}, {"path": "tm/raw", "type": "float", "readonly": false, "cmd": "run tt"},
{"path": "ts", "type": "float", "kids": 4}, {"path": "ts", "type": "float", "readonly": false, "cmd": "run tt", "kids": 4},
{"path": "ts/curve", "type": "text", "readonly": false, "cmd": "tt ts/curve", "kids": 1}, {"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/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/alarm", "type": "float", "readonly": false, "cmd": "tt ts/alarm"},
{"path": "ts/stddev", "type": "float"}, {"path": "ts/stddev", "type": "float", "readonly": false, "cmd": "run tt"},
{"path": "ts/raw", "type": "float"}, {"path": "ts/raw", "type": "float", "readonly": false, "cmd": "run tt"},
{"path": "ts_2", "type": "float", "kids": 4}, {"path": "ts_2", "type": "float", "readonly": false, "cmd": "run tt", "kids": 4},
{"path": "ts_2/curve", "type": "text", "readonly": false, "cmd": "tt ts_2/curve", "kids": 1}, {"path": "ts_2/curve", "type": "text", "readonly": false, "cmd": "tt ts_2/curve", "kids": 1},
{"path": "ts_2/curve/points", "type": "floatvarar", "readonly": false, "cmd": "tt ts_2/curve/points", "visibility": 3}, {"path": "ts_2/curve/points", "type": "floatvarar", "readonly": false, "cmd": "tt ts_2/curve/points", "visibility": 3},
{"path": "ts_2/alarm", "type": "float", "readonly": false, "cmd": "tt ts_2/alarm"}, {"path": "ts_2/alarm", "type": "float", "readonly": false, "cmd": "tt ts_2/alarm"},
{"path": "ts_2/stddev", "type": "float"}, {"path": "ts_2/stddev", "type": "float", "readonly": false, "cmd": "run tt"},
{"path": "ts_2/raw", "type": "float"}, {"path": "ts_2/raw", "type": "float", "readonly": false, "cmd": "run tt"},
{"path": "set", "type": "float", "readonly": false, "cmd": "tt set", "kids": 18}, {"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/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/reg", "type": "float", "readonly": false, "cmd": "run tt"},
{"path": "set/ramp", "type": "float", "readonly": false, "cmd": "tt set/ramp", "description": "maximum ramp in K/min (0: ramp off)"}, {"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/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/smooth", "type": "float", "readonly": false, "cmd": "tt set/smooth", "description": "smooth time (minutes)"},
@ -53,17 +53,17 @@
{"path": "set/resist", "type": "float", "readonly": false, "cmd": "tt set/resist"}, {"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/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/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/maxpowerlim", "type": "float", "readonly": false, "cmd": "run tt", "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/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/maxcurrent", "type": "float", "readonly": false, "cmd": "run tt", "description": "the maximum current before any booster or converter"},
{"path": "set/manualpower", "type": "float", "readonly": false, "cmd": "tt set/manualpower"}, {"path": "set/manualpower", "type": "float", "readonly": false, "cmd": "tt set/manualpower"},
{"path": "set/power", "type": "float"}, {"path": "set/power", "type": "float", "readonly": false, "cmd": "run tt"},
{"path": "set/prop", "type": "float", "readonly": false, "cmd": "tt set/prop", "description": "bigger means more gain"}, {"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/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": "set/deriv", "type": "float", "readonly": false, "cmd": "tt set/deriv"},
{"path": "setsamp", "type": "float", "readonly": false, "cmd": "tt setsamp", "kids": 18}, {"path": "setsamp", "type": "float", "readonly": false, "cmd": "tt setsamp", "kids": 18},
{"path": "setsamp/mode", "type": "enum", "enum": {"disabled": -1, "off": 0, "controlling": 1, "manual": 2}, "readonly": false, "cmd": "tt setsamp/mode"}, {"path": "setsamp/mode", "type": "enum", "enum": {"disabled": -1, "off": 0, "controlling": 1, "manual": 2}, "readonly": false, "cmd": "tt setsamp/mode"},
{"path": "setsamp/reg", "type": "float"}, {"path": "setsamp/reg", "type": "float", "readonly": false, "cmd": "run tt"},
{"path": "setsamp/ramp", "type": "float", "readonly": false, "cmd": "tt setsamp/ramp", "description": "maximum ramp in K/min (0: ramp off)"}, {"path": "setsamp/ramp", "type": "float", "readonly": false, "cmd": "tt setsamp/ramp", "description": "maximum ramp in K/min (0: ramp off)"},
{"path": "setsamp/wramp", "type": "float", "readonly": false, "cmd": "tt setsamp/wramp"}, {"path": "setsamp/wramp", "type": "float", "readonly": false, "cmd": "tt setsamp/wramp"},
{"path": "setsamp/smooth", "type": "float", "readonly": false, "cmd": "tt setsamp/smooth", "description": "smooth time (minutes)"}, {"path": "setsamp/smooth", "type": "float", "readonly": false, "cmd": "tt setsamp/smooth", "description": "smooth time (minutes)"},
@ -72,16 +72,16 @@
{"path": "setsamp/resist", "type": "float", "readonly": false, "cmd": "tt setsamp/resist"}, {"path": "setsamp/resist", "type": "float", "readonly": false, "cmd": "tt setsamp/resist"},
{"path": "setsamp/maxheater", "type": "text", "readonly": false, "cmd": "tt setsamp/maxheater", "description": "maximum heater limit, units should be given without space: W, mW, A, mA"}, {"path": "setsamp/maxheater", "type": "text", "readonly": false, "cmd": "tt setsamp/maxheater", "description": "maximum heater limit, units should be given without space: W, mW, A, mA"},
{"path": "setsamp/linearpower", "type": "float", "readonly": false, "cmd": "tt setsamp/linearpower", "description": "when not 0, it is the maximum effective power, and the power is linear to the heater output"}, {"path": "setsamp/linearpower", "type": "float", "readonly": false, "cmd": "tt setsamp/linearpower", "description": "when not 0, it is the maximum effective power, and the power is linear to the heater output"},
{"path": "setsamp/maxpowerlim", "type": "float", "description": "the maximum power limit (before any booster or converter)"}, {"path": "setsamp/maxpowerlim", "type": "float", "readonly": false, "cmd": "run tt", "description": "the maximum power limit (before any booster or converter)"},
{"path": "setsamp/maxpower", "type": "float", "readonly": false, "cmd": "tt setsamp/maxpower", "description": "maximum power [W]"}, {"path": "setsamp/maxpower", "type": "float", "readonly": false, "cmd": "tt setsamp/maxpower", "description": "maximum power [W]"},
{"path": "setsamp/maxcurrent", "type": "float", "description": "the maximum current before any booster or converter"}, {"path": "setsamp/maxcurrent", "type": "float", "readonly": false, "cmd": "run tt", "description": "the maximum current before any booster or converter"},
{"path": "setsamp/manualpower", "type": "float", "readonly": false, "cmd": "tt setsamp/manualpower"}, {"path": "setsamp/manualpower", "type": "float", "readonly": false, "cmd": "tt setsamp/manualpower"},
{"path": "setsamp/power", "type": "float"}, {"path": "setsamp/power", "type": "float", "readonly": false, "cmd": "run tt"},
{"path": "setsamp/prop", "type": "float", "readonly": false, "cmd": "tt setsamp/prop", "description": "bigger means more gain"}, {"path": "setsamp/prop", "type": "float", "readonly": false, "cmd": "tt setsamp/prop", "description": "bigger means more gain"},
{"path": "setsamp/integ", "type": "float", "readonly": false, "cmd": "tt setsamp/integ", "description": "bigger means faster"}, {"path": "setsamp/integ", "type": "float", "readonly": false, "cmd": "tt setsamp/integ", "description": "bigger means faster"},
{"path": "setsamp/deriv", "type": "float", "readonly": false, "cmd": "tt setsamp/deriv"}, {"path": "setsamp/deriv", "type": "float", "readonly": false, "cmd": "tt setsamp/deriv"},
{"path": "display", "type": "text", "readonly": false, "cmd": "tt display"}, {"path": "display", "type": "text", "readonly": false, "cmd": "tt display"},
{"path": "remote", "type": "bool"}]}, {"path": "remote", "type": "bool", "readonly": false, "cmd": "run tt"}]},
"cc": {"base": "/cc", "params": [ "cc": {"base": "/cc", "params": [
{"path": "", "type": "bool", "kids": 96}, {"path": "", "type": "bool", "kids": 96},
@ -239,16 +239,16 @@
{"path": "", "type": "enum", "enum": {"xds35_auto": 0, "xds35_manual": 1, "sv65": 2, "other": 3, "no": -1}, "readonly": false, "cmd": "hepump", "description": "xds35: scroll pump, sv65: leybold", "kids": 10}, {"path": "", "type": "enum", "enum": {"xds35_auto": 0, "xds35_manual": 1, "sv65": 2, "other": 3, "no": -1}, "readonly": false, "cmd": "hepump", "description": "xds35: scroll pump, sv65: leybold", "kids": 10},
{"path": "send", "type": "text", "readonly": false, "cmd": "hepump send", "visibility": 3}, {"path": "send", "type": "text", "readonly": false, "cmd": "hepump send", "visibility": 3},
{"path": "status", "type": "text", "visibility": 3}, {"path": "status", "type": "text", "visibility": 3},
{"path": "running", "type": "bool", "readonly": false, "cmd": "hepump running", "visibility": 3}, {"path": "running", "type": "bool", "readonly": false, "cmd": "hepump running"},
{"path": "eco", "type": "bool", "readonly": false, "cmd": "hepump eco", "visibility": 3}, {"path": "eco", "type": "bool", "readonly": false, "cmd": "hepump eco"},
{"path": "auto", "type": "bool", "readonly": false, "cmd": "hepump auto", "visibility": 3}, {"path": "auto", "type": "bool", "readonly": false, "cmd": "hepump auto"},
{"path": "valve", "type": "enum", "enum": {"closed": 0, "closing": 1, "opening": 2, "opened": 3, "undefined": 4}, "readonly": false, "cmd": "hepump valve", "visibility": 3}, {"path": "valve", "type": "enum", "enum": {"closed": 0, "closing": 1, "opening": 2, "opened": 3, "undefined": 4}, "readonly": false, "cmd": "hepump valve"},
{"path": "eco_t_lim", "type": "float", "readonly": false, "cmd": "hepump eco_t_lim", "description": "switch off eco mode when T_set < eco_t_lim and T < eco_t_lim * 2", "visibility": 3}, {"path": "eco_t_lim", "type": "float", "readonly": false, "cmd": "hepump eco_t_lim", "description": "switch off eco mode when T_set < eco_t_lim and T < eco_t_lim * 2"},
{"path": "calib", "type": "float", "readonly": false, "cmd": "hepump calib", "visibility": 3}, {"path": "calib", "type": "float", "readonly": false, "cmd": "hepump calib", "visibility": 3},
{"path": "health", "type": "float"}]}, {"path": "health", "type": "float"}]},
"hemot": {"base": "/hepump/hemot", "params": [ "hemot": {"base": "/hepump/hemot", "params": [
{"path": "", "type": "float", "readonly": false, "cmd": "run hemot", "visibility": 3, "kids": 30}, {"path": "", "type": "float", "readonly": false, "cmd": "run hemot", "kids": 30},
{"path": "send", "type": "text", "readonly": false, "cmd": "hemot send", "visibility": 3}, {"path": "send", "type": "text", "readonly": false, "cmd": "hemot send", "visibility": 3},
{"path": "status", "type": "text", "visibility": 3}, {"path": "status", "type": "text", "visibility": 3},
{"path": "is_running", "type": "int", "readonly": false, "cmd": "hemot is_running", "visibility": 3}, {"path": "is_running", "type": "int", "readonly": false, "cmd": "hemot is_running", "visibility": 3},
@ -280,6 +280,16 @@
{"path": "customadr", "type": "text", "readonly": false, "cmd": "hemot customadr"}, {"path": "customadr", "type": "text", "readonly": false, "cmd": "hemot customadr"},
{"path": "custompar", "type": "float", "readonly": false, "cmd": "hemot custompar"}]}, {"path": "custompar", "type": "float", "readonly": false, "cmd": "hemot custompar"}]},
"nvflow": {"base": "/nvflow", "params": [
{"path": "", "type": "float", "kids": 7},
{"path": "send", "type": "text", "readonly": false, "cmd": "nvflow send", "visibility": 3},
{"path": "status", "type": "text", "visibility": 3},
{"path": "stddev", "type": "float"},
{"path": "nsamples", "type": "int", "readonly": false, "cmd": "nvflow nsamples"},
{"path": "offset", "type": "float", "readonly": false, "cmd": "nvflow offset"},
{"path": "scale", "type": "float", "readonly": false, "cmd": "nvflow scale"},
{"path": "save", "type": "bool", "readonly": false, "cmd": "nvflow save", "description": "unchecked: current calib is not saved. set checked: save calib"}]},
"ln2fill": {"base": "/ln2fill", "params": [ "ln2fill": {"base": "/ln2fill", "params": [
{"path": "", "type": "enum", "enum": {"watching": 0, "fill": 1, "inactive": 2}, "readonly": false, "cmd": "ln2fill", "kids": 14}, {"path": "", "type": "enum", "enum": {"watching": 0, "fill": 1, "inactive": 2}, "readonly": false, "cmd": "ln2fill", "kids": 14},
{"path": "send", "type": "text", "readonly": false, "cmd": "ln2fill send", "visibility": 3}, {"path": "send", "type": "text", "readonly": false, "cmd": "ln2fill send", "visibility": 3},
@ -317,32 +327,32 @@
{"path": "vext", "type": "float"}]}, {"path": "vext", "type": "float"}]},
"mf": {"base": "/mf", "params": [ "mf": {"base": "/mf", "params": [
{"path": "", "type": "float", "kids": 26}, {"path": "", "type": "float", "readonly": false, "cmd": "run mf", "kids": 26},
{"path": "persmode", "type": "int", "readonly": false, "cmd": "mf persmode"}, {"path": "persmode", "type": "int", "readonly": false, "cmd": "mf persmode"},
{"path": "perswitch", "type": "int"}, {"path": "perswitch", "type": "int", "readonly": false, "cmd": "run mf"},
{"path": "nowait", "type": "int", "readonly": false, "cmd": "mf nowait"}, {"path": "nowait", "type": "int", "readonly": false, "cmd": "mf nowait"},
{"path": "maxlimit", "type": "float", "visibility": 3}, {"path": "maxlimit", "type": "float", "readonly": false, "cmd": "run mf", "visibility": 3},
{"path": "limit", "type": "float", "readonly": false, "cmd": "mf limit"}, {"path": "limit", "type": "float", "readonly": false, "cmd": "mf limit"},
{"path": "ramp", "type": "float", "readonly": false, "cmd": "mf ramp"}, {"path": "ramp", "type": "float", "readonly": false, "cmd": "mf ramp"},
{"path": "perscurrent", "type": "float", "readonly": false, "cmd": "mf perscurrent"}, {"path": "perscurrent", "type": "float", "readonly": false, "cmd": "mf perscurrent"},
{"path": "perslimit", "type": "float", "readonly": false, "cmd": "mf perslimit"}, {"path": "perslimit", "type": "float", "readonly": false, "cmd": "mf perslimit"},
{"path": "perswait", "type": "int", "readonly": false, "cmd": "mf perswait"}, {"path": "perswait", "type": "int", "readonly": false, "cmd": "mf perswait"},
{"path": "persdelay", "type": "int", "readonly": false, "cmd": "mf persdelay"}, {"path": "persdelay", "type": "int", "readonly": false, "cmd": "mf persdelay"},
{"path": "current", "type": "float"}, {"path": "current", "type": "float", "readonly": false, "cmd": "run mf"},
{"path": "measured", "type": "float"}, {"path": "measured", "type": "float", "readonly": false, "cmd": "run mf"},
{"path": "voltage", "type": "float"}, {"path": "voltage", "type": "float", "readonly": false, "cmd": "run mf"},
{"path": "lastfield", "type": "float", "visibility": 3}, {"path": "lastfield", "type": "float", "readonly": false, "cmd": "run mf", "visibility": 3},
{"path": "ampRamp", "type": "float", "visibility": 3}, {"path": "ampRamp", "type": "float", "readonly": false, "cmd": "run mf", "visibility": 3},
{"path": "inductance", "type": "float", "visibility": 3}, {"path": "inductance", "type": "float", "readonly": false, "cmd": "run mf", "visibility": 3},
{"path": "trainedTo", "type": "float", "readonly": false, "cmd": "mf trainedTo"}, {"path": "trainedTo", "type": "float", "readonly": false, "cmd": "mf trainedTo"},
{"path": "trainMode", "type": "int"}, {"path": "trainMode", "type": "int", "readonly": false, "cmd": "run mf"},
{"path": "external", "type": "int", "readonly": false, "cmd": "mf external"}, {"path": "external", "type": "int", "readonly": false, "cmd": "mf external"},
{"path": "startScript", "type": "text", "readonly": false, "cmd": "mf startScript", "visibility": 3}, {"path": "startScript", "type": "text", "readonly": false, "cmd": "mf startScript", "visibility": 3},
{"path": "is_running", "type": "int", "visibility": 3}, {"path": "is_running", "type": "int", "readonly": false, "cmd": "run mf", "visibility": 3},
{"path": "verbose", "type": "int", "readonly": false, "cmd": "mf verbose", "visibility": 3}, {"path": "verbose", "type": "int", "readonly": false, "cmd": "mf verbose", "visibility": 3},
{"path": "driver", "type": "text", "visibility": 3}, {"path": "driver", "type": "text", "readonly": false, "cmd": "run mf", "visibility": 3},
{"path": "creationCmd", "type": "text", "visibility": 3}, {"path": "creationCmd", "type": "text", "readonly": false, "cmd": "run mf", "visibility": 3},
{"path": "targetValue", "type": "float"}, {"path": "targetValue", "type": "float", "readonly": false, "cmd": "run mf"},
{"path": "status", "type": "text", "readonly": false, "cmd": "mf status", "visibility": 3}]}, {"path": "status", "type": "text", "readonly": false, "cmd": "mf status", "visibility": 3}]},
"lev": {"base": "/lev", "params": [ "lev": {"base": "/lev", "params": [
@ -350,4 +360,9 @@
{"path": "send", "type": "text", "readonly": false, "cmd": "lev send", "visibility": 3}, {"path": "send", "type": "text", "readonly": false, "cmd": "lev send", "visibility": 3},
{"path": "status", "type": "text", "visibility": 3}, {"path": "status", "type": "text", "visibility": 3},
{"path": "mode", "type": "enum", "enum": {"slow": 0, "fast (switches to slow automatically after filling)": 1}, "readonly": false, "cmd": "lev mode"}, {"path": "mode", "type": "enum", "enum": {"slow": 0, "fast (switches to slow automatically after filling)": 1}, "readonly": false, "cmd": "lev mode"},
{"path": "n2", "type": "float"}]}} {"path": "n2", "type": "float"}]},
"prep0": {"base": "/prep0", "params": [
{"path": "", "type": "text", "readonly": false, "cmd": "prep0", "kids": 2},
{"path": "send", "type": "text", "readonly": false, "cmd": "prep0 send", "visibility": 3},
{"path": "status", "type": "text", "visibility": 3}]}}

View File

@ -42,10 +42,15 @@ digits = 2
scale_factor = 0.0156 scale_factor = 0.0156
offset = 15 offset = 15
[res_io]
description = io to lakeshore
class = secop_psi.ls340res.LscIO
uri = tcp://192.168.127.254:3003
[res] [res]
description = temperature on uniax stick description = temperature on uniax stick
class = secop_psi.ls340res.ResChannel class = secop_psi.ls340res.ResChannel
uri = tcp://192.168.127.254:3003 io = res_io
channel = A channel = A
[T] [T]

74
debian/changelog vendored
View File

@ -1,3 +1,77 @@
secop-core (0.13.1) focal; urgency=medium
[ Markus Zolliker ]
* an enum with value 0 should be interpreted as False
* make startup faster in case of errors
[ Enrico Faulhaber ]
* secop_mlz: minor rework entangle client
-- Markus Zolliker <jenkins@jenkins02.admin.frm2.tum.de> Tue, 02 Aug 2022 15:31:52 +0200
secop-core (0.13.0) focal; urgency=medium
[ Georg Brandl ]
* debian: fix email addresses in changelog
[ Markus Zolliker ]
* various small changes
* automatic saving of persistent parameters
* add more tests and fixes for command inheritance
* entangle.AnalogOutput: fix window mechanism
* remote logging (issue 46)
* add timeouts to MultiEvents
* introduce general config file
* improve handling of module init methods
* check for bad read_* and write_* methods
* change name of read_hw_status method in sequencer mixin
* fix doc (stringio - > io)
* enhance logging
* UniqueObject
* ReadHandler and WriteHandler decorators
* do not convert string to float
* check for problematic value range
* unify name and module on Attached property
* ppms: replace IOHandler by Read/WriteHandler
* fix handling commands
* common read/write handlers
* implement a state machine
* proper return value in handler read_* methods
* new poll mechanism
* support for fast poll when busy
* various small fixes
* reset connection on identification
* improve softcal
* move markdown to requirements-dev.txt
* improve k2601b driver
* fix and improved Attached
* fix error in write wrapper and more
* support write_ method on readonly param and more
* init generalConfig.defaults only in secop-server
* HasConvergence mixin
* avoid race conditions in read_*/write_* methods
* reintroduced individual init of generalConfig.defaults
* fix statemachine
* use a common poller thread for modules sharing io
* motor valve using trinamic motor
* improved trinamic driver
* fix error in secop.logging
* avoid deadlock in proxy
* improve poller error handling
* support for OI mercury series
* add 'ts' to the ppms simulation
* allow a configfile path as single argument to secop-server
* fix keithley 2601b after tests
* channel switcher for Lakeshore 370 with scanner
* feature implementation
* allow to convert numpy arrays to ArrayOf
* remove IOHandler stuff
[ Enrico Faulhaber ]
* default unit to UTF8
-- Georg Brandl <jenkins@frm2.tum.de> Tue, 02 Aug 2022 09:47:06 +0200
secop-core (0.12.4) focal; urgency=medium secop-core (0.12.4) focal; urgency=medium
* fix command inheritance * fix command inheritance

View File

@ -356,7 +356,7 @@ class SecopClient(ProxyClient):
except ConnectionClosed: except ConnectionClosed:
pass pass
except Exception as e: except Exception as e:
self.log.error('rxthread ended with %s' % e) self.log.error('rxthread ended with %r', e)
self._rxthread = None self._rxthread = None
self.disconnect(False) self.disconnect(False)
if self._shutdown: if self._shutdown:
@ -490,7 +490,7 @@ class SecopClient(ProxyClient):
def _unhandled_message(self, action, ident, data): def _unhandled_message(self, action, ident, data):
if not self.callback(None, 'unhandledMessage', action, ident, data): if not self.callback(None, 'unhandledMessage', action, ident, data):
self.log.warning('unhandled message: %s %s %r' % (action, ident, data)) self.log.warning('unhandled message: %s %s %r', action, ident, data)
def _set_state(self, online, state=None): def _set_state(self, online, state=None):
# remark: reconnecting is treated as online # remark: reconnecting is treated as online

View File

@ -23,10 +23,11 @@
import sys import sys
import time import time
import json import re
from queue import Queue from queue import Queue
from secop.client import SecopClient from secop.client import SecopClient
from secop.errors import SECoPError from secop.errors import SECoPError
from secop.datatypes import get_datatype
USAGE = """ USAGE = """
Usage: Usage:
@ -58,10 +59,15 @@ class Logger:
if lev == loglevel: if lev == loglevel:
func = self.emit func = self.emit
setattr(self, lev, func) setattr(self, lev, func)
self._minute = 0
@staticmethod def emit(self, fmt, *args, **kwds):
def emit(fmt, *args, **kwds): now = time.time()
print(str(fmt) % args) minute = now // 60
if minute != self._minute:
self._minute = minute
print(time.strftime('--- %H:%M:%S ---', time.localtime(now)))
print('%6.3f' % (now % 60.0), str(fmt) % args)
@staticmethod @staticmethod
def noop(fmt, *args, **kwds): def noop(fmt, *args, **kwds):
@ -77,6 +83,8 @@ class PrettyFloat(float):
class Module: class Module:
_log_pattern = re.compile('.*')
def __init__(self, name, secnode): def __init__(self, name, secnode):
self._name = name self._name = name
self._secnode = secnode self._secnode = secnode
@ -89,15 +97,12 @@ class Module:
def _one_line(self, pname, minwid=0): def _one_line(self, pname, minwid=0):
"""return <module>.<param> = <value> truncated to one line""" """return <module>.<param> = <value> truncated to one line"""
param = getattr(type(self), pname)
try: try:
value = getattr(self, pname) value = getattr(self, pname)
# make floats appear with 7 digits only r = param.format(value)
r = repr(json.loads(json.dumps(value), parse_float=PrettyFloat))
except Exception as e: except Exception as e:
r = repr(e) r = repr(e)
unit = getattr(type(self), pname).unit
if unit:
r += ' %s' % unit
pname = pname.ljust(minwid) pname = pname.ljust(minwid)
vallen = 113 - len(self._name) - len(pname) vallen = 113 - len(self._name) - len(pname)
if len(r) > vallen: if len(r) > vallen:
@ -174,13 +179,21 @@ class Module:
'\n'.join(self._one_line(k, wid) for k in self._parameters), '\n'.join(self._one_line(k, wid) for k in self._parameters),
', '.join(k + '()' for k in self._commands)) ', '.join(k + '()' for k in self._commands))
def logging(self, level='comlog', pattern='.*'):
self._log_pattern = re.compile(pattern)
self._secnode.request('logging', self._name, level)
def handle_log_message_(self, data):
if self._log_pattern.match(data):
self._secnode.log.info('%s: %r', self._name, data)
class Param: class Param:
def __init__(self, name, unit=None): def __init__(self, name, datainfo):
self.name = name self.name = name
self.prev = None self.prev = None
self.prev_time = 0 self.prev_time = 0
self.unit = unit self.datatype = get_datatype(datainfo)
def __get__(self, obj, owner): def __get__(self, obj, owner):
if obj is None: if obj is None:
@ -198,6 +211,9 @@ class Param:
except SECoPError as e: except SECoPError as e:
obj._secnode.log.error(repr(e)) obj._secnode.log.error(repr(e))
def format(self, value):
return self.datatype.format_value(value)
class Command: class Command:
def __init__(self, name, modname, secnode): def __init__(self, name, modname, secnode):
@ -250,14 +266,24 @@ class Client(SecopClient):
self.log.info('overwrite module %s', modname) self.log.info('overwrite module %s', modname)
attrs = {} attrs = {}
for pname, pinfo in moddesc['parameters'].items(): for pname, pinfo in moddesc['parameters'].items():
unit = pinfo['datainfo'].get('unit') attrs[pname] = Param(pname, pinfo['datainfo'])
attrs[pname] = Param(pname, unit)
for cname in moddesc['commands']: for cname in moddesc['commands']:
attrs[cname] = Command(cname, modname, self) attrs[cname] = Command(cname, modname, self)
mobj = type('M_%s' % modname, (Module,), attrs)(modname, self) mobj = type('M_%s' % modname, (Module,), attrs)(modname, self)
if 'status' in mobj._parameters: if 'status' in mobj._parameters:
self.register_callback((modname, 'status'), updateEvent=mobj._status_value_update) self.register_callback((modname, 'status'), updateEvent=mobj._status_value_update)
self.register_callback((modname, 'value'), updateEvent=mobj._status_value_update) self.register_callback((modname, 'value'), updateEvent=mobj._status_value_update)
setattr(main, modname, mobj) setattr(main, modname, mobj)
self.register_callback(None, self.unhandledMessage)
self.log.info('%s', USAGE) self.log.info('%s', USAGE)
def unhandledMessage(self, action, ident, data):
"""handle logging messages"""
if action == 'log':
modname = ident.split(':')[0]
modobj = getattr(main, modname, None)
if modobj:
modobj.handle_log_message_(data)
return
self.log.info('module %s not found', modname)
self.log.info('unhandled: %s %s %r', action, ident, data)

View File

@ -124,6 +124,9 @@ class DataType(HasProperties):
""" """
raise NotImplementedError raise NotImplementedError
def set_main_unit(self, unit):
"""replace $ in unit by argument"""
class Stub(DataType): class Stub(DataType):
"""incomplete datatype, to be replaced with a proper one later during module load """incomplete datatype, to be replaced with a proper one later during module load
@ -155,9 +158,17 @@ class Stub(DataType):
prop.datatype = globals()[stub.name](*stub.args, **stub.kwds) prop.datatype = globals()[stub.name](*stub.args, **stub.kwds)
class HasUnit:
unit = Property('physical unit', Stub('StringType', isUTF8=True), extname='unit', default='')
def set_main_unit(self, unit):
if '$' in self.unit:
self.setProperty('unit', self.unit.replace('$', unit))
# SECoP types: # SECoP types:
class FloatRange(DataType): class FloatRange(HasUnit, DataType):
"""(restricted) float type """(restricted) float type
:param minval: (property **min**) :param minval: (property **min**)
@ -166,7 +177,6 @@ class FloatRange(DataType):
""" """
min = Property('low limit', Stub('FloatRange'), extname='min', default=-sys.float_info.max) min = Property('low limit', Stub('FloatRange'), extname='min', default=-sys.float_info.max)
max = Property('high limit', Stub('FloatRange'), extname='max', default=sys.float_info.max) max = Property('high limit', Stub('FloatRange'), extname='max', default=sys.float_info.max)
unit = Property('physical unit', Stub('StringType', isUTF8=True), extname='unit', default='')
fmtstr = Property('format string', Stub('StringType'), extname='fmtstr', default='%g') fmtstr = Property('format string', Stub('StringType'), extname='fmtstr', default='%g')
absolute_resolution = Property('absolute resolution', Stub('FloatRange', 0), absolute_resolution = Property('absolute resolution', Stub('FloatRange', 0),
extname='absolute_resolution', default=0.0) extname='absolute_resolution', default=0.0)
@ -331,7 +341,7 @@ class IntRange(DataType):
raise BadValueError('incompatible datatypes') raise BadValueError('incompatible datatypes')
class ScaledInteger(DataType): class ScaledInteger(HasUnit, DataType):
"""scaled integer (= fixed resolution float) type """scaled integer (= fixed resolution float) type
:param minval: (property **min**) :param minval: (property **min**)
@ -344,7 +354,6 @@ class ScaledInteger(DataType):
scale = Property('scale factor', FloatRange(sys.float_info.min), extname='scale', mandatory=True) scale = Property('scale factor', FloatRange(sys.float_info.min), extname='scale', mandatory=True)
min = Property('low limit', FloatRange(), extname='min', mandatory=True) min = Property('low limit', FloatRange(), extname='min', mandatory=True)
max = Property('high limit', FloatRange(), extname='max', mandatory=True) max = Property('high limit', FloatRange(), extname='max', mandatory=True)
unit = Property('physical unit', Stub('StringType', isUTF8=True), extname='unit', default='')
fmtstr = Property('format string', Stub('StringType'), extname='fmtstr', default='%g') fmtstr = Property('format string', Stub('StringType'), extname='fmtstr', default='%g')
absolute_resolution = Property('absolute resolution', FloatRange(0), absolute_resolution = Property('absolute resolution', FloatRange(0),
extname='absolute_resolution', default=0.0) extname='absolute_resolution', default=0.0)
@ -806,6 +815,9 @@ class ArrayOf(DataType):
except AttributeError: except AttributeError:
raise BadValueError('incompatible datatypes') from None raise BadValueError('incompatible datatypes') from None
def set_main_unit(self, unit):
self.members.set_main_unit(unit)
class TupleOf(DataType): class TupleOf(DataType):
"""data structure with fields of inhomogeneous type """data structure with fields of inhomogeneous type
@ -872,6 +884,10 @@ class TupleOf(DataType):
for a, b in zip(self.members, other.members): for a, b in zip(self.members, other.members):
a.compatible(b) a.compatible(b)
def set_main_unit(self, unit):
for member in self.members:
member.set_main_unit(unit)
class ImmutableDict(dict): class ImmutableDict(dict):
def _no(self, *args, **kwds): def _no(self, *args, **kwds):
@ -961,6 +977,10 @@ class StructOf(DataType):
except (AttributeError, TypeError, KeyError): except (AttributeError, TypeError, KeyError):
raise BadValueError('incompatible datatypes') from None raise BadValueError('incompatible datatypes') from None
def set_main_unit(self, unit):
for member in self.members.values():
member.set_main_unit(unit)
class CommandType(DataType): class CommandType(DataType):
"""command """command

View File

@ -71,14 +71,6 @@ class HasIO(Module):
elif not io: elif not io:
raise ConfigError("Module %s needs a value for either 'uri' or 'io'" % name) raise ConfigError("Module %s needs a value for either 'uri' or 'io'" % name)
def initModule(self):
try:
self.io.read_is_connected()
except (CommunicationFailedError, AttributeError):
# AttributeError: read_is_connected is not required for an io object
pass
super().initModule()
def communicate(self, *args): def communicate(self, *args):
return self.io.communicate(*args) return self.io.communicate(*args)
@ -118,6 +110,7 @@ class IOBase(Communicator):
_conn = None _conn = None
_last_error = None _last_error = None
_lock = None _lock = None
_last_connect_attempt = 0
def earlyInit(self): def earlyInit(self):
super().earlyInit() super().earlyInit()
@ -169,6 +162,17 @@ class IOBase(Communicator):
return False return False
return self.read_is_connected() return self.read_is_connected()
def check_connection(self):
"""called before communicate"""
if not self.is_connected:
now = time.time()
if now >= self._last_connect_attempt + self.pollinterval:
# we do not try to reconnect more often than pollinterval
_last_connect_attempt = now
if self.read_is_connected():
return
raise SilentError('disconnected') from None
def registerReconnectCallback(self, name, func): def registerReconnectCallback(self, name, func):
"""register reconnect callback """register reconnect callback
@ -250,11 +254,7 @@ class StringIO(IOBase):
wait_before is respected for end_of_lines within a command. wait_before is respected for end_of_lines within a command.
""" """
command = command.encode(self.encoding) command = command.encode(self.encoding)
if not self.is_connected: self.check_connection()
# do not try to reconnect here
# read_is_connected is doing this when called by its poller
self.read_is_connected() # try to reconnect
raise SilentError('disconnected') from None
try: try:
with self._lock: with self._lock:
# read garbage and wait before send # read garbage and wait before send
@ -359,11 +359,7 @@ class BytesIO(IOBase):
@Command((BLOBType(), IntRange(0)), result=BLOBType()) @Command((BLOBType(), IntRange(0)), result=BLOBType())
def communicate(self, request, replylen): # pylint: disable=arguments-differ def communicate(self, request, replylen): # pylint: disable=arguments-differ
"""send a request and receive (at least) <replylen> bytes as reply""" """send a request and receive (at least) <replylen> bytes as reply"""
if not self.is_connected: self.check_connection()
# do not try to reconnect here
# read_is_connected is doing this when called by its poller
self.read_is_connected() # try to reconnect
raise SilentError('disconnected') from None
try: try:
with self._lock: with self._lock:
# read garbage and wait before send # read garbage and wait before send

View File

@ -132,7 +132,8 @@ class StateMachine:
:return: None (for custom cleanup functions this might be a new state) :return: None (for custom cleanup functions this might be a new state)
""" """
if state.stopped: # stop or restart if state.stopped: # stop or restart
state.log.debug('%sed in state %r', repr(state.stopped).lower(), state.status_string) verb = 'stopped' if state.stopped is Stop else 'restarted'
state.log.debug('%s in state %r', verb, state.status_string)
else: else:
state.log.warning('%r raised in state %r', state.last_error, state.status_string) state.log.warning('%r raised in state %r', state.last_error, state.status_string)
@ -196,7 +197,7 @@ class StateMachine:
self.log.debug('called %r %sexc=%r', self.cleanup, self.log.debug('called %r %sexc=%r', self.cleanup,
'ret=%r ' % ret if ret else '', e) 'ret=%r ' % ret if ret else '', e)
if ret is None: if ret is None:
self.log.debug('state: None') self.log.debug('state: None after cleanup')
self.state = None self.state = None
self._idle_event.set() self._idle_event.set()
return None return None

View File

@ -30,7 +30,7 @@ from functools import wraps
from secop.datatypes import ArrayOf, BoolType, EnumType, FloatRange, \ from secop.datatypes import ArrayOf, BoolType, EnumType, FloatRange, \
IntRange, StatusType, StringType, TextType, TupleOf, DiscouragedConversion IntRange, StatusType, StringType, TextType, TupleOf, DiscouragedConversion
from secop.errors import BadValueError, ConfigError, \ from secop.errors import BadValueError, CommunicationFailedError, ConfigError, \
ProgrammingError, SECoPError, secop_error ProgrammingError, SECoPError, secop_error
from secop.lib import formatException, mkthread, UniqueObject, generalConfig from secop.lib import formatException, mkthread, UniqueObject, generalConfig
from secop.lib.enum import Enum from secop.lib.enum import Enum
@ -476,10 +476,12 @@ class Module(HasAccessibles):
aobj.finish() aobj.finish()
# Modify units AFTER applying the cfgdict # Modify units AFTER applying the cfgdict
mainvalue = self.parameters.get('value')
if mainvalue:
mainunit = mainvalue.datatype.unit
if mainunit:
for pname, pobj in self.parameters.items(): for pname, pobj in self.parameters.items():
dt = pobj.datatype pobj.datatype.set_main_unit(mainunit)
if '$' in dt.unit:
dt.setProperty('unit', dt.unit.replace('$', self.parameters['value'].datatype.unit))
# 6) check complete configuration of * properties # 6) check complete configuration of * properties
if not errors: if not errors:
@ -639,7 +641,7 @@ class Module(HasAccessibles):
self.pollInfo.interval = fast_interval if flag else self.pollinterval self.pollInfo.interval = fast_interval if flag else self.pollinterval
self.pollInfo.trigger() self.pollInfo.trigger()
def callPollFunc(self, rfunc): def callPollFunc(self, rfunc, raise_com_failed=False):
"""call read method with proper error handling""" """call read method with proper error handling"""
try: try:
rfunc() rfunc()
@ -656,6 +658,8 @@ class Module(HasAccessibles):
else: else:
# uncatched error: this is more serious # uncatched error: this is more serious
self.log.error('%s: %s', name, formatException()) self.log.error('%s: %s', name, formatException())
if raise_com_failed and isinstance(e, CommunicationFailedError):
raise
def __pollThread(self, modules, started_callback): def __pollThread(self, modules, started_callback):
"""poll thread body """poll thread body
@ -680,7 +684,7 @@ class Module(HasAccessibles):
trg.set() trg.set()
self.registerReconnectCallback('trigger_polls', trigger_all) self.registerReconnectCallback('trigger_polls', trigger_all)
# collect and call all read functions a first time # collect all read functions
for mobj in modules: for mobj in modules:
pinfo = mobj.pollInfo = PollInfo(mobj.pollinterval, self.triggerPoll) pinfo = mobj.pollInfo = PollInfo(mobj.pollinterval, self.triggerPoll)
# trigger a poll interval change when self.pollinterval changes. # trigger a poll interval change when self.pollinterval changes.
@ -691,7 +695,16 @@ class Module(HasAccessibles):
rfunc = getattr(mobj, 'read_' + pname) rfunc = getattr(mobj, 'read_' + pname)
if rfunc.poll: if rfunc.poll:
pinfo.polled_parameters.append((mobj, rfunc, pobj)) pinfo.polled_parameters.append((mobj, rfunc, pobj))
mobj.callPollFunc(rfunc) # call all read functions a first time
try:
for m in modules:
for mobj, rfunc, _ in m.pollInfo.polled_parameters:
mobj.callPollFunc(rfunc, raise_com_failed=True)
except CommunicationFailedError as e:
# when communication failed, probably all parameters and may be more modules are affected.
# as this would take a lot of time (summed up timeouts), we do not continue
# trying and let the server accept connections, further polls might success later
self.log.error('communication failure on startup: %s', e)
started_callback() started_callback()
to_poll = () to_poll = ()
while True: while True:

View File

@ -129,7 +129,7 @@ class PersistentMixin(HasAccessibles):
if getattr(v, 'persistent', False)} if getattr(v, 'persistent', False)}
if data != self.persistentData: if data != self.persistentData:
self.persistentData = data self.persistentData = data
persistentdir = os.path.basename(self.persistentFile) persistentdir = os.path.dirname(self.persistentFile)
tmpfile = self.persistentFile + '.tmp' tmpfile = self.persistentFile + '.tmp'
if not os.path.isdir(persistentdir): if not os.path.isdir(persistentdir):
os.makedirs(persistentdir, exist_ok=True) os.makedirs(persistentdir, exist_ok=True)

View File

@ -28,6 +28,9 @@ Here we support devices which fulfill the official
MLZ TANGO interface for the respective device classes. MLZ TANGO interface for the respective device classes.
""" """
# pylint: disable=too-many-lines
import re import re
import threading import threading
from time import sleep from time import sleep
@ -173,7 +176,7 @@ class PyTangoDevice(Module):
tango_status_mapping = { tango_status_mapping = {
PyTango.DevState.ON: Drivable.Status.IDLE, PyTango.DevState.ON: Drivable.Status.IDLE,
PyTango.DevState.ALARM: Drivable.Status.WARN, PyTango.DevState.ALARM: Drivable.Status.WARN,
PyTango.DevState.OFF: Drivable.Status.ERROR, PyTango.DevState.OFF: Drivable.Status.DISABLED,
PyTango.DevState.FAULT: Drivable.Status.ERROR, PyTango.DevState.FAULT: Drivable.Status.ERROR,
PyTango.DevState.MOVING: Drivable.Status.BUSY, PyTango.DevState.MOVING: Drivable.Status.BUSY,
} }
@ -504,6 +507,9 @@ class AnalogOutput(PyTangoDevice, Drivable):
return stable and at_target return stable and at_target
def read_status(self): def read_status(self):
_st, _sts = super().read_status()
if _st == Readable.Status.DISABLED:
return _st, _sts
if self._isAtTarget(): if self._isAtTarget():
self._timeout = None self._timeout = None
self._moving = False self._moving = False

View File

@ -20,11 +20,13 @@
# ***************************************************************************** # *****************************************************************************
"""oxford instruments mercury IPS power supply""" """oxford instruments mercury IPS power supply"""
import time
from secop.core import Parameter, EnumType, FloatRange, BoolType from secop.core import Parameter, EnumType, FloatRange, BoolType
from secop.lib.enum import Enum from secop.lib.enum import Enum
from secop.errors import BadValueError from secop.errors import BadValueError, HardwareError
from secop_psi.magfield import Magfield from secop_psi.magfield import Magfield
from secop_psi.mercury import MercuryChannel, off_on, Mapped from secop_psi.mercury import MercuryChannel, off_on, Mapped
from secop.lib.statemachine import Retry
Action = Enum(hold=0, run_to_set=1, run_to_zero=2, clamped=3) Action = Enum(hold=0, run_to_set=1, run_to_zero=2, clamped=3)
hold_rtoz_rtos_clmp = Mapped(HOLD=Action.hold, RTOS=Action.run_to_set, hold_rtoz_rtos_clmp = Mapped(HOLD=Action.hold, RTOS=Action.run_to_set,
@ -37,6 +39,12 @@ class Field(MercuryChannel, Magfield):
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)
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( forced_persistent_field = Parameter(
'manual indication that persistent field is bad', BoolType(), readonly=False, default=False) 'manual indication that persistent field is bad', BoolType(), readonly=False, default=False)
@ -46,13 +54,17 @@ class Field(MercuryChannel, Magfield):
slave_currents = None slave_currents = None
__init = True __init = True
def doPoll(self):
super().doPoll()
self.read_current()
def read_value(self): def read_value(self):
self.current = self.query('PSU:SIG:FLD') self.current = self.query('PSU:SIG:FLD')
pf = self.query('PSU:SIG:PFLD') pf = self.query('PSU:SIG:PFLD')
if self.__init: if self.__init:
self.__init = False self.__init = False
self.persistent_field = pf self.persistent_field = pf
if self.switch_heater != 0 or self._field_mismatch is None: if self.switch_heater == self.switch_heater.on or self._field_mismatch is None:
self.forced_persistent_field = False self.forced_persistent_field = False
self._field_mismatch = False self._field_mismatch = False
return self.current return self.current
@ -84,7 +96,13 @@ class Field(MercuryChannel, Magfield):
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): def read_switch_heater(self):
return self.query('PSU:SIG:SWHT', off_on) 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): def write_switch_heater(self, value):
return self.change('PSU:SIG:SWHT', value, off_on) return self.change('PSU:SIG:SWHT', value, off_on)
@ -104,7 +122,11 @@ class Field(MercuryChannel, Magfield):
current = self.query('PSU:SIG:CURR') current = self.query('PSU:SIG:CURR')
for i in range(self.nslaves + 1): for i in range(self.nslaves + 1):
if i: if i:
self.slave_currents[i].append(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)
setattr(self, 'I%d' % i, curri)
setattr(self, 'V%d' % i, volti)
self.slave_currents[i].append(curri)
else: else:
self.slave_currents[i].append(current) self.slave_currents[i].append(current)
min_i = min(self.slave_currents[i]) min_i = min(self.slave_currents[i])
@ -128,12 +150,18 @@ class Field(MercuryChannel, Magfield):
try: try:
self.set_and_go(self.persistent_field) self.set_and_go(self.persistent_field)
except (HardwareError, AssertionError): except (HardwareError, AssertionError):
state.switch_undef = self.switch_on_time or state.now state.switch_undef = self.switch_time[self.switch_heater.on] or state.now
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):
if self.action != 'run_to_set':
self.status = Status.PREPARING, 'restart ramp to field'
return self.start_ramp_to_field
return super().ramp_to_field(state)
def wait_for_switch(self, state): def wait_for_switch(self, state):
if self.now - self.switch_undef < self.wait_switch_on: if state.now - state.switch_undef < self.wait_switch_on:
return Retry() return Retry()
self.set_and_go(self.persistent_field) self.set_and_go(self.persistent_field)
return self.ramp_to_field return self.ramp_to_field

View File

@ -44,6 +44,9 @@ Status = Enum(
FINALIZING=390, FINALIZING=390,
) )
OFF = 0
ON = 1
class Magfield(HasLimits, Drivable): class Magfield(HasLimits, Drivable):
value = Parameter('magnetic field', datatype=FloatRange(unit='T')) value = Parameter('magnetic field', datatype=FloatRange(unit='T'))
@ -52,7 +55,7 @@ class Magfield(HasLimits, Drivable):
'persistent mode', EnumType(Mode), readonly=False, default=Mode.PERSISTENT) 'persistent mode', EnumType(Mode), readonly=False, default=Mode.PERSISTENT)
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=0, on=1), switch_heater = Parameter('switch heater', EnumType(off=OFF, on=ON),
readonly=False, default=0) readonly=False, default=0)
persistent_field = Parameter( persistent_field = Parameter(
'persistent field', FloatRange(unit='$'), readonly=False) 'persistent field', FloatRange(unit='$'), readonly=False)
@ -73,33 +76,22 @@ class Magfield(HasLimits, Drivable):
# ArrayOf(TupleOf(FloatRange(unit='$'), FloatRange(unit='$/min'))), readonly=False) # ArrayOf(TupleOf(FloatRange(unit='$'), FloatRange(unit='$/min'))), readonly=False)
# TODO: the following parameters should be changed into properties after tests # TODO: the following parameters should be changed into properties after tests
wait_switch_on = Parameter( wait_switch_on = Parameter(
'wait time to ensure switch is on', FloatRange(0, unit='s'), readonly=False, default=61) 'wait time to ensure switch is on', FloatRange(0, unit='s'), readonly=False, default=60)
wait_switch_off = Parameter( wait_switch_off = Parameter(
'wait time to ensure switch is off', FloatRange(0, unit='s'), readonly=False, default=61) 'wait time to ensure switch is off', FloatRange(0, unit='s'), readonly=False, default=60)
wait_stable_leads = Parameter( wait_stable_leads = Parameter(
'wait time to ensure current is stable', FloatRange(0, unit='s'), readonly=False, default=6) '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=31) 'wait time to ensure field is stable', FloatRange(0, unit='s'), readonly=False, default=30)
persistent_limit = Parameter( persistent_limit = Parameter(
'above this limit, lead currents are not driven to 0', 'above this limit, lead currents are not driven to 0',
FloatRange(0, unit='$'), readonly=False, default=99) FloatRange(0, unit='$'), readonly=False, default=99)
_state = None _state = None
__init = True
_last_target = None _last_target = None
switch_on_time = None switch_time = None, None
switch_off_time = None
def doPoll(self): 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:
self.read_value() self.read_value()
self._state.cycle() self._state.cycle()
@ -117,6 +109,19 @@ class Magfield(HasLimits, Drivable):
self.registerCallbacks(self) # for update_switch_heater self.registerCallbacks(self) # for update_switch_heater
self._state = StateMachine(logger=self.log, threaded=False, cleanup=self.cleanup_state) 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): def write_target(self, target):
self.check_limits(target) self.check_limits(target)
self.target = target self.target = target
@ -185,14 +190,11 @@ class Magfield(HasLimits, Drivable):
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"""
if value != 0: switch_time = self.switch_time[value]
self.switch_off_time = None if switch_time is None:
if self.switch_on_time is None: switch_time = time.time()
self.switch_on_time = time.time() self.switch_time = [None, None]
else: self.switch_time[value] = switch_time
self.switch_on_time = None
if self.switch_off_time is None:
self.switch_off_time = time.time()
def start_switch_on(self, state): def start_switch_on(self, state):
"""switch heater on""" """switch heater on"""
@ -213,12 +215,10 @@ class Magfield(HasLimits, Drivable):
abs(self.target - self.persistent_field) <= self.tolerance): # short cut abs(self.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()
if self.switch_on_time is None: if self.switch_time[ON] is None:
if state.now - self.switch_off_time > 10:
self.log.warning('switch turned off manually?') self.log.warning('switch turned off manually?')
return self.start_switch_on return self.start_switch_on
return Retry() if state.now - self.switch_time[ON] < self.wait_switch_on:
if state.now - self.switch_on_time < self.wait_switch_on:
return Retry() return Retry()
self._last_target = self.target self._last_target = self.target
return self.start_ramp_to_target return self.start_ramp_to_target
@ -279,12 +279,10 @@ class Magfield(HasLimits, Drivable):
return self.start_switch_on 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_off_time is None: if self.switch_time[OFF] is None:
if state.now - self.switch_on_time > 10:
self.log.warning('switch turned on manually?') self.log.warning('switch turned on manually?')
return self.start_switch_off return self.start_switch_off
return Retry() if state.now - self.switch_time[OFF] < self.wait_switch_off:
if state.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' self.status = Status.IDLE, 'leads current at field, switch off'

View File

@ -345,7 +345,7 @@ class HeaterOutput(HasInput, MercuryChannel, Writable):
class TemperatureLoop(TemperatureSensor, Loop, Drivable): class TemperatureLoop(TemperatureSensor, Loop, Drivable):
channel_type = 'TEMP' channel_type = 'TEMP'
output_module = Attached(HasInput, mandatory=False) output_module = Attached(HasInput, mandatory=False)
ramp = Parameter('ramp rate', FloatRange(0, unit='K/min'), readonly=False) ramp = Parameter('ramp rate', FloatRange(0, unit='$/min'), readonly=False)
enable_ramp = Parameter('enable ramp rate', BoolType(), readonly=False) enable_ramp = Parameter('enable ramp rate', BoolType(), readonly=False)
setpoint = Parameter('working setpoint (differs from target when ramping)', FloatRange(0, unit='$')) setpoint = Parameter('working setpoint (differs from target when ramping)', FloatRange(0, unit='$'))
tolerance = Parameter(default=0.1) tolerance = Parameter(default=0.1)

View File

@ -35,7 +35,9 @@ class PhytronIO(StringIO):
identification = [('0IVR', 'MCC Minilog .*')] identification = [('0IVR', 'MCC Minilog .*')]
def communicate(self, command): def communicate(self, command):
for ntry in range(5, 0, -1): ntry = 5
warn = None
for itry in range(ntry):
try: try:
_, _, reply = super().communicate('\x02' + command).partition('\x02') _, _, reply = super().communicate('\x02' + command).partition('\x02')
if reply[0] == '\x06': # ACK if reply[0] == '\x06': # ACK
@ -43,9 +45,12 @@ class PhytronIO(StringIO):
raise CommunicationFailedError('missing ACK %r (cmd: %r)' raise CommunicationFailedError('missing ACK %r (cmd: %r)'
% (reply, command)) % (reply, command))
except Exception as e: except Exception as e:
if ntry == 1: if itry < ntry - 1:
warn = e
else:
raise raise
self.log.warning('%s - retry', e) if warn:
self.log.warning('needed %d retries after %r', itry, warn)
return reply[1:] return reply[1:]

View File

@ -27,7 +27,8 @@ from os.path import basename, dirname, exists, join
import numpy as np import numpy as np
from scipy.interpolate import splev, splrep # pylint: disable=import-error from scipy.interpolate import splev, splrep # pylint: disable=import-error
from secop.core import Attached, BoolType, Parameter, Readable, StringType, FloatRange from secop.core import Attached, BoolType, Parameter, Readable, StringType, \
FloatRange, Done
def linear(x): def linear(x):
@ -182,7 +183,6 @@ class Sensor(Readable):
description = 'a calibrated sensor value' description = 'a calibrated sensor value'
_value_error = None _value_error = None
enablePoll = False
def checkProperties(self): def checkProperties(self):
if 'description' not in self.propertyValues: if 'description' not in self.propertyValues:
@ -196,6 +196,9 @@ class Sensor(Readable):
if self.description == '_': if self.description == '_':
self.description = '%r calibrated with curve %r' % (self.rawsensor, self.calib) self.description = '%r calibrated with curve %r' % (self.rawsensor, self.calib)
def doPoll(self):
self.read_status()
def write_calib(self, value): def write_calib(self, value):
self._calib = CalCurve(value) self._calib = CalCurve(value)
return value return value
@ -221,3 +224,8 @@ class Sensor(Readable):
def read_value(self): def read_value(self):
return self._calib(self.rawsensor.read_value()) return self._calib(self.rawsensor.read_value())
def read_status(self):
self.update_status(self.rawsensor.status)
return Done

View File

@ -25,7 +25,12 @@ import time
import math import math
from secop.core import Drivable, Parameter, FloatRange, Done, \ from secop.core import Drivable, Parameter, FloatRange, Done, \
Attached, Command, PersistentMixin, PersistentParam, BoolType Attached, Command, PersistentMixin, PersistentParam, BoolType
from secop.errors import BadValueError from secop.errors import BadValueError, SECoPError
from secop.lib.statemachine import Retry, StateMachine, Restart
class Error(SECoPError):
pass
class Uniax(PersistentMixin, Drivable): class Uniax(PersistentMixin, Drivable):
@ -33,11 +38,11 @@ class Uniax(PersistentMixin, Drivable):
motor = Attached() motor = Attached()
transducer = Attached() transducer = Attached()
limit = Parameter('abs limit of force', FloatRange(0, 190, unit='N'), readonly=False, default=150) limit = Parameter('abs limit of force', FloatRange(0, 190, unit='N'), readonly=False, default=150)
tolerance = Parameter('force tolerance', FloatRange(0, 10, unit='N'), readonly=False, default=0.1) tolerance = Parameter('force tolerance', FloatRange(0, 10, unit='N'), readonly=False, default=0.2)
slope = PersistentParam('spring constant', FloatRange(unit='deg/N'), readonly=False, slope = PersistentParam('spring constant', FloatRange(unit='deg/N'), readonly=False,
default=0.5, persistent='auto') default=0.5, persistent='auto')
pid_i = PersistentParam('integral', FloatRange(), readonly=False, default=0.5, persistent='auto') pid_i = PersistentParam('integral', FloatRange(), readonly=False, default=0.5, persistent='auto')
filter_interval = Parameter('filter time', FloatRange(0, 60, unit='s'), readonly=False, default=1) filter_interval = Parameter('filter time', FloatRange(0, 60, unit='s'), readonly=False, default=5)
current_step = Parameter('', FloatRange(unit='deg'), default=0) current_step = Parameter('', FloatRange(unit='deg'), default=0)
force_offset = PersistentParam('transducer offset', FloatRange(unit='N'), readonly=False, default=0, force_offset = PersistentParam('transducer offset', FloatRange(unit='N'), readonly=False, default=0,
initwrite=True, persistent='auto') initwrite=True, persistent='auto')
@ -52,8 +57,11 @@ class Uniax(PersistentMixin, Drivable):
default=0.2, persistent='auto') default=0.2, persistent='auto')
low_pos = Parameter('max. position for positive forces', FloatRange(unit='deg'), readonly=False, needscfg=False) low_pos = Parameter('max. position for positive forces', FloatRange(unit='deg'), readonly=False, needscfg=False)
high_pos = Parameter('min. position for negative forces', FloatRange(unit='deg'), readonly=False, needscfg=False) high_pos = Parameter('min. position for negative forces', FloatRange(unit='deg'), readonly=False, needscfg=False)
substantial_force = Parameter('min. force change expected within motor play', FloatRange(), default=1)
motor_play = Parameter('acceptable motor play within hysteresis', FloatRange(), readonly=False, default=10)
motor_max_play = Parameter('acceptable motor play outside hysteresis', FloatRange(), readonly=False, default=90)
timeout = Parameter('driving finishes when no progress within this delay', FloatRange(), readonly=False, default=300)
pollinterval = 0.1 pollinterval = 0.1
fast_pollfactor = 1
_mot_target = None # for detecting manual motor manipulations _mot_target = None # for detecting manual motor manipulations
_filter_start = 0 _filter_start = 0
@ -61,19 +69,21 @@ class Uniax(PersistentMixin, Drivable):
_sum = 0 _sum = 0
_cnt_rderr = 0 _cnt_rderr = 0
_cnt_wrerr = 0 _cnt_wrerr = 0
_action = None
_last_force = 0
_expected_step = 1
_fail_cnt = 0
_in_cnt = 0
_init_action = False
_zero_pos_tol = None _zero_pos_tol = None
_find_target = 0 _state = None
_force = None # raw force
def earlyInit(self): def earlyInit(self):
super().earlyInit() super().earlyInit()
self._zero_pos_tol = {} self._zero_pos_tol = {}
self._action = self.idle
def initModule(self):
super().initModule()
self._state = StateMachine(logger=self.log, threaded=False, cleanup=self.cleanup)
def doPoll(self):
self.read_value()
self._state.cycle()
def drive_relative(self, step, ntry=3): def drive_relative(self, step, ntry=3):
"""drive relative, try 3 times""" """drive relative, try 3 times"""
@ -84,7 +94,12 @@ class Uniax(PersistentMixin, Drivable):
self.current_step = step self.current_step = step
for _ in range(ntry): for _ in range(ntry):
try: try:
self._mot_target = self.motor.write_target(mot.value + step) if abs(mot.value - mot.target) < mot.tolerance:
# make sure rounding erros do not suppress small steps
newpos = mot.target + step
else:
newpos = mot.value + step
self._mot_target = self.motor.write_target(newpos)
self._cnt_wrerr = max(0, self._cnt_wrerr - 1) self._cnt_wrerr = max(0, self._cnt_wrerr - 1)
return True return True
except Exception as e: except Exception as e:
@ -96,39 +111,52 @@ class Uniax(PersistentMixin, Drivable):
self.motor.reset() self.motor.reset()
return False return False
def reset_filter(self, now=0.0):
self._sum = self._cnt = 0
self._filter_start = now or time.time()
def motor_busy(self): def motor_busy(self):
mot = self.motor mot = self.motor
if mot.isBusy(): if mot.isBusy():
if mot.target != self._mot_target: if mot.target != self._mot_target:
self.action = self.idle raise Error('control stopped - motor moved directly')
return True return True
return False return False
def next_action(self, action): def read_value(self):
"""call next action try:
self._force = force = self.transducer.read_value()
self._cnt_rderr = max(0, self._cnt_rderr - 1)
except Exception as e:
self._cnt_rderr += 1
if self._cnt_rderr > 10:
self.stop()
self.status = 'ERROR', 'too many read errors: %s' % e
self.log.error(self.status[1])
self.read_target()
return Done
:param action: function to be called next time now = time.time()
:param do_now: do next action in the same cycle self._sum += force
""" self._cnt += 1
self._action = action if now < self._filter_start + self.filter_interval:
self._init_action = True return Done
self.log.info('action %r', action.__name__) force = self._sum / self._cnt
self.reset_filter(now)
if abs(force) > self.limit + self.hysteresis:
self.motor.stop()
self.status = 'ERROR', 'above max limit'
self.log.error(self.status[1])
self.read_target()
return Done
if self.zero_pos(force) is None and abs(force) > self.hysteresis:
self.set_zero_pos(force, self.motor.read_value())
return force
def init_action(self): def reset_filter(self, now=0.0):
"""return true when called the first time after next_action""" self._sum = self._cnt = 0
if self._init_action: self._filter_start = now or time.time()
self._init_action = False
return True
return False
def zero_pos(self, value,): def zero_pos(self, value):
"""get high_pos or low_pos, depending on sign of value """get high_pos or low_pos, depending on sign of value
:param force: when not 0, return an estimate for a good starting position :param value: return an estimate for a good starting position
""" """
name = 'high_pos' if value > 0 else 'low_pos' name = 'high_pos' if value > 0 else 'low_pos'
@ -155,207 +183,233 @@ class Uniax(PersistentMixin, Drivable):
self._zero_pos_tol[name] = tol self._zero_pos_tol[name] = tol
self.log.info('set %s = %.1f +- %.1f (@%g N)' % (name, pos, tol, force)) self.log.info('set %s = %.1f +- %.1f (@%g N)' % (name, pos, tol, force))
setattr(self, name, pos) setattr(self, name, pos)
return pos
def find(self, force, target): def cleanup(self, state):
"""find active (engaged) range""" """in case of error, set error status"""
sign = math.copysign(1, target) if state.stopped: # stop or restart
if force * sign > self.hysteresis or force * sign > target * sign: if state.stopped is Restart:
if self.motor_busy():
self.log.info('motor stopped - substantial force detected: %g', force)
self.motor.stop()
elif self.init_action():
self.next_action(self.adjust)
return
if abs(force) > self.hysteresis:
self.set_zero_pos(force, self.motor.read_value())
self.next_action(self.adjust)
return
if force * sign < -self.hysteresis:
self._previous_force = force
self.next_action(self.free)
return
if self.motor_busy():
if sign * self._find_target < 0: # target sign changed
self.motor.stop()
self.next_action(self.find) # restart find
return return
self.status = 'IDLE', 'stopped'
self.log.warning('stopped')
else: else:
self._find_target = target self.status = 'ERROR', str(state.last_error)
zero_pos = self.zero_pos(target) if isinstance(state.last_error, Error):
side_name = 'positive' if target > 0 else 'negative' self.log.error('%s', state.last_error)
if not self.init_action(): else:
self.log.error('%r raised in state %r', str(state.last_error), state.status_string)
self.read_target() # make target invalid
self.motor.stop()
self.write_adjusting(False)
def reset_progress(self, state):
state.prev_force = self.value
state.prev_pos = self.motor.value
state.prev_time = time.time()
def check_progress(self, state):
force_step = self.target - self.value
direction = math.copysign(1, force_step)
try:
force_progress = direction * (self.value - state.prev_force)
except AttributeError: # prev_force undefined?
self.reset_progress(state)
return True
if force_progress >= self.substantial_force:
self.reset_progress(state)
else:
motor_dif = abs(self.motor.value - state.prev_pos)
if motor_dif > self.motor_play:
if motor_dif > self.motor_max_play:
raise Error('force seems not to change substantially %g %g (%g %g)' % (self.value, self.motor.value, state.prev_force, state.prev_pos))
return False
return True
def adjust(self, state):
"""adjust force"""
if state.init:
state.phase = 0 # just initialized
state.in_since = 0
state.direction = math.copysign(1, self.target - self.value)
state.pid_fact = 1
if self.motor_busy():
return Retry()
self.value = self._force
force_step = self.target - self.value
if abs(force_step) < self.tolerance:
if state.in_since == 0:
state.in_since = state.now
if state.now > state.in_since + 10:
return self.within_tolerance
else:
if force_step * state.direction < 0:
if state.pid_fact == 1:
self.log.info('overshoot -> adjust with reduced pid_i')
state.pid_fact = 0.1
state.in_since = 0
if state.phase == 0:
state.phase = 1
self.reset_progress(state)
self.write_adjusting(True)
self.status = 'BUSY', 'adjusting force'
elif not self.check_progress(state):
if abs(self.value) < self.hysteresis:
if motor_dif > self.motor_play:
self.log.warning('adjusting failed - try to find zero pos')
self.set_zero_pos(self.target, None)
return self.find
elif time.time() > state.prev_time + self.timeout:
if state.phase == 1:
state.phase = 2
self.log.warning('no substantial progress since %d sec', self.timeout)
self.status = 'IDLE', 'adjusting timeout'
self.drive_relative(force_step * self.slope * self.pid_i * min(1, state.delta()) * state.pid_fact)
return Retry()
def within_tolerance(self, state):
"""within tolerance"""
if state.init:
self.status = 'IDLE', 'within tolerance'
return Retry()
if self.motor_busy():
return Retry()
force_step = self.target - self.value
if abs(force_step) < self.tolerance * 0.5:
self.current_step = 0
else:
self.check_progress(state)
self.drive_relative(force_step * self.slope * self.pid_i * min(1, state.delta()) * 0.1)
if abs(force_step) > self.tolerance:
return self.out_of_tolerance
return Retry()
def out_of_tolerance(self, state):
"""out of tolerance"""
if state.init:
self.status = 'WARN', 'out of tolerance'
state.in_since = 0
return Retry()
if self.motor_busy():
return Retry()
force_step = self.target - self._force
if abs(force_step) < self.tolerance:
if state.in_since == 0:
state.in_since = state.now
if state.now > state.in_since + 10:
return self.within_tolerance
if abs(force_step) < self.tolerance * 0.5:
return Retry()
self.check_progress(state)
self.drive_relative(force_step * self.slope * self.pid_i * min(1, state.delta()) * 0.1)
return Retry()
def find(self, state):
"""find active (engaged) range"""
if state.init:
state.prev_direction = 0 # find not yet started
self.reset_progress(state)
direction = math.copysign(1, self.target)
self.value = self._force
abs_force = self.value * direction
if abs_force > self.hysteresis or abs_force > self.target * direction:
if self.motor_busy():
self.log.info('motor stopped - substantial force detected: %g', self.value)
self.motor.stop()
elif state.prev_direction == 0:
return self.adjust
if abs_force > self.hysteresis:
self.set_zero_pos(self.value, self.motor.read_value())
return self.adjust
if abs_force < -self.hysteresis:
state.force_before_free = self.value
return self.free
if self.motor_busy():
if direction == -state.prev_direction: # target direction changed
self.motor.stop()
state.init_find = True # restart find
return Retry()
zero_pos = self.zero_pos(self.target)
if state.prev_direction: # find already started
if abs(self.motor.target - self.motor.value) > self.motor.tolerance: if abs(self.motor.target - self.motor.value) > self.motor.tolerance:
# no success on last find try, try short and strong step # no success on last find try, try short and strong step
self.write_adjusting(True) self.write_adjusting(True)
self.log.info('one step to %g', self.motor.value + self.safe_step) self.log.info('one step to %g', self.motor.value + self.safe_step)
self.drive_relative(sign * self.safe_step) self.drive_relative(direction * self.safe_step)
return return Retry()
else:
state.prev_direction = math.copysign(1, self.target)
side_name = 'negative' if direction == -1 else 'positive'
if zero_pos is not None: if zero_pos is not None:
self.status = 'BUSY', 'change to %s side' % side_name self.status = 'BUSY', 'change to %s side' % side_name
zero_pos += sign * (self.hysteresis * self.slope - self.motor.tolerance) zero_pos += direction * (self.hysteresis * self.slope - self.motor.tolerance)
if (self.motor.value - zero_pos) * sign < -self.motor.tolerance: if (self.motor.value - zero_pos) * direction < -self.motor.tolerance:
self.write_adjusting(False) self.write_adjusting(False)
self.log.info('change side to %g', zero_pos) self.log.info('change side to %g', zero_pos)
self.drive_relative(zero_pos - self.motor.value) self.drive_relative(zero_pos - self.motor.value)
return return Retry()
# we are already at or beyond zero_pos # we are already at or beyond zero_pos
self.next_action(self.adjust) return self.adjust
return
self.write_adjusting(False) self.write_adjusting(False)
self.status = 'BUSY', 'find %s side' % side_name self.status = 'BUSY', 'find %s side' % side_name
self.log.info('one turn to %g', self.motor.value + sign * 360) self.log.info('one turn to %g', self.motor.value + direction * 360)
self.drive_relative(sign * 360) self.drive_relative(direction * 360)
return Retry()
def free(self, force, target): def free(self, state):
"""free from high force at other end""" """free from high force at other end"""
if state.init:
state.free_way = None
self.reset_progress(state)
if self.motor_busy(): if self.motor_busy():
return return Retry()
if abs(force) > abs(self._previous_force) + self.tolerance: self.value = self._force
self.stop() if abs(self.value) > abs(state.force_before_free) + self.hysteresis:
self.status = 'ERROR', 'force increase while freeing' raise Error('force increase while freeing')
self.log.error(self.status[1]) if abs(self.value) < self.hysteresis:
return return self.find
if abs(force) < self.hysteresis: if state.free_way is None:
self.next_action(self.find) state.free_way = 0
return self.log.info('free from high force %g', self.value)
if self.init_action():
self._free_way = 0
self.log.info('free from high force %g', force)
self.write_adjusting(True) self.write_adjusting(True)
sign = math.copysign(1, target) direction = math.copysign(1, self.target)
if self._free_way > (abs(self._previous_force) + self.hysteresis) * self.slope: if state.free_way > abs(state.force_before_free + self.hysteresis) * self.slope + self.motor_max_play:
self.stop() raise Error('freeing failed')
self.status = 'ERROR', 'freeing failed' state.free_way += self.safe_step
self.log.error(self.status[1]) self.drive_relative(direction * self.safe_step)
return return Retry()
self._free_way += self.safe_step
self.drive_relative(sign * self.safe_step)
def within_tolerance(self, force, target):
"""within tolerance"""
if self.motor_busy():
return
if abs(target - force) > self.tolerance:
self.next_action(self.adjust)
elif self.init_action():
self.status = 'IDLE', 'within tolerance'
def adjust(self, force, target):
"""adjust force"""
if self.motor_busy():
return
if abs(target - force) < self.tolerance:
self._in_cnt += 1
if self._in_cnt >= 3:
self.next_action(self.within_tolerance)
return
else:
self._in_cnt = 0
if self.init_action():
self._fail_cnt = 0
self.write_adjusting(True)
self.status = 'BUSY', 'adjusting force'
elif not self._filtered:
return
else:
force_step = force - self._last_force
if self._expected_step:
# compare detected / expected step
q = force_step / self._expected_step
if q < 0.1:
self._fail_cnt += 1
elif q > 0.5:
self._fail_cnt = max(0, self._fail_cnt - 1)
if self._fail_cnt >= 10:
if force < self.hysteresis:
self.log.warning('adjusting failed - try to find zero pos')
self.set_zero_pos(target, None)
self.next_action(self.find)
elif self._fail_cnt > 20:
self.stop()
self.status = 'ERROR', 'force seems not to change substantially'
self.log.error(self.status[1])
return
self._last_force = force
force_step = (target - force) * self.pid_i
if abs(target - force) < self.tolerance * 0.5:
self._expected_step = 0
return
self._expected_step = force_step
step = force_step * self.slope
self.drive_relative(step)
def idle(self, *args):
if self.init_action():
self.write_adjusting(False)
if self.status[0] == 'BUSY':
self.status = 'IDLE', 'stopped'
def read_value(self):
try:
force = self.transducer.read_value()
self._cnt_rderr = max(0, self._cnt_rderr - 1)
except Exception as e:
self._cnt_rderr += 1
if self._cnt_rderr > 10:
self.stop()
self.status = 'ERROR', 'too many read errors: %s' % e
self.log.error(self.status[1])
return Done
now = time.time()
if self.motor_busy():
# do not filter while driving
self.value = force
self.reset_filter()
self._filtered = False
else:
self._sum += force
self._cnt += 1
if now < self._filter_start + self.filter_interval:
return Done
force = self._sum / self._cnt
self.value = force
self.reset_filter(now)
self._filtered = True
if abs(force) > self.limit + self.hysteresis:
self.status = 'ERROR', 'above max limit'
self.log.error(self.status[1])
return Done
if self.zero_pos(force) is None and abs(force) > self.hysteresis and self._filtered:
self.set_zero_pos(force, self.motor.read_value())
self._action(self.value, self.target)
return Done
def write_target(self, target): def write_target(self, target):
if abs(target) > self.limit: if abs(target) > self.limit:
raise BadValueError('force above limit') raise BadValueError('force above limit')
if abs(target - self.value) <= self.tolerance: if abs(target - self.value) <= self.tolerance:
if self.isBusy(): if not self.isBusy():
self.stop()
self.next_action(self.within_tolerance)
else:
self.status = 'IDLE', 'already at target' self.status = 'IDLE', 'already at target'
self.next_action(self.within_tolerance) self._state.start(self.within_tolerance)
return target return target
self.log.info('new target %g', target) self.log.info('new target %g', target)
self._cnt_rderr = 0 self._cnt_rderr = 0
self._cnt_wrerr = 0 self._cnt_wrerr = 0
self.status = 'BUSY', 'changed target' self.status = 'BUSY', 'changed target'
self.target = target
if self.value * math.copysign(1, target) > self.hysteresis: if self.value * math.copysign(1, target) > self.hysteresis:
self.next_action(self.adjust) self._state.start(self.adjust)
else: else:
self.next_action(self.find) self._state.start(self.find)
return target return Done
def read_target(self):
if self._state.state is None:
if self.status[1]:
raise Error(self.status[1])
raise Error('inactive')
return self.target
@Command() @Command()
def stop(self): def stop(self):
self._action = self.idle
if self.motor.isBusy(): if self.motor.isBusy():
self.log.info('stop motor') self.log.info('stop motor')
self.motor.stop() self.motor.stop()
self.next_action(self.idle) self.status = 'IDLE', 'stopped'
self._state.stop()
def write_force_offset(self, value): def write_force_offset(self, value):
self.force_offset = value self.force_offset = value