diff --git a/.editorconfig b/.editorconfig index 38d13283..138865db 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,9 +6,13 @@ root = true # Tab indentation [*] indent_style = space -indent_size = 2 +indent_size = 4 # The indent size used in the `package.json` file cannot be changed # https://github.com/npm/npm/pull/3180#issuecomment-16336516 [{.travis.yml,npm-shrinkwrap.json,package.json}] indent_size = 2 + +[vendor/**.js] +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/misc/debuggerUsbMapping.json b/misc/debuggerUsbMapping.json new file mode 100644 index 00000000..0f1e4791 --- /dev/null +++ b/misc/debuggerUsbMapping.json @@ -0,0 +1,66 @@ +[ + { + "vid": "0483", + "pid": "3744", + "name": "ST Link V1.0", + "short_name": "stlink-1.0", + "config_file": "stlink-v1.cfg" + }, + { + "vid": "0483", + "pid": "3748", + "name": "ST Link V2.0", + "short_name": "stlink-2.0", + "config_file": "stlink-v2.cfg" + }, + { + "vid": "0483", + "pid": "374b", + "name": "ST Link V2.1", + "short_name": "stlink-2.1", + "config_file": "stlink-v2-1.cfg" + }, + { + "pid": [ + "0101", + "0102", + "0103", + "0104", + "0105", + "0106", + "0107", + "0108", + "0109", + "0110", + "0111", + "0112", + "0113", + "0114", + "0115", + "0116", + "0117", + "0118" + ], + "vid": "1366", + "name": "JLink", + "short_name": "jlink", + "config_file": "jlink.cfg" + }, + { + "vid": "03eb", + "pid": "2111", + "name": "CMSIS-DAP", + "short_name": "cmsis-dap", + "config_file": "cmsis-dap.cfg" + }, + { + "vid": "03eb", + "pid": [ + "2111", + "2157" + ], + "name": "CMSIS-DAP", + "short_name": "cmsis-dap", + "config_file": "cmsis-dap.cfg" + } +] \ No newline at end of file diff --git a/misc/openOCDMapping.json b/misc/openOCDMapping.json new file mode 100644 index 00000000..ef50f443 --- /dev/null +++ b/misc/openOCDMapping.json @@ -0,0 +1,18 @@ +[ + { + "board": "arduino:samd:mzero_pro_bl_dbg", + "interface": "interface/cmsis-dap.cfg", + "target": "target/at91samdXX.cfg" + }, + { + "board": "arduino:samd:mzero_bl", + "interface": "interface/cmsis-dap.cfg", + "target": "target/at91samdXX.cfg" + }, + { + "board": "AZ3166:stm32f4:MXCHIP_AZ3166", + "interface": "interface/stlink-v2-1.cfg", + "target": "target/stm32f4x.cfg" + } +] + diff --git a/misc/usbmapping.json b/misc/usbmapping.json index 562c7417..93859480 100644 --- a/misc/usbmapping.json +++ b/misc/usbmapping.json @@ -1,76 +1,147 @@ -[{ +[ + { "index_file": "package_index.json", - "boards": [{ + "boards": [ + { "vid": "2341", "pid": "804e", "name": "Arduino/Genuino MKR1000", "package": "arduino", "architecture": "samd", - "id": "mkr1000" - }, { + "id": "mkr1000", + "target": "at91samdXX.cfg" + }, + { "vid": "8087", "pid": "0a9e", "name": "Intel Edison", "package": "arduino", "architecture": "samd", "id": "edison" - }, { + }, + { + "vid": "03eb", + "pid": "2111", + "name": "Arduino M0 Pro (Programming USB Port)", + "package": "arduino", + "architecture": "samd", + "id": "mzero_pro_bl_dbg", + "target": "at91samdXX.cfg", + "interface": "cmsis-dap.cfg" + }, + { + "vid": "03eb", + "pid": "2157", + "name": "Arduino/Genuino Zero (Programming Port)", + "package": "arduino", + "architecture": "samd", + "id": "arduino_zero_edbg", + "target": "at91samdXX.cfg", + "interface": "cmsis-dap.cfg" + }, + { + "vid": "2341", + "pid": ["804d", "004d", "824d"], + "name": "Arduino/Genuino Zero (Native USB Port)", + "package": "arduino", + "architecture": "samd", + "id": "arduino_zero_edbg", + "target": "at91samdXX.cfg", + "interface": "cmsis-dap.cfg" + }, + { "vid": "2a03", "pid": "804f", "name": "Arduino M0 Pro (Native USB Port)", "package": "arduino", "architecture": "samd", - "id": "mzero_pro_bl" - }, { + "id": "mzero_pro_bl", + "target": "at91samdXX.cfg" + }, + { "vid": "2341", "pid": "8041", "name": "Arduino Yún", "package": "arduino", "architecture": "avr", "id": "yun" - }, { + }, + { "vid": "2341", "pid": "003e", "name": "Arduino Due", "package": "arduino", "architecture": "sam", "id": "arduino_due_x" - }] -}, { + } + ] + }, + { + "index_file": "https://www.adafruit.com/package_adafruit_index.json", + "boards": [ + { + "vid": "239a", + "pid": [ + "0010", + "8010", + "0008" + ], + "name": "Adafruit WICED Feather", + "package": "adafruit", + "architecture": "wiced", + "id": "feather", + "target": "stm32f2x.cfg" + } + ] + }, + { "index_file": "http://arduino.esp8266.com/stable/package_esp8266com_index.json", - "boards": [{ + "boards": [ + { "vid": "10c4", "pid": "ea60", "name": "Adafruit HUZZAH ESP8266", "package": "esp8266", "architecture": "esp8266", "id": "huzzah" - }, { + }, + { "vid": "0403", "pid": "6015", "name": "SparkFun ESP8266 Thing Dev", "package": "esp8266", "architecture": "esp8266", "id": "thingdev" - }] -}, { + } + ] + }, + { "index_file": "https://adafruit.github.io/arduino-board-index/package_adafruit_index.json", - "boards": [{ + "boards": [ + { "vid": "239a", "pid": "800b", "name": "Adafruit Feather M0", "package": "adafruit", "architecture": "samd", - "id": "adafruit_feather_m0" - }] -}, { + "id": "adafruit_feather_m0", + "target": "at91samdXX.cfg" + } + ] + }, + { "index_file": "https://raw.githubusercontent.com/VSChina/azureiotdevkit_tools/master/package_azureboard_index.json", - "boards": [{ + "boards": [ + { "vid": "0483", "pid": "374b", "name": "MXCHIP AZ3166", "package": "AZ3166", "architecture": "stm32f4", - "id": "MXCHIP_AZ3166" - }] -}] \ No newline at end of file + "id": "MXCHIP_AZ3166", + "interface": "stlink-v2-1.cfg", + "target": "stm32f4x.cfg" + } + ] + } +] diff --git a/package.json b/package.json index 802371a7..d9ff7497 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "icon": "images/arduino.svg", "categories": [ "Languages", + "Debuggers", "Snippets", "Formatters" ], @@ -87,6 +88,310 @@ "title": "Arduino: Examples" } ], + "debuggers": [ + { + "type": "arduino", + "label": "Arduino", + "startSessionCommand": "arduino.debug.startSession", + "enableBreakpointsFor": { + "languageIds": [ + "c", + "cpp" + ] + }, + "configurationSnippets": [ + { + "label": "Arduino: Launch Debugger", + "description": "Debug Arduino sketch", + "body": { + "name": "Arduino", + "type": "arduino", + "request": "launch", + "program": "$${{file}}", + "cwd": "$${{workspaceRoot}}", + "MIMode": "gdb", + "targetArchitecture": "arm", + "miDebuggerPath": "", + "debugServerPath": "", + "debugServerArgs": "", + "customLaunchSetupCommands": [ + { + "text": "target remote localhost:3333" + }, + { + "text": "file $${{file}}" + }, + { + "text": "load" + }, + { + "text": "monitor reset halt" + }, + { + "text": "monitor reset init" + } + ], + "stopAtEntry": true, + "serverStarted": "Info\\\\ :\\\\ [\\\\w\\\\d\\\\.]*:\\\\ hardware", + "launchCompleteCommand": "exec-continue", + "filterStderr": true, + "args": [] + } + } + ], + "initialConfigurations": [ + { + "name": "Arduino", + "type": "arduino", + "request": "launch", + "program": "${file}", + "cwd": "${workspaceRoot}", + "MIMode": "gdb", + "targetArchitecture": "arm", + "miDebuggerPath": "", + "debugServerPath": "", + "debugServerArgs": "", + "customLaunchSetupCommands": [ + { + "text": "target remote localhost:3333" + }, + { + "text": "file ${file}" + }, + { + "text": "load" + }, + { + "text": "monitor reset halt" + }, + { + "text": "monitor reset init" + } + ], + "stopAtEntry": true, + "serverStarted": "Info\\ :\\ [\\w\\d\\.]*:\\ hardware", + "launchCompleteCommand": "exec-continue", + "filterStderr": true, + "args": [] + } + ], + "configurationAttributes": { + "launch": { + "required": [ + "program" + ], + "properties": { + "program": { + "type": "string", + "description": "Full path to program executable.", + "default": "${workspaceRoot}/arduino.elf" + }, + "args": { + "type": "array", + "description": "Command line arguments passed to the program.", + "items": { + "type": "string" + }, + "default": [] + }, + "type": { + "type": "string", + "description": "The type of the engine.", + "default": "arduino" + }, + "targetArchitecture": { + "type": "string", + "description": "The architecture of the debuggee.", + "default": "arm" + }, + "cwd": { + "type": "string", + "description": "The working directory of the target", + "default": "." + }, + "setupCommands": { + "type": "array", + "description": "One or more GDB commands to execute in order to setup the underlying debugger. Example: \"setupCommands\": [ { \"text\": \"-enable-pretty-printing\", \"description\": \"Enable GDB pretty printing\", \"ignoreFailures\": true }].", + "items": { + "type": "object", + "properties": { + "text": { + "type": "string", + "description": "The debugger command to execute.", + "default": "" + }, + "description": { + "type": "string", + "description": "Optional description for the command.", + "default": "" + }, + "ignoreFailures": { + "type": "boolean", + "description": "If true, failures from the command should be ignored. Default value is false.", + "default": "false" + } + } + }, + "default": [] + }, + "customLaunchSetupCommands": { + "type": "array", + "description": "If provided, this replaces the default commands used to launch a target with some other commands. For example, this can be \"-target-attach\" in order to attach to a target process. An empty command list replaces the launch commands with nothing, which can be useful if the debugger is being provided launch options as command line options. Example: \"customLaunchSetupCommands\": [ { \"text\": \"target-run\", \"description\": \"run target\", \"ignoreFailures\": false }].", + "items": { + "type": "object", + "properties": { + "text": { + "type": "string", + "description": "The debugger command to execute.", + "default": "" + }, + "description": { + "type": "string", + "description": "Optional description for the command.", + "default": "" + }, + "ignoreFailures": { + "type": "boolean", + "description": "If true, failures from the command should be ignored. Default value is false.", + "default": "" + } + } + }, + "default": [] + }, + "launchCompleteCommand": { + "enum": [ + "exec-run", + "exec-continue", + "None" + ], + "description": "The command to execute after the debugger is fully setup in order to cause the target process to run. Allowed values are \"exec-run\", \"exec-continue\", \"None\". The default value is \"exec-run\".", + "default": "exec-run" + }, + "visualizerFile": { + "type": "string", + "description": ".natvis file to be used when debugging this process. This option is not compatible with GDB pretty printing. Please also see \"showDisplayString\" if using this setting.", + "default": "" + }, + "showDisplayString": { + "type": "boolean", + "description": "When a visualizerFile is specified, showDisplayString will enable the display string. Turning this option on can cause slower performance during debugging.", + "default": "true" + }, + "additionalSOLibSearchPath": { + "type": "string", + "description": "Semicolon separated list of directories to use to search for .so files. Example: \"c:\\dir1;c:\\dir2\".", + "default": "" + }, + "MIMode": { + "type": "string", + "description": "Indicates the console debugger that the MIDebugEngine will connect to. Allowed values are \"gdb\" \"lldb\".", + "default": "gdb" + }, + "miDebuggerPath": { + "type": "string", + "description": "The path to the mi debugger (such as gdb). When unspecified, it will search path first for the debugger.", + "default": "/usr/bin/gdb" + }, + "miDebuggerServerAddress": { + "type": "string", + "description": "Network address of the MI Debugger Server to connect to (example: localhost:1234).", + "default": "serveraddress:port" + }, + "stopAtEntry": { + "type": "boolean", + "description": "Optional parameter. If true, the debugger should stop at the entrypoint of the target. If processId is passed, has no effect.", + "default": false + }, + "debugServerPath": { + "type": "string", + "description": "Optional full path to debug server to launch. Defaults to null.", + "default": "" + }, + "debugServerArgs": { + "type": "string", + "description": "Optional debug server args. Defaults to null.", + "default": "" + }, + "serverStarted": { + "type": "string", + "description": "Optional server-started pattern to look for in the debug server output. Defaults to null.", + "default": "" + }, + "filterStdout": { + "type": "boolean", + "description": "Search stdout stream for server-started pattern and log stdout to debug output. Defaults to true.", + "default": "true" + }, + "filterStderr": { + "type": "boolean", + "description": "Search stderr stream for server-started pattern and log stderr to debug output. Defaults to false.", + "default": "false" + }, + "serverLaunchTimeout": { + "type": "integer", + "description": "Optional time, in milliseconds, for the debugger to wait for the debugServer to start up. Default is 10000.", + "default": "10000" + }, + "coreDumpPath": { + "type": "string", + "description": "Optional full path to a core dump file for the specified program. Defaults to null.", + "default": "" + }, + "externalConsole": { + "type": "boolean", + "description": "If true, a console is launched for the debuggee. If false, no console is launched. Note this option is ignored in some cases for technical reasons.", + "default": "false" + }, + "sourceFileMap": { + "type": "object", + "description": "Optional source file mappings passed to the debug engine. Example: '{ \"/original/source/path\":\"/current/source/path\" }'", + "default": { + "": "" + } + }, + "logging": { + "type": "object", + "description": "Optional flags to determine what types of messages should be logged to the Debug Console.", + "default": {}, + "properties": { + "exceptions": { + "type": "boolean", + "description": "Optional flag to determine whether exception messages should be logged to the Debug Console. Defaults to true.", + "default": true + }, + "moduleLoad": { + "type": "boolean", + "description": "Optional flag to determine whether module load events should be logged to the Debug Console. Defaults to true.", + "default": true + }, + "programOutput": { + "type": "boolean", + "description": "Optional flag to determine whether program output should be logged to the Debug Console. Defaults to true.", + "default": true + }, + "engineLogging": { + "type": "boolean", + "description": "Optional flag to determine whether diagnostic engine logs should be logged to the Debug Console. Defaults to false.", + "default": false + }, + "trace": { + "type": "boolean", + "description": "Optional flag to determine whether diagnostic adapter command tracing should be logged to the Debug Console. Defaults to false.", + "default": false + }, + "traceResponse": { + "type": "boolean", + "description": "Optional flag to determine whether diagnostic adapter command and response tracing should be logged to the Debug Console. Defaults to false.", + "default": false + } + } + } + } + } + } + } + ], "keybindings": [ { "command": "arduino.verify", diff --git a/src/arduino/arduino.ts b/src/arduino/arduino.ts index 403b6af3..f7870c07 100644 --- a/src/arduino/arduino.ts +++ b/src/arduino/arduino.ts @@ -133,7 +133,7 @@ export class ArduinoApp { }); } - public async verify() { + public async verify(output: string = "") { const dc = DeviceContext.getIntance(); const boardDescriptor = this.getBoardBuildString(dc); if (!boardDescriptor) { @@ -157,12 +157,22 @@ export class ArduinoApp { if (VscodeSettings.getIntance().logLevel === "verbose") { args.push("--verbose"); } + if (output) { + const outputPath = path.join(vscode.workspace.rootPath, output); + args.push("--pref", `build.path=${outputPath}`); + } + arduinoChannel.show(); - await util.spawn(this._settings.commandPath, arduinoChannel.channel, args).then((result) => { + // we need to return the result of verify + try { + const result = await util.spawn(this._settings.commandPath, arduinoChannel.channel, args); arduinoChannel.end(`Finished verify sketch - ${dc.sketch}${os.EOL}`); - }, (reason) => { + return true; + } catch (reason) { arduinoChannel.error(`Exit with code=${reason.code}${os.EOL}`); - }); + return false; + } + } // Add selected library path to the intellisense search path. diff --git a/src/arduino/board.ts b/src/arduino/board.ts index d2af81aa..2a9d4076 100644 --- a/src/arduino/board.ts +++ b/src/arduino/board.ts @@ -132,7 +132,7 @@ export class Board implements IBoard { return false; } - private getPackageName() { + public getPackageName() { return this.platform.packageName ? this.platform.packageName : this.platform.package.name; } } diff --git a/src/arduino/package.ts b/src/arduino/package.ts index 88d58866..050fdf4e 100644 --- a/src/arduino/package.ts +++ b/src/arduino/package.ts @@ -216,4 +216,9 @@ export interface IBoard { * Upldate the configuration */ updateConfig(configId: string, optionId: string): boolean; + + /** + * Get the board package name + */ + getPackageName(); } diff --git a/src/common/platform.ts b/src/common/platform.ts index f666d1c1..01dfab04 100644 --- a/src/common/platform.ts +++ b/src/common/platform.ts @@ -23,3 +23,10 @@ export function validateArduinoPath(arduinoPath: string): boolean { export function findFile(fileName: string, cwd: string): string { return internalSysLib.findFile(fileName, cwd); } + +export function getExecutableFileName(fileName: string): string { + if (isWindows) { + return `${fileName}.exe`; + } + return fileName; +} diff --git a/src/common/sys/darwin.ts b/src/common/sys/darwin.ts index 56130982..3b3c9562 100644 --- a/src/common/sys/darwin.ts +++ b/src/common/sys/darwin.ts @@ -26,16 +26,17 @@ export function validateArduinoPath(arduinoPath: string): boolean { } export function findFile(fileName: string, cwd: string): string { - let result; + let pathString; try { - result = childProcess.execSync("find ${cwd} -name ${fileName} -type f", { encoding: "utf8" }); - result = path.resolve(result).trim(); + pathString = childProcess.execSync(`find ${cwd} -name ${fileName} -type f`, { encoding: "utf8" }).split("\n"); - if (fileExistsSync(result)) { - result = path.normalize(result); + if (pathString && pathString[0] && fileExistsSync(pathString[0].trim())) { + pathString = path.normalize(pathString[0].trim()); + } else { + pathString = null; } } catch (ex) { // Ignore the errors. } - return result; + return pathString; } diff --git a/src/common/sys/linux.ts b/src/common/sys/linux.ts index 8fabcaa7..d3996c69 100644 --- a/src/common/sys/linux.ts +++ b/src/common/sys/linux.ts @@ -29,11 +29,12 @@ export function validateArduinoPath(arduinoPath: string): boolean { export function findFile(fileName: string, cwd: string): string { let pathString; try { - pathString = childProcess.execSync("find ${cwd} -name ${fileName} -type f", { encoding: "utf8" }); - pathString = path.resolve(pathString).trim(); + pathString = childProcess.execSync(`find ${cwd} -name ${fileName} -type f`, { encoding: "utf8" }).split("\n"); - if (fileExistsSync(pathString)) { - pathString = path.normalize(pathString); + if (pathString && pathString[0] && fileExistsSync(pathString[0].trim())) { + pathString = path.normalize(pathString[0].trim()); + } else { + pathString = null; } } catch (ex) { // Ignore the errors. diff --git a/src/common/sys/win32.ts b/src/common/sys/win32.ts index 12cda96e..21761c08 100644 --- a/src/common/sys/win32.ts +++ b/src/common/sys/win32.ts @@ -32,11 +32,9 @@ export function findFile(fileName: string, cwd: string): string { let result; try { let pathString; - pathString = childProcess.execSync(`dir ${fileName} /S /B`, {encoding: "utf8", cwd}); - pathString = path.resolve(pathString).trim(); - - if (fileExistsSync(pathString)) { - result = path.normalize(pathString); + pathString = childProcess.execSync(`dir ${fileName} /S /B`, {encoding: "utf8", cwd}).split("\n"); + if (pathString && pathString[0] && fileExistsSync(pathString[0].trim())) { + result = path.normalize(pathString[0].trim()); } } catch (ex) { // Ignore the errors. diff --git a/src/common/util.ts b/src/common/util.ts index 2d045bdb..3cfe10f5 100644 --- a/src/common/util.ts +++ b/src/common/util.ts @@ -295,7 +295,7 @@ export function padStart(sourceString: string, targetLength: number, padString?: return sourceString; } - if (!String.prototype.padStart) { + if (!(String.prototype as any).padStart) { // https://github.com/uxitten/polyfill/blob/master/string.polyfill.js padString = String(padString || " "); if (sourceString.length > targetLength) { @@ -308,7 +308,7 @@ export function padStart(sourceString: string, targetLength: number, padString?: return padString.slice(0, targetLength) + sourceString; } } else { - return sourceString.padStart(targetLength, padString); + return (sourceString as any).padStart(targetLength, padString); } } @@ -365,3 +365,7 @@ export function getRegistryValues(hive: string, key: string, name: string): Prom } }); } + +export function convertToHex(number, width = 0) { + return padStart(number.toString(16), width, "0"); +} diff --git a/src/debug/configurator.ts b/src/debug/configurator.ts new file mode 100644 index 00000000..21fb1126 --- /dev/null +++ b/src/debug/configurator.ts @@ -0,0 +1,174 @@ +/*-------------------------------------------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *-------------------------------------------------------------------------------------------*/ + +import * as childProcess from "child_process"; +import * as fs from "fs"; +import * as path from "path"; +import * as vscode from "vscode"; + +import { ArduinoApp } from "../arduino/arduino"; +import { ArduinoSettings } from "../arduino/arduinoSettings"; +import { BoardManager } from "../arduino/boardManager"; +import * as platform from "../common/platform"; +import * as util from "../common/util"; +import { DeviceContext } from "../deviceContext"; +import { DebuggerManager } from "./debuggerManager"; + +/** + * Automatically generate the Arduino board's debug settings. + */ +export class DebugConfigurator { + private _debuggerManager: DebuggerManager; + constructor( + private _extensionRoot: string, + private _arduinoApp: ArduinoApp, + private _arduinoSettings: ArduinoSettings, + private _boardManager: BoardManager, + ) { + this._debuggerManager = new DebuggerManager(_extensionRoot, _arduinoSettings, _boardManager); + } + + public async run(config) { + if (!this._debuggerManager.initialized) { + this._debuggerManager.initialize(); + } + // Default settings: + if (!config.request) { + config = { + name: "Arduino", + type: "arduino", + request: "launch", + program: "${file}", + cwd: "${workspaceRoot}", + MIMode: "gdb", + logging: { + engineLogging: true, + }, + targetArchitecture: "arm", + customLaunchSetupCommands: [ + { + text: "target remote localhost:3333", + }, + { + text: "file ${file}", + }, + { + text: "load", + }, + { + text: "monitor reset halt", + }, + { + text: "monitor reset init", + }, + ], + stopAtEntry: true, + serverStarted: "Info\\ :\\ [\\w\\d\\.]*:\\ hardware", + launchCompleteCommand: "exec-continue", + filterStderr: true, + args: [], + }; + } + + if (!this.resolveOpenOcd(config)) { + return; + } + if (!await this.resolveOpenOcdOptions(config)) { + return; + } + + if (!this.resolveDebuggerPath(config)) { + return; + } + + if (!await this.resolveProgramPath(config)) { + return; + } + + // Use the C++ debugger MIEngine as the real internal debugger + config.type = "cppdbg"; + vscode.commands.executeCommand("vscode.startDebug", config); + } + + private async resolveProgramPath(config) { + const dc = DeviceContext.getIntance(); + + if (!config.program || config.program === "${file}") { + // make a unique temp folder because keeping same temp folder will corrupt the build when board is changed + const outputFolder = path.join(dc.output || `.build`, this._boardManager.currentBoard.board); + util.mkdirRecursivelySync(path.join(vscode.workspace.rootPath, outputFolder)); + config.program = path.join(vscode.workspace.rootPath, outputFolder, `${path.basename(dc.sketch)}.elf`); + + // always compile elf to make sure debug the right elf + if (!await this._arduinoApp.verify(outputFolder)) { + vscode.window.showErrorMessage("Failure to verify the program, please check output for details."); + return false; + } + + config.program = config.program.replace(/\\/g, "/"); + + config.customLaunchSetupCommands.forEach((obj) => { + if (obj.text && obj.text.indexOf("${file}") > 0) { + obj.text = obj.text.replace(/\$\{file\}/, config.program); + } + }); + } + if (!util.fileExistsSync(config.program)) { + vscode.window.showErrorMessage("Cannot find the elf file."); + return false; + } + return true; + } + + private resolveDebuggerPath(config) { + if (!config.miDebuggerPath) { + config.miDebuggerPath = platform.findFile(platform.getExecutableFileName("arm-none-eabi-gdb"), + path.join(this._arduinoSettings.packagePath, "packages", this._boardManager.currentBoard.getPackageName())); + } + if (!util.fileExistsSync(config.miDebuggerPath)) { + config.miDebuggerPath = this._debuggerManager.miDebuggerPath; + } + if (!util.fileExistsSync(config.miDebuggerPath)) { + vscode.window.showErrorMessage("Cannot find the debugger path."); + return false; + } + return true; + } + + private resolveOpenOcd(config) { + const dc = DeviceContext.getIntance(); + if (!config.debugServerPath) { + config.debugServerPath = platform.findFile(platform.getExecutableFileName("openocd"), + path.join(this._arduinoSettings.packagePath, "packages", + this._boardManager.currentBoard.getPackageName())); + } + if (!util.fileExistsSync(config.debugServerPath)) { + config.debugServerPath = this._debuggerManager.debugServerPath; + } + if (!util.fileExistsSync(config.debugServerPath)) { + vscode.window.showErrorMessage("Cannot find the OpenOCD from the launch.json debugServerPath property." + + "Please input the right path of OpenOCD"); + return false; + } + + return true; + } + + private async resolveOpenOcdOptions(config) { + + if (config.debugServerPath && !config.debugServerArgs) { + try { + config.debugServerArgs = await this._debuggerManager.resolveOpenOcdOptions(config); + if (!config.debugServerArgs) { + return false; + } + } catch (error) { + vscode.window.showErrorMessage(error.message); + return false; + } + } + return true; + } +} diff --git a/src/debug/debuggerManager.ts b/src/debug/debuggerManager.ts new file mode 100644 index 00000000..15c3df0a --- /dev/null +++ b/src/debug/debuggerManager.ts @@ -0,0 +1,154 @@ +import * as fs from "fs"; +import * as path from "path"; +import * as vscode from "vscode"; + +import { ArduinoSettings } from "../arduino/arduinoSettings"; +import { BoardManager } from "../arduino/boardManager"; +import * as platform from "../common/platform"; +import * as util from "../common/util"; +import { DeviceContext } from "../deviceContext"; + +export class DebuggerManager { + private _initialized: boolean = false; + private _usbDector; + private _debugServerPath: string; + private _miDebuggerPath: string; + private _debuggerMappings: any = {}; + private _debuggerBoardMappings: any = {}; + constructor( + private _extensionRoot: string, + private _arduinoSettings: ArduinoSettings, + private _boardManager: BoardManager) { + } + + public initialize() { + const debugFileContent = fs.readFileSync(path.join(this._extensionRoot, "misc", "debuggerUsbMapping.json"), "utf8"); + const usbFileContent = fs.readFileSync(path.join(this._extensionRoot, "misc", "usbmapping.json"), "utf8"); + + for (const _debugger of JSON.parse(debugFileContent)) { + if (Array.isArray(_debugger.pid)) { + for (const pid of _debugger.pid) { + this._debuggerMappings[`${pid}%${_debugger.vid}`] = { ..._debugger, pid, vid: _debugger.vid }; + } + } else { + this._debuggerMappings[`${_debugger.pid}%${_debugger.vid}`] = { ..._debugger, pid: _debugger.pid, vid: _debugger.vid }; + } + } + for (const config of JSON.parse(usbFileContent)) { + for (const board of config.boards) { + if (board.interface || board.target) { + this._debuggerBoardMappings[[board.package, board.architecture, board.id].join(":")] = board; + } + } + } + this._usbDector = require("../../../vendor/node-usb-native").detector; + this._debugServerPath = platform.findFile(platform.getExecutableFileName("openocd"), + path.join(this._arduinoSettings.packagePath, "packages")); + if (!util.fileExistsSync(this._debugServerPath)) { + this._debugServerPath = ""; + } + + this._miDebuggerPath = platform.findFile(platform.getExecutableFileName("arm-none-eabi-gdb"), + path.join(this._arduinoSettings.packagePath, "packages")); + if (!util.fileExistsSync(this._miDebuggerPath)) { + this._miDebuggerPath = ""; + } + this._initialized = true; + } + public get initialized(): boolean { + return this._initialized; + } + public get miDebuggerPath(): string { + return this._miDebuggerPath; + } + + public get debugServerPath(): string { + return this._debugServerPath; + } + + public async listDebuggers(): Promise { + const usbDeviceList = await this._usbDector.find(); + const keys = []; + const results = []; + usbDeviceList.forEach((device) => { + if (device.vendorId && device.productId) { + /* tslint:disable:max-line-length*/ + const key = util.convertToHex(device.productId, 4) + "%" + util.convertToHex(device.vendorId, 4); + const relatedDebugger = this._debuggerMappings[key]; + if (relatedDebugger && keys.indexOf(key) < 0) { + keys.push(key); + results.push(relatedDebugger); + } + } + }); + return results; + } + + public async resolveOpenOcdOptions(config): Promise { + const board = this._boardManager.currentBoard.key; + const debugConfig = this._debuggerBoardMappings[board]; + const dc = DeviceContext.getIntance(); + const debuggerConfiged: string = dc.debugger_; + if (!debugConfig) { + throw new Error(`Debug for board ${this._boardManager.currentBoard.name} is not supported by now.`); + } + let resolvedDebugger; + const debuggers = await this.listDebuggers(); + if (!debuggers.length) { + throw new Error(`No supported debuggers are connected.`); + } + // rule 1: if this board has debuggers, use its own debugger + if (debugConfig.interface) { + resolvedDebugger = debuggers.find((_debugger) => { + return _debugger.short_name === debugConfig.interface || _debugger.config_file === debugConfig.interface; + }); + if (!resolvedDebugger) { + throw new Error(`Debug port for board ${this._boardManager.currentBoard.name} is not connected.`); + } + } + // rule 2: if there is only one debugger, use the only debugger + if (!resolvedDebugger && !debuggerConfiged && debuggers.length === 1) { + resolvedDebugger = debuggers[0]; + } + + // rule 3: if there is any configuration about debugger, use this configuration + if (!resolvedDebugger && debuggerConfiged) { + resolvedDebugger = debuggers.find((_debugger) => { + return _debugger.short_name === debuggerConfiged || _debugger.config_file === debuggerConfiged; + }); + } + if (!resolvedDebugger) { + const chosen = await vscode.window.showQuickPick(debuggers.map((l): vscode.QuickPickItem => { + return { + description: `(0x${l.vid}:0x${l.pid})`, + label: l.name, + }; + }).sort((a, b): number => { + return a.label === b.label ? 0 : (a.label > b.label ? 1 : -1); + }), { placeHolder: "Select a debugger" }); + if (chosen && chosen.label) { + resolvedDebugger = debuggers.find((_debugger) => _debugger.name === chosen.label); + if (resolvedDebugger) { + dc.debugger_ = resolvedDebugger.config_file; + } + } + if (!resolvedDebugger) { + return ""; + } + } + + const debugServerPath = config.debugServerPath; + let scriptsFolder = path.join(path.dirname(debugServerPath), "../scripts/"); + if (!util.directoryExistsSync(scriptsFolder)) { + scriptsFolder = path.join(path.dirname(debugServerPath), "../share/openocd/scripts/"); + } + if (!util.directoryExistsSync(scriptsFolder)) { + throw new Error("Cannot find scripts folder from openocd."); + } + if (resolvedDebugger.config_file.includes("jlink")) { + // only swd is supported now + return `-s ${scriptsFolder} -f interface/${resolvedDebugger.config_file} -c "transport select swd" -f target/${debugConfig.target}`; + } + return `-s ${scriptsFolder} -f interface/${resolvedDebugger.config_file} -f target/${debugConfig.target}`; + } +} diff --git a/src/deviceContext.ts b/src/deviceContext.ts index 7d17dfc5..d4501639 100644 --- a/src/deviceContext.ts +++ b/src/deviceContext.ts @@ -38,6 +38,17 @@ export interface IDeviceContext { */ sketch: string; + /** + * Arduino build output path + */ + + output: string; + /** + * Arduino debugger + */ + + debugger_: string; + /** * Arduino custom board configuration * @property {string} @@ -65,6 +76,10 @@ export class DeviceContext implements IDeviceContext, vscode.Disposable { private _sketch: string; + private _output: string; + + private _debugger: string; + private _configuration: string; private _arduinoApp: ArduinoApp; @@ -133,6 +148,8 @@ export class DeviceContext implements IDeviceContext, vscode.Disposable { this._board = deviceConfigJson.board; this._sketch = deviceConfigJson.sketch; this._configuration = deviceConfigJson.configuration; + this._output = deviceConfigJson.output; + this._debugger = deviceConfigJson._debugger; this._onDidChange.fire(); } else { Logger.notifyUserError("arduinoFileError", new Error(constants.messages.ARDUINO_FILE_ERROR)); @@ -142,6 +159,8 @@ export class DeviceContext implements IDeviceContext, vscode.Disposable { this._board = null; this._sketch = null; this._configuration = null; + this._output = null; + this._debugger = null; this._onDidChange.fire(); } return this; @@ -164,6 +183,8 @@ export class DeviceContext implements IDeviceContext, vscode.Disposable { deviceConfigJson.sketch = this.sketch; deviceConfigJson.port = this.port; deviceConfigJson.board = this.board; + deviceConfigJson.output = this.output; + deviceConfigJson._debugger = this.debugger_; deviceConfigJson.configuration = this.configuration; util.mkdirRecursivelySync(path.dirname(deviceConfigFile)); @@ -201,6 +222,24 @@ export class DeviceContext implements IDeviceContext, vscode.Disposable { this.saveContext(); } + public get output() { + return this._output; + } + + public set output(value: string) { + this._output = value; + this.saveContext(); + } + + public get debugger_() { + return this._debugger; + } + + public set debugger_(value: string) { + this._debugger = value; + this.saveContext(); + } + public get configuration() { return this._configuration; } diff --git a/src/extension.ts b/src/extension.ts index 6f61123c..d6fa25b8 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -13,6 +13,7 @@ import { BoardManager } from "./arduino/boardManager"; import { ExampleManager } from "./arduino/exampleManager"; import { LibraryManager } from "./arduino/libraryManager"; import { ARDUINO_MANAGER_PROTOCOL, ARDUINO_MODE, BOARD_CONFIG_URI, BOARD_MANAGER_URI, EXAMPLES_URI, LIBRARY_MANAGER_URI } from "./common/constants"; +import { DebugConfigurator } from "./debug/configurator"; import { DeviceContext } from "./deviceContext"; import { CompletionProvider } from "./langService/completionProvider"; import * as Logger from "./logger/logger"; @@ -128,7 +129,10 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(registerCommand("arduino.verify", async () => { if (!status.compile) { status.compile = "verify"; - await arduinoApp.verify(); + try { + await arduinoApp.verify(); + } catch (ex) { + } delete status.compile; } }, () => { @@ -138,7 +142,10 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(registerCommand("arduino.upload", async () => { if (!status.compile) { status.compile = "upload"; - await arduinoApp.upload(); + try { + await arduinoApp.upload(); + } catch (ex) { + } delete status.compile; } }, @@ -148,6 +155,20 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(registerCommand("arduino.addLibPath", (path) => arduinoApp.addLibPath(path))); + const arduinoConfigurator = new DebugConfigurator(context.extensionPath, arduinoApp, arduinoSettings, boardManager); + // Arduino debugger + context.subscriptions.push(registerCommand("arduino.debug.startSession", async (config) => { + if (!status.debug) { + status.debug = "debug"; + try { + await arduinoConfigurator.run(config); + } catch (ex) { + } + delete status.debug; + + } + })); + // serial monitor commands const serialMonitor = SerialMonitor.getIntance(); context.subscriptions.push(serialMonitor); diff --git a/src/serialmonitor/serialportctrl.ts b/src/serialmonitor/serialportctrl.ts index 9b10dc79..d61ea7fe 100644 --- a/src/serialmonitor/serialportctrl.ts +++ b/src/serialmonitor/serialportctrl.ts @@ -7,158 +7,165 @@ import * as os from "os"; import { OutputChannel, QuickPickItem, StatusBarAlignment, StatusBarItem, window } from "vscode"; interface ISerialPortDetail { - comName: string; - manufacturer: string; - vendorId: string; - productId: string; + comName: string; + manufacturer: string; + vendorId: string; + productId: string; } export class SerialPortCtrl { - public static list(): Promise { - return new Promise((resolve, reject) => { - SerialPortCtrl.serialport.list((e: any, ports: ISerialPortDetail[]) => { - if (e) { - reject(e); - } else { - resolve(ports); - } - }); - }); + public static get serialport(): any { + if (!SerialPortCtrl._serialport) { + SerialPortCtrl._serialport = require("../../../vendor/node-usb-native").SerialPort; } + return SerialPortCtrl._serialport; + } - private static serialport = require("../../../vendor/node-usb-native").SerialPort; + public static list(): Promise { + return new Promise((resolve, reject) => { + SerialPortCtrl.serialport.list((e: any, ports: ISerialPortDetail[]) => { + if (e) { + reject(e); + } else { + resolve(ports); + } + }); + }); + } - private _currentPort: string; - private _currentBaudRate: number; - private _currentSerialPort = null; + private static _serialport: any; - public constructor(port: string, baudRate: number, private _outputChannel: OutputChannel) { - this._currentBaudRate = baudRate; - this._currentPort = port; - } + private _currentPort: string; + private _currentBaudRate: number; + private _currentSerialPort = null; - public get isActive(): boolean { - return this._currentSerialPort && this._currentSerialPort.isOpen(); - } + public constructor(port: string, baudRate: number, private _outputChannel: OutputChannel) { + this._currentBaudRate = baudRate; + this._currentPort = port; + } - public get currentPort(): string { - return this._currentPort; - } + public get isActive(): boolean { + return this._currentSerialPort && this._currentSerialPort.isOpen(); + } + + public get currentPort(): string { + return this._currentPort; + } - public open(): Promise { - this._outputChannel.appendLine(`[Starting] Opening the serial port - ${this._currentPort}`); - return new Promise((resolve, reject) => { - if (this._currentSerialPort && this._currentSerialPort.isOpen()) { - this._currentSerialPort.close((err) => { - if (err) { - return reject(err); - } - this._currentSerialPort = null; - return this.open().then(() => { - resolve(); - }, (error) => { - reject(error); - }); - }); + public open(): Promise { + this._outputChannel.appendLine(`[Starting] Opening the serial port - ${this._currentPort}`); + return new Promise((resolve, reject) => { + if (this._currentSerialPort && this._currentSerialPort.isOpen()) { + this._currentSerialPort.close((err) => { + if (err) { + return reject(err); + } + this._currentSerialPort = null; + return this.open().then(() => { + resolve(); + }, (error) => { + reject(error); + }); + }); + } else { + this._currentSerialPort = new SerialPortCtrl.serialport(this._currentPort, { baudRate: this._currentBaudRate }); + this._outputChannel.show(); + this._currentSerialPort.on("open", () => { + this._currentSerialPort.write("TestingOpen", (err) => { + // TODO: Fix this on the serial port lib: https://github.com/EmergingTechnologyAdvisors/node-serialport/issues/795 + if (err && !(err.message.indexOf("Writing to COM port (GetOverlappedResult): Unknown error code 121") >= 0)) { + this._outputChannel.appendLine(`[Error] Failed to open the serial port - ${this._currentPort}`); + reject(err); } else { - this._currentSerialPort = new SerialPortCtrl.serialport(this._currentPort, { baudRate: this._currentBaudRate }); - this._outputChannel.show(); - this._currentSerialPort.on("open", () => { - this._currentSerialPort.write("TestingOpen", (err) => { - // TODO: Fix this on the serial port lib: https://github.com/EmergingTechnologyAdvisors/node-serialport/issues/795 - if (err && !(err.message.indexOf("Writing to COM port (GetOverlappedResult): Unknown error code 121") >= 0)) { - this._outputChannel.appendLine(`[Error] Failed to open the serial port - ${this._currentPort}`); - reject(err); - } else { - this._outputChannel.appendLine(`[Info] Opened the serial port - ${this._currentPort}`); - resolve(); - } - }); - }); - - this._currentSerialPort.on("data", (_event) => { - this._outputChannel.append(_event.toString()); - }); - - this._currentSerialPort.on("error", (_error) => { - this._outputChannel.appendLine("[Error]" + _error.toString()); - }); + this._outputChannel.appendLine(`[Info] Opened the serial port - ${this._currentPort}`); + resolve(); } + }); }); - } - - public sendMessage(text: string): Promise { - return new Promise((resolve, reject) => { - if (!text || !this._currentSerialPort || !this.isActive) { - resolve(); - return; - } - this._currentSerialPort.write(text, (error) => { - if (!error) { - resolve(); - } else { - return reject(error); - } - }); + this._currentSerialPort.on("data", (_event) => { + this._outputChannel.append(_event.toString()); }); - } - public changePort(newPort: string): Promise { - return new Promise((resolve, reject) => { - if (newPort === this._currentPort) { - resolve(); - return; - } - this._currentPort = newPort; - if (!this._currentSerialPort || !this.isActive) { - resolve(); - return; - } - this._currentSerialPort.close((err) => { - if (err) { - reject(err); - } else { - this._currentSerialPort = null; - resolve(); - } - }); + this._currentSerialPort.on("error", (_error) => { + this._outputChannel.appendLine("[Error]" + _error.toString()); }); - } + } + }); + } - public stop(): Promise { - return new Promise((resolve, reject) => { - if (!this._currentSerialPort || !this.isActive) { - resolve(false); - return; - } - this._currentSerialPort.close((err) => { - if (this._outputChannel) { - this._outputChannel.appendLine(`[Done] Closed the serial port ${os.EOL}`); - } - this._currentSerialPort = null; - if (err) { - reject(err); - } else { - resolve(true); - } - }); - }); - } - public changeBaudRate(newRate: number): Promise { - return new Promise((resolve, reject) => { - this._currentBaudRate = newRate; - if (!this._currentSerialPort || !this.isActive) { - resolve(); - return; - } - this._currentSerialPort.update({ baudRate: this._currentBaudRate }, (err) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); - } + public sendMessage(text: string): Promise { + return new Promise((resolve, reject) => { + if (!text || !this._currentSerialPort || !this.isActive) { + resolve(); + return; + } + + this._currentSerialPort.write(text, (error) => { + if (!error) { + resolve(); + } else { + return reject(error); + } + }); + }); + } + + public changePort(newPort: string): Promise { + return new Promise((resolve, reject) => { + if (newPort === this._currentPort) { + resolve(); + return; + } + this._currentPort = newPort; + if (!this._currentSerialPort || !this.isActive) { + resolve(); + return; + } + this._currentSerialPort.close((err) => { + if (err) { + reject(err); + } else { + this._currentSerialPort = null; + resolve(); + } + }); + }); + } + + public stop(): Promise { + return new Promise((resolve, reject) => { + if (!this._currentSerialPort || !this.isActive) { + resolve(false); + return; + } + this._currentSerialPort.close((err) => { + if (this._outputChannel) { + this._outputChannel.appendLine(`[Done] Closed the serial port ${os.EOL}`); + } + this._currentSerialPort = null; + if (err) { + reject(err); + } else { + resolve(true); + } + }); + }); + } + public changeBaudRate(newRate: number): Promise { + return new Promise((resolve, reject) => { + this._currentBaudRate = newRate; + if (!this._currentSerialPort || !this.isActive) { + resolve(); + return; + } + this._currentSerialPort.update({ baudRate: this._currentBaudRate }, (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + } } diff --git a/src/serialmonitor/usbDetector.ts b/src/serialmonitor/usbDetector.ts index 04c801c7..d168b8b7 100644 --- a/src/serialmonitor/usbDetector.ts +++ b/src/serialmonitor/usbDetector.ts @@ -16,7 +16,7 @@ import { SerialMonitor } from "./serialMonitor"; export class UsbDetector { - private _usbDector: null; + private _usbDector; private _boardDescriptors = null; @@ -39,8 +39,8 @@ export class UsbDetector { this._usbDector.on("add", (device) => { if (device.vendorId && device.productId) { const deviceDescriptor = this.getUsbDeviceDescriptor( - util.padStart(device.vendorId.toString(16), 4, "0"), // vid and pid both are 2 bytes long. - util.padStart(device.productId.toString(16), 4, "0"), + util.convertToHex(device.vendorId, 4), // vid and pid both are 2 bytes long. + util.convertToHex(device.productId, 4), this._extensionRoot); // Not supported device for discovery. @@ -79,7 +79,7 @@ export class UsbDetector { const currBoard = this._boardManager.currentBoard; if (currBoard.board !== deviceDescriptor.id || currBoard.platform.architecture !== deviceDescriptor.architecture - || currBoard.platform.package.name !== deviceDescriptor.package) { + || currBoard.getPackageName() !== deviceDescriptor.package) { vscode.window.showInformationMessage(`Detected board ${deviceDescriptor.name}. Would you like to switch to this board type?`, "Yes", "No") .then((ans) => { @@ -134,7 +134,7 @@ export class UsbDetector { }); } return this._boardDescriptors.find((obj) => { - return obj.vid === vendorId && obj.pid === productId; + return obj.vid === vendorId && (obj.pid === productId || (obj.pid.indexOf && obj.pid.indexOf(productId) >= 0)); }); } }