0
0
Fork 0

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) <noreply@anthropic.com>
This commit is contained in:
Karstein Phobic Nyvold Kvistad 2026-04-25 18:37:38 +02:00
parent 010811b342
commit 64906c4ab5

View file

@ -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)
)