mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-07-08 11:56:36 +00:00
Merge pull request #75 from bcmi-labs/25-10-release
Various bug fixes, plus one or two enhancements.
This commit is contained in:
commit
7c1ebf273c
@ -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.
|
||||||
|
@ -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"
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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) : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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'/>
|
||||||
|
@ -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> {
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -268,7 +268,7 @@ export class MonitorWidget extends ReactWidget implements StatefulWidget {
|
|||||||
return {
|
return {
|
||||||
baudRate,
|
baudRate,
|
||||||
board: selectedBoard,
|
board: selectedBoard,
|
||||||
port: selectedPort
|
port: selectedPort.address
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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) {
|
||||||
|
@ -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>;
|
||||||
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
3
arduino-ide-extension/src/common/types.ts
Normal file
3
arduino-ide-extension/src/common/types.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export type RecursiveRequired<T> = {
|
||||||
|
[P in keyof T]-?: RecursiveRequired<T[P]>;
|
||||||
|
};
|
@ -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);
|
||||||
|
@ -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;
|
||||||
});
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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));
|
||||||
|
@ -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 });
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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> {
|
||||||
|
@ -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();
|
||||||
|
@ -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 {
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
```
|
```
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -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();
|
||||||
|
@ -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",
|
||||||
|
@ -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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user