Files
wago-modbustcp-simulator-py/src/wago_sim/app.py
T
2026-10-03 09:46:11 +02:00

107 lines
3.1 KiB
Python

"""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,
)