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:
314
python_scripting/windows_codesys_agent/scripts/sync_project.py
Normal file
314
python_scripting/windows_codesys_agent/scripts/sync_project.py
Normal file
@@ -0,0 +1,314 @@
|
||||
#!/usr/bin/env python
|
||||
"""Batch-push local source changes into a CODESYS project.
|
||||
|
||||
Receives a list of textual updates and IO mapping changes in a single payload,
|
||||
applies them all in one project open/save cycle.
|
||||
"""
|
||||
|
||||
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):
|
||||
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:
|
||||
return None
|
||||
current = found
|
||||
return current
|
||||
|
||||
|
||||
def _set_text(doc, new_text):
|
||||
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 _apply_textual_updates(proj, updates, results, warnings):
|
||||
for upd in updates:
|
||||
obj_path = upd.get("object_path", "")
|
||||
obj_name = upd.get("object_name", "")
|
||||
decl = upd.get("declaration")
|
||||
impl = upd.get("implementation")
|
||||
|
||||
target_label = obj_path or obj_name
|
||||
if not target_label:
|
||||
warnings.append("skipped update: no object_path or object_name")
|
||||
continue
|
||||
|
||||
try:
|
||||
obj = None
|
||||
if obj_path:
|
||||
obj = _find_by_path(proj, obj_path)
|
||||
if obj is None and obj_name:
|
||||
found = proj.find(obj_name, True)
|
||||
if found:
|
||||
obj = found[0]
|
||||
|
||||
if obj is None:
|
||||
warnings.append("not_found: {0}".format(target_label))
|
||||
continue
|
||||
|
||||
name = _name_of(obj)
|
||||
entry = {"name": name, "path": target_label, "declaration_updated": False, "implementation_updated": False}
|
||||
|
||||
if decl is not None and getattr(obj, "has_textual_declaration", False):
|
||||
_set_text(obj.textual_declaration, decl)
|
||||
entry["declaration_updated"] = True
|
||||
|
||||
if impl is not None and getattr(obj, "has_textual_implementation", False):
|
||||
_set_text(obj.textual_implementation, impl)
|
||||
entry["implementation_updated"] = True
|
||||
|
||||
results.append(entry)
|
||||
except Exception as exc:
|
||||
warnings.append("textual_update_failed({0}): {1}".format(target_label, exc))
|
||||
|
||||
|
||||
def _apply_io_mappings(proj, mappings, results, warnings):
|
||||
for m in mappings:
|
||||
device_path = m.get("device_path", "")
|
||||
param_id = m.get("param_id")
|
||||
param_name = m.get("param_name")
|
||||
variable = m.get("variable", "")
|
||||
|
||||
if not device_path or not variable:
|
||||
warnings.append("io_mapping skipped: missing device_path or variable")
|
||||
continue
|
||||
|
||||
try:
|
||||
device = _find_by_path(proj, device_path)
|
||||
if device is None:
|
||||
warnings.append("device not found: {0}".format(device_path))
|
||||
continue
|
||||
|
||||
target_param = None
|
||||
try:
|
||||
params = device.device_parameters
|
||||
for p in list(params):
|
||||
if param_id is not None and int(p.id) == int(param_id):
|
||||
target_param = p
|
||||
break
|
||||
if param_name and _safe_str(getattr(p, "visible_name", "")) == param_name:
|
||||
target_param = p
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if target_param is None:
|
||||
for conn in list(device.connectors):
|
||||
try:
|
||||
hp = conn.host_parameters
|
||||
for p in list(hp):
|
||||
if param_id is not None and int(p.id) == int(param_id):
|
||||
target_param = p
|
||||
break
|
||||
if param_name and _safe_str(getattr(p, "visible_name", "")) == param_name:
|
||||
target_param = p
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
if target_param:
|
||||
break
|
||||
|
||||
if target_param is None:
|
||||
warnings.append("param not found: id={0} name={1} on {2}".format(param_id, param_name, device_path))
|
||||
continue
|
||||
|
||||
iom = target_param.io_mapping
|
||||
if iom is None:
|
||||
warnings.append("no io_mapping for param on {0}".format(device_path))
|
||||
continue
|
||||
|
||||
old_var = _safe_str(iom.variable) if iom.variable else ""
|
||||
iom.variable = variable
|
||||
results.append({
|
||||
"type": "io_mapping",
|
||||
"device_path": device_path,
|
||||
"param_id": int(target_param.id),
|
||||
"old_variable": old_var,
|
||||
"new_variable": variable,
|
||||
"status": "mapped",
|
||||
})
|
||||
except Exception as exc:
|
||||
warnings.append("io_mapping_failed({0}): {1}".format(device_path, exc))
|
||||
|
||||
|
||||
def _apply_device_params(proj, params, results, warnings):
|
||||
for dp in params:
|
||||
device_path = dp.get("device_path", "")
|
||||
param_id = dp.get("param_id")
|
||||
param_name = dp.get("param_name", "")
|
||||
value = dp.get("value")
|
||||
|
||||
if not device_path or value is None:
|
||||
warnings.append("device_param skipped: missing device_path or value")
|
||||
continue
|
||||
|
||||
try:
|
||||
device = _find_by_path(proj, device_path)
|
||||
if device is None:
|
||||
warnings.append("device not found: {0}".format(device_path))
|
||||
continue
|
||||
|
||||
target = None
|
||||
for p in list(device.device_parameters):
|
||||
if param_id is not None and int(p.id) == int(param_id):
|
||||
target = p
|
||||
break
|
||||
if param_name and _safe_str(getattr(p, "visible_name", "")) == param_name:
|
||||
target = p
|
||||
break
|
||||
|
||||
if target is None:
|
||||
warnings.append("param not found on {0}".format(device_path))
|
||||
continue
|
||||
|
||||
old_val = _safe_str(target.value) if hasattr(target, "value") else "?"
|
||||
target.value = _safe_str(value)
|
||||
results.append({
|
||||
"type": "device_param",
|
||||
"device_path": device_path,
|
||||
"param_id": int(target.id),
|
||||
"old_value": old_val,
|
||||
"new_value": _safe_str(value),
|
||||
"status": "set",
|
||||
})
|
||||
except Exception as exc:
|
||||
warnings.append("device_param_failed({0}): {1}".format(device_path, exc))
|
||||
|
||||
|
||||
def main():
|
||||
payload = load_payload()
|
||||
project_path = payload.get("project_path")
|
||||
if not project_path:
|
||||
raise ValueError("project_path is required.")
|
||||
|
||||
dry_run = bool(payload.get("dry_run", False))
|
||||
|
||||
textual_updates = payload.get("textual_updates", [])
|
||||
io_mappings = payload.get("io_mappings", [])
|
||||
device_params = payload.get("device_params", [])
|
||||
|
||||
if not textual_updates and not io_mappings and not device_params:
|
||||
raise ValueError("Nothing to sync. Provide textual_updates, io_mappings, or device_params.")
|
||||
|
||||
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 = []
|
||||
|
||||
if dry_run:
|
||||
emit({
|
||||
"ok": True,
|
||||
"action": "sync_project",
|
||||
"project_path": pp,
|
||||
"dry_run": True,
|
||||
"textual_update_count": len(textual_updates),
|
||||
"io_mapping_count": len(io_mappings),
|
||||
"device_param_count": len(device_params),
|
||||
"message": "Dry-run: would apply {0} textual updates, {1} IO mappings, {2} device params.".format(
|
||||
len(textual_updates), len(io_mappings), len(device_params)),
|
||||
"warnings": [],
|
||||
})
|
||||
proj.close()
|
||||
return
|
||||
|
||||
if textual_updates:
|
||||
_apply_textual_updates(proj, textual_updates, results, warnings)
|
||||
if io_mappings:
|
||||
_apply_io_mappings(proj, io_mappings, results, warnings)
|
||||
if device_params:
|
||||
_apply_device_params(proj, device_params, results, warnings)
|
||||
|
||||
try:
|
||||
proj.save()
|
||||
except Exception as exc:
|
||||
warnings.append("project_save_failed: {0}".format(exc))
|
||||
|
||||
proj.close()
|
||||
|
||||
emit({
|
||||
"ok": True,
|
||||
"action": "sync_project",
|
||||
"project_path": pp,
|
||||
"dry_run": False,
|
||||
"results": results,
|
||||
"result_count": len(results),
|
||||
"warnings": warnings,
|
||||
})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user