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:
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env python
|
||||
"""Write I/O variable mappings in a CODESYS project via ScriptEngine."""
|
||||
|
||||
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 point to a .project or .projectarchive file. Got: {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 directory: {0}".format(path_value))
|
||||
raise RuntimeError("Multiple project files found: {0}".format(", ".join(candidates)))
|
||||
raise RuntimeError("Project 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_device_by_path(proj, device_path):
|
||||
"""Walk the tree to find a device node matching the given slash-separated path."""
|
||||
parts = [p.strip() for p in device_path.split("/") if p.strip()]
|
||||
current_nodes = _children_of(proj)
|
||||
target = None
|
||||
|
||||
for i, part in enumerate(parts):
|
||||
found = None
|
||||
for node in current_nodes:
|
||||
if _name_of(node) == part:
|
||||
found = node
|
||||
break
|
||||
if found is None:
|
||||
raise RuntimeError(
|
||||
"Device path segment '{0}' not found at depth {1}. Path: {2}".format(part, i, device_path)
|
||||
)
|
||||
if i == len(parts) - 1:
|
||||
target = found
|
||||
else:
|
||||
current_nodes = _children_of(found)
|
||||
|
||||
return target
|
||||
|
||||
|
||||
def _find_param(param_set, param_id=None, param_name=None):
|
||||
"""Find a parameter by numeric id or name."""
|
||||
for param in list(param_set):
|
||||
if param_id is not None:
|
||||
try:
|
||||
if int(param.id) == int(param_id):
|
||||
return param
|
||||
except Exception:
|
||||
pass
|
||||
if param_name is not None:
|
||||
try:
|
||||
pname = _safe_str(getattr(param, "visible_name", "") or getattr(param, "name", ""))
|
||||
if pname == param_name:
|
||||
return param
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
payload = load_payload()
|
||||
project_path = payload.get("project_path")
|
||||
if not project_path:
|
||||
raise ValueError("project_path is required.")
|
||||
|
||||
mappings = payload.get("mappings", [])
|
||||
import_csv_path = payload.get("import_csv_path", "")
|
||||
dry_run = bool(payload.get("dry_run", False))
|
||||
|
||||
if not mappings and not import_csv_path:
|
||||
raise ValueError("Provide 'mappings' list or 'import_csv_path'.")
|
||||
|
||||
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 import_csv_path:
|
||||
try:
|
||||
for top in _children_of(proj):
|
||||
if getattr(top, "is_device", False):
|
||||
if dry_run:
|
||||
results.append({
|
||||
"action": "import_csv",
|
||||
"csv_path": import_csv_path,
|
||||
"dry_run": True,
|
||||
"status": "would_import",
|
||||
})
|
||||
else:
|
||||
top.import_io_mappings_from_csv(import_csv_path)
|
||||
results.append({
|
||||
"action": "import_csv",
|
||||
"csv_path": import_csv_path,
|
||||
"status": "imported",
|
||||
})
|
||||
break
|
||||
except Exception as exc:
|
||||
warnings.append("csv_import_failed: {0}".format(exc))
|
||||
|
||||
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:
|
||||
warnings.append("mapping skipped: missing device_path")
|
||||
continue
|
||||
if not variable:
|
||||
warnings.append("mapping skipped: missing variable for {0}".format(device_path))
|
||||
continue
|
||||
|
||||
try:
|
||||
device = _find_device_by_path(proj, device_path)
|
||||
if device is None:
|
||||
warnings.append("device not found: {0}".format(device_path))
|
||||
continue
|
||||
|
||||
param = _find_param(device.device_parameters, param_id=param_id, param_name=param_name)
|
||||
if param is None:
|
||||
warnings.append("param not found: id={0} name={1} on {2}".format(param_id, param_name, device_path))
|
||||
continue
|
||||
|
||||
iom = param.io_mapping
|
||||
if iom is None:
|
||||
warnings.append("no io_mapping on param {0} of {1}".format(param_id or param_name, device_path))
|
||||
continue
|
||||
|
||||
old_var = _safe_str(iom.variable) if iom.variable else ""
|
||||
entry = {
|
||||
"device_path": device_path,
|
||||
"param_id": int(param.id) if hasattr(param, "id") else -1,
|
||||
"param_name": _safe_str(getattr(param, "visible_name", "")),
|
||||
"old_variable": old_var,
|
||||
"new_variable": variable,
|
||||
}
|
||||
|
||||
if dry_run:
|
||||
entry["status"] = "would_map"
|
||||
else:
|
||||
iom.variable = variable
|
||||
entry["status"] = "mapped"
|
||||
|
||||
results.append(entry)
|
||||
except Exception as exc:
|
||||
warnings.append("mapping_failed({0}): {1}".format(device_path, 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_io_mapping",
|
||||
"project_path": pp,
|
||||
"dry_run": dry_run,
|
||||
"results": results,
|
||||
"result_count": len(results),
|
||||
"warnings": warnings,
|
||||
})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user