diff --git a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/variable/JtagVariableResolver.java b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/variable/JtagVariableResolver.java index 9f81c5ace..c9a3a812d 100644 --- a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/variable/JtagVariableResolver.java +++ b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/variable/JtagVariableResolver.java @@ -67,6 +67,23 @@ private String generatePartOfConfigOptionsForVoltage() } private String generatePartOfConfigOptionsForBoard() + { + var result = new StringBuilder(); + for (Object config : resolveBoardConfigFiles()) + { + result.append(String.format("-f %s ", config)); //$NON-NLS-1$ + } + return result.toString(); + } + + /** + * Resolves the OpenOCD board configuration files for the active launch target. These config files register the + * board-specific OpenOCD commands (e.g. {@code program_esp_bins}). An empty result means no usable board is + * selected for the active target. + * + * @return the list of board config files, never {@code null} + */ + private List resolveBoardConfigFiles() { var parser = new EspConfigParser(); ILaunchTarget activeILaunchTarget = getActiveLaunchTarget().orElseGet(() -> ILaunchTarget.NULL_TARGET); @@ -76,17 +93,20 @@ private String generatePartOfConfigOptionsForBoard() int idx = board.lastIndexOf(" [usb://"); //$NON-NLS-1$ String boardKey = (idx != -1) ? board.substring(0, idx) : board; List boards = parser.getBoardsForTarget(targetName); - List boardConfigs = boards.stream().filter(b -> b.name().equals(boardKey)).findFirst() - .map(Board::config_files).orElse(List.of()); - var result = new StringBuilder(); - if (boardConfigs != null) - { - for (Object config : boardConfigs) - { - result.append(String.format("-f %s ", config)); //$NON-NLS-1$ - } - } - return result.toString(); + return boards.stream().filter(b -> b.name().equals(boardKey)).findFirst().map(Board::config_files) + .orElse(List.of()); + } + + /** + * Checks whether a board configuration is resolvable for the active launch target. When this returns + * {@code false}, OpenOCD would start without a board configuration and board-specific commands such as + * {@code program_esp_bins} would not be available, so the debug session cannot succeed. + * + * @return {@code true} if a board is selected and its OpenOCD config files were found, {@code false} otherwise + */ + public static boolean isBoardConfigResolvable() + { + return !new JtagVariableResolver().resolveBoardConfigFiles().isEmpty(); } } diff --git a/bundles/com.espressif.idf.debug.gdbjtag.openocd/plugin.xml b/bundles/com.espressif.idf.debug.gdbjtag.openocd/plugin.xml index 2fd06585f..64d432f19 100644 --- a/bundles/com.espressif.idf.debug.gdbjtag.openocd/plugin.xml +++ b/bundles/com.espressif.idf.debug.gdbjtag.openocd/plugin.xml @@ -174,6 +174,12 @@ id="com.espressif.idf.debug.gdbjtag.openocd.openocdStatusHandler" plugin="com.espressif.idf.debug.gdbjtag.openocd"> + + diff --git a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchConfigurationDelegate.java b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchConfigurationDelegate.java index 5f59eea21..bd9a0d823 100644 --- a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchConfigurationDelegate.java +++ b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchConfigurationDelegate.java @@ -53,12 +53,14 @@ import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.ILaunchConfiguration; import org.eclipse.debug.core.ILaunchManager; +import org.eclipse.debug.core.IStatusHandler; import org.eclipse.debug.core.model.ISourceLocator; import org.eclipse.embedcdt.core.StringUtils; import org.eclipse.embedcdt.debug.gdbjtag.core.DebugUtils; import org.eclipse.embedcdt.debug.gdbjtag.core.dsf.AbstractGnuMcuLaunchConfigurationDelegate; import org.eclipse.embedcdt.debug.gdbjtag.core.dsf.GnuMcuServerServicesLaunchSequence; +import com.espressif.idf.core.variable.JtagVariableResolver; import com.espressif.idf.debug.gdbjtag.openocd.Activator; import com.espressif.idf.debug.gdbjtag.openocd.Configuration; import com.espressif.idf.debug.gdbjtag.openocd.ui.Messages; @@ -79,6 +81,12 @@ public class LaunchConfigurationDelegate extends AbstractGnuMcuLaunchConfigurati private final static String NON_STOP_FIRST_VERSION = "6.8.50"; //$NON-NLS-1$ private final int STATUS_DLL_NOT_FOUND = -1073741515; + /** + * Status code used to route the "no board selected" failure through {@code BoardNotSelectedStatusHandler}. It must + * match the {@code code} of the corresponding {@code statusHandler} extension in plugin.xml. + */ + public static final int BOARD_NOT_SELECTED_STATUS_CODE = 6001; + ILaunchConfiguration fConfig = null; @SuppressWarnings("unused") private boolean fIsNonStopSession = false; @@ -736,12 +744,56 @@ protected IPath checkBinaryDetails(final ILaunchConfiguration config) throws Cor new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Missing mandatory configuration. " + "Fill-in the 'Config options:' field in the Debugger tab.")); //$NON-NLS-1$ } + + // Abort early with a clear, actionable message when no board is selected for the active target. Otherwise + // OpenOCD would start without a board configuration and fail later with the cryptic + // "invalid command name \"program_esp_bins\"" error. + if (!isBoardConfigured(config)) + { + IStatus status = new Status(IStatus.OK, Activator.PLUGIN_ID, BOARD_NOT_SELECTED_STATUS_CODE, + "No board is selected for the debug target.", null); //$NON-NLS-1$ + IStatusHandler handler = DebugPlugin.getDefault().getStatusHandler(status); + if (handler != null) + { + handler.handleStatus(status, null); + } + throw new DebugException(Status.OK_STATUS); + } } IPath path = super.checkBinaryDetails(config); return path; } + /** + * Determines whether a board configuration is available for the given launch configuration. A board is considered + * configured when it is resolvable from the active launch target, or when the (possibly manually edited) resolved + * Config options already reference a board configuration file. Without either, OpenOCD would start without the + * board-specific commands (e.g. {@code program_esp_bins}) and the debug session would fail. + * + * @param config the debug launch configuration + * @return {@code true} if a board configuration is available, {@code false} otherwise + */ + private boolean isBoardConfigured(ILaunchConfiguration config) + { + if (JtagVariableResolver.isBoardConfigResolvable()) + { + return true; + } + + // Fall back to inspecting the resolved Config options for a manually configured board file. + try + { + String resolvedOptions = Configuration.resolveAll(Configuration.getGdbServerOtherConfig(config), config); + return resolvedOptions != null && resolvedOptions.contains("board/"); //$NON-NLS-1$ + } + catch (CoreException e) + { + Activator.log(e); + } + return false; + } + /** * Get a custom launch sequence, that inserts a GDB server starter. */ diff --git a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/ui/BoardNotSelectedStatusHandler.java b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/ui/BoardNotSelectedStatusHandler.java new file mode 100644 index 000000000..d08b5fffe --- /dev/null +++ b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/ui/BoardNotSelectedStatusHandler.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * Copyright 2026 Espressif Systems (Shanghai) PTE LTD. All rights reserved. + * Use is subject to license terms. + *******************************************************************************/ +package com.espressif.idf.debug.gdbjtag.openocd.ui; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.debug.core.IStatusHandler; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.launchbar.core.ILaunchBarManager; +import org.eclipse.launchbar.core.target.ILaunchTarget; +import org.eclipse.launchbar.ui.target.ILaunchTargetUIManager; +import org.eclipse.swt.widgets.Display; + +import com.espressif.idf.core.logging.Logger; +import com.espressif.idf.debug.gdbjtag.openocd.Activator; + +/** + * Shows a clear, actionable dialog when a debug session is started without a board selected for the active launch + * target. Confirming the dialog opens the launch target editor so the user can select a board. + */ +public class BoardNotSelectedStatusHandler implements IStatusHandler +{ + @Override + public Object handleStatus(IStatus status, Object source) throws CoreException + { + Display.getDefault().asyncExec(() -> { + boolean isYes = MessageDialog.openConfirm(Display.getDefault().getActiveShell(), + Messages.BoardNotSelectedDialog_title, Messages.BoardNotSelectedDialog_message); + if (isYes) + { + editActiveLaunchTarget(); + } + }); + return null; + } + + private void editActiveLaunchTarget() + { + try + { + ILaunchBarManager launchBarManager = Activator.getService(ILaunchBarManager.class); + ILaunchTargetUIManager targetUIManager = Activator.getService(ILaunchTargetUIManager.class); + if (launchBarManager == null || targetUIManager == null) + { + return; + } + ILaunchTarget target = launchBarManager.getActiveLaunchTarget(); + if (target != null) + { + targetUIManager.editLaunchTarget(target); + } + } + catch (Exception e) + { + Logger.log(e); + } + } +} diff --git a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/ui/Messages.java b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/ui/Messages.java index c912f5414..d56236f54 100644 --- a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/ui/Messages.java +++ b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/ui/Messages.java @@ -75,6 +75,9 @@ public class Messages public static String ServerTimeoutErrorDialog_message; public static String ServerTimeoutErrorDialog_customAreaMessage; + public static String BoardNotSelectedDialog_title; + public static String BoardNotSelectedDialog_message; + // ------------------------------------------------------------------------ static diff --git a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/ui/messages.properties b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/ui/messages.properties index ffeeceab4..5264a1290 100644 --- a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/ui/messages.properties +++ b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/ui/messages.properties @@ -419,3 +419,7 @@ OpenOCDConsole_ErrorGuideMessage=Please refer to the troubleshooting guide below ServerTimeoutErrorDialog_customAreaMessage=To increase timeout time visit the Espressif Preference Page. ServerTimeoutErrorDialog_message=Starting OpenOCD timed out. Try to increase the `GDB server launch timeout` ServerTimeoutErrorDialog_title=Problem Occurred + +## Board Not Selected Dialog ## +BoardNotSelectedDialog_title=No board selected +BoardNotSelectedDialog_message=No board is selected. Edit the launch target to select a board? diff --git a/bundles/com.espressif.idf.launch.serial.core/src/com/espressif/idf/launch/serial/internal/Messages.java b/bundles/com.espressif.idf.launch.serial.core/src/com/espressif/idf/launch/serial/internal/Messages.java index d1c1ea8e4..5352d071b 100644 --- a/bundles/com.espressif.idf.launch.serial.core/src/com/espressif/idf/launch/serial/internal/Messages.java +++ b/bundles/com.espressif.idf.launch.serial.core/src/com/espressif/idf/launch/serial/internal/Messages.java @@ -24,6 +24,8 @@ public class Messages extends NLS public static String SerialFlashLaunch_Resume; public static String SerialPortNotFoundTitle; public static String SerialPortNotFoundMsg; + public static String BoardNotSelectedTitle; + public static String BoardNotSelectedMsg; static { // initialize resource bundle diff --git a/bundles/com.espressif.idf.launch.serial.core/src/com/espressif/idf/launch/serial/internal/SerialFlashLaunchConfigDelegate.java b/bundles/com.espressif.idf.launch.serial.core/src/com/espressif/idf/launch/serial/internal/SerialFlashLaunchConfigDelegate.java index a9b2fa6b1..be4b8c0e2 100644 --- a/bundles/com.espressif.idf.launch.serial.core/src/com/espressif/idf/launch/serial/internal/SerialFlashLaunchConfigDelegate.java +++ b/bundles/com.espressif.idf.launch.serial.core/src/com/espressif/idf/launch/serial/internal/SerialFlashLaunchConfigDelegate.java @@ -63,6 +63,7 @@ import com.espressif.idf.core.util.IDFUtil; import com.espressif.idf.core.util.RecheckConfigsHelper; import com.espressif.idf.core.util.StringUtil; +import com.espressif.idf.core.variable.JtagVariableResolver; import com.espressif.idf.launch.serial.util.ESPFlashUtil; import com.espressif.idf.terminal.connector.serial.connector.SerialSettings; import com.espressif.idf.terminal.connector.serial.launcher.SerialLauncherDelegate; @@ -105,6 +106,13 @@ public void launch(ILaunchConfiguration configuration, String mode, ILaunch laun } if (ESPFlashUtil.isJtag()) { + // Abort early with a clear message when no board is selected. Otherwise OpenOCD would run without a board + // configuration and fail with the cryptic "invalid command name \"program_esp_bins\"" error. + if (!isBoardConfigured(configuration)) + { + showBoardNotSelectedMessage(configuration); + return; + } ESPFlashUtil.flashOverJtag(configuration, launch); return; } @@ -271,6 +279,52 @@ private static void showMessage(ILaunchConfiguration configuration) }); } + /** + * Determines whether a board configuration is available for JTAG flashing. A board is considered configured when it + * is resolvable from the active launch target, or when the (possibly manually edited) resolved JTAG flash arguments + * already reference a board configuration file. + * + * @param configuration the launch configuration + * @return {@code true} if a board configuration is available, {@code false} otherwise + */ + private boolean isBoardConfigured(ILaunchConfiguration configuration) + { + if (JtagVariableResolver.isBoardConfigResolvable()) + { + return true; + } + + // Fall back to inspecting the resolved JTAG flash arguments for a manually configured board file. + try + { + String arguments = configuration.getAttribute(IDFLaunchConstants.ATTR_JTAG_FLASH_ARGUMENTS, + StringUtil.EMPTY); + String resolved = VariablesPlugin.getDefault().getStringVariableManager() + .performStringSubstitution(arguments); + return resolved != null && resolved.contains("board/"); //$NON-NLS-1$ + } + catch (CoreException e) + { + Logger.log(e); + } + return false; + } + + private static void showBoardNotSelectedMessage(ILaunchConfiguration configuration) + { + Display.getDefault().asyncExec(() -> { + boolean isYes = MessageDialog.openConfirm(Display.getDefault().getActiveShell(), + com.espressif.idf.launch.serial.internal.Messages.BoardNotSelectedTitle, + com.espressif.idf.launch.serial.internal.Messages.BoardNotSelectedMsg); + if (isYes) + { + ILaunchTargetUIManager targetUIManager = Activator.getService(ILaunchTargetUIManager.class); + ILaunchTargetManager launchTargetManager = Activator.getService(ILaunchTargetManager.class); + targetUIManager.editLaunchTarget(launchTargetManager.getDefaultLaunchTarget(configuration)); + } + }); + } + @Override public boolean buildForLaunch(ILaunchConfiguration configuration, String mode, ILaunchTarget target, IProgressMonitor monitor) throws CoreException diff --git a/bundles/com.espressif.idf.launch.serial.core/src/com/espressif/idf/launch/serial/internal/messages.properties b/bundles/com.espressif.idf.launch.serial.core/src/com/espressif/idf/launch/serial/internal/messages.properties index 304a39100..cb7bfd4ad 100644 --- a/bundles/com.espressif.idf.launch.serial.core/src/com/espressif/idf/launch/serial/internal/messages.properties +++ b/bundles/com.espressif.idf.launch.serial.core/src/com/espressif/idf/launch/serial/internal/messages.properties @@ -11,4 +11,6 @@ SerialFlashLaunch_Pause=Pausing serial port SerialFlashLaunch_Resume=Resuming serial port SerialPortNotFoundTitle=Serial port not found -SerialPortNotFoundMsg=The serial port was not found. Please select it first and flash the project again. \ No newline at end of file +SerialPortNotFoundMsg=The serial port was not found. Please select it first and flash the project again. +BoardNotSelectedTitle=No board selected +BoardNotSelectedMsg=No board is selected. Edit the launch target to select a board? \ No newline at end of file