"""Example plant/PLC model plugin for the pump-station FAT scenario. In a real FAT bench the PLC under test drives the outputs. For a headless demo (or pre-FAT scenario smoke tests) this plugin emulates the expected PLC behavior so the scenario can PASS end-to-end. Register it in a scenario via: plugins: [plugins.pump_plant.pump_plant] The engine calls it once per tick. """ from __future__ import annotations # latch state per engine instance (module-level dict keyed by id) _LATCH = {} def pump_plant(engine, value) -> None: """Emulated PLC with classic start/stop latch circuit: - rising edge on start_button latches the pump ON - e-stop OFF breaks the latch (and the safe state follows) - valve follows the pump; speed setpoint follows level above 4000 """ key = id(engine) start = bool(engine.alias_value("start_button")) em_stop = bool(engine.alias_value("em_stop_ok")) level = float(engine.alias_value("level_sensor")) # rising edge of the start button sets the latch; e-stop clears it if engine.consume_rising_edge("start_button"): _LATCH[key] = True if not em_stop: _LATCH[key] = False pump_on = _LATCH.get(key, False) and em_stop speed = int(max(0.0, (level - 4000.0) * 10.0)) if pump_on else 0 _set_do(engine, "pump_relay", pump_on) _set_do(engine, "valve_open", pump_on) _set_ao(engine, "speed_setpoint", speed) def _set_do(engine, alias_name: str, value: bool) -> None: """Emulate a Modbus master writing the output coil. Uses the engine's global alias location: ``loc.bit`` is the 0-based global digital channel index == the Modbus bit address (Table 122). """ loc = engine.alias_location(alias_name) result = engine.datastore.write_bits(loc.bit, [bool(value)]) assert not isinstance(result, Exception), result def _set_ao(engine, alias_name: str, value: int) -> None: """Emulate a Modbus master writing the output register. ``loc.word`` is the global analog output word index == the Modbus word address in output area 1 (0x0000-0x00FF). """ alias = engine._aliases[alias_name] loc = engine.alias_location(alias_name) raw = int(value * alias.scale + alias.offset) & 0xFFFF result = engine.datastore.write_words(loc.word, [raw]) assert not isinstance(result, Exception), result