diff --git a/src/arduino/boardManager.ts b/src/arduino/boardManager.ts index a2f6d481..c65c1143 100644 --- a/src/arduino/boardManager.ts +++ b/src/arduino/boardManager.ts @@ -13,7 +13,8 @@ import { DeviceContext } from "../deviceContext"; import { ArduinoApp } from "./arduino"; import { IArduinoSettings } from "./arduinoSettings"; import { boardEqual, parseBoardDescriptor } from "./board"; -import { BoardConfigResult, IBoard, IPackage, IPlatform } from "./package"; +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; @@ -69,6 +72,9 @@ export class BoardManager { // Load all supported board types this.loadInstalledBoards(); + this.loadInstalledProgrammers(); + this.updateStatusBar(); + this._boardConfigStatusBar.show(); const dc = DeviceContext.getInstance(); dc.onChangeBoard(() => this.onDeviceContextBoardChange()); @@ -150,6 +156,10 @@ export class BoardManager { return this._boards; } + public get installedProgrammers(): Map { + return this._programmers; + } + public get currentBoard(): IBoard { return this._currentBoard; } @@ -253,6 +263,7 @@ export class BoardManager { this._installedPlatforms.push(existingPlatform); } this.loadInstalledBoardsFromPlatform(existingPlatform); + this.loadInstalledProgrammersFromPlatform(existingPlatform); } } } @@ -470,6 +481,23 @@ export class BoardManager { } } + private loadInstalledProgrammers(): void { + this._programmers = new Map(); + this._installedPlatforms.forEach((plat) => { + this.loadInstalledProgrammersFromPlatform(plat); + }); + } + + private loadInstalledProgrammersFromPlatform(plat: IPlatform) { + if (util.fileExistsSync(path.join(plat.rootBoardPath, "programmers.txt"))) { + const programmersContent = fs.readFileSync(path.join(plat.rootBoardPath, "programmers.txt"), "utf8"); + const res = parseProgrammerDescriptor(programmersContent, plat); + res.forEach((prog) => { + this._programmers.set(prog.name, prog); + }); + } + } + private listBoards(): IBoard[] { const result = []; this._boards.forEach((b) => { diff --git a/src/arduino/package.ts b/src/arduino/package.ts index 3d5054b2..817def5a 100644 --- a/src/arduino/package.ts +++ b/src/arduino/package.ts @@ -267,3 +267,34 @@ export interface IBoard { */ getPackageName(); } + +/** + * Interface for classes that represent an Arduino supported programmer. + * + * @interface + */ +export interface IProgrammer { + /** + * Unique key that represent the programmer in the package:name. + * @property {string} + */ + key: string; + + /** + * Programmer name for Arduino compilation such as `avrisp`, `atmel_ice` + * @property {string} + */ + name: string; + + /** + * The human readable name displayed in the Arduino programmer selection menu + * @property {string} + */ + displayName: string; + + /** + * Reference to the platform that contains this board. + * @prop {IPlatform} + */ + platform: IPlatform; +} diff --git a/src/arduino/programmer.ts b/src/arduino/programmer.ts new file mode 100644 index 00000000..d2fdceb0 --- /dev/null +++ b/src/arduino/programmer.ts @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { IPlatform, IProgrammer } from "./package"; + +export function parseProgrammerDescriptor(programmerDescriptor: string, plat: IPlatform): Map { + const progrmmerLineRegex = /([^\.]+)\.(\S+)=(.+)/; + + const result = new Map(); + const lines = programmerDescriptor.split(/[\r|\r\n|\n]/); + const menuMap = new Map(); + + lines.forEach((line) => { + // Ignore comments. + if (line.startsWith("#")) { + return; + } + + const match = progrmmerLineRegex.exec(line); + if (match && match.length > 3) { + let programmer = result.get(match[1]); + if (!programmer) { + programmer = new Programmer(match[1], plat); + result.set(programmer.name + , programmer); + } + if (match[2] === "name") { + programmer.displayName = match[3].trim(); + } + } + }); + return result; +} + +export class Programmer implements IProgrammer { + constructor(private _name: string, + private _platform: IPlatform, + private _displayName: string = _name) { + } + + public get name(): string { + return this._name; + } + + public get platform(): IPlatform { + return this._platform; + } + + public get displayName(): string { + return this._displayName; + } + + public set displayName(value: string) { + this._displayName = value; + } + + /** + * @returns {string} Return programmer key in format packageName:name + */ + public get key() { + return `${this.getPackageName}:${this.name}`; + } + + private get getPackageName(): string { + return this.platform.packageName ? this.platform.packageName : this.platform.package.name; + } +} diff --git a/src/arduino/programmerManager.ts b/src/arduino/programmerManager.ts index 7a7e2991..b766166e 100644 --- a/src/arduino/programmerManager.ts +++ b/src/arduino/programmerManager.ts @@ -3,28 +3,15 @@ import * as constants from "../common/constants"; import { DeviceContext } from "../deviceContext"; import { ArduinoApp } from "./arduino"; import { IArduinoSettings } from "./arduinoSettings"; +import { IBoard, IProgrammer } from "./package"; export class ProgrammerManager { - private _programmervalue: string; + public static notFoundDisplayValue: string = ""; + private getDisplayName(programmerName: string): string { + const programmer = this._arduinoApp.boardManager.installedProgrammers.get(programmerName); + return programmer ? programmer.displayName : programmerName; } - private getFriendlyName(programmer: string): string { - const friendlyName = this._availableProgrammers[programmer]; - return friendlyName ? friendlyName : programmer; + private getAvailableProgrammers(currentBoard: IBoard): IProgrammer[] { + if (!currentBoard || !currentBoard.platform) { + return []; + } + + // Filter the list of all programmers to those that share the same platform as the board + const availableProgrammers: IProgrammer[] = []; + for (const programmer of this._arduinoApp.boardManager.installedProgrammers.values()) { + if (programmer.platform === currentBoard.platform) { + availableProgrammers.push(programmer); + } + } + + return availableProgrammers; } } diff --git a/src/deviceContext.ts b/src/deviceContext.ts index 710782f1..16451a81 100644 --- a/src/deviceContext.ts +++ b/src/deviceContext.ts @@ -92,6 +92,12 @@ export class DeviceContext implements IDeviceContext, vscode.Disposable { private _sketchStatusBar: vscode.StatusBarItem; + private _prebuild: string; + + private _programmer: string; + + private _suppressSaveContext: boolean = false; + /** * @constructor */ @@ -264,6 +270,14 @@ export class DeviceContext implements IDeviceContext, vscode.Disposable { this.saveContext(); } + public get suppressSaveContext() { + return this._suppressSaveContext; + } + + public set suppressSaveContext(value: boolean) { + this._suppressSaveContext = value; + } + public get buildPreferences() { return this._settings.buildPreferences.value; } diff --git a/test/boardmanager.test.ts b/test/boardmanager.test.ts index 06e79850..a17e1b46 100644 --- a/test/boardmanager.test.ts +++ b/test/boardmanager.test.ts @@ -12,6 +12,7 @@ import { ArduinoSettings } from "../src/arduino/arduinoSettings"; import { parseBoardDescriptor } from "../src/arduino/board"; import { BoardManager } from "../src/arduino/boardManager"; import { IPlatform } from "../src/arduino/package"; +import { parseProgrammerDescriptor } from "../src/arduino/programmer"; import * as util from "../src/common/util"; suite("Arduino: Board Manager.", () => { @@ -67,6 +68,14 @@ suite("Arduino: Board Manager.", () => { "should parse installed boards from custom packages ($sketchbook/hardware directory)"); }); + test("should be able to load installed programmers", () => { + assert.equal(boardManager.installedProgrammers.size, 17, `Expected to find programmers for dummy & AVR boards`); + assert.ok(boardManager.installedProgrammers.get("avrispmkii"), + "should parse installed programmers from Arduino IDE built-in packages"); + assert.ok(boardManager.installedProgrammers.get("esp8266_dummy"), + "should parse installed programmers from custom packages ($sketchbook/hardware directory)"); + }); + test("should parse boards.txt correctly", () => { const arduinoAvrBoard = fs.readFileSync(Path.join(Resources.mockedIDEPackagePath, "arduino/avr/boards.txt"), "utf8"); const platform = { @@ -92,6 +101,26 @@ suite("Arduino: Board Manager.", () => { assert.equal(diecimilaBoard.customConfig, "cpu=atmega328"); }); + test("should parse programmers.txt correctly", () => { + const arduinoAvrBoard = fs.readFileSync(Path.join(Resources.mockedIDEPackagePath, "arduino/avr/programmers.txt"), "utf8"); + const platform = { + name: "Arduino AVR Boards", + architecture: "avr", + package: { + name: "arduino", + }, + }; + const programmerDescriptors = parseProgrammerDescriptor(arduinoAvrBoard, platform); + + const avrispmkii = programmerDescriptors.get("avrispmkii"); + assert.equal(avrispmkii.name, "avrispmkii"); + assert.equal(avrispmkii.displayName, "AVRISP mkII"); + + const usbGemma = programmerDescriptors.get("usbGemma"); + assert.equal(usbGemma.name, "usbGemma"); + assert.equal(usbGemma.displayName, "Arduino Gemma"); + }); + test("should parse platform.txt correctly", () => { const platformConfig = util.parseConfigFile(Path.join(Resources.mockedSketchbookPath, "hardware/esp8266/esp8266/platform.txt")); assert.equal(platformConfig.get("name"), "ESP8266 Modules"); diff --git a/test/devicecontext.test.ts b/test/devicecontext.test.ts index fac14055..4579b5c6 100644 --- a/test/devicecontext.test.ts +++ b/test/devicecontext.test.ts @@ -17,6 +17,7 @@ suite("Arduino: Device Context config", () => { assert.equal(deviceContext.configuration, "cpu=atmega328"); assert.equal(deviceContext.output, null); assert.equal(deviceContext.debugger_, null); + assert.equal(deviceContext.programmer, "arduino:jtag3isp"); done(); }); } catch (error) { diff --git a/test/programmermanager.test.ts b/test/programmermanager.test.ts new file mode 100644 index 00000000..a73baed0 --- /dev/null +++ b/test/programmermanager.test.ts @@ -0,0 +1,100 @@ +import * as assert from "assert"; +import * as TypeMoq from "typemoq"; +import * as vscode from "vscode"; + +import { ArduinoApp } from "../src/arduino/arduino"; +import { ArduinoSettings } from "../src/arduino/arduinoSettings"; +import { Board } from "../src/arduino/board"; +import { BoardManager } from "../src/arduino/boardManager"; +import { IPlatform, IProgrammer } from "../src/arduino/package"; +import { Programmer } from "../src/arduino/programmer"; +import { ProgrammerManager } from "../src/arduino/programmerManager"; +import { DeviceContext } from "../src/deviceContext"; +import * as Resources from "./resources"; + +suite("Arduino: Programmer Manager.", () => { + let programmerManager: ProgrammerManager; + let restoreSuppress: boolean; + let programmers: IProgrammer[]; + + setup((done) => { + // Suppress saving the device context, as not to polute the test arduino.json file + restoreSuppress = DeviceContext.getInstance().suppressSaveContext; + DeviceContext.getInstance().suppressSaveContext = true; + + // Mock two different platforms + const platformMock1 = TypeMoq.Mock.ofType(); + platformMock1.setup((x) => x.packageName).returns(() => "mockplatform1"); + const platformMock2 = TypeMoq.Mock.ofType(); + platformMock2.setup((x) => x.packageName).returns(() => "mockplatform2"); + + // Mock a single selected board, using the the first mocked platform + const mockedBoard = TypeMoq.Mock.ofType(Board); + mockedBoard.setup((x) => x.platform).returns(() => platformMock1.object); + + // Setup a list of installed programmers + programmers = [ + new Programmer("test1", platformMock1.object, "test1_display"), + new Programmer("test2", platformMock1.object), + new Programmer("test3", platformMock2.object)]; + + const installedProgrammers = new Map(); + programmers.forEach((v) => installedProgrammers.set(v.name, v)); + + // Mock the BoardManager with minimal set of required functionality + const mockBoardManager = TypeMoq.Mock.ofType(BoardManager); + mockBoardManager.setup((x) => x.currentBoard).returns(() => mockedBoard.object); + mockBoardManager.setup((x) => x.installedProgrammers).returns(() => installedProgrammers); + mockBoardManager.setup((x) => x.onBoardTypeChanged).returns(() => new vscode.EventEmitter().event); + + // Mock minimal ArduinoApp + const arduinoApp = TypeMoq.Mock.ofType(ArduinoApp); + arduinoApp.setup((x) => x.boardManager).returns(() => mockBoardManager.object); + + try { + programmerManager = new ProgrammerManager(TypeMoq.Mock.ofType(ArduinoSettings).object, arduinoApp.object); + done(); + } catch (error) { + done(`Failed to initialize ProgrammerManager: ${error}`); + } + }); + + teardown(() => { + // Restpre the supression state for the DeviceContext + DeviceContext.getInstance().suppressSaveContext = restoreSuppress; + }); + + test("value stored in arduino.ino should load by default", () => { + assert.equal(programmerManager.currentProgrammer, "arduino:jtag3isp"); + assert.equal(programmerManager.currentDisplayName, "arduino:jtag3isp"); + }); + + test("changing arduino.ino value should change programmer", (done) => { + DeviceContext.getInstance().programmer = programmers[0].name; + setTimeout(() => { + assert.equal(programmerManager.currentProgrammer, programmers[0].key); + assert.equal(programmerManager.currentDisplayName, programmers[0].displayName); + done(); + }, 200); + }); + + test("changing arduino.ino value to null should clear displayname to not found value", (done) => { + DeviceContext.getInstance().programmer = null; + setTimeout(() => { + assert.equal(programmerManager.currentProgrammer, null); + assert.equal(programmerManager.currentDisplayName, ProgrammerManager.notFoundDisplayValue); + done(); + }, 200); + }); + + test("changing arduino.ino value to an unknown value will be accepted, and will replicate value as displayname", (done) => { + const unknownProgrammerValue = "unknown:programmer"; + + DeviceContext.getInstance().programmer = unknownProgrammerValue; + setTimeout(() => { + assert.equal(programmerManager.currentProgrammer, unknownProgrammerValue); + assert.equal(programmerManager.currentDisplayName, unknownProgrammerValue); + done(); + }, 200); + }); +}); diff --git a/test/resources/Arduino15/preferences.txt b/test/resources/Arduino15/preferences.txt index 6a875364..2378e9c3 100644 --- a/test/resources/Arduino15/preferences.txt +++ b/test/resources/Arduino15/preferences.txt @@ -45,7 +45,7 @@ preproc.save_build_files=false preproc.substitute_floats=true preproc.substitute_unicode=true preproc.web_colors=true -programmer=arduino:avrispmkii +programmer=avrispmkii run.display=1 run.options= run.options.memory=false diff --git a/test/resources/ArduinoIDE/hardware/arduino/avr/programmers.txt b/test/resources/ArduinoIDE/hardware/arduino/avr/programmers.txt new file mode 100644 index 00000000..69ddf692 --- /dev/null +++ b/test/resources/ArduinoIDE/hardware/arduino/avr/programmers.txt @@ -0,0 +1,115 @@ +avrisp.name=AVR ISP +avrisp.communication=serial +avrisp.protocol=stk500v1 +avrisp.program.protocol=stk500v1 +avrisp.program.tool=avrdude +avrisp.program.extra_params=-P{serial.port} + +avrispmkii.name=AVRISP mkII +avrispmkii.communication=usb +avrispmkii.protocol=stk500v2 +avrispmkii.program.protocol=stk500v2 +avrispmkii.program.tool=avrdude +avrispmkii.program.extra_params=-Pusb + +usbtinyisp.name=USBtinyISP +usbtinyisp.protocol=usbtiny +usbtinyisp.program.tool=avrdude +usbtinyisp.program.extra_params= + +arduinoisp.name=ArduinoISP +arduinoisp.protocol=arduinoisp +arduinoisp.program.tool=avrdude +arduinoisp.program.extra_params= + +arduinoisporg.name=ArduinoISP.org +arduinoisporg.protocol=arduinoisporg +arduinoisporg.program.tool=avrdude +arduinoisporg.program.extra_params= + +usbasp.name=USBasp +usbasp.communication=usb +usbasp.protocol=usbasp +usbasp.program.protocol=usbasp +usbasp.program.tool=avrdude +usbasp.program.extra_params=-Pusb + +parallel.name=Parallel Programmer +parallel.protocol=dapa +parallel.force=true +# parallel.delay=200 +parallel.program.tool=avrdude +parallel.program.extra_params=-F + +arduinoasisp.name=Arduino as ISP +arduinoasisp.communication=serial +arduinoasisp.protocol=stk500v1 +arduinoasisp.speed=19200 +arduinoasisp.program.protocol=stk500v1 +arduinoasisp.program.speed=19200 +arduinoasisp.program.tool=avrdude +arduinoasisp.program.extra_params=-P{serial.port} -b{program.speed} + +arduinoasispatmega32u4.name=Arduino as ISP (ATmega32U4) +arduinoasispatmega32u4.communication=serial +arduinoasispatmega32u4.protocol=arduino +arduinoasispatmega32u4.speed=19200 +arduinoasispatmega32u4.program.protocol=arduino +arduinoasispatmega32u4.program.speed=19200 +arduinoasispatmega32u4.program.tool=avrdude +arduinoasispatmega32u4.program.extra_params=-P{serial.port} -b{program.speed} + +usbGemma.name=Arduino Gemma +usbGemma.protocol=arduinogemma +usbGemma.program.tool=avrdude +usbGemma.program.extra_params= +usbGemma.config.path={runtime.platform.path}/bootloaders/gemma/avrdude.conf + +## Notes about Dangerous Prototypes Bus Pirate as ISP +## Bus Pirate V3 need Firmware v5.10 or later +## Bus Pirate V4 need Firmware v6.3-r2151 or later +## Could happen that BP does not have enough current to power an Arduino board +## through the ICSP connector. In this case disconnect the +Vcc from ICSP connector +## and power Arduino board in the normal way. +buspirate.name=BusPirate as ISP +buspirate.communication=serial +buspirate.protocol=buspirate +buspirate.program.protocol=buspirate +buspirate.program.tool=avrdude +buspirate.program.extra_params=-P{serial.port} + +# STK500 firmware version v1 and v2 use different serial protocols. +# Using the 'stk500' protocol tells avrdude to try and autodetect the +# firmware version. If this leads to problems, we might need to add +# stk500v1 and stk500v2 entries to allow explicitely selecting the +# firmware version. +stk500.name=Atmel STK500 development board +stk500.communication=serial +stk500.protocol=stk500 +stk500.program.protocol=stk500 +stk500.program.tool=avrdude +stk500.program.extra_params=-P{serial.port} + +jtag3isp.name=Atmel JTAGICE3 (ISP mode) +jtag3isp.communication=usb +jtag3isp.protocol=jtag3isp +jtag3isp.program.protocol=jtag3isp +jtag3isp.program.tool=avrdude +jtag3isp.program.extra_params= + +jtag3.name=Atmel JTAGICE3 (JTAG mode) +jtag3.communication=usb +jtag3.protocol=jtag3 +jtag3.program.protocol=jtag3 +jtag3.program.tool=avrdude +# Set a bitclock of 0.1us (the fastest supported value). This should +# work regardless of the crystal used, since JTAG doesn't use the MCU +# clock but dictates its own clock. +jtag3.program.extra_params=-B0.1 + +atmel_ice.name=Atmel-ICE (AVR) +atmel_ice.communication=usb +atmel_ice.protocol=atmelice_isp +atmel_ice.program.protocol=atmelice_isp +atmel_ice.program.tool=avrdude +atmel_ice.program.extra_params=-Pusb diff --git a/test/resources/Documents/Arduino/hardware/esp8266/esp8266/programmers.txt b/test/resources/Documents/Arduino/hardware/esp8266/esp8266/programmers.txt new file mode 100644 index 00000000..8fe950d4 --- /dev/null +++ b/test/resources/Documents/Arduino/hardware/esp8266/esp8266/programmers.txt @@ -0,0 +1,14 @@ +#********************************************** +# Dummy esp8266 programmer +#********************************************** +esp8266_dummy.name=Dummy esp8266 programmer +esp8266_dummy.program.tool=bootburn +esp8266_dummy.program.cmd=esp8266prog +esp8266_dummy.program.cmd.windows=esp8266prog.exe + +#********************************************** +# Dummy esp8266 bootloader programmer +#********************************************** +esp8266_boot_dummy.name=Dummy esp8266 bootloader programmer +esp8266_boot_dummy.program.tool=bootburn +esp8266_boot_dummy.program.cmd=esp8266bootprog \ No newline at end of file diff --git a/test/resources/blink/.vscode/arduino.json b/test/resources/blink/.vscode/arduino.json index 5990ff4b..32bd2323 100644 --- a/test/resources/blink/.vscode/arduino.json +++ b/test/resources/blink/.vscode/arduino.json @@ -4,5 +4,6 @@ "board": "arduino:avr:diecimila", "output": null, "_debugger": null, - "configuration": "cpu=atmega328" + "configuration": "cpu=atmega328", + "programmer": "arduino:jtag3isp" } \ No newline at end of file