@codesys/mcp-toolkit 1.1.5 → 1.1.6
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 +2 -5
- package/CHANGELOG.md +3 -5
- package/dist/bin.js +2 -41
- package/dist/server.js +319 -503
- package/package.json +3 -3
- package/codesys_scripts/listener_script.py +0 -1539
|
@@ -1,1539 +0,0 @@
|
|
|
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.")
|