From 87101175cee07c3a9cd5b1f159fd680d87805b6d Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Mon, 24 Aug 2026 05:21:41 +0200 Subject: [PATCH] fix(boards): an overlay board could never enter ESP-IDF mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BOARD_SUPPORTS_ESPIDF is a literal set of OSS board kinds and it gates both the toolbar's ESP-IDF option and setBoardLanguageMode, so no overlay board could ever be switched into ESP-IDF: loadExample called setBoardLanguageMode(id, 'espidf') for an `languageMode: 'espidf'` gallery example, the guard returned early, and the example opened in Arduino mode with the toolbar saying "Arduino C++". It stayed invisible because the backend sniffs `app_main` and compiles those sketches as pure IDF anyway — until a board with no Arduino FQBN at all hit it, and the run died in the browser at "No FQBN for board kind" before any request left the page. - ProBoardDef.supportsEspIdf registers the kind into BOARD_SUPPORTS_ESPIDF, the same way supportsMicroPython already did for MicroPython. - ProBoardDef.espidfFqbn + BOARD_KIND_ESPIDF_FQBN + fqbnForLanguage give the IDF lane a target for a board whose Arduino FQBN is null (the ESP32-C5 kits: no arduino-esp32 core supports the C5, and the backend refuses that target for Arduino on purpose, but idf.py builds it fine). Both compile sites resolve through it instead of reading BOARD_KIND_FQBN directly. - The language select shows when EITHER alternative mode exists (the P4 is ESP-IDF but not MicroPython, so it had no select at all), hides options the board does not support, and hides Arduino for a board with no FQBN. - addBoard no longer seeds an Arduino-less board into 'arduino', where it could never compile and the select would show a value it does not carry. Regression test: overlay-espidf-mode.test.ts. --- .../src/__tests__/overlay-espidf-mode.test.ts | 135 ++++++++++++++++++ .../src/components/editor/EditorToolbar.tsx | 20 ++- frontend/src/lib/proBoardRegistry.ts | 17 +++ frontend/src/store/useSimulatorStore.ts | 17 ++- frontend/src/types/board.ts | 26 ++++ frontend/src/utils/boardCompile.ts | 4 +- 6 files changed, 210 insertions(+), 9 deletions(-) create mode 100644 frontend/src/__tests__/overlay-espidf-mode.test.ts diff --git a/frontend/src/__tests__/overlay-espidf-mode.test.ts b/frontend/src/__tests__/overlay-espidf-mode.test.ts new file mode 100644 index 00000000..9f65ac7e --- /dev/null +++ b/frontend/src/__tests__/overlay-espidf-mode.test.ts @@ -0,0 +1,135 @@ +/** + * ESP-IDF language mode for OVERLAY boards. + * + * `BOARD_SUPPORTS_ESPIDF` is a literal set of OSS board kinds, and it is what + * gates BOTH the toolbar's ESP-IDF option AND `setBoardLanguageMode`. An + * overlay board (every Espressif devkit) was therefore unable to enter ESP-IDF + * mode at all: `loadExample` called `setBoardLanguageMode(id, 'espidf')`, the + * guard returned early, and an `languageMode: 'espidf'` gallery example opened + * in Arduino mode with the toolbar reporting "Arduino C++". It happened to + * still compile on boards whose sketch the backend could sniff as pure IDF + * (`app_main` present) — but on the ESP32-C5 kit, which has no Arduino FQBN at + * all, the run died in the browser at "No FQBN for board kind". + * + * So: `ProBoardDef.supportsEspIdf` registers the kind, and + * `ProBoardDef.espidfFqbn` gives the IDF lane a target for a board with no + * Arduino FQBN of its own. Pure Vitest — no QEMU, no network, no DOM. + */ + +import { describe, it, expect } from 'vitest'; +import { registerProBoards } from '../lib/proBoardRegistry'; +import { useSimulatorStore } from '../store/useSimulatorStore'; +import { + BOARD_SUPPORTS_ESPIDF, + BOARD_SUPPORTS_MICROPYTHON, + fqbnForLanguage, + type BoardKind, +} from '../types/board'; + +const IDF_ONLY = 'idf-test-c5' as BoardKind; // no Arduino core, ESP-IDF + MPY +const DUAL = 'idf-test-s3' as BoardKind; // Arduino and ESP-IDF both build +const ARDUINO_ONLY = 'idf-test-plain' as BoardKind; + +registerProBoards([ + { + kind: IDF_ONLY, + label: 'IDF Test C5', + fqbn: null, + espidfFqbn: 'esp32:esp32:esp32c5', + description: 'overlay board with no arduino core', + tag: 'velxio-idf-test-c5', + size: { w: 100, h: 100 }, + supportsMicroPython: true, + supportsEspIdf: true, + esp32Family: 'esp32-c5', + }, + { + kind: DUAL, + label: 'IDF Test S3', + fqbn: 'esp32:esp32:esp32s3', + description: 'overlay board that builds both ways', + tag: 'velxio-idf-test-s3', + size: { w: 100, h: 100 }, + supportsMicroPython: true, + supportsEspIdf: true, + esp32Family: 'esp32-s3', + }, + { + kind: ARDUINO_ONLY, + label: 'IDF Test Plain', + fqbn: 'esp32:esp32:esp32', + description: 'overlay board that never declared ESP-IDF', + tag: 'velxio-idf-test-plain', + size: { w: 100, h: 100 }, + supportsMicroPython: true, + esp32Family: 'esp32', + }, +]); + +describe('ProBoardDef.supportsEspIdf', () => { + it('registers the kind so the toolbar and the store both see it', () => { + expect(BOARD_SUPPORTS_ESPIDF.has(IDF_ONLY)).toBe(true); + expect(BOARD_SUPPORTS_ESPIDF.has(DUAL)).toBe(true); + }); + + it('leaves a board that did not declare it alone', () => { + expect(BOARD_SUPPORTS_ESPIDF.has(ARDUINO_ONLY)).toBe(false); + // …while still being a MicroPython board, so the regression is scoped. + expect(BOARD_SUPPORTS_MICROPYTHON.has(ARDUINO_ONLY)).toBe(true); + }); + + it('lets setBoardLanguageMode actually switch an overlay board to espidf', () => { + const store = useSimulatorStore.getState(); + store.addBoard(DUAL, 0, 0); + const board = useSimulatorStore.getState().boards.find((b) => b.boardKind === DUAL); + expect(board).toBeDefined(); + useSimulatorStore.getState().setBoardLanguageMode(board!.id, 'espidf'); + expect( + useSimulatorStore.getState().boards.find((b) => b.id === board!.id)?.languageMode, + ).toBe('espidf'); + }); + + it('still refuses espidf for a board that never declared it', () => { + const store = useSimulatorStore.getState(); + store.addBoard(ARDUINO_ONLY, 0, 0); + const board = useSimulatorStore + .getState() + .boards.find((b) => b.boardKind === ARDUINO_ONLY); + useSimulatorStore.getState().setBoardLanguageMode(board!.id, 'espidf'); + expect( + useSimulatorStore.getState().boards.find((b) => b.id === board!.id)?.languageMode, + ).not.toBe('espidf'); + }); +}); + +describe('fqbnForLanguage', () => { + it('gives the IDF lane a target for a board with no Arduino FQBN', () => { + expect(fqbnForLanguage(IDF_ONLY, 'espidf')).toBe('esp32:esp32:esp32c5'); + // Arduino stays impossible on that board — the mode has no core at all. + expect(fqbnForLanguage(IDF_ONLY, 'arduino')).toBeNull(); + expect(fqbnForLanguage(IDF_ONLY, undefined)).toBeNull(); + }); + + it('falls through to the Arduino FQBN when no espidf override exists', () => { + expect(fqbnForLanguage(DUAL, 'espidf')).toBe('esp32:esp32:esp32s3'); + expect(fqbnForLanguage(DUAL, 'arduino')).toBe('esp32:esp32:esp32s3'); + }); +}); + +describe('addBoard language seeding', () => { + it('does not seed a board that cannot compile Arduino into arduino mode', () => { + useSimulatorStore.getState().addBoard(IDF_ONLY, 0, 0); + const board = useSimulatorStore.getState().boards.find((b) => b.boardKind === IDF_ONLY); + expect(board?.languageMode).not.toBe('arduino'); + expect(board?.languageMode).toBe('micropython'); + }); + + it('still seeds a normal board into arduino', () => { + useSimulatorStore.getState().addBoard(ARDUINO_ONLY, 0, 0); + const board = useSimulatorStore + .getState() + .boards.filter((b) => b.boardKind === ARDUINO_ONLY) + .pop(); + expect(board?.languageMode).toBe('arduino'); + }); +}); diff --git a/frontend/src/components/editor/EditorToolbar.tsx b/frontend/src/components/editor/EditorToolbar.tsx index 111d1b6e..836321c0 100644 --- a/frontend/src/components/editor/EditorToolbar.tsx +++ b/frontend/src/components/editor/EditorToolbar.tsx @@ -10,7 +10,7 @@ import { type VerificationResult } from '../../simulation/verify/circuitVerifier import { verifyCircuitFromStore } from '../../simulation/verify/verifyFromStore'; import { CircuitVerificationModal } from '../simulator/CircuitVerificationModal'; import type { BoardKind, LanguageMode } from '../../types/board'; -import { BOARD_KIND_FQBN, BOARD_SUPPORTS_ESPIDF, BOARD_SUPPORTS_MICROPYTHON, isKnownBoardKind, isPiBoardKind, boardDisplayName } from '../../types/board'; +import { BOARD_KIND_FQBN, BOARD_SUPPORTS_ESPIDF, BOARD_SUPPORTS_MICROPYTHON, fqbnForLanguage, isKnownBoardKind, isPiBoardKind, boardDisplayName } from '../../types/board'; import { compileCode } from '../../services/compilation'; import { compileOptionsForBoard } from '../../utils/boardCompile'; import { @@ -519,7 +519,7 @@ export const EditorToolbar = ({ return; } - const fqbn = kind ? BOARD_KIND_FQBN[kind] : null; + const fqbn = kind ? fqbnForLanguage(kind, activeBoard?.languageMode) : null; if (!fqbn) { blog('error', `No FQBN for board kind: ${kind}`); @@ -1566,7 +1566,9 @@ export const EditorToolbar = ({ pure ESP-IDF on the ESP32 family — issue #139). The board context pill that used to live here was removed: it duplicated the BoardSelector dropdown elsewhere in the toolbar. */} - {activeBoard && BOARD_SUPPORTS_MICROPYTHON.has(activeBoard.boardKind) && ( + {activeBoard && + (BOARD_SUPPORTS_MICROPYTHON.has(activeBoard.boardKind) || + BOARD_SUPPORTS_ESPIDF.has(activeBoard.boardKind)) && (