From 64906c4ab5c0fccb7e18e66fae3932f746b89ca3 Mon Sep 17 00:00:00 2001 From: Karstein Phobic Nyvold Kvistad Date: Sat, 25 Apr 2026 18:37:38 +0200 Subject: [PATCH] fix(write_variable): use SP22 prepare-then-write API as primary path After 010811b's diagnostic dump revealed the actual online_app surface on SP22 Patch 1: ['Dispose', 'application', 'application_state', 'create_boot_application', 'force_prepared_values', 'get_forced_expressions', 'get_online_device', 'get_prepared_expressions', 'get_prepared_value', 'is_logged_in', 'login', 'logout', 'operation_state', 'read_value', 'read_values', 'reset', 'set_prepared_value', 'set_unforce_value', 'source_download', 'start', 'stop', 'timeout', 'unforce_all_values', 'write_prepared_values'] There is no direct write_value / write / set_value method. The supported pattern is two-step: online_app.set_prepared_value(path, value) # stage online_app.write_prepared_values() # commit Asymmetric to read_value() (which is direct), but it's what the SP21+/SP22 scriptengine surface exposes. This commit: - Makes the prepare-then-write path the primary code path. - Keeps the direct write_value / set_value / write / set fallbacks for older SPs that still expose them (probe order: prepare-first, then direct). - Falls back to dumping dir(online_app) on total failure, same diagnostic pattern that revealed this API in the first place. Note for future reference: 'force_prepared_values()' is the alternate commit method when you want to FORCE a value (override what the program will write next cycle), versus 'write_prepared_values()' which is a normal one-shot write. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/scripts/write_variable.py | 60 +++++++++++++++-------------------- 1 file changed, 26 insertions(+), 34 deletions(-) diff --git a/src/scripts/write_variable.py b/src/scripts/write_variable.py index d77862a..f014bf1 100644 --- a/src/scripts/write_variable.py +++ b/src/scripts/write_variable.py @@ -13,50 +13,42 @@ try: online_app, target_app = ensure_online_connection(primary_project) app_name = getattr(target_app, 'get_name', lambda: "Unknown")() - # The write counterpart of read_value() has shifted across CODESYS SPs. - # Some expose 'write_value(name, value)', some 'set_value(...)', - # and some only the batch form 'write_values([(name, value), ...])'. - # Probe each available method, then fall back to dumping the available - # methods on online_app so future debug sessions know what's exposed. - candidates = [] - - # Single-write methods, (name, value) style - for method_name in ('write_value', 'set_value', 'write', 'set'): - if hasattr(online_app, method_name): - candidates.append((method_name + '(name, value)', method_name, [(VARIABLE_PATH, VARIABLE_VALUE)])) - - # Batch-write methods, [(name, value), ...] style - for method_name in ('write_values', 'set_values'): - if hasattr(online_app, method_name): - candidates.append((method_name + '([(name, value)])', method_name, [[(VARIABLE_PATH, VARIABLE_VALUE)]])) - - if not candidates: - # No known method exposed. Dump diagnostic. - attrs = sorted([a for a in dir(online_app) if not a.startswith('_')]) - raise TypeError( - "Online application exposes no known write method " - "(tried write_value/set_value/write/set/write_values/set_values).\n" - "Available attributes: %s" % attrs - ) - + # 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 - for desc, method_name, args_list in candidates: + + if hasattr(online_app, 'set_prepared_value') and hasattr(online_app, 'write_prepared_values'): try: - getattr(online_app, method_name)(*args_list) - print("DEBUG: %s OK" % desc) + 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 - break except Exception as e: last_err = e - print("DEBUG: %s failed: %s: %s" % (desc, type(e).__name__, 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: - # All known method names were exposed but every call failed. Dump - # diagnostic + last error so we can iterate. attrs = sorted([a for a in dir(online_app) if not a.startswith('_')]) raise RuntimeError( - "All known write methods were rejected. Last error: %s\n" + "Could not write variable. Last error: %s\n" "Available attributes on online_app: %s" % (last_err, attrs) )