From bb40a0820c9c9167c165b5f288f1cbaa57bd2237 Mon Sep 17 00:00:00 2001 From: camea Date: Fri, 29 Jul 2022 15:12:44 +0200 Subject: [PATCH 01/15] use flag instead tolerance for redo check when target is changed, a flag is set, and this is used to jump back in the sequence to the right step for redoing --- secop_psi/magfield.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/secop_psi/magfield.py b/secop_psi/magfield.py index 5a71ff4..cdf04c7 100644 --- a/secop_psi/magfield.py +++ b/secop_psi/magfield.py @@ -84,6 +84,7 @@ class Magfield(HasLimits, Drivable): _state = None _init = True _super_sw_check = False + _target_changed = False switch_time = None def doPoll(self): @@ -113,6 +114,7 @@ class Magfield(HasLimits, Drivable): def write_target(self, target): self.check_limits(target) + self._target_changed = target != self.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 @@ -145,7 +147,7 @@ class Magfield(HasLimits, Drivable): def start_field_change(self, state): self.setFastPoll(True, 1.0) self.status = Status.PREPARING, 'changed target field' - if self.persistent_field == self.target: + if abs(self.target - self.persistent_field) <= self.tolerance: # short cut return self.check_switch_off return self.start_ramp_to_field @@ -158,7 +160,7 @@ class Magfield(HasLimits, Drivable): def ramp_to_field(self, state): """ramping, wait for current at persistent field""" - if self.persistent_field == self.target: # short cut + if abs(self.target - self.persistent_field) <= self.tolerance: # short cut return self.check_switch_off if abs(self.current - self.persistent_field) > self.tolerance: if state.init: @@ -196,11 +198,11 @@ class Magfield(HasLimits, Drivable): def switch_on(self, state): """wait for switch heater open""" - if self.persistent_field == self.target: # short cut + if abs(self.target - self.persistent_field) <= self.tolerance: # short cut return self.check_switch_off if state.now - self.switch_time < self.wait_switch_on: return Retry() - state.set_point = self.target + self._target_changed = False return self.start_ramp_to_target def start_ramp_to_target(self, state): @@ -212,8 +214,8 @@ class Magfield(HasLimits, Drivable): def ramp_to_target(self, state): """ramp field to target""" - if state.set_point != self.target: # target changed - state.set_point = self.target + if self._target_changed: + self._target_changed = False return self.start_ramp_to_target self.persistent_field = self.value # Remarks: assume there is a ramp limiting feature @@ -226,6 +228,9 @@ class Magfield(HasLimits, Drivable): def stabilize_field(self, state): """stabilize field""" + if self._target_changed: + self._target_changed = False + return self.start_ramp_to_target self.persistent_field = self.value if state.now - state.stabilize_start < self.wait_stable_field: if state.init: @@ -253,9 +258,8 @@ class Magfield(HasLimits, Drivable): def switch_off(self, state): """wait for switch heater closed""" - if self.persistent_field != self.target: # redo - return self.start_switch_on - if self.mode == Mode.DRIVEN: + if self._target_changed or self.mode == Mode.DRIVEN: + # target or mode has changed -> redo return self.start_switch_on self.persistent_field = self.value if state.now - self.switch_time < self.wait_switch_off: @@ -272,7 +276,8 @@ class Magfield(HasLimits, Drivable): def ramp_to_zero(self, state): """ramp field to zero""" - if self.persistent_field != self.target or self.mode == Mode.DRIVEN: # redo + if self._target_changed or self.mode == Mode.DRIVEN: + # target or mode has changed -> redo return self.start_field_change if abs(self.current) > self.tolerance: if state.init: From 3496e391f6def1c9a53458835feec7e1f63093d5 Mon Sep 17 00:00:00 2001 From: camea Date: Tue, 2 Aug 2022 11:19:30 +0200 Subject: [PATCH 02/15] check for last_target in conditions for redo --- secop_psi/magfield.py | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/secop_psi/magfield.py b/secop_psi/magfield.py index cdf04c7..5e55270 100644 --- a/secop_psi/magfield.py +++ b/secop_psi/magfield.py @@ -84,7 +84,7 @@ class Magfield(HasLimits, Drivable): _state = None _init = True _super_sw_check = False - _target_changed = False + _last_target = None switch_time = None def doPoll(self): @@ -114,7 +114,6 @@ class Magfield(HasLimits, Drivable): def write_target(self, target): self.check_limits(target) - self._target_changed = target != self.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 @@ -147,7 +146,8 @@ class Magfield(HasLimits, Drivable): def start_field_change(self, state): self.setFastPoll(True, 1.0) self.status = Status.PREPARING, 'changed target field' - if abs(self.target - self.persistent_field) <= self.tolerance: # short cut + if (self.target == self._last_target and + abs(self.target - self.persistent_field) <= self.tolerance): # short cut return self.check_switch_off return self.start_ramp_to_field @@ -160,7 +160,8 @@ class Magfield(HasLimits, Drivable): def ramp_to_field(self, state): """ramping, wait for current at persistent field""" - if abs(self.target - self.persistent_field) <= self.tolerance: # short cut + if (self.target == self._last_target and + abs(self.target - self.persistent_field) <= self.tolerance): # short cut return self.check_switch_off if abs(self.current - self.persistent_field) > self.tolerance: if state.init: @@ -186,11 +187,11 @@ class Magfield(HasLimits, Drivable): def start_switch_on(self, state): """switch heater on""" - self._super_sw_check = False if self.switch_heater != 0: self.status = Status.PREPARING, 'wait for heater on' else: self.status = Status.PREPARING, 'turn switch heater on' + self._super_sw_check = False self.write_switch_heater(True) if not self._super_sw_check: raise ProgrammingError('missing super call in write_switch_heater') @@ -198,11 +199,12 @@ class Magfield(HasLimits, Drivable): def switch_on(self, state): """wait for switch heater open""" - if abs(self.target - self.persistent_field) <= self.tolerance: # short cut + if (self.target == self._last_target and + abs(self.target - self.persistent_field) <= self.tolerance): # short cut return self.check_switch_off if state.now - self.switch_time < self.wait_switch_on: return Retry() - self._target_changed = False + self._last_target = self.target return self.start_ramp_to_target def start_ramp_to_target(self, state): @@ -214,8 +216,8 @@ class Magfield(HasLimits, Drivable): def ramp_to_target(self, state): """ramp field to target""" - if self._target_changed: - self._target_changed = False + 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 # Remarks: assume there is a ramp limiting feature @@ -228,8 +230,8 @@ class Magfield(HasLimits, Drivable): def stabilize_field(self, state): """stabilize field""" - if self._target_changed: - self._target_changed = False + 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 if state.now - state.stabilize_start < self.wait_stable_field: @@ -250,16 +252,15 @@ class Magfield(HasLimits, Drivable): self.status = Status.FINALIZING, 'turn switch heater off' else: self.status = Status.FINALIZING, 'wait for heater off' - self._super_sw_check = False self.write_switch_heater(False) - if not self._super_sw_check: - raise ProgrammingError('missing super call in write_switch_heater') + # no check for super call needed here (would have been detected in start_switch_on) return self.switch_off def switch_off(self, state): """wait for switch heater closed""" - if self._target_changed or self.mode == Mode.DRIVEN: + 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 if state.now - self.switch_time < self.wait_switch_off: @@ -269,15 +270,16 @@ class Magfield(HasLimits, Drivable): def start_ramp_to_zero(self, state): """start ramping current to target - initiate ramp to zero (with corresponding ramp rate + initiate ramp to zero (with corresponding ramp rate) should return ramp_to_zero """ raise NotImplementedError def ramp_to_zero(self, state): """ramp field to zero""" - if self._target_changed or self.mode == Mode.DRIVEN: + if self.target != self._last_target or self.mode == Mode.DRIVEN: # target or mode has changed -> redo + self._last_target = None return self.start_field_change if abs(self.current) > self.tolerance: if state.init: From 8e3cdc80e4a392edeb2f02a85f87a3e8cbf1c164 Mon Sep 17 00:00:00 2001 From: camea Date: Fri, 12 Aug 2022 15:10:23 +0200 Subject: [PATCH 03/15] improvements in magfiels/ips_mercury - read voltage - fix a bug with ._init name conflict --- cfg/main/mb11.cfg | 1 + secop_psi/convergence.py | 11 +++++++++++ secop_psi/ips_mercury.py | 10 +++++++--- secop_psi/magfield.py | 10 ++++++---- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/cfg/main/mb11.cfg b/cfg/main/mb11.cfg index 56b4248..b863724 100644 --- a/cfg/main/mb11.cfg +++ b/cfg/main/mb11.cfg @@ -161,6 +161,7 @@ class = secop_psi.ips_mercury.Field description = magnetic field slot = GRPZ io = ips +tolerance = 0.001 target.max = 11 [om_io] diff --git a/secop_psi/convergence.py b/secop_psi/convergence.py index 15833b5..385a098 100644 --- a/secop_psi/convergence.py +++ b/secop_psi/convergence.py @@ -89,6 +89,10 @@ class HasConvergence: """to be called from write_target""" self.convergence_state.start(self.state_approach) + def interrupt_state(self): + """to be called from stop""" + self.convergence_state.start(self.state_instable) + def state_approach(self, state): """approaching, checking progress (busy)""" state.spent_inside = 0 @@ -157,3 +161,10 @@ class HasConvergence: else: state.spent_inside = max(0, state.spent_inside - state.delta()) return Retry() + + def state_interrupt(self, state): + self.status = IDLE, 'stopped' # stop called + return self.state_instable + + def stop(self): + self.convergence_state.start(self.state_interrupt) diff --git a/secop_psi/ips_mercury.py b/secop_psi/ips_mercury.py index e965f5e..c3ee708 100644 --- a/secop_psi/ips_mercury.py +++ b/secop_psi/ips_mercury.py @@ -35,6 +35,7 @@ CURRENT_CHECK_SIZE = 2 class Field(MercuryChannel, Magfield): action = Parameter('action', EnumType(Action), readonly=False) setpoint = Parameter('field setpoint', FloatRange(unit='T'), default=0) + voltage = Parameter('leads voltage', FloatRange(unit='V'), default=0) atob = Parameter('field to amp', FloatRange(0, unit='A/T'), default=0) forced_persistent_field = Parameter( 'manual indication that persistent field is bad', BoolType(), readonly=False, default=False) @@ -43,13 +44,13 @@ class Field(MercuryChannel, Magfield): _field_mismatch = None nslaves = 3 slave_currents = None - _init = True + __init = True def read_value(self): self.current = self.query('PSU:SIG:FLD') pf = self.query('PSU:SIG:PFLD') - if self._init: - self._init = False + if self.__init: + self.__init = False self.persistent_field = pf if self.switch_heater != 0 or self._field_mismatch is None: self.forced_persistent_field = False @@ -92,6 +93,9 @@ class Field(MercuryChannel, Magfield): def read_atob(self): return self.query('PSU:ATOB') + def read_voltage(self): + return self.query('PSU:SIG:VOLT') + def read_setpoint(self): return self.query('PSU:SIG:FSET') diff --git a/secop_psi/magfield.py b/secop_psi/magfield.py index 5e55270..65a33af 100644 --- a/secop_psi/magfield.py +++ b/secop_psi/magfield.py @@ -82,19 +82,21 @@ class Magfield(HasLimits, Drivable): 'wait time to ensure field is stable', FloatRange(0, unit='s'), readonly=False, default=31) _state = None - _init = True + __init = True _super_sw_check = False _last_target = None switch_time = None def doPoll(self): - if self._init: - self._init = False + if self.__init: + self.__init = False self.switch_time = time.time() 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_value) + self.write_target(self.persistent_field) + else: + self._last_target = self.persistent_field else: self.read_value() self._state.cycle() From 704bba292a785cdd82a793fc7a0efbd0c6b2d272 Mon Sep 17 00:00:00 2001 From: camea Date: Fri, 12 Aug 2022 15:13:07 +0200 Subject: [PATCH 04/15] add mb11 stick --- cfg/stick/mb11stick.cfg | 43 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 cfg/stick/mb11stick.cfg diff --git a/cfg/stick/mb11stick.cfg b/cfg/stick/mb11stick.cfg new file mode 100644 index 0000000..8f900e3 --- /dev/null +++ b/cfg/stick/mb11stick.cfg @@ -0,0 +1,43 @@ +[NODE] +description = MB11 standard sample stick +id = mb11.stick.sea.psi.ch + + +[INTERFACE] +uri = tcp://5000 + +[itc] +class = secop.proxy.Proxy +remote_class = secop_psi.mercury.IO +description = connection to MB11 mercury +module = itc1 +#uri = mb11-ts:3001 +#timeout = 5 + +#[t3] +#class = secop_psi.softcal.Sensor +#rawsensor = r2 +##calib USstick +#calib = /home/l_samenv/sea/tcl/calcurves/X163059.340 +##calib dilatometer stick +#calib = /home/l_samenv/sea/tcl/calcurves/X86023.340 +## unknown +#calib = /home/l_samenv/sea/tcl/calcurves/X70197.340 +#svalue.unit = K + + + + +[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 From b81fc7b122e9f1d7580943d34cb8c974b91205c9 Mon Sep 17 00:00:00 2001 From: dmc Date: Mon, 15 Aug 2022 16:22:02 +0200 Subject: [PATCH 05/15] remove tm from ill5.cfg only one SeaDrivable per sea drivable allowed! --- cfg/main/ill5.cfg | 6 ---- cfg/sea/ill5.config.json | 67 +++++++++++++++++++++++----------------- secop_psi/sea.py | 3 +- 3 files changed, 39 insertions(+), 37 deletions(-) diff --git a/cfg/main/ill5.cfg b/cfg/main/ill5.cfg index f2c5e94..288b2a5 100644 --- a/cfg/main/ill5.cfg +++ b/cfg/main/ill5.cfg @@ -13,12 +13,6 @@ class = secop_psi.sea.SeaDrivable io = sea_main sea_object = tt -[tm] -class = secop_psi.sea.SeaDrivable -io = sea_main -sea_object = tt -rel_paths = tm - [cc] class = secop_psi.sea.SeaReadable io = sea_main diff --git a/cfg/sea/ill5.config.json b/cfg/sea/ill5.config.json index c960299..1e1c09c 100644 --- a/cfg/sea/ill5.config.json +++ b/cfg/sea/ill5.config.json @@ -1,5 +1,5 @@ {"tt": {"base": "/tt", "params": [ -{"path": "", "type": "float", "readonly": false, "cmd": "run tt", "description": "tt", "kids": 19}, +{"path": "", "type": "float", "readonly": false, "cmd": "run tt", "description": "tt", "kids": 17}, {"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}, @@ -62,8 +62,6 @@ {"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": [ @@ -116,23 +114,23 @@ {"path": "hit", "type": "float", "readonly": false, "cmd": "cc hit"}, {"path": "hft", "type": "int", "readonly": false, "cmd": "cc hft"}, {"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": "hch", "type": "int", "readonly": false, "cmd": "cc hch"}, +{"path": "hwr0", "type": "float", "readonly": false, "cmd": "cc hwr0"}, +{"path": "hem0", "type": "float", "readonly": false, "cmd": "cc hem0", "description": "sensor length in mm from top to empty pos."}, +{"path": "hfu0", "type": "float", "readonly": false, "cmd": "cc hfu0", "description": "sensor length in mm from top to full pos."}, +{"path": "hd0", "type": "float", "readonly": false, "cmd": "cc hd0", "description": "external sensor drive current (mA)"}, +{"path": "h0", "type": "float"}, +{"path": "hs0", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}}, +{"path": "h1", "type": "float"}, +{"path": "hs1", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}}, +{"path": "h2", "type": "float"}, +{"path": "hs2", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}}, +{"path": "h3", "type": "float"}, +{"path": "hs3", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}}, +{"path": "h4", "type": "float"}, +{"path": "hs4", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}}, +{"path": "h5", "type": "float"}, +{"path": "hs5", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}}, {"path": "hfb", "type": "float"}, {"path": "nav", "type": "bool", "readonly": false, "cmd": "cc nav"}, {"path": "nu", "type": "float"}, @@ -225,28 +223,29 @@ {"path": "state", "type": "text"}]}, "hefill": {"base": "/hefill", "params": [ -{"path": "", "type": "enum", "enum": {"watching": 0, "fill": 1, "inactive": 2, "manualfill": 3}, "readonly": false, "cmd": "hefill", "kids": 6}, +{"path": "", "type": "enum", "enum": {"watching": 0, "fill": 1, "inactive": 2, "manualfill": 3}, "readonly": false, "cmd": "hefill", "kids": 7}, {"path": "send", "type": "text", "readonly": false, "cmd": "hefill send", "visibility": 3}, {"path": "status", "type": "text", "visibility": 3}, {"path": "fast", "type": "enum", "enum": {"slow": 0, "fast": 1}, "readonly": false, "cmd": "cc hf"}, {"path": "state", "type": "text"}, {"path": "hefull", "type": "float", "readonly": false, "cmd": "cc hh"}, -{"path": "helow", "type": "float", "readonly": false, "cmd": "cc hl"}]}, +{"path": "helow", "type": "float", "readonly": false, "cmd": "cc hl"}, +{"path": "smooth", "type": "float"}]}, "hepump": {"base": "/hepump", "params": [ {"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": "status", "type": "text", "visibility": 3}, -{"path": "running", "type": "bool", "readonly": false, "cmd": "hepump running", "visibility": 3}, -{"path": "eco", "type": "bool", "readonly": false, "cmd": "hepump eco", "visibility": 3}, -{"path": "auto", "type": "bool", "readonly": false, "cmd": "hepump auto", "visibility": 3}, -{"path": "valve", "type": "enum", "enum": {"closed": 0, "closing": 1, "opening": 2, "opened": 3, "undefined": 4}, "readonly": false, "cmd": "hepump valve", "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", "visibility": 3}, +{"path": "running", "type": "bool", "readonly": false, "cmd": "hepump running"}, +{"path": "eco", "type": "bool", "readonly": false, "cmd": "hepump eco"}, +{"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"}, +{"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": "health", "type": "float"}]}, "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": "status", "type": "text", "visibility": 3}, {"path": "is_running", "type": "int", "readonly": false, "cmd": "hemot is_running", "visibility": 3}, @@ -296,4 +295,14 @@ {"path": "tbl_tt_dblctrl_prop_up", "type": "text", "readonly": false, "cmd": "table tbl_tt_dblctrl_prop_up", "description": "enter value pair separated with colon T1:par1 T2:par2 ..."}, {"path": "fix_tt_dblctrl_prop_lo", "type": "bool", "readonly": false, "cmd": "table fix_tt_dblctrl_prop_lo"}, {"path": "val_tt_dblctrl_prop_lo", "type": "float"}, -{"path": "tbl_tt_dblctrl_prop_lo", "type": "text", "readonly": false, "cmd": "table tbl_tt_dblctrl_prop_lo", "description": "enter value pair separated with colon T1:par1 T2:par2 ..."}]}} +{"path": "tbl_tt_dblctrl_prop_lo", "type": "text", "readonly": false, "cmd": "table tbl_tt_dblctrl_prop_lo", "description": "enter value pair separated with colon T1:par1 T2:par2 ..."}]}, + +"prep0v": {"base": "/prep0v", "params": [ +{"path": "", "type": "text", "readonly": false, "cmd": "prep0v", "kids": 2}, +{"path": "send", "type": "text", "readonly": false, "cmd": "prep0v send", "visibility": 3}, +{"path": "status", "type": "text", "visibility": 3}]}, + +"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}]}} diff --git a/secop_psi/sea.py b/secop_psi/sea.py index f4b0eb6..749207c 100644 --- a/secop_psi/sea.py +++ b/secop_psi/sea.py @@ -401,7 +401,6 @@ class SeaModule(Module): hdbpath = None # hdbpath for main writable def __new__(cls, name, logger, cfgdict, srv): - print('N', cls, name) if hasattr(srv, 'extra_sea_modules'): extra_modules = srv.extra_sea_modules else: @@ -667,7 +666,7 @@ class SeaDrivable(SeaModule, Drivable): def write_target(self, value): self.io.query('run %s %s' % (self.sea_object, value)) - #self.status = [self.Status.BUSY, 'driving'] + # self.status = [self.Status.BUSY, 'driving'] return value def update_status(self, value, timestamp, readerror): From 2f6954c4f39af22d58b761c2d760a4bb2aaefc2b Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Tue, 16 Aug 2022 10:02:57 +0200 Subject: [PATCH 06/15] magfield: add persistent_limit parameter - use update_switch_heater instead of write_switch_heater for detecting switch time - switch_time split into switch_on_time/switch_off_time Change-Id: Id90a8b2c2520e24f3ee4a34aee25d41210e5d6d4 --- secop_psi/ips_mercury.py | 1 - secop_psi/magfield.py | 44 ++++++++++++++++++++++++++-------------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/secop_psi/ips_mercury.py b/secop_psi/ips_mercury.py index c3ee708..252f755 100644 --- a/secop_psi/ips_mercury.py +++ b/secop_psi/ips_mercury.py @@ -87,7 +87,6 @@ class Field(MercuryChannel, Magfield): return self.query('PSU:SIG:SWHT', off_on) def write_switch_heater(self, value): - super().write_switch_heater(value) return self.change('PSU:SIG:SWHT', value, off_on) def read_atob(self): diff --git a/secop_psi/magfield.py b/secop_psi/magfield.py index 65a33af..3efa025 100644 --- a/secop_psi/magfield.py +++ b/secop_psi/magfield.py @@ -80,17 +80,19 @@ class Magfield(HasLimits, Drivable): 'wait time to ensure current is stable', FloatRange(0, unit='s'), readonly=False, default=6) wait_stable_field = Parameter( '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 __init = True - _super_sw_check = False _last_target = None - switch_time = None + switch_on_time = None + switch_off_time = None def doPoll(self): if self.__init: self.__init = False - self.switch_time = time.time() 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 @@ -112,6 +114,7 @@ class Magfield(HasLimits, Drivable): 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 write_target(self, target): @@ -180,12 +183,16 @@ class Magfield(HasLimits, Drivable): return Retry() return self.start_switch_on - def write_switch_heater(self, value): - """implementations must super call this!""" - self._super_sw_check = True - if value != self.switch_heater: - self.switch_time = time.time() - return value + def update_switch_heater(self, value): + """is called whenever switch heater was changed""" + if value: + self.switch_off_time = None + if self.switch_on_time is None: + self.switch_on_time = time.time() + else: + self.switch_on_time = None + if self.switch_off_time is None: + self.switch_off_time = time.time() def start_switch_on(self, state): """switch heater on""" @@ -193,10 +200,7 @@ class Magfield(HasLimits, Drivable): self.status = Status.PREPARING, 'wait for heater on' else: self.status = Status.PREPARING, 'turn switch heater on' - self._super_sw_check = False self.write_switch_heater(True) - if not self._super_sw_check: - raise ProgrammingError('missing super call in write_switch_heater') return self.switch_on def switch_on(self, state): @@ -204,7 +208,11 @@ class Magfield(HasLimits, Drivable): if (self.target == self._last_target and abs(self.target - self.persistent_field) <= self.tolerance): # short cut return self.check_switch_off - if state.now - self.switch_time < self.wait_switch_on: + self.read_switch_heater() + if self.switch_on_time is None: + self.log.warning('switch turned off manually - try again') + return self.start_switch_on + if state.now - self.switch_on_time < self.wait_switch_on: return Retry() self._last_target = self.target return self.start_ramp_to_target @@ -255,7 +263,6 @@ class Magfield(HasLimits, Drivable): else: self.status = Status.FINALIZING, 'wait for heater off' self.write_switch_heater(False) - # no check for super call needed here (would have been detected in start_switch_on) return self.switch_off def switch_off(self, state): @@ -265,8 +272,15 @@ class Magfield(HasLimits, Drivable): self._last_target = None return self.start_switch_on self.persistent_field = self.value - if state.now - self.switch_time < self.wait_switch_off: + self.read_switch_heater() + if self.switch_off_time is None: + self.log.warning('switch turned on manually - try again') + return self.start_switch_off + if state.now - self.switch_off_time < self.wait_switch_off: return Retry() + if abs(self.value) > self.persistent_limit: + self.status = Status.IDLE, 'leads current at field, switch off' + return self.finish_state return self.start_ramp_to_zero def start_ramp_to_zero(self, state): From f23e42de1f868796b0aa716dea920a4e53f435c3 Mon Sep 17 00:00:00 2001 From: camea Date: Tue, 16 Aug 2022 16:54:30 +0200 Subject: [PATCH 07/15] fix bugs with persistent_limit parameter take into account that reading the switch heater might be delayed --- cfg/main/mb11.cfg | 18 ++++++++++-------- secop_psi/ips_mercury.py | 21 ++++++++++++++++----- secop_psi/magfield.py | 30 +++++++++++++++++++----------- 3 files changed, 45 insertions(+), 24 deletions(-) diff --git a/cfg/main/mb11.cfg b/cfg/main/mb11.cfg index b863724..359b2ba 100644 --- a/cfg/main/mb11.cfg +++ b/cfg/main/mb11.cfg @@ -82,6 +82,16 @@ description = dynamic needle valve position slot = DB8.P1,DB4.G1 io = itc1 +[mf] +class = secop_psi.ips_mercury.Field +description = magnetic field +slot = GRPZ +io = ips +tolerance = 0.001 +wait_stable_field = 60 +target.max = 11 +persistent_limit = 7 + [lev] class = secop_psi.mercury.HeLevel description = LHe level @@ -156,14 +166,6 @@ description = coil temperature slot = MB1.T1 io = ips -[mf] -class = secop_psi.ips_mercury.Field -description = magnetic field -slot = GRPZ -io = ips -tolerance = 0.001 -target.max = 11 - [om_io] description = dom motor IO class = secop_psi.phytron.PhytronIO diff --git a/secop_psi/ips_mercury.py b/secop_psi/ips_mercury.py index 252f755..6020359 100644 --- a/secop_psi/ips_mercury.py +++ b/secop_psi/ips_mercury.py @@ -119,16 +119,27 @@ class Field(MercuryChannel, Magfield): return current / self.atob return 0 - def start_ramp_to_field(self, state): - self.change('PSU:SIG:FSET', self.persistent_field) + def set_and_go(self, value): + 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_field(self, state): + try: + self.set_and_go(self.persistent_field) + except (HardwareError, AssertionError): + state.switch_undef = self.switch_on_time or state.now + return self.wait_for_switch return self.ramp_to_field + def wait_for_switch(self, state): + if self.now - self.switch_undef < self.wait_switch_on: + return Retry() + self.set_and_go(self.persistent_field) + return self.ramp_to_field + def start_ramp_to_target(self, state): - self.change('PSU:SIG:FSET', self.target) - assert self.write_action('hold') == 'hold' - assert self.write_action('run_to_set') == 'run_to_set' + self.set_and_go(self.target) return self.ramp_to_target def start_ramp_to_zero(self, state): diff --git a/secop_psi/magfield.py b/secop_psi/magfield.py index 3efa025..1d800b2 100644 --- a/secop_psi/magfield.py +++ b/secop_psi/magfield.py @@ -185,7 +185,7 @@ class Magfield(HasLimits, Drivable): def update_switch_heater(self, value): """is called whenever switch heater was changed""" - if value: + if value != 0: self.switch_off_time = None if self.switch_on_time is None: self.switch_on_time = time.time() @@ -196,11 +196,15 @@ class Magfield(HasLimits, Drivable): def start_switch_on(self, state): """switch heater on""" - if self.switch_heater != 0: - self.status = Status.PREPARING, 'wait for heater on' - else: + if self.switch_heater == 0: self.status = Status.PREPARING, 'turn switch heater on' - self.write_switch_heater(True) + try: + self.write_switch_heater(True) + except Exception as e: + self.log.warning('write_switch_heater %r', e) + return Retry() + else: + self.status = Status.PREPARING, 'wait for heater on' return self.switch_on def switch_on(self, state): @@ -210,8 +214,10 @@ class Magfield(HasLimits, Drivable): return self.check_switch_off self.read_switch_heater() if self.switch_on_time is None: - self.log.warning('switch turned off manually - try again') - return self.start_switch_on + if state.now - self.switch_off_time > 10: + self.log.warning('switch turned off manually?') + return self.start_switch_on + return Retry() if state.now - self.switch_on_time < self.wait_switch_on: return Retry() self._last_target = self.target @@ -258,11 +264,11 @@ class Magfield(HasLimits, Drivable): def start_switch_off(self, state): """turn off switch heater""" - if self.switch_heater != 0: + if self.switch_heater == 1: self.status = Status.FINALIZING, 'turn switch heater off' + self.write_switch_heater(False) else: self.status = Status.FINALIZING, 'wait for heater off' - self.write_switch_heater(False) return self.switch_off def switch_off(self, state): @@ -274,8 +280,10 @@ class Magfield(HasLimits, Drivable): self.persistent_field = self.value self.read_switch_heater() if self.switch_off_time is None: - self.log.warning('switch turned on manually - try again') - return self.start_switch_off + if state.now - self.switch_on_time > 10: + self.log.warning('switch turned on manually?') + return self.start_switch_off + return Retry() if state.now - self.switch_off_time < self.wait_switch_off: return Retry() if abs(self.value) > self.persistent_limit: From d5d04dc82f83a090cfb94ad90cb2826e32025413 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Tue, 9 Aug 2022 15:29:12 +0200 Subject: [PATCH 08/15] add simple interactive python client - SECoP modules are accessible as objects in the main python module - parameters are accessed as attributes of these objects - __repr__ is used for listing all parameters - __call__ is used for 'change target and wait until no more busy' typically used from a python interpreter or in a jupyter notebook Change-Id: Idb55684eeff6d1262e5d1517a3ff934f1c1bf208 Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/28980 Tested-by: Jenkins Automated Tests Reviewed-by: Markus Zolliker --- secop/client/interactive.py | 263 ++++++++++++++++++++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 secop/client/interactive.py diff --git a/secop/client/interactive.py b/secop/client/interactive.py new file mode 100644 index 0000000..85d6428 --- /dev/null +++ b/secop/client/interactive.py @@ -0,0 +1,263 @@ +# -*- 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 +# +# ***************************************************************************** +"""simple interactive python client""" + +import sys +import time +import json +from queue import Queue +from secop.client import SecopClient +from secop.errors import SECoPError + +USAGE = """ +Usage: + +from secop.client.interactive import Client + +client = Client('localhost:5000') # start client. +# this connects and creates objects for all SECoP modules in the main namespace + + # list all parameters +. = # change parameter +() # set target and wait until not busy + # 'status' and 'value' changes are shown every 1 sec +client.mininterval = 0.2 # change minimal update interval to 0.2 sec (default is 1 second) + +.watch(1) # watch changes of all parameters of a module +.watch(0) # remove all watching +.watch(status=1, value=1) # add 'status' and 'value' to watched parameters +.watch(value=0) # remove 'value' from watched parameters +""" + +main = sys.modules['__main__'] + + +class Logger: + def __init__(self, loglevel='info'): + func = self.noop + for lev in 'debug', 'info', 'warning', 'error': + if lev == loglevel: + func = self.emit + setattr(self, lev, func) + + @staticmethod + def emit(fmt, *args, **kwds): + print(str(fmt) % args) + + @staticmethod + def noop(fmt, *args, **kwds): + pass + + +class PrettyFloat(float): + def __repr__(self): + result = '%.12g' % self + if '.' in result or 'e' in result: + return result + return result + '.' + + +class Module: + def __init__(self, name, secnode): + self._name = name + self._secnode = secnode + self._parameters = list(secnode.modules[name]['parameters']) + self._commands = list(secnode.modules[name]['commands']) + self._running = None + self._status = None + props = secnode.modules[name]['properties'] + self._title = '# %s (%s)' % (props.get('implementation', ''), props.get('interface_classes', [''])[0]) + + def _one_line(self, pname, minwid=0): + """return . = truncated to one line""" + try: + value = getattr(self, pname) + # make floats appear with 7 digits only + r = repr(json.loads(json.dumps(value), parse_float=PrettyFloat)) + except Exception as e: + r = repr(e) + unit = getattr(type(self), pname).unit + if unit: + r += ' %s' % unit + pname = pname.ljust(minwid) + vallen = 113 - len(self._name) - len(pname) + if len(r) > vallen: + r = r[:vallen - 4] + ' ...' + return '%s.%s = %s' % (self._name, pname, r) + + def _isBusy(self): + return 300 <= self.status[0] < 400 + + def _status_value_update(self, m, p, status, t, e): + if self._running: + try: + self._running.put(True) + if self._running and not self._isBusy(): + self._running.put(False) + except TypeError: # may happen when _running is removed during above lines + pass + + def _watch_parameter(self, m, pname, *args, forced=False, mininterval=0): + """show parameter update""" + pobj = getattr(type(self), pname) + if not args: + args = self._secnode.cache[self._name, pname] + value = args[0] + now = time.time() + if (value != pobj.prev and now >= pobj.prev_time + mininterval) or forced: + self._secnode.log.info('%s', self._one_line(pname)) + pobj.prev = value + pobj.prev_time = now + + def watch(self, *args, **kwds): + enabled = {} + for arg in args: + if arg == 1: # or True + enabled.update({k: True for k in self._parameters}) + elif arg == 0: # or False + enabled.update({k: False for k in self._parameters}) + else: + enabled.update(arg) + enabled.update(kwds) + for pname, enable in enabled.items(): + self._secnode.unregister_callback((self._name, pname), updateEvent=self._watch_parameter) + if enable: + self._secnode.register_callback((self._name, pname), updateEvent=self._watch_parameter) + + def read(self, pname='value'): + value, _, error = self._secnode.readParameter(self._name, pname) + if error: + raise error + return value + + def __call__(self, target=None): + if target is None: + return self.read() + self.target = target # this sets self._running + type(self).value.prev = None # show at least one value + show_final_value = True + try: + while self._running.get(): + self._watch_parameter(self._name, 'value', mininterval=self._secnode.mininterval) + self._watch_parameter(self._name, 'status') + except KeyboardInterrupt: + self._secnode.log.info('-- interrupted --') + self._running = None + self._watch_parameter(self._name, 'status') + self._secnode.readParameter(self._name, 'value') + self._watch_parameter(self._name, 'value', forced=show_final_value) + return self.value + + def __repr__(self): + wid = max(len(k) for k in self._parameters) + return '%s\n%s\nCommands: %s' % ( + self._title, + '\n'.join(self._one_line(k, wid) for k in self._parameters), + ', '.join(k + '()' for k in self._commands)) + + +class Param: + def __init__(self, name, unit=None): + self.name = name + self.prev = None + self.prev_time = 0 + self.unit = unit + + def __get__(self, obj, owner): + if obj is None: + return self + value, _, error = obj._secnode.cache[obj._name, self.name] + if error: + raise error + return value + + def __set__(self, obj, value): + if self.name == 'target': + obj._running = Queue() + try: + obj._secnode.setParameter(obj._name, self.name, value) + except SECoPError as e: + obj._secnode.log.error(repr(e)) + + +class Command: + def __init__(self, name, modname, secnode): + self.name = name + self.modname = modname + self.exec = secnode.execCommand + + def call(self, *args, **kwds): + if kwds: + if args: + raise TypeError('mixed arguments forbidden') + result, _ = self.exec(self.modname, self.name, kwds) + else: + result, _ = self.exec(self.modname, self.name, args or None) + return result + + def __get__(self, obj, owner=None): + if obj is None: + return self + return self.call + + +class Client(SecopClient): + activate = True + secnodes = {} + mininterval = 1 + + def __init__(self, uri, loglevel='info'): + # remove previous client: + prev = self.secnodes.pop(uri, None) + if prev: + prev.log.info('remove previous client to %s', uri) + for modname in prev.modules: + prevnode = getattr(getattr(main, modname, None), 'secnode', None) + if prevnode == prev: + prev.log.info('remove previous module %s', modname) + delattr(main, modname) + prev.disconnect() + self.secnodes[uri] = self + super().__init__(uri, Logger(loglevel)) + self.connect() + for modname, moddesc in self.modules.items(): + prev = getattr(main, modname, None) + if prev is None: + self.log.info('create module %s', modname) + else: + if getattr(prev, 'secnode', None) is None: + self.log.error('skip module %s overwriting a global variable' % modname) + continue + self.log.info('overwrite module %s', modname) + attrs = {} + for pname, pinfo in moddesc['parameters'].items(): + unit = pinfo['datainfo'].get('unit') + attrs[pname] = Param(pname, unit) + for cname in moddesc['commands']: + attrs[cname] = Command(cname, modname, self) + mobj = type('M_%s' % modname, (Module,), attrs)(modname, self) + if 'status' in mobj._parameters: + self.register_callback((modname, 'status'), updateEvent=mobj._status_value_update) + self.register_callback((modname, 'value'), updateEvent=mobj._status_value_update) + + setattr(main, modname, mobj) + self.log.info('%s', USAGE) From ab2fe6200aaf778837bfcf6bd15a24437a4280ee Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Wed, 23 Mar 2022 13:53:46 +0100 Subject: [PATCH 09/15] reintroduced individual init of generalConfig.defaults revert basically the former change "init generalConfig.defaults only in secop-server" The problem of import order when setting generalConfig.defaults has to be solved by not overriding already existing keys when setting the default. Change-Id: I82121e346607dd74146279c4241e13ab63c14096 Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/28011 Tested-by: Jenkins Automated Tests Reviewed-by: Enrico Faulhaber Reviewed-by: Markus Zolliker From 33a7c56fa22dce60d1b8d9efaab61ae9cb10c2c5 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Wed, 25 May 2022 09:23:16 +0200 Subject: [PATCH 10/15] allow to convert numpy arrays to ArrayOf accept all sequences instead of just tuple / list + change Module.announceUpdate to convert value before comparing with previous one (comparing will not work with numpy arrays) Change-Id: I5eceef4297607107e2dde688af2833d3651a8775 Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/28525 Tested-by: Markus Zolliker Reviewed-by: Markus Zolliker --- secop/datatypes.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/secop/datatypes.py b/secop/datatypes.py index 3f5f3df..220a3a9 100644 --- a/secop/datatypes.py +++ b/secop/datatypes.py @@ -760,7 +760,7 @@ class ArrayOf(DataType): def __call__(self, value): """validate an external representation to an internal one""" - if isinstance(value, (tuple, list)): + try: # check number of elements if self.minlen is not None and len(value) < self.minlen: raise BadValueError( @@ -771,8 +771,9 @@ class ArrayOf(DataType): 'Array too big, holds at most %d elements!' % self.minlen) # apply subtype valiation to all elements and return as list return tuple(self.members(elem) for elem in value) - raise BadValueError( - 'Can not convert %s to ArrayOf DataType!' % repr(value)) + except TypeError: + raise BadValueError('%s can not be converted to ArrayOf DataType!' + % type(value).__name__) from None def export_value(self, value): """returns a python object fit for serialisation""" From 7b9c099321507900b556508d40002a0a54e597f2 Mon Sep 17 00:00:00 2001 From: Enrico Faulhaber Date: Mon, 1 Aug 2022 16:40:45 +0200 Subject: [PATCH 11/15] default unit to UTF8 Change-Id: Ic958346beb1a3b164c8d7b2826d59cf7e3991e15 Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/28946 Tested-by: Jenkins Automated Tests Tested-by: Enrico Faulhaber Reviewed-by: Markus Zolliker Reviewed-by: Enrico Faulhaber --- secop/datatypes.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/secop/datatypes.py b/secop/datatypes.py index 220a3a9..5da9a99 100644 --- a/secop/datatypes.py +++ b/secop/datatypes.py @@ -130,10 +130,11 @@ class Stub(DataType): this workaround because datatypes need properties with datatypes defined later """ - def __init__(self, datatype_name, *args): + def __init__(self, datatype_name, *args, **kwds): super().__init__() self.name = datatype_name self.args = args + self.kwds = kwds def __call__(self, value): """validate""" @@ -151,7 +152,7 @@ class Stub(DataType): for prop in dtcls.propertyDict.values(): stub = prop.datatype if isinstance(stub, cls): - prop.datatype = globals()[stub.name](*stub.args) + prop.datatype = globals()[stub.name](*stub.args, **stub.kwds) # SECoP types: @@ -165,7 +166,7 @@ class FloatRange(DataType): """ 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) - unit = Property('physical unit', Stub('StringType'), extname='unit', default='') + unit = Property('physical unit', Stub('StringType', isUTF8=True), extname='unit', default='') fmtstr = Property('format string', Stub('StringType'), extname='fmtstr', default='%g') absolute_resolution = Property('absolute resolution', Stub('FloatRange', 0), extname='absolute_resolution', default=0.0) @@ -343,7 +344,7 @@ class ScaledInteger(DataType): scale = Property('scale factor', FloatRange(sys.float_info.min), extname='scale', mandatory=True) min = Property('low limit', FloatRange(), extname='min', mandatory=True) max = Property('high limit', FloatRange(), extname='max', mandatory=True) - unit = Property('physical unit', Stub('StringType'), extname='unit', default='') + unit = Property('physical unit', Stub('StringType', isUTF8=True), extname='unit', default='') fmtstr = Property('format string', Stub('StringType'), extname='fmtstr', default='%g') absolute_resolution = Property('absolute resolution', FloatRange(0), extname='absolute_resolution', default=0.0) From 21bcc7ce981c954fa24282396abfe683b0b3875f Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Wed, 1 Jun 2022 16:19:27 +0200 Subject: [PATCH 12/15] an enum with value 0 should be interpreted as False for example: bool(Enum(off=0, on=1)('off')) is False Change-Id: Ieb200b4ecf0eed50b657ecc00f73a69810ad828f Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/28586 Tested-by: Jenkins Automated Tests Reviewed-by: Enrico Faulhaber Reviewed-by: Markus Zolliker --- secop/lib/enum.py | 11 +++++++---- test/test_lib_enum.py | 6 ++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/secop/lib/enum.py b/secop/lib/enum.py index 0fb0b38..2bdcf1c 100644 --- a/secop/lib/enum.py +++ b/secop/lib/enum.py @@ -106,6 +106,9 @@ class EnumMember: def __repr__(self): return '<%s%s (%d)>' % (self.enum.name + '.' if self.enum.name else '', self.name, self.value) + def __bool__(self): + return bool(self.value) + # numeric operations: delegate to int. Do we really need any of those? def __add__(self, other): return self.value.__add__(other.value if isinstance(other, EnumMember) else other) @@ -242,7 +245,7 @@ class Enum(dict): name = '' def __init__(self, name='', parent=None, **kwds): - super(Enum, self).__init__() + super().__init__() if isinstance(name, (dict, Enum)) and parent is None: # swap if only parent is given as positional argument name, parent = '', name @@ -309,17 +312,17 @@ class Enum(dict): try: return self[key] except KeyError as e: - raise AttributeError(str(e)) + raise AttributeError(str(e)) from None def __setattr__(self, key, value): if self.name and key != 'name': raise TypeError('Enum %r can not be changed!' % self.name) - super(Enum, self).__setattr__(key, value) + super().__setattr__(key, value) def __setitem__(self, key, value): if self.name: raise TypeError('Enum %r can not be changed!' % self.name) - super(Enum, self).__setitem__(key, value) + super().__setitem__(key, value) def __delitem__(self, key): raise TypeError('Enum %r can not be changed!' % self.name) diff --git a/test/test_lib_enum.py b/test/test_lib_enum.py index 97228e1..7077834 100644 --- a/test/test_lib_enum.py +++ b/test/test_lib_enum.py @@ -77,3 +77,9 @@ def test_Enum(): assert e3.c >= e2.a assert e3.b <= e2.b assert Enum({'self': 0, 'other': 1})('self') == 0 + + +def test_Enum_bool(): + e = Enum('OffOn', off=0, on=1) + assert bool(e(0)) is False + assert bool(e(1)) is True From db43a77614630a6c26c964b235c36876eb316a86 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Tue, 16 Aug 2022 17:17:54 +0200 Subject: [PATCH 13/15] remove some log.debug statements in order to sync with frappy@mlz Change-Id: Iab89cb313fb6969c91663b8ebc56bdcb96ed2dd1 --- secop/lib/statemachine.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/secop/lib/statemachine.py b/secop/lib/statemachine.py index 6ed94c6..309cabf 100644 --- a/secop/lib/statemachine.py +++ b/secop/lib/statemachine.py @@ -186,7 +186,6 @@ class StateMachine: ret = self.state(self) self.init = False if self.stopped: - self.log.debug('%r', self.stopped) self.last_error = self.stopped self.cleanup(self) self.stopped = False @@ -269,7 +268,6 @@ class StateMachine: self.stopped = Restart with self._lock: # wait for running cycle finished if self.stopped: # cleanup is not yet done - self.log.debug('restart') self.last_error = self.stopped self.cleanup(self) # ignore return state on restart self.stopped = False From aa0c0421c345fba19287a2f795426fb05e70a5a1 Mon Sep 17 00:00:00 2001 From: boa Date: Fri, 19 Aug 2022 12:43:36 +0200 Subject: [PATCH 14/15] add Be-Filter addon for boa --- cfg/addons/be-filter-boa.cfg | 14 ++++++++++++++ cfg/sea/be-filter-boa.addon.json | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 cfg/addons/be-filter-boa.cfg create mode 100644 cfg/sea/be-filter-boa.addon.json diff --git a/cfg/addons/be-filter-boa.cfg b/cfg/addons/be-filter-boa.cfg new file mode 100644 index 0000000..304c4be --- /dev/null +++ b/cfg/addons/be-filter-boa.cfg @@ -0,0 +1,14 @@ +[NODE] +description = CryoTel be-filter BOA +id = be-filter-boa.addon.sea.psi.ch + +[sea_addons] +class = secop_psi.sea.SeaClient +description = addons sea connection for be-filter-boa.addon +config = be-filter-boa.addon +service = addons + +[befilter] +class = secop_psi.sea.SeaReadable +iodev = sea_addons +sea_object = befilter diff --git a/cfg/sea/be-filter-boa.addon.json b/cfg/sea/be-filter-boa.addon.json new file mode 100644 index 0000000..c170c4d --- /dev/null +++ b/cfg/sea/be-filter-boa.addon.json @@ -0,0 +1,19 @@ +{"befilter": {"base": "/befilter", "params": [ +{"path": "", "type": "float", "kids": 12}, +{"path": "send", "type": "text", "readonly": false, "cmd": "befilter send", "visibility": 3}, +{"path": "status", "type": "text", "visibility": 3}, +{"path": "cool", "type": "enum", "enum": {"on": 0, "off": 1}, "readonly": false, "cmd": "befilter cool"}, +{"path": "control", "type": "enum", "enum": {"auto_power": 1, "manual_power": 0, "controlled_T": 2}, "readonly": false, "cmd": "befilter control", "description": "recommended mode: auto_power, use coolpower or holdpower depending on T"}, +{"path": "set", "type": "float", "readonly": false, "cmd": "befilter set"}, +{"path": "setpower", "type": "float", "readonly": false, "cmd": "befilter setpower", "visibility": 3}, +{"path": "coolpower", "type": "float", "readonly": false, "cmd": "befilter coolpower", "visibility": 3}, +{"path": "holdpower", "type": "float", "readonly": false, "cmd": "befilter holdpower", "visibility": 3}, +{"path": "cool_threshold", "type": "float", "readonly": false, "cmd": "befilter cool_threshold", "description": "switch to coolpower above this value", "visibility": 3}, +{"path": "hold_threshold", "type": "float", "readonly": false, "cmd": "befilter hold_threshold", "description": "switch to holdpower below this value", "visibility": 3}, +{"path": "power", "type": "float"}, +{"path": "filter", "type": "none", "kids": 5}, +{"path": "filter/period", "type": "float", "readonly": false, "cmd": "befilter filter/period", "description": "oszillation period / sec"}, +{"path": "filter/amplitude", "type": "float", "readonly": false, "cmd": "befilter filter/amplitude", "description": "oszillation amplitude / K (+/-)"}, +{"path": "filter/precision", "type": "float", "readonly": false, "cmd": "befilter filter/precision"}, +{"path": "filter/raw", "type": "float"}, +{"path": "filter/intdif", "type": "float"}]}} From 292d028fe6e14d1bd2a986accc2777e5fd0affb2 Mon Sep 17 00:00:00 2001 From: dmc Date: Mon, 22 Aug 2022 15:55:58 +0200 Subject: [PATCH 15/15] add ill5 p sample stick --- cfg/stick/ill5p.cfg | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 cfg/stick/ill5p.cfg diff --git a/cfg/stick/ill5p.cfg b/cfg/stick/ill5p.cfg new file mode 100644 index 0000000..12f04ee --- /dev/null +++ b/cfg/stick/ill5p.cfg @@ -0,0 +1,16 @@ +[NODE] +description = ILL5 sample stick for pressure cells +id = ill5p.stick.sea.psi.ch + +[sea_stick] +class = secop_psi.sea.SeaClient +description = stick SEA connection to ill5p.stick +config = ill5p.stick +service = stick + +[ts] +class = secop_psi.sea.SeaReadable +io = sea_stick +sea_object = tt +json_file = ill5.config.json +rel_paths = ts