195 lines
6.3 KiB
Python
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
|