@codesys/mcp-toolkit 1.1.5 → 1.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -5
- package/dist/bin.js +2 -41
- package/dist/scriptLoader.js +18 -1
- package/dist/scripts/check_status.py +21 -0
- package/dist/scripts/compile_project.py +65 -0
- package/dist/scripts/create_method.py +80 -0
- package/dist/scripts/create_pou.py +96 -0
- package/dist/scripts/create_project.py +54 -0
- package/dist/scripts/create_property.py +78 -0
- package/dist/scripts/ensure_project_open.py +154 -0
- package/dist/scripts/find_object_by_path.py +111 -0
- package/dist/scripts/get_pou_code.py +91 -0
- package/dist/scripts/get_project_structure.py +72 -0
- package/dist/scripts/open_project.py +25 -0
- package/dist/scripts/save_project.py +29 -0
- package/dist/scripts/set_pou_code.py +104 -0
- package/dist/server.js +319 -503
- package/package.json +16 -4
- package/src/scripts/check_status.py +21 -0
- package/src/scripts/compile_project.py +65 -0
- package/src/scripts/create_method.py +80 -0
- package/src/scripts/create_pou.py +96 -0
- package/src/scripts/create_project.py +54 -0
- package/src/scripts/create_property.py +78 -0
- package/src/scripts/ensure_project_open.py +154 -0
- package/src/scripts/find_object_by_path.py +111 -0
- package/src/scripts/get_pou_code.py +91 -0
- package/src/scripts/get_project_structure.py +72 -0
- package/src/scripts/open_project.py +25 -0
- package/src/scripts/save_project.py +29 -0
- package/src/scripts/set_pou_code.py +104 -0
- package/.claude/settings.local.json +0 -13
- package/CONTRIBUTING.md +0 -72
- package/codesys_scripts/listener_script.py +0 -1539
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codesys/mcp-toolkit",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.7",
|
|
4
4
|
"description": "Model Context Protocol (MCP) server for CODESYS automation platform",
|
|
5
5
|
"main": "dist/server.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
11
|
-
"
|
|
11
|
+
"prebuild": "rm -rf dist",
|
|
12
|
+
"build": "tsc && npm run copy-scripts",
|
|
13
|
+
"copy-scripts": "mkdir -p dist/scripts && cp -r src/scripts/* dist/scripts/",
|
|
12
14
|
"start": "node ./dist/server.js"
|
|
13
15
|
},
|
|
14
16
|
"keywords": [
|
|
@@ -33,6 +35,16 @@
|
|
|
33
35
|
"engines": {
|
|
34
36
|
"node": ">=18.0.0"
|
|
35
37
|
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist/",
|
|
40
|
+
"src/scripts/",
|
|
41
|
+
"docs/",
|
|
42
|
+
"examples/",
|
|
43
|
+
"CHANGELOG.md",
|
|
44
|
+
"LICENSE",
|
|
45
|
+
"README.md",
|
|
46
|
+
"scriptDoc.txt"
|
|
47
|
+
],
|
|
36
48
|
"dependencies": {
|
|
37
49
|
"@modelcontextprotocol/sdk": "^1.10.2",
|
|
38
50
|
"axios": "^1.6.8",
|
|
@@ -41,8 +53,8 @@
|
|
|
41
53
|
"zod": "^3.24.3"
|
|
42
54
|
},
|
|
43
55
|
"devDependencies": {
|
|
44
|
-
"@types/node": "^20.
|
|
56
|
+
"@types/node": "^20.14.1",
|
|
45
57
|
"@types/yargs": "^17.0.33",
|
|
46
|
-
"typescript": "^5.
|
|
58
|
+
"typescript": "^5.8.3"
|
|
47
59
|
}
|
|
48
60
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import sys, scriptengine as script_engine, os, traceback
|
|
2
|
+
project_open = False; project_name = "No project open"; project_path = "N/A"; scripting_ok = False
|
|
3
|
+
try:
|
|
4
|
+
scripting_ok = True; primary_project = script_engine.projects.primary
|
|
5
|
+
if primary_project:
|
|
6
|
+
project_open = True
|
|
7
|
+
try:
|
|
8
|
+
project_path = os.path.normcase(os.path.abspath(primary_project.path))
|
|
9
|
+
try:
|
|
10
|
+
project_name = primary_project.get_name() # Might fail
|
|
11
|
+
if not project_name: project_name = "Unnamed (path: %s)" % os.path.basename(project_path)
|
|
12
|
+
except: project_name = "Unnamed (path: %s)" % os.path.basename(project_path)
|
|
13
|
+
except Exception as e_path: project_path = "N/A (Error: %s)" % e_path; project_name = "Unnamed (Path Error)"
|
|
14
|
+
print("Project Open: %s" % project_open); print("Project Name: %s" % project_name)
|
|
15
|
+
print("Project Path: %s" % project_path); print("Scripting OK: %s" % scripting_ok)
|
|
16
|
+
print("SCRIPT_SUCCESS: Status check complete."); sys.exit(0)
|
|
17
|
+
except Exception as e:
|
|
18
|
+
error_message = "Error during status check: %s" % e
|
|
19
|
+
print(error_message); print("Scripting OK: False")
|
|
20
|
+
# traceback.print_exc() # Optional traceback
|
|
21
|
+
print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import sys, scriptengine as script_engine, os, traceback
|
|
2
|
+
|
|
3
|
+
# Import ensure_project_open function from the template
|
|
4
|
+
# This will be prepended by the script loader
|
|
5
|
+
def ensure_project_open(target_project_path):
|
|
6
|
+
# Function implementation will be included from ensure_project_open.py
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
print("DEBUG: compile_project script: Project='%s'" % PROJECT_FILE_PATH)
|
|
11
|
+
primary_project = ensure_project_open(PROJECT_FILE_PATH)
|
|
12
|
+
project_name = os.path.basename(PROJECT_FILE_PATH)
|
|
13
|
+
target_app = None
|
|
14
|
+
app_name = "N/A"
|
|
15
|
+
|
|
16
|
+
# Try getting active application first
|
|
17
|
+
try:
|
|
18
|
+
target_app = primary_project.active_application
|
|
19
|
+
if target_app:
|
|
20
|
+
app_name = getattr(target_app, 'get_name', lambda: "Unnamed App (Active)")()
|
|
21
|
+
print("DEBUG: Found active application: %s" % app_name)
|
|
22
|
+
except Exception as active_err:
|
|
23
|
+
print("WARN: Could not get active application: %s. Searching..." % active_err)
|
|
24
|
+
|
|
25
|
+
# If no active app, search for the first one
|
|
26
|
+
if not target_app:
|
|
27
|
+
print("DEBUG: Searching for first compilable application...")
|
|
28
|
+
apps = []
|
|
29
|
+
try:
|
|
30
|
+
# Search recursively through all project objects
|
|
31
|
+
all_children = primary_project.get_children(True)
|
|
32
|
+
for child in all_children:
|
|
33
|
+
# Check using the marker property and if build method exists
|
|
34
|
+
if hasattr(child, 'is_application') and child.is_application and hasattr(child, 'build'):
|
|
35
|
+
app_name_found = getattr(child, 'get_name', lambda: "Unnamed App")()
|
|
36
|
+
print("DEBUG: Found potential application object: %s" % app_name_found)
|
|
37
|
+
apps.append(child)
|
|
38
|
+
break # Take the first one found
|
|
39
|
+
except Exception as find_err: print("WARN: Error finding application object: %s" % find_err)
|
|
40
|
+
|
|
41
|
+
if not apps: raise RuntimeError("No compilable application found in project '%s'" % project_name)
|
|
42
|
+
target_app = apps[0]
|
|
43
|
+
app_name = getattr(target_app, 'get_name', lambda: "Unnamed App (First Found)")()
|
|
44
|
+
print("WARN: Compiling first found application: %s" % app_name)
|
|
45
|
+
|
|
46
|
+
print("DEBUG: Calling build() on app '%s'..." % app_name)
|
|
47
|
+
if not hasattr(target_app, 'build'):
|
|
48
|
+
raise TypeError("Selected object '%s' is not an application or doesn't support build()." % app_name)
|
|
49
|
+
|
|
50
|
+
# Execute the build
|
|
51
|
+
target_app.build();
|
|
52
|
+
print("DEBUG: Build command executed for application '%s'." % app_name)
|
|
53
|
+
|
|
54
|
+
# Check messages is harder without direct access to message store from script.
|
|
55
|
+
# Rely on CODESYS UI or log output for now.
|
|
56
|
+
print("Compile Initiated For Application: %s" % app_name)
|
|
57
|
+
print("In Project: %s" % project_name)
|
|
58
|
+
print("SCRIPT_SUCCESS: Application compilation initiated.")
|
|
59
|
+
sys.exit(0)
|
|
60
|
+
except Exception as e:
|
|
61
|
+
detailed_error = traceback.format_exc()
|
|
62
|
+
error_message = "Error initiating compilation for project %s: %s\\n%s" % (PROJECT_FILE_PATH, e, detailed_error)
|
|
63
|
+
print(error_message)
|
|
64
|
+
print("SCRIPT_ERROR: %s" % error_message)
|
|
65
|
+
sys.exit(1)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import sys, scriptengine as script_engine, os, traceback
|
|
2
|
+
|
|
3
|
+
# Import ensure_project_open function from the template
|
|
4
|
+
# This will be prepended by the script loader
|
|
5
|
+
def ensure_project_open(target_project_path):
|
|
6
|
+
# Function implementation will be included from ensure_project_open.py
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
# Import find_object_by_path function from the template
|
|
10
|
+
# This will be prepended by the script loader
|
|
11
|
+
def find_object_by_path_robust(start_node, full_path, target_type_name="object"):
|
|
12
|
+
# Function implementation will be included from find_object_by_path.py
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
PARENT_POU_FULL_PATH = "{PARENT_POU_FULL_PATH}" # e.g., "Application/MyFB"
|
|
16
|
+
METHOD_NAME = "{METHOD_NAME}"
|
|
17
|
+
RETURN_TYPE = "{RETURN_TYPE}" # Can be empty string for no return type
|
|
18
|
+
# Optional: Language
|
|
19
|
+
# LANG_GUID_STR = "{LANG_GUID_STR}" # Example if needed
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
print("DEBUG: create_method script: ParentPOU='%s', Name='%s', ReturnType='%s', Project='%s'" % (PARENT_POU_FULL_PATH, METHOD_NAME, RETURN_TYPE, PROJECT_FILE_PATH))
|
|
23
|
+
primary_project = ensure_project_open(PROJECT_FILE_PATH)
|
|
24
|
+
if not PARENT_POU_FULL_PATH: raise ValueError("Parent POU full path empty.")
|
|
25
|
+
if not METHOD_NAME: raise ValueError("Method name empty.")
|
|
26
|
+
# RETURN_TYPE can be empty
|
|
27
|
+
|
|
28
|
+
# Find the parent POU object
|
|
29
|
+
parent_pou_object = find_object_by_path_robust(primary_project, PARENT_POU_FULL_PATH, "parent POU")
|
|
30
|
+
if not parent_pou_object: raise ValueError("Parent POU object not found: %s" % PARENT_POU_FULL_PATH)
|
|
31
|
+
|
|
32
|
+
parent_pou_name = getattr(parent_pou_object, 'get_name', lambda: PARENT_POU_FULL_PATH)()
|
|
33
|
+
print("DEBUG: Found Parent POU object: %s" % parent_pou_name)
|
|
34
|
+
|
|
35
|
+
# Check if parent object supports creating methods (should implement ScriptIecLanguageMemberContainer)
|
|
36
|
+
if not hasattr(parent_pou_object, 'create_method'):
|
|
37
|
+
raise TypeError("Parent object '%s' of type %s does not support create_method." % (parent_pou_name, type(parent_pou_object).__name__))
|
|
38
|
+
|
|
39
|
+
# Default language to None (usually ST)
|
|
40
|
+
lang_guid = None
|
|
41
|
+
# Use None if RETURN_TYPE is empty string, otherwise use the string
|
|
42
|
+
actual_return_type = RETURN_TYPE if RETURN_TYPE else None
|
|
43
|
+
print("DEBUG: Calling create_method: Name='%s', ReturnType=%s, Lang=%s" % (METHOD_NAME, actual_return_type, lang_guid))
|
|
44
|
+
|
|
45
|
+
# Call the create_method method ON THE PARENT POU
|
|
46
|
+
new_method_object = parent_pou_object.create_method(
|
|
47
|
+
name=METHOD_NAME,
|
|
48
|
+
return_type=actual_return_type,
|
|
49
|
+
language=lang_guid # Pass None to use default
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if new_method_object:
|
|
53
|
+
new_meth_name = getattr(new_method_object, 'get_name', lambda: METHOD_NAME)()
|
|
54
|
+
print("DEBUG: Method object created: %s" % new_meth_name)
|
|
55
|
+
|
|
56
|
+
# --- SAVE THE PROJECT TO PERSIST THE NEW METHOD OBJECT ---
|
|
57
|
+
try:
|
|
58
|
+
print("DEBUG: Saving Project (after method creation)...")
|
|
59
|
+
primary_project.save()
|
|
60
|
+
print("DEBUG: Project saved successfully after method creation.")
|
|
61
|
+
except Exception as save_err:
|
|
62
|
+
print("ERROR: Failed to save Project after creating method: %s" % save_err)
|
|
63
|
+
detailed_error = traceback.format_exc()
|
|
64
|
+
error_message = "Error saving Project after creating method '%s': %s\\n%s" % (METHOD_NAME, save_err, detailed_error)
|
|
65
|
+
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
|
|
66
|
+
# --- END SAVING ---
|
|
67
|
+
|
|
68
|
+
print("Method Created: %s" % new_meth_name)
|
|
69
|
+
print("Parent POU: %s" % PARENT_POU_FULL_PATH)
|
|
70
|
+
print("Return Type: %s" % (RETURN_TYPE if RETURN_TYPE else "(None)"))
|
|
71
|
+
print("SCRIPT_SUCCESS: Method created successfully.")
|
|
72
|
+
sys.exit(0)
|
|
73
|
+
else:
|
|
74
|
+
error_message = "Failed to create method '%s' under '%s'. create_method returned None." % (METHOD_NAME, parent_pou_name)
|
|
75
|
+
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
|
|
76
|
+
|
|
77
|
+
except Exception as e:
|
|
78
|
+
detailed_error = traceback.format_exc()
|
|
79
|
+
error_message = "Error creating method '%s' under POU '%s' in project '%s': %s\\n%s" % (METHOD_NAME, PARENT_POU_FULL_PATH, PROJECT_FILE_PATH, e, detailed_error)
|
|
80
|
+
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import sys, scriptengine as script_engine, os, traceback
|
|
2
|
+
|
|
3
|
+
# Import ensure_project_open function from the template
|
|
4
|
+
# This will be prepended by the script loader
|
|
5
|
+
def ensure_project_open(target_project_path):
|
|
6
|
+
# Function implementation will be included from ensure_project_open.py
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
# Import find_object_by_path function from the template
|
|
10
|
+
# This will be prepended by the script loader
|
|
11
|
+
def find_object_by_path_robust(start_node, full_path, target_type_name="object"):
|
|
12
|
+
# Function implementation will be included from find_object_by_path.py
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
POU_NAME = "{POU_NAME}"
|
|
16
|
+
POU_TYPE_STR = "{POU_TYPE_STR}"
|
|
17
|
+
IMPL_LANGUAGE_STR = "{IMPL_LANGUAGE_STR}"
|
|
18
|
+
PARENT_PATH_REL = "{PARENT_PATH}"
|
|
19
|
+
|
|
20
|
+
pou_type_map = {
|
|
21
|
+
"Program": script_engine.PouType.Program,
|
|
22
|
+
"FunctionBlock": script_engine.PouType.FunctionBlock,
|
|
23
|
+
"Function": script_engine.PouType.Function
|
|
24
|
+
}
|
|
25
|
+
# Map common language names to ImplementationLanguages attributes if needed (optional, None usually works)
|
|
26
|
+
# lang_map = { "ST": script_engine.ImplementationLanguage.st, ... }
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
print("DEBUG: create_pou script: Name='%s', Type='%s', Lang='%s', ParentPath='%s', Project='%s'" % (POU_NAME, POU_TYPE_STR, IMPL_LANGUAGE_STR, PARENT_PATH_REL, PROJECT_FILE_PATH))
|
|
30
|
+
primary_project = ensure_project_open(PROJECT_FILE_PATH)
|
|
31
|
+
if not POU_NAME: raise ValueError("POU name empty.")
|
|
32
|
+
if not PARENT_PATH_REL: raise ValueError("Parent path empty.")
|
|
33
|
+
|
|
34
|
+
# Resolve POU Type Enum
|
|
35
|
+
pou_type_enum = pou_type_map.get(POU_TYPE_STR)
|
|
36
|
+
if not pou_type_enum: raise ValueError("Invalid POU type string: %s. Use Program, FunctionBlock, or Function." % POU_TYPE_STR)
|
|
37
|
+
|
|
38
|
+
# Find parent object using the robust function
|
|
39
|
+
parent_object = find_object_by_path_robust(primary_project, PARENT_PATH_REL, "parent container")
|
|
40
|
+
if not parent_object: raise ValueError("Parent object not found for path: %s" % PARENT_PATH_REL)
|
|
41
|
+
|
|
42
|
+
parent_name = getattr(parent_object, 'get_name', lambda: str(parent_object))()
|
|
43
|
+
print("DEBUG: Using parent object: %s (Type: %s)" % (parent_name, type(parent_object).__name__))
|
|
44
|
+
|
|
45
|
+
# Check if parent object supports creating POUs (should implement ScriptIecLanguageObjectContainer)
|
|
46
|
+
if not hasattr(parent_object, 'create_pou'):
|
|
47
|
+
raise TypeError("Parent object '%s' of type %s does not support create_pou." % (parent_name, type(parent_object).__name__))
|
|
48
|
+
|
|
49
|
+
# Set language GUID to None (let CODESYS default based on parent/settings)
|
|
50
|
+
lang_guid = None
|
|
51
|
+
print("DEBUG: Setting language to None (will use default).")
|
|
52
|
+
# Example if mapping language string: lang_guid = lang_map.get(IMPL_LANGUAGE_STR, None)
|
|
53
|
+
|
|
54
|
+
print("DEBUG: Calling parent_object.create_pou: Name='%s', Type=%s, Lang=%s" % (POU_NAME, pou_type_enum, lang_guid))
|
|
55
|
+
|
|
56
|
+
# Call create_pou using keyword arguments
|
|
57
|
+
new_pou = parent_object.create_pou(
|
|
58
|
+
name=POU_NAME,
|
|
59
|
+
type=pou_type_enum,
|
|
60
|
+
language=lang_guid # Pass None
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
print("DEBUG: parent_object.create_pou returned: %s" % new_pou)
|
|
64
|
+
if new_pou:
|
|
65
|
+
new_pou_name = getattr(new_pou, 'get_name', lambda: POU_NAME)()
|
|
66
|
+
print("DEBUG: POU object created: %s" % new_pou_name)
|
|
67
|
+
|
|
68
|
+
# --- SAVE THE PROJECT TO PERSIST THE NEW POU ---
|
|
69
|
+
try:
|
|
70
|
+
print("DEBUG: Saving Project...")
|
|
71
|
+
primary_project.save() # Save the overall project file
|
|
72
|
+
print("DEBUG: Project saved successfully after POU creation.")
|
|
73
|
+
except Exception as save_err:
|
|
74
|
+
print("ERROR: Failed to save Project after POU creation: %s" % save_err)
|
|
75
|
+
detailed_error = traceback.format_exc()
|
|
76
|
+
error_message = "Error saving Project after creating POU '%s': %s\\n%s" % (new_pou_name, save_err, detailed_error)
|
|
77
|
+
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
|
|
78
|
+
# --- END SAVING ---
|
|
79
|
+
|
|
80
|
+
print("POU Created: %s" % new_pou_name)
|
|
81
|
+
print("Type: %s" % POU_TYPE_STR)
|
|
82
|
+
print("Language: %s (Defaulted)" % IMPL_LANGUAGE_STR)
|
|
83
|
+
print("Parent Path: %s" % PARENT_PATH_REL)
|
|
84
|
+
print("SCRIPT_SUCCESS: POU created successfully.")
|
|
85
|
+
sys.exit(0)
|
|
86
|
+
else:
|
|
87
|
+
error_message = "Failed to create POU '%s'. create_pou returned None." % POU_NAME
|
|
88
|
+
print(error_message)
|
|
89
|
+
print("SCRIPT_ERROR: %s" % error_message)
|
|
90
|
+
sys.exit(1)
|
|
91
|
+
except Exception as e:
|
|
92
|
+
detailed_error = traceback.format_exc()
|
|
93
|
+
error_message = "Error creating POU '%s' in project '%s': %s\\n%s" % (POU_NAME, PROJECT_FILE_PATH, e, detailed_error)
|
|
94
|
+
print(error_message)
|
|
95
|
+
print("SCRIPT_ERROR: Error creating POU '%s': %s" % (POU_NAME, e))
|
|
96
|
+
sys.exit(1)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import sys, scriptengine as script_engine, os, shutil, time, traceback
|
|
2
|
+
# Placeholders
|
|
3
|
+
TEMPLATE_PROJECT_PATH = r'{TEMPLATE_PROJECT_PATH}' # Path to Standard.project
|
|
4
|
+
PROJECT_FILE_PATH = r'{PROJECT_FILE_PATH}' # Path for the new project (Target Path)
|
|
5
|
+
try:
|
|
6
|
+
print("DEBUG: Python script create_project (copy from template):")
|
|
7
|
+
print("DEBUG: Template Source = %s" % TEMPLATE_PROJECT_PATH)
|
|
8
|
+
print("DEBUG: Target Path = %s" % PROJECT_FILE_PATH)
|
|
9
|
+
if not PROJECT_FILE_PATH: raise ValueError("Target project file path empty.")
|
|
10
|
+
if not TEMPLATE_PROJECT_PATH: raise ValueError("Template project file path empty.")
|
|
11
|
+
if not os.path.exists(TEMPLATE_PROJECT_PATH): raise IOError("Template project file not found: %s" % TEMPLATE_PROJECT_PATH)
|
|
12
|
+
|
|
13
|
+
# 1. Copy the template project file to the new location
|
|
14
|
+
target_dir = os.path.dirname(PROJECT_FILE_PATH)
|
|
15
|
+
if not os.path.exists(target_dir): print("DEBUG: Creating target directory: %s" % target_dir); os.makedirs(target_dir)
|
|
16
|
+
# Check if target file already exists
|
|
17
|
+
if os.path.exists(PROJECT_FILE_PATH): print("WARN: Target project file already exists, overwriting: %s" % PROJECT_FILE_PATH)
|
|
18
|
+
|
|
19
|
+
print("DEBUG: Copying '%s' to '%s'..." % (TEMPLATE_PROJECT_PATH, PROJECT_FILE_PATH))
|
|
20
|
+
shutil.copy2(TEMPLATE_PROJECT_PATH, PROJECT_FILE_PATH) # copy2 preserves metadata
|
|
21
|
+
print("DEBUG: File copy complete.")
|
|
22
|
+
|
|
23
|
+
# 2. Open the newly copied project file
|
|
24
|
+
print("DEBUG: Opening the copied project: %s" % PROJECT_FILE_PATH)
|
|
25
|
+
# Set flags for silent opening
|
|
26
|
+
update_mode = script_engine.VersionUpdateFlags.NoUpdates | script_engine.VersionUpdateFlags.SilentMode
|
|
27
|
+
# try:
|
|
28
|
+
# update_mode = script_engine.VersionUpdateFlags.NoUpdates | script_engine.VersionUpdateFlags.SilentMode
|
|
29
|
+
# except AttributeError:
|
|
30
|
+
# print("WARN: VersionUpdateFlags not found, using integer flags for open (1 | 2 = 3).")
|
|
31
|
+
# update_mode = 3
|
|
32
|
+
|
|
33
|
+
project = script_engine.projects.open(PROJECT_FILE_PATH, update_flags=update_mode)
|
|
34
|
+
print("DEBUG: script_engine.projects.open returned: %s" % project)
|
|
35
|
+
if project:
|
|
36
|
+
print("DEBUG: Pausing briefly after open...")
|
|
37
|
+
time.sleep(1.0) # Allow CODESYS to potentially initialize things
|
|
38
|
+
try:
|
|
39
|
+
print("DEBUG: Explicitly saving project after opening copy...")
|
|
40
|
+
project.save();
|
|
41
|
+
print("DEBUG: Project save after opening copy succeeded.")
|
|
42
|
+
except Exception as save_err:
|
|
43
|
+
print("WARN: Explicit save after opening copy failed: %s" % save_err)
|
|
44
|
+
# Decide if this is critical - maybe not, but good to know.
|
|
45
|
+
print("Project Created from Template Copy at: %s" % PROJECT_FILE_PATH)
|
|
46
|
+
print("SCRIPT_SUCCESS: Project copied from template and opened successfully.")
|
|
47
|
+
sys.exit(0)
|
|
48
|
+
else:
|
|
49
|
+
error_message = "Failed to open project copy %s after copying template %s. projects.open returned None." % (PROJECT_FILE_PATH, TEMPLATE_PROJECT_PATH)
|
|
50
|
+
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
detailed_error = traceback.format_exc()
|
|
53
|
+
error_message = "Error creating project '%s' from template '%s': %s\\n%s" % (PROJECT_FILE_PATH, TEMPLATE_PROJECT_PATH, e, detailed_error)
|
|
54
|
+
print(error_message); print("SCRIPT_ERROR: Error copying/opening template: %s" % e); sys.exit(1)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import sys, scriptengine as script_engine, os, traceback
|
|
2
|
+
|
|
3
|
+
# Import ensure_project_open function from the template
|
|
4
|
+
# This will be prepended by the script loader
|
|
5
|
+
def ensure_project_open(target_project_path):
|
|
6
|
+
# Function implementation will be included from ensure_project_open.py
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
# Import find_object_by_path function from the template
|
|
10
|
+
# This will be prepended by the script loader
|
|
11
|
+
def find_object_by_path_robust(start_node, full_path, target_type_name="object"):
|
|
12
|
+
# Function implementation will be included from find_object_by_path.py
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
PARENT_POU_FULL_PATH = "{PARENT_POU_FULL_PATH}" # e.g., "Application/MyFB"
|
|
16
|
+
PROPERTY_NAME = "{PROPERTY_NAME}"
|
|
17
|
+
PROPERTY_TYPE = "{PROPERTY_TYPE}"
|
|
18
|
+
# Optional: Language for Getter/Setter (usually defaults to ST)
|
|
19
|
+
# LANG_GUID_STR = "{LANG_GUID_STR}" # Example if needed
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
print("DEBUG: create_property script: ParentPOU='%s', Name='%s', Type='%s', Project='%s'" % (PARENT_POU_FULL_PATH, PROPERTY_NAME, PROPERTY_TYPE, PROJECT_FILE_PATH))
|
|
23
|
+
primary_project = ensure_project_open(PROJECT_FILE_PATH)
|
|
24
|
+
if not PARENT_POU_FULL_PATH: raise ValueError("Parent POU full path empty.")
|
|
25
|
+
if not PROPERTY_NAME: raise ValueError("Property name empty.")
|
|
26
|
+
if not PROPERTY_TYPE: raise ValueError("Property type empty.")
|
|
27
|
+
|
|
28
|
+
# Find the parent POU object
|
|
29
|
+
parent_pou_object = find_object_by_path_robust(primary_project, PARENT_POU_FULL_PATH, "parent POU")
|
|
30
|
+
if not parent_pou_object: raise ValueError("Parent POU object not found: %s" % PARENT_POU_FULL_PATH)
|
|
31
|
+
|
|
32
|
+
parent_pou_name = getattr(parent_pou_object, 'get_name', lambda: PARENT_POU_FULL_PATH)()
|
|
33
|
+
print("DEBUG: Found Parent POU object: %s" % parent_pou_name)
|
|
34
|
+
|
|
35
|
+
# Check if parent object supports creating properties (should implement ScriptIecLanguageMemberContainer)
|
|
36
|
+
if not hasattr(parent_pou_object, 'create_property'):
|
|
37
|
+
raise TypeError("Parent object '%s' of type %s does not support create_property." % (parent_pou_name, type(parent_pou_object).__name__))
|
|
38
|
+
|
|
39
|
+
# Default language to None (usually ST)
|
|
40
|
+
lang_guid = None
|
|
41
|
+
print("DEBUG: Calling create_property: Name='%s', Type='%s', Lang=%s" % (PROPERTY_NAME, PROPERTY_TYPE, lang_guid))
|
|
42
|
+
|
|
43
|
+
# Call the create_property method ON THE PARENT POU
|
|
44
|
+
new_property_object = parent_pou_object.create_property(
|
|
45
|
+
name=PROPERTY_NAME,
|
|
46
|
+
return_type=PROPERTY_TYPE,
|
|
47
|
+
language=lang_guid # Pass None to use default
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if new_property_object:
|
|
51
|
+
new_prop_name = getattr(new_property_object, 'get_name', lambda: PROPERTY_NAME)()
|
|
52
|
+
print("DEBUG: Property object created: %s" % new_prop_name)
|
|
53
|
+
|
|
54
|
+
# --- SAVE THE PROJECT TO PERSIST THE NEW PROPERTY OBJECT ---
|
|
55
|
+
try:
|
|
56
|
+
print("DEBUG: Saving Project (after property creation)...")
|
|
57
|
+
primary_project.save()
|
|
58
|
+
print("DEBUG: Project saved successfully after property creation.")
|
|
59
|
+
except Exception as save_err:
|
|
60
|
+
print("ERROR: Failed to save Project after creating property: %s" % save_err)
|
|
61
|
+
detailed_error = traceback.format_exc()
|
|
62
|
+
error_message = "Error saving Project after creating property '%s': %s\\n%s" % (PROPERTY_NAME, save_err, detailed_error)
|
|
63
|
+
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
|
|
64
|
+
# --- END SAVING ---
|
|
65
|
+
|
|
66
|
+
print("Property Created: %s" % new_prop_name)
|
|
67
|
+
print("Parent POU: %s" % PARENT_POU_FULL_PATH)
|
|
68
|
+
print("Type: %s" % PROPERTY_TYPE)
|
|
69
|
+
print("SCRIPT_SUCCESS: Property created successfully.")
|
|
70
|
+
sys.exit(0)
|
|
71
|
+
else:
|
|
72
|
+
error_message = "Failed to create property '%s' under '%s'. create_property returned None." % (PROPERTY_NAME, parent_pou_name)
|
|
73
|
+
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
|
|
74
|
+
|
|
75
|
+
except Exception as e:
|
|
76
|
+
detailed_error = traceback.format_exc()
|
|
77
|
+
error_message = "Error creating property '%s' under POU '%s' in project '%s': %s\\n%s" % (PROPERTY_NAME, PARENT_POU_FULL_PATH, PROJECT_FILE_PATH, e, detailed_error)
|
|
78
|
+
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import scriptengine as script_engine
|
|
3
|
+
import os
|
|
4
|
+
import time
|
|
5
|
+
import traceback
|
|
6
|
+
|
|
7
|
+
# --- Function to ensure the correct project is open ---
|
|
8
|
+
MAX_RETRIES = 3
|
|
9
|
+
RETRY_DELAY = 2.0 # seconds (use float for time.sleep)
|
|
10
|
+
|
|
11
|
+
def ensure_project_open(target_project_path):
|
|
12
|
+
print("DEBUG: Ensuring project is open: %s" % target_project_path)
|
|
13
|
+
# Normalize target path once
|
|
14
|
+
normalized_target_path = os.path.normcase(os.path.abspath(target_project_path))
|
|
15
|
+
|
|
16
|
+
for attempt in range(MAX_RETRIES):
|
|
17
|
+
print("DEBUG: Ensure project attempt %d/%d for %s" % (attempt + 1, MAX_RETRIES, normalized_target_path))
|
|
18
|
+
primary_project = None
|
|
19
|
+
try:
|
|
20
|
+
# Getting primary project might fail if CODESYS instance is unstable
|
|
21
|
+
primary_project = script_engine.projects.primary
|
|
22
|
+
except Exception as primary_err:
|
|
23
|
+
print("WARN: Error getting primary project: %s. Assuming none." % primary_err)
|
|
24
|
+
# traceback.print_exc() # Optional: Print stack trace for this error
|
|
25
|
+
primary_project = None
|
|
26
|
+
|
|
27
|
+
current_project_path = ""
|
|
28
|
+
project_ok = False # Flag to check if target is confirmed primary and accessible
|
|
29
|
+
|
|
30
|
+
if primary_project:
|
|
31
|
+
try:
|
|
32
|
+
# Getting path should be relatively safe if primary_project object exists
|
|
33
|
+
current_project_path = os.path.normcase(os.path.abspath(primary_project.path))
|
|
34
|
+
print("DEBUG: Current primary project path: %s" % current_project_path)
|
|
35
|
+
if current_project_path == normalized_target_path:
|
|
36
|
+
# Found the right project as primary, now check if it's usable
|
|
37
|
+
print("DEBUG: Target project path matches primary. Checking access...")
|
|
38
|
+
try:
|
|
39
|
+
# Try a relatively safe operation to confirm object usability
|
|
40
|
+
# Getting children count is a reasonable check
|
|
41
|
+
_ = len(primary_project.get_children(False))
|
|
42
|
+
print("DEBUG: Target project '%s' is primary and accessible." % target_project_path)
|
|
43
|
+
project_ok = True
|
|
44
|
+
return primary_project # SUCCESS CASE 1: Already open and accessible
|
|
45
|
+
except Exception as access_err:
|
|
46
|
+
# Project found, but accessing it failed. Might be unstable.
|
|
47
|
+
print("WARN: Primary project access check failed for '%s': %s. Will attempt reopen." % (current_project_path, access_err))
|
|
48
|
+
# traceback.print_exc() # Optional: Print stack trace
|
|
49
|
+
primary_project = None # Force reopen by falling through
|
|
50
|
+
else:
|
|
51
|
+
# A *different* project is primary
|
|
52
|
+
print("DEBUG: Primary project is '%s', not the target '%s'." % (current_project_path, normalized_target_path))
|
|
53
|
+
# Consider closing the wrong project if causing issues, but for now, just open target
|
|
54
|
+
# try:
|
|
55
|
+
# print("DEBUG: Closing incorrect primary project '%s'..." % current_project_path)
|
|
56
|
+
# primary_project.close() # Be careful with unsaved changes
|
|
57
|
+
# except Exception as close_err:
|
|
58
|
+
# print("WARN: Failed to close incorrect primary project: %s" % close_err)
|
|
59
|
+
primary_project = None # Force open target project
|
|
60
|
+
|
|
61
|
+
except Exception as path_err:
|
|
62
|
+
# Failed even to get the path of the supposed primary project
|
|
63
|
+
print("WARN: Could not get path of current primary project: %s. Assuming not the target." % path_err)
|
|
64
|
+
# traceback.print_exc() # Optional: Print stack trace
|
|
65
|
+
primary_project = None # Force open target project
|
|
66
|
+
|
|
67
|
+
# If target project not confirmed as primary and accessible, attempt to open/reopen
|
|
68
|
+
if not project_ok:
|
|
69
|
+
# Log clearly whether we are opening initially or reopening
|
|
70
|
+
if primary_project is None and current_project_path == "":
|
|
71
|
+
print("DEBUG: No primary project detected. Attempting to open target: %s" % target_project_path)
|
|
72
|
+
elif primary_project is None and current_project_path != "":
|
|
73
|
+
print("DEBUG: Primary project was '%s' but failed access check or needed close. Attempting to open target: %s" % (current_project_path, target_project_path))
|
|
74
|
+
else: # Includes cases where wrong project was open
|
|
75
|
+
print("DEBUG: Target project not primary or initial check failed. Attempting to open/reopen: %s" % target_project_path)
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
# Set flags for silent opening, handle potential attribute errors
|
|
79
|
+
update_mode = script_engine.VersionUpdateFlags.NoUpdates | script_engine.VersionUpdateFlags.SilentMode
|
|
80
|
+
# try:
|
|
81
|
+
# update_mode = script_engine.VersionUpdateFlags.NoUpdates | script_engine.VersionUpdateFlags.SilentMode
|
|
82
|
+
# except AttributeError:
|
|
83
|
+
# print("WARN: VersionUpdateFlags not found, using integer flags for open (1 | 2 = 3).")
|
|
84
|
+
# update_mode = 3 # 1=NoUpdates, 2=SilentMode
|
|
85
|
+
|
|
86
|
+
opened_project = None
|
|
87
|
+
try:
|
|
88
|
+
# The actual open call
|
|
89
|
+
print("DEBUG: Calling script_engine.projects.open('%s', update_flags=%s)..." % (target_project_path, update_mode))
|
|
90
|
+
opened_project = script_engine.projects.open(target_project_path, update_flags=update_mode)
|
|
91
|
+
|
|
92
|
+
if not opened_project:
|
|
93
|
+
# This is a critical failure if open returns None without exception
|
|
94
|
+
print("ERROR: projects.open returned None for %s on attempt %d" % (target_project_path, attempt + 1))
|
|
95
|
+
# Allow retry loop to continue
|
|
96
|
+
else:
|
|
97
|
+
# Open call returned *something*, let's verify
|
|
98
|
+
print("DEBUG: projects.open call returned an object for: %s" % target_project_path)
|
|
99
|
+
print("DEBUG: Pausing for stabilization after open...")
|
|
100
|
+
time.sleep(RETRY_DELAY) # Give CODESYS time
|
|
101
|
+
# Re-verify: Is the project now primary and accessible?
|
|
102
|
+
recheck_primary = None
|
|
103
|
+
try: recheck_primary = script_engine.projects.primary
|
|
104
|
+
except Exception as recheck_primary_err: print("WARN: Error getting primary project after reopen: %s" % recheck_primary_err)
|
|
105
|
+
|
|
106
|
+
if recheck_primary:
|
|
107
|
+
recheck_path = ""
|
|
108
|
+
try: # Getting path might fail
|
|
109
|
+
recheck_path = os.path.normcase(os.path.abspath(recheck_primary.path))
|
|
110
|
+
except Exception as recheck_path_err:
|
|
111
|
+
print("WARN: Failed to get path after reopen: %s" % recheck_path_err)
|
|
112
|
+
|
|
113
|
+
if recheck_path == normalized_target_path:
|
|
114
|
+
print("DEBUG: Target project confirmed as primary after reopening.")
|
|
115
|
+
try: # Final sanity check
|
|
116
|
+
_ = len(recheck_primary.get_children(False))
|
|
117
|
+
print("DEBUG: Reopened project basic access confirmed.")
|
|
118
|
+
return recheck_primary # SUCCESS CASE 2: Successfully opened/reopened
|
|
119
|
+
except Exception as access_err_reopen:
|
|
120
|
+
print("WARN: Reopened project (%s) basic access check failed: %s." % (normalized_target_path, access_err_reopen))
|
|
121
|
+
# traceback.print_exc() # Optional
|
|
122
|
+
# Allow retry loop to continue
|
|
123
|
+
else:
|
|
124
|
+
print("WARN: Different project is primary after reopening! Expected '%s', got '%s'." % (normalized_target_path, recheck_path))
|
|
125
|
+
# Allow retry loop to continue, maybe it fixes itself
|
|
126
|
+
else:
|
|
127
|
+
print("WARN: No primary project found after reopening attempt %d!" % (attempt+1))
|
|
128
|
+
# Allow retry loop to continue
|
|
129
|
+
|
|
130
|
+
except Exception as open_err:
|
|
131
|
+
# Catch errors during the open call itself
|
|
132
|
+
print("ERROR: Exception during projects.open call on attempt %d: %s" % (attempt + 1, open_err))
|
|
133
|
+
traceback.print_exc() # Crucial for diagnosing open failures
|
|
134
|
+
# Allow retry loop to continue
|
|
135
|
+
|
|
136
|
+
except Exception as outer_open_err:
|
|
137
|
+
# Catch errors in the flag setup etc.
|
|
138
|
+
print("ERROR: Unexpected error during open setup/logic attempt %d: %s" % (attempt + 1, outer_open_err))
|
|
139
|
+
traceback.print_exc()
|
|
140
|
+
|
|
141
|
+
# If we didn't return successfully in this attempt, wait before retrying
|
|
142
|
+
if attempt < MAX_RETRIES - 1:
|
|
143
|
+
print("DEBUG: Ensure project attempt %d did not succeed. Waiting %f seconds..." % (attempt + 1, RETRY_DELAY))
|
|
144
|
+
time.sleep(RETRY_DELAY)
|
|
145
|
+
else: # Last attempt failed
|
|
146
|
+
print("ERROR: Failed all ensure_project_open attempts for %s." % normalized_target_path)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# If all retries fail after the loop
|
|
150
|
+
raise RuntimeError("Failed to ensure project '%s' is open and accessible after %d attempts." % (target_project_path, MAX_RETRIES))
|
|
151
|
+
# --- End of function ---
|
|
152
|
+
|
|
153
|
+
# Placeholder for the project file path (must be set in scripts using this snippet)
|
|
154
|
+
PROJECT_FILE_PATH = r"{PROJECT_FILE_PATH}"
|