@codesys/mcp-toolkit 1.1.4 → 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.
@@ -0,0 +1,1539 @@
1
+ import socket
2
+ import sys
3
+ import threading
4
+ import json
5
+ import time
6
+ import traceback # Import traceback for detailed error logging
7
+ import os # Import os for path manipulation
8
+ import shutil # Import shutil for file operations (used in create_project)
9
+
10
+ # Import CODESYS Scripting libraries
11
+ # These imports are conceptual as the exact names might vary.
12
+ # You might need to adjust these based on your CODESYS installation and scripting environment setup.
13
+ # Ensure the CODESYS scripting environment makes these available.
14
+ try:
15
+ # Assuming the scripting environment makes these available globally or via a specific import
16
+ # If not, you might need a different approach to access CODESYS objects
17
+ import scriptengine # Use import scriptengine as script_engine based on your scripts
18
+ from Script import * # Assuming Script object is available
19
+ from ScriptOnline import * # Assuming ScriptOnline object is available
20
+ # Import specific enums used in your scripts
21
+ from scriptengine import PouType # Used in create_pou
22
+ # from scriptengine import ImplementationLanguage # If needed for language mapping
23
+ # from scriptengine import VersionUpdateFlags # Used in open_project
24
+ print("CODESYS Scripting libraries imported.")
25
+ except ImportError:
26
+ print("Warning: CODESYS Scripting libraries not found. Running in simulation mode.")
27
+ # Define dummy classes/objects for testing outside CODESYS if needed
28
+ # (Keep the dummy classes from the previous version for simulation)
29
+ class DummyOnlineApplication:
30
+ def login(self, change_option, delete_foreign_apps):
31
+ print("Dummy login called.")
32
+ time.sleep(0.1)
33
+ return True
34
+ def logout(self):
35
+ print("Dummy logout called.")
36
+ time.sleep(0.1)
37
+ def start(self):
38
+ print("Dummy start called.")
39
+ time.sleep(0.1)
40
+ def stop(self):
41
+ print("Dummy stop called.")
42
+ time.sleep(0.1)
43
+ def read_value(self, expression):
44
+ print(f"Dummy read_value called for {expression}")
45
+ time.sleep(0.05)
46
+ return f"DummyValue_{expression}"
47
+ def write_prepared_values(self):
48
+ print("Dummy write_prepared_values called.")
49
+ time.sleep(0.1)
50
+ def set_prepared_value(self, expression, value):
51
+ print(f"Dummy set_prepared_value called for {expression} = {value}")
52
+ time.sleep(0.05)
53
+ def Dispose(self):
54
+ print("Dummy Dispose called.")
55
+
56
+ class DummyOnlineDevice:
57
+ def Dispose(self):
58
+ print("Dummy Dispose called.")
59
+
60
+ # Dummy Script object for project operations
61
+ class DummyScriptEngine:
62
+ def __init__(self):
63
+ self.projects = DummyProjects()
64
+
65
+ class DummyProjects:
66
+ def __init__(self):
67
+ self.primary = None # Simulate no project open initially
68
+
69
+ def open(self, path, update_flags=None):
70
+ print(f"Dummy scriptengine.projects.open called for {path}")
71
+ time.sleep(0.5)
72
+ # Simulate returning a project object or None
73
+ if os.path.exists(path):
74
+ print("Dummy project opened successfully.")
75
+ self.primary = DummyProject(path) # Set as primary
76
+ return self.primary
77
+ else:
78
+ print("Dummy project not found.")
79
+ self.primary = None
80
+ return None
81
+
82
+ class DummyProject:
83
+ def __init__(self, path):
84
+ self.path = path
85
+ self.active_application = DummyApplication() # Simulate an active application
86
+
87
+ def save(self): # Use lowercase save based on save_project.py
88
+ print(f"Dummy Project.save() called for {self.path}")
89
+ time.sleep(0.3)
90
+ print("Dummy project saved.")
91
+ return True # Simulate success
92
+
93
+ def SaveAs(self, path): # Used in create_project simulation
94
+ print(f"Dummy Project.SaveAs() called for {self.path} to {path}")
95
+ self.path = path # Update path
96
+ time.sleep(0.3)
97
+ print("Dummy project saved as.")
98
+ return True
99
+
100
+ def Close(self):
101
+ print(f"Dummy Project.Close() called for {self.path}")
102
+ time.sleep(0.2)
103
+ # In dummy, we don't unset primary here, real CODESYS might.
104
+
105
+ def FindObject(self, path): # Used in find_object_by_path_robust simulation
106
+ print(f"Dummy Project.FindObject() called for {path}")
107
+ time.sleep(0.1)
108
+ # Simulate finding a POU
109
+ if "PLC_PRG" in path:
110
+ return DummyPou(path)
111
+ # Simulate finding Application
112
+ if path.lower() == "application":
113
+ return self.active_application
114
+ return None # Simulate not found
115
+
116
+ def get_children(self, recursive=False): # Use lowercase get_children based on scripts
117
+ print(f"Dummy Project.get_children(recursive={recursive}) called.")
118
+ # Simulate returning some child objects for structure
119
+ # This dummy structure needs to match what find_object_by_path_robust expects
120
+ children = [
121
+ DummyObject("Device"),
122
+ self.active_application # Include the application
123
+ ]
124
+ if recursive:
125
+ all_children = []
126
+ for child in children:
127
+ all_children.append(child)
128
+ if hasattr(child, 'get_children'):
129
+ all_children.extend(child.get_children(recursive=True))
130
+ return all_children
131
+ return children
132
+
133
+ def get_name(self): # Use lowercase get_name based on scripts
134
+ print(f"Dummy Project.get_name() called for {self.path}")
135
+ return os.path.basename(self.path)
136
+
137
+
138
+ class DummyObject: # Base class for project items
139
+ def __init__(self, name, children=None):
140
+ self._name = name
141
+ self._children = children if children is not None else []
142
+
143
+ def get_name(self): # Use lowercase get_name
144
+ return self._name
145
+
146
+ def get_children(self, recursive=False): # Use lowercase get_children
147
+ print(f"Dummy Object.get_children(recursive={recursive}) called for {self._name}")
148
+ if recursive:
149
+ all_children = []
150
+ for child in self._children:
151
+ all_children.append(child)
152
+ if hasattr(child, 'get_children'):
153
+ all_children.extend(child.get_children(recursive=True))
154
+ return all_children
155
+ return self._children
156
+
157
+ def get_path(self): # Helper for dummy path simulation
158
+ # Simple path simulation - needs refinement for nested objects
159
+ return self._name
160
+
161
+
162
+ class DummyPou(DummyObject): # Simulate POU, Method, Property
163
+ def __init__(self, path):
164
+ super().__init__(path.split('.')[-1]) # Name is the last part of the path
165
+ self.path = path
166
+ self.declaration = f"VAR\n\t// Dummy declaration for {path}\nEND_VAR"
167
+ self.implementation = f"// Dummy implementation for {path}\n"
168
+ self.textual_declaration = DummyTextualObject(self.declaration) # Simulate textual objects
169
+ self.textual_implementation = DummyTextualObject(self.implementation) # Simulate textual objects
170
+
171
+ def GetDeclaration(self): # Use uppercase GetDeclaration based on set_pou_code simulation
172
+ print(f"Dummy Pou.GetDeclaration() called for {self.path}")
173
+ return self.declaration
174
+
175
+ def GetImplementation(self): # Use uppercase GetImplementation based on set_pou_code simulation
176
+ print(f"Dummy Pou.GetImplementation() called for {self.path}")
177
+ return self.implementation
178
+
179
+ # Simulate methods used in set_pou_code
180
+ # Note: The real API uses .textual_declaration/.textual_implementation objects with a .replace() method
181
+ # The dummy TextualObject handles the replace logic.
182
+
183
+ def create_method(self, name, return_type, language): # Use lowercase create_method
184
+ print(f"Dummy Pou.create_method() called for {self.path}. Method: {name}, ReturnType: {return_type}, Lang: {language}")
185
+ # Simulate creating a method object
186
+ method_path = f"{self.path}.{name}"
187
+ new_method = DummyPou(method_path) # Methods are also like POUs in structure
188
+ self._children.append(new_method)
189
+ return new_method # Simulate returning the new method object
190
+
191
+ def create_property(self, name, return_type, language): # Use lowercase create_property
192
+ print(f"Dummy Pou.create_property() called for {self.path}. Property: {name}, Type: {return_type}, Lang: {language}")
193
+ # Simulate creating a property object
194
+ property_path = f"{self.path}.{name}"
195
+ new_property = DummyPou(property_path) # Properties might be simpler objects, but use Pou for now
196
+ self._children.append(new_property)
197
+ return new_property # Simulate returning the new property object
198
+
199
+
200
+ class DummyTextualObject: # Simulate objects like textual_declaration, textual_implementation
201
+ def __init__(self, content):
202
+ self._content = content
203
+
204
+ def replace(self, new_content):
205
+ print(f"Dummy TextualObject.replace() called. Old content: '{self._content[:20]}...', New content: '{new_content[:20]}...'")
206
+ self._content = new_content
207
+ return True # Simulate success
208
+
209
+
210
+ class DummyApplication(DummyObject): # Simulate Application object
211
+ def __init__(self):
212
+ super().__init__("Application")
213
+ self.is_application = True # Attribute used in compile_project
214
+ self._children = [DummyPou("Application.PLC_PRG")] # Default PLC_PRG
215
+
216
+ def FindObject(self, path): # Used in find_object_by_path_robust simulation
217
+ print(f"Dummy Application.FindObject() called for {path}")
218
+ # Simple find logic within application children
219
+ for child in self._children:
220
+ if child.get_path() == path:
221
+ return child
222
+ return None
223
+
224
+ def create_pou(self, name, type, language): # Use lowercase create_pou
225
+ print(f"Dummy Application.create_pou() called. Name: {name}, Type: {type}, Lang: {language}")
226
+ # Simulate creating a new POU under Application
227
+ new_pou_path = f"Application.{name}"
228
+ new_pou = DummyPou(new_pou_path)
229
+ self._children.append(new_pou)
230
+ return new_pou # Simulate returning the new POU object
231
+
232
+ def build(self): # Use lowercase build based on compile_project
233
+ print("Dummy Application.build() called.")
234
+ time.sleep(1) # Simulate compile time
235
+ print("Dummy build finished.")
236
+ return True # Simulate success
237
+
238
+
239
+ class DummyProjectManager: # Simulate ProjectManager object
240
+ def GetActiveProject(self): # Use uppercase GetActiveProject based on listener script
241
+ print("Dummy ProjectManager.GetActiveProject() called.")
242
+ # Simulate returning the currently active project from DummyProjects
243
+ return scriptengine.projects.primary
244
+
245
+
246
+ scriptengine = DummyScriptEngine() # Assign the dummy scriptengine object
247
+ ScriptOnline = DummyScriptOnline() # Assign the dummy ScriptOnline object
248
+ # Define dummy enums if needed
249
+ class DummyOnlineChangeOption:
250
+ Try = 1
251
+ OnlineChangeOption = DummyOnlineChangeOption()
252
+ class DummyPouType: # Simulate PouType enum
253
+ Program = "Program"
254
+ FunctionBlock = "FunctionBlock"
255
+ Function = "Function"
256
+ PouType = DummyPouType()
257
+ # Dummy VersionUpdateFlags if needed
258
+ class DummyVersionUpdateFlags:
259
+ NoUpdates = 1
260
+ SilentMode = 2
261
+ VersionUpdateFlags = DummyVersionUpdateFlags()
262
+
263
+ # Dummy exception for ValuesFailedException
264
+ class DummyValuesFailedException(Exception):
265
+ def __init__(self, failed_expressions, message="Values failed"):
266
+ super().__init__(message)
267
+ self.failed_expressions = failed_expressions
268
+
269
+ ValuesFailedException = DummyValuesFailedException
270
+
271
+
272
+ HOST = '127.0.0.1' # Standard loopback interface address (localhost)
273
+ PORT = 65432 # Port to listen on (non-privileged ports are > 1023)
274
+
275
+ # Global variable to hold the active online application/device
276
+ # In a real scenario, you might manage multiple connections or sessions
277
+ online_app = None
278
+ online_device = None
279
+ active_project = None # Global variable to hold the active project object
280
+
281
+ # --- Helper to get active project ---
282
+ # Adapted from ensure_project_open.py
283
+ MAX_RETRIES = 3
284
+ RETRY_DELAY = 0.5 # seconds (reduced delay for listener context)
285
+
286
+ def ensure_project_is_active(target_project_path):
287
+ """Ensures the specified project is the active project in CODESYS."""
288
+ global active_project
289
+ print("DEBUG: Ensuring project is active: %s" % target_project_path)
290
+ normalized_target_path = os.path.normcase(os.path.abspath(target_project_path))
291
+
292
+ for attempt in range(MAX_RETRIES):
293
+ print("DEBUG: Ensure project active attempt %d/%d for %s" % (attempt + 1, MAX_RETRIES, normalized_target_path))
294
+ current_primary_project = None
295
+ try:
296
+ current_primary_project = scriptengine.projects.primary
297
+ except Exception as primary_err:
298
+ print("WARN: Error getting primary project: %s. Assuming none." % primary_err)
299
+ current_primary_project = None
300
+
301
+ current_project_path = ""
302
+ project_ok = False
303
+
304
+ if current_primary_project:
305
+ try:
306
+ current_project_path = os.path.normcase(os.path.abspath(current_primary_project.path))
307
+ print("DEBUG: Current primary project path: %s" % current_project_path)
308
+ if current_project_path == normalized_target_path:
309
+ print("DEBUG: Target project path matches primary. Checking access...")
310
+ try:
311
+ # Try a relatively safe operation to confirm object usability
312
+ _ = len(current_primary_project.get_children(False))
313
+ print("DEBUG: Target project '%s' is primary and accessible." % target_project_path)
314
+ active_project = current_primary_project # Set global active project
315
+ project_ok = True
316
+ return active_project # SUCCESS CASE 1: Already open and accessible
317
+ except Exception as access_err:
318
+ print("WARN: Primary project access check failed for '%s': %s. Will attempt reopen." % (current_project_path, access_err))
319
+ active_project = None # Force reopen by falling through
320
+ current_primary_project = None # Also clear the reference
321
+ else:
322
+ print("DEBUG: Primary project is '%s', not the target '%s'. Attempting to open target." % (current_project_path, normalized_target_path))
323
+ # Don't explicitly close the wrong project here, just attempt to open the target
324
+ active_project = None
325
+ current_primary_project = None
326
+
327
+ except Exception as path_err:
328
+ print("WARN: Could not get path of current primary project: %s. Assuming not the target." % path_err)
329
+ active_project = None
330
+ current_primary_project = None
331
+
332
+ # If target project not confirmed as primary and accessible, attempt to open/reopen
333
+ if not project_ok:
334
+ print("DEBUG: Attempting to open/reopen target project: %s" % target_project_path)
335
+ try:
336
+ # Use VersionUpdateFlags from scriptengine
337
+ update_mode = scriptengine.VersionUpdateFlags.NoUpdates | scriptengine.VersionUpdateFlags.SilentMode
338
+ opened_project = scriptengine.projects.open(target_project_path, update_flags=update_mode)
339
+
340
+ if not opened_project:
341
+ print("ERROR: projects.open returned None for %s on attempt %d" % (target_project_path, attempt + 1))
342
+ active_project = None
343
+ else:
344
+ print("DEBUG: projects.open call returned an object for: %s" % target_project_path)
345
+ # Give CODESYS time to stabilize
346
+ time.sleep(RETRY_DELAY)
347
+ # Re-verify: Is the project now primary and accessible?
348
+ recheck_primary = None
349
+ try: recheck_primary = scriptengine.projects.primary
350
+ except Exception as recheck_primary_err: print("WARN: Error getting primary project after reopen: %s" % recheck_primary_err)
351
+
352
+ if recheck_primary:
353
+ recheck_path = ""
354
+ try:
355
+ recheck_path = os.path.normcase(os.path.abspath(recheck_primary.path))
356
+ except Exception as recheck_path_err:
357
+ print("WARN: Failed to get path after reopen: %s" % recheck_path_err)
358
+
359
+ if recheck_path == normalized_target_path:
360
+ print("DEBUG: Target project confirmed as primary after reopening.")
361
+ try:
362
+ _ = len(recheck_primary.get_children(False))
363
+ print("DEBUG: Reopened project basic access confirmed.")
364
+ active_project = recheck_primary # Set global active project
365
+ return active_project # SUCCESS CASE 2: Successfully opened/reopened
366
+ except Exception as access_err_reopen:
367
+ print("WARN: Reopened project (%s) basic access check failed: %s." % (normalized_target_path, access_err_reopen))
368
+ active_project = None
369
+ else:
370
+ print("WARN: Different project is primary after reopening! Expected '%s', got '%s'." % (normalized_target_path, recheck_path))
371
+ active_project = None
372
+ else:
373
+ print("WARN: No primary project found after reopening attempt %d!" % (attempt+1))
374
+ active_project = None
375
+
376
+ except Exception as open_err:
377
+ print("ERROR: Exception during projects.open call on attempt %d: %s" % (attempt + 1, open_err))
378
+ traceback.print_exc()
379
+ active_project = None
380
+
381
+ # If we didn't return successfully in this attempt, wait before retrying
382
+ if attempt < MAX_RETRIES - 1:
383
+ print("DEBUG: Ensure project active attempt %d did not succeed. Waiting %f seconds..." % (attempt + 1, RETRY_DELAY))
384
+ time.sleep(RETRY_DELAY)
385
+ else: # Last attempt failed
386
+ print("ERROR: Failed all ensure_project_is_active attempts for %s." % normalized_target_path)
387
+
388
+
389
+ # If all retries fail after the loop
390
+ raise RuntimeError("Failed to ensure project '%s' is open and accessible after %d attempts." % (target_project_path, MAX_RETRIES))
391
+
392
+ # --- End of ensure_project_is_active function ---
393
+
394
+
395
+ # --- Helper to find object by path ---
396
+ # Adapted from find_object_by_path.py
397
+ def find_object_by_path_robust(start_node, full_path, target_type_name="object"):
398
+ """Finds an object within the project hierarchy starting from start_node."""
399
+ print("DEBUG: Finding %s by path: '%s'" % (target_type_name, full_path))
400
+ normalized_path = full_path.replace('\\\\', '/').strip('/')
401
+ path_parts = normalized_path.split('/')
402
+ if not path_parts or not normalized_path: # Handle empty path after strip
403
+ print("ERROR: Path is empty after normalization.")
404
+ return None
405
+
406
+ # Determine the actual starting node (project or application)
407
+ project = None
408
+ app = None
409
+
410
+ # Try to get the project reference from the start_node if it's not the project itself
411
+ if hasattr(start_node, 'project'):
412
+ try: project = start_node.project
413
+ except Exception as proj_ref_err:
414
+ print("WARN: Could not get project reference from start_node: %s" % proj_ref_err)
415
+
416
+ # If start_node *is* the project, use it directly
417
+ if project is None and hasattr(start_node, 'active_application'): # Check for a project-like object
418
+ project = start_node
419
+
420
+ if not project:
421
+ print("ERROR: Could not determine the project from the start_node.")
422
+ return None
423
+
424
+ # Try to get the active application robustly
425
+ if hasattr(project, 'active_application'):
426
+ try: app = project.active_application
427
+ except Exception: pass # Ignore errors getting active app
428
+ if not app:
429
+ try:
430
+ # Search recursively for an application if active_application fails
431
+ if hasattr(project, 'find'): # Check if project has find method
432
+ apps = project.find("Application", True) # Assuming find method exists on project
433
+ if apps: app = apps[0] # Take the first one found
434
+ else:
435
+ print("WARN: Project object does not have a 'find' method.")
436
+ except Exception: pass
437
+
438
+
439
+ # Decide where to start the traversal based on the first path part
440
+ current_obj = start_node # Default to the node passed in
441
+ remaining_path_parts = list(path_parts) # Create a mutable copy
442
+
443
+ if app: # Only consider starting from application if we found one
444
+ app_name_lower = ""
445
+ try: app_name_lower = (getattr(app, 'get_name', lambda: "application")() or "application").lower()
446
+ except Exception: app_name_lower = "application" # Fallback
447
+
448
+ # Check if first part matches EITHER the actual app name OR the generic "Application" string
449
+ if remaining_path_parts[0].lower() == app_name_lower or remaining_path_parts[0].lower() == "application":
450
+ print("DEBUG: Path starts with Application name or the string 'Application'. Beginning search there.")
451
+ current_obj = app
452
+ remaining_path_parts = remaining_path_parts[1:] # Consume the app name part
453
+ # If path was *only* the application name
454
+ if not remaining_path_parts:
455
+ print("DEBUG: Target path is the Application object itself.")
456
+ return current_obj
457
+ else:
458
+ print("DEBUG: Path does not start with Application name or string. Starting search from project root.")
459
+ current_obj = project # Start search from the project root
460
+ else:
461
+ print("DEBUG: No application found or path doesn't start with app name. Starting search from originally provided node.")
462
+ # If start_node was the project, and no app, we still start from project.
463
+ if hasattr(start_node, 'active_application'):
464
+ current_obj = start_node
465
+
466
+
467
+ # Traverse the remaining path parts
468
+ parent_path_str = getattr(current_obj, 'get_name', lambda: str(current_obj))() # Safer name getting
469
+
470
+ for i, part_name in enumerate(remaining_path_parts):
471
+ is_last_part = (i == len(remaining_path_parts) - 1)
472
+ print("DEBUG: Searching for part [%d/%d]: '%s' under '%s'" % (i+1, len(remaining_path_parts), part_name, parent_path_str))
473
+ found_in_parent = None
474
+ try:
475
+ # Prioritize non-recursive find for direct children
476
+ if hasattr(current_obj, 'get_children'):
477
+ children_of_current = current_obj.get_children(False)
478
+ print("DEBUG: Found %d direct children under '%s'." % (len(children_of_current), parent_path_str))
479
+ for child in children_of_current:
480
+ child_name = getattr(child, 'get_name', lambda: None)() # Safer name getting
481
+ # print("DEBUG: Checking child: '%s'" % child_name) # Verbose
482
+ if child_name == part_name:
483
+ found_in_parent = child
484
+ print("DEBUG: Found direct child matching '%s'." % part_name)
485
+ break # Found direct child, stop searching children
486
+
487
+ # If not found directly, AND it's the last part, try recursive find from current parent
488
+ # Note: Recursive find might be slow on large projects. Direct traversal is preferred.
489
+ # The original script tried recursive find only for the last part.
490
+ if not found_in_parent and is_last_part and hasattr(current_obj, 'find'):
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) # Assuming find method exists
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
+ elif not found_in_parent and is_last_part and not hasattr(current_obj, 'find'):
500
+ print("DEBUG: Direct find failed for last part '%s'. Parent object '%s' does not have a 'find' method." % (part_name, parent_path_str))
501
+
502
+
503
+ # Update current object if found
504
+ if found_in_parent:
505
+ current_obj = found_in_parent
506
+ parent_path_str = getattr(current_obj, 'get_name', lambda: part_name)() # Safer name getting
507
+ print("DEBUG: Stepped into '%s'." % parent_path_str)
508
+ else:
509
+ # If not found at any point, the path is invalid from this parent
510
+ print("ERROR: Path part '%s' not found under '%s'." % (part_name, parent_path_str))
511
+ return None # Path broken
512
+
513
+ except Exception as find_err:
514
+ print("ERROR: Exception while searching for '%s' under '%s': %s" % (part_name, parent_path_str, find_err))
515
+ traceback.print_exc()
516
+ return None # Error during search
517
+
518
+ # Final verification (optional but recommended): Check if the found object's name matches the last part
519
+ # This check might be redundant if the traversal logic is perfect, but adds robustness.
520
+ final_expected_name = normalized_path.split('/')[-1]
521
+ found_final_name = getattr(current_obj, 'get_name', lambda: None)() # Safer name getting
522
+
523
+ if found_final_name == final_expected_name:
524
+ print("DEBUG: Final %s found and name verified for path '%s': %s" % (target_type_name, full_path, found_final_name))
525
+ return current_obj
526
+ else:
527
+ print("ERROR: Traversal ended on object '%s' but expected final name was '%s' for path '%s'." % (found_final_name, final_expected_name, full_path))
528
+ return None # Name mismatch implies target not found as expected
529
+
530
+ # --- End of find object function ---
531
+
532
+
533
+ HOST = '127.0.0.1' # Standard loopback interface address (localhost)
534
+ PORT = 65432 # Port to listen on (non-privileged ports are > 1023)
535
+
536
+ # Global variable to hold the active online application/device
537
+ # In a real scenario, you might manage multiple connections or sessions
538
+ online_app = None
539
+ online_device = None
540
+ active_project = None # Global variable to hold the active project object
541
+
542
+
543
+ def open_project_command(args):
544
+ """Handles the 'open_project' command."""
545
+ global active_project
546
+ project_path = args.get("file_path") # Use file_path as defined in server.ts
547
+ if not project_path:
548
+ return {"status": "error", "message": "Missing 'file_path' argument for open_project"}
549
+ try:
550
+ print(f"Attempting to open project: {project_path}")
551
+ # Use the actual CODESYS Scripting API to open the project
552
+ # Based on ensure_project_open.py
553
+ # Use VersionUpdateFlags from scriptengine
554
+ update_mode = scriptengine.VersionUpdateFlags.NoUpdates | scriptengine.VersionUpdateFlags.SilentMode
555
+ project = scriptengine.projects.open(project_path, update_flags=update_mode)
556
+
557
+ if project:
558
+ active_project = project # Set the global active project
559
+ print(f"Project opened successfully: {project_path}")
560
+ return {"status": "success", "message": f"Project opened: {project_path}"}
561
+ else:
562
+ print(f"Failed to open project: {project_path}. projects.open returned None.")
563
+ active_project = None # Ensure no active project if opening fails
564
+ return {"status": "error", "message": f"Failed to open project: {project_path}"}
565
+ except Exception as e:
566
+ print(f"Error opening project: {e}")
567
+ traceback.print_exc() # Print traceback for detailed error
568
+ active_project = None
569
+ return {"status": "error", "message": f"Error opening project: {e}"}
570
+
571
+ def create_project_command(args):
572
+ """Handles the 'create_project' command."""
573
+ global active_project
574
+ file_path = args.get("file_path")
575
+ template_path = args.get("template_path")
576
+ if not file_path or not template_path:
577
+ return {"status": "error", "message": "Missing arguments for create_project"}
578
+ try:
579
+ print(f"Attempting to create project from template: {template_path} at {file_path}")
580
+
581
+ # Based on create_project.py, this involves copying the template file
582
+ target_dir = os.path.dirname(file_path)
583
+ if not os.path.exists(target_dir):
584
+ print(f"DEBUG: Creating target directory: {target_dir}")
585
+ os.makedirs(target_dir)
586
+
587
+ if os.path.exists(file_path):
588
+ print(f"WARN: Target project file already exists, overwriting: {file_path}")
589
+
590
+ print(f"DEBUG: Copying '{template_path}' to '{file_path}'...")
591
+ shutil.copy2(template_path, file_path) # copy2 preserves metadata
592
+ print("DEBUG: File copy complete.")
593
+
594
+ # Then open the newly copied project file
595
+ print(f"DEBUG: Opening the copied project: {file_path}")
596
+ # Use VersionUpdateFlags from scriptengine
597
+ update_mode = scriptengine.VersionUpdateFlags.NoUpdates | scriptengine.VersionUpdateFlags.SilentMode
598
+ project = scriptengine.projects.open(file_path, update_flags=update_mode)
599
+
600
+ if project:
601
+ active_project = project # Set the global active project
602
+ print(f"Project created and opened successfully: {file_path}")
603
+ # Optional: Save the project explicitly after opening the copy, as seen in create_project.py
604
+ try:
605
+ print("DEBUG: Explicitly saving project after opening copy...")
606
+ active_project.save() # Use lowercase save
607
+ print("DEBUG: Project save after opening copy succeeded.")
608
+ except Exception as save_err:
609
+ print(f"WARN: Explicit save after opening copy failed: {save_err}")
610
+
611
+ return {"status": "success", "message": f"Project created and opened: {file_path}"}
612
+ else:
613
+ print(f"Failed to open the newly created project: {file_path}. projects.open returned None.")
614
+ active_project = None
615
+ return {"status": "error", "message": f"Project created at {file_path}, but failed to open it."}
616
+ except Exception as e:
617
+ print(f"Error creating project: {e}")
618
+ traceback.print_exc()
619
+ active_project = None
620
+ return {"status": "error", "message": f"Error creating project: {e}"}
621
+
622
+
623
+ def save_project_command(args):
624
+ """Handles the 'save_project' command."""
625
+ global active_project
626
+ project_file_path = args.get("project_file_path")
627
+ if not project_file_path:
628
+ return {"status": "error", "message": "Missing 'project_file_path' argument for save_project"}
629
+
630
+ # Ensure the correct project is active before saving
631
+ project = ensure_project_is_active(project_file_path)
632
+ if not project:
633
+ return {"status": "error", "message": f"Could not ensure project '{project_file_path}' is active to save."}
634
+
635
+ try:
636
+ print(f"Attempting to save active project: {project.path}")
637
+ # Use the actual CODESYS Scripting API to save the project
638
+ # Based on save_project.py
639
+ project.save() # Use lowercase save
640
+ print("Project saved successfully.")
641
+ return {"status": "success", "message": f"Project saved: {project.path}"}
642
+ except Exception as e:
643
+ print(f"Error saving project: {e}")
644
+ traceback.print_exc()
645
+ return {"status": "error", "message": f"Error saving project: {e}"}
646
+
647
+ def create_pou_command(args):
648
+ """Handles the 'create_pou' command."""
649
+ global active_project
650
+ project_file_path = args.get("project_file_path")
651
+ name = args.get("name")
652
+ pou_type_str = args.get("type") # Use pou_type_str to avoid conflict with PouType enum
653
+ language = args.get("language") # Note: create_pou.py passes None for language
654
+ parent_path = args.get("parent_path")
655
+
656
+ if not all([project_file_path, name, pou_type_str, language, parent_path is not None]):
657
+ return {"status": "error", "message": "Missing arguments for create_pou"}
658
+
659
+ # Ensure the correct project is active
660
+ project = ensure_project_is_active(project_file_path)
661
+ if not project:
662
+ return {"status": "error", "message": f"Could not ensure project '{project_file_path}' is active to create POU in."}
663
+
664
+ try:
665
+ print(f"Attempting to create POU '{name}' (Type: {pou_type_str}, Lang: {language}) under '{parent_path}' in project: {project.path}")
666
+
667
+ # Resolve POU Type Enum based on create_pou.py
668
+ pou_type_map = {
669
+ "Program": PouType.Program,
670
+ "FunctionBlock": PouType.FunctionBlock,
671
+ "Function": PouType.Function
672
+ }
673
+ pou_type_enum = pou_type_map.get(pou_type_str)
674
+ if not pou_type_enum:
675
+ return {"status": "error", "message": f"Invalid POU type string: {pou_type_str}. Use Program, FunctionBlock, or Function."}
676
+
677
+ # Find parent object using the robust function
678
+ parent_object = find_object_by_path_robust(project, parent_path, "parent container")
679
+ if not parent_object:
680
+ return {"status": "error", "message": f"Parent object not found for path: {parent_path}"}
681
+
682
+ # Check if parent object supports creating POUs (based on create_pou.py)
683
+ if not hasattr(parent_object, 'create_pou'):
684
+ return {"status": "error", "message": f"Parent object '{getattr(parent_object, 'get_name', lambda: str(parent_object))()}' of type {type(parent_object).__name__} does not support create_pou."}
685
+
686
+ # Set language GUID to None (let CODESYS default based on parent/settings) based on create_pou.py
687
+ lang_guid = None
688
+ print("DEBUG: Setting language to None (will use default).")
689
+ # Example if mapping language string: lang_guid = lang_map.get(language, None)
690
+
691
+ print(f"DEBUG: Calling parent_object.create_pou: Name='{name}', Type={pou_type_enum}, Lang={lang_guid}")
692
+
693
+ # Call create_pou using keyword arguments based on create_pou.py
694
+ new_pou = parent_object.create_pou(
695
+ name=name,
696
+ type=pou_type_enum,
697
+ language=lang_guid # Pass None
698
+ )
699
+
700
+ if new_pou:
701
+ new_pou_name = getattr(new_pou, 'get_name', lambda: name)()
702
+ print(f"POU object created: {new_pou_name}")
703
+
704
+ # Save the project to persist the new POU based on create_pou.py
705
+ try:
706
+ print("DEBUG: Saving Project...")
707
+ project.save() # Use lowercase save
708
+ print("DEBUG: Project saved successfully after POU creation.")
709
+ except Exception as save_err:
710
+ print(f"ERROR: Failed to save Project after POU creation: {save_err}")
711
+ traceback.print_exc()
712
+ return {"status": "error", "message": f"Error saving Project after creating POU '{new_pou_name}': {save_err}"}
713
+
714
+ return {"status": "success", "message": f"POU '{new_pou_name}' created in '{parent_path}'. Project saved."}
715
+ else:
716
+ return {"status": "error", "message": f"Failed to create POU '{name}'. create_pou returned None."}
717
+
718
+ except Exception as e:
719
+ print(f"Error creating POU: {e}")
720
+ traceback.print_exc()
721
+ return {"status": "error", "message": f"Error creating POU: {e}"}
722
+
723
+ def set_pou_code_command(args):
724
+ """Handles the 'set_pou_code' command."""
725
+ global active_project
726
+ project_file_path = args.get("project_file_path")
727
+ pou_path = args.get("pou_path")
728
+ declaration = args.get("declaration")
729
+ implementation = args.get("implementation")
730
+
731
+ if not all([project_file_path, pou_path]) or (declaration is None and implementation is None):
732
+ return {"status": "error", "message": "Missing arguments for set_pou_code"}
733
+
734
+ # Ensure the correct project is active
735
+ project = ensure_project_is_active(project_file_path)
736
+ if not project:
737
+ return {"status": "error", "message": f"Could not ensure project '{project_file_path}' is active to set POU code in."}
738
+
739
+ try:
740
+ print(f"Attempting to set code for POU '{pou_path}' in project: {project.path}")
741
+
742
+ # Find the POU object using the robust function
743
+ pou_object = find_object_by_path_robust(project, pou_path, "POU object")
744
+ if not pou_object:
745
+ return {"status": "error", "message": f"POU object not found: {pou_path}"}
746
+
747
+ success_decl = True
748
+ success_impl = True
749
+
750
+ # Use the actual CODESYS Scripting API to set the code based on set_pou_code.py
751
+ if declaration is not None:
752
+ print("Setting declaration code...")
753
+ if hasattr(pou_object, 'textual_declaration') and hasattr(pou_object.textual_declaration, 'replace'):
754
+ try:
755
+ pou_object.textual_declaration.replace(declaration)
756
+ print("Set declaration text using replace().")
757
+ declaration_updated = True
758
+ except Exception as decl_err:
759
+ print(f"ERROR: Failed to set declaration text: {decl_err}")
760
+ traceback.print_exc()
761
+ success_decl = False
762
+ else:
763
+ print(f"WARN: Target '{getattr(pou_object, 'get_name', lambda: pou_path)()}' does not have accessible textual_declaration. Skipping declaration update.")
764
+ success_decl = False # Consider this a failure if content was provided but couldn't be set
765
+
766
+ if implementation is not None:
767
+ print("Setting implementation code...")
768
+ if hasattr(pou_object, 'textual_implementation') and hasattr(pou_object.textual_implementation, 'replace'):
769
+ try:
770
+ pou_object.textual_implementation.replace(implementation)
771
+ print("Set implementation text using replace().")
772
+ implementation_updated = True
773
+ except Exception as impl_err:
774
+ print(f"ERROR: Failed to set implementation text: {impl_err}")
775
+ traceback.print_exc()
776
+ success_impl = False
777
+ else:
778
+ print(f"WARN: Target '{getattr(pou_object, 'get_name', lambda: pou_path)()}' does not have accessible textual_implementation. Skipping implementation update.")
779
+ success_impl = False # Consider this a failure if content was provided but couldn't be set
780
+
781
+
782
+ if success_decl and success_impl:
783
+ print("POU code set successfully.")
784
+ # Save the project to persist the code change based on set_pou_code.py
785
+ try:
786
+ print("DEBUG: Saving Project (after code change)...")
787
+ project.save() # Use lowercase save
788
+ print("DEBUG: Project saved successfully after code change.")
789
+ except Exception as save_err:
790
+ print(f"ERROR: Failed to save Project after setting code: {save_err}")
791
+ traceback.print_exc()
792
+ return {"status": "error", "message": f"Error saving Project after code change for '{pou_path}': {save_err}"}
793
+
794
+ return {"status": "success", "message": f"Code set for POU '{pou_path}'. Project saved."}
795
+ else:
796
+ return {"status": "error", "message": f"Failed to set code for POU '{pou_path}'. Declaration success: {success_decl}, Implementation success: {success_impl}"}
797
+
798
+ except Exception as e:
799
+ print(f"Error setting POU code: {e}")
800
+ traceback.print_exc()
801
+ return {"status": "error", "message": f"Error setting POU code: {e}"}
802
+
803
+ def create_method_command(args):
804
+ """Handles the 'create_method' command."""
805
+ global active_project
806
+ project_file_path = args.get("project_file_path")
807
+ function_block_path = args.get("function_block_path")
808
+ name = args.get("name")
809
+ language = args.get("language") # Note: create_method.py passes None for language
810
+ # return_type = args.get("return_type") # create_method.py uses RETURN_TYPE placeholder
811
+
812
+ if not all([project_file_path, function_block_path, name]): # Language and return_type seem optional based on script
813
+ return {"status": "error", "message": "Missing required arguments for create_method"}
814
+
815
+ # Ensure the correct project is active
816
+ project = ensure_project_is_active(project_file_path)
817
+ if not project:
818
+ return {"status": "error", "message": f"Could not ensure project '{project_file_path}' is active to create method in."}
819
+
820
+ try:
821
+ print(f"Attempting to create Method '{name}' in Function Block '{function_block_path}' in project: {project.path}")
822
+
823
+ # Find the Function Block object using the robust function
824
+ fb_object = find_object_by_path_robust(project, function_block_path, "Function Block object")
825
+ if not fb_object:
826
+ return {"status": "error", "message": f"Function Block object not found: {function_block_path}"}
827
+
828
+ # Check if parent object supports creating methods (based on create_method.py)
829
+ if not hasattr(fb_object, 'create_method'):
830
+ return {"status": "error", "message": f"Parent object '{getattr(fb_object, 'get_name', lambda: str(fb_object))()}' of type {type(fb_object).__name__} does not support create_method."}
831
+
832
+ # Default language to None (usually ST) based on create_method.py
833
+ lang_guid = None
834
+ # Return type is also passed as None in the original script's call
835
+ actual_return_type = None # Assuming no return type is specified in the tool call args
836
+
837
+ print(f"DEBUG: Calling fb_object.create_method: Name='{name}', ReturnType={actual_return_type}, Lang={lang_guid}")
838
+
839
+ # Call the create_method method ON THE PARENT POU based on create_method.py
840
+ new_method_object = fb_object.create_method(
841
+ name=name,
842
+ return_type=actual_return_type, # Pass None
843
+ language=lang_guid # Pass None to use default
844
+ )
845
+
846
+ if new_method_object:
847
+ new_meth_name = getattr(new_method_object, 'get_name', lambda: name)()
848
+ print(f"Method object created: {new_meth_name}")
849
+
850
+ # Save the project to persist the new method object based on create_method.py
851
+ try:
852
+ print("DEBUG: Saving Project (after method creation)...")
853
+ project.save() # Use lowercase save
854
+ print("DEBUG: Project saved successfully after method creation.")
855
+ except Exception as save_err:
856
+ print(f"ERROR: Failed to save Project after creating method: {save_err}")
857
+ traceback.print_exc()
858
+ return {"status": "error", "message": f"Error saving Project after creating method '{name}': {save_err}"}
859
+
860
+ return {"status": "success", "message": f"Method '{new_meth_name}' created in '{function_block_path}'. Project saved."}
861
+ else:
862
+ return {"status": "error", "message": f"Failed to create method '{name}' under '{getattr(fb_object, 'get_name', lambda: function_block_path)() }'. create_method returned None."}
863
+
864
+ except Exception as e:
865
+ print(f"Error creating Method: {e}")
866
+ traceback.print_exc()
867
+ return {"status": "error", "message": f"Error creating Method: {e}"}
868
+
869
+ def create_property_command(args):
870
+ """Handles the 'create_property' command."""
871
+ global active_project
872
+ project_file_path = args.get("project_file_path")
873
+ function_block_path = args.get("function_block_path")
874
+ name = args.get("name")
875
+ data_type = args.get("data_type")
876
+ # is_retain = args.get("is_retain", False) # Not used in create_property.py call
877
+ # is_persistent = args.get("is_persistent", False) # Not used in create_property.py call
878
+ # is_constant = args.get("is_constant", False) # Not used in create_property.py call
879
+ # initial_value = args.get("initial_value") # Not used in create_property.py call
880
+ # comment = args.get("comment") # Not used in create_property.py call
881
+
882
+ if not all([project_file_path, function_block_path, name, data_type]):
883
+ return {"status": "error", "message": "Missing required arguments for create_property"}
884
+
885
+ # Ensure the correct project is active
886
+ project = ensure_project_is_active(project_file_path)
887
+ if not project:
888
+ return {"status": "error", "message": f"Could not ensure project '{project_file_path}' is active to create property in."}
889
+
890
+ try:
891
+ print(f"Attempting to create Property '{name}' (Type: {data_type}) in Function Block '{function_block_path}' in project: {project.path}")
892
+
893
+ # Find the Function Block object using the robust function
894
+ fb_object = find_object_by_path_robust(project, function_block_path, "Function Block object")
895
+ if not fb_object:
896
+ return {"status": "error", "message": f"Function Block object not found: {function_block_path}"}
897
+
898
+ # Check if parent object supports creating properties (based on create_property.py)
899
+ if not hasattr(fb_object, 'create_property'):
900
+ return {"status": "error", "message": f"Parent object '{getattr(fb_object, 'get_name', lambda: str(fb_object))()}' of type {type(fb_object).__name__} does not support create_property."}
901
+
902
+ # Default language to None (usually ST) based on create_property.py
903
+ lang_guid = None
904
+ print(f"DEBUG: Calling create_property: Name='{name}', Type='{data_type}', Lang={lang_guid}")
905
+
906
+ # Call the create_property method ON THE PARENT POU based on create_property.py
907
+ # Note: The original script passes data_type as return_type and doesn't use optional args
908
+ new_property_object = fb_object.create_property(
909
+ name=name,
910
+ return_type=data_type, # Based on create_property.py's usage
911
+ language=lang_guid # Pass None to use default
912
+ # Optional arguments from tool schema are not used in the original script's API call
913
+ # is_retain=args.get("is_retain", False),
914
+ # is_persistent=args.get("is_persistent", False),
915
+ # is_constant=args.get("is_constant", False),
916
+ # initial_value=args.get("initial_value"),
917
+ # comment=args.get("comment")
918
+ )
919
+
920
+ if new_property_object:
921
+ new_prop_name = getattr(new_property_object, 'get_name', lambda: name)()
922
+ print(f"Property object created: {new_prop_name}")
923
+
924
+ # Save the project to persist the new property object based on create_property.py
925
+ try:
926
+ print("DEBUG: Saving Project (after property creation)...")
927
+ project.save() # Use lowercase save
928
+ print("DEBUG: Project saved successfully after property creation.")
929
+ except Exception as save_err:
930
+ print(f"ERROR: Failed to save Project after creating property: {save_err}")
931
+ traceback.print_exc()
932
+ return {"status": "error", "message": f"Error saving Project after creating property '{name}': {save_err}"}
933
+
934
+ return {"status": "success", "message": f"Property '{new_prop_name}' created in '{function_block_path}'. Project saved."}
935
+ else:
936
+ return {"status": "error", "message": f"Failed to create property '{name}' under '{getattr(fb_object, 'get_name', lambda: function_block_path)() }'. create_property returned None."}
937
+
938
+ except Exception as e:
939
+ print(f"Error creating Property: {e}")
940
+ traceback.print_exc()
941
+ return {"status": "error", "message": f"Error creating Property: {e}"}
942
+
943
+ def compile_project_command(args):
944
+ """Handles the 'compile_project' command."""
945
+ global active_project
946
+ project_file_path = args.get("project_file_path")
947
+ if not project_file_path:
948
+ return {"status": "error", "message": "Missing 'project_file_path' argument for compile_project"}
949
+
950
+ # Ensure the correct project is active
951
+ project = ensure_project_is_active(project_file_path)
952
+ if not project:
953
+ return {"status": "error", "message": f"Could not ensure project '{project_file_path}' is active to compile."}
954
+
955
+ try:
956
+ print(f"Attempting to compile project: {project.path}")
957
+
958
+ target_app = None
959
+ app_name = "N/A"
960
+
961
+ # Try getting active application first based on compile_project.py
962
+ try:
963
+ if hasattr(project, 'active_application'):
964
+ target_app = project.active_application
965
+ if target_app:
966
+ app_name = getattr(target_app, 'get_name', lambda: "Unnamed App (Active)")()
967
+ print(f"DEBUG: Found active application: {app_name}")
968
+ except Exception as active_err:
969
+ print(f"WARN: Could not get active application: {active_err}. Searching...")
970
+
971
+ # If no active app, search for the first one based on compile_project.py
972
+ if not target_app:
973
+ print("DEBUG: Searching for first compilable application...")
974
+ apps = []
975
+ try:
976
+ # Search recursively through all project objects
977
+ if hasattr(project, 'get_children'):
978
+ all_children = project.get_children(True)
979
+ for child in all_children:
980
+ # Check using the marker property and if build method exists
981
+ if hasattr(child, 'is_application') and child.is_application and hasattr(child, 'build'):
982
+ app_name_found = getattr(child, 'get_name', lambda: "Unnamed App")()
983
+ print(f"DEBUG: Found potential application object: {app_name_found}")
984
+ apps.append(child)
985
+ break # Take the first one found
986
+ except Exception as find_err: print(f"WARN: Error finding application object: {find_err}")
987
+
988
+ if not apps:
989
+ return {"status": "error", "message": f"No compilable application found in project '{os.path.basename(project.path)}'"}
990
+ target_app = apps[0]
991
+ app_name = getattr(target_app, 'get_name', lambda: "Unnamed App (First Found)")()
992
+ print(f"WARN: Compiling first found application: {app_name}")
993
+
994
+ print(f"DEBUG: Calling build() on app '{app_name}'...")
995
+ if not hasattr(target_app, 'build'):
996
+ return {"status": "error", "message": f"Selected object '{app_name}' is not an application or doesn't support build()."}
997
+
998
+ # Execute the build based on compile_project.py
999
+ target_app.build();
1000
+ print(f"DEBUG: Build command executed for application '{app_name}'.")
1001
+
1002
+ # Note: Getting detailed compile output via scripting is not shown in compile_project.py.
1003
+ # The original script relies on CODESYS UI or log output.
1004
+ # We will return a success message and indicate that output might be in CODESYS logs.
1005
+ compile_output = "Compilation initiated. Check CODESYS message view for details." # Default message
1006
+
1007
+ # In a real scenario, you might need to check message views or logs via scripting if available
1008
+ # Example (conceptual):
1009
+ # message_view = scriptengine.get_message_view() # Assuming such an API
1010
+ # compile_output = message_view.get_messages() # Assuming method to get messages
1011
+
1012
+ print("Compile Initiated For Application: %s" % app_name)
1013
+ print("In Project: %s" % os.path.basename(project.path))
1014
+
1015
+ return {"status": "success", "message": "Application compilation initiated.", "output": compile_output}
1016
+
1017
+ except Exception as e:
1018
+ print(f"Error initiating compilation: {e}")
1019
+ traceback.print_exc()
1020
+ error_output = str(e) # Fallback to error message
1021
+ # In a real scenario, you might need to check CODESYS message views via scripting
1022
+ return {"status": "error", "message": f"Error initiating compilation: {e}", "output": error_output}
1023
+
1024
+
1025
+ def execute_script_command(args):
1026
+ """Handles the 'execute_script' command by incorporating logic from specific scripts."""
1027
+ script_name = args.get("script_name")
1028
+ # script_args = args.get("script_args", {}) # Optional arguments for the script
1029
+
1030
+ if not script_name:
1031
+ return {"status": "error", "message": "Missing 'script_name' argument for execute_script"}
1032
+
1033
+ print(f"Attempting to execute logic for script: {script_name}")
1034
+
1035
+ try:
1036
+ # Incorporate logic from specific scripts here
1037
+ if script_name == "check_status.py":
1038
+ print("Executing logic for check_status.py")
1039
+ # Logic based on check_status.py and ensure_project_open.py
1040
+ status_data = {}
1041
+ try:
1042
+ primary_project = scriptengine.projects.primary
1043
+ status_data['Scripting OK'] = 'True' # If script is running, scripting is likely OK
1044
+ status_data['Project Open'] = 'False'
1045
+ status_data['Project Name'] = 'None'
1046
+ status_data['Project Path'] = 'N/A'
1047
+
1048
+ if primary_project:
1049
+ status_data['Project Open'] = 'True'
1050
+ try:
1051
+ status_data['Project Name'] = getattr(primary_project, 'get_name', lambda: os.path.basename(primary_project.path))()
1052
+ status_data['Project Path'] = os.path.abspath(primary_project.path)
1053
+ except Exception as name_path_err:
1054
+ print(f"WARN: Error getting project name/path in check_status logic: {name_path_err}")
1055
+ status_data['Project Name'] = 'Error'
1056
+ status_data['Project Path'] = 'Error'
1057
+
1058
+ output_lines = [f"{key}: {value}" for key, value in status_data.items()]
1059
+ simulated_output = "\n".join(output_lines)
1060
+ print("check_status.py logic finished.")
1061
+ return {"status": "success", "message": "check_status logic executed.", "result": {"output": simulated_output}}
1062
+
1063
+ except Exception as check_err:
1064
+ print(f"Error in check_status.py logic: {check_err}")
1065
+ traceback.print_exc()
1066
+ return {"status": "error", "message": f"Error in check_status logic: {check_err}", "result": {"output": f"Error: {check_err}"}}
1067
+
1068
+ # Add logic for other scripts if needed, or handle them as separate commands
1069
+ else:
1070
+ return {"status": "error", "message": f"Execution of script logic for '{script_name}' is not implemented in the listener."}
1071
+
1072
+ except Exception as e:
1073
+ print(f"Error executing script logic {script_name}: {e}")
1074
+ traceback.print_exc()
1075
+ return {"status": "error", "message": f"Error executing script logic {script_name}: {e}"}
1076
+
1077
+ def get_project_structure_command(args):
1078
+ """Handles the 'get_project_structure' command."""
1079
+ global active_project
1080
+ project_path = args.get("project_path")
1081
+ if not project_path:
1082
+ return {"status": "error", "message": "Missing 'project_path' argument for get_project_structure"}
1083
+
1084
+ # Ensure the correct project is active
1085
+ project = ensure_project_is_active(project_path)
1086
+ if not project:
1087
+ return {"status": "error", "message": f"Could not ensure project '{project_path}' is active to get structure."}
1088
+
1089
+ try:
1090
+ print(f"Attempting to get structure for project: {project.path}")
1091
+
1092
+ # Use the actual CODESYS Scripting API to get the project structure
1093
+ # This requires traversing the project object hierarchy
1094
+ structure_lines = []
1095
+ structure_lines.append(f"Project: {getattr(project, 'get_name', lambda: os.path.basename(project.path))()}")
1096
+
1097
+ def traverse_objects(obj, indent_level):
1098
+ indent = " " * indent_level
1099
+ try:
1100
+ if hasattr(obj, 'get_children'):
1101
+ children = obj.get_children(False) # Get direct children
1102
+ for child in children:
1103
+ child_name = getattr(child, 'get_name', lambda: str(child))()
1104
+ child_type = type(child).__name__ # Get object type name
1105
+ line = f"{indent}{child_name} ({child_type})"
1106
+ # Add more details if available (e.g., POU type)
1107
+ if hasattr(child, 'pou_type'):
1108
+ line = f"{indent}POU: {child_name} ({getattr(child.pou_type, 'name', str(child.pou_type))})" # Assuming pou_type is an enum or has a name
1109
+ elif hasattr(child, 'is_application') and child.is_application:
1110
+ line = f"{indent}Application: {child_name}"
1111
+ elif hasattr(child, 'is_device') and child.is_device:
1112
+ line = f"{indent}Device: {child_name}"
1113
+ elif hasattr(child, 'is_folder') and child.is_folder:
1114
+ line = f"{indent}Folder: {child_name}"
1115
+ elif hasattr(child, 'is_library_manager') and child.is_library_manager:
1116
+ line = f"{indent}Library Manager"
1117
+ elif hasattr(child, 'is_task_configuration') and child.is_task_configuration:
1118
+ line = f"{indent}Task Configuration"
1119
+ # Add more type checks as needed
1120
+
1121
+ structure_lines.append(line)
1122
+ traverse_objects(child, indent_level + 1) # Recurse
1123
+
1124
+ except Exception as traverse_err:
1125
+ print(f"WARN: Error traversing object '{getattr(obj, 'get_name', lambda: str(obj))()}': {traverse_err}")
1126
+ # Continue traversal for other siblings if possible
1127
+
1128
+ # Start traversal from the project's root children
1129
+ if hasattr(project, 'get_children'):
1130
+ traverse_objects(project, 1)
1131
+ else:
1132
+ print("WARN: Project object does not have 'get_children' method.")
1133
+ structure_lines.append(" Could not retrieve project children.")
1134
+
1135
+
1136
+ structure_text = "\n".join(structure_lines)
1137
+
1138
+ print("Project structure retrieved successfully.")
1139
+ return {"status": "success", "message": "Project structure retrieved.", "structure": structure_text}
1140
+
1141
+ except Exception as e:
1142
+ print(f"Error getting project structure: {e}")
1143
+ traceback.print_exc()
1144
+ return {"status": "error", "message": f"Error getting project structure: {e}"}
1145
+
1146
+ def get_pou_code_command(args):
1147
+ """Handles the 'get_pou_code' command."""
1148
+ global active_project
1149
+ project_path = args.get("project_path")
1150
+ pou_path = args.get("pou_path")
1151
+ if not project_path or not pou_path:
1152
+ return {"status": "error", "message": "Missing arguments for get_pou_code"}
1153
+
1154
+ # Ensure the correct project is active
1155
+ project = ensure_project_is_active(project_path)
1156
+ if not project:
1157
+ return {"status": "error", "message": f"Could not ensure project '{project_path}' is active to get POU code."}
1158
+
1159
+ try:
1160
+ print(f"Attempting to get code for POU '{pou_path}' in project: {project.path}")
1161
+
1162
+ # Find the POU object using the robust function
1163
+ pou_object = find_object_by_path_robust(project, pou_path, "POU object")
1164
+ if not pou_object:
1165
+ return {"status": "error", "message": f"POU object not found: {pou_path}"}
1166
+
1167
+ declaration_code = ""
1168
+ implementation_code = ""
1169
+
1170
+ # Use the actual CODESYS Scripting API to get the code based on set_pou_code.py
1171
+ if hasattr(pou_object, 'textual_declaration') and pou_object.textual_declaration is not None:
1172
+ try:
1173
+ # Assuming textual_declaration has a .content or similar property to get text
1174
+ # The set_pou_code.py uses .replace(), not clear how to GET the text.
1175
+ # Let's assume there's a .content or just casting to string works.
1176
+ declaration_code = str(pou_object.textual_declaration) # Attempt to cast to string
1177
+ print("Retrieved declaration code.")
1178
+ except Exception as decl_get_err:
1179
+ print(f"WARN: Could not get declaration text: {decl_get_err}")
1180
+ declaration_code = f"// Error retrieving declaration: {decl_get_err}"
1181
+ traceback.print_exc()
1182
+ else:
1183
+ print(f"Object '{getattr(pou_object, 'get_name', lambda: pou_path)()}' does not appear to have a textual_declaration.")
1184
+
1185
+
1186
+ if hasattr(pou_object, 'textual_implementation') and pou_object.textual_implementation is not None:
1187
+ try:
1188
+ # Assuming textual_implementation has a .content or similar property to get text
1189
+ implementation_code = str(pou_object.textual_implementation) # Attempt to cast to string
1190
+ print("Retrieved implementation code.")
1191
+ except Exception as impl_get_err:
1192
+ print(f"WARN: Could not get implementation text: {impl_get_err}")
1193
+ implementation_code = f"// Error retrieving implementation: {impl_get_err}"
1194
+ traceback.print_exc()
1195
+ else:
1196
+ print(f"Object '{getattr(pou_object, 'get_name', lambda: pou_path)()}' does not appear to have a textual_implementation.")
1197
+
1198
+
1199
+ # Format the output similar to the original server.ts resource
1200
+ code_text = f"// ----- Declaration -----\n{declaration_code}\n\n// ----- Implementation -----\n{implementation_code}"
1201
+
1202
+ print("POU code retrieved successfully.")
1203
+ return {"status": "success", "message": "POU code retrieved.", "code": code_text}
1204
+
1205
+ except Exception as e:
1206
+ print(f"Error getting POU code: {e}")
1207
+ traceback.print_exc()
1208
+ return {"status": "error", "message": f"Error getting POU code: {e}"}
1209
+
1210
+
1211
+ # Add more command handler functions here for other operations (write, export, etc.)
1212
+ # Implement login/logout/read_variable using ScriptOnline based on the stub file provided earlier
1213
+
1214
+ def login_command(args):
1215
+ """Handles the 'login' command using ScriptOnline."""
1216
+ global online_app, online_device
1217
+ # project_file_path = args.get("project_file_path") # Login might be project specific
1218
+
1219
+ # Ensure a project is open and an application/device is selected if necessary
1220
+ # This part is highly dependent on your project structure and how you identify the target
1221
+ # For simplicity, let's assume we can create a default online application for the active project
1222
+ project = scriptengine.projects.primary # Use the primary project
1223
+ if not project:
1224
+ return {"status": "error", "message": "No active project to login with."}
1225
+
1226
+ try:
1227
+ print("Attempting to create online application and device...")
1228
+ # Use the actual ScriptOnline API based on the stub file
1229
+ # Need to get the application object from the project
1230
+ target_app = None
1231
+ if hasattr(project, 'active_application'):
1232
+ target_app = project.active_application
1233
+ if not target_app and hasattr(project, 'find'):
1234
+ apps = project.find("Application", True)
1235
+ if apps: target_app = apps[0]
1236
+
1237
+ if not target_app:
1238
+ return {"status": "error", "message": "No application found in the active project to login with."}
1239
+
1240
+ online_app = ScriptOnline.create_online_application(application=target_app) # Pass the target application
1241
+ # Need to get the device object from the application or project
1242
+ target_device = None
1243
+ if hasattr(target_app, 'device'): # Assuming application has a device attribute
1244
+ target_device = target_app.device
1245
+ elif hasattr(project, 'device'): # Assuming project has a device attribute
1246
+ target_device = project.device
1247
+ # If still no device, try finding one
1248
+ if not target_device and hasattr(project, 'find'):
1249
+ devices = project.find("Device", True)
1250
+ if devices: target_device = devices[0]
1251
+
1252
+
1253
+ if not target_device:
1254
+ # Clean up online_app if created
1255
+ if online_app: online_app.Dispose(); online_app = None
1256
+ return {"status": "error", "message": "No device found in the active project to login with."}
1257
+
1258
+
1259
+ online_device = ScriptOnline.create_online_device(device=target_device) # Pass the target device
1260
+
1261
+ if online_app and online_device:
1262
+ print("Online application and device objects created.")
1263
+ # Perform the login
1264
+ # Use OnlineChangeOption from scriptengine
1265
+ change_option = scriptengine.OnlineChangeOption.Try # Example option
1266
+ delete_foreign_apps = False # Example option
1267
+ print(f"Attempting to login with change_option={change_option}, delete_foreign_apps={delete_foreign_apps}...")
1268
+ online_app.login(change_option, delete_foreign_apps) # Use lowercase login
1269
+
1270
+ # Check if login was successful (conceptual - stub has is_logged_in property)
1271
+ # The login() method itself might raise an exception on failure.
1272
+ # If it returns without exception, assume success for now.
1273
+ # if online_app.is_logged_in: # Check property after calling login()
1274
+ print("Login operation called. Check online status via resource.")
1275
+ return {"status": "success", "message": "Login operation initiated. Check project status resource for online status."}
1276
+ # else:
1277
+ # print("Login failed (is_logged_in is False after call).")
1278
+ # # Clean up if login fails
1279
+ # if online_app: online_app.Dispose(); online_app = None
1280
+ # if online_device: online_device.Dispose(); online_device = None
1281
+ # return {"status": "error", "message": "Login failed."}
1282
+
1283
+ else:
1284
+ print("Failed to create online application or device objects.")
1285
+ # Clean up
1286
+ if online_app: online_app.Dispose(); online_app = None
1287
+ if online_device: online_device.Dispose(); online_device = None
1288
+ return {"status": "error", "message": "Failed to create online application or device objects."}
1289
+
1290
+ except Exception as e:
1291
+ print(f"Error during login: {e}")
1292
+ traceback.print_exc()
1293
+ # Clean up if login fails
1294
+ if online_app:
1295
+ try: online_app.Dispose()
1296
+ except: pass
1297
+ online_app = None
1298
+ if online_device:
1299
+ try: online_device.Dispose()
1300
+ except: pass
1301
+ online_device = None
1302
+ return {"status": "error", "message": f"Error during login: {e}"}
1303
+
1304
+ def logout_command(args):
1305
+ """Handles the 'logout' command using ScriptOnline."""
1306
+ global online_app, online_device
1307
+ try:
1308
+ print("Attempting to logout and dispose online objects...")
1309
+ if online_app:
1310
+ online_app.logout() # Use lowercase logout
1311
+ online_app.Dispose() # Use uppercase Dispose
1312
+ online_app = None
1313
+ print("Online application logged out and disposed.")
1314
+ if online_device:
1315
+ online_device.Dispose() # Use uppercase Dispose
1316
+ online_device = None
1317
+ print("Online device disposed.")
1318
+
1319
+ if online_app is None and online_device is None:
1320
+ return {"status": "success", "message": "Logged out and disposed online objects."}
1321
+ else:
1322
+ return {"status": "error", "message": "Failed to fully dispose online objects."}
1323
+
1324
+ except Exception as e:
1325
+ print(f"Error during logout: {e}")
1326
+ traceback.print_exc()
1327
+ return {"status": "error", "message": f"Error during logout: {e}"}
1328
+
1329
+ def read_variable_command(args):
1330
+ """Handles the 'read_variable' command using ScriptOnline."""
1331
+ global online_app
1332
+ expression = args.get("expression")
1333
+ if not expression:
1334
+ return {"status": "error", "message": "Missing 'expression' argument for read_variable"}
1335
+
1336
+ if online_app is None:
1337
+ return {"status": "error", "message": "Not logged in. Cannot read variable."}
1338
+
1339
+ try:
1340
+ print(f"Attempting to read variable: {expression}")
1341
+ # Use the actual ScriptOnlineApplication API based on the stub file
1342
+ # Assuming read_value returns the value as a string
1343
+ value = online_app.read_value(expression) # Use lowercase read_value
1344
+ print(f"Read value for {expression}: {value}")
1345
+ return {"status": "success", "value": value}
1346
+ except Exception as e:
1347
+ print(f"Error reading variable {expression}: {e}")
1348
+ traceback.print_exc()
1349
+ return {"status": "error", "message": f"Error reading variable {expression}: {e}"}
1350
+
1351
+ # Add more command handler functions here for other operations (write, export, etc.)
1352
+ # Implement write_variable using set_prepared_value and write_prepared_values
1353
+ def write_variable_command(args):
1354
+ """Handles the 'write_variable' command using ScriptOnline."""
1355
+ global online_app
1356
+ expression = args.get("expression")
1357
+ value = args.get("value")
1358
+ if not expression or value is None: # Value can be 0, False, empty string, so check for None
1359
+ return {"status": "error", "message": "Missing 'expression' or 'value' argument for write_variable"}
1360
+
1361
+ if online_app is None:
1362
+ return {"status": "error", "message": "Not logged in. Cannot write variable."}
1363
+
1364
+ try:
1365
+ print(f"Attempting to write variable: {expression} with value: {value}")
1366
+ # Use the actual ScriptOnlineApplication API based on the stub file
1367
+ # Prepare the value first
1368
+ online_app.set_prepared_value(expression, str(value)) # set_prepared_value expects string value
1369
+ print(f"Prepared value for {expression} = {value}. Attempting to write prepared values...")
1370
+ # Write all prepared values
1371
+ online_app.write_prepared_values() # Use lowercase write_prepared_values
1372
+ print("write_prepared_values operation called.")
1373
+ # Note: write_prepared_values might raise ValuesFailedException on failure
1374
+
1375
+ return {"status": "success", "message": "Write operation initiated. Check online status and messages."}
1376
+ except Exception as e:
1377
+ print(f"Error writing variable {expression}: {e}")
1378
+ traceback.print_exc()
1379
+ # Check if it's a ValuesFailedException to get failed expressions
1380
+ if isinstance(e, scriptengine.ValuesFailedException): # Assuming ValuesFailedException is in scriptengine
1381
+ failed_expr = getattr(e, 'failed_expressions', ['Unknown']) # Get failed expressions if available
1382
+ return {"status": "error", "message": f"Error writing variable {expression}: {e}", "failed_expressions": failed_expr}
1383
+ else:
1384
+ return {"status": "error", "message": f"Error writing variable {expression}: {e}"}
1385
+
1386
+
1387
+ def process_command(command_data):
1388
+ """Parses and processes incoming commands."""
1389
+ try:
1390
+ command_obj = json.loads(command_data)
1391
+ command_type = command_obj.get("command")
1392
+ args = command_obj.get("args", {})
1393
+
1394
+ print(f"Processing command type: {command_type}")
1395
+
1396
+ # Map command types to handler functions
1397
+ command_handlers = {
1398
+ "exit": lambda a: {"status": "success", "message": "Shutdown requested."}, # Handled in handle_client loop break
1399
+ "open_project": open_project_command,
1400
+ "create_project": create_project_command,
1401
+ "save_project": save_project_command,
1402
+ "create_pou": create_pou_command,
1403
+ "set_pou_code": set_pou_code_command,
1404
+ "create_method": create_method_command,
1405
+ "create_property": create_property_command,
1406
+ "compile_project": compile_project_command,
1407
+ "execute_script": execute_script_command, # Keep for check_status example
1408
+ "get_project_structure": get_project_structure_command,
1409
+ "get_pou_code": get_pou_code_command,
1410
+ "login": login_command, # Add login command handler
1411
+ "logout": logout_command, # Add logout command handler
1412
+ "read_variable": read_variable_command, # Add read_variable command handler
1413
+ "write_variable": write_variable_command, # Add write_variable command handler
1414
+ # Add more mappings here for other tools/resources
1415
+ }
1416
+
1417
+ handler = command_handlers.get(command_type)
1418
+
1419
+ if handler:
1420
+ return handler(args)
1421
+ else:
1422
+ return {"status": "error", "message": f"Unknown command: {command_type}"}
1423
+
1424
+ except json.JSONDecodeError:
1425
+ print(f"Error decoding JSON: {command_data}")
1426
+ traceback.print_exc()
1427
+ return {"status": "error", "message": "Invalid JSON command received."}
1428
+ except Exception as e:
1429
+ print(f"Error processing command: {e}")
1430
+ traceback.print_exc()
1431
+ return {"status": "error", "message": f"Error processing command: {e}"}
1432
+
1433
+ def handle_client(conn):
1434
+ """Handles communication with a single client connection."""
1435
+ print(f"Connected by {conn.getpeername()}")
1436
+ socket_buffer = "" # Buffer for incomplete messages
1437
+ try:
1438
+ while True:
1439
+ # Receive data
1440
+ data = conn.recv(4096)
1441
+ if not data:
1442
+ print("Client disconnected.")
1443
+ break
1444
+ socket_buffer += data.decode('utf-8')
1445
+
1446
+ # Process messages delimited by newline
1447
+ while '\n' in socket_buffer:
1448
+ message, socket_buffer = socket_buffer.split('\n', 1)
1449
+ if not message.strip(): # Skip empty lines
1450
+ continue
1451
+
1452
+ print(f"Received message: {message}")
1453
+ response = process_command(message)
1454
+ print(f"Sending response: {response}")
1455
+ # Send response as JSON string followed by a newline
1456
+ conn.sendall((json.dumps(response) + '\n').encode('utf-8'))
1457
+
1458
+ # Check for the exit command response to signal shutdown
1459
+ if response.get("command") == "exit" and response.get("status") == "success":
1460
+ print("Exit command processed. Closing client connection.")
1461
+ return # Exit the handler loop
1462
+
1463
+ except Exception as e:
1464
+ print(f"Error handling client: {e}")
1465
+ traceback.print_exc()
1466
+ finally:
1467
+ print(f"Closing connection from {conn.getpeername()}")
1468
+ conn.close()
1469
+ # In a single-client-expected scenario, you might want to signal
1470
+ # the main listener loop to exit here if the client disconnects unexpectedly.
1471
+ # For this example, the listener loop continues to accept new connections.
1472
+
1473
+
1474
+ def run_listener():
1475
+ """Starts the socket listener."""
1476
+ global online_app, online_device, active_project
1477
+ server_socket = None
1478
+ try:
1479
+ server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1480
+ server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # Allow reusing the address
1481
+ server_socket.bind((HOST, PORT))
1482
+ server_socket.listen(1) # Listen for only 1 incoming connection (assuming one MCP server client)
1483
+ print(f"CODESYS Listener Script listening on {HOST}:{PORT}")
1484
+
1485
+ # Keep accepting connections until an external shutdown signal is received
1486
+ # A more robust implementation might use a flag or event for shutdown
1487
+ # triggered by the 'exit' command processed in handle_client.
1488
+ # For this example, the loop continues indefinitely until script termination.
1489
+ while True:
1490
+ print("Waiting for a connection...")
1491
+ conn, addr = server_socket.accept()
1492
+ # Handle the client connection directly in the main thread
1493
+ # This simplifies managing the active_project and online_app globals
1494
+ # If concurrent requests were needed, threading would be necessary,
1495
+ # but managing shared state (active_project, online_app) would be complex.
1496
+ handle_client(conn)
1497
+
1498
+ except Exception as e:
1499
+ print(f"Error in listener: {e}")
1500
+ traceback.print_exc()
1501
+ finally:
1502
+ print("Listener shutting down.")
1503
+ if server_socket:
1504
+ server_socket.close()
1505
+ # Ensure online objects and project are disposed on script exit
1506
+ if online_app:
1507
+ try:
1508
+ online_app.Dispose() # Use uppercase Dispose
1509
+ except:
1510
+ pass
1511
+ online_app = None
1512
+ if online_device:
1513
+ try:
1514
+ online_device.Dispose() # Use uppercase Dispose
1515
+ except:
1516
+ pass
1517
+ online_device = None
1518
+ if active_project:
1519
+ try:
1520
+ # Check if the project object has a close method before calling
1521
+ if hasattr(active_project, 'Close'): # Assuming Close method exists and is uppercase
1522
+ active_project.Close()
1523
+ elif hasattr(active_project, 'close'): # Check for lowercase too
1524
+ active_project.close()
1525
+ except:
1526
+ pass
1527
+ active_project = None
1528
+ print("Online objects and project disposed.")
1529
+
1530
+
1531
+ if __name__ == "__main__":
1532
+ print("CODESYS Listener Script starting...")
1533
+ # You might need to add CODESYS initialization code here if necessary
1534
+ # e.g., setting up the environment, opening a default project on startup
1535
+ # The server.ts will handle launching CODESYS with this script.
1536
+
1537
+ run_listener()
1538
+
1539
+ print("CODESYS Listener Script finished.")