[WIP] dil5 improvements

Change-Id: I2b439bf5898601e10448511479bc67afa3edb4d3
This commit is contained in:
2025-06-05 10:15:57 +02:00
parent 04f7f6ece5
commit 472ae3f04d
3 changed files with 687 additions and 459 deletions

407
frappy_psi/dilution.py Normal file
View File

@ -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 <andrea.plank@psi.ch>
#
# *****************************************************************************
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()

View File

@ -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 <andrea.plank@psi.ch>
#
# *****************************************************************************
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)

View File

@ -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,12 +37,11 @@ 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:
if monotonic() < 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)
@ -51,15 +50,14 @@ class IO(Communicator):
except Exception:
pass
finally:
sys.stderr = prev_stderr
sys.stderr = sys.stdout
self._plc = None
self._last_try = time.time()
self._last_try = monotonic()
raise CommunicationFailedError('logo PLC not reachable')
def communicate(self, cmd):
with self._lock:
if not self._plc:
self._init()
cmd = cmd.split(maxsplit=1)
if len(cmd) == 2:
@ -79,22 +77,165 @@ class IO(Communicator):
raise
class Snap7Mixin(HasIO):
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 Pressure(Snap7Mixin, Readable):
vm_address = Property('VM address', datatype= StringType())
value = Parameter('pressure', datatype = FloatRange(unit = 'mbar'))
#pollinterval = 0.5
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()
def read_value(self):
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, ''
# 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)
@ -103,11 +244,11 @@ class Pressure(Snap7Mixin, Readable):
return IDLE, ''
class Airpressure(Snap7Mixin, Readable):
vm_address = Property('VM address', datatype= StringType())
value = Parameter('airpressure state', datatype = BoolType())
class Airpressure(LogoMixin, Readable):
vm_address = Property('VM address', datatype=StringType())
value = Parameter('airpressure state', datatype=BoolType())
#pollinterval = 0.5
# pollinterval = 0.5
def read_value(self):
if (self.get_vm_value(self.vm_address) > 500):
@ -118,12 +259,13 @@ class Airpressure(Snap7Mixin, Readable):
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())
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):
@ -142,7 +284,7 @@ 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)
@ -152,11 +294,12 @@ class Valve(Snap7Mixin, Drivable):
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)
@ -167,10 +310,10 @@ class FluidMachines(Snap7Mixin, Drivable):
def read_status(self):
return IDLE, ''
class TempSensor(Snap7Mixin, Readable):
vm_address = Property('VM address', datatype= StringType())
value = Parameter('resistance', datatype = FloatRange(unit = 'Ohm'))
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)
@ -178,12 +321,13 @@ class TempSensor(Snap7Mixin, Readable):
def read_status(self):
return IDLE, ''
class HeaterParam(Snap7Mixin, Writable):
vm_address = Property('VM address output', datatype= StringType())
target = Parameter('Heater target', datatype = IntRange())
class HeaterParam(LogoMixin, Writable):
vm_address = Property('VM address output', datatype=StringType())
value = Parameter('Heater Param', datatype = IntRange())
target = Parameter('Heater target', datatype=IntRange())
value = Parameter('Heater Param', datatype=IntRange())
def read_value(self):
return self.get_vm_value(self.vm_address)
@ -195,13 +339,12 @@ class HeaterParam(Snap7Mixin, Writable):
return IDLE, ''
class controlHeater(Snap7Mixin, Writable):
class controlHeater(LogoMixin, Writable):
vm_address = Property('VM address on switch', datatype=StringType())
vm_address = Property('VM address on switch', datatype= StringType())
target = Parameter('Heater state', datatype=BoolType())
target = Parameter('Heater state', datatype = BoolType())
value = Parameter('Heater state', datatype = BoolType())
value = Parameter('Heater state', datatype=BoolType())
def read_value(self):
return self.get_vm_value(self.vm_address_on)
@ -216,11 +359,12 @@ class controlHeater(Snap7Mixin, Writable):
return IDLE, ''
class safetyfeatureState(Snap7Mixin, Readable):
vm_address = Property('VM address state', datatype= StringType())
value = Parameter('safety Feature state', datatype = BoolType())
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)
@ -229,12 +373,12 @@ class safetyfeatureState(Snap7Mixin, Readable):
return IDLE, ''
class safetyfeatureParam(Snap7Mixin, Writable):
vm_address = Property('VM address output', datatype= StringType())
class safetyfeatureParam(LogoMixin, Writable):
vm_address = Property('VM address output', datatype=StringType())
target = Parameter('safety Feature target', datatype = IntRange())
target = Parameter('safety Feature target', datatype=IntRange())
value = Parameter('safety Feature Param', datatype = IntRange())
value = Parameter('safety Feature Param', datatype=IntRange())
def read_value(self):
return self.get_vm_value(self.vm_address)
@ -246,12 +390,12 @@ class safetyfeatureParam(Snap7Mixin, Writable):
return IDLE, ''
class comparatorgekoppeltParam(Snap7Mixin, Writable):
vm_address_1 = Property('VM address output', datatype= StringType())
vm_address_2 = Property('VM address output', datatype= StringType())
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())
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)
@ -263,6 +407,3 @@ class comparatorgekoppeltParam(Snap7Mixin, Writable):
def read_status(self):
return IDLE, ''