@codesys/mcp-toolkit
Version:
Model Context Protocol (MCP) server for CODESYS automation platform
900 lines (816 loc) • 90.2 kB
JavaScript
"use strict";
/**
* server.ts
* MCP Server for interacting with CODESYS via Python scripting.
* Implements all MCP resources and tools that interact with the CODESYS environment.
*
* IMPORTANT: This server receives configuration as parameters from bin.ts,
* which helps avoid issues with command-line argument passing in different execution environments.
* (Incorporates script templates from v1.6.9 and improved tool descriptions)
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.startMcpServer = startMcpServer;
// --- Import 'os' FIRST ---
const os = __importStar(require("os"));
// --- End Import 'os' ---
// --- STARTUP LOG ---
console.error(`>>> SERVER.TS TOP LEVEL EXECUTION @ ${new Date().toISOString()} <<<`);
console.error(`>>> Node: ${process.version}, Platform: ${os.platform()}, Arch: ${os.arch()}`);
console.error(`>>> Initial CWD: ${process.cwd()}`);
// --- End Startup Log ---
// --- Necessary Imports ---
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
const zod_1 = require("zod");
const codesys_interop_1 = require("./codesys_interop"); // Assumes this utility exists
const path = __importStar(require("path"));
const promises_1 = require("fs/promises"); // For file existence check (async)
// --- Wrap server logic in an exported function ---
function startMcpServer(config) {
return __awaiter(this, void 0, void 0, function* () {
console.error(`>>> SERVER.TS startMcpServer() CALLED @ ${new Date().toISOString()} <<<`);
console.error(`>>> Config Received: ${JSON.stringify(config)}`);
// --- Use config values directly ---
const WORKSPACE_DIR = config.workspaceDir;
const codesysExePath = config.codesysPath;
const codesysProfileName = config.profileName;
console.error(`SERVER.TS: Using Workspace Directory: ${WORKSPACE_DIR}`);
console.error(`SERVER.TS: Using CODESYS Path: ${codesysExePath}`);
console.error(`SERVER.TS: Using CODESYS Profile: ${codesysProfileName}`);
// --- Sanity check - confirm the path exists if possible ---
// This helps catch configuration issues early and prevents runtime failures
console.error(`SERVER.TS: Checking existence of CODESYS executable: ${codesysExePath}`);
try {
// Using sync check here as it's part of initial setup before async operations start
const fsChecker = require('fs');
if (!fsChecker.existsSync(codesysExePath)) {
console.error(`SERVER.TS ERROR: Determined CODESYS executable path does not exist: ${codesysExePath}`);
// Consider throwing an error instead of exiting if bin.ts handles the catch
throw new Error(`CODESYS executable not found at specified path: ${codesysExePath}`);
// process.exit(1); // Avoid process.exit inside library functions if possible
}
else {
console.error(`SERVER.TS: Confirmed CODESYS executable exists.`);
}
}
catch (err) {
console.error(`SERVER.TS ERROR: Error checking CODESYS path existence: ${err.message}`);
throw err; // Rethrow the error to be caught by the caller (bin.ts)
// process.exit(1);
}
// --- End Configuration Handling ---
// --- Helper Function (fileExists - async version) ---
function fileExists(filePath) {
return __awaiter(this, void 0, void 0, function* () {
try {
yield (0, promises_1.stat)(filePath);
return true;
}
catch (error) {
if (error.code === 'ENOENT') {
return false; // File does not exist
}
throw error; // Other error
}
});
}
// --- End Helper Function ---
// --- MCP Server Initialization ---
console.error("SERVER.TS: Initializing McpServer...");
const server = new mcp_js_1.McpServer({
name: "CODESYS Control MCP Server",
version: "1.7.1", // Update version as needed
capabilities: {
// Explicitly declare capabilities - enables listChanged notifications if supported by SDK
resources: { listChanged: true }, // Assuming you might want to notify if resources change dynamically
tools: { listChanged: true } // Assuming you might want to notify if tools change dynamically (less common)
}
});
console.error("SERVER.TS: McpServer instance created.");
// --- End MCP Server Initialization ---
// --- Python Script Templates (Imported from v1.6.9) ---
const ENSURE_PROJECT_OPEN_PYTHON_SNIPPET = `
import sys
import scriptengine as script_engine
import os
import time
import traceback
# --- Function to ensure the correct project is open ---
MAX_RETRIES = 3
RETRY_DELAY = 2.0 # seconds (use float for time.sleep)
def ensure_project_open(target_project_path):
print("DEBUG: Ensuring project is open: %s" % target_project_path)
# Normalize target path once
normalized_target_path = os.path.normcase(os.path.abspath(target_project_path))
for attempt in range(MAX_RETRIES):
print("DEBUG: Ensure project attempt %d/%d for %s" % (attempt + 1, MAX_RETRIES, normalized_target_path))
primary_project = None
try:
# Getting primary project might fail if CODESYS instance is unstable
primary_project = script_engine.projects.primary
except Exception as primary_err:
print("WARN: Error getting primary project: %s. Assuming none." % primary_err)
# traceback.print_exc() # Optional: Print stack trace for this error
primary_project = None
current_project_path = ""
project_ok = False # Flag to check if target is confirmed primary and accessible
if primary_project:
try:
# Getting path should be relatively safe if primary_project object exists
current_project_path = os.path.normcase(os.path.abspath(primary_project.path))
print("DEBUG: Current primary project path: %s" % current_project_path)
if current_project_path == normalized_target_path:
# Found the right project as primary, now check if it's usable
print("DEBUG: Target project path matches primary. Checking access...")
try:
# Try a relatively safe operation to confirm object usability
# Getting children count is a reasonable check
_ = len(primary_project.get_children(False))
print("DEBUG: Target project '%s' is primary and accessible." % target_project_path)
project_ok = True
return primary_project # SUCCESS CASE 1: Already open and accessible
except Exception as access_err:
# Project found, but accessing it failed. Might be unstable.
print("WARN: Primary project access check failed for '%s': %s. Will attempt reopen." % (current_project_path, access_err))
# traceback.print_exc() # Optional: Print stack trace
primary_project = None # Force reopen by falling through
else:
# A *different* project is primary
print("DEBUG: Primary project is '%s', not the target '%s'." % (current_project_path, normalized_target_path))
# Consider closing the wrong project if causing issues, but for now, just open target
# try:
# print("DEBUG: Closing incorrect primary project '%s'..." % current_project_path)
# primary_project.close() # Be careful with unsaved changes
# except Exception as close_err:
# print("WARN: Failed to close incorrect primary project: %s" % close_err)
primary_project = None # Force open target project
except Exception as path_err:
# Failed even to get the path of the supposed primary project
print("WARN: Could not get path of current primary project: %s. Assuming not the target." % path_err)
# traceback.print_exc() # Optional: Print stack trace
primary_project = None # Force open target project
# If target project not confirmed as primary and accessible, attempt to open/reopen
if not project_ok:
# Log clearly whether we are opening initially or reopening
if primary_project is None and current_project_path == "":
print("DEBUG: No primary project detected. Attempting to open target: %s" % target_project_path)
elif primary_project is None and current_project_path != "":
print("DEBUG: Primary project was '%s' but failed access check or needed close. Attempting to open target: %s" % (current_project_path, target_project_path))
else: # Includes cases where wrong project was open
print("DEBUG: Target project not primary or initial check failed. Attempting to open/reopen: %s" % target_project_path)
try:
# Set flags for silent opening, handle potential attribute errors
update_mode = script_engine.VersionUpdateFlags.NoUpdates | script_engine.VersionUpdateFlags.SilentMode
# try:
# update_mode = script_engine.VersionUpdateFlags.NoUpdates | script_engine.VersionUpdateFlags.SilentMode
# except AttributeError:
# print("WARN: VersionUpdateFlags not found, using integer flags for open (1 | 2 = 3).")
# update_mode = 3 # 1=NoUpdates, 2=SilentMode
opened_project = None
try:
# The actual open call
print("DEBUG: Calling script_engine.projects.open('%s', update_flags=%s)..." % (target_project_path, update_mode))
opened_project = script_engine.projects.open(target_project_path, update_flags=update_mode)
if not opened_project:
# This is a critical failure if open returns None without exception
print("ERROR: projects.open returned None for %s on attempt %d" % (target_project_path, attempt + 1))
# Allow retry loop to continue
else:
# Open call returned *something*, let's verify
print("DEBUG: projects.open call returned an object for: %s" % target_project_path)
print("DEBUG: Pausing for stabilization after open...")
time.sleep(RETRY_DELAY) # Give CODESYS time
# Re-verify: Is the project now primary and accessible?
recheck_primary = None
try: recheck_primary = script_engine.projects.primary
except Exception as recheck_primary_err: print("WARN: Error getting primary project after reopen: %s" % recheck_primary_err)
if recheck_primary:
recheck_path = ""
try: # Getting path might fail
recheck_path = os.path.normcase(os.path.abspath(recheck_primary.path))
except Exception as recheck_path_err:
print("WARN: Failed to get path after reopen: %s" % recheck_path_err)
if recheck_path == normalized_target_path:
print("DEBUG: Target project confirmed as primary after reopening.")
try: # Final sanity check
_ = len(recheck_primary.get_children(False))
print("DEBUG: Reopened project basic access confirmed.")
return recheck_primary # SUCCESS CASE 2: Successfully opened/reopened
except Exception as access_err_reopen:
print("WARN: Reopened project (%s) basic access check failed: %s." % (normalized_target_path, access_err_reopen))
# traceback.print_exc() # Optional
# Allow retry loop to continue
else:
print("WARN: Different project is primary after reopening! Expected '%s', got '%s'." % (normalized_target_path, recheck_path))
# Allow retry loop to continue, maybe it fixes itself
else:
print("WARN: No primary project found after reopening attempt %d!" % (attempt+1))
# Allow retry loop to continue
except Exception as open_err:
# Catch errors during the open call itself
print("ERROR: Exception during projects.open call on attempt %d: %s" % (attempt + 1, open_err))
traceback.print_exc() # Crucial for diagnosing open failures
# Allow retry loop to continue
except Exception as outer_open_err:
# Catch errors in the flag setup etc.
print("ERROR: Unexpected error during open setup/logic attempt %d: %s" % (attempt + 1, outer_open_err))
traceback.print_exc()
# If we didn't return successfully in this attempt, wait before retrying
if attempt < MAX_RETRIES - 1:
print("DEBUG: Ensure project attempt %d did not succeed. Waiting %f seconds..." % (attempt + 1, RETRY_DELAY))
time.sleep(RETRY_DELAY)
else: # Last attempt failed
print("ERROR: Failed all ensure_project_open attempts for %s." % normalized_target_path)
# If all retries fail after the loop
raise RuntimeError("Failed to ensure project '%s' is open and accessible after %d attempts." % (target_project_path, MAX_RETRIES))
# --- End of function ---
# Placeholder for the project file path (must be set in scripts using this snippet)
PROJECT_FILE_PATH = r"{PROJECT_FILE_PATH}"
`;
const CHECK_STATUS_SCRIPT = `
import sys, scriptengine as script_engine, os, traceback
project_open = False; project_name = "No project open"; project_path = "N/A"; scripting_ok = False
try:
scripting_ok = True; primary_project = script_engine.projects.primary
if primary_project:
project_open = True
try:
project_path = os.path.normcase(os.path.abspath(primary_project.path))
try:
project_name = primary_project.get_name() # Might fail
if not project_name: project_name = "Unnamed (path: %s)" % os.path.basename(project_path)
except: project_name = "Unnamed (path: %s)" % os.path.basename(project_path)
except Exception as e_path: project_path = "N/A (Error: %s)" % e_path; project_name = "Unnamed (Path Error)"
print("Project Open: %s" % project_open); print("Project Name: %s" % project_name)
print("Project Path: %s" % project_path); print("Scripting OK: %s" % scripting_ok)
print("SCRIPT_SUCCESS: Status check complete."); sys.exit(0)
except Exception as e:
error_message = "Error during status check: %s" % e
print(error_message); print("Scripting OK: False")
# traceback.print_exc() # Optional traceback
print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
`;
const OPEN_PROJECT_SCRIPT_TEMPLATE = `
import sys, scriptengine as script_engine, os, traceback
${ENSURE_PROJECT_OPEN_PYTHON_SNIPPET}
try:
project = ensure_project_open(PROJECT_FILE_PATH)
# Get name from object if possible, otherwise use path basename
proj_name = "Unknown"
try:
if project: proj_name = project.get_name() or os.path.basename(PROJECT_FILE_PATH)
else: proj_name = os.path.basename(PROJECT_FILE_PATH) + " (ensure_project_open returned None?)"
except Exception:
proj_name = os.path.basename(PROJECT_FILE_PATH) + " (name retrieval failed)"
print("Project Opened: %s" % proj_name)
print("SCRIPT_SUCCESS: Project opened successfully.")
sys.exit(0)
except Exception as e:
error_message = "Error opening project %s: %s" % (PROJECT_FILE_PATH, e)
print(error_message)
traceback.print_exc()
print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
`;
const CREATE_PROJECT_SCRIPT_TEMPLATE = `
import sys, scriptengine as script_engine, os, shutil, time, traceback
# Placeholders
TEMPLATE_PROJECT_PATH = r'{TEMPLATE_PROJECT_PATH}' # Path to Standard.project
PROJECT_FILE_PATH = r'{PROJECT_FILE_PATH}' # Path for the new project (Target Path)
try:
print("DEBUG: Python script create_project (copy from template):")
print("DEBUG: Template Source = %s" % TEMPLATE_PROJECT_PATH)
print("DEBUG: Target Path = %s" % PROJECT_FILE_PATH)
if not PROJECT_FILE_PATH: raise ValueError("Target project file path empty.")
if not TEMPLATE_PROJECT_PATH: raise ValueError("Template project file path empty.")
if not os.path.exists(TEMPLATE_PROJECT_PATH): raise IOError("Template project file not found: %s" % TEMPLATE_PROJECT_PATH)
# 1. Copy the template project file to the new location
target_dir = os.path.dirname(PROJECT_FILE_PATH)
if not os.path.exists(target_dir): print("DEBUG: Creating target directory: %s" % target_dir); os.makedirs(target_dir)
# Check if target file already exists
if os.path.exists(PROJECT_FILE_PATH): print("WARN: Target project file already exists, overwriting: %s" % PROJECT_FILE_PATH)
print("DEBUG: Copying '%s' to '%s'..." % (TEMPLATE_PROJECT_PATH, PROJECT_FILE_PATH))
shutil.copy2(TEMPLATE_PROJECT_PATH, PROJECT_FILE_PATH) # copy2 preserves metadata
print("DEBUG: File copy complete.")
# 2. Open the newly copied project file
print("DEBUG: Opening the copied project: %s" % PROJECT_FILE_PATH)
# Set flags for silent opening
update_mode = script_engine.VersionUpdateFlags.NoUpdates | script_engine.VersionUpdateFlags.SilentMode
# try:
# update_mode = script_engine.VersionUpdateFlags.NoUpdates | script_engine.VersionUpdateFlags.SilentMode
# except AttributeError:
# print("WARN: VersionUpdateFlags not found, using integer flags for open (1 | 2 = 3).")
# update_mode = 3
project = script_engine.projects.open(PROJECT_FILE_PATH, update_flags=update_mode)
print("DEBUG: script_engine.projects.open returned: %s" % project)
if project:
print("DEBUG: Pausing briefly after open...")
time.sleep(1.0) # Allow CODESYS to potentially initialize things
try:
print("DEBUG: Explicitly saving project after opening copy...")
project.save();
print("DEBUG: Project save after opening copy succeeded.")
except Exception as save_err:
print("WARN: Explicit save after opening copy failed: %s" % save_err)
# Decide if this is critical - maybe not, but good to know.
print("Project Created from Template Copy at: %s" % PROJECT_FILE_PATH)
print("SCRIPT_SUCCESS: Project copied from template and opened successfully.")
sys.exit(0)
else:
error_message = "Failed to open project copy %s after copying template %s. projects.open returned None." % (PROJECT_FILE_PATH, TEMPLATE_PROJECT_PATH)
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
except Exception as e:
detailed_error = traceback.format_exc()
error_message = "Error creating project '%s' from template '%s': %s\\n%s" % (PROJECT_FILE_PATH, TEMPLATE_PROJECT_PATH, e, detailed_error)
print(error_message); print("SCRIPT_ERROR: Error copying/opening template: %s" % e); sys.exit(1)
`;
const SAVE_PROJECT_SCRIPT_TEMPLATE = `
import sys, scriptengine as script_engine, os, traceback
${ENSURE_PROJECT_OPEN_PYTHON_SNIPPET}
try:
primary_project = ensure_project_open(PROJECT_FILE_PATH)
# Get name from object if possible, otherwise use path basename
project_name = "Unknown"
try:
if primary_project: project_name = primary_project.get_name() or os.path.basename(PROJECT_FILE_PATH)
else: project_name = os.path.basename(PROJECT_FILE_PATH) + " (ensure_project_open returned None?)"
except Exception:
project_name = os.path.basename(PROJECT_FILE_PATH) + " (name retrieval failed)"
print("DEBUG: Saving project: %s (%s)" % (project_name, PROJECT_FILE_PATH))
primary_project.save()
print("DEBUG: project.save() executed.")
print("Project Saved: %s" % project_name)
print("SCRIPT_SUCCESS: Project saved successfully.")
sys.exit(0)
except Exception as e:
error_message = "Error saving project %s: %s" % (PROJECT_FILE_PATH, e)
print(error_message)
traceback.print_exc()
print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
`;
const FIND_OBJECT_BY_PATH_PYTHON_SNIPPET = `
import traceback
# --- Find object by path function ---
def find_object_by_path_robust(start_node, full_path, target_type_name="object"):
print("DEBUG: Finding %s by path: '%s'" % (target_type_name, full_path))
normalized_path = full_path.replace('\\\\', '/').strip('/')
path_parts = normalized_path.split('/')
if not path_parts:
print("ERROR: Path is empty.")
return None
# Determine the actual starting node (project or application)
project = start_node # Assume start_node is project initially
if not hasattr(start_node, 'active_application') and hasattr(start_node, 'project'):
# If start_node is not project but has project ref (e.g., an application), get the project
try: project = start_node.project
except Exception as proj_ref_err:
print("WARN: Could not get project reference from start_node: %s" % proj_ref_err)
# Proceed assuming start_node might be the project anyway or search fails
# Try to get the application object robustly if we think we have the project
app = None
if hasattr(project, 'active_application'):
try: app = project.active_application
except Exception: pass # Ignore errors getting active app
if not app:
try:
apps = project.find("Application", True) # Search recursively
if apps: app = apps[0]
except Exception: pass
# Check if the first path part matches the application name
app_name_lower = ""
if app:
try: app_name_lower = (app.get_name() or "application").lower()
except Exception: app_name_lower = "application" # Fallback
# Decide where to start the traversal
current_obj = start_node # Default to the node passed in
if hasattr(project, 'active_application'): # Only adjust if start_node was likely the project
if app and path_parts[0].lower() == app_name_lower:
print("DEBUG: Path starts with Application name '%s'. Beginning search there." % path_parts[0])
current_obj = app
path_parts = path_parts[1:] # Consume the app name part
# If path was *only* the application name
if not path_parts:
print("DEBUG: Target path is the Application object itself.")
return current_obj
else:
print("DEBUG: Path does not start with Application name. Starting search from project root.")
current_obj = project # Start search from the project root
else:
print("DEBUG: Starting search from originally provided node.")
# Traverse the remaining path parts
parent_path_str = getattr(current_obj, 'get_name', lambda: str(current_obj))() # Safer name getting
for i, part_name in enumerate(path_parts):
is_last_part = (i == len(path_parts) - 1)
print("DEBUG: Searching for part [%d/%d]: '%s' under '%s'" % (i+1, len(path_parts), part_name, parent_path_str))
found_in_parent = None
try:
# Prioritize non-recursive find for direct children
children_of_current = current_obj.get_children(False)
print("DEBUG: Found %d direct children under '%s'." % (len(children_of_current), parent_path_str))
for child in children_of_current:
child_name = getattr(child, 'get_name', lambda: None)() # Safer name getting
# print("DEBUG: Checking child: '%s'" % child_name) # Verbose
if child_name == part_name:
found_in_parent = child
print("DEBUG: Found direct child matching '%s'." % part_name)
break # Found direct child, stop searching children
# If not found directly, AND it's the last part, try recursive find from current parent
if not found_in_parent and is_last_part:
print("DEBUG: Direct find failed for last part '%s'. Trying recursive find under '%s'." % (part_name, parent_path_str))
found_recursive_list = current_obj.find(part_name, True) # Recursive find
if found_recursive_list:
# Maybe add a check here if multiple are found?
found_in_parent = found_recursive_list[0] # Take the first match
print("DEBUG: Found last part '%s' recursively." % part_name)
else:
print("DEBUG: Recursive find also failed for last part '%s'." % part_name)
# Update current object if found
if found_in_parent:
current_obj = found_in_parent
parent_path_str = getattr(current_obj, 'get_name', lambda: part_name)() # Safer name getting
print("DEBUG: Stepped into '%s'." % parent_path_str)
else:
# If not found at any point, the path is invalid from this parent
print("ERROR: Path part '%s' not found under '%s'." % (part_name, parent_path_str))
return None # Path broken
except Exception as find_err:
print("ERROR: Exception while searching for '%s' under '%s': %s" % (part_name, parent_path_str, find_err))
traceback.print_exc()
return None # Error during search
# Final verification (optional but recommended): Check if the found object's name matches the last part
final_expected_name = full_path.split('/')[-1]
found_final_name = getattr(current_obj, 'get_name', lambda: None)() # Safer name getting
if found_final_name == final_expected_name:
print("DEBUG: Final %s found and name verified for path '%s': %s" % (target_type_name, full_path, found_final_name))
return current_obj
else:
print("ERROR: Traversal ended on object '%s' but expected final name was '%s'." % (found_final_name, final_expected_name))
return None # Name mismatch implies target not found as expected
# --- End of find object function ---
`;
const CREATE_POU_SCRIPT_TEMPLATE = `
import sys, scriptengine as script_engine, os, traceback
${ENSURE_PROJECT_OPEN_PYTHON_SNIPPET}
${FIND_OBJECT_BY_PATH_PYTHON_SNIPPET}
POU_NAME = "{POU_NAME}"; POU_TYPE_STR = "{POU_TYPE_STR}"; IMPL_LANGUAGE_STR = "{IMPL_LANGUAGE_STR}"; PARENT_PATH_REL = "{PARENT_PATH}"
pou_type_map = { "Program": script_engine.PouType.Program, "FunctionBlock": script_engine.PouType.FunctionBlock, "Function": script_engine.PouType.Function }
# Map common language names to ImplementationLanguages attributes if needed (optional, None usually works)
# lang_map = { "ST": script_engine.ImplementationLanguage.st, ... }
try:
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))
primary_project = ensure_project_open(PROJECT_FILE_PATH)
if not POU_NAME: raise ValueError("POU name empty.")
if not PARENT_PATH_REL: raise ValueError("Parent path empty.")
# Resolve POU Type Enum
pou_type_enum = pou_type_map.get(POU_TYPE_STR)
if not pou_type_enum: raise ValueError("Invalid POU type string: %s. Use Program, FunctionBlock, or Function." % POU_TYPE_STR)
# Find parent object using the robust function
parent_object = find_object_by_path_robust(primary_project, PARENT_PATH_REL, "parent container")
if not parent_object: raise ValueError("Parent object not found for path: %s" % PARENT_PATH_REL)
parent_name = getattr(parent_object, 'get_name', lambda: str(parent_object))()
print("DEBUG: Using parent object: %s (Type: %s)" % (parent_name, type(parent_object).__name__))
# Check if parent object supports creating POUs (should implement ScriptIecLanguageObjectContainer)
if not hasattr(parent_object, 'create_pou'):
raise TypeError("Parent object '%s' of type %s does not support create_pou." % (parent_name, type(parent_object).__name__))
# Set language GUID to None (let CODESYS default based on parent/settings)
lang_guid = None
print("DEBUG: Setting language to None (will use default).")
# Example if mapping language string: lang_guid = lang_map.get(IMPL_LANGUAGE_STR, None)
print("DEBUG: Calling parent_object.create_pou: Name='%s', Type=%s, Lang=%s" % (POU_NAME, pou_type_enum, lang_guid))
# Call create_pou using keyword arguments
new_pou = parent_object.create_pou(
name=POU_NAME,
type=pou_type_enum,
language=lang_guid # Pass None
)
print("DEBUG: parent_object.create_pou returned: %s" % new_pou)
if new_pou:
new_pou_name = getattr(new_pou, 'get_name', lambda: POU_NAME)()
print("DEBUG: POU object created: %s" % new_pou_name)
# --- SAVE THE PROJECT TO PERSIST THE NEW POU ---
try:
print("DEBUG: Saving Project...")
primary_project.save() # Save the overall project file
print("DEBUG: Project saved successfully after POU creation.")
except Exception as save_err:
print("ERROR: Failed to save Project after POU creation: %s" % save_err)
detailed_error = traceback.format_exc()
error_message = "Error saving Project after creating POU '%s': %s\\n%s" % (new_pou_name, save_err, detailed_error)
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
# --- END SAVING ---
print("POU Created: %s" % new_pou_name); print("Type: %s" % POU_TYPE_STR); print("Language: %s (Defaulted)" % IMPL_LANGUAGE_STR); print("Parent Path: %s" % PARENT_PATH_REL)
print("SCRIPT_SUCCESS: POU created successfully."); sys.exit(0)
else:
error_message = "Failed to create POU '%s'. create_pou returned None." % POU_NAME; print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
except Exception as e:
detailed_error = traceback.format_exc()
error_message = "Error creating POU '%s' in project '%s': %s\\n%s" % (POU_NAME, PROJECT_FILE_PATH, e, detailed_error)
print(error_message); print("SCRIPT_ERROR: Error creating POU '%s': %s" % (POU_NAME, e)); sys.exit(1)
`;
const SET_POU_CODE_SCRIPT_TEMPLATE = `
import sys, scriptengine as script_engine, os, traceback
${ENSURE_PROJECT_OPEN_PYTHON_SNIPPET}
${FIND_OBJECT_BY_PATH_PYTHON_SNIPPET}
POU_FULL_PATH = "{POU_FULL_PATH}" # Expecting format like "Application/MyPOU" or "Folder/SubFolder/MyPOU"
DECLARATION_CONTENT = """{DECLARATION_CONTENT}"""
IMPLEMENTATION_CONTENT = """{IMPLEMENTATION_CONTENT}"""
try:
print("DEBUG: set_pou_code script: POU_FULL_PATH='%s', Project='%s'" % (POU_FULL_PATH, PROJECT_FILE_PATH))
primary_project = ensure_project_open(PROJECT_FILE_PATH)
if not POU_FULL_PATH: raise ValueError("POU full path empty.")
# Find the target POU/Method/Property object
target_object = find_object_by_path_robust(primary_project, POU_FULL_PATH, "target object")
if not target_object: raise ValueError("Target object not found using path: %s" % POU_FULL_PATH)
target_name = getattr(target_object, 'get_name', lambda: POU_FULL_PATH)()
print("DEBUG: Found target object: %s" % target_name)
# --- Set Declaration Part ---
declaration_updated = False
# Check if the content is actually provided (might be None/empty if only impl is set)
has_declaration_content = 'DECLARATION_CONTENT' in locals() or 'DECLARATION_CONTENT' in globals()
if has_declaration_content and DECLARATION_CONTENT is not None: # Check not None
if hasattr(target_object, 'textual_declaration'):
decl_obj = target_object.textual_declaration
if decl_obj and hasattr(decl_obj, 'replace'):
try:
print("DEBUG: Accessing textual_declaration...")
decl_obj.replace(DECLARATION_CONTENT)
print("DEBUG: Set declaration text using replace().")
declaration_updated = True
except Exception as decl_err:
print("ERROR: Failed to set declaration text: %s" % decl_err)
traceback.print_exc() # Print stack trace for detailed error
else:
print("WARN: Target '%s' textual_declaration attribute is None or does not have replace(). Skipping declaration update." % target_name)
else:
print("WARN: Target '%s' does not have textual_declaration attribute. Skipping declaration update." % target_name)
else:
print("DEBUG: Declaration content not provided or is None. Skipping declaration update.")
# --- Set Implementation Part ---
implementation_updated = False
has_implementation_content = 'IMPLEMENTATION_CONTENT' in locals() or 'IMPLEMENTATION_CONTENT' in globals()
if has_implementation_content and IMPLEMENTATION_CONTENT is not None: # Check not None
if hasattr(target_object, 'textual_implementation'):
impl_obj = target_object.textual_implementation
if impl_obj and hasattr(impl_obj, 'replace'):
try:
print("DEBUG: Accessing textual_implementation...")
impl_obj.replace(IMPLEMENTATION_CONTENT)
print("DEBUG: Set implementation text using replace().")
implementation_updated = True
except Exception as impl_err:
print("ERROR: Failed to set implementation text: %s" % impl_err)
traceback.print_exc() # Print stack trace for detailed error
else:
print("WARN: Target '%s' textual_implementation attribute is None or does not have replace(). Skipping implementation update." % target_name)
else:
print("WARN: Target '%s' does not have textual_implementation attribute. Skipping implementation update." % target_name)
else:
print("DEBUG: Implementation content not provided or is None. Skipping implementation update.")
# --- SAVE THE PROJECT TO PERSIST THE CODE CHANGE ---
# Only save if something was actually updated to avoid unnecessary saves
if declaration_updated or implementation_updated:
try:
print("DEBUG: Saving Project (after code change)...")
primary_project.save() # Save the overall project file
print("DEBUG: Project saved successfully after code change.")
except Exception as save_err:
print("ERROR: Failed to save Project after setting code: %s" % save_err)
detailed_error = traceback.format_exc()
error_message = "Error saving Project after code change for '%s': %s\\n%s" % (target_name, save_err, detailed_error)
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
else:
print("DEBUG: No code parts were updated, skipping project save.")
# --- END SAVING ---
print("Code Set For: %s" % target_name)
print("Path: %s" % POU_FULL_PATH)
print("SCRIPT_SUCCESS: Declaration and/or implementation set successfully."); sys.exit(0)
except Exception as e:
detailed_error = traceback.format_exc()
error_message = "Error setting code for object '%s' in project '%s': %s\\n%s" % (POU_FULL_PATH, PROJECT_FILE_PATH, e, detailed_error)
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
`;
const CREATE_PROPERTY_SCRIPT_TEMPLATE = `
import sys, scriptengine as script_engine, os, traceback
${ENSURE_PROJECT_OPEN_PYTHON_SNIPPET}
${FIND_OBJECT_BY_PATH_PYTHON_SNIPPET}
PARENT_POU_FULL_PATH = "{PARENT_POU_FULL_PATH}" # e.g., "Application/MyFB"
PROPERTY_NAME = "{PROPERTY_NAME}"
PROPERTY_TYPE = "{PROPERTY_TYPE}"
# Optional: Language for Getter/Setter (usually defaults to ST)
# LANG_GUID_STR = "{LANG_GUID_STR}" # Example if needed
try:
print("DEBUG: create_property script: ParentPOU='%s', Name='%s', Type='%s', Project='%s'" % (PARENT_POU_FULL_PATH, PROPERTY_NAME, PROPERTY_TYPE, PROJECT_FILE_PATH))
primary_project = ensure_project_open(PROJECT_FILE_PATH)
if not PARENT_POU_FULL_PATH: raise ValueError("Parent POU full path empty.")
if not PROPERTY_NAME: raise ValueError("Property name empty.")
if not PROPERTY_TYPE: raise ValueError("Property type empty.")
# Find the parent POU object
parent_pou_object = find_object_by_path_robust(primary_project, PARENT_POU_FULL_PATH, "parent POU")
if not parent_pou_object: raise ValueError("Parent POU object not found: %s" % PARENT_POU_FULL_PATH)
parent_pou_name = getattr(parent_pou_object, 'get_name', lambda: PARENT_POU_FULL_PATH)()
print("DEBUG: Found Parent POU object: %s" % parent_pou_name)
# Check if parent object supports creating properties (should implement ScriptIecLanguageMemberContainer)
if not hasattr(parent_pou_object, 'create_property'):
raise TypeError("Parent object '%s' of type %s does not support create_property." % (parent_pou_name, type(parent_pou_object).__name__))
# Default language to None (usually ST)
lang_guid = None
print("DEBUG: Calling create_property: Name='%s', Type='%s', Lang=%s" % (PROPERTY_NAME, PROPERTY_TYPE, lang_guid))
# Call the create_property method ON THE PARENT POU
new_property_object = parent_pou_object.create_property(
name=PROPERTY_NAME,
return_type=PROPERTY_TYPE,
language=lang_guid # Pass None to use default
)
if new_property_object:
new_prop_name = getattr(new_property_object, 'get_name', lambda: PROPERTY_NAME)()
print("DEBUG: Property object created: %s" % new_prop_name)
# --- SAVE THE PROJECT TO PERSIST THE NEW PROPERTY OBJECT ---
try:
print("DEBUG: Saving Project (after property creation)...")
primary_project.save()
print("DEBUG: Project saved successfully after property creation.")
except Exception as save_err:
print("ERROR: Failed to save Project after creating property: %s" % save_err)
detailed_error = traceback.format_exc()
error_message = "Error saving Project after creating property '%s': %s\\n%s" % (PROPERTY_NAME, save_err, detailed_error)
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
# --- END SAVING ---
print("Property Created: %s" % new_prop_name)
print("Parent POU: %s" % PARENT_POU_FULL_PATH)
print("Type: %s" % PROPERTY_TYPE)
print("SCRIPT_SUCCESS: Property created successfully."); sys.exit(0)
else:
error_message = "Failed to create property '%s' under '%s'. create_property returned None." % (PROPERTY_NAME, parent_pou_name)
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
except Exception as e:
detailed_error = traceback.format_exc()
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)
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
`;
const CREATE_METHOD_SCRIPT_TEMPLATE = `
import sys, scriptengine as script_engine, os, traceback
${ENSURE_PROJECT_OPEN_PYTHON_SNIPPET}
${FIND_OBJECT_BY_PATH_PYTHON_SNIPPET}
PARENT_POU_FULL_PATH = "{PARENT_POU_FULL_PATH}" # e.g., "Application/MyFB"
METHOD_NAME = "{METHOD_NAME}"
RETURN_TYPE = "{RETURN_TYPE}" # Can be empty string for no return type
# Optional: Language
# LANG_GUID_STR = "{LANG_GUID_STR}" # Example if needed
try:
print("DEBUG: create_method script: ParentPOU='%s', Name='%s', ReturnType='%s', Project='%s'" % (PARENT_POU_FULL_PATH, METHOD_NAME, RETURN_TYPE, PROJECT_FILE_PATH))
primary_project = ensure_project_open(PROJECT_FILE_PATH)
if not PARENT_POU_FULL_PATH: raise ValueError("Parent POU full path empty.")
if not METHOD_NAME: raise ValueError("Method name empty.")
# RETURN_TYPE can be empty
# Find the parent POU object
parent_pou_object = find_object_by_path_robust(primary_project, PARENT_POU_FULL_PATH, "parent POU")
if not parent_pou_object: raise ValueError("Parent POU object not found: %s" % PARENT_POU_FULL_PATH)
parent_pou_name = getattr(parent_pou_object, 'get_name', lambda: PARENT_POU_FULL_PATH)()
print("DEBUG: Found Parent POU object: %s" % parent_pou_name)
# Check if parent object supports creating methods (should implement ScriptIecLanguageMemberContainer)
if not hasattr(parent_pou_object, 'create_method'):
raise TypeError("Parent object '%s' of type %s does not support create_method." % (parent_pou_name, type(parent_pou_object).__name__))
# Default language to None (usually ST)
lang_guid = None
# Use None if RETURN_TYPE is empty string, otherwise use the string
actual_return_type = RETURN_TYPE if RETURN_TYPE else None
print("DEBUG: Calling create_method: Name='%s', ReturnType=%s, Lang=%s" % (METHOD_NAME, actual_return_type, lang_guid))
# Call the create_method method ON THE PARENT POU
new_method_object = parent_pou_object.create_method(
name=METHOD_NAME,
return_type=actual_return_type,
language=lang_guid # Pass None to use default
)
if new_method_object:
new_meth_name = getattr(new_method_object, 'get_name', lambda: METHOD_NAME)()
print("DEBUG: Method object created: %s" % new_meth_name)
# --- SAVE THE PROJECT TO PERSIST THE NEW METHOD OBJECT ---
try:
print("DEBUG: Saving Project (after method creation)...")
primary_project.save()
print("DEBUG: Project saved successfully after method creation.")
except Exception as save_err:
print("ERROR: Failed to save Project after creating method: %s" % save_err)
detailed_error = traceback.format_exc()
error_message = "Error saving Project after creating method '%s': %s\\n%s" % (METHOD_NAME, save_err, detailed_error)
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
# --- END SAVING ---
print("Method Created: %s" % new_meth_name)
print("Parent POU: %s" % PARENT_POU_FULL_PATH)
print("Return Type: %s" % (RETURN_TYPE if RETURN_TYPE else "(None)"))
print("SCRIPT_SUCCESS: Method created successfully."); sys.exit(0)
else:
error_message = "Failed to create method '%s' under '%s'. create_method returned None." % (METHOD_NAME, parent_pou_name)
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
except Exception as e:
detailed_error = traceback.format_exc()
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)
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
`;
const COMPILE_PROJECT_SCRIPT_TEMPLATE = `
import sys, scriptengine as script_engine, os, traceback
${ENSURE_PROJECT_OPEN_PYTHON_SNIPPET}
try:
print("DEBUG: compile_project script: Project='%s'" % PROJECT_FILE_PATH)
primary_project = ensure_project_open(PROJECT_FILE_PATH)
project_name = os.path.basename(PROJECT_FILE_PATH)
target_app = None
app_name = "N/A"
# Try getting active application first
try:
target_app = primary_project.active_application
if target_app:
app_name = getattr(target_app, 'get_name', lambda: "Unnamed App (Active)")()
print("DEBUG: Found active application: %s" % app_name)
except Exception as active_err:
print("WARN: Could not get active application: %s. Searching..." % active_err)
# If no active app, search for the first one
if not target_app:
print("DEBUG: Searching for first compilable application...")
apps = []
try:
# Search recursively through all project objects
all_children = primary_project.get_children(True)
for child in all_children:
# Check using the marker property and if build method exists
if hasattr(child, 'is_application') and child.is_application and hasattr(child, 'build'):
app_name_found = getattr(child, 'get_name', lambda: "Unnamed App")()
print("DEBUG: Found potential application object: %s" % app_name_found)
apps.append(child)
break # Take the first one found
except Exception as find_err: print("WARN: Error finding application object: %s" % find_err)
if not apps: raise RuntimeError("No compilable application found in project '%s'" % project_name)
target_app = apps[0]
app_name = getattr(target_app, 'get_name', lambda: "Unnamed App (First Found)")()
print("WARN: Compiling first found application: %s" % app_name)
print("DEBUG: Calling build() on app '%s'..." % app_name)
if not hasattr(target_app, 'build'):
raise TypeError("Selected object '%s' is not an application or doesn't support build()." % app_name)
# Execute the build
target_app.build();
print("DEBUG: Build command executed for application '%s'." % app_name)
# Check messages is harder without direct access to message store from script.
# Rely on CODESYS UI or log output for now.
print("Compile Initiated For Application: %s" % app_name); print("In Project: %s" % project_name)
print("SCRIPT_SUCCESS: Application compilation initiated."); sys.exit(0)
except Exception as e:
detailed_error = traceback.format_exc()
error_message = "Error initiating compilation for project %s: %s\\n%s" % (PROJECT_FILE_PATH, e, detailed_error)
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
`;
const GET_PROJECT_STRUCTURE_SCRIPT_TEMPLATE = `
import sys, scriptengine as script_engine, os, traceback
${ENSURE_PROJECT_OPEN_PYTHON_SNIPPET}
# FIND_OBJECT_BY_PATH_PYTHON_SNIPPET is NOT needed here, we start from project root.
def get_object_structure(obj, indent=0, max_depth=10): # Add max_depth
lines = []; indent_str = " " * indent
if indent > max_depth:
lines.append("%s- Max recursion depth reached." % indent_str)
return lines
try:
name = "Unnamed"; obj_type = type(obj).__name__
guid_str = ""
folder_str = ""
try:
name = getattr(obj, 'get_nam