Merge pull request #46 from bcmi-labs/poll-boards

Implemented the board discovery with polling.
This commit is contained in:
Luca Cipriani 2019-08-02 17:08:08 +02:00 committed by GitHub
commit 2914379586
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 855 additions and 874 deletions

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}

View File

@ -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"
}

View File

@ -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<void> {
// 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: () => <BoardsToolBarItem
key='boardsToolbarItem'
ref={ref => 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'
})
}

View File

@ -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'
})

View File

@ -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');
}
}

View File

@ -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<BoardsConfig.Config>();
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 <div className='selectBoardContainer'>
<BoardsConfig
boardsService={this.boardsService}
boardsServiceClient={this.boardsServiceClient}
onConfigChange={this.fireConfigChanged}
onFocusNodeSet={this.setFocusNode} />
</div>;
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
if (this.focusNode instanceof HTMLInputElement) {
this.focusNode.select();
}
(this.focusNode || this.node).focus();
}
}

View File

@ -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<BoardsConfig.Config> {
@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;
}
}

View File

@ -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<T> 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 <div onClick={this.onClick} className={classNames.join(' ')}>
{name}
{selected ? <i className='fa fa-check'></i> : ''}
</div>;
}
protected onClick = () => {
this.props.onClick(this.props.item);
}
}
export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConfig.State> {
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<HTMLInputElement> | 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 <div className='body'>
{this.renderContainer('boards', this.renderBoards.bind(this))}
{this.renderContainer('ports', this.renderPorts.bind(this))}
</div>;
}
protected renderContainer(title: string, contentRenderer: () => React.ReactNode): React.ReactNode {
return <div className='container'>
<div className='content'>
<div className='title'>
{title}
</div>
{contentRenderer()}
</div>
</div>;
}
protected renderBoards(): React.ReactNode {
const { selectedBoard } = this.state;
return <React.Fragment>
<div className='search'>
<input type='search' placeholder='SEARCH BOARD' onChange={this.updateBoards} ref={this.focusNodeSet} />
<i className='fa fa-search'></i>
</div>
<div className='boards list'>
{this.state.searchResults.map((board, index) => <Item<Board>
key={`${board.name}-${index}`}
item={board}
name={board.name}
selected={!!selectedBoard && Board.equals(board, selectedBoard)}
onClick={this.selectBoard}
missing={!Board.installed(board)}
/>)}
</div>
</React.Fragment>;
}
protected renderPorts(): React.ReactNode {
return !this.state.knownPorts.length ?
(
<div className='loading noselect'>
No ports discovered
</div>
) :
(
<div className='ports list'>
{this.state.knownPorts.map(port => <Item<string>
key={port}
item={port}
name={port}
selected={this.state.selectedPort === port}
onClick={this.selectPort}
/>)}
</div>
);
}
}
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 : ''}`;
}
}
}

View File

@ -13,4 +13,4 @@ export class BoardsListWidget extends ListWidget {
}
}
}
}

View File

@ -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<AttachedBoardsChangeEvent>();
protected readonly onBoardInstalledEmitter = new Emitter<BoardInstalledEvent>();
protected readonly onSelectedBoardsConfigChangedEmitter = new Emitter<BoardsConfig.Config>();
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<void> {
return this.storageService.setData('boards-config', this.boardsConfig);
}
protected async loadState(): Promise<void> {
const boardsConfig = await this.storageService.getData<BoardsConfig.Config>('boards-config');
if (boardsConfig) {
this.boardsConfig = boardsConfig;
}
}
}

View File

@ -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<BoardsDropdownItemComponent.Props> {
render() {
return <div className={`arduino-boards-dropdown-item ${this.props.isSelected ? 'selected' : ''}`} onClick={this.props.onClick}>
<div>{this.props.label}</div>
{this.props.isSelected ? <span className='fa fa-check'></span> : ''}
</div>;
}
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<BoardsDropDown.Props> {
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<BoardsDropDown.Props> {
return ReactDOM.createPortal(this.renderNode(), this.dropdownElement);
}
renderNode(): React.ReactNode {
if (this.props.isOpen) {
return <div className='arduino-boards-dropdown-list'
style={{
position: 'absolute',
top: this.props.coords.top,
left: this.props.coords.left,
width: this.props.coords.width,
paddingTop: this.props.coords.paddingTop
}}>
{
this.props.dropDownItems.map(item => {
return <React.Fragment key={item.label}>
<BoardsDropdownItemComponent isSelected={item.isSelected()} label={item.label} onClick={item.commandExecutor}></BoardsDropdownItemComponent>
</React.Fragment>;
})
}
<BoardsDropdownItemComponent isSelected={false} label={'Select Other Board & Port'} onClick={this.props.openDialog}></BoardsDropdownItemComponent>
</div>
} 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 <div className='arduino-boards-dropdown-list'
style={{
position: 'absolute',
...coords
}}>
{items.map(this.renderItem)}
</div>
}
protected renderItem(item: BoardsDropDown.Item): React.ReactNode {
const { label, selected, onClick } = item;
return <div key={label} className={`arduino-boards-dropdown-item ${selected ? 'selected' : ''}`} onClick={onClick}>
<div>
{label}
</div>
{selected ? <span className='fa fa-check'/> : ''}
</div>
}
}
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<BoardsToolBarItem.Props, BoardsToolBarItem.State> {
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<HTMLElement>) => {
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<HTMLElement>) => {
this.showSelectBoardsMenu(event);
event.stopPropagation();
event.nativeEvent.stopImmediatePropagation();
};
protected showSelectBoardsMenu(event: React.MouseEvent<HTMLElement>) {
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 <React.Fragment>
<div className='arduino-boards-toolbar-item-container'>
<div className='arduino-boards-toolbar-item' title={boardTxt}>
<div className='inner-container' onClick={this.doShowSelectBoardsMenu}>
<span className={!selectedBoard || !this.state.selectedIsAttached ? 'fa fa-times notAttached' : ''}></span>
<div className='arduino-boards-toolbar-item' title={boardsConfigText}>
<div className='inner-container' onClick={this.show}>
<span className={!configuredBoard ? 'fa fa-times notAttached' : ''}/>
<div className='label noWrapInfo'>
<div className='noWrapInfo noselect'>
{selectedBoard ? boardTxt : 'no board selected'}
{boardsConfigText}
</div>
</div>
<span className='fa fa-caret-down caret'></span>
<span className='fa fa-caret-down caret'/>
</div>
</div>
</div>
<BoardsDropDown
isOpen={this.state.isOpen}
coords={this.dropDownListCoord}
dropDownItems={this.state.boardItems}
openDialog={this.openDialog}>
coords={coords}
items={items}
openBoardsConfig={this.openDialog}>
</BoardsDropDown>
</React.Fragment>;
}
protected openDialog = () => {
this.props.commands.executeCommand(ArduinoCommands.OPEN_BOARDS_DIALOG.id);
this.setState({ isOpen: false });
this.setState({ coords: 'hidden' });
};
}
}

View File

@ -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 <FilterableListContainer
service={boardsService}
service={this.boardsService}
windowService={this.windowService}
/>;
}
@ -85,4 +73,4 @@ export namespace ListWidget {
export const LIST_WIDGET_CLASS = 'arduino-list-widget'
}
}
}

View File

@ -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<BoardAndPortSelectableItem.Props> {
render(): React.ReactNode {
if (this.props.item.board || this.props.item.port) {
return <div onClick={this.select} className={`item ${this.props.selected ? 'selected' : ''}`}>
{this.props.item.board ? this.props.item.board.name : this.props.item.port}
{this.props.selected ? <i className='fa fa-check'></i> : ''}
</div>;
}
}
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<BoardAndPortSelectionList.Props, BoardAndPortSelectionList.State> {
constructor(props: BoardAndPortSelectionList.Props) {
super(props);
this.state = {
selection: {}
}
}
reset(): void {
this.setState({ selection: {} });
}
render(): React.ReactNode {
return <div className={`${this.props.type} list`}>
{this.props.list.map((item, idx) => <BoardAndPortSelectableItem
key={(item.board ? item.board.name : item.port || '') + idx}
onSelect={this.doSelect}
item={item}
selected={this.isSelectedItem(item)}
/>)}
</div>
}
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<BoardAndPortSelectionComponent.Props, BoardAndPortSelectionComponent.State> {
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 <React.Fragment>
<div className='body'>
<div className='left container'>
<div className='content'>
<div className='title'>
BOARDS
</div>
<div className='search'>
<input type='search' placeholder='SEARCH BOARD' onChange={this.doFilter} />
<i className='fa fa-search'></i>
</div>
<BoardAndPortSelectionList
ref={ref => { this.boardListComponent = ref }}
type='boards'
onSelect={this.doSelect}
list={this.state.boards.map<BoardAndPortSelection>(board => ({ board }))} />
</div>
</div>
<div className='right container'>
<div className='content'>
<div className='title'>
PORTS
</div>
{
this.state.ports.length ?
<BoardAndPortSelectionList
ref={ref => { this.portListComponent = ref }}
type='ports'
onSelect={this.doSelect}
list={this.state.ports.map<BoardAndPortSelection>(port => ({ port }))} /> : 'loading ports...'
}
</div>
</div>
</div>
</React.Fragment>
}
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<HTMLInputElement>) => {
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<Board[]>(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<BoardAndPortSelection>();
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<BoardAndPortSelection> {
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 = <React.Fragment>
<div className='selectBoardContainer'>
<div className='head'>
<div className='title'>
Select Other Board &amp; Port
</div>
<div className='text'>
<p>Select both a BOARD and a PORT if you want to upload a sketch.</p>
<p>If you only select a BOARD you will be able just to compile,</p>
<p>but not to upload your sketch.</p>
</div>
</div>
<BoardAndPortSelectionComponent
ref={ref => this.boardAndPortSelectionComponent = ref}
boardsService={boardsService}
onSelect={this.onSelect} />
</div>
</React.Fragment>
return content;
}
protected readonly onSelect = (selection: BoardAndPortSelection) => { this.doOnSelect(selection) };
protected doOnSelect(selection: BoardAndPortSelection) {
this.boardAndPort = selection;
this.fireChanged(this.boardAndPort);
}
}

View File

@ -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<BoardAndPortSelection> {
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);
}
}

View File

@ -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<ConnectedBoards.Props, ConnectedBoards.State> {
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) => <option value={i} key={i}>{b.name}</option>);
} else {
let label;
if (this.state.boardsLoading) {
label = "Loading ...";
} else {
label = "No board attached";
}
content = [ <option key="loading" value="0">{label}</option> ];
}
return <div key='arduino-connected-boards' className={`${ARDUINO_TOOLBAR_ITEM_CLASS} item ${ConnectedBoards.Styles.CONNECTED_BOARDS_CLASS}`}>
<select key='arduino-connected-boards-select' disabled={!this.state.boards}
onChange={this.onBoardSelect.bind(this)}
value={this.state.selection}>
<optgroup key='arduino-connected-boards-select-opt-group' label="Attached boards">
{ content }
</optgroup>
<optgroup label="_________" key='arduino-connected-boards-select-opt-group2'>
{ !!this.state.otherBoard && <option value="selected-other" key="selected-other">{this.state.otherBoard.name} (not attached)</option> }
<option value="select-other" key="select-other">Select other Board</option>
</optgroup>
</select>
</div>;
}
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<HTMLSelectElement>) {
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<Board | undefined> {
const {items} = await this.props.boardsService.search({});
const idx = new Map<string, Board>();
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';
}
}

View File

@ -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);
}
}

View File

@ -8,6 +8,7 @@
#outputView {
color: var(--theia-ui-font-color3);
cursor: text;
}
#arduino-verify.arduino-tool-icon:hover,

View File

@ -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<BoardPackage>;
}
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<BoardsServiceClient> {
getAttachedBoards(): Promise<{ boards: Board[] }>;
selectBoard(board: Board | AttachedSerialBoard | AttachedNetworkBoard): Promise<void>;
getSelectBoard(): Promise<Board | AttachedSerialBoard | AttachedNetworkBoard | undefined>;
search(options: { query?: string }): Promise<{ items: BoardPackage[] }>;
install(item: BoardPackage): Promise<void>;
}
@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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<BoardsService, BoardsServiceClient>(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>(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();

View File

@ -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<void>();
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<void> {
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(...[
// <AttachedSerialBoard>{ name: 'Arduino/Genuino Uno', fqbn: 'arduino:avr:uno', port: '/dev/cu.usbmodem14201' },
// <AttachedSerialBoard>{ name: 'Arduino/Genuino Uno', fqbn: 'arduino:avr:uno', port: '/dev/cu.usbmodem142xx' },
// ]);
resolve({ boards });
})
});
}
async selectBoard(board: Board): Promise<void> {
this.selectedBoard = board;
}
async getSelectBoard(): Promise<Board | undefined> {
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);
}

View File

@ -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<void> {
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 {

View File

@ -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: