265 lines
8.5 KiB
Python
265 lines
8.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Debug tool for semantic validation issues.
|
|
|
|
Prints a wiring summary showing:
|
|
- Orphan sensors and actuators
|
|
- Missing PLC registers for monitors/controllers
|
|
- IO mismatches
|
|
- HMI controller targets
|
|
|
|
Usage:
|
|
python3 -m tools.debug_semantics --config outputs/configuration_raw.json
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Set
|
|
|
|
from models.ics_simlab_config_v2 import Config
|
|
|
|
|
|
def debug_semantics(config_path: Path) -> None:
|
|
"""Print a wiring summary for debugging semantic issues."""
|
|
with open(config_path) as f:
|
|
cfg_dict = json.load(f)
|
|
|
|
config = Config(**cfg_dict)
|
|
|
|
print("=" * 70)
|
|
print("SEMANTIC WIRING SUMMARY")
|
|
print("=" * 70)
|
|
|
|
# Build IP -> device mapping
|
|
plc_by_ip: Dict[str, str] = {}
|
|
sensor_by_ip: Dict[str, str] = {}
|
|
actuator_by_ip: Dict[str, str] = {}
|
|
|
|
for plc in config.plcs:
|
|
if plc.network and plc.network.ip:
|
|
plc_by_ip[plc.network.ip] = plc.name
|
|
|
|
for sensor in config.sensors:
|
|
if sensor.network and sensor.network.ip:
|
|
sensor_by_ip[sensor.network.ip] = sensor.name
|
|
|
|
for actuator in config.actuators:
|
|
if actuator.network and actuator.network.ip:
|
|
actuator_by_ip[actuator.network.ip] = actuator.name
|
|
|
|
# Track which sensors/actuators are referenced
|
|
monitored_sensor_ips: Set[str] = set()
|
|
controlled_actuator_ips: Set[str] = set()
|
|
|
|
# PLC Wiring Summary
|
|
print("\n" + "-" * 70)
|
|
print("PLC WIRING")
|
|
print("-" * 70)
|
|
|
|
for plc in config.plcs:
|
|
print(f"\n{plc.name} ({plc.network.ip if plc.network else 'no IP'}):")
|
|
|
|
# Build connection_id -> IP mapping
|
|
conn_to_ip: Dict[str, str] = {}
|
|
for conn in plc.outbound_connections:
|
|
if hasattr(conn, 'ip') and conn.id:
|
|
conn_to_ip[conn.id] = conn.ip
|
|
|
|
# Monitors
|
|
print(f" Monitors ({len(plc.monitors)}):")
|
|
for m in plc.monitors:
|
|
target_ip = conn_to_ip.get(m.outbound_connection_id, "???")
|
|
target_device = (
|
|
plc_by_ip.get(target_ip) or
|
|
sensor_by_ip.get(target_ip) or
|
|
actuator_by_ip.get(target_ip) or
|
|
f"unknown ({target_ip})"
|
|
)
|
|
if target_ip in sensor_by_ip:
|
|
monitored_sensor_ips.add(target_ip)
|
|
print(f" - {m.id} -> {target_device} ({m.value_type})")
|
|
|
|
# Controllers
|
|
print(f" Controllers ({len(plc.controllers)}):")
|
|
for c in plc.controllers:
|
|
target_ip = conn_to_ip.get(c.outbound_connection_id, "???")
|
|
target_device = (
|
|
plc_by_ip.get(target_ip) or
|
|
actuator_by_ip.get(target_ip) or
|
|
sensor_by_ip.get(target_ip) or
|
|
f"unknown ({target_ip})"
|
|
)
|
|
if target_ip in actuator_by_ip:
|
|
controlled_actuator_ips.add(target_ip)
|
|
print(f" - {c.id} -> {target_device} ({c.value_type})")
|
|
|
|
# Local registers
|
|
print(f" Local Registers:")
|
|
for reg_type in ["coil", "discrete_input", "holding_register", "input_register"]:
|
|
regs = getattr(plc.registers, reg_type, [])
|
|
for reg in regs:
|
|
io_str = f" io={reg.io}" if reg.io else ""
|
|
print(f" - {reg_type}: {reg.id or reg.physical_value or 'unnamed'} @{reg.address}{io_str}")
|
|
|
|
# HMI Wiring Summary
|
|
print("\n" + "-" * 70)
|
|
print("HMI WIRING")
|
|
print("-" * 70)
|
|
|
|
for hmi in config.hmis:
|
|
print(f"\n{hmi.name}:")
|
|
|
|
# Build connection_id -> IP mapping
|
|
conn_to_ip: Dict[str, str] = {}
|
|
for conn in hmi.outbound_connections:
|
|
if hasattr(conn, 'ip') and conn.id:
|
|
conn_to_ip[conn.id] = conn.ip
|
|
|
|
# Monitors
|
|
print(f" Monitors ({len(hmi.monitors)}):")
|
|
for m in hmi.monitors:
|
|
target_ip = conn_to_ip.get(m.outbound_connection_id, "???")
|
|
target_device = plc_by_ip.get(target_ip, f"unknown ({target_ip})")
|
|
print(f" - {m.id} -> {target_device} ({m.value_type})")
|
|
|
|
# Controllers
|
|
print(f" Controllers ({len(hmi.controllers)}):")
|
|
for c in hmi.controllers:
|
|
target_ip = conn_to_ip.get(c.outbound_connection_id, "???")
|
|
target_device = plc_by_ip.get(target_ip, f"unknown ({target_ip})")
|
|
print(f" - {c.id} -> {target_device} ({c.value_type})")
|
|
|
|
# Orphan Summary
|
|
print("\n" + "-" * 70)
|
|
print("ORPHAN DEVICES")
|
|
print("-" * 70)
|
|
|
|
orphan_sensors = []
|
|
for sensor in config.sensors:
|
|
if sensor.network and sensor.network.ip:
|
|
if sensor.network.ip not in monitored_sensor_ips:
|
|
orphan_sensors.append(sensor.name)
|
|
|
|
orphan_actuators = []
|
|
for actuator in config.actuators:
|
|
if actuator.network and actuator.network.ip:
|
|
if actuator.network.ip not in controlled_actuator_ips:
|
|
orphan_actuators.append(actuator.name)
|
|
|
|
if orphan_sensors:
|
|
print(f"\nOrphan Sensors (no PLC monitor):")
|
|
for name in orphan_sensors:
|
|
print(f" - {name}")
|
|
else:
|
|
print("\nNo orphan sensors")
|
|
|
|
if orphan_actuators:
|
|
print(f"\nOrphan Actuators (no PLC controller):")
|
|
for name in orphan_actuators:
|
|
print(f" - {name}")
|
|
else:
|
|
print("\nNo orphan actuators")
|
|
|
|
# IO Mismatch Summary
|
|
print("\n" + "-" * 70)
|
|
print("IO MISMATCH CHECK")
|
|
print("-" * 70)
|
|
|
|
mismatches = []
|
|
for plc in config.plcs:
|
|
# Build register id -> io mapping
|
|
reg_io: Dict[str, Dict[str, str]] = {}
|
|
for reg_type in ["coil", "discrete_input", "holding_register", "input_register"]:
|
|
reg_io[reg_type] = {}
|
|
for reg in getattr(plc.registers, reg_type, []):
|
|
if reg.id:
|
|
reg_io[reg_type][reg.id] = reg.io or ""
|
|
|
|
# Check monitors (should be io=input)
|
|
for m in plc.monitors:
|
|
if m.value_type in reg_io:
|
|
actual_io = reg_io[m.value_type].get(m.id, "")
|
|
if actual_io and actual_io != "input":
|
|
mismatches.append(
|
|
f"{plc.name}: monitor '{m.id}' has io='{actual_io}' (should be 'input')"
|
|
)
|
|
|
|
# Check controllers (should be io=output)
|
|
for c in plc.controllers:
|
|
if c.value_type in reg_io:
|
|
actual_io = reg_io[c.value_type].get(c.id, "")
|
|
if actual_io and actual_io != "output":
|
|
mismatches.append(
|
|
f"{plc.name}: controller '{c.id}' has io='{actual_io}' (should be 'output')"
|
|
)
|
|
|
|
if mismatches:
|
|
print("\nIO Mismatches found:")
|
|
for m in mismatches:
|
|
print(f" - {m}")
|
|
else:
|
|
print("\nNo IO mismatches")
|
|
|
|
# Missing Register Check
|
|
print("\n" + "-" * 70)
|
|
print("MISSING REGISTERS CHECK")
|
|
print("-" * 70)
|
|
|
|
missing = []
|
|
for plc in config.plcs:
|
|
# Build set of existing register ids by type
|
|
existing: Dict[str, Set[str]] = {}
|
|
for reg_type in ["coil", "discrete_input", "holding_register", "input_register"]:
|
|
existing[reg_type] = {
|
|
reg.id for reg in getattr(plc.registers, reg_type, [])
|
|
if reg.id
|
|
}
|
|
|
|
# Check monitors
|
|
for m in plc.monitors:
|
|
if m.value_type in existing:
|
|
if m.id not in existing[m.value_type]:
|
|
missing.append(
|
|
f"{plc.name}: monitor '{m.id}' missing local register in {m.value_type}"
|
|
)
|
|
|
|
# Check controllers
|
|
for c in plc.controllers:
|
|
if c.value_type in existing:
|
|
if c.id not in existing[c.value_type]:
|
|
missing.append(
|
|
f"{plc.name}: controller '{c.id}' missing local register in {c.value_type}"
|
|
)
|
|
|
|
if missing:
|
|
print("\nMissing Registers:")
|
|
for m in missing:
|
|
print(f" - {m}")
|
|
else:
|
|
print("\nNo missing registers")
|
|
|
|
print("\n" + "=" * 70)
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(
|
|
description="Debug tool for semantic validation issues"
|
|
)
|
|
parser.add_argument(
|
|
"--config",
|
|
required=True,
|
|
help="Input configuration.json path"
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
config_path = Path(args.config)
|
|
if not config_path.exists():
|
|
raise SystemExit(f"ERROR: Config file not found: {config_path}")
|
|
|
|
debug_semantics(config_path)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|