107 lines
3.1 KiB
Python
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,
|
|
) |