Merge pull request #75 from bcmi-labs/25-10-release

Various bug fixes, plus one or two enhancements.
This commit is contained in:
Akos Kitta 2019-10-28 12:41:29 -04:00 committed by GitHub
commit 7c1ebf273c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 823 additions and 328 deletions

View File

@ -29,10 +29,10 @@ Then you can start the browser example again:
yarn --cwd browser-app start yarn --cwd browser-app start
``` ```
## Arduino-PoC Electron Application ## Arduino Pro IDE Electron Application
The project is built on [Azure DevOps](https://dev.azure.com/typefox/Arduino). The project is built on [Azure DevOps](https://dev.azure.com/typefox/Arduino).
Currently, we provide the Arduino-PoC for the following platforms: Currently, we provide the Arduino Pro IDE for the following platforms:
- Windows, - Windows,
- macOS, and - macOS, and
- Linux. - Linux.

View File

@ -1,6 +1,6 @@
{ {
"name": "arduino-ide-extension", "name": "arduino-ide-extension",
"version": "0.0.1", "version": "0.0.2",
"description": "An extension for Theia building the Arduino IDE", "description": "An extension for Theia building the Arduino IDE",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -30,6 +30,7 @@
"react-select": "^3.0.4", "react-select": "^3.0.4",
"p-queue": "^5.0.0", "p-queue": "^5.0.0",
"ps-tree": "^1.2.0", "ps-tree": "^1.2.0",
"string-natural-compare": "^2.0.3",
"tree-kill": "^1.2.1", "tree-kill": "^1.2.1",
"upath": "^1.1.2", "upath": "^1.1.2",
"which": "^1.3.1" "which": "^1.3.1"

View File

@ -137,7 +137,7 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
@inject(QuickOpenService) @inject(QuickOpenService)
protected readonly quickOpenService: QuickOpenService; protected readonly quickOpenService: QuickOpenService;
@inject(ArduinoWorkspaceService) @inject(ArduinoWorkspaceService)
protected readonly workspaceService: ArduinoWorkspaceService; protected readonly workspaceService: ArduinoWorkspaceService;
@inject(ConfigService) @inject(ConfigService)
@ -164,6 +164,11 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
updateStatusBar(this.boardsServiceClient.boardsConfig); updateStatusBar(this.boardsServiceClient.boardsConfig);
this.registerSketchesInMenu(this.menuRegistry); this.registerSketchesInMenu(this.menuRegistry);
Promise.all([
this.boardsService.getAttachedBoards(),
this.boardsService.getAvailablePorts()
]).then(([{ boards }, { ports }]) => this.boardsServiceClient.tryReconnect(boards, ports));
} }
registerToolbarItems(registry: TabBarToolbarRegistry): void { registerToolbarItems(registry: TabBarToolbarRegistry): void {
@ -268,7 +273,7 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
if (!selectedPort) { if (!selectedPort) {
throw new Error('No ports selected. Please select a port.'); throw new Error('No ports selected. Please select a port.');
} }
await this.coreService.upload({ uri: uri.toString(), board: boardsConfig.selectedBoard, port: selectedPort }); await this.coreService.upload({ uri: uri.toString(), board: boardsConfig.selectedBoard, port: selectedPort.address });
} catch (e) { } catch (e) {
await this.messageService.error(e.toString()); await this.messageService.error(e.toString());
} finally { } finally {
@ -302,7 +307,7 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
registry.registerCommand(ArduinoCommands.OPEN_SKETCH, { registry.registerCommand(ArduinoCommands.OPEN_SKETCH, {
isEnabled: () => true, isEnabled: () => true,
execute: async (sketch: Sketch) => { execute: async (sketch: Sketch) => {
this.workspaceService.openSketchFilesInNewWindow(sketch.uri); this.workspaceService.open(new URI(sketch.uri));
} }
}) })
registry.registerCommand(ArduinoCommands.SAVE_SKETCH, { registry.registerCommand(ArduinoCommands.SAVE_SKETCH, {
@ -321,7 +326,7 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
} }
const sketch = await this.sketchService.createNewSketch(uri.toString()); const sketch = await this.sketchService.createNewSketch(uri.toString());
this.workspaceService.openSketchFilesInNewWindow(sketch.uri); this.workspaceService.open(new URI(sketch.uri));
} catch (e) { } catch (e) {
await this.messageService.error(e.toString()); await this.messageService.error(e.toString());
} }
@ -461,7 +466,7 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
if (destinationFile && !destinationFile.isDirectory) { if (destinationFile && !destinationFile.isDirectory) {
const message = await this.validate(destinationFile); const message = await this.validate(destinationFile);
if (!message) { if (!message) {
await this.workspaceService.openSketchFilesInNewWindow(destinationFileUri.toString()); await this.workspaceService.open(destinationFileUri);
return destinationFileUri; return destinationFileUri;
} else { } else {
this.messageService.warn(message); this.messageService.warn(message);

View File

@ -67,6 +67,9 @@ import { TabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-de
import { ArduinoTabBarDecoratorService } from './shell/arduino-tab-bar-decorator'; import { ArduinoTabBarDecoratorService } from './shell/arduino-tab-bar-decorator';
import { ProblemManager } from '@theia/markers/lib/browser'; import { ProblemManager } from '@theia/markers/lib/browser';
import { ArduinoProblemManager } from './markers/arduino-problem-manager'; import { ArduinoProblemManager } from './markers/arduino-problem-manager';
import { BoardsAutoInstaller } from './boards/boards-auto-installer';
import { AboutDialog } from '@theia/core/lib/browser/about-dialog';
import { ArduinoAboutDialog } from './customization/arduino-about-dialog';
const ElementQueries = require('css-element-queries/src/ElementQueries'); const ElementQueries = require('css-element-queries/src/ElementQueries');
export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind) => { export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind) => {
@ -120,6 +123,10 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
return client; return client;
}).inSingletonScope(); }).inSingletonScope();
// boards auto-installer
bind(BoardsAutoInstaller).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(BoardsAutoInstaller);
// Boards list widget // Boards list widget
bind(BoardsListWidget).toSelf(); bind(BoardsListWidget).toSelf();
bindViewContribution(bind, BoardsListWidgetFrontendContribution); bindViewContribution(bind, BoardsListWidgetFrontendContribution);
@ -170,13 +177,12 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
id: MonitorWidget.ID, id: MonitorWidget.ID,
createWidget: () => context.container.get(MonitorWidget) createWidget: () => context.container.get(MonitorWidget)
})); }));
// Frontend binding for the monitor service. // Frontend binding for the monitor service
bind(MonitorService).toDynamicValue(context => { bind(MonitorService).toDynamicValue(context => {
const connection = context.container.get(WebSocketConnectionProvider); const connection = context.container.get(WebSocketConnectionProvider);
const client = context.container.get(MonitorServiceClientImpl); const client = context.container.get(MonitorServiceClientImpl);
return connection.createProxy(MonitorServicePath, client); return connection.createProxy(MonitorServicePath, client);
}).inSingletonScope(); }).inSingletonScope();
// MonitorConnection
bind(MonitorConnection).toSelf().inSingletonScope(); bind(MonitorConnection).toSelf().inSingletonScope();
// Monitor service client to receive and delegate notifications from the backend. // Monitor service client to receive and delegate notifications from the backend.
bind(MonitorServiceClientImpl).toSelf().inSingletonScope(); bind(MonitorServiceClientImpl).toSelf().inSingletonScope();
@ -192,7 +198,7 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
const themeService = ThemeService.get(); const themeService = ThemeService.get();
themeService.register(...ArduinoTheme.themes); themeService.register(...ArduinoTheme.themes);
// customizing default theia // Customizing default Theia layout
if (!ArduinoAdvancedMode.TOGGLED) { if (!ArduinoAdvancedMode.TOGGLED) {
unbind(OutlineViewContribution); unbind(OutlineViewContribution);
bind(OutlineViewContribution).to(SilentOutlineViewContribution).inSingletonScope(); bind(OutlineViewContribution).to(SilentOutlineViewContribution).inSingletonScope();
@ -213,24 +219,29 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
unbind(SearchInWorkspaceFrontendContribution); unbind(SearchInWorkspaceFrontendContribution);
bind(SearchInWorkspaceFrontendContribution).to(SilentSearchInWorkspaceContribution).inSingletonScope(); bind(SearchInWorkspaceFrontendContribution).to(SilentSearchInWorkspaceContribution).inSingletonScope();
} else { } else {
// We use this CSS class on the body to modify the visibbility of the close button for the editors and views. // We use this CSS class on the body to modify the visibility of the close button for the editors and views.
document.body.classList.add(ArduinoAdvancedMode.LS_ID); document.body.classList.add(ArduinoAdvancedMode.LS_ID);
} }
unbind(FrontendApplication); unbind(FrontendApplication);
bind(FrontendApplication).to(ArduinoFrontendApplication).inSingletonScope(); bind(FrontendApplication).to(ArduinoFrontendApplication).inSingletonScope();
// monaco customizations // Monaco customizations
unbind(MonacoEditorProvider); unbind(MonacoEditorProvider);
bind(ArduinoMonacoEditorProvider).toSelf().inSingletonScope(); bind(ArduinoMonacoEditorProvider).toSelf().inSingletonScope();
bind(MonacoEditorProvider).toService(ArduinoMonacoEditorProvider); bind(MonacoEditorProvider).toService(ArduinoMonacoEditorProvider);
// decorator customizations // Decorator customizations
unbind(TabBarDecoratorService); unbind(TabBarDecoratorService);
bind(ArduinoTabBarDecoratorService).toSelf().inSingletonScope(); bind(ArduinoTabBarDecoratorService).toSelf().inSingletonScope();
bind(TabBarDecoratorService).toService(ArduinoTabBarDecoratorService); bind(TabBarDecoratorService).toService(ArduinoTabBarDecoratorService);
// problem markers // Problem markers
unbind(ProblemManager); unbind(ProblemManager);
bind(ArduinoProblemManager).toSelf().inSingletonScope(); bind(ArduinoProblemManager).toSelf().inSingletonScope();
bind(ProblemManager).toService(ArduinoProblemManager); bind(ProblemManager).toService(ArduinoProblemManager);
// About dialog to show the CLI version
unbind(AboutDialog);
bind(ArduinoAboutDialog).toSelf().inSingletonScope();
bind(AboutDialog).toService(ArduinoAboutDialog);
}); });

View File

@ -0,0 +1,68 @@
import { toUnix } from 'upath';
import URI from '@theia/core/lib/common/uri';
import { isWindows } from '@theia/core/lib/common/os';
import { notEmpty } from '@theia/core/lib/common/objects';
import { MaybePromise } from '@theia/core/lib/common/types';
/**
* Class for determining the default workspace location from the
* `location.hash`, the historical workspace locations, and recent sketch files.
*
* The following logic is used for determining the default workspace location:
* - `hash` points to an exists in location?
* - Yes
* - `validate location`. Is valid sketch location?
* - Yes
* - Done.
* - No
* - `try open recent workspace roots`, then `try open last modified sketches`, finally `create new sketch`.
* - No
* - `try open recent workspace roots`, then `try open last modified sketches`, finally `create new sketch`.
*/
namespace ArduinoWorkspaceRootResolver {
export interface InitOptions {
readonly isValid: (uri: string) => MaybePromise<boolean>;
}
export interface ResolveOptions {
readonly hash?: string
readonly recentWorkspaces: string[];
// Gathered from the default sketch folder. The default sketch folder is defined by the CLI.
readonly recentSketches: string[];
}
}
export class ArduinoWorkspaceRootResolver {
constructor(protected options: ArduinoWorkspaceRootResolver.InitOptions) {
}
async resolve(options: ArduinoWorkspaceRootResolver.ResolveOptions): Promise<{ uri: string } | undefined> {
const { hash, recentWorkspaces, recentSketches } = options;
for (const uri of [this.hashToUri(hash), ...recentWorkspaces, ...recentSketches].filter(notEmpty)) {
const valid = await this.isValid(uri);
if (valid) {
return { uri };
}
}
return undefined;
}
protected isValid(uri: string): MaybePromise<boolean> {
return this.options.isValid.bind(this)(uri);
}
// Note: here, the `hash` was defined as new `URI(yourValidFsPath).path` so we have to map it to a valid FS path first.
// This is important for Windows only and a NOOP on POSIX.
// Note: we set the `new URI(myValidUri).path.toString()` as the `hash`. See:
// - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L143 and
// - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L423
protected hashToUri(hash: string | undefined): string | undefined {
if (hash
&& hash.length > 1
&& hash.startsWith('#')) {
const path = hash.slice(1); // Trim the leading `#`.
return new URI(toUnix(path.slice(isWindows && hash.startsWith('/') ? 1 : 0))).withScheme('file').toString();
}
return undefined;
}
}

View File

@ -1,17 +1,11 @@
import { injectable, inject } from 'inversify'; import { injectable, inject } from 'inversify';
import { toUnix } from 'upath';
import URI from '@theia/core/lib/common/uri';
import { isWindows } from '@theia/core/lib/common/os';
import { LabelProvider } from '@theia/core/lib/browser'; import { LabelProvider } from '@theia/core/lib/browser';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { ConfigService } from '../common/protocol/config-service'; import { ConfigService } from '../common/protocol/config-service';
import { SketchesService } from '../common/protocol/sketches-service'; import { SketchesService } from '../common/protocol/sketches-service';
import { ArduinoWorkspaceRootResolver } from './arduino-workspace-resolver';
import { ArduinoAdvancedMode } from './arduino-frontend-contribution'; import { ArduinoAdvancedMode } from './arduino-frontend-contribution';
/**
* This is workaround to have custom frontend binding for the default workspace, although we
* already have a custom binding for the backend.
*/
@injectable() @injectable()
export class ArduinoWorkspaceService extends WorkspaceService { export class ArduinoWorkspaceService extends WorkspaceService {
@ -25,105 +19,38 @@ export class ArduinoWorkspaceService extends WorkspaceService {
protected readonly labelProvider: LabelProvider; protected readonly labelProvider: LabelProvider;
async getDefaultWorkspacePath(): Promise<string | undefined> { async getDefaultWorkspacePath(): Promise<string | undefined> {
const url = new URL(window.location.href); const [hash, recentWorkspaces, recentSketches] = await Promise.all([
// If `sketch` is set and valid, we use it as is. window.location.hash,
// `sketch` is set as an encoded URI string. this.sketchService.getSketches().then(sketches => sketches.map(({ uri }) => uri)),
const sketch = url.searchParams.get('sketch'); this.server.getRecentWorkspaces()
if (sketch) { ]);
const sketchDirUri = new URI(sketch).toString(); const toOpen = await new ArduinoWorkspaceRootResolver({
if (await this.sketchService.isSketchFolder(sketchDirUri)) { isValid: this.isValid.bind(this)
if (await this.configService.isInSketchDir(sketchDirUri)) { }).resolve({
if (ArduinoAdvancedMode.TOGGLED) { hash,
return (await this.configService.getConfiguration()).sketchDirUri recentWorkspaces,
} else { recentSketches
return sketchDirUri; });
} if (toOpen) {
} const { uri } = toOpen;
return (await this.configService.getConfiguration()).sketchDirUri await this.server.setMostRecentlyUsedWorkspace(uri);
} return toOpen.uri;
} }
return (await this.sketchService.createNewSketch()).uri;
const { hash } = window.location;
// Note: here, the `uriPath` was defined as new `URI(yourValidFsPath).path` so we have to map it to a valid FS path first.
// This is important for Windows only and a NOOP on UNIX.
if (hash.length > 1 && hash.startsWith('#')) {
let uri = this.toUri(hash.slice(1));
if (uri && await this.sketchService.isSketchFolder(uri)) {
return this.openSketchFilesInNewWindow(uri);
}
}
// If we cannot acquire the FS path from the `location.hash` we try to get the most recently used workspace that was a valid sketch folder.
// XXX: Check if `WorkspaceServer#getRecentWorkspaces()` returns with inverse-chrolonolgical order.
const candidateUris = await this.server.getRecentWorkspaces();
for (const uri of candidateUris) {
if (await this.sketchService.isSketchFolder(uri)) {
return this.openSketchFilesInNewWindow(uri);
}
}
const config = await this.configService.getConfiguration();
const { sketchDirUri } = config;
const stat = await this.fileSystem.getFileStat(sketchDirUri);
if (!stat) {
// The folder for the workspace root does not exist yet, create it.
await this.fileSystem.createFolder(sketchDirUri);
await this.sketchService.createNewSketch(sketchDirUri);
}
const sketches = await this.sketchService.getSketches(sketchDirUri);
if (!sketches.length) {
const sketch = await this.sketchService.createNewSketch(sketchDirUri);
sketches.unshift(sketch);
}
const uri = sketches[0].uri;
this.server.setMostRecentlyUsedWorkspace(uri);
this.openSketchFilesInNewWindow(uri);
if (ArduinoAdvancedMode.TOGGLED && await this.configService.isInSketchDir(uri)) {
return (await this.configService.getConfiguration()).sketchDirUri;
}
return uri;
} }
private toUri(uriPath: string | undefined): string | undefined { private async isValid(uri: string): Promise<boolean> {
if (uriPath) { const exists = await this.fileSystem.exists(uri);
return new URI(toUnix(uriPath.slice(isWindows && uriPath.startsWith('/') ? 1 : 0))).withScheme('file').toString(); if (!exists) {
return false;
} }
return undefined; // The workspace root location must exist. However, when opening a workspace root in pro-mode,
} // the workspace root must not be a sketch folder. It can be the default sketch directory, or any other directories, for instance.
if (!ArduinoAdvancedMode.TOGGLED) {
async openSketchFilesInNewWindow(uri: string): Promise<string> { return true;
const url = new URL(window.location.href);
const currentSketch = url.searchParams.get('sketch');
// Nothing to do if we want to open the same sketch which is already opened.
const sketchUri = new URI(uri);
if (!!currentSketch && new URI(currentSketch).toString() === sketchUri.toString()) {
return uri;
} }
const sketchFolder = await this.sketchService.isSketchFolder(uri);
url.searchParams.set('sketch', uri); return sketchFolder;
// If in advanced mode, we root folder of all sketch folders as the hash, so the default workspace will be opened on the root
// Note: we set the `new URI(myValidUri).path.toString()` as the `hash`. See:
// - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L143 and
// - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L423
if (ArduinoAdvancedMode.TOGGLED && await this.configService.isInSketchDir(uri)) {
url.hash = new URI((await this.configService.getConfiguration()).sketchDirUri).path.toString();
} else {
// Otherwise, we set the hash as is
const hash = await this.fileSystem.getFsPath(sketchUri.toString());
if (hash) {
url.hash = sketchUri.path.toString()
}
}
// Preserve the current window if the `sketch` is not in the `searchParams`.
if (!currentSketch) {
setTimeout(() => window.location.href = url.toString(), 100);
return uri;
}
this.windowService.openNewWindow(url.toString());
return uri;
} }
} }

View File

@ -0,0 +1,63 @@
import { injectable, inject } from 'inversify';
import { MessageService } from '@theia/core/lib/common/message-service';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { BoardsService, Board } from '../../common/protocol/boards-service';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution';
import { InstallationProgressDialog } from '../components/installation-progress-dialog';
import { BoardsConfig } from './boards-config';
/**
* Listens on `BoardsConfig.Config` changes, if a board is selected which does not
* have the corresponding core installed, it proposes the user to install the core.
*/
@injectable()
export class BoardsAutoInstaller implements FrontendApplicationContribution {
@inject(MessageService)
protected readonly messageService: MessageService;
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
@inject(BoardsListWidgetFrontendContribution)
protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution;
onStart(): void {
this.boardsServiceClient.onBoardsConfigChanged(this.ensureCoreExists.bind(this));
this.ensureCoreExists(this.boardsServiceClient.boardsConfig);
}
protected ensureCoreExists(config: BoardsConfig.Config): void {
const { selectedBoard } = config;
if (selectedBoard) {
this.boardsService.search({}).then(({ items }) => {
const candidates = items
.filter(item => item.boards.some(board => Board.sameAs(board, selectedBoard)))
.filter(({ installable, installedVersion }) => installable && !installedVersion);
for (const candidate of candidates) {
// tslint:disable-next-line:max-line-length
this.messageService.info(`The \`"${candidate.name}"\` core has to be installed for the currently selected \`"${selectedBoard.name}"\` board. Do you want to install it now?`, 'Yes', 'Install Manually').then(async answer => {
if (answer === 'Yes') {
const dialog = new InstallationProgressDialog(candidate.name);
dialog.open();
try {
await this.boardsService.install(candidate);
} finally {
dialog.close();
}
}
if (answer) {
this.boardsManagerFrontendContribution.openView({ reveal: true }).then(widget => widget.refresh(candidate.name.toLocaleLowerCase()));
}
});
}
})
}
}
}

View File

@ -1,13 +1,13 @@
import * as React from 'react'; import * as React from 'react';
import { DisposableCollection } from '@theia/core'; import { DisposableCollection } from '@theia/core';
import { BoardsService, Board, AttachedSerialBoard, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service'; import { BoardsService, Board, Port, AttachedSerialBoard, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service';
import { BoardsServiceClientImpl } from './boards-service-client-impl'; import { BoardsServiceClientImpl } from './boards-service-client-impl';
export namespace BoardsConfig { export namespace BoardsConfig {
export interface Config { export interface Config {
selectedBoard?: Board; selectedBoard?: Board;
selectedPort?: string; selectedPort?: Port;
} }
export interface Props { export interface Props {
@ -19,7 +19,8 @@ export namespace BoardsConfig {
export interface State extends Config { export interface State extends Config {
searchResults: Array<Board & { packageName: string }>; searchResults: Array<Board & { packageName: string }>;
knownPorts: string[]; knownPorts: Port[];
showAllPorts: boolean;
} }
} }
@ -47,7 +48,7 @@ export abstract class Item<T> extends React.Component<{
{label} {label}
</div> </div>
{!detail ? '' : <div className='detail'>{detail}</div>} {!detail ? '' : <div className='detail'>{detail}</div>}
{!selected ? '' : <div className='selected-icon'><i className='fa fa-check'/></div>} {!selected ? '' : <div className='selected-icon'><i className='fa fa-check' /></div>}
</div>; </div>;
} }
@ -68,16 +69,17 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
this.state = { this.state = {
searchResults: [], searchResults: [],
knownPorts: [], knownPorts: [],
showAllPorts: false,
...boardsConfig ...boardsConfig
} }
} }
componentDidMount() { componentDidMount() {
this.updateBoards(); this.updateBoards();
this.props.boardsService.getAttachedBoards().then(({ boards }) => this.updatePorts(boards)); this.props.boardsService.getAvailablePorts().then(({ ports }) => this.updatePorts(ports));
const { boardsServiceClient: client } = this.props; const { boardsServiceClient: client } = this.props;
this.toDispose.pushAll([ this.toDispose.pushAll([
client.onBoardsChanged(event => this.updatePorts(event.newState.boards, AttachedBoardsChangeEvent.diff(event).detached)), client.onBoardsChanged(event => this.updatePorts(event.newState.ports, AttachedBoardsChangeEvent.diff(event).detached.ports)),
client.onBoardsConfigChanged(({ selectedBoard, selectedPort }) => { client.onBoardsConfigChanged(({ selectedBoard, selectedPort }) => {
this.setState({ selectedBoard, selectedPort }, () => this.fireConfigChanged()); this.setState({ selectedBoard, selectedPort }, () => this.fireConfigChanged());
}) })
@ -101,11 +103,11 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
this.queryBoards({ query }).then(({ searchResults }) => this.setState({ searchResults })); this.queryBoards({ query }).then(({ searchResults }) => this.setState({ searchResults }));
} }
protected updatePorts = (boards: Board[] = [], detachedBoards: Board[] = []) => { protected updatePorts = (ports: Port[] = [], removedPorts: Port[] = []) => {
this.queryPorts(Promise.resolve({ boards })).then(({ knownPorts }) => { this.queryPorts(Promise.resolve({ ports })).then(({ knownPorts }) => {
let { selectedPort } = this.state; let { selectedPort } = this.state;
const removedPorts = detachedBoards.filter(AttachedSerialBoard.is).map(({ port }) => port); // If the currently selected port is not available anymore, unset the selected port.
if (!!selectedPort && removedPorts.indexOf(selectedPort) !== -1) { if (removedPorts.some(port => Port.equals(port, selectedPort))) {
selectedPort = undefined; selectedPort = undefined;
} }
this.setState({ knownPorts, selectedPort }, () => this.fireConfigChanged()); this.setState({ knownPorts, selectedPort }, () => this.fireConfigChanged());
@ -130,18 +132,24 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
return this.props.boardsService.getAttachedBoards(); return this.props.boardsService.getAttachedBoards();
} }
protected queryPorts = (attachedBoards: Promise<{ boards: Board[] }> = this.attachedBoards) => { protected get availablePorts(): Promise<{ ports: Port[] }> {
return new Promise<{ knownPorts: string[] }>(resolve => { return this.props.boardsService.getAvailablePorts();
attachedBoards }
.then(({ boards }) => boards
.filter(AttachedSerialBoard.is) protected queryPorts = (availablePorts: Promise<{ ports: Port[] }> = this.availablePorts) => {
.map(({ port }) => port) return new Promise<{ knownPorts: Port[] }>(resolve => {
.sort()) availablePorts
.then(({ ports }) => ports
.sort(Port.compare))
.then(knownPorts => resolve({ knownPorts })); .then(knownPorts => resolve({ knownPorts }));
}); });
} }
protected selectPort = (selectedPort: string | undefined) => { protected toggleFilterPorts = () => {
this.setState({ showAllPorts: !this.state.showAllPorts });
}
protected selectPort = (selectedPort: Port | undefined) => {
this.setState({ selectedPort }, () => this.fireConfigChanged()); this.setState({ selectedPort }, () => this.fireConfigChanged());
} }
@ -156,17 +164,20 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
render(): React.ReactNode { render(): React.ReactNode {
return <div className='body'> return <div className='body'>
{this.renderContainer('boards', this.renderBoards.bind(this))} {this.renderContainer('boards', this.renderBoards.bind(this))}
{this.renderContainer('ports', this.renderPorts.bind(this))} {this.renderContainer('ports', this.renderPorts.bind(this), this.renderPortsFooter.bind(this))}
</div>; </div>;
} }
protected renderContainer(title: string, contentRenderer: () => React.ReactNode): React.ReactNode { protected renderContainer(title: string, contentRenderer: () => React.ReactNode, footerRenderer?: () => React.ReactNode): React.ReactNode {
return <div className='container'> return <div className='container'>
<div className='content'> <div className='content'>
<div className='title'> <div className='title'>
{title} {title}
</div> </div>
{contentRenderer()} {contentRenderer()}
<div className='footer'>
{(footerRenderer ? footerRenderer() : '')}
</div>
</div> </div>
</div>; </div>;
} }
@ -214,7 +225,9 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
} }
protected renderPorts(): React.ReactNode { protected renderPorts(): React.ReactNode {
return !this.state.knownPorts.length ? const filter = this.state.showAllPorts ? () => true : Port.isBoardPort;
const ports = this.state.knownPorts.filter(filter);
return !ports.length ?
( (
<div className='loading noselect'> <div className='loading noselect'>
No ports discovered No ports discovered
@ -222,17 +235,31 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
) : ) :
( (
<div className='ports list'> <div className='ports list'>
{this.state.knownPorts.map(port => <Item<string> {ports.map(port => <Item<Port>
key={port} key={Port.toString(port)}
item={port} item={port}
label={port} label={Port.toString(port)}
selected={this.state.selectedPort === port} selected={Port.equals(this.state.selectedPort, port)}
onClick={this.selectPort} onClick={this.selectPort}
/>)} />)}
</div> </div>
); );
} }
protected renderPortsFooter(): React.ReactNode {
return <div className='noselect'>
<label
title='Shows all available ports when enabled'>
<input
type='checkbox'
defaultChecked={this.state.showAllPorts}
onChange={this.toggleFilterPorts}
/>
<span>Show all ports</span>
</label>
</div>;
}
} }
export namespace BoardsConfig { export namespace BoardsConfig {
@ -244,7 +271,7 @@ export namespace BoardsConfig {
if (AttachedSerialBoard.is(other)) { if (AttachedSerialBoard.is(other)) {
return !!selectedBoard return !!selectedBoard
&& Board.equals(other, selectedBoard) && Board.equals(other, selectedBoard)
&& selectedPort === other.port; && Port.sameAs(selectedPort, other.port);
} }
return sameAs(config, other); return sameAs(config, other);
} }
@ -260,7 +287,7 @@ export namespace BoardsConfig {
return options.default; return options.default;
} }
const { name } = selectedBoard; const { name } = selectedBoard;
return `${name}${port ? ' at ' + port : ''}`; return `${name}${port ? ' at ' + Port.toString(port) : ''}`;
} }
} }

View File

@ -1,8 +1,10 @@
import { injectable, inject, postConstruct } from 'inversify'; import { injectable, inject, postConstruct } from 'inversify';
import { Emitter, ILogger } from '@theia/core'; import { Emitter } from '@theia/core/lib/common/event';
import { BoardsServiceClient, AttachedBoardsChangeEvent, BoardInstalledEvent, AttachedSerialBoard } from '../../common/protocol/boards-service'; import { ILogger } from '@theia/core/lib/common/logger';
import { LocalStorageService } from '@theia/core/lib/browser/storage-service';
import { RecursiveRequired } from '../../common/types';
import { BoardsServiceClient, AttachedBoardsChangeEvent, BoardInstalledEvent, AttachedSerialBoard, Board, Port } from '../../common/protocol/boards-service';
import { BoardsConfig } from './boards-config'; import { BoardsConfig } from './boards-config';
import { LocalStorageService } from '@theia/core/lib/browser';
@injectable() @injectable()
export class BoardsServiceClientImpl implements BoardsServiceClient { export class BoardsServiceClientImpl implements BoardsServiceClient {
@ -13,10 +15,18 @@ export class BoardsServiceClientImpl implements BoardsServiceClient {
@inject(LocalStorageService) @inject(LocalStorageService)
protected storageService: LocalStorageService; protected storageService: LocalStorageService;
protected readonly onAttachedBoardsChangedEmitter = new Emitter<AttachedBoardsChangeEvent>();
protected readonly onBoardInstalledEmitter = new Emitter<BoardInstalledEvent>(); protected readonly onBoardInstalledEmitter = new Emitter<BoardInstalledEvent>();
protected readonly onAttachedBoardsChangedEmitter = new Emitter<AttachedBoardsChangeEvent>();
protected readonly onSelectedBoardsConfigChangedEmitter = new Emitter<BoardsConfig.Config>(); protected readonly onSelectedBoardsConfigChangedEmitter = new Emitter<BoardsConfig.Config>();
/**
* Used for the auto-reconnecting. Sometimes, the attached board gets disconnected after uploading something to it.
* It happens with certain boards on Windows. For example, the `MKR1000` boards is selected on post `COM5` on Windows,
* perform an upload, the board automatically disconnects and reconnects, but on another port, `COM10`.
* We have to listen on such changes and auto-reconnect the same board on another port.
* See: https://arduino.slack.com/archives/CJJHJCJSJ/p1568645417013000?thread_ts=1568640504.009400&cid=CJJHJCJSJ
*/
protected latestValidBoardsConfig: RecursiveRequired<BoardsConfig.Config> | undefined = undefined;
protected _boardsConfig: BoardsConfig.Config = {}; protected _boardsConfig: BoardsConfig.Config = {};
readonly onBoardsChanged = this.onAttachedBoardsChangedEmitter.event; readonly onBoardsChanged = this.onAttachedBoardsChangedEmitter.event;
@ -29,17 +39,47 @@ export class BoardsServiceClientImpl implements BoardsServiceClient {
} }
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void { notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
this.logger.info('Attached boards changed: ', JSON.stringify(event)); this.logger.info('Attached boards and available ports changed: ', JSON.stringify(event));
const detachedBoards = AttachedBoardsChangeEvent.diff(event).detached.filter(AttachedSerialBoard.is).map(({ port }) => port); const { detached, attached } = AttachedBoardsChangeEvent.diff(event);
const { selectedPort, selectedBoard } = this.boardsConfig; const { selectedPort, selectedBoard } = this.boardsConfig;
this.onAttachedBoardsChangedEmitter.fire(event); this.onAttachedBoardsChangedEmitter.fire(event);
// Dynamically unset the port if the selected board was an attached one and we detached it. // Dynamically unset the port if is not available anymore. A port can be "detached" when removing a board.
if (!!selectedPort && detachedBoards.indexOf(selectedPort) !== -1) { if (detached.ports.some(port => Port.equals(selectedPort, port))) {
this.boardsConfig = { this.boardsConfig = {
selectedBoard, selectedBoard,
selectedPort: undefined selectedPort: undefined
}; };
} }
// Try to reconnect.
this.tryReconnect(attached.boards, attached.ports);
}
async tryReconnect(attachedBoards: Board[], availablePorts: Port[]): Promise<boolean> {
if (this.latestValidBoardsConfig && !this.canUploadTo(this.boardsConfig)) {
for (const board of attachedBoards.filter(AttachedSerialBoard.is)) {
if (this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn
&& this.latestValidBoardsConfig.selectedBoard.name === board.name
&& Port.sameAs(this.latestValidBoardsConfig.selectedPort, board.port)) {
this.boardsConfig = this.latestValidBoardsConfig;
return true;
}
}
// If we could not find an exact match, we compare the board FQBN-name pairs and ignore the port, as it might have changed.
// See documentation on `latestValidBoardsConfig`.
for (const board of attachedBoards.filter(AttachedSerialBoard.is)) {
if (this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn
&& this.latestValidBoardsConfig.selectedBoard.name === board.name) {
this.boardsConfig = {
...this.latestValidBoardsConfig,
selectedPort: availablePorts.find(port => Port.sameAs(port, board.port))
};
return true;
}
}
}
return false;
} }
notifyBoardInstalled(event: BoardInstalledEvent): void { notifyBoardInstalled(event: BoardInstalledEvent): void {
@ -50,6 +90,9 @@ export class BoardsServiceClientImpl implements BoardsServiceClient {
set boardsConfig(config: BoardsConfig.Config) { set boardsConfig(config: BoardsConfig.Config) {
this.logger.info('Board config changed: ', JSON.stringify(config)); this.logger.info('Board config changed: ', JSON.stringify(config));
this._boardsConfig = config; this._boardsConfig = config;
if (this.canUploadTo(this._boardsConfig)) {
this.latestValidBoardsConfig = this._boardsConfig;
}
this.saveState().then(() => this.onSelectedBoardsConfigChangedEmitter.fire(this._boardsConfig)); this.saveState().then(() => this.onSelectedBoardsConfigChangedEmitter.fire(this._boardsConfig));
} }
@ -58,14 +101,22 @@ export class BoardsServiceClientImpl implements BoardsServiceClient {
} }
protected saveState(): Promise<void> { protected saveState(): Promise<void> {
return this.storageService.setData('boards-config', this.boardsConfig); return this.storageService.setData('latest-valid-boards-config', this.latestValidBoardsConfig);
} }
protected async loadState(): Promise<void> { protected async loadState(): Promise<void> {
const boardsConfig = await this.storageService.getData<BoardsConfig.Config>('boards-config'); const storedValidBoardsConfig = await this.storageService.getData<RecursiveRequired<BoardsConfig.Config>>('latest-valid-boards-config');
if (boardsConfig) { if (storedValidBoardsConfig) {
this.boardsConfig = boardsConfig; this.latestValidBoardsConfig = storedValidBoardsConfig;
} }
} }
protected canVerify(config: BoardsConfig.Config | undefined): config is BoardsConfig.Config & { selectedBoard: Board } {
return !!config && !!config.selectedBoard;
}
protected canUploadTo(config: BoardsConfig.Config | undefined): config is RecursiveRequired<BoardsConfig.Config> {
return this.canVerify(config) && !!config.selectedPort && !!config.selectedBoard.fqbn;
}
} }

View File

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import * as ReactDOM from 'react-dom'; import * as ReactDOM from 'react-dom';
import { CommandRegistry, DisposableCollection } from '@theia/core'; import { CommandRegistry, DisposableCollection } from '@theia/core';
import { BoardsService, Board, AttachedSerialBoard } from '../../common/protocol/boards-service'; import { BoardsService, Board, AttachedSerialBoard, Port } from '../../common/protocol/boards-service';
import { ArduinoCommands } from '../arduino-commands'; import { ArduinoCommands } from '../arduino-commands';
import { BoardsServiceClientImpl } from './boards-service-client-impl'; import { BoardsServiceClientImpl } from './boards-service-client-impl';
import { BoardsConfig } from './boards-config'; import { BoardsConfig } from './boards-config';
@ -88,6 +88,7 @@ export namespace BoardsToolBarItem {
export interface State { export interface State {
boardsConfig: BoardsConfig.Config; boardsConfig: BoardsConfig.Config;
attachedBoards: Board[]; attachedBoards: Board[];
availablePorts: Port[];
coords: BoardsDropDownListCoords | 'hidden'; coords: BoardsDropDownListCoords | 'hidden';
} }
} }
@ -104,6 +105,7 @@ export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props,
this.state = { this.state = {
boardsConfig: this.props.boardsServiceClient.boardsConfig, boardsConfig: this.props.boardsServiceClient.boardsConfig,
attachedBoards: [], attachedBoards: [],
availablePorts: [],
coords: 'hidden' coords: 'hidden'
}; };
@ -116,10 +118,13 @@ export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props,
const { boardsServiceClient: client, boardService } = this.props; const { boardsServiceClient: client, boardService } = this.props;
this.toDispose.pushAll([ this.toDispose.pushAll([
client.onBoardsConfigChanged(boardsConfig => this.setState({ boardsConfig })), client.onBoardsConfigChanged(boardsConfig => this.setState({ boardsConfig })),
client.onBoardsChanged(({ newState }) => this.setState({ attachedBoards: newState.boards })) client.onBoardsChanged(({ newState }) => this.setState({ attachedBoards: newState.boards, availablePorts: newState.ports }))
]); ]);
boardService.getAttachedBoards().then(({ boards: attachedBoards }) => { Promise.all([
this.setState({ attachedBoards }) boardService.getAttachedBoards(),
boardService.getAvailablePorts()
]).then(([{boards: attachedBoards}, { ports: availablePorts }]) => {
this.setState({ attachedBoards, availablePorts })
}); });
} }
@ -149,29 +154,32 @@ export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props,
}; };
render(): React.ReactNode { render(): React.ReactNode {
const { boardsConfig, coords, attachedBoards } = this.state; const { boardsConfig, coords, attachedBoards, availablePorts } = this.state;
const boardsConfigText = BoardsConfig.Config.toString(boardsConfig, { default: 'no board selected' }); const title = BoardsConfig.Config.toString(boardsConfig, { default: 'no board selected' });
const configuredBoard = attachedBoards const configuredBoard = attachedBoards
.filter(AttachedSerialBoard.is) .filter(AttachedSerialBoard.is)
.filter(board => availablePorts.some(port => Port.sameAs(port, board.port)))
.filter(board => BoardsConfig.Config.sameAs(boardsConfig, board)).shift(); .filter(board => BoardsConfig.Config.sameAs(boardsConfig, board)).shift();
const items = attachedBoards.filter(AttachedSerialBoard.is).map(board => ({ const items = attachedBoards.filter(AttachedSerialBoard.is).map(board => ({
label: `${board.name} at ${board.port}`, label: `${board.name} at ${board.port}`,
selected: configuredBoard === board, selected: configuredBoard === board,
onClick: () => this.props.boardsServiceClient.boardsConfig = { onClick: () => {
selectedBoard: board, this.props.boardsServiceClient.boardsConfig = {
selectedPort: board.port selectedBoard: board,
selectedPort: availablePorts.find(port => Port.sameAs(port, board.port))
}
} }
})); }));
return <React.Fragment> return <React.Fragment>
<div className='arduino-boards-toolbar-item-container'> <div className='arduino-boards-toolbar-item-container'>
<div className='arduino-boards-toolbar-item' title={boardsConfigText}> <div className='arduino-boards-toolbar-item' title={title}>
<div className='inner-container' onClick={this.show}> <div className='inner-container' onClick={this.show}>
<span className={!configuredBoard ? 'fa fa-times notAttached' : ''}/> <span className={!configuredBoard ? 'fa fa-times notAttached' : ''}/>
<div className='label noWrapInfo'> <div className='label noWrapInfo'>
<div className='noWrapInfo noselect'> <div className='noWrapInfo noselect'>
{boardsConfigText} {title}
</div> </div>
</div> </div>
<span className='fa fa-caret-down caret'/> <span className='fa fa-caret-down caret'/>

View File

@ -1,5 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import debounce = require('lodash.debounce'); import debounce = require('lodash.debounce');
import { Event } from '@theia/core/lib/common/event';
import { Searchable } from '../../../common/protocol/searchable'; import { Searchable } from '../../../common/protocol/searchable';
import { Installable } from '../../../common/protocol/installable'; import { Installable } from '../../../common/protocol/installable';
import { InstallationProgressDialog } from '../installation-progress-dialog'; import { InstallationProgressDialog } from '../installation-progress-dialog';
@ -20,6 +21,7 @@ export class FilterableListContainer<T> extends React.Component<FilterableListCo
componentWillMount(): void { componentWillMount(): void {
this.search = debounce(this.search, 500); this.search = debounce(this.search, 500);
this.handleFilterTextChange(''); this.handleFilterTextChange('');
this.props.filterTextChangeEvent(this.handleFilterTextChange.bind(this));
} }
render(): React.ReactNode { render(): React.ReactNode {
@ -57,8 +59,8 @@ export class FilterableListContainer<T> extends React.Component<FilterableListCo
this.setState({ filterText }); this.setState({ filterText });
this.search(filterText); this.search(filterText);
} }
protected search (query: string): void { protected search(query: string): void {
const { searchable } = this.props; const { searchable } = this.props;
searchable.search({ query: query.trim() }).then(result => { searchable.search({ query: query.trim() }).then(result => {
const { items } = result; const { items } = result;
@ -97,6 +99,7 @@ export namespace FilterableListContainer {
readonly itemRenderer: ListItemRenderer<T>; readonly itemRenderer: ListItemRenderer<T>;
readonly resolveContainer: (element: HTMLElement) => void; readonly resolveContainer: (element: HTMLElement) => void;
readonly resolveFocus: (element: HTMLElement | undefined) => void; readonly resolveFocus: (element: HTMLElement | undefined) => void;
readonly filterTextChangeEvent: Event<string>;
} }
export interface State<T> { export interface State<T> {

View File

@ -2,6 +2,7 @@ import * as React from 'react';
import { injectable, postConstruct } from 'inversify'; import { injectable, postConstruct } from 'inversify';
import { Message } from '@phosphor/messaging'; import { Message } from '@phosphor/messaging';
import { Deferred } from '@theia/core/lib/common/promise-util'; import { Deferred } from '@theia/core/lib/common/promise-util';
import { Emitter } from '@theia/core/lib/common/event';
import { MaybePromise } from '@theia/core/lib/common/types'; import { MaybePromise } from '@theia/core/lib/common/types';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget'; import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import { Installable } from '../../../common/protocol/installable'; import { Installable } from '../../../common/protocol/installable';
@ -17,6 +18,7 @@ export abstract class ListWidget<T> extends ReactWidget {
*/ */
protected focusNode: HTMLElement | undefined; protected focusNode: HTMLElement | undefined;
protected readonly deferredContainer = new Deferred<HTMLElement>(); protected readonly deferredContainer = new Deferred<HTMLElement>();
protected readonly filterTextChangeEmitter = new Emitter<string>();
constructor(protected options: ListWidget.Options<T>) { constructor(protected options: ListWidget.Options<T>) {
super(); super();
@ -31,6 +33,7 @@ export abstract class ListWidget<T> extends ReactWidget {
this.scrollOptions = { this.scrollOptions = {
suppressScrollX: true suppressScrollX: true
} }
this.toDispose.push(this.filterTextChangeEmitter);
} }
@postConstruct() @postConstruct()
@ -63,7 +66,12 @@ export abstract class ListWidget<T> extends ReactWidget {
searchable={this.options.searchable} searchable={this.options.searchable}
installable={this.options.installable} installable={this.options.installable}
itemLabel={this.options.itemLabel} itemLabel={this.options.itemLabel}
itemRenderer={this.options.itemRenderer} />; itemRenderer={this.options.itemRenderer}
filterTextChangeEvent={this.filterTextChangeEmitter.event}/>;
}
refresh(filterText: string): void {
this.deferredContainer.promise.then(() => this.filterTextChangeEmitter.fire(filterText));
} }
} }

View File

@ -0,0 +1,25 @@
import { injectable, inject, postConstruct } from 'inversify';
import { AboutDialog, ABOUT_CONTENT_CLASS } from '@theia/core/lib/browser/about-dialog';
import { ConfigService } from '../../common/protocol/config-service';
@injectable()
export class ArduinoAboutDialog extends AboutDialog {
@inject(ConfigService)
protected readonly configService: ConfigService;
@postConstruct()
protected async init(): Promise<void> {
const [, version] = await Promise.all([super.init(), this.configService.getVersion()]);
if (version) {
const { firstChild } = this.contentNode;
if (firstChild instanceof HTMLElement && firstChild.classList.contains(ABOUT_CONTENT_CLASS)) {
const cliVersion = document.createElement('div');
cliVersion.textContent = version;
firstChild.appendChild(cliVersion);
// TODO: anchor to the commit in the `arduino-cli` repository.
}
}
}
}

View File

@ -1,24 +1,38 @@
import { injectable, inject } from 'inversify'; import { injectable, inject } from 'inversify';
import { FileSystem } from '@theia/filesystem/lib/common'; import { FileSystem } from '@theia/filesystem/lib/common/filesystem';
import { FrontendApplication } from '@theia/core/lib/browser'; import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { ArduinoFrontendContribution } from '../arduino-frontend-contribution'; import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { ArduinoFrontendContribution, ArduinoAdvancedMode } from '../arduino-frontend-contribution';
@injectable() @injectable()
export class ArduinoFrontendApplication extends FrontendApplication { export class ArduinoFrontendApplication extends FrontendApplication {
@inject(ArduinoFrontendContribution)
protected readonly frontendContribution: ArduinoFrontendContribution;
@inject(FileSystem) @inject(FileSystem)
protected readonly fileSystem: FileSystem; protected readonly fileSystem: FileSystem;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(ArduinoFrontendContribution)
protected readonly frontendContribution: ArduinoFrontendContribution;
protected async initializeLayout(): Promise<void> { protected async initializeLayout(): Promise<void> {
await super.initializeLayout(); super.initializeLayout().then(() => {
const location = new URL(window.location.href); // If not in PRO mode, we open the sketch file with all the related files.
const sketchPath = location.searchParams.get('sketch'); // Otherwise, we reuse the workbench's restore functionality and we do not open anything at all.
if (sketchPath && await this.fileSystem.exists(sketchPath)) { // TODO: check `otherwise`. Also, what if we check for opened editors, instead of blindly opening them?
this.frontendContribution.openSketchFiles(decodeURIComponent(sketchPath)); if (!ArduinoAdvancedMode.TOGGLED) {
} this.workspaceService.roots.then(roots => {
for (const root of roots) {
this.fileSystem.exists(root.uri).then(exists => {
if (exists) {
this.frontendContribution.openSketchFiles(root.uri);
}
});
}
});
}
});
} }
} }

View File

@ -268,7 +268,7 @@ export class MonitorWidget extends ReactWidget implements StatefulWidget {
return { return {
baudRate, baudRate,
board: selectedBoard, board: selectedBoard,
port: selectedPort port: selectedPort.address
} }
} }

View File

@ -73,11 +73,18 @@ div#select-board-dialog .selectBoardContainer .body .list .item.selected i{
text-transform: uppercase; text-transform: uppercase;
} }
#select-board-dialog .selectBoardContainer .body .container .content .footer {
padding: 10px 5px 10px 0px;
}
#select-board-dialog .selectBoardContainer .body .container .content .loading { #select-board-dialog .selectBoardContainer .body .container .content .loading {
font-size: var(--theia-ui-font-size1); font-size: var(--theia-ui-font-size1);
color: #7f8c8d; color: #7f8c8d;
padding: 10px 5px 10px 10px; padding: 10px 5px 10px 10px;
text-transform: uppercase; text-transform: uppercase;
/* The max, min-height comes from `.body .list` 265px + 47px top padding - 2 * 10px top padding */
max-height: 292px;
min-height: 292px;
} }
#select-board-dialog .selectBoardContainer .body .list .item { #select-board-dialog .selectBoardContainer .body .list .item {
@ -209,6 +216,7 @@ button.theia-button.main {
.arduino-boards-dropdown-item .fa-check { .arduino-boards-dropdown-item .fa-check {
color: var(--theia-accent-color1); color: var(--theia-accent-color1);
align-self: center;
} }
.arduino-boards-dropdown-item.selected, .arduino-boards-dropdown-item.selected,

View File

@ -1,23 +1,42 @@
import { JsonRpcServer } from '@theia/core'; import { isWindows, isOSX } from '@theia/core/lib/common/os';
import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory';
import { Searchable } from './searchable'; import { Searchable } from './searchable';
import { Installable } from './installable'; import { Installable } from './installable';
import { ArduinoComponent } from './arduino-component'; import { ArduinoComponent } from './arduino-component';
const naturalCompare: (left: string, right: string) => number = require('string-natural-compare').caseInsensitive;
export interface AttachedBoardsChangeEvent { export interface AttachedBoardsChangeEvent {
readonly oldState: Readonly<{ boards: Board[] }>; readonly oldState: Readonly<{ boards: Board[], ports: Port[] }>;
readonly newState: Readonly<{ boards: Board[] }>; readonly newState: Readonly<{ boards: Board[], ports: Port[] }>;
} }
export namespace AttachedBoardsChangeEvent { export namespace AttachedBoardsChangeEvent {
export function diff(event: AttachedBoardsChangeEvent): Readonly<{ attached: Board[], detached: Board[] }> { export function diff(event: AttachedBoardsChangeEvent): Readonly<{
attached: {
boards: Board[],
ports: Port[]
},
detached: {
boards: Board[],
ports: Port[]
}
}> {
const diff = <T>(left: T[], right: T[]) => { const diff = <T>(left: T[], right: T[]) => {
return left.filter(item => right.indexOf(item) === -1); return left.filter(item => right.indexOf(item) === -1);
} }
const { boards: newBoards } = event.newState; const { boards: newBoards } = event.newState;
const { boards: oldBoards } = event.oldState; const { boards: oldBoards } = event.oldState;
const { ports: newPorts } = event.newState;
const { ports: oldPorts } = event.oldState;
return { return {
detached: diff(oldBoards, newBoards), detached: {
attached: diff(newBoards, oldBoards) boards: diff(oldBoards, newBoards),
ports: diff(oldPorts, newPorts)
},
attached: {
boards: diff(newBoards, oldBoards),
ports: diff(newPorts, oldPorts)
}
}; };
} }
@ -37,6 +56,114 @@ export const BoardsServicePath = '/services/boards-service';
export const BoardsService = Symbol('BoardsService'); export const BoardsService = Symbol('BoardsService');
export interface BoardsService extends Installable<BoardPackage>, Searchable<BoardPackage>, JsonRpcServer<BoardsServiceClient> { export interface BoardsService extends Installable<BoardPackage>, Searchable<BoardPackage>, JsonRpcServer<BoardsServiceClient> {
getAttachedBoards(): Promise<{ boards: Board[] }>; getAttachedBoards(): Promise<{ boards: Board[] }>;
getAvailablePorts(): Promise<{ ports: Port[] }>;
}
export interface Port {
readonly address: string;
readonly protocol: Port.Protocol;
/**
* Optional label for the protocol. For example: `Serial Port (USB)`.
*/
readonly label?: string;
}
export namespace Port {
export type Protocol = 'serial' | 'network' | 'unknown';
export namespace Protocol {
export function toProtocol(protocol: string | undefined): Protocol {
if (protocol === 'serial') {
return 'serial';
} else if (protocol === 'network') {
return 'network';
} else {
return 'unknown';
}
}
}
export function toString(port: Port, options: { useLabel: boolean } = { useLabel: false }): string {
if (options.useLabel && port.label) {
return `${port.address} ${port.label}`
}
return port.address;
}
export function compare(left: Port, right: Port): number {
// Board ports have higher priorities, they come first.
if (isBoardPort(left) && !isBoardPort(right)) {
return -1;
}
if (!isBoardPort(left) && isBoardPort(right)) {
return 1;
}
let result = left.protocol.toLocaleLowerCase().localeCompare(right.protocol.toLocaleLowerCase());
if (result !== 0) {
return result;
}
result = naturalCompare(left.address, right.address);
if (result !== 0) {
return result;
}
return (left.label || '').localeCompare(right.label || '');
}
export function equals(left: Port | undefined, right: Port | undefined): boolean {
if (left && right) {
return left.address === right.address
&& left.protocol === right.protocol
&& (left.label || '') === (right.label || '');
}
return left === right;
}
// Based on: https://github.com/arduino/Arduino/blob/93581b03d723e55c60caedb4729ffc6ea808fe78/arduino-core/src/processing/app/SerialPortList.java#L48-L74
export function isBoardPort(port: Port): boolean {
const address = port.address.toLocaleLowerCase();
if (isWindows) {
// `COM1` seems to be the default serial port on Windows.
return address !== 'COM1'.toLocaleLowerCase();
}
// On macOS and Linux, the port should start with `/dev/`.
if (!address.startsWith('/dev/')) {
return false
}
if (isOSX) {
// Example: `/dev/cu.usbmodem14401`
if (/(tty|cu)\..*/.test(address.substring('/dev/'.length))) {
return [
'/dev/cu.MALS',
'/dev/cu.SOC',
'/dev/cu.Bluetooth-Incoming-Port'
].map(a => a.toLocaleLowerCase()).every(a => a !== address);
}
}
// Example: `/dev/ttyACM0`
if (/(ttyS|ttyUSB|ttyACM|ttyAMA|rfcomm|ttyO)[0-9]{1,3}/.test(address.substring('/dev/'.length))) {
// Default ports were `/dev/ttyS0` -> `/dev/ttyS31` on Ubuntu 16.04.2.
if (address.startsWith('/dev/ttyS')) {
const index = Number.parseInt(address.substring('/dev/ttyS'.length), 10);
if (!Number.isNaN(index) && 0 <= index && 31 >= index) {
return false;
}
}
return true;
}
return false;
}
export function sameAs(left: Port | undefined, right: string | undefined) {
if (left && right) {
if (left.protocol !== 'serial') {
console.log(`Unexpected protocol for port: ${JSON.stringify(left)}. Ignoring protocol, comparing addresses with ${right}.`);
}
return left.address === right;
}
return false;
}
} }
export interface BoardPackage extends ArduinoComponent { export interface BoardPackage extends ArduinoComponent {
@ -59,6 +186,17 @@ export namespace Board {
return left.name === right.name && left.fqbn === right.fqbn; return left.name === right.name && left.fqbn === right.fqbn;
} }
export function sameAs(left: Board, right: string | Board): boolean {
// How to associate a selected board with one of the available cores: https://typefox.slack.com/archives/CJJHJCJSJ/p1571142327059200
// 1. How to use the FQBN if any and infer the package ID from it: https://typefox.slack.com/archives/CJJHJCJSJ/p1571147549069100
// 2. How to trim the `/Genuino` from the name: https://arduino.slack.com/archives/CJJHJCJSJ/p1571146951066800?thread_ts=1571142327.059200&cid=CJJHJCJSJ
const other = typeof right === 'string' ? { name: right } : right;
if (left.fqbn && other.fqbn) {
return left.fqbn === other.fqbn;
}
return left.name.replace('/Genuino', '') === other.name.replace('/Genuino', '');
}
export function compare(left: Board, right: Board): number { export function compare(left: Board, right: Board): number {
let result = left.name.localeCompare(right.name); let result = left.name.localeCompare(right.name);
if (result === 0) { if (result === 0) {

View File

@ -2,6 +2,7 @@ export const ConfigServicePath = '/services/config-service';
export const ConfigService = Symbol('ConfigService'); export const ConfigService = Symbol('ConfigService');
export interface ConfigService { export interface ConfigService {
getVersion(): Promise<string>;
getConfiguration(): Promise<Config>; getConfiguration(): Promise<Config>;
isInDataDir(uri: string): Promise<boolean>; isInDataDir(uri: string): Promise<boolean>;
isInSketchDir(uri: string): Promise<boolean>; isInSketchDir(uri: string): Promise<boolean>;

View File

@ -3,11 +3,15 @@ export const SketchesService = Symbol('SketchesService');
export interface SketchesService { export interface SketchesService {
/** /**
* Returns with the direct sketch folders from the location of the `fileStat`. * Returns with the direct sketch folders from the location of the `fileStat`.
* The sketches returns with inverchronological order, the first item is the most recent one. * The sketches returns with inverse-chronological order, the first item is the most recent one.
*/ */
getSketches(uri?: string): Promise<Sketch[]> getSketches(uri?: string): Promise<Sketch[]>
getSketchFiles(uri: string): Promise<string[]> getSketchFiles(uri: string): Promise<string[]>
createNewSketch(parentUri: string): Promise<Sketch> /**
* Creates a new sketch folder in the `parentUri` location. If `parentUri` is not specified,
* it falls back to the default `sketchDirUri` from the CLI.
*/
createNewSketch(parentUri?: string): Promise<Sketch>
isSketchFolder(uri: string): Promise<boolean> isSketchFolder(uri: string): Promise<boolean>
} }

View File

@ -0,0 +1,3 @@
export type RecursiveRequired<T> = {
[P in keyof T]-?: RecursiveRequired<T[P]>;
};

View File

@ -133,7 +133,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
return parentLogger.child('discovery'); return parentLogger.child('discovery');
}).inSingletonScope().whenTargetNamed('discovery'); }).inSingletonScope().whenTargetNamed('discovery');
// Default workspace server extension to initialize and use a fallback workspace (`~/Arduino-PoC/workspace/`) // Default workspace server extension to initialize and use a fallback workspace.
// If nothing was set previously. // If nothing was set previously.
bind(DefaultWorkspaceServerExt).toSelf().inSingletonScope(); bind(DefaultWorkspaceServerExt).toSelf().inSingletonScope();
rebind(WorkspaceServer).toService(DefaultWorkspaceServerExt); rebind(WorkspaceServer).toService(DefaultWorkspaceServerExt);

View File

@ -1,6 +1,6 @@
import * as os from 'os'; import * as os from 'os';
import * as which from 'which'; import * as which from 'which';
import * as cp from 'child_process'; import { spawn } from 'child_process';
import { join, delimiter } from 'path'; import { join, delimiter } from 'path';
import { injectable, inject } from 'inversify'; import { injectable, inject } from 'inversify';
import { ILogger } from '@theia/core'; import { ILogger } from '@theia/core';
@ -26,40 +26,74 @@ export class ArduinoCli {
}); });
} }
async getVersion(): Promise<string> {
const execPath = await this.getExecPath();
return this.spawn(`"${execPath}"`, ['version']);
return new Promise<string>((resolve, reject) => {
const buffers: Buffer[] = [];
const cp = spawn(`"${execPath}"`, ['version'], { windowsHide: true, shell: true });
cp.stdout.on('data', (b: Buffer) => buffers.push(b));
cp.on('error', error => reject(error));
cp.on('exit', (code, signal) => {
if (code === 0) {
const result = Buffer.concat(buffers).toString('utf8').trim()
resolve(result);
return;
}
if (signal) {
reject(new Error(`Process exited with signal: ${signal}`));
return;
}
if (code) {
reject(new Error(`Process exited with exit code: ${code}`));
return;
}
});
});
}
async getDefaultConfig(): Promise<Config> { async getDefaultConfig(): Promise<Config> {
const command = await this.getExecPath(); const execPath = await this.getExecPath();
return new Promise<Config>((resolve, reject) => { const result = await this.spawn(`"${execPath}"`, ['config', 'dump', '--format', 'json']);
cp.execFile( const { sketchbook_path, arduino_data } = JSON.parse(result);
command, if (!sketchbook_path) {
['config', 'dump', '--format', 'json'], throw new Error(`Could not parse config. 'sketchbook_path' was missing from: ${result}`);
{ encoding: 'utf8' }, }
(error, stdout, stderr) => { if (!arduino_data) {
throw new Error(`Could not parse config. 'arduino_data' was missing from: ${result}`);
}
return {
sketchDirUri: FileUri.create(sketchbook_path).toString(),
dataDirUri: FileUri.create(arduino_data).toString()
};
}
if (error) { private spawn(command: string, args?: string[]): Promise<string> {
throw error; return new Promise<string>((resolve, reject) => {
} const buffers: Buffer[] = [];
const cp = spawn(command, args, { windowsHide: true, shell: true });
if (stderr) { cp.stdout.on('data', (b: Buffer) => buffers.push(b));
throw new Error(stderr); cp.on('error', error => {
} this.logger.error(`Error executing ${command} with args: ${JSON.stringify(args)}.`, error);
reject(error);
const { sketchbook_path, arduino_data } = JSON.parse(stdout.trim()); });
cp.on('exit', (code, signal) => {
if (!sketchbook_path) { if (code === 0) {
reject(new Error(`Could not parse config. 'sketchbook_path' was missing from: ${stdout}`)); const result = Buffer.concat(buffers).toString('utf8').trim()
return; resolve(result);
} return;
}
if (!arduino_data) { if (signal) {
reject(new Error(`Could not parse config. 'arduino_data' was missing from: ${stdout}`)); this.logger.error(`Unexpected signal '${signal}' when executing ${command} with args: ${JSON.stringify(args)}.`);
return; reject(new Error(`Process exited with signal: ${signal}`));
} return;
}
resolve({ if (code) {
sketchDirUri: FileUri.create(sketchbook_path).toString(), this.logger.error(`Unexpected exit code '${code}' when executing ${command} with args: ${JSON.stringify(args)}.`);
dataDirUri: FileUri.create(arduino_data).toString() reject(new Error(`Process exited with exit code: ${code}`));
}); return;
}); }
});
}); });
} }

View File

@ -32,8 +32,9 @@ export class ArduinoDaemon implements BackendApplicationContribution {
try { try {
if (!this.cliContribution.debugCli) { if (!this.cliContribution.debugCli) {
const executable = await this.cli.getExecPath(); const executable = await this.cli.getExecPath();
this.logger.info(`>>> Starting 'arduino-cli' daemon... [${executable}]`); const version = await this.cli.getVersion();
const daemon = exec(`${executable} daemon -v --log-level info --format json --log-format json`, this.logger.info(`>>> Starting ${version.toLocaleLowerCase()} daemon from ${executable}...`);
const daemon = exec(`"${executable}" daemon -v --log-level info --format json --log-format json`,
{ encoding: 'utf8', maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => { { encoding: 'utf8', maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
if (err || stderr) { if (err || stderr) {
console.log(err || new Error(stderr)); console.log(err || new Error(stderr));

View File

@ -1,7 +1,7 @@
import * as PQueue from 'p-queue'; import * as PQueue from 'p-queue';
import { injectable, inject, postConstruct, named } from 'inversify'; import { injectable, inject, postConstruct, named } from 'inversify';
import { ILogger } from '@theia/core/lib/common/logger'; import { ILogger } from '@theia/core/lib/common/logger';
import { BoardsService, AttachedSerialBoard, BoardPackage, Board, AttachedNetworkBoard, BoardsServiceClient } from '../common/protocol/boards-service'; import { BoardsService, AttachedSerialBoard, BoardPackage, Board, AttachedNetworkBoard, BoardsServiceClient, Port } from '../common/protocol/boards-service';
import { PlatformSearchReq, PlatformSearchResp, PlatformInstallReq, PlatformInstallResp, PlatformListReq, PlatformListResp } from './cli-protocol/commands/core_pb'; import { PlatformSearchReq, PlatformSearchResp, PlatformInstallReq, PlatformInstallResp, PlatformListReq, PlatformListResp } from './cli-protocol/commands/core_pb';
import { CoreClientProvider } from './core-client-provider'; import { CoreClientProvider } from './core-client-provider';
import { BoardListReq, BoardListResp } from './cli-protocol/commands/board_pb'; import { BoardListReq, BoardListResp } from './cli-protocol/commands/board_pb';
@ -20,7 +20,6 @@ export class BoardsServiceImpl implements BoardsService {
@inject(ToolOutputServiceServer) @inject(ToolOutputServiceServer)
protected readonly toolOutputService: ToolOutputServiceServer; protected readonly toolOutputService: ToolOutputServiceServer;
protected selectedBoard: Board | undefined;
protected discoveryInitialized = false; protected discoveryInitialized = false;
protected discoveryTimer: NodeJS.Timeout | undefined; protected discoveryTimer: NodeJS.Timeout | undefined;
/** /**
@ -29,44 +28,58 @@ export class BoardsServiceImpl implements BoardsService {
* This state is updated via periodical polls. * This state is updated via periodical polls.
*/ */
protected _attachedBoards: { boards: Board[] } = { boards: [] }; protected _attachedBoards: { boards: Board[] } = { boards: [] };
protected _availablePorts: { ports: Port[] } = { ports: [] };
protected client: BoardsServiceClient | undefined; protected client: BoardsServiceClient | undefined;
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 }); protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
@postConstruct() @postConstruct()
protected async init(): Promise<void> { protected async init(): Promise<void> {
this.discoveryTimer = setInterval(() => { this.discoveryTimer = setInterval(() => {
this.discoveryLogger.trace('Discovering attached boards...'); this.discoveryLogger.trace('Discovering attached boards and available ports...');
this.doGetAttachedBoards().then(({ boards }) => { this.doGetAttachedBoardsAndAvailablePorts().then(({ boards, ports }) => {
const update = (oldState: Board[], newState: Board[], message: string) => { const update = (oldBoards: Board[], newBoards: Board[], oldPorts: Port[], newPorts: Port[], message: string) => {
this._attachedBoards = { boards: newState }; this._attachedBoards = { boards: newBoards };
this.discoveryLogger.info(`${message} - Discovered boards: ${JSON.stringify(newState)}`); this._availablePorts = { ports: newPorts };
this.discoveryLogger.info(`${message} - Discovered boards: ${JSON.stringify(newBoards)} and available ports: ${JSON.stringify(newPorts)}`);
if (this.client) { if (this.client) {
this.client.notifyAttachedBoardsChanged({ this.client.notifyAttachedBoardsChanged({
oldState: { oldState: {
boards: oldState boards: oldBoards,
ports: oldPorts
}, },
newState: { newState: {
boards: newState boards: newBoards,
ports: newPorts
} }
}); });
} }
} }
const sortedBoards = boards.sort(Board.compare); const sortedBoards = boards.sort(Board.compare);
this.discoveryLogger.trace(`Discovery done. ${JSON.stringify(sortedBoards)}`); const sortedPorts = ports.sort(Port.compare);
this.discoveryLogger.trace(`Discovery done. Boards: ${JSON.stringify(sortedBoards)}. Ports: ${sortedPorts}`);
if (!this.discoveryInitialized) { if (!this.discoveryInitialized) {
update([], sortedBoards, 'Initialized attached boards.'); update([], sortedBoards, [], sortedPorts, 'Initialized attached boards and available ports.');
this.discoveryInitialized = true; this.discoveryInitialized = true;
} else { } else {
this.getAttachedBoards().then(({ boards: currentBoards }) => { Promise.all([
this.getAttachedBoards(),
this.getAvailablePorts()
]).then(([{ boards: currentBoards }, { ports: currentPorts }]) => {
this.discoveryLogger.trace(`Updating discovered boards... ${JSON.stringify(currentBoards)}`); this.discoveryLogger.trace(`Updating discovered boards... ${JSON.stringify(currentBoards)}`);
if (currentBoards.length !== sortedBoards.length) { if (currentBoards.length !== sortedBoards.length || currentPorts.length !== sortedPorts.length) {
update(currentBoards, sortedBoards, 'Updated discovered boards.'); update(currentBoards, sortedBoards, currentPorts, sortedPorts, 'Updated discovered boards and available ports.');
return; return;
} }
// `currentBoards` is already sorted. // `currentBoards` is already sorted.
for (let i = 0; i < sortedBoards.length; i++) { for (let i = 0; i < sortedBoards.length; i++) {
if (Board.compare(sortedBoards[i], currentBoards[i]) !== 0) { if (Board.compare(sortedBoards[i], currentBoards[i]) !== 0) {
update(currentBoards, sortedBoards, 'Updated discovered boards.'); update(currentBoards, sortedBoards, currentPorts, sortedPorts, 'Updated discovered boards.');
return;
}
}
for (let i = 0; i < sortedPorts.length; i++) {
if (Port.compare(sortedPorts[i], currentPorts[i]) !== 0) {
update(currentBoards, sortedBoards, currentPorts, sortedPorts, 'Updated discovered boards.');
return; return;
} }
} }
@ -91,13 +104,18 @@ export class BoardsServiceImpl implements BoardsService {
return this._attachedBoards; return this._attachedBoards;
} }
private async doGetAttachedBoards(): Promise<{ boards: Board[] }> { async getAvailablePorts(): Promise<{ ports: Port[] }> {
return this._availablePorts;
}
private async doGetAttachedBoardsAndAvailablePorts(): Promise<{ boards: Board[], ports: Port[] }> {
return this.queue.add(() => { return this.queue.add(() => {
return new Promise<{ boards: Board[] }>(async resolve => { return new Promise<{ boards: Board[], ports: Port[] }>(async resolve => {
const coreClient = await this.coreClientProvider.getClient(); const coreClient = await this.coreClientProvider.getClient();
const boards: Board[] = []; const boards: Board[] = [];
const ports: Port[] = [];
if (!coreClient) { if (!coreClient) {
resolve({ boards }); resolve({ boards, ports });
return; return;
} }
@ -105,10 +123,43 @@ export class BoardsServiceImpl implements BoardsService {
const req = new BoardListReq(); const req = new BoardListReq();
req.setInstance(instance); req.setInstance(instance);
const resp = await new Promise<BoardListResp>((resolve, reject) => client.boardList(req, (err, resp) => (!!err ? reject : resolve)(!!err ? err : resp))); const resp = await new Promise<BoardListResp>((resolve, reject) => client.boardList(req, (err, resp) => (!!err ? reject : resolve)(!!err ? err : resp)));
for (const portsList of resp.getPortsList()) { const portsList = resp.getPortsList();
const protocol = portsList.getProtocol(); // TODO: remove unknown board mocking!
const address = portsList.getAddress(); // You also have to manually import `DetectedPort`.
for (const board of portsList.getBoardsList()) { // const unknownPortList = new DetectedPort();
// unknownPortList.setAddress(platform() === 'win32' ? 'COM3' : platform() === 'darwin' ? '/dev/cu.usbmodem94401' : '/dev/ttyACM0');
// unknownPortList.setProtocol('serial');
// unknownPortList.setProtocolLabel('Serial Port (USB)');
// portsList.push(unknownPortList);
for (const portList of portsList) {
const protocol = Port.Protocol.toProtocol(portList.getProtocol());
const address = portList.getAddress();
// Available ports can exist with unknown attached boards.
// The `BoardListResp` looks like this for a known attached board:
// [
// {
// "address": "COM10",
// "protocol": "serial",
// "protocol_label": "Serial Port (USB)",
// "boards": [
// {
// "name": "Arduino MKR1000",
// "FQBN": "arduino:samd:mkr1000"
// }
// ]
// }
// ]
// And the `BoardListResp` looks like this for an unknown board:
// [
// {
// "address": "COM9",
// "protocol": "serial",
// "protocol_label": "Serial Port (USB)",
// }
// ]
ports.push({ protocol, address });
for (const board of portList.getBoardsList()) {
const name = board.getName() || 'unknown'; const name = board.getName() || 'unknown';
const fqbn = board.getFqbn(); const fqbn = board.getFqbn();
const port = address; const port = address;
@ -118,13 +169,15 @@ export class BoardsServiceImpl implements BoardsService {
fqbn, fqbn,
port port
}); });
} else { // We assume, it is a `network` board. } else if (protocol === 'network') { // We assume, it is a `network` board.
boards.push(<AttachedNetworkBoard>{ boards.push(<AttachedNetworkBoard>{
name, name,
fqbn, fqbn,
address, address,
port port
}); });
} else {
console.warn(`Unknown protocol for port: ${address}.`);
} }
} }
} }
@ -133,7 +186,7 @@ export class BoardsServiceImpl implements BoardsService {
// <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.usbmodem14201' },
// <AttachedSerialBoard>{ name: 'Arduino/Genuino Uno', fqbn: 'arduino:avr:uno', port: '/dev/cu.usbmodem142xx' }, // <AttachedSerialBoard>{ name: 'Arduino/Genuino Uno', fqbn: 'arduino:avr:uno', port: '/dev/cu.usbmodem142xx' },
// ]); // ]);
resolve({ boards }); resolve({ boards, ports });
}) })
}); });
} }

View File

@ -1,5 +1,8 @@
import { injectable, inject } from 'inversify'; import { mkdirpSync, existsSync } from 'fs-extra';
import { injectable, inject, postConstruct } from 'inversify';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { FileUri } from '@theia/core/lib/node/file-uri';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { ConfigService, Config } from '../common/protocol/config-service'; import { ConfigService, Config } from '../common/protocol/config-service';
import { ArduinoCli } from './arduino-cli'; import { ArduinoCli } from './arduino-cli';
@ -8,9 +11,28 @@ export class ConfigServiceImpl implements ConfigService {
@inject(ArduinoCli) @inject(ArduinoCli)
protected readonly cli: ArduinoCli; protected readonly cli: ArduinoCli;
protected readonly config: Deferred<Config> = new Deferred();
@postConstruct()
protected init(): void {
this.cli.getDefaultConfig().then(config => {
const { dataDirUri, sketchDirUri } = config;
for (const uri of [dataDirUri, sketchDirUri]) {
const path = FileUri.fsPath(uri);
if (!existsSync(path)) {
mkdirpSync(path);
}
}
this.config.resolve(config);
});
}
async getConfiguration(): Promise<Config> { async getConfiguration(): Promise<Config> {
return this.cli.getDefaultConfig(); return this.config.promise;
}
async getVersion(): Promise<string> {
return this.cli.getVersion();
} }
async isInDataDir(uri: string): Promise<boolean> { async isInDataDir(uri: string): Promise<boolean> {

View File

@ -108,14 +108,8 @@ export class CoreClientProviderImpl implements CoreClientProvider {
const initResp = await new Promise<InitResp>(resolve => { const initResp = await new Promise<InitResp>(resolve => {
let resp: InitResp | undefined = undefined; let resp: InitResp | undefined = undefined;
const stream = client.init(initReq); const stream = client.init(initReq);
stream.on('data', (data: InitResp) => { stream.on('data', (data: InitResp) => resp = data);
if (!resp) { stream.on('end', () => resolve(resp));
resp = data;
}
})
stream.on('end', () => {
resolve(resp);
})
}); });
const instance = initResp.getInstance(); const instance = initResp.getInstance();

View File

@ -83,7 +83,9 @@ export namespace DaemonLog {
export function log(logger: ILogger, logMessages: string): void { export function log(logger: ILogger, logMessages: string): void {
const parsed = parse(logMessages); const parsed = parse(logMessages);
for (const log of parsed) { for (const log of parsed) {
logger.log(toLogLevel(log), toMessage(log)); const logLevel = toLogLevel(log);
const message = toMessage(log, { omitLogLevel: true });
logger.log(logLevel, message);
} }
} }
@ -109,12 +111,13 @@ export namespace DaemonLog {
export function toPrettyString(logMessages: string): string { export function toPrettyString(logMessages: string): string {
const parsed = parse(logMessages); const parsed = parse(logMessages);
return parsed.map(toMessage).join('\n') + '\n'; return parsed.map(log => toMessage(log)).join('\n') + '\n';
} }
function toMessage(log: DaemonLog): string { function toMessage(log: DaemonLog, options: { omitLogLevel: boolean } = { omitLogLevel: false }): string {
const details = Object.keys(log).filter(key => key !== 'msg' && key !== 'level' && key !== 'time').map(key => toDetails(log, key)).join(', '); const details = Object.keys(log).filter(key => key !== 'msg' && key !== 'level' && key !== 'time').map(key => toDetails(log, key)).join(', ');
return `[${log.level.toUpperCase()}] ${log.msg}${!!details ? ` [${details}]` : ''}` const logLevel = options.omitLogLevel ? '' : `[${log.level.toUpperCase()}] `;
return `${logLevel}${log.msg}${!!details ? ` [${details}]` : ''}`
} }
function toDetails(log: DaemonLog, key: string): string { function toDetails(log: DaemonLog, key: string): string {

View File

@ -16,7 +16,19 @@ export class SketchesServiceImpl implements SketchesService {
async getSketches(uri?: string): Promise<Sketch[]> { async getSketches(uri?: string): Promise<Sketch[]> {
const sketches: Array<Sketch & { mtimeMs: number }> = []; const sketches: Array<Sketch & { mtimeMs: number }> = [];
const fsPath = FileUri.fsPath(uri ? uri : (await this.configService.getConfiguration()).sketchDirUri); let fsPath: undefined | string;
if (!uri) {
const { sketchDirUri } = (await this.configService.getConfiguration());
fsPath = FileUri.fsPath(sketchDirUri);
if (!fs.existsSync(fsPath)) {
fs.mkdirpSync(fsPath);
}
} else {
fsPath = FileUri.fsPath(uri);
}
if (!fs.existsSync(fsPath)) {
return [];
}
const fileNames = fs.readdirSync(fsPath); const fileNames = fs.readdirSync(fsPath);
for (const fileName of fileNames) { for (const fileName of fileNames) {
const filePath = path.join(fsPath, fileName); const filePath = path.join(fsPath, fileName);
@ -56,12 +68,13 @@ export class SketchesServiceImpl implements SketchesService {
return this.getSketchFiles(FileUri.create(sketchDir).toString()); return this.getSketchFiles(FileUri.create(sketchDir).toString());
} }
async createNewSketch(parentUri: string): Promise<Sketch> { async createNewSketch(parentUri?: string): Promise<Sketch> {
const monthNames = ['january', 'february', 'march', 'april', 'may', 'june', const monthNames = ['january', 'february', 'march', 'april', 'may', 'june',
'july', 'august', 'september', 'october', 'november', 'december' 'july', 'august', 'september', 'october', 'november', 'december'
]; ];
const today = new Date(); const today = new Date();
const parent = FileUri.fsPath(parentUri); const uri = !!parentUri ? parentUri : (await this.configService.getConfiguration()).sketchDirUri;
const parent = FileUri.fsPath(uri);
const sketchBaseName = `sketch_${monthNames[today.getMonth()]}${today.getDate()}`; const sketchBaseName = `sketch_${monthNames[today.getMonth()]}${today.getDate()}`;
let sketchName: string | undefined; let sketchName: string | undefined;
@ -81,7 +94,7 @@ export class SketchesServiceImpl implements SketchesService {
const sketchDir = path.join(parent, sketchName) const sketchDir = path.join(parent, sketchName)
const sketchFile = path.join(sketchDir, `${sketchName}.ino`); const sketchFile = path.join(sketchDir, `${sketchName}.ino`);
fs.mkdirSync(sketchDir); fs.mkdirpSync(sketchDir);
fs.writeFileSync(sketchFile, ` fs.writeFileSync(sketchFile, `
void setup() { void setup() {
// put your setup code here, to run once: // put your setup code here, to run once:

View File

@ -52,7 +52,7 @@ jobs:
- task: PublishBuildArtifacts@1 - task: PublishBuildArtifacts@1
inputs: inputs:
pathtoPublish: electron/build/dist/$(ArduinoPoC.AppName) pathtoPublish: electron/build/dist/$(ArduinoPoC.AppName)
artifactName: 'Arduino-PoC - Applications' artifactName: 'Arduino Pro IDE - Applications'
condition: or(in(variables['Agent.OS'], 'Windows_NT'), in(variables['Build.Reason'], 'Manual', 'Schedule')) condition: or(in(variables['Agent.OS'], 'Windows_NT'), in(variables['Build.Reason'], 'Manual', 'Schedule'))
displayName: Publish displayName: Publish
- job: Release - job: Release
@ -65,16 +65,16 @@ jobs:
- task: DownloadBuildArtifacts@0 - task: DownloadBuildArtifacts@0
displayName: Download displayName: Download
inputs: inputs:
artifactName: 'Arduino-PoC - Applications' artifactName: 'Arduino Pro IDE - Applications'
downloadPath: 'gh-release' downloadPath: 'gh-release'
- task: GithubRelease@0 - task: GithubRelease@0
inputs: inputs:
gitHubConnection: typefox-service-account1 gitHubConnection: typefox-service-account1
repositoryName: bcmi-labs/arduino-editor repositoryName: bcmi-labs/arduino-editor
assets: | assets: |
gh-release/Arduino-PoC - Applications/*.zip gh-release/Arduino Pro IDE - Applications/*.zip
gh-release/Arduino-PoC - Applications/*.dmg gh-release/Arduino Pro IDE - Applications/*.dmg
gh-release/Arduino-PoC - Applications/*.tar.xz gh-release/Arduino Pro IDE - Applications/*.tar.xz
target: $(Build.SourceVersion) target: $(Build.SourceVersion)
action: Edit action: Edit
tagSource: auto tagSource: auto

View File

@ -1,10 +1,11 @@
{ {
"private": true, "private": true,
"name": "browser-app", "name": "browser-app",
"version": "0.0.1", "version": "0.0.2",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@theia/core": "next", "@theia/core": "next",
"@theia/cpp": "next",
"@theia/editor": "next", "@theia/editor": "next",
"@theia/file-search": "next", "@theia/file-search": "next",
"@theia/filesystem": "next", "@theia/filesystem": "next",
@ -16,22 +17,21 @@
"@theia/process": "next", "@theia/process": "next",
"@theia/terminal": "next", "@theia/terminal": "next",
"@theia/workspace": "next", "@theia/workspace": "next",
"@theia/cpp": "next",
"@theia/textmate-grammars": "next", "@theia/textmate-grammars": "next",
"arduino-ide-extension": "0.0.1" "arduino-ide-extension": "0.0.2"
}, },
"devDependencies": { "devDependencies": {
"@theia/cli": "next" "@theia/cli": "next"
}, },
"scripts": { "scripts": {
"prepare": "theia build --mode development", "prepare": "theia build --mode development",
"start": "theia start --root-dir=../workspace", "start": "theia start",
"watch": "theia build --watch --mode development" "watch": "theia build --watch --mode development"
}, },
"theia": { "theia": {
"frontend": { "frontend": {
"config": { "config": {
"applicationName": "Arduino Editor", "applicationName": "Arduino Pro IDE",
"defaultTheme": "arduino-theme", "defaultTheme": "arduino-theme",
"preferences": { "preferences": {
"editor.autoSave": "on" "editor.autoSave": "on"

View File

@ -1,10 +1,11 @@
{ {
"private": true, "private": true,
"name": "electron-app", "name": "electron-app",
"version": "0.0.1", "version": "0.0.2",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@theia/core": "next", "@theia/core": "next",
"@theia/cpp": "next",
"@theia/editor": "next", "@theia/editor": "next",
"@theia/electron": "next", "@theia/electron": "next",
"@theia/file-search": "next", "@theia/file-search": "next",
@ -17,9 +18,8 @@
"@theia/process": "next", "@theia/process": "next",
"@theia/terminal": "next", "@theia/terminal": "next",
"@theia/workspace": "next", "@theia/workspace": "next",
"@theia/cpp": "next",
"@theia/textmate-grammars": "next", "@theia/textmate-grammars": "next",
"arduino-ide-extension": "0.0.1" "arduino-ide-extension": "0.0.2"
}, },
"devDependencies": { "devDependencies": {
"@theia/cli": "next", "@theia/cli": "next",
@ -27,14 +27,14 @@
}, },
"scripts": { "scripts": {
"prepare": "theia build --mode development", "prepare": "theia build --mode development",
"start": "theia start --root-dir=../workspace", "start": "theia start",
"watch": "theia build --watch --mode development" "watch": "theia build --watch --mode development"
}, },
"theia": { "theia": {
"target": "electron", "target": "electron",
"frontend": { "frontend": {
"config": { "config": {
"applicationName": "Arduino Editor", "applicationName": "Arduino Pro IDE",
"defaultTheme": "arduino-theme", "defaultTheme": "arduino-theme",
"preferences": { "preferences": {
"editor.autoSave": "on" "editor.autoSave": "on"

View File

@ -1,13 +1,13 @@
## Electron ## Electron
All-in-one packager producing the `Arduino-PoC` Electron-based application. All-in-one packager producing the `Arduino Pro IDE` Electron-based application.
## Prerequisites ## Prerequisites
The prerequisites are defined [here](https://github.com/theia-ide/theia/blob/master/doc/Developing.md#prerequisites). The prerequisites are defined [here](https://github.com/theia-ide/theia/blob/master/doc/Developing.md#prerequisites).
### Build: ### Build:
To build the Arduino-PoC Electron-based Theia application you have to do the followings: To build the Arduino Pro IDE application you have to do the followings:
```bash ```bash
yarn --cwd ./electron/packager/ && yarn --cwd ./electron/packager/ package yarn --cwd ./electron/packager/ && yarn --cwd ./electron/packager/ package
``` ```

View File

@ -1,18 +1,21 @@
{ {
"name": "arduino-electron", "name": "arduino.Pro.IDE",
"description": "Arduino-PoC Electron", "description": "Arduino Pro IDE",
"main": "src-gen/frontend/electron-main.js", "main": "src-gen/frontend/electron-main.js",
"author": "TypeFox", "author": "TypeFox",
"dependencies": { "dependencies": {
"google-protobuf": "^3.5.0", "google-protobuf": "^3.5.0",
"arduino-ide-extension": "file:../working-copy/arduino-ide-extension" "arduino-ide-extension": "file:../working-copy/arduino-ide-extension"
}, },
"resolutions": {
"**/fs-extra": "^4.0.2"
},
"devDependencies": { "devDependencies": {
"electron-builder": "^20.36.2" "electron-builder": "^21.2.0"
}, },
"scripts": { "scripts": {
"build": "theia build --mode development", "build": "theia build --mode development",
"build:release": "theia build --mode production", "build:release": "theia build --mode development",
"package": "electron-builder --publish=never", "package": "electron-builder --publish=never",
"package:preview": "electron-builder --dir" "package:preview": "electron-builder --dir"
}, },
@ -21,16 +24,17 @@
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/TypeFox/arduino-poc.git" "url": "git+https://github.com/arduino/arduino-pro-ide.git"
}, },
"// Notes:" : [ "// Notes:": [
"The `electronVersion` version was pinned for `@grpc/grpc-js` -> Node.js version constraints.", "The `electronVersion` version was pinned for `@grpc/grpc-js` -> Node.js version constraints.",
"`google-protobuf` was declared as it is not picked up by the `electron-builder` as a runtime dependency." "`google-protobuf` was declared as it is not picked up by the `electron-builder` as a runtime dependency.",
"The resolution for `fs-extra` was required due to this: https://spectrum.chat/theia/general/our-theia-electron-builder-app-no-longer-starts~f5cf09a0-6d88-448b-8818-24ad0ec2ee7c"
], ],
"build": { "build": {
"productName": "Arduino-PoC", "productName": "Arduino Pro IDE",
"appId": "arduino.PoC", "appId": "arduino.Pro.IDE",
"electronVersion": "4.0.0", "electronVersion": "4.2.0",
"asar": false, "asar": false,
"directories": { "directories": {
"buildResources": "resources" "buildResources": "resources"
@ -49,40 +53,40 @@
"!node_modules/onigasm/*" "!node_modules/onigasm/*"
], ],
"win": { "win": {
"target": [ "target": [
"zip" "zip"
], ],
"artifactName": "${productName}-${env.ARDUINO_VERSION}-${os}.${ext}" "artifactName": "${productName}-${env.ARDUINO_VERSION}-${os}.${ext}"
}, },
"mac": { "mac": {
"target": [ "target": [
"dmg" "dmg"
], ],
"artifactName": "${productName}-${env.ARDUINO_VERSION}-${os}.${ext}", "artifactName": "${productName}-${env.ARDUINO_VERSION}-${os}.${ext}",
"darkModeSupport": true "darkModeSupport": true
}, },
"linux": { "linux": {
"target": [ "target": [
"tar.xz" "tar.xz"
], ],
"artifactName": "${productName}-${env.ARDUINO_VERSION}-${os}.${ext}" "artifactName": "${productName}-${env.ARDUINO_VERSION}-${os}.${ext}"
}, },
"dmg": { "dmg": {
"icon": "resources/icon.icns", "icon": "resources/icon.icns",
"iconSize": 128, "iconSize": 128,
"contents": [ "contents": [
{ {
"x": 380, "x": 380,
"y": 240, "y": 240,
"type": "link", "type": "link",
"path": "/Applications" "path": "/Applications"
}, },
{ {
"x": 122, "x": 122,
"y": 240, "y": 240,
"type": "file" "type": "file"
} }
] ]
} }
} }
} }

View File

@ -25,7 +25,7 @@ const yargs = require('yargs');
process.stderr.write(`Unexpected platform: ${platform}.`); process.stderr.write(`Unexpected platform: ${platform}.`);
process.exit(1); process.exit(1);
} }
process.stdout.write(`Arduino-PoC-${versionInfo().version}-${os}.${ext}`); process.stdout.write(`Arduino Pro IDE-${versionInfo().version}-${os}.${ext}`);
process.exit(0); process.exit(0);
} }
}) })

View File

@ -5,7 +5,8 @@
const fs = require('fs'); const fs = require('fs');
const join = require('path').join; const join = require('path').join;
const shell = require('shelljs'); const shell = require('shelljs');
shell.env.THEIA_ELECTRON_SKIP_REPLACE_FFMPEG = '1'; shell.env.THEIA_ELECTRON_SKIP_REPLACE_FFMPEG = '1'; // Do not run the ffmpeg validation for the packager.
shell.env.NODE_OPTIONS = '--max_old_space_size=4096'; // Increase heap size for the CI
const utils = require('./utils'); const utils = require('./utils');
const { version, release } = utils.versionInfo(); const { version, release } = utils.versionInfo();
@ -64,7 +65,7 @@
//-------------------------------------------------------------------------------------------------+ //-------------------------------------------------------------------------------------------------+
// Rebuild the extension with the copied `yarn.lock`. It is a must to use the same Theia versions. | // Rebuild the extension with the copied `yarn.lock`. It is a must to use the same Theia versions. |
//-------------------------------------------------------------------------------------------------+ //-------------------------------------------------------------------------------------------------+
exec(`yarn --network-timeout 1000000 --cwd ${path('..', workingCopy)}`, 'Building the Arduino Theia extensions'); exec(`yarn --network-timeout 1000000 --cwd ${path('..', workingCopy)}`, 'Building the Arduino Pro IDE extensions');
// Collect all unused dependencies by the backend. We have to remove them from the electron app. // Collect all unused dependencies by the backend. We have to remove them from the electron app.
// The `bundle.js` already contains everything we need for the frontend. // The `bundle.js` already contains everything we need for the frontend.
// We have to do it before changing the dependencies to `local-path`. // We have to do it before changing the dependencies to `local-path`.
@ -87,7 +88,7 @@
devDependencies: pkg.devDependencies devDependencies: pkg.devDependencies
}, null, 2)); }, null, 2));
echo(`📜 Effective 'package.json' for the Arduino-PoC application is: echo(`📜 Effective 'package.json' for the Arduino Pro IDE application is:
----------------------- -----------------------
${fs.readFileSync(path('..', 'build', 'package.json')).toString()} ${fs.readFileSync(path('..', 'build', 'package.json')).toString()}
----------------------- -----------------------
@ -108,7 +109,7 @@ ${fs.readFileSync(path('..', 'build', 'package.json')).toString()}
// Install all private and public dependencies for the electron application and build Theia. | // Install all private and public dependencies for the electron application and build Theia. |
//-------------------------------------------------------------------------------------------+ //-------------------------------------------------------------------------------------------+
exec(`yarn --network-timeout 1000000 --cwd ${path('..', 'build')}`, 'Installing dependencies'); exec(`yarn --network-timeout 1000000 --cwd ${path('..', 'build')}`, 'Installing dependencies');
exec(`yarn --network-timeout 1000000 --cwd ${path('..', 'build')} build${release ? ':release' : ''}`, 'Building the Arduino-PoC application'); exec(`yarn --network-timeout 1000000 --cwd ${path('..', 'build')} build${release ? ':release' : ''}`, 'Building the Arduino Pro IDE application');
//------------------------------------------------------------------------------+ //------------------------------------------------------------------------------+
// Create a throw away dotenv file which we use to feed the builder with input. | // Create a throw away dotenv file which we use to feed the builder with input. |
@ -124,7 +125,7 @@ ${fs.readFileSync(path('..', 'build', 'package.json')).toString()}
//-----------------------------------+ //-----------------------------------+
// Package the electron application. | // Package the electron application. |
//-----------------------------------+ //-----------------------------------+
exec(`yarn --network-timeout 1000000 --cwd ${path('..', 'build')} package`, `Packaging your Arduino-PoC application`); exec(`yarn --network-timeout 1000000 --cwd ${path('..', 'build')} package`, `Packaging your Arduino Pro IDE application`);
echo(`🎉 Success. Your application is at: ${path('..', 'build', 'dist')}`); echo(`🎉 Success. Your application is at: ${path('..', 'build', 'dist')}`);
restore(); restore();

View File

@ -2,7 +2,7 @@
"private": true, "private": true,
"name": "packager", "name": "packager",
"version": "1.0.0", "version": "1.0.0",
"description": "Packager for the Arduino-PoC electron application", "description": "Packager for the Arduino Pro IDE electron application",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"package": "node index.js", "package": "node index.js",

View File

@ -11924,6 +11924,11 @@ string-argv@^0.1.1:
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.1.2.tgz#c5b7bc03fb2b11983ba3a72333dd0559e77e4738" resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.1.2.tgz#c5b7bc03fb2b11983ba3a72333dd0559e77e4738"
integrity sha512-mBqPGEOMNJKXRo7z0keX0wlAhbBAjilUdPW13nN0PecVryZxdHIeM7TqbsSUA7VYuS00HGC6mojP7DlQzfa9ZA== integrity sha512-mBqPGEOMNJKXRo7z0keX0wlAhbBAjilUdPW13nN0PecVryZxdHIeM7TqbsSUA7VYuS00HGC6mojP7DlQzfa9ZA==
string-natural-compare@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-2.0.3.tgz#9dbe1dd65490a5fe14f7a5c9bc686fc67cb9c6e4"
integrity sha512-4Kcl12rNjc+6EKhY8QyDVuQTAlMWwRiNbsxnVwBUKFr7dYPQuXVrtNU4sEkjF9LHY0AY6uVbB3ktbkIH4LC+BQ==
string-template@~0.2.1: string-template@~0.2.1:
version "0.2.1" version "0.2.1"
resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add"