Wraps ScriptSymbolConfigObject (ScriptLib/Stubs/scriptengine/ScriptSymbolConfigObject.pyi
and helpme-codesys.com/en/ScriptingEngine/ScriptSymbolConfigObject.html).
Discovery (read-only):
find_symbol_config locate the SymbolConfiguration object(s) in a project
list_all_signatures every POU/FB/Method (compile=true to force build)
list_all_datatypes every DUT/struct/enum/alias
list_configured_symbols only the variables actually configured for export,
with configured/maximal/effective access per variable
get_symbol_config_settings every knob: feature flags, attr filter, comment
filter, direct I/O access (+ obstacles), layout calc
Setup:
create_symbol_config application.create_symbol_config(...) -- IDEMPOTENT,
no-ops with success if a symbol config already exists
anywhere in the project tree
Mutate:
set_symbol_config_settings partial-update of any subset of the 6 knobs;
refuses to enable direct I/O if obstacles exist
set_symbol_access per-variable configured_access setter
set_signature_access_bulk expose every variable in one signature at once
Output:
export_symbol_xsd write the get_symbol_configuration_xsd() bytes
All 10 tools share a SYMCONF_HELPERS list (ensure_project_open +
find_symbol_config_object). Enum mapping for SymbolAccess /
SymbolConfigContentFeatureFlags / SymbolAttributeFilterTypes /
SymbolCommentFilterType lives partly in TS (string -> int), partly in
Python (probe enum class at runtime, fall back to int literal).
Tool count: 31 -> 41.
Plan: docs/superpowers/plans/2026-04-28-symbol-config-tools.md.
Live verification + README/function-test doc updates land in a follow-up.
181 lines
7.4 KiB
Python
181 lines
7.4 KiB
Python
import sys, scriptengine as script_engine, os, traceback, json
|
|
|
|
# set_symbol_config_settings: partial-update of every knob on the
|
|
# SymbolConfiguration object. Only fields explicitly supplied (i.e.
|
|
# the corresponding APPLY_* placeholder is '1') are written; everything
|
|
# else is left untouched.
|
|
#
|
|
# RTFM (Stubs/scriptengine/ScriptSymbolConfigObject.pyi):
|
|
# - content_feature_flags (SymbolConfigContentFeatureFlags bitmask)
|
|
# - symbol_attribute_filter_type (SymbolAttributeFilterTypes enum)
|
|
# - symbol_attribute_filter_data (str)
|
|
# - symbol_comment_filter_type (SymbolCommentFilterType enum)
|
|
# - enable_direct_io_access (bool)
|
|
# - client_side_layout_calculator_guid (System.Guid)
|
|
|
|
APPLY_CONTENT_FLAGS = "{APPLY_CONTENT_FLAGS}" == '1'
|
|
CONTENT_FLAGS_INT = "{CONTENT_FLAGS_INT}"
|
|
|
|
APPLY_ATTR_FILTER_TYPE = "{APPLY_ATTR_FILTER_TYPE}" == '1'
|
|
ATTR_FILTER_TYPE = "{ATTR_FILTER_TYPE}" # enum member name
|
|
|
|
APPLY_ATTR_FILTER_DATA = "{APPLY_ATTR_FILTER_DATA}" == '1'
|
|
ATTR_FILTER_DATA = r"{ATTR_FILTER_DATA}"
|
|
|
|
APPLY_COMMENT_FILTER_TYPE = "{APPLY_COMMENT_FILTER_TYPE}" == '1'
|
|
COMMENT_FILTER_TYPE = "{COMMENT_FILTER_TYPE}"
|
|
|
|
APPLY_DIRECT_IO = "{APPLY_DIRECT_IO}" == '1'
|
|
DIRECT_IO = "{DIRECT_IO}" == '1'
|
|
|
|
APPLY_LAYOUT = "{APPLY_LAYOUT}" == '1'
|
|
LAYOUT_CALCULATOR = "{LAYOUT_CALCULATOR}"
|
|
|
|
COMPATIBILITY_GUID_LITERAL = '00000000-0000-0000-0000-000000000000'
|
|
OPTIMIZED_GUID_LITERAL = '0141eb75-141b-4ea1-9a8c-75f952b22a6c'
|
|
|
|
|
|
def _resolve_guid(label):
|
|
label_l = (label or '').strip().lower()
|
|
if label_l in ('', 'compatibility', 'default', 'compat'):
|
|
guid_str = COMPATIBILITY_GUID_LITERAL
|
|
elif label_l in ('optimized', 'optimised', 'optimal'):
|
|
guid_str = OPTIMIZED_GUID_LITERAL
|
|
else:
|
|
guid_str = label
|
|
try:
|
|
from System import Guid
|
|
return Guid(guid_str)
|
|
except Exception as e:
|
|
print("DEBUG: System.Guid(%r) failed: %s" % (guid_str, e))
|
|
return guid_str
|
|
|
|
|
|
def _enum_member(enum_cls, name):
|
|
"""Look up an enum member case-insensitively. Returns the enum value
|
|
or raises ValueError listing the available members."""
|
|
if name is None:
|
|
raise ValueError("enum member name is None")
|
|
available = [m for m in dir(enum_cls) if not m.startswith('_')]
|
|
for m in available:
|
|
if m.lower() == name.lower():
|
|
return getattr(enum_cls, m)
|
|
raise ValueError("Unknown member '%s'. Available: %s" % (name, ', '.join(available)))
|
|
|
|
|
|
try:
|
|
print("DEBUG: set_symbol_config_settings: Project='%s'" % PROJECT_FILE_PATH)
|
|
primary_project = ensure_project_open(PROJECT_FILE_PATH)
|
|
project_basename = os.path.basename(PROJECT_FILE_PATH)
|
|
|
|
sc_obj = ensure_symbol_config(primary_project)
|
|
sc_path = symbol_config_path(primary_project, sc_obj)
|
|
print("DEBUG: Symbol Configuration: %s" % sc_path)
|
|
|
|
changes = []
|
|
|
|
if APPLY_CONTENT_FLAGS:
|
|
try:
|
|
from scriptengine import SymbolConfigContentFeatureFlags as cff_enum
|
|
except Exception:
|
|
cff_enum = None
|
|
try:
|
|
target_int = int(CONTENT_FLAGS_INT)
|
|
except Exception:
|
|
raise ValueError("contentFeatureFlags integer parse failed: %r" % CONTENT_FLAGS_INT)
|
|
# Try setting the int directly first (CODESYS enums are int-backed
|
|
# System.Enum values; the property setter usually accepts the int).
|
|
try:
|
|
sc_obj.content_feature_flags = target_int
|
|
changes.append("content_feature_flags = %d" % target_int)
|
|
print("DEBUG: content_feature_flags set to int %d" % target_int)
|
|
except Exception as e:
|
|
print("DEBUG: int assignment failed: %s -- trying enum coercion" % e)
|
|
if cff_enum is None:
|
|
raise
|
|
# Fall back to constructing the enum from int via Enum.ToObject
|
|
try:
|
|
from System import Enum
|
|
ev = Enum.ToObject(cff_enum, target_int)
|
|
sc_obj.content_feature_flags = ev
|
|
changes.append("content_feature_flags = %s (%d via Enum.ToObject)" % (ev, target_int))
|
|
except Exception as e2:
|
|
raise RuntimeError("Could not assign content_feature_flags: %s / %s" % (e, e2))
|
|
|
|
if APPLY_ATTR_FILTER_TYPE:
|
|
try:
|
|
from scriptengine import SymbolAttributeFilterTypes as af_enum
|
|
except Exception as e:
|
|
raise RuntimeError("SymbolAttributeFilterTypes not importable: %s" % e)
|
|
ev = _enum_member(af_enum, ATTR_FILTER_TYPE)
|
|
sc_obj.symbol_attribute_filter_type = ev
|
|
changes.append("symbol_attribute_filter_type = %s" % ev)
|
|
|
|
if APPLY_ATTR_FILTER_DATA:
|
|
sc_obj.symbol_attribute_filter_data = ATTR_FILTER_DATA
|
|
changes.append("symbol_attribute_filter_data = %r" % ATTR_FILTER_DATA)
|
|
|
|
if APPLY_COMMENT_FILTER_TYPE:
|
|
try:
|
|
from scriptengine import SymbolCommentFilterType as cmt_enum
|
|
except Exception as e:
|
|
raise RuntimeError("SymbolCommentFilterType not importable: %s" % e)
|
|
ev = _enum_member(cmt_enum, COMMENT_FILTER_TYPE)
|
|
sc_obj.symbol_comment_filter_type = ev
|
|
changes.append("symbol_comment_filter_type = %s" % ev)
|
|
|
|
if APPLY_DIRECT_IO:
|
|
# Defensive: if user wants to ENABLE direct IO and it's blocked
|
|
# by obstacles, refuse with the explanations rather than silently
|
|
# leaving the flag set with no effect.
|
|
if DIRECT_IO and hasattr(sc_obj, 'check_effective_direct_io_access'):
|
|
try:
|
|
obs = sc_obj.check_effective_direct_io_access()
|
|
obs_str = str(obs).lower()
|
|
if obs_str not in ('none', 'directioaccessobstacles.none', '0', 'directioaccessobstacles_none'):
|
|
explanations = []
|
|
if hasattr(sc_obj, 'get_direct_io_obstacle_explanations'):
|
|
try:
|
|
explanations = list(sc_obj.get_direct_io_obstacle_explanations(obs)) or []
|
|
except Exception:
|
|
pass
|
|
raise RuntimeError(
|
|
"Refusing to enable direct I/O access: blocked by obstacles=%s. "
|
|
"Explanations: %s" % (obs, ' | '.join(str(e) for e in explanations)))
|
|
except RuntimeError:
|
|
raise
|
|
except Exception as e:
|
|
print("DEBUG: direct I/O obstacle pre-check failed (continuing): %s" % e)
|
|
sc_obj.enable_direct_io_access = DIRECT_IO
|
|
changes.append("enable_direct_io_access = %s" % DIRECT_IO)
|
|
|
|
if APPLY_LAYOUT:
|
|
guid = _resolve_guid(LAYOUT_CALCULATOR)
|
|
sc_obj.client_side_layout_calculator_guid = guid
|
|
changes.append("client_side_layout_calculator_guid = %s" % guid)
|
|
|
|
if not changes:
|
|
print("No fields supplied; nothing changed.")
|
|
else:
|
|
primary_project.save()
|
|
|
|
result = {
|
|
'project': project_basename,
|
|
'symbol_config_path': sc_path,
|
|
'changes_applied': changes,
|
|
'change_count': len(changes),
|
|
}
|
|
print("### SYMBOL_CONFIG_SET_START ###")
|
|
print(json.dumps(result))
|
|
print("### SYMBOL_CONFIG_SET_END ###")
|
|
for ch in changes:
|
|
print("Applied: %s" % ch)
|
|
print("SCRIPT_SUCCESS: set_symbol_config_settings completed.")
|
|
sys.exit(0)
|
|
except Exception as e:
|
|
detailed = traceback.format_exc()
|
|
msg = "Error in set_symbol_config_settings for project '%s': %s\n%s" % (
|
|
PROJECT_FILE_PATH, e, detailed)
|
|
print(msg)
|
|
print("SCRIPT_ERROR: %s" % msg)
|
|
sys.exit(1)
|