diff --git a/CHANGELOG.md b/CHANGELOG.md index 7709e8a..12680fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,8 @@ - Visualisation service objects (visualisation manager and related service GUIDs) are recognised and no longer exported. - `Export To Files` writes the export into a sibling folder named `.codescribe_staging` and only swaps it into place once the export completes. Earlier versions deleted the target folder up front, so a locked folder (an open Explorer window, IDE, git client or antivirus) aborted the export immediately and a mid-export crash left the on-disk copy destroyed. If the target folder is locked and cannot be swapped, the staged files are synced into it instead and the export still succeeds; if that also fails, the error dialog reports the staging folder path, so the completed export is preserved. Syncing also removes files the export did not write, as the swap would have, so a rendering from a previous export is never left beside a newer native xml; any that cannot be removed are named. - Device-tree sibling devices (Ethernet, Modbus and fieldbus devices that sit next to `Plc Logic` as direct children of the PLC device) are exported to `/devices/.xml`, one file per top-level device. On import, each tracked device is removed and recreated from its exported xml; devices fixed by the device package (for example `Local_IO` and `HMI` on IFM hardware) cannot be removed and are skipped with a message, with their configuration carried by the project template. A `_NO_EXPORT` folder added as a direct child of the PLC device disables the device-tree export. -- New `Export Lib To Files` script exports library projects, which keep their objects directly under the project root rather than under a Device. It writes the same on-disk format as `Export To Files`, without the device and application folder levels. Importing a library export back into a project is not yet supported. +- New `Export Lib To Files` script exports library projects, which keep their objects directly under the project root rather than under a Device. It writes the same on-disk format as `Export To Files` in a sibling folder called `POUs`, without the device and application folder levels, so the two exports live side by side without overwriting each other. Service objects (Library Manager, Project Information, Project Settings, and the Task/Symbol/Visualization/Alarm/Recipe manager objects) are skipped. +- New `Import Lib From Files` script reads the `POUs` folder back into a library project. Objects that CODESYS generates implicitly (for example functions generated from the project information) cannot be removed and are left in place during the import instead of aborting it. ## v0.2.0 diff --git a/README.md b/README.md index d2f425d..a0f2c78 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ CODESCRIBE supplies five scripts that run inside the CODESYS ScriptEngine and ap - `Save As Template` generates a "template" project by making a copy of the current project and deleting all exportable objects - `Update From Template` updates the current working project by copying the template file and importing the plaintext files - `Export Lib To Files` exports a library project, which keeps its objects directly under the project root rather than under a Device (see [Export Lib To Files](#export-lib-to-files)) +- `Import Lib From Files` imports the files exported by `Export Lib To Files` back into a library project A typical workflow: @@ -115,9 +116,9 @@ Exports made with older versions of CODESCRIBE use different filenames for some ## Export Lib To Files -Library projects keep their POUs, DUTs, GVLs and folders directly under the project root rather than under a Device, so `Export To Files` (which walks device entrypoints) exports nothing for them. `Export Lib To Files` walks the project root instead and writes the same on-disk format as `Export To Files`, without the device and application folder levels. Service objects (Library Manager, Project Information, Project Settings, and the Task/Symbol/Visualization/Alarm/Recipe manager objects) are skipped. +Library projects keep their POUs, DUTs, GVLs and folders directly under the project root rather than under a Device, so `Export To Files` (which walks device entrypoints) exports nothing for them. `Export Lib To Files` walks the project root instead and writes the same on-disk format as `Export To Files` in a sibling folder called `POUs`, without the device and application folder levels. Service objects (Library Manager, Project Information, Project Settings, and the Task/Symbol/Visualization/Alarm/Recipe manager objects) are skipped. -Importing a library export back into a project is not yet supported. +`Import Lib From Files` reads the `POUs` folder back into a library project: it removes tracked objects from the project root and recreates them from the exported files. ## Project Templates @@ -209,7 +210,7 @@ Once installed, proceed to [Adding the Script Toolbar to CODESYS](#adding-the-sc 5. Under Categories, scroll down, select ScriptEngine Commands and pick the script you want to add - - Scripts supplied by this repo are `Export To Files`, `Export Lib To Files`, `Import From Files`, `Save As Template` and `Update From Template` + - Scripts supplied by this repo are `Export To Files`, `Export Lib To Files`, `Import From Files`, `Import Lib From Files`, `Save As Template` and `Update From Template` ![Step 5](docs/step_5.png) diff --git a/config.json b/config.json index d37de0b..aeb7bc7 100644 --- a/config.json +++ b/config.json @@ -20,6 +20,13 @@ "Path": "codescribe\\src\\script_import_from_files.py", "Params": [] }, + { + "Name": "Import Lib From Files", + "Desc": "Import a library project from files on disk.", + "Icon": "codescribe\\icons\\import_lib_from_files.ico", + "Path": "codescribe\\src\\script_lib_import_from_files.py", + "Params": [] + }, { "Name": "Save As Template", "Desc": "Save as template.", diff --git a/icons/import_lib_from_files.ico b/icons/import_lib_from_files.ico new file mode 100644 index 0000000..a4a1ffc Binary files /dev/null and b/icons/import_lib_from_files.ico differ diff --git a/icons/import_lib_from_files.png b/icons/import_lib_from_files.png new file mode 100644 index 0000000..eac1d07 Binary files /dev/null and b/icons/import_lib_from_files.png differ diff --git a/src/entrypoint.py b/src/entrypoint.py index 2a99c86..a476dcd 100644 --- a/src/entrypoint.py +++ b/src/entrypoint.py @@ -11,6 +11,13 @@ def get_src_folder(project): return src_folder +def get_lib_src_folder(project): + # Sibling of the device/application export folder so the two exports never clash. + working_dir = os.path.dirname(project.path) + project_name, _ = os.path.splitext(os.path.basename(project.path)) + return os.path.join(working_dir, "{}.POUs".format(project_name)) + + def get_device_entrypoints(project): children = project.get_children() for child in children: diff --git a/src/import_export.py b/src/import_export.py index 1d14074..6f3ab3c 100644 --- a/src/import_export.py +++ b/src/import_export.py @@ -446,5 +446,16 @@ def export_visualisation_manager(child_obj, parent_obj, parent_folder_path, expo def remove_tracked_objects(obj_list): for obj in obj_list: if get_object_type(obj) in OBJECT_TYPE_TO_EXPORT_FUNCTION: - print("Removing " + obj.get_name()) - obj.remove() + try: + print("Removing " + obj.get_name()) + obj.remove() + except StandardError as remove_error: + # Implicitly generated objects (e.g. functions generated from the project + # information) cannot be removed; leave them in place. + print( + "Warning: could not remove '" + + obj.get_name() + + "' (" + + str(remove_error) + + "); leaving it in place" + ) diff --git a/src/import_from_files.py b/src/import_from_files.py index 593b352..6c43416 100644 --- a/src/import_from_files.py +++ b/src/import_from_files.py @@ -2,10 +2,17 @@ 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 entrypoint import ( + find_application, + find_communication, + get_device_entrypoints, + get_lib_src_folder, + get_src_folder, +) from import_export import * from util import * +SKIP_NAMES = ["Library Information"] def first_word_of_line_iter(f): for line in f.readlines(): @@ -97,3 +104,18 @@ def import_from_files(project): import_communication(communication, device_folder) import_device_tree_siblings(device_obj, device_folder) + + +def import_lib_from_files(project): + pous_folder = get_lib_src_folder(project) + print("Reading from: " + pous_folder) + assert_path_exists(pous_folder) + + # Service/manager objects are not in OBJECT_TYPE_TO_EXPORT_FUNCTION, so they are + # left in place here (they are ignored by the lib export too). + tracked_children = [obj for obj in project.get_children() if obj.get_name() not in SKIP_NAMES] + remove_tracked_objects(tracked_children) + + for child in sorted(os.listdir(pous_folder), key=lambda x: x.count(".")): + if os.path.splitext(child)[0] not in SKIP_NAMES: + import_directory_child(child, pous_folder, project) diff --git a/src/script_lib_export_to_files.py b/src/script_lib_export_to_files.py index 33520e0..ead3227 100644 --- a/src/script_lib_export_to_files.py +++ b/src/script_lib_export_to_files.py @@ -5,7 +5,7 @@ import scriptengine # type: ignore -from entrypoint import get_src_folder +from entrypoint import get_lib_src_folder import graphical_export from import_export import OBJECT_TYPE_TO_EXPORT_FUNCTION, SERVICE_EXPORT_FUNCTIONS, write_native from object_type import ObjectType, get_object_type @@ -17,6 +17,7 @@ SKIP_NAMES = [ "Library Manager", "Project Information", + "Library Information", "Project Settings", "Task Configuration", "Symbol Configuration", @@ -69,7 +70,7 @@ def export_child(child_obj, parent_obj, parent_folder_path): assert_project_open() graphical_export.reset_stats() - src_folder = get_src_folder(scriptengine.projects.primary) + src_folder = get_lib_src_folder(scriptengine.projects.primary) print("Writing to: " + src_folder) # A device project keeps its objects under Devices, which this walker never diff --git a/src/script_lib_import_from_files.py b/src/script_lib_import_from_files.py new file mode 100644 index 0000000..83bd5a9 --- /dev/null +++ b/src/script_lib_import_from_files.py @@ -0,0 +1,31 @@ +# REMEMBER: this is python 2.7 +from __future__ import print_function + +import scriptengine # type: ignore + +from import_from_files import import_lib_from_files +from util import * + +try: + print_python_version() + assert_project_open() + + ui_continue = scriptengine.system.ui.prompt( + "Import Lib From Files will overwrite unexported changes in the current project!\n\nDo you want to continue?", + choice=scriptengine.PromptChoice.YesNo, + default_result=scriptengine.PromptResult.No, + store_description="Don't show again", + store_key="import_lib_from_files_confirm", + ) + + if ui_continue == scriptengine.PromptResult.Yes: + import_lib_from_files(scriptengine.projects.primary) + ui_info("Import Lib From Files complete.") + else: + print("Import cancelled.") +except Exception as e: + print(e) + ui_error_with_traceback("Import Lib From Files failed!") + raise e + +print("Done!")