Two related v5-sweep fixes for the online/runtime tool family: 1. Auto-login helper for headless mode In headless mode each MCP call spawns a fresh CODESYS --noUI process, so the login state established by connect_to_device dies before the next call. Pre-fix, only connect_to_device and download_to_device did their own login(); the other four (start_stop_application, read_variable, write_variable, read_running_version_online) silently failed in headless with 'Application not logged in.' (start/stop) or 'Invalid expression' (read/write). They worked in persistent mode only because the login carried across calls. Added ensure_logged_in(online_app, login_wait_seconds=30) to ensure_online_connection.py. Idempotent: short-circuits via online_app.is_logged_in (persistent mode is a no-op, no extra login roundtrip). When not logged in, runs the same enum-probe + call-shape probe + STABLE_STATES settle-wait pattern as connect_to_device.py. Added to start_stop_application.py, read_variable.py, write_variable.py, read_running_version_online.py. 2. _MCP_PROJECT_VERSION GVL emitted as plain VAR_GLOBAL, not CONSTANT CODESYS inlines VAR_GLOBAL CONSTANT scalars at compile time and strips them from the online symbol table. The whole point of _MCP_PROJECT_VERSION.sVersion is to be readable live from the running PLC, so CONSTANT was the wrong storage class. read_running_version_online failed against EVERY project bumped via the old template -- 'Invalid expression' on the runtime read. Dropped CONSTANT from VERSION_GVL_DECLARATION_TEMPLATE in bump_project_version.py. Existing projects auto-migrate on the next bump because maintain_version_gvl()'s existing-GVL branch overwrites textual_declaration with the (now non-CONSTANT) template. The string is still effectively read-only at runtime -- only the bump tool updates it. read_running_version_online.py also got a more precise error message that explicitly fingerprints the 'Invalid expression' failure mode and points at the CONSTANT root cause. Useful for any user landing on a project that pre-dates this fix. Verified end-to-end against local CODESYS Control Win V3 (PLATEA, port 11740) on MCPTest2 v1.3.4.0: - connect_to_device, get_application_state, download_to_device, start_stop_application (both directions), read_variable (PLC_PRG.watchdog1 = 225 ticking), write_variable (200 -> 204 in 4s proves write took), disconnect_from_device: all 7 PASS. - read_running_version_online failure reproduced (CONSTANT inlined), fix landed -- next bump on MCPTest2 will validate. 37/37 unit/integration tests green. TEST_OVERVIEW.md updated with the v5 device sweep, with the headless-mode deep-dive, and with the broken-by-design notes on read_running_version_online.
68 lines
2.8 KiB
Python
68 lines
2.8 KiB
Python
import sys, scriptengine as script_engine, os, traceback
|
|
|
|
VARIABLE_PATH = "{VARIABLE_PATH}"
|
|
VARIABLE_VALUE = "{VARIABLE_VALUE}"
|
|
|
|
try:
|
|
print("DEBUG: write_variable script: Variable='%s', Value='%s', Project='%s'" % (
|
|
VARIABLE_PATH, VARIABLE_VALUE, PROJECT_FILE_PATH))
|
|
primary_project = ensure_project_open(PROJECT_FILE_PATH)
|
|
if not VARIABLE_PATH:
|
|
raise ValueError("Variable path empty.")
|
|
|
|
online_app, target_app = ensure_online_connection(primary_project)
|
|
app_name = getattr(target_app, 'get_name', lambda: "Unknown")()
|
|
# Auto-login -- idempotent in persistent mode, required in headless.
|
|
ensure_logged_in(online_app)
|
|
|
|
# SP21+/SP22 uses a two-step prepare-then-write pattern:
|
|
# 1) set_prepared_value(name, value) -- stage the value
|
|
# 2) write_prepared_values() -- commit the staged writes
|
|
# Older SPs (and some sandboxed configurations) expose direct
|
|
# write_value(name, value) / write(name, value). Try the modern path
|
|
# first, fall back to direct.
|
|
written = False
|
|
last_err = None
|
|
|
|
if hasattr(online_app, 'set_prepared_value') and hasattr(online_app, 'write_prepared_values'):
|
|
try:
|
|
online_app.set_prepared_value(VARIABLE_PATH, VARIABLE_VALUE)
|
|
online_app.write_prepared_values()
|
|
print("DEBUG: set_prepared_value + write_prepared_values OK")
|
|
written = True
|
|
except Exception as e:
|
|
last_err = e
|
|
print("DEBUG: set_prepared_value + write_prepared_values failed: %s: %s" % (type(e).__name__, e))
|
|
|
|
if not written:
|
|
for method_name in ('write_value', 'set_value', 'write', 'set'):
|
|
if not hasattr(online_app, method_name):
|
|
continue
|
|
try:
|
|
getattr(online_app, method_name)(VARIABLE_PATH, VARIABLE_VALUE)
|
|
print("DEBUG: %s(name, value) OK" % method_name)
|
|
written = True
|
|
break
|
|
except Exception as e:
|
|
last_err = e
|
|
print("DEBUG: %s(name, value) failed: %s: %s" % (method_name, type(e).__name__, e))
|
|
|
|
if not written:
|
|
attrs = sorted([a for a in dir(online_app) if not a.startswith('_')])
|
|
raise RuntimeError(
|
|
"Could not write variable. Last error: %s\n"
|
|
"Available attributes on online_app: %s" % (last_err, attrs)
|
|
)
|
|
|
|
print("Variable: %s" % VARIABLE_PATH)
|
|
print("Value Written: %s" % VARIABLE_VALUE)
|
|
print("Application: %s" % app_name)
|
|
print("SCRIPT_SUCCESS: Variable written successfully.")
|
|
sys.exit(0)
|
|
except Exception as e:
|
|
detailed_error = traceback.format_exc()
|
|
error_message = "Error writing variable '%s' in project %s: %s\n%s" % (
|
|
VARIABLE_PATH, PROJECT_FILE_PATH, e, detailed_error)
|
|
print(error_message)
|
|
print("SCRIPT_ERROR: %s" % error_message)
|
|
sys.exit(1)
|