diff --git a/doc/awl2scl.md b/doc/awl2scl.md new file mode 100644 index 0000000..f49a176 --- /dev/null +++ b/doc/awl2scl.md @@ -0,0 +1,317 @@ +# awl2scl — AWL/STL → SCL Konvertierung + +Werkzeuge zur (halb-)automatischen Umwandlung der Siemens-AWL/STL-Bausteine +dieses Projekts nach SCL für den Umstieg auf TIA Portal V20. + +- **`lib/stlxml2awl.py`** — wandelt TIA-Openness-XML (SimaticML `StatementList`) + in lesbaren AWL-Text (`.awl`) um und schätzt je Baustein eine Aufwands­kategorie + A/B/C. Aufruf über `bin\xmls2awl.bat`. +- **`lib/awl2scl.py`** — wandelt `.awl`-Dateien mechanisch nach `.scl` um, optional + mit Lesbarkeits- und Parameter-Optimierung. Aufruf über `bin\awl2scl.bat` bzw. + `bin\awl2scl_deep.bat`. +- **`lib/sclopt.py`** — SCL-Zwischenstruktur (IR) und die `--improve-gotos`-Durchläufe. +- **`lib/paramize.py`** — die `--globals2params`-Umwandlung samt persistenter + toChange-Liste für die Aufrufer. + +> **Wichtig:** Das Werkzeug erzeugt syntaktisch zielgerichteten, mechanisch +> verhaltensgleichen SCL-Text — **keine** in TIA verifizierte Korrektheit. Die +> Freigabe (Kompilat in TIA Portal V20, PLCSIM-Vergleich AWL↔SCL) bleibt ein +> manueller Schritt pro Baustein (siehe `SCL-Export/README.md`). + +## Inhaltsverzeichnis + +- [Datenfluss](#datenfluss) +- [Kategorien A/B/C](#kategorien-abc) +- [`bin`-Skripte](#bin-skripte) +- [CLI-Optionen](#cli-optionen-libawl2sclpy) +- [`--improve-gotos`](#--improve-gotos) +- [`--globals2params`](#--globals2params) +- [Schutz handgeschriebener Dateien](#schutz-handgeschriebener-dateien) +- [Log & Ausgaben](#log--ausgaben) + +## Datenfluss + +``` + TIA-Openness-XML lib/stlxml2awl.py lib/awl2scl.py + (SimaticML StatementList) ───────────────► *.awl ───────────────► *.scl + (bin\xmls2awl.bat) (bin\awl2scl.bat / + bin\awl2scl_deep.bat) +``` + +`awl2scl.py` liest **zwei** `.awl`-Formate transparent ein: + +1. **gerendert** — von `stlxml2awl.py` erzeugt, Kopf `=== FC 181: Name [STL] ===`. +2. **nativ** — klassische STEP7/TIA-AWL-Quelle, Kopf `FUNCTION_BLOCK "Name"`, + `BEGIN`/`NETWORK`, deutsche Mnemonik (`U`/`UN`/`O`/`SPA`/`SPB`/`AUF` …). + +## Kategorien A/B/C + +Die Kategorie wird je Datei **selbst** bestimmt (nicht aus einem Log übernommen), +mit denselben Kriterien wie in `stlxml2awl.py` (Namensmuster aus +`SCL-Export/README.md` + Sprung-/Pointer-Analyse): + +| Kat. | Bedeutung | Behandlung | +|------|-----------|------------| +| **A** | einfache Verknüpfungslogik, mechanisch übersetzbar | wird übersetzt | +| **B** | Ablaufsteuerung / viele Sprungmarken | nur mit `--category B` | +| **C** | Pointer-/Register-Indirektzugriffe | nur mit `--category C` | + +Bausteine, die trotz passender Kategorie ein technisch nicht sicher übersetzbares +Konstrukt enthalten (Timer/Zähler-Mnemonik, `OPN DB[var]`, registerindirekte +Operanden, unbekannte Sprünge), werden **nicht** übersetzt, sondern im Log unter +**„Manuelle Prüfung nötig"** mit Grund gelistet. + +## `bin`-Skripte + +| Skript | Zweck | +|--------|-------| +| `bin\xmls2awl.bat` | XML → `.awl` (+ Kategorie-Log) | +| `bin\awl2scl.bat` | `.awl` → `.scl`, **mechanisch** (Argumente durchgereicht) | +| `bin\awl2scl_deep.bat` | `.awl` → `.scl` **mit** `--improve-gotos --globals2params` | + +Beispiele: + +```bat +REM Nur Kategorie A, mechanisch nach SCL-Export\\ +bin\awl2scl.bat --dir . --category A + +REM Tiefenlauf: Kontrollfluss aufräumen + Globale zu Parametern +bin\awl2scl_deep.bat --dir . --category A +``` + +## CLI-Optionen (`lib/awl2scl.py`) + +| Option | Wirkung | +|--------|---------| +| ` …` | einzelne Dateien | +| `--dir ` | rekursiv alle `.awl` | +| `--category A[,B,C]` | zu übersetzende Kategorien (Default `A`) | +| `--target-dir ` | Zielordner (Default `$PV_SCL_EXPORT` bzw. `\SCL-Export`) | +| `--improve-gotos` | Kontrollfluss aufräumen (verlustfrei) | +| `--globals2params` | globale Merker → Parameter + toChange für Aufrufer | +| `--force` | vorhandene **generierte** `.scl` überschreiben | +| `--tochange ` | toChange-Liste (Default `\data\globals2params_tochange.json`) | +| `--log ` | Log-Datei | + +`--improve-gotos` und `--globals2params` implizieren `--force` (Regenerierung). +Der Zielpfad je Baustein ist `\\.scl`. + +--- + +## `--improve-gotos` + +Räumt den mechanisch erzeugten Kontrollfluss **verlustfrei** auf. Die Übersetzung +läuft dazu über eine typisierte Zwischenstruktur (IR, `lib/sclopt.py`); ohne den +Schalter ist die Ausgabe byte-identisch zur rein mechanischen Variante. Durchläufe: + +- **`merge_adjacent_ifdo`** — aufeinanderfolgende `IF`-Blöcke mit identischer + Bedingung zusammenfassen (aus dem AWL-Idiom `S x; S y; JC L` entsteht sonst + dieselbe Bedingung mehrfach). +- **`guard_skip_to_if`** — `IF c THEN GOTO L; … ; L:` → `IF NOT c THEN … END_IF`. +- **`synth_case`** / **`synth_case_across_networks`** — Verteilung auf **eine** + Variable gegen Konstanten → `CASE` (auch über REGION-Grenzen hinweg; die + REGION-Titel werden zu Branch-Kommentaren). +- **`drop_unused_labels`** — nicht mehr angesprungene Marken entfernen. +- **`normalize_bool`** — überflüssige Klammern und `NOT`-Verschachtelungen + vereinfachen (`NOT (a <> b)` → `a = b`). + +Nicht sauber auflösbare Sprünge (Rücksprünge, gemeinsame Sammel-Austrittsmarken) +bleiben als strukturierter `GOTO` erhalten — die Passes greifen nur beim sicher +erkannten Muster. + +### Beispiel 1 — `HWtoAMRZielFC` (FC 181): Guard-Sprünge → `CASE` + +Zwei Netzwerke prüfen dieselbe Variable `"MW190"` gegen Konstanten. Mechanisch: + +```scl + REGION Schleuse 3004: (BST 3) + IF ("MW190" <> 3004) THEN GOTO X3004E; END_IF; + "M180.6" := TRUE; + RETURN; + X3004E: ; + END_REGION + + REGION Schleuse 3565: (BST 4) + IF ("MW190" <> 3565) THEN GOTO X3565E; END_IF; + "M180.6" := TRUE; + RETURN; + X3565E: ; + END_REGION +``` + +Mit `--improve-gotos` (Guard→IF + netzwerkübergreifende `CASE`-Synthese): + +```scl + REGION CASE "MW190" + CASE "MW190" OF + 3004: // Schleuse 3004: (BST 3) + "M180.6" := TRUE; + 3565: // Schleuse 3565: (BST 4) + "M180.6" := TRUE; + END_CASE; + END_REGION +``` + +### Beispiel 2 — `MBlinken` (FB): mehrfache Bedingung zusammenfassen + +Das AWL setzt mit *einer* Verknüpfung drei Merker und springt (`R x; R y; R z; SPB L`). +Mechanisch entsteht viermal dieselbe Bedingung: + +```scl + REGION Anzeige bei Error 1-10 1-10 mal blinken lassen + IF (((((((((NOT (#Error_1) AND NOT (#Error_2)) AND …) AND NOT (#Error_10))) THEN #hAnzeige := FALSE; END_IF; + IF (((((((((NOT (#Error_1) AND NOT (#Error_2)) AND …) AND NOT (#Error_10))) THEN #Flanke_Ausschalttakt := FALSE; END_IF; + IF (((((((((NOT (#Error_1) AND NOT (#Error_2)) AND …) AND NOT (#Error_10))) THEN #Kein_Blinken := FALSE; END_IF; + IF (((((((((NOT (#Error_1) AND NOT (#Error_2)) AND …) AND NOT (#Error_10))) THEN GOTO NoAn; END_IF; +``` + +Mit `--improve-gotos` (`merge_adjacent_ifdo` + Klammer-Normalisierung): + +```scl + REGION Anzeige bei Error 1-10 1-10 mal blinken lassen + IF ((((((((NOT (#Error_1) AND NOT (#Error_2)) AND …) AND NOT (#Error_9)) AND NOT (#Error_10) THEN + #hAnzeige := FALSE; + #Flanke_Ausschalttakt := FALSE; + #Kein_Blinken := FALSE; + GOTO NoAn; + END_IF; +``` + +(Das verbleibende `GOTO NoAn` zeigt auf eine gemeinsame Austrittsmarke und bleibt +bewusst als strukturierter Sprung erhalten.) + +--- + +## `--globals2params` + +Baut **globale Merker-Zugriffe** eines Bausteins in echte Parameter um und +verwaltet eine persistente **toChange-Liste** (`data\globals2params_tochange.json`) +für die Anpassung der Aufrufer — auch solcher, die noch als `.awl` (nicht +konvertiert) vorliegen. + +**Ablauf je Baustein** (`lib/paramize.py`): + +1. **Richtungsanalyse:** jedes Global (`"MW190"`, `"M180.6"`, `Mx.y`, `Ex`, `Ax` …) + wird als `Input` (nur gelesen), `Output` (nur geschrieben) oder `InOut` (beides, + bzw. uneindeutig — konservativ) eingestuft. +2. **Parametername** deterministisch aus dem Global-Token (`"MW190"`→`MW190`, + `"M180.6"`→`M180_6`), Kommentar `// war "…"`. Semantische Umbenennung bleibt manuell. +3. **Rumpf umschreiben:** jeder Global-Zugriff wird `#Param`. +4. **Aufrufer finden** (direkte `CALL "B"` und indirekte `UC/CC FC[..]`) über **alle** + `.awl`-Quellen und je Aufrufer den parameterbasierten Aufruf vormerken. + +**Verhaltenserhaltend by construction:** Jeder Aufrufer forwardet exakt dasselbe +Global an den Parameter (`Input` mit `:=`, `Output`/`InOut` mit `=>`), solange er +das Global vor/nach dem Aufruf wie bisher nutzt. + +**Grenze:** indirekte Aufrufe (`UC/CC FC[var]`, z.B. die CASE-Verteiler in +`MHMOmniSchleuse`) können nicht automatisch umgeschrieben werden → in toChange als +`manual` markiert. + +### Beispiel 1 — `HWtoAMRZielFC`: globale Merker werden Parameter + +`--globals2params` (hier mit `--improve-gotos` kombiniert) erzeugt aus dem Baustein, +der `"MW190"` liest und `"M180.6"` schreibt: + +```scl +FUNCTION "HWtoAMRZielFC" : Void + VAR_INPUT + MW190 : Word; // war "MW190" + END_VAR + VAR_OUTPUT + M180_6 : Bool; // war "M180.6" + END_VAR +BEGIN + REGION CASE #MW190 + CASE #MW190 OF + 3004: // Schleuse 3004: (BST 3) + #M180_6 := TRUE; + 3565: // Schleuse 3565: (BST 4) + #M180_6 := TRUE; + END_CASE; + END_REGION + … +``` + +`HWtoAMRZielFC` wird nur **indirekt** (über `UC FC[..]`-Verteiler) aufgerufen — +daher ein `manual`-Eintrag in der toChange-Liste: + +```json +"=A01+UH07-KF00|DatenIn": { + "path": "…\\DatenIn.awl", + "changes": [ + { "func": "HWtoAMRZielFC", + "call": "// MANUELL: indirekter Aufruf von HWtoAMRZielFC auf Parameter umstellen", + "status": "manual" } + ] +} +``` + +### Beispiel 2 — direkter Aufrufer wird automatisch umgeschrieben (2-Phasen) + +Ein Baustein `Sub_Callee`, der `"MW50"` liest und `"M60.0"` schreibt, und ein +direkter Aufrufer `Top_Caller` (`CALL FC "Sub_Callee"`). + +**Lauf 1** — `Sub_Callee` wird parametrisiert; der Aufruf-Rewrite wird als +`pending` in `data\globals2params_tochange.json` vermerkt: + +```scl +FUNCTION "Sub_Callee" : Void + VAR_INPUT + MW50 : Word; // war "MW50" + END_VAR + VAR_OUTPUT + M60_0 : Bool; // war "M60.0" + END_VAR +BEGIN + REGION test + #M60_0 := (#MW50 >= 100); + END_REGION +``` + +```json +"_|Top_Caller": { + "changes": [ + { "func": "Sub_Callee", + "call": "\"Sub_Callee\"(M60_0 => \"M60.0\", MW50 := \"MW50\");", + "status": "pending" } + ] +} +``` + +**Lauf 2** — beim Konvertieren von `Top_Caller` wird der vorgemerkte Rewrite +angewandt (Status `pending` → `applied`); aus dem mechanischen `"Sub_Callee"();` +wird der parameterbasierte Aufruf: + +```scl + REGION ruft callee + "Sub_Callee"(M60_0 => "M60.0", MW50 := "MW50"); + END_REGION +``` + +So bleibt die toChange-Liste über mehrere Konvertierungsläufe hinweg gültig, bis +jeder Aufrufer konvertiert und sein Aufruf angepasst wurde. + +--- + +## Schutz handgeschriebener Dateien + +Generierte `.scl` tragen die Kopfzeile +`// Mechanische AWL->SCL-Konvertierung (awl2scl.py)`. Mit `--force` werden **nur** +Dateien mit diesem Marker überschrieben; handgeschriebene Piloten (z.B. +`SCL-Export\=A01+UH06-KF00\MZylinder.scl`, `MHMOmniSchleuse.scl`, `FifoIn.scl`, +`DatenOut.scl`) bleiben unangetastet und erscheinen im Log unter +**„Geschützt, handgeschriebene .scl nicht überschrieben"**. + +## Log & Ausgaben + +Jeder Lauf schreibt `log\awl2scl_.log` mit: + +- Kopfstatistik (angeforderte Kategorien, aktive Schalter, Zähler), +- **Manuelle Prüfung nötig** (Baustein + Grund), +- **Parametrisiert** (neue Parameter je Baustein + Anzahl Aufrufer/davon manuell), +- **GOTO-lastig** (viele Sprünge, nur ohne `--improve-gotos`), +- **Geschützt** / **Übersprungen** (Kategorie bzw. Ziel existiert), +- **Fehler**. + +Bei `--globals2params` zusätzlich der Pfad der toChange-Liste. diff --git a/doc/awl2scl.pdf b/doc/awl2scl.pdf new file mode 100644 index 0000000..640d418 Binary files /dev/null and b/doc/awl2scl.pdf differ