Update .gitignore and README.md for CODESYS MCP project. Added entries to .gitignore for VSIX files and VS Code settings. Expanded README to include architecture, components, deployment instructions, and details about the Cursor extension.

This commit is contained in:
2026-05-24 15:47:21 +03:00
parent acdd740d01
commit bedc5d0628
55 changed files with 11120 additions and 3 deletions

View File

@@ -0,0 +1,208 @@
#!/usr/bin/env python
"""Write/update textual objects (POU, FB, GVL, NVL, struct, program) in a CODESYS project."""
from __future__ import print_function
import os
from _common import emit, load_payload
def _discover_project_file(path_value):
if os.path.isfile(path_value):
lower_path = path_value.lower()
if lower_path.endswith(".project") or lower_path.endswith(".projectarchive"):
return path_value
raise RuntimeError("project_path must be .project or .projectarchive: {0}".format(path_value))
if os.path.isdir(path_value):
candidates = []
for entry in os.listdir(path_value):
full_path = os.path.join(path_value, entry)
if os.path.isfile(full_path):
lower_name = entry.lower()
if lower_name.endswith(".project") or lower_name.endswith(".projectarchive"):
candidates.append(full_path)
if len(candidates) == 1:
return candidates[0]
if not candidates:
raise RuntimeError("No .project/.projectarchive in: {0}".format(path_value))
raise RuntimeError("Multiple project files: {0}".format(", ".join(candidates)))
raise RuntimeError("Path does not exist: {0}".format(path_value))
def _safe_str(val):
try:
return "{0}".format(val)
except Exception:
return "?"
def _name_of(obj):
try:
return obj.get_name(False)
except Exception:
try:
return obj.get_name()
except Exception:
return _safe_str(obj)
def _children_of(obj):
try:
return list(obj.get_children(False))
except Exception:
try:
return list(obj.get_children())
except Exception:
return []
def _find_by_path(root, path_str):
"""Walk slash-separated path from project root to find the target object."""
parts = [p.strip() for p in path_str.split("/") if p.strip()]
current = root
for i, part in enumerate(parts):
found = None
for child in _children_of(current):
if _name_of(child) == part:
found = child
break
if found is None:
raise RuntimeError("Object not found at '{0}' (segment {1} of '{2}')".format(
part, i, path_str))
current = found
return current
def _find_by_name(root, name):
"""Use project.find() with recursive search."""
results = root.find(name, True)
if not results:
raise RuntimeError("Object '{0}' not found in project.".format(name))
if len(results) > 1:
paths = []
for r in results:
try:
paths.append(_name_of(r))
except Exception:
paths.append("?")
raise RuntimeError("Multiple objects named '{0}': {1}. Use object_path instead.".format(
name, ", ".join(paths)))
return results[0]
def _set_text(doc, new_text):
"""Replace full text of a ScriptTextDocument."""
try:
doc.replace(new_text=new_text)
except TypeError:
length = doc.length
if length > 0:
doc.remove(offset=0, length=length)
if new_text:
doc.append(new_text)
def main():
payload = load_payload()
project_path = payload.get("project_path")
if not project_path:
raise ValueError("project_path is required.")
updates = payload.get("updates", [])
if not updates:
single = {}
for key in ("object_path", "object_name", "declaration", "implementation"):
if key in payload:
single[key] = payload[key]
if single:
updates = [single]
if not updates:
raise ValueError("Provide 'updates' list or single object_path/object_name + declaration/implementation.")
dry_run = bool(payload.get("dry_run", False))
pp = os.path.abspath(os.path.expanduser(project_path))
pp = _discover_project_file(pp)
if "projects" not in globals():
raise RuntimeError("Run through CODESYS ScriptEngine (--runscript).")
if projects.primary:
projects.primary.close()
proj = projects.open(pp, primary=True)
results = []
warnings = []
for upd in updates:
obj_path = upd.get("object_path", "")
obj_name = upd.get("object_name", "")
decl_text = upd.get("declaration")
impl_text = upd.get("implementation")
if not obj_path and not obj_name:
warnings.append("update skipped: no object_path or object_name")
continue
try:
if obj_path:
obj = _find_by_path(proj, obj_path)
else:
obj = _find_by_name(proj, obj_name)
name = _name_of(obj)
entry = {
"name": name,
"object_path": obj_path or obj_name,
"has_declaration": getattr(obj, "has_textual_declaration", False),
"has_implementation": getattr(obj, "has_textual_implementation", False),
"declaration_updated": False,
"implementation_updated": False,
}
if decl_text is not None:
if not getattr(obj, "has_textual_declaration", False):
warnings.append("{0}: object has no textual declaration".format(name))
elif dry_run:
entry["declaration_updated"] = "would_update"
else:
_set_text(obj.textual_declaration, decl_text)
entry["declaration_updated"] = True
if impl_text is not None:
if not getattr(obj, "has_textual_implementation", False):
warnings.append("{0}: object has no textual implementation".format(name))
elif dry_run:
entry["implementation_updated"] = "would_update"
else:
_set_text(obj.textual_implementation, impl_text)
entry["implementation_updated"] = True
results.append(entry)
except Exception as exc:
warnings.append("update_failed({0}): {1}".format(obj_path or obj_name, exc))
if not dry_run:
try:
proj.save()
except Exception as exc:
warnings.append("project_save_failed: {0}".format(exc))
proj.close()
emit({
"ok": True,
"action": "write_pou",
"project_path": pp,
"dry_run": dry_run,
"results": results,
"result_count": len(results),
"warnings": warnings,
})
if __name__ == "__main__":
main()