ics-simlab-config-gen-claude/spec/ics_simlab_contract.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."
}
]
}