diff --git a/src/scripts/connect_to_device.py b/src/scripts/connect_to_device.py index 1119a97..df1b037 100644 --- a/src/scripts/connect_to_device.py +++ b/src/scripts/connect_to_device.py @@ -1,7 +1,10 @@ import sys, scriptengine as script_engine, os, traceback +LOGIN_WAIT_SECONDS = {LOGIN_WAIT_SECONDS} + try: - print("DEBUG: connect_to_device script: Project='%s'" % PROJECT_FILE_PATH) + print("DEBUG: connect_to_device script: Project='%s', LoginWaitSec=%d" % ( + PROJECT_FILE_PATH, LOGIN_WAIT_SECONDS)) primary_project = ensure_project_open(PROJECT_FILE_PATH) online_app, target_app = ensure_online_connection(primary_project) @@ -65,13 +68,28 @@ try: if not logged_in: raise RuntimeError("All login() call shapes failed. Last error: %s" % last_err) - print("DEBUG: Login successful.") + print("DEBUG: login() returned. Waiting up to %d seconds for state to stabilise" % LOGIN_WAIT_SECONDS) + print("DEBUG: (CODESYS may pop a credential dialog -- enter device password if prompted.)") - # Check connection state - state = "connected" - if hasattr(online_app, 'application_state'): + # Poll application_state. CODESYS shows a modal credential dialog the + # first time you log into a device with a password; login() may return + # immediately while the dialog is still up, leaving the application in + # an undefined state. Pump the message loop via system.delay() so the + # dialog renders and the user has time to fill it in. Exit early once + # the state lands on a recognisable terminal value. + STABLE_STATES = ('run', 'stop', 'connected', 'halt', 'breakpoint') + state = "unknown" + for elapsed in range(LOGIN_WAIT_SECONDS): + if hasattr(online_app, 'application_state'): + try: + state = str(online_app.application_state) + except Exception: + pass + if state.lower() in STABLE_STATES: + print("DEBUG: state stabilised at '%s' after %ds" % (state, elapsed)) + break try: - state = str(online_app.application_state) + script_engine.system.delay(1000) except Exception: pass diff --git a/src/server.ts b/src/server.ts index 1f992d2..f8290f5 100644 --- a/src/server.ts +++ b/src/server.ts @@ -855,17 +855,25 @@ export async function startMcpServer(config: ServerConfig): Promise { s.tool( 'connect_to_device', - 'Connects (logs in) to the PLC runtime for the active application. Requires a configured device/gateway in the project.', + 'Connects (logs in) to the PLC runtime for the active application. Requires a configured device/gateway in the project. The first connect to a password-protected runtime pops a credential dialog in CODESYS that the user must fill in -- the loginWaitSeconds parameter controls how long the script polls for state stabilisation while that dialog is up.', { projectFilePath: z.string().describe("Path to the project file."), + loginWaitSeconds: z.number().int().min(0).max(600).optional().describe("Seconds to wait for the application state to stabilise after login() returns. Used to give the user time to fill in a credential dialog. Default: 60. Range 0-600."), }, - async (args: { projectFilePath: string }) => { + async (args: { projectFilePath: string; loginWaitSeconds?: number }) => { const escaped = resolvePath(args.projectFilePath, workspaceDir); + const waitSec = args.loginWaitSeconds ?? 60; const script = scriptManager.prepareScriptWithHelpers( - 'connect_to_device', { PROJECT_FILE_PATH: escaped }, + 'connect_to_device', + { + PROJECT_FILE_PATH: escaped, + LOGIN_WAIT_SECONDS: String(waitSec), + }, ['ensure_project_open', 'ensure_online_connection'] ); - const result = await executor.executeScript(script, 60_000); + // Tool-side timeout = wait window + 30s headroom for actual login work + const ipcTimeoutMs = (waitSec + 30) * 1000; + const result = await executor.executeScript(script, ipcTimeoutMs); return formatToolResponse(result, `Connected to device for ${args.projectFilePath}.`); } );