================================================================================ APPUNTI OPERAZIONI - ics-simlab-config-gen_claude ================================================================================ Data: 2026-01-27 ================================================================================ NOTA: diario.md vs appunti.txt ------------------------------ - appunti.txt: note operative rapide, bullet point, aggiornare subito - diario.md: registro giornaliero per tesi, aggiornare a fine richiesta lunga (>30 parole) - Scopri bug o fix? → qui (appunti.txt) - Finisci richiesta lunga? → scrivi entry in diario.md con rationale ================================================================================ PROBLEMA INIZIALE ----------------- PLC2 crashava all'avvio con "ConnectionRefusedError" quando tentava di scrivere a PLC1 via Modbus TCP prima che PLC1 fosse pronto. Causa: callback cbs[key]() chiamata direttamente senza gestione errori. SOLUZIONE IMPLEMENTATA ---------------------- File modificato: tools/compile_ir.py (linee 24, 30-40, 49) Aggiunto: - import time - Funzione _safe_callback() con retry logic (30 tentativi × 0.2s = 6s) - Modifica _write() per chiamare _safe_callback(cbs[key]) invece di cbs[key]() Risultato: - PLC2 non crasha più - Retry automatico se PLC1 non è pronto - Warning solo dopo 30 tentativi falliti - Container continua a girare anche in caso di errore FILE CREATI ----------- build_scenario.py - Builder deterministico (config → IR → logic) validate_fix.py - Validatore presenza fix nei file generati CLEANUP_SUMMARY.txt - Summary pulizia progetto README.md (aggiornato) - Documentazione completa docs/ (7 file): - README_FIX.md - Doc principale fix - QUICKSTART.txt - Guida rapida - RUNTIME_FIX.md - Fix dettagliato + troubleshooting - CHANGES.md - Modifiche con diff - DELIVERABLES.md - Summary completo - FIX_SUMMARY.txt - Confronto codice before/after - CORRECT_COMMANDS.txt - Come usare path assoluti con sudo scripts/ (3 file): - run_simlab.sh - Launcher ICS-SimLab con path corretti - test_simlab.sh - Test interattivo - diagnose_runtime.sh - Diagnostica container PULIZIA PROGETTO ---------------- Spostato in docs/: - 7 file documentazione dalla root Spostato in scripts/: - 3 script bash dalla root Cancellato: - database/, docker/, inputs/ (cartelle vuote) - outputs/last_raw_response.txt (temporaneo) - outputs/logic/, logic_ir/, logic_water_tank/ (vecchie versioni) Mantenuto: - outputs/scenario_run/ (SCENARIO FINALE per ICS-SimLab) - outputs/configuration.json (config base) - outputs/ir/ (IR intermedio) STRUTTURA FINALE ---------------- Root: 4 file essenziali (main.py, build_scenario.py, validate_fix.py, README.md) docs/: documentazione (60K) scripts/: utility (20K) outputs/: solo file necessari (56K) + cartelle codice sorgente (tools/, services/, models/, templates/, helpers/) + riferimenti (examples/, spec/, prompts/) COMANDI UTILI ------------- # Build scenario completo python3 build_scenario.py --overwrite # Valida fix presente python3 validate_fix.py # Esegui ICS-SimLab (IMPORTANTE: path assoluti con sudo!) ./scripts/run_simlab.sh # O manualmente: cd /home/stefano/projects/ICS-SimLab-main/curtin-ics-simlab sudo ./start.sh /home/stefano/projects/ics-simlab-config-gen_claude/outputs/scenario_run # Monitor PLC2 logs sudo docker logs $(sudo docker ps --format '{{.Names}}' | grep plc2) -f # Stop cd /home/stefano/projects/ICS-SimLab-main/curtin-ics-simlab && sudo ./stop.sh PROBLEMA PATH CON SUDO ----------------------- Errore ricevuto: FileNotFoundError quando usato ~/projects/... Causa: sudo NON espande ~ a /home/stefano Soluzione: - Usare SEMPRE percorsi assoluti con sudo - Oppure usare ./scripts/run_simlab.sh (gestisce automaticamente) WORKFLOW COMPLETO ----------------- 1. Testo → configuration.json (LLM): python3 main.py --input-file prompts/input_testuale.txt 2. Config → Scenario completo: python3 build_scenario.py --overwrite 3. Valida fix: python3 validate_fix.py 4. Esegui: ./scripts/run_simlab.sh VALIDAZIONE FIX --------------- $ python3 validate_fix.py ✅ plc1.py: OK (retry fix present) ✅ plc2.py: OK (retry fix present) Verifica manuale: $ grep "_safe_callback" outputs/scenario_run/logic/plc2.py (deve trovare la funzione e la chiamata in _write) COSA CERCARE NEI LOG --------------------- ✅ Successo: NO "Exception in thread" errors in PLC2 ⚠️ Warning: "WARNING: Callback failed after 30 attempts" (PLC1 lento ma ok) ❌ Errore: Container crasha (fix non presente o problema diverso) NOTE IMPORTANTI --------------- 1. SEMPRE usare percorsi assoluti con sudo (no ~) 2. Rebuild scenario dopo modifiche config: python3 build_scenario.py --overwrite 3. Validare sempre dopo rebuild: python3 validate_fix.py 4. Fix è nel generatore (tools/compile_ir.py) quindi si propaga automaticamente 5. Solo dipendenza: time.sleep (stdlib, no package extra) STATUS FINALE ------------- ✅ Fix implementato e testato ✅ Scenario pronto in outputs/scenario_run/ ✅ Validatore conferma presenza fix ✅ Documentazione completa ✅ Progetto pulito e organizzato ✅ Script pronti per esecuzione Pronto per testing con ICS-SimLab! ================================================================================ NUOVA FEATURE: PROCESS SPEC PIPELINE (LLM → process_spec.json → HIL logic) ================================================================================ Data: 2026-01-27 OBIETTIVO --------- Generare fisica di processo tramite LLM senza codice Python free-form. Pipeline: prompt testuale → LLM (structured output) → process_spec.json → compilazione deterministica → HIL logic. FILE CREATI ----------- models/process_spec.py - Modello Pydantic per ProcessSpec - model: Literal["water_tank_v1"] (enum-ready) - dt: float (time step) - params: WaterTankParams (level_min/max/init, area, q_in_max, k_out) - signals: WaterTankSignals (mapping chiavi HIL) tools/generate_process_spec.py - Generazione LLM → process_spec.json - Usa structured output (json_schema) per output valido - Legge prompt + config per contesto tools/compile_process_spec.py - Compilazione deterministica spec → HIL logic - Implementa fisica water_tank_v1 - d(level)/dt = (Q_in - Q_out) / area - Q_in = q_in_max se valvola aperta - Q_out = k_out * sqrt(level) (scarico gravitazionale) tools/validate_process_spec.py - Validatore con tick test - Controlla modello supportato - Verifica dt > 0, min < max, init in bounds - Verifica chiavi segnali esistono in HIL physical_values - Tick test: 100 step per verificare bounds examples/water_tank/prompt.txt - Prompt esempio per water tank FISICA IMPLEMENTATA (water_tank_v1) ----------------------------------- Equazioni: - Q_in = q_in_max if valve_open >= 0.5 else 0 - Q_out = k_out * sqrt(level) - d_level = (Q_in - Q_out) / area * dt - level = clamp(level + d_level, level_min, level_max) Parametri tipici: - dt = 0.1s (10 Hz) - level_min = 0, level_max = 1.0 (metri) - level_init = 0.5 (50% capacità) - area = 1.0 m^2 - q_in_max = 0.02 m^3/s - k_out = 0.01 m^2.5/s COMANDI PIPELINE PROCESS SPEC ----------------------------- # 1. Genera process_spec.json da prompt (richiede OPENAI_API_KEY) python3 -m tools.generate_process_spec \ --prompt examples/water_tank/prompt.txt \ --config outputs/configuration.json \ --out outputs/process_spec.json # 2. Valida process_spec.json contro config python3 -m tools.validate_process_spec \ --spec outputs/process_spec.json \ --config outputs/configuration.json # 3. Compila process_spec.json in HIL logic python3 -m tools.compile_process_spec \ --spec outputs/process_spec.json \ --out outputs/hil_logic.py \ --overwrite CONTRATTO HIL RISPETTATO ------------------------ - Inizializza tutte le chiavi physical_values (setdefault) - Legge solo io:"input" (valve_open_key) - Scrive solo io:"output" (tank_level_key, level_measured_key) - Clamp level tra min/max VANTAGGI APPROCCIO ------------------ 1. LLM genera solo spec strutturata, non codice Python 2. Compilazione deterministica e verificabile 3. Validazione pre-runtime con tick test 4. Estensibile: aggiungere nuovi modelli (es. bottle_line_v1) è semplice NOTE ---- - ProcessSpec usa Pydantic con extra="forbid" per sicurezza - JSON Schema per structured output generato da Pydantic - Tick test verifica 100 step con valvola aperta e chiusa - Se chiavi non esistono in HIL, validazione fallisce ================================================================================ INTEGRAZIONE PROCESS SPEC IN SCENARIO ASSEMBLY ================================================================================ Data: 2026-01-27 OBIETTIVO --------- Integrare la pipeline process_spec nel flusso di build scenario, così che Curtin ICS-SimLab possa eseguire end-to-end con fisica generata da LLM. MODIFICHE EFFETTUATE -------------------- 1. build_scenario.py aggiornato: - Nuovo argomento --process-spec (opzionale) - Se fornito, compila process_spec.json nel file HIL corretto (es. hil_1.py) - Sostituisce/sovrascrive la logica HIL generata da IR - Aggiunto Step 5: verifica che tutti i file logic/*.py referenziati esistano 2. tools/verify_scenario.py creato: - Verifica standalone che scenario sia completo - Controlla configuration.json esiste - Controlla logic/ directory esiste - Controlla tutti i file logic referenziati esistono - Mostra file orfani (non referenziati) FLUSSO COMPLETO CON PROCESS SPEC -------------------------------- # 1. Genera configuration.json (LLM o manuale) python3 main.py --input-file prompts/input_testuale.txt # 2. Genera process_spec.json (LLM con structured output) python3 -m tools.generate_process_spec \ --prompt examples/water_tank/prompt.txt \ --config outputs/configuration.json \ --out outputs/process_spec.json # 3. Valida process_spec.json python3 -m tools.validate_process_spec \ --spec outputs/process_spec.json \ --config outputs/configuration.json # 4. Build scenario con process_spec (sostituisce HIL da IR) python3 build_scenario.py \ --out outputs/scenario_run \ --process-spec outputs/process_spec.json \ --overwrite # 5. Verifica scenario completo python3 -m tools.verify_scenario --scenario outputs/scenario_run -v # 6. Esegui in ICS-SimLab cd /home/stefano/projects/ICS-SimLab-main/curtin-ics-simlab sudo ./start.sh /home/stefano/projects/ics-simlab-config-gen_claude/outputs/scenario_run FLUSSO SENZA PROCESS SPEC (compatibilità backward) -------------------------------------------------- # Build scenario con IR (come prima) python3 build_scenario.py --out outputs/scenario_run --overwrite VERIFICA FILE LOGIC ------------------- Il nuovo Step 5 in build_scenario.py verifica: - Tutti i plcs[].logic esistono in logic/ - Tutti i hils[].logic esistono in logic/ - Se manca un file, build fallisce con errore chiaro Comando standalone: python3 -m tools.verify_scenario --scenario outputs/scenario_run -v STRUTTURA SCENARIO FINALE ------------------------- outputs/scenario_run/ ├── configuration.json (configurazione ICS-SimLab) └── logic/ ├── plc1.py (logica PLC1, da IR) ├── plc2.py (logica PLC2, da IR) └── hil_1.py (logica HIL, da process_spec o IR) NOTE IMPORTANTI --------------- - --process-spec è opzionale: se non fornito, usa IR per HIL (comportamento precedente) - Il file HIL viene sovrascritto se esiste (--overwrite implicito per Step 2b) - Il nome file HIL è preso da config (hils[].logic), non hardcoded - Verifica finale assicura che scenario sia completo prima di eseguire ================================================================================ PROBLEMA SQLITE DATABASE ICS-SimLab ================================================================================ Data: 2026-01-27 SINTOMO ------- Tutti i container (HIL, sensors, actuators, UI) crashano con: sqlite3.OperationalError: unable to open database file CAUSA ----- Il file `physical_interactions.db` diventa una DIRECTORY invece che un file. Succede quando Docker crea il volume mount point PRIMA che ICS-SimLab crei il DB. Verifica: $ ls -la ~/projects/ICS-SimLab-main/curtin-ics-simlab/simulation/communications/ drwxr-xr-x 2 root root 4096 Jan 27 15:49 physical_interactions.db ← DIRECTORY! SOLUZIONE --------- Pulire completamente e riavviare: cd ~/projects/ICS-SimLab-main/curtin-ics-simlab # Stop e rimuovi tutti i container e volumi sudo docker-compose down -v --remove-orphans sudo docker system prune -af # Rimuovi directory simulation corrotta sudo rm -rf simulation # Riavvia (crea DB PRIMA di Docker) sudo ./start.sh /home/stefano/projects/ics-simlab-config-gen_claude/outputs/scenario_run NOTA IMPORTANTE: PATH ASSOLUTO ------------------------------ SEMPRE usare path assoluto completo (NO ~ che non viene espanso da sudo). SBAGLIATO: sudo ./start.sh ~/projects/.../outputs/scenario_run CORRETTO: sudo ./start.sh /home/stefano/projects/.../outputs/scenario_run SEQUENZA STARTUP CORRETTA ICS-SimLab ------------------------------------ 1. rm -r simulation (pulisce vecchia simulazione) 2. python3 main.py $1 (crea DB + container directories) 3. docker compose build (build immagini) 4. docker compose up (avvia container) Il DB viene creato al passo 2, PRIMA che Docker monti i volumi. Se Docker parte con volumi già definiti ma file mancante, crea directory. ================================================================================ FISICA HIL MIGLIORATA: MODELLO ACCOPPIATO TANK + BOTTLE ================================================================================ Data: 2026-01-27 OSSERVAZIONI ------------ - La fisica HIL generata era troppo semplificata: - Range 0..1 normalizzati con clamp continuo - bottle_at_filler derivato direttamente da conveyor_cmd (logica invertita) - Nessun tracking della distanza bottiglia - Nessun accoppiamento: bottiglia si riempie senza svuotare tank - Nessun reset bottiglia quando esce - Esempio funzionante (examples/water_tank/bottle_factory_logic.py) usa: - Range interi: tank 0-1000, bottle 0-200, distance 0-130 - Boolean per stati attuatori - Accoppiamento: bottle fill SOLO se outlet_valve=True AND distance in [0,30] - Reset: quando distance < 0, nuova bottiglia con fill=0 e distance=130 - Due thread separati per tank e bottle MODIFICHE EFFETTUATE -------------------- File: tools/compile_ir.py, funzione render_hil_multi() 1. Detect se presenti ENTRAMBI TankLevelBlock e BottleLineBlock 2. Se sì, genera fisica accoppiata stile esempio: - Variabile interna _bottle_distance (0-130) - bottle_at_filler = (0 <= _bottle_distance <= 30) - Tank dynamics: +18 se inlet ON, -6 se outlet ON - Bottle fill: +6 SOLO se outlet ON AND bottle at filler (conservazione) - Conveyor: distance -= 4; se < 0 reset a 130 e fill = 0 - Clamp: tank 0-1000, bottle 0-200 - time.sleep(0.6) come esempio 3. Se no, fallback a fisica semplice precedente RANGE E SEMANTICA ----------------- - tank_level: 0-1000 (500 = 50% pieno) - bottle_fill: 0-200 (200 = pieno) - bottle_distance: 0-130 interno (0-30 = sotto filler) - bottle_at_filler: 0 o 1 (boolean) - Actuator states: letti come bool() VERIFICA -------- .venv/bin/python3 build_scenario.py --out outputs/scenario_run --overwrite cat outputs/scenario_run/logic/hil_1.py grep "bottle_at_filler" outputs/scenario_run/logic/hil_1.py grep "_bottle_distance" outputs/scenario_run/logic/hil_1.py DA FARE ------- - Verificare che sensori leggano correttamente i nuovi range - Eventualmente aggiungere thread separati come esempio (ora è single loop) - Testare end-to-end con ICS-SimLab ================================================================================ FIX CRITICO: CONTRATTO ICS-SimLab logic() DEVE GIRARE FOREVER ================================================================================ Data: 2026-01-27 ROOT CAUSE IDENTIFICATA ----------------------- ICS-SimLab chiama logic() UNA SOLA VOLTA in un thread e si aspetta che giri per sempre. Il nostro codice generato invece ritornava subito → thread muore → nessun traffico. Vedi: ICS-SimLab/src/components/plc.py linee 352-365: logic_thread = Thread(target=logic.logic, args=(...), daemon=True) logic_thread.start() ... logic_thread.join() # ← Aspetta forever! CONFRONTO CON ESEMPIO FUNZIONANTE (examples/water_tank/) -------------------------------------------------------- Esempio funzionante PLC: def logic(...): time.sleep(2) # Aspetta sync while True: # Loop infinito # logica time.sleep(0.1) Nostro codice PRIMA: def logic(...): # logica return # ← ERRORE: ritorna subito! MODIFICHE EFFETTUATE -------------------- File: tools/compile_ir.py 1. PLC logic ora genera: - time.sleep(2) all'inizio per sync - while True: loop infinito - Logica dentro il loop con indent +4 - time.sleep(0.1) alla fine del loop - _heartbeat() per log ogni 5 secondi 2. HIL logic ora genera: - Inizializzazione diretta (non setdefault) - time.sleep(3) per sync - while True: loop infinito - Fisica dentro il loop con indent +4 - time.sleep(0.1) alla fine del loop 3. _safe_callback migliorato: - Cattura OSError e ConnectionException - Ritorna bool per tracking - 20 tentativi × 0.25s = 5s retry STRUTTURA GENERATA ORA ---------------------- PLC: def logic(input_registers, output_registers, state_update_callbacks): time.sleep(2) while True: _heartbeat() # logica con _write() e _get_float() time.sleep(0.1) HIL: def logic(physical_values): physical_values['key'] = initial_value time.sleep(3) while True: # fisica time.sleep(0.1) VERIFICA -------- # Rebuild scenario .venv/bin/python3 build_scenario.py --out outputs/scenario_run --overwrite # Verifica while True presente grep "while True" outputs/scenario_run/logic/*.py # Verifica time.sleep presente grep "time.sleep" outputs/scenario_run/logic/*.py # Esegui in ICS-SimLab cd ~/projects/ICS-SimLab-main/curtin-ics-simlab sudo docker-compose down -v sudo rm -rf simulation sudo ./start.sh /home/stefano/projects/ics-simlab-config-gen_claude/outputs/scenario_run # Verifica nei log sudo docker logs plc1 2>&1 | grep HEARTBEAT sudo docker logs plc2 2>&1 | grep HEARTBEAT ================================================================================ MIGLIORAMENTI PLC E HIL: INIZIALIZZAZIONE + EXTERNAL WATCHER ================================================================================ Data: 2026-01-27 CONTESTO -------- Confrontando con examples/water_tank/logic/plc1.py abbiamo notato che: 1. Il PLC esempio inizializza gli output e chiama i callback PRIMA del loop 2. Il PLC esempio traccia prev_output_valve per rilevare modifiche esterne (HMI) 3. Il nostro generatore non faceva né l'uno né l'altro MODIFICHE EFFETTUATE -------------------- A) PLC Generation (tools/compile_ir.py): 1. Explicit initialization phase PRIMA del while loop: - Setta ogni output a 0 - Chiama callback per ogni output - Aggiorna _prev_outputs per tracking 2. External-output watcher (_check_external_changes): - Nuova funzione che rileva cambi esterni agli output (es. HMI) - Chiamata all'inizio di ogni iterazione del loop - Se output cambiato esternamente, chiama callback 3. _prev_outputs tracking: - Dict globale che tiene traccia dei valori scritti dal PLC - _write() aggiorna _prev_outputs quando scrive - Evita double-callback: se il PLC ha scritto il valore, non serve callback 4. _collect_output_keys(): - Nuova funzione helper che estrae tutte le chiavi output dalle regole - Usata per generare lista _output_keys per il watcher B) HIL Generation (tools/compile_ir.py): 1. Bottle fill threshold: - Bottiglia si riempie SOLO se bottle_fill < 200 (max) - Evita overflow logico C) Validator (services/validation/plc_callback_validation.py): 1. Riconosce pattern _write(): - Se file definisce funzione _write(), skip strict validation - _write() gestisce internamente write + callback + tracking PATTERN GENERATO ORA -------------------- PLC (plc1.py, plc2.py): def logic(input_registers, output_registers, state_update_callbacks): global _prev_outputs # --- Explicit initialization: set outputs and call callbacks --- if 'tank_input_valve' in output_registers: output_registers['tank_input_valve']['value'] = 0 _prev_outputs['tank_input_valve'] = 0 if 'tank_input_valve' in state_update_callbacks: _safe_callback(state_update_callbacks['tank_input_valve']) ... # Wait for other components to start time.sleep(2) _output_keys = ['tank_input_valve', 'tank_output_valve'] # Main loop - runs forever while True: _heartbeat() # Check for external changes (e.g., HMI) _check_external_changes(output_registers, state_update_callbacks, _output_keys) # Control logic with _write() ... time.sleep(0.1) HIL (hil_1.py): def logic(physical_values): ... while True: ... # Conservation: if bottle is at filler AND not full, water goes to bottle if outlet_valve_on: tank_level -= 6 if bottle_at_filler and bottle_fill < 200: # threshold bottle_fill += 6 ... FUNZIONI HELPER GENERATE ------------------------ _write(out_regs, cbs, key, value): - Scrive valore se diverso - Aggiorna _prev_outputs[key] per tracking - Chiama callback se presente _check_external_changes(out_regs, cbs, keys): - Per ogni key in keys: - Se valore attuale != _prev_outputs[key] - Valore cambiato esternamente (HMI) - Chiama callback - Aggiorna _prev_outputs _safe_callback(cb, retries, delay): - Retry logic per startup race conditions - Cattura OSError e ConnectionException VERIFICA -------- # Rebuild .venv/bin/python3 build_scenario.py --overwrite # Verifica initialization grep "Explicit initialization" outputs/scenario_run/logic/plc*.py # Verifica external watcher grep "_check_external_changes" outputs/scenario_run/logic/plc*.py # Verifica bottle threshold grep "bottle_fill < 200" outputs/scenario_run/logic/hil_1.py ================================================================================ FIX: AUTO-GENERAZIONE PLC MONITORS + SCALA THRESHOLD ASSOLUTI ================================================================================ Data: 2026-01-27 PROBLEMI IDENTIFICATI --------------------- 1) PLC monitors vuoti: i PLC non avevano outbound_connections ai sensori e monitors era sempre []. I sensori erano attivi ma nessuno li interrogava. 2) Scala mismatch: HIL usa range interi (tank 0-1000, bottle 0-200) ma i threshold PLC erano normalizzati (0.2, 0.8 su scala 0-1). Risultato: 482 >= 0.8 sempre True -> logica sbagliata. 3) Modifiche manuali a configuration.json non persistono dopo rebuild. SOLUZIONE IMPLEMENTATA ---------------------- A) Auto-generazione PLC monitors (tools/enrich_config.py): - Nuovo tool che arricchisce configuration.json - Per ogni PLC input register: - Trova il HIL output corrispondente (es. water_tank_level -> water_tank_level_output) - Trova il sensore che espone quel valore - Aggiunge outbound_connection al sensore - Aggiunge monitor entry per polling - Per ogni PLC output register: - Trova l'attuatore corrispondente (es. tank_input_valve -> tank_input_valve_input) - Aggiunge outbound_connection all'attuatore - Aggiunge controller entry B) Scala threshold assoluti (models/ir_v1.py + tools/compile_ir.py): - Aggiunto signal_max a HysteresisFillRule e ThresholdOutputRule - make_ir_from_config.py: imposta signal_max=1000 per tank, signal_max=200 per bottle - compile_ir.py: converte threshold normalizzati in assoluti: - low=0.2, signal_max=1000 -> abs_low=200 - high=0.8, signal_max=1000 -> abs_high=800 - threshold=0.2, signal_max=200 -> abs_threshold=40 C) Pipeline aggiornata (build_scenario.py): - Nuovo Step 0: chiama enrich_config.py - Usa configuration_enriched.json per tutti gli step successivi FILE MODIFICATI --------------- - tools/enrich_config.py (NUOVO) - Arricchisce config con monitors - models/ir_v1.py - Aggiunto signal_max ai rule - tools/make_ir_from_config.py - Imposta signal_max per tank/bottle - tools/compile_ir.py - Usa threshold assoluti - build_scenario.py - Aggiunto Step 0 enrichment VERIFICA -------- # Rebuild scenario .venv/bin/python3 build_scenario.py --overwrite # Verifica monitors generati grep -A10 '"monitors"' outputs/configuration_enriched.json # Verifica threshold assoluti nel PLC grep "lvl <=" outputs/scenario_run/logic/plc1.py # Dovrebbe mostrare: if lvl <= 200.0 e elif lvl >= 800.0 grep "v <" outputs/scenario_run/logic/plc2.py # Dovrebbe mostrare: if v < 40.0 # Esegui ICS-SimLab cd ~/projects/ICS-SimLab-main/curtin-ics-simlab sudo docker-compose down -v sudo rm -rf simulation sudo ./start.sh /home/stefano/projects/ics-simlab-config-gen_claude/outputs/scenario_run ================================================================================ FIX: VALORI INIZIALI RULE-AWARE (NO PIU' TUTTI ZERO) ================================================================================ Data: 2026-01-28 PROBLEMA OSSERVATO ------------------ - UI piatta: tank level ~482, bottle fill ~18 (non cambiano mai) - Causa: init impostava TUTTI gli output a 0 - Con tank a 500 (mid-range tra low=200 e high=800), la logica hysteresis non scrive nulla -> entrambe le valvole restano a 0 -> nessun flusso - Sistema bloccato in steady state SOLUZIONE --------- Valori iniziali derivati dalle regole invece che tutti zero: 1) HysteresisFillRule: - inlet_out = 0 (chiuso) - outlet_out = 1 (APERTO) <- questo fa partire il drenaggio - Tank scende -> raggiunge low=200 -> inlet si apre -> ciclo parte 2) ThresholdOutputRule: - output_id = true_value (tipicamente 1) - Attiva l'output inizialmente FILE MODIFICATO --------------- - tools/compile_ir.py - Nuova funzione _compute_initial_values(rules) -> Dict[str, int] - render_plc_rules() usa init_values invece di 0 fisso - Commento nel codice generato spiega il perché VERIFICA -------- # Rebuild .venv/bin/python3 build_scenario.py --overwrite # Verifica init values nel PLC generato grep -A3 "Explicit initialization" outputs/scenario_run/logic/plc1.py # Deve mostrare: outlet = 1, inlet = 0 grep "tank_output_valve.*value.*=" outputs/scenario_run/logic/plc1.py # Deve mostrare: output_registers['tank_output_valve']['value'] = 1 # Esegui e verifica che tank level cambi cd ~/projects/ICS-SimLab-main/curtin-ics-simlab sudo docker-compose down -v && sudo rm -rf simulation sudo ./start.sh /home/stefano/projects/ics-simlab-config-gen_claude/outputs/scenario_run # Dopo ~30 secondi, UI deve mostrare tank level che scende ================================================================================ FIX: HMI MONITOR ADDRESS DERIVAZIONE DA REGISTER MAP PLC ================================================================================ Data: 2026-01-28 PROBLEMA OSSERVATO ------------------ HMI logs mostrano ripetuti: "ERROR - Error: couldn't read values" per monitors (water_tank_level, bottle_fill_level, bottle_at_filler). Causa: i monitors HMI usavano value_type/address indovinati invece di derivarli dalla mappa registri del PLC target. Es: - HMI monitor bottle_fill_level: address=2 (SBAGLIATO) - PLC2 register bottle_fill_level: address=1 (CORRETTO) - HMI tentava di leggere holding_register@2 che non esiste -> errore Modbus SOLUZIONE IMPLEMENTATA ---------------------- File modificato: tools/enrich_config.py 1) Nuova funzione helper find_register_mapping(device, id): - Cerca in tutti i tipi registro (coil, discrete_input, holding_register, input_register) - Ritorna (value_type, address, count) se trova il registro per id - Ritorna None se non trovato 2) Nuova funzione enrich_hmi_connections(config): - Per ogni HMI monitor che polla un PLC: - Trova il PLC target tramite outbound_connection IP - Cerca il registro nel PLC tramite find_register_mapping - Aggiorna value_type, address, count per matchare il PLC - Stampa "FIX:" quando corregge un valore - Stampa "WARNING:" se registro non trovato (non indovina default) - Stessa logica per controllers HMI 3) main() aggiornato: - Chiama enrich_hmi_connections() dopo enrich_plc_connections() - Summary include anche HMI monitors/controllers ESEMPIO OUTPUT -------------- $ python3 -m tools.enrich_config --config outputs/configuration.json \ --out outputs/configuration_enriched.json --overwrite Enriching PLC connections... Fixing HMI monitors/controllers... FIX: hmi_1 monitor 'bottle_fill_level': holding_register@2 -> holding_register@1 (from plc2) Summary: plc1: 4 outbound_connections, 1 monitors, 2 controllers plc2: 4 outbound_connections, 2 monitors, 2 controllers hmi_1: 3 monitors, 1 controllers VERIFICA -------- # Rebuild scenario python3 build_scenario.py --out outputs/scenario_run --overwrite # Verifica che bottle_fill_level abbia address corretto grep -A5 '"id": "bottle_fill_level"' outputs/configuration_enriched.json | grep address # Deve mostrare: "address": 1 (non 2) # Esegui ICS-SimLab cd /home/stefano/projects/ICS-SimLab-main/curtin-ics-simlab sudo docker-compose down -v && sudo rm -rf simulation sudo ./start.sh /home/stefano/projects/ics-simlab-config-gen_claude/outputs/scenario_run # Verifica che HMI non mostri più "couldn't read values" sudo docker logs hmi_1 2>&1 | grep -i error # UI deve mostrare valori che cambiano nel tempo ================================================================================ FIX: EXCLUDE_NONE=TRUE IN BUILD_CONFIG (IDENTITY NULL CRASH) ================================================================================ Data: 2026-01-30 PROBLEMA OSSERVATO ------------------ PLCs (plc1, plc2) crashavano all'avvio con: TypeError: 'NoneType' object is not subscriptable in /home/plc/plc.py linea 78. Causa: configuration.json conteneva "identity": null per i PLC. ICS-SimLab/src/components/plc.py fa: if "identity" in configs: identity.MajorMinorRevision = configs["identity"]["major_minor_revision"] Il check "identity" in configs è True anche se il valore è None! Quindi tenta configs[None]["major_minor_revision"] -> crash. SOLUZIONE --------- File modificato: tools/build_config.py def config_to_dict(cfg: Config) -> Dict[str, Any]: """Convert Pydantic model to dict for JSON serialization. Uses exclude_none=True to remove null values, which prevents ICS-SimLab runtime errors like 'identity': None causing TypeError when PLC code checks 'if "identity" in configs'. """ return cfg.model_dump(mode="json", exclude_none=True) # era exclude_none=False VERIFICA -------- python3 build_scenario.py --out outputs/scenario_run --overwrite grep -n "identity" outputs/scenario_run/configuration.json # Non deve trovare "identity" se non definita docker logs plc1 2>&1 | head -20 # Non deve crashare con TypeError ================================================================================ WORKAROUND: HMI STARTUP RACE CONDITION (scripts/e2e.sh) ================================================================================ Data: 2026-01-30 PROBLEMA OSSERVATO ------------------ operator_hmi si avvia PRIMA che plc1 sia pronto su 192.168.100.21:502. Risultato: Connection refused iniziale, poi HMI rimane in stato di errore anche dopo che PLC diventa disponibile. Log ripetuti: "ERROR - Error: couldn't read values" NOTA: Questo è un problema architetturale di ICS-SimLab che non possiamo modificare. La soluzione è un workaround host-side. SOLUZIONE IMPLEMENTATA ---------------------- Nuovo file: scripts/e2e.sh Script E2E che: 1. Verifica prerequisiti (scenario, simlab) 2. Stop container esistenti 3. Avvia simlab (docker compose up -d) 4. Polling: aspetta che plc1 e plc2 siano raggiungibili su porta 502 5. Restart operator_hmi dopo che PLCs sono pronti 6. Monitora log per N secondi 7. Salva log in outputs/run_/ 8. Genera summary con conteggio errori 9. Stop simlab (a meno di --no-stop) COMANDI ------- # E2E test completo (start, test, stop) ./scripts/e2e.sh # E2E test senza stop finale (per debug manuale) ./scripts/e2e.sh --no-stop # Verifica risultati ls outputs/run_*/ cat outputs/run_*/summary.txt cat outputs/run_*/operator_hmi.log OSSERVAZIONE AGGIUNTIVA ----------------------- Anche DOPO restart HMI, i log mostrano "couldn't read values". Questo indica un problema più profondo nel data flow, non solo race condition. Possibili cause: - HIL non sta producendo valori (physical_values non aggiornati) - Sensori non ricevono dati da HIL - Monitors PLC non leggono correttamente dai sensori DA INVESTIGARE: - docker logs physical_io_hil (solo "Starting HIL"?) - water_tank_level_sensor register value (sempre 0?) - Confrontare con example funzionante PROSSIMO STEP ------------- Investigare perché HIL non produce valori o perché sensori non li ricevono. ================================================================================ TOOL: MODBUS PROBE DIAGNOSTICO (tools/probe_modbus.py) ================================================================================ Data: 2026-01-30 SINTOMO OSSERVATO ----------------- - PLC heartbeat mostra last_write_ok=False continuamente - HMI riporta "couldn't read values" anche dopo restart - Non è chiaro se il problema è: - Connettività TCP - Eccezione Modbus (illegal address/function) - Mismatch tipo registro/indirizzo tra monitors e target SOLUZIONE IMPLEMENTATA ---------------------- Nuovo file: tools/probe_modbus.py Script diagnostico che: 1. Legge configuration.json 2. Estrae tutti i target dei monitor (HMI→PLC, PLC→Sensor) 3. Per ogni target: - Verifica connettività TCP - Esegue lettura Modbus con funzione corretta (coils/discrete/holding/input) - Riporta OK + valore, oppure FAIL + dettaglio errore 4. Genera report con diagnosi automatica INTEGRAZIONE IN E2E ------------------- scripts/e2e.sh ora include: - Step 4.5: Esegue probe via docker exec in operator_hmi - Salva risultati in outputs/run_/probe.txt - Mostra summary nel report finale - Cattura log anche dei sensori COMANDI ------- # Test manuale del probe (da host con pymodbus) python3 tools/probe_modbus.py --config outputs/scenario_run/configuration.json # E2E completo con probe ./scripts/e2e.sh # Verifica risultati probe cat outputs/run_*/probe.txt OUTPUT ESEMPIO -------------- ====================================================================== MODBUS PROBE REPORT ====================================================================== Total targets: 7 TCP reachable: 7/7 Modbus OK: 3/7 --- operator_hmi monitors --- [OK] operator_hmi -> 192.168.100.21:502 input_register@200 (id=water_tank_level_reg) value=[0] [FAIL] operator_hmi -> 192.168.100.22:502 input_register@210 (id=bottle_fill_level_reg) (TCP_OK) Modbus error: ... --- plc1 monitors --- [OK] plc1 -> 192.168.100.31:502 input_register@1 (id=water_tank_level_output) value=[500] DIAGNOSI -------- Se "TCP_FAIL": container non running, network isolation Se "FAIL" con TCP_OK: indirizzo sbagliato, tipo registro sbagliato, device non serve Se tutti OK con value=0: HIL non sta producendo valori INTERPRETAZIONE --------------- - value=[0] per sensori che dovrebbero avere dati → HIL non produce - Modbus error "IllegalAddress" → indirizzo configurato sbagliato - Modbus error "IllegalFunction" → tipo registro sbagliato (es. holding vs input) ================================================================================ FIX: PYMODBUS API COMPATIBILITY IN PROBE (tools/probe_modbus.py) ================================================================================ Data: 2026-01-30 PROBLEMA OSSERVATO ------------------ Il probe falliva con TypeError per TUTTI i target: TypeError: ModbusClientMixin.read_input_registers() got an unexpected keyword argument 'slave' TypeError: ... got an unexpected keyword argument 'unit' La versione di pymodbus nel container ICS-SimLab è diversa da quella locale. Diverse versioni di pymodbus hanno API diverse: - pymodbus 2.x: usa `unit` come keyword argument - pymodbus 3.x: usa `slave` come keyword argument - alcune versioni: accettano solo keyword args (address=, count=) - alcune versioni: accettano solo positional args SOLUZIONE IMPLEMENTATA ---------------------- File modificato: tools/probe_modbus.py Il probe ora prova 4 varianti API in sequenza: ```python api_variants = [ lambda: func(address=target.address, count=target.count), # keyword, no slave lambda: func(target.address, target.count), # positional only lambda: func(address=..., count=..., slave=target.slave_id), # pymodbus 3.x lambda: func(target.address, target.count, unit=target.slave_id), # pymodbus 2.x ] for api_call in api_variants: try: result = api_call() break except TypeError: continue ``` La prima variante che non solleva TypeError viene usata. VERIFICA -------- sudo ./scripts/e2e.sh cat outputs/run_*/probe.txt # Ora dovrebbe mostrare Modbus OK/FAIL con valori reali, non TypeError NOTA IMPORTANTE --------------- Questo fix rende il probe compatibile con qualsiasi versione di pymodbus. La prima variante (keyword args senza slave) è la più probabile per ICS-SimLab. STATUS ATTUALE -------------- - TCP reachable: 8/8 ✓ - Probe API compatibility: FIXATO (vedi sotto) - Prossimo: verificare che il probe mostri valori o errori Modbus reali ================================================================================ FIX: PYTHON LAMBDA CLOSURE BUG IN PROBE (tools/probe_modbus.py) ================================================================================ Data: 2026-01-30 PROBLEMA OSSERVATO ------------------ Nonostante il fix precedente con api_variants, il probe continuava a fallire con TypeErrors catturati dal handler Exception esterno invece che dal handler TypeError interno. Output osservato: Exception: TypeError: ModbusClientMixin.read_input_registers() got an unexpected keyword argument 'slave' Il formato "Exception: TypeError:" indica che l'errore era catturato da: except Exception as e: error=f"Exception: {type(e).__name__}: {e}" invece che dal loop interno: except TypeError as e: error=f"All API variants failed: {last_error}" CAUSA ROOT ---------- Le lambda Python catturano variabili per riferimento, non per valore. Anche se il codice sembrava corretto, qualche subtilità del closure Python causava il mancato catching dell'eccezione. SOLUZIONE IMPLEMENTATA ---------------------- File modificato: tools/probe_modbus.py Sostituito l'approccio con lambda: ```python api_variants = [ lambda: func(address=target.address, count=target.count), # NON FUNZIONAVA ... ] for api_call in api_variants: try: result = api_call() break except TypeError: continue ``` Con approccio esplicito if/else: ```python addr = target.address cnt = target.count sid = target.slave_id if result is None: try: result = func(address=addr, count=cnt) except TypeError as e: last_error = e result = None if result is None: try: result = func(addr, cnt) except TypeError as e: last_error = e result = None # ... etc per altre varianti ``` CORREZIONE SECONDARIA --------------------- File modificato: scripts/e2e.sh Bug: grep con pattern "Exception\|Traceback" produceva output multilinea causando errore bash "integer expression expected". Fix: Usato -E per extended regex e | head -1 per garantire singola linea: ```bash PLC1_CRASHES=$(grep -Ec "Exception|Traceback" "$RUN_DIR/plc1.log" 2>/dev/null | head -1 || echo "0") ``` VERIFICA -------- sudo ./scripts/e2e.sh cat outputs/run_*/probe.txt | head -30 # Aspettativa: vedere "All API variants failed" oppure "Modbus OK" con valori ================================================================================ FEATURE: CONTROL PLAN - LOGICA HIL DICHIARATIVA ================================================================================ Data: 2026-01-30 OBIETTIVO --------- Introdurre un nuovo artefatto "control_plan" che permetta di specificare la fisica HIL in modo dichiarativo (loop, azioni, profili) senza codice Python free-form. Il compilatore deterministico genera codice valido per ICS-SimLab. FILE CREATI ----------- models/control_plan.py - Schema Pydantic per ControlPlan v0.1 - ControlPlanHIL con init, params, tasks - Tasks: LoopTask, PlaybackTask - Actions: SetAction, AddAction, IfAction - Profiles: GaussianProfile, RampProfile, StepProfile tools/safe_eval.py - Parser/evaluator espressioni sicuro - Usa ast.parse per validare AST - Whitelist di nodi e funzioni (min, max, clamp, etc.) - Blocca import, attribute access, subscript tools/compile_control_plan.py - Compiler: control_plan.json → HIL *.py - CLI: --control-plan, --out, --config, --validate-only - Genera codice con while True loop - Threading automatico se >1 task - Warmup sleep opzionale tests/fixtures/control_plan_*.json - Test fixtures: - control_plan_bottle_like.json (tank + bottle) - control_plan_electrical_like.json (power grid) - control_plan_ied_like.json (IED con protezioni) tests/test_compile_control_plan.py - 24 test per schema, compiler, validation FILE MODIFICATI --------------- build_scenario.py - Aggiunto --control-plan argument - Step 2c: compila control_plan se presente - Auto-detect outputs/control_plan.json SCHEMA CONTROL PLAN v0.1 ------------------------ { "version": "v0.1", "hils": [{ "name": "...", // deve matchare hils[].name in config "warmup_s": 3.0, // opzionale, delay prima di tasks "init": {"var": value}, // valori iniziali physical_values "params": {"const": value}, // costanti (read-only) "tasks": [ { "type": "loop", "name": "physics", "dt_s": 0.1, "actions": [ {"set": ["var", "expr"]}, // var = expr {"add": ["var", "expr"]}, // var += expr {"if": "cond", "then": [...], "else": [...]} ] }, { "type": "playback", "name": "noise", "dt_s": 0.1, "target": "var", "profile": {"kind": "gaussian", "height": 50, "std": 2, "entries": 100} } ] }] } ESPRESSIONI SICURE ------------------ Allowlist funzioni: min, max, abs, int, float, bool, clamp Allowlist operatori: +, -, *, /, //, %, **, <, <=, >, >=, ==, !=, and, or, not Allowlist: ternary (x if cond else y) Bloccato: import, attribute access (x.attr), subscript (x[i]), lambda COMANDI ------- # Compila control_plan python3 -m tools.compile_control_plan \ --control-plan tests/fixtures/control_plan_bottle_like.json \ --out outputs/test_logic # Solo validazione python3 -m tools.compile_control_plan \ --control-plan tests/fixtures/control_plan_bottle_like.json \ --validate-only # Build scenario con control plan python3 build_scenario.py \ --out outputs/scenario_run \ --control-plan outputs/control_plan.json \ --overwrite # Test python3 -m pytest tests/test_compile_control_plan.py -v VANTAGGI -------- 1. LLM genera spec strutturata, non Python 2. Compilazione deterministica e verificabile 3. Validazione pre-runtime (variabili undefined, espressioni invalide) 4. Sicuro: espressioni parse con AST whitelist 5. Flessibile: loop, profili (Gaussian, ramp, step), condizionali 6. Threading automatico per task paralleli NOTA IMPORTANTE --------------- control_plan è alternativo a process_spec e IR per HIL. Se specificato, sovrascrive la logica HIL generata da IR. PLC logic continua a usare IR (per ora). VERIFICA -------- python3 -m pytest tests/test_compile_control_plan.py -v # ✅ 24 passed ================================================================================ E2E HARNESS: BOTTLE LINE CON CONTROL PLAN v0.1 ================================================================================ Data: 2026-01-30 OBIETTIVO --------- Creare un harness E2E riproducibile per testare la pipeline ControlPlan: control_plan.json → HIL python generato → scenario build → validate_logic Permette di validare l'intero flusso senza richiedere runtime Docker. FILE CREATI ----------- examples/control_plans/bottle_line_v0.1.json Control plan con 2 HIL: - water_hil: fisica serbatoio (level, input/output valves) - filler_hil: fisica bottiglia (fill level, at_filler, conveyor) Parametri realistici: tank 0-1000, bottle 0-200, conveyor 0-130 prompts/e2e_bottle.txt Prompt per LLM che genera configuration.json compatibile: - 2 PLC (plc1 tank, plc2 filler) - 2 HIL (water_hil, filler_hil) - Sensori e attuatori per ogni HIL - Nomi HIL esatti per matchare control_plan scripts/e2e_bottle_control_plan.sh Script E2E che: 1. (Opzionale) genera config via LLM 2. Builda scenario con control plan 3. Valida logica generata 4. Stampa comando per ICS-SimLab (non esegue) Opzioni: --skip-llm, --use-config PATH tests/test_e2e_bottle_control_plan.py 17 test di integrazione: - Control plan valido e compilabile - Python generato sintatticamente corretto - Funzione logic(physical_values) presente - while True loop presente - Warmup delay incluso - Clamp function inclusa COMANDI E2E ----------- # Solo compilazione (senza LLM) ./scripts/e2e_bottle_control_plan.sh --skip-llm --use-config examples/water_tank/configuration.json # Con LLM (richiede OPENAI_API_KEY) ./scripts/e2e_bottle_control_plan.sh # Test di integrazione (no Docker) python3 -m pytest tests/test_e2e_bottle_control_plan.py -v ARCHITETTURA SCENARIO BOTTLE LINE --------------------------------- PLC1 (192.168.100.21): - Legge water_tank_level da sensore - Controlla tank_input_valve, tank_output_valve - Logica isteresi: low=200, high=800 PLC2 (192.168.100.22): - Legge bottle_fill_level, bottle_at_filler - Controlla conveyor_cmd, fill_valve - Logica: riempi se at_filler e level < 180 water_hil (192.168.100.31): - Fisica tank: inflow +18, outflow -12 per step - Range 0-1000, init 500 filler_hil (192.168.100.32): - Fisica bottle: fill +8 per step, max 200 - Conveyor: distance -= 5, reset a 130 quando < 0 - at_filler = 1 se distance <= 30 REPO HYGIENE ------------ - Pulito .gitignore (rimossi duplicati) - .claude/settings.local.json già in .gitignore - Solo appunti.txt (lowercase) e diario.md (come da CLAUDE.md) VERIFICA -------- python3 -m pytest tests/test_e2e_bottle_control_plan.py -v # ✅ 17 passed python3 -m tools.compile_control_plan \ --control-plan examples/control_plans/bottle_line_v0.1.json \ --validate-only # ✅ Validation: OK PROSSIMI STEP ------------- - [ ] Test E2E con LLM (richiede OPENAI_API_KEY) - [ ] Test runtime con ICS-SimLab - [ ] Aggiungere altri esempi control_plan (ied, power_grid) ================================================================================ FIX: HIL INIT NON RILEVATA DA VALIDATOR (pv alias vs physical_values) ================================================================================ Data: 2026-02-02 PROBLEMA OSSERVATO ------------------ tools.validate_logic --check-hil-init falliva su HIL generati da compile_control_plan.py: - water_hil.py: water_tank_level - filler_hil.py: bottle_fill_level, bottle_at_filler, bottle_distance ROOT CAUSE ---------- Il compilatore generava: pv = physical_values # Alias pv['water_tank_level'] = 500 Ma il validatore (services/validation/hil_init_validation.py) usa AST parser che cerca SPECIFICAMENTE `physical_values[...]`, non alias come `pv[...]`. Pattern riconosciuti dal validatore: - physical_values["x"] = ... - physical_values.setdefault("x", ...) - physical_values.update({...}) NON riconosciuti: - pv["x"] = ... (alias!) SOLUZIONE IMPLEMENTATA ---------------------- File modificato: tools/compile_control_plan.py 1) compile_hil() ora accetta parametro opzionale config_physical_values: Set[str] - Se fornito, inizializza TUTTI i keys dal config (non solo plan.init) 2) Genera `physical_values.setdefault('', )` PRIMA dell'alias: - PRIMA: pv = physical_values; pv['key'] = value - DOPO: physical_values.setdefault('key', value); pv = physical_values 3) Valori default: - Se key in plan.init → usa valore da init - Se key solo in config → usa 0 come default 4) compile_control_plan() passa config physical_values a compile_hil() usando get_hil_physical_values_keys() già esistente FILE TEST CREATI ---------------- tests/test_compile_control_plan_hil_init.py - 5 test regressione: - test_compiled_hil_passes_init_validation - test_compiled_hil_contains_setdefault_calls - test_setdefault_before_alias - test_init_value_preserved_from_plan - test_config_only_keys_initialized_with_default tests/fixtures/config_hil_bottle_like.json - Config fixture minimo per test FILE MODIFICATI --------------- tests/test_compile_control_plan.py - Aggiornato test esistente che cercava pv['tank_level'] = 500 → ora cerca physical_values.setdefault('tank_level', 500) CODICE GENERATO ORA ------------------- def logic(physical_values): """...""" # === Initialize physical values (validator-compatible) === physical_values.setdefault('bottle_at_filler', 1) physical_values.setdefault('bottle_distance', 0) physical_values.setdefault('bottle_fill_level', 0) physical_values.setdefault('conveyor_cmd', 0) physical_values.setdefault('fill_valve', 0) pv = physical_values # Alias for generated code # ... rest of logic VERIFICA -------- # Test di regressione (29 test) python3 -m pytest tests/test_compile_control_plan.py tests/test_compile_control_plan_hil_init.py -v # ✅ 29 passed # Validazione su scenario compilato python3 -m tools.compile_control_plan \ --control-plan examples/control_plans/bottle_line_v0.1.json \ --config tests/fixtures/config_hil_bottle_like.json \ --out /tmp/test_logic \ --overwrite python3 -m tools.validate_logic \ --config tests/fixtures/config_hil_bottle_like.json \ --logic-dir /tmp/test_logic \ --check-hil-init # ✅ OK: logica coerente con configuration.json NOTA IMPORTANTE --------------- Se config non fornito a compile_control_plan, inizializza solo keys da plan.init. Per garantire validazione OK con --check-hil-init, passare sempre --config. ================================================================================ VALIDAZIONE NETWORK: DUPLICATE IP E SUBNET ================================================================================ Data: 2026-02-02 PROBLEMA OSSERVATO ------------------ ICS-SimLab docker-compose fallisce con: "failed to set up container networking: Address already in use" Root cause: più device con stesso IP sulla stessa docker_network. Esempio trovato in outputs/scenario_bottle_cp/configuration.json: - ui.network.ip = 192.168.100.10 - hmis[0].network.ip = 192.168.100.10 → DUPLICATO! SOLUZIONE IMPLEMENTATA ---------------------- 1) Nuovo validatore in tools/semantic_validation.py: - validate_network_config(config) -> List[SemanticError] Controlla: - IP duplicati entro stessa docker_network - docker_network referenziato esiste in ip_networks[] - IP è dentro la subnet dichiarata (usa ipaddress module) 2) Nuovo CLI: tools/check_networking.py python3 -m tools.check_networking --config [--strict] [--json] Exit codes: 0 = OK 1 = Issues found 2 = Config file error 3) Integrazione in validate_all_semantics(): Network validation è P0, eseguita PRIMA degli altri check. build_config.py già chiama validate_all_semantics(), quindi la pipeline fallisce automaticamente su IP duplicati. FILE CREATI ----------- - tools/check_networking.py - CLI per validazione network - tests/test_network_validation.py - 11 test per network validation - tests/fixtures/config_duplicate_ip.json - IP duplicati (ui + hmi stesso IP) - tests/fixtures/config_out_of_subnet_ip.json - IP fuori subnet - tests/fixtures/config_unknown_docker_network.json - docker_network non esistente FILE MODIFICATI --------------- - tools/semantic_validation.py - Aggiunto validate_network_config() VERIFICA -------- # Test di regressione python3 -m pytest tests/test_network_validation.py tests/test_semantic_validation_p0.py -v # ✅ 43 passed # CLI su config con problema python3 -m tools.check_networking --config outputs/scenario_bottle_cp/configuration.json # Output: # NETWORK VALIDATION ISSUES (1): # - network[ot_network]: Duplicate IP 192.168.100.10: ui (ui), hmi_supervision (hmi). # CLI JSON output python3 -m tools.check_networking --config outputs/scenario_bottle_cp/configuration.json --json # {"config": "...", "issues": [...], "status": "error"} PROSSIMI STEP ------------- - [ ] Implementare auto-repair per assegnare IP liberi (Task D opzionale) - [ ] Fixare il config scenario_bottle_cp assegnando IP diverso a HMI