diff --git a/package.json b/package.json index 0d526c16..20cf48a1 100644 --- a/package.json +++ b/package.json @@ -93,10 +93,18 @@ "light": "images/ArduinoUpload_16.svg" } }, + { + "command": "arduino.cliUpload", + "title": "Arduino CLI: Upload" + }, { "command": "arduino.uploadUsingProgrammer", "title": "Arduino: Upload Using Programmer" }, + { + "command": "arduino.cliUploadUsingProgrammer", + "title": "Arduino CLI: Upload Using Programmer" + }, { "command": "arduino.selectProgrammer", "title": "Arduino: Select Programmer" @@ -442,6 +450,11 @@ "type": "object", "title": "Arduino configuration", "properties": { + "arduino.useArduinoCli": { + "type": "boolean", + "default": false, + "markdownDescription": "Use Arduino CLI installed instead of Arduino IDE. `#arduino.path#` must be set, as there is no default path for 'arduino-cli'. (Requires a restart after change)" + }, "arduino.path": { "type": "string", "default": "", diff --git a/src/arduino/arduino.ts b/src/arduino/arduino.ts index 263a5069..5a59ca19 100644 --- a/src/arduino/arduino.ts +++ b/src/arduino/arduino.ts @@ -93,13 +93,23 @@ export class ArduinoApp { } } - public async upload() { + /** + * Upload code to selected board + * @param {bool} [compile=true] - Indicates whether to compile the code when using the CLI to upload + * @param {bool} [useProgrammer=false] - Indicate whether a specific programmer should be used + */ + public async upload(compile: boolean = true, useProgrammer: boolean = false) { const dc = DeviceContext.getInstance(); const boardDescriptor = this.getBoardBuildString(); if (!boardDescriptor) { return; } + const selectProgrammer = useProgrammer ? this.getProgrammerString() : null; + if (useProgrammer && !selectProgrammer) { + return; + } + if (!ArduinoWorkspace.rootPath) { vscode.window.showWarningMessage("Cannot find the sketch file."); return; @@ -140,8 +150,25 @@ export class ArduinoApp { } } + if (!compile && !this.useArduinoCli()) { + arduinoChannel.error("This command is only availble when using the Arduino CLI"); + return; + } + const appPath = path.join(ArduinoWorkspace.rootPath, dc.sketch); - const args = ["--upload", "--board", boardDescriptor]; + // TODO: add the --clean argument to the cli args when v 0.14 is released (this will clean up the build folder after uploading) + const args = (!compile && this.useArduinoCli()) ? ["upload", "-b", boardDescriptor] : + this.useArduinoCli() ? ["compile", "--upload", "-b", boardDescriptor] : + ["--upload", "--board", boardDescriptor]; + + if (useProgrammer) { + if (this.useArduinoCli()) { + args.push("--programmer", selectProgrammer) + } else { + args.push("--useprogrammer", "--pref", "programmer=" + selectProgrammer) + } + } + if (dc.port) { args.push("--port", dc.port); } @@ -149,7 +176,7 @@ export class ArduinoApp { if (VscodeSettings.getInstance().logLevel === "verbose") { args.push("--verbose"); } - if (dc.output) { + if (dc.output && compile) { const outputPath = path.resolve(ArduinoWorkspace.rootPath, dc.output); const dirPath = path.dirname(outputPath); if (!util.directoryExistsSync(dirPath)) { @@ -157,77 +184,13 @@ export class ArduinoApp { return; } - args.push("--pref", `build.path=${outputPath}`); - arduinoChannel.info(`Please see the build logs in Output path: ${outputPath}`); - } else { - const msg = "Output path is not specified. Unable to reuse previously compiled files. Upload could be slow. See README."; - arduinoChannel.warning(msg); - } - await util.spawn(this._settings.commandPath, arduinoChannel.channel, args).then(async () => { - UsbDetector.getInstance().resumeListening(); - if (needRestore) { - await serialMonitor.openSerialMonitor(); - } - arduinoChannel.end(`Uploaded the sketch: ${dc.sketch}${os.EOL}`); - }, (reason) => { - arduinoChannel.error(`Exit with code=${reason.code}${os.EOL}`); - }); - } - - public async uploadUsingProgrammer() { - const dc = DeviceContext.getInstance(); - const boardDescriptor = this.getBoardBuildString(); - if (!boardDescriptor) { - return; - } - - const selectProgrammer = this.getProgrammerString(); - if (!selectProgrammer) { - return; - } + if (this.useArduinoCli()) { + args.push("--build-path", outputPath); - if (!ArduinoWorkspace.rootPath) { - vscode.window.showWarningMessage("Cannot find the sketch file."); - return; - } - - if (!dc.sketch || !util.fileExistsSync(path.join(ArduinoWorkspace.rootPath, dc.sketch))) { - await this.getMainSketch(dc); - } - if (!dc.port) { - const choice = await vscode.window.showInformationMessage( - "Serial port is not specified. Do you want to select a serial port for uploading?", - "Yes", "No"); - if (choice === "Yes") { - vscode.commands.executeCommand("arduino.selectSerialPort"); - } - return; - } - - arduinoChannel.show(); - arduinoChannel.start(`Upload sketch - ${dc.sketch}`); - - const serialMonitor = SerialMonitor.getInstance(); - - const needRestore = await serialMonitor.closeSerialMonitor(dc.port); - UsbDetector.getInstance().pauseListening(); - await vscode.workspace.saveAll(false); - - const appPath = path.join(ArduinoWorkspace.rootPath, dc.sketch); - const args = ["--upload", "--board", boardDescriptor, "--port", dc.port, "--useprogrammer", - "--pref", "programmer=" + selectProgrammer, appPath]; - if (VscodeSettings.getInstance().logLevel === "verbose") { - args.push("--verbose"); - } - if (dc.output) { - const outputPath = path.resolve(ArduinoWorkspace.rootPath, dc.output); - const dirPath = path.dirname(outputPath); - if (!util.directoryExistsSync(dirPath)) { - Logger.notifyUserError("InvalidOutPutPath", new Error(constants.messages.INVALID_OUTPUT_PATH + outputPath)); - return; + } else { + args.push("--pref", `build.path=${outputPath}`); } - args.push("--pref", `build.path=${outputPath}`); arduinoChannel.info(`Please see the build logs in Output path: ${outputPath}`); } else { const msg = "Output path is not specified. Unable to reuse previously compiled files. Upload could be slow. See README."; @@ -277,7 +240,7 @@ export class ArduinoApp { } const appPath = path.join(ArduinoWorkspace.rootPath, dc.sketch); - const args = ["--verify", "--board", boardDescriptor, appPath]; + const args = this.useArduinoCli() ? ["compile", "-b", boardDescriptor, appPath] : ["--verify", "--board", boardDescriptor, appPath]; if (VscodeSettings.getInstance().logLevel === "verbose") { args.push("--verbose"); } @@ -289,7 +252,13 @@ export class ArduinoApp { return; } - args.push("--pref", `build.path=${outputPath}`); + if (this.useArduinoCli()) { + args.push("--build-path", outputPath); + + } else { + args.push("--pref", `build.path=${outputPath}`); + } + arduinoChannel.info(`Please see the build logs in Output path: ${outputPath}`); } else { const msg = "Output path is not specified. Unable to reuse previously compiled files. Verify could be slow. See README."; @@ -495,9 +464,14 @@ export class ArduinoApp { } } - /** - * Install arduino board package based on package name and platform hardware architecture. - */ + /** + * Installs arduino board package. + * (If using the aduino CLI this installs the corrosponding core.) + * @param {string} packageName - board vendor + * @param {string} arch - board architecture + * @param {string} version - version of board package or core to download + * @param {boolean} [showOutput=true] - show raw output from command + */ public async installBoard(packageName: string, arch: string = "", version: string = "", showOutput: boolean = true) { arduinoChannel.show(); const updatingIndex = packageName === "dummy" && !arch && !version; @@ -505,23 +479,28 @@ export class ArduinoApp { arduinoChannel.start(`Update package index files...`); } else { try { - const packagePath = path.join(this._settings.packagePath, "packages", packageName); + const packagePath = path.join(this._settings.packagePath, "packages", packageName, arch); if (util.directoryExistsSync(packagePath)) { util.rmdirRecursivelySync(packagePath); } arduinoChannel.start(`Install package - ${packageName}...`); } catch (error) { arduinoChannel.start(`Install package - ${packageName} failed under directory : ${error.path}${os.EOL} -Please make sure the folder is not occupied by other procedures .`); + Please make sure the folder is not occupied by other procedures .`); arduinoChannel.error(`Error message - ${error.message}${os.EOL}`); arduinoChannel.error(`Exit with code=${error.code}${os.EOL}`); return; } } + arduinoChannel.info(`${packageName}${arch && ":" + arch}${version && ":" + version}`); try { - await util.spawn(this._settings.commandPath, - showOutput ? arduinoChannel.channel : null, - ["--install-boards", `${packageName}${arch && ":" + arch}${version && ":" + version}`]); + this.useArduinoCli() ? + await util.spawn(this._settings.commandPath, + showOutput ? arduinoChannel.channel : null, + ["core", "install", `${packageName}${arch && ":" + arch}${version && "@" + version}`]) : + await util.spawn(this._settings.commandPath, + showOutput ? arduinoChannel.channel : null, + ["--install-boards", `${packageName}${arch && ":" + arch}${version && ":" + version}`]); if (updatingIndex) { arduinoChannel.end("Updated package index files."); @@ -548,6 +527,13 @@ Please make sure the folder is not occupied by other procedures .`); arduinoChannel.end(`Uninstalled board package - ${boardName}${os.EOL}`); } + /** + * Downloads or updates a library + * @param {string} libName - name of the library to download + * @param {string} version - version of library to download + * @param {boolean} [showOutput=true] - show raw output from command + */ + public async installLibrary(libName: string, version: string = "", showOutput: boolean = true) { arduinoChannel.show(); const updatingIndex = (libName === "dummy" && !version); @@ -557,6 +543,10 @@ Please make sure the folder is not occupied by other procedures .`); arduinoChannel.start(`Install library - ${libName}`); } try { + this.useArduinoCli() ? + await util.spawn(this._settings.commandPath, + showOutput ? arduinoChannel.channel : null, + ["lib", "install", `${libName}${version && "@" + version}`]) : await util.spawn(this._settings.commandPath, showOutput ? arduinoChannel.channel : null, ["--install-library", `${libName}${version && ":" + version}`]); @@ -769,6 +759,15 @@ Please make sure the folder is not occupied by other procedures .`); this._programmerManager = value; } + /** + * Checks if the arduino cli is being used + * @returns {bool} - true if arduino cli is being use + */ + private useArduinoCli() { + return this._settings.useArduinoCli; + // return VscodeSettings.getInstance().useArduinoCli; + } + private getProgrammerString(): string { const selectProgrammer = this.programmerManager.currentProgrammer; if (!selectProgrammer) { diff --git a/src/arduino/arduinoSettings.ts b/src/arduino/arduinoSettings.ts index 58419519..8d994d81 100644 --- a/src/arduino/arduinoSettings.ts +++ b/src/arduino/arduinoSettings.ts @@ -22,6 +22,7 @@ export interface IArduinoSettings { preferencePath: string; defaultBaudRate: number; preferences: Map; + useArduinoCli: boolean; reloadPreferences(): void; } @@ -38,18 +39,21 @@ export class ArduinoSettings implements IArduinoSettings { private _preferences: Map; + private _useArduinoCli: boolean; + public constructor() { } public async initialize() { const platform = os.platform(); this._commandPath = VscodeSettings.getInstance().commandPath; + this._useArduinoCli = VscodeSettings.getInstance().useArduinoCli; await this.tryResolveArduinoPath(); await this.tryGetDefaultBaudRate(); if (platform === "win32") { await this.updateWindowsPath(); if (this._commandPath === "") { - this._commandPath = "arduino_debug.exe"; + this._useArduinoCli ? this._commandPath = "arduino-cli.exe" : this._commandPath = "arduino_debug.exe"; } } else if (platform === "linux") { if (util.directoryExistsSync(path.join(this._arduinoPath, "portable"))) { @@ -150,6 +154,10 @@ export class ArduinoSettings implements IArduinoSettings { return this._preferences; } + public get useArduinoCli() { + return this._useArduinoCli; + } + public get defaultBaudRate() { return this._defaultBaudRate; } diff --git a/src/arduino/programmerManager.ts b/src/arduino/programmerManager.ts index a060f211..eee12725 100644 --- a/src/arduino/programmerManager.ts +++ b/src/arduino/programmerManager.ts @@ -58,46 +58,54 @@ export class ProgrammerManager { dc.programmer = chosen; } + /** + * Gets a specific programmer from the programmers list. + * If using the Arduino IDE, adds prefix "adruino:" + * @param {ProgrammerList} newProgrammer - a list of the available programmers + */ public getProgrammer(newProgrammer: ProgrammerList) { + let prefix = ""; + if (!this._settings.useArduinoCli) { + prefix = "arduino:"}; switch (newProgrammer) { case ProgrammerList["AVR ISP"]: - this._programmervalue = "arduino:avrisp"; + this._programmervalue = prefix + "avrisp"; break; case ProgrammerList["AVRISP mkII"]: - this._programmervalue = "arduino:avrispmkii"; + this._programmervalue = prefix + "avrispmkii"; break; case ProgrammerList.USBtinyISP: - this._programmervalue = "arduino:usbtinyisp"; + this._programmervalue = prefix + "usbtinyisp"; break; case ProgrammerList.ArduinoISP: - this._programmervalue = "arduino:arduinoisp"; + this._programmervalue = prefix + "arduinoisp"; break; case ProgrammerList.USBasp: - this._programmervalue = "arduino:usbasp"; + this._programmervalue = prefix + "usbasp"; break; case ProgrammerList["Parallel Programmer"]: - this._programmervalue = "arduino:parallel"; + this._programmervalue = prefix + "parallel"; break; case ProgrammerList["Arduino as ISP"]: - this._programmervalue = "arduino:arduinoasisp"; + this._programmervalue = prefix + "arduinoasisp"; break; case ProgrammerList["Arduino Gemma"]: - this._programmervalue = "arduino:usbGemma"; + this._programmervalue = prefix + "usbGemma"; break; case ProgrammerList["BusPirate as ISP"]: - this._programmervalue = "arduino:buspirate"; + this._programmervalue = prefix + "buspirate"; break; case ProgrammerList["Atmel STK500 development board"]: - this._programmervalue = "arduino:stk500"; + this._programmervalue = prefix + "stk500"; break; case ProgrammerList["Atmel JTAGICE3 (ISP mode)"]: - this._programmervalue = "arduino:jtag3isp"; + this._programmervalue = prefix + "jtag3isp"; break; case ProgrammerList["Atmel JTAGICE3 (JTAG mode)"]: - this._programmervalue = "arduino:jtag3"; + this._programmervalue = prefix + "jtag3"; break; case ProgrammerList["Atmel-ICE (AVR)"]: - this._programmervalue = "arduino:atmel_ice"; + this._programmervalue = prefix + "atmel_ice"; break; default: break; diff --git a/src/arduino/vscodeSettings.ts b/src/arduino/vscodeSettings.ts index f66e268b..14726cd8 100644 --- a/src/arduino/vscodeSettings.ts +++ b/src/arduino/vscodeSettings.ts @@ -15,6 +15,7 @@ const configKeys = { IGNORE_BOARDS: "arduino.ignoreBoards", SKIP_HEADER_PROVIDER: "arduino.skipHeaderProvider", DEFAULT_BAUD_RATE: "arduino.defaultBaudRate", + USE_ARDUINO_CLI: "arduino.useArduinoCli", }; export interface IVscodeSettings { @@ -28,6 +29,7 @@ export interface IVscodeSettings { ignoreBoards: string[]; skipHeaderProvider: boolean; defaultBaudRate: number; + useArduinoCli: boolean; updateAdditionalUrls(urls: string | string[]): void; } @@ -83,6 +85,10 @@ export class VscodeSettings implements IVscodeSettings { return this.getConfigValue(configKeys.DEFAULT_BAUD_RATE); } + public get useArduinoCli(): boolean { + return this.getConfigValue(configKeys.USE_ARDUINO_CLI); + } + public get skipHeaderProvider(): boolean { return this.getConfigValue(configKeys.SKIP_HEADER_PROVIDER); } diff --git a/src/common/platform.ts b/src/common/platform.ts index aa17cadf..1675c8b0 100644 --- a/src/common/platform.ts +++ b/src/common/platform.ts @@ -14,8 +14,8 @@ export function resolveArduinoPath(): string { return internalSysLib.resolveArduinoPath(); } -export function validateArduinoPath(arduinoPath: string): boolean { - return internalSysLib.validateArduinoPath(arduinoPath); +export function validateArduinoPath(arduinoPath: string, useArduinoCli = false): boolean { + return internalSysLib.validateArduinoPath(arduinoPath, useArduinoCli); } export function findFile(fileName: string, cwd: string): string { diff --git a/src/common/sys/darwin.ts b/src/common/sys/darwin.ts index 235aaf38..4d5c7e98 100644 --- a/src/common/sys/darwin.ts +++ b/src/common/sys/darwin.ts @@ -18,8 +18,9 @@ export function resolveArduinoPath(): string { return result || ""; } -export function validateArduinoPath(arduinoPath: string): boolean { - return fileExistsSync(path.join(resolveMacArduinoAppPath(arduinoPath), "/Contents/MacOS/Arduino")); +export function validateArduinoPath(arduinoPath: string, useArduinoCli = false): boolean { + return fileExistsSync(path.join(resolveMacArduinoAppPath(arduinoPath), useArduinoCli ? "arduino-cli" : "/Contents/MacOS/Arduino")); + } export function findFile(fileName: string, cwd: string): string { diff --git a/src/common/sys/linux.ts b/src/common/sys/linux.ts index 6c9189d7..fbb904b4 100644 --- a/src/common/sys/linux.ts +++ b/src/common/sys/linux.ts @@ -20,8 +20,8 @@ export function resolveArduinoPath(): string { return pathString || ""; } -export function validateArduinoPath(arduinoPath: string): boolean { - return fileExistsSync(path.join(arduinoPath, "arduino")); +export function validateArduinoPath(arduinoPath: string, useArduinoCli = false): boolean { + return fileExistsSync(path.join(arduinoPath, useArduinoCli ? "arduino-cli" : "arduino")); } export function findFile(fileName: string, cwd: string): string { diff --git a/src/common/sys/win32.ts b/src/common/sys/win32.ts index dbbbaaac..ad237e27 100644 --- a/src/common/sys/win32.ts +++ b/src/common/sys/win32.ts @@ -27,8 +27,9 @@ export async function resolveArduinoPath() { return pathString; } -export function validateArduinoPath(arduinoPath: string): boolean { - return fileExistsSync(path.join(arduinoPath, "arduino_debug.exe")); +export function validateArduinoPath(arduinoPath: string, useArduinoCli = false): boolean { + return fileExistsSync(path.join(arduinoPath, useArduinoCli ? "arduino-cli.exe" : "arduino_debug.exe")); + } export function findFile(fileName: string, cwd: string): string { diff --git a/src/common/util.ts b/src/common/util.ts index 2af4b83b..696c99ea 100644 --- a/src/common/util.ts +++ b/src/common/util.ts @@ -200,6 +200,13 @@ export function isArduinoFile(filePath): boolean { return fileExistsSync(filePath) && (path.extname(filePath) === ".ino" || path.extname(filePath) === ".pde"); } +/** + * Send a command to arduino + * @param {string} command - base command path (either Arduino IDE or CLI) + * @param {vscode.OutputChannel} outputChannel - output display channel + * @param {string[]} [args=[]] - arguments to pass to the command + * @param {any} [options={}] - options and flags for the arguments + */ export function spawn(command: string, outputChannel: vscode.OutputChannel, args: string[] = [], options: any = {}): Thenable { return new Promise((resolve, reject) => { const stdout = ""; diff --git a/src/extension.ts b/src/extension.ts index c4112bd7..b6cf7a78 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -91,7 +91,9 @@ export async function activate(context: vscode.ExtensionContext) { const arduinoPath = arduinoContextModule.default.arduinoApp.settings.arduinoPath; const commandPath = arduinoContextModule.default.arduinoApp.settings.commandPath; - if (!arduinoPath || !validateArduinoPath(arduinoPath)) { // Pop up vscode User Settings page when cannot resolve arduino path. + const useArduinoCli = arduinoContextModule.default.arduinoApp.settings.useArduinoCli; + // Pop up vscode User Settings page when cannot resolve arduino path. + if (!arduinoPath || !validateArduinoPath(arduinoPath, useArduinoCli)) { Logger.notifyUserError("InvalidArduinoPath", new Error(constants.messages.INVALID_ARDUINO_PATH)); vscode.commands.executeCommand("workbench.action.openGlobalSettings"); } else if (!commandPath || !util.fileExistsSync(commandPath)) { @@ -152,6 +154,24 @@ export async function activate(context: vscode.ExtensionContext) { return { board: arduinoContextModule.default.boardManager.currentBoard.name }; }); + registerArduinoCommand("arduino.cliUpload", async () => { + if (!status.compile) { + status.compile = "cliUpload"; + try { + await vscode.window.withProgress({ + location: vscode.ProgressLocation.Window, + title: "Arduino: Using CLI to upload...", + }, async () => { + await arduinoContextModule.default.arduinoApp.upload(false); + }); + } catch (ex) { + } + delete status.compile; + } + }, () => { + return { board: arduinoContextModule.default.boardManager.currentBoard.name }; + }); + registerArduinoCommand("arduino.setSketchFile", async () => { const sketchFileName = deviceContext.sketch; const newSketchFileName = await vscode.window.showInputBox({ @@ -177,7 +197,20 @@ export async function activate(context: vscode.ExtensionContext) { if (!status.compile) { status.compile = "upload"; try { - await arduinoContextModule.default.arduinoApp.uploadUsingProgrammer(); + await arduinoContextModule.default.arduinoApp.upload(true, true); + } catch (ex) { + } + delete status.compile; + } + }, () => { + return { board: arduinoContextModule.default.boardManager.currentBoard.name }; + }); + + registerArduinoCommand("arduino.cliUploadUsingProgrammer", async () => { + if (!status.compile) { + status.compile = "cliUpload"; + try { + await arduinoContextModule.default.arduinoApp.upload(false, true); } catch (ex) { } delete status.compile; diff --git a/test/extension.test.ts b/test/extension.test.ts index e1a8de12..9c56fc41 100644 --- a/test/extension.test.ts +++ b/test/extension.test.ts @@ -55,6 +55,8 @@ suite("Arduino: Extension Tests", () => { "arduino.loadPackages", "arduino.installBoard", "arduino.setSketchFile", + "arduino.cliUpload", + "arduino.cliUploadUsingProgrammer", ]; const foundArduinoCommands = commands.filter((value) => { diff --git a/test/librarymanager.test.ts b/test/librarymanager.test.ts index 7e43af26..5ff5cc84 100644 --- a/test/librarymanager.test.ts +++ b/test/librarymanager.test.ts @@ -79,7 +79,7 @@ suite("Arduino: Library Manager.", () => { if (util.directoryExistsSync(libPath)) { done(); } else { - done(new Error("AzureIoTHub library install failure, can't find library path :" + libPath)); + done(new Error("AzureIoTHub library install failure, can't find library path: " + libPath)); } });