273 lines
12 KiB
JSON
273 lines
12 KiB
JSON
{
|
|
"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(<directory>/logic/<plc[logic]>, <container>/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(<directory>/logic/<hil[logic]>, <container>/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": "<sensor['hil']>",
|
|
"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": "<actuator['hil']>",
|
|
"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."
|
|
}
|
|
]
|
|
}
|