From 2b7c9b6fb6a047c2ceec85c808b0121d0fb504cb Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Thu, 20 Aug 2026 19:48:24 +0200 Subject: [PATCH] feat(boards): let a board declare the RAM and flash it is physically fitted with An overlay board was invisible to the build-options model: its kind is not in the OSS family sets, so isEsp32Family() said no, the Board Options menu never appeared, and every compile went out with board_options: null - the conservative family default of CONFIG_SPIRAM=n. That is wrong for a module that always ships PSRAM. The ESP32-S3-EYE carries 8 MB of octal PSRAM and esp_camera allocates its frame buffer with MALLOC_CAP_SPIRAM, so its camera examples compiled cleanly and then died at run time with 'cam_dma_config(509): frame buffer malloc failed' - a hardware fact surfacing as a mysterious driver error. Boards now declare defaultBoardOptions, sent when the project saved none of its own. ONLY the declared keys are sent: the backend backfills the rest from its own defaults, which are deliberately not the modal's (huge_app vs min_spiffs), so a board that gains one declared knob does not silently get repartitioned. Boards that declare nothing keep sending null and build bit-for-bit as before. Also route the family predicates through the registry, so overlay ESP32 boards finally get the Tools menu they were missing. --- frontend/src/lib/proBoardRegistry.ts | 15 +++++++++ frontend/src/services/compilation.ts | 10 +++++- frontend/src/types/boardOptions.ts | 49 +++++++++++++++++++++++++--- 3 files changed, 69 insertions(+), 5 deletions(-) diff --git a/frontend/src/lib/proBoardRegistry.ts b/frontend/src/lib/proBoardRegistry.ts index 81c63e4b..db7df46d 100644 --- a/frontend/src/lib/proBoardRegistry.ts +++ b/frontend/src/lib/proBoardRegistry.ts @@ -17,6 +17,7 @@ * `ad.id in BOARD_KIND_LABELS`, and registration inserts the label. */ import type React from 'react'; +import type { ESP32BoardOptions } from '../types/boardOptions'; import { BOARD_KIND_LABELS, BOARD_KIND_FQBN, @@ -166,6 +167,20 @@ export interface ProBoardDef { * first compile resolves against nothing. */ defaultLibraries?: string[]; + /** + * Build options this board is physically born with, merged over the family + * defaults whenever the project has not saved its own. + * + * A module carries the RAM and flash it carries: the ESP32-S3-EYE always has + * 8 MB of octal PSRAM, and its camera driver allocates the frame buffer with + * MALLOC_CAP_SPIRAM. Compiled with the conservative family default + * (CONFIG_SPIRAM=n) the sketch builds fine and then dies at run time with + * `cam_dma_config(509): frame buffer malloc failed` — a hardware fact + * reported as a mysterious driver error. Boards that ship the RAM say so + * here; everything else keeps sending no options at all. + */ + defaultBoardOptions?: Partial; + /** Sidebar / toolbar accents (fall back to a neutral chip icon). */ icon?: string; color?: string; diff --git a/frontend/src/services/compilation.ts b/frontend/src/services/compilation.ts index fb91dadc..f4dcc966 100644 --- a/frontend/src/services/compilation.ts +++ b/frontend/src/services/compilation.ts @@ -1,6 +1,7 @@ import axios from 'axios'; import { getApiBase } from '../lib/apiBase'; import type { ESP32BoardOptions, SpiffsFile } from '../types/boardOptions'; +import { implicitBoardOptions } from '../types/boardOptions'; export interface SketchFile { name: string; @@ -117,7 +118,14 @@ export async function compileCode( // Translate camelCase frontend keys to snake_case backend keys. Backend // only inspects these fields for esp32:* FQBNs — other boards pass them // through unread. - const board_options = extras?.boardOptions ? { ...extras.boardOptions } : null; + // A project that never opened the Board Options modal still has to build for + // the module it is running on: ask the board what it ships with. Null for + // every board that declares nothing, which is all of the OSS ones. + const board_options = extras?.boardOptions + ? { ...extras.boardOptions } + : extras?.boardKind + ? implicitBoardOptions(extras.boardKind as never) + : null; const spiffs_files = extras?.spiffsFiles?.length ? extras.spiffsFiles.map((f) => ({ name: f.name, content_b64: f.contentB64 })) : null; diff --git a/frontend/src/types/boardOptions.ts b/frontend/src/types/boardOptions.ts index 484f6c85..cc72dc14 100644 --- a/frontend/src/types/boardOptions.ts +++ b/frontend/src/types/boardOptions.ts @@ -7,6 +7,7 @@ // modal isn't exposed there. import type { BoardKind } from './board'; +import { getProBoard } from '../lib/proBoardRegistry'; export type ESP32PartitionScheme = | 'default' // 1.2MB APP / 1.5MB SPIFFS (OTA) @@ -85,11 +86,22 @@ const ESP32_XTENSA_KINDS: ReadonlySet = new Set([ 'wemos-lolin32-lite', ]); +// Overlay boards are not in the OSS kind sets, but they declare the chip they +// carry (`esp32Family`). Without consulting the registry an overlay ESP32 +// board silently lost the whole Tools menu — no partition scheme, no PSRAM, +// no flash size — on hardware where those knobs matter most. +function proEsp32Family(kind: BoardKind): ProEsp32Family | null { + return (getProBoard(kind)?.esp32Family as ProEsp32Family | undefined) ?? null; +} + +type ProEsp32Family = 'esp32' | 'esp32-s3' | 'esp32-c3' | 'esp32-c6' | 'esp32-p4' | 'esp32-c5'; + export function isEsp32Family(kind: BoardKind): boolean { return ( ESP32_XTENSA_KINDS.has(kind) || ESP32_S3_KINDS.has(kind) || - ESP32_C3_KINDS.has(kind) + ESP32_C3_KINDS.has(kind) || + proEsp32Family(kind) !== null ); } @@ -98,22 +110,51 @@ export function isEsp32Family(kind: BoardKind): boolean { // - ESP32-S3: optional, "enabled" or "opi" (octal) // - ESP32-C3: no PSRAM support at all — section hidden in UI export function boardSupportsPsram(kind: BoardKind): boolean { + const pro = proEsp32Family(kind); + if (pro) return pro !== 'esp32-c3' && pro !== 'esp32-c6'; return ESP32_XTENSA_KINDS.has(kind) || ESP32_S3_KINDS.has(kind); } export function boardSupportsOpiPsram(kind: BoardKind): boolean { - return ESP32_S3_KINDS.has(kind); + return ESP32_S3_KINDS.has(kind) || proEsp32Family(kind) === 'esp32-s3'; } export function getDefaultOptionsForKind(kind: BoardKind): ESP32BoardOptions { if (!isEsp32Family(kind)) return { ...DEFAULT_ESP32_OPTIONS }; - const defaults = { ...DEFAULT_ESP32_OPTIONS }; + const defaults = { ...DEFAULT_ESP32_OPTIONS, ...boardBornOptions(kind) }; // C3 has no PSRAM controller — clear the field so a stale 'enabled' // value from an upgraded project doesn't reach the backend. - if (ESP32_C3_KINDS.has(kind)) defaults.psram = 'disabled'; + if (ESP32_C3_KINDS.has(kind) || !boardSupportsPsram(kind)) defaults.psram = 'disabled'; + if (defaults.psram === 'opi' && !boardSupportsOpiPsram(kind)) defaults.psram = 'enabled'; return defaults; } +/** What the module physically ships with, as declared by an overlay board + * (`ProBoardDef.defaultBoardOptions`). Empty for every OSS board. */ +function boardBornOptions(kind: BoardKind): Partial { + return getProBoard(kind)?.defaultBoardOptions ?? {}; +} + +/** + * Options to compile with when the project saved none of its own. + * + * Returns null for every board whose module declares nothing, so their + * requests keep sending `board_options: null` and build bit-for-bit as before. + * When a board DOES declare something, only the declared keys are sent: the + * backend backfills the rest from ITS defaults, which are deliberately not the + * same as the modal's (`huge_app` there, `min_spiffs` here). Sending a + * "complete" set would quietly repartition every board that gained a single + * declared knob. + */ +export function implicitBoardOptions(kind: BoardKind): Partial | null { + const born = boardBornOptions(kind); + if (Object.keys(born).length === 0) return null; + const opts: Partial = { ...born }; + if (opts.psram && !boardSupportsPsram(kind)) opts.psram = 'disabled'; + if (opts.psram === 'opi' && !boardSupportsOpiPsram(kind)) opts.psram = 'enabled'; + return opts; +} + // Human-readable label for a partition scheme. Used in the modal dropdown // so users see "min_spiffs - 1.9MB APP / 190KB SPIFFS (OTA)" instead of // the bare key.