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:
303
python_scripting/windows_codesys_agent/scripts/read_device_io.py
Normal file
303
python_scripting/windows_codesys_agent/scripts/read_device_io.py
Normal file
@@ -0,0 +1,303 @@
|
||||
#!/usr/bin/env python
|
||||
"""Read device I/O channels, parameters, and variable mappings from a CODESYS project."""
|
||||
|
||||
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 _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 _channel_type_str(ct):
|
||||
mapping = {0: "none", 1: "input", 2: "output", 3: "output_readonly"}
|
||||
try:
|
||||
return mapping.get(int(ct), _safe_str(ct))
|
||||
except Exception:
|
||||
return _safe_str(ct)
|
||||
|
||||
|
||||
def _read_io_mapping(param):
|
||||
"""Extract IO mapping info from a device parameter."""
|
||||
try:
|
||||
iom = param.io_mapping
|
||||
if iom is None:
|
||||
return None
|
||||
result = {}
|
||||
try:
|
||||
result["variable"] = _safe_str(iom.variable) if iom.variable else ""
|
||||
except Exception:
|
||||
result["variable"] = ""
|
||||
try:
|
||||
result["default_variable"] = _safe_str(iom.default_variable) if iom.default_variable else ""
|
||||
except Exception:
|
||||
result["default_variable"] = ""
|
||||
try:
|
||||
result["creates_variable"] = bool(iom.mapping_creates_variable)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
result["maps_to_existing"] = bool(iom.maps_to_existing_variable)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
result["auto_iec_address"] = bool(iom.automatic_iec_address)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
addr = iom.manual_iec_address
|
||||
result["iec_address"] = _safe_str(addr) if addr else ""
|
||||
except Exception:
|
||||
result["iec_address"] = ""
|
||||
return result
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _read_params(param_set, channels_only):
|
||||
"""Read device parameters, optionally filtering to I/O channels only."""
|
||||
params = []
|
||||
try:
|
||||
param_list = list(param_set)
|
||||
except Exception:
|
||||
return params
|
||||
|
||||
for param in param_list:
|
||||
try:
|
||||
ct = param.channel_type
|
||||
ct_str = _channel_type_str(ct)
|
||||
if channels_only and ct_str == "none":
|
||||
continue
|
||||
|
||||
entry = {
|
||||
"name": _safe_str(getattr(param, "visible_name", "") or getattr(param, "name", "")),
|
||||
"id": int(param.id) if hasattr(param, "id") else -1,
|
||||
"channel_type": ct_str,
|
||||
"bit_size": int(param.bit_size) if hasattr(param, "bit_size") else 0,
|
||||
}
|
||||
try:
|
||||
entry["iec_type"] = _safe_str(param.iec_type) if param.iec_type else ""
|
||||
except Exception:
|
||||
entry["iec_type"] = ""
|
||||
try:
|
||||
entry["type_string"] = _safe_str(param.type_string)
|
||||
except Exception:
|
||||
entry["type_string"] = ""
|
||||
try:
|
||||
entry["section"] = _safe_str(param.section) if param.section else ""
|
||||
except Exception:
|
||||
entry["section"] = ""
|
||||
|
||||
mapping = _read_io_mapping(param)
|
||||
if mapping is not None:
|
||||
entry["io_mapping"] = mapping
|
||||
|
||||
if getattr(param, "has_sub_elements", False):
|
||||
sub_entries = []
|
||||
try:
|
||||
for sub in list(param):
|
||||
sub_entry = {
|
||||
"name": _safe_str(getattr(sub, "visible_name", "") or getattr(sub, "identifier", "")),
|
||||
"bit_size": int(sub.bit_size) if hasattr(sub, "bit_size") else 0,
|
||||
}
|
||||
try:
|
||||
sub_entry["iec_type"] = _safe_str(sub.iec_type) if hasattr(sub, "iec_type") and sub.iec_type else ""
|
||||
except Exception:
|
||||
pass
|
||||
sub_mapping = _read_io_mapping(sub)
|
||||
if sub_mapping is not None:
|
||||
sub_entry["io_mapping"] = sub_mapping
|
||||
sub_entries.append(sub_entry)
|
||||
except Exception:
|
||||
pass
|
||||
if sub_entries:
|
||||
entry["sub_elements"] = sub_entries
|
||||
|
||||
params.append(entry)
|
||||
except Exception:
|
||||
continue
|
||||
return params
|
||||
|
||||
|
||||
def _walk_devices(node, path_parts, results, warnings, channels_only, max_depth, depth):
|
||||
if depth > max_depth:
|
||||
return
|
||||
|
||||
name = _name_of(node)
|
||||
is_dev = getattr(node, "is_device", False)
|
||||
|
||||
if is_dev:
|
||||
device_info = {
|
||||
"name": name,
|
||||
"path": "/".join(path_parts + [name]),
|
||||
}
|
||||
|
||||
dev_id = _get_device_id(node)
|
||||
if dev_id:
|
||||
device_info["device_id"] = dev_id
|
||||
|
||||
try:
|
||||
params = node.device_parameters
|
||||
device_info["parameters"] = _read_params(params, channels_only)
|
||||
device_info["parameter_count"] = len(device_info["parameters"])
|
||||
except Exception as exc:
|
||||
device_info["parameters"] = []
|
||||
device_info["parameter_count"] = 0
|
||||
warnings.append("params_failed({0}): {1}".format(name, exc))
|
||||
|
||||
connectors_info = []
|
||||
try:
|
||||
conn_set = node.connectors
|
||||
for conn in list(conn_set):
|
||||
ci = {
|
||||
"connector_id": int(conn.connector_id),
|
||||
"interface": _safe_str(conn.interface),
|
||||
"role": "parent" if int(conn.connector_role) == 0 else "child",
|
||||
}
|
||||
try:
|
||||
hp = conn.host_parameters
|
||||
ci["host_parameters"] = _read_params(hp, channels_only)
|
||||
except Exception:
|
||||
pass
|
||||
connectors_info.append(ci)
|
||||
except Exception as exc:
|
||||
warnings.append("connectors_failed({0}): {1}".format(name, exc))
|
||||
|
||||
if connectors_info:
|
||||
device_info["connectors"] = connectors_info
|
||||
|
||||
results.append(device_info)
|
||||
|
||||
for child in _children_of(node):
|
||||
try:
|
||||
_walk_devices(child, path_parts + [name], results, warnings, channels_only, max_depth, depth + 1)
|
||||
except Exception as exc:
|
||||
warnings.append("walk_child_failed({0}): {1}".format(name, exc))
|
||||
|
||||
|
||||
def main():
|
||||
payload = load_payload()
|
||||
project_path = payload.get("project_path")
|
||||
if not project_path:
|
||||
raise ValueError("project_path is required.")
|
||||
|
||||
channels_only = bool(payload.get("channels_only", True))
|
||||
max_depth = int(payload.get("max_depth", 10))
|
||||
export_csv_path = payload.get("export_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()
|
||||
|
||||
if pp.lower().endswith(".projectarchive"):
|
||||
project_dir = os.path.dirname(pp)
|
||||
proj = projects.open_archive(pp, project_dir, overwrite=False)
|
||||
else:
|
||||
proj = projects.open(pp, primary=True, allow_readonly=True)
|
||||
|
||||
devices = []
|
||||
warnings = []
|
||||
|
||||
try:
|
||||
top_nodes = _children_of(proj)
|
||||
except Exception as exc:
|
||||
top_nodes = []
|
||||
warnings.append("top_level_enumeration_failed: {0}".format(exc))
|
||||
|
||||
for top in top_nodes:
|
||||
try:
|
||||
_walk_devices(top, [], devices, warnings, channels_only, max_depth, 0)
|
||||
except Exception as exc:
|
||||
warnings.append("walk_failed: {0}".format(exc))
|
||||
|
||||
if export_csv_path:
|
||||
try:
|
||||
for top in top_nodes:
|
||||
if getattr(top, "is_device", False):
|
||||
top.export_io_mappings_as_csv(export_csv_path)
|
||||
break
|
||||
except Exception as exc:
|
||||
warnings.append("csv_export_failed: {0}".format(exc))
|
||||
|
||||
proj.close()
|
||||
|
||||
emit({
|
||||
"ok": True,
|
||||
"action": "read_device_io",
|
||||
"project_path": pp,
|
||||
"devices": devices,
|
||||
"device_count": len(devices),
|
||||
"channels_only": channels_only,
|
||||
"warnings": warnings,
|
||||
})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user