"""Application assembly: wiring datastore, registers, watchdog, node, server.""" from __future__ import annotations import asyncio from dataclasses import dataclass from .datastore import WagoDatastore from .modules import ModuleType, VirtualNode from .registers import ConfigRegisterBank from .server import WagoSimServer from .stats import ModbusStatistics from .watchdog import ModbusWatchdog @dataclass class SimulatorApp: """Fully wired simulator instance for one virtual node.""" datastore: WagoDatastore node: VirtualNode stats: ModbusStatistics watchdog: ModbusWatchdog registers: ConfigRegisterBank server: WagoSimServer @property def modules(self) -> list[ModuleType]: return self.node.modules def build_app( *, modules: list[ModuleType], host: str = "0.0.0.0", port: int = 502, mac_id: tuple[int, int, int, int, int, int] = (0x00, 0x30, 0xDE, 0x11, 0x22, 0x33), firmware_description: str = "WAGO 750-362 Simulator", ) -> SimulatorApp: """Assemble all simulator components for a node configuration.""" stats = ModbusStatistics() server_ref: dict[str, WagoSimServer] = {} def on_expire() -> None: datastore.clear_outputs() stats.slave_device_failure += 1 def on_connections_close() -> None: """Watchdog 0x1009=1: close TCP connections (from sync context). Called by the watchdog (possibly from its tick task). Schedules the async close on the running event loop when one exists. """ server = server_ref.get("server") if server is None: return try: loop = asyncio.get_running_loop() except RuntimeError: return loop.create_task(server.close_all_connections()) watchdog = ModbusWatchdog( on_expire=on_expire, on_connections_close=on_connections_close, ) datastore = WagoDatastore(config_bank=None) # type: ignore[arg-type] registers = ConfigRegisterBank( statistics=stats, watchdog=watchdog, tcp_connection_counter=lambda: server.connection_count, mac_id=mac_id, firmware_description=firmware_description, on_software_reset=lambda: datastore.clear_outputs(), on_kbus_reset=lambda: None, ) datastore._config = registers # late binding to break the cycle node = VirtualNode(datastore, modules) registers.set_process_image_bits( analog_in=node.analog_in_bits, analog_out=node.analog_out_bits, digital_in=node.digital_in_bits, digital_out=node.digital_out_bits, ) registers.set_module_description(node.description()) server = WagoSimServer( datastore, stats, watchdog, host=host, port=port, response_delay_provider=lambda: registers.response_delay_ms, idle_timeout_provider=lambda: registers.idle_timeout_ms, ) server_ref: dict[str, WagoSimServer] = {"server": server} return SimulatorApp( datastore=datastore, node=node, stats=stats, watchdog=watchdog, registers=registers, server=server, )