diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c9f877d..a0529ebd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,24 @@ # Change Log All notable changes to this project will be documented in this file. +## Version 0.4.0 + +### Added +- Support for Arduino CLI (#1017) + +### Changed +- Autogenerate c_cpp_properties.json with all complier arguments and libraries for IntelliSense (#1183) +- Detects available programmers for selected board (#1118) + +### Fixed +- Typos + +### Breaking Changes +- Unifies all build commands under a single + +### Known Issues +- Arduino CLI doesn't work on Mac (#1205) + ## Version 0.3.5 - Release date: November 22, 2020 diff --git a/README.md b/README.md index 30f60388..3bc3adff 100644 --- a/README.md +++ b/README.md @@ -16,11 +16,19 @@ Welcome to the Visual Studio Code extension for **Arduino** preview ! * Integrated Arduino Debugging New ## Prerequisites -The Arduino IDE is required. Please install it from the [download page](https://www.arduino.cc/en/main/software#download). +Either the Arduino IDE or Arduino CLI are required. + +### Arduino IDE +The Arduino IDE can be installed the Arduino [download page](https://www.arduino.cc/en/main/software#download). - The supported Arduino IDE versions are `1.6.x` and later. - The Windows Store's version of the Arduino IDE is not supported because of the sandbox environment that the application runs in. - *Note:* Arduino IDE `1.8.7` has some breaking changes, causing board package and library installation failures. +### Arduino CLI +The Arduino CLI can be downloaded from the repository's [release page](https://github.com/arduino/arduino-cli/releases/tag/0.13.0) +- The extension has only been tested with v0.13.0. +- If you use the CLI you will have to set `arduino.path` since the CLI does not have a defualt path. + ## Installation Open VS Code and press F1 or Ctrl + Shift + P to open command palette, select **Install Extension** and type `vscode-arduino`. @@ -48,12 +56,16 @@ This extension provides several commands in the Command Palette (F1 o - **Arduino: Select Serial Port**: Change the current serial port. - **Arduino: Send Text to Serial Port**: Send a line of text via the current serial port. - **Arduino: Upload**: Build sketch and upload to Arduino board. +- **Arduino: CLI Upload**: Upload complied code without building sketch (CLI only). - **Arduino: Upload Using Programmer**: Upload using an external programmer. +- **Arduino: CLI Upload Using Programmer**: Upload using an external programmer without building sketch (CLI only). - **Arduino: Verify**: Build sketch. +- **Arduino: Rebuild IntelliSense Configuration**: Forced/manual rebuild of the IntelliSense configuration. The extension analyzes Arduino's build output and sets the Intellisense include paths, defines, compiler arguments accordingly. ## Keybindings - **Arduino: Upload** Alt + Cmd + U *or* Alt + Ctrl + U - **Arduino: Verify** Alt + Cmd + R *or* Alt + Ctrl + R +- **Arduino: Rebuild IntelliSense Configuration** Alt + Cmd + I *or* Alt + Ctrl + I ## Options | Option | Description | @@ -62,11 +74,12 @@ This extension provides several commands in the Command Palette (F1 o | `arduino.commandPath` | Path to an executable (or script) relative to `arduino.path`. The default value is `arduino_debug.exe` for windows,`Contents/MacOS/Arduino` for Mac and `arduino` for Linux, You also can use a custom launch script to run Arduino by modifying this setting. (Requires a restart after change) Example: `run-arduino.bat` for Windows, `Contents/MacOS/run-arduino.sh` for Mac and `bin/run-arduino.sh` for Linux. | | `arduino.additionalUrls` | Additional Boards Manager URLs for 3rd party packages. You can have multiple URLs in one string with a comma(`,`) as separator, or have a string array. The default value is empty. | | `arduino.logLevel` | CLI output log level. Could be info or verbose. The default value is `"info"`. | -| `arduino.allowPDEFiletype` | Allow the VSCode Arduino extension to open .pde files from pre-1.0.0 versions of Ardiuno. Note that this will break Processing code. Default value is `false`. | +| `arduino.allowPDEFiletype` | Allow the VSCode Arduino extension to open .pde files from pre-1.0.0 versions of Ardiuno. Note that this will break Processing code. Default value is `false`. | | `arduino.enableUSBDetection` | Enable/disable USB detection from the VSCode Arduino extension. The default value is `true`. When your device is plugged in to your computer, it will pop up a message "`Detected board ****, Would you like to switch to this board type`". After clicking the `Yes` button, it will automatically detect which serial port (COM) is connected a USB device. If your device does not support this feature, please provide us with the PID/VID of your device; the code format is defined in `misc/usbmapping.json`.To learn more about how to list the vid/pid, use the following tools: https://github.com/EmergingTechnologyAdvisors/node-serialport `npm install -g serialport` `serialport-list -f jsonline`| | `arduino.disableTestingOpen` | Enable/disable automatic sending of a test message to the serial port for checking the open status. The default value is `false` (a test message will be sent). | | `arduino.skipHeaderProvider` | Enable/disable the extension providing completion items for headers. This functionality is included in newer versions of the C++ extension. The default value is `false`.| | `arduino.defaultBaudRate` | Default baud rate for the serial port monitor. The default value is 115200. Supported values are 300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 74880, 115200, 230400 and 250000 | +| `arduino.disableIntelliSenseAutoGen` | When `true` vscode-arduino will not auto-generate an IntelliSense configuration (i.e. `.vscode/c_cpp_properties.json`) by analyzing Arduino's compiler output. | The following Visual Studio Code settings are available for the Arduino extension. These can be set in global user preferences Ctrl + , or workspace settings (`.vscode/settings.json`). The latter overrides the former. @@ -75,7 +88,7 @@ The following Visual Studio Code settings are available for the Arduino extensio "arduino.path": "C:/Program Files (x86)/Arduino", "arduino.commandPath": "arduino_debug.exe", "arduino.logLevel": "info", - "arduino.allowPDEFiletype": false, + "arduino.allowPDEFiletype": false, "arduino.enableUSBDetection": true, "arduino.disableTestingOpen": false, "arduino.skipHeaderProvider": false, @@ -98,7 +111,9 @@ The following settings are as per sketch settings of the Arduino extension. You "board": "adafruit:samd:adafruit_feather_m0", "output": "../build", "debugger": "jlink", - "prebuild": "bash prebuild.sh" + "prebuild": "./prebuild.sh", + "postbuild": "./postbuild.sh", + "intelliSenseGen": "global" } ``` - `sketch` - The main sketch file name of Arduino. @@ -106,7 +121,68 @@ The following settings are as per sketch settings of the Arduino extension. You - `board` - Currently selected Arduino board alias. Can be set by the `Arduino: Change Board Type` command. Also, you can find the board list there. - `output` - Arduino build output path. If not set, Arduino will create a new temporary output folder each time, which means it cannot reuse the intermediate result of the previous build leading to long verify/upload time, so it is recommended to set the field. Arduino requires that the output path should not be the workspace itself or in a subfolder of the workspace, otherwise, it may not work correctly. By default, this option is not set. It's worth noting that the contents of this file could be deleted during the build process, so pick (or create) a directory that will not store files you want to keep. - `debugger` - The short name of the debugger that will be used when the board itself does not have a debugger and there is more than one debugger available. You can find the list of debuggers [here](https://github.com/Microsoft/vscode-arduino/blob/master/misc/debuggerUsbMapping.json). By default, this option is not set. -- `prebuild` - External command before building the sketch file. You should only set one `prebuild` command. `command1 && command2` does not work. If you need to run multiple commands before the build, then create a script. +- `prebuild` - External command which will be invoked before any sketch build (verify, upload, ...). For details see the [Pre- and Post-Build Commands](#Pre--and-Post-Build-Commands) section. +- `postbuild` - External command to be run after the sketch has been built successfully. See the afore mentioned section for more details. +- `intelliSenseGen` - Override the global setting for auto-generation of the IntelliSense configuration (i.e. `.vscode/c_cpp_properties.json`). Three options are available: + - `"global"`: Use the global settings (default) + - `"disable"`: Disable the auto-generation even if globally enabled + - `"enable"`: Enable the auto-generation even if globally disabled +- `buildPreferences` - Set Arduino preferences which then are used during any build (verify, upload, ...). This allows for extra defines, compiler options or includes. The preference key-value pairs must be set as follows: +```json + "buildPreferences": [ + ["build.extra_flags", "-DMY_DEFINE=666 -DANOTHER_DEFINE=3.14 -Wall"], + ["compiler.cpp.extra_flags", "-DYET_ANOTER=\"hello\""] + ] +} +``` + +## Pre- and Post-Build Commands +On Windows the commands run within a `cmd`-, on Linux and OSX within a `bash`-instance. Therefore your command can be anything what you can run within those shells. Instead of running a command you can invoke a script. This makes writing more complex pre-/post-build mechanisms much easier and opens up the possibility to run python or other scripting languages. +The commands run within the workspace root directory and vscode-arduino sets the following environment variables: +**`VSCA_BUILD_MODE`** The current build mode, one of `Verifying`, `Uploading`, `Uploading (programmer)` or `Analyzing`. This allows you to run your script on certain build modes only. +**`VSCA_SKETCH`** The sketch file relative to your workspace root directory. +**`VSCA_BOARD`** Your board and configuration, e.g. `arduino:avr:nano:cpu=atmega328`. +**`VSCA_WORKSPACE_DIR`** The absolute path of your workspace root directory. +**`VSCA_LOG_LEVEL`** The current log level. This allows you to control the verbosity of your scripts. +**`VSCA_SERIAL`** The serial port used for uploading. Not set if you haven't set one in your `arduino.json`. +**`VSCA_BUILD_DIR`** The build directory. Not set if you haven't set one in your `arduino.json`. + +For example under Windows the following `arduino.json` setup +```json +{ + "board": "arduino:avr:nano", + "sketch": "test.ino", + "configuration": "cpu=atmega328", + "prebuild": "IF \"%VSCA_BUILD_MODE%\"==\"Verifying\" (echo VSCA_BUILD_MODE=%VSCA_BUILD_MODE% && echo VSCA_BOARD=%VSCA_BOARD%)" +} +``` +will produce +``` +[Starting] Verifying sketch 'test.ino' +Running pre-build command: "IF "%VSCA_BUILD_MODE%"=="Verifying" (echo VSCA_BUILD_MODE=%VSCA_BUILD_MODE% && echo VSCA_BOARD=%VSCA_BOARD%)" +VSCA_BUILD_MODE=Verifying +VSCA_BOARD=arduino:avr:nano:cpu=atmega328 +Loading configuration... +<...> +``` +when verifying. + +## IntelliSense +vscode-arduino auto-configures IntelliSense by default. vscode-arduino analyzes Arduino's compiler output by running a separate build and generates the corresponding configuration file at `.vscode/c_cpp_properties.json`. vscode-arduino tries as hard as possible to keep things up to date, e.g. it runs the analysis when switching the board or the sketch. + +It doesn't makes sense though to run the analysis repeatedly. Therefore if the workspace reports problems ("squiggles") - for instance after adding new includes from a new library - run the analysis manually: + +Manual rebuild: **Arduino: Rebuild IntelliSense Configuration**, +Keybindings: Alt + Cmd + I *or* Alt + Ctrl + I + +When the analysis is invoked manually it ignores any global and project specific disable. + +### IntelliSense Configurations +vscode-arduino's analysis stores the result as a dedicated IntelliSense-configuration named `Arduino`. You have to select it from the far right of the status bar when you're in one of your source files as shown here: + +![74001156-cfce8280-496a-11ea-9b9d-7d30c83765c1](https://user-images.githubusercontent.com/21954933/74351237-2696ea80-4db7-11ea-9f7a-1bfc652ad5f5.png) + +This system allows you to setup and use own IntelliSense configurations in parallel to the automatically generated configurations provided through vscode-arduino. Just add your configuration to `c_cpp_properties.json` and name it differently from the default configuration (`Arduino`), e.g. `My awesome configuration` and select it from the status bar or via the command palette command **C/C++: Select a Configuration...** ## Debugging Arduino Code preview Before you start to debug your Arduino code, please read [this document](https://code.visualstudio.com/docs/editor/debugging) to learn about the basic mechanisms of debugging in Visual Studio Code. Also see [debugging for C++ in VSCode](https://code.visualstudio.com/docs/languages/cpp#_debugging) for further reference. diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index fdacfadc..95505d66 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -25,7 +25,7 @@ Visual Studio Code Extension for Arduino incorporates third party material from 18. vscode-extension-telemetry (https://github.com/Microsoft/vscode-extension-telemetry) 19. winreg (https://github.com/fresc81/node-winreg) 20. Winston (https://github.com/winstonjs/winston) - +21. cocopa (https://github.com/elektronikworkshop/cocopa) %% body-parser NOTICES, INFORMATION, AND LICENSE BEGIN HERE ========================================= @@ -532,4 +532,31 @@ THE SOFTWARE. ========================================= END OF Winston NOTICES, INFORMATION, AND LICENSE +%% cocopa NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +Copyright (C) 2020 Uli Franke - Elektronik Workshop + +All rights reserved. + +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF cocopa NOTICES, INFORMATION, AND LICENSE diff --git a/misc/arduinoValidator.json b/misc/arduinoValidator.json index 73baff4f..5386fc4f 100644 --- a/misc/arduinoValidator.json +++ b/misc/arduinoValidator.json @@ -32,6 +32,38 @@ "description": "Arduino Debugger Settings", "type": "string", "minLength": 1 + }, + "intelliSenseGen": { + "description": "Disable/enable the automatic generation of the IntelliSense configuration file (c_cpp_properties.json) for this project (overrides the global setting). When set to \"global\" the global extension settings will be used.", + "type": "string", + "default": "global", + "enum": [ + "global", + "disable", + "enable" + ] + }, + "prebuild": { + "description": "Command to be run before every build", + "type": "string", + "minLength": 1 + }, + "postbuild": { + "description": "Command to be run after every build", + "type": "string", + "minLength": 1 + }, + "buildPreferences": { + "description": "Arduino preferences which are passed to the Arduino back-end during build", + "type": "array", + "items": { + "type":"array", + "minItems": 2, + "maxItems": 2, + "items": { + "type": "string" + } + } } } -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 8229e3f9..50043aaa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "vscode-arduino", - "version": "0.3.5", + "version": "0.4.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -219,8 +219,7 @@ "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", - "dev": true + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" }, "@types/compare-versions": { "version": "3.3.0", @@ -485,15 +484,15 @@ } }, "ajv": { - "version": "6.12.4", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", - "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", "dev": true, "requires": { - "fast-deep-equal": "^3.1.1", + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "json-schema-traverse": "^0.3.0" } }, "ajv-errors": { @@ -546,7 +545,6 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, "requires": { "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" @@ -556,7 +554,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -564,8 +561,7 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" } } }, @@ -1561,6 +1557,38 @@ "semver": "^5.4.1" } }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "cocopa": { + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/cocopa/-/cocopa-0.0.13.tgz", + "integrity": "sha512-MKWdCbaC9MpT+CMNXRP2rufpIndIaCKY1KD3XXwg6I2jNI1uFjEKNGXObL1mJ+7X53FTzKaZydJbPinCG+WJTQ==", + "requires": { + "chalk": "^3.0.0", + "commander": "^4.1.1", + "shlex": "^2.0.1" + }, + "dependencies": { + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==" + } + } + }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -2429,6 +2457,18 @@ "v8-compile-cache": "^2.0.3" }, "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, "ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", @@ -2444,6 +2484,12 @@ "ms": "^2.1.1" } }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, "glob-parent": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", @@ -2459,6 +2505,12 @@ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -2944,9 +2996,9 @@ } }, "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", "dev": true }, "fast-json-stable-stringify": { @@ -3507,12 +3559,6 @@ "dev": true, "optional": true }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, @@ -5046,9 +5092,9 @@ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, "inquirer": { "version": "7.3.3", @@ -5472,9 +5518,9 @@ "dev": true }, "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", "dev": true }, "json-stable-stringify-without-jsonify": { @@ -7648,6 +7694,11 @@ "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" }, + "shlex": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/shlex/-/shlex-2.0.2.tgz", + "integrity": "sha512-i4p9nNXgBTILspHwZlBCNsZzwuVWW8SFx5dyIONrjL0R+AbMOPbg7ndqgGfjYivkYRTtZMKqIT8HT+QyOhPQWA==" + }, "signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", @@ -8101,7 +8152,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, "requires": { "has-flag": "^4.0.0" }, @@ -8109,8 +8159,7 @@ "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" } } }, @@ -8136,6 +8185,18 @@ "string-width": "^3.0.0" }, "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, "ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", @@ -8148,12 +8209,24 @@ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", "dev": true }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", "dev": true }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", @@ -8544,9 +8617,9 @@ } }, "typescript": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", - "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==", + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", + "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==", "dev": true }, "unc-path-regex": { diff --git a/package.json b/package.json index 6629987b..a97d952f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "vscode-arduino", "displayName": "Arduino", "description": "Arduino for Visual Studio Code", - "version": "0.3.5", + "version": "0.4.0", "publisher": "vsciot-vscode", "aiKey": "83dd2c27-6594-41d3-85a9-bdb22070eb42", "preview": true, @@ -36,11 +36,13 @@ "*", "onCommand:arduino.verify", "onCommand:arduino.upload", + "onCommand:arduino.cliUpload", "onCommand:arduino.uploadUsingProgrammer", + "onCommand:arduiono.cliUploadUsingProgrammer", + "onCommand:arduino.rebuildIntelliSenseConfig", "onCommand:arduino.selectProgrammer", "onCommand:arduino.selectSerialPort", "onCommand:arduino.changeBaudRate", - "onCommand:arduino.addLibPath", "onCommand:arduino.openSerialMonitor", "onCommand:arduino.sendMessageToSerialPort", "onCommand:arduino.closeSerialMonitor", @@ -93,10 +95,22 @@ "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.rebuildIntelliSenseConfig", + "title": "Arduino: Rebuild IntelliSense Configuration" + }, { "command": "arduino.selectProgrammer", "title": "Arduino: Select Programmer" @@ -235,7 +249,7 @@ }, "targetArchitecture": { "type": "string", - "description": "The architecture of the debuggee.", + "description": "The architecture of the debugger.", "default": "arm" }, "cwd": { @@ -436,12 +450,22 @@ "command": "arduino.upload", "key": "ctrl+alt+u", "mac": "cmd+alt+u" + }, + { + "command": "arduino.rebuildIntelliSenseConfig", + "key": "ctrl+alt+i", + "mac": "cmd+alt+i" } ], "configuration": { "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": "", @@ -491,6 +515,11 @@ "arduino.defaultBaudRate": { "type": "number", "default": 115200 + }, + "arduino.disableIntelliSenseAutoGen": { + "type": "boolean", + "default": false, + "description": "When disabled vscode-arduino will not auto-generate an IntelliSense configuration (i.e. c_cpp_properties.json) by analyzing the compiler output." } } }, @@ -546,10 +575,11 @@ "devDependencies": { "@types/compare-versions": "^3.0.0", "@types/mocha": "^5.2.7", - "@types/node": "^6.0.40", + "@types/node": "^6.14.9", "@types/vscode": "^1.43.0", "@types/winreg": "^1.2.30", "acorn": "^7.4.0", + "ajv": "^5.0.0", "del": "^2.2.2", "eslint": "^6.8.0", "eslint-config-standard": "^10.2.1", @@ -570,12 +600,13 @@ "plugin-error": "^1.0.1", "tslint": "^5.20.1", "typemoq": "^1.6.0", - "typescript": "^2.2.1", + "typescript": "^3.7.5", "vscode-test": "^1.4.0", "webpack": "^4.44.1" }, "dependencies": { "body-parser": "^1.16.1", + "cocopa": "0.0.13", "compare-versions": "^3.4.0", "eventemitter2": "^4.1.0", "express": "^4.14.1", diff --git a/src/arduino/arduino.ts b/src/arduino/arduino.ts index 263a5069..785df236 100644 --- a/src/arduino/arduino.ts +++ b/src/arduino/arduino.ts @@ -9,12 +9,15 @@ import * as vscode from "vscode"; import * as constants from "../common/constants"; import * as util from "../common/util"; -import * as Logger from "../logger/logger"; +import * as logger from "../logger/logger"; import { DeviceContext } from "../deviceContext"; import { IArduinoSettings } from "./arduinoSettings"; import { BoardManager } from "./boardManager"; import { ExampleManager } from "./exampleManager"; +import { AnalysisManager, + isCompilerParserEnabled, + makeCompilerParserContext } from "./intellisense"; import { LibraryManager } from "./libraryManager"; import { VscodeSettings } from "./vscodeSettings"; @@ -24,6 +27,20 @@ import { SerialMonitor } from "../serialmonitor/serialMonitor"; import { UsbDetector } from "../serialmonitor/usbDetector"; import { ProgrammerManager } from "./programmerManager"; +/** + * Supported build modes. For further explanation see the documentation + * of ArduinoApp.build(). + * The strings are used for status reporting within the above function. + */ +export enum BuildMode { + Verify = "Verifying", + Analyze = "Analyzing", + Upload = "Uploading", + CliUpload = "Uploading using Arduino CLI", + UploadProgrammer = "Uploading (programmer)", + CliUploadProgrammer = "Uploading (programmer) using Arduino CLI", +}; + /** * Represent an Arduino application based on the official Arduino IDE. */ @@ -37,10 +54,30 @@ export class ArduinoApp { private _programmerManager: ProgrammerManager; + /** + * IntelliSense analysis manager. + * Makes sure that analysis builds and regular builds go along + * and that multiple subsequent analysis requests - as triggered + * by board/board-configuration changes - are bundled to a single + * analysis build run. + */ + private _analysisManager: AnalysisManager; + + /** + * Indicates if a build is currently in progress. + * If so any call to this.build() will return false immediately. + */ + private _building: boolean = false; + /** * @param {IArduinoSettings} _settings ArduinoSetting object. */ constructor(private _settings: IArduinoSettings) { + const analysisDelayMs = 1000 * 3; + this._analysisManager = new AnalysisManager( + () => this._building, + async () => { await this.build(BuildMode.Analyze); }, + analysisDelayMs); } /** @@ -63,6 +100,17 @@ export class ArduinoApp { } catch (ex) { } } + + // set up event handling for IntelliSense analysis + const requestAnalysis = async () => { + if (isCompilerParserEnabled()) { + await this._analysisManager.requestAnalysis(); + } + }; + const dc = DeviceContext.getInstance(); + dc.onChangeBoard(requestAnalysis); + dc.onChangeConfiguration(requestAnalysis); + dc.onChangeSketch(requestAnalysis); } /** @@ -87,381 +135,59 @@ export class ArduinoApp { public async setPref(key, value) { try { await util.spawn(this._settings.commandPath, - null, - ["--pref", `${key}=${value}`, "--save-prefs"]); + ["--pref", `${key}=${value}`, "--save-prefs"]); } catch (ex) { } } - public async upload() { - const dc = DeviceContext.getInstance(); - const boardDescriptor = this.getBoardBuildString(); - if (!boardDescriptor) { - return; - } - - 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.configuration || !/upload_method=[^=,]*st[^,]*link/i.test(dc.configuration)) && !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); - - if (dc.prebuild) { - arduinoChannel.info(`Run prebuild command: ${dc.prebuild}`); - const prebuildargs = dc.prebuild.split(" "); - const prebuildCommand = prebuildargs.shift(); - try { - await util.spawn(prebuildCommand, arduinoChannel.channel, prebuildargs, { shell: true, cwd: ArduinoWorkspace.rootPath }); - } catch (ex) { - arduinoChannel.error(`Run prebuild failed: \n${ex.error}`); - return; - } - } - - const appPath = path.join(ArduinoWorkspace.rootPath, dc.sketch); - const args = ["--upload", "--board", boardDescriptor]; - if (dc.port) { - args.push("--port", dc.port); - } - args.push(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; - } - - 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 (!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; - } - - 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}`); - }); + /** + * Returns true if a build is currently in progress. + */ + public get building() { + return this._building; } - public async verify(output: string = "") { - const dc = DeviceContext.getInstance(); - const boardDescriptor = this.getBoardBuildString(); - if (!boardDescriptor) { - return; - } - - 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); - } - - await vscode.workspace.saveAll(false); - - arduinoChannel.start(`Verify sketch - ${dc.sketch}`); - - if (dc.prebuild) { - arduinoChannel.info(`Run prebuild command: ${dc.prebuild}`); - const prebuildargs = dc.prebuild.split(" "); - const prebuildCommand = prebuildargs.shift(); - try { - await util.spawn(prebuildCommand, arduinoChannel.channel, prebuildargs, { shell: true, cwd: ArduinoWorkspace.rootPath }); - } catch (ex) { - arduinoChannel.error(`Run prebuild failed: \n${ex.error}`); - return; - } - } - - const appPath = path.join(ArduinoWorkspace.rootPath, dc.sketch); - const args = ["--verify", "--board", boardDescriptor, appPath]; - if (VscodeSettings.getInstance().logLevel === "verbose") { - args.push("--verbose"); - } - if (output || dc.output) { - const outputPath = path.resolve(ArduinoWorkspace.rootPath, output || dc.output); - const dirPath = path.dirname(outputPath); - if (!util.directoryExistsSync(dirPath)) { - Logger.notifyUserError("InvalidOutPutPath", new Error(constants.messages.INVALID_OUTPUT_PATH + outputPath)); - 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. Verify could be slow. See README."; - arduinoChannel.warning(msg); - } + /** + * Runs the arduino builder to build/compile and - if necessary - upload + * the current sketch. + * @param buildMode Build mode. + * * BuildMode.Upload: Compile and upload + * * BuildMode.UploadProgrammer: Compile and upload using the user + * selectable programmer + * * BuildMode.Analyze: Compile, analyze the output and generate + * IntelliSense configuration from it. + * * BuildMode.Verify: Just compile. + * All build modes except for BuildMode.Analyze run interactively, i.e. if + * something is missing, it tries to query the user for the missing piece + * of information (sketch, board, etc.). Analyze runs non interactively and + * just returns false. + * @param buildDir Override the build directory set by the project settings + * with the given directory. + * @returns true on success, false if + * * another build is currently in progress + * * board- or programmer-manager aren't initialized yet + * * or something went wrong during the build + */ + public async build(buildMode: BuildMode, buildDir?: string) { - arduinoChannel.show(); - // we need to return the result of verify - try { - await util.spawn(this._settings.commandPath, arduinoChannel.channel, args); - arduinoChannel.end(`Finished verify sketch - ${dc.sketch}${os.EOL}`); - return true; - } catch (reason) { - const msg = reason.code ? - `Exit with code=${reason.code}${os.EOL}` : - reason.message ? - reason.message : - JSON.stringify(reason); - arduinoChannel.error(msg); + if (!this._boardManager || !this._programmerManager || this._building) { return false; } - } - - public tryToUpdateIncludePaths() { - const configFilePath = path.join(ArduinoWorkspace.rootPath, constants.CPP_CONFIG_FILE); - if (!fs.existsSync(configFilePath)) { - return; - } - const cppConfigFile = fs.readFileSync(configFilePath, "utf8"); - const cppConfig = JSON.parse(cppConfigFile) as { configurations: Array<{ - includePath: string[], - forcedInclude: string[], - defines: string[], - }> }; - const libPaths = this.getDefaultPackageLibPaths(); - const defaultForcedInclude = this.getDefaultForcedIncludeFiles(); - const defines = this.getDefaultDefines(); - const configuration = cppConfig.configurations[0]; - - let cppConfigFileUpdated = false; - // cpp extension changes \\ to \\\\ in paths in JSON string, revert them first - configuration.includePath = configuration.includePath.map((path) => path.replace(/\\\\/g, "\\")); - configuration.forcedInclude = configuration.forcedInclude.map((path) => path.replace(/\\\\/g, "\\")); - configuration.defines = configuration.defines.map((path) => path.replace(/\\\\/g, "\\")); - - for (const libPath of libPaths) { - if (configuration.includePath.indexOf(libPath) === -1) { - cppConfigFileUpdated = true; - configuration.includePath.push(libPath); - } - } - for (const forcedIncludePath of defaultForcedInclude) { - if (configuration.forcedInclude.indexOf(forcedIncludePath) === -1) { - cppConfigFileUpdated = true; - configuration.forcedInclude.push(forcedIncludePath); - } - } - - for (const define of defines) { - if (configuration.defines.indexOf(define) === -1) { - cppConfigFileUpdated = true; - configuration.defines.push(define); - } - } - // remove all unexisting paths - // concern mistake removal, comment temporary - // for (let pathIndex = 0; pathIndex < configuration.includePath.length; pathIndex++) { - // let libPath = configuration.includePath[pathIndex]; - // if (libPath.indexOf("${workspaceFolder}") !== -1) { - // continue; - // } - // if (/\*$/.test(libPath)) { - // libPath = libPath.match(/^[^\*]*/)[0]; - // } - // if (!fs.existsSync(libPath)) { - // cppConfigFileUpdated = true; - // configuration.includePath.splice(pathIndex, 1); - // pathIndex--; - // } - // } - // for (let pathIndex = 0; pathIndex < configuration.forcedInclude.length; pathIndex++) { - // const forcedIncludePath = configuration.forcedInclude[pathIndex]; - // if (forcedIncludePath.indexOf("${workspaceFolder}") !== -1) { - // continue; - // } - // if (!fs.existsSync(forcedIncludePath)) { - // cppConfigFileUpdated = true; - // configuration.forcedInclude.splice(pathIndex, 1); - // pathIndex--; - // } - // } - - if (cppConfigFileUpdated) { - fs.writeFileSync(configFilePath, JSON.stringify(cppConfig, null, 4)); - } - } - - // Add selected library path to the intellisense search path. - public addLibPath(libraryPath: string) { - let libPaths; - if (libraryPath) { - libPaths = [libraryPath]; - } else { - libPaths = this.getDefaultPackageLibPaths(); - } - - const defaultForcedInclude = this.getDefaultForcedIncludeFiles(); - const defaultDefines = this.getDefaultDefines(); + this._building = true; - if (!ArduinoWorkspace.rootPath) { - return; - } - const configFilePath = path.join(ArduinoWorkspace.rootPath, constants.CPP_CONFIG_FILE); - let deviceContext = null; - if (!util.fileExistsSync(configFilePath)) { - util.mkdirRecursivelySync(path.dirname(configFilePath)); - deviceContext = {}; - } else { - deviceContext = util.tryParseJSON(fs.readFileSync(configFilePath, "utf8")); - } - if (!deviceContext) { - Logger.notifyAndThrowUserError("arduinoFileError", new Error(constants.messages.ARDUINO_FILE_ERROR)); - } - - deviceContext.configurations = deviceContext.configurations || []; - let configSection = null; - deviceContext.configurations.forEach((section) => { - if (section.name === util.getCppConfigPlatform()) { - configSection = section; - } - }); - - if (!configSection) { - configSection = { - name: util.getCppConfigPlatform(), - includePath: [], - }; - deviceContext.configurations.push(configSection); - } - - libPaths.forEach((childLibPath) => { - childLibPath = path.resolve(path.normalize(childLibPath)); - if (configSection.includePath && configSection.includePath.length) { - for (const existingPath of configSection.includePath) { - if (childLibPath === path.resolve(path.normalize(existingPath))) { - return; - } - } - } else { - configSection.includePath = []; - } - configSection.includePath.unshift(childLibPath); + return await this._build(buildMode, buildDir) + .then((ret) => { + this._building = false; + return ret; + }) + .catch((reason) => { + this._building = false; + logger.notifyUserError("ArduinoApp.build", + reason, + `Unhandled exception when cleaning up build "${buildMode}": ${JSON.stringify(reason)}`); + return false; }); - - if (!configSection.forcedInclude) { - configSection.forcedInclude = defaultForcedInclude; - } else { - for (let i = 0; i < configSection.forcedInclude.length; i++) { - if (/arduino\.h$/i.test(configSection.forcedInclude[i])) { - configSection.forcedInclude.splice(i, 1); - i--; - } - } - configSection.forcedInclude = defaultForcedInclude.concat(configSection.forcedInclude); - } - - if (!configSection.defines) { - configSection.defines = defaultDefines; - } - - fs.writeFileSync(configFilePath, JSON.stringify(deviceContext, null, 4)); } // Include the *.h header files from selected library to the arduino sketch. @@ -495,9 +221,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,24 +236,32 @@ 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}`]); - + if (this.useArduinoCli()) { + await util.spawn(this._settings.commandPath, + ["core", "install", `${packageName}${arch && ":" + arch}${version && "@" + version}`], + undefined, + { channel: showOutput ? arduinoChannel.channel : null }); + } else { + await util.spawn(this._settings.commandPath, + ["--install-boards", `${packageName}${arch && ":" + arch}${version && ":" + version}`], + undefined, + { channel: showOutput ? arduinoChannel.channel : null }); + } if (updatingIndex) { arduinoChannel.end("Updated package index files."); } else { @@ -548,6 +287,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,10 +303,17 @@ Please make sure the folder is not occupied by other procedures .`); arduinoChannel.start(`Install library - ${libName}`); } try { - await util.spawn(this._settings.commandPath, - showOutput ? arduinoChannel.channel : null, - ["--install-library", `${libName}${version && ":" + version}`]); - + if (this.useArduinoCli()) { + await util.spawn(this._settings.commandPath, + ["lib", "install", `${libName}${version && "@" + version}`], + undefined, + { channel: showOutput ? arduinoChannel.channel : undefined }); + } else { + await util.spawn(this._settings.commandPath, + ["--install-library", `${libName}${version && ":" + version}`], + undefined, + { channel: showOutput ? arduinoChannel.channel : undefined }); + } if (updatingIndex) { arduinoChannel.end("Updated library index files."); } else { @@ -586,62 +339,6 @@ Please make sure the folder is not occupied by other procedures .`); arduinoChannel.end(`Removed library - ${libName}${os.EOL}`); } - public getDefaultPackageLibPaths(): string[] { - const result = []; - const boardDescriptor = this._boardManager.currentBoard; - if (!boardDescriptor) { - return result; - } - const toolsPath = boardDescriptor.platform.rootBoardPath; - result.push(path.normalize(path.join(toolsPath, "**"))); - const hardwareToolPath = path.join(toolsPath, "..", "..", "tools"); - if (fs.existsSync(hardwareToolPath)) { - result.push(path.normalize(path.join(hardwareToolPath, "**"))); - } - - // Add default libraries to include path - result.push(path.normalize(path.join(this._settings.defaultLibPath, "**"))); - - const userLibsPath = (path.join(this._settings.sketchbookPath, "libraries", "**")); - result.push(userLibsPath); - // if (util.directoryExistsSync(path.join(toolsPath, "cores"))) { - // const coreLibs = fs.readdirSync(path.join(toolsPath, "cores")); - // if (coreLibs && coreLibs.length > 0) { - // coreLibs.forEach((coreLib) => { - // result.push(path.normalize(path.join(toolsPath, "cores", coreLib))); - // }); - // } - // } - // return result; - - // /hardware// -> /tools - const toolPath = path.join(toolsPath, "..", "..", "..", "tools"); - if (fs.existsSync(toolPath)) { - result.push(path.normalize(path.join(toolPath, "**"))); - } - return result; - } - - public getDefaultForcedIncludeFiles(): string[] { - const result = []; - const boardDescriptor = this._boardManager.currentBoard; - if (!boardDescriptor) { - return result; - } - const arduinoHeadFilePath = path.normalize(path.join(boardDescriptor.platform.rootBoardPath, "cores", "arduino", "Arduino.h")); - if (fs.existsSync(arduinoHeadFilePath)) { - result.push(arduinoHeadFilePath); - } - return result; - } - - public getDefaultDefines(): string[] { - const result = []; - // USBCON is required in order for Serial to be recognized by intellisense - result.push("USBCON"); - return result; - } - public openExample(example) { function tmpName(name) { let counter = 0; @@ -681,6 +378,7 @@ Please make sure the folder is not occupied by other procedures .`); const dc = DeviceContext.getInstance(); const arduinoJson = { sketch: sketchFile, + // TODO EW, 2020-02-18: COM1 is Windows specific - what about OSX and Linux users? port: dc.port || "COM1", board: dc.board, configuration: dc.configuration, @@ -688,43 +386,6 @@ Please make sure the folder is not occupied by other procedures .`); const arduinoConfigFilePath = path.join(destExample, constants.ARDUINO_CONFIG_FILE); util.mkdirRecursivelySync(path.dirname(arduinoConfigFilePath)); fs.writeFileSync(arduinoConfigFilePath, JSON.stringify(arduinoJson, null, 4)); - - // Generate cpptools intellisense config - const cppConfigFilePath = path.join(destExample, constants.CPP_CONFIG_FILE); - - // Current workspace - let includePath = ["${workspaceRoot}"]; - // Defaut package for this board - const defaultPackageLibPaths = this.getDefaultPackageLibPaths(); - includePath = includePath.concat(defaultPackageLibPaths); - // Arduino built-in package tools - includePath.push(path.join(this._settings.arduinoPath, "hardware", "tools", "**")); - // Arduino built-in libraries - includePath.push(path.join(this._settings.arduinoPath, "libraries", "**")); - // Arduino custom package tools - includePath.push(path.join(this._settings.sketchbookPath, "hardware", "tools", "**")); - // Arduino custom libraries - includePath.push(path.join(this._settings.sketchbookPath, "libraries", "**")); - - const forcedInclude = this.getDefaultForcedIncludeFiles(); - - const defines = [ - "ARDUINO=10800", - ]; - const cppConfig = { - configurations: [{ - name: util.getCppConfigPlatform(), - defines, - includePath, - forcedInclude, - intelliSenseMode: "clang-x64", - cStandard: "c11", - cppStandard: "c++17", - }], - version: 3, - }; - util.mkdirRecursivelySync(path.dirname(cppConfigFilePath)); - fs.writeFileSync(cppConfigFilePath, JSON.stringify(cppConfig, null, 4)); } // Step 3: Open the arduino project at a new vscode window. @@ -769,29 +430,354 @@ Please make sure the folder is not occupied by other procedures .`); this._programmerManager = value; } - private getProgrammerString(): string { - const selectProgrammer = this.programmerManager.currentProgrammer; - if (!selectProgrammer) { - Logger.notifyUserError("getProgrammerString", new Error(constants.messages.NO_PROGRAMMMER_SELECTED)); - return; + /** + * Runs the pre or post build command. + * Usually before one of + * * verify + * * upload + * * upload using programmer + * @param dc Device context prepared during one of the above actions + * @param what "pre" if the pre-build command should be run, "post" if the + * post-build command should be run. + * @returns True if successful, false on error. + */ + protected async runPrePostBuildCommand(dc: DeviceContext, + environment: any, + what: "pre" | "post"): Promise { + const cmdline = what === "pre" + ? dc.prebuild + : dc.postbuild; + + if (!cmdline) { + return true; // Successfully done nothing. + } + + arduinoChannel.info(`Running ${what}-build command: "${cmdline}"`); + let cmd: string; + let args: string[]; + // pre-/post-build commands feature full bash support on UNIX systems. + // On Windows you have full cmd support. + if (os.platform() === "win32") { + args = []; + cmd = cmdline; + } else { + args = ["-c", cmdline]; + cmd = "bash"; + } + try { + await util.spawn(cmd, + args, + { + shell: os.platform() === "win32", + cwd: ArduinoWorkspace.rootPath, + env: {...environment}, + }, + { channel: arduinoChannel.channel }); + } catch (ex) { + const msg = ex.error + ? `${ex.error}` + : ex.code + ? `Exit code = ${ex.code}` + : JSON.stringify(ex); + arduinoChannel.error(`Running ${what}-build command failed: ${os.EOL}${msg}`); + return false; } - return selectProgrammer; + return true; } - private getBoardBuildString(): string { - const selectedBoard = this.boardManager.currentBoard; - if (!selectedBoard) { - Logger.notifyUserError("getBoardBuildString", new Error(constants.messages.NO_BOARD_SELECTED)); - return; - } - return selectedBoard.getBuildConfig(); + /** + * 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 async getMainSketch(dc: DeviceContext) { - await dc.resolveMainSketch(); - if (!dc.sketch) { - vscode.window.showErrorMessage("No sketch file was found. Please specify the sketch in the arduino.json file"); - throw new Error("No sketch file was found."); + /** + * Private implementation. Not to be called directly. The wrapper build() + * manages the build state. + * @param buildMode See build() + * @param buildDir See build() + * @see https://github.com/arduino/Arduino/blob/master/build/shared/manpage.adoc + */ + private async _build(buildMode: BuildMode, buildDir?: string): Promise { + const dc = DeviceContext.getInstance(); + const args: string[] = []; + let restoreSerialMonitor: boolean = false; + const verbose = VscodeSettings.getInstance().logLevel === constants.LogLevel.Verbose; + + if (!this.boardManager.currentBoard) { + if (buildMode !== BuildMode.Analyze) { + logger.notifyUserError("boardManager.currentBoard", new Error(constants.messages.NO_BOARD_SELECTED)); + } + return false; + } + const boardDescriptor = this.boardManager.currentBoard.getBuildConfig(); + + if (this.useArduinoCli()) { + args.push("-b", boardDescriptor); + } else { + args.push("--board", boardDescriptor); + } + + if (!ArduinoWorkspace.rootPath) { + vscode.window.showWarningMessage("Workspace doesn't seem to have a folder added to it yet."); + return false; + } + + if (!dc.sketch || !util.fileExistsSync(path.join(ArduinoWorkspace.rootPath, dc.sketch))) { + if (buildMode === BuildMode.Analyze) { + // Analyze runs non interactively + return false; + } + if (!await dc.resolveMainSketch()) { + vscode.window.showErrorMessage("No sketch file was found. Please specify the sketch in the arduino.json file"); + return false; + } + } + + const selectSerial = async () => { + 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"); + } + } + + if (buildMode === BuildMode.Upload) { + if ((!dc.configuration || !/upload_method=[^=,]*st[^,]*link/i.test(dc.configuration)) && !dc.port) { + await selectSerial(); + return false; + } + + if (this.useArduinoCli()) { + args.push("compile", "--upload"); + } else { + args.push("--upload"); + } + + if (dc.port) { + args.push("--port", dc.port); + } + } else if (buildMode === BuildMode.CliUpload) { + if ((!dc.configuration || !/upload_method=[^=,]*st[^,]*link/i.test(dc.configuration)) && !dc.port) { + await selectSerial(); + return false; + } + + if (!this.useArduinoCli()) { + arduinoChannel.error("This command is only available when using the Arduino CLI"); + return false; + } + + args.push("upload"); + + if (dc.port) { + args.push("--port", dc.port); + } + } else if (buildMode === BuildMode.UploadProgrammer) { + const programmer = this.programmerManager.currentProgrammer; + if (!programmer) { + logger.notifyUserError("programmerManager.currentProgrammer", new Error(constants.messages.NO_PROGRAMMMER_SELECTED)); + return false; + } + if (!dc.port) { + await selectSerial(); + return false; + } + + if (this.useArduinoCli()) { + args.push("compile", + "--upload", + "--programmer", programmer); + } else { + args.push("--upload", + "--useprogrammer", + "--pref", `programmer=arduino:${programmer}`); + } + + args.push("--port", dc.port); + if (!this.useArduinoCli()) { + args.push("--verify"); + } + } else if (buildMode === BuildMode.CliUploadProgrammer) { + const programmer = this.programmerManager.currentProgrammer; + if (!programmer) { + logger.notifyUserError("programmerManager.currentProgrammer", new Error(constants.messages.NO_PROGRAMMMER_SELECTED)); + return false; + } + if (!dc.port) { + await selectSerial(); + return false; + } + if (!this.useArduinoCli()) { + arduinoChannel.error("This command is only available when using the Arduino CLI"); + return false; + } + + args.push("upload", + "--programmer", programmer, + "--port", dc.port); + } else { + if (this.useArduinoCli()) { + args.unshift("compile"); + } else { + args.push("--verify"); + } } + + if (dc.buildPreferences) { + for (const pref of dc.buildPreferences) { + // Note: BuildPrefSetting makes sure that each preference + // value consists of exactly two items (key and value). + args.push("--pref", `${pref[0]}=${pref[1]}`); + } + } + + // We always build verbosely but filter the output based on the settings + + this._settings.useArduinoCli ? args.push("--verbose") : args.push("--verbose-build"); + + if (verbose && !this._settings.useArduinoCli) { + args.push("--verbose-upload"); + } + + await vscode.workspace.saveAll(false); + + // we prepare the channel here since all following code will + // or at leas can possibly output to it + arduinoChannel.show(); + arduinoChannel.start(`${buildMode} sketch '${dc.sketch}'`); + + if (buildDir || dc.output) { + // 2020-02-29, EW: This whole code appears a bit wonky to me. + // What if the user specifies an output directory "../builds/my project" + buildDir = path.resolve(ArduinoWorkspace.rootPath, buildDir || dc.output); + const dirPath = path.dirname(buildDir); + if (!util.directoryExistsSync(dirPath)) { + logger.notifyUserError("InvalidOutPutPath", new Error(constants.messages.INVALID_OUTPUT_PATH + buildDir)); + return false; + } + + if (this.useArduinoCli()) { + args.push("--build-path", buildDir); + + } else { + args.push("--pref", `build.path=${buildDir}`); + } + + arduinoChannel.info(`Please see the build logs in output path: ${buildDir}`); + } else { + const msg = "Output path is not specified. Unable to reuse previously compiled files. Build will be slower. See README."; + arduinoChannel.warning(msg); + } + + // Environment variables passed to pre- and post-build commands + const env = { + VSCA_BUILD_MODE: buildMode, + VSCA_SKETCH: dc.sketch, + VSCA_BOARD: boardDescriptor, + VSCA_WORKSPACE_DIR: ArduinoWorkspace.rootPath, + VSCA_LOG_LEVEL: verbose ? constants.LogLevel.Verbose : constants.LogLevel.Info, + }; + if (dc.port) { + env["VSCA_SERIAL"] = dc.port; + } + if (buildDir) { + env["VSCA_BUILD_DIR"] = buildDir; + } + + // TODO EW: What should we do with pre-/post build commands when running + // analysis? Some could use it to generate/manipulate code which could + // be a prerequisite for a successful build + if (!await this.runPrePostBuildCommand(dc, env, "pre")) { + return false; + } + + // stop serial monitor when everything is prepared and good + // what makes restoring of its previous state easier + if (buildMode === BuildMode.Upload || + buildMode === BuildMode.UploadProgrammer || + buildMode === BuildMode.CliUpload || + buildMode === BuildMode.CliUploadProgrammer) { + restoreSerialMonitor = await SerialMonitor.getInstance().closeSerialMonitor(dc.port); + UsbDetector.getInstance().pauseListening(); + } + + // Push sketch as last argument + args.push(path.join(ArduinoWorkspace.rootPath, dc.sketch)); + + const cocopa = makeCompilerParserContext(dc); + + const cleanup = async (result: "ok" | "error") => { + let ret = true; + if (result === "ok") { + ret = await this.runPrePostBuildCommand(dc, env, "post"); + } + await cocopa.conclude(); + if (buildMode === BuildMode.Upload || buildMode === BuildMode.UploadProgrammer) { + UsbDetector.getInstance().resumeListening(); + if (restoreSerialMonitor) { + await SerialMonitor.getInstance().openSerialMonitor(); + } + } + return ret; + } + const stdoutcb = (line: string) => { + if (cocopa.callback) { + cocopa.callback(line); + } + if (verbose) { + arduinoChannel.channel.append(line); + } + } + const stderrcb = (line: string) => { + if (os.platform() === "win32") { + line = line.trim(); + if (line.length <= 0) { + return; + } + line = line.replace(/(?:\r|\r\n|\n)+/g, os.EOL); + line = `${line}${os.EOL}`; + } + if (!verbose) { + // Don't spill log with spurious info from the backend. This + // list could be fetched from a config file to accommodate + // messages of unknown board packages, newer backend revisions + const filters = [ + /^Picked\sup\sJAVA_TOOL_OPTIONS:\s+/, + /^\d+\d+-\d+-\d+T\d+:\d+:\d+.\d+Z\s(?:INFO|WARN)\s/, + /^(?:DEBUG|TRACE|INFO)\s+/, + ]; + for (const f of filters) { + if (line.match(f)) { + return; + } + } + } + arduinoChannel.channel.append(line); + } + + return await util.spawn( + this._settings.commandPath, + args, + undefined, + { /*channel: arduinoChannel.channel,*/ stdout: stdoutcb, stderr: stderrcb }, + ).then(async () => { + const ret = await cleanup("ok"); + if (ret) { + arduinoChannel.end(`${buildMode} sketch '${dc.sketch}'${os.EOL}`); + } + return ret; + }, async (reason) => { + await cleanup("error"); + const msg = reason.code + ? `Exit with code=${reason.code}` + : JSON.stringify(reason); + arduinoChannel.error(`${buildMode} sketch '${dc.sketch}': ${msg}${os.EOL}`); + return false; + }); } } diff --git a/src/arduino/arduinoContentProvider.ts b/src/arduino/arduinoContentProvider.ts index 6c893eb2..e3dcce7f 100644 --- a/src/arduino/arduinoContentProvider.ts +++ b/src/arduino/arduinoContentProvider.ts @@ -8,20 +8,17 @@ import ArduinoActivator from "../arduinoActivator"; import ArduinoContext from "../arduinoContext"; import * as Constants from "../common/constants"; import * as JSONHelper from "../common/cycle"; +import { DeviceContext } from "../deviceContext"; import * as Logger from "../logger/logger"; import LocalWebServer from "./localWebServer"; -import { VscodeSettings } from "./vscodeSettings"; export class ArduinoContentProvider implements vscode.TextDocumentContentProvider { private _webserver: LocalWebServer; private _onDidChange = new vscode.EventEmitter(); - constructor( - private _extensionPath: string) { - this.initialize(); - } + constructor(private _extensionPath: string) { } - public initialize() { + public async initialize() { this._webserver = new LocalWebServer(this._extensionPath); // Arduino Boards Manager this.addHandlerWithLogger("show-boardmanager", "/boardmanager", (req, res) => this.getHtmlView(req, res)); @@ -50,7 +47,7 @@ export class ArduinoContentProvider implements vscode.TextDocumentContentProvide this.addHandlerWithLogger("load-examples", "/api/examples", async (req, res) => await this.getExamples(req, res)); this.addHandlerWithLogger("open-example", "/api/openexample", (req, res) => this.openExample(req, res), true); - this._webserver.start(); + await this._webserver.start(); } public async provideTextDocumentContent(uri: vscode.Uri): Promise { @@ -207,7 +204,6 @@ export class ArduinoContentProvider implements vscode.TextDocumentContentProvide return res.status(400).send("BAD Request! Missing { libraryPath } parameters!"); } else { try { - await ArduinoContext.arduinoApp.addLibPath(req.body.libraryPath); await ArduinoContext.arduinoApp.includeLibrary(req.body.libraryPath); return res.json({ status: "OK", @@ -262,6 +258,8 @@ export class ArduinoContentProvider implements vscode.TextDocumentContentProvide } else { try { ArduinoContext.boardManager.currentBoard.updateConfig(req.body.configId, req.body.optionId); + const dc = DeviceContext.getInstance(); + dc.configuration = ArduinoContext.boardManager.currentBoard.customConfig; return res.json({ status: "OK", }); 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/board.ts b/src/arduino/board.ts index 39908022..294ce7cf 100644 --- a/src/arduino/board.ts +++ b/src/arduino/board.ts @@ -1,14 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -import { DeviceContext } from "../deviceContext"; -import { IBoard, IBoardConfigItem, IPlatform } from "./package"; +import { BoardConfigResult, IBoard, IBoardConfigItem, IPlatform } from "./package"; export function parseBoardDescriptor(boardDescriptor: string, plat: IPlatform): Map { const boardLineRegex = /([^\.]+)\.(\S+)=(.+)/; const result = new Map(); - const lines = boardDescriptor.split(/[\r|\r\n|\n]/); + const lines = boardDescriptor.split(/(?:\r|\r\n|\n)/); const menuMap = new Map(); lines.forEach((line) => { @@ -18,7 +17,7 @@ export function parseBoardDescriptor(boardDescriptor: string, plat: IPlatform): } const match = boardLineRegex.exec(line); - if (match && match.length > 3) { + if (match) { if (line.startsWith("menu.")) { menuMap.set(match[2], match[3]); return; @@ -52,15 +51,15 @@ export class Board implements IBoard { this._configItems = []; } - public get board(): string { + public get board() { return this._board; } - public get platform(): IPlatform { + public get platform() { return this._platform; } - public addParameter(key: string, value: string): void { + public addParameter(key: string, value: string) { const match = key.match(MENU_REGEX); if (match) { const existingItem = this._configItems.find((item) => item.id === match[1]); @@ -82,8 +81,7 @@ export class Board implements IBoard { } } } - - public getBuildConfig(): string { + public getBuildConfig() { return `${this.getPackageName()}:${this.platform.architecture}:${this.board}${this.customConfig ? ":" + this.customConfig : ""}`; } @@ -100,36 +98,84 @@ export class Board implements IBoard { } } - public get configItems(): IBoardConfigItem[] { + public get configItems() { return this._configItems; } - public loadConfig(configString: string): void { + public loadConfig(configString: string) { + // An empty or undefined config string resets the configuration + if (!configString) { + this.resetConfig(); + return BoardConfigResult.Success; + } const configSections = configString.split(","); const keyValueRegex = /(\S+)=(\S+)/; - configSections.forEach((configSection) => { - const match = configSection.match(keyValueRegex); - if (match && match.length >= 2) { - this.updateConfig(match[1], match[2]); + let result = BoardConfigResult.Success; + for (const section of configSections) { + const match = section.match(keyValueRegex); + if (!match) { + return BoardConfigResult.InvalidFormat; + } + const r = this.updateConfig(match[1], match[2]); + switch (r) { + case BoardConfigResult.SuccessNoChange: + result = r; + break; + case BoardConfigResult.Success: + break; + default: + return r; } - }); + }; + return result; } - public updateConfig(configId: string, optionId: string): boolean { + /** + * For documentation see the documentation on IBoard.updateConfig(). + */ + public updateConfig(configId: string, optionId: string) { const targetConfig = this._configItems.find((config) => config.id === configId); if (!targetConfig) { - return false; + return BoardConfigResult.InvalidConfigID; + } + // Iterate through all options and ... + for (const o of targetConfig.options) { + // Make sure that we only set valid options, e.g. when loading + // from config files. + if (o.id === optionId) { + if (targetConfig.selectedOption !== optionId) { + targetConfig.selectedOption = optionId; + return BoardConfigResult.Success; + } + return BoardConfigResult.SuccessNoChange; + } } - if (targetConfig.selectedOption !== optionId) { - targetConfig.selectedOption = optionId; - const dc = DeviceContext.getInstance(); - dc.configuration = this.customConfig; - return true; + return BoardConfigResult.InvalidOptionID; + } + + public resetConfig() { + for (const c of this._configItems) { + c.selectedOption = c.options[0].id; } - return false; } public getPackageName() { return this.platform.packageName ? this.platform.packageName : this.platform.package.name; } } + +/** + * Test if two boards are of the same type, i.e. have the same key. + * @param {IBoard | undefined} a A board. + * @param {IBoard | undefined} b And another board. + * @returns {boolean} true if two boards are of the same type, else false. + */ +export function boardEqual(a: IBoard | undefined, + b: IBoard | undefined) { + if (a && b) { + return a.key === b.key; + } else if (a || b) { + return false; + } + return true; +} diff --git a/src/arduino/boardManager.ts b/src/arduino/boardManager.ts index d8267afa..c65c1143 100644 --- a/src/arduino/boardManager.ts +++ b/src/arduino/boardManager.ts @@ -12,8 +12,9 @@ import { arduinoChannel } from "../common/outputChannel"; import { DeviceContext } from "../deviceContext"; import { ArduinoApp } from "./arduino"; import { IArduinoSettings } from "./arduinoSettings"; -import { parseBoardDescriptor } from "./board"; -import { IBoard, IPackage, IPlatform } from "./package"; +import { boardEqual, parseBoardDescriptor } from "./board"; +import { BoardConfigResult, IBoard, IPackage, IPlatform, IProgrammer } from "./package"; +import { parseProgrammerDescriptor } from "./programmer"; import { VscodeSettings } from "./vscodeSettings"; export class BoardManager { @@ -22,6 +23,8 @@ export class BoardManager { private _platforms: IPlatform[]; + private _programmers: Map; + private _installedPlatforms: IPlatform[]; private _boards: Map; @@ -67,15 +70,20 @@ export class BoardManager { // Load default platforms from arduino installation directory and user manually installed platforms. this.loadInstalledPlatforms(); - // Load all supported boards type. + // Load all supported board types this.loadInstalledBoards(); + this.loadInstalledProgrammers(); this.updateStatusBar(); this._boardConfigStatusBar.show(); const dc = DeviceContext.getInstance(); - dc.onDidChange(() => { - this.updateStatusBar(); - }); + 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() { @@ -119,13 +127,21 @@ export class BoardManager { public doChangeBoardType(targetBoard: IBoard) { const dc = DeviceContext.getInstance(); - dc.board = targetBoard.key; - this._currentBoard = targetBoard; - dc.configuration = this._currentBoard.customConfig; - this._boardConfigStatusBar.text = targetBoard.name; - this._arduinoApp.addLibPath(null); - this._onBoardTypeChanged.fire(); + 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[] { @@ -140,6 +156,10 @@ export class BoardManager { return this._boards; } + public get installedProgrammers(): Map { + return this._programmers; + } + public get currentBoard(): IBoard { return this._currentBoard; } @@ -243,23 +263,17 @@ export class BoardManager { this._installedPlatforms.push(existingPlatform); } this.loadInstalledBoardsFromPlatform(existingPlatform); + this.loadInstalledProgrammersFromPlatform(existingPlatform); } } } - public updateStatusBar(show: boolean = true): void { + private updateStatusBar(show: boolean = true): void { if (show) { this._boardConfigStatusBar.show(); - const dc = DeviceContext.getInstance(); - const selectedBoard = this._boards.get(dc.board); - if (selectedBoard) { - this._currentBoard = selectedBoard; - this._boardConfigStatusBar.text = selectedBoard.name; - if (dc.configuration) { - this._currentBoard.loadConfig(dc.configuration); - } + if (this._currentBoard) { + this._boardConfigStatusBar.text = this._currentBoard.name; } else { - this._currentBoard = null; this._boardConfigStatusBar.text = ""; - private static _programmerManager: ProgrammerManager = null; - - private _currentprogrammer: ProgrammerList; - - private _programmervalue: string; + private _programmerValue: string; + private _programmerDisplayName: string; private _programmerStatusBar: vscode.StatusBarItem; constructor(private _settings: IArduinoSettings, private _arduinoApp: ArduinoApp) { - this._programmerStatusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, constants.statusBarPriority.PROGRAMMER); + this._programmerStatusBar = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Right, + constants.statusBarPriority.PROGRAMMER, + ); this._programmerStatusBar.command = "arduino.selectProgrammer"; this._programmerStatusBar.tooltip = "Select Programmer"; - this._programmerStatusBar.text = "