{ "meta": { "title": "ICS-SimLab: knowledge base da 3 esempi (water tank, smart grid, IED)", "created_at": "2026-01-26", "timezone": "Europe/Rome", "scope": "Regole e pattern per generare file logic/*.py coerenti con configuration.json (Curtin ICS-SimLab) + considerazioni operative per pipeline e validazione", "status": "living_document", "notes": [ "Alcune regole sono inferite dai pattern dei 3 esempi; la conferma definitiva si ottiene leggendo src/components/plc.py e src/components/hil.py." ] }, "examples": [ { "name": "water_tank", "focus": [ "PLC con controllo a soglia su livello", "HIL con dinamica semplice + thread", "RTU per sensori/attuatori, TCP per HMI/PLC (pattern generale)" ] }, { "name": "smart_grid", "focus": [ "PLC con commutazione (transfer switch) e state flag", "HIL con profilo temporale (segnale) + thread", "HMI principalmente read-only (monitor)" ] }, { "name": "ied", "focus": [ "Mix tipi Modbus nello stesso PLC (coil, holding_register, discrete_input, input_register)", "Evidenza che input_registers/output_registers sono raggruppati per io (input/output) e non per tipo Modbus", "Attenzione a collisioni di nomi variabile/funzione helper (bug tipico)" ] } ], "setup_py_observed_behavior": { "plc": { "copies_logic": "shutil.copy(/logic/, /src/logic.py)", "copies_runtime": [ "src/components/plc.py", "src/components/utils.py" ], "implication": "Ogni PLC ha un solo file di logica effettivo dentro il container: src/logic.py (scelto via plc['logic'] nel JSON)." }, "hil": { "copies_logic": "shutil.copy(/logic/, /src/logic.py)", "copies_runtime": [ "src/components/hil.py", "src/components/utils.py" ], "implication": "Ogni HIL ha un solo file di logica effettivo dentro il container: src/logic.py (scelto via hil['logic'] nel JSON)." }, "sensors": { "logic_is_generic": true, "copies_runtime": [ "src/components/sensor.py", "src/components/utils.py" ], "generated_config_json": { "database.table": "", "inbound_connections": "sensor['inbound_connections']", "registers": "sensor['registers']" }, "implication": "Il comportamento specifico non sta nei sensori: il sensore è un trasduttore genericizzato (physical_values -> Modbus)." }, "actuators": { "logic_is_generic": true, "copies_runtime": [ "src/components/actuator.py", "src/components/utils.py" ], "generated_config_json": { "database.table": "", "inbound_connections": "actuator['inbound_connections']", "registers": "actuator['registers']" }, "implication": "Il comportamento specifico non sta negli attuatori: l'attuatore è un trasduttore genericizzato (Modbus -> physical_values).", "note": "Se nel JSON esistesse actuator['logic'], dagli estratti visti non viene copiato; quindi è ignorato oppure gestito altrove nel setup.py completo." } }, "core_contract": { "principle": "Il JSON definisce l'interfaccia (nomi/id, io, connessioni, indirizzi). La logica implementa solo comportamenti usando questi nomi.", "addressing_rule": { "in_code_access": "Per accedere ai segnali nel codice si usano gli id/nome (stringhe), non gli address Modbus.", "in_json": "Gli address e le connessioni (TCP/RTU) vivono nel configuration.json." }, "plc_logic": { "required_function": "logic(input_registers, output_registers, state_update_callbacks)", "data_model_assumption": { "input_registers": "dict: id -> { 'value': <...>, ... }", "output_registers": "dict: id -> { 'value': <...>, ... }", "state_update_callbacks": "dict: id -> callable" }, "io_rule": { "read": "Leggere solo id con io:'input' (presenti in input_registers).", "write": "Scrivere solo id con io:'output' (presenti in output_registers)." }, "callback_rule": { "must_call_after_write": true, "description": "Dopo ogni modifica a output_registers[id]['value'] chiamare state_update_callbacks[id]().", "why": "Propaga il cambiamento ai controller/alla rete (pubblica lo stato)." }, "grouping_rule_inferred": { "statement": "input_registers/output_registers sembrano raggruppati per campo io (input/output) e non per tipo Modbus.", "evidence": "Nell'esempio IED un holding_register con io:'input' viene letto da input_registers['tap_change_command'].", "confidence": 0.8, "verification_needed": "Confermare leggendo src/components/plc.py (costruzione dei dict)." }, "recommended_skeleton": [ "sleep iniziale breve (sync)", "loop infinito: leggi input -> calcola -> scrivi output + callback -> sleep dt" ] }, "hil_logic": { "required_function": "logic(physical_values)", "physical_values_model": "dict: name -> value", "init_rule": { "must_initialize_all_keys_from_json": true, "description": "Inizializzare tutte le chiavi definite in hils[].physical_values (almeno quelle usate)." }, "io_rule": { "update_only_outputs": "Aggiornare dinamicamente solo physical_values con io:'output'.", "read_inputs": "Leggere come condizioni/ingressi solo physical_values con io:'input'." }, "runtime_pattern": [ "sleep iniziale breve", "thread daemon per simulazione fisica", "update periodico con dt fisso (time.sleep)" ] } }, "networking_and_protocol_patterns": { "default_choice": { "field_devices": "Modbus RTU (sensori/attuatori come slave)", "supervision": "Modbus TCP (HMI <-> PLC) tipicamente su port 502" }, "supported_topologies_seen": [ "HMI legge PLC via Modbus/TCP (monitors).", "PLC legge sensori via Modbus/RTU (monitors).", "PLC comanda attuatori via Modbus/RTU (controllers).", "Un PLC può comandare un altro PLC via Modbus/TCP (PLC->PLC controller)." ], "address_mapping_note": { "statement": "Indirizzi interni al PLC e indirizzi remoti dei device possono differire; nel codice si usa sempre l'id.", "impact": "Il generator di logica non deve ragionare sugli address." } }, "common_patterns_to_reuse": { "plc_patterns": [ { "name": "threshold_control", "from_example": "water_tank", "description": "Se input < low -> apri/attiva; se > high -> chiudi/disattiva (con isteresi se serve)." }, { "name": "transfer_switch", "from_example": "smart_grid", "description": "Commutazione stato in base a soglia, con flag per evitare spam di callback (state_change)." }, { "name": "ied_command_application", "from_example": "ied", "description": "Leggi comandi (anche da holding_register input), applica a uscite (coil/holding_register output)." } ], "hil_patterns": [ { "name": "simple_dynamics_dt", "from_example": "water_tank", "description": "Aggiorna variabile fisica con dinamica semplice (es. livello) in funzione di stati valvole/pompe." }, { "name": "profile_signal", "from_example": "smart_grid", "description": "Genera un segnale nel tempo (profilo) e aggiorna physical_values periodicamente." }, { "name": "logic_with_inputs_cutoff", "from_example": "ied", "description": "Usa input (breaker_state, tap_position) per determinare output (tensioni)." } ], "hmi_patterns": [ { "name": "read_only_hmi", "from_example": "smart_grid", "description": "HMI solo monitors, nessun controller, per supervisione passiva." } ] }, "pitfalls_and_quality_rules": { "name_collisions": { "problem": "Collisione tra variabile e funzione helper (es: tap_change variabile che schiaccia tap_change funzione).", "rule": "Nomi helper devono essere univoci; usare prefissi tipo apply_, calc_, handle_." }, "missing_callbacks": { "problem": "Scrivere un output senza chiamare la callback può non propagare il comando.", "rule": "Ogni write su output -> callback immediata." }, "missing_else_in_physics": { "problem": "In HIL, gestire solo ON e non OFF può congelare lo stato (es. household_power resta = solar_power).", "rule": "Copri sempre ON/OFF e fallback." }, "uninitialized_keys": { "problem": "KeyError o stato muto se physical_values non inizializzati.", "rule": "In HIL inizializza tutte le chiavi del JSON." }, "overcomplicated_first_iteration": { "problem": "Scenario troppo grande rende debugging impossibile.", "rule": "Partire minimale (pochi segnali), poi espandere." } }, "recommended_work_order": { "default": [ "1) Definisci JSON (segnali/id + io + connessioni + mapping registers/physical_values).", "2) Estrai interfacce attese (sets di input/output per PLC; input/output fisici per HIL).", "3) Genera logica da template usando SOLO questi nomi.", "4) Valida (statico + runtime mock).", "5) Esegui in ICS-SimLab e itera." ], "why_json_first": "Il JSON è la specifica dell'interfaccia: decide quali id esistono e quali file di logica vengono caricati." }, "validation_strategy": { "static_checks": [ "Tutti gli id usati in input_registers[...] devono esistere nel JSON con io:'input'.", "Tutti gli id usati in output_registers[...] devono esistere nel JSON con io:'output'.", "Tutte le chiavi physical_values[...] usate nel codice HIL devono esistere in hils[].physical_values.", "No collisioni di nomi con funzioni helper (best effort: linting + regole naming)." ], "runtime_mock_checks": [ "Eseguire logic() PLC con dizionari mock e verificare che non crashi.", "Tracciare chiamate callback e verificare che ogni output write abbia callback associata.", "Eseguire logic() HIL per pochi cicli verificando che aggiorni solo io:'output' (best effort)." ], "golden_fixtures": [ "Usare i 3 esempi (water_tank, smart_grid, ied) come test di regressione." ] }, "project_organization_decisions": { "repo_strategy": { "choice": "stesso repo, moduli separati", "reason": "JSON e logica devono evolvere insieme; test end-to-end e fixture condivisi evitano divergenze." }, "suggested_structure": { "src/ics_config_gen": "generazione e repair configuration.json", "src/ics_logic_gen": "estrazione interfacce + generatori logica + validator", "examples": "golden fixtures (3 scenari)", "spec": "contract/patterns/pitfalls", "tests": "static + runtime mock + regression sui 3 esempi", "tools": "CLI: generate_json.py, generate_logic.py, validate_all.py" } }, "open_questions_to_confirm_in_code": [ { "question": "Come vengono costruiti esattamente input_registers e output_registers nel runtime PLC?", "where_to_check": "src/components/plc.py", "why": "Confermare la regola di raggruppamento per io (input/output) e la struttura degli oggetti register." }, { "question": "Come viene applicata la callback e cosa aggiorna esattamente (controller publish)?", "where_to_check": "src/components/plc.py e/o utils.py", "why": "Capire gli effetti di callback mancanti o chiamate ripetute." }, { "question": "Formato esatto di sensor.py/actuator.py: come mappano registers <-> physical_values?", "where_to_check": "src/components/sensor.py, src/components/actuator.py", "why": "Utile per generator di JSON e per scalings coerenti." } ] }