forked from beremiz/beremiz
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathProjectController.py
More file actions
253 lines (229 loc) · 11.7 KB
/
ProjectController.py
File metadata and controls
253 lines (229 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
import traceback, os, re
from jinja2 import Environment, FileSystemLoader
from runtime.typemapping import DebugTypesSize
import util.paths as paths
import hashlib
class ProjectController:
def __init__(self):
self.__loader = FileSystemLoader(
os.path.join(paths.AbsDir(__file__), "templates")
)
self.ResetIECProgramsAndVariables()
def SetCSVFile(self, filename):
"""
Open a CSV file containing IEC variables.
"""
self._csvfile = filename
def ResetIECProgramsAndVariables(self):
"""
Reset variable and program list that are parsed from
CSV file generated by IEC2C compiler.
"""
self._ProgramList = None
self._VariablesList = None
self._DbgVariablesList = None
self._IECPathToIdx = {}
self._Ticktime = 0
self.TracedIECPath = []
self.TracedIECTypes = []
def GetIECProgramsAndVariables(self):
"""
Parse CSV-like file VARIABLES.csv resulting from IEC2C compiler.
Each section is marked with a line staring with '//'
list of all variables used in various POUs
"""
if self._ProgramList is None or self._VariablesList is None:
try:
# describes CSV columns
ProgramsListAttributeName = ["num", "C_path", "type"]
VariablesListAttributeName = [
"num",
"vartype",
"IEC_path",
"C_path",
"type",
"derived",
"retain",
]
self._ProgramList = []
self._VariablesList = []
self._DbgVariablesList = []
self._IECPathToIdx = {}
# Separate sections
ListGroup = []
for line in open(self._csvfile, "r").readlines():
strippedline = line.strip()
if strippedline.startswith("//"):
# Start new section
ListGroup.append([])
elif len(strippedline) > 0 and len(ListGroup) > 0:
# append to this section
ListGroup[-1].append(strippedline)
# first section contains programs
for line in ListGroup[0]:
# Split and Maps each field to dictionnary entries
attrs = dict(
list(zip(ProgramsListAttributeName, line.strip().split(";")))
)
# Truncate "C_path" to remove conf an resources names
attrs["C_path"] = "__".join(attrs["C_path"].split(".", 2)[1:])
# Push this dictionnary into result.
self._ProgramList.append(attrs)
# second section contains all variables
config_FBs = {}
# Track global array variables: {parent_c_path: {element_type, dim_max: [max_idx_per_dim]}}
config_arrays = {}
# Track global variable names (defined at CONFIG0 level) to detect external references
global_var_names = set()
# First pass: collect global variable names
for line in ListGroup[1]:
line_parts = line.strip().split(";")
if len(line_parts) >= 4:
c_path = line_parts[3]
path_parts = c_path.split(".", 2)
# Global variables are at CONFIG0.VARNAME level (exactly 2 parts)
# or CONFIG0.VARNAME.xxx (3+ parts where part[2] starts with "value" or is a member)
if len(path_parts) >= 2 and path_parts[0].startswith("CONFIG"):
# Check if this is a CONFIG0.VARNAME pattern (not CONFIG0.RES0.xxx)
if not path_parts[1].startswith("RES"):
global_var_names.add(path_parts[1])
Idx = 0
for line in ListGroup[1]:
# Split and Maps each field to dictionnary entries
attrs = dict(
list(zip(VariablesListAttributeName, line.strip().split(";")))
)
# Truncate "C_path" to remove conf an resources names
parts = attrs["C_path"].split(".", 2)
if len(parts) > 2:
config_FB = config_FBs.get(tuple(parts[:2]))
if config_FB:
parts = [config_FB] + parts[2:]
attrs["C_path"] = ".".join(parts)
else:
# Check if this is a global array/struct variable at CONFIG level
# Pattern: CONFIG0.VARNAME.value.table[x] or CONFIG0.VARNAME.value.fieldname
# These should become: CONFIG0__VARNAME.value.table[x] or CONFIG0__VARNAME.value.fieldname
if parts[0].startswith("CONFIG") and parts[2].startswith("value"):
# Global array or struct access - keep CONFIG prefix
attrs["C_path"] = parts[0] + "__" + parts[1] + "." + parts[2]
# Track global array variables for extern declarations
# Pattern: value.table[x] or value.table[x][y]... indicates array element
if parts[2].startswith("value.table["):
parent_c_path = parts[0] + "__" + parts[1]
# Extract per-dimension indices from subscripts like "value.table[1][2]"
indices = [int(m) for m in re.findall(r'\[(\d+)\]', parts[2])]
if parent_c_path not in config_arrays:
config_arrays[parent_c_path] = {
"element_type": attrs["type"],
"dim_max": [0] * len(indices),
}
# Track maximum index seen in each dimension
for d, idx in enumerate(indices):
if idx > config_arrays[parent_c_path]["dim_max"][d]:
config_arrays[parent_c_path]["dim_max"][d] = idx
else:
# For resource-level variables (RES0.INSTANCE.xxx)
# Check if this is an external variable reference (references a global)
# External variables should be skipped - only the global is needed for debugging
# Pattern: INSTANCE.VARNAME.xxx where VARNAME is a global variable name
sub_parts = parts[2].split(".")
if len(sub_parts) >= 2 and sub_parts[1] in global_var_names:
# This is an external variable - skip it entirely
# The global variable will be used for monitoring/forcing instead
continue
attrs["C_path"] = "__".join(parts[1:])
else:
attrs["C_path"] = "__".join(parts)
if attrs["vartype"] == "FB":
config_FBs[tuple(parts)] = attrs["C_path"]
if attrs["vartype"] != "FB" and attrs["type"] in DebugTypesSize:
# Push this dictionnary into result.
self._DbgVariablesList.append(attrs)
# Fill in IEC<->C translation dicts
IEC_path = attrs["IEC_path"]
self._IECPathToIdx[IEC_path] = (Idx, attrs["type"])
# Ignores numbers given in CSV file
# Idx=int(attrs["num"])
# Count variables only, ignore FBs
Idx += 1
self._VariablesList.append(attrs)
# Add synthetic entries for global array variables to _VariablesList
# These are needed to generate extern declarations
# Use vartype "GLOBAL_ARRAY" to use the correct extern format (no __IEC_ prefix)
# Type name uses per-dimension sizes to match matiec's naming convention
# e.g. ARRAY [0..2, 0..2] OF INT -> __ARRAY_OF_INT_3_3 (not __ARRAY_OF_INT_9)
for parent_c_path, info in config_arrays.items():
dim_sizes = [str(m + 1) for m in info["dim_max"]]
dim_suffix = "_".join(dim_sizes)
array_type = f"__ARRAY_OF_{info['element_type']}_{dim_suffix}"
self._VariablesList.append({
"C_path": parent_c_path,
"type": array_type,
"vartype": "GLOBAL_ARRAY",
})
# third section contains ticktime
if len(ListGroup) > 2:
self._Ticktime = int(ListGroup[2][0])
except Exception:
self.ResetIECProgramsAndVariables()
raise Exception(
f"Cannot open/parse VARIABLES.csv!\n{traceback.format_exc()}"
)
def Generate_plc_debug_cvars(self):
"""
Generate debug C variables out of PLC variable list
"""
if not self._DbgVariablesList and not self._VariablesList:
self.GetIECProgramsAndVariables()
type_suffix = {
"EXT": "_P_ENUM",
"IN": "_P_ENUM",
"MEM": "_O_ENUM",
"OUT": "_O_ENUM",
"VAR": "_ENUM",
}
variable_decl_array = [
f"{{&({v['C_path']}), {v['type']}{type_suffix[v['vartype']]}}}"
for v in self._DbgVariablesList
]
enum_types = list(
set([v["type"] + type_suffix[v["vartype"]] for v in self._DbgVariablesList])
)
types = {
"EXT": ("extern __IEC_", "_p"),
"IN": ("extern __IEC_", "_p"),
"MEM": ("extern __IEC_", "_p"),
"OUT": ("extern __IEC_", "_p"),
"VAR": ("extern __IEC_", "_t"),
"FB": ("extern ", ""),
"GLOBAL_ARRAY": ("extern __IEC_", "_t"),
}
extern_variables_declarations = [
f"{types[v['vartype']][0]}{v['type']}{types[v['vartype']][1]} {v['C_path']};"
for v in self._VariablesList
if "." not in v["C_path"]
]
return variable_decl_array, extern_variables_declarations, enum_types
def Generate_embedded_plc_debugger(self, st_file):
dvars, externs, enums = self.Generate_plc_debug_cvars()
MD5 = hashlib.md5(open(st_file, "rb").read()).hexdigest()
if MD5 is None:
raise ("Error building project: md5 object is null\n")
template = Environment(loader=self.__loader).get_template("debug.c.j2")
cfile = os.path.join(paths.AbsDir(self._csvfile), "debug.c")
debug_text = template.render(
debug={
"externs": externs,
"vars": dvars,
"enums": enums,
"types": list(set(a.split("_", 1)[0] for a in enums)),
"md5": MD5,
}
)
with open(cfile, "w") as f:
f.write(debug_text)
# Wrap debugger code around (* comments *)
# Add MD5 value to debug.cpp file
c_debug = 'char md5[] = "' + MD5 + '";\n' + debug_text
return cfile, c_debug