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