@codesys/mcp-toolkit 1.1.3 → 1.1.5
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/.claude/settings.local.json +13 -0
- package/CHANGELOG.md +38 -26
- package/CONTRIBUTING.md +72 -72
- package/LICENSE +21 -21
- package/README.md +145 -145
- package/README.md.enhanced +155 -155
- package/codesys_scripts/listener_script.py +1539 -0
- package/dist/bin.js +41 -2
- package/dist/scriptLoader.js +158 -0
- package/dist/server.js +582 -1185
- package/docs/index.md +26 -26
- package/docs/installation.md +84 -84
- package/examples/README.md +78 -78
- package/examples/sample_config.json +32 -32
- package/package.json +48 -48
- package/scriptDoc.txt +8679 -8679
package/dist/server.js
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
*
|
|
7
7
|
* IMPORTANT: This server receives configuration as parameters from bin.ts,
|
|
8
8
|
* which helps avoid issues with command-line argument passing in different execution environments.
|
|
9
|
-
* (Incorporates script templates from v1.6.9 and improved tool descriptions)
|
|
10
9
|
*/
|
|
11
10
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
11
|
if (k2 === undefined) k2 = k;
|
|
@@ -64,9 +63,23 @@ console.error(`>>> Initial CWD: ${process.cwd()}`);
|
|
|
64
63
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
65
64
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
66
65
|
const zod_1 = require("zod");
|
|
67
|
-
|
|
66
|
+
// import { executeCodesysScript } from "./codesys_interop"; // We will replace this
|
|
68
67
|
const path = __importStar(require("path"));
|
|
69
|
-
const promises_1 = require("fs/promises");
|
|
68
|
+
const promises_1 = require("fs/promises");
|
|
69
|
+
const scriptLoader_1 = require("./scriptLoader");
|
|
70
|
+
// Add imports for networking and child processes
|
|
71
|
+
const net = __importStar(require("net"));
|
|
72
|
+
const child_process_1 = require("child_process");
|
|
73
|
+
// --- End Define an interface for configuration ---
|
|
74
|
+
// --- Global variables for persistent CODESYS process and socket client ---
|
|
75
|
+
let codesysProcess = null;
|
|
76
|
+
let codesysClient = null;
|
|
77
|
+
let commandQueue = [];
|
|
78
|
+
let isProcessingCommand = false;
|
|
79
|
+
let socketConnected = false;
|
|
80
|
+
let socketConnecting = false;
|
|
81
|
+
let socketQueue = ''; // Buffer for incoming socket data
|
|
82
|
+
// --- End Global variables ---
|
|
70
83
|
// --- Wrap server logic in an exported function ---
|
|
71
84
|
function startMcpServer(config) {
|
|
72
85
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -76,20 +89,19 @@ function startMcpServer(config) {
|
|
|
76
89
|
const WORKSPACE_DIR = config.workspaceDir;
|
|
77
90
|
const codesysExePath = config.codesysPath;
|
|
78
91
|
const codesysProfileName = config.profileName;
|
|
92
|
+
const codesysScriptsDir = config.codesysScriptsDir; // Get the scripts directory
|
|
79
93
|
console.error(`SERVER.TS: Using Workspace Directory: ${WORKSPACE_DIR}`);
|
|
80
94
|
console.error(`SERVER.TS: Using CODESYS Path: ${codesysExePath}`);
|
|
81
95
|
console.error(`SERVER.TS: Using CODESYS Profile: ${codesysProfileName}`);
|
|
96
|
+
console.error(`SERVER.TS: Using CODESYS Scripts Dir: ${codesysScriptsDir}`);
|
|
82
97
|
// --- Sanity check - confirm the path exists if possible ---
|
|
83
|
-
// This helps catch configuration issues early and prevents runtime failures
|
|
84
98
|
console.error(`SERVER.TS: Checking existence of CODESYS executable: ${codesysExePath}`);
|
|
85
99
|
try {
|
|
86
100
|
// Using sync check here as it's part of initial setup before async operations start
|
|
87
101
|
const fsChecker = require('fs');
|
|
88
102
|
if (!fsChecker.existsSync(codesysExePath)) {
|
|
89
103
|
console.error(`SERVER.TS ERROR: Determined CODESYS executable path does not exist: ${codesysExePath}`);
|
|
90
|
-
// Consider throwing an error instead of exiting if bin.ts handles the catch
|
|
91
104
|
throw new Error(`CODESYS executable not found at specified path: ${codesysExePath}`);
|
|
92
|
-
// process.exit(1); // Avoid process.exit inside library functions if possible
|
|
93
105
|
}
|
|
94
106
|
else {
|
|
95
107
|
console.error(`SERVER.TS: Confirmed CODESYS executable exists.`);
|
|
@@ -98,7 +110,6 @@ function startMcpServer(config) {
|
|
|
98
110
|
catch (err) {
|
|
99
111
|
console.error(`SERVER.TS ERROR: Error checking CODESYS path existence: ${err.message}`);
|
|
100
112
|
throw err; // Rethrow the error to be caught by the caller (bin.ts)
|
|
101
|
-
// process.exit(1);
|
|
102
113
|
}
|
|
103
114
|
// --- End Configuration Handling ---
|
|
104
115
|
// --- Helper Function (fileExists - async version) ---
|
|
@@ -117,914 +128,216 @@ function startMcpServer(config) {
|
|
|
117
128
|
});
|
|
118
129
|
}
|
|
119
130
|
// --- End Helper Function ---
|
|
131
|
+
// --- Persistent CODESYS Instance Management ---
|
|
132
|
+
/**
|
|
133
|
+
* Launches the persistent CODESYS process with the listener script.
|
|
134
|
+
*/
|
|
135
|
+
function launchCodesysPersistent() {
|
|
136
|
+
if (codesysProcess) {
|
|
137
|
+
console.error("SERVER.TS: CODESYS process already running.");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const listenerScriptPath = path.join(codesysScriptsDir, 'listener_script.py');
|
|
141
|
+
console.error(`SERVER.TS: Launching CODESYS with listener script: ${listenerScriptPath}`);
|
|
142
|
+
// Command to launch CODESYS with the listener script and no UI
|
|
143
|
+
const command = `"${codesysExePath}" --profile="${codesysProfileName}" --runscript="${listenerScriptPath}" --noUI`;
|
|
144
|
+
console.error(`SERVER.TS: Executing command: ${command}`);
|
|
145
|
+
// Use spawn to run the command in the background
|
|
146
|
+
codesysProcess = (0, child_process_1.spawn)(command, [], {
|
|
147
|
+
shell: true, // Use shell to handle quotes and spaces in path/profile
|
|
148
|
+
detached: true, // Allow the child process to run independently of the parent
|
|
149
|
+
stdio: ['ignore', 'pipe', 'pipe'] // Pipe stdout and stderr
|
|
150
|
+
});
|
|
151
|
+
console.error(`SERVER.TS: CODESYS process spawned with PID: ${codesysProcess.pid}`);
|
|
152
|
+
codesysProcess.stdout.on('data', (data) => {
|
|
153
|
+
console.log(`CODESYS STDOUT: ${data}`);
|
|
154
|
+
});
|
|
155
|
+
codesysProcess.stderr.on('data', (data) => {
|
|
156
|
+
console.error(`CODESYS STDERR: ${data}`);
|
|
157
|
+
});
|
|
158
|
+
codesysProcess.on('error', (err) => {
|
|
159
|
+
console.error(`SERVER.TS ERROR: Failed to start CODESYS process: ${err.message}`);
|
|
160
|
+
codesysProcess = null;
|
|
161
|
+
socketConnected = false;
|
|
162
|
+
socketConnecting = false;
|
|
163
|
+
// Attempt to reconnect or handle error appropriately
|
|
164
|
+
});
|
|
165
|
+
codesysProcess.on('close', (code) => {
|
|
166
|
+
console.error(`SERVER.TS: CODESYS process exited with code ${code}`);
|
|
167
|
+
codesysProcess = null;
|
|
168
|
+
socketConnected = false;
|
|
169
|
+
socketConnecting = false;
|
|
170
|
+
// Handle process exit - maybe attempt to relaunch?
|
|
171
|
+
});
|
|
172
|
+
// Unref the process to allow the Node.js event loop to exit if the server is done
|
|
173
|
+
codesysProcess.unref();
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Connects the socket client to the running listener script.
|
|
177
|
+
*/
|
|
178
|
+
function connectToCodesysListener() {
|
|
179
|
+
if (codesysClient && socketConnected) {
|
|
180
|
+
console.error("SERVER.TS: Socket client already connected.");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (socketConnecting) {
|
|
184
|
+
console.error("SERVER.TS: Socket client already attempting to connect.");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
socketConnecting = true;
|
|
188
|
+
console.error(`SERVER.TS: Attempting to connect to CODESYS listener on ${net.isIPv6(HOST) ? `[${HOST}]` : HOST}:${PORT}`);
|
|
189
|
+
codesysClient = new net.Socket();
|
|
190
|
+
codesysClient.connect(PORT, HOST, () => {
|
|
191
|
+
console.error("SERVER.TS: Socket connected to CODESYS listener.");
|
|
192
|
+
socketConnected = true;
|
|
193
|
+
socketConnecting = false;
|
|
194
|
+
// Process any queued commands now that the socket is connected
|
|
195
|
+
processCommandQueue();
|
|
196
|
+
});
|
|
197
|
+
codesysClient.on('data', (data) => {
|
|
198
|
+
socketQueue += data.toString();
|
|
199
|
+
// Process incoming data, assuming newline-delimited JSON messages
|
|
200
|
+
let newlineIndex;
|
|
201
|
+
while ((newlineIndex = socketQueue.indexOf('\n')) !== -1) {
|
|
202
|
+
const message = socketQueue.substring(0, newlineIndex);
|
|
203
|
+
socketQueue = socketQueue.substring(newlineIndex + 1);
|
|
204
|
+
try {
|
|
205
|
+
const response = JSON.parse(message);
|
|
206
|
+
console.log("SERVER.TS: Received response from listener:", response);
|
|
207
|
+
// Find and resolve the corresponding command in the queue
|
|
208
|
+
if (commandQueue.length > 0) {
|
|
209
|
+
const { resolve } = commandQueue.shift(); // Get the oldest command
|
|
210
|
+
resolve(response);
|
|
211
|
+
// Process the next command in the queue
|
|
212
|
+
processCommandQueue();
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
console.warn("SERVER.TS: Received unexpected response (command queue empty):", response);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch (e) {
|
|
219
|
+
console.error("SERVER.TS ERROR: Failed to parse JSON response from listener:", e, "Raw data:", message);
|
|
220
|
+
// Handle parsing error - potentially reject the current command
|
|
221
|
+
if (commandQueue.length > 0) {
|
|
222
|
+
const { reject } = commandQueue.shift();
|
|
223
|
+
reject(new Error(`Failed to parse listener response: ${message}`));
|
|
224
|
+
processCommandQueue();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
codesysClient.on('close', () => {
|
|
230
|
+
console.error("SERVER.TS: Socket connection to CODESYS listener closed.");
|
|
231
|
+
socketConnected = false;
|
|
232
|
+
socketConnecting = false;
|
|
233
|
+
// Handle socket close - attempt to reconnect? Reject pending commands?
|
|
234
|
+
// For now, reject all pending commands
|
|
235
|
+
while (commandQueue.length > 0) {
|
|
236
|
+
const { reject } = commandQueue.shift();
|
|
237
|
+
reject(new Error("Socket connection to CODESYS listener closed."));
|
|
238
|
+
}
|
|
239
|
+
// Attempt to reconnect after a delay
|
|
240
|
+
setTimeout(connectToCodesysListener, 5000); // Retry every 5 seconds
|
|
241
|
+
});
|
|
242
|
+
codesysClient.on('error', (err) => {
|
|
243
|
+
console.error(`SERVER.TS ERROR: Socket error: ${err.message}`);
|
|
244
|
+
// Error event is often followed by 'close', so cleanup is in 'close' handler
|
|
245
|
+
socketConnected = false;
|
|
246
|
+
socketConnecting = false;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Sends a command to the CODESYS listener script and waits for a response.
|
|
251
|
+
* This function replaces executeCodesysScript.
|
|
252
|
+
*/
|
|
253
|
+
function sendCodesysCommand(command) {
|
|
254
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
255
|
+
return new Promise((resolve, reject) => {
|
|
256
|
+
commandQueue.push({ command, resolve, reject });
|
|
257
|
+
processCommandQueue();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Processes the command queue. Sends the next command if the socket is connected and not busy.
|
|
263
|
+
*/
|
|
264
|
+
function processCommandQueue() {
|
|
265
|
+
if (commandQueue.length > 0 && socketConnected && !isProcessingCommand) {
|
|
266
|
+
isProcessingCommand = true;
|
|
267
|
+
const { command } = commandQueue[0]; // Get the oldest command without removing it yet
|
|
268
|
+
const commandString = JSON.stringify(command) + '\n';
|
|
269
|
+
console.log("SERVER.TS: Sending command to listener:", command);
|
|
270
|
+
codesysClient.write(commandString, (err) => {
|
|
271
|
+
if (err) {
|
|
272
|
+
console.error("SERVER.TS ERROR: Failed to send command:", err);
|
|
273
|
+
// If sending fails, reject the current command and process the next
|
|
274
|
+
const { reject } = commandQueue.shift();
|
|
275
|
+
reject(err);
|
|
276
|
+
isProcessingCommand = false;
|
|
277
|
+
processCommandQueue();
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
// Command sent successfully, waiting for response in 'data' handler
|
|
281
|
+
// isProcessingCommand remains true until response is received
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
else if (commandQueue.length > 0 && !socketConnected && !socketConnecting) {
|
|
286
|
+
console.warn("SERVER.TS: Commands in queue, but socket not connected. Attempting to reconnect.");
|
|
287
|
+
connectToCodesysListener();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Gracefully shuts down the CODESYS process and socket connection.
|
|
292
|
+
*/
|
|
293
|
+
function shutdownCodesysPersistent() {
|
|
294
|
+
console.error("SERVER.TS: Shutting down persistent CODESYS instance.");
|
|
295
|
+
if (codesysClient && socketConnected) {
|
|
296
|
+
// Send exit command and close socket
|
|
297
|
+
sendCodesysCommand({ command: "exit" }).catch(err => console.error("SERVER.TS ERROR sending exit command:", err));
|
|
298
|
+
// Give some time for the exit command to be processed before closing socket
|
|
299
|
+
setTimeout(() => {
|
|
300
|
+
if (codesysClient) {
|
|
301
|
+
codesysClient.end();
|
|
302
|
+
codesysClient = null;
|
|
303
|
+
}
|
|
304
|
+
}, 1000); // Wait 1 second
|
|
305
|
+
}
|
|
306
|
+
else if (codesysClient && !socketConnected) {
|
|
307
|
+
// Socket not connected, just destroy it
|
|
308
|
+
codesysClient.destroy();
|
|
309
|
+
codesysClient = null;
|
|
310
|
+
}
|
|
311
|
+
if (codesysProcess) {
|
|
312
|
+
console.error(`SERVER.TS: Terminating CODESYS process (PID: ${codesysProcess.pid}).`);
|
|
313
|
+
// Use kill with a signal to allow graceful shutdown if CODESYS handles it
|
|
314
|
+
// 'SIGTERM' (15) is a common signal for graceful termination
|
|
315
|
+
codesysProcess.kill('SIGTERM');
|
|
316
|
+
codesysProcess = null;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// --- End Persistent CODESYS Instance Management ---
|
|
320
|
+
// --- Preload script templates ---
|
|
321
|
+
try {
|
|
322
|
+
yield (0, scriptLoader_1.preloadAllScriptTemplates)();
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
console.error(`SERVER.TS ERROR: Error preloading script templates: ${err.message}`);
|
|
326
|
+
// Continue execution, as script templates will be loaded on demand as needed
|
|
327
|
+
}
|
|
120
328
|
// --- MCP Server Initialization ---
|
|
121
329
|
console.error("SERVER.TS: Initializing McpServer...");
|
|
122
330
|
const server = new mcp_js_1.McpServer({
|
|
123
331
|
name: "CODESYS Control MCP Server",
|
|
124
|
-
version: "1.
|
|
332
|
+
version: "1.9.0", // Updated version for persistent instance
|
|
125
333
|
capabilities: {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
tools: { listChanged: true } // Assuming you might want to notify if tools change dynamically (less common)
|
|
334
|
+
resources: { listChanged: true },
|
|
335
|
+
tools: { listChanged: true }
|
|
129
336
|
}
|
|
130
337
|
});
|
|
131
338
|
console.error("SERVER.TS: McpServer instance created.");
|
|
132
339
|
// --- End MCP Server Initialization ---
|
|
133
|
-
// ---
|
|
134
|
-
const ENSURE_PROJECT_OPEN_PYTHON_SNIPPET = `
|
|
135
|
-
import sys
|
|
136
|
-
import scriptengine as script_engine
|
|
137
|
-
import os
|
|
138
|
-
import time
|
|
139
|
-
import traceback
|
|
140
|
-
|
|
141
|
-
# --- Function to ensure the correct project is open ---
|
|
142
|
-
MAX_RETRIES = 3
|
|
143
|
-
RETRY_DELAY = 2.0 # seconds (use float for time.sleep)
|
|
144
|
-
|
|
145
|
-
def ensure_project_open(target_project_path):
|
|
146
|
-
print("DEBUG: Ensuring project is open: %s" % target_project_path)
|
|
147
|
-
# Normalize target path once
|
|
148
|
-
normalized_target_path = os.path.normcase(os.path.abspath(target_project_path))
|
|
149
|
-
|
|
150
|
-
for attempt in range(MAX_RETRIES):
|
|
151
|
-
print("DEBUG: Ensure project attempt %d/%d for %s" % (attempt + 1, MAX_RETRIES, normalized_target_path))
|
|
152
|
-
primary_project = None
|
|
153
|
-
try:
|
|
154
|
-
# Getting primary project might fail if CODESYS instance is unstable
|
|
155
|
-
primary_project = script_engine.projects.primary
|
|
156
|
-
except Exception as primary_err:
|
|
157
|
-
print("WARN: Error getting primary project: %s. Assuming none." % primary_err)
|
|
158
|
-
# traceback.print_exc() # Optional: Print stack trace for this error
|
|
159
|
-
primary_project = None
|
|
160
|
-
|
|
161
|
-
current_project_path = ""
|
|
162
|
-
project_ok = False # Flag to check if target is confirmed primary and accessible
|
|
163
|
-
|
|
164
|
-
if primary_project:
|
|
165
|
-
try:
|
|
166
|
-
# Getting path should be relatively safe if primary_project object exists
|
|
167
|
-
current_project_path = os.path.normcase(os.path.abspath(primary_project.path))
|
|
168
|
-
print("DEBUG: Current primary project path: %s" % current_project_path)
|
|
169
|
-
if current_project_path == normalized_target_path:
|
|
170
|
-
# Found the right project as primary, now check if it's usable
|
|
171
|
-
print("DEBUG: Target project path matches primary. Checking access...")
|
|
172
|
-
try:
|
|
173
|
-
# Try a relatively safe operation to confirm object usability
|
|
174
|
-
# Getting children count is a reasonable check
|
|
175
|
-
_ = len(primary_project.get_children(False))
|
|
176
|
-
print("DEBUG: Target project '%s' is primary and accessible." % target_project_path)
|
|
177
|
-
project_ok = True
|
|
178
|
-
return primary_project # SUCCESS CASE 1: Already open and accessible
|
|
179
|
-
except Exception as access_err:
|
|
180
|
-
# Project found, but accessing it failed. Might be unstable.
|
|
181
|
-
print("WARN: Primary project access check failed for '%s': %s. Will attempt reopen." % (current_project_path, access_err))
|
|
182
|
-
# traceback.print_exc() # Optional: Print stack trace
|
|
183
|
-
primary_project = None # Force reopen by falling through
|
|
184
|
-
else:
|
|
185
|
-
# A *different* project is primary
|
|
186
|
-
print("DEBUG: Primary project is '%s', not the target '%s'." % (current_project_path, normalized_target_path))
|
|
187
|
-
# Consider closing the wrong project if causing issues, but for now, just open target
|
|
188
|
-
# try:
|
|
189
|
-
# print("DEBUG: Closing incorrect primary project '%s'..." % current_project_path)
|
|
190
|
-
# primary_project.close() # Be careful with unsaved changes
|
|
191
|
-
# except Exception as close_err:
|
|
192
|
-
# print("WARN: Failed to close incorrect primary project: %s" % close_err)
|
|
193
|
-
primary_project = None # Force open target project
|
|
194
|
-
|
|
195
|
-
except Exception as path_err:
|
|
196
|
-
# Failed even to get the path of the supposed primary project
|
|
197
|
-
print("WARN: Could not get path of current primary project: %s. Assuming not the target." % path_err)
|
|
198
|
-
# traceback.print_exc() # Optional: Print stack trace
|
|
199
|
-
primary_project = None # Force open target project
|
|
200
|
-
|
|
201
|
-
# If target project not confirmed as primary and accessible, attempt to open/reopen
|
|
202
|
-
if not project_ok:
|
|
203
|
-
# Log clearly whether we are opening initially or reopening
|
|
204
|
-
if primary_project is None and current_project_path == "":
|
|
205
|
-
print("DEBUG: No primary project detected. Attempting to open target: %s" % target_project_path)
|
|
206
|
-
elif primary_project is None and current_project_path != "":
|
|
207
|
-
print("DEBUG: Primary project was '%s' but failed access check or needed close. Attempting to open target: %s" % (current_project_path, target_project_path))
|
|
208
|
-
else: # Includes cases where wrong project was open
|
|
209
|
-
print("DEBUG: Target project not primary or initial check failed. Attempting to open/reopen: %s" % target_project_path)
|
|
210
|
-
|
|
211
|
-
try:
|
|
212
|
-
# Set flags for silent opening, handle potential attribute errors
|
|
213
|
-
update_mode = script_engine.VersionUpdateFlags.NoUpdates | script_engine.VersionUpdateFlags.SilentMode
|
|
214
|
-
# try:
|
|
215
|
-
# update_mode = script_engine.VersionUpdateFlags.NoUpdates | script_engine.VersionUpdateFlags.SilentMode
|
|
216
|
-
# except AttributeError:
|
|
217
|
-
# print("WARN: VersionUpdateFlags not found, using integer flags for open (1 | 2 = 3).")
|
|
218
|
-
# update_mode = 3 # 1=NoUpdates, 2=SilentMode
|
|
219
|
-
|
|
220
|
-
opened_project = None
|
|
221
|
-
try:
|
|
222
|
-
# The actual open call
|
|
223
|
-
print("DEBUG: Calling script_engine.projects.open('%s', update_flags=%s)..." % (target_project_path, update_mode))
|
|
224
|
-
opened_project = script_engine.projects.open(target_project_path, update_flags=update_mode)
|
|
225
|
-
|
|
226
|
-
if not opened_project:
|
|
227
|
-
# This is a critical failure if open returns None without exception
|
|
228
|
-
print("ERROR: projects.open returned None for %s on attempt %d" % (target_project_path, attempt + 1))
|
|
229
|
-
# Allow retry loop to continue
|
|
230
|
-
else:
|
|
231
|
-
# Open call returned *something*, let's verify
|
|
232
|
-
print("DEBUG: projects.open call returned an object for: %s" % target_project_path)
|
|
233
|
-
print("DEBUG: Pausing for stabilization after open...")
|
|
234
|
-
time.sleep(RETRY_DELAY) # Give CODESYS time
|
|
235
|
-
# Re-verify: Is the project now primary and accessible?
|
|
236
|
-
recheck_primary = None
|
|
237
|
-
try: recheck_primary = script_engine.projects.primary
|
|
238
|
-
except Exception as recheck_primary_err: print("WARN: Error getting primary project after reopen: %s" % recheck_primary_err)
|
|
239
|
-
|
|
240
|
-
if recheck_primary:
|
|
241
|
-
recheck_path = ""
|
|
242
|
-
try: # Getting path might fail
|
|
243
|
-
recheck_path = os.path.normcase(os.path.abspath(recheck_primary.path))
|
|
244
|
-
except Exception as recheck_path_err:
|
|
245
|
-
print("WARN: Failed to get path after reopen: %s" % recheck_path_err)
|
|
246
|
-
|
|
247
|
-
if recheck_path == normalized_target_path:
|
|
248
|
-
print("DEBUG: Target project confirmed as primary after reopening.")
|
|
249
|
-
try: # Final sanity check
|
|
250
|
-
_ = len(recheck_primary.get_children(False))
|
|
251
|
-
print("DEBUG: Reopened project basic access confirmed.")
|
|
252
|
-
return recheck_primary # SUCCESS CASE 2: Successfully opened/reopened
|
|
253
|
-
except Exception as access_err_reopen:
|
|
254
|
-
print("WARN: Reopened project (%s) basic access check failed: %s." % (normalized_target_path, access_err_reopen))
|
|
255
|
-
# traceback.print_exc() # Optional
|
|
256
|
-
# Allow retry loop to continue
|
|
257
|
-
else:
|
|
258
|
-
print("WARN: Different project is primary after reopening! Expected '%s', got '%s'." % (normalized_target_path, recheck_path))
|
|
259
|
-
# Allow retry loop to continue, maybe it fixes itself
|
|
260
|
-
else:
|
|
261
|
-
print("WARN: No primary project found after reopening attempt %d!" % (attempt+1))
|
|
262
|
-
# Allow retry loop to continue
|
|
263
|
-
|
|
264
|
-
except Exception as open_err:
|
|
265
|
-
# Catch errors during the open call itself
|
|
266
|
-
print("ERROR: Exception during projects.open call on attempt %d: %s" % (attempt + 1, open_err))
|
|
267
|
-
traceback.print_exc() # Crucial for diagnosing open failures
|
|
268
|
-
# Allow retry loop to continue
|
|
269
|
-
|
|
270
|
-
except Exception as outer_open_err:
|
|
271
|
-
# Catch errors in the flag setup etc.
|
|
272
|
-
print("ERROR: Unexpected error during open setup/logic attempt %d: %s" % (attempt + 1, outer_open_err))
|
|
273
|
-
traceback.print_exc()
|
|
274
|
-
|
|
275
|
-
# If we didn't return successfully in this attempt, wait before retrying
|
|
276
|
-
if attempt < MAX_RETRIES - 1:
|
|
277
|
-
print("DEBUG: Ensure project attempt %d did not succeed. Waiting %f seconds..." % (attempt + 1, RETRY_DELAY))
|
|
278
|
-
time.sleep(RETRY_DELAY)
|
|
279
|
-
else: # Last attempt failed
|
|
280
|
-
print("ERROR: Failed all ensure_project_open attempts for %s." % normalized_target_path)
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
# If all retries fail after the loop
|
|
284
|
-
raise RuntimeError("Failed to ensure project '%s' is open and accessible after %d attempts." % (target_project_path, MAX_RETRIES))
|
|
285
|
-
# --- End of function ---
|
|
286
|
-
|
|
287
|
-
# Placeholder for the project file path (must be set in scripts using this snippet)
|
|
288
|
-
PROJECT_FILE_PATH = r"{PROJECT_FILE_PATH}"
|
|
289
|
-
`;
|
|
290
|
-
const CHECK_STATUS_SCRIPT = `
|
|
291
|
-
import sys, scriptengine as script_engine, os, traceback
|
|
292
|
-
project_open = False; project_name = "No project open"; project_path = "N/A"; scripting_ok = False
|
|
293
|
-
try:
|
|
294
|
-
scripting_ok = True; primary_project = script_engine.projects.primary
|
|
295
|
-
if primary_project:
|
|
296
|
-
project_open = True
|
|
297
|
-
try:
|
|
298
|
-
project_path = os.path.normcase(os.path.abspath(primary_project.path))
|
|
299
|
-
try:
|
|
300
|
-
project_name = primary_project.get_name() # Might fail
|
|
301
|
-
if not project_name: project_name = "Unnamed (path: %s)" % os.path.basename(project_path)
|
|
302
|
-
except: project_name = "Unnamed (path: %s)" % os.path.basename(project_path)
|
|
303
|
-
except Exception as e_path: project_path = "N/A (Error: %s)" % e_path; project_name = "Unnamed (Path Error)"
|
|
304
|
-
print("Project Open: %s" % project_open); print("Project Name: %s" % project_name)
|
|
305
|
-
print("Project Path: %s" % project_path); print("Scripting OK: %s" % scripting_ok)
|
|
306
|
-
print("SCRIPT_SUCCESS: Status check complete."); sys.exit(0)
|
|
307
|
-
except Exception as e:
|
|
308
|
-
error_message = "Error during status check: %s" % e
|
|
309
|
-
print(error_message); print("Scripting OK: False")
|
|
310
|
-
# traceback.print_exc() # Optional traceback
|
|
311
|
-
print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
|
|
312
|
-
`;
|
|
313
|
-
const OPEN_PROJECT_SCRIPT_TEMPLATE = `
|
|
314
|
-
import sys, scriptengine as script_engine, os, traceback
|
|
315
|
-
${ENSURE_PROJECT_OPEN_PYTHON_SNIPPET}
|
|
316
|
-
try:
|
|
317
|
-
project = ensure_project_open(PROJECT_FILE_PATH)
|
|
318
|
-
# Get name from object if possible, otherwise use path basename
|
|
319
|
-
proj_name = "Unknown"
|
|
320
|
-
try:
|
|
321
|
-
if project: proj_name = project.get_name() or os.path.basename(PROJECT_FILE_PATH)
|
|
322
|
-
else: proj_name = os.path.basename(PROJECT_FILE_PATH) + " (ensure_project_open returned None?)"
|
|
323
|
-
except Exception:
|
|
324
|
-
proj_name = os.path.basename(PROJECT_FILE_PATH) + " (name retrieval failed)"
|
|
325
|
-
print("Project Opened: %s" % proj_name)
|
|
326
|
-
print("SCRIPT_SUCCESS: Project opened successfully.")
|
|
327
|
-
sys.exit(0)
|
|
328
|
-
except Exception as e:
|
|
329
|
-
error_message = "Error opening project %s: %s" % (PROJECT_FILE_PATH, e)
|
|
330
|
-
print(error_message)
|
|
331
|
-
traceback.print_exc()
|
|
332
|
-
print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
|
|
333
|
-
`;
|
|
334
|
-
const CREATE_PROJECT_SCRIPT_TEMPLATE = `
|
|
335
|
-
import sys, scriptengine as script_engine, os, shutil, time, traceback
|
|
336
|
-
# Placeholders
|
|
337
|
-
TEMPLATE_PROJECT_PATH = r'{TEMPLATE_PROJECT_PATH}' # Path to Standard.project
|
|
338
|
-
PROJECT_FILE_PATH = r'{PROJECT_FILE_PATH}' # Path for the new project (Target Path)
|
|
339
|
-
try:
|
|
340
|
-
print("DEBUG: Python script create_project (copy from template):")
|
|
341
|
-
print("DEBUG: Template Source = %s" % TEMPLATE_PROJECT_PATH)
|
|
342
|
-
print("DEBUG: Target Path = %s" % PROJECT_FILE_PATH)
|
|
343
|
-
if not PROJECT_FILE_PATH: raise ValueError("Target project file path empty.")
|
|
344
|
-
if not TEMPLATE_PROJECT_PATH: raise ValueError("Template project file path empty.")
|
|
345
|
-
if not os.path.exists(TEMPLATE_PROJECT_PATH): raise IOError("Template project file not found: %s" % TEMPLATE_PROJECT_PATH)
|
|
346
|
-
|
|
347
|
-
# 1. Copy the template project file to the new location
|
|
348
|
-
target_dir = os.path.dirname(PROJECT_FILE_PATH)
|
|
349
|
-
if not os.path.exists(target_dir): print("DEBUG: Creating target directory: %s" % target_dir); os.makedirs(target_dir)
|
|
350
|
-
# Check if target file already exists
|
|
351
|
-
if os.path.exists(PROJECT_FILE_PATH): print("WARN: Target project file already exists, overwriting: %s" % PROJECT_FILE_PATH)
|
|
352
|
-
|
|
353
|
-
print("DEBUG: Copying '%s' to '%s'..." % (TEMPLATE_PROJECT_PATH, PROJECT_FILE_PATH))
|
|
354
|
-
shutil.copy2(TEMPLATE_PROJECT_PATH, PROJECT_FILE_PATH) # copy2 preserves metadata
|
|
355
|
-
print("DEBUG: File copy complete.")
|
|
356
|
-
|
|
357
|
-
# 2. Open the newly copied project file
|
|
358
|
-
print("DEBUG: Opening the copied project: %s" % PROJECT_FILE_PATH)
|
|
359
|
-
# Set flags for silent opening
|
|
360
|
-
update_mode = script_engine.VersionUpdateFlags.NoUpdates | script_engine.VersionUpdateFlags.SilentMode
|
|
361
|
-
# try:
|
|
362
|
-
# update_mode = script_engine.VersionUpdateFlags.NoUpdates | script_engine.VersionUpdateFlags.SilentMode
|
|
363
|
-
# except AttributeError:
|
|
364
|
-
# print("WARN: VersionUpdateFlags not found, using integer flags for open (1 | 2 = 3).")
|
|
365
|
-
# update_mode = 3
|
|
366
|
-
|
|
367
|
-
project = script_engine.projects.open(PROJECT_FILE_PATH, update_flags=update_mode)
|
|
368
|
-
print("DEBUG: script_engine.projects.open returned: %s" % project)
|
|
369
|
-
if project:
|
|
370
|
-
print("DEBUG: Pausing briefly after open...")
|
|
371
|
-
time.sleep(1.0) # Allow CODESYS to potentially initialize things
|
|
372
|
-
try:
|
|
373
|
-
print("DEBUG: Explicitly saving project after opening copy...")
|
|
374
|
-
project.save();
|
|
375
|
-
print("DEBUG: Project save after opening copy succeeded.")
|
|
376
|
-
except Exception as save_err:
|
|
377
|
-
print("WARN: Explicit save after opening copy failed: %s" % save_err)
|
|
378
|
-
# Decide if this is critical - maybe not, but good to know.
|
|
379
|
-
print("Project Created from Template Copy at: %s" % PROJECT_FILE_PATH)
|
|
380
|
-
print("SCRIPT_SUCCESS: Project copied from template and opened successfully.")
|
|
381
|
-
sys.exit(0)
|
|
382
|
-
else:
|
|
383
|
-
error_message = "Failed to open project copy %s after copying template %s. projects.open returned None." % (PROJECT_FILE_PATH, TEMPLATE_PROJECT_PATH)
|
|
384
|
-
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
|
|
385
|
-
except Exception as e:
|
|
386
|
-
detailed_error = traceback.format_exc()
|
|
387
|
-
error_message = "Error creating project '%s' from template '%s': %s\\n%s" % (PROJECT_FILE_PATH, TEMPLATE_PROJECT_PATH, e, detailed_error)
|
|
388
|
-
print(error_message); print("SCRIPT_ERROR: Error copying/opening template: %s" % e); sys.exit(1)
|
|
389
|
-
`;
|
|
390
|
-
const SAVE_PROJECT_SCRIPT_TEMPLATE = `
|
|
391
|
-
import sys, scriptengine as script_engine, os, traceback
|
|
392
|
-
${ENSURE_PROJECT_OPEN_PYTHON_SNIPPET}
|
|
393
|
-
try:
|
|
394
|
-
primary_project = ensure_project_open(PROJECT_FILE_PATH)
|
|
395
|
-
# Get name from object if possible, otherwise use path basename
|
|
396
|
-
project_name = "Unknown"
|
|
397
|
-
try:
|
|
398
|
-
if primary_project: project_name = primary_project.get_name() or os.path.basename(PROJECT_FILE_PATH)
|
|
399
|
-
else: project_name = os.path.basename(PROJECT_FILE_PATH) + " (ensure_project_open returned None?)"
|
|
400
|
-
except Exception:
|
|
401
|
-
project_name = os.path.basename(PROJECT_FILE_PATH) + " (name retrieval failed)"
|
|
402
|
-
|
|
403
|
-
print("DEBUG: Saving project: %s (%s)" % (project_name, PROJECT_FILE_PATH))
|
|
404
|
-
primary_project.save()
|
|
405
|
-
print("DEBUG: project.save() executed.")
|
|
406
|
-
print("Project Saved: %s" % project_name)
|
|
407
|
-
print("SCRIPT_SUCCESS: Project saved successfully.")
|
|
408
|
-
sys.exit(0)
|
|
409
|
-
except Exception as e:
|
|
410
|
-
error_message = "Error saving project %s: %s" % (PROJECT_FILE_PATH, e)
|
|
411
|
-
print(error_message)
|
|
412
|
-
traceback.print_exc()
|
|
413
|
-
print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
|
|
414
|
-
`;
|
|
415
|
-
const FIND_OBJECT_BY_PATH_PYTHON_SNIPPET = `
|
|
416
|
-
import traceback
|
|
417
|
-
# --- Find object by path function ---
|
|
418
|
-
def find_object_by_path_robust(start_node, full_path, target_type_name="object"):
|
|
419
|
-
print("DEBUG: Finding %s by path: '%s'" % (target_type_name, full_path))
|
|
420
|
-
normalized_path = full_path.replace('\\\\', '/').strip('/')
|
|
421
|
-
path_parts = normalized_path.split('/')
|
|
422
|
-
if not path_parts:
|
|
423
|
-
print("ERROR: Path is empty.")
|
|
424
|
-
return None
|
|
425
|
-
|
|
426
|
-
# Determine the actual starting node (project or application)
|
|
427
|
-
project = start_node # Assume start_node is project initially
|
|
428
|
-
if not hasattr(start_node, 'active_application') and hasattr(start_node, 'project'):
|
|
429
|
-
# If start_node is not project but has project ref (e.g., an application), get the project
|
|
430
|
-
try: project = start_node.project
|
|
431
|
-
except Exception as proj_ref_err:
|
|
432
|
-
print("WARN: Could not get project reference from start_node: %s" % proj_ref_err)
|
|
433
|
-
# Proceed assuming start_node might be the project anyway or search fails
|
|
434
|
-
|
|
435
|
-
# Try to get the application object robustly if we think we have the project
|
|
436
|
-
app = None
|
|
437
|
-
if hasattr(project, 'active_application'):
|
|
438
|
-
try: app = project.active_application
|
|
439
|
-
except Exception: pass # Ignore errors getting active app
|
|
440
|
-
if not app:
|
|
441
|
-
try:
|
|
442
|
-
apps = project.find("Application", True) # Search recursively
|
|
443
|
-
if apps: app = apps[0]
|
|
444
|
-
except Exception: pass
|
|
445
|
-
|
|
446
|
-
# Check if the first path part matches the application name
|
|
447
|
-
app_name_lower = ""
|
|
448
|
-
if app:
|
|
449
|
-
try: app_name_lower = (app.get_name() or "application").lower()
|
|
450
|
-
except Exception: app_name_lower = "application" # Fallback
|
|
451
|
-
|
|
452
|
-
# Decide where to start the traversal
|
|
453
|
-
current_obj = start_node # Default to the node passed in
|
|
454
|
-
if hasattr(project, 'active_application'): # Only adjust if start_node was likely the project
|
|
455
|
-
if app and path_parts[0].lower() == app_name_lower:
|
|
456
|
-
print("DEBUG: Path starts with Application name '%s'. Beginning search there." % path_parts[0])
|
|
457
|
-
current_obj = app
|
|
458
|
-
path_parts = path_parts[1:] # Consume the app name part
|
|
459
|
-
# If path was *only* the application name
|
|
460
|
-
if not path_parts:
|
|
461
|
-
print("DEBUG: Target path is the Application object itself.")
|
|
462
|
-
return current_obj
|
|
463
|
-
else:
|
|
464
|
-
print("DEBUG: Path does not start with Application name. Starting search from project root.")
|
|
465
|
-
current_obj = project # Start search from the project root
|
|
466
|
-
else:
|
|
467
|
-
print("DEBUG: Starting search from originally provided node.")
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
# Traverse the remaining path parts
|
|
471
|
-
parent_path_str = getattr(current_obj, 'get_name', lambda: str(current_obj))() # Safer name getting
|
|
472
|
-
|
|
473
|
-
for i, part_name in enumerate(path_parts):
|
|
474
|
-
is_last_part = (i == len(path_parts) - 1)
|
|
475
|
-
print("DEBUG: Searching for part [%d/%d]: '%s' under '%s'" % (i+1, len(path_parts), part_name, parent_path_str))
|
|
476
|
-
found_in_parent = None
|
|
477
|
-
try:
|
|
478
|
-
# Prioritize non-recursive find for direct children
|
|
479
|
-
children_of_current = current_obj.get_children(False)
|
|
480
|
-
print("DEBUG: Found %d direct children under '%s'." % (len(children_of_current), parent_path_str))
|
|
481
|
-
for child in children_of_current:
|
|
482
|
-
child_name = getattr(child, 'get_name', lambda: None)() # Safer name getting
|
|
483
|
-
# print("DEBUG: Checking child: '%s'" % child_name) # Verbose
|
|
484
|
-
if child_name == part_name:
|
|
485
|
-
found_in_parent = child
|
|
486
|
-
print("DEBUG: Found direct child matching '%s'." % part_name)
|
|
487
|
-
break # Found direct child, stop searching children
|
|
488
|
-
|
|
489
|
-
# If not found directly, AND it's the last part, try recursive find from current parent
|
|
490
|
-
if not found_in_parent and is_last_part:
|
|
491
|
-
print("DEBUG: Direct find failed for last part '%s'. Trying recursive find under '%s'." % (part_name, parent_path_str))
|
|
492
|
-
found_recursive_list = current_obj.find(part_name, True) # Recursive find
|
|
493
|
-
if found_recursive_list:
|
|
494
|
-
# Maybe add a check here if multiple are found?
|
|
495
|
-
found_in_parent = found_recursive_list[0] # Take the first match
|
|
496
|
-
print("DEBUG: Found last part '%s' recursively." % part_name)
|
|
497
|
-
else:
|
|
498
|
-
print("DEBUG: Recursive find also failed for last part '%s'." % part_name)
|
|
499
|
-
|
|
500
|
-
# Update current object if found
|
|
501
|
-
if found_in_parent:
|
|
502
|
-
current_obj = found_in_parent
|
|
503
|
-
parent_path_str = getattr(current_obj, 'get_name', lambda: part_name)() # Safer name getting
|
|
504
|
-
print("DEBUG: Stepped into '%s'." % parent_path_str)
|
|
505
|
-
else:
|
|
506
|
-
# If not found at any point, the path is invalid from this parent
|
|
507
|
-
print("ERROR: Path part '%s' not found under '%s'." % (part_name, parent_path_str))
|
|
508
|
-
return None # Path broken
|
|
509
|
-
|
|
510
|
-
except Exception as find_err:
|
|
511
|
-
print("ERROR: Exception while searching for '%s' under '%s': %s" % (part_name, parent_path_str, find_err))
|
|
512
|
-
traceback.print_exc()
|
|
513
|
-
return None # Error during search
|
|
514
|
-
|
|
515
|
-
# Final verification (optional but recommended): Check if the found object's name matches the last part
|
|
516
|
-
final_expected_name = full_path.split('/')[-1]
|
|
517
|
-
found_final_name = getattr(current_obj, 'get_name', lambda: None)() # Safer name getting
|
|
518
|
-
|
|
519
|
-
if found_final_name == final_expected_name:
|
|
520
|
-
print("DEBUG: Final %s found and name verified for path '%s': %s" % (target_type_name, full_path, found_final_name))
|
|
521
|
-
return current_obj
|
|
522
|
-
else:
|
|
523
|
-
print("ERROR: Traversal ended on object '%s' but expected final name was '%s'." % (found_final_name, final_expected_name))
|
|
524
|
-
return None # Name mismatch implies target not found as expected
|
|
525
|
-
|
|
526
|
-
# --- End of find object function ---
|
|
527
|
-
`;
|
|
528
|
-
const CREATE_POU_SCRIPT_TEMPLATE = `
|
|
529
|
-
import sys, scriptengine as script_engine, os, traceback
|
|
530
|
-
${ENSURE_PROJECT_OPEN_PYTHON_SNIPPET}
|
|
531
|
-
${FIND_OBJECT_BY_PATH_PYTHON_SNIPPET}
|
|
532
|
-
POU_NAME = "{POU_NAME}"; POU_TYPE_STR = "{POU_TYPE_STR}"; IMPL_LANGUAGE_STR = "{IMPL_LANGUAGE_STR}"; PARENT_PATH_REL = "{PARENT_PATH}"
|
|
533
|
-
pou_type_map = { "Program": script_engine.PouType.Program, "FunctionBlock": script_engine.PouType.FunctionBlock, "Function": script_engine.PouType.Function }
|
|
534
|
-
# Map common language names to ImplementationLanguages attributes if needed (optional, None usually works)
|
|
535
|
-
# lang_map = { "ST": script_engine.ImplementationLanguage.st, ... }
|
|
536
|
-
|
|
537
|
-
try:
|
|
538
|
-
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))
|
|
539
|
-
primary_project = ensure_project_open(PROJECT_FILE_PATH)
|
|
540
|
-
if not POU_NAME: raise ValueError("POU name empty.")
|
|
541
|
-
if not PARENT_PATH_REL: raise ValueError("Parent path empty.")
|
|
542
|
-
|
|
543
|
-
# Resolve POU Type Enum
|
|
544
|
-
pou_type_enum = pou_type_map.get(POU_TYPE_STR)
|
|
545
|
-
if not pou_type_enum: raise ValueError("Invalid POU type string: %s. Use Program, FunctionBlock, or Function." % POU_TYPE_STR)
|
|
546
|
-
|
|
547
|
-
# Find parent object using the robust function
|
|
548
|
-
parent_object = find_object_by_path_robust(primary_project, PARENT_PATH_REL, "parent container")
|
|
549
|
-
if not parent_object: raise ValueError("Parent object not found for path: %s" % PARENT_PATH_REL)
|
|
550
|
-
|
|
551
|
-
parent_name = getattr(parent_object, 'get_name', lambda: str(parent_object))()
|
|
552
|
-
print("DEBUG: Using parent object: %s (Type: %s)" % (parent_name, type(parent_object).__name__))
|
|
553
|
-
|
|
554
|
-
# Check if parent object supports creating POUs (should implement ScriptIecLanguageObjectContainer)
|
|
555
|
-
if not hasattr(parent_object, 'create_pou'):
|
|
556
|
-
raise TypeError("Parent object '%s' of type %s does not support create_pou." % (parent_name, type(parent_object).__name__))
|
|
557
|
-
|
|
558
|
-
# Set language GUID to None (let CODESYS default based on parent/settings)
|
|
559
|
-
lang_guid = None
|
|
560
|
-
print("DEBUG: Setting language to None (will use default).")
|
|
561
|
-
# Example if mapping language string: lang_guid = lang_map.get(IMPL_LANGUAGE_STR, None)
|
|
562
|
-
|
|
563
|
-
print("DEBUG: Calling parent_object.create_pou: Name='%s', Type=%s, Lang=%s" % (POU_NAME, pou_type_enum, lang_guid))
|
|
564
|
-
|
|
565
|
-
# Call create_pou using keyword arguments
|
|
566
|
-
new_pou = parent_object.create_pou(
|
|
567
|
-
name=POU_NAME,
|
|
568
|
-
type=pou_type_enum,
|
|
569
|
-
language=lang_guid # Pass None
|
|
570
|
-
)
|
|
571
|
-
|
|
572
|
-
print("DEBUG: parent_object.create_pou returned: %s" % new_pou)
|
|
573
|
-
if new_pou:
|
|
574
|
-
new_pou_name = getattr(new_pou, 'get_name', lambda: POU_NAME)()
|
|
575
|
-
print("DEBUG: POU object created: %s" % new_pou_name)
|
|
576
|
-
|
|
577
|
-
# --- SAVE THE PROJECT TO PERSIST THE NEW POU ---
|
|
578
|
-
try:
|
|
579
|
-
print("DEBUG: Saving Project...")
|
|
580
|
-
primary_project.save() # Save the overall project file
|
|
581
|
-
print("DEBUG: Project saved successfully after POU creation.")
|
|
582
|
-
except Exception as save_err:
|
|
583
|
-
print("ERROR: Failed to save Project after POU creation: %s" % save_err)
|
|
584
|
-
detailed_error = traceback.format_exc()
|
|
585
|
-
error_message = "Error saving Project after creating POU '%s': %s\\n%s" % (new_pou_name, save_err, detailed_error)
|
|
586
|
-
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
|
|
587
|
-
# --- END SAVING ---
|
|
588
|
-
|
|
589
|
-
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)
|
|
590
|
-
print("SCRIPT_SUCCESS: POU created successfully."); sys.exit(0)
|
|
591
|
-
else:
|
|
592
|
-
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)
|
|
593
|
-
except Exception as e:
|
|
594
|
-
detailed_error = traceback.format_exc()
|
|
595
|
-
error_message = "Error creating POU '%s' in project '%s': %s\\n%s" % (POU_NAME, PROJECT_FILE_PATH, e, detailed_error)
|
|
596
|
-
print(error_message); print("SCRIPT_ERROR: Error creating POU '%s': %s" % (POU_NAME, e)); sys.exit(1)
|
|
597
|
-
`;
|
|
598
|
-
const SET_POU_CODE_SCRIPT_TEMPLATE = `
|
|
599
|
-
import sys, scriptengine as script_engine, os, traceback
|
|
600
|
-
${ENSURE_PROJECT_OPEN_PYTHON_SNIPPET}
|
|
601
|
-
${FIND_OBJECT_BY_PATH_PYTHON_SNIPPET}
|
|
602
|
-
POU_FULL_PATH = "{POU_FULL_PATH}" # Expecting format like "Application/MyPOU" or "Folder/SubFolder/MyPOU"
|
|
603
|
-
DECLARATION_CONTENT = """{DECLARATION_CONTENT}"""
|
|
604
|
-
IMPLEMENTATION_CONTENT = """{IMPLEMENTATION_CONTENT}"""
|
|
605
|
-
|
|
606
|
-
try:
|
|
607
|
-
print("DEBUG: set_pou_code script: POU_FULL_PATH='%s', Project='%s'" % (POU_FULL_PATH, PROJECT_FILE_PATH))
|
|
608
|
-
primary_project = ensure_project_open(PROJECT_FILE_PATH)
|
|
609
|
-
if not POU_FULL_PATH: raise ValueError("POU full path empty.")
|
|
610
|
-
|
|
611
|
-
# Find the target POU/Method/Property object
|
|
612
|
-
target_object = find_object_by_path_robust(primary_project, POU_FULL_PATH, "target object")
|
|
613
|
-
if not target_object: raise ValueError("Target object not found using path: %s" % POU_FULL_PATH)
|
|
614
|
-
|
|
615
|
-
target_name = getattr(target_object, 'get_name', lambda: POU_FULL_PATH)()
|
|
616
|
-
print("DEBUG: Found target object: %s" % target_name)
|
|
617
|
-
|
|
618
|
-
# --- Set Declaration Part ---
|
|
619
|
-
declaration_updated = False
|
|
620
|
-
# Check if the content is actually provided (might be None/empty if only impl is set)
|
|
621
|
-
has_declaration_content = 'DECLARATION_CONTENT' in locals() or 'DECLARATION_CONTENT' in globals()
|
|
622
|
-
if has_declaration_content and DECLARATION_CONTENT is not None: # Check not None
|
|
623
|
-
if hasattr(target_object, 'textual_declaration'):
|
|
624
|
-
decl_obj = target_object.textual_declaration
|
|
625
|
-
if decl_obj and hasattr(decl_obj, 'replace'):
|
|
626
|
-
try:
|
|
627
|
-
print("DEBUG: Accessing textual_declaration...")
|
|
628
|
-
decl_obj.replace(DECLARATION_CONTENT)
|
|
629
|
-
print("DEBUG: Set declaration text using replace().")
|
|
630
|
-
declaration_updated = True
|
|
631
|
-
except Exception as decl_err:
|
|
632
|
-
print("ERROR: Failed to set declaration text: %s" % decl_err)
|
|
633
|
-
traceback.print_exc() # Print stack trace for detailed error
|
|
634
|
-
else:
|
|
635
|
-
print("WARN: Target '%s' textual_declaration attribute is None or does not have replace(). Skipping declaration update." % target_name)
|
|
636
|
-
else:
|
|
637
|
-
print("WARN: Target '%s' does not have textual_declaration attribute. Skipping declaration update." % target_name)
|
|
638
|
-
else:
|
|
639
|
-
print("DEBUG: Declaration content not provided or is None. Skipping declaration update.")
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
# --- Set Implementation Part ---
|
|
643
|
-
implementation_updated = False
|
|
644
|
-
has_implementation_content = 'IMPLEMENTATION_CONTENT' in locals() or 'IMPLEMENTATION_CONTENT' in globals()
|
|
645
|
-
if has_implementation_content and IMPLEMENTATION_CONTENT is not None: # Check not None
|
|
646
|
-
if hasattr(target_object, 'textual_implementation'):
|
|
647
|
-
impl_obj = target_object.textual_implementation
|
|
648
|
-
if impl_obj and hasattr(impl_obj, 'replace'):
|
|
649
|
-
try:
|
|
650
|
-
print("DEBUG: Accessing textual_implementation...")
|
|
651
|
-
impl_obj.replace(IMPLEMENTATION_CONTENT)
|
|
652
|
-
print("DEBUG: Set implementation text using replace().")
|
|
653
|
-
implementation_updated = True
|
|
654
|
-
except Exception as impl_err:
|
|
655
|
-
print("ERROR: Failed to set implementation text: %s" % impl_err)
|
|
656
|
-
traceback.print_exc() # Print stack trace for detailed error
|
|
657
|
-
else:
|
|
658
|
-
print("WARN: Target '%s' textual_implementation attribute is None or does not have replace(). Skipping implementation update." % target_name)
|
|
659
|
-
else:
|
|
660
|
-
print("WARN: Target '%s' does not have textual_implementation attribute. Skipping implementation update." % target_name)
|
|
661
|
-
else:
|
|
662
|
-
print("DEBUG: Implementation content not provided or is None. Skipping implementation update.")
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
# --- SAVE THE PROJECT TO PERSIST THE CODE CHANGE ---
|
|
666
|
-
# Only save if something was actually updated to avoid unnecessary saves
|
|
667
|
-
if declaration_updated or implementation_updated:
|
|
668
|
-
try:
|
|
669
|
-
print("DEBUG: Saving Project (after code change)...")
|
|
670
|
-
primary_project.save() # Save the overall project file
|
|
671
|
-
print("DEBUG: Project saved successfully after code change.")
|
|
672
|
-
except Exception as save_err:
|
|
673
|
-
print("ERROR: Failed to save Project after setting code: %s" % save_err)
|
|
674
|
-
detailed_error = traceback.format_exc()
|
|
675
|
-
error_message = "Error saving Project after code change for '%s': %s\\n%s" % (target_name, save_err, detailed_error)
|
|
676
|
-
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
|
|
677
|
-
else:
|
|
678
|
-
print("DEBUG: No code parts were updated, skipping project save.")
|
|
679
|
-
# --- END SAVING ---
|
|
680
|
-
|
|
681
|
-
print("Code Set For: %s" % target_name)
|
|
682
|
-
print("Path: %s" % POU_FULL_PATH)
|
|
683
|
-
print("SCRIPT_SUCCESS: Declaration and/or implementation set successfully."); sys.exit(0)
|
|
684
|
-
|
|
685
|
-
except Exception as e:
|
|
686
|
-
detailed_error = traceback.format_exc()
|
|
687
|
-
error_message = "Error setting code for object '%s' in project '%s': %s\\n%s" % (POU_FULL_PATH, PROJECT_FILE_PATH, e, detailed_error)
|
|
688
|
-
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
|
|
689
|
-
`;
|
|
690
|
-
const CREATE_PROPERTY_SCRIPT_TEMPLATE = `
|
|
691
|
-
import sys, scriptengine as script_engine, os, traceback
|
|
692
|
-
${ENSURE_PROJECT_OPEN_PYTHON_SNIPPET}
|
|
693
|
-
${FIND_OBJECT_BY_PATH_PYTHON_SNIPPET}
|
|
694
|
-
PARENT_POU_FULL_PATH = "{PARENT_POU_FULL_PATH}" # e.g., "Application/MyFB"
|
|
695
|
-
PROPERTY_NAME = "{PROPERTY_NAME}"
|
|
696
|
-
PROPERTY_TYPE = "{PROPERTY_TYPE}"
|
|
697
|
-
# Optional: Language for Getter/Setter (usually defaults to ST)
|
|
698
|
-
# LANG_GUID_STR = "{LANG_GUID_STR}" # Example if needed
|
|
699
|
-
|
|
700
|
-
try:
|
|
701
|
-
print("DEBUG: create_property script: ParentPOU='%s', Name='%s', Type='%s', Project='%s'" % (PARENT_POU_FULL_PATH, PROPERTY_NAME, PROPERTY_TYPE, PROJECT_FILE_PATH))
|
|
702
|
-
primary_project = ensure_project_open(PROJECT_FILE_PATH)
|
|
703
|
-
if not PARENT_POU_FULL_PATH: raise ValueError("Parent POU full path empty.")
|
|
704
|
-
if not PROPERTY_NAME: raise ValueError("Property name empty.")
|
|
705
|
-
if not PROPERTY_TYPE: raise ValueError("Property type empty.")
|
|
706
|
-
|
|
707
|
-
# Find the parent POU object
|
|
708
|
-
parent_pou_object = find_object_by_path_robust(primary_project, PARENT_POU_FULL_PATH, "parent POU")
|
|
709
|
-
if not parent_pou_object: raise ValueError("Parent POU object not found: %s" % PARENT_POU_FULL_PATH)
|
|
710
|
-
|
|
711
|
-
parent_pou_name = getattr(parent_pou_object, 'get_name', lambda: PARENT_POU_FULL_PATH)()
|
|
712
|
-
print("DEBUG: Found Parent POU object: %s" % parent_pou_name)
|
|
713
|
-
|
|
714
|
-
# Check if parent object supports creating properties (should implement ScriptIecLanguageMemberContainer)
|
|
715
|
-
if not hasattr(parent_pou_object, 'create_property'):
|
|
716
|
-
raise TypeError("Parent object '%s' of type %s does not support create_property." % (parent_pou_name, type(parent_pou_object).__name__))
|
|
717
|
-
|
|
718
|
-
# Default language to None (usually ST)
|
|
719
|
-
lang_guid = None
|
|
720
|
-
print("DEBUG: Calling create_property: Name='%s', Type='%s', Lang=%s" % (PROPERTY_NAME, PROPERTY_TYPE, lang_guid))
|
|
721
|
-
|
|
722
|
-
# Call the create_property method ON THE PARENT POU
|
|
723
|
-
new_property_object = parent_pou_object.create_property(
|
|
724
|
-
name=PROPERTY_NAME,
|
|
725
|
-
return_type=PROPERTY_TYPE,
|
|
726
|
-
language=lang_guid # Pass None to use default
|
|
727
|
-
)
|
|
728
|
-
|
|
729
|
-
if new_property_object:
|
|
730
|
-
new_prop_name = getattr(new_property_object, 'get_name', lambda: PROPERTY_NAME)()
|
|
731
|
-
print("DEBUG: Property object created: %s" % new_prop_name)
|
|
732
|
-
|
|
733
|
-
# --- SAVE THE PROJECT TO PERSIST THE NEW PROPERTY OBJECT ---
|
|
734
|
-
try:
|
|
735
|
-
print("DEBUG: Saving Project (after property creation)...")
|
|
736
|
-
primary_project.save()
|
|
737
|
-
print("DEBUG: Project saved successfully after property creation.")
|
|
738
|
-
except Exception as save_err:
|
|
739
|
-
print("ERROR: Failed to save Project after creating property: %s" % save_err)
|
|
740
|
-
detailed_error = traceback.format_exc()
|
|
741
|
-
error_message = "Error saving Project after creating property '%s': %s\\n%s" % (PROPERTY_NAME, save_err, detailed_error)
|
|
742
|
-
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
|
|
743
|
-
# --- END SAVING ---
|
|
744
|
-
|
|
745
|
-
print("Property Created: %s" % new_prop_name)
|
|
746
|
-
print("Parent POU: %s" % PARENT_POU_FULL_PATH)
|
|
747
|
-
print("Type: %s" % PROPERTY_TYPE)
|
|
748
|
-
print("SCRIPT_SUCCESS: Property created successfully."); sys.exit(0)
|
|
749
|
-
else:
|
|
750
|
-
error_message = "Failed to create property '%s' under '%s'. create_property returned None." % (PROPERTY_NAME, parent_pou_name)
|
|
751
|
-
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
|
|
752
|
-
|
|
753
|
-
except Exception as e:
|
|
754
|
-
detailed_error = traceback.format_exc()
|
|
755
|
-
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)
|
|
756
|
-
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
|
|
757
|
-
`;
|
|
758
|
-
const CREATE_METHOD_SCRIPT_TEMPLATE = `
|
|
759
|
-
import sys, scriptengine as script_engine, os, traceback
|
|
760
|
-
${ENSURE_PROJECT_OPEN_PYTHON_SNIPPET}
|
|
761
|
-
${FIND_OBJECT_BY_PATH_PYTHON_SNIPPET}
|
|
762
|
-
PARENT_POU_FULL_PATH = "{PARENT_POU_FULL_PATH}" # e.g., "Application/MyFB"
|
|
763
|
-
METHOD_NAME = "{METHOD_NAME}"
|
|
764
|
-
RETURN_TYPE = "{RETURN_TYPE}" # Can be empty string for no return type
|
|
765
|
-
# Optional: Language
|
|
766
|
-
# LANG_GUID_STR = "{LANG_GUID_STR}" # Example if needed
|
|
767
|
-
|
|
768
|
-
try:
|
|
769
|
-
print("DEBUG: create_method script: ParentPOU='%s', Name='%s', ReturnType='%s', Project='%s'" % (PARENT_POU_FULL_PATH, METHOD_NAME, RETURN_TYPE, PROJECT_FILE_PATH))
|
|
770
|
-
primary_project = ensure_project_open(PROJECT_FILE_PATH)
|
|
771
|
-
if not PARENT_POU_FULL_PATH: raise ValueError("Parent POU full path empty.")
|
|
772
|
-
if not METHOD_NAME: raise ValueError("Method name empty.")
|
|
773
|
-
# RETURN_TYPE can be empty
|
|
774
|
-
|
|
775
|
-
# Find the parent POU object
|
|
776
|
-
parent_pou_object = find_object_by_path_robust(primary_project, PARENT_POU_FULL_PATH, "parent POU")
|
|
777
|
-
if not parent_pou_object: raise ValueError("Parent POU object not found: %s" % PARENT_POU_FULL_PATH)
|
|
778
|
-
|
|
779
|
-
parent_pou_name = getattr(parent_pou_object, 'get_name', lambda: PARENT_POU_FULL_PATH)()
|
|
780
|
-
print("DEBUG: Found Parent POU object: %s" % parent_pou_name)
|
|
781
|
-
|
|
782
|
-
# Check if parent object supports creating methods (should implement ScriptIecLanguageMemberContainer)
|
|
783
|
-
if not hasattr(parent_pou_object, 'create_method'):
|
|
784
|
-
raise TypeError("Parent object '%s' of type %s does not support create_method." % (parent_pou_name, type(parent_pou_object).__name__))
|
|
785
|
-
|
|
786
|
-
# Default language to None (usually ST)
|
|
787
|
-
lang_guid = None
|
|
788
|
-
# Use None if RETURN_TYPE is empty string, otherwise use the string
|
|
789
|
-
actual_return_type = RETURN_TYPE if RETURN_TYPE else None
|
|
790
|
-
print("DEBUG: Calling create_method: Name='%s', ReturnType=%s, Lang=%s" % (METHOD_NAME, actual_return_type, lang_guid))
|
|
791
|
-
|
|
792
|
-
# Call the create_method method ON THE PARENT POU
|
|
793
|
-
new_method_object = parent_pou_object.create_method(
|
|
794
|
-
name=METHOD_NAME,
|
|
795
|
-
return_type=actual_return_type,
|
|
796
|
-
language=lang_guid # Pass None to use default
|
|
797
|
-
)
|
|
798
|
-
|
|
799
|
-
if new_method_object:
|
|
800
|
-
new_meth_name = getattr(new_method_object, 'get_name', lambda: METHOD_NAME)()
|
|
801
|
-
print("DEBUG: Method object created: %s" % new_meth_name)
|
|
802
|
-
|
|
803
|
-
# --- SAVE THE PROJECT TO PERSIST THE NEW METHOD OBJECT ---
|
|
804
|
-
try:
|
|
805
|
-
print("DEBUG: Saving Project (after method creation)...")
|
|
806
|
-
primary_project.save()
|
|
807
|
-
print("DEBUG: Project saved successfully after method creation.")
|
|
808
|
-
except Exception as save_err:
|
|
809
|
-
print("ERROR: Failed to save Project after creating method: %s" % save_err)
|
|
810
|
-
detailed_error = traceback.format_exc()
|
|
811
|
-
error_message = "Error saving Project after creating method '%s': %s\\n%s" % (METHOD_NAME, save_err, detailed_error)
|
|
812
|
-
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
|
|
813
|
-
# --- END SAVING ---
|
|
814
|
-
|
|
815
|
-
print("Method Created: %s" % new_meth_name)
|
|
816
|
-
print("Parent POU: %s" % PARENT_POU_FULL_PATH)
|
|
817
|
-
print("Return Type: %s" % (RETURN_TYPE if RETURN_TYPE else "(None)"))
|
|
818
|
-
print("SCRIPT_SUCCESS: Method created successfully."); sys.exit(0)
|
|
819
|
-
else:
|
|
820
|
-
error_message = "Failed to create method '%s' under '%s'. create_method returned None." % (METHOD_NAME, parent_pou_name)
|
|
821
|
-
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
|
|
822
|
-
|
|
823
|
-
except Exception as e:
|
|
824
|
-
detailed_error = traceback.format_exc()
|
|
825
|
-
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)
|
|
826
|
-
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
|
|
827
|
-
`;
|
|
828
|
-
const COMPILE_PROJECT_SCRIPT_TEMPLATE = `
|
|
829
|
-
import sys, scriptengine as script_engine, os, traceback
|
|
830
|
-
${ENSURE_PROJECT_OPEN_PYTHON_SNIPPET}
|
|
831
|
-
try:
|
|
832
|
-
print("DEBUG: compile_project script: Project='%s'" % PROJECT_FILE_PATH)
|
|
833
|
-
primary_project = ensure_project_open(PROJECT_FILE_PATH)
|
|
834
|
-
project_name = os.path.basename(PROJECT_FILE_PATH)
|
|
835
|
-
target_app = None
|
|
836
|
-
app_name = "N/A"
|
|
837
|
-
|
|
838
|
-
# Try getting active application first
|
|
839
|
-
try:
|
|
840
|
-
target_app = primary_project.active_application
|
|
841
|
-
if target_app:
|
|
842
|
-
app_name = getattr(target_app, 'get_name', lambda: "Unnamed App (Active)")()
|
|
843
|
-
print("DEBUG: Found active application: %s" % app_name)
|
|
844
|
-
except Exception as active_err:
|
|
845
|
-
print("WARN: Could not get active application: %s. Searching..." % active_err)
|
|
846
|
-
|
|
847
|
-
# If no active app, search for the first one
|
|
848
|
-
if not target_app:
|
|
849
|
-
print("DEBUG: Searching for first compilable application...")
|
|
850
|
-
apps = []
|
|
851
|
-
try:
|
|
852
|
-
# Search recursively through all project objects
|
|
853
|
-
all_children = primary_project.get_children(True)
|
|
854
|
-
for child in all_children:
|
|
855
|
-
# Check using the marker property and if build method exists
|
|
856
|
-
if hasattr(child, 'is_application') and child.is_application and hasattr(child, 'build'):
|
|
857
|
-
app_name_found = getattr(child, 'get_name', lambda: "Unnamed App")()
|
|
858
|
-
print("DEBUG: Found potential application object: %s" % app_name_found)
|
|
859
|
-
apps.append(child)
|
|
860
|
-
break # Take the first one found
|
|
861
|
-
except Exception as find_err: print("WARN: Error finding application object: %s" % find_err)
|
|
862
|
-
|
|
863
|
-
if not apps: raise RuntimeError("No compilable application found in project '%s'" % project_name)
|
|
864
|
-
target_app = apps[0]
|
|
865
|
-
app_name = getattr(target_app, 'get_name', lambda: "Unnamed App (First Found)")()
|
|
866
|
-
print("WARN: Compiling first found application: %s" % app_name)
|
|
867
|
-
|
|
868
|
-
print("DEBUG: Calling build() on app '%s'..." % app_name)
|
|
869
|
-
if not hasattr(target_app, 'build'):
|
|
870
|
-
raise TypeError("Selected object '%s' is not an application or doesn't support build()." % app_name)
|
|
871
|
-
|
|
872
|
-
# Execute the build
|
|
873
|
-
target_app.build();
|
|
874
|
-
print("DEBUG: Build command executed for application '%s'." % app_name)
|
|
875
|
-
|
|
876
|
-
# Check messages is harder without direct access to message store from script.
|
|
877
|
-
# Rely on CODESYS UI or log output for now.
|
|
878
|
-
print("Compile Initiated For Application: %s" % app_name); print("In Project: %s" % project_name)
|
|
879
|
-
print("SCRIPT_SUCCESS: Application compilation initiated."); sys.exit(0)
|
|
880
|
-
except Exception as e:
|
|
881
|
-
detailed_error = traceback.format_exc()
|
|
882
|
-
error_message = "Error initiating compilation for project %s: %s\\n%s" % (PROJECT_FILE_PATH, e, detailed_error)
|
|
883
|
-
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
|
|
884
|
-
`;
|
|
885
|
-
const GET_PROJECT_STRUCTURE_SCRIPT_TEMPLATE = `
|
|
886
|
-
import sys, scriptengine as script_engine, os, traceback
|
|
887
|
-
${ENSURE_PROJECT_OPEN_PYTHON_SNIPPET}
|
|
888
|
-
# FIND_OBJECT_BY_PATH_PYTHON_SNIPPET is NOT needed here, we start from project root.
|
|
889
|
-
|
|
890
|
-
def get_object_structure(obj, indent=0, max_depth=10): # Add max_depth
|
|
891
|
-
lines = []; indent_str = " " * indent
|
|
892
|
-
if indent > max_depth:
|
|
893
|
-
lines.append("%s- Max recursion depth reached." % indent_str)
|
|
894
|
-
return lines
|
|
895
|
-
try:
|
|
896
|
-
name = "Unnamed"; obj_type = type(obj).__name__
|
|
897
|
-
guid_str = ""
|
|
898
|
-
folder_str = ""
|
|
899
|
-
try:
|
|
900
|
-
name = getattr(obj, 'get_name', lambda: "Unnamed")() or "Unnamed" # Safer get_name
|
|
901
|
-
if hasattr(obj, 'guid'): guid_str = " {%s}" % obj.guid
|
|
902
|
-
if hasattr(obj, 'is_folder') and obj.is_folder: folder_str = " [Folder]"
|
|
903
|
-
except Exception as name_err:
|
|
904
|
-
print("WARN: Error getting name/guid/folder status for an object: %s" % name_err)
|
|
905
|
-
name = "!!! Error Getting Name !!!"
|
|
906
|
-
|
|
907
|
-
lines.append("%s- %s (%s)%s%s" % (indent_str, name, obj_type, folder_str, guid_str))
|
|
908
|
-
|
|
909
|
-
# Get children only if the object potentially has them
|
|
910
|
-
children = []
|
|
911
|
-
can_have_children = hasattr(obj, 'get_children') and (
|
|
912
|
-
not hasattr(obj, 'is_folder') or # If it's not clear if it's a folder (e.g., project root)
|
|
913
|
-
(hasattr(obj, 'is_folder') and obj.is_folder) or # If it is a folder
|
|
914
|
-
# Add known container types explicitly, check marker interfaces too
|
|
915
|
-
hasattr(obj, 'is_project') or hasattr(obj, 'is_application') or hasattr(obj, 'is_device') or hasattr(obj,'is_pou')
|
|
916
|
-
)
|
|
917
|
-
|
|
918
|
-
if can_have_children:
|
|
919
|
-
try:
|
|
920
|
-
children = obj.get_children(False)
|
|
921
|
-
# print("DEBUG: %s has %d children" % (name, len(children))) # Verbose
|
|
922
|
-
except Exception as get_child_err:
|
|
923
|
-
lines.append("%s ERROR getting children: %s" % (indent_str, get_child_err))
|
|
924
|
-
# traceback.print_exc() # Optional
|
|
925
|
-
|
|
926
|
-
for child in children:
|
|
927
|
-
lines.extend(get_object_structure(child, indent + 1, max_depth)) # Recurse
|
|
928
|
-
|
|
929
|
-
except Exception as e:
|
|
930
|
-
lines.append("%s- Error processing node: %s" % (indent_str, e))
|
|
931
|
-
traceback.print_exc() # Print detailed error for this node
|
|
932
|
-
return lines
|
|
933
|
-
try:
|
|
934
|
-
print("DEBUG: Getting structure for: %s" % PROJECT_FILE_PATH)
|
|
935
|
-
primary_project = ensure_project_open(PROJECT_FILE_PATH)
|
|
936
|
-
project_name = os.path.basename(PROJECT_FILE_PATH)
|
|
937
|
-
print("DEBUG: Getting structure for project: %s" % project_name)
|
|
938
|
-
# Use the project object obtained from ensure_project_open
|
|
939
|
-
structure_list = get_object_structure(primary_project, max_depth=15) # Set a reasonable depth
|
|
940
|
-
structure_output = "\\n".join(structure_list)
|
|
941
|
-
# Ensure markers are printed distinctly
|
|
942
|
-
print("\\n--- PROJECT STRUCTURE START ---")
|
|
943
|
-
print(structure_output)
|
|
944
|
-
print("--- PROJECT STRUCTURE END ---\\n")
|
|
945
|
-
print("SCRIPT_SUCCESS: Project structure retrieved."); sys.exit(0)
|
|
946
|
-
except Exception as e:
|
|
947
|
-
detailed_error = traceback.format_exc()
|
|
948
|
-
error_message = "Error getting structure for %s: %s\\n%s" % (PROJECT_FILE_PATH, e, detailed_error)
|
|
949
|
-
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
|
|
950
|
-
`;
|
|
951
|
-
const GET_POU_CODE_SCRIPT_TEMPLATE = `
|
|
952
|
-
import sys, scriptengine as script_engine, os, traceback
|
|
953
|
-
${ENSURE_PROJECT_OPEN_PYTHON_SNIPPET}
|
|
954
|
-
${FIND_OBJECT_BY_PATH_PYTHON_SNIPPET}
|
|
955
|
-
POU_FULL_PATH = "{POU_FULL_PATH}"; CODE_START_MARKER = "### POU CODE START ###"; CODE_END_MARKER = "### POU CODE END ###"
|
|
956
|
-
DECL_START_MARKER = "### POU DECLARATION START ###"; DECL_END_MARKER = "### POU DECLARATION END ###"
|
|
957
|
-
IMPL_START_MARKER = "### POU IMPLEMENTATION START ###"; IMPL_END_MARKER = "### POU IMPLEMENTATION END ###"
|
|
958
|
-
|
|
959
|
-
try:
|
|
960
|
-
print("DEBUG: Getting code: POU_FULL_PATH='%s', Project='%s'" % (POU_FULL_PATH, PROJECT_FILE_PATH))
|
|
961
|
-
primary_project = ensure_project_open(PROJECT_FILE_PATH)
|
|
962
|
-
if not POU_FULL_PATH: raise ValueError("POU full path empty.")
|
|
963
|
-
|
|
964
|
-
# Find the target POU/Method/Property object
|
|
965
|
-
target_object = find_object_by_path_robust(primary_project, POU_FULL_PATH, "target object")
|
|
966
|
-
if not target_object: raise ValueError("Target object not found using path: %s" % POU_FULL_PATH)
|
|
967
|
-
|
|
968
|
-
target_name = getattr(target_object, 'get_name', lambda: POU_FULL_PATH)()
|
|
969
|
-
print("DEBUG: Found target object: %s" % target_name)
|
|
970
|
-
|
|
971
|
-
declaration_code = ""; implementation_code = ""
|
|
972
|
-
|
|
973
|
-
# --- Get Declaration Part ---
|
|
974
|
-
if hasattr(target_object, 'textual_declaration'):
|
|
975
|
-
decl_obj = target_object.textual_declaration
|
|
976
|
-
if decl_obj and hasattr(decl_obj, 'text'):
|
|
977
|
-
try:
|
|
978
|
-
declaration_code = decl_obj.text
|
|
979
|
-
print("DEBUG: Got declaration text.")
|
|
980
|
-
except Exception as decl_read_err:
|
|
981
|
-
print("ERROR: Failed to read declaration text: %s" % decl_read_err)
|
|
982
|
-
declaration_code = "/* ERROR reading declaration: %s */" % decl_read_err
|
|
983
|
-
else:
|
|
984
|
-
print("WARN: textual_declaration exists but is None or has no 'text' attribute.")
|
|
985
|
-
else:
|
|
986
|
-
print("WARN: No textual_declaration attribute.")
|
|
987
|
-
|
|
988
|
-
# --- Get Implementation Part ---
|
|
989
|
-
if hasattr(target_object, 'textual_implementation'):
|
|
990
|
-
impl_obj = target_object.textual_implementation
|
|
991
|
-
if impl_obj and hasattr(impl_obj, 'text'):
|
|
992
|
-
try:
|
|
993
|
-
implementation_code = impl_obj.text
|
|
994
|
-
print("DEBUG: Got implementation text.")
|
|
995
|
-
except Exception as impl_read_err:
|
|
996
|
-
print("ERROR: Failed to read implementation text: %s" % impl_read_err)
|
|
997
|
-
implementation_code = "/* ERROR reading implementation: %s */" % impl_read_err
|
|
998
|
-
else:
|
|
999
|
-
print("WARN: textual_implementation exists but is None or has no 'text' attribute.")
|
|
1000
|
-
else:
|
|
1001
|
-
print("WARN: No textual_implementation attribute.")
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
print("Code retrieved for: %s" % target_name)
|
|
1005
|
-
# Print declaration between markers, ensuring markers are on separate lines
|
|
1006
|
-
print("\\n" + DECL_START_MARKER)
|
|
1007
|
-
print(declaration_code)
|
|
1008
|
-
print(DECL_END_MARKER + "\\n")
|
|
1009
|
-
# Print implementation between markers
|
|
1010
|
-
print(IMPL_START_MARKER)
|
|
1011
|
-
print(implementation_code)
|
|
1012
|
-
print(IMPL_END_MARKER + "\\n")
|
|
1013
|
-
|
|
1014
|
-
# --- LEGACY MARKERS for backward compatibility if needed ---
|
|
1015
|
-
# Combine both for old marker format, adding a separator line
|
|
1016
|
-
# legacy_combined_code = declaration_code + "\\n\\n// Implementation\\n" + implementation_code
|
|
1017
|
-
# print(CODE_START_MARKER); print(legacy_combined_code); print(CODE_END_MARKER)
|
|
1018
|
-
# --- END LEGACY ---
|
|
1019
|
-
|
|
1020
|
-
print("SCRIPT_SUCCESS: Code retrieved."); sys.exit(0)
|
|
1021
|
-
except Exception as e:
|
|
1022
|
-
detailed_error = traceback.format_exc()
|
|
1023
|
-
error_message = "Error getting code for object '%s' in project '%s': %s\\n%s" % (POU_FULL_PATH, PROJECT_FILE_PATH, e, detailed_error)
|
|
1024
|
-
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
|
|
1025
|
-
`;
|
|
1026
|
-
// --- End Python Script Templates ---
|
|
1027
|
-
// --- Zod Schemas (moved for clarity before usage) ---
|
|
340
|
+
// --- Zod Schemas ---
|
|
1028
341
|
const PouTypeEnum = zod_1.z.enum(["Program", "FunctionBlock", "Function"]);
|
|
1029
342
|
const ImplementationLanguageEnum = zod_1.z.enum(["ST", "LD", "FBD", "SFC", "IL", "CFC", "StructuredText", "LadderDiagram", "FunctionBlockDiagram", "SequentialFunctionChart", "InstructionList", "ContinuousFunctionChart"]);
|
|
1030
343
|
// --- End Zod Schemas ---
|
|
@@ -1035,171 +348,162 @@ except Exception as e:
|
|
|
1035
348
|
var _a, _b, _c, _d, _e;
|
|
1036
349
|
console.error(`SERVER.TS Resource request: ${uri.href}`);
|
|
1037
350
|
try {
|
|
1038
|
-
|
|
1039
|
-
const
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
351
|
+
// Replace executeCodesysScript with sendCodesysCommand
|
|
352
|
+
const response = yield sendCodesysCommand({
|
|
353
|
+
command: "execute_script",
|
|
354
|
+
args: { script_name: "check_status.py" } // Assuming listener script has a generic script execution command
|
|
355
|
+
});
|
|
356
|
+
if (response.status === "success") {
|
|
357
|
+
const result = response.result; // Assuming the listener returns the script output in 'result'
|
|
358
|
+
const outputLines = result.output.split(/[\r\n]+/).filter(line => line.trim());
|
|
359
|
+
const statusData = {};
|
|
360
|
+
outputLines.forEach(line => {
|
|
361
|
+
const match = line.match(/^([^:]+):\s*(.*)$/);
|
|
362
|
+
if (match) {
|
|
363
|
+
statusData[match[1].trim()] = match[2].trim();
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
const statusText = `CODESYS Status:\n - Scripting OK: ${(_a = statusData['Scripting OK']) !== null && _a !== void 0 ? _a : 'Unknown'}\n - Project Open: ${(_b = statusData['Project Open']) !== null && _b !== void 0 ? _b : 'Unknown'}\n - Project Name: ${(_c = statusData['Project Name']) !== null && _c !== void 0 ? _c : 'Unknown'}\n - Project Path: ${(_d = statusData['Project Path']) !== null && _d !== void 0 ? _d : 'N/A'}`;
|
|
367
|
+
const isError = ((_e = statusData['Scripting OK']) === null || _e === void 0 ? void 0 : _e.toLowerCase()) !== 'true'; // Check only scripting status for error
|
|
368
|
+
return { contents: [{ uri: uri.href, text: statusText, contentType: "text/plain" }], isError: isError };
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
// Handle error response from listener
|
|
372
|
+
return { contents: [{ uri: uri.href, text: `Listener Error: ${response.message}`, contentType: "text/plain" }], isError: true };
|
|
373
|
+
}
|
|
1048
374
|
}
|
|
1049
375
|
catch (error) {
|
|
1050
376
|
console.error(`Error resource ${uri.href}:`, error);
|
|
1051
|
-
|
|
1052
|
-
return { contents: [{ uri: uri.href, text: `Failed status script: ${error.message}`, contentType: "text/plain" }], isError: true };
|
|
377
|
+
return { contents: [{ uri: uri.href, text: `Failed to communicate with listener: ${error.message}`, contentType: "text/plain" }], isError: true };
|
|
1053
378
|
}
|
|
1054
379
|
}));
|
|
1055
|
-
//
|
|
380
|
+
// Templates
|
|
1056
381
|
const projectStructureTemplate = new mcp_js_1.ResourceTemplate("codesys://project/{+project_path}/structure", { list: undefined });
|
|
1057
382
|
const pouCodeTemplate = new mcp_js_1.ResourceTemplate("codesys://project/{+project_path}/pou/{+pou_path}/code", { list: undefined });
|
|
1058
|
-
// *** END DEFINE TEMPLATES ***
|
|
1059
383
|
server.resource("project-structure", projectStructureTemplate, (uri, params) => __awaiter(this, void 0, void 0, function* () {
|
|
1060
|
-
// *** DEFINE VARIABLES (like projectPath) ***
|
|
1061
384
|
const projectPathParam = params.project_path;
|
|
1062
385
|
if (typeof projectPathParam !== 'string') {
|
|
1063
|
-
return {
|
|
386
|
+
return {
|
|
387
|
+
contents: [{ uri: uri.href, text: `Error: Invalid project path type (${typeof projectPathParam}).`, contentType: "text/plain" }],
|
|
388
|
+
isError: true
|
|
389
|
+
};
|
|
1064
390
|
}
|
|
1065
|
-
const projectPath = projectPathParam;
|
|
391
|
+
const projectPath = projectPathParam;
|
|
1066
392
|
if (!projectPath) {
|
|
1067
|
-
return {
|
|
393
|
+
return {
|
|
394
|
+
contents: [{ uri: uri.href, text: "Error: Project path missing.", contentType: "text/plain" }],
|
|
395
|
+
isError: true
|
|
396
|
+
};
|
|
1068
397
|
}
|
|
1069
|
-
// *** END DEFINE VARIABLES ***
|
|
1070
398
|
console.error(`Resource request: project structure for ${projectPath}`);
|
|
1071
399
|
try {
|
|
1072
400
|
const absoluteProjPath = path.normalize(path.isAbsolute(projectPath) ? projectPath : path.join(WORKSPACE_DIR, projectPath));
|
|
1073
|
-
|
|
1074
|
-
//
|
|
1075
|
-
const
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
const endMarker = "--- PROJECT STRUCTURE END ---";
|
|
1083
|
-
const startIndex = result.output.indexOf(startMarker);
|
|
1084
|
-
const endIndex = result.output.indexOf(endMarker);
|
|
1085
|
-
if (startIndex !== -1 && endIndex !== -1 && startIndex < endIndex) {
|
|
1086
|
-
structureText = result.output.substring(startIndex + startMarker.length, endIndex).replace(/\\n/g, '\n').trim();
|
|
1087
|
-
}
|
|
1088
|
-
else {
|
|
1089
|
-
console.error("Error: Could not find structure markers in script output.");
|
|
1090
|
-
structureText = `Could not parse structure markers in output for ${absoluteProjPath}.\n\nOutput:\n${result.output}`;
|
|
1091
|
-
isError = true;
|
|
1092
|
-
}
|
|
401
|
+
// Replace executeCodesysScript with sendCodesysCommand
|
|
402
|
+
// The listener script will handle opening the project and getting the structure
|
|
403
|
+
const response = yield sendCodesysCommand({
|
|
404
|
+
command: "get_project_structure", // New command for listener
|
|
405
|
+
args: { project_path: absoluteProjPath }
|
|
406
|
+
});
|
|
407
|
+
if (response.status === "success") {
|
|
408
|
+
const structureText = response.structure; // Assuming listener returns structure in 'structure' field
|
|
409
|
+
return { contents: [{ uri: uri.href, text: structureText, contentType: "text/plain" }], isError: false };
|
|
1093
410
|
}
|
|
1094
411
|
else {
|
|
1095
|
-
isError
|
|
412
|
+
return { contents: [{ uri: uri.href, text: `Listener Error: ${response.message}`, contentType: "text/plain" }], isError: true };
|
|
1096
413
|
}
|
|
1097
|
-
// **** RETURN MCP STRUCTURE ****
|
|
1098
|
-
return { contents: [{ uri: uri.href, text: structureText, contentType: "text/plain" }], isError: isError };
|
|
1099
414
|
}
|
|
1100
415
|
catch (error) {
|
|
1101
416
|
console.error(`Error getting structure ${uri.href}:`, error);
|
|
1102
|
-
|
|
1103
|
-
return { contents: [{ uri: uri.href, text: `Failed structure script for '${projectPath}': ${error.message}`, contentType: "text/plain" }], isError: true };
|
|
417
|
+
return { contents: [{ uri: uri.href, text: `Failed to communicate with listener for '${projectPath}': ${error.message}`, contentType: "text/plain" }], isError: true };
|
|
1104
418
|
}
|
|
1105
419
|
}));
|
|
1106
420
|
server.resource("pou-code", pouCodeTemplate, (uri, params) => __awaiter(this, void 0, void 0, function* () {
|
|
1107
|
-
// *** DEFINE VARIABLES (like projectPath, pouPath) ***
|
|
1108
421
|
const projectPathParam = params.project_path;
|
|
1109
422
|
const pouPathParam = params.pou_path;
|
|
1110
423
|
if (typeof projectPathParam !== 'string' || typeof pouPathParam !== 'string') {
|
|
1111
|
-
return {
|
|
424
|
+
return {
|
|
425
|
+
contents: [{ uri: uri.href, text: "Error: Invalid project or POU path type.", contentType: "text/plain" }],
|
|
426
|
+
isError: true
|
|
427
|
+
};
|
|
1112
428
|
}
|
|
1113
|
-
const projectPath = projectPathParam;
|
|
1114
|
-
const pouPath = pouPathParam;
|
|
429
|
+
const projectPath = projectPathParam;
|
|
430
|
+
const pouPath = pouPathParam;
|
|
1115
431
|
if (!projectPath || !pouPath) {
|
|
1116
|
-
return {
|
|
432
|
+
return {
|
|
433
|
+
contents: [{ uri: uri.href, text: "Error: Project or POU path missing.", contentType: "text/plain" }],
|
|
434
|
+
isError: true
|
|
435
|
+
};
|
|
1117
436
|
}
|
|
1118
|
-
// *** END DEFINE VARIABLES ***
|
|
1119
437
|
console.error(`Resource request: POU code: Project='${projectPath}', POU='${pouPath}'`);
|
|
1120
438
|
try {
|
|
1121
439
|
const absoluteProjPath = path.normalize(path.isAbsolute(projectPath) ? projectPath : path.join(WORKSPACE_DIR, projectPath));
|
|
1122
440
|
const sanitizedPouPath = String(pouPath).replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
|
1123
|
-
|
|
1124
|
-
//
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
// ... (marker parsing logic using new markers) ...
|
|
1133
|
-
const declStartMarker = "### POU DECLARATION START ###";
|
|
1134
|
-
const declEndMarker = "### POU DECLARATION END ###";
|
|
1135
|
-
const implStartMarker = "### POU IMPLEMENTATION START ###";
|
|
1136
|
-
const implEndMarker = "### POU IMPLEMENTATION END ###";
|
|
1137
|
-
const declStartIdx = result.output.indexOf(declStartMarker);
|
|
1138
|
-
const declEndIdx = result.output.indexOf(declEndMarker);
|
|
1139
|
-
const implStartIdx = result.output.indexOf(implStartMarker);
|
|
1140
|
-
const implEndIdx = result.output.indexOf(implEndMarker);
|
|
1141
|
-
let declaration = "/* Declaration not found in output */";
|
|
1142
|
-
let implementation = "/* Implementation not found in output */";
|
|
1143
|
-
if (declStartIdx !== -1 && declEndIdx !== -1 && declStartIdx < declEndIdx) {
|
|
1144
|
-
declaration = result.output.substring(declStartIdx + declStartMarker.length, declEndIdx).replace(/\\n/g, '\n').trim();
|
|
1145
|
-
}
|
|
1146
|
-
else {
|
|
1147
|
-
console.error(`WARN: Declaration markers not found correctly for ${sanitizedPouPath}`);
|
|
1148
|
-
}
|
|
1149
|
-
if (implStartIdx !== -1 && implEndIdx !== -1 && implStartIdx < implEndIdx) {
|
|
1150
|
-
implementation = result.output.substring(implStartIdx + implStartMarker.length, implEndIdx).replace(/\\n/g, '\n').trim();
|
|
1151
|
-
}
|
|
1152
|
-
else {
|
|
1153
|
-
console.error(`WARN: Implementation markers not found correctly for ${sanitizedPouPath}`);
|
|
1154
|
-
}
|
|
1155
|
-
codeText = `// ----- Declaration -----\n${declaration}\n\n// ----- Implementation -----\n${implementation}`;
|
|
1156
|
-
// *** END MARKER PARSING ***
|
|
441
|
+
// Replace executeCodesysScript with sendCodesysCommand
|
|
442
|
+
// The listener script will handle opening the project and getting the POU code
|
|
443
|
+
const response = yield sendCodesysCommand({
|
|
444
|
+
command: "get_pou_code", // New command for listener
|
|
445
|
+
args: { project_path: absoluteProjPath, pou_path: sanitizedPouPath }
|
|
446
|
+
});
|
|
447
|
+
if (response.status === "success") {
|
|
448
|
+
const codeText = response.code; // Assuming listener returns code in 'code' field
|
|
449
|
+
return { contents: [{ uri: uri.href, text: codeText, contentType: "text/plain" }], isError: false };
|
|
1157
450
|
}
|
|
1158
451
|
else {
|
|
1159
|
-
isError
|
|
452
|
+
return { contents: [{ uri: uri.href, text: `Listener Error: ${response.message}`, contentType: "text/plain" }], isError: true };
|
|
1160
453
|
}
|
|
1161
|
-
// **** RETURN MCP STRUCTURE ****
|
|
1162
|
-
return { contents: [{ uri: uri.href, text: codeText, contentType: "text/plain" }], isError: isError };
|
|
1163
454
|
}
|
|
1164
455
|
catch (error) {
|
|
1165
456
|
console.error(`Error getting POU code ${uri.href}:`, error);
|
|
1166
|
-
|
|
1167
|
-
return { contents: [{ uri: uri.href, text: `Failed POU code script for '${pouPath}' in '${projectPath}': ${error.message}`, contentType: "text/plain" }], isError: true };
|
|
457
|
+
return { contents: [{ uri: uri.href, text: `Failed to communicate with listener for '${pouPath}' in '${projectPath}': ${error.message}`, contentType: "text/plain" }], isError: true };
|
|
1168
458
|
}
|
|
1169
459
|
}));
|
|
1170
460
|
// --- End Resources ---
|
|
1171
461
|
// --- Tools ---
|
|
1172
|
-
server.tool("open_project",
|
|
1173
|
-
"Opens an existing CODESYS project file.", // Tool Description
|
|
1174
|
-
{
|
|
462
|
+
server.tool("open_project", "Opens an existing CODESYS project file.", {
|
|
1175
463
|
filePath: zod_1.z.string().describe("Path to the project file (e.g., 'C:/Projects/MyPLC.project' or '/Users/user/projects/my_project.project').")
|
|
1176
464
|
}, (args) => __awaiter(this, void 0, void 0, function* () {
|
|
1177
465
|
const { filePath } = args;
|
|
1178
466
|
let absPath = path.normalize(path.isAbsolute(filePath) ? filePath : path.join(WORKSPACE_DIR, filePath));
|
|
1179
467
|
console.error(`Tool call: open_project: ${absPath}`);
|
|
1180
468
|
try {
|
|
1181
|
-
|
|
1182
|
-
const
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
469
|
+
// Replace executeCodesysScript with sendCodesysCommand
|
|
470
|
+
const response = yield sendCodesysCommand({
|
|
471
|
+
command: "open_project", // Command matches tool name
|
|
472
|
+
args: { file_path: absPath }
|
|
473
|
+
});
|
|
474
|
+
if (response.status === "success") {
|
|
475
|
+
return {
|
|
476
|
+
content: [{
|
|
477
|
+
type: "text",
|
|
478
|
+
text: `Project opened: ${absPath}`
|
|
479
|
+
}],
|
|
480
|
+
isError: false
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
return {
|
|
485
|
+
content: [{ type: "text", text: `Listener Error: ${response.message}` }],
|
|
486
|
+
isError: true
|
|
487
|
+
};
|
|
488
|
+
}
|
|
1186
489
|
}
|
|
1187
490
|
catch (e) {
|
|
1188
491
|
console.error(`Error open_project ${absPath}: ${e}`);
|
|
1189
|
-
return {
|
|
492
|
+
return {
|
|
493
|
+
content: [{ type: "text", text: `Failed to communicate with listener: ${e.message}` }],
|
|
494
|
+
isError: true
|
|
495
|
+
};
|
|
1190
496
|
}
|
|
1191
497
|
}));
|
|
1192
|
-
server.tool("create_project",
|
|
1193
|
-
"Creates a new CODESYS project from the standard template.", // Tool Description
|
|
1194
|
-
{
|
|
498
|
+
server.tool("create_project", "Creates a new CODESYS project from the standard template.", {
|
|
1195
499
|
filePath: zod_1.z.string().describe("Path where the new project file should be created (e.g., 'C:/Projects/NewPLC.project' or '/Users/user/projects/new_project.project').")
|
|
1196
500
|
}, (args) => __awaiter(this, void 0, void 0, function* () {
|
|
1197
501
|
const { filePath } = args;
|
|
1198
502
|
let absPath = path.normalize(path.isAbsolute(filePath) ? filePath : path.join(WORKSPACE_DIR, filePath));
|
|
1199
503
|
console.error(`Tool call: create_project (copy template): ${absPath}`);
|
|
504
|
+
// Template finding logic remains the same as it doesn't require CODESYS interaction
|
|
1200
505
|
let templatePath = "";
|
|
1201
506
|
try {
|
|
1202
|
-
// Template finding logic (same as before)
|
|
1203
507
|
const baseDir = path.dirname(path.dirname(codesysExePath));
|
|
1204
508
|
templatePath = path.normalize(path.join(baseDir, 'Templates', 'Standard.project'));
|
|
1205
509
|
if (!(yield fileExists(templatePath))) {
|
|
@@ -1226,52 +530,83 @@ except Exception as e:
|
|
|
1226
530
|
else {
|
|
1227
531
|
console.error(`DEBUG: Found template relative to exe: ${templatePath}`);
|
|
1228
532
|
}
|
|
1229
|
-
// *** END TEMPLATE FINDING ***
|
|
1230
533
|
}
|
|
1231
534
|
catch (e) {
|
|
1232
535
|
console.error(`Template Error: ${e.message}`);
|
|
1233
|
-
return {
|
|
536
|
+
return {
|
|
537
|
+
content: [{ type: "text", text: `Template Error: ${e.message}` }],
|
|
538
|
+
isError: true
|
|
539
|
+
};
|
|
1234
540
|
}
|
|
1235
541
|
try {
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
const
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
542
|
+
// Replace executeCodesysScript with sendCodesysCommand
|
|
543
|
+
// The listener script will handle creating the project from template
|
|
544
|
+
const response = yield sendCodesysCommand({
|
|
545
|
+
command: "create_project", // Command matches tool name
|
|
546
|
+
args: { file_path: absPath, template_path: templatePath }
|
|
547
|
+
});
|
|
548
|
+
if (response.status === "success") {
|
|
549
|
+
return {
|
|
550
|
+
content: [{
|
|
551
|
+
type: "text",
|
|
552
|
+
text: `Project created from template: ${absPath}`
|
|
553
|
+
}],
|
|
554
|
+
isError: false
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
return {
|
|
559
|
+
content: [{ type: "text", text: `Listener Error: ${response.message}` }],
|
|
560
|
+
isError: true
|
|
561
|
+
};
|
|
562
|
+
}
|
|
1246
563
|
}
|
|
1247
564
|
catch (e) {
|
|
1248
565
|
console.error(`Error create_project ${absPath}: ${e}`);
|
|
1249
|
-
return {
|
|
566
|
+
return {
|
|
567
|
+
content: [{ type: "text", text: `Failed to communicate with listener: ${e.message}` }],
|
|
568
|
+
isError: true
|
|
569
|
+
};
|
|
1250
570
|
}
|
|
1251
571
|
}));
|
|
1252
|
-
server.tool("save_project",
|
|
1253
|
-
"Saves the currently open CODESYS project.", // Tool Description
|
|
1254
|
-
{
|
|
572
|
+
server.tool("save_project", "Saves the currently open CODESYS project.", {
|
|
1255
573
|
projectFilePath: zod_1.z.string().describe("Path to the project file to ensure is open before saving (e.g., 'C:/Projects/MyPLC.project').")
|
|
1256
574
|
}, (args) => __awaiter(this, void 0, void 0, function* () {
|
|
1257
575
|
const { projectFilePath } = args;
|
|
1258
576
|
let absPath = path.normalize(path.isAbsolute(projectFilePath) ? projectFilePath : path.join(WORKSPACE_DIR, projectFilePath));
|
|
1259
577
|
console.error(`Tool call: save_project: ${absPath}`);
|
|
1260
578
|
try {
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
const
|
|
1264
|
-
|
|
1265
|
-
|
|
579
|
+
// Replace executeCodesysScript with sendCodesysCommand
|
|
580
|
+
// The listener script will handle ensuring the project is open and saving it
|
|
581
|
+
const response = yield sendCodesysCommand({
|
|
582
|
+
command: "save_project", // Command matches tool name
|
|
583
|
+
args: { project_file_path: absPath }
|
|
584
|
+
});
|
|
585
|
+
if (response.status === "success") {
|
|
586
|
+
return {
|
|
587
|
+
content: [{
|
|
588
|
+
type: "text",
|
|
589
|
+
text: `Project saved: ${absPath}`
|
|
590
|
+
}],
|
|
591
|
+
isError: false
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
return {
|
|
596
|
+
content: [{ type: "text", text: `Listener Error: ${response.message}` }],
|
|
597
|
+
isError: true
|
|
598
|
+
};
|
|
599
|
+
}
|
|
1266
600
|
}
|
|
1267
601
|
catch (e) {
|
|
1268
602
|
console.error(`Error save_project ${absPath}: ${e}`);
|
|
1269
|
-
return {
|
|
603
|
+
return {
|
|
604
|
+
content: [{ type: "text", text: `Failed to communicate with listener: ${e.message}` }],
|
|
605
|
+
isError: true
|
|
606
|
+
};
|
|
1270
607
|
}
|
|
1271
608
|
}));
|
|
1272
|
-
server.tool("create_pou",
|
|
1273
|
-
"Creates a new Program, Function Block, or Function POU within the specified CODESYS project.", // Tool Description
|
|
1274
|
-
{
|
|
609
|
+
server.tool("create_pou", "Creates a new Program, Function Block, or Function POU within the specified CODESYS project.", {
|
|
1275
610
|
projectFilePath: zod_1.z.string().describe("Path to the project file (e.g., 'C:/Projects/MyPLC.project')."),
|
|
1276
611
|
name: zod_1.z.string().describe("Name for the new POU (must be a valid IEC identifier)."),
|
|
1277
612
|
type: PouTypeEnum.describe("Type of POU (Program, FunctionBlock, Function)."),
|
|
@@ -1284,199 +619,261 @@ except Exception as e:
|
|
|
1284
619
|
const sanName = name.trim();
|
|
1285
620
|
console.error(`Tool call: create_pou: Name='${sanName}', Type='${type}', Lang='${language}', Parent='${sanParentPath}', Project='${absPath}'`);
|
|
1286
621
|
try {
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
622
|
+
// Replace executeCodesysScript with sendCodesysCommand
|
|
623
|
+
// The listener script will handle opening the project and creating the POU
|
|
624
|
+
const response = yield sendCodesysCommand({
|
|
625
|
+
command: "create_pou", // Command matches tool name
|
|
626
|
+
args: {
|
|
627
|
+
project_file_path: absPath,
|
|
628
|
+
name: sanName,
|
|
629
|
+
type: type,
|
|
630
|
+
language: language,
|
|
631
|
+
parent_path: sanParentPath
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
if (response.status === "success") {
|
|
635
|
+
return {
|
|
636
|
+
content: [{
|
|
637
|
+
type: "text",
|
|
638
|
+
text: `POU '${sanName}' created in '${sanParentPath}' of ${absPath}. Project saved.`
|
|
639
|
+
}],
|
|
640
|
+
isError: false
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
return {
|
|
645
|
+
content: [{ type: "text", text: `Listener Error: ${response.message}` }],
|
|
646
|
+
isError: true
|
|
647
|
+
};
|
|
648
|
+
}
|
|
1298
649
|
}
|
|
1299
650
|
catch (e) {
|
|
1300
651
|
console.error(`Error create_pou ${sanName} in ${absPath}: ${e}`);
|
|
1301
|
-
return {
|
|
652
|
+
return {
|
|
653
|
+
content: [{ type: "text", text: `Failed to communicate with listener: ${e.message}` }],
|
|
654
|
+
isError: true
|
|
655
|
+
};
|
|
1302
656
|
}
|
|
1303
657
|
}));
|
|
1304
|
-
server.tool("set_pou_code",
|
|
1305
|
-
"Sets the declaration and/or implementation code for a specific POU, Method, or Property.", // Tool Description
|
|
1306
|
-
{
|
|
658
|
+
server.tool("set_pou_code", "Sets the declaration and/or implementation code for a specific POU, Method, or Property.", {
|
|
1307
659
|
projectFilePath: zod_1.z.string().describe("Path to the project file (e.g., 'C:/Projects/MyPLC.project')."),
|
|
1308
|
-
pouPath: zod_1.z.string().describe("
|
|
1309
|
-
|
|
1310
|
-
|
|
660
|
+
pouPath: zod_1.z.string().describe("Relative path to the POU, Method, or Property (e.g., 'Application.PLC_PRG' or 'Application.MyFB.MyMethod')."),
|
|
661
|
+
declaration: zod_1.z.string().optional().describe("The new declaration code for the POU."),
|
|
662
|
+
implementation: zod_1.z.string().optional().describe("The new implementation code for the POU.")
|
|
1311
663
|
}, (args) => __awaiter(this, void 0, void 0, function* () {
|
|
1312
|
-
const { projectFilePath, pouPath,
|
|
1313
|
-
if (declarationCode === undefined && implementationCode === undefined) {
|
|
1314
|
-
return { content: [{ type: "text", text: "Error: At least one of declarationCode or implementationCode must be provided." }], isError: true };
|
|
1315
|
-
}
|
|
664
|
+
const { projectFilePath, pouPath, declaration, implementation } = args;
|
|
1316
665
|
let absPath = path.normalize(path.isAbsolute(projectFilePath) ? projectFilePath : path.join(WORKSPACE_DIR, projectFilePath));
|
|
1317
666
|
const sanPouPath = pouPath.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
|
1318
|
-
console.error(`Tool call: set_pou_code:
|
|
667
|
+
console.error(`Tool call: set_pou_code: POU='${sanPouPath}', Project='${absPath}'`);
|
|
668
|
+
if (declaration === undefined && implementation === undefined) {
|
|
669
|
+
return {
|
|
670
|
+
content: [{ type: "text", text: "Error: Either 'declaration' or 'implementation' must be provided." }],
|
|
671
|
+
isError: true
|
|
672
|
+
};
|
|
673
|
+
}
|
|
1319
674
|
try {
|
|
1320
|
-
|
|
1321
|
-
//
|
|
1322
|
-
const
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
675
|
+
// Replace executeCodesysScript with sendCodesysCommand
|
|
676
|
+
// The listener script will handle opening the project and setting the POU code
|
|
677
|
+
const response = yield sendCodesysCommand({
|
|
678
|
+
command: "set_pou_code", // Command matches tool name
|
|
679
|
+
args: {
|
|
680
|
+
project_file_path: absPath,
|
|
681
|
+
pou_path: sanPouPath,
|
|
682
|
+
declaration: declaration,
|
|
683
|
+
implementation: implementation
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
if (response.status === "success") {
|
|
687
|
+
return {
|
|
688
|
+
content: [{
|
|
689
|
+
type: "text",
|
|
690
|
+
text: `Code set for POU '${sanPouPath}' in ${absPath}. Project saved.`
|
|
691
|
+
}],
|
|
692
|
+
isError: false
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
return {
|
|
697
|
+
content: [{ type: "text", text: `Listener Error: ${response.message}` }],
|
|
698
|
+
isError: true
|
|
699
|
+
};
|
|
700
|
+
}
|
|
1333
701
|
}
|
|
1334
702
|
catch (e) {
|
|
1335
703
|
console.error(`Error set_pou_code ${sanPouPath} in ${absPath}: ${e}`);
|
|
1336
|
-
return {
|
|
704
|
+
return {
|
|
705
|
+
content: [{ type: "text", text: `Failed to communicate with listener: ${e.message}` }],
|
|
706
|
+
isError: true
|
|
707
|
+
};
|
|
1337
708
|
}
|
|
1338
709
|
}));
|
|
1339
|
-
server.tool("
|
|
1340
|
-
"Creates a new Property within a specific Function Block POU.", // Tool Description
|
|
1341
|
-
{
|
|
710
|
+
server.tool("create_method", "Creates a new Method within a Function Block.", {
|
|
1342
711
|
projectFilePath: zod_1.z.string().describe("Path to the project file (e.g., 'C:/Projects/MyPLC.project')."),
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
propertyType: zod_1.z.string().describe("Data type of the property (e.g., 'BOOL', 'INT', 'MyDUT').")
|
|
712
|
+
functionBlockPath: zod_1.z.string().describe("Relative path to the parent Function Block (e.g., 'Application.MyFB')."),
|
|
713
|
+
name: zod_1.z.string().describe("Name for the new Method (must be a valid IEC identifier)."),
|
|
714
|
+
language: ImplementationLanguageEnum.describe("Implementation language (ST, LD, FBD, etc.). CODESYS default will be used if specific language is not set."),
|
|
1347
715
|
}, (args) => __awaiter(this, void 0, void 0, function* () {
|
|
1348
|
-
const { projectFilePath,
|
|
716
|
+
const { projectFilePath, functionBlockPath, name, language } = args;
|
|
1349
717
|
let absPath = path.normalize(path.isAbsolute(projectFilePath) ? projectFilePath : path.join(WORKSPACE_DIR, projectFilePath));
|
|
1350
|
-
const
|
|
1351
|
-
const
|
|
1352
|
-
|
|
1353
|
-
console.error(`Tool call: create_property: Name='${sanPropName}', Type='${sanPropType}', ParentPOU='${sanParentPath}', Project='${absPath}'`);
|
|
1354
|
-
if (!sanPropName || !sanPropType) {
|
|
1355
|
-
return { content: [{ type: "text", text: `Error: Property name and type cannot be empty.` }], isError: true };
|
|
1356
|
-
}
|
|
718
|
+
const sanFBPath = functionBlockPath.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
|
719
|
+
const sanName = name.trim();
|
|
720
|
+
console.error(`Tool call: create_method: Name='${sanName}', Lang='${language}', Parent='${sanFBPath}', Project='${absPath}'`);
|
|
1357
721
|
try {
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
722
|
+
// Replace executeCodesysScript with sendCodesysCommand
|
|
723
|
+
const response = yield sendCodesysCommand({
|
|
724
|
+
command: "create_method", // Command matches tool name
|
|
725
|
+
args: {
|
|
726
|
+
project_file_path: absPath,
|
|
727
|
+
function_block_path: sanFBPath,
|
|
728
|
+
name: sanName,
|
|
729
|
+
language: language
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
if (response.status === "success") {
|
|
733
|
+
return {
|
|
734
|
+
content: [{
|
|
735
|
+
type: "text",
|
|
736
|
+
text: `Method '${sanName}' created in '${sanFBPath}' of ${absPath}. Project saved.`
|
|
737
|
+
}],
|
|
738
|
+
isError: false
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
return {
|
|
743
|
+
content: [{ type: "text", text: `Listener Error: ${response.message}` }],
|
|
744
|
+
isError: true
|
|
745
|
+
};
|
|
746
|
+
}
|
|
1371
747
|
}
|
|
1372
748
|
catch (e) {
|
|
1373
|
-
console.error(`Error
|
|
1374
|
-
return {
|
|
749
|
+
console.error(`Error create_method ${sanName} in ${absPath}: ${e}`);
|
|
750
|
+
return {
|
|
751
|
+
content: [{ type: "text", text: `Failed to communicate with listener: ${e.message}` }],
|
|
752
|
+
isError: true
|
|
753
|
+
};
|
|
1375
754
|
}
|
|
1376
755
|
}));
|
|
1377
|
-
server.tool("
|
|
1378
|
-
"Creates a new Method within a specific Function Block POU.", // Tool Description
|
|
1379
|
-
{
|
|
756
|
+
server.tool("create_property", "Creates a new Property within a Function Block.", {
|
|
1380
757
|
projectFilePath: zod_1.z.string().describe("Path to the project file (e.g., 'C:/Projects/MyPLC.project')."),
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
758
|
+
functionBlockPath: zod_1.z.string().describe("Relative path to the parent Function Block (e.g., 'Application.MyFB')."),
|
|
759
|
+
name: zod_1.z.string().describe("Name for the new Property (must be a valid IEC identifier)."),
|
|
760
|
+
dataType: zod_1.z.string().describe("Data type of the Property (e.g., 'INT', 'BOOL')."),
|
|
761
|
+
isRetain: zod_1.z.boolean().optional().default(false).describe("Whether the Property should be RETAIN."),
|
|
762
|
+
isPersistent: zod_1.z.boolean().optional().default(false).describe("Whether the Property should be PERSISTENT."),
|
|
763
|
+
isConstant: zod_1.z.boolean().optional().default(false).describe("Whether the Property should be CONSTANT."),
|
|
764
|
+
initialValue: zod_1.z.string().optional().describe("Initial value for the Property."),
|
|
765
|
+
comment: zod_1.z.string().optional().describe("Comment for the Property.")
|
|
1385
766
|
}, (args) => __awaiter(this, void 0, void 0, function* () {
|
|
1386
|
-
const { projectFilePath,
|
|
767
|
+
const { projectFilePath, functionBlockPath, name, dataType, isRetain, isPersistent, isConstant, initialValue, comment } = args;
|
|
1387
768
|
let absPath = path.normalize(path.isAbsolute(projectFilePath) ? projectFilePath : path.join(WORKSPACE_DIR, projectFilePath));
|
|
1388
|
-
const
|
|
1389
|
-
const
|
|
1390
|
-
|
|
1391
|
-
console.error(`Tool call: create_method: Name='${sanMethName}', Return='${sanReturnType}', ParentPOU='${sanParentPath}', Project='${absPath}'`);
|
|
1392
|
-
if (!sanMethName) {
|
|
1393
|
-
return { content: [{ type: "text", text: `Error: Method name cannot be empty.` }], isError: true };
|
|
1394
|
-
}
|
|
769
|
+
const sanFBPath = functionBlockPath.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
|
770
|
+
const sanName = name.trim();
|
|
771
|
+
console.error(`Tool call: create_property: Name='${sanName}', Type='${dataType}', Parent='${sanFBPath}', Project='${absPath}'`);
|
|
1395
772
|
try {
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
773
|
+
// Replace executeCodesysScript with sendCodesysCommand
|
|
774
|
+
const response = yield sendCodesysCommand({
|
|
775
|
+
command: "create_property", // Command matches tool name
|
|
776
|
+
args: {
|
|
777
|
+
project_file_path: absPath,
|
|
778
|
+
function_block_path: sanFBPath,
|
|
779
|
+
name: sanName,
|
|
780
|
+
data_type: dataType,
|
|
781
|
+
is_retain: isRetain,
|
|
782
|
+
is_persistent: isPersistent,
|
|
783
|
+
is_constant: isConstant,
|
|
784
|
+
initial_value: initialValue,
|
|
785
|
+
comment: comment
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
if (response.status === "success") {
|
|
789
|
+
return {
|
|
790
|
+
content: [{
|
|
791
|
+
type: "text",
|
|
792
|
+
text: `Property '${sanName}' created in '${sanFBPath}' of ${absPath}. Project saved.`
|
|
793
|
+
}],
|
|
794
|
+
isError: false
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
else {
|
|
798
|
+
return {
|
|
799
|
+
content: [{ type: "text", text: `Listener Error: ${response.message}` }],
|
|
800
|
+
isError: true
|
|
801
|
+
};
|
|
802
|
+
}
|
|
1409
803
|
}
|
|
1410
804
|
catch (e) {
|
|
1411
|
-
console.error(`Error
|
|
1412
|
-
return {
|
|
805
|
+
console.error(`Error create_property ${sanName} in ${absPath}: ${e}`);
|
|
806
|
+
return {
|
|
807
|
+
content: [{ type: "text", text: `Failed to communicate with listener: ${e.message}` }],
|
|
808
|
+
isError: true
|
|
809
|
+
};
|
|
1413
810
|
}
|
|
1414
811
|
}));
|
|
1415
|
-
server.tool("compile_project",
|
|
1416
|
-
|
|
1417
|
-
{
|
|
1418
|
-
projectFilePath: zod_1.z.string().describe("Path to the project file containing the application to compile (e.g., 'C:/Projects/MyPLC.project').")
|
|
812
|
+
server.tool("compile_project", "Compiles the specified CODESYS project.", {
|
|
813
|
+
projectFilePath: zod_1.z.string().describe("Path to the project file (e.g., 'C:/Projects/MyPLC.project').")
|
|
1419
814
|
}, (args) => __awaiter(this, void 0, void 0, function* () {
|
|
1420
815
|
const { projectFilePath } = args;
|
|
1421
816
|
let absPath = path.normalize(path.isAbsolute(projectFilePath) ? projectFilePath : path.join(WORKSPACE_DIR, projectFilePath));
|
|
1422
|
-
console.error(`Tool call: compile_project: ${absPath}`);
|
|
817
|
+
console.error(`Tool call: compile_project: Project='${absPath}'`);
|
|
1423
818
|
try {
|
|
1424
|
-
|
|
1425
|
-
const
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
819
|
+
// Replace executeCodesysScript with sendCodesysCommand
|
|
820
|
+
const response = yield sendCodesysCommand({
|
|
821
|
+
command: "compile_project", // Command matches tool name
|
|
822
|
+
args: { project_file_path: absPath }
|
|
823
|
+
});
|
|
824
|
+
if (response.status === "success") {
|
|
825
|
+
return {
|
|
826
|
+
content: [{
|
|
827
|
+
type: "text",
|
|
828
|
+
text: `Project compiled: ${absPath}. Output:\n${response.output}` // Assuming listener returns compile output
|
|
829
|
+
}],
|
|
830
|
+
isError: false
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
return {
|
|
835
|
+
content: [{ type: "text", text: `Listener Error: ${response.message}\nOutput:\n${response.output}` }],
|
|
836
|
+
isError: true
|
|
837
|
+
};
|
|
1437
838
|
}
|
|
1438
|
-
return { content: [{ type: "text", text: message }], isError: isError };
|
|
1439
839
|
}
|
|
1440
840
|
catch (e) {
|
|
1441
841
|
console.error(`Error compile_project ${absPath}: ${e}`);
|
|
1442
|
-
return {
|
|
842
|
+
return {
|
|
843
|
+
content: [{ type: "text", text: `Failed to communicate with listener: ${e.message}` }],
|
|
844
|
+
isError: true
|
|
845
|
+
};
|
|
1443
846
|
}
|
|
1444
847
|
}));
|
|
1445
848
|
// --- End Tools ---
|
|
1446
849
|
console.error("SERVER.TS: Resources and Tools defined.");
|
|
1447
|
-
// ---
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
console.error("
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
850
|
+
// --- Start the persistent CODESYS instance and connect the socket client ---
|
|
851
|
+
launchCodesysPersistent();
|
|
852
|
+
// Give CODESYS a moment to start before attempting to connect
|
|
853
|
+
setTimeout(connectToCodesysListener, 2000); // Wait 2 seconds
|
|
854
|
+
// --- End Startup of Persistent Instance ---
|
|
855
|
+
// --- Start the MCP Server Transport ---
|
|
856
|
+
console.error("SERVER.TS: Starting StdioServerTransport...");
|
|
857
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
858
|
+
server.connect(transport);
|
|
859
|
+
console.error("SERVER.TS: StdioServerTransport started.");
|
|
860
|
+
// --- End Start the MCP Server Transport ---
|
|
861
|
+
// --- Handle process signals for graceful shutdown ---
|
|
862
|
+
process.on('SIGINT', () => {
|
|
863
|
+
console.error("SERVER.TS: Received SIGINT. Shutting down.");
|
|
864
|
+
shutdownCodesysPersistent();
|
|
865
|
+
process.exit(0);
|
|
866
|
+
});
|
|
867
|
+
process.on('SIGTERM', () => {
|
|
868
|
+
console.error("SERVER.TS: Received SIGTERM. Shutting down.");
|
|
869
|
+
shutdownCodesysPersistent();
|
|
870
|
+
process.exit(0);
|
|
871
|
+
});
|
|
872
|
+
// --- End Signal Handling ---
|
|
873
|
+
console.error(">>> SERVER.TS startMcpServer() FINISHED <<<");
|
|
1466
874
|
});
|
|
1467
|
-
}
|
|
1468
|
-
// ---
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
process.exit(0);
|
|
1473
|
-
});
|
|
1474
|
-
process.on('SIGTERM', () => {
|
|
1475
|
-
console.error('\nSERVER.TS: SIGTERM received, shutting down...');
|
|
1476
|
-
process.exit(0);
|
|
1477
|
-
});
|
|
1478
|
-
process.on('unhandledRejection', (reason, promise) => {
|
|
1479
|
-
console.error('SERVER.TS: Unhandled Rejection at:', promise, 'reason:', reason);
|
|
1480
|
-
});
|
|
1481
|
-
// --- End Graceful Shutdown / Unhandled Rejection ---
|
|
1482
|
-
console.error(">>> SERVER.TS Module Parsed <<<"); // Log end of script parsing
|
|
875
|
+
}
|
|
876
|
+
// --- Define the socket listener host and port ---
|
|
877
|
+
const HOST = '127.0.0.1';
|
|
878
|
+
const PORT = 65432;
|
|
879
|
+
// --- End Define host and port ---
|