// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. import * as fs from "fs"; import * as path from "path"; import * as url from "url"; import * as vscode from "vscode"; import * as util from "../common/util"; import * as constants from "../common/constants"; import { arduinoChannel } from "../common/outputChannel"; import { versionCompare } from "../common/sharedUtilities/utils"; import { DeviceContext } from "../deviceContext"; import { ArduinoApp } from "./arduino"; import { IArduinoSettings } from "./arduinoSettings"; import { boardEqual, parseBoardDescriptor } from "./board"; import { BoardConfigResult, IBoard, IPackage, IPlatform, IProgrammer } from "./package"; import { parseProgrammerDescriptor } from "./programmer"; import { VscodeSettings } from "./vscodeSettings"; export class BoardManager { private _packages: IPackage[]; private _platforms: IPlatform[]; private _programmers: Map<string, IProgrammer>; private _installedPlatforms: IPlatform[]; private _boards: Map<string, IBoard>; private _boardConfigStatusBar: vscode.StatusBarItem; private _currentBoard: IBoard; private _onBoardTypeChanged = new vscode.EventEmitter<void>(); constructor(private _settings: IArduinoSettings, private _arduinoApp: ArduinoApp) { this._boardConfigStatusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, constants.statusBarPriority.BOARD); this._boardConfigStatusBar.command = "arduino.showBoardConfig"; this._boardConfigStatusBar.tooltip = "Show Board Config"; } public async loadPackages(update: boolean = false) { this._packages = []; this._platforms = []; this._installedPlatforms = []; const additionalUrls = this._arduinoApp.getAdditionalUrls(); if (update) { // Update index files. await this.setPreferenceUrls(additionalUrls); await this._arduinoApp.initialize(true); } // Parse package index files. const indexFiles = ["package_index.json"].concat(additionalUrls); const rootPackageFolder = this._settings.packagePath; for (const indexFile of indexFiles) { const indexFileName = this.getIndexFileName(indexFile); if (!indexFileName) { continue; } if (!update && !util.fileExistsSync(path.join(rootPackageFolder, indexFileName))) { await this.setPreferenceUrls(additionalUrls); await this._arduinoApp.initialize(true); } this.loadPackageContent(indexFileName); } // Load default platforms from arduino installation directory and user manually installed platforms. this.loadInstalledPlatforms(); // Load all supported board types this.loadInstalledBoards(); this.loadInstalledProgrammers(); this.updateStatusBar(); this._boardConfigStatusBar.show(); const dc = DeviceContext.getInstance(); dc.onChangeBoard(() => this.onDeviceContextBoardChange()); dc.onChangeConfiguration(() => this.onDeviceContextConfigurationChange()); // load initial board from DeviceContext by emulating // a board change event. this.onDeviceContextBoardChange(); this.updateStatusBar(true); } public async changeBoardType() { const supportedBoardTypes = this.listBoards(); if (supportedBoardTypes.length === 0) { vscode.window.showInformationMessage("No supported board is available."); return; } // TODO:? Add separator item between different platforms. const chosen = await vscode.window.showQuickPick(<vscode.QuickPickItem[]>supportedBoardTypes.map((entry): vscode.QuickPickItem => { return <vscode.QuickPickItem>{ label: entry.name, description: entry.platform.name, entry, }; }).sort((a, b): number => { if (a.description === b.description) { return a.label === b.label ? 0 : (a.label > b.label ? 1 : -1); } else { return a.description > b.description ? 1 : -1; } }), { placeHolder: "Select board type" }); if (chosen && chosen.label) { this.doChangeBoardType((<any>chosen).entry); } } public async updatePackageIndex(indexUri: string): Promise<boolean> { let allUrls = this._arduinoApp.getAdditionalUrls(); if (!(allUrls.indexOf(indexUri) >= 0)) { allUrls = allUrls.concat(indexUri); VscodeSettings.getInstance().updateAdditionalUrls(allUrls); await this._arduinoApp.setPref("boardsmanager.additional.urls", this._arduinoApp.getAdditionalUrls().join(",")); } return true; } public get onBoardTypeChanged(): vscode.Event<void> { return this._onBoardTypeChanged.event; } public doChangeBoardType(targetBoard: IBoard) { const dc = DeviceContext.getInstance(); if (dc.board === targetBoard.key) { return; } // Resetting the board first that we don't overwrite the configuration // of the previous board. this._currentBoard = null; // This will cause a configuration changed event which will have no // effect because no current board is set. dc.configuration = targetBoard.customConfig; // This will generate a device context board event which will set the // correct board and configuration. We know that it will trigger - we // made sure above that the boards actually differ dc.board = targetBoard.key; } public get packages(): IPackage[] { return this._packages; } public get platforms(): IPlatform[] { return this._platforms; } public get installedBoards(): Map<string, IBoard> { return this._boards; } public get installedProgrammers(): Map<string, IProgrammer> { return this._programmers; } public get currentBoard(): IBoard { return this._currentBoard; } public getInstalledPlatforms(): any[] { // Always using manually installed platforms to overwrite the same platform from arduino installation directory. const installedPlatforms = this.getDefaultPlatforms(); const mergePlatform = (plat) => { const find = installedPlatforms.find((_plat) => { return _plat.packageName === plat.packageName && _plat.architecture === plat.architecture; }); if (!find) { installedPlatforms.push(plat); } else { find.defaultPlatform = plat.defaultPlatform; find.version = plat.version; find.rootBoardPath = plat.rootBoardPath; } }; const customPlatforms = this.getCustomPlatforms(); const manuallyInstalled = this.getManuallyInstalledPlatforms(); customPlatforms.forEach(mergePlatform); manuallyInstalled.forEach(mergePlatform); return installedPlatforms; } public loadPackageContent(indexFile: string): void { const indexFileName = this.getIndexFileName(indexFile); if (!util.fileExistsSync(path.join(this._settings.packagePath, indexFileName))) { return; } const packageContent = fs.readFileSync(path.join(this._settings.packagePath, indexFileName), "utf8"); if (!packageContent) { return; } let rawModel = null; try { rawModel = JSON.parse(packageContent); } catch (ex) { arduinoChannel.error(`Invalid json file "${path.join(this._settings.packagePath, indexFileName)}". Suggest to remove it manually and allow boardmanager to re-download it.`); return; } if (!rawModel || !rawModel.packages || !rawModel.packages.length) { return; } this._packages = this._packages.concat(rawModel.packages); rawModel.packages.forEach((pkg) => { pkg.platforms.forEach((plat) => { plat.package = pkg; const addedPlatform = this._platforms .find((_plat) => _plat.architecture === plat.architecture && _plat.package.name === plat.package.name); if (addedPlatform) { // union boards from all versions. // We should not union boards: https://github.com/Microsoft/vscode-arduino/issues/414 // addedPlatform.boards = util.union(addedPlatform.boards, plat.boards, (a, b) => { // return a.name === b.name; // }); // Check if platform name is the same, if not, we should use the name from the latest version. if (addedPlatform.name !== plat.name) { addedPlatform.name = plat.name; } addedPlatform.versions.push(plat.version); // Check if this is the latest version. Platforms typically support more boards in later versions. addedPlatform.versions.sort(versionCompare); if (plat.version === addedPlatform.versions[addedPlatform.versions.length - 1]) { addedPlatform.boards = plat.boards; } } else { plat.versions = [plat.version]; // Clear the version information since the plat will be used to contain all supported versions. plat.version = ""; this._platforms.push(plat); } }); }); } public updateInstalledPlatforms(pkgName: string, arch: string) { const archPath = path.join(this._settings.packagePath, "packages", pkgName, "hardware", arch); const allVersion = util.filterJunk(util.readdirSync(archPath, true)); if (allVersion && allVersion.length) { const newPlatform = { packageName: pkgName, architecture: arch, version: allVersion[0], rootBoardPath: path.join(archPath, allVersion[0]), defaultPlatform: false, }; const existingPlatform = this._platforms.find((_plat) => { return _plat.package.name === pkgName && _plat.architecture === arch; }); if (existingPlatform) { existingPlatform.defaultPlatform = newPlatform.defaultPlatform; if (!existingPlatform.installedVersion) { existingPlatform.installedVersion = newPlatform.version; existingPlatform.rootBoardPath = newPlatform.rootBoardPath; this._installedPlatforms.push(existingPlatform); } this.loadInstalledBoardsFromPlatform(existingPlatform); this.loadInstalledProgrammersFromPlatform(existingPlatform); } } } private updateStatusBar(show: boolean = true): void { if (show) { this._boardConfigStatusBar.show(); if (this._currentBoard) { this._boardConfigStatusBar.text = this._currentBoard.name; } else { this._boardConfigStatusBar.text = "<Select Board Type>"; } } else { this._boardConfigStatusBar.hide(); } } /** * Event callback if DeviceContext detected a new board - either when * loaded from configuration file or when set by the doChangeBoardType * member. */ private onDeviceContextBoardChange() { const dc = DeviceContext.getInstance(); const newBoard = this._boards.get(dc.board); if (boardEqual(newBoard, this._currentBoard)) { return; } if (newBoard) { this._currentBoard = newBoard; if (dc.configuration) { // In case the configuration is incompatible, we reset it as // setting partially valid configurations can lead to nasty // surprises. When setting a new board this is acceptable const r = this._currentBoard.loadConfig(dc.configuration); if (r !== BoardConfigResult.Success && r !== BoardConfigResult.SuccessNoChange) { this._currentBoard.resetConfig(); // we don't reset dc.configuration to give the user a // chance to fix her/his configuration this.invalidConfigWarning(r); } } else { this._currentBoard.resetConfig(); dc.configuration = undefined; } } else { this._currentBoard = null; } this._onBoardTypeChanged.fire(); this.updateStatusBar(); } /** * Event callback if DeviceContext detected a configuration change * - either when loaded from configuration file or when set by the * doChangeBoardType member. */ private onDeviceContextConfigurationChange() { const dc = DeviceContext.getInstance(); if (this._currentBoard) { const r = this._currentBoard.loadConfig(dc.configuration); if (r !== BoardConfigResult.Success && r !== BoardConfigResult.SuccessNoChange) { this._currentBoard.resetConfig(); // We reset the configuration here but do not write it back // to the configuration file - this can be annoying when // someone tries to set a special configuration and doesn't // get it right the first time. this.invalidConfigWarning(r); } } } private invalidConfigWarning(result: BoardConfigResult) { let what = ""; switch (result) { case BoardConfigResult.InvalidFormat: what = ": Invalid format must be of the form \"key1=value2,key1=value2,...\""; break; case BoardConfigResult.InvalidConfigID: what = ": Invalid configuration key"; break; case BoardConfigResult.InvalidOptionID: what = ": Invalid configuration value"; break; } vscode.window.showWarningMessage(`Invalid board configuration detected in configuration file${what}. Falling back to defaults.`); } private loadInstalledPlatforms() { const installed = this.getInstalledPlatforms(); installed.forEach((platform) => { const existingPlatform = this._platforms.find((_plat) => { return _plat.package.name === platform.packageName && _plat.architecture === platform.architecture; }); if (existingPlatform) { existingPlatform.defaultPlatform = platform.defaultPlatform; if (!existingPlatform.installedVersion) { existingPlatform.installedVersion = platform.version; existingPlatform.rootBoardPath = platform.rootBoardPath; this._installedPlatforms.push(existingPlatform); } } else { platform.installedVersion = platform.version; this._installedPlatforms.push(platform); } }); } // Default arduino package information from arduino installation directory. private getDefaultPlatforms(): IPlatform[] { const defaultPlatforms = []; try { const packageBundled = fs.readFileSync(path.join(this._settings.defaultPackagePath, "package_index_bundled.json"), "utf8"); if (!packageBundled) { return defaultPlatforms; } const bundledObject = JSON.parse(packageBundled); if (bundledObject && bundledObject.packages) { for (const pkg of bundledObject.packages) { for (const platform of pkg.platforms) { if (platform.version) { defaultPlatforms.push({ packageName: pkg.name, architecture: platform.architecture, version: platform.version, rootBoardPath: path.join(this._settings.defaultPackagePath, pkg.name, platform.architecture), defaultPlatform: true, }); } } } } } catch (ex) { } return defaultPlatforms; } private getCustomPlatforms(): IPlatform[] { const customPlatforms = []; const hardwareFolder = path.join(this._settings.sketchbookPath, "hardware"); if (!util.directoryExistsSync(hardwareFolder)) { return customPlatforms; } const dirs = util.filterJunk(util.readdirSync(hardwareFolder, true)); // in Mac, filter .DS_Store file. if (!dirs || dirs.length < 1) { return customPlatforms; } for (const packageName of dirs) { const architectures = util.filterJunk(util.readdirSync(path.join(hardwareFolder, packageName), true)); if (!architectures || architectures.length < 1) { continue; } architectures.forEach((architecture) => { const platformFolder = path.join(hardwareFolder, packageName, architecture); if (util.fileExistsSync(path.join(platformFolder, "boards.txt")) && util.fileExistsSync(path.join(platformFolder, "platform.txt"))) { const configs = util.parseConfigFile(path.join(platformFolder, "platform.txt")); customPlatforms.push({ packageName, architecture, version: configs.get("version"), rootBoardPath: path.join(hardwareFolder, packageName, architecture), defaultPlatform: false, }); } }); } return customPlatforms; } // User manually installed packages. private getManuallyInstalledPlatforms(): any[] { const manuallyInstalled = []; const rootPackagePath = path.join(path.join(this._settings.packagePath, "packages")); if (!util.directoryExistsSync(rootPackagePath)) { return manuallyInstalled; } const dirs = util.filterJunk(util.readdirSync(rootPackagePath, true)); // in Mac, filter .DS_Store file. for (const packageName of dirs) { const archPath = path.join(this._settings.packagePath, "packages", packageName, "hardware"); if (!util.directoryExistsSync(archPath)) { continue; } const architectures = util.filterJunk(util.readdirSync(archPath, true)); architectures.forEach((architecture) => { const allVersion = util.filterJunk(util.readdirSync(path.join(archPath, architecture), true)); if (allVersion && allVersion.length) { manuallyInstalled.push({ packageName, architecture, version: allVersion[0], rootBoardPath: path.join(archPath, architecture, allVersion[0]), defaultPlatform: false, }); } }); } return manuallyInstalled; } private loadInstalledBoards(): void { this._boards = new Map<string, IBoard>(); this._installedPlatforms.forEach((plat) => { this.loadInstalledBoardsFromPlatform(plat); }); } private loadInstalledBoardsFromPlatform(plat: IPlatform) { if (util.fileExistsSync(path.join(plat.rootBoardPath, "boards.txt"))) { const boardContent = fs.readFileSync(path.join(plat.rootBoardPath, "boards.txt"), "utf8"); const res = parseBoardDescriptor(boardContent, plat); res.forEach((bd) => { this._boards.set(bd.key, bd); }); } } private loadInstalledProgrammers(): void { this._programmers = new Map<string, IProgrammer>(); this._installedPlatforms.forEach((plat) => { this.loadInstalledProgrammersFromPlatform(plat); }); } private loadInstalledProgrammersFromPlatform(plat: IPlatform) { if (util.fileExistsSync(path.join(plat.rootBoardPath, "programmers.txt"))) { const programmersContent = fs.readFileSync(path.join(plat.rootBoardPath, "programmers.txt"), "utf8"); const res = parseProgrammerDescriptor(programmersContent, plat); res.forEach((prog) => { this._programmers.set(prog.name, prog); }); } } private listBoards(): IBoard[] { const result = []; this._boards.forEach((b) => { result.push(b); }); return result; } private getIndexFileName(uriString: string): string { if (!uriString) { return; } const normalizedUrl = url.parse(uriString); if (!normalizedUrl) { return; } return normalizedUrl.pathname.substr(normalizedUrl.pathname.lastIndexOf("/") + 1); } private async setPreferenceUrls(additionalUrls: string[]) { const settingsUrls = additionalUrls.join(","); if (this._settings.preferences.get("boardsmanager.additional.urls") !== settingsUrls) { await this._arduinoApp.setPref("boardsmanager.additional.urls", settingsUrls); } } }