#!/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()