ics-simlab-config-gen-claude/tools/debug_semantics.py

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()