From e862846164bfd0671291fdb57081008c6f844291 Mon Sep 17 00:00:00 2001 From: Karstein Phobic Nyvold Kvistad Date: Sat, 25 Apr 2026 18:16:54 +0200 Subject: [PATCH] fix(connect_to_device): probe OnlineChangeOption + try login() call shapes Defensively iterates plausible signatures for IOnlineApplication.login() across SP versions. Prior version hard-coded 'login(OnlineChangeOption.TryOnlineChange)' which fails on SP21+/SP22: - 'TryOnlineChange' enum member was removed - login() now requires (OnlineChangeOption, bool) -- two positional args Concrete failure observed today on SP22 Patch 1: - First fallback raised: 'type' object has no attribute 'TryOnlineChange' - Second fallback raised: login() takes exactly 2 arguments (0 given) ...with the runtime actually reachable on port 11740. So a real call was waiting for the right arguments. New approach: 1) Discover OnlineChangeOption members at runtime via dir(); print them so future debug sessions see exactly what's exposed on this CODESYS. 2) Try (enum, False) / (enum, True) / (enum,) for each candidate enum value, prioritising 'Try'-ish names then 'WithDownload' / others. 3) Fall back to bool-only and no-arg for very old SPs. 4) Log every attempt (DEBUG line) so a failure trace shows the full matrix that was tried. Companion fixes for download_to_device, write_variable, etc. coming as separate commits if their root cause is the same login API drift. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/scripts/connect_to_device.py | 72 +++++++++++++++++++++++++------- 1 file changed, 57 insertions(+), 15 deletions(-) diff --git a/src/scripts/connect_to_device.py b/src/scripts/connect_to_device.py index 20ad815..1119a97 100644 --- a/src/scripts/connect_to_device.py +++ b/src/scripts/connect_to_device.py @@ -7,24 +7,66 @@ try: online_app, target_app = ensure_online_connection(primary_project) app_name = getattr(target_app, 'get_name', lambda: "Unknown")() - # Login to the device + # Login to the device. The login() signature shifted across SPs: + # - Older: login() with no args, or login(OnlineChangeOption.TryOnlineChange) + # - SP21+/SP22: login(OnlineChangeOption, bool) -- two required positional + # args, with several enum members renamed (TryOnlineChange removed). + # Probe what's available, then try a sequence of plausible call shapes. print("DEBUG: Calling login() on online application...") - - if hasattr(online_app, 'login'): - # Try with OnlineChangeOption if available - if hasattr(script_engine, 'OnlineChangeOption'): - try: - online_app.login(script_engine.OnlineChangeOption.TryOnlineChange) - print("DEBUG: Logged in with TryOnlineChange option.") - except Exception as e: - print("DEBUG: Login with OnlineChangeOption failed, trying plain login: %s" % e) - online_app.login() - else: - online_app.login() - print("DEBUG: Login successful.") - else: + if not hasattr(online_app, 'login'): raise TypeError("Online application does not support login().") + # Discover OnlineChangeOption members defensively. Different SPs expose + # different names. Build candidate enum values in priority order. + enum_candidates = [] + if hasattr(script_engine, 'OnlineChangeOption'): + oc = script_engine.OnlineChangeOption + oc_members = sorted([m for m in dir(oc) if not m.startswith('_')]) + print("DEBUG: OnlineChangeOption members: %s" % oc_members) + # Priority order: prefer "try"-ish (no-download), then "download" variants + for preferred in ('Try', 'TryOnlineChange', 'OnlineChangeOnly', + 'WithDownload', 'ForceDownload', 'None_', 'None'): + if preferred in oc_members: + try: + enum_candidates.append((preferred, getattr(oc, preferred))) + except Exception: + pass + # Append all remaining members as fallbacks + for m in oc_members: + if m not in [n for n, _ in enum_candidates]: + try: + enum_candidates.append((m, getattr(oc, m))) + except Exception: + pass + + # Build call-shape candidates for login(): a list of (description, args-tuple). + call_shapes = [] + for nm, val in enum_candidates: + call_shapes.append(("login(%s, False)" % nm, (val, False))) + call_shapes.append(("login(%s, True)" % nm, (val, True))) + call_shapes.append(("login(%s)" % nm, (val,))) + # Also try plain bools and no-arg as fall-backs (for very old SPs) + call_shapes.append(("login(False)", (False,))) + call_shapes.append(("login(True)", (True,))) + call_shapes.append(("login()", ())) + + last_err = None + logged_in = False + for desc, args in call_shapes: + try: + online_app.login(*args) + print("DEBUG: %s succeeded" % desc) + logged_in = True + break + except Exception as e: + last_err = e + # Print only the short error to keep log readable + print("DEBUG: %s failed: %s: %s" % (desc, type(e).__name__, e)) + + if not logged_in: + raise RuntimeError("All login() call shapes failed. Last error: %s" % last_err) + print("DEBUG: Login successful.") + # Check connection state state = "connected" if hasattr(online_app, 'application_state'):