Skip to content

Commit a936e4c

Browse files
authoredAug 14, 2019
Merge pull request #49 from bcmi-labs/boards-manager
generalized the boards and the libraries views.
2 parents b24d440 + 7c2a295 commit a936e4c

36 files changed

+839
-486
lines changed
 

‎arduino-ide-extension/src/browser/arduino-commands.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,14 @@ export namespace ArduinoCommands {
4343
id: "arduino-toggle-pro-mode"
4444
}
4545

46+
export const CONNECT_TODO: Command = {
47+
id: 'connect-to-attached-board',
48+
label: 'Connect to Attached Board'
49+
}
50+
51+
export const SEND: Command = {
52+
id: 'send',
53+
label: 'Send a Message to the Connected Board'
54+
}
55+
4656
}

‎arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
55
import { MessageService } from '@theia/core/lib/common/message-service';
66
import { CommandContribution, CommandRegistry, Command } from '@theia/core/lib/common/command';
77
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
8-
import { BoardsService } from '../common/protocol/boards-service';
8+
import { BoardsService, AttachedSerialBoard } from '../common/protocol/boards-service';
99
import { ArduinoCommands } from './arduino-commands';
1010
import { CoreService } from '../common/protocol/core-service';
1111
import { WorkspaceServiceExt } from './workspace-service-ext';
@@ -19,7 +19,18 @@ import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service
1919
import { SketchFactory } from './sketch-factory';
2020
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
2121
import { EditorManager, EditorMainMenu } from '@theia/editor/lib/browser';
22-
import { ContextMenuRenderer, OpenerService, Widget, StatusBar, ShellLayoutRestorer, StatusBarAlignment, LabelProvider } from '@theia/core/lib/browser';
22+
import {
23+
ContextMenuRenderer,
24+
OpenerService,
25+
Widget,
26+
StatusBar,
27+
ShellLayoutRestorer,
28+
StatusBarAlignment,
29+
QuickOpenItem,
30+
QuickOpenMode,
31+
QuickOpenService,
32+
LabelProvider
33+
} from '@theia/core/lib/browser';
2334
import { OpenFileDialogProps, FileDialogService } from '@theia/filesystem/lib/browser/file-dialog';
2435
import { FileSystem, FileStat } from '@theia/filesystem/lib/common';
2536
import { ArduinoToolbarContextMenu } from './arduino-file-menu';
@@ -34,6 +45,7 @@ import { MaybePromise } from '@theia/core/lib/common/types';
3445
import { BoardsConfigDialog } from './boards/boards-config-dialog';
3546
import { BoardsToolBarItem } from './boards/boards-toolbar-item';
3647
import { BoardsConfig } from './boards/boards-config';
48+
import { MonitorService } from '../common/protocol/monitor-service';
3749

3850
export namespace ArduinoMenus {
3951
export const SKETCH = [...MAIN_MENU_BAR, '3_sketch'];
@@ -56,6 +68,12 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
5668
@inject(CoreService)
5769
protected readonly coreService: CoreService;
5870

71+
@inject(MonitorService)
72+
protected readonly monitorService: MonitorService;
73+
74+
// TODO: make this better!
75+
protected connectionId: string | undefined;
76+
5977
@inject(WorkspaceServiceExt)
6078
protected readonly workspaceServiceExt: WorkspaceServiceExt;
6179

@@ -115,6 +133,9 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
115133

116134
@inject(LabelProvider)
117135
protected readonly labelProvider: LabelProvider;
136+
137+
@inject(QuickOpenService)
138+
protected readonly quickOpenService: QuickOpenService;
118139

119140
protected boardsToolbarItem: BoardsToolBarItem | null;
120141
protected wsSketchCount: number = 0;
@@ -293,14 +314,73 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
293314
this.boardsServiceClient.boardsConfig = boardsConfig;
294315
}
295316
}
296-
})
317+
});
297318
registry.registerCommand(ArduinoCommands.TOGGLE_PRO_MODE, {
298319
execute: () => {
299320
const oldModeState = ARDUINO_PRO_MODE;
300321
window.localStorage.setItem('arduino-pro-mode', oldModeState ? 'false' : 'true');
301322
registry.executeCommand('reset.layout');
302323
},
303324
isToggled: () => ARDUINO_PRO_MODE
325+
});
326+
registry.registerCommand(ArduinoCommands.CONNECT_TODO, {
327+
execute: async () => {
328+
const { boardsConfig } = this.boardsServiceClient;
329+
const { selectedBoard, selectedPort } = boardsConfig;
330+
if (!selectedBoard) {
331+
this.messageService.warn('No boards selected.');
332+
return;
333+
}
334+
const { name } = selectedBoard;
335+
if (!selectedPort) {
336+
this.messageService.warn(`No ports selected for board: '${name}'.`);
337+
return;
338+
}
339+
const attachedBoards = await this.boardsService.getAttachedBoards();
340+
const connectedBoard = attachedBoards.boards.filter(AttachedSerialBoard.is).find(board => BoardsConfig.Config.sameAs(boardsConfig, board));
341+
if (!connectedBoard) {
342+
this.messageService.warn(`The selected '${name}' board is not connected on ${selectedPort}.`);
343+
return;
344+
}
345+
if (this.connectionId) {
346+
console.log('>>> Disposing existing monitor connection before establishing a new one...');
347+
const result = await this.monitorService.disconnect(this.connectionId);
348+
if (!result) {
349+
// TODO: better!!!
350+
console.error(`Could not close connection: ${this.connectionId}. Check the backend logs.`);
351+
} else {
352+
console.log(`<<< Disposed ${this.connectionId} connection.`)
353+
}
354+
}
355+
const { connectionId } = await this.monitorService.connect({ board: selectedBoard, port: selectedPort });
356+
this.connectionId = connectionId;
357+
}
358+
});
359+
registry.registerCommand(ArduinoCommands.SEND, {
360+
isEnabled: () => !!this.connectionId,
361+
execute: async () => {
362+
const { monitorService, connectionId } = this;
363+
const model = {
364+
onType(lookFor: string, acceptor: (items: QuickOpenItem[]) => void): void {
365+
acceptor([
366+
new QuickOpenItem({
367+
label: "Type your message and press 'Enter' to send it to the board. Escape to cancel.",
368+
run: (mode: QuickOpenMode): boolean => {
369+
if (mode !== QuickOpenMode.OPEN) {
370+
return false;
371+
}
372+
monitorService.send(connectionId!, lookFor + '\n');
373+
return true;
374+
}
375+
})
376+
]);
377+
}
378+
};
379+
const options = {
380+
placeholder: "Your message. The message will be suffixed with a LF ['\\n'].",
381+
};
382+
this.quickOpenService.open(model, options);
383+
}
304384
})
305385
}
306386

‎arduino-ide-extension/src/browser/arduino-frontend-module.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { ArduinoLanguageGrammarContribution } from './language/arduino-language-
1313
import { LibraryService, LibraryServicePath } from '../common/protocol/library-service';
1414
import { BoardsService, BoardsServicePath, BoardsServiceClient } from '../common/protocol/boards-service';
1515
import { SketchesService, SketchesServicePath } from '../common/protocol/sketches-service';
16-
import { LibraryListWidgetFrontendContribution } from './library/list-widget-frontend-contribution';
1716
import { CoreService, CoreServicePath } from '../common/protocol/core-service';
1817
import { BoardsListWidget } from './boards/boards-list-widget';
1918
import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution';
@@ -52,6 +51,11 @@ import { ScmContribution } from '@theia/scm/lib/browser/scm-contribution';
5251
import { SilentScmContribution } from './customization/silent-scm-contribution';
5352
import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
5453
import { SilentSearchInWorkspaceContribution } from './customization/silent-search-in-workspace-contribution';
54+
import { LibraryListWidgetFrontendContribution } from './library/library-widget-frontend-contribution';
55+
import { LibraryItemRenderer } from './library/library-item-renderer';
56+
import { BoardItemRenderer } from './boards/boards-item-renderer';
57+
import { MonitorServiceClientImpl } from './monitor/monitor-service-client-impl';
58+
import { MonitorServicePath, MonitorService, MonitorServiceClient } from '../common/protocol/monitor-service';
5559
const ElementQueries = require('css-element-queries/src/ElementQueries');
5660

5761
if (!ARDUINO_PRO_MODE) {
@@ -87,6 +91,7 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
8791
createWidget: () => context.container.get(LibraryListWidget)
8892
}));
8993
bind(FrontendApplicationContribution).toService(LibraryListWidgetFrontendContribution);
94+
bind(LibraryItemRenderer).toSelf().inSingletonScope();
9095

9196
// Sketch list service
9297
bind(SketchesService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, SketchesServicePath)).inSingletonScope();
@@ -113,6 +118,7 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
113118
createWidget: () => context.container.get(BoardsListWidget)
114119
}));
115120
bind(FrontendApplicationContribution).toService(BoardsListWidgetFrontendContribution);
121+
bind(BoardItemRenderer).toSelf().inSingletonScope();
116122

117123
// Board select dialog
118124
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
@@ -145,6 +151,20 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
145151
return workspaceServiceExt;
146152
});
147153

154+
// Frontend binding for the monitor service.
155+
bind(MonitorService).toDynamicValue(context => {
156+
const connection = context.container.get(WebSocketConnectionProvider);
157+
const client = context.container.get(MonitorServiceClientImpl);
158+
return connection.createProxy(MonitorServicePath, client);
159+
}).inSingletonScope();
160+
// Monitor service client to receive and delegate notifications from the backend.
161+
bind(MonitorServiceClientImpl).toSelf().inSingletonScope();
162+
bind(MonitorServiceClient).toDynamicValue(context => {
163+
const client = context.container.get(MonitorServiceClientImpl);
164+
WebSocketConnectionProvider.createProxy(context.container, MonitorServicePath, client);
165+
return client;
166+
}).inSingletonScope();
167+
148168
bind(AWorkspaceService).toSelf().inSingletonScope();
149169
rebind(WorkspaceService).to(AWorkspaceService).inSingletonScope();
150170
bind(SketchFactory).toSelf().inSingletonScope();

‎arduino-ide-extension/src/browser/boards/boards-config.tsx

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react';
22
import { DisposableCollection } from '@theia/core';
3-
import { BoardsService, Board, AttachedSerialBoard } from '../../common/protocol/boards-service';
3+
import { BoardsService, Board, AttachedSerialBoard, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service';
44
import { BoardsServiceClientImpl } from './boards-service-client-impl';
55

66
export namespace BoardsConfig {
@@ -18,31 +18,36 @@ export namespace BoardsConfig {
1818
}
1919

2020
export interface State extends Config {
21-
searchResults: Board[];
21+
searchResults: Array<Board & { packageName: string }>;
2222
knownPorts: string[];
2323
}
2424

2525
}
2626

2727
export abstract class Item<T> extends React.Component<{
2828
item: T,
29-
name: string,
29+
label: string,
3030
selected: boolean,
3131
onClick: (item: T) => void,
32-
missing?: boolean }> {
32+
missing?: boolean,
33+
detail?: string
34+
}> {
3335

3436
render(): React.ReactNode {
35-
const { selected, name, missing } = this.props;
37+
const { selected, label, missing, detail } = this.props;
3638
const classNames = ['item'];
3739
if (selected) {
3840
classNames.push('selected');
3941
}
4042
if (missing === true) {
4143
classNames.push('missing')
4244
}
43-
return <div onClick={this.onClick} className={classNames.join(' ')}>
44-
{name}
45-
{selected ? <i className='fa fa-check'></i> : ''}
45+
return <div onClick={this.onClick} className={classNames.join(' ')} title={`${label}${!detail ? '' : detail}`}>
46+
<div className='label'>
47+
{label}
48+
</div>
49+
{!detail ? '' : <div className='detail'>{detail}</div>}
50+
{!selected ? '' : <div className='selected-icon'><i className='fa fa-check'/></div>}
4651
</div>;
4752
}
4853

@@ -72,7 +77,7 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
7277
this.props.boardsService.getAttachedBoards().then(({ boards }) => this.updatePorts(boards));
7378
const { boardsServiceClient: client } = this.props;
7479
this.toDispose.pushAll([
75-
client.onBoardsChanged(event => this.updatePorts(event.newState.boards)),
80+
client.onBoardsChanged(event => this.updatePorts(event.newState.boards, AttachedBoardsChangeEvent.diff(event).detached)),
7681
client.onBoardsConfigChanged(({ selectedBoard, selectedPort }) => {
7782
this.setState({ selectedBoard, selectedPort }, () => this.fireConfigChanged());
7883
})
@@ -96,23 +101,24 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
96101
this.queryBoards({ query }).then(({ searchResults }) => this.setState({ searchResults }));
97102
}
98103

99-
protected updatePorts = (boards: Board[] = []) => {
104+
protected updatePorts = (boards: Board[] = [], detachedBoards: Board[] = []) => {
100105
this.queryPorts(Promise.resolve({ boards })).then(({ knownPorts }) => {
101106
let { selectedPort } = this.state;
102-
if (!!selectedPort && knownPorts.indexOf(selectedPort) === -1) {
107+
const removedPorts = detachedBoards.filter(AttachedSerialBoard.is).map(({ port }) => port);
108+
if (!!selectedPort && removedPorts.indexOf(selectedPort) !== -1) {
103109
selectedPort = undefined;
104110
}
105111
this.setState({ knownPorts, selectedPort }, () => this.fireConfigChanged());
106112
});
107113
}
108114

109-
protected queryBoards = (options: { query?: string } = {}): Promise<{ searchResults: Board[] }> => {
115+
protected queryBoards = (options: { query?: string } = {}): Promise<{ searchResults: Array<Board & { packageName: string }> }> => {
110116
const { boardsService } = this.props;
111117
const query = (options.query || '').toLocaleLowerCase();
112-
return new Promise<{ searchResults: Board[] }>(resolve => {
118+
return new Promise<{ searchResults: Array<Board & { packageName: string }> }>(resolve => {
113119
boardsService.search(options)
114120
.then(({ items }) => items
115-
.map(item => item.boards)
121+
.map(item => item.boards.map(board => ({ ...board, packageName: item.name })))
116122
.reduce((acc, curr) => acc.concat(curr), [])
117123
.filter(board => board.name.toLocaleLowerCase().indexOf(query) !== -1)
118124
.sort(Board.compare))
@@ -139,7 +145,7 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
139145
this.setState({ selectedPort }, () => this.fireConfigChanged());
140146
}
141147

142-
protected selectBoard = (selectedBoard: Board | undefined) => {
148+
protected selectBoard = (selectedBoard: Board & { packageName: string } | undefined) => {
143149
this.setState({ selectedBoard }, () => this.fireConfigChanged());
144150
}
145151

@@ -166,18 +172,40 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
166172
}
167173

168174
protected renderBoards(): React.ReactNode {
169-
const { selectedBoard } = this.state;
175+
const { selectedBoard, searchResults } = this.state;
176+
// Board names are not unique. We show the corresponding core name as a detail.
177+
// https://github.com/arduino/arduino-cli/pull/294#issuecomment-513764948
178+
const distinctBoardNames = new Map<string, number>();
179+
for (const { name } of searchResults) {
180+
const counter = distinctBoardNames.get(name) || 0;
181+
distinctBoardNames.set(name, counter + 1);
182+
}
183+
184+
// Due to the non-unique board names, we have to check the package name as well.
185+
const selected = (board: Board & { packageName: string }) => {
186+
if (!!selectedBoard) {
187+
if (Board.equals(board, selectedBoard)) {
188+
if ('packageName' in selectedBoard) {
189+
return board.packageName === (selectedBoard as any).packageName;
190+
}
191+
return true;
192+
}
193+
}
194+
return false;
195+
}
196+
170197
return <React.Fragment>
171198
<div className='search'>
172199
<input type='search' placeholder='SEARCH BOARD' onChange={this.updateBoards} ref={this.focusNodeSet} />
173200
<i className='fa fa-search'></i>
174201
</div>
175202
<div className='boards list'>
176-
{this.state.searchResults.map((board, index) => <Item<Board>
177-
key={`${board.name}-${index}`}
203+
{this.state.searchResults.map(board => <Item<Board & { packageName: string }>
204+
key={`${board.name}-${board.packageName}`}
178205
item={board}
179-
name={board.name}
180-
selected={!!selectedBoard && Board.equals(board, selectedBoard)}
206+
label={board.name}
207+
detail={(distinctBoardNames.get(board.name) || 0) > 1 ? ` - ${board.packageName}` : undefined}
208+
selected={selected(board)}
181209
onClick={this.selectBoard}
182210
missing={!Board.installed(board)}
183211
/>)}
@@ -197,7 +225,7 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
197225
{this.state.knownPorts.map(port => <Item<string>
198226
key={port}
199227
item={port}
200-
name={port}
228+
label={port}
201229
selected={this.state.selectedPort === port}
202230
onClick={this.selectPort}
203231
/>)}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import * as React from 'react';
2+
import { injectable } from 'inversify';
3+
import { ListItemRenderer } from '../components/component-list/list-item-renderer';
4+
import { BoardPackage } from '../../common/protocol/boards-service';
5+
6+
@injectable()
7+
export class BoardItemRenderer extends ListItemRenderer<BoardPackage> {
8+
9+
renderItem(item: BoardPackage, install: (item: BoardPackage) => Promise<void>): React.ReactNode {
10+
const name = <span className='name'>{item.name}</span>;
11+
const author = <span className='author'>{item.author}</span>;
12+
const installedVersion = !!item.installedVersion && <div className='version-info'>
13+
<span className='version'>Version {item.installedVersion}</span>
14+
<span className='installed'>INSTALLED</span>
15+
</div>;
16+
17+
const summary = <div className='summary'>{item.summary}</div>;
18+
const description = <div className='summary'>{item.description}</div>;
19+
20+
const moreInfo = !!item.moreInfoLink && <a href={item.moreInfoLink} onClick={this.onClick}>More info</a>;
21+
const installButton = item.installable && !item.installedVersion &&
22+
<button className='install' onClick={install.bind(this, item)}>INSTALL</button>;
23+
24+
const versions = (() => {
25+
const { availableVersions } = item;
26+
if (!!item.installedVersion || availableVersions.length === 0) {
27+
return undefined;
28+
} else if (availableVersions.length === 1) {
29+
return <label>{availableVersions[0]}</label>
30+
} else {
31+
return <select>{item.availableVersions.map(version => <option value={version} key={version}>{version}</option>)}</select>;
32+
}
33+
})();
34+
35+
return <div className='component-list-item noselect'>
36+
<div className='header'>
37+
<span>{name} by {author}</span>
38+
{installedVersion}
39+
</div>
40+
<div className='content'>
41+
{summary}
42+
{description}
43+
</div>
44+
<div className='info'>
45+
{moreInfo}
46+
</div>
47+
<div className='footer'>
48+
{installButton}
49+
{versions}
50+
</div>
51+
</div>;
52+
}
53+
54+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { inject, injectable } from 'inversify';
2+
import { BoardPackage, BoardsService } from '../../common/protocol/boards-service';
3+
import { ListWidget } from '../components/component-list/list-widget';
4+
import { BoardItemRenderer } from './boards-item-renderer';
5+
6+
@injectable()
7+
export class BoardsListWidget extends ListWidget<BoardPackage> {
8+
9+
static WIDGET_ID = 'boards-list-widget';
10+
static WIDGET_LABEL = 'Boards Manager';
11+
12+
constructor(
13+
@inject(BoardsService) protected service: BoardsService,
14+
@inject(BoardItemRenderer) protected itemRenderer: BoardItemRenderer) {
15+
16+
super({
17+
id: BoardsListWidget.WIDGET_ID,
18+
label: BoardsListWidget.WIDGET_LABEL,
19+
iconClass: 'fa fa-microchip',
20+
searchable: service,
21+
installable: service,
22+
itemLabel: (item: BoardPackage) => item.name,
23+
itemRenderer
24+
});
25+
}
26+
27+
}

‎arduino-ide-extension/src/browser/boards/boards-list-widget.tsx

Lines changed: 0 additions & 16 deletions
This file was deleted.

‎arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@ export class BoardsServiceClientImpl implements BoardsServiceClient {
3030

3131
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
3232
this.logger.info('Attached boards changed: ', JSON.stringify(event));
33-
const { boards } = event.newState;
33+
const detachedBoards = AttachedBoardsChangeEvent.diff(event).detached.filter(AttachedSerialBoard.is).map(({ port }) => port);
3434
const { selectedPort, selectedBoard } = this.boardsConfig;
3535
this.onAttachedBoardsChangedEmitter.fire(event);
36-
// Dynamically unset the port if there is not corresponding attached boards for it.
37-
if (!!selectedPort && boards.filter(AttachedSerialBoard.is).map(({ port }) => port).indexOf(selectedPort) === -1) {
36+
// Dynamically unset the port if the selected board was an attached one and we detached it.
37+
if (!!selectedPort && detachedBoards.indexOf(selectedPort) !== -1) {
3838
this.boardsConfig = {
3939
selectedBoard,
4040
selectedPort: undefined

‎arduino-ide-extension/src/browser/boards/boards-widget-frontend-contribution.ts

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,12 @@
11
import { injectable } from 'inversify';
2-
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
3-
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
4-
import { ListWidget } from './list-widget';
5-
import { BoardsListWidget } from './boards-list-widget';
62
import { MenuModelRegistry } from '@theia/core';
3+
import { BoardsListWidget } from './boards-list-widget';
74
import { ArduinoMenus } from '../arduino-frontend-contribution';
5+
import { BoardPackage } from '../../common/protocol/boards-service';
6+
import { ListWidgetFrontendContribution } from '../components/component-list/list-widget-frontend-contribution';
87

98
@injectable()
10-
export abstract class ListWidgetFrontendContribution extends AbstractViewContribution<ListWidget> implements FrontendApplicationContribution {
11-
12-
async initializeLayout(): Promise<void> {
13-
// await this.openView();
14-
}
15-
16-
}
17-
18-
@injectable()
19-
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution {
9+
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<BoardPackage> {
2010

2111
static readonly OPEN_MANAGER = `${BoardsListWidget.WIDGET_ID}:toggle`;
2212

‎arduino-ide-extension/src/browser/boards/list-widget.tsx

Lines changed: 0 additions & 76 deletions
This file was deleted.
Lines changed: 9 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,25 @@
11
import * as React from 'react';
2-
import { WindowService } from '@theia/core/lib/browser/window/window-service';
3-
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
2+
import { ListItemRenderer } from './list-item-renderer';
43

5-
export class ComponentListItem extends React.Component<ComponentListItem.Props> {
4+
export class ComponentListItem<T> extends React.Component<ComponentListItem.Props<T>> {
65

7-
protected onClick = (event: React.SyntheticEvent<HTMLAnchorElement, Event>) => {
8-
const { target } = event.nativeEvent;
9-
if (target instanceof HTMLAnchorElement) {
10-
this.props.windowService.openNewWindow(target.href);
11-
event.nativeEvent.preventDefault();
12-
}
13-
}
14-
15-
protected async install(item: ArduinoComponent): Promise<void> {
6+
protected async install(item: T): Promise<void> {
167
await this.props.install(item);
178
}
189

1910
render(): React.ReactNode {
20-
const { item } = this.props;
21-
22-
const style = ComponentListItem.Styles;
23-
const name = <span className={style.NAME_CLASS}>{item.name}</span>;
24-
const author = <span className={style.AUTHOR_CLASS}>{item.author}</span>;
25-
const installedVersion = !!item.installedVersion && <div className={style.VERSION_INFO_CLASS}>
26-
<span className={style.VERSION_CLASS}>Version {item.installedVersion}</span>
27-
<span className={style.INSTALLED_CLASS}>INSTALLED</span>
28-
</div>;
29-
30-
const summary = <div className={style.SUMMARY_CLASS}>{item.summary}</div>;
31-
32-
const moreInfo = !!item.moreInfoLink && <a href={item.moreInfoLink} onClick={this.onClick}>More info</a>;
33-
const install = this.props.install && item.installable && !item.installedVersion &&
34-
<button className={style.INSTALL_BTN_CLASS} onClick={this.install.bind(this, item)}>INSTALL</button>;
35-
36-
return <div className={[style.LIST_ITEM_CLASS, style.NO_SELECT_CLASS].join(' ')}>
37-
<div className={style.HEADER_CLASS}>
38-
<span>{name} by {author}</span>
39-
{installedVersion}
40-
</div>
41-
<div className={style.CONTENT_CLASS}>
42-
{summary}
43-
</div>
44-
<div className={style.FOOTER_CLASS}>
45-
{moreInfo}
46-
{install}
47-
</div>
48-
</div>;
11+
const { item, itemRenderer, install } = this.props;
12+
return itemRenderer.renderItem(item, install.bind(this));
4913
}
5014

5115
}
5216

5317
export namespace ComponentListItem {
5418

55-
export interface Props {
56-
readonly item: ArduinoComponent;
57-
readonly windowService: WindowService;
58-
readonly install: (comp: ArduinoComponent) => Promise<void>;
59-
}
60-
61-
export namespace Styles {
62-
export const LIST_ITEM_CLASS = 'component-list-item';
63-
export const HEADER_CLASS = 'header';
64-
export const VERSION_INFO_CLASS = 'version-info';
65-
export const CONTENT_CLASS = 'content';
66-
export const FOOTER_CLASS = 'footer';
67-
export const INSTALLED_CLASS = 'installed';
68-
export const NO_SELECT_CLASS = 'noselect';
69-
70-
export const NAME_CLASS = 'name';
71-
export const AUTHOR_CLASS = 'author';
72-
export const VERSION_CLASS = 'version';
73-
export const SUMMARY_CLASS = 'summary';
74-
export const DESCRIPTION_CLASS = 'description';
75-
export const INSTALL_BTN_CLASS = 'install';
19+
export interface Props<T> {
20+
readonly item: T;
21+
readonly install: (item: T) => Promise<void>;
22+
readonly itemRenderer: ListItemRenderer<T>;
7623
}
7724

7825
}
Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
import * as React from 'react';
2-
import { WindowService } from '@theia/core/lib/browser/window/window-service';
32
import { ComponentListItem } from './component-list-item';
4-
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
3+
import { ListItemRenderer } from './list-item-renderer';
54

6-
export class ComponentList extends React.Component<ComponentList.Props> {
5+
export class ComponentList<T> extends React.Component<ComponentList.Props<T>> {
76

87
protected container?: HTMLElement;
98

109
render(): React.ReactNode {
1110
return <div
1211
className={'items-container'}
13-
ref={element => this.container = element || undefined}>
12+
ref={this.setRef}>
1413
{this.props.items.map(item => this.createItem(item))}
1514
</div>;
1615
}
@@ -21,19 +20,28 @@ export class ComponentList extends React.Component<ComponentList.Props> {
2120
}
2221
}
2322

24-
protected createItem(item: ArduinoComponent): React.ReactNode {
25-
return <ComponentListItem key={item.name} item={item} windowService={this.props.windowService} install={this.props.install} />
23+
protected setRef = (element: HTMLElement | null) => {
24+
this.container = element || undefined;
25+
}
26+
27+
protected createItem(item: T): React.ReactNode {
28+
return <ComponentListItem<T>
29+
key={this.props.itemLabel(item)}
30+
item={item}
31+
itemRenderer={this.props.itemRenderer}
32+
install={this.props.install} />
2633
}
2734

2835
}
2936

3037
export namespace ComponentList {
3138

32-
export interface Props {
33-
readonly items: ArduinoComponent[];
34-
readonly windowService: WindowService;
35-
readonly install: (comp: ArduinoComponent) => Promise<void>;
36-
readonly resolveContainer?: (element: HTMLElement) => void;
39+
export interface Props<T> {
40+
readonly items: T[];
41+
readonly itemLabel: (item: T) => string;
42+
readonly itemRenderer: ListItemRenderer<T>;
43+
readonly install: (item: T) => Promise<void>;
44+
readonly resolveContainer: (element: HTMLElement) => void;
3745
}
3846

3947
}

‎arduino-ide-extension/src/browser/components/component-list/filterable-list-container.tsx

Lines changed: 38 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
import * as React from 'react';
2-
import { WindowService } from '@theia/core/lib/browser/window/window-service';
2+
import debounce = require('lodash.debounce');
3+
import { Searchable } from '../../../common/protocol/searchable';
4+
import { Installable } from '../../../common/protocol/installable';
5+
import { InstallationProgressDialog } from '../installation-progress-dialog';
36
import { SearchBar } from './search-bar';
47
import { ComponentList } from './component-list';
5-
import { LibraryService } from '../../../common/protocol/library-service';
6-
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
7-
import { InstallationProgressDialog } from '../installation-progress-dialog';
8+
import { ListItemRenderer } from './list-item-renderer';
89

9-
export class FilterableListContainer extends React.Component<FilterableListContainer.Props, FilterableListContainer.State> {
10+
export class FilterableListContainer<T> extends React.Component<FilterableListContainer.Props<T>, FilterableListContainer.State<T>> {
1011

11-
constructor(props: Readonly<FilterableListContainer.Props>) {
12+
constructor(props: Readonly<FilterableListContainer.Props<T>>) {
1213
super(props);
1314
this.state = {
1415
filterText: '',
1516
items: []
1617
};
17-
this.handleFilterTextChange = this.handleFilterTextChange.bind(this);
1818
}
1919

2020
componentWillMount(): void {
21+
this.search = debounce(this.search, 500);
2122
this.handleFilterTextChange('');
2223
}
2324

@@ -42,36 +43,43 @@ export class FilterableListContainer extends React.Component<FilterableListConta
4243
}
4344

4445
protected renderComponentList(): React.ReactNode {
45-
return <ComponentList
46+
const { itemLabel, resolveContainer, itemRenderer } = this.props;
47+
return <ComponentList<T>
4648
items={this.state.items}
49+
itemLabel={itemLabel}
50+
itemRenderer={itemRenderer}
4751
install={this.install.bind(this)}
48-
windowService={this.props.windowService}
49-
resolveContainer={this.props.resolveContainer}
52+
resolveContainer={resolveContainer}
5053
/>
5154
}
5255

53-
private handleFilterTextChange(filterText: string): void {
54-
const { props } = this.state;
55-
this.props.service.search({ query: filterText, props }).then(result => {
56+
protected handleFilterTextChange = (filterText: string) => {
57+
this.setState({ filterText });
58+
this.search(filterText);
59+
}
60+
61+
protected search (query: string): void {
62+
const { searchable } = this.props;
63+
searchable.search({ query: query.trim() }).then(result => {
5664
const { items } = result;
5765
this.setState({
58-
filterText,
5966
items: this.sort(items)
6067
});
6168
});
6269
}
6370

64-
protected sort(items: ArduinoComponent[]): ArduinoComponent[] {
65-
return items.sort((left, right) => left.name.localeCompare(right.name));
71+
protected sort(items: T[]): T[] {
72+
const { itemLabel } = this.props;
73+
return items.sort((left, right) => itemLabel(left).localeCompare(itemLabel(right)));
6674
}
6775

68-
protected async install(comp: ArduinoComponent): Promise<void> {
69-
const dialog = new InstallationProgressDialog(comp.name);
76+
protected async install(item: T): Promise<void> {
77+
const { installable, searchable, itemLabel } = this.props;
78+
const dialog = new InstallationProgressDialog(itemLabel(item));
7079
dialog.open();
7180
try {
72-
await this.props.service.install(comp);
73-
const { props } = this.state;
74-
const { items } = await this.props.service.search({ query: this.state.filterText, props });
81+
await installable.install(item);
82+
const { items } = await searchable.search({ query: this.state.filterText });
7583
this.setState({ items: this.sort(items) });
7684
} finally {
7785
dialog.close();
@@ -82,23 +90,18 @@ export class FilterableListContainer extends React.Component<FilterableListConta
8290

8391
export namespace FilterableListContainer {
8492

85-
export interface Props {
86-
readonly service: ComponentSource;
87-
readonly windowService: WindowService;
88-
readonly resolveContainer?: (element: HTMLElement) => void;
89-
readonly resolveFocus?: (element: HTMLElement | undefined) => void;
93+
export interface Props<T> {
94+
readonly installable: Installable<T>;
95+
readonly searchable: Searchable<T>;
96+
readonly itemLabel: (item: T) => string;
97+
readonly itemRenderer: ListItemRenderer<T>;
98+
readonly resolveContainer: (element: HTMLElement) => void;
99+
readonly resolveFocus: (element: HTMLElement | undefined) => void;
90100
}
91101

92-
export interface State {
102+
export interface State<T> {
93103
filterText: string;
94-
items: ArduinoComponent[];
95-
props?: LibraryService.Search.Props;
96-
}
97-
98-
export interface ComponentSource {
99-
search(req: { query: string, props?: LibraryService.Search.Props }): Promise<{ items: ArduinoComponent[] }>
100-
install(board: ArduinoComponent): Promise<void>;
104+
items: T[];
101105
}
102106

103107
}
104-
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import * as React from 'react';
2+
import { inject, injectable } from 'inversify';
3+
import { WindowService } from '@theia/core/lib/browser/window/window-service';
4+
5+
@injectable()
6+
export abstract class ListItemRenderer<T> {
7+
8+
@inject(WindowService)
9+
protected windowService: WindowService;
10+
11+
protected onClick = (event: React.SyntheticEvent<HTMLAnchorElement, Event>) => {
12+
const { target } = event.nativeEvent;
13+
if (target instanceof HTMLAnchorElement) {
14+
this.windowService.openNewWindow(target.href);
15+
event.nativeEvent.preventDefault();
16+
}
17+
}
18+
19+
abstract renderItem(item: T, install: (item: T) => Promise<void>): React.ReactNode;
20+
21+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { injectable } from 'inversify';
2+
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
3+
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
4+
import { ListWidget } from './list-widget';
5+
6+
@injectable()
7+
export abstract class ListWidgetFrontendContribution<T> extends AbstractViewContribution<ListWidget<T>> implements FrontendApplicationContribution {
8+
9+
async initializeLayout(): Promise<void> {
10+
}
11+
12+
}
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,30 @@
11
import * as React from 'react';
2-
import { inject, injectable, postConstruct } from 'inversify';
2+
import { injectable, postConstruct } from 'inversify';
33
import { Message } from '@phosphor/messaging';
44
import { Deferred } from '@theia/core/lib/common/promise-util';
55
import { MaybePromise } from '@theia/core/lib/common/types';
66
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
7-
import { WindowService } from '@theia/core/lib/browser/window/window-service';
8-
import { LibraryFilterableListContainer } from './library-filterable-list-container';
9-
import { LibraryService } from '../../common/protocol/library-service';
7+
import { Installable } from '../../../common/protocol/installable';
8+
import { Searchable } from '../../../common/protocol/searchable';
9+
import { FilterableListContainer } from './filterable-list-container';
10+
import { ListItemRenderer } from './list-item-renderer';
1011

1112
@injectable()
12-
export class LibraryListWidget extends ReactWidget {
13-
14-
static WIDGET_ID = 'library-list-widget';
15-
static WIDGET_LABEL = 'Library Manager';
16-
17-
@inject(LibraryService)
18-
protected readonly libraryService: LibraryService;
19-
20-
@inject(WindowService)
21-
protected readonly windowService: WindowService;
13+
export abstract class ListWidget<T> extends ReactWidget {
2214

2315
/**
2416
* Do not touch or use it. It is for setting the focus on the `input` after the widget activation.
2517
*/
2618
protected focusNode: HTMLElement | undefined;
2719
protected readonly deferredContainer = new Deferred<HTMLElement>();
2820

29-
constructor() {
21+
constructor(protected options: ListWidget.Options<T>) {
3022
super();
31-
this.id = LibraryListWidget.WIDGET_ID
32-
this.title.label = LibraryListWidget.WIDGET_LABEL;
33-
this.title.caption = LibraryListWidget.WIDGET_LABEL
34-
this.title.iconClass = 'library-tab-icon';
23+
const { id, label, iconClass } = options;
24+
this.id = id;
25+
this.title.label = label;
26+
this.title.caption = label;
27+
this.title.iconClass = iconClass
3528
this.title.closable = true;
3629
this.addClass('arduino-list-widget');
3730
this.node.tabIndex = 0; // To be able to set the focus on the widget.
@@ -64,25 +57,25 @@ export class LibraryListWidget extends ReactWidget {
6457
}
6558

6659
render(): React.ReactNode {
67-
return <LibraryFilterableListContainer
60+
return <FilterableListContainer<T>
6861
resolveContainer={this.deferredContainer.resolve}
6962
resolveFocus={this.onFocusResolved}
70-
service={this.libraryService}
71-
windowService={this.windowService}
72-
/>;
63+
searchable={this.options.searchable}
64+
installable={this.options.installable}
65+
itemLabel={this.options.itemLabel}
66+
itemRenderer={this.options.itemRenderer} />;
7367
}
7468

7569
}
7670

7771
export namespace ListWidget {
78-
79-
/**
80-
* Props for customizing the abstract list widget.
81-
*/
82-
export interface Props {
72+
export interface Options<T> {
8373
readonly id: string;
84-
readonly title: string;
74+
readonly label: string;
8575
readonly iconClass: string;
76+
readonly installable: Installable<T>;
77+
readonly searchable: Searchable<T>;
78+
readonly itemLabel: (item: T) => string;
79+
readonly itemRenderer: ListItemRenderer<T>;
8680
}
87-
8881
}
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { AbstractDialog } from "@theia/core/lib/browser";
1+
import { AbstractDialog } from '@theia/core/lib/browser';
22

3+
export class InstallationProgressDialog extends AbstractDialog<undefined> {
34

4-
export class InstallationProgressDialog extends AbstractDialog<string> {
5-
readonly value: "does-not-matter";
5+
readonly value = undefined;
66

77
constructor(componentName: string) {
88
super({ title: 'Installation in progress' });
99
this.contentNode.textContent = `Installing ${componentName}. Please wait.`;
1010
}
1111

12-
}
12+
}

‎arduino-ide-extension/src/browser/library/library-component-list-item.tsx

Lines changed: 0 additions & 50 deletions
This file was deleted.

‎arduino-ide-extension/src/browser/library/library-component-list.tsx

Lines changed: 0 additions & 17 deletions
This file was deleted.

‎arduino-ide-extension/src/browser/library/library-filterable-list-container.tsx

Lines changed: 0 additions & 110 deletions
This file was deleted.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import * as React from 'react';
2+
import { injectable } from 'inversify';
3+
import { Library } from '../../common/protocol/library-service';
4+
import { ListItemRenderer } from '../components/component-list/list-item-renderer';
5+
6+
@injectable()
7+
export class LibraryItemRenderer extends ListItemRenderer<Library> {
8+
9+
renderItem(item: Library, install: (item: Library) => Promise<void>): React.ReactNode {
10+
const name = <span className='name'>{item.name}</span>;
11+
const author = <span className='author'>by {item.author}</span>;
12+
const installedVersion = !!item.installedVersion && <div className='version-info'>
13+
<span className='version'>Version {item.installedVersion}</span>
14+
<span className='installed'>INSTALLED</span>
15+
</div>;
16+
17+
const summary = <div className='summary'>{item.summary}</div>;
18+
19+
const moreInfo = !!item.moreInfoLink && <a href={item.moreInfoLink} onClick={this.onClick}>More info</a>;
20+
const installButton = item.installable && !item.installedVersion &&
21+
<button className='install' onClick={install.bind(this, item)}>INSTALL</button>;
22+
23+
const versions = (() => {
24+
const { availableVersions } = item;
25+
if (!!item.installedVersion || availableVersions.length === 0) {
26+
return undefined;
27+
} else if (availableVersions.length === 1) {
28+
return <label>{availableVersions[0]}</label>
29+
} else {
30+
return <select>{item.availableVersions.map(version => <option value={version} key={version}>{version}</option>)}</select>;
31+
}
32+
})();
33+
34+
return <div className='component-list-item noselect'>
35+
<div className='header'>
36+
<span>{name} {author}</span>
37+
{installedVersion}
38+
</div>
39+
<div className='content'>
40+
{summary}
41+
</div>
42+
<div className='info'>
43+
{moreInfo}
44+
</div>
45+
<div className='footer'>
46+
{installButton}
47+
{versions}
48+
</div>
49+
</div>;
50+
}
51+
52+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { inject, injectable } from 'inversify';
2+
import { Library, LibraryService } from '../../common/protocol/library-service';
3+
import { ListWidget } from '../components/component-list/list-widget';
4+
import { LibraryItemRenderer } from './library-item-renderer';
5+
6+
@injectable()
7+
export class LibraryListWidget extends ListWidget<Library> {
8+
9+
static WIDGET_ID = 'library-list-widget';
10+
static WIDGET_LABEL = 'Library Manager';
11+
12+
constructor(
13+
@inject(LibraryService) protected service: LibraryService,
14+
@inject(LibraryItemRenderer) protected itemRenderer: LibraryItemRenderer) {
15+
16+
super({
17+
id: LibraryListWidget.WIDGET_ID,
18+
label: LibraryListWidget.WIDGET_LABEL,
19+
iconClass: 'library-tab-icon',
20+
searchable: service,
21+
installable: service,
22+
itemLabel: (item: Library) => item.name,
23+
itemRenderer
24+
});
25+
}
26+
27+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { injectable } from 'inversify';
2+
import { Emitter } from '@theia/core/lib/common/event';
3+
import { MonitorServiceClient, MonitorReadEvent, MonitorError } from '../../common/protocol/monitor-service';
4+
5+
@injectable()
6+
export class MonitorServiceClientImpl implements MonitorServiceClient {
7+
8+
protected readonly onReadEmitter = new Emitter<MonitorReadEvent>();
9+
protected readonly onErrorEmitter = new Emitter<MonitorError>();
10+
readonly onRead = this.onReadEmitter.event;
11+
readonly onError = this.onErrorEmitter.event;
12+
13+
notifyRead(event: MonitorReadEvent): void {
14+
this.onReadEmitter.fire(event);
15+
const { connectionId, data } = event;
16+
console.log(`Received data from ${connectionId}: ${data}`);
17+
}
18+
19+
notifyError(error: MonitorError): void {
20+
this.onErrorEmitter.fire(error);
21+
}
22+
23+
}

‎arduino-ide-extension/src/browser/style/board-select-dialog.css

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,20 @@ div#select-board-dialog .selectBoardContainer .body .list .item.selected i{
8383
#select-board-dialog .selectBoardContainer .body .list .item {
8484
padding: 10px 5px 10px 10px;
8585
display: flex;
86-
justify-content: space-between;
86+
justify-content: end;
87+
}
88+
89+
#select-board-dialog .selectBoardContainer .body .list .item .selected-icon {
90+
margin-left: auto;
91+
}
92+
93+
#select-board-dialog .selectBoardContainer .body .list .item .detail {
94+
font-size: var(--theia-ui-font-size1);
95+
color: var(--theia-disabled-color0);
96+
width: 155px; /* used heuristics for the calculation */
97+
white-space: pre;
98+
overflow: hidden;
99+
text-overflow: ellipsis;
87100
}
88101

89102
#select-board-dialog .selectBoardContainer .body .list .item.missing {

‎arduino-ide-extension/src/common/protocol/boards-service.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,27 @@
1-
import { ArduinoComponent } from "./arduino-component";
2-
import { JsonRpcServer } from "@theia/core";
1+
import { JsonRpcServer } from '@theia/core';
2+
import { Searchable } from './searchable';
3+
import { Installable } from './installable';
4+
import { ArduinoComponent } from './arduino-component';
35

46
export interface AttachedBoardsChangeEvent {
57
readonly oldState: Readonly<{ boards: Board[] }>;
68
readonly newState: Readonly<{ boards: Board[] }>;
79
}
10+
export namespace AttachedBoardsChangeEvent {
11+
12+
export function diff(event: AttachedBoardsChangeEvent): Readonly<{ attached: Board[], detached: Board[] }> {
13+
const diff = <T>(left: T[], right: T[]) => {
14+
return left.filter(item => right.indexOf(item) === -1);
15+
}
16+
const { boards: newBoards } = event.newState;
17+
const { boards: oldBoards } = event.oldState;
18+
return {
19+
detached: diff(oldBoards, newBoards),
20+
attached: diff(newBoards, oldBoards)
21+
};
22+
}
23+
24+
}
825

926
export interface BoardInstalledEvent {
1027
readonly pkg: Readonly<BoardPackage>;
@@ -18,10 +35,8 @@ export interface BoardsServiceClient {
1835

1936
export const BoardsServicePath = '/services/boards-service';
2037
export const BoardsService = Symbol('BoardsService');
21-
export interface BoardsService extends JsonRpcServer<BoardsServiceClient> {
38+
export interface BoardsService extends Installable<BoardPackage>, Searchable<BoardPackage>, JsonRpcServer<BoardsServiceClient> {
2239
getAttachedBoards(): Promise<{ boards: Board[] }>;
23-
search(options: { query?: string }): Promise<{ items: BoardPackage[] }>;
24-
install(item: BoardPackage): Promise<void>;
2540
}
2641

2742
export interface BoardPackage extends ArduinoComponent {
@@ -34,10 +49,6 @@ export interface Board {
3449
fqbn?: string
3550
}
3651

37-
export interface Port {
38-
port?: string;
39-
}
40-
4152
export namespace Board {
4253

4354
export function is(board: any): board is Board {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export interface Installable<T> {
2+
install(item: T): Promise<void>;
3+
}

‎arduino-ide-extension/src/common/protocol/library-service.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,13 @@
1-
import { ArduinoComponent } from "./arduino-component";
1+
import { Searchable } from './searchable';
2+
import { Installable } from './installable';
3+
import { ArduinoComponent } from './arduino-component';
24

35
export const LibraryServicePath = '/services/library-service';
46
export const LibraryService = Symbol('LibraryService');
5-
export interface LibraryService {
6-
search(options: { query?: string, props?: LibraryService.Search.Props }): Promise<{ items: Library[] }>;
7+
export interface LibraryService extends Installable<Library>, Searchable<Library> {
78
install(library: Library): Promise<void>;
89
}
910

10-
export namespace LibraryService {
11-
export namespace Search {
12-
export interface Props {
13-
[key: string]: string | undefined;
14-
}
15-
}
16-
}
17-
1811
export interface Library extends ArduinoComponent {
1912
readonly builtIn?: boolean;
2013
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { JsonRpcServer } from '@theia/core';
2+
import { Board } from './boards-service';
3+
4+
export interface MonitorError {
5+
readonly message: string;
6+
readonly code: number
7+
}
8+
9+
export interface MonitorReadEvent {
10+
readonly connectionId: string;
11+
readonly data: string;
12+
}
13+
14+
export const MonitorServiceClient = Symbol('MonitorServiceClient');
15+
export interface MonitorServiceClient {
16+
notifyRead(event: MonitorReadEvent): void;
17+
notifyError(event: MonitorError): void;
18+
}
19+
20+
export const MonitorServicePath = '/services/serial-monitor';
21+
export const MonitorService = Symbol('MonitorService');
22+
export interface MonitorService extends JsonRpcServer<MonitorServiceClient> {
23+
connect(config: ConnectionConfig): Promise<{ connectionId: string }>;
24+
disconnect(connectionId: string): Promise<boolean>;
25+
send(connectionId: string, data: string | Uint8Array): Promise<void>;
26+
}
27+
28+
export interface ConnectionConfig {
29+
readonly board: Board;
30+
readonly port: string;
31+
/**
32+
* Defaults to [`SERIAL`](ConnectionType#SERIAL).
33+
*/
34+
readonly type?: ConnectionType;
35+
/**
36+
* Defaults to `9600`.
37+
*/
38+
readonly baudRate?: number;
39+
}
40+
41+
export enum ConnectionType {
42+
SERIAL = 0
43+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export interface Searchable<T> {
2+
search(options: Searchable.Options): Promise<{ items: T[] }>;
3+
}
4+
export namespace Searchable {
5+
export interface Options {
6+
/**
7+
* Defaults to empty an empty string.
8+
*/
9+
readonly query?: string;
10+
}
11+
}

‎arduino-ide-extension/src/node/arduino-backend-module.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import { DefaultWorkspaceServerExt } from './default-workspace-server-ext';
1919
import { WorkspaceServer } from '@theia/workspace/lib/common';
2020
import { SketchesServiceImpl } from './sketches-service-impl';
2121
import { SketchesService, SketchesServicePath } from '../common/protocol/sketches-service';
22+
import { MonitorServiceImpl } from './monitor/monitor-service-impl';
23+
import { MonitorService, MonitorServicePath, MonitorServiceClient } from '../common/protocol/monitor-service';
24+
import { MonitorClientProvider } from './monitor/monitor-client-provider';
2225

2326
export default new ContainerModule((bind, unbind, isBound, rebind) => {
2427
bind(ArduinoDaemon).toSelf().inSingletonScope();
@@ -104,4 +107,25 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
104107
// If nothing was set previously.
105108
bind(DefaultWorkspaceServerExt).toSelf().inSingletonScope();
106109
rebind(WorkspaceServer).toService(DefaultWorkspaceServerExt);
110+
111+
// Shared monitor client provider service for the backend.
112+
bind(MonitorClientProvider).toSelf().inSingletonScope();
113+
114+
// Connection scoped service for the serial monitor.
115+
const monitorServiceConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => {
116+
bind(MonitorServiceImpl).toSelf().inSingletonScope();
117+
bind(MonitorService).toService(MonitorServiceImpl);
118+
bindBackendService<MonitorService, MonitorServiceClient>(MonitorServicePath, MonitorService, (service, client) => {
119+
service.setClient(client);
120+
client.onDidCloseConnection(() => service.dispose());
121+
return service;
122+
});
123+
});
124+
bind(ConnectionContainerModule).toConstantValue(monitorServiceConnectionModule);
125+
126+
// Logger for the monitor service.
127+
bind(ILogger).toDynamicValue(ctx => {
128+
const parentLogger = ctx.container.get<ILogger>(ILogger);
129+
return parentLogger.child('monitor-service');
130+
}).inSingletonScope().whenTargetNamed('monitor-service');
107131
});

‎arduino-ide-extension/src/node/boards-service-impl.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { PlatformSearchReq, PlatformSearchResp, PlatformInstallReq, PlatformInst
66
import { CoreClientProvider } from './core-client-provider';
77
import { BoardListReq, BoardListResp } from './cli-protocol/commands/board_pb';
88
import { ToolOutputServiceServer } from '../common/protocol/tool-output-service';
9-
import { Deferred } from '@theia/core/lib/common/promise-util';
109

1110
@injectable()
1211
export class BoardsServiceImpl implements BoardsService {
@@ -23,7 +22,6 @@ export class BoardsServiceImpl implements BoardsService {
2322

2423
protected selectedBoard: Board | undefined;
2524
protected discoveryInitialized = false;
26-
protected discoveryReady = new Deferred<void>();
2725
protected discoveryTimer: NodeJS.Timeout | undefined;
2826
/**
2927
* Poor man's serial discovery:
@@ -41,7 +39,6 @@ export class BoardsServiceImpl implements BoardsService {
4139
this.doGetAttachedBoards().then(({ boards }) => {
4240
const update = (oldState: Board[], newState: Board[], message: string) => {
4341
this._attachedBoards = { boards: newState };
44-
this.discoveryReady.resolve();
4542
this.discoveryLogger.info(`${message} - Discovered boards: ${JSON.stringify(newState)}`);
4643
if (this.client) {
4744
this.client.notifyAttachedBoardsChanged({
@@ -91,7 +88,6 @@ export class BoardsServiceImpl implements BoardsService {
9188
}
9289

9390
async getAttachedBoards(): Promise<{ boards: Board[] }> {
94-
await this.discoveryReady.promise;
9591
return this._attachedBoards;
9692
}
9793

@@ -163,7 +159,7 @@ export class BoardsServiceImpl implements BoardsService {
163159

164160
let items = resp.getSearchOutputList().map(item => {
165161
let installedVersion: string | undefined;
166-
const matchingPlatform = installedPlatforms.find(ip => ip.getId().startsWith(`${item.getId()}`));
162+
const matchingPlatform = installedPlatforms.find(ip => ip.getId() === item.getId());
167163
if (!!matchingPlatform) {
168164
installedVersion = matchingPlatform.getInstalled();
169165
}
@@ -172,12 +168,13 @@ export class BoardsServiceImpl implements BoardsService {
172168
id: item.getId(),
173169
name: item.getName(),
174170
author: item.getMaintainer(),
175-
availableVersions: [item.getInstalled()],
171+
availableVersions: [item.getLatest()],
176172
description: item.getBoardsList().map(b => b.getName()).join(", "),
177173
installable: true,
178174
summary: "Boards included in this package:",
179175
installedVersion,
180176
boards: item.getBoardsList().map(b => <Board>{ name: b.getName(), fqbn: b.getFqbn() }),
177+
moreInfoLink: item.getWebsite()
181178
}
182179
return result;
183180
});

‎arduino-ide-extension/src/node/core-client-provider-impl.ts

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import {
66
InitReq,
77
Configuration,
88
UpdateIndexReq,
9-
UpdateIndexResp
9+
UpdateIndexResp,
10+
UpdateLibrariesIndexReq,
11+
UpdateLibrariesIndexResp
1012
} from './cli-protocol/commands/commands_pb';
1113
import { WorkspaceServiceExt } from '../browser/workspace-service-ext';
1214
import { FileSystem } from '@theia/filesystem/lib/common';
@@ -111,20 +113,34 @@ export class CoreClientProviderImpl implements CoreClientProvider {
111113
}
112114

113115
// in a separate promise, try and update the index
114-
let succeeded = true;
116+
let indexUpdateSucceeded = true;
115117
for (let i = 0; i < 10; i++) {
116118
try {
117119
await this.updateIndex(client, instance);
118-
succeeded = true;
120+
indexUpdateSucceeded = true;
119121
break;
120122
} catch (e) {
121123
this.toolOutputService.publishNewOutput("daemon", `Error while updating index in attempt ${i}: ${e}`);
122124
}
123125
}
124-
if (!succeeded) {
126+
if (!indexUpdateSucceeded) {
125127
this.toolOutputService.publishNewOutput("daemon", `Was unable to update the index. Please restart to try again.`);
126128
}
127129

130+
let libIndexUpdateSucceeded = true;
131+
for (let i = 0; i < 10; i++) {
132+
try {
133+
await this.updateLibraryIndex(client, instance);
134+
libIndexUpdateSucceeded = true;
135+
break;
136+
} catch (e) {
137+
this.toolOutputService.publishNewOutput("daemon", `Error while updating library index in attempt ${i}: ${e}`);
138+
}
139+
}
140+
if (!libIndexUpdateSucceeded) {
141+
this.toolOutputService.publishNewOutput("daemon", `Was unable to update the library index. Please restart to try again.`);
142+
}
143+
128144
const result = {
129145
client,
130146
instance
@@ -134,6 +150,38 @@ export class CoreClientProviderImpl implements CoreClientProvider {
134150
return result;
135151
}
136152

153+
protected async updateLibraryIndex(client: ArduinoCoreClient, instance: Instance): Promise<void> {
154+
const req = new UpdateLibrariesIndexReq();
155+
req.setInstance(instance);
156+
const resp = client.updateLibrariesIndex(req);
157+
let file: string | undefined;
158+
resp.on('data', (data: UpdateLibrariesIndexResp) => {
159+
const progress = data.getDownloadProgress();
160+
if (progress) {
161+
if (!file && progress.getFile()) {
162+
file = `${progress.getFile()}`;
163+
}
164+
if (progress.getCompleted()) {
165+
if (file) {
166+
if (/\s/.test(file)) {
167+
this.toolOutputService.publishNewOutput("daemon", `${file} completed.\n`);
168+
} else {
169+
this.toolOutputService.publishNewOutput("daemon", `Download of '${file}' completed.\n'`);
170+
}
171+
} else {
172+
this.toolOutputService.publishNewOutput("daemon", `The library index has been successfully updated.\n'`);
173+
}
174+
file = undefined;
175+
}
176+
}
177+
});
178+
await new Promise<void>((resolve, reject) => {
179+
resp.on('error', reject);
180+
resp.on('end', resolve);
181+
});
182+
}
183+
184+
137185
protected async updateIndex(client: ArduinoCoreClient, instance: Instance): Promise<void> {
138186
const updateReq = new UpdateIndexReq();
139187
updateReq.setInstance(instance);
@@ -165,4 +213,4 @@ export class CoreClientProviderImpl implements CoreClientProvider {
165213
});
166214
}
167215

168-
}
216+
}

‎arduino-ide-extension/src/node/library-service-impl.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export class LibraryServiceImpl implements LibraryService {
1414
@inject(ToolOutputServiceServer)
1515
protected readonly toolOutputService: ToolOutputServiceServer;
1616

17-
async search(options: { query?: string, props: LibraryService.Search.Props }): Promise<{ items: Library[] }> {
17+
async search(options: { query?: string }): Promise<{ items: Library[] }> {
1818
const coreClient = await this.coreClientProvider.getClient();
1919
if (!coreClient) {
2020
return { items: [] };
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import * as grpc from '@grpc/grpc-js';
2+
import { injectable, postConstruct } from 'inversify';
3+
import { Deferred } from '@theia/core/lib/common/promise-util';
4+
import { MonitorClient } from '../cli-protocol/monitor/monitor_grpc_pb';
5+
6+
@injectable()
7+
export class MonitorClientProvider {
8+
9+
readonly deferred = new Deferred<MonitorClient>();
10+
11+
@postConstruct()
12+
protected init(): void {
13+
this.deferred.resolve(new MonitorClient('localhost:50051', grpc.credentials.createInsecure()));
14+
}
15+
16+
get client(): Promise<MonitorClient> {
17+
return this.deferred.promise;
18+
}
19+
20+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { v4 } from 'uuid';
2+
import * as grpc from '@grpc/grpc-js';
3+
import { TextDecoder, TextEncoder } from 'util';
4+
import { injectable, inject, named } from 'inversify';
5+
import { ILogger, Disposable, DisposableCollection } from '@theia/core';
6+
import { MonitorService, MonitorServiceClient, ConnectionConfig, ConnectionType } from '../../common/protocol/monitor-service';
7+
import { StreamingOpenReq, StreamingOpenResp, MonitorConfig } from '../cli-protocol/monitor/monitor_pb';
8+
import { MonitorClientProvider } from './monitor-client-provider';
9+
10+
export interface MonitorDuplex {
11+
readonly toDispose: Disposable;
12+
readonly duplex: grpc.ClientDuplexStream<StreamingOpenReq, StreamingOpenResp>;
13+
}
14+
15+
type ErrorCode = { code: number };
16+
type MonitorError = Error & ErrorCode;
17+
namespace MonitorError {
18+
19+
export function is(error: Error & Partial<ErrorCode>): error is MonitorError {
20+
return typeof error.code === 'number';
21+
}
22+
23+
/**
24+
* The frontend has refreshed the browser, for instance.
25+
*/
26+
export function isClientCancelledError(error: MonitorError): boolean {
27+
return error.code === 1 && error.message === 'Cancelled on client';
28+
}
29+
30+
/**
31+
* When detaching a physical device when the duplex channel is still opened.
32+
*/
33+
export function isDeviceNotConfiguredError(error: MonitorError): boolean {
34+
return error.code === 2 && error.message === 'device not configured';
35+
}
36+
37+
}
38+
39+
@injectable()
40+
export class MonitorServiceImpl implements MonitorService {
41+
42+
@inject(ILogger)
43+
@named('monitor-service')
44+
protected readonly logger: ILogger;
45+
46+
@inject(MonitorClientProvider)
47+
protected readonly monitorClientProvider: MonitorClientProvider;
48+
49+
protected client?: MonitorServiceClient;
50+
protected readonly connections = new Map<string, MonitorDuplex>();
51+
52+
setClient(client: MonitorServiceClient | undefined): void {
53+
this.client = client;
54+
}
55+
56+
dispose(): void {
57+
for (const [connectionId, duplex] of this.connections.entries()) {
58+
this.doDisconnect(connectionId, duplex);
59+
}
60+
}
61+
62+
async connect(config: ConnectionConfig): Promise<{ connectionId: string }> {
63+
const client = await this.monitorClientProvider.client;
64+
const duplex = client.streamingOpen();
65+
const connectionId = v4();
66+
const toDispose = new DisposableCollection(
67+
Disposable.create(() => this.disconnect(connectionId))
68+
);
69+
70+
duplex.on('error', ((error: Error) => {
71+
if (MonitorError.is(error) && (
72+
MonitorError.isClientCancelledError(error)
73+
|| MonitorError.isDeviceNotConfiguredError(error)
74+
)) {
75+
if (this.client) {
76+
this.client.notifyError(error);
77+
}
78+
}
79+
this.logger.error(`Error occurred for connection ${connectionId}.`, error);
80+
toDispose.dispose();
81+
}).bind(this));
82+
83+
duplex.on('data', ((resp: StreamingOpenResp) => {
84+
if (this.client) {
85+
const raw = resp.getData();
86+
const data = typeof raw === 'string' ? raw : new TextDecoder('utf8').decode(raw);
87+
this.client.notifyRead({ connectionId, data });
88+
}
89+
}).bind(this));
90+
91+
const { type, port } = config;
92+
const req = new StreamingOpenReq();
93+
const monitorConfig = new MonitorConfig();
94+
monitorConfig.setType(this.mapType(type));
95+
monitorConfig.setTarget(port);
96+
if (config.baudRate !== undefined) {
97+
monitorConfig.setAdditionalconfig({ 'BaudRate': config.baudRate });
98+
}
99+
req.setMonitorconfig(monitorConfig);
100+
101+
return new Promise<{ connectionId: string }>(resolve => {
102+
duplex.write(req, () => {
103+
this.connections.set(connectionId, { toDispose, duplex });
104+
resolve({ connectionId });
105+
});
106+
});
107+
}
108+
109+
async disconnect(connectionId: string): Promise<boolean> {
110+
this.logger.info(`>>> Received disconnect request for connection: ${connectionId}`);
111+
const disposable = this.connections.get(connectionId);
112+
if (!disposable) {
113+
this.logger.warn(`<<< No connection was found for ID: ${connectionId}`);
114+
return false;
115+
}
116+
const result = await this.doDisconnect(connectionId, disposable);
117+
if (result) {
118+
this.logger.info(`<<< Successfully disconnected from ${connectionId}.`);
119+
} else {
120+
this.logger.info(`<<< Could not disconnected from ${connectionId}.`);
121+
}
122+
return result;
123+
}
124+
125+
protected async doDisconnect(connectionId: string, duplex: MonitorDuplex): Promise<boolean> {
126+
const { toDispose } = duplex;
127+
this.logger.info(`>>> Disposing monitor connection: ${connectionId}...`);
128+
try {
129+
toDispose.dispose();
130+
this.logger.info(`<<< Connection disposed: ${connectionId}.`);
131+
return true;
132+
} catch (e) {
133+
this.logger.error(`<<< Error occurred when disposing monitor connection: ${connectionId}. ${e}`);
134+
return false;
135+
}
136+
}
137+
138+
async send(connectionId: string, data: string): Promise<void> {
139+
const duplex = this.duplex(connectionId);
140+
if (duplex) {
141+
const req = new StreamingOpenReq();
142+
req.setData(new TextEncoder().encode(data));
143+
return new Promise<void>(resolve => duplex.duplex.write(req, resolve));
144+
} else {
145+
throw new Error(`No connection with ID: ${connectionId}.`);
146+
}
147+
}
148+
149+
protected mapType(type?: ConnectionType): MonitorConfig.TargetType {
150+
switch (type) {
151+
case ConnectionType.SERIAL: return MonitorConfig.TargetType.SERIAL;
152+
default: return MonitorConfig.TargetType.SERIAL;
153+
}
154+
}
155+
156+
protected duplex(connectionId: string): MonitorDuplex | undefined {
157+
const monitorClient = this.connections.get(connectionId);
158+
if (!monitorClient) {
159+
this.logger.warn(`Could not find monitor client for connection ID: ${connectionId}`);
160+
}
161+
return monitorClient;
162+
}
163+
164+
}

0 commit comments

Comments
 (0)
Please sign in to comment.