From e77c9721cd9436a84aca1190c67ac44bb60a03b0 Mon Sep 17 00:00:00 2001
From: Akos Kitta <kittaakos@typefox.io>
Date: Tue, 21 Jul 2020 17:39:22 +0200
Subject: [PATCH] init: programmers

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
---
 .../browser/arduino-frontend-contribution.tsx |  6 +-
 .../browser/arduino-ide-frontend-module.ts    | 10 +--
 ...s-config-store.ts => boards-data-store.ts} | 78 +++++++++++++------
 .../boards/boards-details-menu-updater.ts     | 28 +++++--
 .../quick-open/boards-quick-open-service.ts   | 20 ++---
 .../browser/contributions/upload-sketch.ts    |  8 +-
 .../browser/contributions/verify-sketch.ts    |  8 +-
 .../src/common/protocol/boards-service.ts     | 18 +++++
 arduino-ide-extension/src/common/utils.ts     |  4 +
 .../src/node/boards-service-impl.ts           | 35 +++++++--
 10 files changed, 151 insertions(+), 64 deletions(-)
 rename arduino-ide-extension/src/browser/boards/{boards-config-store.ts => boards-data-store.ts} (64%)

diff --git a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx
index 9d337ac1..bb926a04 100644
--- a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx
+++ b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx
@@ -36,7 +36,7 @@ import { ColorContribution } from '@theia/core/lib/browser/color-application-con
 import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
 import { ArduinoDaemon } from '../common/protocol/arduino-daemon';
 import { ConfigService } from '../common/protocol/config-service';
-import { BoardsConfigStore } from './boards/boards-config-store';
+import { BoardsDataStore } from './boards/boards-data-store';
 import { MainMenuManager } from '../common/main-menu-manager';
 import { FileSystemExt } from '../common/protocol/filesystem-ext';
 import { ArduinoMenus } from './menu/arduino-menus';
@@ -130,8 +130,8 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
     @inject(ConfigService)
     protected readonly configService: ConfigService;
 
-    @inject(BoardsConfigStore)
-    protected readonly boardsConfigStore: BoardsConfigStore;
+    @inject(BoardsDataStore)
+    protected readonly boardsDataStore: BoardsDataStore;
 
     @inject(MainMenuManager)
     protected readonly mainMenuManager: MainMenuManager;
diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts
index 42daed28..d7e3f1aa 100644
--- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts
+++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts
@@ -82,8 +82,8 @@ import {
 } from '@theia/core/lib/browser/connection-status-service';
 import { ConfigServiceClientImpl } from './config-service-client-impl';
 import { CoreServiceClientImpl } from './core-service-client-impl';
-import { BoardsDetailsMenuUpdater } from './boards/boards-details-menu-updater';
-import { BoardsConfigStore } from './boards/boards-config-store';
+import { BoardsDataMenuUpdater } from './boards/boards-details-menu-updater';
+import { BoardsDataStore } from './boards/boards-data-store';
 import { ILogger } from '@theia/core';
 import { FileSystemExt, FileSystemExtPath } from '../common/protocol/filesystem-ext';
 import { WorkspaceFrontendContribution as TheiaWorkspaceFrontendContribution, FileMenuContribution as TheiaFileMenuContribution } from '@theia/workspace/lib/browser';
@@ -183,9 +183,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
     }).inSingletonScope();
 
     // To be able to track, and update the menu based on the core settings (aka. board details) of the currently selected board.
-    bind(FrontendApplicationContribution).to(BoardsDetailsMenuUpdater).inSingletonScope();
-    bind(BoardsConfigStore).toSelf().inSingletonScope();
-    bind(FrontendApplicationContribution).toService(BoardsConfigStore);
+    bind(FrontendApplicationContribution).to(BoardsDataMenuUpdater).inSingletonScope();
+    bind(BoardsDataStore).toSelf().inSingletonScope();
+    bind(FrontendApplicationContribution).toService(BoardsDataStore);
     // Logger for the Arduino daemon
     bind(ILogger).toDynamicValue(ctx => {
         const parentLogger = ctx.container.get<ILogger>(ILogger);
diff --git a/arduino-ide-extension/src/browser/boards/boards-config-store.ts b/arduino-ide-extension/src/browser/boards/boards-data-store.ts
similarity index 64%
rename from arduino-ide-extension/src/browser/boards/boards-config-store.ts
rename to arduino-ide-extension/src/browser/boards/boards-data-store.ts
index 8967174c..8ef40676 100644
--- a/arduino-ide-extension/src/browser/boards/boards-config-store.ts
+++ b/arduino-ide-extension/src/browser/boards/boards-data-store.ts
@@ -1,14 +1,15 @@
 import { injectable, inject, named } from 'inversify';
 import { ILogger } from '@theia/core/lib/common/logger';
+import { deepClone } from '@theia/core/lib/common/objects';
 import { MaybePromise } from '@theia/core/lib/common/types';
 import { Event, Emitter } from '@theia/core/lib/common/event';
-import { deepClone, notEmpty } from '@theia/core/lib/common/objects';
 import { FrontendApplicationContribution, LocalStorageService } from '@theia/core/lib/browser';
-import { BoardsService, ConfigOption, Installable, BoardDetails } from '../../common/protocol';
+import { notEmpty } from '../../common/utils';
 import { BoardsServiceClientImpl } from './boards-service-client-impl';
+import { BoardsService, ConfigOption, Installable, BoardDetails, Programmer } from '../../common/protocol';
 
 @injectable()
-export class BoardsConfigStore implements FrontendApplicationContribution {
+export class BoardsDataStore implements FrontendApplicationContribution {
 
     @inject(ILogger)
     @named('store')
@@ -60,39 +61,59 @@ export class BoardsConfigStore implements FrontendApplicationContribution {
         fqbn: string,
         boardsPackageVersion: MaybePromise<Installable.Version | undefined> = this.getBoardsPackageVersion(fqbn)): Promise<string> {
 
-        const configOptions = await this.getConfig(fqbn, boardsPackageVersion);
+        const { configOptions } = await this.getData(fqbn, boardsPackageVersion);
         return ConfigOption.decorate(fqbn, configOptions);
     }
 
-    async getConfig(
+    async getData(
         fqbn: string,
-        boardsPackageVersion: MaybePromise<Installable.Version | undefined> = this.getBoardsPackageVersion(fqbn)): Promise<ConfigOption[]> {
+        boardsPackageVersion: MaybePromise<Installable.Version | undefined> = this.getBoardsPackageVersion(fqbn)): Promise<BoardsDataStore.Data> {
 
         const version = await boardsPackageVersion;
         if (!version) {
-            return [];
+            return BoardsDataStore.Data.EMPTY;
         }
         const key = this.getStorageKey(fqbn, version);
-        let configOptions = await this.storageService.getData<ConfigOption[] | undefined>(key, undefined);
-        if (configOptions) {
-            return configOptions;
+        let data = await this.storageService.getData<BoardsDataStore.Data | undefined>(key, undefined);
+        if (data) {
+            if (data.programmers !== undefined) { // to be backward compatible. We did not save the `programmers` into the `localStorage`.
+                return data;
+            }
         }
 
-        const details = await this.getBoardDetailsSafe(fqbn);
-        if (!details) {
-            return [];
+        const boardDetails = await this.getBoardDetailsSafe(fqbn);
+        if (!boardDetails) {
+            return BoardsDataStore.Data.EMPTY;
         }
 
-        configOptions = details.configOptions;
-        await this.storageService.setData(key, configOptions);
-        return configOptions;
+        data = { configOptions: boardDetails.configOptions, programmers: boardDetails.programmers };
+        await this.storageService.setData(key, data);
+        return data;
     }
 
-    async setSelected(
+    async selectProgrammer(
+        { fqbn, programmer }: { fqbn: string, programmer: Programmer },
+        boardsPackageVersion: MaybePromise<Installable.Version | undefined> = this.getBoardsPackageVersion(fqbn)): Promise<boolean> {
+
+        const { configOptions, programmers } = deepClone(await this.getData(fqbn, boardsPackageVersion));
+        if (!programmers.find(p => Programmer.equals(programmer, p))) {
+            return false;
+        }
+
+        const version = await boardsPackageVersion;
+        if (!version) {
+            return false;
+        }
+        await this.setData({ fqbn, data: { configOptions, programmers }, version });
+        this.fireChanged();
+        return true;
+    }
+
+    async selectConfigOption(
         { fqbn, option, selectedValue }: { fqbn: string, option: string, selectedValue: string },
         boardsPackageVersion: MaybePromise<Installable.Version | undefined> = this.getBoardsPackageVersion(fqbn)): Promise<boolean> {
 
-        const configOptions = deepClone(await this.getConfig(fqbn, boardsPackageVersion));
+        const { configOptions, programmers } = deepClone(await this.getData(fqbn, boardsPackageVersion));
         const configOption = configOptions.find(c => c.option === option);
         if (!configOption) {
             return false;
@@ -113,16 +134,16 @@ export class BoardsConfigStore implements FrontendApplicationContribution {
         if (!version) {
             return false;
         }
-        await this.setConfig({ fqbn, configOptions, version });
+        await this.setData({ fqbn, data: { configOptions, programmers }, version });
         this.fireChanged();
         return true;
     }
 
-    protected async setConfig(
-        { fqbn, configOptions, version }: { fqbn: string, configOptions: ConfigOption[], version: Installable.Version }): Promise<void> {
+    protected async setData(
+        { fqbn, data, version }: { fqbn: string, data: BoardsDataStore.Data, version: Installable.Version }): Promise<void> {
 
         const key = this.getStorageKey(fqbn, version);
-        return this.storageService.setData(key, configOptions);
+        return this.storageService.setData(key, data);
     }
 
     protected getStorageKey(fqbn: string, version: Installable.Version): string {
@@ -159,3 +180,16 @@ export class BoardsConfigStore implements FrontendApplicationContribution {
     }
 
 }
+
+export namespace BoardsDataStore {
+    export interface Data {
+        readonly configOptions: ConfigOption[];
+        readonly programmers: Programmer[];
+    }
+    export namespace Data {
+        export const EMPTY: Data = {
+            configOptions: [],
+            programmers: []
+        };
+    }
+}
diff --git a/arduino-ide-extension/src/browser/boards/boards-details-menu-updater.ts b/arduino-ide-extension/src/browser/boards/boards-details-menu-updater.ts
index e7f1688d..8a135703 100644
--- a/arduino-ide-extension/src/browser/boards/boards-details-menu-updater.ts
+++ b/arduino-ide-extension/src/browser/boards/boards-details-menu-updater.ts
@@ -5,12 +5,12 @@ import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposa
 import { BoardsServiceClientImpl } from './boards-service-client-impl';
 import { Board, ConfigOption } from '../../common/protocol';
 import { FrontendApplicationContribution } from '@theia/core/lib/browser';
-import { BoardsConfigStore } from './boards-config-store';
+import { BoardsDataStore } from './boards-data-store';
 import { MainMenuManager } from '../../common/main-menu-manager';
 import { ArduinoMenus } from '../menu/arduino-menus';
 
 @injectable()
-export class BoardsDetailsMenuUpdater implements FrontendApplicationContribution {
+export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
 
     @inject(CommandRegistry)
     protected readonly commandRegistry: CommandRegistry;
@@ -21,8 +21,8 @@ export class BoardsDetailsMenuUpdater implements FrontendApplicationContribution
     @inject(MainMenuManager)
     protected readonly mainMenuManager: MainMenuManager;
 
-    @inject(BoardsConfigStore)
-    protected readonly boardsConfigStore: BoardsConfigStore;
+    @inject(BoardsDataStore)
+    protected readonly boardsDataStore: BoardsDataStore;
 
     @inject(BoardsServiceClientImpl)
     protected readonly boardsServiceClient: BoardsServiceClientImpl;
@@ -30,7 +30,7 @@ export class BoardsDetailsMenuUpdater implements FrontendApplicationContribution
     protected readonly toDisposeOnBoardChange = new DisposableCollection();
 
     onStart(): void {
-        this.boardsConfigStore.onChanged(() => this.updateMenuActions(this.boardsServiceClient.boardsConfig.selectedBoard));
+        this.boardsDataStore.onChanged(() => this.updateMenuActions(this.boardsServiceClient.boardsConfig.selectedBoard));
         this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) => this.updateMenuActions(selectedBoard));
         this.updateMenuActions(this.boardsServiceClient.boardsConfig.selectedBoard);
     }
@@ -41,8 +41,8 @@ export class BoardsDetailsMenuUpdater implements FrontendApplicationContribution
             this.mainMenuManager.update();
             const { fqbn } = selectedBoard;
             if (fqbn) {
-                const configOptions = await this.boardsConfigStore.getConfig(fqbn);
-                const boardsConfigMenuPath = [...ArduinoMenus.TOOLS, 'z_boardsConfig']; // `z_` is for ordering.
+                const { configOptions, programmers } = await this.boardsDataStore.getData(fqbn);
+                const boardsConfigMenuPath = [...ArduinoMenus.TOOLS, 'z01_boardsConfig']; // `z_` is for ordering.
                 for (const { label, option, values } of configOptions.sort(ConfigOption.LABEL_COMPARATOR)) {
                     const menuPath = [...boardsConfigMenuPath, `${option}`];
                     const commands = new Map<string, Disposable & { label: string }>()
@@ -51,7 +51,7 @@ export class BoardsDetailsMenuUpdater implements FrontendApplicationContribution
                         const command = { id };
                         const selectedValue = value.value;
                         const handler = {
-                            execute: () => this.boardsConfigStore.setSelected({ fqbn, option, selectedValue }),
+                            execute: () => this.boardsDataStore.selectConfigOption({ fqbn, option, selectedValue }),
                             isToggled: () => value.selected
                         };
                         commands.set(id, Object.assign(this.commandRegistry.registerCommand(command, handler), { label: value.label }));
@@ -67,6 +67,18 @@ export class BoardsDetailsMenuUpdater implements FrontendApplicationContribution
                         })
                     ]);
                 }
+                const programmersMenuPath = [...ArduinoMenus.TOOLS, 'z02_programmers'];
+                for (const programmer of programmers) {
+                    const { id, name } = programmer;
+                    const menuPath = [...programmersMenuPath, `${name}`];
+                    const command = { id: `${fqbn}-programmer--${id}` };
+                    const handler = { execute: () => this.boardsDataStore.selectProgrammer({ fqbn, programmer }) };
+                    this.menuRegistry.registerMenuAction(menuPath, { commandId: command.id, label: name });
+                    this.toDisposeOnBoardChange.pushAll([
+                        this.commandRegistry.registerCommand(command, handler),
+                        Disposable.create(() => this.menuRegistry.unregisterMenuAction(command, menuPath))
+                    ]);
+                }
                 this.mainMenuManager.update();
             }
         }
diff --git a/arduino-ide-extension/src/browser/boards/quick-open/boards-quick-open-service.ts b/arduino-ide-extension/src/browser/boards/quick-open/boards-quick-open-service.ts
index 6f9f6b74..45c76041 100644
--- a/arduino-ide-extension/src/browser/boards/quick-open/boards-quick-open-service.ts
+++ b/arduino-ide-extension/src/browser/boards/quick-open/boards-quick-open-service.ts
@@ -17,7 +17,7 @@ import {
 import { naturalCompare } from '../../../common/utils';
 import { BoardsService, Port, Board, ConfigOption, ConfigValue } from '../../../common/protocol';
 import { CoreServiceClientImpl } from '../../core-service-client-impl';
-import { BoardsConfigStore } from '../boards-config-store';
+import { BoardsDataStore } from '../boards-data-store';
 import { BoardsServiceClientImpl, AvailableBoard } from '../boards-service-client-impl';
 
 @injectable()
@@ -41,8 +41,8 @@ export class BoardsQuickOpenService implements QuickOpenContribution, QuickOpenM
     @inject(BoardsServiceClientImpl)
     protected readonly boardsServiceClient: BoardsServiceClientImpl;
 
-    @inject(BoardsConfigStore)
-    protected readonly configStore: BoardsConfigStore;
+    @inject(BoardsDataStore)
+    protected readonly boardsDataStore: BoardsDataStore;
 
     @inject(CoreServiceClientImpl)
     protected coreServiceClient: CoreServiceClientImpl;
@@ -52,7 +52,7 @@ export class BoardsQuickOpenService implements QuickOpenContribution, QuickOpenM
     // Attached boards plus the user's config.
     protected availableBoards: AvailableBoard[] = [];
     // Only for the `selected` one from the `availableBoards`. Note: the `port` of the `selected` is optional.
-    protected boardConfigs: ConfigOption[] = [];
+    protected data: BoardsDataStore.Data = BoardsDataStore.Data.EMPTY;
     protected allBoards: Board.Detailed[] = []
     protected selectedBoard?: (AvailableBoard & { port: Port });
 
@@ -86,7 +86,7 @@ export class BoardsQuickOpenService implements QuickOpenContribution, QuickOpenM
             placeholder += 'No board selected.';
         }
         placeholder += 'Type to filter boards';
-        if (this.boardConfigs.length) {
+        if (this.data.configOptions.length) {
             placeholder += ' or use the ↓↑ keys to adjust the board settings...';
         } else {
             placeholder += '...';
@@ -129,7 +129,7 @@ export class BoardsQuickOpenService implements QuickOpenContribution, QuickOpenM
 
         // Show the config only if the `input` is empty.
         if (!lookFor.trim().length) {
-            toAccept.push(...this.boardConfigs.map((config, i) => {
+            toAccept.push(...this.data.configOptions.map((config, i) => {
                 let group: QuickOpenGroupItemOptions | undefined = undefined;
                 if (i === 0) {
                     group = { groupLabel: 'Board Settings', showBorder: true };
@@ -157,14 +157,14 @@ export class BoardsQuickOpenService implements QuickOpenContribution, QuickOpenM
     protected async update(availableBoards: AvailableBoard[]): Promise<void> {
         // `selectedBoard` is not an attached board, we need to show the board settings for it (TODO: clarify!)
         const selectedBoard = availableBoards.filter(AvailableBoard.hasPort).find(({ selected }) => selected);
-        const [configs, boards] = await Promise.all([
-            selectedBoard && selectedBoard.fqbn ? this.configStore.getConfig(selectedBoard.fqbn) : Promise.resolve([]),
+        const [data, boards] = await Promise.all([
+            selectedBoard && selectedBoard.fqbn ? this.boardsDataStore.getData(selectedBoard.fqbn) : Promise.resolve(BoardsDataStore.Data.EMPTY),
             this.boardsService.allBoards({})
         ]);
         this.allBoards = Board.decorateBoards(selectedBoard, boards)
             .filter(board => !availableBoards.some(availableBoard => Board.sameAs(availableBoard, board)));
         this.availableBoards = availableBoards;
-        this.boardConfigs = configs;
+        this.data = data;
         this.selectedBoard = selectedBoard;
 
         if (this.isOpen) {
@@ -280,7 +280,7 @@ export class BoardsQuickOpenService implements QuickOpenContribution, QuickOpenM
                     return;
                 }
                 const { fqbn } = this.selectedBoard;
-                this.configStore.setSelected({
+                this.boardsDataStore.selectConfigOption({
                     fqbn,
                     option: config.option,
                     selectedValue: value.value
diff --git a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts
index 8ce87723..5012338f 100644
--- a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts
+++ b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts
@@ -1,7 +1,7 @@
 import { inject, injectable } from 'inversify';
 import { CoreService } from '../../common/protocol';
 import { MonitorConnection } from '../monitor/monitor-connection';
-import { BoardsConfigStore } from '../boards/boards-config-store';
+import { BoardsDataStore } from '../boards/boards-data-store';
 import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl';
 import { ArduinoMenus } from '../menu/arduino-menus';
 import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
@@ -16,8 +16,8 @@ export class UploadSketch extends SketchContribution {
     @inject(MonitorConnection)
     protected readonly monitorConnection: MonitorConnection;
 
-    @inject(BoardsConfigStore)
-    protected readonly boardsConfigStore: BoardsConfigStore;
+    @inject(BoardsDataStore)
+    protected readonly boardsDataStore: BoardsDataStore;
 
     @inject(BoardsServiceClientImpl)
     protected readonly boardsServiceClientImpl: BoardsServiceClientImpl;
@@ -77,7 +77,7 @@ export class UploadSketch extends SketchContribution {
             if (!boardsConfig.selectedBoard.fqbn) {
                 throw new Error(`No core is installed for the '${boardsConfig.selectedBoard.name}' board. Please install the core.`);
             }
-            const fqbn = await this.boardsConfigStore.appendConfigToFqbn(boardsConfig.selectedBoard.fqbn);
+            const fqbn = await this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard.fqbn);
             await this.coreService.upload({
                 sketchUri: sketch.uri,
                 fqbn,
diff --git a/arduino-ide-extension/src/browser/contributions/verify-sketch.ts b/arduino-ide-extension/src/browser/contributions/verify-sketch.ts
index 6c52783c..3421c5be 100644
--- a/arduino-ide-extension/src/browser/contributions/verify-sketch.ts
+++ b/arduino-ide-extension/src/browser/contributions/verify-sketch.ts
@@ -1,7 +1,7 @@
 import { inject, injectable } from 'inversify';
 import { ArduinoMenus } from '../menu/arduino-menus';
 import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
-import { BoardsConfigStore } from '../boards/boards-config-store';
+import { BoardsDataStore } from '../boards/boards-data-store';
 import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl';
 import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution';
 import { CoreService } from '../../common/protocol';
@@ -12,8 +12,8 @@ export class VerifySketch extends SketchContribution {
     @inject(CoreService)
     protected readonly coreService: CoreService;
 
-    @inject(BoardsConfigStore)
-    protected readonly boardsConfigStore: BoardsConfigStore;
+    @inject(BoardsDataStore)
+    protected readonly boardsDataStore: BoardsDataStore;
 
     @inject(BoardsServiceClientImpl)
     protected readonly boardsServiceClientImpl: BoardsServiceClientImpl;
@@ -65,7 +65,7 @@ export class VerifySketch extends SketchContribution {
             if (!boardsConfig.selectedBoard.fqbn) {
                 throw new Error(`No core is installed for the '${boardsConfig.selectedBoard.name}' board. Please install the core.`);
             }
-            const fqbn = await this.boardsConfigStore.appendConfigToFqbn(boardsConfig.selectedBoard.fqbn);
+            const fqbn = await this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard.fqbn);
             await this.coreService.compile({
                 sketchUri: sketch.uri,
                 fqbn,
diff --git a/arduino-ide-extension/src/common/protocol/boards-service.ts b/arduino-ide-extension/src/common/protocol/boards-service.ts
index ba0b3dd3..8c6c7dc0 100644
--- a/arduino-ide-extension/src/common/protocol/boards-service.ts
+++ b/arduino-ide-extension/src/common/protocol/boards-service.ts
@@ -205,6 +205,7 @@ export interface BoardDetails {
     readonly fqbn: string;
     readonly requiredTools: Tool[];
     readonly configOptions: ConfigOption[];
+    readonly programmers: Programmer[];
 }
 
 export interface Tool {
@@ -269,6 +270,23 @@ export interface ConfigValue {
     readonly selected: boolean;
 }
 
+export interface Programmer {
+    readonly name: string;
+    readonly platform: string;
+    readonly id: string;
+}
+export namespace Programmer {
+    export function equals(left: Programmer | undefined, right: Programmer | undefined): boolean {
+        if (!left) {
+            return !right;
+        }
+        if (!right) {
+            return !left;
+        }
+        return left.id === right.id && left.name === right.name && left.platform === right.platform;
+    }
+}
+
 export namespace Board {
 
     export function is(board: any): board is Board {
diff --git a/arduino-ide-extension/src/common/utils.ts b/arduino-ide-extension/src/common/utils.ts
index 77dbaf54..7c438918 100644
--- a/arduino-ide-extension/src/common/utils.ts
+++ b/arduino-ide-extension/src/common/utils.ts
@@ -1 +1,5 @@
 export const naturalCompare: (left: string, right: string) => number = require('string-natural-compare').caseInsensitive;
+
+export function notEmpty(arg: string | undefined | null): arg is string {
+    return !!arg;
+}
diff --git a/arduino-ide-extension/src/node/boards-service-impl.ts b/arduino-ide-extension/src/node/boards-service-impl.ts
index d70f72a1..a46e1360 100644
--- a/arduino-ide-extension/src/node/boards-service-impl.ts
+++ b/arduino-ide-extension/src/node/boards-service-impl.ts
@@ -1,7 +1,7 @@
 import { injectable, inject, postConstruct, named } from 'inversify';
 import { ILogger } from '@theia/core/lib/common/logger';
 import { Deferred } from '@theia/core/lib/common/promise-util';
-import { BoardsService, BoardsPackage, Board, BoardsServiceClient, Port, BoardDetails, Tool, ConfigOption, ConfigValue } from '../common/protocol';
+import { BoardsService, BoardsPackage, Board, BoardsServiceClient, Port, BoardDetails, Tool, ConfigOption, ConfigValue, Programmer } from '../common/protocol';
 import {
     PlatformSearchReq, PlatformSearchResp, PlatformInstallReq, PlatformInstallResp, PlatformListReq,
     PlatformListResp, Platform, PlatformUninstallResp, PlatformUninstallReq
@@ -10,6 +10,7 @@ import { CoreClientProvider } from './core-client-provider';
 import { BoardListReq, BoardListResp, BoardDetailsReq, BoardDetailsResp } from './cli-protocol/commands/board_pb';
 import { ToolOutputServiceServer } from '../common/protocol/tool-output-service';
 import { Installable } from '../common/protocol/installable';
+import { ListProgrammersAvailableForUploadReq, ListProgrammersAvailableForUploadResp } from './cli-protocol/commands/upload_pb';
 
 @injectable()
 export class BoardsServiceImpl implements BoardsService {
@@ -209,10 +210,10 @@ export class BoardsServiceImpl implements BoardsService {
         const { client, instance } = coreClient;
 
         const { fqbn } = options;
-        const req = new BoardDetailsReq();
-        req.setInstance(instance);
-        req.setFqbn(fqbn);
-        const resp = await new Promise<BoardDetailsResp>((resolve, reject) => client.boardDetails(req, (err, resp) => {
+        const detailsReq = new BoardDetailsReq();
+        detailsReq.setInstance(instance);
+        detailsReq.setFqbn(fqbn);
+        const detailsResp = await new Promise<BoardDetailsResp>((resolve, reject) => client.boardDetails(detailsReq, (err, resp) => {
             if (err) {
                 reject(err);
                 return;
@@ -220,13 +221,13 @@ export class BoardsServiceImpl implements BoardsService {
             resolve(resp);
         }));
 
-        const requiredTools = resp.getToolsdependenciesList().map(t => <Tool>{
+        const requiredTools = detailsResp.getToolsdependenciesList().map(t => <Tool>{
             name: t.getName(),
             packager: t.getPackager(),
             version: t.getVersion()
         });
 
-        const configOptions = resp.getConfigOptionsList().map(c => <ConfigOption>{
+        const configOptions = detailsResp.getConfigOptionsList().map(c => <ConfigOption>{
             label: c.getOptionLabel(),
             option: c.getOption(),
             values: c.getValuesList().map(v => <ConfigValue>{
@@ -236,10 +237,28 @@ export class BoardsServiceImpl implements BoardsService {
             })
         });
 
+        const listReq = new ListProgrammersAvailableForUploadReq();
+        listReq.setInstance(instance);
+        listReq.setFqbn(fqbn);
+        const listResp = await new Promise<ListProgrammersAvailableForUploadResp>((resolve, reject) => client.listProgrammersAvailableForUpload(listReq, (err, resp) => {
+            if (err) {
+                reject(err);
+                return;
+            }
+            resolve(resp);
+        }));
+
+        const programmers = listResp.getProgrammersList().map(p => <Programmer>{
+            id: p.getId(),
+            name: p.getName(),
+            platform: p.getPlatform()
+        });
+
         return {
             fqbn,
             requiredTools,
-            configOptions
+            configOptions,
+            programmers
         };
     }