🎉 Initial commit

This commit is contained in:
Jonas Bengtsson
2026-10-03 09:46:11 +02:00
commit 973d619cee
34 changed files with 5787 additions and 0 deletions
+125
View File
@@ -0,0 +1,125 @@
"""Command-line interface: run a node configuration and a scenario.
Examples::
wago-sim serve --node config/pump_node.yaml --port 502
wago-sim run --node config/pump_node.yaml --scenario scenarios/pump_station_fat.yaml
wago-sim validate --scenario scenarios/pump_station_fat.yaml
"""
from __future__ import annotations
import argparse
import asyncio
import inspect
import logging
import sys
from pathlib import Path
from .app import build_app
from .scenario.dsl import load_node_modules, load_yaml, validate_scenario
from .scenario.engine import ScenarioEngine
from .scenario.report import result_summary, write_csv, write_jsonl
def _build_logger(verbose: bool) -> None:
logging.basicConfig(
level=logging.DEBUG if verbose else logging.INFO,
format="%(asctime)s %(levelname)-7s %(name)s: %(message)s",
)
async def _serve(args: argparse.Namespace) -> int:
node_config = load_yaml(args.node)
modules = load_node_modules(node_config)
app = build_app(modules=modules, host=args.host, port=args.port)
await app.server.start()
print(f"Simulating WAGO 750-362 with {len(modules)} modules "
f"on {args.host}:{args.port} (Ctrl+C to stop)")
try:
await asyncio.Event().wait()
except asyncio.CancelledError:
pass
finally:
await app.server.stop()
return 0
def _run_scenario(args: argparse.Namespace) -> int:
node_config = load_yaml(args.node)
modules = load_node_modules(node_config)
scenario = load_yaml(args.scenario)
problems = validate_scenario(scenario)
if problems:
print("Scenario validation problems:", file=sys.stderr)
for p in problems:
print(f" - {p}", file=sys.stderr)
return 2
app = build_app(modules=modules)
engine = ScenarioEngine(app.node, scenario)
engine.start()
result = None
max_ticks = 2_000_000
ticks = 0
while result is None and ticks < max_ticks:
result = engine.pump()
ticks += 1
if result is None:
print("Scenario did not terminate (tick limit reached)", file=sys.stderr)
return 3
print(result_summary(result))
if args.report_jsonl:
write_jsonl(result, args.report_jsonl)
if args.report_csv:
write_csv(result, args.report_csv)
return 0 if result.passed else 1
def _validate(args: argparse.Namespace) -> int:
scenario = load_yaml(args.scenario)
problems = validate_scenario(scenario)
if problems:
print("Scenario validation problems:")
for p in problems:
print(f" - {p}")
return 2
print("OK")
return 0
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
prog="wago-sim",
description="WAGO 750-362 Modbus TCP simulator for PLC FAT testing",
)
parser.add_argument("-v", "--verbose", action="store_true")
sub = parser.add_subparsers(dest="command", required=True)
serve = sub.add_parser("serve", help="run the Modbus TCP server")
serve.add_argument("--node", required=True, help="node config YAML")
serve.add_argument("--host", default="0.0.0.0")
serve.add_argument("--port", type=int, default=502)
serve.set_defaults(func=_serve)
runp = sub.add_parser("run", help="run a scenario headless (no server)")
runp.add_argument("--node", required=True, help="node config YAML")
runp.add_argument("--scenario", required=True, help="scenario YAML")
runp.add_argument("--report-jsonl", default=None)
runp.add_argument("--report-csv", default=None)
runp.set_defaults(func=_run_scenario)
val = sub.add_parser("validate", help="validate a scenario file")
val.add_argument("--scenario", required=True)
val.set_defaults(func=_validate)
args = parser.parse_args(argv)
_build_logger(args.verbose)
if inspect.iscoroutinefunction(args.func):
return asyncio.run(args.func(args))
return int(args.func(args))
if __name__ == "__main__":
raise SystemExit(main())