ics-simlab-config-gen-claude/services/validation/logic_validation.py

195 lines
6.3 KiB
Python

from __future__ import annotations
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, List, Set, Tuple
from services.validation.hil_init_validation import validate_hil_initialization
from services.interface_extract import extract_hil_io, extract_plc_io, load_config
from services.validation.plc_callback_validation import validate_plc_callbacks
# Regex semplici (coprono quasi tutti i casi negli esempi)
RE_IN = re.compile(r'input_registers\[\s*["\']([^"\']+)["\']\s*\]')
RE_OUT = re.compile(r'output_registers\[\s*["\']([^"\']+)["\']\s*\]')
RE_PV = re.compile(r'physical_values\[\s*["\']([^"\']+)["\']\s*\]')
@dataclass
class Issue:
file: str
kind: str # "PLC_INPUT", "PLC_OUTPUT", "PLC_CALLBACK", "HIL_PV", "MAPPING"
key: str
message: str
def _find_keys(py_text: str) -> Tuple[Set[str], Set[str], Set[str]]:
ins = set(RE_IN.findall(py_text))
outs = set(RE_OUT.findall(py_text))
pvs = set(RE_PV.findall(py_text))
return ins, outs, pvs
def validate_logic_against_config(
config_path: str,
logic_dir: str,
plc_logic_map: Dict[str, str] | None = None,
hil_logic_map: Dict[str, str] | None = None,
*,
check_callbacks: bool = False,
callback_window: int = 3,
check_hil_init: bool = False,
) -> List[Issue]:
"""
Valida che i file .py nella cartella logic_dir usino solo chiavi definite nel JSON.
- PLC: chiavi usate in input_registers[...] devono esistere tra gli id io:'input' del PLC
- PLC: chiavi usate in output_registers[...] devono esistere tra gli id io:'output' del PLC
- HIL: chiavi usate in physical_values[...] devono esistere tra hils[].physical_values
Se check_callbacks=True:
- PLC: ogni write su output_registers["X"]["value"] deve avere state_update_callbacks["X"]() subito dopo
(entro callback_window istruzioni nello stesso blocco).
"""
cfg: Dict[str, Any] = load_config(config_path)
plc_io = extract_plc_io(cfg)
hil_io = extract_hil_io(cfg)
# mapping da JSON se non passato
if plc_logic_map is None:
plc_logic_map = {p["name"]: p.get("logic", "") for p in cfg.get("plcs", [])}
if hil_logic_map is None:
hil_logic_map = {h["name"]: h.get("logic", "") for h in cfg.get("hils", [])}
issues: List[Issue] = []
logic_root = Path(logic_dir)
# --- PLC ---
for plc_name, io_sets in plc_io.items():
fname = plc_logic_map.get(plc_name, "")
if not fname:
issues.append(
Issue(
file=str(logic_root),
kind="MAPPING",
key=plc_name,
message=f"PLC '{plc_name}' non ha campo logic nel JSON.",
)
)
continue
fpath = logic_root / fname
if not fpath.exists():
issues.append(
Issue(
file=str(fpath),
kind="MAPPING",
key=plc_name,
message=f"File logica PLC mancante: {fname}",
)
)
continue
text = fpath.read_text(encoding="utf-8", errors="replace")
used_in, used_out, _ = _find_keys(text)
allowed_in = io_sets["inputs"]
allowed_out = io_sets["outputs"]
for k in sorted(used_in - allowed_in):
issues.append(
Issue(
file=str(fpath),
kind="PLC_INPUT",
key=k,
message=f"Chiave letta da input_registers non definita come io:'input' per {plc_name}",
)
)
for k in sorted(used_out - allowed_out):
issues.append(
Issue(
file=str(fpath),
kind="PLC_OUTPUT",
key=k,
message=f"Chiave scritta su output_registers non definita come io:'output' per {plc_name}",
)
)
if check_callbacks:
cb_issues = validate_plc_callbacks(str(fpath), window=callback_window)
for cbi in cb_issues:
issues.append(
Issue(
file=cbi.file,
kind="PLC_CALLBACK",
key=cbi.key,
message=cbi.message,
)
)
# --- HIL ---
for hil_name, io_sets in hil_io.items():
fname = (hil_logic_map or {}).get(hil_name, "") # safety se map None
if not fname:
issues.append(
Issue(
file=str(logic_root),
kind="MAPPING",
key=hil_name,
message=f"HIL '{hil_name}' non ha campo logic nel JSON.",
)
)
continue
fpath = logic_root / fname
if not fpath.exists():
issues.append(
Issue(
file=str(fpath),
kind="MAPPING",
key=hil_name,
message=f"File logica HIL mancante: {fname}",
)
)
continue
text = fpath.read_text(encoding="utf-8", errors="replace")
_, _, used_pv = _find_keys(text)
# insieme di chiavi definite nel JSON per questo HIL
allowed_pv = io_sets["inputs"] | io_sets["outputs"]
# 1) check: il codice non deve usare physical_values non definite nel JSON
for k in sorted(used_pv - allowed_pv):
issues.append(
Issue(
file=str(fpath),
kind="HIL_PV",
key=k,
message=f"Chiave physical_values non definita in hils[].physical_values per {hil_name}",
)
)
# 2) check opzionale: tutte le physical_values del JSON devono essere inizializzate nel file HIL
if check_hil_init:
required_init = io_sets["outputs"]
init_issues = validate_hil_initialization(str(fpath), required_keys=required_init)
for ii in init_issues:
issues.append(
Issue(
file=ii.file,
kind="HIL_INIT",
key=ii.key,
message=ii.message,
)
)
return issues