diff --git a/package.json b/package.json index 1f35bcf2..3a15b895 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "activationEvents": [ "workspaceContains:./pico_sdk_import.cmake", "workspaceContains:./.pico-rs", + "workspaceContains:./prj.conf", "onWebviewPanel:newPicoProject", "onWebviewPanel:newPicoMicroPythonProject" ], @@ -80,13 +81,13 @@ "command": "raspberry-pi-pico.switchSDK", "title": "Switch Pico SDK", "category": "Raspberry Pi Pico", - "enablement": "raspberry-pi-pico.isPicoProject && !raspberry-pi-pico.isRustProject" + "enablement": "raspberry-pi-pico.isPicoProject && !raspberry-pi-pico.isRustProject && !raspberry-pi-pico.isZephyrProject" }, { "command": "raspberry-pi-pico.switchBoard", "title": "Switch Board", "category": "Raspberry Pi Pico", - "enablement": "raspberry-pi-pico.isPicoProject && !raspberry-pi-pico.isRustProject" + "enablement": "raspberry-pi-pico.isPicoProject" }, { "command": "raspberry-pi-pico.launchTargetPath", @@ -159,13 +160,25 @@ "title": "Get OpenOCD root", "category": "Raspberry Pi Pico", "enablement": "false" - }, + }, { "command": "raspberry-pi-pico.getSVDPath", "title": "Get SVD Path (rust only)", "category": "Raspberry Pi Pico", "enablement": "false" }, + { + "command": "raspberry-pi-pico.getWestPath", + "title": "Get West path", + "category": "Raspberry Pi Pico", + "enablement": "false" + }, + { + "command": "raspberry-pi-pico.getZephyrWorkspacePath", + "title": "Get Zephyr workspace path", + "category": "Raspberry Pi Pico", + "enablement": "false" + }, { "command": "raspberry-pi-pico.compileProject", "title": "Compile Pico Project", @@ -210,7 +223,7 @@ "command": "raspberry-pi-pico.switchBuildType", "title": "Switch Build Type", "category": "Raspberry Pi Pico", - "enablement": "raspberry-pi-pico.isPicoProject && !raspberry-pi-pico.isRustProject" + "enablement": "raspberry-pi-pico.isPicoProject && !raspberry-pi-pico.isRustProject && !raspberry-pi-pico.isZephyrProject" }, { "command": "raspberry-pi-pico.importProject", @@ -244,12 +257,6 @@ "category": "Raspberry Pi Pico", "enablement": "raspberry-pi-pico.isPicoProject && !raspberry-pi-pico.isRustProject" }, - { - "command": "raspberry-pi-pico.getRTTDecoderPath", - "title": "Get RTT Decoder module path", - "category": "Raspberry Pi Pico", - "enablement": "false" - }, { "command": "raspberry-pi-pico.sbomTargetPathDebug", "title": "Get path of the project debug SBOM (rust only)", diff --git a/scripts/pico_project.py b/scripts/pico_project.py index a113dd3d..e0f2432c 100644 --- a/scripts/pico_project.py +++ b/scripts/pico_project.py @@ -1159,6 +1159,7 @@ def generateProjectFiles( ] } + # TODO: use get picotool path command! tasks = f"""{{ "version": "2.0.0", "tasks": [ diff --git a/src/commands/configureCmake.mts b/src/commands/configureCmake.mts index e1b43dfc..9614cd0f 100644 --- a/src/commands/configureCmake.mts +++ b/src/commands/configureCmake.mts @@ -47,10 +47,10 @@ export default class ConfigureCmakeCommand extends Command { } else { void window.showWarningMessage( "CMake failed to configure your build. " + - "See the developer console for details " + - "(Help -> Toggle Developer Tools). " + - "You can also use the CMake Tools Extension Integration " + - "to get more information about the error." + "See the developer console for details " + + "(Help -> Toggle Developer Tools). " + + "You can also use the CMake Tools Extension Integration " + + "to get more information about the error." ); } } @@ -110,15 +110,15 @@ export class CleanCMakeCommand extends Command { } else { void window.showWarningMessage( "CMake could not be reconfigured. " + - "See the developer console for details " + - "(Help -> Toggle Developer Tools). " + - "You can also use the CMake Tools Extension Integration " + - "to get more information about the error." + "See the developer console for details " + + "(Help -> Toggle Developer Tools). " + + "You can also use the CMake Tools Extension Integration " + + "to get more information about the error." ); } const ws = workspaceFolder.uri.fsPath; - const cMakeCachePath = join(ws, "build","CMakeCache.txt"); + const cMakeCachePath = join(ws, "build", "CMakeCache.txt"); const newBuildType = cmakeGetPicoVar(cMakeCachePath, "CMAKE_BUILD_TYPE"); this._ui.updateBuildType(newBuildType ?? "unknown"); } @@ -159,7 +159,7 @@ export class SwitchBuildTypeCommand extends Command { } const ws = workspaceFolder.uri.fsPath; - const cMakeCachePath = join(ws, "build","CMakeCache.txt"); + const cMakeCachePath = join(ws, "build", "CMakeCache.txt"); const oldBuildType = cmakeGetPicoVar(cMakeCachePath, "CMAKE_BUILD_TYPE"); // QuickPick for the build type @@ -173,10 +173,10 @@ export class SwitchBuildTypeCommand extends Command { } else { void window.showWarningMessage( "CMake failed to configure your build. " + - "See the developer console for details " + - "(Help -> Toggle Developer Tools). " + - "You can also use the CMake Tools Extension Integration " + - "to get more information about the error." + "See the developer console for details " + + "(Help -> Toggle Developer Tools). " + + "You can also use the CMake Tools Extension Integration " + + "to get more information about the error." ); } diff --git a/src/commands/getPaths.mts b/src/commands/getPaths.mts index c41cd91b..acedf35f 100644 --- a/src/commands/getPaths.mts +++ b/src/commands/getPaths.mts @@ -13,6 +13,8 @@ import { buildPicotoolPath, buildSDKPath, buildToolchainPath, + buildWestPath, + buildZephyrWorkspacePath, downloadAndInstallOpenOCD, downloadAndInstallPicotool, } from "../utils/download.mjs"; @@ -26,10 +28,19 @@ import { getSupportedToolchains } from "../utils/toolchainUtil.mjs"; import Logger from "../logger.mjs"; import { rustProjectGetSelectedChip } from "../utils/rustUtil.mjs"; import { OPENOCD_VERSION } from "../utils/sharedConstants.mjs"; +import { + getBoardFromZephyrProject, + ZEPHYR_PICO, + ZEPHYR_PICO2, + ZEPHYR_PICO2_W, + ZEPHYR_PICO_W, +} from "./switchBoard.mjs"; export class GetPythonPathCommand extends CommandWithResult { + public static readonly id = "getPythonPath"; + constructor() { - super("getPythonPath"); + super(GetPythonPathCommand.id); } async execute(): Promise { @@ -47,8 +58,10 @@ export class GetPythonPathCommand extends CommandWithResult { } export class GetEnvPathCommand extends CommandWithResult { + public static readonly id = "getEnvPath"; + constructor() { - super("getEnvPath"); + super(GetEnvPathCommand.id); } async execute(): Promise { @@ -66,8 +79,10 @@ export class GetEnvPathCommand extends CommandWithResult { } export class GetGDBPathCommand extends CommandWithResult { + public static readonly id = "getGDBPath"; + constructor(private readonly _extensionUri: Uri) { - super("getGDBPath"); + super(GetGDBPathCommand.id); } async execute(): Promise { @@ -157,8 +172,10 @@ export class GetGDBPathCommand extends CommandWithResult { } export class GetCompilerPathCommand extends CommandWithResult { + public static readonly id = "getCompilerPath"; + constructor() { - super("getCompilerPath"); + super(GetCompilerPathCommand.id); } async execute(): Promise { @@ -196,8 +213,10 @@ export class GetCompilerPathCommand extends CommandWithResult { } export class GetCxxCompilerPathCommand extends CommandWithResult { + public static readonly id = "getCxxCompilerPath"; + constructor() { - super("getCxxCompilerPath"); + super(GetCxxCompilerPathCommand.id); } async execute(): Promise { @@ -253,6 +272,37 @@ export class GetChipCommand extends CommandWithResult { const workspaceFolder = workspace.workspaceFolders?.[0]; const isRustProject = State.getInstance().isRustProject; + const isZephyrProject = State.getInstance().isZephyrProject; + + if (isZephyrProject) { + const board = await getBoardFromZephyrProject( + join(workspaceFolder.uri.fsPath, ".vscode", "tasks.json") + ); + + if (board === undefined) { + this._logger.error("Failed to read Zephyr board from tasks.json"); + + return ""; + } + + switch (board) { + case ZEPHYR_PICO: + case ZEPHYR_PICO_W: + return "rp2040"; + case ZEPHYR_PICO2: + case ZEPHYR_PICO2_W: + return "rp2350"; + default: + this._logger.error(`Unsupported Zephyr board: ${board}`); + void window.showErrorMessage( + `Unsupported Zephyr board: ${board}. ` + + `Supported boards are: ${ZEPHYR_PICO}, ${ZEPHYR_PICO_W}, ` + + `${ZEPHYR_PICO2}, ${ZEPHYR_PICO2_W}` + ); + + return "rp2040"; + } + } if (isRustProject) { // read .pico-rs @@ -307,8 +357,10 @@ export class GetChipCommand extends CommandWithResult { } export class GetChipUppercaseCommand extends CommandWithResult { + public static readonly id = "getChipUppercase"; + constructor() { - super("getChipUppercase"); + super(GetChipUppercaseCommand.id); } async execute(): Promise { @@ -320,8 +372,10 @@ export class GetChipUppercaseCommand extends CommandWithResult { } export class GetTargetCommand extends CommandWithResult { + public static readonly id = "getTarget"; + constructor() { - super("getTarget"); + super(GetTargetCommand.id); } async execute(): Promise { @@ -334,7 +388,28 @@ export class GetTargetCommand extends CommandWithResult { const workspaceFolder = workspace.workspaceFolders?.[0]; const isRustProject = State.getInstance().isRustProject; + const isZephyrProject = State.getInstance().isZephyrProject; + if (isZephyrProject) { + const board = await getBoardFromZephyrProject( + join(workspaceFolder.uri.fsPath, ".vscode", "tasks.json") + ); + + if (board === undefined) { + return "rp2040"; + } + + switch (board) { + case ZEPHYR_PICO: + case ZEPHYR_PICO_W: + return "rp2040"; + case ZEPHYR_PICO2: + case ZEPHYR_PICO2_W: + return "rp2350"; + default: + return "rp2040"; + } + } if (isRustProject) { const chip = rustProjectGetSelectedChip(workspaceFolder.uri.fsPath); @@ -505,3 +580,41 @@ export class GetSVDPathCommand extends CommandWithResult { ); } } + +export class GetWestPathCommand extends CommandWithResult { + public static readonly id = "getWestPath"; + + constructor() { + super(GetWestPathCommand.id); + } + + execute(): string | undefined { + const result = buildWestPath(); + + if (result === null || !result) { + return undefined; + } + + return result; + } +} + +export class GetZephyrWorkspacePathCommand extends CommandWithResult< + string | undefined +> { + public static readonly id = "getZephyrWorkspacePath"; + + constructor() { + super(GetZephyrWorkspacePathCommand.id); + } + + execute(): string | undefined { + const result = buildZephyrWorkspacePath(); + + if (result === null || !result) { + return undefined; + } + + return result; + } +} diff --git a/src/commands/newProject.mts b/src/commands/newProject.mts index 8e9f4cdc..4d29b833 100644 --- a/src/commands/newProject.mts +++ b/src/commands/newProject.mts @@ -5,6 +5,7 @@ import { NewProjectPanel } from "../webview/newProjectPanel.mjs"; // eslint-disable-next-line max-len import { NewMicroPythonProjectPanel } from "../webview/newMicroPythonProjectPanel.mjs"; import { NewRustProjectPanel } from "../webview/newRustProjectPanel.mjs"; +import { NewZephyrProjectPanel } from "../webview/newZephyrProjectPanel.mjs"; /** * Enum for the language of the project. @@ -15,6 +16,7 @@ export enum ProjectLang { cCpp = 1, micropython = 2, rust = 3, + zephyr = 5, } export default class NewProjectCommand extends CommandWithArgs { @@ -23,6 +25,7 @@ export default class NewProjectCommand extends CommandWithArgs { private static readonly micropythonOption = "MicroPython"; private static readonly cCppOption = "C/C++"; private static readonly rustOption = "Rust (experimental)"; + private static readonly zephyrOption = "Zephyr"; public static readonly id = "newProject"; @@ -39,6 +42,8 @@ export default class NewProjectCommand extends CommandWithArgs { ? NewProjectCommand.micropythonOption : preSelectedType === ProjectLang.rust ? NewProjectCommand.rustOption + : preSelectedType === ProjectLang.zephyr + ? NewProjectCommand.zephyrOption : undefined; } @@ -51,6 +56,7 @@ export default class NewProjectCommand extends CommandWithArgs { NewProjectCommand.cCppOption, NewProjectCommand.micropythonOption, NewProjectCommand.rustOption, + NewProjectCommand.zephyrOption, ], { placeHolder: "Select which language to use for your new project", @@ -70,6 +76,9 @@ export default class NewProjectCommand extends CommandWithArgs { } else if (lang === NewProjectCommand.rustOption) { // create a new project with Rust NewRustProjectPanel.createOrShow(this._extensionUri); + } else if (lang === NewProjectCommand.zephyrOption) { + // create a new project with MicroPython + NewZephyrProjectPanel.createOrShow(this._extensionUri); } else { // show webview where the process of creating a new project is continued NewProjectPanel.createOrShow(this._extensionUri); diff --git a/src/commands/switchBoard.mts b/src/commands/switchBoard.mts index f979c1e0..2e21c599 100644 --- a/src/commands/switchBoard.mts +++ b/src/commands/switchBoard.mts @@ -3,9 +3,10 @@ import Logger from "../logger.mjs"; import { commands, ProgressLocation, + Uri, window, workspace, - type Uri, + type WorkspaceFolder, } from "vscode"; import { existsSync, readdirSync, readFileSync, writeFileSync } from "fs"; import { @@ -29,6 +30,131 @@ import VersionBundlesLoader from "../utils/versionBundles.mjs"; import State from "../state.mjs"; import { unknownErrorToString } from "../utils/errorHelper.mjs"; +interface IBoardFile { + [key: string]: string; +} + +interface ITask { + label: string; + args: string[]; +} + +const PICO_BOARD = "pico"; +const PICO_W_BOARD = "pico_w"; +const PICO2_BOARD = "pico2"; +const PICO2_W_BOARD = "pico2_w"; +export const ZEPHYR_PICO = "rpi_pico"; +export const ZEPHYR_PICO_W = "rpi_pico/rp2040/w"; +export const ZEPHYR_PICO2 = "rpi_pico2/rp2350a/m33"; +export const ZEPHYR_PICO2_W = "rpi_pico2/rp2350a/m33/w"; +const VALID_ZEPHYR_BOARDS = [ + ZEPHYR_PICO, + ZEPHYR_PICO_W, + ZEPHYR_PICO2, + ZEPHYR_PICO2_W, +]; + +function stringToZephyrBoard(e: string): string { + if (e === PICO_BOARD) { + return "rpi_pico"; + } else if (e === PICO_W_BOARD) { + return "rpi_pico/rp2040/w"; + } else if (e === PICO2_BOARD) { + return "rpi_pico2/rp2350a/m33"; + } else if (e === PICO2_W_BOARD) { + return "rpi_pico2/rp2350a/m33/w"; + } else { + throw new Error(`Unknown Board Type: ${e}`); + } +} + +/** + * Reads and parses the tasks.json file at the given path. + * If a overwriteBoard is provided, it will replace the board in the tasks.json file. + * + * @param tasksJsonPath The path to the tasks.json file. + * @param overwriteBoard The board to overwrite in the tasks.json file. + * @returns The current board if found, otherwise undefined. + */ +async function touchTasksJson( + tasksJsonPath: string, + overwriteBoard?: string +): Promise { + const tasksUri = Uri.file(tasksJsonPath); + + try { + await workspace.fs.stat(tasksUri); + + const td = new TextDecoder("utf-8"); + const tasksJson = JSON.parse( + td.decode(await workspace.fs.readFile(tasksUri)) + ) as { tasks: ITask[] }; + + const compileTask = tasksJson.tasks.find( + t => t.label === "Compile Project" + ); + if (compileTask === undefined) { + return undefined; + } + + // find index of -b in the args and then thing after it should match on of the board strings + const bIndex = compileTask.args.findIndex(a => a === "-b"); + if (bIndex === -1 || bIndex === compileTask.args.length - 1) { + return undefined; + } + + let currentBoard = compileTask.args[bIndex + 1]; + + if (overwriteBoard !== undefined) { + if (!VALID_ZEPHYR_BOARDS.includes(currentBoard)) { + const cont = await window.showWarningMessage( + `Current board "${currentBoard}" is not a known ` + + "Zephyr board. Do you want to continue?", + { + modal: true, + }, + "Continue", + "Cancel" + ); + + if (cont !== "Continue") { + return; + } + } + + compileTask.args[bIndex + 1] = overwriteBoard; + currentBoard = overwriteBoard; + const te = new TextEncoder(); + await workspace.fs.writeFile( + tasksUri, + te.encode(JSON.stringify(tasksJson, null, 2)) + ); + } + + if (VALID_ZEPHYR_BOARDS.includes(currentBoard)) { + return currentBoard; + } else { + return undefined; + } + } catch (error) { + Logger.log( + `Failed to read tasks.json file: ${unknownErrorToString(error)}` + ); + void window.showErrorMessage( + "Failed to read tasks.json file. " + + "Make sure the file exists and has a Compile Project task." + ); + + return undefined; + } +} + +export async function getBoardFromZephyrProject( + tasksJsonPath: string +): Promise { + return touchTasksJson(tasksJsonPath); +} + export default class SwitchBoardCommand extends Command { private _logger: Logger = new Logger("SwitchBoardCommand"); private _versionBundlesLoader: VersionBundlesLoader; @@ -41,79 +167,79 @@ export default class SwitchBoardCommand extends Command { } public static async askBoard( - sdkVersion: string + sdkVersion: string, + isZephyrProject = false ): Promise<[string, boolean] | undefined> { - const quickPickItems: string[] = ["pico", "pico_w"]; + const quickPickItems: string[] = [PICO_BOARD, PICO_W_BOARD]; const workspaceFolder = workspace.workspaceFolders?.[0]; + if (workspaceFolder === undefined) { + return; + } + if (!compareLt(sdkVersion, "2.0.0")) { - quickPickItems.push("pico2"); + quickPickItems.push(PICO2_BOARD); } if (!compareLt(sdkVersion, "2.1.0")) { - quickPickItems.push("pico2_w"); + quickPickItems.push(PICO2_W_BOARD); } - const sdkPath = buildSDKPath(sdkVersion); - const boardHeaderDirList = []; + const boardFiles: IBoardFile = {}; - if(workspaceFolder !== undefined) { + if (!isZephyrProject) { + const sdkPath = buildSDKPath(sdkVersion); + const boardHeaderDirList = []; const ws = workspaceFolder.uri.fsPath; - const cMakeCachePath = join(ws, "build","CMakeCache.txt"); + const cMakeCachePath = join(ws, "build", "CMakeCache.txt"); let picoBoardHeaderDirs = cmakeGetPicoVar( cMakeCachePath, - "PICO_BOARD_HEADER_DIRS"); + "PICO_BOARD_HEADER_DIRS" + ); - if(picoBoardHeaderDirs){ - if(picoBoardHeaderDirs.startsWith("'")){ - const substrLen = picoBoardHeaderDirs.length-1; - picoBoardHeaderDirs = picoBoardHeaderDirs.substring(1,substrLen); + if (picoBoardHeaderDirs) { + if (picoBoardHeaderDirs.startsWith("'")) { + const substrLen = picoBoardHeaderDirs.length - 1; + picoBoardHeaderDirs = picoBoardHeaderDirs.substring(1, substrLen); } const picoBoardHeaderDirList = picoBoardHeaderDirs.split(";"); - picoBoardHeaderDirList.forEach( - item => { - let boardPath = resolve(item); - const normalized = normalize(item); - - //If path is not absolute, join workspace path - if(boardPath !== normalized){ - boardPath = join(ws,normalized); - } + picoBoardHeaderDirList.forEach(item => { + let boardPath = resolve(item); + const normalized = normalize(item); - if(existsSync(boardPath)){ - boardHeaderDirList.push(boardPath); - } + //If path is not absolute, join workspace path + if (boardPath !== normalized) { + boardPath = join(ws, normalized); } - ); + + if (existsSync(boardPath)) { + boardHeaderDirList.push(boardPath); + } + }); } - } - const systemBoardHeaderDir = - join(sdkPath,"src", "boards", "include","boards"); + const systemBoardHeaderDir = join( + sdkPath, + "src", + "boards", + "include", + "boards" + ); boardHeaderDirList.push(systemBoardHeaderDir); - interface IBoardFile{ - [key: string]: string; - }; - - const boardFiles:IBoardFile = {}; - - boardHeaderDirList.forEach( - path =>{ - readdirSync(path).forEach( - file => { - const fullFilename = join(path, file); - if(fullFilename.endsWith(".h")) { - const boardName = file.slice(0, -2); // remove .h - boardFiles[boardName] = fullFilename; - quickPickItems.push(boardName); - } + boardHeaderDirList.forEach(path => { + readdirSync(path).forEach(file => { + const fullFilename = join(path, file); + if (fullFilename.endsWith(".h")) { + const boardName = file.slice(0, -2); // remove .h + boardFiles[boardName] = fullFilename; + quickPickItems.push(boardName); } - ) - } - ); + }); + }); + } // show quick pick for board type const board = await window.showQuickPick(quickPickItems, { @@ -121,22 +247,24 @@ export default class SwitchBoardCommand extends Command { }); if (board === undefined) { - return board; + return; + } else if (isZephyrProject) { + return [board, false]; } // Check that board doesn't have an RP2040 on it - const data = readFileSync(boardFiles[board]) + const data = readFileSync(boardFiles[board]); if (data.includes("rp2040")) { return [board, false]; } const useRiscV = await window.showQuickPick(["No", "Yes"], { - placeHolder: "Use Risc-V?", + placeHolder: "Use RISC-V?", }); if (useRiscV === undefined) { - return undefined; + return; } return [board, useRiscV === "Yes"]; @@ -145,6 +273,7 @@ export default class SwitchBoardCommand extends Command { async execute(): Promise { const workspaceFolder = workspace.workspaceFolders?.[0]; const isRustProject = State.getInstance().isRustProject; + const isZephyrProject = State.getInstance().isZephyrProject; // check it has a CMakeLists.txt if ( @@ -204,6 +333,40 @@ export default class SwitchBoardCommand extends Command { return; } + if (isZephyrProject) { + await this._switchBoardZephyr(workspaceFolder); + } else { + await this._switchBoardPicoSDK(workspaceFolder); + } + } + + private async _switchBoardZephyr(wsf: WorkspaceFolder): Promise { + const latestSdkVersion = await this._versionBundlesLoader.getLatestSDK(); + if (latestSdkVersion === undefined) { + void window.showErrorMessage( + "Failed to get latest SDK version - cannot update board" + ); + + return; + } + const boardRes = await SwitchBoardCommand.askBoard(latestSdkVersion, true); + + if (boardRes === undefined) { + this._logger.info("User cancelled board type selection."); + + return; + } + + const board = stringToZephyrBoard(boardRes[0]); + const taskJsonFile = join(wsf.uri.fsPath, ".vscode", "tasks.json"); + await touchTasksJson(taskJsonFile, board); + + // TODO: maybe reload cmake + } + + private async _switchBoardPicoSDK( + workspaceFolder: WorkspaceFolder + ): Promise { const versions = await cmakeGetSelectedToolchainAndSDKVersions( workspaceFolder.uri ); diff --git a/src/contextKeys.mts b/src/contextKeys.mts index 4b515be4..4722bcff 100644 --- a/src/contextKeys.mts +++ b/src/contextKeys.mts @@ -1,6 +1,11 @@ import { extensionName } from "./commands/command.mjs"; export enum ContextKeys { + // General key to check if the current project is a pico project + // that is supported by the extension (C/C++, Rust or Zpephyr) isPicoProject = `${extensionName}.isPicoProject`, + // Key to check if the current project is a rust pico project isRustProject = `${extensionName}.isRustProject`, + // Key to check if the current project is a zephyr pico project + isZephyrProject = `${extensionName}.isZephyrProject`, } diff --git a/src/extension.mts b/src/extension.mts index 4b2b2a1f..8063d185 100644 --- a/src/extension.mts +++ b/src/extension.mts @@ -5,6 +5,8 @@ import { type WebviewPanel, commands, ProgressLocation, + Uri, + FileSystemError, } from "vscode"; import { extensionName, @@ -50,6 +52,8 @@ import { GetPicotoolPathCommand, GetOpenOCDRootCommand, GetSVDPathCommand, + GetWestPathCommand, + GetZephyrWorkspacePathCommand, } from "./commands/getPaths.mjs"; import { downloadAndInstallCmake, @@ -81,7 +85,13 @@ import ConfigureCmakeCommand, { import ImportProjectCommand from "./commands/importProject.mjs"; import { homedir } from "os"; import NewExampleProjectCommand from "./commands/newExampleProject.mjs"; -import SwitchBoardCommand from "./commands/switchBoard.mjs"; +import SwitchBoardCommand, { + getBoardFromZephyrProject, + ZEPHYR_PICO, + ZEPHYR_PICO2, + ZEPHYR_PICO2_W, + ZEPHYR_PICO_W, +} from "./commands/switchBoard.mjs"; import UninstallPicoSDKCommand from "./commands/uninstallPicoSDK.mjs"; import UpdateOpenOCDCommand from "./commands/updateOpenOCD.mjs"; import FlashProjectSWDCommand from "./commands/flashProjectSwd.mjs"; @@ -96,8 +106,13 @@ import { import State from "./state.mjs"; import { cmakeToolsForcePicoKit } from "./utils/cmakeToolsUtil.mjs"; import { NewRustProjectPanel } from "./webview/newRustProjectPanel.mjs"; -import { OPENOCD_VERSION } from "./utils/sharedConstants.mjs"; +import { + CMAKELISTS_ZEPHYR_HEADER, + OPENOCD_VERSION, +} from "./utils/sharedConstants.mjs"; import VersionBundlesLoader from "./utils/versionBundles.mjs"; +import { unknownErrorToString } from "./utils/errorHelper.mjs"; +import { setupZephyr } from "./utils/setupZephyr.mjs"; export async function activate(context: ExtensionContext): Promise { Logger.info(LoggerSource.extension, "Extension activation triggered"); @@ -136,6 +151,8 @@ export async function activate(context: ExtensionContext): Promise { new GetPicotoolPathCommand(), new GetOpenOCDRootCommand(), new GetSVDPathCommand(context.extensionUri), + new GetWestPathCommand(), + new GetZephyrWorkspacePathCommand(), new CompileProjectCommand(), new RunProjectCommand(), new FlashProjectSWDCommand(), @@ -251,12 +268,132 @@ export async function activate(context: ExtensionContext): Promise { return; } + // Set Pico Zephyr Project false by default + await commands.executeCommand( + "setContext", + ContextKeys.isZephyrProject, + false + ); + + const cmakeListsContents = new TextDecoder().decode( + await workspace.fs.readFile(Uri.file(cmakeListsFilePath)) + ); + + // Check for pico_zephyr in CMakeLists.txt + if (cmakeListsContents.startsWith(CMAKELISTS_ZEPHYR_HEADER)) { + Logger.info(LoggerSource.extension, "Project is of type: Zephyr"); + + const vb = new VersionBundlesLoader(context.extensionUri); + const latest = await vb.getLatest(); + if (latest === undefined) { + Logger.error( + LoggerSource.extension, + "Failed to get latest version bundle for Zephyr project." + ); + + void window.showErrorMessage( + "Failed to get latest version bundle for Zephyr project." + ); + + return; + } + + // TODO: read selected ninja and cmake versions from project + const result = await setupZephyr({ + extUri: context.extensionUri, + cmakeMode: 4, + cmakePath: "", + cmakeVersion: latest[1].cmake, + ninjaMode: 4, + ninjaPath: "", + ninjaVersion: latest[1].ninja, + }); + if (result === undefined) { + void window.showErrorMessage( + "Failed to setup Zephyr Toolchain. See logs for details." + ); + + return; + } + void window.showInformationMessage( + "Zephyr Toolchain setup done. You can now build your project." + ); + + await commands.executeCommand( + "setContext", + ContextKeys.isPicoProject, + true + ); + await commands.executeCommand( + "setContext", + ContextKeys.isZephyrProject, + true + ); + State.getInstance().isZephyrProject = true; + + ui.showStatusBarItems(false, true); + + // Update the board info if it can be found in tasks.json + const tasksJsonFilePath = join( + workspaceFolder.uri.fsPath, + ".vscode", + "tasks.json" + ); + + // Update UI with board description + const board = await getBoardFromZephyrProject(tasksJsonFilePath); + + if (board !== undefined) { + if (board === ZEPHYR_PICO2_W) { + ui.updateBoard("Pico 2W"); + } else if (board === ZEPHYR_PICO2) { + ui.updateBoard("Pico 2"); + } else if (board === ZEPHYR_PICO_W) { + ui.updateBoard("Pico W"); + } else if (board === ZEPHYR_PICO) { + ui.updateBoard("Pico"); + } else { + ui.updateBoard("Other"); + } + } + + // check if build dir is empty and recommend to run a build to + // get the intellisense working + // TODO: maybe run cmake configure automatically if build folder empty + try { + const buildDirContents = await workspace.fs.readDirectory( + Uri.file(join(workspaceFolder.uri.fsPath, "build")) + ); + + if (buildDirContents.length === 0) { + void window.showWarningMessage( + "To get full intellisense support please build the project once." + ); + } + } catch (error) { + if (error instanceof FileSystemError && error.code === "FileNotFound") { + void window.showWarningMessage( + "To get full intellisense support please build the project once." + ); + + Logger.debug( + LoggerSource.extension, + 'No "build" folder found. Intellisense might not work ' + + "properly until a build has been done." + ); + } else { + Logger.error( + LoggerSource.extension, + "Error when reading build folder:", + unknownErrorToString(error) + ); + } + } + + return; + } // check for pico_sdk_init() in CMakeLists.txt - if ( - !readFileSync(cmakeListsFilePath) - .toString("utf-8") - .includes("pico_sdk_init()") - ) { + else if (!cmakeListsContents.includes("pico_sdk_init()")) { Logger.warn( LoggerSource.extension, "No pico_sdk_init() in CMakeLists.txt found." @@ -817,108 +954,6 @@ export async function activate(context: ExtensionContext): Promise { return; } - /* - const pythonPath = settings.getString(SettingsKey.python3Path); - if (pythonPath && pythonPath.includes("/.pico-sdk/python")) { - // check if python path exists - if (!existsSync(pythonPath.replace(HOME_VAR, homedir()))) { - Logger.warn( - LoggerSource.extension, - "Python path in settings does not exist.", - "Installing Python3 to default path." - ); - const pythonVersion = /\/\.pico-sdk\/python\/([.0-9]+)\//.exec( - pythonPath - )?.[1]; - if (pythonVersion === undefined) { - Logger.error( - LoggerSource.extension, - "Failed to get Python version from path." - ); - await commands.executeCommand( - "setContext", - ContextKeys.isPicoProject, - false - ); - - return; - } - - let result: string | undefined; - await window.withProgress( - { - location: ProgressLocation.Notification, - title: - "Downloading and installing Python. This may take a long while...", - cancellable: false, - }, - async progress => { - if (process.platform === "win32") { - const versionBundle = await new VersionBundlesLoader( - context.extensionUri - ).getPythonWindowsAmd64Url(pythonVersion); - - if (versionBundle === undefined) { - Logger.error( - LoggerSource.extension, - "Failed to get Python download url from version bundle." - ); - await commands.executeCommand( - "setContext", - ContextKeys.isPicoProject, - false - ); - - return; - } - - // ! because data.pythonMode === 0 => versionBundle !== undefined - result = await downloadEmbedPython(versionBundle); - } else if (process.platform === "darwin") { - const result1 = await setupPyenv(); - if (!result1) { - progress.report({ - increment: 100, - }); - - return; - } - const result2 = await pyenvInstallPython(pythonVersion); - - if (result2 !== null) { - result = result2; - } - } else { - Logger.info( - LoggerSource.extension, - "Automatic Python installation is only", - "supported on Windows and macOS." - ); - - await window.showErrorMessage( - "Automatic Python installation is only " + - "supported on Windows and macOS." - ); - } - progress.report({ - increment: 100, - }); - } - ); - - if (result === undefined) { - Logger.error(LoggerSource.extension, "Failed to install Python3."); - await commands.executeCommand( - "setContext", - ContextKeys.isPicoProject, - false - ); - - return; - } - } - }*/ - ui.showStatusBarItems(); ui.updateSDKVersion(selectedToolchainAndSDKVersions[0]); diff --git a/src/logger.mts b/src/logger.mts index c88860f2..83796e16 100644 --- a/src/logger.mts +++ b/src/logger.mts @@ -44,6 +44,8 @@ export enum LoggerSource { vscodeConfigUtil = "vscodeConfigUtil", rustUtil = "rustUtil", projectRust = "projectRust", + zephyrSetup = "setupZephyr", + projectZephyr = "projectZephyr", } /** diff --git a/src/state.mts b/src/state.mts index 054c97ce..c555de4c 100644 --- a/src/state.mts +++ b/src/state.mts @@ -1,6 +1,7 @@ export default class State { private static instance?: State; public isRustProject = false; + public isZephyrProject = false; public constructor() {} diff --git a/src/ui.mts b/src/ui.mts index 921d67be..34aa2f50 100644 --- a/src/ui.mts +++ b/src/ui.mts @@ -2,6 +2,11 @@ import { window, type StatusBarItem, StatusBarAlignment } from "vscode"; import Logger from "./logger.mjs"; import type { PicoProjectActivityBar } from "./webview/activityBar.mjs"; import State from "./state.mjs"; +import { extensionName } from "./commands/command.mjs"; +import CompileProjectCommand from "./commands/compileProject.mjs"; +import RunProjectCommand from "./commands/runProject.mjs"; +import SwitchSDKCommand from "./commands/switchSDK.mjs"; +import SwitchBoardCommand from "./commands/switchBoard.mjs"; enum StatusBarItemKey { compile = "raspberry-pi-pico.compileProject", @@ -17,34 +22,40 @@ const STATUS_BAR_ITEMS: { command: string; tooltip: string; rustSupport: boolean; + zephyrSupport: boolean; }; } = { [StatusBarItemKey.compile]: { // alt. "$(gear) Compile" text: "$(file-binary) Compile", - command: "raspberry-pi-pico.compileProject", + command: `${extensionName}.${CompileProjectCommand.id}`, tooltip: "Compile Project", rustSupport: true, + zephyrSupport: true, }, [StatusBarItemKey.run]: { // alt. "$(gear) Compile" text: "$(run) Run", - command: "raspberry-pi-pico.runProject", + command: `${extensionName}.${RunProjectCommand.id}`, tooltip: "Run Project", rustSupport: true, + zephyrSupport: true, }, [StatusBarItemKey.picoSDKQuickPick]: { text: "Pico SDK: ", - command: "raspberry-pi-pico.switchSDK", + command: `${extensionName}.${SwitchSDKCommand.id}`, tooltip: "Select Pico SDK", rustSupport: false, + zephyrSupport: false, }, [StatusBarItemKey.picoBoardQuickPick]: { text: "Board: ", rustText: "Chip: ", - command: "raspberry-pi-pico.switchBoard", + // TODO: zephyrCommand option to zwphyr switch borad command or merge them that better + command: `${extensionName}.${SwitchBoardCommand.id}`, tooltip: "Select Chip", rustSupport: true, + zephyrSupport: true, }, }; @@ -69,9 +80,15 @@ export default class UI { }); } - public showStatusBarItems(isRustProject = false): void { + public showStatusBarItems( + isRustProject = false, + isZephyrProject = false + ): void { Object.values(this._items) .filter(item => !isRustProject || STATUS_BAR_ITEMS[item.id].rustSupport) + .filter( + item => !isZephyrProject || STATUS_BAR_ITEMS[item.id].zephyrSupport + ) .forEach(item => item.show()); } diff --git a/src/utils/cmakeUtil.mts b/src/utils/cmakeUtil.mts index 7aa0aaab..b0e5a015 100644 --- a/src/utils/cmakeUtil.mts +++ b/src/utils/cmakeUtil.mts @@ -1,10 +1,10 @@ -import { exec } from "child_process"; -import { workspace, type Uri, window, ProgressLocation } from "vscode"; +import { exec, execFile } from "child_process"; +import { Uri, workspace, window, ProgressLocation, commands } from "vscode"; import { showRequirementsNotMetErrorMessage } from "./requirementsUtil.mjs"; import { dirname, join, resolve } from "path"; import Settings from "../settings.mjs"; import { HOME_VAR, SettingsKey } from "../settings.mjs"; -import { existsSync, readFileSync, rmSync } from "fs"; +import { readFileSync } from "fs"; import Logger, { LoggerSource } from "../logger.mjs"; import { readFile, writeFile } from "fs/promises"; import { rimraf, windows as rimrafWindows } from "rimraf"; @@ -12,7 +12,17 @@ import { homedir } from "os"; import which from "which"; import { compareLt } from "./semverUtil.mjs"; import { buildCMakeIncPath } from "./download.mjs"; -import {EOL} from "os"; +import { EOL } from "os"; +import { vsExists } from "./vsHelpers.mjs"; +import State from "../state.mjs"; +import { + CURRENT_DTC_VERSION, + CURRENT_GPERF_VERSION, + CURRENT_WGET_VERSION, +} from "./sharedConstants.mjs"; +import { extensionName } from "../commands/command.mjs"; +import { GetZephyrWorkspacePathCommand } from "../commands/getPaths.mjs"; +import { getBoardFromZephyrProject } from "../commands/switchBoard.mjs"; export const CMAKE_DO_NOT_EDIT_HEADER_PREFIX = // eslint-disable-next-line max-len @@ -42,6 +52,54 @@ export async function getPythonPath(): Promise { return `${pythonPath.replaceAll("\\", "/")}`; } +async function findZephyrBinaries(): Promise { + const isWindows = process.platform === "win32"; + + if (isWindows) { + // get paths to the latest installed gperf and dtc and wget + const wgetPath = join(homedir(), ".pico-sdk", "wget", CURRENT_WGET_VERSION); + const gperfPath = join( + homedir(), + ".pico-sdk", + "gperf", + CURRENT_GPERF_VERSION + ); + const zipPath = join(homedir(), ".pico-sdk", "7zip"); + const dtcPath = join( + homedir(), + ".pico-sdk", + "dtc", + CURRENT_DTC_VERSION, + "bin" + ); + + const missingTools = []; + if (!(await vsExists(wgetPath))) { + missingTools.push("wget"); + } + if (!(await vsExists(gperfPath))) { + missingTools.push("gperf"); + } + if (!(await vsExists(zipPath))) { + missingTools.push("7zip"); + } + if (!(await vsExists(dtcPath))) { + missingTools.push("dtc"); + } + if (missingTools.length > 0) { + void showRequirementsNotMetErrorMessage(missingTools); + + return []; + } + + return [wgetPath, gperfPath, zipPath, dtcPath]; + } + + // TODO: macOS and linux stuff + + return []; +} + export async function getPath(): Promise { const settings = Settings.getInstance(); if (settings === undefined) { @@ -49,6 +107,7 @@ export async function getPath(): Promise { return ""; } + const isZephyrProject = State.getInstance().isZephyrProject; const ninjaPath = ( (await which( @@ -64,6 +123,9 @@ export async function getPath(): Promise { { nothrow: true } )) || "" ).replaceAll("\\", "/"); + + const zephyrBinaries = isZephyrProject ? await findZephyrBinaries() : []; + Logger.debug( LoggerSource.cmake, "Using python:", @@ -91,15 +153,24 @@ export async function getPath(): Promise { const isWindows = process.platform === "win32"; - return `${ninjaPath.includes("/") ? dirname(ninjaPath) : ""}${ + let result = `${ninjaPath.includes("/") ? dirname(ninjaPath) : ""}${ cmakePath.includes("/") ? `${isWindows ? ";" : ":"}${dirname(cmakePath)}` : "" - }${ + }${isWindows ? ";" : ":"}${ pythonPath.includes("/") ? `${dirname(pythonPath)}${isWindows ? ";" : ":"}` : "" }`; + + if (zephyrBinaries.length > 0) { + result += + (isWindows ? ";" : ":") + zephyrBinaries.join(isWindows ? ";" : ":"); + } + + Logger.debug(LoggerSource.cmake, "Using PATH:", result); + + return result; } export async function configureCmakeNinja( @@ -126,7 +197,7 @@ export async function configureCmakeNinja( return false; } - if (existsSync(join(folder.fsPath, "build", "CMakeCache.txt"))) { + if (await vsExists(join(folder.fsPath, "build", "CMakeCache.txt"))) { // check if the build directory has been moved const buildDir = join(folder.fsPath, "build"); @@ -150,7 +221,7 @@ export async function configureCmakeNinja( ` - Deleting CMakeCache.txt and regenerating.` ); - rmSync(join(buildDir, "CMakeCache.txt")); + await workspace.fs.delete(Uri.file(join(buildDir, "CMakeCache.txt"))); } } } @@ -197,6 +268,8 @@ export async function configureCmakeNinja( customEnv[isWindows ? "Path" : "PATH"] = customPath + customEnv[isWindows ? "Path" : "PATH"]; const pythonPath = await getPythonPath(); + const isZephyrProject = State.getInstance().isZephyrProject; + const buildDir = join(folder.fsPath, "build"); const command = `${process.env.ComSpec === "powershell.exe" ? "&" : ""}"${cmake}" ${ @@ -204,16 +277,41 @@ export async function configureCmakeNinja( ? `-DPython3_EXECUTABLE="${pythonPath.replaceAll("\\", "/")}" ` : "" }` + - `-G Ninja -B ./build "${folder.fsPath}"` + + `-G Ninja -B "${buildDir}" "${folder.fsPath}"` + (buildType ? ` -DCMAKE_BUILD_TYPE=${buildType}` : ""); + const zephyrWorkspace = await commands.executeCommand( + `${extensionName}.${GetZephyrWorkspacePathCommand.id}` + ); + const westExe = isWindows ? "west.exe" : "west"; + const westPath = join(zephyrWorkspace, "venv", "Scripts", westExe); + const zephyrBoard = await getBoardFromZephyrProject( + join(folder.fsPath, ".vscode", "tasks.json") + ); + if (isZephyrProject && zephyrBoard === undefined) { + void window.showErrorMessage( + "Failed to configure CMake for the current Zephyr project. " + + "Could not determine the board from .vscode/tasks.json." + ); + + return false; + } + const zephyrCommand = `${ + process.env.ComSpec === "powershell.exe" ? "&" : "" + }"${westPath}" build --cmake-only -b ${zephyrBoard} -d "${buildDir}" "${ + folder.fsPath + }"`; + await new Promise((resolve, reject) => { // use exec to be able to cancel the process const child = exec( - command, + isZephyrProject ? zephyrCommand : command, { env: customEnv, - cwd: folder.fsPath, + cwd: isZephyrProject + ? zephyrWorkspace || folder.fsPath + : folder.fsPath, + windowsHide: false, }, error => { progress.report({ increment: 100 }); @@ -351,19 +449,32 @@ export async function cmakeUpdateSDK( let modifiedContent = content.replace( updateSectionRegex, - `# ${CMAKE_DO_NOT_EDIT_HEADER_PREFIX}` + EOL + - "if(WIN32)" + EOL + - " set(USERHOME $ENV{USERPROFILE})" + EOL + - "else()" + EOL + - " set(USERHOME $ENV{HOME})" + EOL + - "endif()" + EOL + - `set(sdkVersion ${newSDKVersion})` + EOL + - `set(toolchainVersion ${newToolchainVersion})` + EOL + - `set(picotoolVersion ${newPicotoolVersion})` + EOL + - `set(picoVscode ${buildCMakeIncPath(false)}/pico-vscode.cmake)` + EOL + - "if (EXISTS ${picoVscode})" + EOL + - " include(${picoVscode})" + EOL + - "endif()" + EOL + + `# ${CMAKE_DO_NOT_EDIT_HEADER_PREFIX}` + + EOL + + "if(WIN32)" + + EOL + + " set(USERHOME $ENV{USERPROFILE})" + + EOL + + "else()" + + EOL + + " set(USERHOME $ENV{HOME})" + + EOL + + "endif()" + + EOL + + `set(sdkVersion ${newSDKVersion})` + + EOL + + `set(toolchainVersion ${newToolchainVersion})` + + EOL + + `set(picotoolVersion ${newPicotoolVersion})` + + EOL + + `set(picoVscode ${buildCMakeIncPath(false)}/pico-vscode.cmake)` + + EOL + + "if (EXISTS ${picoVscode})" + + EOL + + " include(${picoVscode})" + + EOL + + "endif()" + + EOL + // eslint-disable-next-line max-len "# ====================================================================================" ); @@ -541,3 +652,52 @@ export function cmakeGetPicoVar( return match[1]; } + +/** + * Get the version string of a CMake executable. + * Works for both stable releases (e.g. "3.31.5") + * and prereleases like "3.31.0-rc4". + * + * @param cmakePath Path to the cmake executable (absolute or in PATH). + * @returns Promise that resolves to the version string (e.g. "3.31.5" or "3.31.0-rc4"), + * or undefined if not found/parse failed. + */ +export async function getCmakeVersion( + cmakePath: string +): Promise { + return new Promise(resolve => { + execFile(cmakePath, ["--version"], { windowsHide: true }, (err, stdout) => { + if (err) { + console.error(`Failed to run cmake at ${cmakePath}: ${err.message}`); + resolve(undefined); + + return; + } + + const firstLine = stdout.split(/\r?\n/)[0].trim(); + // Expected: "cmake version 3.31.5" or "cmake version 3.31.0-rc4" + const prefix = "cmake version "; + if (firstLine.toLowerCase().startsWith(prefix)) { + const version = firstLine.substring(prefix.length).trim(); + resolve(version); + } else { + console.error(`Unexpected cmake --version output: ${firstLine}`); + resolve(undefined); + } + }); + }); +} + +/** + * Get the version string of the system-installed CMake (in PATH). + * @returns Promise that resolves to a version string (e.g. "3.31.5") + * or undefined if cmake not available. + */ +export async function getSystemCmakeVersion(): Promise { + const cmakePath = await which("cmake", { nothrow: true }); + if (!cmakePath) { + return undefined; + } + + return getCmakeVersion(cmakePath); +} diff --git a/src/utils/download.mts b/src/utils/download.mts index 9258dec4..939ae9c5 100644 --- a/src/utils/download.mts +++ b/src/utils/download.mts @@ -7,6 +7,7 @@ import { rmSync, } from "fs"; import { mkdir } from "fs/promises"; +import { type ExecOptions, exec } from "child_process"; import { homedir, tmpdir } from "os"; import { basename, dirname, join } from "path"; import { join as joinPosix } from "path/posix"; @@ -22,7 +23,7 @@ import { cloneRepository, initSubmodules, ensureGit } from "./gitUtil.mjs"; import { HOME_VAR, SettingsKey } from "../settings.mjs"; import Settings from "../settings.mjs"; import which from "which"; -import { ProgressLocation, type Uri, window } from "vscode"; +import { ProgressLocation, Uri, window, workspace } from "vscode"; import { fileURLToPath } from "url"; import { type GithubReleaseAssetData, @@ -224,6 +225,25 @@ export async function downloadAndReadFile( return response.statusCode === HTTP_STATUS_OK ? response.body : undefined; } +export function buildWestPath(): string { + return joinPosix( + homeDirectory.replaceAll("\\", "/"), + ".pico-sdk", + "zephyr_workspace", + "venv", + process.platform === "win32" ? "Scripts" : "bin", + process.platform === "win32" ? "west.exe" : "west" + ); +} + +export function buildZephyrWorkspacePath(): string { + return joinPosix( + homeDirectory.replaceAll("\\", "/"), + ".pico-sdk", + "zephyr_workspace" + ); +} + /** * Downloads and installs an archive from a URL. * @@ -415,7 +435,7 @@ async function downloadFileUndici( }); } -async function downloadFileGot( +export async function downloadFileGot( url: URL, archiveFilePath: string, extraHeaders?: { [key: string]: string }, @@ -1160,6 +1180,29 @@ export async function downloadAndInstallCmake( ); } +function _runCommand( + command: string, + options: ExecOptions +): Promise { + Logger.info(LoggerSource.downloader, command); + + return new Promise(resolve => { + const generatorProcess = exec(command, options, (error, stdout, stderr) => { + Logger.info(LoggerSource.downloader, stdout); + Logger.info(LoggerSource.downloader, stderr); + if (error) { + Logger.error(LoggerSource.downloader, `${error.message}`); + resolve(null); // indicate error + } + }); + + generatorProcess.on("exit", code => { + // Resolve with exit code or -1 if code is undefined + resolve(code); + }); + }); +} + /** * Downloads and installs Python3 Embed. * @@ -1295,13 +1338,15 @@ export async function downloadEmbedPython( }); }*/ + let pythonExe; + try { // unpack the archive const success = unzipFile(archiveFilePath, targetDirectory); // delete tmp file rmSync(archiveFilePath, { recursive: true, force: true }); - return success ? `${settingsTargetDirectory}/python.exe` : undefined; + pythonExe = success ? `${settingsTargetDirectory}/python.exe` : undefined; } catch (error) { Logger.error( LoggerSource.downloader, @@ -1310,6 +1355,66 @@ export async function downloadEmbedPython( return; } + + // Set up pip for embeddable Python to allow installation of packages + if (pythonExe) { + const fullPythonExe = `${targetDirectory}/python.exe`; + + const getPipURL = new URL("https://bootstrap.pypa.io/get-pip.py"); + const success = await downloadFileGot( + getPipURL, + joinPosix(targetDirectory, "get-pip.py") + ); + + if (!success) { + return undefined; + } + + const dllDir = `${targetDirectory}/DLLs`; + await workspace.fs.createDirectory(Uri.file(dllDir)); + + // Write to *._pth to allow use of installed packages + const pthFile = `${targetDirectory}/python312._pth`; + let pthContents = ( + await workspace.fs.readFile(Uri.file(pthFile)) + ).toString(); + pthContents += "\nimport site"; + await workspace.fs.writeFile(Uri.file(pthFile), Buffer.from(pthContents)); + + const installPipCommand: string = [ + `${ + process.env.ComSpec === "powershell.exe" ? "&" : "" + }"${fullPythonExe}"`, + "get-pip.py", + ].join(" "); + + let commandResult = await _runCommand(installPipCommand, { + cwd: targetDirectory, + windowsHide: true, + }); + + if (commandResult !== 0) { + return undefined; + } + + const installVirtualenvCommand: string = [ + `${ + process.env.ComSpec === "powershell.exe" ? "&" : "" + }"${fullPythonExe}"`, + "-m pip install virtualenv", + ].join(" "); + + commandResult = await _runCommand(installVirtualenvCommand, { + cwd: targetDirectory, + windowsHide: true, + }); + + if (commandResult !== 0) { + return undefined; + } + } + + return pythonExe; } /** diff --git a/src/utils/ninjaUtil.mts b/src/utils/ninjaUtil.mts index 2c24fb13..ced635ec 100644 --- a/src/utils/ninjaUtil.mts +++ b/src/utils/ninjaUtil.mts @@ -2,6 +2,8 @@ import { readdirSync, statSync } from "fs"; import Logger from "../logger.mjs"; import { join } from "path"; import { homedir } from "os"; +import { execFile } from "child_process"; +import which from "which"; export interface InstalledNinja { version: string; @@ -46,3 +48,48 @@ export function detectInstalledToolchains(): InstalledNinja[] { return installedNinjas; } + +/** + * Get the version string of a ninja executable. + * Works for both stable releases (e.g. "1.12.1") + * and old releases like "release-". + * + * @param ninjaPath Path to the ninja executable (absolute or in PATH). + * @returns Promise that resolves to the version string (e.g. "1.12.1" or "release-120715"), + * or undefined if not found/parse failed. + */ +export async function getNinjaVersion( + ninjaPath: string +): Promise { + return new Promise(resolve => { + execFile(ninjaPath, ["--version"], { windowsHide: true }, (err, stdout) => { + if (err) { + console.error(`Failed to run ninja at ${ninjaPath}: ${err.message}`); + resolve(undefined); + + return; + } + + const firstLine = stdout.split(/\r?\n/)[0].trim().toLowerCase(); + // Expected: "1.12.1" or "release-120715" + if ( + firstLine.length > 0 && + (firstLine.includes(".") || firstLine.startsWith("release-")) + ) { + resolve(firstLine); + } else { + console.error(`Unexpected ninja --version output: ${stdout}`); + resolve(undefined); + } + }); + }); +} + +export async function getSystemNinjaVersion(): Promise { + const ninjaPath = await which("ninja", { nothrow: true }); + if (!ninjaPath) { + return undefined; + } + + return getNinjaVersion(ninjaPath); +} diff --git a/src/utils/projectGeneration/projectRust.mts b/src/utils/projectGeneration/projectRust.mts index 1e4f4719..10dd646e 100644 --- a/src/utils/projectGeneration/projectRust.mts +++ b/src/utils/projectGeneration/projectRust.mts @@ -224,7 +224,7 @@ async function generateVSCodeConfig(projectRoot: string): Promise { } catch (error) { Logger.error( LoggerSource.projectRust, - "Failed to write extensions.json file", + "Failed to write vscode configuration files:", unknownErrorToString(error) ); diff --git a/src/utils/projectGeneration/projectZephyr.mts b/src/utils/projectGeneration/projectZephyr.mts new file mode 100644 index 00000000..8363b95e --- /dev/null +++ b/src/utils/projectGeneration/projectZephyr.mts @@ -0,0 +1,1222 @@ +/* eslint-disable max-len */ +/* eslint-disable @typescript-eslint/naming-convention */ +import { join } from "path"; +import Logger, { LoggerSource } from "../../logger.mjs"; +import { unknownErrorToString } from "../errorHelper.mjs"; +import { + GetChipUppercaseCommand, + GetOpenOCDRootCommand, + GetPicotoolPathCommand, + GetTargetCommand, + GetWestPathCommand, + GetZephyrWorkspacePathCommand, +} from "../../commands/getPaths.mjs"; +import { extensionName } from "../../commands/command.mjs"; +import { commands, Uri, window, workspace } from "vscode"; +import { + CURRENT_DTC_VERSION, + CURRENT_GPERF_VERSION, + CURRENT_WGET_VERSION, +} from "../sharedConstants.mjs"; +import type { VersionBundle } from "../versionBundles.mjs"; +import { HOME_VAR } from "../../settings.mjs"; +import { TextEncoder } from "util"; +import { + BoardType, + ZephyrProjectBase, + type ZephyrSubmitMessageValue, +} from "../../webview/sharedEnums.mjs"; +import { + WIFI_HTTP_C, + WIFI_HTTP_H, + WIFI_JSON_DEFINITIONS_H, + WIFI_PING_C, + WIFI_PING_H, + WIFI_WIFI_C, + WIFI_WIFI_H, +} from "./zephyrFiles.mjs"; +import { homedir } from "os"; + +// Kconfig snippets +const spiKconfig: string = "CONFIG_SPI=y"; +const i2cKconfig: string = "CONFIG_I2C=y"; +const gpioKconfig: string = "CONFIG_GPIO=y"; +const sensorKconfig: string = "CONFIG_SENSOR=y"; +const shellKconfig: string = "CONFIG_SHELL=y"; +const wifiKconfig: string = `CONFIG_NETWORKING=y +CONFIG_TEST_RANDOM_GENERATOR=y + +CONFIG_MAIN_STACK_SIZE=5200 +CONFIG_SHELL_STACK_SIZE=5200 +CONFIG_NET_TX_STACK_SIZE=2048 +CONFIG_NET_RX_STACK_SIZE=2048 +CONFIG_LOG_BUFFER_SIZE=4096 + +CONFIG_NET_PKT_RX_COUNT=10 +CONFIG_NET_PKT_TX_COUNT=10 +CONFIG_NET_BUF_RX_COUNT=20 +CONFIG_NET_BUF_TX_COUNT=20 +CONFIG_NET_MAX_CONN=10 +CONFIG_NET_MAX_CONTEXTS=10 +CONFIG_NET_DHCPV4=y + +CONFIG_NET_IPV4=y +CONFIG_NET_IPV6=n + +CONFIG_NET_TCP=y +CONFIG_NET_SOCKETS=y + +CONFIG_DNS_RESOLVER=y +CONFIG_DNS_SERVER_IP_ADDRESSES=y +CONFIG_DNS_SERVER1="192.0.2.2" +CONFIG_DNS_RESOLVER_AI_MAX_ENTRIES=10 + +# Network address config +CONFIG_NET_CONFIG_AUTO_INIT=n +CONFIG_NET_CONFIG_SETTINGS=y +CONFIG_NET_CONFIG_NEED_IPV4=y +CONFIG_NET_CONFIG_MY_IPV4_ADDR="192.0.2.1" +CONFIG_NET_CONFIG_PEER_IPV4_ADDR="192.0.2.2" +CONFIG_NET_CONFIG_MY_IPV4_GW="192.0.2.2" + +CONFIG_NET_LOG=y +CONFIG_INIT_STACKS=y + +CONFIG_NET_STATISTICS=y +CONFIG_NET_STATISTICS_PERIODIC_OUTPUT=n + +CONFIG_HTTP_CLIENT=y + +CONFIG_WIFI=y +CONFIG_WIFI_LOG_LEVEL_ERR=y +# printing of scan results puts pressure on queues in new locking +# design in net_mgmt. So, use a higher timeout for a crowded +# environment. +CONFIG_NET_MGMT_EVENT_QUEUE_TIMEOUT=5000 +CONFIG_NET_MGMT_EVENT_QUEUE_SIZE=16`; + +// Shell Kconfig values +const shellSpiKconfig: string = "CONFIG_SPI_SHELL=y"; +const shellI2CKconfig: string = "CONFIG_I2C_SHELL=y"; +const shellGPIOKconfig: string = "CONFIG_GPIO_SHELL=y"; +const shellSensorKconfig: string = "CONFIG_SENSOR_SHELL=y"; +const shellWifiKconfig: string = `CONFIG_WIFI_LOG_LEVEL_ERR=y +CONFIG_NET_L2_WIFI_SHELL=y +CONFIG_NET_SHELL=y`; + +/** + * Convert the enum to the Zephyr board name + * + * @param e BoardType enum + * @returns string Zephyr board name + * @throws Error if unknown board type + */ +function enumToBoard(e: BoardType): string { + switch (e) { + case BoardType.pico: + return "rpi_pico"; + case BoardType.picoW: + return "rpi_pico/rp2040/w"; + case BoardType.pico2: + return "rpi_pico2/rp2350a/m33"; + case BoardType.pico2W: + return "rpi_pico2/rp2350a/m33/w"; + default: + throw new Error(`Unknown Board Type: ${e as string}`); + } +} + +async function generateVSCodeConfig( + projectRoot: string, + latestVb: [string, VersionBundle], + ninjaPath: string, + cmakePath: string, + te: TextEncoder = new TextEncoder(), + data: ZephyrSubmitMessageValue +): Promise { + const vsc = join(projectRoot, ".vscode"); + + // create extensions.json + const extensions = { + recommendations: [ + "marus25.cortex-debug", + "ms-vscode.cpptools", + "ms-vscode.cmake-tools", + "ms-vscode.vscode-serial-monitor", + "raspberry-pi.raspberry-pi-pico", + ], + }; + + // TODO: why run, maybe to make sure installed but that should be done before! + const openOCDPath: string | undefined = await commands.executeCommand( + `${extensionName}.${GetOpenOCDRootCommand.id}` + ); + if (!openOCDPath) { + Logger.error(LoggerSource.projectRust, "Failed to get OpenOCD path"); + + void window.showErrorMessage("Failed to get OpenOCD path"); + + return false; + } + + const cppProperties = { + version: 4, + configurations: [ + { + name: "Zephyr", + intelliSenseMode: "linux-gcc-arm", + compilerPath: + // TODO: maybe move into command (the part before the executable) / test if not .exe works on win32 + "${userHome}/.pico-sdk/zephyr_workspace/zephyr-sdk/arm-zephyr-eabi/bin/arm-zephyr-eabi-gcc", + includePath: [ + "${workspaceFolder}/**", + "${workspaceFolder}/build/zephyr/include", + "${userHome}/.pico-sdk/zephyr_workspace/zephyr/include", + "${userHome}/.pico-sdk/zephyr_workspace/zephyr/modules/cmsis_6", + "${userHome}/.pico-sdk/zephyr_workspace/zephyr/modules/hal_infineon", + "${userHome}/.pico-sdk/zephyr_workspace/zephyr/modules/hal_rpi_pico", + ], + compileCommands: "${workspaceFolder}/build/compile_commands.json", + cppStandard: "gnu++20", + cStandard: "gnu17", + forcedInclude: [ + "${userHome}/.pico-sdk/zephyr_workspace/zephyr/include/zephyr/devicetree.h", + "${workspaceFolder}/build/zephyr/include/generated/zephyr/autoconf.h", + "${workspaceFolder}/build/zephyr/include/generated/zephyr/version.h", + ], + }, + ], + }; + + const launch = { + version: "0.2.0", + configurations: [ + { + name: "Pico Debug (Zephyr)", + cwd: "${workspaceFolder}", + // TODO: command launch target path? + executable: "${workspaceFolder}/build/zephyr/zephyr.elf", + request: "launch", + type: "cortex-debug", + servertype: "openocd", + // TODO: maybe svd file + serverpath: `\${command:${extensionName}.${GetOpenOCDRootCommand.id}}/openocd`, + searchDir: [ + `\${command:${extensionName}.${GetOpenOCDRootCommand.id}}/scripts`, + ], + toolchainPrefix: "arm-zephyr-eabi", + armToolchainPath: + // TODO: maybe just full get zephyr compiler path command + `\${command:${extensionName}.${GetZephyrWorkspacePathCommand.id}}/zephyr-sdk/arm-zephyr-eabi/bin`, + // TODO: get chip dynamically maybe: chip: `\${command:${extensionName}.${GetChipCommand.id}}`, + // meaning only one cfg required + device: `\${command:${extensionName}.${GetChipUppercaseCommand.id}}`, + svdFile: `\${userHome}/.pico-sdk/sdk/${latestVb[0]}/src/\${command:${extensionName}.getChip}/hardware_regs/\${command:${extensionName}.${GetChipUppercaseCommand.id}}.svd`, + configFiles: [ + "interface/cmsis-dap.cfg", + `target/\${command:${extensionName}.${GetTargetCommand.id}}.cfg`, + ], + runToEntryPoint: "main", + // Fix for no_flash binaries, where monitor reset halt doesn't do what is expected + // Also works fine for flash binaries + openOCDLaunchCommands: ["adapter speed 5000"], + // TODO: add zephyr build support to support this. + rtos: "Zephyr", + }, + ], + }; + + const settings = { + "cmake.options.statusBarVisibility": "hidden", + "cmake.options.advanced": { + build: { + statusBarVisibility: "hidden", + }, + launch: { + statusBarVisibility: "hidden", + }, + debug: { + statusBarVisibility: "hidden", + }, + }, + "cmake.configureOnEdit": false, + "cmake.automaticReconfigure": false, + "cmake.configureOnOpen": false, + "cmake.generator": "Ninja", + "cmake.cmakePath": "", + "C_Cpp.debugShortcut": false, + "terminal.integrated.env.windows": { + // TODO: contitionally cmake: \${env:USERPROFILE}/.pico-sdk/cmake/${latestVb.cmake}/bin + Path: `\${env:USERPROFILE}/.pico-sdk/dtc/${CURRENT_DTC_VERSION}/bin;\${env:USERPROFILE}/.pico-sdk/gperf/${CURRENT_GPERF_VERSION};\${env:USERPROFILE}/.pico-sdk/wget/${CURRENT_WGET_VERSION};\${env:USERPROFILE}/.pico-sdk/7zip;\${env:PATH}`, + }, + "terminal.integrated.env.osx": { + PATH: `\${env:HOME}/.pico-sdk/dtc/${CURRENT_DTC_VERSION}/bin:\${env:HOME}/.pico-sdk/gperf/${CURRENT_GPERF_VERSION}:\${env:HOME}/.pico-sdk/wget:\${env:PATH}`, + }, + "terminal.integrated.env.linux": { + PATH: `\${env:HOME}/.pico-sdk/dtc/${CURRENT_DTC_VERSION}/bin:\${env:HOME}/.pico-sdk/gperf/${CURRENT_GPERF_VERSION}:\${env:HOME}/.pico-sdk/wget:\${env:PATH}`, + }, + "raspberry-pi-pico.cmakeAutoConfigure": true, + "raspberry-pi-pico.useCmakeTools": false, + "raspberry-pi-pico.cmakePath": "", + "raspberry-pi-pico.ninjaPath": "", + "editor.formatOnSave": true, + "search.exclude": { + "build/": true, + }, + }; + + if (ninjaPath.length > 0) { + settings["raspberry-pi-pico.ninjaPath"] = ninjaPath.replace( + homedir().replace("\\", "/"), + HOME_VAR + ); + + // Add to PATH + const pathWindows = ninjaPath + .replace(HOME_VAR, "${env:USERPROFILE}") + .replace(homedir().replace("\\", "/"), "${env:USERPROFILE}"); + const pathPosix = ninjaPath + .replace(HOME_VAR, "${env:HOME}") + .replace(homedir().replace("\\", "/"), "${env:HOME}"); + + settings[ + "terminal.integrated.env.windows" + ].Path = `${pathWindows};${settings["terminal.integrated.env.windows"].Path}`; + settings[ + "terminal.integrated.env.osx" + ].PATH = `${pathPosix}:${settings["terminal.integrated.env.osx"].PATH}`; + settings[ + "terminal.integrated.env.linux" + ].PATH = `${pathPosix}:${settings["terminal.integrated.env.linux"].PATH}`; + } else { + // assume in PATH + settings["raspberry-pi-pico.ninjaPath"] = "ninja"; + } + + if (cmakePath.length > 0) { + const pathWindows = cmakePath + .replace(HOME_VAR, "${env:USERPROFILE}") + .replace(homedir().replace("\\", "/"), "${env:USERPROFILE}"); + const pathPosix = cmakePath + .replace(HOME_VAR, "${env:HOME}") + .replace(homedir().replace("\\", "/"), "${env:HOME}"); + settings["cmake.cmakePath"] = cmakePath.replace( + homedir().replace("\\", "/"), + "${userHome}" + ); + settings["raspberry-pi-pico.cmakePath"] = cmakePath.replace( + homedir().replace("\\", "/"), + HOME_VAR + ); + // add to path + settings[ + "terminal.integrated.env.windows" + ].Path = `${pathWindows};${settings["terminal.integrated.env.windows"].Path}`; + settings[ + "terminal.integrated.env.osx" + ].PATH = `${pathPosix}:${settings["terminal.integrated.env.osx"].PATH}`; + settings[ + "terminal.integrated.env.linux" + ].PATH = `${pathPosix}:${settings["terminal.integrated.env.linux"].PATH}`; + } else { + // assume in PATH + settings["cmake.cmakePath"] = "cmake"; + settings["raspberry-pi-pico.cmakePath"] = "cmake"; + } + + const westArgs = [ + "build", + "-p", + "auto", + "-b", + enumToBoard(data.boardType), + "-d", + // TODO: check! "" + '"${workspaceFolder}"/build', + '"${workspaceFolder}"', + ]; + + // If console is USB, use the local snippet + if (data.console === "USB") { + westArgs.push("-S", "usb_serial_port"); + } + + westArgs.push( + "--", + `-DOPENOCD=\${command:${extensionName}.${GetOpenOCDRootCommand.id}}/openocd`, + `-DOPENOCD_DEFAULT_PATH=\${command:${extensionName}.${GetOpenOCDRootCommand.id}}/scripts` + ); + + const tasks = { + version: "2.0.0", + tasks: [ + { + label: "Compile Project", + type: "shell", + command: `\${command:${extensionName}.${GetWestPathCommand.id}}`, + args: westArgs, + group: { + kind: "build", + isDefault: true, + }, + presentation: { + reveal: "always", + panel: "dedicated", + }, + problemMatcher: "$gcc", + options: { + cwd: "${command:raspberry-pi-pico.getZephyrWorkspacePath}", + }, + windows: { + options: { + shell: { + executable: "cmd.exe", + args: ["/d", "/c"], + }, + }, + }, + }, + // TODO: test + { + label: "Flash", + type: "shell", + group: { + kind: "build", + }, + command: "${command:raspberry-pi-pico.getWestPath}", + args: ["flash", "--build-dir", '"${workspaceFolder}"/build'], + options: { + cwd: "${command:raspberry-pi-pico.getZephyrWorkspacePath}", + }, + }, + { + label: "Run Project", + type: "shell", + command: `\${command:${extensionName}.${GetPicotoolPathCommand.id}}`, + // TODO: support for launch target path command + args: ["load", '"${workspaceFolder}"/build/zephyr/zephyr.elf', "-fx"], + presentation: { + reveal: "always", + panel: "dedicated", + }, + problemMatcher: [], + }, + ], + }; + + try { + await workspace.fs.createDirectory(Uri.file(vsc)); + await workspace.fs.writeFile( + Uri.file(join(vsc, "extensions.json")), + te.encode(JSON.stringify(extensions, null, 2)) + ); + await workspace.fs.writeFile( + Uri.file(join(vsc, "launch.json")), + te.encode(JSON.stringify(launch, null, 2)) + ); + await workspace.fs.writeFile( + Uri.file(join(vsc, "settings.json")), + te.encode(JSON.stringify(settings, null, 2)) + ); + await workspace.fs.writeFile( + Uri.file(join(vsc, "tasks.json")), + te.encode(JSON.stringify(tasks, null, 2)) + ); + await workspace.fs.writeFile( + Uri.file(join(vsc, "c_cpp_properties.json")), + te.encode(JSON.stringify(cppProperties, null, 2)) + ); + + return true; + } catch (error) { + Logger.error( + LoggerSource.projectRust, + "Failed to write vscode configuration files:", + unknownErrorToString(error) + ); + + return false; + } +} + +async function generateWifiMainC( + projectRoot: string, + prjBase: ZephyrProjectBase, + te: TextEncoder = new TextEncoder() +): Promise { + const mainC = `/* + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +// Local includes +#include "http.h" +#include "json_definitions.h" +#include "ping.h" +#include "wifi.h" +#include "wifi_info.h" + +LOG_MODULE_REGISTER(wifi_example); + +/* HTTP server to connect to */ +const char HTTP_HOSTNAME[] = "google.com"; +const char HTTP_PATH[] = "/"; +const char JSON_HOSTNAME[] = "jsonplaceholder.typicode.com"; +const char JSON_GET_PATH[] = "/posts/1"; +const char JSON_POST_PATH[] = "/posts"; + +int main(void) +{ + printk("Starting wifi example on %s\\n", CONFIG_BOARD_TARGET); + + wifi_connect(WIFI_SSID, WIFI_PSK); + + // Ping Google DNS 4 times + printk("Pinging 8.8.8.8 to demonstrate connection:\\n"); + ping("8.8.8.8", 4); + + printk("Now performing http GET request to google.com...\\n"); + http_get_example(HTTP_HOSTNAME, HTTP_PATH); + k_sleep(K_SECONDS(1)); + + // Using https://jsonplaceholder.typicode.com/ to demonstrate GET and POST requests with JSON + struct json_example_object get_post_result; + int json_get_status = json_get_example(JSON_HOSTNAME, JSON_GET_PATH, &get_post_result); + if (json_get_status < 0) + { + LOG_ERR("Error in json_get_example"); + } else { + printk("Got JSON result:\\n"); + printk("Title: %s\\n", get_post_result.title); + printk("Body: %s\\n", get_post_result.body); + printk("User ID: %d\\n", get_post_result.userId); + printk("ID: %d\\n", get_post_result.id); + } + k_sleep(K_SECONDS(1)); + + struct json_example_object new_post_result; + struct json_example_payload new_post = { + .body = "RPi", + .title = "Pico", + .userId = 199 + }; + + json_get_status = json_post_example(JSON_HOSTNAME, JSON_POST_PATH, &new_post, &new_post_result); + if (json_get_status < 0) + { + LOG_ERR("Error in json_post_example"); + } else { + printk("Got JSON result:\\n"); + printk("Title: %s\\n", new_post_result.title); + printk("Body: %s\\n", new_post_result.body); + printk("User ID: %d\\n", new_post_result.userId); + printk("ID: %d\\n", new_post_result.id); + } + k_sleep(K_SECONDS(1)); + + return 0; +} +`; + + // write the file + try { + await workspace.fs.createDirectory(Uri.file(join(projectRoot, "src"))); + await workspace.fs.writeFile( + Uri.file(join(projectRoot, "src", "main.c")), + te.encode(mainC) + ); + + return true; + } catch (error) { + Logger.error( + LoggerSource.projectZephyr, + "Failed to write main.c file", + unknownErrorToString(error) + ); + + return false; + } +} + +async function generateMainC( + projectRoot: string, + prjBase: ZephyrProjectBase, + te: TextEncoder = new TextEncoder() +): Promise { + if (prjBase === ZephyrProjectBase.wifi) { + return generateWifiMainC(projectRoot, prjBase, te); + } + + const isBlinky = prjBase === ZephyrProjectBase.blinky; + + let mainC = `#include +#include +`; + + if (isBlinky) { + mainC = mainC.concat("#include \n"); + } + mainC = mainC.concat("\n"); + + mainC = mainC.concat( + "LOG_MODULE_REGISTER(main, CONFIG_LOG_DEFAULT_LEVEL);\n\n" + ); + + if (isBlinky) { + mainC = mainC.concat("#define LED0_NODE DT_ALIAS(led0)\n\n"); + mainC = mainC.concat( + "static const struct gpio_dt_spec led = " + + "GPIO_DT_SPEC_GET(LED0_NODE, gpios);\n\n" + ); + } + + mainC = mainC.concat("int main(void) {\n"); + + if (isBlinky) { + mainC = mainC.concat(' printk("Hello World! Blinky sample\\n");\n\n'); + } else { + mainC = mainC.concat(' printk("Hello World from Pico!\\n");\n\n'); + } + + if (isBlinky) { + const blinkySetup = ` bool led_state = false; + + if (!gpio_is_ready_dt(&led)) { + return 0; + } + + if (gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE) < 0) { + return 0; + } + +`; + mainC = mainC.concat(blinkySetup); + } + + // main loop + mainC = mainC.concat(" while (1) {\n"); + + if (isBlinky) { + const blinkyLoop = ` if (gpio_pin_toggle_dt(&led) < 0) { + return 0; + } + + led_state = !led_state; + printk("LED state: %s\\n", led_state ? "ON" : "OFF"); + +`; + mainC = mainC.concat(blinkyLoop); + } else { + mainC = mainC.concat( + ' printk("Running on %s...\\n", CONFIG_BOARD);\n\n' + ); + } + + mainC = mainC.concat(" k_sleep(K_MSEC(1000));\n}\n\n"); + mainC = mainC.concat(" return 0;\n}\n"); + + // write the file + try { + await workspace.fs.createDirectory(Uri.file(join(projectRoot, "src"))); + await workspace.fs.writeFile( + Uri.file(join(projectRoot, "src", "main.c")), + te.encode(mainC) + ); + + return true; + } catch (error) { + Logger.error( + LoggerSource.projectZephyr, + "Failed to write main.c file", + unknownErrorToString(error) + ); + + return false; + } +} + +async function generateAdditionalCodeFiles( + projectRoot: string, + prjBase: ZephyrProjectBase, + te: TextEncoder = new TextEncoder() +): Promise { + if (prjBase !== ZephyrProjectBase.wifi) { + return true; + } + + const wifiInfoTemplate = `// Fill in your WiFi information here +#define WIFI_SSID "" +#define WIFI_PSK "" +`; + + try { + await workspace.fs.createDirectory(Uri.file(join(projectRoot, "src"))); + await workspace.fs.writeFile( + Uri.file(join(projectRoot, "src", "http.c")), + te.encode(WIFI_HTTP_C) + ); + await workspace.fs.writeFile( + Uri.file(join(projectRoot, "src", "http.h")), + te.encode(WIFI_HTTP_H) + ); + + await workspace.fs.writeFile( + Uri.file(join(projectRoot, "src", "json_definitions.h")), + te.encode(WIFI_JSON_DEFINITIONS_H) + ); + + await workspace.fs.writeFile( + Uri.file(join(projectRoot, "src", "ping.c")), + te.encode(WIFI_PING_C) + ); + await workspace.fs.writeFile( + Uri.file(join(projectRoot, "src", "ping.h")), + te.encode(WIFI_PING_H) + ); + + await workspace.fs.writeFile( + Uri.file(join(projectRoot, "src", "wifi.c")), + te.encode(WIFI_WIFI_C) + ); + await workspace.fs.writeFile( + Uri.file(join(projectRoot, "src", "wifi.h")), + te.encode(WIFI_WIFI_H) + ); + + await workspace.fs.writeFile( + Uri.file(join(projectRoot, "src", "wifi_info.h")), + te.encode(wifiInfoTemplate) + ); + + return true; + } catch (error) { + Logger.error( + LoggerSource.projectZephyr, + "Failed to write additional code files", + unknownErrorToString(error) + ); + + return false; + } +} + +async function generateGitIgnore( + projectRoot: string, + te: TextEncoder = new TextEncoder(), + projBase: ZephyrProjectBase +): Promise { + let gitIgnore = `# Created by https://www.toptal.com/developers/gitignore/api/c,cmake,visualstudiocode,ninja,windows,macos,linux +# Edit at https://www.toptal.com/developers/gitignore?templates=c,cmake,visualstudiocode,ninja,windows,macos,linux + +### C ### +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf + +### CMake ### +CMakeLists.txt.user +CMakeCache.txt +CMakeFiles +CMakeScripts +Testing +Makefile +cmake_install.cmake +install_manifest.txt +compile_commands.json +CTestTestfile.cmake +_deps + +### CMake Patch ### +CMakeUserPresets.json + +# External projects +*-prefix/ + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Ninja ### +.ninja_deps +.ninja_log + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/c,cmake,visualstudiocode,ninja,windows,macos,linux +`; + + if (projBase === ZephyrProjectBase.wifi) { + gitIgnore = gitIgnore.concat("\r\n", "wifi_info.h\r\n"); + } + + try { + await workspace.fs.writeFile( + Uri.file(join(projectRoot, ".gitignore")), + te.encode(gitIgnore) + ); + + return true; + } catch (error) { + Logger.error( + LoggerSource.projectRust, + "Failed to write .gitignore file", + unknownErrorToString(error) + ); + + return false; + } +} + +async function generateCMakeList( + projectRoot: string, + te: TextEncoder = new TextEncoder(), + data: ZephyrSubmitMessageValue +): Promise { + // TODO: maybe dynamic check cmake minimum cmake version on cmake selection + // TODO: license notice required anymore? + let cmakeList = `#pico-zephyr-project +#------------------------------------------------------------------------------- +# Zephyr Example Application +# +# Copyright (c) 2021 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 +#------------------------------------------------------------------------------- +# NOTE: Please do not remove the #pico-zephyr-project header, it is used by +# the Raspberry Pi Pico SDK extension to identify the project type. +#------------------------------------------------------------------------------- + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(${data.projectName} LANGUAGES C) +`; + + const appSources = ["src/main.c"]; + + if (data.projectBase === ZephyrProjectBase.wifi) { + cmakeList = cmakeList.concat("\nFILE(GLOB app_sources src/*.c)\n"); + appSources.push("src/http.c"); + appSources.push("src/ping.c"); + appSources.push("src/wifi.c"); + } + + cmakeList = cmakeList.concat( + `\ntarget_sources(app PRIVATE ${appSources.join(" ")})\n` + ); + + try { + await workspace.fs.writeFile( + Uri.file(join(projectRoot, "CMakeLists.txt")), + te.encode(cmakeList) + ); + + return true; + } catch (error) { + Logger.error( + LoggerSource.projectZephyr, + "Failed to write CMakeLists.txt file", + unknownErrorToString(error) + ); + + return false; + } +} + +async function generateConf( + projectRoot: string, + prjBase: ZephyrProjectBase, + te: TextEncoder = new TextEncoder(), + data: ZephyrSubmitMessageValue +): Promise { + let conf = `CONFIG_LOG=y +CONFIG_LOG_PRINTK=y +CONFIG_LOG_DEFAULT_LEVEL=3 +`; + + // TODO: already do in UI + if (prjBase === ZephyrProjectBase.blinky) { + data.gpioFeature = true; + } + + conf = conf.concat( + "\r\n", + "\r\n", + "# Enable Modules:", + "\r\n", + data.gpioFeature ? gpioKconfig + "\r\n" : "", + data.i2cFeature ? i2cKconfig + "\r\n" : "", + data.spiFeature ? spiKconfig + "\r\n" : "", + data.sensorFeature ? sensorKconfig + "\r\n" : "", + data.wifiFeature ? wifiKconfig + "\r\n" : "", + data.shellFeature ? "\r\n" + "# Enabling shells:" + "\r\n" : "", + data.shellFeature ? shellKconfig + "\r\n" : "", + data.shellFeature && data.gpioFeature ? shellGPIOKconfig + "\r\n" : "", + data.shellFeature && data.i2cFeature ? shellI2CKconfig + "\r\n" : "", + data.shellFeature && data.spiFeature ? shellSpiKconfig + "\r\n" : "", + data.shellFeature && data.sensorFeature ? shellSensorKconfig + "\r\n" : "", + data.shellFeature && data.wifiFeature ? shellWifiKconfig + "\r\n" : "" + ); + + try { + await workspace.fs.writeFile( + Uri.file(join(projectRoot, "prj.conf")), + te.encode(conf) + ); + + return true; + } catch (error) { + Logger.error( + LoggerSource.projectZephyr, + "Failed to write prj.conf file", + unknownErrorToString(error) + ); + + return false; + } +} + +async function addSnippets( + projectRoot: string, + te: TextEncoder = new TextEncoder(), + data: ZephyrSubmitMessageValue +): Promise { + // check for all snippet options if we should even continue + if (data.console !== "USB") { + return true; + } + + const usbSerialSnippetYml = `name: usb_serial_port +append: + EXTRA_CONF_FILE: usb_serial_port.conf + EXTRA_DTC_OVERLAY_FILE: usb_serial_port.overlay +`; + + const usbSerialPortConf = `CONFIG_USB_DEVICE_STACK=y +CONFIG_USB_DEVICE_PRODUCT="Raspberry Pi Pico Example for Zephyr" +CONFIG_USB_DEVICE_VID=0x2E8A +CONFIG_USB_DEVICE_PID=0x0001 + +CONFIG_USB_DEVICE_INITIALIZE_AT_BOOT=y + +CONFIG_SERIAL=y +CONFIG_UART_LINE_CTRL=y + +CONFIG_USB_DRIVER_LOG_LEVEL_ERR=y +CONFIG_USB_DEVICE_LOG_LEVEL_ERR=y +CONFIG_USB_CDC_ACM_LOG_LEVEL_ERR=y +`; + + const usbSerialPortOverlay = `/ { + chosen { + zephyr,console = &usb_serial_port; + zephyr,shell-uart = &usb_serial_port; + }; +}; + +&zephyr_udc0 { + usb_serial_port: usb_serial_port { + compatible = "zephyr,cdc-acm-uart"; + }; +}; +`; + + try { + await workspace.fs.createDirectory(Uri.file(join(projectRoot, "snippets"))); + + if (data.console === "USB") { + const usbSerialFolder = join(projectRoot, "snippets", "usb_serial_port"); + await workspace.fs.createDirectory(Uri.file(usbSerialFolder)); + + await workspace.fs.writeFile( + Uri.file(join(usbSerialFolder, "snippet.yml")), + te.encode(usbSerialSnippetYml) + ); + await workspace.fs.writeFile( + Uri.file(join(usbSerialFolder, "usb_serial_port.conf")), + te.encode(usbSerialPortConf) + ); + await workspace.fs.writeFile( + Uri.file(join(usbSerialFolder, "usb_serial_port.overlay")), + te.encode(usbSerialPortOverlay) + ); + } + + return true; + } catch (error) { + Logger.error( + LoggerSource.projectZephyr, + "Failed to write code snippets:", + unknownErrorToString(error) + ); + + return false; + } +} + +/** + * Generates a new Zephyr project in the given folder. + * + * @param projectFolder The path where the project folder should be created. + * @param projectName The name of the project folder to create. + * @param prjBase The base template to use for the project. + * @returns True if the project was created successfully, false otherwise. + */ +export async function generateZephyrProject( + projectFolder: string, + projectName: string, + latestVb: [string, VersionBundle], + ninjaPath: string, + cmakePath: string, + data: ZephyrSubmitMessageValue +): Promise { + const projectRoot = join(projectFolder, projectName); + + try { + await workspace.fs.createDirectory(Uri.file(projectRoot)); + } catch (error) { + const msg = unknownErrorToString(error); + if ( + msg.includes("EPERM") || + msg.includes("EACCES") || + msg.includes("access denied") + ) { + Logger.error( + LoggerSource.projectRust, + "Failed to create project folder", + "Permission denied. Please check your permissions." + ); + + void window.showErrorMessage( + "Failed to create project folder. " + + "Permission denied - Please check your permissions." + ); + } else { + Logger.error( + LoggerSource.projectRust, + "Failed to create project folder", + unknownErrorToString(error) + ); + + void window.showErrorMessage( + "Failed to create project folder. " + + "See the output panel for more details." + ); + } + + return false; + } + + const te = new TextEncoder(); + + let result = await generateGitIgnore(projectRoot, te, data.projectBase); + if (!result) { + Logger.debug( + LoggerSource.projectRust, + "Failed to generate .gitignore file" + ); + + return false; + } + + result = await generateMainC(projectRoot, data.projectBase, te); + if (!result) { + Logger.debug(LoggerSource.projectRust, "Failed to generate main.c file"); + + return false; + } + + result = await generateAdditionalCodeFiles(projectRoot, data.projectBase, te); + if (!result) { + Logger.debug( + LoggerSource.projectRust, + "Failed to generate additional code files" + ); + + return false; + } + + result = await generateVSCodeConfig( + projectRoot, + latestVb, + ninjaPath, + cmakePath, + te, + data + ); + if (!result) { + Logger.debug( + LoggerSource.projectRust, + "Failed to generate .vscode configuration files." + ); + + return false; + } + + result = await generateCMakeList(projectRoot, te, data); + if (!result) { + Logger.debug( + LoggerSource.projectRust, + "Failed to generate CMakeLists.txt file" + ); + + return false; + } + + result = await generateConf(projectRoot, data.projectBase, te, data); + if (!result) { + Logger.debug(LoggerSource.projectRust, "Failed to generate prj.conf file"); + + return false; + } + + result = await addSnippets(projectRoot, te, data); + if (!result) { + Logger.debug( + LoggerSource.projectRust, + "Failed to generate code snippets file" + ); + + return false; + } + + return true; +} diff --git a/src/utils/projectGeneration/zephyrFiles.mts b/src/utils/projectGeneration/zephyrFiles.mts new file mode 100644 index 00000000..2eb40245 --- /dev/null +++ b/src/utils/projectGeneration/zephyrFiles.mts @@ -0,0 +1,463 @@ +/* eslint-disable max-len */ + +export const WIFI_HTTP_C = `#include "http.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "json_definitions.h" + +LOG_MODULE_REGISTER(http); + +#define HTTP_PORT "80" + +static K_SEM_DEFINE(json_response_complete, 0, 1); +static K_SEM_DEFINE(http_response_complete, 0, 1); +static const char * json_post_headers[] = { "Content-Type: application/json\\r\\n", NULL }; + +// Holds the HTTP response +static char response_buffer[2048]; + +// Holds the JSON payload +char json_payload_buffer[128]; + +// Keeps track of JSON parsing result +static struct json_example_object * returned_placeholder_post = NULL; +static int json_parse_result = -1; + +// void http_get(const char * hostname, const char * path); + +int connect_socket(const char * hostname) +{ + static struct addrinfo hints; + struct addrinfo *res; + int st, sock; + + LOG_DBG("Looking up IP addresses:"); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + st = getaddrinfo(hostname, HTTP_PORT, &hints, &res); + if (st != 0) { + LOG_ERR("Unable to resolve address, quitting"); + return -1; + } + LOG_DBG("getaddrinfo status: %d", st); + + dump_addrinfo(res); + + sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol); + if (sock < 0) + { + LOG_ERR("Issue setting up socket: %d", sock); + return -1; + } + LOG_DBG("sock = %d", sock); + + LOG_INF("Connecting to server..."); + int connect_result = connect(sock, res->ai_addr, res->ai_addrlen); + if (connect_result != 0) + { + LOG_ERR("Issue during connect: %d", sock); + return -1; + } + + return sock; +} + +static void http_response_cb(struct http_response *rsp, + enum http_final_call final_data, + void *user_data) +{ + printk("HTTP Callback: %.*s", rsp->data_len, rsp->recv_buf); + + if (HTTP_DATA_FINAL == final_data){ + printk("\\n"); + k_sem_give(&http_response_complete); + } +} + +void http_get_example(const char * hostname, const char * path) +{ + int sock = connect_socket(hostname); + if (sock < 0) + { + LOG_ERR("Issue setting up socket: %d", sock); + return; + } + + LOG_INF("Connected. Making HTTP request..."); + + struct http_request req = { 0 }; + int ret; + + req.method = HTTP_GET; + req.host = hostname; + req.url = path; + req.protocol = "HTTP/1.1"; + req.response = http_response_cb; + req.recv_buf = response_buffer; + req.recv_buf_len = sizeof(response_buffer); + + /* sock is a file descriptor referencing a socket that has been connected + * to the HTTP server. + */ + ret = http_client_req(sock, &req, 5000, NULL); + LOG_INF("HTTP Client Request returned: %d", ret); + if (ret < 0) + { + LOG_ERR("Error sending HTTP Client Request"); + return; + } + + k_sem_take(&http_response_complete, K_FOREVER); + + LOG_INF("HTTP GET complete"); + + LOG_INF("Close socket"); + + (void)close(sock); +} + +static void json_response_cb(struct http_response *rsp, + enum http_final_call final_data, + void *user_data) +{ + LOG_DBG("JSON Callback: %.*s", rsp->data_len, rsp->recv_buf); + + if (rsp->body_found) + { + LOG_DBG("Body:"); + printk("%.*s\\n", rsp->body_frag_len, rsp->body_frag_start); + + if (returned_placeholder_post != NULL) + { + json_parse_result = json_obj_parse( + rsp->body_frag_start, + rsp->body_frag_len, + json_example_object_descr, + ARRAY_SIZE(json_example_object_descr), + returned_placeholder_post + ); + + if (json_parse_result < 0) + { + LOG_ERR("JSON Parse Error: %d", json_parse_result); + } + else + { + LOG_DBG("json_obj_parse return code: %d", json_parse_result); + LOG_DBG("Title: %s", returned_placeholder_post->title); + LOG_DBG("Body: %s", returned_placeholder_post->body); + LOG_DBG("User ID: %d", returned_placeholder_post->id); + LOG_DBG("ID: %d", returned_placeholder_post->userId); + } + } else { + LOG_ERR("No pointer passed to copy JSON GET result to"); + } + } + + if (HTTP_DATA_FINAL == final_data){ + k_sem_give(&json_response_complete); + } +} + +int json_get_example(const char * hostname, const char * path, struct json_example_object * result) +{ + json_parse_result = -1; + returned_placeholder_post = result; + + int sock = connect_socket(hostname); + if (sock < 0) + { + LOG_ERR("Issue setting up socket: %d", sock); + return -1; + } + + LOG_INF("Connected. Get JSON Payload..."); + + struct http_request req = { 0 }; + int ret; + + req.method = HTTP_GET; + req.host = hostname; + req.url = path; + req.protocol = "HTTP/1.1"; + req.response = json_response_cb; + req.recv_buf = response_buffer; + req.recv_buf_len = sizeof(response_buffer); + + /* sock is a file descriptor referencing a socket that has been connected + * to the HTTP server. + */ + ret = http_client_req(sock, &req, 5000, NULL); + LOG_INF("HTTP Client Request returned: %d", ret); + if (ret < 0) + { + LOG_ERR("Error sending HTTP Client Request"); + return -1; + } + + k_sem_take(&json_response_complete, K_FOREVER); + + LOG_INF("JSON Response complete"); + + LOG_INF("Close socket"); + + (void)close(sock); + + return json_parse_result; +} + +int json_post_example(const char * hostname, const char * path, struct json_example_payload * payload, struct json_example_object * result) +{ + json_parse_result = -1; + returned_placeholder_post = result; + + int sock = connect_socket(hostname); + if (sock < 0) + { + LOG_ERR("Issue setting up socket: %d", sock); + return -1; + } + + LOG_INF("Connected. Post JSON Payload..."); + + // Parse the JSON object into a buffer + int required_buffer_len = json_calc_encoded_len( + json_example_payload_descr, + ARRAY_SIZE(json_example_payload_descr), + payload + ); + if (required_buffer_len > sizeof(json_payload_buffer)) + { + LOG_ERR("Payload is too large. Increase size of json_payload_buffer in http.c"); + return -1; + } + + int encode_status = json_obj_encode_buf( + json_example_payload_descr, + ARRAY_SIZE(json_example_payload_descr), + payload, + json_payload_buffer, + sizeof(json_payload_buffer) + ); + if (encode_status < 0) + { + LOG_ERR("Error encoding JSON payload: %d", encode_status); + return -1; + } + + LOG_DBG("%s", json_payload_buffer); + + struct http_request req = { 0 }; + int ret; + + req.method = HTTP_POST; + req.host = hostname; + req.url = path; + req.header_fields = json_post_headers; + req.protocol = "HTTP/1.1"; + req.response = json_response_cb; + req.payload = json_payload_buffer; + req.payload_len = strlen(json_payload_buffer); + req.recv_buf = response_buffer; + req.recv_buf_len = sizeof(response_buffer); + + ret = http_client_req(sock, &req, 5000, NULL); + LOG_INF("HTTP Client Request returned: %d", ret); + if (ret < 0) + { + LOG_ERR("Error sending HTTP Client Request"); + return -1; + } + + k_sem_take(&json_response_complete, K_FOREVER); + + LOG_INF("JSON POST complete"); + + LOG_INF("Close socket"); + + (void)close(sock); + + return json_parse_result; +} + +void dump_addrinfo(const struct addrinfo *ai) +{ + LOG_INF("addrinfo @%p: ai_family=%d, ai_socktype=%d, ai_protocol=%d, " + "sa_family=%d, sin_port=%x", + ai, ai->ai_family, ai->ai_socktype, ai->ai_protocol, ai->ai_addr->sa_family, + ntohs(((struct sockaddr_in *)ai->ai_addr)->sin_port)); +} +`; + +export const WIFI_HTTP_H = `#pragma once + +#include +#include + +#include "json_definitions.h" + +void dump_addrinfo(const struct addrinfo *ai); + +void http_get_example(const char * hostname, const char * path); + +int json_get_example(const char * hostname, const char * path, struct json_example_object * result); + +int json_post_example(const char * hostname, const char * path, struct json_example_payload * payload, struct json_example_object * result); +`; + +export const WIFI_JSON_DEFINITIONS_H = `#pragma once + +#include + +struct json_example_object { + const char *title; + const char *body; + int id; + int userId; +}; + +static const struct json_obj_descr json_example_object_descr[] = { + JSON_OBJ_DESCR_PRIM(struct json_example_object, title, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM(struct json_example_object, body, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM(struct json_example_object, id, JSON_TOK_NUMBER), + JSON_OBJ_DESCR_PRIM(struct json_example_object, userId, JSON_TOK_NUMBER), +}; + +struct json_example_payload { + const char *title; + const char *body; + int userId; +}; + +static const struct json_obj_descr json_example_payload_descr[] = { + JSON_OBJ_DESCR_PRIM(struct json_example_payload, title, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM(struct json_example_payload, body, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM(struct json_example_payload, userId, JSON_TOK_NUMBER), +}; +`; + +export const WIFI_PING_C = `#include "ping.h" + +#include +#include +#include +#include + +LOG_MODULE_REGISTER(ping); + +int icmp_echo_reply_handler(struct net_icmp_ctx *ctx, + struct net_pkt *pkt, + struct net_icmp_ip_hdr *hdr, + struct net_icmp_hdr *icmp_hdr, + void *user_data) +{ + uint32_t cycles; + char ipv4[INET_ADDRSTRLEN]; + zsock_inet_ntop(AF_INET, &hdr->ipv4->src, ipv4, INET_ADDRSTRLEN); + + uint32_t *start_cycles = user_data; + + cycles = k_cycle_get_32() - *start_cycles; + + LOG_INF("Reply from %s: bytes=%d time=%dms TTL=%d", + ipv4, + ntohs(hdr->ipv4->len), + ((uint32_t)k_cyc_to_ns_floor64(cycles) / 1000000), + hdr->ipv4->ttl); + + return 0; +} + +void ping(char* ipv4_addr, uint8_t count) +{ + uint32_t cycles; + int ret; + struct net_icmp_ctx icmp_context; + + // Register handler for echo reply + ret = net_icmp_init_ctx(&icmp_context, NET_ICMPV4_ECHO_REPLY, 0, icmp_echo_reply_handler); + if (ret != 0) { + LOG_ERR("Failed to init ping, err: %d", ret); + } + + struct net_if *iface = net_if_get_default(); + struct sockaddr_in dst_addr; + net_addr_pton(AF_INET, ipv4_addr, &dst_addr.sin_addr); + dst_addr.sin_family = AF_INET; + + for (int i = 0; i < count; i++) + { + cycles = k_cycle_get_32(); + ret = net_icmp_send_echo_request(&icmp_context, iface, (struct sockaddr *)&dst_addr, NULL, &cycles); + if (ret != 0) { + LOG_ERR("Failed to send ping, err: %d", ret); + } + k_sleep(K_SECONDS(1)); + } + + net_icmp_cleanup_ctx(&icmp_context); +} +`; + +export const WIFI_PING_H = `#pragma once + +#include + +void ping(char* ipv4_addr, uint8_t count); +`; + +export const WIFI_WIFI_C = `#include "wifi.h" + +#include +#include + +LOG_MODULE_REGISTER(wifi); + +void wifi_connect(const char * ssid, const char * psk) +{ + struct net_if *iface = net_if_get_default(); + struct wifi_connect_req_params cnx_params = { 0 }; + + cnx_params.ssid = ssid; + cnx_params.ssid_length = strlen(cnx_params.ssid); + cnx_params.psk = psk; + cnx_params.psk_length = strlen(cnx_params.psk); + cnx_params.security = WIFI_SECURITY_TYPE_NONE; + cnx_params.band = WIFI_FREQ_BAND_UNKNOWN; + cnx_params.channel = WIFI_CHANNEL_ANY; + cnx_params.mfp = WIFI_MFP_OPTIONAL; + cnx_params.wpa3_ent_mode = WIFI_WPA3_ENTERPRISE_NA; + cnx_params.eap_ver = 1; + cnx_params.bandwidth = WIFI_FREQ_BANDWIDTH_20MHZ; + cnx_params.verify_peer_cert = false; + + int connection_result = 1; + + while (connection_result != 0){ + LOG_INF("Attempting to connect to network %s", ssid); + connection_result = net_mgmt(NET_REQUEST_WIFI_CONNECT, iface, + &cnx_params, sizeof(struct wifi_connect_req_params)); + if (connection_result) { + LOG_ERR("Connection request failed with error: %d\\n", connection_result); + } + k_sleep(K_MSEC(1000)); + } + + LOG_INF("Connection succeeded."); +} +`; + +export const WIFI_WIFI_H = `#pragma once + +void wifi_connect(const char * ssid, const char * psk); +`; diff --git a/src/utils/setupZephyr.mts b/src/utils/setupZephyr.mts new file mode 100644 index 00000000..81669c64 --- /dev/null +++ b/src/utils/setupZephyr.mts @@ -0,0 +1,1305 @@ +import { existsSync } from "fs"; +import { window, workspace, ProgressLocation, Uri } from "vscode"; +import { type ExecOptions, exec } from "child_process"; +import { dirname, join } from "path"; +import { join as joinPosix } from "path/posix"; +import { homedir } from "os"; +import Logger, { LoggerSource } from "../logger.mjs"; +import type { Progress as GotProgress } from "got"; + +import { + buildCMakePath, + buildNinjaPath, + buildZephyrWorkspacePath, + downloadAndInstallArchive, + downloadAndInstallCmake, + downloadAndInstallNinja, + downloadAndInstallOpenOCD, + downloadAndInstallPicotool, + downloadAndInstallSDK, + downloadFileGot, +} from "../utils/download.mjs"; +import Settings, { HOME_VAR } from "../settings.mjs"; +import findPython, { showPythonNotFoundError } from "../utils/pythonHelper.mjs"; +import { ensureGit } from "../utils/gitUtil.mjs"; +import VersionBundlesLoader, { type VersionBundle } from "./versionBundles.mjs"; +import { + CURRENT_DTC_VERSION, + CURRENT_GPERF_VERSION, + CURRENT_WGET_VERSION, + LICENSE_URL_7ZIP, + OPENOCD_VERSION, + WINDOWS_X86_7ZIP_DOWNLOAD_URL, + WINDOWS_X86_DTC_DOWNLOAD_URL, + WINDOWS_X86_GPERF_DOWNLOAD_URL, + WINDOWS_X86_WGET_DOWNLOAD_URL, +} from "./sharedConstants.mjs"; +import { SDK_REPOSITORY_URL } from "./githubREST.mjs"; +import { vsExists } from "./vsHelpers.mjs"; +import which from "which"; + +interface ZephyrSetupValue { + cmakeMode: number; + cmakePath: string; + cmakeVersion: string; + extUri: Uri; + ninjaMode: number; + ninjaPath: string; + ninjaVersion: string; +} + +interface ZephyrSetupOutputs { + cmakeExecutable: string; + ninjaExecutable: string; + gitPath: string; + latestVb: [string, VersionBundle]; +} + +// Compute and cache the home directory +const homeDirectory: string = homedir(); + +const zephyrManifestContent: string = ` +manifest: + self: + west-commands: scripts/west-commands.yml + + remotes: + - name: zephyrproject-rtos + url-base: https://github.com/zephyrproject-rtos + + projects: + - name: zephyr + remote: zephyrproject-rtos + revision: main + import: + # By using name-allowlist we can clone only the modules that are + # strictly needed by the application. + name-allowlist: + - cmsis_6 # required by the ARM Cortex-M port + - hal_rpi_pico # required for Pico board support + - hal_infineon # required for Wifi chip support + - segger # required for Segger RTT support +`; + +// TODO: maybe move into download.mts +function buildDtcPath(version: string): string { + return joinPosix( + homeDirectory.replaceAll("\\", "/"), + ".pico-sdk", + "dtc", + version + ); +} + +function buildGperfPath(version: string): string { + return joinPosix( + homeDirectory.replaceAll("\\", "/"), + ".pico-sdk", + "gperf", + version + ); +} + +function buildWgetPath(version: string): string { + return joinPosix( + homeDirectory.replaceAll("\\", "/"), + ".pico-sdk", + "wget", + version + ); +} + +function build7ZipPathWin32(): string { + return join(homeDirectory, ".pico-sdk", "7zip"); +} + +function generateCustomEnv( + isWindows: boolean, + latestVb: VersionBundle, + cmakeExe: string, + pythonExe: string +): NodeJS.ProcessEnv { + const customEnv = process.env; + + const customPath = [ + dirname(cmakeExe), + join(homedir(), ".pico-sdk", "dtc", CURRENT_DTC_VERSION, "bin"), + // TODO: better git path + join(homedir(), ".pico-sdk", "git", "cmd"), + join(homedir(), ".pico-sdk", "gperf", CURRENT_GPERF_VERSION, "bin"), + join(homedir(), ".pico-sdk", "ninja", latestVb.ninja), + dirname(pythonExe), + join(homedir(), ".pico-sdk", "wget", CURRENT_WGET_VERSION), + join(homedir(), ".pico-sdk", "7zip"), + "", // Need this to add separator to end + ].join(process.platform === "win32" ? ";" : ":"); + + customEnv[isWindows ? "Path" : "PATH"] = + customPath + customEnv[isWindows ? "Path" : "PATH"]; + + return customEnv; +} + +// TODO: duplicate code with _runGenerator +function _runCommand( + command: string, + options: ExecOptions +): Promise { + Logger.debug(LoggerSource.zephyrSetup, `Running: ${command}`); + + return new Promise(resolve => { + const proc = exec( + ((process.env.ComSpec === "powershell.exe" || + process.env.ComSpec === + "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe") && + command.startsWith('"') + ? "&" + : "") + command, + options, + (error, stdout, stderr) => { + Logger.debug(LoggerSource.zephyrSetup, stdout); + Logger.debug(LoggerSource.zephyrSetup, stderr); + if (error) { + Logger.error( + LoggerSource.zephyrSetup, + `An error occurred executing a command: ${error.message}` + ); + resolve(null); // indicate error + } + } + ); + + proc.on("exit", code => { + // Resolve with exit code or -1 if code is undefined + resolve(code); + }); + }); +} + +async function checkGit(): Promise { + const settings = Settings.getInstance(); + if (settings === undefined) { + Logger.error(LoggerSource.zephyrSetup, "Settings not initialized."); + + return; + } + + return window.withProgress( + { + location: ProgressLocation.Notification, + title: "Ensuring Git is available", + cancellable: false, + }, + async progress2 => { + // TODO: this does take about 2s - may be reduced + const gitPath = await ensureGit(settings, { returnPath: true }); + if (typeof gitPath !== "string" || gitPath.length === 0) { + progress2.report({ + message: "Failed", + increment: 100, + }); + + return; + } + + progress2.report({ + message: "Success", + increment: 100, + }); + + return gitPath; + } + ); +} + +async function checkCmake( + cmakeMode: number, + cmakeVersion: string, + cmakePath: string +): Promise { + let progLastState = 0; + let installedSuccessfully = false; + + switch (cmakeMode) { + case 4: + case 2: + installedSuccessfully = await window.withProgress( + { + location: ProgressLocation.Notification, + title: "Downloading and installing CMake", + cancellable: false, + }, + async progress => { + const result = await downloadAndInstallCmake( + cmakeVersion, + (prog: GotProgress) => { + const per = prog.percent * 100; + progress.report({ + increment: per - progLastState, + }); + progLastState = per; + } + ); + + if (!result) { + progress.report({ + message: "Failed", + increment: 100, + }); + + return false; + } + + progress.report({ + message: "Success", + increment: 100, + }); + + return true; + } + ); + + if (!installedSuccessfully) { + return; + } + + return joinPosix(buildCMakePath(cmakeVersion), "bin", "cmake"); + case 1: + // Don't need to add anything to path if already available via system + return ""; + case 3: + // normalize path returned by the os selector to posix path for the settings json + // and cross platform compatibility + return process.platform === "win32" + ? // TODO: maybe use path.sep for split + joinPosix(...cmakePath.split("\\")) + : cmakePath; + default: + void window.showErrorMessage("Unknown CMake version selected."); + + return; + } +} + +async function checkNinja( + ninjaMode: number, + ninjaVersion: string, + ninjaPath: string +): Promise { + let progLastState = 0; + let installedSuccessfully = false; + + switch (ninjaMode) { + case 4: + case 2: + installedSuccessfully = await window.withProgress( + { + location: ProgressLocation.Notification, + title: "Downloading and installing Ninja", + cancellable: false, + }, + async progress => { + const result = await downloadAndInstallNinja( + ninjaVersion, + (prog: GotProgress) => { + const per = prog.percent * 100; + progress.report({ + increment: per - progLastState, + }); + progLastState = per; + } + ); + + if (!result) { + progress.report({ + message: "Failed", + increment: 100, + }); + + return false; + } + + progress.report({ + message: "Success", + increment: 100, + }); + + return true; + } + ); + + if (!installedSuccessfully) { + return; + } + + return joinPosix(buildNinjaPath(ninjaVersion), "bin", "ninja"); + case 1: + // Don't need to add anything to path if already available via system + return ""; + case 3: + // normalize path returned by the os selector to posix path for the settings json + // and cross platform compatibility + return process.platform === "win32" + ? // TODO: maybe use path.sep for split + joinPosix(...ninjaPath.split("\\")) + : ninjaPath; + default: + void window.showErrorMessage("Unknown Ninja version selected."); + + return; + } +} + +async function checkPicotool(latestVb: VersionBundle): Promise { + let progLastState = 0; + + return window.withProgress( + { + location: ProgressLocation.Notification, + title: "Downloading and installing Picotool", + cancellable: false, + }, + async progress => { + if ( + await downloadAndInstallPicotool( + latestVb.picotool, + (prog: GotProgress) => { + const per = prog.percent * 100; + progress.report({ + increment: per - progLastState, + }); + progLastState = per; + } + ) + ) { + progress.report({ + message: "Success", + increment: 100, + }); + + return true; + } else { + progress.report({ + message: "Failed", + increment: 100, + }); + + return false; + } + } + ); +} + +async function checkSdk( + latestVb: [string, VersionBundle], + python3Path: string +): Promise { + return window.withProgress( + { + location: ProgressLocation.Notification, + title: "Downloading and installing Pico SDK", + cancellable: false, + }, + async progress => { + const result = await downloadAndInstallSDK( + latestVb[0], + SDK_REPOSITORY_URL, + python3Path + ); + + if (!result) { + Logger.error( + LoggerSource.zephyrSetup, + "Failed to download and install the Pico SDK." + ); + + progress.report({ + message: "Failed", + increment: 100, + }); + + return false; + } else { + progress.report({ + message: "Success", + increment: 100, + }); + + return true; + } + } + ); +} + +async function checkDtc(): Promise { + return window.withProgress( + { + location: ProgressLocation.Notification, + title: "Downloading and installing DTC", + cancellable: false, + }, + async progress => { + const result = await downloadAndInstallArchive( + WINDOWS_X86_DTC_DOWNLOAD_URL, + buildDtcPath(CURRENT_DTC_VERSION), + "dtc-msys2-1.6.1-x86_64.zip", + "dtc" + ); + + if (!result) { + Logger.error( + LoggerSource.zephyrSetup, + "Failed to download and install DTC." + ); + + progress.report({ + message: "Failed", + increment: 100, + }); + + return false; + } else { + progress.report({ + message: "Success", + increment: 100, + }); + + return true; + } + } + ); +} + +async function checkGperf(): Promise { + return window.withProgress( + { + location: ProgressLocation.Notification, + title: "Downloading and installing gperf", + cancellable: false, + }, + async progress => { + const result = await downloadAndInstallArchive( + WINDOWS_X86_GPERF_DOWNLOAD_URL, + buildGperfPath(CURRENT_GPERF_VERSION), + `gperf-${CURRENT_GPERF_VERSION}-win64_x64.zip`, + "gperf" + ); + + if (!result) { + progress.report({ + message: "Failed", + increment: 100, + }); + + return false; + } else { + progress.report({ + message: "Success", + increment: 100, + }); + + return true; + } + } + ); +} + +async function checkWget(): Promise { + return window.withProgress( + { + location: ProgressLocation.Notification, + title: "Downloading and installing wget", + cancellable: false, + }, + async progress2 => { + const result = await downloadAndInstallArchive( + WINDOWS_X86_WGET_DOWNLOAD_URL, + buildWgetPath(CURRENT_WGET_VERSION), + `wget-${CURRENT_WGET_VERSION}-win64.zip`, + "wget" + ); + + if (result) { + progress2.report({ + message: "Successfully downloaded and installed wget.", + increment: 100, + }); + + return true; + } else { + progress2.report({ + message: "Failed", + increment: 100, + }); + + return false; + } + } + ); +} + +async function check7Zip(): Promise { + return window.withProgress( + { + location: ProgressLocation.Notification, + title: "Downloading and installing 7-Zip", + cancellable: false, + }, + async progress => { + Logger.info(LoggerSource.zephyrSetup, "Installing 7-Zip..."); + const installDir = build7ZipPathWin32(); + + if (await vsExists(installDir)) { + const installDirContents = await workspace.fs.readDirectory( + Uri.file(installDir) + ); + + if (installDirContents.length !== 0) { + Logger.info(LoggerSource.zephyrSetup, "7-Zip is already installed."); + + progress.report({ + message: "7-Zip already installed.", + increment: 100, + }); + + return true; + } + } else { + await workspace.fs.createDirectory(Uri.file(installDir)); + } + + const licenseURL = new URL(LICENSE_URL_7ZIP); + const licenseTarget = join(installDir, "License.txt"); + const licenseResult = await downloadFileGot(licenseURL, licenseTarget); + if (!licenseResult) { + progress.report({ + message: "Failed", + increment: 100, + }); + + return false; + } + + const downloadURL = new URL(WINDOWS_X86_7ZIP_DOWNLOAD_URL); + // rename latest 7zr.exe to 7z.exe for compaibility with Zephyr installer script + const downloadTarget = join(installDir, "7z.exe"); + const result = await downloadFileGot(downloadURL, downloadTarget); + + if (!result) { + progress.report({ + message: "Failed", + increment: 100, + }); + + return false; + } + + progress.report({ + message: "Success", + increment: 100, + }); + + return true; + } + ); +} + +async function checkMacosLinuxDeps(isWindows: boolean): Promise { + if (isWindows) { + return true; + } + + const wget = await which("wget", { nothrow: true }); + if (!wget) { + void window.showErrorMessage( + "wget not found in PATH. Please install wget " + + "and make sure it is available in PATH." + ); + + return false; + } + const dtc = await which("dtc", { nothrow: true }); + if (!dtc) { + void window.showErrorMessage( + "dtc (Device Tree Compiler) not found in PATH. Please install dtc " + + "and make sure it is available in PATH." + ); + + return false; + } + const gperf = await which("gperf", { nothrow: true }); + if (!gperf) { + void window.showErrorMessage( + "gperf not found in PATH. Please install gperf " + + "and make sure it is available in PATH." + ); + + return false; + } + + return true; +} + +async function checkWindowsDeps(isWindows: boolean): Promise { + if (!isWindows) { + return true; + } + + let installedSuccessfully = await checkDtc(); + if (!installedSuccessfully) { + void window.showErrorMessage( + "Failed to install DTC. Cannot continue Zephyr setup." + ); + + return false; + } + + installedSuccessfully = await checkGperf(); + if (!installedSuccessfully) { + void window.showErrorMessage( + "Failed to install gperf. Cannot continue Zephyr setup." + ); + + return false; + } + + installedSuccessfully = await checkWget(); + if (!installedSuccessfully) { + void window.showErrorMessage( + "Failed to install wget. Cannot continue Zephyr setup." + ); + + return false; + } + + installedSuccessfully = await check7Zip(); + if (!installedSuccessfully) { + void window.showErrorMessage( + "Failed to install 7-Zip. Cannot continue Zephyr setup." + ); + + return false; + } + + return true; +} + +async function checkOpenOCD(): Promise { + let progLastState = 0; + + return window.withProgress( + { + location: ProgressLocation.Notification, + title: "Downloading and installing OpenOCD", + cancellable: false, + }, + async progress => { + const result = await downloadAndInstallOpenOCD( + OPENOCD_VERSION, + (prog: GotProgress) => { + const per = prog.percent * 100; + progress.report({ + increment: per - progLastState, + }); + progLastState = per; + } + ); + + if (result) { + progress.report({ + message: "Success", + increment: 100, + }); + + return true; + } else { + progress.report({ + message: "Failed", + increment: 100, + }); + + return false; + } + } + ); +} + +export async function setupZephyr( + data: ZephyrSetupValue +): Promise { + Logger.info(LoggerSource.zephyrSetup, "Setting up Zephyr..."); + + const latestVb = await new VersionBundlesLoader(data.extUri).getLatest(); + if (latestVb === undefined) { + Logger.error( + LoggerSource.zephyrSetup, + "Failed to get latest version bundles." + ); + void window.showErrorMessage( + "Failed to get latest version bundles. Cannot continue Zephyr setup." + ); + + return; + } + + const output: ZephyrSetupOutputs = { + cmakeExecutable: "", + ninjaExecutable: "", + gitPath: "", + latestVb, + }; + + let isWindows = false; + const endResult: boolean = await window.withProgress( + { + location: ProgressLocation.Notification, + title: "Setting up Zephyr Toolchain", + }, + async progress => { + let installedSuccessfully = true; + + const gitPath = await checkGit(); + if (gitPath === undefined) { + progress.report({ + message: "Failed", + increment: 100, + }); + + return false; + } + output.gitPath = gitPath; + + // Handle CMake install + const cmakePath = await checkCmake( + data.cmakeMode, + data.cmakeVersion, + data.cmakePath + ); + if (cmakePath === undefined) { + progress.report({ + message: "Failed", + increment: 100, + }); + void window.showErrorMessage( + "Failed to install or find CMake. Cannot continue Zephyr setup." + ); + + return false; + } + output.cmakeExecutable = cmakePath; + + const ninjaPath = await checkNinja( + data.ninjaMode, + data.ninjaVersion, + data.ninjaPath + ); + if (ninjaPath === undefined) { + progress.report({ + message: "Failed", + increment: 100, + }); + void window.showErrorMessage( + "Failed to install or find Ninja. Cannot continue Zephyr setup." + ); + + return false; + } + output.ninjaExecutable = ninjaPath; + + installedSuccessfully = await checkPicotool(latestVb[1]); + if (!installedSuccessfully) { + progress.report({ + message: "Failed", + increment: 100, + }); + void window.showErrorMessage( + "Failed to install Picotool. Cannot continue Zephyr setup." + ); + + return false; + } + + // install python (if necessary) + const python3Path = await findPython(); + if (!python3Path) { + progress.report({ + message: "Failed", + increment: 100, + }); + Logger.error( + LoggerSource.zephyrSetup, + "Failed to find Python3 executable." + ); + showPythonNotFoundError(); + + return false; + } + + // required for svd files + const sdk = await checkSdk(latestVb, python3Path); + if (!sdk) { + progress.report({ + message: "Failed", + increment: 100, + }); + void window.showErrorMessage( + "Failed to install Pico SDK. Cannot continue Zephyr setup." + ); + + return false; + } + + isWindows = process.platform === "win32"; + + installedSuccessfully = await checkWindowsDeps(isWindows); + if (!installedSuccessfully) { + progress.report({ + message: "Failed", + increment: 100, + }); + + return false; + } + + installedSuccessfully = await checkMacosLinuxDeps(isWindows); + if (!installedSuccessfully) { + progress.report({ + message: "Failed", + increment: 100, + }); + + return false; + } + + installedSuccessfully = await checkOpenOCD(); + if (!installedSuccessfully) { + progress.report({ + message: "Failed", + increment: 100, + }); + void window.showErrorMessage( + "Failed to install OpenOCD. Cannot continue Zephyr setup." + ); + + return false; + } + + const pythonExe = python3Path?.replace( + HOME_VAR, + homedir().replaceAll("\\", "/") + ); + const customEnv = generateCustomEnv( + isWindows, + latestVb[1], + output.cmakeExecutable, + pythonExe + ); + + const zephyrWorkspaceDirectory = buildZephyrWorkspacePath(); + const zephyrManifestDir: string = joinPosix( + zephyrWorkspaceDirectory, + "manifest" + ); + const zephyrManifestFile: string = joinPosix( + zephyrManifestDir, + "west.yml" + ); + + await workspace.fs.writeFile( + Uri.file(zephyrManifestFile), + Buffer.from(zephyrManifestContent) + ); + + const createVenvCommandVenv: string = `"${pythonExe}" -m venv venv`; + const createVenvCommandVirtualenv: string = + `"${pythonExe}" -m ` + "virtualenv venv"; + + // Create a Zephyr workspace, copy the west manifest in and initialise the workspace + await workspace.fs.createDirectory(Uri.file(zephyrWorkspaceDirectory)); + + // Generic result to get value from runCommand calls + let result: number | null; + const venvPython: string = joinPosix( + zephyrWorkspaceDirectory, + "venv", + process.platform === "win32" ? "Scripts" : "bin", + process.platform === "win32" ? "python.exe" : "python" + ); + + installedSuccessfully = await window.withProgress( + { + location: ProgressLocation.Notification, + title: "Setup Python virtual environment for Zephyr", + cancellable: false, + }, + async progress2 => { + if (existsSync(venvPython)) { + Logger.info( + LoggerSource.zephyrSetup, + "Existing Python virtual environment for Zephyr found." + ); + + return true; + } + + result = await _runCommand(createVenvCommandVenv, { + cwd: zephyrWorkspaceDirectory, + windowsHide: true, + env: customEnv, + }); + if (result !== 0) { + Logger.warn( + LoggerSource.zephyrSetup, + "Could not create virtual environment with venv," + + "trying with virtualenv..." + ); + + result = await _runCommand(createVenvCommandVirtualenv, { + cwd: zephyrWorkspaceDirectory, + windowsHide: true, + env: customEnv, + }); + + if (result !== 0) { + progress2.report({ + message: "Failed", + increment: 100, + }); + + return false; + } + } + + progress2.report({ + message: "Success", + increment: 100, + }); + + Logger.info( + LoggerSource.zephyrSetup, + "Zephyr Python virtual environment created." + ); + + return true; + } + ); + if (!installedSuccessfully) { + progress.report({ + message: "Failed", + increment: 100, + }); + + return false; + } + + const venvPythonCommand: string = joinPosix( + process.env.ComSpec === "powershell.exe" ? "&" : "", + venvPython + ); + + installedSuccessfully = await window.withProgress( + { + location: ProgressLocation.Notification, + title: "Install Zephyr Python dependencies", + cancellable: false, + }, + async progress2 => { + const installWestCommand: string = + `${venvPythonCommand} -m pip install ` + "west pyelftools"; + + const installExitCode = await _runCommand(installWestCommand, { + cwd: zephyrWorkspaceDirectory, + windowsHide: true, + env: customEnv, + }); + if (installExitCode === 0) { + progress2.report({ + message: "Success", + increment: 100, + }); + + return true; + } else { + progress2.report({ + message: "Failed", + increment: 100, + }); + + return false; + } + } + ); + if (!installedSuccessfully) { + progress.report({ + message: "Failed", + increment: 100, + }); + + return false; + } + + const westExe: string = joinPosix( + zephyrWorkspaceDirectory, + "venv", + process.platform === "win32" ? "Scripts" : "bin", + process.platform === "win32" ? "west.exe" : "west" + ); + + installedSuccessfully = await window.withProgress( + { + location: ProgressLocation.Notification, + title: "Setting up West workspace", + cancellable: false, + }, + async progress2 => { + const zephyrWorkspaceFiles = await workspace.fs.readDirectory( + Uri.file(zephyrWorkspaceDirectory) + ); + + const westAlreadyExists = zephyrWorkspaceFiles.find( + x => x[0] === ".west" + ); + if (westAlreadyExists) { + Logger.info( + LoggerSource.zephyrSetup, + "West workspace already initialised." + ); + } else { + Logger.info( + LoggerSource.zephyrSetup, + "No West workspace found. Initialising..." + ); + + const westInitCommand: string = `"${westExe}" init -l manifest`; + result = await _runCommand(westInitCommand, { + cwd: zephyrWorkspaceDirectory, + windowsHide: true, + env: customEnv, + }); + + Logger.info( + LoggerSource.zephyrSetup, + `West workspace initialization ended with exit code ${result}.` + ); + + if (result !== 0) { + progress2.report({ + message: "Failed", + increment: 100, + }); + + return false; + } + } + + const westUpdateCommand: string = `"${westExe}" update`; + result = await _runCommand(westUpdateCommand, { + cwd: zephyrWorkspaceDirectory, + windowsHide: true, + env: customEnv, + }); + + if (result === 0) { + progress2.report({ + message: "Success", + increment: 100, + }); + + return true; + } else { + progress2.report({ + message: "Failed", + increment: 100, + }); + + return false; + } + } + ); + if (!installedSuccessfully) { + progress.report({ + message: "Failed", + increment: 100, + }); + + return false; + } + + const zephyrExportCommand: string = `"${westExe}" zephyr-export`; + Logger.info(LoggerSource.zephyrSetup, "Exporting Zephyr CMake Files..."); + + // TODO: maybe progress + result = await _runCommand(zephyrExportCommand, { + cwd: zephyrWorkspaceDirectory, + windowsHide: true, + env: customEnv, + }); + if (result !== 0) { + Logger.error( + LoggerSource.zephyrSetup, + "Error exporting Zephyr CMake files." + ); + progress.report({ + message: "Failed", + increment: 100, + }); + void window.showErrorMessage("Error exporting Zephyr CMake files."); + + return false; + } + + installedSuccessfully = await window.withProgress( + { + location: ProgressLocation.Notification, + title: "Installing West Python dependencies", + cancellable: false, + }, + async progress2 => { + const westPipPackagesCommand: string = + `"${westExe}" packages ` + "pip --install"; + + result = await _runCommand(westPipPackagesCommand, { + cwd: zephyrWorkspaceDirectory, + windowsHide: true, + env: customEnv, + }); + + if (result === 0) { + Logger.debug( + LoggerSource.zephyrSetup, + "West Python dependencies installed." + ); + progress2.report({ + message: "Success", + increment: 100, + }); + + return true; + } else { + Logger.error( + LoggerSource.zephyrSetup, + "Error installing West Python dependencies." + ); + progress2.report({ + message: "Failed", + increment: 100, + }); + + return false; + } + } + ); + if (!installedSuccessfully) { + progress.report({ + message: "Failed", + increment: 100, + }); + + return false; + } + + installedSuccessfully = await window.withProgress( + { + location: ProgressLocation.Notification, + title: "Fetching Zephyr binary blobs", + cancellable: false, + }, + async progress2 => { + const westBlobsFetchCommand: string = + `"${westExe}" blobs fetch ` + "hal_infineon"; + + result = await _runCommand(westBlobsFetchCommand, { + cwd: zephyrWorkspaceDirectory, + windowsHide: true, + env: customEnv, + }); + + if (result === 0) { + progress2.report({ + message: "Success", + increment: 100, + }); + + return true; + } else { + progress2.report({ + message: "Failed", + increment: 100, + }); + + return false; + } + } + ); + if (!installedSuccessfully) { + progress.report({ + message: "Failed", + increment: 100, + }); + + return false; + } + + installedSuccessfully = await window.withProgress( + { + location: ProgressLocation.Notification, + title: "Installing Zephyr SDK", + cancellable: false, + }, + async progress2 => { + // was -b ${zephyrWorkspaceDirectory} which results in zephyr-sdk- in it + const westInstallSDKCommand: string = + `"${westExe}" sdk install ` + + `-t arm-zephyr-eabi -d "${zephyrWorkspaceDirectory}/zephyr-sdk"`; + + result = await _runCommand(westInstallSDKCommand, { + cwd: zephyrWorkspaceDirectory, + windowsHide: true, + env: customEnv, + }); + + if (result === 0) { + progress2.report({ + message: "Success", + increment: 100, + }); + + return true; + } else { + progress2.report({ + message: "Failed", + increment: 100, + }); + + return false; + } + } + ); + if (!installedSuccessfully) { + progress.report({ + message: "Failed", + increment: 100, + }); + + return false; + } + + progress.report({ + message: "Complete", + increment: 100, + }); + + return true; + } + ); + + if (endResult) { + Logger.info(LoggerSource.zephyrSetup, "Zephyr setup complete."); + + return output; + } else { + Logger.error(LoggerSource.zephyrSetup, "Zephyr setup failed."); + + return undefined; + } +} diff --git a/src/utils/sharedConstants.mts b/src/utils/sharedConstants.mts index eddd5f2e..0ab60d2d 100644 --- a/src/utils/sharedConstants.mts +++ b/src/utils/sharedConstants.mts @@ -1,8 +1,26 @@ export const WINDOWS_X86_PYTHON_DOWNLOAD_URL = - "https://www.python.org/ftp/python/3.12.6/python-3.12.6-embed-amd64.zip"; + "https://www.python.org/ftp/python/3.13.7/python-3.13.7-embed-amd64.zip"; export const WINDOWS_ARM64_PYTHON_DOWNLOAD_URL = - "https://www.python.org/ftp/python/3.12.6/python-3.12.6-embed-arm64.zip"; -export const CURRENT_PYTHON_VERSION = "3.12.6"; + "https://www.python.org/ftp/python/3.13.7/python-3.13.7-embed-arm64.zip"; +export const CURRENT_PYTHON_VERSION = "3.13.7"; export const CURRENT_DATA_VERSION = "0.18.0"; export const OPENOCD_VERSION = "0.12.0+dev"; + +export const WINDOWS_X86_DTC_DOWNLOAD_URL = + "https://github.com/oss-winget/oss-winget-storage/raw/" + + "96ea1b934342f45628a488d3b50d0c37cf06012c/packages/dtc/" + + "1.6.1/dtc-msys2-1.6.1-x86_64.zip"; +export const CURRENT_DTC_VERSION = "1.6.1"; +export const WINDOWS_X86_GPERF_DOWNLOAD_URL = + "https://github.com/oss-winget/oss-winget-storage/raw/" + + "d033d1c0fb054de32043af1d4d3be71b91c38221/packages/" + + "gperf/3.1/gperf-3.1-win64_x64.zip"; +export const CURRENT_GPERF_VERSION = "3.1"; +export const WINDOWS_X86_WGET_DOWNLOAD_URL = + "https:///eternallybored.org/misc/wget/releases/wget-1.21.4-win64.zip"; +export const CURRENT_WGET_VERSION = "1.21.4"; +export const LICENSE_URL_7ZIP = "https://7-zip.org/license.txt"; +export const WINDOWS_X86_7ZIP_DOWNLOAD_URL = "https://www.7-zip.org/a/7zr.exe"; + +export const CMAKELISTS_ZEPHYR_HEADER = "#pico-zephyr-project"; diff --git a/src/utils/versionBundles.mts b/src/utils/versionBundles.mts index b7ff3653..6f0ae60a 100644 --- a/src/utils/versionBundles.mts +++ b/src/utils/versionBundles.mts @@ -16,7 +16,7 @@ export interface VersionBundle { picotool: string; toolchain: string; riscvToolchain: string; - modifiers: { [triple: string] : {[tool: string]: string}}; + modifiers: { [triple: string]: { [tool: string]: string } }; } export interface VersionBundles { @@ -108,20 +108,20 @@ export default class VersionBundlesLoader { const platformDouble = `${process.platform}_${process.arch}`; if (modifiers[platformDouble] !== undefined) { chosenBundle.cmake = - modifiers[platformDouble]["cmake"] ?? chosenBundle.cmake + modifiers[platformDouble]["cmake"] ?? chosenBundle.cmake; chosenBundle.ninja = - modifiers[platformDouble]["ninja"] ?? chosenBundle.ninja + modifiers[platformDouble]["ninja"] ?? chosenBundle.ninja; chosenBundle.picotool = - modifiers[platformDouble]["picotool"] ?? chosenBundle.picotool + modifiers[platformDouble]["picotool"] ?? chosenBundle.picotool; chosenBundle.toolchain = - modifiers[platformDouble]["toolchain"] ?? chosenBundle.toolchain + modifiers[platformDouble]["toolchain"] ?? chosenBundle.toolchain; chosenBundle.riscvToolchain = modifiers[platformDouble]["riscvToolchain"] ?? - chosenBundle.riscvToolchain + chosenBundle.riscvToolchain; } } } diff --git a/src/utils/vsHelpers.mts b/src/utils/vsHelpers.mts new file mode 100644 index 00000000..014bc2c4 --- /dev/null +++ b/src/utils/vsHelpers.mts @@ -0,0 +1,15 @@ +import { FileSystemError, Uri, workspace } from "vscode"; + +export async function vsExists(path: string): Promise { + try { + await workspace.fs.stat(Uri.file(path)); + + return true; + } catch (err) { + if (err instanceof FileSystemError && err.code === "FileNotFound") { + return false; + } + // rethrow unexpected errors + throw err; + } +} diff --git a/src/webview/activityBar.mts b/src/webview/activityBar.mts index c64602bf..77fd49b8 100644 --- a/src/webview/activityBar.mts +++ b/src/webview/activityBar.mts @@ -45,6 +45,7 @@ const DOCUMENTATION_COMMANDS_PARENT_LABEL = "Documentation"; const NEW_C_CPP_PROJECT_LABEL = "New C/C++ Project"; const NEW_MICROPYTHON_PROJECT_LABEL = "New MicroPython Project"; const NEW_RUST_PROJECT_LABEL = "New Rust Project"; +const NEW_ZEPHYR_PROJECT_LABEL = "New Zephyr Project"; const IMPORT_PROJECT_LABEL = "Import Project"; const EXAMPLE_PROJECT_LABEL = "New Project From Example"; const SWITCH_SDK_LABEL = "Switch SDK"; @@ -111,6 +112,9 @@ export class PicoProjectActivityBar case NEW_RUST_PROJECT_LABEL: element.iconPath = new ThemeIcon("file-directory-create"); break; + case NEW_ZEPHYR_PROJECT_LABEL: + element.iconPath = new ThemeIcon("file-directory-create"); + break; case IMPORT_PROJECT_LABEL: // alt. "repo-pull" element.iconPath = new ThemeIcon("repo-clone"); @@ -220,6 +224,15 @@ export class PicoProjectActivityBar arguments: [ProjectLang.rust], } ), + new QuickAccessCommand( + NEW_ZEPHYR_PROJECT_LABEL, + TreeItemCollapsibleState.None, + { + command: `${extensionName}.${NewProjectCommand.id}`, + title: NEW_ZEPHYR_PROJECT_LABEL, + arguments: [ProjectLang.zephyr], + } + ), new QuickAccessCommand( IMPORT_PROJECT_LABEL, TreeItemCollapsibleState.None, diff --git a/src/webview/newProjectPanel.mts b/src/webview/newProjectPanel.mts index dc14cff7..bbf47484 100644 --- a/src/webview/newProjectPanel.mts +++ b/src/webview/newProjectPanel.mts @@ -62,6 +62,7 @@ import { unknownErrorToString } from "../utils/errorHelper.mjs"; import type { Progress as GotProgress } from "got"; import findPython, { showPythonNotFoundError } from "../utils/pythonHelper.mjs"; import { OPENOCD_VERSION } from "../utils/sharedConstants.mjs"; +import { BoardType } from "./sharedEnums.mjs"; export const NINJA_AUTO_INSTALL_DISABLED = false; // process.platform === "linux" && process.arch === "arm64"; @@ -123,14 +124,6 @@ export interface WebviewMessage { key?: string; } -enum BoardType { - pico = "pico", - picoW = "pico_w", - pico2 = "pico2", - pico2W = "pico2_w", - other = "other", -} - enum ConsoleOption { consoleOverUART = "Console over UART", consoleOverUSB = "Console over USB (disables other USB use)", @@ -184,7 +177,7 @@ async function enumToBoard(e: BoardType, sdkPath: string): Promise { readdirSync(`${sdkPath}/src/boards/include/boards`) .filter((file: string) => file.endsWith(".h")) .forEach((file: string) => { - quickPickItems.push(file.slice(0, -2)); // remove .h + quickPickItems.push(file.slice(0, -2)); // remove .h }); // show quick pick for board type diff --git a/src/webview/newZephyrProjectPanel.mts b/src/webview/newZephyrProjectPanel.mts new file mode 100644 index 00000000..9451708e --- /dev/null +++ b/src/webview/newZephyrProjectPanel.mts @@ -0,0 +1,935 @@ +/* eslint-disable max-len */ +import type { Webview, Progress } from "vscode"; +import { + Uri, + ViewColumn, + window, + type WebviewPanel, + type Disposable, + ColorThemeKind, + workspace, + ProgressLocation, + commands, +} from "vscode"; +import Settings from "../settings.mjs"; +import Logger from "../logger.mjs"; +import { compare } from "../utils/semverUtil.mjs"; +import type { WebviewMessage } from "./newProjectPanel.mjs"; +import { + getNonce, + getProjectFolderDialogOptions, + getWebviewOptions, +} from "./newProjectPanel.mjs"; +import { existsSync } from "fs"; +import { join } from "path"; +import { PythonExtension } from "@vscode/python-extension"; +import { unknownErrorToString } from "../utils/errorHelper.mjs"; +import { setupZephyr } from "../utils/setupZephyr.mjs"; +import { getCmakeReleases, getNinjaReleases } from "../utils/githubREST.mjs"; +import { getSystemCmakeVersion } from "../utils/cmakeUtil.mjs"; +import { generateZephyrProject } from "../utils/projectGeneration/projectZephyr.mjs"; +import { BoardType, type ZephyrSubmitMessageValue } from "./sharedEnums.mjs"; +import { getSystemNinjaVersion } from "../utils/ninjaUtil.mjs"; + +export class NewZephyrProjectPanel { + public static currentPanel: NewZephyrProjectPanel | undefined; + + public static readonly viewType = "newZephyrProject"; + + private readonly _panel: WebviewPanel; + private readonly _extensionUri: Uri; + private readonly _settings: Settings; + private readonly _logger: Logger = new Logger("NewZephyrProjectPanel"); + private _disposables: Disposable[] = []; + + private _projectRoot?: Uri; + private _pythonExtensionApi?: PythonExtension; + private _systemCmakeVersion: string | undefined; + private _systemNinjaVersion: string | undefined; + + public static createOrShow(extensionUri: Uri, projectUri?: Uri): void { + const column = window.activeTextEditor + ? window.activeTextEditor.viewColumn + : undefined; + + if (NewZephyrProjectPanel.currentPanel) { + NewZephyrProjectPanel.currentPanel._panel.reveal(column); + // update already exiting panel with new project root + if (projectUri) { + NewZephyrProjectPanel.currentPanel._projectRoot = projectUri; + // update webview + void NewZephyrProjectPanel.currentPanel._panel.webview.postMessage({ + command: "changeLocation", + value: projectUri?.fsPath, + }); + } + + return; + } + + const panel = window.createWebviewPanel( + NewZephyrProjectPanel.viewType, + "New Zephyr Pico Project", + column || ViewColumn.One, + getWebviewOptions(extensionUri) + ); + + const settings = Settings.getInstance(); + if (!settings) { + panel.dispose(); + + void window + .showErrorMessage( + "Failed to load settings. Please restart VS Code or reload the window.", + "Reload Window" + ) + .then(selected => { + if (selected === "Reload Window") { + void commands.executeCommand("workbench.action.reloadWindow"); + } + }); + + return; + } + + NewZephyrProjectPanel.currentPanel = new NewZephyrProjectPanel( + panel, + settings, + extensionUri, + projectUri + ); + } + + public static revive(panel: WebviewPanel, extensionUri: Uri): void { + const settings = Settings.getInstance(); + if (settings === undefined) { + // TODO: maybe add restart button + void window.showErrorMessage( + "Failed to load settings. Please restart VSCode." + ); + + return; + } + + NewZephyrProjectPanel.currentPanel = new NewZephyrProjectPanel( + panel, + settings, + extensionUri + ); + } + + private constructor( + panel: WebviewPanel, + settings: Settings, + extensionUri: Uri, + projectUri?: Uri + ) { + this._panel = panel; + this._extensionUri = extensionUri; + this._settings = settings; + + this._projectRoot = projectUri ?? this._settings.getLastProjectRoot(); + + void this._update(); + + this._panel.onDidDispose(() => this.dispose(), null, this._disposables); + + // Update the content based on view changes + this._panel.onDidChangeViewState( + async () => { + if (this._panel.visible) { + await this._update(); + } + }, + null, + this._disposables + ); + + workspace.onDidChangeConfiguration( + async () => { + await this._updateTheme(); + }, + null, + this._disposables + ); + + this._panel.webview.onDidReceiveMessage( + async (message: WebviewMessage) => { + switch (message.command) { + case "changeLocation": + { + const newLoc = await window.showOpenDialog( + getProjectFolderDialogOptions(this._projectRoot, false) + ); + + if (newLoc && newLoc[0]) { + // overwrite preview folderUri + this._projectRoot = newLoc[0]; + await this._settings.setLastProjectRoot(newLoc[0]); + + // update webview + await this._panel.webview.postMessage({ + command: "changeLocation", + value: newLoc[0].fsPath, + }); + } + } + break; + case "cancel": + this.dispose(); + break; + case "error": + void window.showErrorMessage(message.value as string); + break; + case "submit": + { + const data = message.value as ZephyrSubmitMessageValue; + + if ( + this._projectRoot === undefined || + this._projectRoot.fsPath === "" + ) { + void window.showErrorMessage( + "No project root selected. Please select a project root." + ); + await this._panel.webview.postMessage({ + command: "submitDenied", + }); + + return; + } + + if ( + data.projectName === undefined || + data.projectName.length === 0 + ) { + void window.showWarningMessage( + "The project name is empty. Please enter a project name." + ); + await this._panel.webview.postMessage({ + command: "submitDenied", + }); + + return; + } + + // check if projectRoot/projectName folder already exists + if ( + existsSync(join(this._projectRoot.fsPath, data.projectName)) + ) { + void window.showErrorMessage( + "Project already exists. " + + "Please select a different project name or root." + ); + await this._panel.webview.postMessage({ + command: "submitDenied", + }); + + return; + } + + // close panel before generating project + this.dispose(); + + await window.withProgress( + { + location: ProgressLocation.Notification, + title: `Generating Zephyr project ${ + data.projectName ?? "undefined" + } in ${this._projectRoot?.fsPath}...`, + }, + async progress => + this._generateProjectOperation(progress, data, message) + ); + } + break; + } + }, + null, + this._disposables + ); + + if (projectUri !== undefined) { + // update webview + void this._panel.webview.postMessage({ + command: "changeLocation", + value: projectUri.fsPath, + }); + } + } + + private async _generateProjectOperation( + progress: Progress<{ message?: string; increment?: number }>, + data: ZephyrSubmitMessageValue, + message: WebviewMessage + ): Promise { + const projectPath = this._projectRoot?.fsPath ?? ""; + + if ( + typeof message.value !== "object" || + message.value === null || + projectPath.length === 0 + ) { + void window.showErrorMessage( + "Failed to generate Zephyrproject. " + + "Please try again and check your settings." + ); + + return; + } + + if (data.cmakeMode === 0) { + progress.report({ + message: "Failed", + increment: 100, + }); + void window.showErrorMessage("Unknown cmake version selected."); + + return; + } else if (data.ninjaMode === 0) { + progress.report({ + message: "Failed", + increment: 100, + }); + void window.showErrorMessage("Unknown ninja version selected."); + + return; + } + + // Setup Zephyr before doing anything else + const zephyrSetupOutputs = await setupZephyr({ + cmakeMode: data.cmakeMode, + cmakePath: data.cmakePath, + cmakeVersion: data.cmakeVersion, + extUri: this._extensionUri, + ninjaMode: data.ninjaMode, + ninjaPath: data.ninjaPath, + ninjaVersion: data.ninjaVersion, + }); + + if (zephyrSetupOutputs === undefined) { + progress.report({ + message: "Failed", + increment: 100, + }); + + return; + } + + this._logger.info("Generating new Zephyr Project..."); + + const result = await generateZephyrProject( + projectPath, + data.projectName, + zephyrSetupOutputs.latestVb, + zephyrSetupOutputs.ninjaExecutable, + zephyrSetupOutputs.cmakeExecutable, + data + ); + if (!result) { + progress.report({ + message: "Failed", + increment: 100, + }); + void window.showErrorMessage( + "Failed to generate Zephyrproject. " + + "Please try again and check your settings." + ); + + return; + } + + this._logger.info( + `Zephyr Project generated at ${projectPath}/${data.projectName}` + ); + + // Open the folder + void commands.executeCommand( + `vscode.openFolder`, + Uri.file(join(projectPath, data.projectName)), + { + forceNewWindow: (workspace.workspaceFolders?.length ?? 0) > 0, + } + ); + + return; + } + + private async _update(): Promise { + this._panel.title = "New Zephyr Pico Project"; + + this._panel.iconPath = Uri.joinPath( + this._extensionUri, + "web", + "raspberry-128.png" + ); + if (!this._pythonExtensionApi) { + this._pythonExtensionApi = await PythonExtension.api(); + } + const html = await this._getHtmlForWebview(this._panel.webview); + + if (html !== "") { + try { + this._panel.webview.html = html; + } catch (error) { + this._logger.error( + "Failed to set webview html. Webview might have been disposed. Error: ", + unknownErrorToString(error) + ); + // properly dispose panel + this.dispose(); + + return; + } + await this._updateTheme(); + } else { + void window.showErrorMessage( + "Failed to load webview for new Zephyr project" + ); + this.dispose(); + } + } + + private async _updateTheme(): Promise { + try { + await this._panel.webview.postMessage({ + command: "setTheme", + theme: + window.activeColorTheme.kind === ColorThemeKind.Dark || + window.activeColorTheme.kind === ColorThemeKind.HighContrast + ? "dark" + : "light", + }); + } catch (error) { + this._logger.error( + "Failed to update theme in webview. Webview might have been disposed. Error:", + unknownErrorToString(error) + ); + // properly dispose panel + this.dispose(); + } + } + + public dispose(): void { + NewZephyrProjectPanel.currentPanel = undefined; + + this._panel.dispose(); + + while (this._disposables.length) { + const x = this._disposables.pop(); + + if (x) { + x.dispose(); + } + } + } + + private async _getHtmlForWebview(webview: Webview): Promise { + const mainScriptUri = webview.asWebviewUri( + Uri.joinPath(this._extensionUri, "web", "zephyr", "main.js") + ); + + const mainStyleUri = webview.asWebviewUri( + Uri.joinPath(this._extensionUri, "web", "main.css") + ); + + const tailwindcssScriptUri = webview.asWebviewUri( + Uri.joinPath(this._extensionUri, "web", "tailwindcss-3_3_5.js") + ); + + // images + const navHeaderSvgUri = webview.asWebviewUri( + Uri.joinPath(this._extensionUri, "web", "raspberrypi-nav-header.svg") + ); + + const navHeaderDarkSvgUri = webview.asWebviewUri( + Uri.joinPath(this._extensionUri, "web", "raspberrypi-nav-header-dark.svg") + ); + + // TODO: add support for onDidChangeActiveEnvironment and filter envs that don't directly point + // to an executable + //const environments = this._pythonExtensionApi?.environments; + //const knownEnvironments = environments?.known; + //const activeEnv = environments?.getActiveEnvironmentPath(); + + let ninjasHtml = ""; + const ninjaReleases = await getNinjaReleases(); + ninjaReleases + .sort((a, b) => compare(b.replace("v", ""), a.replace("v", ""))) + .forEach(ninja => { + ninjasHtml += ``; + }); + + let cmakesHtml = ""; + const cmakeReleases = await getCmakeReleases(); + cmakeReleases.forEach(cmake => { + cmakesHtml += ``; + }); + + // TODO: check python version, workaround, only allow python3 commands on unix + //const isPythonSystemAvailable = + // (await which("python3", { nothrow: true })) !== null || + // (await which("python", { nothrow: true })) !== null; + + this._systemCmakeVersion = await getSystemCmakeVersion(); + this._systemNinjaVersion = await getSystemNinjaVersion(); + + // Restrict the webview to only load specific scripts + const nonce = getNonce(); + + const defaultTheme = + window.activeColorTheme.kind === ColorThemeKind.Dark || + window.activeColorTheme.kind === ColorThemeKind.HighContrast + ? "dark" + : "light"; + + return ` + + + + + + + + + + New Pico Zephyr Project + + + + + +
+
+ + +
+
+

Basic Settings

+
+
+ +
+
+ + +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

Modules

+

Kconfig options to enable the below modules

+
    +
  • +
    + + +
    +
  • +
  • +
    + + +
    +
  • +
  • +
    + + +
    +
  • +
+
    +
  • +
    + + +
    +
  • +
  • +
    + + +
    +
  • +
  • +
    + + +
    +
  • +
+
+ +
+
+

Stdio support:

+
+
+ + +
+
+ + +
+
+
+ +
+ +
+

+ CMake Version: +

+ +
+ + + + + + + + + + + + + + + + +
+
+ + +
+

+ Ninja Version: +

+ +
+ + + + + + + + + + + + + + + + +
+
+
+
+ +
+ + +
+
+ + + + `; + } +} + +// Python path option for later +//
+//
+// + +// ${ +// knownEnvironments && knownEnvironments.length > 0 +// ? ` +//
+// +// +// +// +//
+// ` +// : "" +// } + +// ${ +// process.platform === "darwin" || +// process.platform === "win32" +// ? ` +// ${ +// isPythonSystemAvailable +// ? `
+// +// +//
` +// : "" +// } + +//
+// +// +// +//
` +// : "" +// } +//
+//
diff --git a/src/webview/sharedEnums.mts b/src/webview/sharedEnums.mts new file mode 100644 index 00000000..d8d19b7e --- /dev/null +++ b/src/webview/sharedEnums.mts @@ -0,0 +1,34 @@ +export enum BoardType { + pico = "pico", + picoW = "pico_w", + pico2 = "pico2", + pico2W = "pico2_w", + other = "other", +} + +export enum ZephyrProjectBase { + simple = "simple", + blinky = "blinky", + wifi = "wifi", +} + +export interface ZephyrSubmitMessageValue { + projectName: string; + pythonMode: number; + pythonPath: string; + console: string; + boardType: BoardType; + spiFeature: boolean; + i2cFeature: boolean; + gpioFeature: boolean; + wifiFeature: boolean; + sensorFeature: boolean; + shellFeature: boolean; + cmakeMode: number; + cmakePath: string; + cmakeVersion: string; + projectBase: ZephyrProjectBase; + ninjaMode: number; + ninjaPath: string; + ninjaVersion: string; +} diff --git a/web/zephyr/main.js b/web/zephyr/main.js new file mode 100644 index 00000000..5e155ab5 --- /dev/null +++ b/web/zephyr/main.js @@ -0,0 +1,572 @@ +"use strict"; + +const CMD_CHANGE_LOCATION = "changeLocation"; +const CMD_SUBMIT = "submit"; +const CMD_CANCEL = "cancel"; +const CMD_SET_THEME = "setTheme"; +const CMD_ERROR = "error"; +const CMD_SUBMIT_DENIED = "submitDenied"; + +var submitted = false; +var previousTemplate = "simple"; +var previousGpioState = false; + +(function () { + const vscode = acquireVsCodeApi(); + + // CMake version selection handling + { + const modeEl = document.getElementById('cmake-mode'); + const systemRow = document.getElementById('cmake-secondary-system'); + const latestRow = document.getElementById('cmake-secondary-latest'); + const selectRow = document.getElementById('cmake-secondary-select'); + const customRow = document.getElementById('cmake-secondary-custom'); + + const fileInput = document.getElementById('cmake-path-executable'); + const fileLabel = document.getElementById('cmake-file-label'); + const fileBox = document.getElementById('cmake-filebox'); + + const latestValEl = document.getElementById('cmake-latest-val'); + if (latestValEl && typeof window.latestCmakeVersion === 'string') { + latestValEl.textContent = `: ${window.latestCmakeVersion}`; + } + + // Update label text when a file is chosen + fileInput?.addEventListener('change', () => { + const f = fileInput.files && fileInput.files[0]; + fileLabel.textContent = f ? f.name : 'No file selected'; + }); + + // Make label keyboard-activatable (Enter/Space) + fileBox?.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + fileInput?.click(); + } + }); + + function toggleSection(el, show) { + if (!el) return; + el.classList.toggle('hidden', !show); + el.querySelectorAll('input, select, button, textarea').forEach(ctrl => { + ctrl.disabled = !show; + ctrl.tabIndex = show ? 0 : -1; + }); + // If this is the custom row, also toggle the label interactivity + const label = el.querySelector('#cmake-filebox'); + if (label) { + label.setAttribute('aria-disabled', String(!show)); + label.classList.toggle('pointer-events-none', !show); + label.classList.toggle('opacity-60', !show); + } + } + + function setMode(mode) { + toggleSection(systemRow, mode === 'system'); + toggleSection(latestRow, mode === 'latest' || mode === 'default'); + toggleSection(selectRow, mode === 'select'); + toggleSection(customRow, mode === 'custom'); + } + + // TODO: add state saving/loading via state.js + // modeEl.value = window.savedCmakeMode ?? modeEl.value; + + modeEl.addEventListener('change', e => setMode(e.target.value)); + setMode(modeEl.value); + } + + // Ninja version selection handling + { + const modeEl = document.getElementById('ninja-mode'); + const systemRow = document.getElementById('ninja-secondary-system'); + const latestRow = document.getElementById('ninja-secondary-latest'); + const selectRow = document.getElementById('ninja-secondary-select'); + const customRow = document.getElementById('ninja-secondary-custom'); + + const fileInput = document.getElementById('ninja-path-executable'); + const fileLabel = document.getElementById('ninja-file-label'); + const fileBox = document.getElementById('ninja-filebox'); + + const latestValEl = document.getElementById('ninja-latest-val'); + if (latestValEl && typeof window.latestNinjaVersion === 'string') { + latestValEl.textContent = `: ${window.latestNinjaVersion}`; + } + // Update label text when a file is chosen + fileInput?.addEventListener('change', () => { + const f = fileInput.files && fileInput.files[0]; + fileLabel.textContent = f ? f.name : 'No file selected'; + }); + + // Make label keyboard-activatable (Enter/Space) + fileBox?.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + fileInput?.click(); + } + }); + + function toggleSection(el, show) { + if (!el) return; + el.classList.toggle('hidden', !show); + el.querySelectorAll('input, select, button, textarea').forEach(ctrl => { + ctrl.disabled = !show; + ctrl.tabIndex = show ? 0 : -1; + }); + // If this is the custom row, also toggle the label interactivity + const label = el.querySelector('#ninja-filebox'); + if (label) { + label.setAttribute('aria-disabled', String(!show)); + label.classList.toggle('pointer-events-none', !show); + label.classList.toggle('opacity-60', !show); + } + } + + function setMode(mode) { + toggleSection(systemRow, mode === 'system'); + toggleSection(latestRow, mode === 'latest' || mode === 'default'); + toggleSection(selectRow, mode === 'select'); + toggleSection(customRow, mode === 'custom'); + } + + // TODO: add state saving/loading via state.js + // modeEl.value = window.savedNinjaMode ?? modeEl.value; + + modeEl.addEventListener('change', e => setMode(e.target.value)); + setMode(modeEl.value); + } + + { + const templateSelector = document.getElementById("sel-template"); + const gpioCheckbox = document.getElementById("gpio-features-cblist"); + if (templateSelector) { + templateSelector.addEventListener("change", function (event) { + try { + const template = templateSelector.value; + + if (gpioCheckbox) { + if (template === "blinky") { + previousGpioState = gpioCheckbox.checked; + gpioCheckbox.checked = true; + gpioCheckbox.disabled = true; + } else if (previousTemplate === "blinky") { + gpioCheckbox.checked = previousGpioState; + gpioCheckbox.disabled = false; + } + } + + previousTemplate = template; + } catch (error) { + console.error("[raspberry-pi-pico - new zephyr pico project] Error handling template change:", error); + } + }); + } + } + + // needed so a element isn't hidden behind the navbar on scroll + const navbarOffsetHeight = document.getElementById("top-navbar").offsetHeight; + + // returns true if project name input is valid + function projectNameFormValidation(projectNameElement) { + if (typeof examples !== "undefined") { + return true; + } + + const projectNameError = document.getElementById("inp-project-name-error"); + const projectName = projectNameElement.value; + + var invalidChars = /[\/:*?"<>| ]/; + // check for reserved names in Windows + var reservedNames = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/i; + if ( + projectName.trim().length == 0 || + invalidChars.test(projectName) || + reservedNames.test(projectName) + ) { + projectNameError.hidden = false; + //projectNameElement.scrollIntoView({ behavior: "smooth" }); + window.scrollTo({ + top: projectNameElement.offsetTop - navbarOffsetHeight, + behavior: "smooth", + }); + + return false; + } + + projectNameError.hidden = true; + return true; + } + + window.changeLocation = () => { + // Send a message back to the extension + vscode.postMessage({ + command: CMD_CHANGE_LOCATION, + value: null, + }); + }; + + window.cancelBtnClick = () => { + // close webview + vscode.postMessage({ + command: CMD_CANCEL, + value: null, + }); + }; + + window.submitBtnClick = () => { + // get all values of inputs + const projectNameElement = document.getElementById("inp-project-name"); + + const projectName = projectNameElement.value; + if ( + projectName !== undefined && + !projectNameFormValidation(projectNameElement) + ) { + submitted = false; + return; + } + + // selected python version + const pythonVersionRadio = document.getElementsByName( + "python-version-radio" + ); + let pythonMode = null; + let pythonPath = null; + for (let i = 0; i < pythonVersionRadio.length; i++) { + if (pythonVersionRadio[i].checked) { + pythonMode = Number(pythonVersionRadio[i].value); + break; + } + } + if (pythonVersionRadio.length == 0) { + // default to python mode 0 == python ext version + pythonMode = 0; + } + + // if python version is null or not a number, smaller than 0 or bigger than 3, set it to 0 + if ( + pythonMode === null || + isNaN(pythonMode) || + pythonMode < 0 || + pythonMode > 3 + ) { + pythonMode = 0; + console.debug("Invalid python version value: " + pythonMode.toString()); + vscode.postMessage({ + command: CMD_ERROR, + value: "Please select a valid python version.", + }); + submitted = false; + + return; + } + + // if (pythonMode === 0) { + // const pyenvKnownSel = document.getElementById("sel-pyenv-known"); + // pythonPath = pyenvKnownSel.value; + // } else if (pythonMode === 2) { + // const files = document.getElementById("python-path-executable").files; + + // if (files.length == 1) { + // pythonPath = files[0].name; + // } else { + // console.debug("Please select a valid python executable file"); + // vscode.postMessage({ + // command: CMD_ERROR, + // value: "Please select a valid python executable file.", + // }); + // submitted = false; + + // return; + // } + // } + + // Get console mode + const consoleRadio = document.getElementsByName("console-radio"); + let consoleSelection = null; + for (let i = 0; i < consoleRadio.length; i++) { + if (consoleRadio[i].checked) { + consoleSelection = consoleRadio[i].value; + + break; + } + } + + if ( + consoleSelection === null || + !(consoleSelection === "UART" || consoleSelection === "USB") + ) { + consoleSelection = 0; + console.debug("Invalid console selection value: " + consoleSelection); + vscode.postMessage({ + command: CMD_ERROR, + value: `Please select a valid console, got: ${consoleSelection}`, + }); + submitted = false; + return; + } + + const spiFeature = document.getElementById("spi-features-cblist").checked; + const i2cFeature = document.getElementById("i2c-features-cblist").checked; + const gpioFeature = document.getElementById("gpio-features-cblist").checked; + const wifiFeature = document.getElementById("wifi-features-cblist").checked; + const sensorFeature = document.getElementById( + "sensor-features-cblist" + ).checked; + const shellFeature = document.getElementById( + "shell-features-cblist" + ).checked; + + // --- CMake: collect values from the new controls --- + let cmakeMode = null; // numeric contract: 0..4 + let cmakePath = null; // string | null + let cmakeVersion = null; // string | null + + const cmakeModeSel = document.getElementById('cmake-mode'); + const selCmake = document.getElementById('sel-cmake'); // shown in "select" mode + const cmakeFileInp = document.getElementById('cmake-path-executable'); // shown in "custom" mode + const latestCmakeVersion = document.getElementById('cmake-latest-label'); // get latest version + + // Fallback to "latest" if the select isn't there for some reason + const cmakeModeStr = (cmakeModeSel?.value || 'latest'); + + // Map string modes -> numeric API + // 0 = default bundle, 1 = system, 2 = select version, 3 = custom path, 4 = latest + switch (cmakeModeStr) { + // default to latest + case 'default': cmakeMode = 4; break; + case 'system': cmakeMode = 1; break; + case 'select': cmakeMode = 2; break; + case 'custom': cmakeMode = 3; break; + case 'latest': cmakeMode = 4; break; + default: + console.debug('Invalid cmake mode string: ' + cmakeModeStr); + vscode.postMessage({ + command: CMD_ERROR, + value: `Please select a valid CMake mode (got: ${cmakeModeStr}).` + }); + submitted = false; + return; + } + + // Validate + collect per-mode extras + if (cmakeMode === 4) { + if (!latestCmakeVersion) { + console.error('Latest CMake version element not found'); + vscode.postMessage({ + command: CMD_ERROR, + value: 'Internal error: latest CMake version not found.' + }); + submitted = false; + return; + } + cmakeVersion = latestCmakeVersion.textContent.trim(); + } else if (cmakeMode === 2) { + // specific version chosen from dropdown + if (!selCmake || !selCmake.value) { + vscode.postMessage({ + command: CMD_ERROR, + value: 'Please select a CMake version.' + }); + submitted = false; + return; + } + cmakeVersion = selCmake.value; + } else if (cmakeMode === 3) { + // custom executable file + const files = cmakeFileInp?.files || []; + if (files.length !== 1) { + console.debug('Please select a valid CMake executable file'); + vscode.postMessage({ + command: CMD_ERROR, + value: 'Please select a valid CMake executable file.' + }); + submitted = false; + return; + } + + cmakePath = files[0].name; + } + + // Final sanity check: numeric range 1..4 + if (cmakeMode === null || isNaN(cmakeMode) || cmakeMode < 1 || cmakeMode > 4) { + console.debug('Invalid cmake version value: ' + cmakeMode); + vscode.postMessage({ + command: CMD_ERROR, + value: 'Please select a valid CMake version.' + }); + submitted = false; + return; + } + // --- end CMake block --- + + // --- Ninja: collect values from the new controls --- + let ninjaMode = null; // numeric contract: 0..4 + let ninjaPath = null; + let ninjaVersion = null; // string | null + + const ninjaModeSel = document.getElementById('ninja-mode'); + const selNinja = document.getElementById('sel-ninja'); // shown in "select" mode + const ninjaFileInp = document.getElementById('ninja-path-executable'); // shown in "custom" mode + const latestNinjaVersion = document.getElementById('ninja-latest-label'); // get latest version + + // Fallback to "latest" if the select isn't there for some reason + const ninjaModeStr = (ninjaModeSel?.value || 'latest'); + + // Map string modes -> numeric API + // 0 = default bundle, 1 = system, 2 = select version, 3 = custom path, 4 = latest + switch (ninjaModeStr) { + // default to latest + case 'default': ninjaMode = 4; break; + case 'system': ninjaMode = 1; break; + case 'select': ninjaMode = 2; break; + case 'custom': ninjaMode = 3; break; + case 'latest': ninjaMode = 4; break; + default: + console.debug('Invalid ninja mode string: ' + ninjaModeStr); + vscode.postMessage({ + command: CMD_ERROR, + value: `Please select a valid Ninja mode (got: ${ninjaModeModeStr}).` + }); + submitted = false; + return; + } + + // Validate + collect per-mode extras + if (ninjaMode === 4) { + if (!latestNinjaVersion) { + console.error('Latest Ninja version element not found'); + vscode.postMessage({ + command: CMD_ERROR, + value: 'Internal error: latest Ninja version not found.' + }); + submitted = false; + return; + } + ninjaVersion = latestNinjaVersion.textContent.trim(); + } else if (ninjaMode === 2) { + // specific version chosen from dropdown + if (!selNinja || !selNinja.value) { + vscode.postMessage({ + command: CMD_ERROR, + value: 'Please select a Ninja version.' + }); + submitted = false; + return; + } + ninjaVersion = selNinja.value; + } else if (ninjaMode === 3) { + // custom executable file + const files = ninjaFileInp?.files || []; + if (files.length !== 1) { + console.debug('Please select a valid Ninja executable file'); + vscode.postMessage({ + command: CMD_ERROR, + value: 'Please select a valid Ninja executable file.' + }); + submitted = false; + return; + } + + ninjaPath = files[0].name; + } + + // Final sanity check: numeric range 1..4 + if (ninjaMode === null || isNaN(ninjaMode) || ninjaMode < 1 || ninjaMode > 4) { + console.debug('Invalid ninja version value: ' + ninjaMode); + vscode.postMessage({ + command: CMD_ERROR, + value: 'Please select a valid Ninja version.' + }); + submitted = false; + return; + } + // --- end Ninja block --- + + /* Catch silly users who spam the submit button */ + if (submitted) { + console.error("already submitted"); + return; + } + submitted = true; + + //post all data values to the extension + vscode.postMessage({ + command: CMD_SUBMIT, + value: { + projectName: projectName, + pythonMode: Number(pythonMode), + pythonPath: pythonPath, + console: consoleSelection, + boardType: document.getElementById("sel-board-type").value, + spiFeature: spiFeature, + i2cFeature: i2cFeature, + gpioFeature: gpioFeature, + wifiFeature: wifiFeature, + sensorFeature: sensorFeature, + shellFeature: shellFeature, + cmakeMode: Number(cmakeMode), + cmakePath: cmakePath, + cmakeVersion: cmakeVersion, + projectBase: document.getElementById("sel-template").value, + ninjaMode: Number(ninjaMode), + ninjaPath: ninjaPath, + ninjaVersion: ninjaVersion, + }, + }); + }; + + function _onMessage(event) { + // JSON data sent from the extension + const message = event.data; + + switch (message.command) { + case CMD_CHANGE_LOCATION: + // update UI + document.getElementById("inp-project-location").value = message.value; + break; + case CMD_SET_THEME: + console.log("set theme", message.theme); + // update UI + if (message.theme == "dark") { + // explicitly choose dark mode + localStorage.theme = "dark"; + document.body.classList.add("dark"); + } else if (message.theme == "light") { + document.body.classList.remove("dark"); + // explicitly choose light mode + localStorage.theme = "light"; + } + break; + case CMD_SUBMIT_DENIED: + submitted = false; + break; + default: + console.error("Unknown command: " + message.command); + break; + } + } + + window.addEventListener("message", _onMessage); + + // add onclick event handlers to avoid inline handlers + document + .getElementById("btn-change-project-location") + .addEventListener("click", changeLocation); + document + .getElementById("btn-cancel") + .addEventListener("click", cancelBtnClick); + document + .getElementById("btn-create") + .addEventListener("click", submitBtnClick); + + document + .getElementById("inp-project-name") + .addEventListener("input", function () { + const projName = document.getElementById("inp-project-name").value; + console.log(`${projName} is now`); + // TODO: future examples stuff (maybe) + }); + + const pythonVersionRadio = document.getElementsByName("python-version-radio"); + if (pythonVersionRadio.length > 0) pythonVersionRadio[0].checked = true; +})();