894 lines
30 KiB
Plaintext
894 lines
30 KiB
Plaintext
================================================================================
|
||
APPUNTI OPERAZIONI - ics-simlab-config-gen_claude
|
||
================================================================================
|
||
Data: 2026-01-27
|
||
================================================================================
|
||
|
||
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
|
||
|
||
|
||
================================================================================
|