diff --git a/frappy_psi/dilution.py b/frappy_psi/dilution.py new file mode 100644 index 0000000..11f78da --- /dev/null +++ b/frappy_psi/dilution.py @@ -0,0 +1,407 @@ +# ***************************************************************************** +# +# 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: +# Andrea Plank +# +# ***************************************************************************** + +import time +from frappy.core import Readable, Drivable, Parameter, Attached, FloatRange, \ + Command, IDLE, BUSY, WARN, ERROR, Property +from frappy.datatypes import EnumType, IntRange, BoolType, StructOf, StringType +from frappy.states import Retry, Finish, status_code, HasStates +from frappy.lib.enum import Enum +from frappy.errors import ImpossibleError, HardwareError +from frappy.addrparam import AddrParam, AddrMixin +from frappy.lib import formatStatusBits +from frappy.persistent import PersistentMixin, PersistentParam +from frappy_psi.logo import LogoMixin, DigitalActuator + +T = Enum( # target states + off = 0, + sorbpumped = 2, + condense = 5, + remove = 7, + remove_and_sorbpump = 9, + remove_and_condense = 10, + manual = 11, + test = 12, + ) + +V = Enum(T, # value status inherits from target status + sorbpumping=1, + condensing=4, + circulating=6, + removing=8, + ) + + +class Dilution(HasStates, Drivable): + condenseline_pressure = Attached() + condense_valve = Attached() + dump_valve = Attached() + + circulate_pump = Attached() + compressor = Attached(mandatory=False) + turbopump = Attached(mandatory=False) + condenseline_valve = Attached() + circuitshort_valve = Attached() + still_pressure = Attached() + + value = Parameter('current state', EnumType(T), default=0) + target = Parameter('target state', EnumType(T), default=0) + + sorbpumped = Parameter('sorb pump done', BoolType(), default=False) + + #ls372 = Attached() + V5 = Attached() #Name noch ändern!!! + p1 = Attached() #Name noch ändern!!! + + condensing_p_low = Parameter('Lower limit for condenseline pressure', FloatRange(unit='mbar')) + condensing_p_high = Parameter('Higher limit for condenseline pressure', FloatRange(unit='mbar')) + dump_target = Parameter('low dump pressure limit indicating end of condensation phase', + FloatRange(unit='mbar'), default=20) + end_condense_pressure = Parameter('low condense pressure indicating end of condensation phase', + FloatRange(unit='mbar'), default=500) + turbo_condense_pressure = Parameter('low condense pressure before turbo start', + FloatRange(unit='mbar'), default=900) + turbo_still_pressure = Parameter('low still pressure before turbo start', + FloatRange(unit='mbar'), default=10) + turbo_off_delay = Parameter('wait time after switching turbo off', + FloatRange(unit='s'), default=300) + turbo_off_speed = Parameter('speed to wait for after switching turbo off', + FloatRange(unit='s'), default=60) + end_remove_still_pressure = Parameter('pressure reached before end of remove', + FloatRange(unit='mbar'), default=1e-4) + st = StringType() + valve_set = StructOf(close=st, open=st, check_open=st, check_closed=st) + condense_valves = Parameter('valve to act when condensing', valve_set) + valves_after_remove = Parameter('valve to act after remove', valve_set) + check_after_remove = Parameter('check for manual valves after remove', valve_set) + _start_time = 0 + init = True + _warn_manual_work = None + + def write_target(self, target): + """ + if (target == Targetstates.SORBPUMP): + if self.value == target: + return self.target + self.start_machine(self.sorbpump) + self.value = Targetstates.SORBPUMP + return self.value + """ + if self.value == self.target: + return target # not sure if this is correct. may be a step wants to be repeated? + + self.start_machine(getattr(self, target.name, None)) + return target + + """ + @status_code(BUSY, 'sorbpump state') + def sorbpump(self, state): + #Heizt Tsorb auf und wartet ab. + if self.init: + self.ls372.write_target(40) #Setze Tsorb auf 40K + self.start_time = self.now + self.init = false + return Retry + + if self.now - self.start_time < 2400: # 40 Minuten warten + return Retry + + self.ls372.write_target(0) + + if self.ls372.read_value() > 10: # Warten bis Tsorb unter 10K + return Retry + + return self.condense + """ + + @status_code(BUSY, 'start test') + def test(self, state): + """Nur zum testen, ob UI funktioniert""" + self.init = False + if state.init: + state._start = state.now + return self.wait_test + + @status_code(BUSY) + def wait_test(self, state): + if state.now < state.start + 20: + return Retry + return self.final_status(IDLE, 'end test') + + @status_code(BUSY) + def condense(self, state): + """Führt das Kondensationsverfahren durch.""" + if state.init: + self.value = V.condensing + self.handle_valves(**self.condense_valves) + return Retry + if self.wait_valves(): + return Retry + self.check_valve_result() + return Retry + + @status_code(BUSY) + def condensing(self, state): + if self.condenseline_pressure.read_value() < self.condensing_p_low: + self.condense_valve.write_target(1) + elif self.condenseline_pressure.read_value() > self.condensing_p_high: + self.condense_valve.write_target(0) + + if self.p1.read_value() > self.dump_target: + return Retry + + self.condense_valve.write_target(1) + + if self.turbopump is not None: + return self.condense_wait_before_turbo_start + + return self.wait_for_condense_line_pressure + + @status_code(BUSY) + def wait_for_condense_line_pressure(self, state): + if self.condenseline_pressure.read_value() > self.end_condense_pressure: + return Retry + self.condense_valve.write_target(0) + return self.circulate + + @status_code(BUSY, 'condense (wait before starting turbo)') + def condense_wait_before_turbo_start(self, state): + if (self.condenseline_pressure.read_value() > self.turbo_condense_pressure + and self.still_pressure.read_value() > self.turbo_still_pressure): + return Retry + self.turbopump.write_target(1) + return self.wait_for_condense_line_pressure + + @status_code(BUSY) + def circulate(self, state): + """Zirkuliert die Mischung.""" + if state.init: + self.handle_valves(**self.condense_valves) + if self.wait_valves(): + return Retry + self.check_valve_result() + self.value = V.circulating + return Finish + + @status_code(BUSY, 'remove (wait for turbo shut down)') + def remove(self, state): + """Entfernt die Mischung.""" + + if state.init: + self.condenseline_valve.write_target(0) + self.dump_valve.write_target(1) + if self.turbopump is not None: + self._start_time = state.now + self.turbopump.write_target(0) + return Retry + + if self.turbopump is not None: + self.turbopump.write_target(0) + + if (state.now - self._start_time < self.turbo_off_delay + or self.turbopump.read_speed() > self.turbo_off_speed): + return Retry + + self.circuitshort_valve.write_target(1) + + if self.turbopump is not None: + return self.remove_wait_for_still_pressure + + return self.remove_endsequence + + @status_code(BUSY, 'remove (wait for still pressure low)') + def remove_wait_for_still_pressure(self, state): + if self.still_pressure.read_value() > self.turbo_still_pressure: + return Retry + self.turbopump.write_target(1) + return self.remove_endsequence + + @status_code(BUSY) + def remove_endsequence(self, state): + if self.still_pressure.read_value() > self.end_remove_still_pressure: + return Retry + self.circuitshort_valve.write_target(0) + self.dump_valve.write_target(0) + + if self.compressor is not None: + self.compressor.write_target(0) + self.circulate_pump.write_target(0) + return self.close_valves_after_remove + + @status_code(BUSY) + def close_valves_after_remove(self, state): + if state.init: + self.handle_valves(**self.valves_after_remove) + if self.wait_valves(): + return Retry + self.check_valve_result() + self._warn_manual_work = True + return self.final_status(WARN, 'please check manual valves') + + def read_status(self): + status = super().read_status() + if status[0] < ERROR and self._warn_manual_work: + try: + self.handle_valves(**self.check_after_remove) + self._warn_manual_work = False + except ImpossibleError: + return WARN, f'please close manual valves {",".join(self._valves_failed[False])}' + return status + + def handle_valves(self, check_closed=(), check_open=(), close=(), open=()): + """check ot set given valves + + raises ImpossibleError, when checks fails + """ + self._valves_to_wait_for = {} + self._valves_failed = {True: [], False: []} + for flag, valves in enumerate([check_closed, check_open]): + for vname in valves.split(): + if self.secNode.modules[vname].read_value() != flag: + self._valves_failed[flag].append(vname) + for flag, valves in enumerate([close, open]): + for vname in valves.split(): + valve = self.secNode.modules[vname] + valve.write_target(flag) + if valve.isBusy(): + self._valves_to_wait_for[vname] = (valve, flag) + elif valve.read_value() != flag: + self._valves_failed[flag].append(vname) + + def wait_valves(self): + busy = False + for vname, (valve, flag) in dict(self._valves_to_wait_for.items()): + statuscode = valve.read_status()[0] + if statuscode == BUSY: + busy = True + continue + if valve.read_value() == flag and statuscode == IDLE: + self._valves_to_wait_for.pop(vname) + else: + self._valves_failed[flag].append(vname) + return busy + + def check_valve_result(self): + result = [] + for flag, valves in self._valves_failed.items(): + if valves: + result.append(f"{','.join(valves)} not {'open' if flag else 'closed'}") + if result: + raise ImpossibleError(f"failed: {', '.join(result)}") + + +class DIL5(Dilution): + condense_valves = { + 'close': 'V2 V4 V9', + 'check_closed': 'MV10 MV13 MV8 MVB MV2', + 'check_open': 'MV1 MV3a MV3b GV1 MV9 MV11 MV12 MV14', + 'open': 'V1 V5 compressor pump', + } + valves_after_remove = { + 'close': 'V1 V2 V4 V5 V9', + 'check_closed': 'MV10 MV13 MV8 MVB MV2', + 'open': '', + 'check_open': '', + } + check_after_remove = { + 'close': '', + 'check_closed': 'MV1 MV9 MV10 MV11 MV12', + 'open': '', + 'check_open': '', + } + + +class Interlock(LogoMixin, AddrMixin, Readable): + value = AddrParam('interlock state (bitmap)', + IntRange(0, 31), addr='V414', readonly=False) + p5lim = AddrParam('safety limit on p5 to protect forepump', + FloatRange(), value=1300, addr='VW16 VW18', readonly=False) + p2lim = AddrParam('safety limit on p2 to protect compressor', + FloatRange(), value=4000, addr='VW8 VW10', readonly=False) + p1lim = AddrParam('safety limit to protect dump', + FloatRange(), value=1300, addr='VW12 VW14', readonly=False) + p2max = AddrParam('limit pn p2 for mechanism to put mix to dump', + FloatRange(), value=3000, addr='VW20 VW22', readonly=False) + conditions = { # starting with bit 1 + 'off (p5>p5lim)': {'forepump': False}, + 'off (p2>p2lim)': {'compressor': False}, + 'off (p1>p2lim)': {'forepump': False, 'compressor': False}, + 'open (p2>p2max)': {'V4': True}} + + reset_param = Property('addr for reset', StringType(), default='V418.1') + _mismatch = None + _prefix = '' + + def doPoll(self): + self.read_status() # this includes read_value + + def initialReads(self): + super().initialReads() + self.reset() + + @Command + def reset(self): + """reset the interlock""" + self._prefix = '' + self.set_vm_value(self.reset_param, 1) + for actions in self.conditions.values(): + for mname in actions: + self.secNode.modules[mname].reset_fault() + if self.read_value() != 0: + raise HardwareError('can not clear status byte') + self.set_vm_value(self.reset_param, 0) + self.read_status() # update status (this may trigger ERROR again) + + def read_status(self): + if self._mismatch is None: # init + self._mismatch = set() + bits = self.read_value() + if bits: + keys = formatStatusBits(bits, self.conditions, 1) + statustext = [] + for key in keys: + actions = self.conditions[key] + statustext.append(f"{' and '.join(actions)} {key}") + for module, value in actions.items(): + modobj = self.secNode.modules[module] + if modobj.target != value: + self._prefix = 'switched ' + modobj.set_fault(value, f'switched {key}') + return ERROR, f"{self._prefix}{', '.join(statustext)}" + if self._mismatch: + return ERROR, f"mismatch on values for {', '.join(self._mismatch)}" + return IDLE, '' + + def addressed_read(self, pobj): + values = [self.get_vm_value(a) for a in pobj.addr.split()] + if any(v != values[0] for v in values): + self._mismatch.add(pobj.name) + self.read_status() + else: + self._mismatch.discard(pobj.name) + return values[0] + + def addressed_write(self, pobj, value): + for addr in pobj.addr.split(): + self.set_vm_value(addr, value) + self.read_status() + + diff --git a/frappy_psi/dilution_statemachine.py b/frappy_psi/dilution_statemachine.py deleted file mode 100644 index 342c9da..0000000 --- a/frappy_psi/dilution_statemachine.py +++ /dev/null @@ -1,320 +0,0 @@ -# ***************************************************************************** -# -# 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: -# Andrea Plank -# -# ***************************************************************************** - -from frappy.core import Drivable, Parameter, EnumType, Attached, FloatRange, \ - Command, IDLE, BUSY, WARN, ERROR, Property -from frappy.datatypes import StatusType, EnumType, ArrayOf, BoolType, IntRange -from frappy.states import StateMachine, Retry, Finish, status_code, HasStates -from frappy.lib.enum import Enum -from frappy.errors import ImpossibleError -import time -Targetstates = Enum( - SORBPUMP = 0, - CONDENSE = 1, - CIRCULATE = 2, - REMOVE = 3, - MANUAL = 4, - TEST = 5, - STOP = 6, -) - -class Dilution(HasStates, Drivable): - - condenseline_pressure = Attached() - condense_valve = Attached() - dump_valve = Attached() - - circulate_pump = Attached() - compressor = Attached(mandatory=False) - turbopump = Attached(mandatory=False) - condenseline_valve = Attached() - circuitshort_valve = Attached() - still_pressure = Attached() - #ls372 = Attached() - V5 = Attached() #Name noch ändern!!! - p1 = Attached() #Name noch ändern!!! - - condensing_p_low = Property('Lower limit for condenseline pressure', IntRange()) - - condensing_p_high = Property('Higher limit for condenseline pressure', IntRange()) - - target = Parameter('target state', EnumType(Targetstates)) - - value = Parameter('current state', EnumType(Targetstates)) - - init = True - - - - def write_target(self, target): - """ - if (target == Targetstates.SORBPUMP): - if self.value == target: - return self.target - self.start_machine(self.sorbpump) - self.value = Targetstates.SORBPUMP - return self.value - """ - if (target == Targetstates.TEST): - self.value = Targetstates.TEST - self.init = True - self.start_machine(self.test) - - if (target == Targetstates.REMOVE): - if self.value == target: - return target - if self.value != Teststates.CIRCULATE: - self.final_status(WARN, "state before is not circulate") - return self.value - self.value = Targetstates.REMOVE - self.init = True - self.start_machine(self.remove) - - elif (target == Targetstates.CIRCULATE): - if self.value == target: - return target - self.value = Targetstates.CIRCULATE - self.init = True - self.start_machine(self.circulate) - - elif (target == Targetstates.CONDENSE): - if self.value == target: - return target - self.value = Targetstates.CONDENSE - self.init = True - self.start_machine(self.condense) - - elif(target == Targetstates.MANUAL): - self.value = Targetstates.MANUAL - self.stop_machine() - - elif (target == Targetstates.STOP): - self.value = Targetstates.STOP - self.stop_machine() - return self.value - - """ - @status_code(BUSY, 'sorbpump state') - def sorbpump(self, state): - #Heizt Tsorb auf und wartet ab. - if self.init: - self.ls372.write_target(40) #Setze Tsorb auf 40K - self.start_time = self.now - self.init = false - return Retry - - if self.now - self.start_time < 2400: # 40 Minuten warten - return Retry - - self.ls372.write_target(0) - - if self.ls372.read_value() > 10: # Warten bis Tsorb unter 10K - return Retry - - return self.condense - """ - - @status_code(BUSY, 'test mode') - def test(self, state): - "Nur zum testen, ob UI funktioniert" - self.init = False - self.condense_valve.write_target(1) - time.sleep(1) - self.condense_valve.write_target(0) - self.dump_valve.write_target(1) - time.sleep(1) - self.dump_valve.write_target(0) - self.compressor.write_target(1) - return True - - @status_code(BUSY) - def wait_for_condense_line_pressure(self, state): - if (self.condenseline_pressure.read_value > 500): - return Retry - self.condense_valve.write_target(0) - return self.circulate - - def initialize_condense_valves(self, state): - raise NotImplementedError - - @status_code(BUSY) - def condense(self, state): - """Führt das Kondensationsverfahren durch.""" - if state.init: - self.initialize_condense_valves() - self.circuitshort_valve.write_target(0) - self.dump_valve.write_target(0) - self.condense_valve.write_target(0) - - self.condenseline_valve.write_target(1) - self.V5.write_target(1) - - if (self.compressor is not None): - self.compressor.write_target(1) - - self.circulate_pump.write_target(1) - return Retry - - if self.condenseline_pressure.read_value() < self.condensing_p_low: - self.condense_valve.write_target(1) - elif (self.condenseline_pressure.read_value() > self.condensing_p_high): - self.condense_valve.write_target(0) - - if (self.p1.read_value() > 20): - return Retry - - self.condense_valve.write_target(1) - - if (self.turbopump is not None): - return self.condense_wait_before_turbo_start - - return self.wait_for_condense_line_pressure - - @status_code(BUSY, 'condense (wait before starting turbo)') - def condense_wait_before_turbo_start(self, state): - if (self.condenseline_pressure.read_value() > 900 and self.still_pressure.read_value() > 10): - return Retry - else: - self.turbopump.write_target(1) - return self.wait_for_condense_line_pressure - - def initialize_circulation_valves(self, state): - raise NotImplementedError - - @status_code(BUSY) - def circulate(self, state): - """Zirkuliert die Mischung.""" - if state.init: - self.initialize_circulation_valves() - return Retry - - @status_code(BUSY, 'remove (wait for turbo shut down)') - def remove(self, state): - """Entfernt die Mischung.""" - - if state.init: - self.condenseline_valve.write_target(0) - self.dump_valve.write_target(1) - self.start_time = self.now - return Retry - - if self.turbopump is not None: - self.turbopump.write_target(0) - - if (self.now - self.start_time < 300 or self.turbopump.read_speed() > 60): - return Retry - - self.circuitshort_valve.write_target(1) - - if self.turbopump is not None: - return self.remove_wait_for_still_pressure - - return remove_endsequence - - @status_code(BUSY, 'remove (wait for still pressure low)') - def remove_wait_for_still_pressure(self, state): - if self.still_pressure.read_value() > 20: - return Retry - self.turbopump.write_target(1) - return self.remove_endsequence - - @status_code(BUSY) - def remove_endsequence(self, state): - if self.still_pressure.read_value() > 1e-4: - return Retry - self.circuitshort_valve.write_target(0) - self.dump_valve.write_target(0) - - if self.compressor is not None: - self.compressor.write_target(0) - self.remove_check_manual_valves() - self.remove_close_valves() - self.circulate_pump.write_target(0) - return Finish - - def remove_check_manual_valves(self): - raise NotImplementedError - - def remove_close_valves(self): - raise NotImplementedError - - -class DIL5(Dilution): - - MV10 = Attached() - MV13 = Attached() - MV8 = Attached() - MVB = Attached() - MV2 = Attached() - MV1 = Attached() - MV3a = Attached() - MV3b = Attached() - GV1 = Attached() - MV14 = Attached() - MV12 = Attached() - MV11 = Attached() - MV9 = Attached() - GV2 = Attached() - - def earlyInit(self): - self.circulate_closed_valves = [self.condense_valve, self.dump_valve, self.circuitshort_valve, self.MV10, self.MV13, self.MV8, self.MVB, self.MV2] - self.circulate_open_valves = [self.MV11, self.circulate_pump, self.GV2, self.V5, self.compressor, self.condenseline_valve, self.MV1, self.MV3a, self.MV3b, self.GV1, self.MV9, self.MV14] - self.condense_closed_valves = [self.MV10, self.MV13, self.MV8, self.MVB, self.MV2] - self.condense_open_valves = [self.MV1, self.MV3a, self.MV3b, self.GV1, self.MV9, self.MV14, self.MV12, self.MV11] - self.remove_check_closed_valves = [self.MV11, self.MV9, self.MV12, self.MV1] - self.remove_closed_valves = [self.condenseline_valve, self.circuitshort_valve, self.V5, self.condense_valve, self.dump_valve] - super().earlyInit() - - def initialize_condense_valves(self): - #Anfangszustand der Ventile überprüfen - for valve in self.condense_open_valves: - if valve.read_value() == 0: - self.stop_machine() - raise ImpossibleError(f'valve {valve.name} must be open') - - for valve in self.condense_closed_valves: - if valve.read_value == 1: - self.stop_machine() - return ImpossibleError(f'valve {valve.name} must be closed') - - def initialize_circulation_valves(self): - #Anfangszustand der Ventile überprüfen - self.value = Targetstates.CIRCULATE - for valve in self.circulate_closed_valves: - if (valve.read_value() == 1): - self.stop_machine() - raise ImpossibleError(f'valve {valve.name} must be open') - - for valve in self.circulate_open_valves: - if (valve.read_value() == 0): - valve.write_target(1) - self.stop_machine() - raise ImpossibleError(f'valve {valve.name} must be open') - - def remove_check_manual_valves(self): - for valve in self.remove_check_closed_valves: - if (valve.read_value() == 1): - self.final_status(WARN, "manual valve {valve.name} must be closed") - - def remove_close_valves(self): - for valve in self.remove_closed_valves: - valve.write_target(0) - diff --git a/frappy_psi/logo.py b/frappy_psi/logo.py index f770605..7e2726d 100644 --- a/frappy_psi/logo.py +++ b/frappy_psi/logo.py @@ -17,17 +17,17 @@ # # # ***************************************************************************** +import sys +from time import monotonic from ast import literal_eval import snap7 -from frappy.core import Readable, Parameter, FloatRange, HasIO, StringIO, Property, StringType,IDLE, BUSY, WARN, ERROR,Writable, Drivable, BoolType, IntRange, Communicator -from frappy.errors import CommunicationFailedError +from frappy.core import Attached, Command, Readable, Parameter, FloatRange, HasIO, Property, StringType, \ + IDLE, BUSY, WARN, ERROR, Writable, Drivable, BoolType, IntRange, Communicator, StatusType +from frappy.errors import CommunicationFailedError, ConfigError from threading import RLock -import sys -import time + class IO(Communicator): - - tcap_client = Property('tcap_client', IntRange()) tsap_server = Property('tcap_server', IntRange()) ip_address = Property('numeric ip address', StringType()) @@ -37,30 +37,28 @@ class IO(Communicator): def initModule(self): self._lock = RLock() super().initModule() + def _init(self): - if not self._plc: - if time.time() < self._last_try + 10: - raise CommunicationFailedError('logo PLC not reachable') - self._plc = snap7.logo.Logo() - prev_stderr = sys.stdout - sys.stderr = open('/dev/null', 'w') # suppress output of snap7 - try: - self._plc.connect(self.ip_address, self.tcap_client, self.tsap_server) - if self._plc.get_connected(): - return - except Exception: - pass - finally: - sys.stderr = prev_stderr - self._plc = None - self._last_try = time.time() - raise CommunicationFailedError('logo PLC not reachable') - - - + if monotonic() < self._last_try + 10: + raise CommunicationFailedError('logo PLC not reachable') + self._plc = snap7.logo.Logo() + sys.stderr = open('/dev/null', 'w') # suppress output of snap7 + try: + self._plc.connect(self.ip_address, self.tcap_client, self.tsap_server) + if self._plc.get_connected(): + return + except Exception: + pass + finally: + sys.stderr = sys.stdout + self._plc = None + self._last_try = monotonic() + raise CommunicationFailedError('logo PLC not reachable') + def communicate(self, cmd): with self._lock: - self._init() + if not self._plc: + self._init() cmd = cmd.split(maxsplit=1) if len(cmd) == 2: self.comLog('> %s %s', cmd[0], cmd[1]) @@ -76,59 +74,203 @@ class IO(Communicator): self.comLog('? %r', e) self.log.exception('error in plc read') self._plc = None - raise - - - -class Snap7Mixin(HasIO): + raise + + +class LogoMixin(HasIO): ioclass = IO - + def get_vm_value(self, vm_address): return literal_eval(self.io.communicate(vm_address)) - def set_vm_value(self, vm_address, value): - return literal_eval(self.io.communicate(f'{vm_address} {value}')) + return literal_eval(self.io.communicate(f'{vm_address} {round(value)}')) + + +class DigitalActuator(LogoMixin, Writable): + """output with or without feedback""" + output_addr = Property('VM address output', datatype=StringType(), default='') + target_addr = Property('VM address target', datatype=StringType(), default='') + feedback_addr = Property('VM address feedback', datatype=StringType(), default='') + target = Parameter('target', datatype=BoolType()) + value = Parameter('feedback or output', datatype=BoolType()) + _input = 'output' + _value_addr = None + _target_addr = None + _fault = None + + def doPoll(self): + self.read_status() # this calls also read_value + + def checkProperties(self): + super().checkProperties() + if self.feedback_addr: + self._input = 'feedback' + self._value_addr = self.feedback_addr + else: + self._input = 'output' + self._value_addr = self.output_addr + self._target_addr = self.target_addr or self.output_addr + if self._target_addr and self._value_addr: + self._check_feedback = self.feedback_addr and self.output_addr + return + raise ConfigError('need either output_addr or both feedback_addr and target_addr') + + def initialReads(self): + super().initialReads() + self.target = self.value + + def set_fault(self, value, statustext): + """on a fault condition, set target to value + + and status to statustext + """ + self.write_target(value) + self._fault = statustext + self.read_status() + + def reset_fault(self): + """reset fault condition""" + self._fault = None + self.read_status() -class Pressure(Snap7Mixin, Readable): - vm_address = Property('VM address', datatype= StringType()) - value = Parameter('pressure', datatype = FloatRange(unit = 'mbar')) - - #pollinterval = 0.5 - def read_value(self): - return self.get_vm_value(self.vm_address) - + return self.get_vm_value(self._value_addr) + + def write_target(self, target): + self._fault = None + self.set_vm_value(self._target_addr, target) + value = self.read_value() + if value != target and self.feedback_addr: + # retry only if we have a feedback and the feedback did not change yet + for i in range(20): + if self.read_value() == target: + self.log.debug('tried %d times', i) + break + self.set_vm_value(self._target_addr, target) + + def read_status(self): + if self._fault: + return ERROR, self._fault + value = self.read_value() + if value != self.target: + return ERROR, 'value and target do not match' + if self._check_feedback: + if value != self.get_vm_value(self._check_feedback): + return ERROR, f'feedback does not match output' + if self.feedback_addr: + return IDLE, 'feedback confirmed' + return IDLE, '' + + +class DelayedActuator(DigitalActuator, Drivable): + delay_addr = Property('address of delay value', StringType()) + _pulse_start = 0 + _pulse_end = 0 + _fault = None + + def read_status(self): + if self._fault: + return ERROR, self._fault + value = self.read_value() + fberror = None + if self._pulse_start: + now = monotonic() + if now < self._pulse_start + 1: + value = 1 + elif now < self._pulse_end - 1: + if not value: + self._pulse_start = 0 + return WARN, f'{self._input} is not on during pulse - due to interlock?' + if value: + if now < self._pulse_end + 1: + return BUSY, 'pulsing' + self.log.warn('pulse timeout') + self.set_vm_value(self._target_addr, 0) + self._pulse_start = 0 + self.set_vm_value(self.delay_addr, 0) + elif self._check_feedback and value != self.get_vm_value(self._check_feedback): + fberror = ERROR, f'feedback does not match output' + if value != self.target: + return ERROR, 'value does not match target' + self.setFastPoll(False) + if fberror: + return fberror + if self.feedback_addr: + return IDLE, 'feedback confirmed' + return IDLE, '' + + def write_target(self, value): + self._pulse_start = 0 + if not value: + self.set_vm_value(self.delay_addr, 0) + return super().write_target(value) + + @Command(argument=FloatRange(0)) + def pulse(self, delay): + """open for delay seconds""" + self.set_vm_value(self.delay_addr, delay) + self.set_vm_value(self._target_addr, 1) + self.set_vm_value(self._target_addr, 0) + self.setFastPoll(True, 0.5) + self.status = BUSY, 'pulsing' + now = monotonic() + self._pulse_start = now + self._pulse_end = now + delay + + +class Value(LogoMixin, Readable): + addr = Property('VM address', datatype=StringType()) + + def read_value(self): + return self.get_vm_value(self.addr) + def read_status(self): return IDLE, '' -class Airpressure(Snap7Mixin, Readable): - vm_address = Property('VM address', datatype= StringType()) - value = Parameter('airpressure state', datatype = BoolType()) - - #pollinterval = 0.5 - +# TODO: the following classes are too specific, they have to be moved + +class Pressure(LogoMixin, Drivable): + vm_address = Property('VM address', datatype=StringType()) + value = Parameter('pressure', datatype=FloatRange(unit='mbar')) + + # pollinterval = 0.5 + + def read_value(self): + return self.get_vm_value(self.vm_address) + + def read_status(self): + return IDLE, '' + + +class Airpressure(LogoMixin, Readable): + vm_address = Property('VM address', datatype=StringType()) + value = Parameter('airpressure state', datatype=BoolType()) + + # pollinterval = 0.5 + def read_value(self): if (self.get_vm_value(self.vm_address) > 500): return 1 else: return 0 - - def read_status(self): - return IDLE, '' - -class Valve(Snap7Mixin, Drivable): - vm_address_input = Property('VM address input', datatype= StringType()) - vm_address_output = Property('VM address output', datatype= StringType()) - target = Parameter('Valve target', datatype = BoolType()) - value = Parameter('Value state', datatype = BoolType()) + def read_status(self): + return IDLE, '' + + +class Valve(LogoMixin, Drivable): + vm_address_input = Property('VM address input', datatype=StringType()) + vm_address_output = Property('VM address output', datatype=StringType()) + + target = Parameter('Valve target', datatype=BoolType()) + value = Parameter('Value state', datatype=BoolType()) _remaining_tries = None - + def read_value(self): return self.get_vm_value(self.vm_address_input) - + def write_target(self, target): self.set_vm_value(self.vm_address_output, target) self._remaining_tries = 5 @@ -142,127 +284,126 @@ class Valve(Snap7Mixin, Drivable): if value != self.target: if self._remaining_tries is None: self.target = self.read_value() - return IDLE,'' + return IDLE, '' self._remaining_tries -= 1 if self._remaining_tries < 0: self.setFastPoll(False) return ERROR, 'too many tries to switch' - self.set_vm_value(self.vm_address_output, self.target) + self.set_vm_value(self.vm_address_output, self.target) return BUSY, 'switching (try again)' self.setFastPoll(False) return IDLE, '' - -class FluidMachines(Snap7Mixin, Drivable): - vm_address_output = Property('VM address output', datatype= StringType()) - target = Parameter('Valve target', datatype = BoolType()) - value = Parameter('Value state', datatype = BoolType()) - + +class FluidMachines(LogoMixin, Drivable): + vm_address_output = Property('VM address output', datatype=StringType()) + + target = Parameter('Valve target', datatype=BoolType()) + value = Parameter('Valve state', datatype=BoolType()) + def read_value(self): return self.get_vm_value(self.vm_address_output) - + def write_target(self, target): return self.set_vm_value(self.vm_address_output, target) - - def read_status(self): - return IDLE, '' - -class TempSensor(Snap7Mixin, Readable): - vm_address = Property('VM address', datatype= StringType()) - value = Parameter('resistance', datatype = FloatRange(unit = 'Ohm')) - - - def read_value(self): - return self.get_vm_value(self.vm_address) - + def read_status(self): return IDLE, '' -class HeaterParam(Snap7Mixin, Writable): - vm_address = Property('VM address output', datatype= StringType()) - - target = Parameter('Heater target', datatype = IntRange()) - - value = Parameter('Heater Param', datatype = IntRange()) - + +class TempSensor(LogoMixin, Readable): + vm_address = Property('VM address', datatype=StringType()) + value = Parameter('resistance', datatype=FloatRange(unit='Ohm')) + def read_value(self): return self.get_vm_value(self.vm_address) - + + def read_status(self): + return IDLE, '' + + +class HeaterParam(LogoMixin, Writable): + vm_address = Property('VM address output', datatype=StringType()) + + target = Parameter('Heater target', datatype=IntRange()) + + value = Parameter('Heater Param', datatype=IntRange()) + + def read_value(self): + return self.get_vm_value(self.vm_address) + def write_target(self, target): return self.set_vm_value(self.vm_address, target) - + def read_status(self): - return IDLE, '' - + return IDLE, '' -class controlHeater(Snap7Mixin, Writable): - - vm_address = Property('VM address on switch', datatype= StringType()) - target = Parameter('Heater state', datatype = BoolType()) - - value = Parameter('Heater state', datatype = BoolType()) - +class controlHeater(LogoMixin, Writable): + vm_address = Property('VM address on switch', datatype=StringType()) + + target = Parameter('Heater state', datatype=BoolType()) + + value = Parameter('Heater state', datatype=BoolType()) + def read_value(self): return self.get_vm_value(self.vm_address_on) - + def write_target(self, target): if (target): return self.set_vm_value(self.vm_address, True) else: return self.set_vm_value(self.vm_address, False) - + def read_status(self): - return IDLE, '' - - -class safetyfeatureState(Snap7Mixin, Readable): - - vm_address = Property('VM address state', datatype= StringType()) - - value = Parameter('safety Feature state', datatype = BoolType()) - - def read_value(self): - return self.get_vm_value(self.vm_address) - - def read_status(self): - return IDLE, '' + return IDLE, '' -class safetyfeatureParam(Snap7Mixin, Writable): - vm_address = Property('VM address output', datatype= StringType()) - - target = Parameter('safety Feature target', datatype = IntRange()) - - value = Parameter('safety Feature Param', datatype = IntRange()) - + + +class safetyfeatureState(LogoMixin, Readable): + vm_address = Property('VM address state', datatype=StringType()) + + value = Parameter('safety Feature state', datatype=BoolType()) + def read_value(self): return self.get_vm_value(self.vm_address) - + + def read_status(self): + return IDLE, '' + + +class safetyfeatureParam(LogoMixin, Writable): + vm_address = Property('VM address output', datatype=StringType()) + + target = Parameter('safety Feature target', datatype=IntRange()) + + value = Parameter('safety Feature Param', datatype=IntRange()) + + def read_value(self): + return self.get_vm_value(self.vm_address) + def write_target(self, target): return self.set_vm_value(self.vm_address, target) - + def read_status(self): - return IDLE, '' + return IDLE, '' -class comparatorgekoppeltParam(Snap7Mixin, Writable): - vm_address_1 = Property('VM address output', datatype= StringType()) - vm_address_2 = Property('VM address output', datatype= StringType()) - - target = Parameter('safety Feature target', datatype = IntRange()) - value = Parameter('safety Feature Param', datatype = IntRange()) - +class comparatorgekoppeltParam(LogoMixin, Writable): + vm_address_1 = Property('VM address output', datatype=StringType()) + vm_address_2 = Property('VM address output', datatype=StringType()) + + target = Parameter('safety Feature target', datatype=IntRange()) + value = Parameter('safety Feature Param', datatype=IntRange()) + def read_value(self): return self.get_vm_value(self.vm_address_1) - + def write_target(self, target): self.set_vm_value(self.vm_address_1, target) return self.set_vm_value(self.vm_address_2, target) - - def read_status(self): - return IDLE, '' - - - + def read_status(self): + return IDLE, '' +