diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index fefe9ce5a..743f0a6c4 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -17,11 +17,13 @@ "rainbow-panda.panda", "ms-python.python", "ms-python.vscode-pylance", - "mechatroner.rainbow-csv" + "mechatroner.rainbow-csv", + "Serhioromano.vscode-st" ] } }, "forwardPorts": [22], "postCreateCommand": "", - "remoteUser": "dev" + "remoteUser": "dev", + "runArgs": ["--name", "xml2st-devcontainer"] } diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2c74a440c..72dec78f1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,7 +34,7 @@ jobs: pip3 install pyinstaller - name: Build with PyInstaller - run: pyinstaller xml2st.py --add-data plcopen:plcopen + run: pyinstaller xml2st.py --add-data plcopen:plcopen --add-data templates:templates - name: Upload artifact uses: actions/upload-artifact@v4 @@ -62,7 +62,7 @@ jobs: pip3 install pyinstaller - name: Build with PyInstaller - run: pyinstaller xml2st.py --add-data plcopen:plcopen + run: pyinstaller xml2st.py --add-data plcopen:plcopen --add-data templates:templates - name: Upload artifact uses: actions/upload-artifact@v4 @@ -95,7 +95,7 @@ jobs: pip3 install pyinstaller - name: Build with PyInstaller - run: pyinstaller xml2st.py -F --add-data plcopen:plcopen + run: pyinstaller xml2st.py -F --add-data plcopen:plcopen --add-data templates:templates - name: Upload artifact uses: actions/upload-artifact@v4 @@ -128,7 +128,7 @@ jobs: pip3 install pyinstaller - name: Build with PyInstaller - run: pyinstaller xml2st.py -F --add-data plcopen:plcopen + run: pyinstaller xml2st.py -F --add-data plcopen:plcopen --add-data templates:templates - name: Upload artifact uses: actions/upload-artifact@v4 @@ -156,7 +156,7 @@ jobs: pip install pyinstaller - name: Build with PyInstaller - run: pyinstaller xml2st.py -F --add-data plcopen:plcopen + run: pyinstaller xml2st.py -F --add-data plcopen:plcopen --add-data templates:templates - name: Upload artifact uses: actions/upload-artifact@v4 @@ -184,7 +184,7 @@ jobs: pip install pyinstaller - name: Build with PyInstaller - run: pyinstaller xml2st.py -F --add-data plcopen:plcopen + run: pyinstaller xml2st.py -F --add-data plcopen:plcopen --add-data templates:templates - name: Upload artifact uses: actions/upload-artifact@v4 diff --git a/ComplexParser.py b/ComplexParser.py new file mode 100644 index 000000000..ed035e27b --- /dev/null +++ b/ComplexParser.py @@ -0,0 +1,528 @@ +import os, re +from jinja2 import Environment, FileSystemLoader +from util import paths +from STParser import ( + TYPE, + FUNCTION_BLOCK, + PROGRAM, + CONFIGURATION, + RESOURCE, + STRUCT, + ARRAY, + VARIABLE, + PROGRAM_DEFINITION, + ALL_BLOCKS, + CLOSABLE_BLOCKS, + BASE_TYPES, +) + +## GLOABL VARIABLES + +EMPTY_LINE = re.compile(r"^\s*$") +FUNCTION_BLOCK_ST_TEMPLATE = "function_block.st.j2" +CSV_VARS_TEMPLATE = "variable_declaration.csv.j2" + +## CUSTOM CLASSES FOR BLOCK INSTANCES + + +class _InsertLine: + def __init__(self, index): + self.index = index + + +class _BlockInstance: + + def __init__(self, type): + self.inner_blocks = [] + self.lines = [] + self.opened = True + self.type = type + + def close(self, line): + if self.inner_blocks and self.inner_blocks[-1].opened: + self.inner_blocks[-1].close(line) + else: + self.opened = False + self.AppendLine(line) + + def AddBlock(self, block): + """ + Add a block to the inner blocks. + """ + if self.inner_blocks == [] or not self.inner_blocks[-1].opened: + self.inner_blocks.append(block) + self.lines.append(_InsertLine(len(self.inner_blocks) - 1)) + else: + self.inner_blocks[-1].AddBlock(block) + + def AppendLine(self, line): + """ + Add a block to the inner blocks. + """ + if self.inner_blocks == [] or not self.inner_blocks[-1].opened: + self.lines.append(line) + else: + self.inner_blocks[-1].AppendLine(line) + + def __str__(self): + line_break = "\n " + return f"{self.type} : \n {line_break.join([str(b) for b in self.inner_blocks])} \nEND_{self.type};\n" + + +class _NamedBlockInstance(_BlockInstance): + def __init__(self, type, name): + super().__init__(type) + self.name = name + + +class _VariableInstance(_NamedBlockInstance): + def __init__(self, name, data_type, value=None): + super().__init__(VARIABLE.name, name) + self.data_type = data_type + self.value = value + self.opened = False + self.simple = data_type in BASE_TYPES + + def VerifyCustomTypes(self, custom_types): + """ + Verify if the data type is a custom type. + """ + if not self.simple: + self.simple = next( + (t.simple for t in custom_types if t.name == self.data_type), False + ) + + def __str__(self): + if self.value: + return f"{self.data_type} {self.name} := {self.value};" + else: + return f"{self.data_type} {self.name};" + + +class _ArrayInstance(_VariableInstance): + def __init__(self, name, start, end, data_type): + super().__init__(name, data_type) + self.type = ARRAY.name + self.start = start + self.end = end + self.opened = False + + +class _StructInstance(_NamedBlockInstance): + + def __init__(self, name): + self.simple = True + super().__init__(STRUCT.name, name) + + def AddBlock(self, block): + """ + Add a block to the inner blocks. + """ + super().AddBlock(block) + if self.simple and hasattr(block, "simple"): + self.simple = block.simple + + +## MAIN COMPLEX PARSER CLASS + + +class ComplexParser: + + ## PRIVATE METHODS AND CONSTRUCTORS + + def __init__(self): + self.blocks = [] + self.arrays = [] + self.structs = [] + self.programs = [] + self.csv_vars = [] + self.array_dependant = [] + self.array_dependant_names = [] + self.complex_structs = [] + self.function_blocks = [] + self.__loader = FileSystemLoader( + os.path.join(paths.AbsDir(__file__), "templates") + ) + + def __clear(self): + """ + Clear the current state of the parser. + """ + self.blocks = [] + self.arrays = [] + self.structs = [] + self.programs = [] + self.csv_vars = [] + self.array_dependant = [] + self.array_dependant_names = [] + self.complex_structs = [] + self.function_blocks = [] + + def __close(self, line): + """ + Close the last opened block. + """ + if self.blocks == []: + raise Exception("No block opened to close.") + self.blocks[-1].close(line) + + def __appendBlock(self, block): + """ + Append a block to the blocks list. + """ + if self.blocks == [] or not self.blocks[-1].opened: + self.blocks.append(block) + else: + self.blocks[-1].AddBlock(block) + + def __appendLine(self, line): + """ + Append a line to the current block's lines. + """ + if self.blocks == []: + if EMPTY_LINE.match(line): + pass + else: + raise Exception("No block opened to append line.") + else: + self.blocks[-1].AppendLine(line) + + def __classifyBlock(self, info): + """ + Classify the block based on its type. + """ + if info["type"] == ARRAY.name: + instance = _ArrayInstance( + info["name"], info["start"], info["end"], info["data_type"] + ) + self.arrays.append(instance) + return instance + elif info["type"] == STRUCT.name: + instance = _StructInstance(info["name"]) + self.structs.append(instance) + return instance + elif info["type"] == VARIABLE.name: + instance = _VariableInstance( + info["name"], info["data_type"], info.get("value") + ) + return instance + elif "name" in info.keys(): + return _NamedBlockInstance(info["type"], info["name"]) + else: + return _BlockInstance(info["type"]) + + def __getBlockLines(self, block, ignoreComplexStructs=True): + """ + Get the lines of the block. + """ + + lines = [] + if ( + ignoreComplexStructs + and isinstance(block, _StructInstance) + and block.name not in self.array_dependant_names + ): + return [] + for line in block.lines: + if isinstance(line, _InsertLine): + lines.extend(self.__getBlockLines(block.inner_blocks[line.index])) + else: + lines.append(line) + + return lines + + def __isBlockAnArrayType(self, block): + for a in self.arrays: + if block.name == a.data_type: + return True + return False + + def __analyseTypes(self, block): + """ + Separate structs that have to be manually parsed + """ + + changes = [b for b in block.inner_blocks if self.__isBlockAnArrayType(b)] + + for c in changes: + self.__checkSubtypes(c, block.inner_blocks) + + self.array_dependant_names = list(set(self.array_dependant_names)) + + complex_blocks = [ + b for b in block.inner_blocks if b.name not in self.array_dependant_names + ] + self.complex_structs = [ + b for b in complex_blocks if isinstance(b, _StructInstance) + ] + + def __checkSubtypes(self, subtype, blocks): + if isinstance(subtype, _StructInstance): + self.array_dependant.append(subtype) + self.array_dependant_names.append(subtype.name) + for inner_block in subtype.inner_blocks: + block = self.__findBlock(inner_block, blocks) + if block: + self.__checkSubtypes(block, blocks) + + def __findBlock(self, block, block_list): + for b in block_list: + if b.name == block.data_type: + return b + return None + + def __separateOuterBlocks(self): + """ + Get the custom types from the blocks. + """ + self.custom_types = [] + for block in self.blocks: + if block.type == TYPE.name: + self.__analyseTypes(block) + elif block.type == FUNCTION_BLOCK.name: + self.function_blocks.append(block) + elif block.type == PROGRAM.name: + self.programs.append(block) + + def _parseStTree(self): + """ + Parse the ST file to extract complex variables. + """ + + with open(self.__stFile, "r") as f: + lines = f.readlines() + + for line in lines: + info = next((t.GetInfo(line) for t in ALL_BLOCKS if t.GetInfo(line)), None) + if info: + block = self.__classifyBlock(info) + self.__appendBlock(block) + elif next((True for t in CLOSABLE_BLOCKS if t.end.match(line)), False): + self.__close(line) + continue + self.__appendLine(line) + + self.__separateOuterBlocks() + + def __getSTLines(self): + """ + Get the ST file content as a string. + """ + lines = [] + for block in [b for b in self.blocks]: + if block.type == TYPE.name: + for inner_block in block.inner_blocks: + if ( + not isinstance(inner_block, _StructInstance) + or inner_block.name in self.array_dependant_names + ): + break + else: + lines.append(self.__rewriteStructsAsFunctionBlocks()) + continue + lines.extend(self.__getBlockLines(block)) + lines.append(self.__rewriteStructsAsFunctionBlocks()) + else: + lines.extend(self.__getBlockLines(block)) + return lines + + def __rewriteStructsAsFunctionBlocks(self): + template = Environment(loader=self.__loader).get_template( + FUNCTION_BLOCK_ST_TEMPLATE + ) + program_text = "" + for struct in self.complex_structs: + lines = [l.strip() for l in self.__getBlockLines(struct, False)[1:-1]] + program_text += f"{template.render(name=struct.name, vars=lines)}\n\n" + + return program_text + + def __rewriteSTWithComplexStructs(self): + """ + Rewrite the ST file with complex variables. + """ + program_text = "".join(self.__getSTLines()) + with open(self.__stFile, "w") as f: + f.write(program_text) + + return program_text + + def __getCustomType(self, type_name): + """ + Get the complex type by its name. + """ + for custom_type in self.array_dependant: + if custom_type.name == type_name: + return custom_type + return None + + def __getFunctionBlock(self, name): + """ + Get the function block by its name. + """ + for block in self.function_blocks: + if block.name == name: + return block + return None + + def __spreadDeclarations( + self, block, prefix="", write_base_types=True, raw_type=False, value_added=False + ): + """ + Spread the declarations of the block. + """ + if block.type == ARRAY.name: + array_prefix = prefix + if not raw_type: + array_prefix = f"{array_prefix}.{block.name.upper()}" + if not value_added: + array_prefix = f"{array_prefix}.value" + for i in range(0, block.end + 1 - block.start): + indexed_prefix = f"{array_prefix}.table[{i}]" + if block.data_type in BASE_TYPES: + self.csv_vars.append( + {"name": indexed_prefix, "type": block.data_type} + ) + else: + type = self.__getCustomType(block.data_type) + if type: + self.__spreadDeclarations( + type, prefix=indexed_prefix, value_added=True + ) + elif block.type == VARIABLE.name: + prefix = f"{prefix}.{block.name.upper()}" + if block.data_type in BASE_TYPES and write_base_types: + self.csv_vars.append({"name": prefix, "type": block.data_type}) + elif block.data_type in self.array_dependant_names: + type = self.__getCustomType(block.data_type) + if type: + self.__spreadDeclarations( + type, prefix=prefix, raw_type=True, value_added=value_added + ) + elif block.data_type in [b.name for b in self.function_blocks]: + function_block = self.__getFunctionBlock(block.data_type) + if function_block: + for inner_block in function_block.inner_blocks: + self.__spreadDeclarations( + inner_block, + prefix=prefix, + write_base_types=False, + value_added=value_added, + ) + elif block.type == STRUCT.name: + if not value_added: + prefix = f"{prefix}.value" + for inner_block in block.inner_blocks: + if isinstance(inner_block, _VariableInstance): + self.__spreadDeclarations( + inner_block, prefix=prefix, value_added=True + ) + + def __addVarDeclarations(self, program, prefix=""): + program_block = next((p for p in self.programs if p.name == program), None) + if program_block: + for block in program_block.inner_blocks: + if isinstance(block, _VariableInstance): + self.__spreadDeclarations(block, prefix, write_base_types=False) + + def __findProgramInstances(self): + """ + Find program instances in the ST file. + """ + for block in self.blocks: + if block.type == CONFIGURATION.name: + for resource in filter( + lambda x: x.type == RESOURCE.name, block.inner_blocks + ): + program_instances = [ + PROGRAM_DEFINITION.GetInfo(line) + for line in resource.lines + if PROGRAM_DEFINITION.GetInfo(line) + ] + for program_instance in program_instances: + prefix = f"{block.name.upper()}.{resource.name.upper()}.{program_instance['instance'].upper()}" + self.__addVarDeclarations(program_instance["program"], prefix) + + def __appendVarsToCSV(self, csv_file): + """ + Append new variable lines before the Ticktime section using regex matching. + """ + + content = [] + with open(csv_file, "r") as f: + content = f.readlines() + + ticktime_idx = None + var_number_pattern = re.compile(r"^\s*(\d+);") # captures first number in line + ticktime_pattern = re.compile(r"\s*//\s*Ticktime\s*$") + + # Find Ticktime section using regex + for i, line in enumerate(content): + if ticktime_pattern.match(line): + ticktime_idx = i + break + + if ticktime_idx is None: + raise ValueError("Ticktime section not found in lines.") + + while ticktime_idx >= 1 and EMPTY_LINE.match(content[ticktime_idx - 1]): + ticktime_idx -= 1 + + last_var_number = -1 + + if ticktime_idx > 0: + extracted_var_number = var_number_pattern.match( + content[ticktime_idx - 1] + ).group(1) + if extracted_var_number: + last_var_number = int(extracted_var_number) + + template = Environment(loader=self.__loader).get_template(CSV_VARS_TEMPLATE) + + formatted_vars = [] + for var in self.csv_vars: + last_var_number += 1 + formatted_vars.append(f"{template.render(i=last_var_number, var=var)}\n") + + with open(csv_file, "w") as f: + f.writelines( + content[:ticktime_idx] + formatted_vars + [""] + content[ticktime_idx:] + ) + + return content[:ticktime_idx] + formatted_vars + [""] + content[ticktime_idx:] + + def __rewriteCSVWithComplexVars(self, csv_file): + + self.__findProgramInstances() + self.__appendVarsToCSV(csv_file) + + ## PUBLIC METHODS + + def AddComplexVars(self, st_file, csv_file): + + if not st_file or not os.path.isfile(st_file): + raise Exception("ST file not valid. Please provide a valid ST file path.") + + if not csv_file or not os.path.isfile(csv_file): + raise Exception("ST file not valid. Please provide a valid CSV file path.") + + self.__clear() + + self.__stFile = st_file + self._parseStTree() + + self.__rewriteCSVWithComplexVars(csv_file) + + def RewriteST(self, st_file): + """ + Rewrite the ST file with complex variables. + """ + if not st_file or not os.path.isfile(st_file): + raise Exception("ST file not valid. Please provide a valid ST file path.") + + self.__clear() + + self.__stFile = st_file + self._parseStTree() + + return self.__rewriteSTWithComplexStructs() diff --git a/GlueGenerator.py b/GlueGenerator.py new file mode 100644 index 000000000..2d90aa704 --- /dev/null +++ b/GlueGenerator.py @@ -0,0 +1,91 @@ +import os +import re +from jinja2 import Environment, FileSystemLoader +from util import paths + +# LOCATED_VARIABLES.h example: +# __LOCATED_VAR(BOOL,__QX0_0,Q,X,0,0) +# __LOCATED_VAR(INT,__QW0,Q,W,0) +# __LOCATED_VAR(BOOL,__QX0_1,Q,X,0,1) + +class GlueGenerator: + + def __init__(self): + self.__loader = FileSystemLoader(os.path.join(paths.AbsDir(__file__), "templates")) + + def __glue_logic(self, varName): + """ + Generate glue logic based on variable type. + """ + + # Extract indices + print(f"Linking variable {varName}") + try: + parts = varName.split("_") + pos1 = int(parts[2][2:]) # number after QX0 or QW0 + pos2 = int(parts[3]) if len(parts) > 3 else 0 + except Exception as e: + raise Exception(f"Error parsing variable name '{varName}': {e}") + + kind = varName[2] # I, Q, M + sub = varName[3] # X, B, W, D, L + + if kind == 'I': + if sub == 'X': + return f"bool_input_ptr[{pos1}][{pos2}] = (IEC_BOOL *){varName};" + elif sub == 'B': + return f"byte_input_ptr[{pos1}] = (IEC_BYTE *){varName};" + elif sub == 'W': + return f"int_input_ptr[{pos1}] = (IEC_UINT *){varName};" + elif sub == 'D': + return f"dint_input_ptr[{pos1}] = (IEC_UDINT *){varName};" + elif sub == 'L': + return f"lint_input_ptr[{pos1}] = (IEC_ULINT *){varName};" + + elif kind == 'Q': + if sub == 'X': + return f"bool_output_ptr[{pos1}][{pos2}] = (IEC_BOOL *){varName};" + elif sub == 'B': + return f"byte_output_ptr[{pos1}] = (IEC_BYTE *){varName};" + elif sub == 'W': + return f"int_output_ptr[{pos1}] = (IEC_UINT *){varName};" + elif sub == 'D': + return f"dint_output_ptr[{pos1}] = (IEC_UDINT *){varName};" + elif sub == 'L': + return f"lint_output_ptr[{pos1}] = (IEC_ULINT *){varName};" + + elif kind == 'M': + if sub == 'W': + return f"int_memory_ptr[{pos1}] = (IEC_UINT *){varName};" + elif sub == 'D': + return f"dint_memory_ptr[{pos1}] = (IEC_UDINT *){varName};" + elif sub == 'L': + return f"lint_memory_ptr[{pos1}] = (IEC_ULINT *){varName};" + + raise Exception(f"Unhandled variable type: {varName}") + + def __parse_line(self, line): + """ + Parse a line from LOCATED_VARIABLES.h to extract variable information. + Example: __LOCATED_VAR(BOOL,__QX0_0,Q,X,0,0) + """ + m = re.match(r"__LOCATED_VAR\(([^,]+),([^,]+),.*\)", line.strip()) + if not m: + print(f"Warning: Line '{line.strip()}' does not match expected format.") + return None + varType, varName = m.group(1), m.group(2) + return {"type": varType, "name": varName, "glue_code": self.__glue_logic(varName)} + + def generate_glue_variables(self, located_vars_lines): + """ + Generate glue variables from the LOCATED_VARIABLES content. + """ + parsed = [] + for line in located_vars_lines: + entry = self.__parse_line(line) + if entry is not None: + parsed.append(entry) + + env = Environment(loader=self.__loader) + template = env.get_template("glueVars.c.j2") + return template.render(vars=parsed) diff --git a/PLCGenerator.py b/PLCGenerator.py index b4572463a..0614ea63c 100644 --- a/PLCGenerator.py +++ b/PLCGenerator.py @@ -1890,7 +1890,7 @@ def GeneratePaths(self, connections, body, order=False, to_inout=False): else: paths.append(variable) elif isinstance(next, CoilClass): - paths.append( + paths.extend( self.GeneratePaths( next.connectionPointIn.getconnections(), body, order ) diff --git a/ProjectController.py b/ProjectController.py index 86a67ecb6..986470f72 100644 --- a/ProjectController.py +++ b/ProjectController.py @@ -1,14 +1,16 @@ -import os -import traceback +import traceback, os 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): @@ -157,11 +159,10 @@ def Generate_plc_debug_cvars(self): ] return variable_decl_array, extern_variables_declarations, enum_types - def Generate_embedded_plc_debugger(self): + def Generate_embedded_plc_debugger(self, st_file): dvars, externs, enums = self.Generate_plc_debug_cvars() - loader = FileSystemLoader(os.path.join(paths.AbsDir(__file__))) - template = Environment(loader=loader).get_template("debug.c.j2") + 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={ @@ -171,6 +172,16 @@ def Generate_embedded_plc_debugger(self): "types": list(set(a.split("_", 1)[0] for a in enums)), } ) + with open(cfile, "w") as f: f.write(debug_text) - return cfile, debug_text + + # Wrap debugger code around (* comments *) + MD5 = hashlib.md5(open(st_file, "rb").read()).hexdigest() + if MD5 is None: + raise ("Error building project: md5 object is null\n") + + # Add MD5 value to debug.cpp file + c_debug = 'char md5[] = "' + MD5 + '";\n' + debug_text + + return cfile, c_debug diff --git a/STParser.py b/STParser.py new file mode 100644 index 000000000..70fc79a5f --- /dev/null +++ b/STParser.py @@ -0,0 +1,171 @@ +import re + +BASE_TYPES = [ + "BOOL", + "SINT", + "INT", + "DINT", + "LINT", + "USINT", + "UINT", + "UDINT", + "ULINT", + "REAL", + "LREAL", + "TIME", + "DATE", + "TOD", + "DT", + "STRING", + "BYTE", + "WORD", + "DWORD", + "LWORD" +] + + +class _Block: + def __init__(self, name): + self.name = name.upper() + self.start = re.compile(rf"^\s*{re.escape(self.name)}\s*$") + self.end = re.compile(rf"^\s*END_{re.escape(self.name)}\s*$") + + def GetInfo(self, line): + match = self.start.match(line) + if match: + return { + "type": self.name, + } + return None + + +class _NamedBlock(_Block): + def __init__(self, name): + super().__init__(name) + self.start = re.compile( + rf"^\s*{re.escape(self.name)}\s+(?P[A-Za-z_][A-Za-z0-9_]*)\s*$" + ) + + def GetInfo(self, line): + match = self.start.match(line) + if match: + return { + "name": match.group("name"), + "type": self.name, + } + return None + + +class _StructBlock(_NamedBlock): + def __init__(self, name): + super().__init__(name) + self.start = re.compile( + rf"^\s*(?P[A-Za-z_][A-Za-z0-9_]*)\s*:\s*{self.name}\s*$" + ) + self.end = re.compile(rf"^\s*END_{re.escape(self.name)}\s*;\s*$") + + +class _DataType: + def __init__(self, name): + self.name = name.upper() + self.definition = re.compile( + r"^\s*" + r"(?P[A-Za-z_][A-Za-z0-9_]*)" # var_name + r"\s*:\s*" + r"(?P[A-Za-z_][A-Za-z0-9_]*)" # var_type + r"(?:\s*:=\s*(?P[^;]+))?" # optional := value + r"\s*;" + r"\s*$" + ) + + def GetInfo(self, line): + match = self.definition.match(line) + if match: + return { + "name": match.group("name"), + "type": self.name, + "data_type": match.group("type"), + "value": match.group("value"), + } + return None + + +class _ArrayType(_DataType): + def __init__(self): + super().__init__("array") + self.definition = re.compile( + rf"^\s*(?P[A-Za-z_][A-Za-z0-9_]*)\s*:\s*{self.name}\s*\[" + r"(?P-?\d+)\s*\.\.\s*(?P-?\d+)" + r"\]\s*OF\s*" + r"(?P[A-Za-z_][A-Za-z0-9_]*)" + r"(?:\s*:=\s*(?P[^;]+))?" # optional := value + r"\s*;\s*$" + ) + + def GetInfo(self, line): + match = self.definition.match(line) + if match: + return { + "name": match.group("name"), + "type": self.name, + "data_type": match.group("type"), + "start": int(match.group("start")), + "end": int(match.group("end")), + } + return None + + +class _ProgramDefinition(_NamedBlock): + def __init__(self): + super().__init__("program_definition") + self.definition = re.compile( + r"^\s*PROGRAM\s+" + r"(?P[a-zA-Z_][a-zA-Z0-9_]*)\s+WITH\s+" + r"(?P[a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*" + r"(?P[a-zA-Z_][a-zA-Z0-9_]*)\s*;" + ) + + def GetInfo(self, line): + match = self.definition.match(line) + if match: + return { + "instance": match.group("instance"), + "task": match.group("task"), + "program": match.group("program"), + "type": self.name, + } + return None + + +TYPE = _Block("type") +FUNCTION_BLOCK = _NamedBlock("function_block") +PROGRAM = _NamedBlock("program") +CONFIGURATION = _NamedBlock("configuration") +RESOURCE = _NamedBlock("resource") +RESOURCE.start = re.compile( + rf"^\s*{RESOURCE.name}\s+(?P[A-Za-z_][A-Za-z0-9_]*)\s+ON\s+PLC\s*$" +) +STRUCT = _StructBlock("struct") +VARIABLE = _DataType("variable") +ARRAY = _ArrayType() +PROGRAM_DEFINITION = _ProgramDefinition() + +ALL_BLOCKS = [ + TYPE, + FUNCTION_BLOCK, + PROGRAM, + CONFIGURATION, + RESOURCE, + STRUCT, + ARRAY, + VARIABLE, +] + +CLOSABLE_BLOCKS = [ + TYPE, + FUNCTION_BLOCK, + PROGRAM, + CONFIGURATION, + RESOURCE, + STRUCT, +] diff --git a/debug.c.j2 b/templates/debug.c.j2 similarity index 100% rename from debug.c.j2 rename to templates/debug.c.j2 diff --git a/templates/function_block.st.j2 b/templates/function_block.st.j2 new file mode 100644 index 000000000..3c0bb40ed --- /dev/null +++ b/templates/function_block.st.j2 @@ -0,0 +1,12 @@ +FUNCTION_BLOCK {{name}} + VAR_INPUT + {%-for line in vars%} + {{line}} + {%- endfor %} + END_VAR + VAR + local_temp : DINT; + END_VAR + + local_temp := 0; +END_FUNCTION_BLOCK \ No newline at end of file diff --git a/templates/glueVars.c.j2 b/templates/glueVars.c.j2 new file mode 100644 index 000000000..b6b4fafd3 --- /dev/null +++ b/templates/glueVars.c.j2 @@ -0,0 +1,151 @@ + +/******************************************************************************** + * Copyright (C) 2025 Autonomy + * + * This file is generated by xml2st. + * Do not edit this file directly. + * If you want to change the content, edit the jinja2 template + * or the source PLC program and regenerate. + ********************************************************************************/ + +#include "iec_std_lib.h" + +#define __LOCATED_VAR(type, name, ...) type __##name; +#include "LOCATED_VARIABLES.h" +#undef __LOCATED_VAR +#define __LOCATED_VAR(type, name, ...) type* name = &__##name; +#include "LOCATED_VARIABLES.h" +#undef __LOCATED_VAR + +TIME __CURRENT_TIME; +BOOL __DEBUG; +extern unsigned long long common_ticktime__; + +#ifdef ARDUINO + + //OpenPLC Buffers + #if defined(__AVR_ATmega328P__) || defined(__AVR_ATmega168__) || defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega16U4__) + + #define MAX_DIGITAL_INPUT 8 + #define MAX_DIGITAL_OUTPUT 32 + #define MAX_ANALOG_INPUT 6 + #define MAX_ANALOG_OUTPUT 32 + #define MAX_MEMORY_WORD 0 + #define MAX_MEMORY_DWORD 0 + #define MAX_MEMORY_LWORD 0 + + IEC_BOOL *bool_input[MAX_DIGITAL_INPUT/8][8]; + IEC_BOOL *bool_output[MAX_DIGITAL_OUTPUT/8][8]; + IEC_UINT *int_input[MAX_ANALOG_INPUT]; + IEC_UINT *int_output[MAX_ANALOG_OUTPUT]; + + // Match pointer names with Linux version + static IEC_BOOL *(*bool_input_ptr)[8] = bool_input; + static IEC_BOOL *(*bool_output_ptr)[8] = bool_output; + static IEC_UINT *(*int_input_ptr) = int_input; + static IEC_UINT *(*int_output_ptr) = int_output; + + #else + + #define MAX_DIGITAL_INPUT 56 + #define MAX_DIGITAL_OUTPUT 56 + #define MAX_ANALOG_INPUT 32 + #define MAX_ANALOG_OUTPUT 32 + #define MAX_MEMORY_WORD 20 + #define MAX_MEMORY_DWORD 20 + #define MAX_MEMORY_LWORD 20 + + IEC_BOOL *bool_input[MAX_DIGITAL_INPUT/8][8]; + IEC_BOOL *bool_output[MAX_DIGITAL_OUTPUT/8][8]; + IEC_UINT *int_input[MAX_ANALOG_INPUT]; + IEC_UINT *int_output[MAX_ANALOG_OUTPUT]; + IEC_UINT *int_memory[MAX_MEMORY_WORD]; + IEC_UDINT *dint_memory[MAX_MEMORY_DWORD]; + IEC_ULINT *lint_memory[MAX_MEMORY_LWORD]; + + // Match pointer names with Linux version + static IEC_BOOL *(*bool_input_ptr)[8] = bool_input; + static IEC_BOOL *(*bool_output_ptr)[8] = bool_output; + static IEC_UINT *(*int_input_ptr) = int_input; + static IEC_UINT *(*int_output_ptr) = int_output; + static IEC_UINT *(*int_memory_ptr) = int_memory; + static IEC_UDINT *(*dint_memory_ptr) = dint_memory; + static IEC_ULINT *(*lint_memory_ptr) = lint_memory; + + #endif + + +#else + + #define BUFFER_SIZE 1024 + + //Internal buffers for I/O and memory. These buffers are defined in the + //main program + + //Booleans + static IEC_BOOL *(*bool_input_ptr)[8] = NULL; + static IEC_BOOL *(*bool_output_ptr)[8] = NULL; + + //Bytes + static IEC_BYTE *(*byte_input_ptr) = NULL; + static IEC_BYTE *(*byte_output_ptr) = NULL; + + //Analog I/O + static IEC_UINT *(*int_input_ptr) = NULL; + static IEC_UINT *(*int_output_ptr) = NULL; + + //32bit I/O + static IEC_UDINT *(*dint_input_ptr) = NULL; + static IEC_UDINT *(*dint_output_ptr) = NULL; + + //64bit I/O + static IEC_ULINT *(*lint_input_ptr) = NULL; + static IEC_ULINT *(*lint_output_ptr) = NULL; + + //Memory + static IEC_UINT *(*int_memory_ptr) = NULL; + static IEC_UDINT *(*dint_memory_ptr) = NULL; + static IEC_ULINT *(*lint_memory_ptr) = NULL; + + void setBufferPointers(IEC_BOOL *input_bool[BUFFER_SIZE][8], IEC_BOOL *output_bool[BUFFER_SIZE][8], + IEC_BYTE *input_byte[BUFFER_SIZE], IEC_BYTE *output_byte[BUFFER_SIZE], + IEC_UINT *input_int[BUFFER_SIZE], IEC_UINT *output_int[BUFFER_SIZE], + IEC_UDINT *input_dint[BUFFER_SIZE], IEC_UDINT *output_dint[BUFFER_SIZE], + IEC_ULINT *input_lint[BUFFER_SIZE], IEC_ULINT *output_lint[BUFFER_SIZE], + IEC_UINT *int_memory[BUFFER_SIZE], IEC_UDINT *dint_memory[BUFFER_SIZE], + IEC_ULINT *lint_memory[BUFFER_SIZE]) + { + bool_input_ptr = input_bool; + bool_output_ptr = output_bool; + byte_input_ptr = input_byte; + byte_output_ptr = output_byte; + int_input_ptr = input_int; + int_output_ptr = output_int; + dint_input_ptr = input_dint; + dint_output_ptr = output_dint; + lint_input_ptr = input_lint; + lint_output_ptr = output_lint; + int_memory_ptr = int_memory; + dint_memory_ptr = dint_memory; + lint_memory_ptr = lint_memory; + } +#endif + +void glueVars() +{ + {% for v in vars %} + {{ v.glue_code }} + {% endfor %} +} + +void updateTime() +{ + __CURRENT_TIME.tv_sec += common_ticktime__ / 1000000000ULL; + __CURRENT_TIME.tv_nsec += common_ticktime__ % 1000000000ULL; + + if (__CURRENT_TIME.tv_nsec >= 1000000000ULL) + { + __CURRENT_TIME.tv_nsec -= 1000000000ULL; + __CURRENT_TIME.tv_sec += 1; + } +} diff --git a/templates/variable_declaration.csv.j2 b/templates/variable_declaration.csv.j2 new file mode 100644 index 000000000..a0e10d3b2 --- /dev/null +++ b/templates/variable_declaration.csv.j2 @@ -0,0 +1 @@ +{{i}};VAR;{{var["name"]}};{{var["name"]}};{{var["type"]}}; \ No newline at end of file diff --git a/xml2st.py b/xml2st.py index 2eac55ee1..81660b0de 100755 --- a/xml2st.py +++ b/xml2st.py @@ -5,12 +5,15 @@ import PLCGenerator from PLCControler import PLCControler from ProjectController import ProjectController +from ComplexParser import ComplexParser +from GlueGenerator import GlueGenerator def compile_xml_to_st(xml_file_path): if not os.path.isfile(xml_file_path) or not xml_file_path.lower().endswith(".xml"): print( - f"Error: Invalid file '{xml_file_path}'. A path to a xml file is expected." + f"Error: Invalid file '{xml_file_path}'. A path to a xml file is expected.", + file=sys.stderr, ) return print(f"Compiling file {xml_file_path}") @@ -25,19 +28,19 @@ def compile_xml_to_st(xml_file_path): if result is not None: if isinstance(result, (tuple, list)) and len(result) == 2: (num, line) = result - print(f"PLC syntax error at line {num}:\n{line}") + print(f"PLC syntax error at line {num}:\n{line}", file=sys.stderr) return elif isinstance(result, str): print(result) return else: - print("Unknown error! Exiting...") + print("Unknown error! Exiting...", file=sys.stderr) return project_tree = plcopen.LoadProject(file_name) if project_tree is None or len(project_tree) < 2: - print(f"Error: Failed to load XML project file.") + print("Error: Failed to load XML project file.", file=sys.stderr) return project = project_tree[0] @@ -56,37 +59,78 @@ def compile_xml_to_st(xml_file_path): sys.exit(1) -def generate_debugger_file(csv_file): +def generate_debugger_file(csv_file, st_file): if not os.path.isfile(csv_file) or not csv_file.lower().endswith(".csv"): - print(f"Error: Invalid file '{csv_file}'. A path to a csv file is expected.") + print( + f"Error: Invalid file '{csv_file}'. A path to a csv file is expected.", + file=sys.stderr, + ) return None, None controler = ProjectController() controler.SetCSVFile(csv_file) - return controler.Generate_embedded_plc_debugger()[1] + return controler.Generate_embedded_plc_debugger(st_file)[1] -def append_debugger_to_st(program_text, debug_text): - # Wrap debugger code around (* comments *) +def append_debugger_to_st(st_file, debug_text): c_debug_lines = debug_text.split("\n") c_debug = [f"(*DBG:{line}*)" for line in c_debug_lines] c_debug = "\n".join(c_debug) - # Concatenate debugger code with st program - return f"{program_text}\n{c_debug}" + with open(st_file, "a") as f: + f.write("\n") + f.write(c_debug) + +def generate_gluevars(located_vars_file): + if not os.path.isfile(located_vars_file) or not located_vars_file.lower().endswith(".h"): + print( + f"Error: Invalid file '{located_vars_file}'. A path to a LOCATED_VARIABLES.h file is expected.", + file=sys.stderr, + ) + return None + + # Read the LOCATED_VARIABLES.h file + with open(located_vars_file, "r") as f: + located_vars = f.readlines() + + # Create an instance of GlueGenerator + generator = GlueGenerator() + glueVars = generator.generate_glue_variables(located_vars) + + if glueVars is None: + print("Error: Failed to generate glue variables.", file=sys.stderr) + return None + # Save the generated glue variables to a file + glue_vars_file = os.path.join(os.path.dirname(located_vars_file), "glueVars.c") + with open(glue_vars_file, "w") as f: + f.write(glueVars) + + # Print success message + print(f"Glue variables saved to {glue_vars_file}") def main(): parser = argparse.ArgumentParser( description="Process a PLCopen XML file and transpiles it into a Structured Text (ST) program." ) - parser.add_argument("--generate-st", type=str, help="The path to the XML file") + parser.add_argument( + "--generate-st", + metavar=("XML_FILE"), + type=str, + help="The path to the XML file" + ) parser.add_argument( "--generate-debug", nargs=2, - metavar=("XML_FILE", "CSV_FILE"), + metavar=("ST_FILE", "CSV_FILE"), type=str, - help="Paths to the XML file and the variables CSV file", + help="Paths to the ST file and the variables CSV file", + ) + parser.add_argument( + "--generate-gluevars", + metavar=("LOCATED_VARS_FILE"), + type=str, + help="The path to the LOCATED_VARIABLES.h file" ) args = parser.parse_args() @@ -101,38 +145,52 @@ def main(): print("Saving ST file...") + st_file = os.path.abspath(args.generate_st).replace("plc.xml", "program.st") + with open(st_file, "w") as file: + file.write(program_text) + + print("Parsing complex variables...") + + complex_parser = ComplexParser() + complex_parser.RewriteST(st_file) + except Exception as e: print(f"Error generating ST file: {e}", file=sys.stderr) sys.exit(1) elif args.generate_debug and len(args.generate_debug) == 2: try: - program_text = compile_xml_to_st(args.generate_debug[0]) + complex_parser = ComplexParser() + complex_parser.AddComplexVars( + args.generate_debug[0], args.generate_debug[1] + ) - if program_text is None: - # This exception will always be caught - raise Exception("Compilation failed, no program text generated.") + debug_text = generate_debugger_file( + args.generate_debug[1], args.generate_debug[0] + ) - debug_text = generate_debugger_file(args.generate_debug[1]) + append_debugger_to_st(args.generate_debug[0], debug_text) - program_text = append_debugger_to_st(program_text, debug_text) + except Exception as e: + print(f"Error generating debug: {e}", file=sys.stderr) + sys.exit(1) - print("Saving files...") + elif args.generate_gluevars: + try: + print("Generating glue variables...") + generate_gluevars(args.generate_gluevars) except Exception as e: - print(f"Error generating debug: {e}", file=sys.stderr) + print(f"Error generating glue variables: {e}", file=sys.stderr) sys.exit(1) else: - print("Error: No valid arguments provided. Use --help for usage information.") + print( + "Error: No valid arguments provided. Use --help for usage information.", + file=sys.stderr, + ) return - st_file = os.path.abspath(args.generate_st or args.generate_debug[0]).replace( - "plc.xml", "program.st" - ) - with open(st_file, "w") as file: - file.write(program_text) - if __name__ == "__main__": main()