simaticml-decoder 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- simaticml_decoder/__init__.py +13 -0
- simaticml_decoder/cli.py +113 -0
- simaticml_decoder/emit.py +365 -0
- simaticml_decoder/fold.py +652 -0
- simaticml_decoder/instructions.py +105 -0
- simaticml_decoder/ir.py +182 -0
- simaticml_decoder/model.py +264 -0
- simaticml_decoder/operand.py +141 -0
- simaticml_decoder/parse.py +596 -0
- simaticml_decoder/scl_reconstruct.py +75 -0
- simaticml_decoder-0.1.0.dist-info/METADATA +118 -0
- simaticml_decoder-0.1.0.dist-info/RECORD +14 -0
- simaticml_decoder-0.1.0.dist-info/WHEEL +4 -0
- simaticml_decoder-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""The instruction catalog — deliberately *data, not logic*.
|
|
2
|
+
|
|
3
|
+
fold.py reasons about pin *categories* (power-flow in/out, comparison pre/out,
|
|
4
|
+
box en/eno, flip-flop, OR-junction), never about specific instruction names.
|
|
5
|
+
This table maps Part Name -> category + pin vocabulary + a render hint. Adding a
|
|
6
|
+
new instruction (TOF, CTU, NContact, ...) is a row here, not a change to the
|
|
7
|
+
traversal. That is what lets v0's "parse-but-flag everything else" degrade
|
|
8
|
+
gracefully: a name absent from this table folds to an ir.Unhandled node.
|
|
9
|
+
|
|
10
|
+
Pin names per the Part Pin Vocabulary in SIMATICML_READING_GUIDE.md. Entries are
|
|
11
|
+
the confirmed instructions from the six sample blocks plus their clearly-symmetric
|
|
12
|
+
siblings (TOF/TP, Sr, NBox/NContact, the comparison family).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from enum import Enum
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Category(str, Enum):
|
|
22
|
+
POWER_FLOW = "power_flow" # in/out, contact-like: passes or blocks power
|
|
23
|
+
COIL = "coil" # in/out, writes its operand
|
|
24
|
+
OR_JUNCTION = "or_junction" # merges parallel branches (Part Name="O")
|
|
25
|
+
COMPARISON = "comparison" # pre/out, contact-like compare
|
|
26
|
+
EDGE = "edge" # pre/out or in/out, rising/falling detection
|
|
27
|
+
FLIPFLOP = "flipflop" # named set/reset inputs -> q
|
|
28
|
+
BOX = "box" # en/eno operation box (Move/Add/Inc/timers/system FC)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class Spec:
|
|
33
|
+
name: str
|
|
34
|
+
category: Category
|
|
35
|
+
power_in: str | None = None # name of the power-in pin, if any
|
|
36
|
+
power_out: str | None = None # name of the power-out pin, if any
|
|
37
|
+
render: str | None = None # operator/keyword hint for emit (":=", "<", ...)
|
|
38
|
+
pins: tuple[str, ...] = field(default_factory=tuple) # informative pin list
|
|
39
|
+
note: str = ""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Helper constructors keep the table compact and readable.
|
|
43
|
+
def _pf(name, render=None):
|
|
44
|
+
return Spec(name, Category.POWER_FLOW, "in", "out", render, ("in", "operand", "out"))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _coil(name, render):
|
|
48
|
+
return Spec(name, Category.COIL, "in", "out", render, ("in", "operand", "out"))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _cmp(name, op):
|
|
52
|
+
return Spec(name, Category.COMPARISON, "pre", "out", op, ("pre", "in1", "in2", "out"))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _box(name, pins, render=None):
|
|
56
|
+
return Spec(name, Category.BOX, "en", "eno", render, pins)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
CATALOG: dict[str, Spec] = {
|
|
60
|
+
# --- power flow -------------------------------------------------------- #
|
|
61
|
+
"Contact": _pf("Contact"),
|
|
62
|
+
"Coil": _coil("Coil", ":="),
|
|
63
|
+
"SCoil": _coil("SCoil", "S"), # set coil ( S )
|
|
64
|
+
"RCoil": _coil("RCoil", "R"), # reset coil ( R )
|
|
65
|
+
"O": Spec("O", Category.OR_JUNCTION, None, "out", "OR", ("in1", "in2", "out")),
|
|
66
|
+
# --- comparisons (pre/out, contact-like) ------------------------------ #
|
|
67
|
+
"Lt": _cmp("Lt", "<"),
|
|
68
|
+
"Le": _cmp("Le", "<="),
|
|
69
|
+
"Eq": _cmp("Eq", "="),
|
|
70
|
+
"Ne": _cmp("Ne", "<>"),
|
|
71
|
+
"Ge": _cmp("Ge", ">="),
|
|
72
|
+
"Gt": _cmp("Gt", ">"),
|
|
73
|
+
# --- edge detection ---------------------------------------------------- #
|
|
74
|
+
"PContact": Spec("PContact", Category.EDGE, "pre", "out", "rising",
|
|
75
|
+
("pre", "operand", "bit", "out")),
|
|
76
|
+
"NContact": Spec("NContact", Category.EDGE, "pre", "out", "falling",
|
|
77
|
+
("pre", "operand", "bit", "out")),
|
|
78
|
+
"PBox": Spec("PBox", Category.EDGE, "in", "out", "rising", ("in", "bit", "out")),
|
|
79
|
+
"NBox": Spec("NBox", Category.EDGE, "in", "out", "falling", ("in", "bit", "out")),
|
|
80
|
+
# --- flip-flops -------------------------------------------------------- #
|
|
81
|
+
"Rs": Spec("Rs", Category.FLIPFLOP, None, "q", "reset_priority",
|
|
82
|
+
("s1", "r", "operand", "q")),
|
|
83
|
+
"Sr": Spec("Sr", Category.FLIPFLOP, None, "q", "set_priority",
|
|
84
|
+
("s", "r1", "operand", "q")),
|
|
85
|
+
# --- boxes (en/eno) ---------------------------------------------------- #
|
|
86
|
+
"Move": _box("Move", ("en", "in", "out1", "eno"), ":="),
|
|
87
|
+
"Add": _box("Add", ("en", "in1", "in2", "out", "eno"), "+"),
|
|
88
|
+
"Sub": _box("Sub", ("en", "in1", "in2", "out", "eno"), "-"),
|
|
89
|
+
"Mul": _box("Mul", ("en", "in1", "in2", "out", "eno"), "*"),
|
|
90
|
+
"Div": _box("Div", ("en", "in1", "in2", "out", "eno"), "/"),
|
|
91
|
+
"Inc": _box("Inc", ("en", "operand", "eno"), "+1"),
|
|
92
|
+
"Dec": _box("Dec", ("en", "operand", "eno"), "-1"),
|
|
93
|
+
"Calculate": _box("Calculate", ("en", "eno"), "equation"),
|
|
94
|
+
# --- IEC timers (system FBs; UPPERCASE pins; need Instance) ------------ #
|
|
95
|
+
"TON": _box("TON", ("IN", "PT", "Q", "ET")),
|
|
96
|
+
"TOF": _box("TOF", ("IN", "PT", "Q", "ET")),
|
|
97
|
+
"TP": _box("TP", ("IN", "PT", "Q", "ET")),
|
|
98
|
+
# --- system FC example ------------------------------------------------- #
|
|
99
|
+
"RD_LOC_T": _box("RD_LOC_T", ("en", "RET_VAL", "OUT", "eno")),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def lookup(name: str) -> Spec | None:
|
|
104
|
+
"""Return the Spec for a Part name, or None (caller folds to ir.Unhandled)."""
|
|
105
|
+
return CATALOG.get(name)
|
simaticml_decoder/ir.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Phase 2 output: the folded *semantics*, with no memory of the XML.
|
|
2
|
+
|
|
3
|
+
A network becomes an ordered list of statements. Rung conditions become a boolean
|
|
4
|
+
expression tree. Two cross-cutting commitments live here from the start:
|
|
5
|
+
|
|
6
|
+
* Traceability — every node carries the source ``uid``(s) it came from, so emit
|
|
7
|
+
can map any rendered claim back to the net it was derived from.
|
|
8
|
+
* Loud failure — anything the folder could not interpret becomes an ``Unhandled``
|
|
9
|
+
node carrying its part name + uid, so emit can surface it visibly instead of a
|
|
10
|
+
silent omission (the worst possible failure in authoritative-looking SCL).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from enum import Enum
|
|
17
|
+
|
|
18
|
+
# --------------------------------------------------------------------------- #
|
|
19
|
+
# Boolean / value expressions (rung conditions, comparison operands) #
|
|
20
|
+
# --------------------------------------------------------------------------- #
|
|
21
|
+
@dataclass
|
|
22
|
+
class VarRef:
|
|
23
|
+
"""A resolved operand, already rendered to its display form by operand.py."""
|
|
24
|
+
|
|
25
|
+
name: str # e.g. "#FI_Forward", '"DB".field', "%MW100"
|
|
26
|
+
uid: str | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class Literal:
|
|
31
|
+
value: str # rendered constant, e.g. "0", "TRUE", "T#3s"
|
|
32
|
+
uid: str | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class Not:
|
|
37
|
+
operand: Expr
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class And:
|
|
42
|
+
operands: list[Expr] = field(default_factory=list)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class Or:
|
|
47
|
+
operands: list[Expr] = field(default_factory=list) # n-ary, matches O cardinality
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class Compare:
|
|
52
|
+
op: str # "<" | "<=" | "=" | ">=" | ">" | "<>"
|
|
53
|
+
left: Expr
|
|
54
|
+
right: Expr
|
|
55
|
+
uid: str | None = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class EdgeKind(str, Enum):
|
|
59
|
+
RISING = "rising" # P_TRIG / PContact / PBox
|
|
60
|
+
FALLING = "falling" # N_TRIG / NContact / NBox
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class Edge:
|
|
65
|
+
kind: EdgeKind
|
|
66
|
+
signal: Expr # monitored signal / incoming power flow
|
|
67
|
+
mem_bit: VarRef | None = None # edge-memory operand
|
|
68
|
+
uid: str | None = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class RawExpr:
|
|
73
|
+
"""Free-form expression text taken verbatim (e.g. a Calculate-box Equation)."""
|
|
74
|
+
|
|
75
|
+
text: str
|
|
76
|
+
uid: str | None = None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class Unhandled:
|
|
81
|
+
"""A construct parsed but not folded. Rendered loudly by emit, never dropped."""
|
|
82
|
+
|
|
83
|
+
part_name: str
|
|
84
|
+
uid: str | None = None
|
|
85
|
+
note: str = ""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
Expr = (
|
|
89
|
+
VarRef | Literal | Not | And | Or | Compare | Edge | RawExpr | Unhandled
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# --------------------------------------------------------------------------- #
|
|
94
|
+
# Statements (one rung's effect) #
|
|
95
|
+
# --------------------------------------------------------------------------- #
|
|
96
|
+
class AssignKind(str, Enum):
|
|
97
|
+
NORMAL = "normal" # Coil -> target := <cond>;
|
|
98
|
+
NEGATED = "negated" # negated Coil -> target := NOT <cond>;
|
|
99
|
+
SET = "set" # SCoil -> IF <cond> THEN target := TRUE;
|
|
100
|
+
RESET = "reset" # RCoil -> IF <cond> THEN target := FALSE;
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class Assign:
|
|
105
|
+
target: VarRef
|
|
106
|
+
value: Expr
|
|
107
|
+
kind: AssignKind = AssignKind.NORMAL
|
|
108
|
+
is_latch: bool = False # set when a structural seal-in was detected
|
|
109
|
+
note: str | None = None # surfaced by emit when load-bearing
|
|
110
|
+
uid: str | None = None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass
|
|
114
|
+
class FlipFlop:
|
|
115
|
+
"""Rs / Sr — reset- or set-priority bistable on a stored operand."""
|
|
116
|
+
|
|
117
|
+
target: VarRef
|
|
118
|
+
set_expr: Expr
|
|
119
|
+
reset_expr: Expr
|
|
120
|
+
reset_priority: bool = True # Rs == reset-dominant; Sr == set-dominant
|
|
121
|
+
uid: str | None = None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class BoxCall:
|
|
126
|
+
"""A system FB/FC box: TON/TOF/TP, Move/Add/Inc, RD_LOC_T, ..."""
|
|
127
|
+
|
|
128
|
+
instruction: str # "TON", "Move", ...
|
|
129
|
+
inputs: dict[str, Expr] = field(default_factory=dict) # pin -> expression
|
|
130
|
+
outputs: dict[str, VarRef] = field(default_factory=dict) # pin -> destination
|
|
131
|
+
instance: str | None = None # rendered instance name, for system FBs
|
|
132
|
+
enable: Expr | None = None # the en / IN power-flow condition
|
|
133
|
+
uid: str | None = None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass
|
|
137
|
+
class UserCall:
|
|
138
|
+
"""A Call/CallInfo to a user FC/FB — parameters self-documented in the XML."""
|
|
139
|
+
|
|
140
|
+
name: str
|
|
141
|
+
block_type: str # FC | FB
|
|
142
|
+
instance: str | None = None
|
|
143
|
+
params: dict[str, Expr | VarRef] = field(default_factory=dict)
|
|
144
|
+
enable: Expr | None = None
|
|
145
|
+
uid: str | None = None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
Statement = Assign | FlipFlop | BoxCall | UserCall | Unhandled
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# --------------------------------------------------------------------------- #
|
|
152
|
+
# Network + block level #
|
|
153
|
+
# --------------------------------------------------------------------------- #
|
|
154
|
+
@dataclass
|
|
155
|
+
class NetworkLogic:
|
|
156
|
+
index: int
|
|
157
|
+
language: str
|
|
158
|
+
title: str | None = None
|
|
159
|
+
comment: str | None = None
|
|
160
|
+
statements: list[Statement] = field(default_factory=list) # folded LAD/FBD
|
|
161
|
+
scl_text: str | None = None # set for reconstructed SCL networks
|
|
162
|
+
warnings: list[str] = field(default_factory=list)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass
|
|
166
|
+
class TagRef:
|
|
167
|
+
"""One side of the cross-reference table: where a tag is written / read."""
|
|
168
|
+
|
|
169
|
+
network_index: int
|
|
170
|
+
uid: str | None = None
|
|
171
|
+
role: str = "" # "write" | "read" + context (pin/operand)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclass
|
|
175
|
+
class DecodedBlock:
|
|
176
|
+
name: str
|
|
177
|
+
kind: str
|
|
178
|
+
interface: object # model.Interface, carried through for the sidecar
|
|
179
|
+
networks: list[NetworkLogic] = field(default_factory=list)
|
|
180
|
+
xref: dict[str, list[TagRef]] = field(default_factory=dict) # tag -> uses
|
|
181
|
+
instruction_inventory: dict[str, int] = field(default_factory=dict)
|
|
182
|
+
warnings: list[str] = field(default_factory=list)
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""Phase 1 output: a faithful, behaviour-free mirror of the SimaticML XML.
|
|
2
|
+
|
|
3
|
+
This layer mirrors the *syntax* of the export (blocks, sections, parts, wires)
|
|
4
|
+
exactly as it appears — it does not interpret logic. Interpretation happens in
|
|
5
|
+
fold.py against ir.py. Keeping the two apart is the front-end/middle-end split:
|
|
6
|
+
adding FBD (same FlgNet) or a new output target touches one phase, not all three.
|
|
7
|
+
|
|
8
|
+
Field coverage follows the "Stable Parser Model" in SIMATICML_READING_GUIDE.md.
|
|
9
|
+
Large open enumerations (the 20 Access scopes, 13 address areas) are kept as
|
|
10
|
+
plain ``str`` plus a ``raw`` escape hatch rather than exhaustive enums, so an
|
|
11
|
+
unfamiliar value round-trips instead of raising.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from enum import Enum
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BlockKind(str, Enum):
|
|
21
|
+
FC = "FC"
|
|
22
|
+
FB = "FB"
|
|
23
|
+
OB = "OB"
|
|
24
|
+
DB = "DB"
|
|
25
|
+
UNKNOWN = "UNKNOWN"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Language(str, Enum):
|
|
29
|
+
"""Per-compile-unit programming language. Subset we render in v0 + passthrough."""
|
|
30
|
+
|
|
31
|
+
LAD = "LAD"
|
|
32
|
+
FBD = "FBD"
|
|
33
|
+
SCL = "SCL"
|
|
34
|
+
STL = "STL"
|
|
35
|
+
GRAPH = "GRAPH"
|
|
36
|
+
OTHER = "OTHER" # DB / SDB / IEC variants etc. — parsed, not rendered in v0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ----------------------------------------------------------------------------- #
|
|
40
|
+
# Interface (block signature + local declarations) #
|
|
41
|
+
# ----------------------------------------------------------------------------- #
|
|
42
|
+
@dataclass
|
|
43
|
+
class Member:
|
|
44
|
+
name: str
|
|
45
|
+
datatype: str # may be a quoted UDT ref, e.g. '"PLC_System"'
|
|
46
|
+
is_udt: bool = False # True when datatype was quoted in the XML
|
|
47
|
+
version: str | None = None # system/complex types (e.g. TON_TIME "1.0")
|
|
48
|
+
start_value: str | None = None # TIA literal, e.g. "T#3s", "16#0"
|
|
49
|
+
remanence: str | None = None # e.g. "Retain"
|
|
50
|
+
comment: str | None = None
|
|
51
|
+
children: list[Member] = field(default_factory=list) # nested struct members
|
|
52
|
+
raw: dict = field(default_factory=dict)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class Section:
|
|
57
|
+
name: str # Input | Output | InOut | Static | Temp | Constant | Return
|
|
58
|
+
members: list[Member] = field(default_factory=list)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class Interface:
|
|
63
|
+
sections: list[Section] = field(default_factory=list)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ----------------------------------------------------------------------------- #
|
|
67
|
+
# Operands referenced by Access nodes #
|
|
68
|
+
# ----------------------------------------------------------------------------- #
|
|
69
|
+
@dataclass
|
|
70
|
+
class Component:
|
|
71
|
+
"""One level of a (possibly dotted) symbol path, e.g. System.CLK100ms."""
|
|
72
|
+
|
|
73
|
+
name: str
|
|
74
|
+
slice_access: str | None = None # SliceAccessModifier, e.g. "x0" / "b1"
|
|
75
|
+
access_modifier: str = "None" # None | Array | Reference | ReferenceToArray
|
|
76
|
+
simple_access_modifier: str = "None" # None | Periphery | QualityInformation | combos
|
|
77
|
+
indices: list[Access] = field(default_factory=list) # array subscripts (Access children)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class Symbol:
|
|
82
|
+
components: list[Component] = field(default_factory=list)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class Constant:
|
|
87
|
+
type: str | None = None # ConstantType (may be Informative)
|
|
88
|
+
value: str | None = None # ConstantValue (may be Informative)
|
|
89
|
+
name: str | None = None
|
|
90
|
+
raw: dict = field(default_factory=dict)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class Address:
|
|
95
|
+
area: str # Input | Output | Memory | DB | Timer | Counter | ...
|
|
96
|
+
type: str | None = None
|
|
97
|
+
bit_offset: int | None = None # Byte*8 + Bit
|
|
98
|
+
block_number: int | None = None # DB number, for DB access
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# Operand = Symbol | Constant | Address (other scope children parsed into ``raw``)
|
|
102
|
+
Operand = Symbol | Constant | Address | None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class Access:
|
|
107
|
+
"""A data reference. Per-use (never deduplicated): each occurrence is its own node."""
|
|
108
|
+
|
|
109
|
+
uid: str
|
|
110
|
+
scope: str # 20 possible values — kept as str (see guide)
|
|
111
|
+
operand: Operand = None
|
|
112
|
+
raw: dict = field(default_factory=dict)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ----------------------------------------------------------------------------- #
|
|
116
|
+
# Instructions (Part) and user/system calls (Call) #
|
|
117
|
+
# ----------------------------------------------------------------------------- #
|
|
118
|
+
@dataclass
|
|
119
|
+
class TemplateValue:
|
|
120
|
+
name: str # e.g. "Card", "SrcType", "time_type"
|
|
121
|
+
kind: str # Cardinality | Type | Operation
|
|
122
|
+
value: str | None = None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass
|
|
126
|
+
class Instance:
|
|
127
|
+
"""Backing static member for a system FB (TON/CTU/...) — same shape as Symbol."""
|
|
128
|
+
|
|
129
|
+
scope: str
|
|
130
|
+
components: list[Component] = field(default_factory=list)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass
|
|
134
|
+
class Part:
|
|
135
|
+
uid: str
|
|
136
|
+
name: str # Contact | Coil | O | Move | TON | Lt | Rs | ...
|
|
137
|
+
disabled_eno: bool = False
|
|
138
|
+
version: str | None = None # system FB/FC type version
|
|
139
|
+
template_values: list[TemplateValue] = field(default_factory=list)
|
|
140
|
+
negated_pins: list[str] = field(default_factory=list) # from <Negated Name="..."/>
|
|
141
|
+
invisible_pins: list[str] = field(default_factory=list)
|
|
142
|
+
instance: Instance | None = None # system FB instance ref
|
|
143
|
+
equation: str | None = None # Calculate box only
|
|
144
|
+
comment: str | None = None
|
|
145
|
+
raw: dict = field(default_factory=dict)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@dataclass
|
|
149
|
+
class Parameter:
|
|
150
|
+
name: str
|
|
151
|
+
section: str # Input | Output | InOut | Return
|
|
152
|
+
type: str | None = None
|
|
153
|
+
informative: bool = False
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass
|
|
157
|
+
class Call:
|
|
158
|
+
uid: str
|
|
159
|
+
name: str # called block name
|
|
160
|
+
block_type: str # FC | FB | OB | DB | UDT | FBT | FCT
|
|
161
|
+
instance: Instance | None = None # for FB calls
|
|
162
|
+
parameters: list[Parameter] = field(default_factory=list)
|
|
163
|
+
comment: str | None = None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# ----------------------------------------------------------------------------- #
|
|
167
|
+
# Wires (the graph edges) #
|
|
168
|
+
# ----------------------------------------------------------------------------- #
|
|
169
|
+
class EndpointKind(str, Enum):
|
|
170
|
+
POWERRAIL = "Powerrail"
|
|
171
|
+
IDENT_CON = "IdentCon" # -> Access node (data), by uid
|
|
172
|
+
NAME_CON = "NameCon" # -> Part/Call pin, by (uid, name)
|
|
173
|
+
OPEN_CON = "OpenCon" # explicitly unused output
|
|
174
|
+
OPEN_BRANCH = "Openbranch" # unterminated branch
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass
|
|
178
|
+
class Endpoint:
|
|
179
|
+
kind: EndpointKind
|
|
180
|
+
uid: str | None = None # for IdentCon / NameCon / OpenCon
|
|
181
|
+
pin: str | None = None # for NameCon
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass
|
|
185
|
+
class Wire:
|
|
186
|
+
"""First endpoint is the source; the rest are sinks (fan-out)."""
|
|
187
|
+
|
|
188
|
+
uid: str
|
|
189
|
+
endpoints: list[Endpoint] = field(default_factory=list)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@dataclass
|
|
193
|
+
class Label:
|
|
194
|
+
uid: str
|
|
195
|
+
name: str
|
|
196
|
+
comment: str | None = None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ----------------------------------------------------------------------------- #
|
|
200
|
+
# Network sources (one per programming language) #
|
|
201
|
+
# ----------------------------------------------------------------------------- #
|
|
202
|
+
@dataclass
|
|
203
|
+
class FlgNet:
|
|
204
|
+
"""LAD/FBD network: indexed parts/calls/accesses + the wire list. UIds are
|
|
205
|
+
scoped to *this* compile unit only (reused across networks)."""
|
|
206
|
+
|
|
207
|
+
accesses: dict[str, Access] = field(default_factory=dict)
|
|
208
|
+
parts: dict[str, Part] = field(default_factory=dict)
|
|
209
|
+
calls: dict[str, Call] = field(default_factory=dict)
|
|
210
|
+
wires: list[Wire] = field(default_factory=list)
|
|
211
|
+
labels: list[Label] = field(default_factory=list)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@dataclass
|
|
215
|
+
class StructuredText:
|
|
216
|
+
"""SCL network: an ordered, interleaved token/access stream (a tokenised AST).
|
|
217
|
+
Reconstructed to text by scl_reconstruct.py — not folded."""
|
|
218
|
+
|
|
219
|
+
items: list[object] = field(default_factory=list) # Access | Token | comment nodes (raw)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# StatementList (STL) and Graph (SFC) are parsed-but-not-rendered in v0; kept as
|
|
223
|
+
# raw element trees so nothing is lost.
|
|
224
|
+
@dataclass
|
|
225
|
+
class RawSource:
|
|
226
|
+
language: Language
|
|
227
|
+
element: object = None # retained xml.etree element for later phases
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
NetworkSource = FlgNet | StructuredText | RawSource | None
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@dataclass
|
|
234
|
+
class Network:
|
|
235
|
+
index: int # 1-based, in document order
|
|
236
|
+
language: Language
|
|
237
|
+
title: str | None = None
|
|
238
|
+
comment: str | None = None
|
|
239
|
+
source: NetworkSource = None # None == empty network
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# ----------------------------------------------------------------------------- #
|
|
243
|
+
# Top level #
|
|
244
|
+
# ----------------------------------------------------------------------------- #
|
|
245
|
+
@dataclass
|
|
246
|
+
class Block:
|
|
247
|
+
kind: BlockKind
|
|
248
|
+
id: str
|
|
249
|
+
name: str
|
|
250
|
+
number: int | None = None
|
|
251
|
+
language: Language = Language.LAD # block-level default; networks may differ
|
|
252
|
+
memory_layout: str | None = None
|
|
253
|
+
memory_reserve: int | None = None # FB only
|
|
254
|
+
set_eno_automatically: bool = False
|
|
255
|
+
title: str | None = None
|
|
256
|
+
comment: str | None = None
|
|
257
|
+
interface: Interface = field(default_factory=Interface)
|
|
258
|
+
networks: list[Network] = field(default_factory=list)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@dataclass
|
|
262
|
+
class Document:
|
|
263
|
+
engineering_version: str | None # e.g. "V21"
|
|
264
|
+
block: Block
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Render a model.Access into its TIA display string.
|
|
2
|
+
|
|
3
|
+
Split out because it is needed in two phases (fold labels its nodes; emit prints
|
|
4
|
+
them) and is a self-contained pile of display conventions. Conventions applied
|
|
5
|
+
(SIMATICML_READING_GUIDE.md "Display Conventions"):
|
|
6
|
+
|
|
7
|
+
LocalVariable scope -> "#name"
|
|
8
|
+
GlobalVariable scope -> '"DB".field'
|
|
9
|
+
Address scope -> "%MW100" (area letter + offset)
|
|
10
|
+
SliceAccessModifier "x0" -> ".%X0"
|
|
11
|
+
multi-component symbol -> dotted path "System.CLK100ms"
|
|
12
|
+
array AccessModifier -> "name[<index>]"
|
|
13
|
+
LiteralConstant -> the value verbatim ("0", "TRUE", "T#3s")
|
|
14
|
+
|
|
15
|
+
Phase 1 (parse) is implemented first; this is filled alongside it so folded
|
|
16
|
+
networks have readable names.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from . import model
|
|
22
|
+
|
|
23
|
+
# Address Area -> TIA display prefix (SIMATICML_READING_GUIDE.md "Area values").
|
|
24
|
+
_AREA_PREFIX: dict[str, str] = {
|
|
25
|
+
"Input": "%I",
|
|
26
|
+
"Output": "%Q",
|
|
27
|
+
"Memory": "%M",
|
|
28
|
+
"PeripheryInput": "%PI",
|
|
29
|
+
"PeripheryOutput": "%PQ",
|
|
30
|
+
"Timer": "%T",
|
|
31
|
+
"Counter": "%C",
|
|
32
|
+
"Local": "%L",
|
|
33
|
+
"DB": "%DB",
|
|
34
|
+
"DI": "%DI",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Address Type -> width letter used in the absolute display (%MW, %MB, ...).
|
|
38
|
+
# Bool is special-cased to the byte.bit form and is not in this table.
|
|
39
|
+
_TYPE_WIDTH: dict[str, str] = {
|
|
40
|
+
"byte": "B",
|
|
41
|
+
"sint": "B",
|
|
42
|
+
"usint": "B",
|
|
43
|
+
"char": "B",
|
|
44
|
+
"word": "W",
|
|
45
|
+
"int": "W",
|
|
46
|
+
"uint": "W",
|
|
47
|
+
"s5time": "W",
|
|
48
|
+
"dword": "D",
|
|
49
|
+
"dint": "D",
|
|
50
|
+
"udint": "D",
|
|
51
|
+
"real": "D",
|
|
52
|
+
"time": "D",
|
|
53
|
+
"lword": "L",
|
|
54
|
+
"lint": "L",
|
|
55
|
+
"ulint": "L",
|
|
56
|
+
"lreal": "L",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def render(access: model.Access) -> str:
|
|
61
|
+
"""model.Access -> display string (TIA conventions; see module docstring)."""
|
|
62
|
+
operand = access.operand
|
|
63
|
+
if isinstance(operand, model.Symbol):
|
|
64
|
+
return _render_symbol(operand, access.scope)
|
|
65
|
+
if isinstance(operand, model.Constant):
|
|
66
|
+
return _render_constant(operand)
|
|
67
|
+
if isinstance(operand, model.Address):
|
|
68
|
+
return _render_address(operand)
|
|
69
|
+
# Undef / Unnamed / scopes whose child landed in raw (Expression, CallInfo,
|
|
70
|
+
# ...): no readable operand. Fail visibly rather than silently — a wrong name
|
|
71
|
+
# is worse than an obvious placeholder.
|
|
72
|
+
return f"<{access.scope or 'Undef'}#{access.uid}>"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# --------------------------------------------------------------------------- #
|
|
76
|
+
# Symbols #
|
|
77
|
+
# --------------------------------------------------------------------------- #
|
|
78
|
+
def _render_symbol(symbol: model.Symbol, scope: str | None) -> str:
|
|
79
|
+
if not symbol.components:
|
|
80
|
+
return f"<{scope or 'Symbol'}>"
|
|
81
|
+
is_global = scope == "GlobalVariable"
|
|
82
|
+
parts = [
|
|
83
|
+
_render_component(comp, quote=(is_global and i == 0))
|
|
84
|
+
for i, comp in enumerate(symbol.components)
|
|
85
|
+
]
|
|
86
|
+
path = ".".join(parts)
|
|
87
|
+
# Local/interface variables get the '#' prefix; global access is already
|
|
88
|
+
# quoted at its root ('"DB".field') and takes no prefix.
|
|
89
|
+
if scope == "LocalVariable":
|
|
90
|
+
return "#" + path
|
|
91
|
+
return path
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _render_component(comp: model.Component, quote: bool = False) -> str:
|
|
95
|
+
text = f'"{comp.name}"' if quote else comp.name
|
|
96
|
+
# Array subscripts: one or more Access children rendered recursively.
|
|
97
|
+
if comp.access_modifier in ("Array", "ReferenceToArray") and comp.indices:
|
|
98
|
+
subs = ", ".join(render(idx) for idx in comp.indices)
|
|
99
|
+
text += f"[{subs}]"
|
|
100
|
+
# Bit/byte/word/dword slice: "x0" -> ".%X0", "b1" -> ".%B1".
|
|
101
|
+
if comp.slice_access:
|
|
102
|
+
letter = comp.slice_access[0].upper()
|
|
103
|
+
number = comp.slice_access[1:]
|
|
104
|
+
text += f".%{letter}{number}"
|
|
105
|
+
return text
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# --------------------------------------------------------------------------- #
|
|
109
|
+
# Constants #
|
|
110
|
+
# --------------------------------------------------------------------------- #
|
|
111
|
+
def _render_constant(const: model.Constant) -> str:
|
|
112
|
+
if const.value is not None:
|
|
113
|
+
return const.value
|
|
114
|
+
if const.name is not None:
|
|
115
|
+
return const.name
|
|
116
|
+
return "<const>"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# --------------------------------------------------------------------------- #
|
|
120
|
+
# Absolute addresses #
|
|
121
|
+
# --------------------------------------------------------------------------- #
|
|
122
|
+
def _render_address(addr: model.Address) -> str:
|
|
123
|
+
prefix = _AREA_PREFIX.get(addr.area, "%" + (addr.area or "?"))
|
|
124
|
+
# DB-qualified access carries a block number: DB10.DBW0-style.
|
|
125
|
+
db_qualifier = ""
|
|
126
|
+
if addr.block_number is not None and addr.area in ("DB", "DI"):
|
|
127
|
+
prefix = f"%DB{addr.block_number}" if addr.area == "DB" else f"%DI{addr.block_number}"
|
|
128
|
+
db_qualifier = ".DB" if addr.area == "DB" else ".DI"
|
|
129
|
+
|
|
130
|
+
if addr.bit_offset is None:
|
|
131
|
+
return prefix + db_qualifier
|
|
132
|
+
|
|
133
|
+
byte, bit = divmod(addr.bit_offset, 8)
|
|
134
|
+
type_lc = (addr.type or "").lower()
|
|
135
|
+
if type_lc in ("bool", "bit", ""):
|
|
136
|
+
return f"{prefix}{db_qualifier}{'X' if db_qualifier else ''}{byte}.{bit}"
|
|
137
|
+
width = _TYPE_WIDTH.get(type_lc)
|
|
138
|
+
if width:
|
|
139
|
+
return f"{prefix}{db_qualifier}{width}{byte}"
|
|
140
|
+
# Unknown width: fall back to the bit-addressed form so nothing is lost.
|
|
141
|
+
return f"{prefix}{db_qualifier}{byte}.{bit}"
|