UNPKG

@codesys/mcp-toolkit

Version:

Model Context Protocol (MCP) server for CODESYS automation platform

900 lines (816 loc) 90.2 kB
"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