diff --git a/cli/src/commands/board/build-runtime.ts b/cli/src/commands/board/build-runtime.ts new file mode 100644 index 00000000..120bc106 --- /dev/null +++ b/cli/src/commands/board/build-runtime.ts @@ -0,0 +1,39 @@ +import { Command } from "commander"; +import chalk from "chalk"; +import { logger } from "../../core/logger"; +import { DEFAULT_DEVICE_NAME } from "../../config/project-config"; +import { getFlashRuntimeHandler } from "./flash-runtime"; + + +export async function handleBuildRuntimeCommand(board: string, options: { deviceName?: string }) { + try { + const handler = getFlashRuntimeHandler(board); + + if (!handler.isSetup()) { + logger.warn(`The environment for ${board} is not set up. Run 'bscript board setup ${board}' and try again.`); + return; + } + + const buildDir = await handler.build(options.deviceName); + + logger.br(); + logger.success(`Success to build the BlueScript runtime for ${board}`); + logger.info(`Build artifacts: ${chalk.yellow(buildDir)}`); + logger.info('To flash from another host, copy the build directory there and run:'); + logger.info(` ${chalk.yellow(`esptool.py --chip ${board} -p write_flash @flash_args`)} (in the copied directory)`); + logger.info(`or connect the board to this host and run ${chalk.yellow(`bscript board flash-runtime ${board}`)}`); + } catch (error) { + logger.error(`Failed to build the runtime for ${board}`); + logger.showError(error); + process.exit(1); + } +} + +export function registerBuildRuntimeCommand(program: Command) { + program + .command('build-runtime') + .description('build the BlueScript runtime for the board without flashing it.') + .argument('', 'the name of the board to build for (e.g., esp32, esp32s3)') + .option('-d, --device-name ', `BLE device name embedded in the runtime, the default is '${DEFAULT_DEVICE_NAME}'`) + .action(handleBuildRuntimeCommand); +} diff --git a/cli/src/commands/board/flash-runtime.ts b/cli/src/commands/board/flash-runtime.ts index 2a8da814..17423811 100644 --- a/cli/src/commands/board/flash-runtime.ts +++ b/cli/src/commands/board/flash-runtime.ts @@ -3,7 +3,8 @@ import inquirer from 'inquirer'; import * as path from 'path'; import * as os from 'os'; import { SerialPort } from 'serialport' -import { BoardName } from "../../config/board-utils"; +import { Esp32FamilyBoardName, isEsp32FamilyBoard } from "../../config/board-utils"; +import { ESP32_TARGET_BUILD_DIRS } from "@bscript/lang"; import { logger, runStep } from "../../core/logger"; import { execShell } from '../../core/command-exec'; import chalk from "chalk"; @@ -13,35 +14,69 @@ import { DEFAULT_DEVICE_NAME } from "../../config/project-config"; const RUNTIME_ESP_PORT_DIR = (runtimeDir: string) => path.join(runtimeDir, 'ports/esp32'); -abstract class FlashRuntimeHandler extends CommandHandlerWithUpdateCheck { +export abstract class FlashRuntimeHandler extends CommandHandlerWithUpdateCheck { abstract isSetup(): boolean; abstract eraseFlash(port: string): Promise; abstract flashRuntime(port: string, deviceName?: string): Promise; + // Build the runtime without flashing it. Returns the directory that holds the build artifacts. + abstract buildRuntime(deviceName?: string): Promise; async flash(port: string, deviceName?: string) { await runStep('Erasing flash...', () => this.eraseFlash(port)); await runStep('Flashing BlueScript runtime...', () => this.flashRuntime(port, deviceName)); } + + async build(deviceName?: string): Promise { + let buildDir = ''; + await runStep('Building BlueScript runtime...', async () => { + buildDir = await this.buildRuntime(deviceName); + }); + return buildDir; + } } -class ESP32FlashRuntimeHandler extends FlashRuntimeHandler { - readonly boardName: BoardName = 'esp32'; +export class ESP32FlashRuntimeHandler extends FlashRuntimeHandler { + readonly boardName: Esp32FamilyBoardName; + + constructor(boardName: Esp32FamilyBoardName = 'esp32') { + super(); + this.boardName = boardName; + } + + private get targetArgs(): string[] { + if (this.boardName === 'esp32') { + return []; + } + return [ + '-B', ESP32_TARGET_BUILD_DIRS[this.boardName], + '-D', `IDF_TARGET=${this.boardName}`, + '-D', `SDKCONFIG=sdkconfig.${this.boardName}`, + ]; + } isSetup(): boolean { return this.globalConfigHandler.isBoardSetup(this.boardName); } async eraseFlash(port: string) { - await this.runIdfPy(['erase-flash', '-p', port]); + await this.runIdfPy([...this.targetArgs, 'erase-flash', '-p', port]); } - + async flashRuntime(port: string, deviceName?: string) { deviceName = deviceName ?? DEFAULT_DEVICE_NAME; await this.runIdfPy( - ['-D', `DEVICE_NAME=${deviceName}`, 'build', 'flash', '-p', port], + [...this.targetArgs, '-D', `DEVICE_NAME=${deviceName}`, 'build', 'flash', '-p', port], ); } + async buildRuntime(deviceName?: string): Promise { + deviceName = deviceName ?? DEFAULT_DEVICE_NAME; + await this.runIdfPy( + [...this.targetArgs, '-D', `DEVICE_NAME=${deviceName}`, 'build'], + ); + return path.join(this.getEspPortDir(), ESP32_TARGET_BUILD_DIRS[this.boardName]); + } + private async runIdfPy(args: string[]) { const osType = os.platform(); const exportFile = this.getExportFile(); @@ -59,7 +94,7 @@ class ESP32FlashRuntimeHandler extends FlashRuntimeHandler { } private getExportFile() { - const boardConfig = this.globalConfigHandler.getBoardConfig('esp32'); + const boardConfig = this.globalConfigHandler.getBoardConfig(this.boardName); if (!boardConfig) { throw new Error('An unexpected error occurred: cannot find board config.'); } @@ -67,12 +102,12 @@ class ESP32FlashRuntimeHandler extends FlashRuntimeHandler { } } -function getFlashRuntimeHandler(board: string) { +export function getFlashRuntimeHandler(board: string) { if (board === 'host') { throw new Error('flash-runtime is not supported for the host board'); } - if (board === 'esp32') { - return new ESP32FlashRuntimeHandler(); + if (isEsp32FamilyBoard(board)) { + return new ESP32FlashRuntimeHandler(board); } throw new Error(`Unsupported board name: ${board}`); } @@ -147,8 +182,8 @@ export function registerFlashRuntimeCommand(program: Command) { program .command('flash-runtime') .description('flash the BlueScript runtime to the board.') - .argument('', 'the name of the board to flash (e.g., esp32)') + .argument('', 'the name of the board to flash (e.g., esp32, esp32s3)') .option('-p, --port ', 'serial port to flash to') .option('-d, --device-name ', `device name to flash to, the default is '${DEFAULT_DEVICE_NAME}'`) .action(handleFlashRuntimeCommand); -} \ No newline at end of file +} diff --git a/cli/src/commands/board/remove.ts b/cli/src/commands/board/remove.ts index 44ff3b82..3fdfc84b 100644 --- a/cli/src/commands/board/remove.ts +++ b/cli/src/commands/board/remove.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import inquirer from 'inquirer'; -import { BoardName, isValidBoard } from "../../config/board-utils"; -import { logger, runStep } from "../../core/logger"; +import { BoardName, isValidBoard, isEsp32FamilyBoard, ESP32_FAMILY_BOARD_NAMES } from "../../config/board-utils"; +import { logger, runStep, skip } from "../../core/logger"; import { CommandHandlerWithUpdateCheck } from "../command"; import { BoardEnv, createBoardEnv } from "../../platforms/board-env"; @@ -17,10 +17,28 @@ class RemoveHandler extends CommandHandlerWithUpdateCheck { } async remove() { - await runStep('Removing...', async () => this.boardEnv.removeBoardRoot()); + await runStep('Removing...', async () => { + if (this.isBoardRootShared()) { + return skip(`the ESP-IDF installation is still used by ${this.otherBoardsSharingRoot().join(', ')}.`); + } + this.boardEnv.removeBoardRoot(); + }); this.globalConfigHandler.removeBoardConfig(this.boardName); this.globalConfigHandler.save(); } + + // Boards of the ESP32 family share one ESP-IDF installation. + private otherBoardsSharingRoot(): BoardName[] { + if (!isEsp32FamilyBoard(this.boardName)) { + return []; + } + return ESP32_FAMILY_BOARD_NAMES.filter( + b => b !== this.boardName && this.globalConfigHandler.isBoardSetup(b)); + } + + private isBoardRootShared(): boolean { + return this.otherBoardsSharingRoot().length > 0; + } isSetup(): boolean { return this.globalConfigHandler.isBoardSetup(this.boardName); @@ -76,7 +94,7 @@ export function registerRemoveCommand(program: Command) { program .command('remove') .description('remove the environment for the specified board') - .argument('', 'name of the board to remove (e.g., esp32)') + .argument('', 'name of the board to remove (e.g., esp32, esp32s3)') .option('-f, --force', 'skip confirmation prompt') .action(handleRemoveCommand); } \ No newline at end of file diff --git a/cli/src/commands/board/setup/base.ts b/cli/src/commands/board/setup/base.ts index c4f5dbe7..cfb1a334 100644 --- a/cli/src/commands/board/setup/base.ts +++ b/cli/src/commands/board/setup/base.ts @@ -36,7 +36,7 @@ export abstract class SetupHandler extends CommandHandlerWithUpdateCheck { async setup() { this.boardEnv.ensureBlueScriptDir(); - this.boardEnv.refreshBoardRoot(); + this.prepareBoardRoot(); for (const step of this.setupSteps) { await runStep(step.actionMessage, step.action); } @@ -48,6 +48,11 @@ export abstract class SetupHandler extends CommandHandlerWithUpdateCheck { return this.setupSteps.map(step => step.description); }; + // Prepare the directory for the board environment. By default the directory is recreated. + protected prepareBoardRoot() { + this.boardEnv.refreshBoardRoot(); + } + abstract loadBoardSetupSteps(): void; abstract setBoardConfig(): Promise; diff --git a/cli/src/commands/board/setup/esp32.ts b/cli/src/commands/board/setup/esp32.ts index 10681f6e..d2a5a9b6 100644 --- a/cli/src/commands/board/setup/esp32.ts +++ b/cli/src/commands/board/setup/esp32.ts @@ -4,25 +4,52 @@ import { skip } from "../../../core/logger"; import * as path from 'path'; import * as os from 'os'; import * as fs from '../../../core/fs'; -import { BoardName } from "../../../config/board-utils"; +import { BoardName, Esp32FamilyBoardName, ESP32_FAMILY_BOARD_NAMES } from "../../../config/board-utils"; import { Esp32UnixEnv, Esp32WindowsEnv } from "../../../platforms/board-env/esp32-env"; import { GLOBAL_SETTINGS } from "../../../config/constants"; export abstract class Esp32SetupHandler extends SetupHandler { - boardName: BoardName = "esp32"; + boardName: BoardName; abstract boardEnv: Esp32UnixEnv | Esp32WindowsEnv; protected espIdfPath?: string; protected pythonCommand?: string; protected makeCommand?: string; - constructor(espIdfPath?: string) { + constructor(target: Esp32FamilyBoardName, espIdfPath?: string) { super(); + this.boardName = target; this.espIdfPath = espIdfPath; } + // Other boards of the ESP32 family that have already been set up. + // They share the ESP-IDF installation with this board. + protected otherEsp32FamilyBoardsSetup(): Esp32FamilyBoardName[] { + return ESP32_FAMILY_BOARD_NAMES.filter( + b => b !== this.boardName && this.globalConfigHandler.isBoardSetup(b)); + } + + protected reuseExistingEspIdf(): boolean { + return this.otherEsp32FamilyBoardsSetup().length > 0 && this.boardEnv.isEspIdfInstalled(); + } + + protected prepareBoardRoot() { + if (this.reuseExistingEspIdf()) { + this.boardEnv.ensureBoardRoot(); + } else { + this.boardEnv.refreshBoardRoot(); + } + } + loadEspIdfSetupSteps(): void { - if (this.espIdfPath) { + if (this.reuseExistingEspIdf()) { + const others = this.otherEsp32FamilyBoardsSetup().join(', '); + this.setupSteps.push({ + description: `Reuse ESP-IDF ${this.boardEnv.idfVersion} already installed for ${others}.`, + actionMessage: `Reusing existing ESP-IDF...`, + action: async () => skip('already installed.'), + }); + } else if (this.espIdfPath) { this.setupSteps.push({ description: `Copy ESP-IDF from ${this.espIdfPath}.`, actionMessage: `Copying ESP-IDF from ${this.espIdfPath}...`, @@ -37,7 +64,7 @@ export abstract class Esp32SetupHandler extends SetupHandler { } this.setupSteps.push({ - description: "Run ESP-IDF install script.", + description: `Run ESP-IDF install script for ${this.boardName}.`, actionMessage: "Running ESP-IDF install script...", action: this.runEspIdfInstallScriptStep.bind(this), }); @@ -87,9 +114,9 @@ export abstract class Esp32SetupHandler extends SetupHandler { export class Esp32DarwinSetupHandler extends Esp32SetupHandler { boardEnv: Esp32UnixEnv; - constructor(espIdfPath?: string) { - super(espIdfPath); - this.boardEnv = new Esp32UnixEnv(); + constructor(target: Esp32FamilyBoardName = 'esp32', espIdfPath?: string) { + super(target, espIdfPath); + this.boardEnv = new Esp32UnixEnv(target); } loadBoardSetupSteps(): void { @@ -147,9 +174,9 @@ export class Esp32LinuxSetupHandler extends Esp32SetupHandler { } } - constructor(espIdfPath?: string) { - super(espIdfPath); - this.boardEnv = new Esp32UnixEnv(); + constructor(target: Esp32FamilyBoardName = 'esp32', espIdfPath?: string) { + super(target, espIdfPath); + this.boardEnv = new Esp32UnixEnv(target); this.distType = this.getDistribution(); } @@ -300,9 +327,9 @@ KERNEL=="ttyUSB[0-9]*", MODE="0666" export class Esp32WindowsSetupHandler extends Esp32SetupHandler { boardEnv: Esp32WindowsEnv; - constructor(espIdfPath?: string) { - super(espIdfPath); - this.boardEnv = new Esp32WindowsEnv(); + constructor(target: Esp32FamilyBoardName = 'esp32', espIdfPath?: string) { + super(target, espIdfPath); + this.boardEnv = new Esp32WindowsEnv(target); } loadBoardSetupSteps(): void { diff --git a/cli/src/commands/board/setup/index.ts b/cli/src/commands/board/setup/index.ts index 90e3a12d..a276af25 100644 --- a/cli/src/commands/board/setup/index.ts +++ b/cli/src/commands/board/setup/index.ts @@ -7,17 +7,18 @@ import chalk from "chalk"; import { SetupHandler } from "./base"; import { Esp32DarwinSetupHandler, Esp32WindowsSetupHandler, Esp32LinuxSetupHandler } from "./esp32"; import { HostUnixSetupHandler, HostWindowsSetupHandler } from "./host"; +import { isEsp32FamilyBoard } from "../../../config/board-utils"; function getSetupHandler(board: string, espIdfPath?: string): SetupHandler { const osType = os.platform(); - if (board === 'esp32') { + if (isEsp32FamilyBoard(board)) { if (osType === 'darwin') - return new Esp32DarwinSetupHandler(espIdfPath); + return new Esp32DarwinSetupHandler(board, espIdfPath); if (osType === 'linux') - return new Esp32LinuxSetupHandler(espIdfPath); + return new Esp32LinuxSetupHandler(board, espIdfPath); if (osType === 'win32') - return new Esp32WindowsSetupHandler(espIdfPath); + return new Esp32WindowsSetupHandler(board, espIdfPath); throw new Error(`Unsupported OS type: ${osType}.`); } if (board === 'host') { @@ -82,7 +83,7 @@ export function registerSetupCommand(program: Command) { program .command('setup') .description('set up the environment for the specified board') - .argument('', 'name of the board to setup (e.g., esp32)') + .argument('', 'name of the board to setup (e.g., esp32, esp32s3)') .option('--esp-idf ', 'path to an existing ESP-IDF directory to copy') .action(handleSetupCommand); } diff --git a/cli/src/commands/board/update.ts b/cli/src/commands/board/update.ts index 85634deb..7dc16e76 100644 --- a/cli/src/commands/board/update.ts +++ b/cli/src/commands/board/update.ts @@ -7,6 +7,7 @@ import * as path from 'path'; import { CommonBoardEnv, createBoardEnv, Esp32Env } from "../../platforms/board-env"; import { Esp32BoardConfig, GlobalConfig, GlobalConfigHandler } from "../../config/global-config"; import chalk from "chalk"; +import { ESP32_FAMILY_BOARD_NAMES, Esp32FamilyBoardName } from "../../config/board-utils"; class UpdateHandler extends CommandHandler { @@ -16,6 +17,8 @@ class UpdateHandler extends CommandHandler { private existingEspDir: string | undefined; private tmpRuntimeDir = path.join(GLOBAL_SETTINGS.BLUESCRIPT_DIR, 'tmp-runtime'); private tmpEspDir = path.join(GLOBAL_SETTINGS.BLUESCRIPT_DIR, 'tmp-esp'); + // The ESP-IDF installation is shared by the ESP32 family; refresh it at most once. + private espIdfRefreshed = false; constructor() { super(); @@ -47,7 +50,9 @@ class UpdateHandler extends CommandHandler { async update() { try { await this.updateRuntimeStep(); - await this.updateEsp32Step(); + for (const board of ESP32_FAMILY_BOARD_NAMES) { + await this.updateEsp32Step(board); + } await this.updateHostStep(); this.globalConfigHandler.setVersion(GLOBAL_SETTINGS.VM_VERSION); this.globalConfigHandler.save(); @@ -79,14 +84,14 @@ class UpdateHandler extends CommandHandler { }); } - private updateEsp32Step() { - return runStep('Updating the environment for esp32...', async () => { - if (!("esp32" in (this.oldGlobalConfig?.boards ?? {}))) { + private updateEsp32Step(board: Esp32FamilyBoardName) { + return runStep(`Updating the environment for ${board}...`, async () => { + if (!(board in (this.oldGlobalConfig?.boards ?? {}))) { return skip('not setup'); } - const esp32Config = this.oldGlobalConfig?.boards.esp32; - const esp32Env = createBoardEnv('esp32'); - await this.updateEsp32(esp32Env, esp32Config); + const esp32Config = this.oldGlobalConfig?.boards[board]; + const esp32Env = createBoardEnv(board); + await this.updateEsp32(board, esp32Env, esp32Config); }); } @@ -126,19 +131,22 @@ class UpdateHandler extends CommandHandler { }); } - private async updateEsp32(esp32Env: Esp32Env, boardConfig?: Esp32BoardConfig) { + private async updateEsp32(board: Esp32FamilyBoardName, esp32Env: Esp32Env, boardConfig?: Esp32BoardConfig) { this.existingEspDir = esp32Env.espRootDir; if (boardConfig?.idfVersion !== esp32Env.idfVersion) { - fs.moveDir(esp32Env.espRootDir, this.tmpEspDir); - esp32Env.refreshBoardRoot(); - await esp32Env.cloneEspIdf(); + if (!this.espIdfRefreshed) { + fs.moveDir(esp32Env.espRootDir, this.tmpEspDir); + esp32Env.refreshBoardRoot(); + await esp32Env.cloneEspIdf(); + this.espIdfRefreshed = true; + } await esp32Env.runEspIdfInstallScript(); } let pythonCommand = boardConfig?.toolchain.python ?? await esp32Env.getPythonCommand(); let makeCommand = boardConfig?.toolchain.make ?? await esp32Env.getMakeCommand(); const xtensaGccDir = await esp32Env.getXtensaGccDir(pythonCommand); - this.globalConfigHandler.setBoardConfig('esp32', { + this.globalConfigHandler.setBoardConfig(board, { idfVersion: esp32Env.idfVersion, rootDir: esp32Env.espRootDir, exportFile: esp32Env.idfExportFile, diff --git a/cli/src/config/board-utils.ts b/cli/src/config/board-utils.ts index 2563f65e..95fb8b81 100644 --- a/cli/src/config/board-utils.ts +++ b/cli/src/config/board-utils.ts @@ -1,3 +1,10 @@ -export const BOARD_NAMES = ['esp32', 'host'] as const; +export const BOARD_NAMES = ['esp32', 'esp32s3', 'host'] as const; export type BoardName = (typeof BOARD_NAMES)[number]; export const isValidBoard = (board: string): board is BoardName => (BOARD_NAMES as readonly string[]).includes(board); + +// Boards of the ESP32 family. They share the same ESP-IDF installation and the +// same runtime port (microcontroller/ports/esp32), but use different chip targets. +export const ESP32_FAMILY_BOARD_NAMES = ['esp32', 'esp32s3'] as const; +export type Esp32FamilyBoardName = (typeof ESP32_FAMILY_BOARD_NAMES)[number]; +export const isEsp32FamilyBoard = (board: string): board is Esp32FamilyBoardName => + (ESP32_FAMILY_BOARD_NAMES as readonly string[]).includes(board); diff --git a/cli/src/config/global-config.ts b/cli/src/config/global-config.ts index 9074a870..aeab0d57 100644 --- a/cli/src/config/global-config.ts +++ b/cli/src/config/global-config.ts @@ -29,6 +29,7 @@ const hostBoardSchema = z.object({ const boardConfigSchema = z.object({ esp32: esp32BoardSchema.optional(), + esp32s3: esp32BoardSchema.optional(), host: hostBoardSchema.optional(), }); diff --git a/cli/src/config/project-config.ts b/cli/src/config/project-config.ts index e06674c9..242c3b27 100644 --- a/cli/src/config/project-config.ts +++ b/cli/src/config/project-config.ts @@ -33,12 +33,18 @@ const esp32ProjectSchema = baseConfigSchema.extend({ espIdfComponents: z.array(z.string()).default([]), }); +const esp32s3ProjectSchema = baseConfigSchema.extend({ + boardName: z.literal('esp32s3'), + espIdfComponents: z.array(z.string()).default([]), +}); + const hostProjectSchema = baseConfigSchema.extend({ boardName: z.literal('host'), }); const projectConfigSchema = z.discriminatedUnion('boardName', [ esp32ProjectSchema, + esp32s3ProjectSchema, hostProjectSchema, ]); diff --git a/cli/src/index.ts b/cli/src/index.ts index de0ca217..f103e94f 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -7,6 +7,7 @@ import packageJson from '../package.json'; import { registerSetupCommand } from './commands/board/setup/index'; import { registerRemoveCommand } from './commands/board/remove'; import { registerFlashRuntimeCommand } from './commands/board/flash-runtime'; +import { registerBuildRuntimeCommand } from './commands/board/build-runtime'; import { registerListCommand } from './commands/board/list'; import { registerCreateProjectCommand } from './commands/project/create'; import { registerRunCommand } from './commands/project/run'; @@ -26,6 +27,7 @@ function registerBoardCommands(program: Command) { registerSetupCommand(boardCommand); registerRemoveCommand(boardCommand); registerFlashRuntimeCommand(boardCommand); + registerBuildRuntimeCommand(boardCommand); registerListCommand(boardCommand); registerFullcleanCommand(boardCommand); registerUpdateCommand(boardCommand); diff --git a/cli/src/platforms/board-env/esp32-env.ts b/cli/src/platforms/board-env/esp32-env.ts index 83cfd2c5..8a9cc602 100644 --- a/cli/src/platforms/board-env/esp32-env.ts +++ b/cli/src/platforms/board-env/esp32-env.ts @@ -3,14 +3,24 @@ import * as fs from '../../core/fs'; import { GLOBAL_SETTINGS } from "../../config/constants"; import { simpleExec, execShell, execWithLog } from '../../core/command-exec'; import { BoardEnv, isPackageInstalledOnUnix, isPackageInstalledOnWindows } from './common-env'; +import { Esp32FamilyBoardName } from '../../config/board-utils'; const XTENSA_TOOLCHAIN_DIR = 'xtensa-esp-elf'; -const XTENSA_GCC_NAME = 'xtensa-esp32-elf-gcc'; -const XTENSA_AR_NAME = 'xtensa-esp32-elf-ar'; -const XTENSA_LD_NAME = 'xtensa-esp32-elf-ld'; export abstract class Esp32Env extends BoardEnv { + // Chip target passed to ESP-IDF (`esp32`, `esp32s3`, ...). + readonly target: Esp32FamilyBoardName; + + constructor(target: Esp32FamilyBoardName = 'esp32') { + super(); + this.target = target; + } + + // The ESP-IDF installation is shared by all boards of the ESP32 family. get espRootDir() { return path.join(GLOBAL_SETTINGS.BLUESCRIPT_DIR, 'esp'); } + get xtensaGccName() { return `xtensa-${this.target}-elf-gcc`; } + get xtensaArName() { return `xtensa-${this.target}-elf-ar`; } + get xtensaLdName() { return `xtensa-${this.target}-elf-ld`; } get idfDir() { return path.join(this.espRootDir, 'esp-idf'); } get idfToolsPyFile() { return path.join(this.idfDir, 'tools/idf_tools.py'); } get idfVersion() { return 'v5.4'; } @@ -50,6 +60,10 @@ export abstract class Esp32Env extends BoardEnv { } } + isEspIdfInstalled() { + return fs.exists(this.idfToolsPyFile); + } + removeBoardRoot() { fs.removeDir(this.espRootDir); } @@ -59,6 +73,10 @@ export abstract class Esp32Env extends BoardEnv { fs.makeDir(this.espRootDir); } + ensureBoardRoot() { + fs.makeDir(this.espRootDir); + } + protected parseKeyValueExport(stdout: string): Map { const env = new Map(); @@ -119,12 +137,12 @@ export abstract class Esp32Env extends BoardEnv { export class Esp32UnixEnv extends Esp32Env { get idfInstallShFile() { return path.join(this.idfDir, 'install.sh'); } get idfExportFile() { return path.join(this.idfDir, 'export.sh'); } - get xtensaGccFileName() { return XTENSA_GCC_NAME; } - get xtensaArFileName() { return XTENSA_AR_NAME; } - get xtensaLdFileName() { return XTENSA_LD_NAME; } + get xtensaGccFileName() { return this.xtensaGccName; } + get xtensaArFileName() { return this.xtensaArName; } + get xtensaLdFileName() { return this.xtensaLdName; } async runEspIdfInstallScript() { - await execShell(`bash ${JSON.stringify(this.idfInstallShFile)} esp32`); + await execShell(`bash ${JSON.stringify(this.idfInstallShFile)} ${this.target}`); } async getXtensaGccDir(pythonCommand: string) { @@ -156,12 +174,12 @@ export class Esp32WindowsEnv extends Esp32Env { get idfExportBatFile() { return path.join(this.idfDir, 'export.bat'); } get idfInstallBatFile() { return path.join(this.idfDir, 'install.bat'); } get idfExportFile() { return this.idfExportBatFile; } - get xtensaGccFileName(): string { return `${XTENSA_GCC_NAME}.exe`; } - get xtensaArFileName(): string { return `${XTENSA_AR_NAME}.exe`; } - get xtensaLdFileName(): string { return `${XTENSA_LD_NAME}.exe`; } + get xtensaGccFileName(): string { return `${this.xtensaGccName}.exe`; } + get xtensaArFileName(): string { return `${this.xtensaArName}.exe`; } + get xtensaLdFileName(): string { return `${this.xtensaLdName}.exe`; } async runEspIdfInstallScript() { - await execShell(`${this.idfInstallBatFile} esp32`); + await execShell(`${this.idfInstallBatFile} ${this.target}`); } async getXtensaGccDir(pythonCommand: string) { diff --git a/cli/src/platforms/board-env/index.ts b/cli/src/platforms/board-env/index.ts index fee8cbaa..5d110fd2 100644 --- a/cli/src/platforms/board-env/index.ts +++ b/cli/src/platforms/board-env/index.ts @@ -1,23 +1,24 @@ import * as os from 'os'; import { Esp32Env, Esp32UnixEnv, Esp32WindowsEnv } from './esp32-env'; import { CommonBoardEnv, BoardEnv } from './common-env'; -import { BoardName } from '../../config/board-utils'; +import { BoardName, isEsp32FamilyBoard } from '../../config/board-utils'; import { HostEnv, HostUnixEnv, HostWindowsEnv } from './host-env'; type BoardEnvMap = { esp32: Esp32Env; + esp32s3: Esp32Env; host: HostEnv; }; export function createBoardEnv(board: B): BoardEnvMap[B]; export function createBoardEnv(board: BoardName): BoardEnvMap[BoardName] { const osType = os.platform(); - if (board === 'esp32') { + if (isEsp32FamilyBoard(board)) { if (osType === 'darwin' || osType === 'linux') - return new Esp32UnixEnv(); + return new Esp32UnixEnv(board); if (osType === 'win32') - return new Esp32WindowsEnv(); + return new Esp32WindowsEnv(board); throw new Error(`Unsupported OS type: ${osType}.`); } if (board === 'host') { diff --git a/cli/src/platforms/compiler/esp32-compiler-adapter.ts b/cli/src/platforms/compiler/esp32-compiler-adapter.ts index 5dce8205..518bd91b 100644 --- a/cli/src/platforms/compiler/esp32-compiler-adapter.ts +++ b/cli/src/platforms/compiler/esp32-compiler-adapter.ts @@ -1,6 +1,6 @@ import { GlobalConfigHandler, Esp32BoardConfig } from "../../config/global-config"; import { ProjectConfigHandler, PROJECT_DEFAULT_PATHS } from "../../config/project-config"; -import { BoardName } from "../../config/board-utils"; +import { Esp32FamilyBoardName } from "../../config/board-utils"; import { CompilerSession, MemoryImage, MemoryLayout, Esp32Toolchain, Esp32ToolchainConfig, Project, PackageForEsp32 @@ -9,23 +9,35 @@ import { CompilerAdapter, CompileContext } from "./compiler-adapter"; import * as path from 'path'; -const DUMMY_MEMORY_LAYOUT: MemoryLayout = { - iram: { address: 0x40096c34, size: 1000000 }, - dram: { address: 0x3ffd5b1c, size: 1000000 }, - iflash: { address: 0x40150000, size: 1000000 }, - dflash: { address: 0x3f43a000, size: 1000000 }, +// Memory layouts used only for `project check` (no device is connected). +// The real layout is obtained from the device at runtime. +const DUMMY_MEMORY_LAYOUTS: Record = { + esp32: { + iram: { address: 0x40096c34, size: 1000000 }, + dram: { address: 0x3ffd5b1c, size: 1000000 }, + iflash: { address: 0x40150000, size: 1000000 }, + dflash: { address: 0x3f43a000, size: 1000000 }, + }, + esp32s3: { + iram: { address: 0x40380000, size: 1000000 }, + dram: { address: 0x3fc90000, size: 1000000 }, + iflash: { address: 0x42100000, size: 1000000 }, + dflash: { address: 0x3c100000, size: 1000000 }, + }, }; export class Esp32CompilerAdapter implements CompilerAdapter { - readonly boardName: BoardName = 'esp32'; + readonly boardName: Esp32FamilyBoardName; private boardConfig: Esp32BoardConfig; private compiler?: CompilerSession; constructor( private globalConfigHandler: GlobalConfigHandler, private projectConfigHandler: ProjectConfigHandler, + boardName: Esp32FamilyBoardName = 'esp32', ) { - const boardConfig = this.globalConfigHandler.getBoardConfig('esp32'); + this.boardName = boardName; + const boardConfig = this.globalConfigHandler.getBoardConfig(boardName); if (boardConfig === undefined) { throw new Error(`The environment for ${this.boardName} is not set up.`); } @@ -33,7 +45,7 @@ export class Esp32CompilerAdapter implements CompilerAdapter { } async buildForCheck(): Promise { - return this.buildProject({ memoryLayout: DUMMY_MEMORY_LAYOUT }); + return this.buildProject({ memoryLayout: DUMMY_MEMORY_LAYOUTS[this.boardName] }); } async buildProject(context?: CompileContext): Promise { @@ -65,6 +77,7 @@ export class Esp32CompilerAdapter implements CompilerAdapter { } return { runtimeDir, + target: this.boardName, compilerToolchain: this.boardConfig.toolchain, espDir: this.boardConfig.rootDir, }; @@ -73,7 +86,7 @@ export class Esp32CompilerAdapter implements CompilerAdapter { function createEsp32PackageReader( - _boardName: BoardName, + boardName: Esp32FamilyBoardName, projectConfigHandler: ProjectConfigHandler, ): (name: string) => PackageForEsp32 { return (name: string) => { @@ -83,8 +96,8 @@ function createEsp32PackageReader( const root = isMain ? mainRoot : subPackageRoot; try { const configHandler = isMain - ? projectConfigHandler.asBoard('esp32') - : ProjectConfigHandler.load(root).asBoard('esp32'); + ? projectConfigHandler.asBoard(boardName) + : ProjectConfigHandler.load(root).asBoard(boardName); return new PackageForEsp32( name, { diff --git a/cli/src/platforms/compiler/index.ts b/cli/src/platforms/compiler/index.ts index 32cbe9ec..12174c61 100644 --- a/cli/src/platforms/compiler/index.ts +++ b/cli/src/platforms/compiler/index.ts @@ -1,6 +1,6 @@ import { GlobalConfigHandler } from "../../config/global-config"; import { ProjectConfigHandler } from "../../config/project-config"; -import { BoardName } from "../../config/board-utils"; +import { BoardName, isEsp32FamilyBoard } from "../../config/board-utils"; import { CompilerAdapter } from "../compiler/compiler-adapter"; import { Esp32CompilerAdapter } from "../compiler/esp32-compiler-adapter"; import { HostCompilerAdapter } from "../compiler/host-compiler-adapter"; @@ -13,8 +13,8 @@ export function getCompilerAdapter( globalConfigHandler: GlobalConfigHandler, projectConfigHandler: ProjectConfigHandler, ): CompilerAdapter { - if (boardName === 'esp32') { - return new Esp32CompilerAdapter(globalConfigHandler, projectConfigHandler); + if (isEsp32FamilyBoard(boardName)) { + return new Esp32CompilerAdapter(globalConfigHandler, projectConfigHandler, boardName); } if (boardName === 'host') { return new HostCompilerAdapter(globalConfigHandler, projectConfigHandler); diff --git a/cli/src/platforms/runtime/index.ts b/cli/src/platforms/runtime/index.ts index 7ac1bf31..88823f18 100644 --- a/cli/src/platforms/runtime/index.ts +++ b/cli/src/platforms/runtime/index.ts @@ -1,6 +1,6 @@ import { GlobalConfigHandler } from "../../config/global-config"; import { DEFAULT_DEVICE_NAME } from "../../config/project-config"; -import { BoardName } from "../../config/board-utils"; +import { BoardName, isEsp32FamilyBoard } from "../../config/board-utils"; import { ProgramOutput } from "../../core/logger/program-output"; import { BoardRuntime } from "../runtime/board-runtime"; import { Esp32BoardRuntime } from "../runtime/esp32-board-runtime"; @@ -16,7 +16,7 @@ export function getBoardRuntime( deviceName?: string, onUnexpectedDisconnect?: () => void, ): BoardRuntime { - if (boardName === 'esp32') { + if (isEsp32FamilyBoard(boardName)) { const _deviceName = deviceName ?? DEFAULT_DEVICE_NAME; return new Esp32BoardRuntime(_deviceName, programOutput, onUnexpectedDisconnect); } diff --git a/cli/tests/commands/board/esp32s3.test.ts b/cli/tests/commands/board/esp32s3.test.ts new file mode 100644 index 00000000..765f6a2c --- /dev/null +++ b/cli/tests/commands/board/esp32s3.test.ts @@ -0,0 +1,211 @@ +import { handleSetupCommand } from '../../../src/commands/board/setup'; +import { handleRemoveCommand } from '../../../src/commands/board/remove'; +import { handleFlashRuntimeCommand } from '../../../src/commands/board/flash-runtime'; +import os from 'os'; +import * as path from 'path'; +import * as fs from '../../../src/core/fs'; +import { + mockedSimpleExec, + mockedExecWithLog, + mockedExecShell, + mockedInquirer, + mockedLogger, +} from '../mock-helpers'; +import { + deleteGlobalEnv, + getGlobalConfig, + setupGlobalEnv, + setupGlobalEnvWithEsp32, + spyGlobalSettings, + getTestEspRootDir, + getTestEspIdfExportFile, + getTestRuntimeDir, + isEsp32IdfToolsExportPythonCommand, + setupEmpyGlobalEnv, + DUMMY_VM_VERSION, +} from '../global-env-helper'; +import { GLOBAL_SETTINGS } from '../../../src/config/constants'; +import { Esp32UnixEnv } from '../../../src/platforms/board-env/esp32-env'; + +jest.mock('serialport', () => ({ SerialPort: { list: jest.fn(async () => []) } })); +jest.mock('os', () => ({ + ...jest.requireActual('os'), + platform: jest.fn(() => 'darwin'), +})); + +function esp32s3ToolchainDir() { + return path.join(GLOBAL_SETTINGS.BLUESCRIPT_DIR, '.espressif/tools/xtensa-esp-elf/bin'); +} + +function mockShellCommands() { + mockedSimpleExec.mockImplementation(async (cmd, args) => { + if (cmd === 'which') { + return ''; + } + if (isEsp32IdfToolsExportPythonCommand(cmd) && args.some((arg: string) => arg.includes('export'))) { + const gccDir = esp32s3ToolchainDir(); + fs.makeDir(gccDir); + fs.writeFile(path.join(gccDir, 'xtensa-esp32-elf-gcc'), ''); + fs.writeFile(path.join(gccDir, 'xtensa-esp32s3-elf-gcc'), ''); + return `PATH=${gccDir}:/xtensa-esp-elf-gdb/bin`; + } + if (cmd === 'python' && args[1]?.includes('import sys')) { + return '3'; + } + return ''; + }); + mockedExecWithLog.mockImplementation(async () => ''); + mockedExecShell.mockImplementation(async () => {}); +} + +function setupGlobalEnvWithEsp32AndEsp32s3() { + const board = { + idfVersion: new Esp32UnixEnv().idfVersion, + rootDir: getTestEspRootDir(), + exportFile: getTestEspIdfExportFile(), + toolchain: { gcc: 'gcc', ar: 'ar', ld: 'ld', make: 'make', python: 'python' }, + }; + setupGlobalEnv({ + version: DUMMY_VM_VERSION, + runtimeDir: getTestRuntimeDir(), + boards: { esp32: board, esp32s3: { ...board } }, + }); + fs.makeDir(getTestEspRootDir()); + fs.makeDir(getTestRuntimeDir()); +} + +describe('esp32s3 board', () => { + beforeAll(() => { + spyGlobalSettings('esp32s3'); + }); + + beforeEach(() => { + deleteGlobalEnv(); + }); + + afterEach(() => { + jest.clearAllMocks(); + deleteGlobalEnv(); + }); + + describe('setup', () => { + it('clones ESP-IDF and installs the esp32s3 toolchain when no ESP32-family board is set up', async () => { + setupEmpyGlobalEnv(); + mockShellCommands(); + mockedInquirer.prompt.mockResolvedValue({ proceed: true }); + + await handleSetupCommand('esp32s3', {}); + + expect(mockedExecWithLog).toHaveBeenCalledWith( + 'git', expect.arrayContaining(['clone']), expect.anything()); + expect(mockedExecShell).toHaveBeenCalledWith(expect.stringContaining('install.sh" esp32s3')); + const config = getGlobalConfig(); + expect(config.boards.esp32s3.toolchain.gcc).toBe(path.join(esp32s3ToolchainDir(), 'xtensa-esp32s3-elf-gcc')); + expect(config.boards.esp32s3.toolchain.ld).toBe(path.join(esp32s3ToolchainDir(), 'xtensa-esp32s3-elf-ld')); + expect(mockedLogger.error).not.toHaveBeenCalled(); + }); + + it('reuses the ESP-IDF installed for esp32 instead of cloning again', async () => { + setupGlobalEnvWithEsp32(); + const env = new Esp32UnixEnv('esp32s3'); + fs.makeDir(path.dirname(env.idfToolsPyFile)); + fs.writeFile(env.idfToolsPyFile, ''); + const marker = path.join(env.espRootDir, 'MARKER'); + fs.writeFile(marker, 'keep'); + mockShellCommands(); + mockedInquirer.prompt.mockResolvedValue({ proceed: true }); + + await handleSetupCommand('esp32s3', {}); + + expect(mockedExecWithLog).not.toHaveBeenCalledWith('git', expect.anything(), expect.anything()); + expect(mockedExecShell).toHaveBeenCalledWith(expect.stringContaining('install.sh" esp32s3')); + // The shared ESP-IDF directory must not be wiped. + expect(fs.exists(marker)).toBe(true); + const config = getGlobalConfig(); + expect(Object.keys(config.boards)).toEqual(expect.arrayContaining(['esp32', 'esp32s3'])); + expect(mockedLogger.error).not.toHaveBeenCalled(); + }); + }); + + describe('remove', () => { + it('keeps the shared ESP-IDF directory while esp32 is still set up', async () => { + setupGlobalEnvWithEsp32AndEsp32s3(); + mockedInquirer.prompt.mockResolvedValue({ proceed: true }); + + await handleRemoveCommand('esp32s3', {}); + + expect(fs.exists(getTestEspRootDir())).toBe(true); + expect(Object.keys(getGlobalConfig().boards)).not.toContain('esp32s3'); + expect(Object.keys(getGlobalConfig().boards)).toContain('esp32'); + expect(mockedLogger.error).not.toHaveBeenCalled(); + }); + + it('removes the ESP-IDF directory when it is the last ESP32-family board', async () => { + setupGlobalEnvWithEsp32AndEsp32s3(); + mockedInquirer.prompt.mockResolvedValue({ proceed: true }); + + await handleRemoveCommand('esp32s3', {}); + await handleRemoveCommand('esp32', {}); + + expect(fs.exists(getTestEspRootDir())).toBe(false); + expect(Object.keys(getGlobalConfig().boards)).not.toContain('esp32'); + }); + }); + + describe('flash-runtime', () => { + it('builds with the esp32s3 target in its own build directory', async () => { + setupGlobalEnvWithEsp32AndEsp32s3(); + mockedExecShell.mockImplementation(async () => {}); + + await handleFlashRuntimeCommand('esp32s3', { port: '/dev/ttyACM0' }); + + expect(mockedExecShell).toHaveBeenCalledWith( + expect.stringContaining('-B build-esp32s3 -D IDF_TARGET=esp32s3 -D SDKCONFIG=sdkconfig.esp32s3 erase-flash'), + { cwd: expect.stringContaining(path.join('ports', 'esp32')) }); + expect(mockedExecShell).toHaveBeenCalledWith( + expect.stringContaining('-B build-esp32s3 -D IDF_TARGET=esp32s3 -D SDKCONFIG=sdkconfig.esp32s3 -D DEVICE_NAME='), + { cwd: expect.stringContaining(path.join('ports', 'esp32')) }); + expect(mockedLogger.error).not.toHaveBeenCalled(); + }); + + it('fails when only esp32 is set up', async () => { + setupGlobalEnvWithEsp32(); + + await handleFlashRuntimeCommand('esp32s3', { port: '/dev/ttyACM0' }); + + expect(mockedLogger.warn).toHaveBeenCalledWith(expect.stringContaining('esp32s3 is not set up')); + expect(mockedExecShell).not.toHaveBeenCalled(); + }); + }); +}); + +describe('board build-runtime', () => { + beforeAll(() => { spyGlobalSettings('build-runtime'); }); + beforeEach(() => { deleteGlobalEnv(); }); + afterEach(() => { jest.clearAllMocks(); deleteGlobalEnv(); }); + + it('builds the esp32s3 runtime without flashing and reports the build directory', async () => { + const { handleBuildRuntimeCommand } = await import('../../../src/commands/board/build-runtime'); + setupGlobalEnvWithEsp32AndEsp32s3(); + mockedExecShell.mockImplementation(async () => {}); + + await handleBuildRuntimeCommand('esp32s3', { deviceName: 'my-s3' }); + + expect(mockedExecShell).toHaveBeenCalledTimes(1); + const [cmd] = mockedExecShell.mock.calls[0]; + expect(cmd).toContain('-B build-esp32s3 -D IDF_TARGET=esp32s3 -D SDKCONFIG=sdkconfig.esp32s3 -D DEVICE_NAME=my-s3 build'); + expect(cmd).not.toContain('flash'); + expect(mockedLogger.info).toHaveBeenCalledWith(expect.stringContaining(path.join('ports', 'esp32', 'build-esp32s3'))); + expect(mockedLogger.error).not.toHaveBeenCalled(); + }); + + it('warns when the board is not set up', async () => { + const { handleBuildRuntimeCommand } = await import('../../../src/commands/board/build-runtime'); + setupGlobalEnvWithEsp32(); + + await handleBuildRuntimeCommand('esp32s3', {}); + + expect(mockedLogger.warn).toHaveBeenCalledWith(expect.stringContaining('esp32s3 is not set up')); + expect(mockedExecShell).not.toHaveBeenCalled(); + }); +}); diff --git a/lang/src/compiler/board-toolchain/esp32-toolchain.ts b/lang/src/compiler/board-toolchain/esp32-toolchain.ts index ff5a8ab6..35ae2dcf 100644 --- a/lang/src/compiler/board-toolchain/esp32-toolchain.ts +++ b/lang/src/compiler/board-toolchain/esp32-toolchain.ts @@ -9,8 +9,17 @@ import { ElfReader } from "./tools/elf-reader"; import generateLinkerScript from "./tools/linker-script"; +export type Esp32Target = 'esp32' | 'esp32s3'; + +export const ESP32_TARGET_BUILD_DIRS: Record = { + esp32: 'build', + esp32s3: 'build-esp32s3', +}; + export type Esp32ToolchainConfig = { runtimeDir: string, + // Chip target of the runtime firmware. Defaults to 'esp32'. + target?: Esp32Target, compilerToolchain: { gcc: string, ar: string, @@ -34,7 +43,9 @@ export class Esp32Toolchain implements BoardToolchain [s.name, s])); @@ -193,13 +204,13 @@ class EspIdfComponents { include_dirs: string[] }}; - constructor(runtimeDir: string, espDir: string) { - this.sdkConfigDir = path.join(runtimeDir, 'ports/esp32/build/config'); - const dependenciesFile = path.join(runtimeDir, 'ports/esp32/build/project_description.json'); + constructor(runtimeBuildDir: string, espDir: string, target: Esp32Target) { + this.sdkConfigDir = path.join(runtimeBuildDir, 'config'); + const dependenciesFile = path.join(runtimeBuildDir, 'project_description.json'); this.dependenciesInfo = JSON.parse(fs.readFileSync(dependenciesFile).toString()).build_component_info; this.commonIncludeDirs = this.getIncludeDirs(this.commonComponents); this.commonArchiveFiles = this.getArchiveFilePaths(this.commonComponents); - this.ldFiles = this.getLdFiles(espDir); + this.ldFiles = this.getLdFiles(espDir, target); } public getIncludeDirs(rootComponentNames: string[]) { @@ -214,18 +225,24 @@ class EspIdfComponents { return includeDirs; } - private getLdFiles(espDir: string) { + private getLdFiles(espDir: string, target: Esp32Target) { // These paths are extracted from logs of `idf.py build` command. // Should be improved. + const romLdFiles: Record = { + esp32: [ + 'esp32.rom.ld', 'esp32.rom.api.ld', 'esp32.rom.libgcc.ld', + 'esp32.rom.newlib-data.ld', 'esp32.rom.syscalls.ld', 'esp32.rom.newlib-funcs.ld', + ], + esp32s3: [ + 'esp32s3.rom.ld', 'esp32s3.rom.api.ld', 'esp32s3.rom.libgcc.ld', + 'esp32s3.rom.bt_funcs.ld', 'esp32s3.rom.wdt.ld', + 'esp32s3.rom.version.ld', 'esp32s3.rom.newlib.ld', + ], + }; return [ - path.join(espDir, `esp-idf/components/esp_rom/esp32/ld/esp32.rom.ld`), - path.join(espDir, `esp-idf/components/esp_rom/esp32/ld/esp32.rom.api.ld`), - path.join(espDir, `esp-idf/components/esp_rom/esp32/ld/esp32.rom.libgcc.ld`), - path.join(espDir, `esp-idf/components/esp_rom/esp32/ld/esp32.rom.newlib-data.ld`), - path.join(espDir, `esp-idf/components/esp_rom/esp32/ld/esp32.rom.syscalls.ld`), - path.join(espDir, `esp-idf/components/esp_rom/esp32/ld/esp32.rom.newlib-funcs.ld`), - path.join(espDir, `esp-idf/components/soc/esp32/ld/esp32.peripherals.ld`), - ] + ...romLdFiles[target].map(f => path.join(espDir, `esp-idf/components/esp_rom/${target}/ld/${f}`)), + path.join(espDir, `esp-idf/components/soc/${target}/ld/${target}.peripherals.ld`), + ]; } public getArchiveFilePaths(rootComponentNames: string[]) { diff --git a/lang/src/index.ts b/lang/src/index.ts index 89a76b5d..83072b55 100644 --- a/lang/src/index.ts +++ b/lang/src/index.ts @@ -2,6 +2,6 @@ export { ErrorLog as CompileError } from './transpiler/utils'; export { CompilerSession } from './compiler/compiler-session'; export { Project } from './compiler/project'; export { Package, PackageForEsp32, PackageForHostUnix, PackageForHostWindows } from './compiler/package'; -export { Esp32Toolchain, Esp32ToolchainConfig } from './compiler/board-toolchain/esp32-toolchain'; +export { Esp32Toolchain, Esp32ToolchainConfig, Esp32Target, ESP32_TARGET_BUILD_DIRS } from './compiler/board-toolchain/esp32-toolchain'; export { HostToolchain, HostToolchainConfig, HostUnixToolchain, HostWindowsToolchain } from './compiler/board-toolchain/host-toolchain'; export { MemoryLayout, MemoryImage, CompileOutput, SharedLibrary } from './compiler/board-toolchain/board-toolchain'; \ No newline at end of file diff --git a/microcontroller/ports/esp32/.gitignore b/microcontroller/ports/esp32/.gitignore new file mode 100644 index 00000000..f8541604 --- /dev/null +++ b/microcontroller/ports/esp32/.gitignore @@ -0,0 +1 @@ +sdkconfig.esp32s3 diff --git a/microcontroller/ports/esp32/main/ble.c b/microcontroller/ports/esp32/main/ble.c index 91ece14a..a0a1545d 100644 --- a/microcontroller/ports/esp32/main/ble.c +++ b/microcontroller/ports/esp32/main/ble.c @@ -434,7 +434,10 @@ void bs_ble_init(void) } ESP_ERROR_CHECK( ret ); +#if CONFIG_IDF_TARGET_ESP32 + // Only the original ESP32 has a Classic BT controller whose memory can be released. ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT)); +#endif esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); ret = esp_bt_controller_init(&bt_cfg); diff --git a/microcontroller/ports/esp32/main/memory.c b/microcontroller/ports/esp32/main/memory.c index baf1c57a..42e1fdbc 100644 --- a/microcontroller/ports/esp32/main/memory.c +++ b/microcontroller/ports/esp32/main/memory.c @@ -36,9 +36,20 @@ static esp_partition_mmap_handle_t mapped_iflash_hdlr; static esp_partition_mmap_handle_t mapped_dflash_hdlr; static void iram_init() { - uint32_t available_size = heap_caps_get_largest_free_block(MALLOC_CAP_EXEC | MALLOC_CAP_32BIT) - 4; + uint32_t largest_block = heap_caps_get_largest_free_block(MALLOC_CAP_EXEC | MALLOC_CAP_32BIT); + if (largest_block <= 4) { + // No executable heap is available. On chips with memory protection (e.g. ESP32-S3) + // this happens when CONFIG_ESP_SYSTEM_MEMPROT_FEATURE is enabled. + BS_LOG_ERROR("No executable memory is available for IRAM. Disable CONFIG_ESP_SYSTEM_MEMPROT_FEATURE.") + abort(); + } + uint32_t available_size = largest_block - 4; iram_size = ALIGN_DOWN(MIN(DEFAULT_IRAM_SIZE, available_size), 4); iram_address = heap_caps_malloc(iram_size, MALLOC_CAP_EXEC | MALLOC_CAP_32BIT); + if (iram_address == NULL) { + BS_LOG_ERROR("Failed to allocate IRAM (%d bytes).", (int)iram_size) + abort(); + } BS_LOG_INFO("IRAM Address: %p Size: %d\n", iram_address, (int)iram_size) } @@ -46,6 +57,10 @@ static void dram_init() { uint32_t available_size = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT) - 4; dram_size = MIN(DEFAULT_DRAM_SIZE, available_size); dram_address = heap_caps_malloc(dram_size, MALLOC_CAP_8BIT); + if (dram_address == NULL) { + BS_LOG_ERROR("Failed to allocate DRAM (%d bytes).", (int)dram_size) + abort(); + } BS_LOG_INFO("DRAM Address: %p Size: %d\n", dram_address, (int)dram_size) } diff --git a/microcontroller/ports/esp32/sdkconfig.defaults b/microcontroller/ports/esp32/sdkconfig.defaults new file mode 100644 index 00000000..54b0e450 --- /dev/null +++ b/microcontroller/ports/esp32/sdkconfig.defaults @@ -0,0 +1,23 @@ +# Default settings shared by all chip targets of the ESP32 family +# (esp32, esp32s3). They are applied when a fresh sdkconfig is generated, +# e.g. `idf.py -B build-esp32s3 -DIDF_TARGET=esp32s3 -DSDKCONFIG=sdkconfig.esp32s3 build`. +# +# Bluetooth (BLE via Bluedroid) +CONFIG_BT_ENABLED=y +CONFIG_BT_BLUEDROID_ENABLED=y +CONFIG_BT_BLE_ENABLED=y +CONFIG_BT_GATTS_ENABLE=y +# ble.c uses the BLE 4.2 advertising API (esp_ble_gap_config_adv_data etc.), +# which is disabled by default on chips with BLE 5.0 support such as ESP32-S3. +CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y +# +# Partition table with the iflash/dflash partitions used for dynamically loaded code +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" +# +# BlueScript executes code that is loaded into RAM at runtime, so the heap must +# be able to hand out executable memory (MALLOC_CAP_EXEC). On ESP32-S3 (and +# other chips with PMS) memory protection is enabled by default and removes +# executable memory from the heap, which makes bs_memory_init() fail. +CONFIG_ESP_SYSTEM_MEMPROT_FEATURE=n +CONFIG_ESP_SYSTEM_MEMPROT_FEATURE_LOCK=n diff --git a/website/docs/reference/bsconfig.md b/website/docs/reference/bsconfig.md index 0c9f27c6..16218f0d 100644 --- a/website/docs/reference/bsconfig.md +++ b/website/docs/reference/bsconfig.md @@ -26,7 +26,7 @@ These fields are shared across all supported boards. | Field | Required | Default | Description | | :--- | :---: | :--- | :--- | | `projectName` | Yes | — | Project name. Also used as the main package name during compilation. | -| `boardName` | Yes | — | Target board. Supported: `esp32`, `host`. | +| `boardName` | Yes | — | Target board. Supported: `esp32`, `esp32s3`, `host`. | | `version` | No | `"1.0.0"` | Project version string. | | `vmVersion` | No | CLI version | BlueScript runtime version this project targets. | | `srcDir` | No | `"."` | Directory containing BlueScript (`.bs`) and C (`.c`) source files, relative to the project root. | @@ -70,7 +70,7 @@ Use `bscript project install` to add packages instead of editing this field by h ## ESP32 fields -When `boardName` is `"esp32"`, the following additional fields are available. These fields do not apply to `host` projects. +When `boardName` is `"esp32"` or `"esp32s3"`, the following additional fields are available. These fields do not apply to `host` projects. | Field | Required | Default | Description | | :--- | :---: | :--- | :--- | diff --git a/website/docs/reference/cli.md b/website/docs/reference/cli.md index b22c5951..99bfb52e 100644 --- a/website/docs/reference/cli.md +++ b/website/docs/reference/cli.md @@ -33,7 +33,7 @@ This command generates a new directory containing: | Option | Alias | Description | | :--- | :--- | :--- | -| `--board` | `-b` | Specify the target board (`esp32` or `host`). If omitted, an interactive selection list will appear. | +| `--board` | `-b` | Specify the target board (`esp32`, `esp32s3`, or `host`). If omitted, an interactive selection list will appear. | **Example:** ```bash @@ -153,14 +153,14 @@ bscript board setup ``` **Arguments:** -* ``: The target board identifier (`esp32` or `host`). +* ``: The target board identifier (`esp32`, `esp32s3`, or `host`). **Platform requirements:** | Board | macOS | Windows | Linux | | :--- | :--- | :--- | :--- | | `host` | `cc`, `make` | MinGW-w64: `gcc`, `mingw32-make` | `gcc`, `make` | -| `esp32` | Homebrew, Git, Python 3, `make` | Git, Python 3, `make` or `mingw32-make`. See [Windows prerequisites](../tutorial/get-started/setup-environment-windows.md). | None (Requirements are automatically installed by setup command.) | +| `esp32`, `esp32s3` | Homebrew, Git, Python 3, `make` | Git, Python 3, `make` or `mingw32-make`. See [Windows prerequisites](../tutorial/get-started/setup-environment-windows.md). | None (Requirements are automatically installed by setup command.) | For `host`, see [Try Without Microcontroller](../tutorial/guides/try-without-microcontroller.md). @@ -176,7 +176,7 @@ bscript board flash-runtime [options] ``` **Arguments:** -* ``: The target board identifier (e.g., `esp32`). +* ``: The target board identifier (`esp32` or `esp32s3`). **Options:** @@ -195,9 +195,29 @@ bscript board flash-runtime esp32 -d my-device --- +### `bscript board build-runtime ` + +Builds the BlueScript runtime for the board **without flashing it**. Useful when the board is connected to another host: copy the printed build directory there and flash it with `esptool.py --chip -p write_flash @flash_args`, or connect the board to this host and run `bscript board flash-runtime`. + +**Arguments:** + +* ``: The target board identifier (`esp32` or `esp32s3`). + +**Options:** + +| Option | Alias | Description | +| :--- | :--- | :--- | +| `--device-name ` | `-d` | BLE device name embedded in the runtime (default `BlueScript`). | + +**Example:** + +```bash +bscript board build-runtime esp32s3 -d my-device +``` + ### `bscript board list` -Lists all board architectures currently supported by the installed CLI version (`esp32` and `host`). +Lists all board architectures currently supported by the installed CLI version (`esp32`, `esp32s3`, and `host`). ```bash bscript board list @@ -216,7 +236,7 @@ bscript board remove [options] By default, this command asks for confirmation before deleting files. **Arguments:** -* ``: The target board identifier (`esp32` or `host`). +* ``: The target board identifier (`esp32`, `esp32s3`, or `host`). **Options:** @@ -279,5 +299,5 @@ bscript repl -b esp32 -d my-device | Option | Alias | Description | | :--- | :--- | :--- | -| `--board` | `-b` | Specify the target board (`esp32` or `host`). | +| `--board` | `-b` | Specify the target board (`esp32`, `esp32s3`, or `host`). | | `--device-name` | `-d` | Bluetooth device name to connect to (default: `"BLUESCRIPT"`). **ESP32 only** — must match the name set during `bscript board flash-runtime`. Ignored for `host`. | diff --git a/website/docs/tutorial/get-started/introduction.md b/website/docs/tutorial/get-started/introduction.md index 01767527..208cbc5c 100644 --- a/website/docs/tutorial/get-started/introduction.md +++ b/website/docs/tutorial/get-started/introduction.md @@ -57,6 +57,7 @@ The architecture of BlueScript is based on the research paper *["BlueScript: A D BlueScript targets microcontroller development first. The primary supported platform is: - **Espressif ESP32** — real hardware development over Bluetooth +- **Espressif ESP32-S3** — same workflow as ESP32; use the board name `esp32s3` You can also run BlueScript on the **host runtime** without a microcontroller. This is useful for language checks and quick experiments. See [Try Without Microcontroller](../guides/try-without-microcontroller.md). diff --git a/website/docs/tutorial/get-started/setup-environment.md b/website/docs/tutorial/get-started/setup-environment.md index fa7fe81b..afead8f7 100644 --- a/website/docs/tutorial/get-started/setup-environment.md +++ b/website/docs/tutorial/get-started/setup-environment.md @@ -94,6 +94,10 @@ Download the necessary environment files for the ESP32 platform: bscript board setup esp32 ``` +:::tip ESP32-S3 +If you use an ESP32-S3 board, replace `esp32` with `esp32s3` in this and the following commands (`bscript board setup esp32s3`, `bscript board flash-runtime esp32s3`, `bscript project create my-app -b esp32s3`). Both boards share one ESP-IDF installation, so setting up the second one is fast. +::: + ### 2. Flash the Runtime Connect your ESP32 to your computer via USB and flash the runtime: