diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..55712c19 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib" +} \ No newline at end of file diff --git a/arduino-ide-extension/src/browser/arduino-commands.ts b/arduino-ide-extension/src/browser/arduino-commands.ts index 0875e04e..e56f4e2d 100644 --- a/arduino-ide-extension/src/browser/arduino-commands.ts +++ b/arduino-ide-extension/src/browser/arduino-commands.ts @@ -35,20 +35,11 @@ export namespace ArduinoCommands { category: 'File' } - export const REFRESH_BOARDS: Command = { - id: "arduino-refresh-attached-boards", - label: "Refresh attached boards" - } - - export const SELECT_BOARD: Command = { - id: "arduino-select-board" - } - export const OPEN_BOARDS_DIALOG: Command = { id: "arduino-open-boards-dialog" } - export const TOGGLE_PROMODE: Command = { + export const TOGGLE_PRO_MODE: Command = { id: "arduino-toggle-pro-mode" } diff --git a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx index 124b4972..63f4930a 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx +++ b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx @@ -5,22 +5,21 @@ import { EditorWidget } from '@theia/editor/lib/browser/editor-widget'; import { MessageService } from '@theia/core/lib/common/message-service'; import { CommandContribution, CommandRegistry, Command } from '@theia/core/lib/common/command'; import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; -import { BoardsService, Board } from '../common/protocol/boards-service'; +import { BoardsService } from '../common/protocol/boards-service'; import { ArduinoCommands } from './arduino-commands'; -import { ConnectedBoards } from './components/connected-boards'; import { CoreService } from '../common/protocol/core-service'; import { WorkspaceServiceExt } from './workspace-service-ext'; import { ToolOutputServiceClient } from '../common/protocol/tool-output-service'; import { QuickPickService } from '@theia/core/lib/common/quick-pick-service'; import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution'; -import { BoardsNotificationService } from './boards-notification-service'; +import { BoardsServiceClientImpl } from './boards/boards-service-client-impl'; import { WorkspaceRootUriAwareCommandHandler, WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands'; import { SelectionService, MenuContribution, MenuModelRegistry, MAIN_MENU_BAR } from '@theia/core'; import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; import { SketchFactory } from './sketch-factory'; import { ArduinoToolbar } from './toolbar/arduino-toolbar'; import { EditorManager, EditorMainMenu } from '@theia/editor/lib/browser'; -import { ContextMenuRenderer, OpenerService, Widget, StatusBar, ShellLayoutRestorer } from '@theia/core/lib/browser'; +import { ContextMenuRenderer, OpenerService, Widget, StatusBar, ShellLayoutRestorer, StatusBarAlignment } from '@theia/core/lib/browser'; import { OpenFileDialogProps, FileDialogService } from '@theia/filesystem/lib/browser/file-dialog'; import { FileSystem, FileStat } from '@theia/filesystem/lib/common'; import { ArduinoToolbarContextMenu } from './arduino-file-menu'; @@ -32,8 +31,9 @@ import { FileDownloadCommands } from '@theia/filesystem/lib/browser/download/fil import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu'; import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution'; import { MaybePromise } from '@theia/core/lib/common/types'; -import { SelectBoardDialog } from './boards/select-board-dialog'; +import { BoardsConfigDialog } from './boards/boards-config-dialog'; import { BoardsToolBarItem } from './boards/boards-toolbar-item'; +import { BoardsConfig } from './boards/boards-config'; export namespace ArduinoMenus { export const SKETCH = [...MAIN_MENU_BAR, '3_sketch']; @@ -41,8 +41,7 @@ export namespace ArduinoMenus { } export const ARDUINO_PRO_MODE: boolean = (() => { - const proModeStr = window.localStorage.getItem('arduino-pro-mode'); - return proModeStr === 'true'; + return window.localStorage.getItem('arduino-pro-mode') === 'true'; })(); @injectable() @@ -52,7 +51,7 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C protected readonly messageService: MessageService; @inject(BoardsService) - protected readonly boardService: BoardsService; + protected readonly boardsService: BoardsService; @inject(CoreService) protected readonly coreService: CoreService; @@ -69,8 +68,8 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C @inject(BoardsListWidgetFrontendContribution) protected readonly boardsListWidgetFrontendContribution: BoardsListWidgetFrontendContribution; - @inject(BoardsNotificationService) - protected readonly boardsNotificationService: BoardsNotificationService; + @inject(BoardsServiceClientImpl) + protected readonly boardsServiceClient: BoardsServiceClientImpl; @inject(SelectionService) protected readonly selectionService: SelectionService; @@ -99,8 +98,8 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C @inject(SketchesService) protected readonly sketches: SketchesService; - @inject(SelectBoardDialog) - protected readonly selectBoardsDialog: SelectBoardDialog; + @inject(BoardsConfigDialog) + protected readonly boardsConfigDialog: BoardsConfigDialog; @inject(MenuModelRegistry) protected readonly menuRegistry: MenuModelRegistry; @@ -129,6 +128,15 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C protected async init(): Promise { // This is a hack. Otherwise, the backend services won't bind. await this.workspaceServiceExt.roots(); + + const updateStatusBar = (config: BoardsConfig.Config) => { + this.statusBar.setElement('arduino-selected-board', { + alignment: StatusBarAlignment.RIGHT, + text: BoardsConfig.Config.toString(config) + }); + } + this.boardsServiceClient.onBoardsConfigChanged(updateStatusBar); + updateStatusBar(this.boardsServiceClient.boardsConfig); } registerToolbarItems(registry: TabBarToolbarRegistry): void { @@ -157,15 +165,13 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C text: '$(arrow-down)' }); registry.registerItem({ - id: ConnectedBoards.TOOLBAR_ID, + id: BoardsToolBarItem.TOOLBAR_ID, render: () => this.boardsToolbarItem = ref} commands={this.commands} - statusBar={this.statusBar} - contextMenuRenderer={this.contextMenuRenderer} - boardsNotificationService={this.boardsNotificationService} - boardService={this.boardService} />, + boardsServiceClient={this.boardsServiceClient} + boardService={this.boardsService} />, isVisible: widget => this.isArduinoToolbar(widget) }) } @@ -186,7 +192,14 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C } try { - await this.coreService.compile({ uri: uri.toString() }); + const { boardsConfig } = this.boardsServiceClient; + if (!boardsConfig || !boardsConfig.selectedBoard) { + throw new Error('No boards selected. Please select a board.'); + } + if (!boardsConfig.selectedBoard.fqbn) { + throw new Error(`No core is installed for ${boardsConfig.selectedBoard.name}. Please install the board.`); + } + await this.coreService.compile({ uri: uri.toString(), board: boardsConfig.selectedBoard }); } catch (e) { await this.messageService.error(e.toString()); } @@ -207,7 +220,15 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C } try { - await this.coreService.upload({ uri: uri.toString() }); + const { boardsConfig } = this.boardsServiceClient; + if (!boardsConfig || !boardsConfig.selectedBoard) { + throw new Error('No boards selected. Please select a board.'); + } + const { selectedPort } = boardsConfig; + if (!selectedPort) { + throw new Error('No ports selected. Please select a port.'); + } + await this.coreService.upload({ uri: uri.toString(), board: boardsConfig.selectedBoard, port: selectedPort }); } catch (e) { await this.messageService.error(e.toString()); } @@ -261,26 +282,16 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C } } })); - registry.registerCommand(ArduinoCommands.REFRESH_BOARDS, { - isEnabled: () => true, - execute: () => this.boardsNotificationService.notifyBoardsInstalled() - }); - registry.registerCommand(ArduinoCommands.SELECT_BOARD, { - isEnabled: () => true, - execute: async (board: Board) => { - this.selectBoard(board); - } - }) registry.registerCommand(ArduinoCommands.OPEN_BOARDS_DIALOG, { isEnabled: () => true, execute: async () => { - const boardAndPort = await this.selectBoardsDialog.open(); - if (boardAndPort && boardAndPort.board) { - this.selectBoard(boardAndPort.board); + const boardsConfig = await this.boardsConfigDialog.open(); + if (boardsConfig) { + this.boardsServiceClient.boardsConfig = boardsConfig; } } }) - registry.registerCommand(ArduinoCommands.TOGGLE_PROMODE, { + registry.registerCommand(ArduinoCommands.TOGGLE_PRO_MODE, { execute: () => { const oldModeState = ARDUINO_PRO_MODE; window.localStorage.setItem('arduino-pro-mode', oldModeState ? 'false' : 'true'); @@ -290,13 +301,6 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C }) } - protected async selectBoard(board: Board) { - await this.boardService.selectBoard(board); - if (this.boardsToolbarItem) { - this.boardsToolbarItem.setSelectedBoard(board); - } - } - registerMenus(registry: MenuModelRegistry) { if (!ARDUINO_PRO_MODE) { registry.unregisterMenuAction(FileSystemCommands.UPLOAD); @@ -335,7 +339,7 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C registry.registerSubmenu(ArduinoMenus.TOOLS, 'Tools'); registry.registerMenuAction(CommonMenus.HELP, { - commandId: ArduinoCommands.TOGGLE_PROMODE.id, + commandId: ArduinoCommands.TOGGLE_PRO_MODE.id, label: 'Advanced Mode' }) } diff --git a/arduino-ide-extension/src/browser/arduino-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-frontend-module.ts index 729bc310..b6b757c1 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-frontend-module.ts @@ -11,7 +11,7 @@ import { LibraryListWidget } from './library/library-list-widget'; import { ArduinoFrontendContribution, ARDUINO_PRO_MODE } from './arduino-frontend-contribution'; import { ArduinoLanguageGrammarContribution } from './language/arduino-language-grammar-contribution'; import { LibraryService, LibraryServicePath } from '../common/protocol/library-service'; -import { BoardsService, BoardsServicePath } from '../common/protocol/boards-service'; +import { BoardsService, BoardsServicePath, BoardsServiceClient } from '../common/protocol/boards-service'; import { SketchesService, SketchesServicePath } from '../common/protocol/sketches-service'; import { LibraryListWidgetFrontendContribution } from './library/list-widget-frontend-contribution'; import { CoreService, CoreServicePath } from '../common/protocol/core-service'; @@ -22,7 +22,7 @@ import { WorkspaceServiceExtImpl } from './workspace-service-ext-impl'; import { ToolOutputServiceClient } from '../common/protocol/tool-output-service'; import { ToolOutputService } from '../common/protocol/tool-output-service'; import { ToolOutputServiceClientImpl } from './tool-output/client-service-impl'; -import { BoardsNotificationService } from './boards-notification-service'; +import { BoardsServiceClientImpl } from './boards/boards-service-client-impl'; import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; import { AWorkspaceService } from './arduino-workspace-service'; import { ThemeService } from '@theia/core/lib/browser/theming'; @@ -46,8 +46,8 @@ import { SilentMonacoStatusBarContribution } from './customization/silent-monaco import { ApplicationShell } from '@theia/core/lib/browser'; import { CustomApplicationShell } from './customization/custom-application-shell'; import { CustomFrontendApplication } from './customization/custom-frontend-application'; -import { SelectBoardDialog, SelectBoardDialogProps } from './boards/select-board-dialog'; -import { SelectBoardDialogWidget } from './boards/select-board-dialog-widget'; +import { BoardsConfigDialog, BoardsConfigDialogProps } from './boards/boards-config-dialog'; +import { BoardsConfigDialogWidget } from './boards/boards-config-dialog-widget'; const ElementQueries = require('css-element-queries/src/ElementQueries'); if (!ARDUINO_PRO_MODE) { @@ -87,12 +87,19 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un // Sketch list service bind(SketchesService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, SketchesServicePath)).inSingletonScope(); - // Boards Notification service for updating boards list - // TODO (post-PoC): move this to boards service/backend - bind(BoardsNotificationService).toSelf().inSingletonScope(); - // Boards service - bind(BoardsService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, BoardsServicePath)).inSingletonScope(); + bind(BoardsService).toDynamicValue(context => { + const connection = context.container.get(WebSocketConnectionProvider); + const client = context.container.get(BoardsServiceClientImpl); + return connection.createProxy(BoardsServicePath, client); + }).inSingletonScope(); + // Boards service client to receive and delegate notifications from the backend. + bind(BoardsServiceClientImpl).toSelf().inSingletonScope(); + bind(BoardsServiceClient).toDynamicValue(context => { + const client = context.container.get(BoardsServiceClientImpl); + WebSocketConnectionProvider.createProxy(context.container, BoardsServicePath, client); + return client; + }).inSingletonScope(); // Boards list widget bind(BoardsListWidget).toSelf(); @@ -104,9 +111,9 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un bind(FrontendApplicationContribution).toService(BoardsListWidgetFrontendContribution); // Board select dialog - bind(SelectBoardDialogWidget).toSelf().inSingletonScope(); - bind(SelectBoardDialog).toSelf().inSingletonScope(); - bind(SelectBoardDialogProps).toConstantValue({ + bind(BoardsConfigDialogWidget).toSelf().inSingletonScope(); + bind(BoardsConfigDialog).toSelf().inSingletonScope(); + bind(BoardsConfigDialogProps).toConstantValue({ title: 'Select Board' }) diff --git a/arduino-ide-extension/src/browser/boards-notification-service.ts b/arduino-ide-extension/src/browser/boards-notification-service.ts deleted file mode 100644 index 0d529685..00000000 --- a/arduino-ide-extension/src/browser/boards-notification-service.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { EventEmitter } from "events"; -import { injectable } from "inversify"; - -// TODO (post-PoC): move this to the backend / BoardsService -@injectable() -export class BoardsNotificationService { - - protected readonly emitter = new EventEmitter(); - - public on(event: 'boards-installed', listener: (...args: any[]) => void): this { - this.emitter.on(event, listener); - return this; - } - - public notifyBoardsInstalled() { - this.emitter.emit('boards-installed'); - } - -} \ No newline at end of file diff --git a/arduino-ide-extension/src/browser/boards/boards-config-dialog-widget.tsx b/arduino-ide-extension/src/browser/boards/boards-config-dialog-widget.tsx new file mode 100644 index 00000000..70ff3133 --- /dev/null +++ b/arduino-ide-extension/src/browser/boards/boards-config-dialog-widget.tsx @@ -0,0 +1,55 @@ +import * as React from 'react'; +import { injectable, inject } from 'inversify'; +import { Emitter } from '@theia/core/lib/common/event'; +import { ReactWidget, Message } from '@theia/core/lib/browser'; +import { BoardsService } from '../../common/protocol/boards-service'; +import { BoardsConfig } from './boards-config'; +import { BoardsServiceClientImpl } from './boards-service-client-impl'; + +@injectable() +export class BoardsConfigDialogWidget extends ReactWidget { + + @inject(BoardsService) + protected readonly boardsService: BoardsService; + + @inject(BoardsServiceClientImpl) + protected readonly boardsServiceClient: BoardsServiceClientImpl; + + protected readonly onBoardConfigChangedEmitter = new Emitter(); + readonly onBoardConfigChanged = this.onBoardConfigChangedEmitter.event; + + protected focusNode: HTMLElement | undefined; + + constructor() { + super(); + this.id = 'select-board-dialog'; + } + + protected fireConfigChanged = (config: BoardsConfig.Config) => { + this.onBoardConfigChangedEmitter.fire(config); + } + + protected setFocusNode = (element: HTMLElement | undefined) => { + this.focusNode = element; + } + + protected render(): React.ReactNode { + return
+ +
; + } + + protected onActivateRequest(msg: Message): void { + super.onActivateRequest(msg); + if (this.focusNode instanceof HTMLInputElement) { + this.focusNode.select(); + } + (this.focusNode || this.node).focus(); + } + + +} diff --git a/arduino-ide-extension/src/browser/boards/boards-config-dialog.ts b/arduino-ide-extension/src/browser/boards/boards-config-dialog.ts new file mode 100644 index 00000000..92495b2a --- /dev/null +++ b/arduino-ide-extension/src/browser/boards/boards-config-dialog.ts @@ -0,0 +1,113 @@ +import { injectable, inject, postConstruct } from 'inversify'; +import { Message } from '@phosphor/messaging'; +import { AbstractDialog, DialogProps, Widget, DialogError } from '@theia/core/lib/browser'; +import { BoardsService } from '../../common/protocol/boards-service'; +import { BoardsConfig } from './boards-config'; +import { BoardsConfigDialogWidget } from './boards-config-dialog-widget'; +import { BoardsServiceClientImpl } from './boards-service-client-impl'; + +@injectable() +export class BoardsConfigDialogProps extends DialogProps { +} + +@injectable() +export class BoardsConfigDialog extends AbstractDialog { + + @inject(BoardsConfigDialogWidget) + protected readonly widget: BoardsConfigDialogWidget; + + @inject(BoardsService) + protected readonly boardService: BoardsService; + + @inject(BoardsServiceClientImpl) + protected readonly boardsServiceClient: BoardsServiceClientImpl; + + protected config: BoardsConfig.Config = {}; + + constructor(@inject(BoardsConfigDialogProps) protected readonly props: BoardsConfigDialogProps) { + super(props); + + this.contentNode.classList.add('select-board-dialog'); + this.contentNode.appendChild(this.createDescription()); + + this.appendCloseButton('CANCEL'); + this.appendAcceptButton('OK'); + } + + @postConstruct() + protected init(): void { + this.toDispose.push(this.boardsServiceClient.onBoardsConfigChanged(config => { + this.config = config; + this.update(); + })); + } + + protected createDescription(): HTMLElement { + const head = document.createElement('div'); + head.classList.add('head'); + + const title = document.createElement('div'); + title.textContent = 'Select Other Board & Port'; + title.classList.add('title'); + head.appendChild(title); + + const text = document.createElement('div'); + text.classList.add('text'); + head.appendChild(text); + + for (const paragraph of [ + 'Select both a Board and a Port if you want to upload a sketch.', + 'If you only select a Board you will be able just to compile, but not to upload your sketch.' + ]) { + const p = document.createElement('p'); + p.textContent = paragraph; + text.appendChild(p); + } + + return head; + } + + protected onAfterAttach(msg: Message): void { + if (this.widget.isAttached) { + Widget.detach(this.widget); + } + Widget.attach(this.widget, this.contentNode); + this.toDisposeOnDetach.push(this.widget.onBoardConfigChanged(config => { + this.config = config; + this.update(); + })); + super.onAfterAttach(msg); + this.update(); + } + + protected onUpdateRequest(msg: Message) { + super.onUpdateRequest(msg); + this.widget.update(); + } + + protected onActivateRequest(msg: Message): void { + super.onActivateRequest(msg); + this.widget.activate(); + } + + protected handleEnter(event: KeyboardEvent): boolean | void { + if (event.target instanceof HTMLTextAreaElement) { + return false; + } + } + + protected isValid(value: BoardsConfig.Config): DialogError { + if (!value.selectedBoard) { + if (value.selectedPort) { + return 'Please pick a board connected to the port you have selected.'; + } + return false; + } + return ''; + } + + get value(): BoardsConfig.Config { + return this.config; + } + +} diff --git a/arduino-ide-extension/src/browser/boards/boards-config.tsx b/arduino-ide-extension/src/browser/boards/boards-config.tsx new file mode 100644 index 00000000..61640d30 --- /dev/null +++ b/arduino-ide-extension/src/browser/boards/boards-config.tsx @@ -0,0 +1,240 @@ +import * as React from 'react'; +import { DisposableCollection } from '@theia/core'; +import { BoardsService, Board, AttachedSerialBoard } from '../../common/protocol/boards-service'; +import { BoardsServiceClientImpl } from './boards-service-client-impl'; + +export namespace BoardsConfig { + + export interface Config { + selectedBoard?: Board; + selectedPort?: string; + } + + export interface Props { + readonly boardsService: BoardsService; + readonly boardsServiceClient: BoardsServiceClientImpl; + readonly onConfigChange: (config: Config) => void; + readonly onFocusNodeSet: (element: HTMLElement | undefined) => void; + } + + export interface State extends Config { + searchResults: Board[]; + knownPorts: string[]; + } + +} + +export abstract class Item extends React.Component<{ + item: T, + name: string, + selected: boolean, + onClick: (item: T) => void, + missing?: boolean }> { + + render(): React.ReactNode { + const { selected, name, missing } = this.props; + const classNames = ['item']; + if (selected) { + classNames.push('selected'); + } + if (missing === true) { + classNames.push('missing') + } + return
+ {name} + {selected ? : ''} +
; + } + + protected onClick = () => { + this.props.onClick(this.props.item); + } + +} + +export class BoardsConfig extends React.Component { + + protected toDispose = new DisposableCollection(); + + constructor(props: BoardsConfig.Props) { + super(props); + + const { boardsConfig } = props.boardsServiceClient; + this.state = { + searchResults: [], + knownPorts: [], + ...boardsConfig + } + } + + componentDidMount() { + this.updateBoards(); + this.props.boardsService.getAttachedBoards().then(({ boards }) => this.updatePorts(boards)); + const { boardsServiceClient: client } = this.props; + this.toDispose.pushAll([ + client.onBoardsChanged(event => this.updatePorts(event.newState.boards)), + client.onBoardsConfigChanged(({ selectedBoard, selectedPort }) => { + this.setState({ selectedBoard, selectedPort }, () => this.fireConfigChanged()); + }) + ]); + } + + componentWillUnmount(): void { + this.toDispose.dispose(); + } + + protected fireConfigChanged() { + const { selectedBoard, selectedPort } = this.state; + this.props.onConfigChange({ selectedBoard, selectedPort }); + } + + protected updateBoards = (eventOrQuery: React.ChangeEvent | string = '') => { + const query = (typeof eventOrQuery === 'string' + ? eventOrQuery + : eventOrQuery.target.value.toLowerCase() + ).trim(); + this.queryBoards({ query }).then(({ searchResults }) => this.setState({ searchResults })); + } + + protected updatePorts = (boards: Board[] = []) => { + this.queryPorts(Promise.resolve({ boards })).then(({ knownPorts }) => { + let { selectedPort } = this.state; + if (!!selectedPort && knownPorts.indexOf(selectedPort) === -1) { + selectedPort = undefined; + } + this.setState({ knownPorts, selectedPort }, () => this.fireConfigChanged()); + }); + } + + protected queryBoards = (options: { query?: string } = {}): Promise<{ searchResults: Board[] }> => { + const { boardsService } = this.props; + const query = (options.query || '').toLocaleLowerCase(); + return new Promise<{ searchResults: Board[] }>(resolve => { + boardsService.search(options) + .then(({ items }) => items + .map(item => item.boards) + .reduce((acc, curr) => acc.concat(curr), []) + .filter(board => board.name.toLocaleLowerCase().indexOf(query) !== -1) + .sort(Board.compare)) + .then(searchResults => resolve({ searchResults })); + }); + } + + protected get attachedBoards(): Promise<{ boards: Board[] }> { + return this.props.boardsService.getAttachedBoards(); + } + + protected queryPorts = (attachedBoards: Promise<{ boards: Board[] }> = this.attachedBoards) => { + return new Promise<{ knownPorts: string[] }>(resolve => { + attachedBoards + .then(({ boards }) => boards + .filter(AttachedSerialBoard.is) + .map(({ port }) => port) + .sort()) + .then(knownPorts => resolve({ knownPorts })); + }); + } + + protected selectPort = (selectedPort: string | undefined) => { + this.setState({ selectedPort }, () => this.fireConfigChanged()); + } + + protected selectBoard = (selectedBoard: Board | undefined) => { + this.setState({ selectedBoard }, () => this.fireConfigChanged()); + } + + protected focusNodeSet = (element: HTMLElement | null) => { + this.props.onFocusNodeSet(element || undefined); + } + + render(): React.ReactNode { + return
+ {this.renderContainer('boards', this.renderBoards.bind(this))} + {this.renderContainer('ports', this.renderPorts.bind(this))} +
; + } + + protected renderContainer(title: string, contentRenderer: () => React.ReactNode): React.ReactNode { + return
+
+
+ {title} +
+ {contentRenderer()} +
+
; + } + + protected renderBoards(): React.ReactNode { + const { selectedBoard } = this.state; + return +
+ + +
+
+ {this.state.searchResults.map((board, index) => + key={`${board.name}-${index}`} + item={board} + name={board.name} + selected={!!selectedBoard && Board.equals(board, selectedBoard)} + onClick={this.selectBoard} + missing={!Board.installed(board)} + />)} +
+
; + } + + protected renderPorts(): React.ReactNode { + return !this.state.knownPorts.length ? + ( +
+ No ports discovered +
+ ) : + ( +
+ {this.state.knownPorts.map(port => + key={port} + item={port} + name={port} + selected={this.state.selectedPort === port} + onClick={this.selectPort} + />)} +
+ ); + } + +} + +export namespace BoardsConfig { + + export namespace Config { + + export function sameAs(config: Config, other: Config | AttachedSerialBoard): boolean { + const { selectedBoard, selectedPort } = config; + if (AttachedSerialBoard.is(other)) { + return !!selectedBoard + && Board.equals(other, selectedBoard) + && selectedPort === other.port; + } + return sameAs(config, other); + } + + export function equals(left: Config, right: Config): boolean { + return left.selectedBoard === right.selectedBoard + && left.selectedPort === right.selectedPort; + } + + export function toString(config: Config, options: { default: string } = { default: '' }): string { + const { selectedBoard, selectedPort: port } = config; + if (!selectedBoard) { + return options.default; + } + const { name } = selectedBoard; + return `${name}${port ? ' at ' + port : ''}`; + } + + } + +} diff --git a/arduino-ide-extension/src/browser/boards/boards-list-widget.tsx b/arduino-ide-extension/src/browser/boards/boards-list-widget.tsx index 88d8265b..5ef50d07 100644 --- a/arduino-ide-extension/src/browser/boards/boards-list-widget.tsx +++ b/arduino-ide-extension/src/browser/boards/boards-list-widget.tsx @@ -13,4 +13,4 @@ export class BoardsListWidget extends ListWidget { } } -} \ No newline at end of file +} diff --git a/arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts b/arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts new file mode 100644 index 00000000..f2b8c2f2 --- /dev/null +++ b/arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts @@ -0,0 +1,71 @@ +import { injectable, inject, postConstruct } from 'inversify'; +import { Emitter, ILogger } from '@theia/core'; +import { BoardsServiceClient, AttachedBoardsChangeEvent, BoardInstalledEvent, AttachedSerialBoard } from '../../common/protocol/boards-service'; +import { BoardsConfig } from './boards-config'; +import { LocalStorageService } from '@theia/core/lib/browser'; + +@injectable() +export class BoardsServiceClientImpl implements BoardsServiceClient { + + @inject(ILogger) + protected logger: ILogger; + + @inject(LocalStorageService) + protected storageService: LocalStorageService; + + protected readonly onAttachedBoardsChangedEmitter = new Emitter(); + protected readonly onBoardInstalledEmitter = new Emitter(); + protected readonly onSelectedBoardsConfigChangedEmitter = new Emitter(); + + protected _boardsConfig: BoardsConfig.Config = {}; + + readonly onBoardsChanged = this.onAttachedBoardsChangedEmitter.event; + readonly onBoardInstalled = this.onBoardInstalledEmitter.event; + readonly onBoardsConfigChanged = this.onSelectedBoardsConfigChangedEmitter.event; + + @postConstruct() + protected init(): void { + this.loadState(); + } + + notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void { + this.logger.info('Attached boards changed: ', JSON.stringify(event)); + const { boards } = event.newState; + const { selectedPort, selectedBoard } = this.boardsConfig; + this.onAttachedBoardsChangedEmitter.fire(event); + // Dynamically unset the port if there is not corresponding attached boards for it. + if (!!selectedPort && boards.filter(AttachedSerialBoard.is).map(({ port }) => port).indexOf(selectedPort) === -1) { + this.boardsConfig = { + selectedBoard, + selectedPort: undefined + }; + } + } + + notifyBoardInstalled(event: BoardInstalledEvent): void { + this.logger.info('Board installed: ', JSON.stringify(event)); + this.onBoardInstalledEmitter.fire(event); + } + + set boardsConfig(config: BoardsConfig.Config) { + this.logger.info('Board config changed: ', JSON.stringify(config)); + this._boardsConfig = config; + this.saveState().then(() => this.onSelectedBoardsConfigChangedEmitter.fire(this._boardsConfig)); + } + + get boardsConfig(): BoardsConfig.Config { + return this._boardsConfig; + } + + protected saveState(): Promise { + return this.storageService.setData('boards-config', this.boardsConfig); + } + + protected async loadState(): Promise { + const boardsConfig = await this.storageService.getData('boards-config'); + if (boardsConfig) { + this.boardsConfig = boardsConfig; + } + } + +} diff --git a/arduino-ide-extension/src/browser/boards/boards-toolbar-item.tsx b/arduino-ide-extension/src/browser/boards/boards-toolbar-item.tsx index 38e0df3f..aca66750 100644 --- a/arduino-ide-extension/src/browser/boards/boards-toolbar-item.tsx +++ b/arduino-ide-extension/src/browser/boards/boards-toolbar-item.tsx @@ -1,61 +1,42 @@ import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import { CommandRegistry, DisposableCollection } from '@theia/core'; import { BoardsService, Board, AttachedSerialBoard } from '../../common/protocol/boards-service'; -import { ContextMenuRenderer, StatusBar, StatusBarAlignment } from '@theia/core/lib/browser'; -import { BoardsNotificationService } from '../boards-notification-service'; -import { Command, CommandRegistry } from '@theia/core'; import { ArduinoCommands } from '../arduino-commands'; -import ReactDOM = require('react-dom'); +import { BoardsServiceClientImpl } from './boards-service-client-impl'; +import { BoardsConfig } from './boards-config'; -export interface BoardsDropdownItem { - label: string; - commandExecutor: () => void; - isSelected: () => boolean; -} - -export interface BoardsDropDownListCoord { - top: number; - left: number; - width: number; - paddingTop: number; -} - -export namespace BoardsDropdownItemComponent { - export interface Props { - label: string; - onClick: () => void; - isSelected: boolean; - } -} - -export class BoardsDropdownItemComponent extends React.Component { - render() { - return
-
{this.props.label}
- {this.props.isSelected ? : ''} -
; - } +export interface BoardsDropDownListCoords { + readonly top: number; + readonly left: number; + readonly width: number; + readonly paddingTop: number; } export namespace BoardsDropDown { export interface Props { - readonly coords: BoardsDropDownListCoord; - readonly isOpen: boolean; - readonly dropDownItems: BoardsDropdownItem[]; - readonly openDialog: () => void; + readonly coords: BoardsDropDownListCoords | 'hidden'; + readonly items: Item[]; + readonly openBoardsConfig: () => void; + } + export interface Item { + readonly label: string; + readonly selected: boolean; + readonly onClick: () => void; } } export class BoardsDropDown extends React.Component { - protected dropdownId: string = 'boards-dropdown-container'; + protected dropdownElement: HTMLElement; constructor(props: BoardsDropDown.Props) { super(props); - let list = document.getElementById(this.dropdownId); + let list = document.getElementById('boards-dropdown-container'); if (!list) { list = document.createElement('div'); - list.id = this.dropdownId; + list.id = 'boards-dropdown-container'; document.body.appendChild(list); this.dropdownElement = list; } @@ -65,179 +46,149 @@ export class BoardsDropDown extends React.Component { return ReactDOM.createPortal(this.renderNode(), this.dropdownElement); } - renderNode(): React.ReactNode { - if (this.props.isOpen) { - return
- { - this.props.dropDownItems.map(item => { - return - - ; - }) - } - -
- } else { + protected renderNode(): React.ReactNode { + const { coords, items } = this.props; + if (coords === 'hidden') { return ''; } + items.push({ + label: 'Select Other Board & Port', + selected: false, + onClick: () => this.props.openBoardsConfig() + }) + return
+ {items.map(this.renderItem)} +
} + + protected renderItem(item: BoardsDropDown.Item): React.ReactNode { + const { label, selected, onClick } = item; + return
+
+ {label} +
+ {selected ? : ''} +
+ } + } export namespace BoardsToolBarItem { + export interface Props { - readonly contextMenuRenderer: ContextMenuRenderer; - readonly boardsNotificationService: BoardsNotificationService; readonly boardService: BoardsService; + readonly boardsServiceClient: BoardsServiceClientImpl; readonly commands: CommandRegistry; - readonly statusBar: StatusBar; } export interface State { - selectedBoard?: Board; - selectedIsAttached: boolean; - boardItems: BoardsDropdownItem[]; - isOpen: boolean; + boardsConfig: BoardsConfig.Config; + attachedBoards: Board[]; + coords: BoardsDropDownListCoords | 'hidden'; } } export class BoardsToolBarItem extends React.Component { - protected attachedBoards: Board[]; - protected dropDownListCoord: BoardsDropDownListCoord; + static TOOLBAR_ID: 'boards-toolbar'; + + protected readonly toDispose: DisposableCollection = new DisposableCollection(); constructor(props: BoardsToolBarItem.Props) { super(props); this.state = { - selectedBoard: undefined, - selectedIsAttached: true, - boardItems: [], - isOpen: false + boardsConfig: this.props.boardsServiceClient.boardsConfig, + attachedBoards: [], + coords: 'hidden' }; document.addEventListener('click', () => { - this.setState({ isOpen: false }); + this.setState({ coords: 'hidden' }); }); } componentDidMount() { - this.setAttachedBoards(); - } - - setSelectedBoard(board: Board) { - if (this.attachedBoards && this.attachedBoards.length) { - this.setState({ selectedIsAttached: !!this.attachedBoards.find(attachedBoard => attachedBoard.name === board.name) }); - } - this.setState({ selectedBoard: board }); - } - - protected async setAttachedBoards() { - this.props.boardService.getAttachedBoards().then(attachedBoards => { - this.attachedBoards = attachedBoards.boards; - if (this.attachedBoards.length) { - this.createBoardDropdownItems(); - this.props.boardService.selectBoard(this.attachedBoards[0]).then(() => this.setSelectedBoard(this.attachedBoards[0])); - } - }) - } - - protected createBoardDropdownItems() { - const boardItems: BoardsDropdownItem[] = []; - this.attachedBoards.forEach(board => { - const { commands } = this.props; - const port = this.getPort(board); - const command: Command = { - id: 'selectBoard' + port - } - commands.registerCommand(command, { - execute: () => { - commands.executeCommand(ArduinoCommands.SELECT_BOARD.id, board); - this.setState({ isOpen: false, selectedBoard: board }); - } - }); - boardItems.push({ - commandExecutor: () => commands.executeCommand(command.id), - label: board.name + ' at ' + port, - isSelected: () => this.doIsSelectedBoard(board) - }); + const { boardsServiceClient: client, boardService } = this.props; + this.toDispose.pushAll([ + client.onBoardsConfigChanged(boardsConfig => this.setState({ boardsConfig })), + client.onBoardsChanged(({ newState }) => this.setState({ attachedBoards: newState.boards })) + ]); + boardService.getAttachedBoards().then(({ boards: attachedBoards }) => { + this.setState({ attachedBoards }) }); - this.setState({ boardItems }); } - protected doIsSelectedBoard = (board: Board) => this.isSelectedBoard(board); - protected isSelectedBoard(board: Board): boolean { - return AttachedSerialBoard.is(board) && - !!this.state.selectedBoard && - AttachedSerialBoard.is(this.state.selectedBoard) && - board.port === this.state.selectedBoard.port && - board.fqbn === this.state.selectedBoard.fqbn; + componentWillUnmount(): void { + this.toDispose.dispose(); } - protected getPort(board: Board): string { - if (AttachedSerialBoard.is(board)) { - return board.port; + protected readonly show = (event: React.MouseEvent) => { + const { currentTarget: element } = event; + if (element instanceof HTMLElement) { + if (this.state.coords === 'hidden') { + const rect = element.getBoundingClientRect(); + this.setState({ + coords: { + top: rect.top, + left: rect.left, + width: rect.width, + paddingTop: rect.height + } + }); + } else { + this.setState({ coords: 'hidden'}); + } } - return ''; - } - - protected readonly doShowSelectBoardsMenu = (event: React.MouseEvent) => { - this.showSelectBoardsMenu(event); event.stopPropagation(); event.nativeEvent.stopImmediatePropagation(); }; - protected showSelectBoardsMenu(event: React.MouseEvent) { - const el = (event.currentTarget as HTMLElement); - if (el) { - this.dropDownListCoord = { - top: el.getBoundingClientRect().top, - left: el.getBoundingClientRect().left, - paddingTop: el.getBoundingClientRect().height, - width: el.getBoundingClientRect().width - } - this.setState({ isOpen: !this.state.isOpen }); - } - } render(): React.ReactNode { - const selectedBoard = this.state.selectedBoard; - const port = selectedBoard ? this.getPort(selectedBoard) : undefined; - const boardTxt = selectedBoard && `${selectedBoard.name}${port ? ' at ' + port : ''}` || ''; - this.props.statusBar.setElement('arduino-selected-board', { - alignment: StatusBarAlignment.RIGHT, - text: boardTxt - }); + const { boardsConfig, coords, attachedBoards } = this.state; + const boardsConfigText = BoardsConfig.Config.toString(boardsConfig, { default: 'no board selected' }); + const configuredBoard = attachedBoards + .filter(AttachedSerialBoard.is) + .filter(board => BoardsConfig.Config.sameAs(boardsConfig, board)).shift(); + + const items = attachedBoards.filter(AttachedSerialBoard.is).map(board => ({ + label: `${board.name} at ${board.port}`, + selected: configuredBoard === board, + onClick: () => this.props.boardsServiceClient.boardsConfig = { + selectedBoard: board, + selectedPort: board.port + } + })); + return
-
-
- +
+
+
- {selectedBoard ? boardTxt : 'no board selected'} + {boardsConfigText}
- +
+ coords={coords} + items={items} + openBoardsConfig={this.openDialog}> ; } protected openDialog = () => { this.props.commands.executeCommand(ArduinoCommands.OPEN_BOARDS_DIALOG.id); - this.setState({ isOpen: false }); + this.setState({ coords: 'hidden' }); }; -} \ No newline at end of file + +} diff --git a/arduino-ide-extension/src/browser/boards/list-widget.tsx b/arduino-ide-extension/src/browser/boards/list-widget.tsx index ae40dc6d..a0ad85ca 100644 --- a/arduino-ide-extension/src/browser/boards/list-widget.tsx +++ b/arduino-ide-extension/src/browser/boards/list-widget.tsx @@ -3,10 +3,9 @@ import { inject, injectable, postConstruct } from 'inversify'; import { Message } from '@phosphor/messaging'; import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget'; import { WindowService } from '@theia/core/lib/browser/window/window-service'; +import { BoardsService } from '../../common/protocol/boards-service'; import { FilterableListContainer } from '../components/component-list/filterable-list-container'; -import { BoardsService, Board, BoardPackage } from '../../common/protocol/boards-service'; -import { BoardsNotificationService } from '../boards-notification-service'; -import { LibraryService } from '../../common/protocol/library-service'; +import { BoardsServiceClientImpl } from './boards-service-client-impl'; @injectable() export abstract class ListWidget extends ReactWidget { @@ -17,8 +16,8 @@ export abstract class ListWidget extends ReactWidget { @inject(WindowService) protected readonly windowService: WindowService; - @inject(BoardsNotificationService) - protected readonly boardsNotificationService: BoardsNotificationService; + @inject(BoardsServiceClientImpl) + protected readonly boardsServiceClient: BoardsServiceClientImpl; constructor() { super(); @@ -51,19 +50,8 @@ export abstract class ListWidget extends ReactWidget { } render(): React.ReactNode { - const boardsServiceDelegate = this.boardsService; - const boardsService: BoardsService = { - getAttachedBoards: () => boardsServiceDelegate.getAttachedBoards(), - selectBoard: (board: Board) => boardsServiceDelegate.selectBoard(board), - getSelectBoard: () => boardsServiceDelegate.getSelectBoard(), - search: (options: { query?: string, props?: LibraryService.Search.Props }) => boardsServiceDelegate.search(options), - install: async (item: BoardPackage) => { - await boardsServiceDelegate.install(item); - this.boardsNotificationService.notifyBoardsInstalled(); - } - } return ; } @@ -85,4 +73,4 @@ export namespace ListWidget { export const LIST_WIDGET_CLASS = 'arduino-list-widget' } -} \ No newline at end of file +} diff --git a/arduino-ide-extension/src/browser/boards/select-board-dialog-widget.tsx b/arduino-ide-extension/src/browser/boards/select-board-dialog-widget.tsx deleted file mode 100644 index d1e9f4df..00000000 --- a/arduino-ide-extension/src/browser/boards/select-board-dialog-widget.tsx +++ /dev/null @@ -1,305 +0,0 @@ -import * as React from 'react'; -import { ReactWidget } from '@theia/core/lib/browser'; -import { injectable, inject } from 'inversify'; -import { BoardsService, Board, BoardPackage, AttachedSerialBoard } from '../../common/protocol/boards-service'; -import { BoardsNotificationService } from '../boards-notification-service'; -import { Emitter, Event } from '@theia/core'; - -export interface BoardAndPortSelection { - board?: Board; - port?: string; -} - -export namespace BoardAndPortSelectableItem { - export interface Props { - item: BoardAndPortSelection, - selected: boolean, - onSelect: (selection: BoardAndPortSelection) => void - } -} - -export class BoardAndPortSelectableItem extends React.Component { - - render(): React.ReactNode { - if (this.props.item.board || this.props.item.port) { - return
- {this.props.item.board ? this.props.item.board.name : this.props.item.port} - {this.props.selected ? : ''} -
; - } - } - - protected readonly select = (() => { - this.props.onSelect({ board: this.props.item.board, port: this.props.item.port }) - }).bind(this); -} - -export namespace BoardAndPortSelectionList { - export interface Props { - type: 'boards' | 'ports'; - list: BoardAndPortSelection[]; - onSelect: (selection: BoardAndPortSelection) => void; - } - - export interface State { - selection: BoardAndPortSelection - } -} - -export class BoardAndPortSelectionList extends React.Component { - - constructor(props: BoardAndPortSelectionList.Props) { - super(props); - - this.state = { - selection: {} - } - } - - reset(): void { - this.setState({ selection: {} }); - } - - render(): React.ReactNode { - return
- {this.props.list.map((item, idx) => )} -
- } - - protected readonly doSelect = (boardAndPortSelection: BoardAndPortSelection) => { - this.setState({ selection: boardAndPortSelection }); - this.props.onSelect(boardAndPortSelection); - } - - protected readonly isSelectedItem = ((item: BoardAndPortSelection) => { - if (this.state.selection.board) { - return (this.state.selection.board === item.board); - } else if (this.state.selection.port) { - return (this.state.selection.port === item.port); - } - return false; - }); - - protected readonly isSelectedPort = ((port: string) => { - return (this.state.selection.port && this.state.selection.port === port) || false; - }); -} - -export namespace BoardAndPortSelectionComponent { - export interface Props { - boardsService: BoardsService; - onSelect: (selection: BoardAndPortSelection) => void; - } - - export interface State { - boards: Board[]; - ports: string[]; - selection: BoardAndPortSelection; - } -} - -export class BoardAndPortSelectionComponent extends React.Component { - - protected allBoards: Board[] = []; - protected boardListComponent: BoardAndPortSelectionList | null; - protected portListComponent: BoardAndPortSelectionList | null; - - constructor(props: BoardAndPortSelectionComponent.Props) { - super(props); - - this.state = { - boards: [], - ports: [], - selection: {} - } - } - - componentDidMount() { - this.searchAvailableBoards(); - this.setPorts(); - } - - reset(): void { - if (this.boardListComponent) { - this.boardListComponent.reset(); - } - if (this.portListComponent) { - this.portListComponent.reset(); - } - this.setState({ selection: {} }); - } - - render(): React.ReactNode { - return -
-
-
-
- BOARDS -
-
- - -
- { this.boardListComponent = ref }} - type='boards' - onSelect={this.doSelect} - list={this.state.boards.map(board => ({ board }))} /> -
-
-
-
-
- PORTS -
- { - this.state.ports.length ? - { this.portListComponent = ref }} - type='ports' - onSelect={this.doSelect} - list={this.state.ports.map(port => ({ port }))} /> : 'loading ports...' - } -
-
-
-
- } - - protected sort(items: Board[]): Board[] { - return items.sort((a, b) => { - if (a.name < b.name) { - return -1; - } else if (a.name === b.name) { - return 0; - } else { - return 1; - } - }); - } - - protected readonly doSelect = (boardAndPortSelection: BoardAndPortSelection) => { - const selection = this.state.selection; - if (boardAndPortSelection.board) { - selection.board = boardAndPortSelection.board; - } - if (boardAndPortSelection.port) { - selection.port = boardAndPortSelection.port; - } - this.setState({ selection }); - this.props.onSelect(this.state.selection); - } - - protected readonly doFilter = (event: React.ChangeEvent) => { - const boards = this.allBoards.filter(board => board.name.toLowerCase().indexOf(event.target.value.toLowerCase()) >= 0); - this.setState({ boards }) - } - - protected async searchAvailableBoards() { - const boardPkg = await this.props.boardsService.search({}); - const boards = [].concat.apply([], boardPkg.items.map(item => item.boards)) as Board[]; - this.allBoards = this.sort(boards); - this.setState({ boards: this.allBoards }); - } - - protected async setPorts() { - const ports: string[] = []; - const { boards } = await this.props.boardsService.getAttachedBoards(); - boards.forEach(board => { - if (AttachedSerialBoard.is(board)) { - ports.push(board.port); - } - }); - this.setState({ ports }); - } -} - -@injectable() -export class SelectBoardDialogWidget extends ReactWidget { - @inject(BoardsService) - protected readonly boardsService: BoardsService; - @inject(BoardsNotificationService) - protected readonly boardsNotificationService: BoardsNotificationService; - - protected readonly onChangedEmitter = new Emitter(); - protected boardAndPortSelectionComponent: BoardAndPortSelectionComponent | null; - protected attachedBoards: Promise<{ boards: Board[] }>; - - boardAndPort: BoardAndPortSelection = {}; - - constructor() { - super(); - this.id = 'select-board-dialog'; - - this.toDispose.push(this.onChangedEmitter); - } - - get onChanged(): Event { - return this.onChangedEmitter.event; - } - - reset(): void { - if (this.boardAndPortSelectionComponent) { - this.boardAndPortSelectionComponent.reset(); - } - this.boardAndPort = {}; - } - - setAttachedBoards(attachedBoards: Promise<{ boards: Board[] }>): void { - this.attachedBoards = attachedBoards; - } - - protected fireChanged(boardAndPort: BoardAndPortSelection): void { - this.onChangedEmitter.fire(boardAndPort); - } - - protected render(): React.ReactNode { - let content: React.ReactNode; - - const boardsServiceDelegate = this.boardsService; - const attachedBoards = this.attachedBoards; - const boardsService: BoardsService = { - getAttachedBoards: () => attachedBoards, - selectBoard: (board: Board) => boardsServiceDelegate.selectBoard(board), - getSelectBoard: () => boardsServiceDelegate.getSelectBoard(), - search: (options: { query?: string }) => boardsServiceDelegate.search(options), - install: async (item: BoardPackage) => { - await boardsServiceDelegate.install(item); - this.boardsNotificationService.notifyBoardsInstalled(); - } - } - - content = -
-
-
- Select Other Board & Port -
-
-

Select both a BOARD and a PORT if you want to upload a sketch.

-

If you only select a BOARD you will be able just to compile,

-

but not to upload your sketch.

-
-
- this.boardAndPortSelectionComponent = ref} - boardsService={boardsService} - onSelect={this.onSelect} /> -
-
- - return content; - } - - protected readonly onSelect = (selection: BoardAndPortSelection) => { this.doOnSelect(selection) }; - protected doOnSelect(selection: BoardAndPortSelection) { - this.boardAndPort = selection; - this.fireChanged(this.boardAndPort); - } -} \ No newline at end of file diff --git a/arduino-ide-extension/src/browser/boards/select-board-dialog.ts b/arduino-ide-extension/src/browser/boards/select-board-dialog.ts deleted file mode 100644 index 70cbb0ba..00000000 --- a/arduino-ide-extension/src/browser/boards/select-board-dialog.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { AbstractDialog, DialogProps, Widget, Panel, DialogError } from '@theia/core/lib/browser'; -import { injectable, inject } from 'inversify'; -import { SelectBoardDialogWidget, BoardAndPortSelection } from './select-board-dialog-widget'; -import { Message } from '@phosphor/messaging'; -import { Disposable } from '@theia/core'; -import { Board, BoardsService, AttachedSerialBoard } from '../../common/protocol/boards-service'; - -@injectable() -export class SelectBoardDialogProps extends DialogProps { - -} - -@injectable() -export class SelectBoardDialog extends AbstractDialog { - - protected readonly dialogPanel: Panel; - protected attachedBoards: Board[]; - - constructor( - @inject(SelectBoardDialogProps) protected readonly props: SelectBoardDialogProps, - @inject(SelectBoardDialogWidget) protected readonly widget: SelectBoardDialogWidget, - @inject(BoardsService) protected readonly boardService: BoardsService - ) { - super({ title: props.title }); - - this.dialogPanel = new Panel(); - this.dialogPanel.addWidget(this.widget); - - this.contentNode.classList.add('select-board-dialog'); - - this.toDispose.push(this.widget.onChanged(() => this.update())); - this.toDispose.push(this.dialogPanel); - - this.attachedBoards = []; - this.init(); - - this.appendCloseButton('CANCEL'); - this.appendAcceptButton('OK'); - } - - protected init() { - const boards = this.boardService.getAttachedBoards(); - boards.then(b => this.attachedBoards = b.boards); - this.widget.setAttachedBoards(boards); - } - - protected onAfterAttach(msg: Message): void { - Widget.attach(this.dialogPanel, this.contentNode); - - this.toDisposeOnDetach.push(Disposable.create(() => { - Widget.detach(this.dialogPanel); - })) - - super.onAfterAttach(msg); - this.update(); - } - - protected onUpdateRequest(msg: Message) { - super.onUpdateRequest(msg); - this.widget.update(); - } - - protected onActivateRequest(msg: Message): void { - this.widget.activate(); - } - - protected handleEnter(event: KeyboardEvent): boolean | void { - if (event.target instanceof HTMLTextAreaElement) { - return false; - } - } - - protected isValid(value: BoardAndPortSelection): DialogError { - if (!value.board) { - if (value.port) { - return 'Please pick the Board connected to the Port you have selected'; - } - return false; - } - return ''; - } - - get value(): BoardAndPortSelection { - const boardAndPortSelection = this.widget.boardAndPort; - if (this.attachedBoards.length) { - boardAndPortSelection.board = this.attachedBoards.find(b => { - const isAttachedBoard = !!boardAndPortSelection.board && - b.name === boardAndPortSelection.board.name && - b.fqbn === boardAndPortSelection.board.fqbn; - if (boardAndPortSelection.port) { - return isAttachedBoard && - AttachedSerialBoard.is(b) && - b.port === boardAndPortSelection.port; - } else { - return isAttachedBoard; - } - - }) - || boardAndPortSelection.board; - } - return boardAndPortSelection; - } - - close(): void { - this.widget.reset(); - super.close(); - } - - onAfterDetach(msg: Message) { - this.widget.reset(); - super.onAfterDetach(msg); - } -} \ No newline at end of file diff --git a/arduino-ide-extension/src/browser/components/connected-boards.tsx b/arduino-ide-extension/src/browser/components/connected-boards.tsx deleted file mode 100644 index 700c78b0..00000000 --- a/arduino-ide-extension/src/browser/components/connected-boards.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import * as React from 'react'; -import { BoardsService, Board } from '../../common/protocol/boards-service'; -// import { SelectBoardDialog } from './select-board-dialog'; -import { QuickPickService } from '@theia/core/lib/common/quick-pick-service'; -import { BoardsNotificationService } from '../boards-notification-service'; -import { ARDUINO_TOOLBAR_ITEM_CLASS } from '../toolbar/arduino-toolbar'; - -export class ConnectedBoards extends React.Component { - static TOOLBAR_ID: 'connected-boards-toolbar'; - - constructor(props: ConnectedBoards.Props) { - super(props); - this.state = { boardsLoading: false }; - - props.boardsNotificationService.on('boards-installed', () => this.onBoardsInstalled()); - } - - render(): React.ReactNode { - let content = []; - if (!!this.state.boards && this.state.boards.length > 0) { - content = this.state.boards.map((b, i) => ); - } else { - let label; - if (this.state.boardsLoading) { - label = "Loading ..."; - } else { - label = "No board attached"; - } - content = [ ]; - } - - return
- -
; - } - - componentDidMount(): void { - this.reloadBoards(); - } - - protected onBoardsInstalled() { - if (!!this.findUnknownBoards()) { - this.reloadBoards(); - } - } - - protected findUnknownBoards(): Board[] { - if (!this.state || !this.state.boards) { - return []; - } - - return this.state.boards.filter(b => !b.fqbn || b.name === "unknown"); - } - - protected async reloadBoards() { - const prevSelection = this.state.selection; - this.setState({ boardsLoading: true, boards: undefined, selection: "loading" }); - const { boards } = await this.props.boardsService.getAttachedBoards() - this.setState({ boards, boardsLoading: false, selection: prevSelection }); - - if (boards) { - this.setState({ selection: "0" }); - await this.props.boardsService.selectBoard(boards[0]); - - const unknownBoards = this.findUnknownBoards(); - if (unknownBoards && unknownBoards.length > 0) { - this.props.onUnknownBoard(unknownBoards[0]); - } - } - } - - protected async onBoardSelect(evt: React.ChangeEvent) { - const selection = evt.target.value; - if (selection === "select-other" || selection === "selected-other") { - let selectedBoard = this.state.otherBoard; - if (selection === "select-other" || !selectedBoard) { - selectedBoard = await this.selectedInstalledBoard(); - } - if (!selectedBoard) { - return; - } - - await this.props.boardsService.selectBoard(selectedBoard); - this.setState({otherBoard: selectedBoard, selection: "selected-other"}); - return; - } - - const selectedBoard = (this.state.boards || [])[parseInt(selection, 10)]; - if (!selectedBoard) { - return; - } - await this.props.boardsService.selectBoard(selectedBoard); - this.setState({selection}); - } - - protected async selectedInstalledBoard(): Promise { - const {items} = await this.props.boardsService.search({}); - - const idx = new Map(); - items.filter(pkg => !!pkg.installedVersion).forEach(pkg => pkg.boards.forEach(brd => idx.set(`${brd.name}`, brd) )); - - if (idx.size === 0) { - this.props.onNoBoardsInstalled(); - return; - } - - const selection = await this.props.quickPickService.show(Array.from(idx.keys())); - if (!selection) { - return; - } - - return idx.get(selection); - } - -} - -export namespace ConnectedBoards { - - export interface Props { - readonly boardsService: BoardsService; - readonly boardsNotificationService: BoardsNotificationService; - readonly quickPickService: QuickPickService; - readonly onNoBoardsInstalled: () => void; - readonly onUnknownBoard: (board: Board) => void; - } - - export interface State { - boardsLoading: boolean; - boards?: Board[]; - otherBoard?: Board; - selection?: string; - } - - export namespace Styles { - export const CONNECTED_BOARDS_CLASS = 'connected-boards'; - } - -} \ No newline at end of file diff --git a/arduino-ide-extension/src/browser/style/board-select-dialog.css b/arduino-ide-extension/src/browser/style/board-select-dialog.css index 9ac99445..58dea0c5 100644 --- a/arduino-ide-extension/src/browser/style/board-select-dialog.css +++ b/arduino-ide-extension/src/browser/style/board-select-dialog.css @@ -7,11 +7,11 @@ div#select-board-dialog .selectBoardContainer .body { overflow: hidden; } -div#select-board-dialog .selectBoardContainer .head { - margin-bottom: 10px; +div.dialogContent.select-board-dialog > div.head { + padding-left: 21px; } -div#select-board-dialog .selectBoardContainer .head .title { +div.dialogContent.select-board-dialog > div.head .title { font-weight: 400; letter-spacing: .02em; font-size: 1.2em; @@ -31,11 +31,11 @@ div#select-board-dialog .selectBoardContainer .body .list .item.selected i{ color: var(--theia-arduino-light); } -#select-board-dialog .selectBoardContainer .body .search input, -#select-board-dialog .selectBoardContainer .body .boards.list, -#select-board-dialog .selectBoardContainer .body .search, -#select-board-dialog .selectBoardContainer .body .ports.list { - background: white; +#select-board-dialog .selectBoardContainer .search, +#select-board-dialog .selectBoardContainer .search input, +#select-board-dialog .selectBoardContainer .list, +#select-board-dialog .selectBoardContainer .list { + background: white; /* TODO find a theia color instead! */ } #select-board-dialog .selectBoardContainer .body .search input { @@ -43,7 +43,7 @@ div#select-board-dialog .selectBoardContainer .body .list .item.selected i{ width: 100%; height: auto; max-height: 37px; - padding: 10px 8px; + padding: 10px 5px 10px 10px; margin: 0; vertical-align: top; display: flex; @@ -56,6 +56,7 @@ div#select-board-dialog .selectBoardContainer .body .list .item.selected i{ #select-board-dialog .selectBoardContainer .body .container { flex: 1; + padding: 0px 10px 0px 0px; } #select-board-dialog .selectBoardContainer .body .left.container .content { @@ -66,27 +67,41 @@ div#select-board-dialog .selectBoardContainer .body .list .item.selected i{ margin: 0 0 0 5px; } -#select-board-dialog .selectBoardContainer .body .container .content .title{ +#select-board-dialog .selectBoardContainer .body .container .content .title { color: #7f8c8d; - margin-bottom: 10px; + padding: 0px 0px 10px 0px; + text-transform: uppercase; +} + +#select-board-dialog .selectBoardContainer .body .container .content .loading { + font-size: var(--theia-ui-font-size1); + color: #7f8c8d; + padding: 10px 5px 10px 10px; + text-transform: uppercase; } #select-board-dialog .selectBoardContainer .body .list .item { - padding: 10px 5px 10px 20px; + padding: 10px 5px 10px 10px; display: flex; justify-content: space-between; } + +#select-board-dialog .selectBoardContainer .body .list .item.missing { + color: var(--theia-disabled-color0); +} + #select-board-dialog .selectBoardContainer .body .list .item:hover { background: var(--theia-ui-button-color-secondary-hover); } #select-board-dialog .selectBoardContainer .body .list { max-height: 265px; + min-height: 265px; overflow-y: auto; } -#select-board-dialog .selectBoardContainer .body .boards.list { - min-height: 265px; +#select-board-dialog .selectBoardContainer .body .ports.list { + margin: 47px 0px 0px 0px /* 47 is 37 as input height for the `Boards`, plus 10 margin bottom. */ } #select-board-dialog .selectBoardContainer .body .search { @@ -129,7 +144,7 @@ button.theia-button.main { align-items: baseline; width: 100%; } - + .arduino-boards-toolbar-item-container .arduino-boards-toolbar-item .inner-container .notAttached { width: 10px; height: 10px; @@ -184,4 +199,4 @@ button.theia-button.main { .arduino-boards-dropdown-item.selected, .arduino-boards-dropdown-item:hover { background: var(--theia-ui-button-color-secondary-hover); -} \ No newline at end of file +} diff --git a/arduino-ide-extension/src/browser/style/main.css b/arduino-ide-extension/src/browser/style/main.css index f60d6c95..8ebf4b32 100644 --- a/arduino-ide-extension/src/browser/style/main.css +++ b/arduino-ide-extension/src/browser/style/main.css @@ -8,6 +8,7 @@ #outputView { color: var(--theia-ui-font-color3); + cursor: text; } #arduino-verify.arduino-tool-icon:hover, diff --git a/arduino-ide-extension/src/common/protocol/boards-service.ts b/arduino-ide-extension/src/common/protocol/boards-service.ts index ff77a0d3..5b781d81 100644 --- a/arduino-ide-extension/src/common/protocol/boards-service.ts +++ b/arduino-ide-extension/src/common/protocol/boards-service.ts @@ -1,12 +1,25 @@ import { ArduinoComponent } from "./arduino-component"; +import { JsonRpcServer } from "@theia/core"; + +export interface AttachedBoardsChangeEvent { + readonly oldState: Readonly<{ boards: Board[] }>; + readonly newState: Readonly<{ boards: Board[] }>; +} + +export interface BoardInstalledEvent { + readonly pkg: Readonly; +} + +export const BoardsServiceClient = Symbol('BoardsServiceClient'); +export interface BoardsServiceClient { + notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void; + notifyBoardInstalled(event: BoardInstalledEvent): void +} export const BoardsServicePath = '/services/boards-service'; export const BoardsService = Symbol('BoardsService'); -export interface BoardsService { +export interface BoardsService extends JsonRpcServer { getAttachedBoards(): Promise<{ boards: Board[] }>; - selectBoard(board: Board | AttachedSerialBoard | AttachedNetworkBoard): Promise; - getSelectBoard(): Promise; - search(options: { query?: string }): Promise<{ items: BoardPackage[] }>; install(item: BoardPackage): Promise; } @@ -21,13 +34,41 @@ export interface Board { fqbn?: string } +export interface Port { + port?: string; +} + +export namespace Board { + + export function is(board: any): board is Board { + return !!board && 'name' in board; + } + + export function equals(left: Board, right: Board): boolean { + return left.name === right.name && left.fqbn === right.fqbn; + } + + export function compare(left: Board, right: Board): number { + let result = left.name.localeCompare(right.name); + if (result === 0) { + result = (left.fqbn || '').localeCompare(right.fqbn || ''); + } + return result; + } + + export function installed(board: Board): boolean { + return !!board.fqbn; + } + +} + export interface AttachedSerialBoard extends Board { port: string; } export namespace AttachedSerialBoard { - export function is(b: Board): b is AttachedSerialBoard { - return 'port' in b; + export function is(b: Board | any): b is AttachedSerialBoard { + return !!b && 'port' in b; } } diff --git a/arduino-ide-extension/src/common/protocol/core-service.ts b/arduino-ide-extension/src/common/protocol/core-service.ts index 8f671a56..a6318b55 100644 --- a/arduino-ide-extension/src/common/protocol/core-service.ts +++ b/arduino-ide-extension/src/common/protocol/core-service.ts @@ -1,3 +1,5 @@ +import { Board } from "./boards-service"; + export const CoreServicePath = '/services/core-service'; export const CoreService = Symbol('CoreService'); export interface CoreService { @@ -10,12 +12,15 @@ export namespace CoreService { export namespace Upload { export interface Options { readonly uri: string; + readonly board: Board; + readonly port: string; } } export namespace Compile { export interface Options { readonly uri: string; + readonly board: Board; } } } \ No newline at end of file diff --git a/arduino-ide-extension/src/node/arduino-backend-module.ts b/arduino-ide-extension/src/node/arduino-backend-module.ts index a81dc42d..296835d8 100644 --- a/arduino-ide-extension/src/node/arduino-backend-module.ts +++ b/arduino-ide-extension/src/node/arduino-backend-module.ts @@ -3,7 +3,7 @@ import { ArduinoDaemon } from './arduino-daemon'; import { ILogger } from '@theia/core/lib/common/logger'; import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application'; import { LibraryService, LibraryServicePath } from '../common/protocol/library-service'; -import { BoardsService, BoardsServicePath } from '../common/protocol/boards-service'; +import { BoardsService, BoardsServicePath, BoardsServiceClient } from '../common/protocol/boards-service'; import { LibraryServiceImpl } from './library-service-impl'; import { BoardsServiceImpl } from './boards-service-impl'; import { CoreServiceImpl } from './core-service-impl'; @@ -44,7 +44,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { const boardsServiceConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => { bind(BoardsServiceImpl).toSelf().inSingletonScope(); bind(BoardsService).toService(BoardsServiceImpl); - bindBackendService(BoardsServicePath, BoardsService); + bindBackendService(BoardsServicePath, BoardsService, (service, client) => { + service.setClient(client); + client.onDidCloseConnection(() => service.dispose()); + return service; + }); }); bind(ConnectionContainerModule).toConstantValue(boardsServiceConnectionModule); @@ -90,6 +94,12 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { return parentLogger.child('daemon'); }).inSingletonScope().whenTargetNamed('daemon'); + // Logger for the "serial discovery". + bind(ILogger).toDynamicValue(ctx => { + const parentLogger = ctx.container.get(ILogger); + return parentLogger.child('discovery'); + }).inSingletonScope().whenTargetNamed('discovery'); + // Default workspace server extension to initialize and use a fallback workspace (`~/Arduino-PoC/workspace/`) // If nothing was set previously. bind(DefaultWorkspaceServerExt).toSelf().inSingletonScope(); diff --git a/arduino-ide-extension/src/node/boards-service-impl.ts b/arduino-ide-extension/src/node/boards-service-impl.ts index ef2f45d5..7c41e875 100644 --- a/arduino-ide-extension/src/node/boards-service-impl.ts +++ b/arduino-ide-extension/src/node/boards-service-impl.ts @@ -1,14 +1,20 @@ import * as PQueue from 'p-queue'; -import { injectable, inject } from 'inversify'; -import { BoardsService, AttachedSerialBoard, BoardPackage, Board, AttachedNetworkBoard } from '../common/protocol/boards-service'; +import { injectable, inject, postConstruct, named } from 'inversify'; +import { ILogger } from '@theia/core/lib/common/logger'; +import { BoardsService, AttachedSerialBoard, BoardPackage, Board, AttachedNetworkBoard, BoardsServiceClient } from '../common/protocol/boards-service'; import { PlatformSearchReq, PlatformSearchResp, PlatformInstallReq, PlatformInstallResp, PlatformListReq, PlatformListResp } from './cli-protocol/commands/core_pb'; import { CoreClientProvider } from './core-client-provider'; import { BoardListReq, BoardListResp } from './cli-protocol/commands/board_pb'; import { ToolOutputServiceServer } from '../common/protocol/tool-output-service'; +import { Deferred } from '@theia/core/lib/common/promise-util'; @injectable() export class BoardsServiceImpl implements BoardsService { + @inject(ILogger) + @named('discovery') + protected discoveryLogger: ILogger; + @inject(CoreClientProvider) protected readonly coreClientProvider: CoreClientProvider; @@ -16,9 +22,80 @@ export class BoardsServiceImpl implements BoardsService { protected readonly toolOutputService: ToolOutputServiceServer; protected selectedBoard: Board | undefined; + protected discoveryInitialized = false; + protected discoveryReady = new Deferred(); + protected discoveryTimer: NodeJS.Timeout | undefined; + /** + * Poor man's serial discovery: + * Stores the state of the currently discovered, attached boards. + * This state is updated via periodical polls. + */ + protected _attachedBoards: { boards: Board[] } = { boards: [] }; + protected client: BoardsServiceClient | undefined; protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 }); - public async getAttachedBoards(): Promise<{ boards: Board[] }> { + @postConstruct() + protected async init(): Promise { + this.discoveryTimer = setInterval(() => { + this.discoveryLogger.trace('Discovering attached boards...'); + this.doGetAttachedBoards().then(({ boards }) => { + const update = (oldState: Board[], newState: Board[], message: string) => { + this._attachedBoards = { boards: newState }; + this.discoveryReady.resolve(); + this.discoveryLogger.info(`${message} - Discovered boards: ${JSON.stringify(newState)}`); + if (this.client) { + this.client.notifyAttachedBoardsChanged({ + oldState: { + boards: oldState + }, + newState: { + boards: newState + } + }); + } + } + const sortedBoards = boards.sort(Board.compare); + this.discoveryLogger.trace(`Discovery done. ${JSON.stringify(sortedBoards)}`); + if (!this.discoveryInitialized) { + update([], sortedBoards, 'Initialized attached boards.'); + this.discoveryInitialized = true; + } else { + this.getAttachedBoards().then(({ boards: currentBoards }) => { + this.discoveryLogger.trace(`Updating discovered boards... ${JSON.stringify(currentBoards)}`); + if (currentBoards.length !== sortedBoards.length) { + update(currentBoards, sortedBoards, 'Updated discovered boards.'); + return; + } + // `currentBoards` is already sorted. + for (let i = 0; i < sortedBoards.length; i++) { + if (Board.compare(sortedBoards[i], currentBoards[i]) !== 0) { + update(currentBoards, sortedBoards, 'Updated discovered boards.'); + return; + } + } + this.discoveryLogger.trace('No new boards were discovered.'); + }); + } + }); + }, 1000); + } + + setClient(client: BoardsServiceClient | undefined): void { + this.client = client; + } + + dispose(): void { + if (this.discoveryTimer !== undefined) { + clearInterval(this.discoveryTimer); + } + } + + async getAttachedBoards(): Promise<{ boards: Board[] }> { + await this.discoveryReady.promise; + return this._attachedBoards; + } + + private async doGetAttachedBoards(): Promise<{ boards: Board[] }> { return this.queue.add(() => { return new Promise<{ boards: Board[] }>(async resolve => { const coreClient = await this.coreClientProvider.getClient(); @@ -55,19 +132,16 @@ export class BoardsServiceImpl implements BoardsService { } } } + // TODO: remove mock board! + // boards.push(...[ + // { name: 'Arduino/Genuino Uno', fqbn: 'arduino:avr:uno', port: '/dev/cu.usbmodem14201' }, + // { name: 'Arduino/Genuino Uno', fqbn: 'arduino:avr:uno', port: '/dev/cu.usbmodem142xx' }, + // ]); resolve({ boards }); }) }); } - async selectBoard(board: Board): Promise { - this.selectedBoard = board; - } - - async getSelectBoard(): Promise { - return this.selectedBoard; - } - async search(options: { query?: string }): Promise<{ items: BoardPackage[] }> { const coreClient = await this.coreClientProvider.getClient(); if (!coreClient) { @@ -138,6 +212,9 @@ export class BoardsServiceImpl implements BoardsService { resp.on('end', resolve); resp.on('error', reject); }); + if (this.client) { + this.client.notifyBoardInstalled({ pkg }); + } console.info("Board installation done", pkg); } diff --git a/arduino-ide-extension/src/node/core-service-impl.ts b/arduino-ide-extension/src/node/core-service-impl.ts index f085f372..549611d9 100644 --- a/arduino-ide-extension/src/node/core-service-impl.ts +++ b/arduino-ide-extension/src/node/core-service-impl.ts @@ -2,7 +2,7 @@ import { inject, injectable } from 'inversify'; import { FileSystem } from '@theia/filesystem/lib/common/filesystem'; import { CoreService } from '../common/protocol/core-service'; import { CompileReq, CompileResp } from './cli-protocol/commands/compile_pb'; -import { BoardsService, AttachedSerialBoard, AttachedNetworkBoard } from '../common/protocol/boards-service'; +import { BoardsService } from '../common/protocol/boards-service'; import { CoreClientProvider } from './core-client-provider'; import * as path from 'path'; import { ToolOutputServiceServer } from '../common/protocol/tool-output-service'; @@ -38,7 +38,7 @@ export class CoreServiceImpl implements CoreService { } const { client, instance } = coreClient; - const currentBoard = await this.boardsService.getSelectBoard(); + const currentBoard = options.board; if (!currentBoard) { throw new Error("no board selected"); } @@ -72,7 +72,7 @@ export class CoreServiceImpl implements CoreService { } async upload(options: CoreService.Upload.Options): Promise { - await this.compile({uri: options.uri}); + await this.compile({ uri: options.uri, board: options.board }); console.log('upload', options); const { uri } = options; @@ -82,7 +82,7 @@ export class CoreServiceImpl implements CoreService { } const sketchpath = path.dirname(sketchFilePath); - const currentBoard = await this.boardsService.getSelectBoard(); + const currentBoard = options.board; if (!currentBoard) { throw new Error("no board selected"); } @@ -100,13 +100,7 @@ export class CoreServiceImpl implements CoreService { req.setInstance(instance); req.setSketchPath(sketchpath); req.setFqbn(currentBoard.fqbn); - if (AttachedSerialBoard.is(currentBoard)) { - req.setPort(currentBoard.port); - } else if (AttachedNetworkBoard.is(currentBoard)) { - throw new Error("can only upload to serial boards"); - } else { - throw new Error("board is not attached"); - } + req.setPort(options.port); const result = client.upload(req); try { diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 1f178231..26eac589 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -40,20 +40,20 @@ jobs: env: GITHUB_TOKEN: $(Personal.GitHub.Token) RELEASE_TAG: $(Release.Tag) - condition: in(variables['Build.Reason'], 'Manual', 'Schedule') + condition: or(in(variables['Agent.OS'], 'Windows_NT'), in(variables['Build.Reason'], 'Manual', 'Schedule')) displayName: Package - bash: | export ARDUINO_POC_NAME=$(./electron/packager/cli name) echo "##vso[task.setvariable variable=ArduinoPoC.AppName]$ARDUINO_POC_NAME" env: RELEASE_TAG: $(Release.Tag) - condition: in(variables['Build.Reason'], 'Manual', 'Schedule') + condition: or(in(variables['Agent.OS'], 'Windows_NT'), in(variables['Build.Reason'], 'Manual', 'Schedule')) displayName: '[Config] Use - ARDUINO_POC_NAME env' - task: PublishBuildArtifacts@1 inputs: pathtoPublish: electron/build/dist/$(ArduinoPoC.AppName) artifactName: 'Arduino-PoC - Applications' - condition: in(variables['Build.Reason'], 'Manual', 'Schedule') + condition: or(in(variables['Agent.OS'], 'Windows_NT'), in(variables['Build.Reason'], 'Manual', 'Schedule')) displayName: Publish - job: Release pool: