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:
318
python_scripting/windows_codesys_agent/scripts/manage_device.py
Normal file
318
python_scripting/windows_codesys_agent/scripts/manage_device.py
Normal file
@@ -0,0 +1,318 @@
|
||||
#!/usr/bin/env python
|
||||
"""Add, remove, or update devices 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 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:
|
||||
raise RuntimeError("Not found: '{0}' at segment {1} of '{2}'".format(part, i, path_str))
|
||||
current = found
|
||||
return current
|
||||
|
||||
|
||||
def _get_device_id(obj):
|
||||
try:
|
||||
did = obj.get_device_identification()
|
||||
return {"type": int(did.type), "id": _safe_str(did.id), "version": _safe_str(did.version)}
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _action_add(proj, op, warnings):
|
||||
parent_path = op.get("parent_path", "")
|
||||
name = op.get("name", "")
|
||||
dev_type = op.get("device_type")
|
||||
dev_id = op.get("device_id", "")
|
||||
dev_version = op.get("device_version", "")
|
||||
module_id = op.get("module_id")
|
||||
|
||||
if not parent_path or not name or dev_type is None or not dev_id or not dev_version:
|
||||
raise ValueError("add requires: parent_path, name, device_type, device_id, device_version")
|
||||
|
||||
parent = _find_by_path(proj, parent_path)
|
||||
args = [name, int(dev_type), dev_id, dev_version]
|
||||
if module_id is not None:
|
||||
args.append(module_id)
|
||||
|
||||
method = op.get("method", "add")
|
||||
if method == "plug":
|
||||
parent.plug(*args)
|
||||
elif method == "insert":
|
||||
index = int(op.get("index", -1))
|
||||
parent.insert(name, index, int(dev_type), dev_id, dev_version)
|
||||
else:
|
||||
parent.add(*args)
|
||||
|
||||
return {
|
||||
"action": "add",
|
||||
"parent_path": parent_path,
|
||||
"name": name,
|
||||
"device_type": dev_type,
|
||||
"device_id": dev_id,
|
||||
"device_version": dev_version,
|
||||
"status": "added",
|
||||
}
|
||||
|
||||
|
||||
def _action_remove(proj, op, warnings):
|
||||
device_path = op.get("device_path", "")
|
||||
if not device_path:
|
||||
raise ValueError("remove requires: device_path")
|
||||
obj = _find_by_path(proj, device_path)
|
||||
name = _name_of(obj)
|
||||
dev_id = _get_device_id(obj)
|
||||
obj.remove()
|
||||
return {
|
||||
"action": "remove",
|
||||
"device_path": device_path,
|
||||
"name": name,
|
||||
"device_id": dev_id,
|
||||
"status": "removed",
|
||||
}
|
||||
|
||||
|
||||
def _action_update(proj, op, warnings):
|
||||
device_path = op.get("device_path", "")
|
||||
dev_type = op.get("device_type")
|
||||
dev_id = op.get("device_id", "")
|
||||
dev_version = op.get("device_version", "")
|
||||
module_id = op.get("module_id")
|
||||
|
||||
if not device_path or dev_type is None or not dev_id or not dev_version:
|
||||
raise ValueError("update requires: device_path, device_type, device_id, device_version")
|
||||
|
||||
obj = _find_by_path(proj, device_path)
|
||||
old_id = _get_device_id(obj)
|
||||
|
||||
args = [int(dev_type), dev_id, dev_version]
|
||||
if module_id is not None:
|
||||
args.append(module_id)
|
||||
obj.update(*args)
|
||||
|
||||
return {
|
||||
"action": "update",
|
||||
"device_path": device_path,
|
||||
"old_device_id": old_id,
|
||||
"new_device_id": {"type": dev_type, "id": dev_id, "version": dev_version},
|
||||
"status": "updated",
|
||||
}
|
||||
|
||||
|
||||
def _action_rename(proj, op, warnings):
|
||||
device_path = op.get("device_path", "")
|
||||
new_name = op.get("new_name", "")
|
||||
if not device_path or not new_name:
|
||||
raise ValueError("rename requires: device_path, new_name")
|
||||
obj = _find_by_path(proj, device_path)
|
||||
old_name = _name_of(obj)
|
||||
obj.rename(new_name)
|
||||
return {
|
||||
"action": "rename",
|
||||
"device_path": device_path,
|
||||
"old_name": old_name,
|
||||
"new_name": new_name,
|
||||
"status": "renamed",
|
||||
}
|
||||
|
||||
|
||||
def _action_enable(proj, op, warnings):
|
||||
device_path = op.get("device_path", "")
|
||||
enabled = op.get("enabled", True)
|
||||
if not device_path:
|
||||
raise ValueError("enable/disable requires: device_path")
|
||||
obj = _find_by_path(proj, device_path)
|
||||
if enabled:
|
||||
obj.enable()
|
||||
else:
|
||||
obj.disable()
|
||||
return {
|
||||
"action": "enable" if enabled else "disable",
|
||||
"device_path": device_path,
|
||||
"status": "enabled" if enabled else "disabled",
|
||||
}
|
||||
|
||||
|
||||
def _action_set_param(proj, op, warnings):
|
||||
"""Set a device parameter value by id or name."""
|
||||
device_path = op.get("device_path", "")
|
||||
param_id = op.get("param_id")
|
||||
param_name = op.get("param_name", "")
|
||||
value = op.get("value")
|
||||
|
||||
if not device_path:
|
||||
raise ValueError("set_param requires: device_path")
|
||||
if value is None:
|
||||
raise ValueError("set_param requires: value")
|
||||
|
||||
obj = _find_by_path(proj, device_path)
|
||||
params = obj.device_parameters
|
||||
target = None
|
||||
|
||||
for p in list(params):
|
||||
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 or _safe_str(getattr(p, "name", "")) == param_name):
|
||||
target = p
|
||||
break
|
||||
|
||||
if target is None:
|
||||
raise RuntimeError("Parameter not found: id={0} name={1} on {2}".format(param_id, param_name, device_path))
|
||||
|
||||
old_value = _safe_str(target.value) if hasattr(target, "value") else "?"
|
||||
target.value = _safe_str(value)
|
||||
|
||||
return {
|
||||
"action": "set_param",
|
||||
"device_path": device_path,
|
||||
"param_id": int(target.id),
|
||||
"param_name": _safe_str(getattr(target, "visible_name", "")),
|
||||
"old_value": old_value,
|
||||
"new_value": _safe_str(value),
|
||||
"status": "set",
|
||||
}
|
||||
|
||||
|
||||
ACTIONS = {
|
||||
"add": _action_add,
|
||||
"remove": _action_remove,
|
||||
"update": _action_update,
|
||||
"rename": _action_rename,
|
||||
"enable": _action_enable,
|
||||
"disable": _action_enable,
|
||||
"set_param": _action_set_param,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
payload = load_payload()
|
||||
project_path = payload.get("project_path")
|
||||
if not project_path:
|
||||
raise ValueError("project_path is required.")
|
||||
|
||||
operations = payload.get("operations", [])
|
||||
if not operations:
|
||||
single_action = payload.get("action_type", "")
|
||||
if single_action:
|
||||
operations = [payload]
|
||||
else:
|
||||
raise ValueError("Provide 'operations' list or a single operation with 'action_type'.")
|
||||
|
||||
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 op in operations:
|
||||
action_type = op.get("action_type", "")
|
||||
handler = ACTIONS.get(action_type)
|
||||
if not handler:
|
||||
warnings.append("unknown action_type: {0}".format(action_type))
|
||||
continue
|
||||
|
||||
try:
|
||||
if dry_run:
|
||||
results.append({
|
||||
"action": action_type,
|
||||
"dry_run": True,
|
||||
"status": "would_execute",
|
||||
"details": op,
|
||||
})
|
||||
else:
|
||||
result = handler(proj, op, warnings)
|
||||
results.append(result)
|
||||
except Exception as exc:
|
||||
warnings.append("{0}_failed: {1}".format(action_type, 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": "manage_device",
|
||||
"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