1640 lines
54 KiB
Plaintext
1640 lines
54 KiB
Plaintext
================================================================================
|
||
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_<timestamp>/
|
||
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_<ts>/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('<key>', <default>)` 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 <path> [--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
|