223 lines
7.0 KiB
Python
223 lines
7.0 KiB
Python
#!/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()
|