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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ Exporting communication devices has been hardcoded to create folders for top-lev

Devices that have no `Communication` object at all are also supported since v0.2.0. The communication step is skipped for that device and no `communication` folder is created in the export; before v0.2.0 this aborted the export, import or Save As Template for the whole device.

A second layout is also supported: on standard CODESYS hardware, Ethernet, Modbus and fieldbus devices often sit in the device tree as direct children of the PLC device, next to `Plc Logic`, rather than under a `Communication` node. These device-tree siblings are exported to `<device>/devices/<name>.xml` using a native recursive export, one file per top-level device. Children that contain an `Application` (for example the SafetyPLC and StandardPLC of a compound safety project) are excluded, as they are already exported through their own device entrypoint.

**To disable the device-tree export, add a folder with the name `_NO_EXPORT` as a direct child of the PLC device.** The existing `_NO_EXPORT` folder under `Communication` continues to disable only the `Communication` export.

## Status

CODESCRIBE has been tested only on CODESYS V3.5 SP11, using the project structure supplied by the IFM CR711s packages. Version 0.2.0 was validated end-to-end on V3.5 SP11 (32-bit) with an IFM CR711S compound (Standard + SIL2) project: export/import round-trip, code generation, simulation and a hardware download.
Expand Down
115 changes: 115 additions & 0 deletions src/device_tree_import_export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# REMEMBER: this is python 2.7
import os

from communication_import_export import NO_EXPORT_FOLDER_NAME
from import_export import read_native, write_native
from object_type import ObjectType, get_object_type
from util import *

DEVICE_TREE_FOLDER_NAME = "devices"


def no_export_device_tree(device_obj):
# Only a direct FOLDER child of the PLC device disables this export. A _NO_EXPORT
# folder nested under Communication keeps disabling the Communication export alone.
for child in device_obj.get_children():
if get_object_type(child) == ObjectType.FOLDER and child.get_name() == NO_EXPORT_FOLDER_NAME:
return True
return False


def _find_device_tree_sibling(device_obj, name):
# Match the export scope: only direct DEVICE children of the PLC device.
for child in device_obj.get_children():
if get_object_type(child) == ObjectType.DEVICE and child.get_name() == name:
return child
return None


def export_device_tree_siblings(device_obj, device_folder, application, communication):
"""
Export device-tree devices that sit next to Plc Logic as direct children of the PLC device
(Ethernet, Modbus, fieldbus). Each one is written as a single native recursive xml file under
<device_folder>/devices. Returns True when at least one device was exported.
"""
if no_export_device_tree(device_obj):
return False

siblings = []
for child in device_obj.get_children():
if get_object_type(child) != ObjectType.DEVICE:
continue
if communication is not None and child == communication:
continue
# Skip children that carry an Application anywhere in their subtree, not just the
# Application passed in. In compound safety projects (e.g. the IFM CR711s) the
# SafetyPLC and StandardPLC are DEVICE children that each carry their own
# Application and are already exported through their own device entrypoint.
if first_or_none(child.find("Application", recursive=True)) is not None:
continue
siblings.append(child)

if len(siblings) < 1:
return False

devices_folder = os.path.join(device_folder, DEVICE_TREE_FOLDER_NAME)
os.mkdir(devices_folder)

for child in siblings:
write_native(child, os.path.join(devices_folder, child.get_name() + ".xml"), recursive=True)

return True


def import_device_tree_siblings(device_obj, device_folder):
# import_native always pastes the archived object as a child of the receiving node,
# it never replaces in place. So the only way to update a tracked device is to
# remove it and import the xml into the PLC device. Devices fixed by the device
# package (e.g. Local_IO and HMI on IFM hardware) cannot be removed; their
# configuration is carried by the project template, like the fixed top-level
# communication devices, so they are skipped with a message.
devices_folder = os.path.join(device_folder, DEVICE_TREE_FOLDER_NAME)
if not os.path.exists(devices_folder):
return

if no_export_device_tree(device_obj):
return

for child_name in sorted(os.listdir(devices_folder)):
name, ext = os.path.splitext(child_name)
if ext != ".xml":
continue

device_node = _find_device_tree_sibling(device_obj, name)
if device_node is not None:
try:
device_node.remove()
except Exception:
print("Cannot remove fixed device " + name + ", skipping its import (the template carries its configuration)")
continue
# Missing device (fresh project from a template) and removed device both
# import the same way: the xml recreates the device under the PLC device.
read_native(os.path.join(devices_folder, child_name), device_obj)


def remove_tracked_device_tree_devices(device_obj, device_folder):
devices_folder = os.path.join(device_folder, DEVICE_TREE_FOLDER_NAME)
if not os.path.exists(devices_folder):
return

if no_export_device_tree(device_obj):
return

for child_name in os.listdir(devices_folder):
name, ext = os.path.splitext(child_name)
if ext != ".xml":
continue

device_node = _find_device_tree_sibling(device_obj, name)
if device_node is None:
continue
try:
device_node.remove()
except Exception:
# Fixed package devices stay in the template; import skips them too.
print("Cannot remove fixed device " + name + ", leaving it in the template")
3 changes: 3 additions & 0 deletions src/import_from_files.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os

from communication_import_export import import_communication
from device_tree_import_export import import_device_tree_siblings
from entrypoint import find_application, find_communication, get_device_entrypoints, get_src_folder
from import_export import *
from util import *
Expand Down Expand Up @@ -94,3 +95,5 @@ def import_from_files(project):
communication = find_communication(device_obj)
if communication is not None:
import_communication(communication, device_folder)

import_device_tree_siblings(device_obj, device_folder)
3 changes: 3 additions & 0 deletions src/script_export_to_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import scriptengine # type: ignore

from communication_import_export import export_communication
from device_tree_import_export import export_device_tree_siblings
from entrypoint import find_application, find_communication, get_device_entrypoints, get_src_folder
from import_export import OBJECT_TYPE_TO_EXPORT_FUNCTION, write_native
from object_type import ObjectType, get_object_type
Expand Down Expand Up @@ -62,6 +63,8 @@ def export_child(child_obj, parent_obj, parent_folder_path):
if communication is not None:
export_communication(communication, device_folder)

export_device_tree_siblings(device_obj, device_folder, application, communication)

finalize_export_folder(src_folder, staging_folder)
except Exception as e:
print(e)
Expand Down
9 changes: 8 additions & 1 deletion src/script_save_as_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
import scriptengine # type: ignore

from communication_import_export import remove_tracked_communication_devices
from entrypoint import find_application, find_communication, get_device_entrypoints
from device_tree_import_export import remove_tracked_device_tree_devices
from entrypoint import find_application, find_communication, get_device_entrypoints, get_src_folder
from import_export import *
from project_template import find_template_paths_and_versions, generate_template_path
from util import *
Expand Down Expand Up @@ -53,6 +54,12 @@ def delete_old_templates(template_paths):
if communication is not None:
remove_tracked_communication_devices(communication)

# The list of tracked devices lives in the primary project's export folder.
# The template copy has its own file name, so its src folder never exists
# on disk and would make this a no-op.
device_folder = os.path.join(get_src_folder(scriptengine.projects.primary), device_obj.get_name())
remove_tracked_device_tree_devices(device_obj, device_folder)

template_project.save()

if len(template_paths) > 0:
Expand Down
1 change: 1 addition & 0 deletions tools/ci/import_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"entrypoint",
"import_export",
"communication_import_export",
"device_tree_import_export",
"import_from_files",
"project_template",
]
Expand Down
Loading