Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ Items are exported in formatted structured text (`.st`) where possible, and in n

Since v0.2.0, Actions and Transitions export as `.st` rather than native xml. The kind is encoded in the filename (`MyPou.MyAction.action.st`, `MyPou.MyTransition.transition.st`) and the file contains the implementation text only, as these objects have no textual declaration. Exports made with earlier versions (where these objects were `.xml`) still import correctly; re-exporting once after upgrading migrates the tracked files to the new format.

Visualisations export as `<name>.vis.xml`. Earlier versions wrote `<name>.xml`, which silently collided with any POU of the same name (a `Main` program plus a `Main` visualisation is common). Old plain `.xml` exports still import correctly; re-exporting once migrates the tracked files.

## Project Templates

The intention of CODESCRIBE is not to export a complete copy of the project, but to only export the implementation logic of the project, enabling collaboration via git and other source control methods. An empty, but configured, underlying project file should also be committed to the repo to manage any other configuration that CODESYS provides (e.g. project level configuration, device configuration). For example, `Example Project_template_v1.project`:
Expand Down
3 changes: 2 additions & 1 deletion src/communication_import_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,6 @@ def remove_tracked_communication_devices(communication_obj):

# remove all children from top level devices
for top_level_device in communication_obj.get_children():
for child in top_level_device.get_children():
# snapshot the children: removing while iterating the live collection skips entries
for child in list(top_level_device.get_children()):
child.remove()
28 changes: 24 additions & 4 deletions src/import_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,16 @@ def export_gvl(child_obj, parent_obj, parent_folder_path, export_child_fn):
write_st_decl_only(child_obj, f)


def find_gvl_or_error(parent_obj, name, err):
# Persistent variable lists carry their own GUID, so a freshly imported GVL can be
# either type.
for gvl_type in (ObjectType.GVL, ObjectType.GVL_PERSISTENT):
found = first_of_type_or_none(parent_obj.find(name), gvl_type)
if found is not None:
return found
raise ValueError(err)


def import_gvl(child, dir_path, dir_parent_obj, import_dir_fn):
"""
Import the native xml and then overwrite the textual definition with the structured text.
Expand All @@ -140,8 +150,8 @@ def import_gvl(child, dir_path, dir_parent_obj, import_dir_fn):
gvl_xml_path = os.path.join(dir_path, name + ".gvl.xml")
if os.path.exists(gvl_xml_path):
import_native(gvl_xml_path, dir_path, dir_parent_obj, import_dir_fn)
imported_obj = first_of_type_or_error(
dir_parent_obj.find(name), ObjectType.GVL, name + " GVL should have been created, but cannot be found"
imported_obj = find_gvl_or_error(
dir_parent_obj, name, name + " GVL should have been created, but cannot be found"
)
else:
imported_obj = dir_parent_obj.create_gvl(name)
Expand All @@ -154,6 +164,12 @@ def export_native(child_obj, parent_obj, parent_folder_path, export_child_fn):
write_native(child_obj, os.path.join(parent_folder_path, child_obj.get_name() + ".xml"), recursive=False)


def export_visualisation(child_obj, parent_obj, parent_folder_path, export_child_fn):
# Visualisations regularly share a name with a POU (a Main program and a Main
# visualisation, for example), which would collide on Main.xml.
write_native(child_obj, os.path.join(parent_folder_path, child_obj.get_name() + ".vis.xml"), recursive=False)


def export_native_recursive(child_obj, parent_obj, parent_folder_path, export_child_fn):
write_native(child_obj, os.path.join(parent_folder_path, child_obj.get_name() + ".xml"), recursive=True)

Expand All @@ -172,7 +188,9 @@ def import_dut(child, dir_path, dir_parent_obj, import_dir_fn):
dut_obj = dir_parent_obj.create_dut(filename)
with open_utf8(os.path.join(dir_path, child), "r") as f:
f.seek(0)
dut_obj.textual_declaration.replace(f.read())
# Normalize trailing whitespace the same way import_st does, so that
# export -> import -> export round-trips are byte-identical.
dut_obj.textual_declaration.replace(f.read().strip() + u"\n")


def export_method(child_obj, parent_obj, parent_folder_path, export_child_fn):
Expand Down Expand Up @@ -287,11 +305,13 @@ def import_sub_pou(child, dir_path, dir_parent_obj, import_dir_fn):
ObjectType.FOLDER: export_folder,
ObjectType.POU: export_pou,
ObjectType.GVL: export_gvl, # EVL, NVL are "special types" of GVL which show up with the same UUID
ObjectType.GVL_PERSISTENT: export_gvl, # persistent variable lists have their own GUID
ObjectType.EVC: export_native,
ObjectType.VISUALISATION: export_native,
ObjectType.VISUALISATION: export_visualisation,
ObjectType.TASK_CONFIGURATION: export_native_recursive,
ObjectType.DUT: export_dut,
ObjectType.METHOD: export_method,
ObjectType.METHOD_NORET: export_method, # methods without a return type have their own GUID
ObjectType.PROPERTY: export_sub_pou,
ObjectType.ACTION: export_action,
ObjectType.TRANSITION: export_transition,
Expand Down
6 changes: 6 additions & 0 deletions src/import_from_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ def import_directory_child(child, dir_path, dir_parent_obj):
pass
if ext == ".st":
import_gvl(child, dir_path, dir_parent_obj, import_directory)
elif filename.endswith(".vis"):
# Visualisations export with a .vis.xml suffix so they cannot collide with a
# POU of the same name. Plain <name>.xml visualisations from older exports
# still arrive through the bare-xml import below.
if ext == ".xml":
import_native(child, dir_path, dir_parent_obj, import_directory)
elif "." in filename:
# . means some sort of sub POU (method, action, transition, property)
if ext == ".xml":
Expand Down
29 changes: 28 additions & 1 deletion src/object_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,20 @@
class ObjectType:
POU = "POU"
DUT = "DUT"
GVL = "EVL"
GVL = "GVL"
GVL_PERSISTENT = "GVL_PERSISTENT"
EVC = "EVC"
INTERFACE = "INTERFACE"
TASK = "TASK"
METHOD = "METHOD"
METHOD_NORET = "METHOD_NORET"
PROPERTY = "PROPERTY"
PROPERTY_METHOD = "PROPERTY_METHOD"
ACTION = "ACTION"
TRANSITION = "TRANSITION"
IMAGEPOOL = "IMAGEPOOL"
TEXTLIST = "TEXTLIST"
GLOBAL_TEXTLIST = "GLOBAL_TEXTLIST"
LIBRARY_MANAGER = "LIBRARY_MANAGER"
TASK_CONFIGURATION = "TASK_CONFIGURATION"
PROJECT_INFORMATION = "PROJECT_INFORMATION"
Expand All @@ -19,6 +27,10 @@ class ObjectType:
FOLDER = "FOLDER"
CALL_TO_POU = "CALL_TO_POU"
VISUALISATION = "VISUALISATION"
VISUALISATION_MANAGER = "VISUALISATION_MANAGER"
TARGET_VISUALISATION = "TARGET_VISUALISATION"
WEB_VISUALISATION = "WEB_VISUALISATION"
VISUALISATION_STYLE = "VISUALISATION_STYLE"
UNKNOWN = "UNKNOWN"

@classmethod
Expand All @@ -38,10 +50,18 @@ def __iter__(cls):
"6f9dac99-8de1-4efc-8465-68ac443b7d08": ObjectType.POU,
"2db5746d-d284-4425-9f7f-2663a34b0ebc": ObjectType.DUT,
"ffbfa93a-b94d-45fc-a329-229860183b1d": ObjectType.GVL,
"261bd6e6-249c-4232-bb6f-84c2fbeef430": ObjectType.GVL_PERSISTENT,
"327b6465-4e7f-4116-846a-8369c730fd66": ObjectType.EVC,
"6654496c-404d-479a-aad2-8551054e5f1e": ObjectType.INTERFACE,
"98a2708a-9b18-4f31-82ed-a1465b24fa2d": ObjectType.TASK,
"f8a58466-d7f6-439f-bbb8-d4600e41d099": ObjectType.METHOD,
"f89f7675-27f1-46b3-8abb-b7da8e774ffd": ObjectType.METHOD_NORET,
"5a3b8626-d3e9-4f37-98b5-66420063d91e": ObjectType.PROPERTY,
"792f2eb6-721e-4e64-ba20-bc98351056db": ObjectType.PROPERTY_METHOD,
"8ac092e5-3128-4e26-9e7e-11016c6684f2": ObjectType.ACTION,
"bb0b9044-714e-4614-ad3e-33cbdf34d16b": ObjectType.IMAGEPOOL,
"2bef0454-1bd3-412a-ac2c-af0f31dbc40f": ObjectType.TEXTLIST,
"63784cbb-9ba0-45e6-9d69-babf3f040511": ObjectType.GLOBAL_TEXTLIST,
"a10c6218-cb94-436f-91c6-e1652575253d": ObjectType.TRANSITION,
"adb5cb65-8e1d-4a00-b70a-375ea27582f3": ObjectType.LIBRARY_MANAGER,
"ae1de277-a207-4a28-9efb-456c06bd52f3": ObjectType.TASK_CONFIGURATION,
Expand All @@ -51,6 +71,13 @@ def __iter__(cls):
"738bea1e-99bb-4f04-90bb-a7a567e74e3a": ObjectType.FOLDER,
"413e2a7d-adb1-4d2c-be29-6ae6e4fab820": ObjectType.CALL_TO_POU,
"f18bec89-9fef-401d-9953-2f11739a6808": ObjectType.VISUALISATION,
# Visualisation service objects. Named so the UNKNOWN export fallback does not pick
# them up: exporting the manager makes every later import raise an interactive
# overwrite dialog, and the project template carries these objects anyway.
"4d3fdb8f-ab50-4c35-9d3a-d4bb9bb9a628": ObjectType.VISUALISATION_MANAGER,
"bc63f5fa-d286-4786-994e-7b27e4f97bd5": ObjectType.TARGET_VISUALISATION,
"0fdbf158-1ae0-47d9-9269-cd84be308e9d": ObjectType.WEB_VISUALISATION,
"8e687a04-7ca7-42d3-be06-fcbda676c5ef": ObjectType.VISUALISATION_STYLE,
}


Expand Down
6 changes: 2 additions & 4 deletions src/project_template.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
# REMEMBER: this is python 2.7
import os

import scriptengine # type: ignore

PROJECT_EXT = ".project"
TEMPLATE_FILEPART = "_template_v"


def find_template_paths_and_versions(project):
working_dir = os.path.dirname(project.path)
project_name, _ = os.path.splitext(os.path.basename(scriptengine.projects.primary.path))
project_name, _ = os.path.splitext(os.path.basename(project.path))

template_name_start = project_name + TEMPLATE_FILEPART

Expand All @@ -33,7 +31,7 @@ def find_template_paths_and_versions(project):

def generate_template_path(project, version_number):
working_dir = os.path.dirname(project.path)
project_name, _ = os.path.splitext(os.path.basename(scriptengine.projects.primary.path))
project_name, _ = os.path.splitext(os.path.basename(project.path))

template_name_start = project_name + TEMPLATE_FILEPART

Expand Down
23 changes: 21 additions & 2 deletions src/script_export_to_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@

from communication_import_export import export_communication
from entrypoint import find_application, find_communication, get_device_entrypoints, get_src_folder
from import_export import OBJECT_TYPE_TO_EXPORT_FUNCTION
from object_type import get_object_type
from import_export import OBJECT_TYPE_TO_EXPORT_FUNCTION, write_native
from object_type import ObjectType, get_object_type
from util import *


Expand All @@ -18,6 +18,25 @@ def export_child(child_obj, parent_obj, parent_folder_path):
export_fn = OBJECT_TYPE_TO_EXPORT_FUNCTION.get(child_obj_type)
if export_fn is not None:
export_fn(child_obj, parent_obj, parent_folder_path, export_child)
return

if child_obj_type == ObjectType.UNKNOWN:
# An unmapped GUID would otherwise be dropped from the export without a trace
# (seen with diagram objects such as an LD nested inside a CFC). Fall back to a
# native export and keep walking the children.
try:
write_native(child_obj, os.path.join(parent_folder_path, child_obj.get_name() + ".xml"), recursive=False)
except Exception as fallback_error:
print(
"Warning: failed to export '"
+ child_obj.get_name()
+ "' (unmapped type GUID "
+ str(child_obj.type)
+ "): "
+ str(fallback_error)
)
for c in child_obj.get_children():
export_child(c, child_obj, parent_folder_path)


try:
Expand Down
Loading