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.
This commit is contained in:
David Montero Crespo 2026-08-20 19:48:24 +02:00
parent f4ff3e7430
commit 2b7c9b6fb6
3 changed files with 69 additions and 5 deletions

View File

@ -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<ESP32BoardOptions>;
/** Sidebar / toolbar accents (fall back to a neutral chip icon). */
icon?: string;
color?: string;

View File

@ -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;

View File

@ -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<BoardKind> = 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<ESP32BoardOptions> {
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<ESP32BoardOptions> | null {
const born = boardBornOptions(kind);
if (Object.keys(born).length === 0) return null;
const opts: Partial<ESP32BoardOptions> = { ...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.