mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-06-05 19:56:34 +00:00
Added support for 3rd party core settings.
Closes arduino/arduino-pro-ide#10. Signed-off-by: Akos Kitta <kittaakos@typefox.io>
This commit is contained in:
parent
5c16f8d6c9
commit
12f2aa35ff
.vscode
arduino-debugger-extension
arduino-ide-extension
data/cli/schema
package.jsonsrc
browser
arduino-daemon-client-impl.tsarduino-frontend-contribution.tsxarduino-ide-frontend-module.ts
boards
boards-auto-installer.tsboards-config-quick-open-service.tsboards-config-store.tsboards-config.tsxboards-details-menu-updater.tsboards-list-widget.tsboards-service-client-impl.tsboards-toolbar-item.tsxboards-widget-frontend-contribution.ts
components/component-list
editor-mode.tslanguage
menu
monitor
style
common/protocol
electron-browser
node
test
21
.vscode/launch.json
vendored
21
.vscode/launch.json
vendored
@ -83,6 +83,27 @@
|
||||
"smartStep": true,
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"outputCapture": "std"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"protocol": "inspector",
|
||||
"name": "Run Test [current]",
|
||||
"program": "${workspaceRoot}/node_modules/mocha/bin/_mocha",
|
||||
"args": [
|
||||
"--require",
|
||||
"reflect-metadata/Reflect",
|
||||
"--no-timeouts",
|
||||
"--colors",
|
||||
"**/${fileBasenameNoExtension}.js"
|
||||
],
|
||||
"env": {
|
||||
"TS_NODE_PROJECT": "${workspaceRoot}/tsconfig.json"
|
||||
},
|
||||
"sourceMaps": true,
|
||||
"smartStep": true,
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"outputCapture": "std"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -26,8 +26,8 @@
|
||||
],
|
||||
"theiaExtensions": [
|
||||
{
|
||||
"backend": "lib/node/backend-module",
|
||||
"frontend": "lib/browser/frontend-module"
|
||||
"backend": "lib/node/arduino-debug-backend-module",
|
||||
"frontend": "lib/browser/arduino-debug-frontend-module"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ import { ArduinoDebugSessionManager } from './arduino-debug-session-manager';
|
||||
|
||||
import '../../src/browser/style/index.css';
|
||||
|
||||
|
||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(ArduinoVariableResolver).toSelf().inSingletonScope();
|
||||
bind(VariableContribution).toService(ArduinoVariableResolver);
|
@ -110,5 +110,8 @@
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"// TODOs": [
|
||||
"additionalProperties should be true. See the new telemetry entry"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
|
@ -15,8 +15,8 @@
|
||||
"lint": "tslint -c ./tslint.json --project ./tsconfig.json",
|
||||
"build": "tsc && ncp ./src/node/cli-protocol/ ./lib/node/cli-protocol/ && yarn lint",
|
||||
"watch": "tsc -w",
|
||||
"test": "mocha \"./src/test/**/*.test.ts\"",
|
||||
"test:watch": "mocha --watch --watch-files src \"./src/test/**/*.test.ts\""
|
||||
"test": "mocha \"./lib/test/**/*.test.js\"",
|
||||
"test:watch": "mocha --watch --watch-files lib \"./lib/test/**/*.test.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^0.6.18",
|
||||
@ -79,18 +79,16 @@
|
||||
"protoc": "1.0.4",
|
||||
"shelljs": "^0.8.3",
|
||||
"temp": "^0.9.1",
|
||||
"ts-node": "^8.6.2",
|
||||
"uuid": "^3.2.1",
|
||||
"yargs": "^11.1.0"
|
||||
},
|
||||
"mocha": {
|
||||
"require": [
|
||||
"ts-node/register",
|
||||
"reflect-metadata/Reflect"
|
||||
],
|
||||
"reporter": "spec",
|
||||
"colors": true,
|
||||
"watch-extensions": "ts,tsx",
|
||||
"watch-extensions": "js",
|
||||
"timeout": 10000
|
||||
},
|
||||
"files": [
|
||||
@ -101,12 +99,12 @@
|
||||
],
|
||||
"theiaExtensions": [
|
||||
{
|
||||
"backend": "lib/node/arduino-backend-module",
|
||||
"frontend": "lib/browser/arduino-frontend-module"
|
||||
"backend": "lib/node/arduino-ide-backend-module",
|
||||
"frontend": "lib/browser/arduino-ide-frontend-module"
|
||||
},
|
||||
{
|
||||
"frontend": "lib/browser/menu/browser-arduino-menu-module",
|
||||
"frontendElectron": "lib/electron-browser/electron-arduino-menu-module"
|
||||
"frontendElectron": "lib/electron-browser/menu/electron-arduino-menu-module"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { injectable, inject } from 'inversify';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { Event, Emitter } from '@theia/core/lib/common/event';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { ArduinoDaemonClient } from '../common/protocol/arduino-daemon';
|
||||
import { ArduinoDaemonClient } from '../common/protocol';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoDaemonClientImpl implements ArduinoDaemonClient {
|
||||
|
@ -5,9 +5,8 @@ import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { CommandContribution, CommandRegistry, Command, CommandHandler } from '@theia/core/lib/common/command';
|
||||
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { BoardsService } from '../common/protocol/boards-service';
|
||||
import { BoardsService, BoardsServiceClient, CoreService, Sketch, SketchesService, ToolOutputServiceClient } from '../common/protocol';
|
||||
import { ArduinoCommands } from './arduino-commands';
|
||||
import { CoreService } from '../common/protocol/core-service';
|
||||
import { BoardsServiceClientImpl } from './boards/boards-service-client-impl';
|
||||
import { WorkspaceRootUriAwareCommandHandler, WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
|
||||
import { SelectionService, MenuContribution, MenuModelRegistry, MAIN_MENU_BAR, MenuPath } from '@theia/core';
|
||||
@ -19,8 +18,6 @@ import {
|
||||
} from '@theia/core/lib/browser';
|
||||
import { OpenFileDialogProps, FileDialogService } from '@theia/filesystem/lib/browser/file-dialog';
|
||||
import { FileSystem, FileStat } from '@theia/filesystem/lib/common';
|
||||
import { Sketch, SketchesService } from '../common/protocol/sketches-service';
|
||||
import { ToolOutputServiceClient } from '../common/protocol/tool-output-service';
|
||||
import { CommonCommands, CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||
import { FileSystemCommands } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution';
|
||||
import { FileDownloadCommands } from '@theia/filesystem/lib/browser/download/file-download-command-contribution';
|
||||
@ -45,6 +42,8 @@ import { ColorContribution } from '@theia/core/lib/browser/color-application-con
|
||||
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
|
||||
import { ArduinoDaemon } from '../common/protocol/arduino-daemon';
|
||||
import { ConfigService } from '../common/protocol/config-service';
|
||||
import { BoardsConfigStore } from './boards/boards-config-store';
|
||||
import { MainMenuManager } from './menu/main-menu-manager';
|
||||
|
||||
export namespace ArduinoMenus {
|
||||
export const SKETCH = [...MAIN_MENU_BAR, '3_sketch'];
|
||||
@ -75,7 +74,11 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
|
||||
protected readonly toolOutputServiceClient: ToolOutputServiceClient;
|
||||
|
||||
@inject(BoardsServiceClientImpl)
|
||||
protected readonly boardsServiceClient: BoardsServiceClientImpl;
|
||||
protected readonly boardsServiceClientImpl: BoardsServiceClientImpl;
|
||||
|
||||
// Unused but do not remove it. It's required by DI, otherwise `init` method is not called.
|
||||
@inject(BoardsServiceClient)
|
||||
protected readonly boardsServiceClient: BoardsServiceClient;
|
||||
|
||||
@inject(SelectionService)
|
||||
protected readonly selectionService: SelectionService;
|
||||
@ -143,6 +146,12 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
|
||||
@inject(ConfigService)
|
||||
protected readonly configService: ConfigService;
|
||||
|
||||
@inject(BoardsConfigStore)
|
||||
protected readonly boardsConfigStore: BoardsConfigStore;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
protected readonly mainMenuManager: MainMenuManager;
|
||||
|
||||
protected application: FrontendApplication;
|
||||
protected wsSketchCount: number = 0; // TODO: this does not belong here, does it?
|
||||
|
||||
@ -154,15 +163,10 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
|
||||
text: BoardsConfig.Config.toString(config)
|
||||
});
|
||||
}
|
||||
this.boardsServiceClient.onBoardsConfigChanged(updateStatusBar);
|
||||
updateStatusBar(this.boardsServiceClient.boardsConfig);
|
||||
this.boardsServiceClientImpl.onBoardsConfigChanged(updateStatusBar);
|
||||
updateStatusBar(this.boardsServiceClientImpl.boardsConfig);
|
||||
|
||||
this.registerSketchesInMenu(this.menuRegistry);
|
||||
|
||||
Promise.all([
|
||||
this.boardsService.getAttachedBoards(),
|
||||
this.boardsService.getAvailablePorts()
|
||||
]).then(([{ boards }, { ports }]) => this.boardsServiceClient.tryReconnect(boards, ports));
|
||||
}
|
||||
|
||||
onStart(app: FrontendApplication): void {
|
||||
@ -210,8 +214,7 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
|
||||
render: () => <BoardsToolBarItem
|
||||
key='boardsToolbarItem'
|
||||
commands={this.commandRegistry}
|
||||
boardsServiceClient={this.boardsServiceClient}
|
||||
boardService={this.boardsService} />,
|
||||
boardsServiceClient={this.boardsServiceClientImpl} />,
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
priority: 2
|
||||
});
|
||||
@ -276,10 +279,7 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
|
||||
});
|
||||
|
||||
registry.registerCommand(ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG, {
|
||||
execute: () => {
|
||||
this.editorMode.toggleCompileForDebug();
|
||||
this.editorMode.menuContentChanged.fire();
|
||||
},
|
||||
execute: () => this.editorMode.toggleCompileForDebug(),
|
||||
isToggled: () => this.editorMode.compileForDebug
|
||||
});
|
||||
|
||||
@ -345,7 +345,7 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
|
||||
execute: async () => {
|
||||
const boardsConfig = await this.boardsConfigDialog.open();
|
||||
if (boardsConfig) {
|
||||
this.boardsServiceClient.boardsConfig = boardsConfig;
|
||||
this.boardsServiceClientImpl.boardsConfig = boardsConfig;
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -377,18 +377,18 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
|
||||
}
|
||||
|
||||
try {
|
||||
const { boardsConfig } = this.boardsServiceClient;
|
||||
const { boardsConfig } = this.boardsServiceClientImpl;
|
||||
if (!boardsConfig || !boardsConfig.selectedBoard) {
|
||||
throw new Error('No boards selected. Please select a board.');
|
||||
}
|
||||
if (!boardsConfig.selectedBoard.fqbn) {
|
||||
throw new Error(`No core is installed for ${boardsConfig.selectedBoard.name}. Please install the board.`);
|
||||
throw new Error(`No core is installed for the '${boardsConfig.selectedBoard.name}' board. Please install the core.`);
|
||||
}
|
||||
// Reveal the Output view asynchronously (don't await it)
|
||||
const fqbn = await this.boardsConfigStore.appendConfigToFqbn(boardsConfig.selectedBoard.fqbn);
|
||||
this.outputContribution.openView({ reveal: true });
|
||||
await this.coreService.compile({
|
||||
uri: uri.toString(),
|
||||
board: boardsConfig.selectedBoard,
|
||||
sketchUri: uri.toString(),
|
||||
fqbn,
|
||||
optimizeForDebug: this.editorMode.compileForDebug
|
||||
});
|
||||
} catch (e) {
|
||||
@ -413,7 +413,7 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
|
||||
}
|
||||
|
||||
try {
|
||||
const { boardsConfig } = this.boardsServiceClient;
|
||||
const { boardsConfig } = this.boardsServiceClientImpl;
|
||||
if (!boardsConfig || !boardsConfig.selectedBoard) {
|
||||
throw new Error('No boards selected. Please select a board.');
|
||||
}
|
||||
@ -421,11 +421,14 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
|
||||
if (!selectedPort) {
|
||||
throw new Error('No ports selected. Please select a port.');
|
||||
}
|
||||
// Reveal the Output view asynchronously (don't await it)
|
||||
if (!boardsConfig.selectedBoard.fqbn) {
|
||||
throw new Error(`No core is installed for the '${boardsConfig.selectedBoard.name}' board. Please install the core.`);
|
||||
}
|
||||
this.outputContribution.openView({ reveal: true });
|
||||
const fqbn = await this.boardsConfigStore.appendConfigToFqbn(boardsConfig.selectedBoard.fqbn);
|
||||
await this.coreService.upload({
|
||||
uri: uri.toString(),
|
||||
board: boardsConfig.selectedBoard,
|
||||
sketchUri: uri.toString(),
|
||||
fqbn,
|
||||
port: selectedPort.address,
|
||||
optimizeForDebug: this.editorMode.compileForDebug
|
||||
});
|
||||
|
@ -75,6 +75,9 @@ import { ArduinoFrontendConnectionStatusService, ArduinoApplicationConnectionSta
|
||||
import { FrontendConnectionStatusService, ApplicationConnectionStatusContribution } from '@theia/core/lib/browser/connection-status-service';
|
||||
import { ConfigServiceClientImpl } from './config-service-client-impl';
|
||||
import { CoreServiceClientImpl } from './core-service-client-impl';
|
||||
import { BoardsDetailsMenuUpdater } from './boards/boards-details-menu-updater';
|
||||
import { BoardsConfigStore } from './boards/boards-config-store';
|
||||
import { ILogger } from '@theia/core';
|
||||
|
||||
const ElementQueries = require('css-element-queries/src/ElementQueries');
|
||||
|
||||
@ -144,12 +147,28 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
|
||||
// Boards service client to receive and delegate notifications from the backend.
|
||||
bind(BoardsServiceClientImpl).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(BoardsServiceClientImpl);
|
||||
bind(BoardsServiceClient).toDynamicValue(context => {
|
||||
bind(BoardsServiceClient).toDynamicValue(async context => {
|
||||
const client = context.container.get(BoardsServiceClientImpl);
|
||||
const service = context.container.get<BoardsService>(BoardsService);
|
||||
const [attachedBoards, availablePorts] = await Promise.all([
|
||||
service.getAttachedBoards(),
|
||||
service.getAvailablePorts()
|
||||
]);
|
||||
client.init({ attachedBoards, availablePorts });
|
||||
WebSocketConnectionProvider.createProxy(context.container, BoardsServicePath, client);
|
||||
return client;
|
||||
}).inSingletonScope();
|
||||
|
||||
// To be able to track, and update the menu based on the core settings (aka. board details) of the currently selected board.
|
||||
bind(FrontendApplicationContribution).to(BoardsDetailsMenuUpdater).inSingletonScope();
|
||||
bind(BoardsConfigStore).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(BoardsConfigStore);
|
||||
// Logger for the Arduino daemon
|
||||
bind(ILogger).toDynamicValue(ctx => {
|
||||
const parentLogger = ctx.container.get<ILogger>(ILogger);
|
||||
return parentLogger.child('store');
|
||||
}).inSingletonScope().whenTargetNamed('store');
|
||||
|
||||
// Boards auto-installer
|
||||
bind(BoardsAutoInstaller).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(BoardsAutoInstaller);
|
@ -35,9 +35,9 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
|
||||
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)))
|
||||
this.boardsService.search({}).then(packages => {
|
||||
const candidates = packages
|
||||
.filter(pkg => pkg.boards.some(board => Board.sameAs(board, selectedBoard)))
|
||||
.filter(({ installable, installedVersion }) => installable && !installedVersion);
|
||||
for (const candidate of candidates) {
|
||||
// tslint:disable-next-line:max-line-length
|
||||
|
@ -0,0 +1,43 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { QuickOpenItem, QuickOpenModel } from '@theia/core/lib/common/quick-open-model';
|
||||
import { QuickOpenService, QuickOpenOptions } from '@theia/core/lib/browser/quick-open/quick-open-service';
|
||||
import { BoardsService, BoardsServiceClient } from '../../common/protocol';
|
||||
|
||||
@injectable()
|
||||
export class BoardsConfigQuickOpenService {
|
||||
|
||||
@inject(QuickOpenService)
|
||||
protected readonly quickOpenService: QuickOpenService;
|
||||
|
||||
@inject(BoardsService)
|
||||
protected readonly boardsService: BoardsService;
|
||||
|
||||
@inject(BoardsServiceClient)
|
||||
protected readonly boardsServiceClient: BoardsServiceClient;
|
||||
|
||||
async selectBoard(): Promise<void> {
|
||||
|
||||
}
|
||||
|
||||
protected open(items: QuickOpenItem | QuickOpenItem[], placeholder: string): void {
|
||||
this.quickOpenService.open(this.getModel(Array.isArray(items) ? items : [items]), this.getOptions(placeholder));
|
||||
}
|
||||
|
||||
protected getOptions(placeholder: string, fuzzyMatchLabel: boolean = true, onClose: (canceled: boolean) => void = () => { }): QuickOpenOptions {
|
||||
return QuickOpenOptions.resolve({
|
||||
placeholder,
|
||||
fuzzyMatchLabel,
|
||||
fuzzySort: false,
|
||||
onClose
|
||||
});
|
||||
}
|
||||
|
||||
protected getModel(items: QuickOpenItem | QuickOpenItem[]): QuickOpenModel {
|
||||
return {
|
||||
onType(_: string, acceptor: (items: QuickOpenItem[]) => void): void {
|
||||
acceptor(Array.isArray(items) ? items : [items]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
161
arduino-ide-extension/src/browser/boards/boards-config-store.ts
Normal file
161
arduino-ide-extension/src/browser/boards/boards-config-store.ts
Normal file
@ -0,0 +1,161 @@
|
||||
import { injectable, inject, named } from 'inversify';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { Event, Emitter } from '@theia/core/lib/common/event';
|
||||
import { deepClone, notEmpty } from '@theia/core/lib/common/objects';
|
||||
import { FrontendApplicationContribution, LocalStorageService } from '@theia/core/lib/browser';
|
||||
import { BoardsService, ConfigOption, Installable, BoardDetails } from '../../common/protocol';
|
||||
import { BoardsServiceClientImpl } from './boards-service-client-impl';
|
||||
|
||||
@injectable()
|
||||
export class BoardsConfigStore implements FrontendApplicationContribution {
|
||||
|
||||
@inject(ILogger)
|
||||
@named('store')
|
||||
protected readonly logger: ILogger;
|
||||
|
||||
@inject(BoardsService)
|
||||
protected readonly boardsService: BoardsService;
|
||||
|
||||
@inject(BoardsServiceClientImpl)
|
||||
protected readonly boardsServiceClient: BoardsServiceClientImpl;
|
||||
|
||||
@inject(LocalStorageService)
|
||||
protected readonly storageService: LocalStorageService;
|
||||
|
||||
protected readonly onChangedEmitter = new Emitter<void>();
|
||||
|
||||
onStart(): void {
|
||||
this.boardsServiceClient.onBoardsPackageInstalled(async ({ pkg }) => {
|
||||
const { installedVersion: version } = pkg;
|
||||
if (!version) {
|
||||
return;
|
||||
}
|
||||
let shouldFireChanged = false;
|
||||
for (const fqbn of pkg.boards.map(({ fqbn }) => fqbn).filter(notEmpty).filter(fqbn => !!fqbn)) {
|
||||
const key = this.getStorageKey(fqbn, version);
|
||||
let data = await this.storageService.getData<ConfigOption[] | undefined>(key);
|
||||
if (!data || !data.length) {
|
||||
const details = await this.getBoardDetailsSafe(fqbn);
|
||||
if (details) {
|
||||
data = details.configOptions;
|
||||
if (data.length) {
|
||||
await this.storageService.setData(key, data);
|
||||
shouldFireChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shouldFireChanged) {
|
||||
this.fireChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get onChanged(): Event<void> {
|
||||
return this.onChangedEmitter.event;
|
||||
}
|
||||
|
||||
async appendConfigToFqbn(
|
||||
fqbn: string,
|
||||
boardsPackageVersion: MaybePromise<Installable.Version | undefined> = this.getBoardsPackageVersion(fqbn)): Promise<string> {
|
||||
|
||||
const configOptions = await this.getConfig(fqbn, boardsPackageVersion);
|
||||
return ConfigOption.decorate(fqbn, configOptions);
|
||||
}
|
||||
|
||||
async getConfig(
|
||||
fqbn: string,
|
||||
boardsPackageVersion: MaybePromise<Installable.Version | undefined> = this.getBoardsPackageVersion(fqbn)): Promise<ConfigOption[]> {
|
||||
|
||||
const version = await boardsPackageVersion;
|
||||
if (!version) {
|
||||
return [];
|
||||
}
|
||||
const key = this.getStorageKey(fqbn, version);
|
||||
let configOptions = await this.storageService.getData<ConfigOption[] | undefined>(key, undefined);
|
||||
if (configOptions) {
|
||||
return configOptions;
|
||||
}
|
||||
|
||||
const details = await this.getBoardDetailsSafe(fqbn);
|
||||
if (!details) {
|
||||
return [];
|
||||
}
|
||||
|
||||
configOptions = details.configOptions;
|
||||
await this.storageService.setData(key, configOptions);
|
||||
return configOptions;
|
||||
}
|
||||
|
||||
async setSelected(
|
||||
{ fqbn, option, selectedValue }: { fqbn: string, option: string, selectedValue: string },
|
||||
boardsPackageVersion: MaybePromise<Installable.Version | undefined> = this.getBoardsPackageVersion(fqbn)): Promise<boolean> {
|
||||
|
||||
const configOptions = deepClone(await this.getConfig(fqbn, boardsPackageVersion));
|
||||
const configOption = configOptions.find(c => c.option === option);
|
||||
if (!configOption) {
|
||||
return false;
|
||||
}
|
||||
let updated = false;
|
||||
for (const value of configOption.values) {
|
||||
if (value.value === selectedValue) {
|
||||
(value as any).selected = true;
|
||||
updated = true;
|
||||
} else {
|
||||
(value as any).selected = false;
|
||||
}
|
||||
}
|
||||
if (!updated) {
|
||||
return false;
|
||||
}
|
||||
const version = await boardsPackageVersion;
|
||||
if (!version) {
|
||||
return false;
|
||||
}
|
||||
await this.setConfig({ fqbn, configOptions, version });
|
||||
this.fireChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async setConfig(
|
||||
{ fqbn, configOptions, version }: { fqbn: string, configOptions: ConfigOption[], version: Installable.Version }): Promise<void> {
|
||||
|
||||
const key = this.getStorageKey(fqbn, version);
|
||||
return this.storageService.setData(key, configOptions);
|
||||
}
|
||||
|
||||
protected getStorageKey(fqbn: string, version: Installable.Version): string {
|
||||
return `.arduinoProIDE-configOptions-${version}-${fqbn}`;
|
||||
}
|
||||
|
||||
protected async getBoardDetailsSafe(fqbn: string): Promise<BoardDetails | undefined> {
|
||||
try {
|
||||
const details = this.boardsService.getBoardDetails({ fqbn });
|
||||
return details;
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.message.includes('loading board data') && err.message.includes('is not installed')) {
|
||||
this.logger.warn(`The boards package is not installed for board with FQBN: ${fqbn}`);
|
||||
} else {
|
||||
this.logger.error(`An unexpected error occurred while retrieving the board details for ${fqbn}.`, err);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected fireChanged(): void {
|
||||
this.onChangedEmitter.fire();
|
||||
}
|
||||
|
||||
protected async getBoardsPackageVersion(fqbn: string): Promise<Installable.Version | undefined> {
|
||||
if (!fqbn) {
|
||||
return undefined;
|
||||
}
|
||||
const boardsPackage = await this.boardsService.getContainerBoardPackage({ fqbn });
|
||||
if (!boardsPackage) {
|
||||
return undefined;
|
||||
}
|
||||
return boardsPackage.installedVersion;
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { DisposableCollection } from '@theia/core';
|
||||
import { BoardsService, Board, Port, AttachedSerialBoard, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service';
|
||||
import { BoardsService, Board, Port, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service';
|
||||
import { BoardsServiceClientImpl } from './boards-service-client-impl';
|
||||
import { CoreServiceClientImpl } from '../core-service-client-impl';
|
||||
import { ArduinoDaemonClientImpl } from '../arduino-daemon-client-impl';
|
||||
@ -36,11 +36,11 @@ export abstract class Item<T> extends React.Component<{
|
||||
selected: boolean,
|
||||
onClick: (item: T) => void,
|
||||
missing?: boolean,
|
||||
detail?: string
|
||||
details?: string
|
||||
}> {
|
||||
|
||||
render(): React.ReactNode {
|
||||
const { selected, label, missing, detail } = this.props;
|
||||
const { selected, label, missing, details } = this.props;
|
||||
const classNames = ['item'];
|
||||
if (selected) {
|
||||
classNames.push('selected');
|
||||
@ -48,11 +48,11 @@ export abstract class Item<T> extends React.Component<{
|
||||
if (missing === true) {
|
||||
classNames.push('missing')
|
||||
}
|
||||
return <div onClick={this.onClick} className={classNames.join(' ')} title={`${label}${!detail ? '' : detail}`}>
|
||||
return <div onClick={this.onClick} className={classNames.join(' ')} title={`${label}${!details ? '' : details}`}>
|
||||
<div className='label'>
|
||||
{label}
|
||||
</div>
|
||||
{!detail ? '' : <div className='detail'>{detail}</div>}
|
||||
{!details ? '' : <div className='details'>{details}</div>}
|
||||
{!selected ? '' : <div className='selected-icon'><i className='fa fa-check' /></div>}
|
||||
</div>;
|
||||
}
|
||||
@ -82,13 +82,15 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
|
||||
|
||||
componentDidMount() {
|
||||
this.updateBoards();
|
||||
this.props.boardsService.getAvailablePorts().then(({ ports }) => this.updatePorts(ports));
|
||||
this.props.boardsService.getAvailablePorts().then(ports => this.updatePorts(ports));
|
||||
const { boardsServiceClient, coreServiceClient, daemonClient } = this.props;
|
||||
this.toDispose.pushAll([
|
||||
boardsServiceClient.onBoardsChanged(event => this.updatePorts(event.newState.ports, AttachedBoardsChangeEvent.diff(event).detached.ports)),
|
||||
boardsServiceClient.onAttachedBoardsChanged(event => this.updatePorts(event.newState.ports, AttachedBoardsChangeEvent.diff(event).detached.ports)),
|
||||
boardsServiceClient.onBoardsConfigChanged(({ selectedBoard, selectedPort }) => {
|
||||
this.setState({ selectedBoard, selectedPort }, () => this.fireConfigChanged());
|
||||
}),
|
||||
boardsServiceClient.onBoardsPackageInstalled(() => this.updateBoards(this.state.query)),
|
||||
boardsServiceClient.onBoardsPackageUninstalled(() => this.updateBoards(this.state.query)),
|
||||
coreServiceClient.onIndexUpdated(() => this.updateBoards(this.state.query)),
|
||||
daemonClient.onDaemonStarted(() => this.updateBoards(this.state.query)),
|
||||
daemonClient.onDaemonStopped(() => this.setState({ searchResults: [] }))
|
||||
@ -110,11 +112,11 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
|
||||
: eventOrQuery.target.value.toLowerCase()
|
||||
).trim();
|
||||
this.setState({ query });
|
||||
this.queryBoards({ query }).then(({ searchResults }) => this.setState({ searchResults }));
|
||||
this.queryBoards({ query }).then(searchResults => this.setState({ searchResults }));
|
||||
}
|
||||
|
||||
protected updatePorts = (ports: Port[] = [], removedPorts: Port[] = []) => {
|
||||
this.queryPorts(Promise.resolve({ ports })).then(({ knownPorts }) => {
|
||||
this.queryPorts(Promise.resolve(ports)).then(({ knownPorts }) => {
|
||||
let { selectedPort } = this.state;
|
||||
// If the currently selected port is not available anymore, unset the selected port.
|
||||
if (removedPorts.some(port => Port.equals(port, selectedPort))) {
|
||||
@ -124,35 +126,17 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
|
||||
});
|
||||
}
|
||||
|
||||
protected queryBoards = (options: { query?: string } = {}): Promise<{ searchResults: Array<Board & { packageName: string }> }> => {
|
||||
const { boardsService } = this.props;
|
||||
const query = (options.query || '').toLocaleLowerCase();
|
||||
return new Promise<{ searchResults: Array<Board & { packageName: string }> }>(resolve => {
|
||||
boardsService.search(options)
|
||||
.then(({ items }) => items
|
||||
.map(item => item.boards.map(board => ({ ...board, packageName: item.name })))
|
||||
.reduce((acc, curr) => acc.concat(curr), [])
|
||||
.filter(board => board.name.toLocaleLowerCase().indexOf(query) !== -1)
|
||||
.sort(Board.compare))
|
||||
.then(searchResults => resolve({ searchResults }));
|
||||
});
|
||||
protected queryBoards = (options: { query?: string } = {}): Promise<Array<Board & { packageName: string }>> => {
|
||||
return this.props.boardsService.searchBoards(options);
|
||||
}
|
||||
|
||||
protected get attachedBoards(): Promise<{ boards: Board[] }> {
|
||||
return this.props.boardsService.getAttachedBoards();
|
||||
}
|
||||
|
||||
protected get availablePorts(): Promise<{ ports: Port[] }> {
|
||||
protected get availablePorts(): Promise<Port[]> {
|
||||
return this.props.boardsService.getAvailablePorts();
|
||||
}
|
||||
|
||||
protected queryPorts = (availablePorts: Promise<{ ports: Port[] }> = this.availablePorts) => {
|
||||
return new Promise<{ knownPorts: Port[] }>(resolve => {
|
||||
availablePorts
|
||||
.then(({ ports }) => ports
|
||||
.sort(Port.compare))
|
||||
.then(knownPorts => resolve({ knownPorts }));
|
||||
});
|
||||
protected queryPorts = async (availablePorts: Promise<Port[]> = this.availablePorts) => {
|
||||
const ports = await availablePorts;
|
||||
return { knownPorts: ports.sort(Port.compare) };
|
||||
}
|
||||
|
||||
protected toggleFilterPorts = () => {
|
||||
@ -194,41 +178,20 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
|
||||
|
||||
protected renderBoards(): React.ReactNode {
|
||||
const { selectedBoard, searchResults } = this.state;
|
||||
// Board names are not unique. We show the corresponding core name as a detail.
|
||||
// https://github.com/arduino/arduino-cli/pull/294#issuecomment-513764948
|
||||
const distinctBoardNames = new Map<string, number>();
|
||||
for (const { name } of searchResults) {
|
||||
const counter = distinctBoardNames.get(name) || 0;
|
||||
distinctBoardNames.set(name, counter + 1);
|
||||
}
|
||||
|
||||
// Due to the non-unique board names, we have to check the package name as well.
|
||||
const selected = (board: Board & { packageName: string }) => {
|
||||
if (!!selectedBoard) {
|
||||
if (Board.equals(board, selectedBoard)) {
|
||||
if ('packageName' in selectedBoard) {
|
||||
return board.packageName === (selectedBoard as any).packageName;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return <React.Fragment>
|
||||
<div className='search'>
|
||||
<input type='search' className='theia-input' placeholder='SEARCH BOARD' onChange={this.updateBoards} ref={this.focusNodeSet} />
|
||||
<i className='fa fa-search'></i>
|
||||
</div>
|
||||
<div className='boards list'>
|
||||
{this.state.searchResults.map(board => <Item<Board & { packageName: string }>
|
||||
{Board.decorateBoards(selectedBoard, searchResults).map(board => <Item<Board & { packageName: string }>
|
||||
key={`${board.name}-${board.packageName}`}
|
||||
item={board}
|
||||
label={board.name}
|
||||
detail={(distinctBoardNames.get(board.name) || 0) > 1 ? ` - ${board.packageName}` : undefined}
|
||||
selected={selected(board)}
|
||||
details={board.details}
|
||||
selected={board.selected}
|
||||
onClick={this.selectBoard}
|
||||
missing={!Board.installed(board)}
|
||||
missing={board.missing}
|
||||
/>)}
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
@ -276,9 +239,9 @@ export namespace BoardsConfig {
|
||||
|
||||
export namespace Config {
|
||||
|
||||
export function sameAs(config: Config, other: Config | AttachedSerialBoard): boolean {
|
||||
export function sameAs(config: Config, other: Config | Board): boolean {
|
||||
const { selectedBoard, selectedPort } = config;
|
||||
if (AttachedSerialBoard.is(other)) {
|
||||
if (Board.is(other)) {
|
||||
return !!selectedBoard
|
||||
&& Board.equals(other, selectedBoard)
|
||||
&& Port.sameAs(selectedPort, other.port);
|
||||
|
@ -0,0 +1,90 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { CommandRegistry } from '@theia/core/lib/common/command';
|
||||
import { MenuModelRegistry, MenuNode } from '@theia/core/lib/common/menu';
|
||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { BoardsServiceClientImpl } from './boards-service-client-impl';
|
||||
import { Board, ConfigOption } from '../../common/protocol';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
|
||||
import { ArduinoMenus } from '../arduino-frontend-contribution';
|
||||
import { BoardsConfigStore } from './boards-config-store';
|
||||
import { MainMenuManager } from '../menu/main-menu-manager';
|
||||
|
||||
@injectable()
|
||||
export class BoardsDetailsMenuUpdater implements FrontendApplicationContribution {
|
||||
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
protected readonly mainMenuManager: MainMenuManager;
|
||||
|
||||
@inject(BoardsConfigStore)
|
||||
protected readonly boardsConfigStore: BoardsConfigStore;
|
||||
|
||||
@inject(BoardsServiceClientImpl)
|
||||
protected readonly boardsServiceClient: BoardsServiceClientImpl;
|
||||
|
||||
protected readonly toDisposeOnBoardChange = new DisposableCollection();
|
||||
|
||||
onStart(): void {
|
||||
this.boardsConfigStore.onChanged(() => this.updateMenuActions(this.boardsServiceClient.boardsConfig.selectedBoard));
|
||||
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) => this.updateMenuActions(selectedBoard));
|
||||
this.updateMenuActions(this.boardsServiceClient.boardsConfig.selectedBoard);
|
||||
}
|
||||
|
||||
protected async updateMenuActions(selectedBoard: Board | undefined): Promise<void> {
|
||||
if (selectedBoard) {
|
||||
this.toDisposeOnBoardChange.dispose();
|
||||
this.mainMenuManager.update();
|
||||
const { fqbn } = selectedBoard;
|
||||
if (fqbn) {
|
||||
const configOptions = await this.boardsConfigStore.getConfig(fqbn);
|
||||
const boardsConfigMenuPath = [...ArduinoMenus.TOOLS, 'z_boardsConfig']; // `z_` is for ordering.
|
||||
for (const { label, option, values } of configOptions.sort(ConfigOption.LABEL_COMPARATOR)) {
|
||||
const menuPath = [...boardsConfigMenuPath, `${option}`];
|
||||
const commands = new Map<string, Disposable>()
|
||||
for (const value of values) {
|
||||
const id = `${fqbn}-${option}--${value.value}`;
|
||||
const command = { id, label: value.label };
|
||||
const selectedValue = value.value;
|
||||
const handler = {
|
||||
execute: () => this.boardsConfigStore.setSelected({ fqbn, option, selectedValue }),
|
||||
isToggled: () => value.selected
|
||||
};
|
||||
commands.set(id, this.commandRegistry.registerCommand(command, handler));
|
||||
}
|
||||
this.menuRegistry.registerSubmenu(menuPath, label);
|
||||
this.toDisposeOnBoardChange.pushAll([
|
||||
...commands.values(),
|
||||
Disposable.create(() => this.unregisterSubmenu(menuPath)), // We cannot dispose submenu entries: https://github.com/eclipse-theia/theia/issues/7299
|
||||
...Array.from(commands.keys()).map((commandId, index) => {
|
||||
this.menuRegistry.registerMenuAction(menuPath, { commandId, order: String(index) })
|
||||
return Disposable.create(() => this.menuRegistry.unregisterMenuAction(commandId))
|
||||
})
|
||||
]);
|
||||
}
|
||||
this.mainMenuManager.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected unregisterSubmenu(menuPath: string[]): void {
|
||||
if (menuPath.length < 2) {
|
||||
throw new Error(`Expected at least two item as a menu-path. Got ${JSON.stringify(menuPath)} instead.`);
|
||||
}
|
||||
const toRemove = menuPath[menuPath.length - 1];
|
||||
const parentMenuPath = menuPath.slice(0, menuPath.length - 1);
|
||||
// This is unsafe. Calling `getMenu` with a non-existing menu-path will result in a new menu creation.
|
||||
// https://github.com/eclipse-theia/theia/issues/7300
|
||||
const parent = this.menuRegistry.getMenu(parentMenuPath);
|
||||
const index = parent.children.findIndex(({ id }) => id === toRemove);
|
||||
if (index === -1) {
|
||||
throw new Error(`Could not find menu with menu-path: ${JSON.stringify(menuPath)}.`);
|
||||
}
|
||||
(parent.children as Array<MenuNode>).splice(index, 1);
|
||||
}
|
||||
|
||||
}
|
@ -1,17 +1,17 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { BoardPackage, BoardsService } from '../../common/protocol/boards-service';
|
||||
import { BoardsPackage, BoardsService } from '../../common/protocol/boards-service';
|
||||
import { ListWidget } from '../components/component-list/list-widget';
|
||||
import { ListItemRenderer } from '../components/component-list/list-item-renderer';
|
||||
|
||||
@injectable()
|
||||
export class BoardsListWidget extends ListWidget<BoardPackage> {
|
||||
export class BoardsListWidget extends ListWidget<BoardsPackage> {
|
||||
|
||||
static WIDGET_ID = 'boards-list-widget';
|
||||
static WIDGET_LABEL = 'Boards Manager';
|
||||
|
||||
constructor(
|
||||
@inject(BoardsService) protected service: BoardsService,
|
||||
@inject(ListItemRenderer) protected itemRenderer: ListItemRenderer<BoardPackage>) {
|
||||
@inject(ListItemRenderer) protected itemRenderer: ListItemRenderer<BoardsPackage>) {
|
||||
|
||||
super({
|
||||
id: BoardsListWidget.WIDGET_ID,
|
||||
@ -19,7 +19,7 @@ export class BoardsListWidget extends ListWidget<BoardPackage> {
|
||||
iconClass: 'fa fa-microchip',
|
||||
searchable: service,
|
||||
installable: service,
|
||||
itemLabel: (item: BoardPackage) => item.name,
|
||||
itemLabel: (item: BoardsPackage) => item.name,
|
||||
itemRenderer
|
||||
});
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { injectable, inject, optional } from 'inversify';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { LocalStorageService } from '@theia/core/lib/browser/storage-service';
|
||||
import { StorageService } from '@theia/core/lib/browser/storage-service';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { RecursiveRequired } from '../../common/types';
|
||||
import { BoardsServiceClient, AttachedBoardsChangeEvent, BoardInstalledEvent, AttachedSerialBoard, Board, Port, BoardUninstalledEvent } from '../../common/protocol/boards-service';
|
||||
import { BoardsServiceClient, AttachedBoardsChangeEvent, BoardInstalledEvent, Board, Port, BoardUninstalledEvent } from '../../common/protocol';
|
||||
import { BoardsConfig } from './boards-config';
|
||||
|
||||
@injectable()
|
||||
@ -14,16 +14,18 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
|
||||
@inject(ILogger)
|
||||
protected logger: ILogger;
|
||||
|
||||
@optional()
|
||||
@inject(MessageService)
|
||||
protected messageService: MessageService;
|
||||
|
||||
@inject(LocalStorageService)
|
||||
protected storageService: LocalStorageService;
|
||||
@inject(StorageService)
|
||||
protected storageService: StorageService;
|
||||
|
||||
protected readonly onBoardInstalledEmitter = new Emitter<BoardInstalledEvent>();
|
||||
protected readonly onBoardUninstalledEmitter = new Emitter<BoardUninstalledEvent>();
|
||||
protected readonly onBoardsPackageInstalledEmitter = new Emitter<BoardInstalledEvent>();
|
||||
protected readonly onBoardsPackageUninstalledEmitter = new Emitter<BoardUninstalledEvent>();
|
||||
protected readonly onAttachedBoardsChangedEmitter = new Emitter<AttachedBoardsChangeEvent>();
|
||||
protected readonly onSelectedBoardsConfigChangedEmitter = new Emitter<BoardsConfig.Config>();
|
||||
protected readonly onBoardsConfigChangedEmitter = new Emitter<BoardsConfig.Config>();
|
||||
protected readonly onAvailableBoardsChangedEmitter = new Emitter<AvailableBoard[]>();
|
||||
|
||||
/**
|
||||
* Used for the auto-reconnecting. Sometimes, the attached board gets disconnected after uploading something to it.
|
||||
@ -34,35 +36,51 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
|
||||
*/
|
||||
protected latestValidBoardsConfig: RecursiveRequired<BoardsConfig.Config> | undefined = undefined;
|
||||
protected _boardsConfig: BoardsConfig.Config = {};
|
||||
protected _attachedBoards: Board[] = []; // This does not contain the `Unknown` boards. They're visible from the available ports only.
|
||||
protected _availablePorts: Port[] = [];
|
||||
protected _availableBoards: AvailableBoard[] = [];
|
||||
|
||||
readonly onBoardsChanged = this.onAttachedBoardsChangedEmitter.event;
|
||||
readonly onBoardInstalled = this.onBoardInstalledEmitter.event;
|
||||
readonly onBoardUninstalled = this.onBoardUninstalledEmitter.event;
|
||||
readonly onBoardsConfigChanged = this.onSelectedBoardsConfigChangedEmitter.event;
|
||||
/**
|
||||
* Event when the state of the attached/detached boards has changed. For instance, the user have detached a physical board.
|
||||
*/
|
||||
readonly onAttachedBoardsChanged = this.onAttachedBoardsChangedEmitter.event;
|
||||
readonly onBoardsPackageInstalled = this.onBoardsPackageInstalledEmitter.event;
|
||||
readonly onBoardsPackageUninstalled = this.onBoardsPackageUninstalledEmitter.event;
|
||||
/**
|
||||
* Unlike `onAttachedBoardsChanged` this even fires when the user modifies the selected board in the IDE.\
|
||||
* This even also fires, when the boards package was not available for the currently selected board,
|
||||
* and the user installs the board package. Note: installing a board package will set the `fqbn` of the
|
||||
* currently selected board.\
|
||||
* This even also emitted when the board package for the currently selected board was uninstalled.
|
||||
*/
|
||||
readonly onBoardsConfigChanged = this.onBoardsConfigChangedEmitter.event;
|
||||
readonly onAvailableBoardsChanged = this.onAvailableBoardsChangedEmitter.event;
|
||||
|
||||
async onStart(): Promise<void> {
|
||||
return this.loadState();
|
||||
}
|
||||
|
||||
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
|
||||
this.logger.info('Attached boards and available ports changed: ', JSON.stringify(event));
|
||||
const { detached, attached } = AttachedBoardsChangeEvent.diff(event);
|
||||
const { selectedPort, selectedBoard } = this.boardsConfig;
|
||||
this.onAttachedBoardsChangedEmitter.fire(event);
|
||||
// Dynamically unset the port if is not available anymore. A port can be "detached" when removing a board.
|
||||
if (detached.ports.some(port => Port.equals(selectedPort, port))) {
|
||||
this.boardsConfig = {
|
||||
selectedBoard,
|
||||
selectedPort: undefined
|
||||
};
|
||||
}
|
||||
// Try to reconnect.
|
||||
this.tryReconnect(attached.boards, attached.ports);
|
||||
/**
|
||||
* When the FE connects to the BE, the BE stets the known boards and ports.\
|
||||
* This is a DI workaround for not being able to inject the service into the client.
|
||||
*/
|
||||
init({ attachedBoards, availablePorts }: { attachedBoards: Board[], availablePorts: Port[] }): void {
|
||||
this._attachedBoards = attachedBoards;
|
||||
this._availablePorts = availablePorts;
|
||||
this.reconcileAvailableBoards().then(() => this.tryReconnect());
|
||||
}
|
||||
|
||||
async tryReconnect(attachedBoards: Board[], availablePorts: Port[]): Promise<boolean> {
|
||||
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
|
||||
this.logger.info('Attached boards and available ports changed: ', JSON.stringify(event));
|
||||
this._attachedBoards = event.newState.boards;
|
||||
this.onAttachedBoardsChangedEmitter.fire(event);
|
||||
this._availablePorts = event.newState.ports;
|
||||
this.reconcileAvailableBoards().then(() => this.tryReconnect());
|
||||
}
|
||||
|
||||
protected async tryReconnect(): Promise<boolean> {
|
||||
if (this.latestValidBoardsConfig && !this.canUploadTo(this.boardsConfig)) {
|
||||
for (const board of attachedBoards.filter(AttachedSerialBoard.is)) {
|
||||
for (const board of this.availableBoards.filter(({ state }) => state !== AvailableBoard.State.incomplete)) {
|
||||
if (this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn
|
||||
&& this.latestValidBoardsConfig.selectedBoard.name === board.name
|
||||
&& Port.sameAs(this.latestValidBoardsConfig.selectedPort, board.port)) {
|
||||
@ -73,13 +91,13 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
|
||||
}
|
||||
// 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)) {
|
||||
for (const board of this.availableBoards.filter(({ state }) => state !== AvailableBoard.State.incomplete)) {
|
||||
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))
|
||||
selectedPort: board.port
|
||||
};
|
||||
return true;
|
||||
}
|
||||
@ -90,21 +108,52 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
|
||||
|
||||
notifyBoardInstalled(event: BoardInstalledEvent): void {
|
||||
this.logger.info('Board installed: ', JSON.stringify(event));
|
||||
this.onBoardInstalledEmitter.fire(event);
|
||||
this.onBoardsPackageInstalledEmitter.fire(event);
|
||||
const { selectedBoard } = this.boardsConfig;
|
||||
const { installedVersion, id } = event.pkg;
|
||||
if (selectedBoard) {
|
||||
const installedBoard = event.pkg.boards.find(({ name }) => name === selectedBoard.name);
|
||||
if (installedBoard && (!selectedBoard.fqbn || selectedBoard.fqbn === installedBoard.fqbn)) {
|
||||
this.logger.info(`Board package ${id}[${installedVersion}] was installed. Updating the FQBN of the currently selected ${selectedBoard.name} board. [FQBN: ${installedBoard.fqbn}]`);
|
||||
this.boardsConfig = {
|
||||
...this.boardsConfig,
|
||||
selectedBoard: installedBoard
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notifyBoardUninstalled(event: BoardUninstalledEvent): void {
|
||||
this.logger.info('Board uninstalled: ', JSON.stringify(event));
|
||||
this.onBoardUninstalledEmitter.fire(event);
|
||||
this.onBoardsPackageUninstalledEmitter.fire(event);
|
||||
const { selectedBoard } = this.boardsConfig;
|
||||
if (selectedBoard && selectedBoard.fqbn) {
|
||||
const uninstalledBoard = event.pkg.boards.find(({ name }) => name === selectedBoard.name);
|
||||
if (uninstalledBoard && uninstalledBoard.fqbn === selectedBoard.fqbn) {
|
||||
this.logger.info(`Board package ${event.pkg.id} was uninstalled. Discarding the FQBN of the currently selected ${selectedBoard.name} board.`);
|
||||
const selectedBoardWithoutFqbn = {
|
||||
name: selectedBoard.name
|
||||
// No FQBN
|
||||
};
|
||||
this.boardsConfig = {
|
||||
...this.boardsConfig,
|
||||
selectedBoard: selectedBoardWithoutFqbn
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set boardsConfig(config: BoardsConfig.Config) {
|
||||
this.doSetBoardsConfig(config);
|
||||
this.saveState().finally(() => this.reconcileAvailableBoards().finally(() => this.onBoardsConfigChangedEmitter.fire(this._boardsConfig)));
|
||||
}
|
||||
|
||||
protected doSetBoardsConfig(config: BoardsConfig.Config): void {
|
||||
this.logger.info('Board config changed: ', JSON.stringify(config));
|
||||
this._boardsConfig = config;
|
||||
if (this.canUploadTo(this._boardsConfig)) {
|
||||
this.latestValidBoardsConfig = this._boardsConfig;
|
||||
}
|
||||
this.saveState().then(() => this.onSelectedBoardsConfigChangedEmitter.fire(this._boardsConfig));
|
||||
}
|
||||
|
||||
get boardsConfig(): BoardsConfig.Config {
|
||||
@ -123,7 +172,7 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
|
||||
}
|
||||
|
||||
if (!config.selectedBoard) {
|
||||
if (!options.silent) {
|
||||
if (!options.silent && this.messageService) {
|
||||
this.messageService.warn('No boards selected.', { timeout: 3000 });
|
||||
}
|
||||
return false;
|
||||
@ -133,7 +182,7 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
|
||||
}
|
||||
|
||||
/**
|
||||
* `true` if the `canVerify` and the `config.selectedPort` is also set with FQBN, hence can upload to board. Otherwise, `false`.
|
||||
* `true` if `canVerify`, the board has an FQBN and the `config.selectedPort` is also set, hence can upload to board. Otherwise, `false`.
|
||||
*/
|
||||
canUploadTo(
|
||||
config: BoardsConfig.Config | undefined = this.boardsConfig,
|
||||
@ -145,14 +194,14 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
|
||||
|
||||
const { name } = config.selectedBoard;
|
||||
if (!config.selectedPort) {
|
||||
if (!options.silent) {
|
||||
if (!options.silent && this.messageService) {
|
||||
this.messageService.warn(`No ports selected for board: '${name}'.`, { timeout: 3000 });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!config.selectedBoard.fqbn) {
|
||||
if (!options.silent) {
|
||||
if (!options.silent && this.messageService) {
|
||||
this.messageService.warn(`The FQBN is not available for the selected board ${name}. Do you have the corresponding core installed?`, { timeout: 3000 });
|
||||
}
|
||||
return false;
|
||||
@ -161,8 +210,93 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
|
||||
return true;
|
||||
}
|
||||
|
||||
protected saveState(): Promise<void> {
|
||||
return this.storageService.setData('latest-valid-boards-config', this.latestValidBoardsConfig);
|
||||
get availableBoards(): AvailableBoard[] {
|
||||
return this._availableBoards;
|
||||
}
|
||||
|
||||
protected async reconcileAvailableBoards(): Promise<void> {
|
||||
const attachedBoards = this._attachedBoards;
|
||||
const availablePorts = this._availablePorts;
|
||||
// Unset the port on the user's config, if it is not available anymore.
|
||||
if (this.boardsConfig.selectedPort && !availablePorts.some(port => Port.sameAs(port, this.boardsConfig.selectedPort))) {
|
||||
this.doSetBoardsConfig({ selectedBoard: this.boardsConfig.selectedBoard, selectedPort: undefined });
|
||||
this.onBoardsConfigChangedEmitter.fire(this._boardsConfig);
|
||||
}
|
||||
const boardsConfig = this.boardsConfig;
|
||||
const currentAvailableBoards = this._availableBoards;
|
||||
const availableBoards: AvailableBoard[] = [];
|
||||
const availableBoardPorts = availablePorts.filter(Port.isBoardPort);
|
||||
const attachedSerialBoards = attachedBoards.filter(({ port }) => !!port);
|
||||
|
||||
for (const boardPort of availableBoardPorts) {
|
||||
let state = AvailableBoard.State.incomplete; // Initial pessimism.
|
||||
let board = attachedSerialBoards.find(({ port }) => Port.sameAs(boardPort, port));
|
||||
if (board) {
|
||||
state = AvailableBoard.State.recognized;
|
||||
} else {
|
||||
// If the selected board is not recognized because it is a 3rd party board: https://github.com/arduino/arduino-cli/issues/623
|
||||
// We still want to show it without the red X in the boards toolbar: https://github.com/arduino/arduino-pro-ide/issues/198#issuecomment-599355836
|
||||
const lastSelectedBoard = await this.getLastSelectedBoardOnPort(boardPort);
|
||||
if (lastSelectedBoard) {
|
||||
board = {
|
||||
...lastSelectedBoard,
|
||||
port: boardPort
|
||||
};
|
||||
state = AvailableBoard.State.guessed;
|
||||
}
|
||||
}
|
||||
if (!board) {
|
||||
availableBoards.push({ name: 'Unknown', port: boardPort, state });
|
||||
} else {
|
||||
const selected = BoardsConfig.Config.sameAs(boardsConfig, board);
|
||||
availableBoards.push({ ...board, state, selected, port: boardPort });
|
||||
}
|
||||
}
|
||||
|
||||
if (boardsConfig.selectedBoard && !availableBoards.some(({ selected }) => selected)) {
|
||||
availableBoards.push({
|
||||
...boardsConfig.selectedBoard,
|
||||
port: boardsConfig.selectedPort,
|
||||
selected: true,
|
||||
state: AvailableBoard.State.incomplete
|
||||
});
|
||||
}
|
||||
|
||||
const sortedAvailableBoards = availableBoards.sort(AvailableBoard.COMPARATOR);
|
||||
let hasChanged = sortedAvailableBoards.length !== currentAvailableBoards.length;
|
||||
for (let i = 0; !hasChanged && i < sortedAvailableBoards.length; i++) {
|
||||
hasChanged = AvailableBoard.COMPARATOR(sortedAvailableBoards[i], currentAvailableBoards[i]) !== 0;
|
||||
}
|
||||
if (hasChanged) {
|
||||
this._availableBoards = sortedAvailableBoards;
|
||||
this.onAvailableBoardsChangedEmitter.fire(this._availableBoards);
|
||||
}
|
||||
}
|
||||
|
||||
protected async getLastSelectedBoardOnPort(port: Port | string | undefined): Promise<Board | undefined> {
|
||||
if (!port) {
|
||||
return undefined;
|
||||
}
|
||||
const key = this.getLastSelectedBoardOnPortKey(port);
|
||||
return this.storageService.getData<Board>(key);
|
||||
}
|
||||
|
||||
protected async saveState(): Promise<void> {
|
||||
// We save the port with the selected board name/FQBN, to be able to guess a better board name.
|
||||
// Required when the attached board belongs to a 3rd party boards package, and neither the name, nor
|
||||
// the FQBN can be retrieved with a `board list` command.
|
||||
// https://github.com/arduino/arduino-cli/issues/623
|
||||
const { selectedBoard, selectedPort } = this.boardsConfig;
|
||||
if (selectedBoard && selectedPort) {
|
||||
const key = this.getLastSelectedBoardOnPortKey(selectedPort);
|
||||
await this.storageService.setData(key, selectedBoard);
|
||||
}
|
||||
await this.storageService.setData('latest-valid-boards-config', this.latestValidBoardsConfig);
|
||||
}
|
||||
|
||||
protected getLastSelectedBoardOnPortKey(port: Port | string): string {
|
||||
// TODO: we lose the port's `protocol` info (`serial`, `network`, etc.) here if the `port` is a `string`.
|
||||
return `last-selected-board-on-port:${typeof port === 'string' ? port : Port.toString(port)}`;
|
||||
}
|
||||
|
||||
protected async loadState(): Promise<void> {
|
||||
@ -176,3 +310,63 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Representation of a ready-to-use board, configured by the user. Not all of the available boards are
|
||||
* necessarily recognized by the CLI (e.g.: it is a 3rd party board) or correctly configured but ready for `verify`.
|
||||
* If it has the selected board and a associated port, it can be used for `upload`.
|
||||
*/
|
||||
export interface AvailableBoard extends Board {
|
||||
readonly state: AvailableBoard.State;
|
||||
readonly selected?: boolean;
|
||||
readonly port?: Port;
|
||||
}
|
||||
|
||||
export namespace AvailableBoard {
|
||||
|
||||
export enum State {
|
||||
/**
|
||||
* Retrieved from the CLI via the `board list` command.
|
||||
*/
|
||||
'recognized',
|
||||
/**
|
||||
* Guessed the name/FQBN of the board from the available board ports (3rd party).
|
||||
*/
|
||||
'guessed',
|
||||
/**
|
||||
* We do not know anything about this board, probably a 3rd party. The user has not selected a board for this port yet.
|
||||
*/
|
||||
'incomplete'
|
||||
}
|
||||
|
||||
export function isWithPort(board: AvailableBoard): board is AvailableBoard & { port: Port } {
|
||||
return !!board.port;
|
||||
}
|
||||
|
||||
export const COMPARATOR = (left: AvailableBoard, right: AvailableBoard) => {
|
||||
let result = left.name.localeCompare(right.name);
|
||||
if (result !== 0) {
|
||||
return result;
|
||||
}
|
||||
if (left.fqbn && right.fqbn) {
|
||||
result = left.name.localeCompare(right.name);
|
||||
if (result !== 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
if (left.port && right.port) {
|
||||
result = Port.compare(left.port, right.port);
|
||||
if (result !== 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
if (!!left.selected && !right.selected) {
|
||||
return -1;
|
||||
}
|
||||
if (!!right.selected && !left.selected) {
|
||||
return 1;
|
||||
}
|
||||
return left.state - right.state;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { CommandRegistry, DisposableCollection } from '@theia/core';
|
||||
import { BoardsService, Board, AttachedSerialBoard, Port } from '../../common/protocol/boards-service';
|
||||
import { ArduinoCommands } from '../arduino-commands';
|
||||
import { BoardsServiceClientImpl } from './boards-service-client-impl';
|
||||
import { CommandRegistry } from '@theia/core/lib/common/command';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { Port } from '../../common/protocol';
|
||||
import { BoardsConfig } from './boards-config';
|
||||
import { ArduinoCommands } from '../arduino-commands';
|
||||
import { BoardsServiceClientImpl, AvailableBoard } from './boards-service-client-impl';
|
||||
|
||||
export interface BoardsDropDownListCoords {
|
||||
readonly top: number;
|
||||
@ -16,14 +17,9 @@ export interface BoardsDropDownListCoords {
|
||||
export namespace BoardsDropDown {
|
||||
export interface Props {
|
||||
readonly coords: BoardsDropDownListCoords | 'hidden';
|
||||
readonly items: Item[];
|
||||
readonly items: Array<AvailableBoard & { onClick: () => void, port: Port }>;
|
||||
readonly openBoardsConfig: () => void;
|
||||
}
|
||||
export interface Item {
|
||||
readonly label: string;
|
||||
readonly selected: boolean;
|
||||
readonly onClick: () => void;
|
||||
}
|
||||
}
|
||||
|
||||
export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
|
||||
@ -51,48 +47,30 @@ export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
|
||||
if (coords === 'hidden') {
|
||||
return '';
|
||||
}
|
||||
items.push({
|
||||
label: 'Select Other Board & Port',
|
||||
selected: false,
|
||||
onClick: () => this.props.openBoardsConfig()
|
||||
})
|
||||
return <div className='arduino-boards-dropdown-list'
|
||||
style={{
|
||||
position: 'absolute',
|
||||
...coords
|
||||
}}>
|
||||
{items.map(this.renderItem)}
|
||||
{this.renderItem({
|
||||
label: 'Select Other Board & Port',
|
||||
onClick: () => this.props.openBoardsConfig()
|
||||
})}
|
||||
{items.map(({ name, port, selected, onClick }) => ({ label: `${name} at ${Port.toString(port)}`, selected, onClick })).map(this.renderItem)}
|
||||
</div>
|
||||
}
|
||||
|
||||
protected renderItem(item: BoardsDropDown.Item): React.ReactNode {
|
||||
const { label, selected, onClick } = item;
|
||||
protected renderItem({ label, selected, onClick }: { label: string, selected?: boolean, onClick: () => void }): React.ReactNode {
|
||||
return <div key={label} className={`arduino-boards-dropdown-item ${selected ? 'selected' : ''}`} onClick={onClick}>
|
||||
<div>
|
||||
{label}
|
||||
</div>
|
||||
{selected ? <span className='fa fa-check'/> : ''}
|
||||
{selected ? <span className='fa fa-check' /> : ''}
|
||||
</div>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace BoardsToolBarItem {
|
||||
|
||||
export interface Props {
|
||||
readonly boardService: BoardsService;
|
||||
readonly boardsServiceClient: BoardsServiceClientImpl;
|
||||
readonly commands: CommandRegistry;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
boardsConfig: BoardsConfig.Config;
|
||||
attachedBoards: Board[];
|
||||
availablePorts: Port[];
|
||||
coords: BoardsDropDownListCoords | 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props, BoardsToolBarItem.State> {
|
||||
|
||||
static TOOLBAR_ID: 'boards-toolbar';
|
||||
@ -102,10 +80,9 @@ export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props,
|
||||
constructor(props: BoardsToolBarItem.Props) {
|
||||
super(props);
|
||||
|
||||
const { availableBoards } = props.boardsServiceClient;
|
||||
this.state = {
|
||||
boardsConfig: this.props.boardsServiceClient.boardsConfig,
|
||||
attachedBoards: [],
|
||||
availablePorts: [],
|
||||
availableBoards,
|
||||
coords: 'hidden'
|
||||
};
|
||||
|
||||
@ -115,17 +92,7 @@ export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { boardsServiceClient: client, boardService } = this.props;
|
||||
this.toDispose.pushAll([
|
||||
client.onBoardsConfigChanged(boardsConfig => this.setState({ boardsConfig })),
|
||||
client.onBoardsChanged(({ newState }) => this.setState({ attachedBoards: newState.boards, availablePorts: newState.ports }))
|
||||
]);
|
||||
Promise.all([
|
||||
boardService.getAttachedBoards(),
|
||||
boardService.getAvailablePorts()
|
||||
]).then(([{boards: attachedBoards}, { ports: availablePorts }]) => {
|
||||
this.setState({ attachedBoards, availablePorts })
|
||||
});
|
||||
this.props.boardsServiceClient.onAvailableBoardsChanged(availableBoards => this.setState({ availableBoards }));
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
@ -146,7 +113,7 @@ export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props,
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.setState({ coords: 'hidden'});
|
||||
this.setState({ coords: 'hidden' });
|
||||
}
|
||||
}
|
||||
event.stopPropagation();
|
||||
@ -154,41 +121,52 @@ export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props,
|
||||
};
|
||||
|
||||
render(): React.ReactNode {
|
||||
const { boardsConfig, coords, attachedBoards, availablePorts } = this.state;
|
||||
const { coords, availableBoards } = this.state;
|
||||
const boardsConfig = this.props.boardsServiceClient.boardsConfig;
|
||||
const title = BoardsConfig.Config.toString(boardsConfig, { default: 'no board selected' });
|
||||
const configuredBoard = attachedBoards
|
||||
.filter(AttachedSerialBoard.is)
|
||||
.filter(board => availablePorts.some(port => Port.sameAs(port, board.port)))
|
||||
.filter(board => BoardsConfig.Config.sameAs(boardsConfig, board)).shift();
|
||||
|
||||
const items = attachedBoards.filter(AttachedSerialBoard.is).map(board => ({
|
||||
label: `${board.name} at ${board.port}`,
|
||||
selected: configuredBoard === board,
|
||||
onClick: () => {
|
||||
this.props.boardsServiceClient.boardsConfig = {
|
||||
selectedBoard: board,
|
||||
selectedPort: availablePorts.find(port => Port.sameAs(port, board.port))
|
||||
}
|
||||
const decorator = (() => {
|
||||
const selectedBoard = availableBoards.find(({ selected }) => selected);
|
||||
if (!selectedBoard || !selectedBoard.port) {
|
||||
return 'fa fa-times notAttached'
|
||||
}
|
||||
}));
|
||||
if (selectedBoard.state === AvailableBoard.State.guessed) {
|
||||
return 'fa fa-exclamation-triangle guessed'
|
||||
}
|
||||
return ''
|
||||
})();
|
||||
|
||||
return <React.Fragment>
|
||||
<div className='arduino-boards-toolbar-item-container'>
|
||||
<div className='arduino-boards-toolbar-item' title={title}>
|
||||
<div className='inner-container' onClick={this.show}>
|
||||
<span className={!configuredBoard ? 'fa fa-times notAttached' : ''}/>
|
||||
<span className={decorator} />
|
||||
<div className='label noWrapInfo'>
|
||||
<div className='noWrapInfo noselect'>
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
<span className='fa fa-caret-down caret'/>
|
||||
<span className='fa fa-caret-down caret' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BoardsDropDown
|
||||
coords={coords}
|
||||
items={items}
|
||||
items={availableBoards.filter(AvailableBoard.isWithPort).map(board => ({
|
||||
...board,
|
||||
onClick: () => {
|
||||
if (board.state === AvailableBoard.State.incomplete) {
|
||||
this.props.boardsServiceClient.boardsConfig = {
|
||||
selectedPort: board.port
|
||||
};
|
||||
this.openDialog();
|
||||
} else {
|
||||
this.props.boardsServiceClient.boardsConfig = {
|
||||
selectedBoard: board,
|
||||
selectedPort: board.port
|
||||
}
|
||||
}
|
||||
}
|
||||
}))}
|
||||
openBoardsConfig={this.openDialog}>
|
||||
</BoardsDropDown>
|
||||
</React.Fragment>;
|
||||
@ -200,3 +178,16 @@ export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props,
|
||||
};
|
||||
|
||||
}
|
||||
export namespace BoardsToolBarItem {
|
||||
|
||||
export interface Props {
|
||||
readonly boardsServiceClient: BoardsServiceClientImpl;
|
||||
readonly commands: CommandRegistry;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
availableBoards: AvailableBoard[];
|
||||
coords: BoardsDropDownListCoords | 'hidden';
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,11 +2,11 @@ import { injectable } from 'inversify';
|
||||
import { MenuModelRegistry } from '@theia/core';
|
||||
import { BoardsListWidget } from './boards-list-widget';
|
||||
import { ArduinoMenus } from '../arduino-frontend-contribution';
|
||||
import { BoardPackage } from '../../common/protocol/boards-service';
|
||||
import { BoardsPackage } from '../../common/protocol/boards-service';
|
||||
import { ListWidgetFrontendContribution } from '../components/component-list/list-widget-frontend-contribution';
|
||||
|
||||
@injectable()
|
||||
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<BoardPackage> {
|
||||
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<BoardsPackage> {
|
||||
|
||||
static readonly OPEN_MANAGER = `${BoardsListWidget.WIDGET_ID}:toggle`;
|
||||
|
||||
|
@ -72,12 +72,7 @@ export class FilterableListContainer<T extends ArduinoComponent> extends React.C
|
||||
|
||||
protected search(query: string): void {
|
||||
const { searchable } = this.props;
|
||||
searchable.search({ query: query.trim() }).then(result => {
|
||||
const { items } = result;
|
||||
this.setState({
|
||||
items: this.sort(items)
|
||||
});
|
||||
});
|
||||
searchable.search({ query: query.trim() }).then(items => this.setState({ items: this.sort(items) }));
|
||||
}
|
||||
|
||||
protected sort(items: T[]): T[] {
|
||||
@ -91,7 +86,7 @@ export class FilterableListContainer<T extends ArduinoComponent> extends React.C
|
||||
dialog.open();
|
||||
try {
|
||||
await installable.install({ item, version });
|
||||
const { items } = await searchable.search({ query: this.state.filterText });
|
||||
const items = await searchable.search({ query: this.state.filterText });
|
||||
this.setState({ items: this.sort(items) });
|
||||
} finally {
|
||||
dialog.close();
|
||||
@ -113,7 +108,7 @@ export class FilterableListContainer<T extends ArduinoComponent> extends React.C
|
||||
dialog.open();
|
||||
try {
|
||||
await installable.uninstall({ item });
|
||||
const { items } = await searchable.search({ query: this.state.filterText });
|
||||
const items = await searchable.search({ query: this.state.filterText });
|
||||
this.setState({ items: this.sort(items) });
|
||||
} finally {
|
||||
dialog.close();
|
||||
|
@ -1,16 +1,17 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { ApplicationShell, FrontendApplicationContribution, FrontendApplication, Widget } from '@theia/core/lib/browser';
|
||||
import { OutputWidget } from '@theia/output/lib/browser/output-widget';
|
||||
import { EditorWidget } from '@theia/editor/lib/browser';
|
||||
import { ArduinoShellLayoutRestorer } from './shell/arduino-shell-layout-restorer';
|
||||
import { OutputWidget } from '@theia/output/lib/browser/output-widget';
|
||||
import { MainMenuManager } from './menu/main-menu-manager';
|
||||
import { BoardsListWidget } from './boards/boards-list-widget';
|
||||
import { LibraryListWidget } from './library/library-list-widget';
|
||||
import { ArduinoShellLayoutRestorer } from './shell/arduino-shell-layout-restorer';
|
||||
|
||||
@injectable()
|
||||
export class EditorMode implements FrontendApplicationContribution {
|
||||
|
||||
readonly menuContentChanged = new Emitter<void>();
|
||||
@inject(MainMenuManager)
|
||||
protected readonly mainMenuManager: MainMenuManager;
|
||||
|
||||
protected app: FrontendApplication;
|
||||
|
||||
@ -62,6 +63,7 @@ export class EditorMode implements FrontendApplicationContribution {
|
||||
const oldState = this.compileForDebug;
|
||||
const newState = !oldState;
|
||||
window.localStorage.setItem(EditorMode.COMPILE_FOR_DEBUG_KEY, String(newState));
|
||||
this.mainMenuManager.update();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ import { injectable, inject, postConstruct } from 'inversify';
|
||||
import { BaseLanguageClientContribution } from '@theia/languages/lib/browser';
|
||||
import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl';
|
||||
import { BoardsConfig } from '../boards/boards-config';
|
||||
import { Board, BoardPackage } from '../../common/protocol/boards-service';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoLanguageClientContribution extends BaseLanguageClientContribution {
|
||||
@ -26,18 +25,6 @@ export class ArduinoLanguageClientContribution extends BaseLanguageClientContrib
|
||||
@postConstruct()
|
||||
protected init() {
|
||||
this.boardsServiceClient.onBoardsConfigChanged(this.selectBoard.bind(this));
|
||||
const restartIfAffected = (pkg: BoardPackage) => {
|
||||
if (!this.boardConfig) {
|
||||
this.restart();
|
||||
return;
|
||||
}
|
||||
const { selectedBoard } = this.boardConfig;
|
||||
if (selectedBoard && pkg.boards.some(board => Board.sameAs(board, selectedBoard))) {
|
||||
this.restart();
|
||||
}
|
||||
}
|
||||
this.boardsServiceClient.onBoardInstalled(({ pkg }) => restartIfAffected(pkg));
|
||||
this.boardsServiceClient.onBoardUninstalled(({ pkg }) => restartIfAffected(pkg));
|
||||
}
|
||||
|
||||
selectBoard(config: BoardsConfig.Config): void {
|
||||
|
@ -0,0 +1,22 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { BrowserMainMenuFactory, MenuBarWidget } from '@theia/core/lib/browser/menu/browser-menu-plugin';
|
||||
import { MainMenuManager } from './main-menu-manager';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoBrowserMainMenuFactory extends BrowserMainMenuFactory implements MainMenuManager {
|
||||
|
||||
protected menuBar: MenuBarWidget | undefined;
|
||||
|
||||
createMenuBar(): MenuBarWidget {
|
||||
this.menuBar = super.createMenuBar();
|
||||
return this.menuBar;
|
||||
}
|
||||
|
||||
update() {
|
||||
if (this.menuBar) {
|
||||
this.menuBar.clearMenus();
|
||||
this.fillMenuBar(this.menuBar);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,10 +1,14 @@
|
||||
import { BrowserMenuBarContribution } from '@theia/core/lib/browser/menu/browser-menu-plugin';
|
||||
import { ArduinoMenuContribution } from './arduino-menu-contribution';
|
||||
import { ContainerModule, interfaces } from 'inversify';
|
||||
|
||||
import '../../../src/browser/style/browser-menu.css'
|
||||
import { ContainerModule } from 'inversify';
|
||||
import { BrowserMenuBarContribution, BrowserMainMenuFactory } from '@theia/core/lib/browser/menu/browser-menu-plugin';
|
||||
import { MainMenuManager } from './main-menu-manager';
|
||||
import { ArduinoMenuContribution } from './arduino-menu-contribution';
|
||||
import { ArduinoBrowserMainMenuFactory } from './arduino-browser-main-menu-factory';
|
||||
|
||||
export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind) => {
|
||||
unbind(BrowserMenuBarContribution);
|
||||
bind(BrowserMenuBarContribution).to(ArduinoMenuContribution).inSingletonScope();
|
||||
|
||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(ArduinoBrowserMainMenuFactory).toSelf().inSingletonScope();
|
||||
bind(MainMenuManager).toService(ArduinoBrowserMainMenuFactory);
|
||||
rebind(BrowserMainMenuFactory).toService(ArduinoBrowserMainMenuFactory);
|
||||
rebind(BrowserMenuBarContribution).to(ArduinoMenuContribution).inSingletonScope();
|
||||
});
|
||||
|
@ -0,0 +1,8 @@
|
||||
export const MainMenuManager = Symbol('MainMenuManager');
|
||||
export interface MainMenuManager {
|
||||
/**
|
||||
* Call this method if you have changed the content of the main menu (updated a toggle flag, removed/added new groups or menu items)
|
||||
* and you want to re-render it from scratch. Works for electron too.
|
||||
*/
|
||||
update(): void;
|
||||
}
|
@ -4,7 +4,7 @@ import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||
import { MonitorService, MonitorConfig, MonitorError, Status, MonitorReadEvent } from '../../common/protocol/monitor-service';
|
||||
import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl';
|
||||
import { Port, Board, BoardsService, AttachedSerialBoard, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service';
|
||||
import { Port, Board, BoardsService, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service';
|
||||
import { MonitorServiceClientImpl } from './monitor-service-client-impl';
|
||||
import { BoardsConfig } from '../boards/boards-config';
|
||||
import { MonitorModel } from './monitor-model';
|
||||
@ -110,12 +110,12 @@ export class MonitorConnection {
|
||||
}
|
||||
});
|
||||
this.boardsServiceClient.onBoardsConfigChanged(this.handleBoardConfigChange.bind(this));
|
||||
this.boardsServiceClient.onBoardsChanged(event => {
|
||||
this.boardsServiceClient.onAttachedBoardsChanged(event => {
|
||||
if (this.autoConnect && this.connected) {
|
||||
const { boardsConfig } = this.boardsServiceClient;
|
||||
if (this.boardsServiceClient.canUploadTo(boardsConfig, { silent: false })) {
|
||||
const { attached } = AttachedBoardsChangeEvent.diff(event);
|
||||
if (attached.boards.some(board => AttachedSerialBoard.is(board) && BoardsConfig.Config.sameAs(boardsConfig, board))) {
|
||||
if (attached.boards.some(board => !!board.port && BoardsConfig.Config.sameAs(boardsConfig, board))) {
|
||||
const { selectedBoard: board, selectedPort: port } = boardsConfig;
|
||||
const { baudRate } = this.monitorModel;
|
||||
this.disconnect()
|
||||
@ -225,7 +225,7 @@ export class MonitorConnection {
|
||||
if (this.boardsServiceClient.canUploadTo(boardsConfig, { silent: false })) {
|
||||
// Instead of calling `getAttachedBoards` and filtering for `AttachedSerialBoard` we have to check the available ports.
|
||||
// The connected board might be unknown. See: https://github.com/arduino/arduino-pro-ide/issues/127#issuecomment-563251881
|
||||
this.boardsService.getAvailablePorts().then(({ ports }) => {
|
||||
this.boardsService.getAvailablePorts().then(ports => {
|
||||
if (ports.some(port => Port.equals(port, boardsConfig.selectedPort))) {
|
||||
new Promise<void>(resolve => {
|
||||
// First, disconnect if connected.
|
||||
|
@ -178,7 +178,7 @@ export class MonitorWidget extends ReactWidget {
|
||||
this.monitorModel.lineEnding = option.value;
|
||||
}
|
||||
|
||||
protected readonly onChangeBaudRate = async (option: SelectOption<MonitorConfig.BaudRate>) => {
|
||||
protected readonly onChangeBaudRate = (option: SelectOption<MonitorConfig.BaudRate>) => {
|
||||
this.monitorModel.baudRate = option.value;
|
||||
}
|
||||
|
||||
|
@ -97,7 +97,7 @@ div#select-board-dialog .selectBoardContainer .body .list .item.selected i {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .body .list .item .detail {
|
||||
#select-board-dialog .selectBoardContainer .body .list .item .details {
|
||||
font-size: var(--theia-ui-font-size1);
|
||||
opacity: var(--theia-mod-disabled-opacity);
|
||||
width: 155px; /* used heuristics for the calculation */
|
||||
@ -169,6 +169,13 @@ button.theia-button.main {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.arduino-boards-toolbar-item-container .arduino-boards-toolbar-item .inner-container .guessed {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
color: var(--theia-warningBackground);
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.arduino-boards-toolbar-item-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -46,7 +46,7 @@
|
||||
See above: `.filterable-list-container .items-container > div:nth-child(odd|event)`.
|
||||
We have to increase `z-index` of the scroll-bar thumb. Otherwise, the thumb is not visible.
|
||||
https://github.com/arduino/arduino-pro-ide/issues/82 */
|
||||
.arduino-list-widget .ps__rail-y > .ps__thumb-y {
|
||||
.arduino-list-widget .filterable-list-container .items-container .ps__rail-y {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,6 @@ import { isWindows, isOSX } from '@theia/core/lib/common/os';
|
||||
import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory';
|
||||
import { Searchable } from './searchable';
|
||||
import { Installable } from './installable';
|
||||
import { Detailable } from './detailable';
|
||||
import { ArduinoComponent } from './arduino-component';
|
||||
const naturalCompare: (left: string, right: string) => number = require('string-natural-compare').caseInsensitive;
|
||||
|
||||
@ -22,21 +21,24 @@ export namespace AttachedBoardsChangeEvent {
|
||||
ports: Port[]
|
||||
}
|
||||
}> {
|
||||
const diff = <T>(left: T[], right: T[]) => {
|
||||
return left.filter(item => right.indexOf(item) === -1);
|
||||
// In `lefts` AND not in `rights`.
|
||||
const diff = <T>(lefts: T[], rights: T[], sameAs: (left: T, right: T) => boolean) => {
|
||||
return lefts.filter(left => rights.findIndex(right => sameAs(left, right)) === -1);
|
||||
}
|
||||
const { boards: newBoards } = event.newState;
|
||||
const { boards: oldBoards } = event.oldState;
|
||||
const { ports: newPorts } = event.newState;
|
||||
const { ports: oldPorts } = event.oldState;
|
||||
const boardSameAs = (left: Board, right: Board) => Board.sameAs(left, right);
|
||||
const portSameAs = (left: Port, right: Port) => Port.sameAs(left, right);
|
||||
return {
|
||||
detached: {
|
||||
boards: diff(oldBoards, newBoards),
|
||||
ports: diff(oldPorts, newPorts)
|
||||
boards: diff(oldBoards, newBoards, boardSameAs),
|
||||
ports: diff(oldPorts, newPorts, portSameAs)
|
||||
},
|
||||
attached: {
|
||||
boards: diff(newBoards, oldBoards),
|
||||
ports: diff(newPorts, oldPorts)
|
||||
boards: diff(newBoards, oldBoards, boardSameAs),
|
||||
ports: diff(newPorts, oldPorts, portSameAs)
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -44,11 +46,11 @@ export namespace AttachedBoardsChangeEvent {
|
||||
}
|
||||
|
||||
export interface BoardInstalledEvent {
|
||||
readonly pkg: Readonly<BoardPackage>;
|
||||
readonly pkg: Readonly<BoardsPackage>;
|
||||
}
|
||||
|
||||
export interface BoardUninstalledEvent {
|
||||
readonly pkg: Readonly<BoardPackage>;
|
||||
readonly pkg: Readonly<BoardsPackage>;
|
||||
}
|
||||
|
||||
export const BoardsServiceClient = Symbol('BoardsServiceClient');
|
||||
@ -60,9 +62,13 @@ export interface BoardsServiceClient {
|
||||
|
||||
export const BoardsServicePath = '/services/boards-service';
|
||||
export const BoardsService = Symbol('BoardsService');
|
||||
export interface BoardsService extends Installable<BoardPackage>, Searchable<BoardPackage>, Detailable<BoardDetails>, JsonRpcServer<BoardsServiceClient> {
|
||||
getAttachedBoards(): Promise<{ boards: Board[] }>;
|
||||
getAvailablePorts(): Promise<{ ports: Port[] }>;
|
||||
export interface BoardsService extends Installable<BoardsPackage>, Searchable<BoardsPackage>, JsonRpcServer<BoardsServiceClient> {
|
||||
getAttachedBoards(): Promise<Board[]>;
|
||||
getAvailablePorts(): Promise<Port[]>;
|
||||
getBoardDetails(options: { fqbn: string }): Promise<BoardDetails>;
|
||||
getBoardPackage(options: { id: string }): Promise<BoardsPackage | undefined>;
|
||||
getContainerBoardPackage(options: { fqbn: string }): Promise<BoardsPackage | undefined>;
|
||||
searchBoards(options: { query?: string }): Promise<Array<Board & { packageName: string }>>;
|
||||
}
|
||||
|
||||
export interface Port {
|
||||
@ -160,38 +166,114 @@ export namespace Port {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function sameAs(left: Port | undefined, right: string | undefined) {
|
||||
export function sameAs(left: Port | undefined, right: Port | string | undefined) {
|
||||
if (left && right) {
|
||||
if (left.protocol !== 'serial') {
|
||||
console.log(`Unexpected protocol for port: ${JSON.stringify(left)}. Ignoring protocol, comparing addresses with ${right}.`);
|
||||
console.log(`Unexpected protocol for 'left' port: ${JSON.stringify(left)}. Ignoring 'protocol', comparing 'addresses' with ${JSON.stringify(right)}.`);
|
||||
}
|
||||
return left.address === right;
|
||||
if (typeof right === 'string') {
|
||||
return left.address === right;
|
||||
}
|
||||
if (right.protocol !== 'serial') {
|
||||
console.log(`Unexpected protocol for 'right' port: ${JSON.stringify(right)}. Ignoring 'protocol', comparing 'addresses' with ${JSON.stringify(left)}.`);
|
||||
}
|
||||
return left.address === right.address;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export interface BoardPackage extends ArduinoComponent {
|
||||
id: string;
|
||||
boards: Board[];
|
||||
export interface BoardsPackage extends ArduinoComponent {
|
||||
readonly id: string;
|
||||
readonly boards: Board[];
|
||||
}
|
||||
|
||||
export interface Board {
|
||||
name: string
|
||||
fqbn?: string
|
||||
readonly name: string;
|
||||
readonly fqbn?: string;
|
||||
readonly port?: Port;
|
||||
}
|
||||
|
||||
export interface BoardDetails extends Board {
|
||||
fqbn: string;
|
||||
|
||||
requiredTools: Tool[];
|
||||
export interface BoardDetails {
|
||||
readonly fqbn: string;
|
||||
readonly requiredTools: Tool[];
|
||||
readonly configOptions: ConfigOption[];
|
||||
}
|
||||
|
||||
export interface Tool {
|
||||
readonly packager: string;
|
||||
readonly name: string;
|
||||
readonly version: string;
|
||||
readonly version: Installable.Version;
|
||||
}
|
||||
|
||||
export interface ConfigOption {
|
||||
readonly option: string;
|
||||
readonly label: string;
|
||||
readonly values: ConfigValue[];
|
||||
}
|
||||
export namespace ConfigOption {
|
||||
|
||||
/**
|
||||
* Appends the configuration options to the `fqbn` argument.
|
||||
* Throws an error if the `fqbn` does not have the `segment(':'segment)*` format.
|
||||
* The provided output format is always segment(':'segment)*(':'option'='value(','option'='value)*)?
|
||||
* Validation can be disabled with the `{ validation: false }` option.
|
||||
*/
|
||||
export function decorate(fqbn: string, configOptions: ConfigOption[], { validate } = { validate: true }): string {
|
||||
if (validate) {
|
||||
if (!isValidFqbn(fqbn)) {
|
||||
throw new ConfigOptionError(`${fqbn} is not a valid FQBN.`);
|
||||
}
|
||||
if (isValidFqbnWithOptions(fqbn)) {
|
||||
throw new ConfigOptionError(`${fqbn} is already decorated with the configuration options.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!configOptions.length) {
|
||||
return fqbn;
|
||||
}
|
||||
|
||||
const toValue = (values: ConfigValue[]) => {
|
||||
const selectedValue = values.find(({ selected }) => selected);
|
||||
if (!selectedValue) {
|
||||
console.warn(`None of the config values was selected. Values were: ${JSON.stringify(values)}`);
|
||||
return undefined;
|
||||
}
|
||||
return selectedValue.value;
|
||||
};
|
||||
const options = configOptions
|
||||
.map(({ option, values }) => [option, toValue(values)])
|
||||
.filter(([, value]) => !!value)
|
||||
.map(([option, value]) => `${option}=${value}`)
|
||||
.join(',');
|
||||
|
||||
return `${fqbn}:${options}`;
|
||||
}
|
||||
|
||||
export function isValidFqbn(fqbn: string): boolean {
|
||||
return /^\w+(:\w+)*$/.test(fqbn);
|
||||
}
|
||||
|
||||
export function isValidFqbnWithOptions(fqbn: string): boolean {
|
||||
return /^\w+(:\w+)*(:\w+=\w+(,\w+=\w+)*)$/.test(fqbn);
|
||||
}
|
||||
|
||||
export class ConfigOptionError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
Object.setPrototypeOf(this, ConfigOptionError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export const LABEL_COMPARATOR = (left: ConfigOption, right: ConfigOption) => left.label.toLocaleLowerCase().localeCompare(right.label.toLocaleLowerCase());
|
||||
|
||||
}
|
||||
|
||||
export interface ConfigValue {
|
||||
readonly label: string;
|
||||
readonly value: string;
|
||||
readonly selected: boolean;
|
||||
}
|
||||
|
||||
export namespace Board {
|
||||
@ -232,25 +314,36 @@ export namespace Board {
|
||||
return `${board.name}${fqbn}`;
|
||||
}
|
||||
|
||||
}
|
||||
export function decorateBoards(
|
||||
selectedBoard: Board | undefined,
|
||||
searchResults: Array<Board & { packageName: string }>): Array<Board & { selected: boolean, missing: boolean, packageName: string, details?: string }> {
|
||||
// Board names are not unique. We show the corresponding core name as a detail.
|
||||
// https://github.com/arduino/arduino-cli/pull/294#issuecomment-513764948
|
||||
const distinctBoardNames = new Map<string, number>();
|
||||
for (const { name } of searchResults) {
|
||||
const counter = distinctBoardNames.get(name) || 0;
|
||||
distinctBoardNames.set(name, counter + 1);
|
||||
}
|
||||
|
||||
export interface AttachedSerialBoard extends Board {
|
||||
port: string;
|
||||
}
|
||||
|
||||
export namespace AttachedSerialBoard {
|
||||
export function is(b: Board | any): b is AttachedSerialBoard {
|
||||
return !!b && 'port' in b;
|
||||
// Due to the non-unique board names, we have to check the package name as well.
|
||||
const selected = (board: Board & { packageName: string }) => {
|
||||
if (!!selectedBoard) {
|
||||
if (Board.equals(board, selectedBoard)) {
|
||||
if ('packageName' in selectedBoard) {
|
||||
return board.packageName === (selectedBoard as any).packageName;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return searchResults.map(board => ({
|
||||
...board,
|
||||
details: (distinctBoardNames.get(board.name) || 0) > 1 ? ` - ${board.packageName}` : undefined,
|
||||
selected: selected(board),
|
||||
missing: !installed(board)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export interface AttachedNetworkBoard extends Board {
|
||||
address: string;
|
||||
port: string;
|
||||
}
|
||||
|
||||
export namespace AttachedNetworkBoard {
|
||||
export function is(b: Board): b is AttachedNetworkBoard {
|
||||
return 'address' in b && 'port' in b;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory';
|
||||
import { Board } from './boards-service';
|
||||
|
||||
export const CoreServiceClient = Symbol('CoreServiceClient');
|
||||
export interface CoreServiceClient {
|
||||
@ -15,20 +14,18 @@ export interface CoreService extends JsonRpcServer<CoreServiceClient> {
|
||||
|
||||
export namespace CoreService {
|
||||
|
||||
export namespace Upload {
|
||||
export namespace Compile {
|
||||
export interface Options {
|
||||
readonly uri: string;
|
||||
readonly board: Board;
|
||||
readonly port: string;
|
||||
readonly sketchUri: string;
|
||||
readonly fqbn: string;
|
||||
readonly optimizeForDebug: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Compile {
|
||||
export interface Options {
|
||||
readonly uri: string;
|
||||
readonly board: Board;
|
||||
readonly optimizeForDebug: boolean;
|
||||
export namespace Upload {
|
||||
export interface Options extends Compile.Options {
|
||||
readonly port: string;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,10 +0,0 @@
|
||||
|
||||
export interface Detailable<T> {
|
||||
detail(options: Detailable.Options): Promise<{ item?: T }>;
|
||||
}
|
||||
|
||||
export namespace Detailable {
|
||||
export interface Options {
|
||||
readonly id: string;
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
export interface Searchable<T> {
|
||||
search(options: Searchable.Options): Promise<{ items: T[] }>;
|
||||
search(options: Searchable.Options): Promise<T[]>;
|
||||
}
|
||||
export namespace Searchable {
|
||||
export interface Options {
|
||||
@ -8,4 +8,4 @@ export namespace Searchable {
|
||||
*/
|
||||
readonly query?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,30 +0,0 @@
|
||||
import * as electron from 'electron';
|
||||
import { injectable, inject, postConstruct } from 'inversify';
|
||||
import { isOSX } from '@theia/core/lib/common/os';
|
||||
import { ElectronMenuContribution } from '@theia/core/lib/electron-browser/menu/electron-menu-contribution';
|
||||
import { EditorMode } from '../browser/editor-mode';
|
||||
|
||||
@injectable()
|
||||
export class ElectronArduinoMenuContribution extends ElectronMenuContribution {
|
||||
|
||||
@inject(EditorMode)
|
||||
protected readonly editorMode: EditorMode;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.editorMode.menuContentChanged.event(() => {
|
||||
const createdMenuBar = this.factory.createMenuBar();
|
||||
if (isOSX) {
|
||||
electron.remote.Menu.setApplicationMenu(createdMenuBar);
|
||||
} else {
|
||||
electron.remote.getCurrentWindow().setMenu(createdMenuBar);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected hideTopPanel(): void {
|
||||
// NOOP
|
||||
// We reuse the `div` for the Arduino toolbar.
|
||||
}
|
||||
|
||||
}
|
17
arduino-ide-extension/src/electron-browser/menu/electron-arduino-menu-contribution.ts
Normal file
17
arduino-ide-extension/src/electron-browser/menu/electron-arduino-menu-contribution.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { ElectronMenuContribution } from '@theia/core/lib/electron-browser/menu/electron-menu-contribution';
|
||||
import { MainMenuManager } from '../../browser/menu/main-menu-manager';
|
||||
|
||||
@injectable()
|
||||
export class ElectronArduinoMenuContribution extends ElectronMenuContribution implements MainMenuManager {
|
||||
|
||||
protected hideTopPanel(): void {
|
||||
// NOOP
|
||||
// We reuse the `div` for the Arduino toolbar.
|
||||
}
|
||||
|
||||
update(): void {
|
||||
(this as any).setMenu();
|
||||
}
|
||||
|
||||
}
|
@ -1,8 +1,10 @@
|
||||
import { ContainerModule } from 'inversify';
|
||||
import { ElectronMenuContribution } from '@theia/core/lib/electron-browser/menu/electron-menu-contribution'
|
||||
import { ElectronArduinoMenuContribution } from './electron-arduino-menu-contribution';
|
||||
import { MainMenuManager } from '../../browser/menu/main-menu-manager';
|
||||
|
||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(ElectronArduinoMenuContribution).toSelf().inSingletonScope();
|
||||
bind(MainMenuManager).toService(ElectronArduinoMenuContribution);
|
||||
rebind(ElectronMenuContribution).to(ElectronArduinoMenuContribution);
|
||||
});
|
@ -85,7 +85,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(ConnectionContainerModule).toConstantValue(sketchesServiceConnectionModule);
|
||||
|
||||
// Boards service
|
||||
const boardsServiceConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => {
|
||||
const boardsServiceConnectionModule = ConnectionContainerModule.create(async ({ bind, bindBackendService }) => {
|
||||
bind(BoardsServiceImpl).toSelf().inSingletonScope();
|
||||
bind(BoardsService).toService(BoardsServiceImpl);
|
||||
bindBackendService<BoardsService, BoardsServiceClient>(BoardsServicePath, BoardsService, (service, client) => {
|
@ -1,10 +1,7 @@
|
||||
import { injectable, inject, postConstruct, named } from 'inversify';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import {
|
||||
BoardsService, AttachedSerialBoard, BoardPackage, Board, AttachedNetworkBoard, BoardsServiceClient,
|
||||
Port, BoardDetails, Tool
|
||||
} from '../common/protocol/boards-service';
|
||||
import { BoardsService, BoardsPackage, Board, BoardsServiceClient, Port, BoardDetails, Tool, ConfigOption, ConfigValue } from '../common/protocol';
|
||||
import {
|
||||
PlatformSearchReq, PlatformSearchResp, PlatformInstallReq, PlatformInstallResp, PlatformListReq,
|
||||
PlatformListResp, Platform, PlatformUninstallResp, PlatformUninstallReq
|
||||
@ -37,8 +34,8 @@ export class BoardsServiceImpl implements BoardsService {
|
||||
* Stores the state of the currently discovered and attached boards.
|
||||
* This state is updated via periodical polls. If there diff, a change event will be sent out to the frontend.
|
||||
*/
|
||||
protected attachedBoards: { boards: Board[] } = { boards: [] };
|
||||
protected availablePorts: { ports: Port[] } = { ports: [] };
|
||||
protected attachedBoards: Board[] = [];
|
||||
protected availablePorts: Port[] = [];
|
||||
protected started = new Deferred<void>();
|
||||
protected client: BoardsServiceClient | undefined;
|
||||
|
||||
@ -49,8 +46,8 @@ export class BoardsServiceImpl implements BoardsService {
|
||||
this.doGetAttachedBoardsAndAvailablePorts()
|
||||
.then(({ boards, ports }) => {
|
||||
const update = (oldBoards: Board[], newBoards: Board[], oldPorts: Port[], newPorts: Port[], message: string) => {
|
||||
this.attachedBoards = { boards: newBoards };
|
||||
this.availablePorts = { ports: newPorts };
|
||||
this.attachedBoards = newBoards;
|
||||
this.availablePorts = newPorts;
|
||||
this.discoveryLogger.info(`${message} - Discovered boards: ${JSON.stringify(newBoards)} and available ports: ${JSON.stringify(newPorts)}`);
|
||||
if (this.client) {
|
||||
this.client.notifyAttachedBoardsChanged({
|
||||
@ -76,7 +73,7 @@ export class BoardsServiceImpl implements BoardsService {
|
||||
Promise.all([
|
||||
this.getAttachedBoards(),
|
||||
this.getAvailablePorts()
|
||||
]).then(([{ boards: currentBoards }, { ports: currentPorts }]) => {
|
||||
]).then(([currentBoards, currentPorts]) => {
|
||||
this.discoveryLogger.trace(`Updating discovered boards... ${JSON.stringify(currentBoards)}`);
|
||||
if (currentBoards.length !== sortedBoards.length || currentPorts.length !== sortedPorts.length) {
|
||||
update(currentBoards, sortedBoards, currentPorts, sortedPorts, 'Updated discovered boards and available ports.');
|
||||
@ -118,12 +115,12 @@ export class BoardsServiceImpl implements BoardsService {
|
||||
this.client = undefined;
|
||||
}
|
||||
|
||||
async getAttachedBoards(): Promise<{ boards: Board[] }> {
|
||||
async getAttachedBoards(): Promise<Board[]> {
|
||||
await this.started.promise;
|
||||
return this.attachedBoards;
|
||||
}
|
||||
|
||||
async getAvailablePorts(): Promise<{ ports: Port[] }> {
|
||||
async getAvailablePorts(): Promise<Port[]> {
|
||||
await this.started.promise;
|
||||
return this.availablePorts;
|
||||
}
|
||||
@ -192,23 +189,8 @@ export class BoardsServiceImpl implements BoardsService {
|
||||
for (const board of portList.getBoardsList()) {
|
||||
const name = board.getName() || 'unknown';
|
||||
const fqbn = board.getFqbn();
|
||||
const port = address;
|
||||
if (protocol === 'serial') {
|
||||
boards.push(<AttachedSerialBoard>{
|
||||
name,
|
||||
fqbn,
|
||||
port
|
||||
});
|
||||
} else if (protocol === 'network') { // We assume, it is a `network` board.
|
||||
boards.push(<AttachedNetworkBoard>{
|
||||
name,
|
||||
fqbn,
|
||||
address,
|
||||
port
|
||||
});
|
||||
} else {
|
||||
console.warn(`Unknown protocol for port: ${address}.`);
|
||||
}
|
||||
const port = { address, protocol };
|
||||
boards.push({ name, fqbn, port });
|
||||
}
|
||||
}
|
||||
// TODO: remove mock board!
|
||||
@ -219,37 +201,79 @@ export class BoardsServiceImpl implements BoardsService {
|
||||
return { boards, ports };
|
||||
}
|
||||
|
||||
async detail(options: { id: string }): Promise<{ item?: BoardDetails }> {
|
||||
async getBoardDetails(options: { fqbn: string }): Promise<BoardDetails> {
|
||||
const coreClient = await this.coreClientProvider.client();
|
||||
if (!coreClient) {
|
||||
return {};
|
||||
throw new Error(`Cannot acquire core client provider.`);
|
||||
}
|
||||
const { client, instance } = coreClient;
|
||||
|
||||
const { fqbn } = options;
|
||||
const req = new BoardDetailsReq();
|
||||
req.setInstance(instance);
|
||||
req.setFqbn(options.id);
|
||||
const resp = await new Promise<BoardDetailsResp>((resolve, reject) => client.boardDetails(req, (err, resp) => (!!err ? reject : resolve)(!!err ? err : resp)));
|
||||
req.setFqbn(fqbn);
|
||||
const resp = await new Promise<BoardDetailsResp>((resolve, reject) => client.boardDetails(req, (err, resp) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(resp);
|
||||
}));
|
||||
|
||||
const tools = await Promise.all(resp.getRequiredToolsList().map(async t => <Tool>{
|
||||
const requiredTools = resp.getRequiredToolsList().map(t => <Tool>{
|
||||
name: t.getName(),
|
||||
packager: t.getPackager(),
|
||||
version: t.getVersion()
|
||||
}));
|
||||
});
|
||||
|
||||
const configOptions = resp.getConfigOptionsList().map(c => <ConfigOption>{
|
||||
label: c.getOptionLabel(),
|
||||
option: c.getOption(),
|
||||
values: c.getValuesList().map(v => <ConfigValue>{
|
||||
value: v.getValue(),
|
||||
label: v.getValueLabel(),
|
||||
selected: v.getSelected()
|
||||
})
|
||||
});
|
||||
|
||||
return {
|
||||
item: {
|
||||
name: resp.getName(),
|
||||
fqbn: options.id,
|
||||
requiredTools: tools
|
||||
}
|
||||
fqbn,
|
||||
requiredTools,
|
||||
configOptions
|
||||
};
|
||||
}
|
||||
|
||||
async search(options: { query?: string }): Promise<{ items: BoardPackage[] }> {
|
||||
async getBoardPackage(options: { id: string }): Promise<BoardsPackage | undefined> {
|
||||
const { id: expectedId } = options;
|
||||
if (!expectedId) {
|
||||
return undefined;
|
||||
}
|
||||
const packages = await this.search({ query: expectedId });
|
||||
return packages.find(({ id }) => id === expectedId);
|
||||
}
|
||||
|
||||
async getContainerBoardPackage(options: { fqbn: string }): Promise<BoardsPackage | undefined> {
|
||||
const { fqbn: expectedFqbn } = options;
|
||||
if (!expectedFqbn) {
|
||||
return undefined;
|
||||
}
|
||||
const packages = await this.search({});
|
||||
return packages.find(({ boards }) => boards.some(({ fqbn }) => fqbn === expectedFqbn));
|
||||
}
|
||||
|
||||
async searchBoards(options: { query?: string }): Promise<Array<Board & { packageName: string }>> {
|
||||
const query = (options.query || '').toLocaleLowerCase();
|
||||
const results = await this.search(options);
|
||||
return results.map(item => item.boards.map(board => ({ ...board, packageName: item.name })))
|
||||
.reduce((acc, curr) => acc.concat(curr), [])
|
||||
.filter(board => board.name.toLocaleLowerCase().indexOf(query) !== -1)
|
||||
.sort(Board.compare);
|
||||
}
|
||||
|
||||
async search(options: { query?: string }): Promise<BoardsPackage[]> {
|
||||
const coreClient = await this.coreClientProvider.client();
|
||||
if (!coreClient) {
|
||||
return { items: [] };
|
||||
return [];
|
||||
}
|
||||
const { client, instance } = coreClient;
|
||||
|
||||
@ -265,7 +289,7 @@ export class BoardsServiceImpl implements BoardsService {
|
||||
req.setAllVersions(true);
|
||||
req.setInstance(instance);
|
||||
const resp = await new Promise<PlatformSearchResp>((resolve, reject) => client.platformSearch(req, (err, resp) => (!!err ? reject : resolve)(!!err ? err : resp)));
|
||||
const packages = new Map<string, BoardPackage>();
|
||||
const packages = new Map<string, BoardsPackage>();
|
||||
const toPackage = (platform: Platform) => {
|
||||
let installedVersion: string | undefined;
|
||||
const matchingPlatform = installedPlatforms.find(ip => ip.getId() === platform.getId());
|
||||
@ -307,7 +331,7 @@ export class BoardsServiceImpl implements BoardsService {
|
||||
if (!leftInstalled && rightInstalled) {
|
||||
return 1;
|
||||
}
|
||||
return Installable.Version.COMPARATOR(right.getLatest(), left.getLatest()); // Higher version comes first.
|
||||
return Installable.Version.COMPARATOR(left.getLatest(), right.getLatest()); // Higher version comes first.
|
||||
}
|
||||
for (const id of groupedById.keys()) {
|
||||
groupedById.get(id)!.sort(installedAwareVersionComparator);
|
||||
@ -326,10 +350,10 @@ export class BoardsServiceImpl implements BoardsService {
|
||||
}
|
||||
}
|
||||
|
||||
return { items: [...packages.values()] };
|
||||
return [...packages.values()];
|
||||
}
|
||||
|
||||
async install(options: { item: BoardPackage, version?: Installable.Version }): Promise<void> {
|
||||
async install(options: { item: BoardsPackage, version?: Installable.Version }): Promise<void> {
|
||||
const pkg = options.item;
|
||||
const version = !!options.version ? options.version : pkg.availableVersions[0];
|
||||
const coreClient = await this.coreClientProvider.client();
|
||||
@ -338,11 +362,11 @@ export class BoardsServiceImpl implements BoardsService {
|
||||
}
|
||||
const { client, instance } = coreClient;
|
||||
|
||||
const [platform, boardName] = pkg.id.split(":");
|
||||
const [platform, architecture] = pkg.id.split(":");
|
||||
|
||||
const req = new PlatformInstallReq();
|
||||
req.setInstance(instance);
|
||||
req.setArchitecture(boardName);
|
||||
req.setArchitecture(architecture);
|
||||
req.setPlatformPackage(platform);
|
||||
req.setVersion(version);
|
||||
|
||||
@ -359,12 +383,14 @@ export class BoardsServiceImpl implements BoardsService {
|
||||
resp.on('error', reject);
|
||||
});
|
||||
if (this.client) {
|
||||
this.client.notifyBoardInstalled({ pkg });
|
||||
const packages = await this.search({});
|
||||
const updatedPackage = packages.find(({ id }) => id === pkg.id) || pkg;
|
||||
this.client.notifyBoardInstalled({ pkg: updatedPackage });
|
||||
}
|
||||
console.info("Board installation done", pkg);
|
||||
}
|
||||
|
||||
async uninstall(options: { item: BoardPackage }): Promise<void> {
|
||||
async uninstall(options: { item: BoardsPackage }): Promise<void> {
|
||||
const pkg = options.item;
|
||||
const coreClient = await this.coreClientProvider.client();
|
||||
if (!coreClient) {
|
||||
@ -372,11 +398,11 @@ export class BoardsServiceImpl implements BoardsService {
|
||||
}
|
||||
const { client, instance } = coreClient;
|
||||
|
||||
const [platform, boardName] = pkg.id.split(":");
|
||||
const [platform, architecture] = pkg.id.split(":");
|
||||
|
||||
const req = new PlatformUninstallReq();
|
||||
req.setInstance(instance);
|
||||
req.setArchitecture(boardName);
|
||||
req.setArchitecture(architecture);
|
||||
req.setPlatformPackage(platform);
|
||||
|
||||
console.info("Starting board uninstallation", pkg);
|
||||
@ -393,6 +419,7 @@ export class BoardsServiceImpl implements BoardsService {
|
||||
resp.on('error', reject);
|
||||
});
|
||||
if (this.client) {
|
||||
// Here, unlike at `install` we send out the argument `pkg`. Otherwise, we would not know about the board FQBN.
|
||||
this.client.notifyBoardUninstalled({ pkg });
|
||||
}
|
||||
console.info("Board uninstallation done", pkg);
|
||||
|
@ -36,10 +36,10 @@ export class CoreServiceImpl implements CoreService {
|
||||
|
||||
async compile(options: CoreService.Compile.Options): Promise<void> {
|
||||
console.log('compile', options);
|
||||
const { uri } = options;
|
||||
const sketchFilePath = await this.fileSystem.getFsPath(options.uri);
|
||||
const { sketchUri, fqbn } = options;
|
||||
const sketchFilePath = await this.fileSystem.getFsPath(sketchUri);
|
||||
if (!sketchFilePath) {
|
||||
throw new Error(`Cannot resolve filesystem path for URI: ${uri}.`);
|
||||
throw new Error(`Cannot resolve filesystem path for URI: ${sketchUri}.`);
|
||||
}
|
||||
const sketchpath = path.dirname(sketchFilePath);
|
||||
|
||||
@ -49,18 +49,14 @@ export class CoreServiceImpl implements CoreService {
|
||||
}
|
||||
const { client, instance } = coreClient;
|
||||
|
||||
const currentBoard = options.board;
|
||||
if (!currentBoard) {
|
||||
throw new Error("no board selected");
|
||||
}
|
||||
if (!currentBoard.fqbn) {
|
||||
throw new Error(`selected board (${currentBoard.name}) has no FQBN`);
|
||||
if (!fqbn) {
|
||||
throw new Error('The selected board has no FQBN.');
|
||||
}
|
||||
|
||||
const compilerReq = new CompileReq();
|
||||
compilerReq.setInstance(instance);
|
||||
compilerReq.setSketchpath(sketchpath);
|
||||
compilerReq.setFqbn(currentBoard.fqbn!);
|
||||
compilerReq.setFqbn(fqbn);
|
||||
compilerReq.setOptimizefordebug(options.optimizeForDebug);
|
||||
compilerReq.setPreprocess(false);
|
||||
compilerReq.setVerbose(true);
|
||||
@ -84,23 +80,15 @@ export class CoreServiceImpl implements CoreService {
|
||||
}
|
||||
|
||||
async upload(options: CoreService.Upload.Options): Promise<void> {
|
||||
await this.compile({ uri: options.uri, board: options.board, optimizeForDebug: options.optimizeForDebug });
|
||||
|
||||
await this.compile(options);
|
||||
console.log('upload', options);
|
||||
const { uri } = options;
|
||||
const sketchFilePath = await this.fileSystem.getFsPath(options.uri);
|
||||
const { sketchUri, fqbn } = options;
|
||||
const sketchFilePath = await this.fileSystem.getFsPath(sketchUri);
|
||||
if (!sketchFilePath) {
|
||||
throw new Error(`Cannot resolve filesystem path for URI: ${uri}.`);
|
||||
throw new Error(`Cannot resolve filesystem path for URI: ${sketchUri}.`);
|
||||
}
|
||||
const sketchpath = path.dirname(sketchFilePath);
|
||||
|
||||
const currentBoard = options.board;
|
||||
if (!currentBoard) {
|
||||
throw new Error("no board selected");
|
||||
}
|
||||
if (!currentBoard.fqbn) {
|
||||
throw new Error(`selected board (${currentBoard.name}) has no FQBN`);
|
||||
}
|
||||
|
||||
const coreClient = await this.coreClientProvider.client();
|
||||
if (!coreClient) {
|
||||
@ -108,10 +96,14 @@ export class CoreServiceImpl implements CoreService {
|
||||
}
|
||||
const { client, instance } = coreClient;
|
||||
|
||||
if (!fqbn) {
|
||||
throw new Error('The selected board has no FQBN.');
|
||||
}
|
||||
|
||||
const req = new UploadReq();
|
||||
req.setInstance(instance);
|
||||
req.setSketchPath(sketchpath);
|
||||
req.setFqbn(currentBoard.fqbn);
|
||||
req.setFqbn(fqbn);
|
||||
req.setPort(options.port);
|
||||
const result = client.upload(req);
|
||||
|
||||
|
@ -25,10 +25,10 @@ export class LibraryServiceImpl implements LibraryService {
|
||||
@inject(ToolOutputServiceServer)
|
||||
protected readonly toolOutputService: ToolOutputServiceServer;
|
||||
|
||||
async search(options: { query?: string }): Promise<{ items: Library[] }> {
|
||||
async search(options: { query?: string }): Promise<Library[]> {
|
||||
const coreClient = await this.coreClientProvider.client();
|
||||
if (!coreClient) {
|
||||
return { items: [] };
|
||||
return [];
|
||||
}
|
||||
const { client, instance } = coreClient;
|
||||
|
||||
@ -68,7 +68,7 @@ export class LibraryServiceImpl implements LibraryService {
|
||||
}, item.getLatest()!, availableVersions)
|
||||
})
|
||||
|
||||
return { items };
|
||||
return items;
|
||||
}
|
||||
|
||||
async install(options: { item: Library, version?: Installable.Version }): Promise<void> {
|
||||
|
@ -0,0 +1,310 @@
|
||||
import { expect } from 'chai';
|
||||
import { Container, injectable } from 'inversify';
|
||||
import { Event } from '@theia/core/lib/common/event';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { MockLogger } from '@theia/core/lib/common/test/mock-logger';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { StorageService } from '@theia/core/lib/browser/storage-service';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { BoardsService, Board, Port, BoardsPackage, BoardDetails, BoardsServiceClient } from '../../common/protocol';
|
||||
import { BoardsServiceClientImpl, AvailableBoard } from '../../browser/boards/boards-service-client-impl';
|
||||
import { BoardsConfig } from '../../browser/boards/boards-config';
|
||||
|
||||
// tslint:disable: no-unused-expression
|
||||
|
||||
describe('boards-service-client-impl', () => {
|
||||
|
||||
describe('onAvailableBoardsChanged', () => {
|
||||
|
||||
const ESP8266: Port = { protocol: 'serial', address: '/dev/cu.SLAB_USBtoUART' };
|
||||
const UNO: Board = { name: 'Arduino Uno', fqbn: 'arduino:avr:uno', port: { protocol: 'serial', address: '/dev/cu.usbmodem14501' } };
|
||||
const MKR1000: Board = { name: 'Arduino MKR1000', fqbn: 'arduino:samd:mkr1000', port: { protocol: 'serial', address: '/dev/cu.usbmodem14601' } };
|
||||
const NANO: Board = { name: 'Arduino Nano', fqbn: 'arduino:avr:nano' };
|
||||
|
||||
const recognized = AvailableBoard.State.recognized;
|
||||
const guessed = AvailableBoard.State.guessed;
|
||||
const incomplete = AvailableBoard.State.incomplete;
|
||||
|
||||
let server: MockBoardsService;
|
||||
let client: BoardsServiceClientImpl;
|
||||
// let storage: MockStorageService;
|
||||
|
||||
beforeEach(() => {
|
||||
const container = init();
|
||||
server = container.get(MockBoardsService);
|
||||
client = container.get(BoardsServiceClientImpl);
|
||||
// storage = container.get(MockStorageService);
|
||||
server.setClient(client);
|
||||
});
|
||||
|
||||
it('should have no available boards by default', () => {
|
||||
expect(client.availableBoards).to.have.length(0);
|
||||
});
|
||||
|
||||
it('should be notified when a board is attached', async () => {
|
||||
await attach(MKR1000);
|
||||
expect(availableBoards()).to.have.length(1);
|
||||
expect(availableBoards()[0].state).to.be.equal(recognized);
|
||||
expect(!!availableBoards()[0].selected).to.be.false;
|
||||
});
|
||||
|
||||
it('should be notified when a unknown board is attached', async () => {
|
||||
await attach(ESP8266);
|
||||
expect(availableBoards()).to.have.length(1);
|
||||
expect(availableBoards()[0].state).to.be.equal(incomplete);
|
||||
});
|
||||
|
||||
it('should be notified when a board is detached', async () => {
|
||||
await attach(MKR1000, UNO, ESP8266);
|
||||
expect(availableBoards()).to.have.length(3);
|
||||
await detach(MKR1000);
|
||||
expect(availableBoards()).to.have.length(2);
|
||||
});
|
||||
|
||||
it('should be notified when an unknown board is detached', async () => {
|
||||
await attach(MKR1000, UNO, ESP8266);
|
||||
expect(availableBoards()).to.have.length(3);
|
||||
await detach(ESP8266);
|
||||
expect(availableBoards()).to.have.length(2);
|
||||
});
|
||||
|
||||
it('should recognize boards config as an available board', async () => {
|
||||
await configureBoards({ selectedBoard: NANO });
|
||||
expect(availableBoards()).to.have.length(1);
|
||||
expect(availableBoards()[0].state).to.be.equal(incomplete);
|
||||
expect(availableBoards()[0].selected).to.be.true;
|
||||
});
|
||||
|
||||
it('should discard the boards config port when corresponding board is detached', async () => {
|
||||
await attach(MKR1000);
|
||||
expect(availableBoards()).to.have.length(1);
|
||||
expect(availableBoards()[0].state).to.be.equal(recognized);
|
||||
expect(availableBoards()[0].selected).to.be.false;
|
||||
|
||||
await configureBoards({ selectedBoard: MKR1000, selectedPort: server.portFor(MKR1000) });
|
||||
expect(availableBoards()).to.have.length(1);
|
||||
expect(availableBoards()[0].state).to.be.equal(recognized);
|
||||
expect(availableBoards()[0].selected).to.be.true;
|
||||
|
||||
await detach(MKR1000);
|
||||
expect(availableBoards()).to.have.length(1);
|
||||
expect(availableBoards()[0].state).to.be.equal(incomplete);
|
||||
expect(availableBoards()[0].selected).to.be.true;
|
||||
});
|
||||
|
||||
it("should consider selected unknown boards as 'guessed'", async () => {
|
||||
await attach(ESP8266);
|
||||
await configureBoards({ selectedBoard: { name: 'guessed' }, selectedPort: ESP8266 });
|
||||
expect(availableBoards()).to.have.length(1);
|
||||
expect(availableBoards()[0].state).to.be.equal(guessed);
|
||||
expect(availableBoards()[0].name).to.be.equal('guessed');
|
||||
expect(availableBoards()[0].fqbn).to.be.undefined;
|
||||
expect(client.canVerify(client.boardsConfig)).to.be.true;
|
||||
});
|
||||
|
||||
it('should not reconnect last valid selected if port is gone', async () => {
|
||||
await attach(ESP8266, UNO);
|
||||
await configureBoards({ selectedBoard: { name: 'NodeMCU 0.9 (ESP-12 Module)', fqbn: 'esp8266:esp8266:nodemcu' }, selectedPort: ESP8266 });
|
||||
await detach(ESP8266);
|
||||
expect(availableBoards()).to.have.length(2);
|
||||
const selected = availableBoards().find(({ selected }) => selected);
|
||||
expect(selected).to.be.not.undefined;
|
||||
expect(selected!.port).to.be.undefined;
|
||||
expect(selected!.name).to.be.equal('NodeMCU 0.9 (ESP-12 Module)');
|
||||
});
|
||||
|
||||
function availableBoards(): AvailableBoard[] {
|
||||
return client.availableBoards.slice();
|
||||
}
|
||||
|
||||
async function configureBoards(config: BoardsConfig.Config): Promise<void> {
|
||||
return awaitAll(() => { client.boardsConfig = config; }, client.onAvailableBoardsChanged);
|
||||
}
|
||||
|
||||
async function detach(...toDetach: Array<Board | Port>): Promise<void> {
|
||||
return awaitAll(() => server.detach(...toDetach), client.onAttachedBoardsChanged, client.onAvailableBoardsChanged);
|
||||
}
|
||||
|
||||
async function attach(...toAttach: Array<Board | Port>): Promise<void> {
|
||||
return awaitAll(() => server.attach(...toAttach), client.onAttachedBoardsChanged, client.onAvailableBoardsChanged);
|
||||
}
|
||||
|
||||
async function awaitAll(exec: () => MaybePromise<void>, ...waitFor: Event<any>[]): Promise<void> {
|
||||
return new Promise<void>(async resolve => {
|
||||
const toDispose = new DisposableCollection();
|
||||
const promises = waitFor.map(event => {
|
||||
const deferred = new Deferred<void>();
|
||||
toDispose.push(event(() => deferred.resolve()));
|
||||
return deferred.promise;
|
||||
});
|
||||
await exec();
|
||||
await Promise.all(promises);
|
||||
toDispose.dispose();
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function init(): Container {
|
||||
const container = new Container({ defaultScope: 'Singleton' });
|
||||
container.bind(MockBoardsService).toSelf();
|
||||
container.bind(MockLogger).toSelf();
|
||||
container.bind(ILogger).toService(MockLogger);
|
||||
container.bind(MockStorageService).toSelf();
|
||||
container.bind(StorageService).toService(MockStorageService);
|
||||
container.bind(BoardsServiceClientImpl).toSelf();
|
||||
return container;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class MockBoardsService implements BoardsService {
|
||||
|
||||
private client: BoardsServiceClient | undefined;
|
||||
|
||||
boards: Board[] = [];
|
||||
ports: Port[] = [];
|
||||
|
||||
attach(...toAttach: Array<Board | Port>): void {
|
||||
const oldState = { boards: this.boards.slice(), ports: this.ports.slice() };
|
||||
for (const what of toAttach) {
|
||||
if (Board.is(what)) {
|
||||
if (what.port) {
|
||||
this.ports.push(what.port);
|
||||
}
|
||||
this.boards.push(what);
|
||||
} else {
|
||||
this.ports.push(what);
|
||||
}
|
||||
}
|
||||
const newState = { boards: this.boards, ports: this.ports };
|
||||
if (this.client) {
|
||||
this.client.notifyAttachedBoardsChanged({ oldState, newState });
|
||||
}
|
||||
}
|
||||
|
||||
detach(...toRemove: Array<Board | Port>): void {
|
||||
const oldState = { boards: this.boards.slice(), ports: this.ports.slice() };
|
||||
for (const what of toRemove) {
|
||||
if (Board.is(what)) {
|
||||
const index = this.boards.indexOf(what);
|
||||
if (index === -1) {
|
||||
throw new Error(`${what} board is not attached. Boards were: ${JSON.stringify(oldState.boards)}`);
|
||||
}
|
||||
this.boards.splice(index, 1);
|
||||
if (what.port) {
|
||||
const portIndex = this.ports.findIndex(port => Port.sameAs(what.port, port));
|
||||
if (portIndex === -1) {
|
||||
throw new Error(`${what} port is not available. Ports were: ${JSON.stringify(oldState.ports)}`);
|
||||
}
|
||||
this.ports.splice(portIndex, 1);
|
||||
}
|
||||
} else {
|
||||
const index = this.ports.indexOf(what);
|
||||
if (index === -1) {
|
||||
throw new Error(`${what} port is not available. Ports were: ${JSON.stringify(oldState.ports)}`);
|
||||
}
|
||||
this.ports.splice(index, 1);
|
||||
}
|
||||
}
|
||||
const newState = { boards: this.boards, ports: this.ports };
|
||||
if (this.client) {
|
||||
this.client.notifyAttachedBoardsChanged({ oldState, newState });
|
||||
}
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.setState({ boards: [], ports: [], silent: true });
|
||||
}
|
||||
|
||||
setState({ boards, ports, silent }: { boards: Board[], ports: Port[], silent?: boolean }): void {
|
||||
const oldState = { boards: this.boards, ports: this.ports };
|
||||
const newState = { boards, ports };
|
||||
if (this.client && !silent) {
|
||||
this.client.notifyAttachedBoardsChanged({ oldState, newState });
|
||||
}
|
||||
}
|
||||
|
||||
portFor(board: Board): Port {
|
||||
if (!board.port) {
|
||||
throw new Error(`${JSON.stringify(board)} does not have a port.`);
|
||||
}
|
||||
const port = this.ports.find(port => Port.sameAs(port, board.port));
|
||||
if (!port) {
|
||||
throw new Error(`Could not find port for board: ${JSON.stringify(board)}. Ports were: ${JSON.stringify(this.ports)}.`);
|
||||
}
|
||||
return port;
|
||||
}
|
||||
|
||||
// BoardsService API
|
||||
|
||||
async getAttachedBoards(): Promise<Board[]> {
|
||||
return this.boards;
|
||||
}
|
||||
|
||||
async getAvailablePorts(): Promise<Port[]> {
|
||||
throw this.ports;
|
||||
}
|
||||
|
||||
async getBoardDetails(): Promise<BoardDetails> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getBoardPackage(): Promise<BoardsPackage> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getContainerBoardPackage(): Promise<BoardsPackage> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
searchBoards(): Promise<Array<Board & { packageName: string; }>> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
install(): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
uninstall(): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
search(): Promise<BoardsPackage[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.reset();
|
||||
this.client = undefined;
|
||||
}
|
||||
|
||||
setClient(client: BoardsServiceClient | undefined): void {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@injectable()
|
||||
class MockStorageService implements StorageService {
|
||||
|
||||
private store: Map<string, any> = new Map();
|
||||
|
||||
reset(): void {
|
||||
this.store.clear();
|
||||
}
|
||||
|
||||
async setData<T>(key: string, data: T): Promise<void> {
|
||||
this.store.set(key, data);
|
||||
}
|
||||
|
||||
async getData<T>(key: string): Promise<T | undefined>;
|
||||
async getData<T>(key: string, defaultValue?: T): Promise<T | undefined> {
|
||||
const data = this.store.get(key);
|
||||
return data ? data : defaultValue;
|
||||
}
|
||||
|
||||
}
|
116
arduino-ide-extension/src/test/common/boards-service.test.ts
Normal file
116
arduino-ide-extension/src/test/common/boards-service.test.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import { expect } from 'chai';
|
||||
import { ConfigOption, AttachedBoardsChangeEvent } from '../../common/protocol';
|
||||
import { fail } from 'assert';
|
||||
|
||||
describe('boards-service', () => {
|
||||
|
||||
describe('AttachedBoardsChangeEvent', () => {
|
||||
|
||||
it('should detect one attached port', () => {
|
||||
const event = <AttachedBoardsChangeEvent & any>{
|
||||
oldState: {
|
||||
boards: [
|
||||
{ name: 'Arduino MKR1000', fqbn: 'arduino:samd:mkr1000', port: '/dev/cu.usbmodem14601' },
|
||||
{ name: 'Arduino Uno', fqbn: 'arduino:avr:uno', port: '/dev/cu.usbmodem14501' }
|
||||
],
|
||||
ports: [
|
||||
{ protocol: 'serial', address: '/dev/cu.usbmodem14501' },
|
||||
{ protocol: 'serial', address: '/dev/cu.usbmodem14601' },
|
||||
{ protocol: 'serial', address: '/dev/cu.Bluetooth-Incoming-Port' },
|
||||
{ protocol: 'serial', address: '/dev/cu.MALS' },
|
||||
{ protocol: 'serial', address: '/dev/cu.SOC' }
|
||||
]
|
||||
},
|
||||
newState: {
|
||||
boards: [
|
||||
{ name: 'Arduino MKR1000', fqbn: 'arduino:samd:mkr1000', 'port': '/dev/cu.usbmodem1460' },
|
||||
{ name: 'Arduino Uno', fqbn: 'arduino:avr:uno', 'port': '/dev/cu.usbmodem14501' }
|
||||
],
|
||||
ports: [
|
||||
{ protocol: 'serial', address: '/dev/cu.SLAB_USBtoUART' },
|
||||
{ protocol: 'serial', address: '/dev/cu.usbmodem14501' },
|
||||
{ protocol: 'serial', address: '/dev/cu.usbmodem14601' },
|
||||
{ protocol: 'serial', address: '/dev/cu.Bluetooth-Incoming-Port' },
|
||||
{ protocol: 'serial', address: '/dev/cu.MALS' },
|
||||
{ protocol: 'serial', address: '/dev/cu.SOC' }
|
||||
]
|
||||
}
|
||||
};
|
||||
const diff = AttachedBoardsChangeEvent.diff(event);
|
||||
expect(diff.attached.boards).to.be.empty; // tslint:disable-line:no-unused-expression
|
||||
expect(diff.detached.boards).to.be.empty; // tslint:disable-line:no-unused-expression
|
||||
expect(diff.detached.ports).to.be.empty; // tslint:disable-line:no-unused-expression
|
||||
expect(diff.attached.ports.length).to.be.equal(1);
|
||||
expect(diff.attached.ports[0].address).to.be.equal('/dev/cu.SLAB_USBtoUART');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('ConfigOption', () => {
|
||||
|
||||
([
|
||||
['', false],
|
||||
['foo', true],
|
||||
['foo:bar', true],
|
||||
['foo:bar:baz', true],
|
||||
['foo:', false],
|
||||
[':foo', false],
|
||||
[':foo:', false],
|
||||
['foo:bar:', false]
|
||||
] as Array<[string, boolean]>).forEach(([fqbn, expectation]) => {
|
||||
it(`"${fqbn}" should ${expectation ? '' : 'not '}be a valid FQBN`, () => {
|
||||
expect(ConfigOption.isValidFqbn(fqbn)).to.be.equal(expectation);
|
||||
});
|
||||
});
|
||||
|
||||
([
|
||||
['', false],
|
||||
['foo:bar:option1', false],
|
||||
['foo:bar:option1=', false],
|
||||
['foo:bar:baz:option1=value1', true],
|
||||
['foo:bar:baz:option1=value1,option2=value2', true],
|
||||
['foo:bar:baz:option1=value1,option2=value2,', false],
|
||||
['foo:bar:baz,option1=value1,option2=value2', false],
|
||||
['foo:bar:baz:option1=value1,option2=value2,options3', false],
|
||||
['foo:bar:baz:option1=value1,option2=value2, options3=value3', false],
|
||||
] as Array<[string, boolean]>).forEach(([fqbn, expectation]) => {
|
||||
it(`"${fqbn}" should ${expectation ? '' : 'not '}be a valid FQBN with options`, () => {
|
||||
expect(ConfigOption.isValidFqbnWithOptions(fqbn)).to.be.equal(expectation);
|
||||
});
|
||||
});
|
||||
|
||||
([
|
||||
[
|
||||
'foo:bar:baz',
|
||||
JSON.parse('[{"label":"CPU Frequency","option":"xtal","values":[{"value":"80","label":"80 MHz","selected":true},{"value":"160","label":"160 MHz","selected":false}]},{"label":"VTables","option":"vt","values":[{"value":"flash","label":"Flash","selected":true},{"value":"heap","label":"Heap","selected":false},{"value":"iram","label":"IRAM","selected":false}]},{"label":"Exceptions","option":"exception","values":[{"value":"legacy","label":"Legacy (new can return nullptr)","selected":true},{"value":"disabled","label":"Disabled (new can abort)","selected":false},{"value":"enabled","label":"Enabled","selected":false}]},{"label":"SSL Support","option":"ssl","values":[{"value":"all","label":"All SSL ciphers (most compatible)","selected":true},{"value":"basic","label":"Basic SSL ciphers (lower ROM use)","selected":false}]},{"label":"Flash Size","option":"eesz","values":[{"value":"4M2M","label":"4MB (FS:2MB OTA:~1019KB)","selected":true},{"value":"4M3M","label":"4MB (FS:3MB OTA:~512KB)","selected":false},{"value":"4M1M","label":"4MB (FS:1MB OTA:~1019KB)","selected":false},{"value":"4M","label":"4MB (FS:none OTA:~1019KB)","selected":false}]},{"label":"lwIP Variant","option":"ip","values":[{"value":"lm2f","label":"v2 Lower Memory","selected":true},{"value":"hb2f","label":"v2 Higher Bandwidth","selected":false},{"value":"lm2n","label":"v2 Lower Memory (no features)","selected":false},{"value":"hb2n","label":"v2 Higher Bandwidth (no features)","selected":false},{"value":"lm6f","label":"v2 IPv6 Lower Memory","selected":false},{"value":"hb6f","label":"v2 IPv6 Higher Bandwidth","selected":false},{"value":"hb1","label":"v1.4 Higher Bandwidth","selected":false},{"value":"src","label":"v1.4 Compile from source","selected":false}]},{"label":"Debug port","option":"dbg","values":[{"value":"Disabled","label":"Disabled","selected":true},{"value":"Serial","label":"Serial","selected":false},{"value":"Serial1","label":"Serial1","selected":false}]},{"label":"Debug Level","option":"lvl","values":[{"value":"None____","label":"None","selected":true},{"value":"SSL","label":"SSL","selected":false},{"value":"TLS_MEM","label":"TLS_MEM","selected":false},{"value":"HTTP_CLIENT","label":"HTTP_CLIENT","selected":false},{"value":"HTTP_SERVER","label":"HTTP_SERVER","selected":false},{"value":"SSLTLS_MEM","label":"SSL+TLS_MEM","selected":false},{"value":"SSLHTTP_CLIENT","label":"SSL+HTTP_CLIENT","selected":false},{"value":"SSLHTTP_SERVER","label":"SSL+HTTP_SERVER","selected":false},{"value":"TLS_MEMHTTP_CLIENT","label":"TLS_MEM+HTTP_CLIENT","selected":false},{"value":"TLS_MEMHTTP_SERVER","label":"TLS_MEM+HTTP_SERVER","selected":false},{"value":"HTTP_CLIENTHTTP_SERVER","label":"HTTP_CLIENT+HTTP_SERVER","selected":false},{"value":"SSLTLS_MEMHTTP_CLIENT","label":"SSL+TLS_MEM+HTTP_CLIENT","selected":false},{"value":"SSLTLS_MEMHTTP_SERVER","label":"SSL+TLS_MEM+HTTP_SERVER","selected":false},{"value":"SSLHTTP_CLIENTHTTP_SERVER","label":"SSL+HTTP_CLIENT+HTTP_SERVER","selected":false},{"value":"TLS_MEMHTTP_CLIENTHTTP_SERVER","label":"TLS_MEM+HTTP_CLIENT+HTTP_SERVER","selected":false},{"value":"SSLTLS_MEMHTTP_CLIENTHTTP_SERVER","label":"SSL+TLS_MEM+HTTP_CLIENT+HTTP_SERVER","selected":false},{"value":"CORE","label":"CORE","selected":false},{"value":"WIFI","label":"WIFI","selected":false},{"value":"HTTP_UPDATE","label":"HTTP_UPDATE","selected":false},{"value":"UPDATER","label":"UPDATER","selected":false},{"value":"OTA","label":"OTA","selected":false},{"value":"OOM","label":"OOM","selected":false},{"value":"MDNS","label":"MDNS","selected":false},{"value":"COREWIFIHTTP_UPDATEUPDATEROTAOOMMDNS","label":"CORE+WIFI+HTTP_UPDATE+UPDATER+OTA+OOM+MDNS","selected":false},{"value":"SSLTLS_MEMHTTP_CLIENTHTTP_SERVERCOREWIFIHTTP_UPDATEUPDATEROTAOOMMDNS","label":"SSL+TLS_MEM+HTTP_CLIENT+HTTP_SERVER+CORE+WIFI+HTTP_UPDATE+UPDATER+OTA+OOM+MDNS","selected":false},{"value":"NoAssert-NDEBUG","label":"NoAssert-NDEBUG","selected":false}]},{"label":"Erase Flash","option":"wipe","values":[{"value":"none","label":"Only Sketch","selected":true},{"value":"sdk","label":"Sketch + WiFi Settings","selected":false},{"value":"all","label":"All Flash Contents","selected":false}]},{"label":"Upload Speed","option":"baud","values":[{"value":"115200","label":"115200","selected":true},{"value":"57600","label":"57600","selected":false},{"value":"230400","label":"230400","selected":false},{"value":"460800","label":"460800","selected":false},{"value":"921600","label":"921600","selected":false},{"value":"3000000","label":"3000000","selected":false}]}]'),
|
||||
'foo:bar:baz:xtal=80,vt=flash,exception=legacy,ssl=all,eesz=4M2M,ip=lm2f,dbg=Disabled,lvl=None____,wipe=none,baud=115200'
|
||||
],
|
||||
[
|
||||
'foo:bar:baz',
|
||||
JSON.parse('[]'),
|
||||
'foo:bar:baz'
|
||||
],
|
||||
[
|
||||
'foo:bar:baz:xtal=80',
|
||||
{},
|
||||
undefined
|
||||
]
|
||||
] as Array<[string, Array<ConfigOption>, string | undefined]>).forEach(([fqbn, configOptions, expectation]) => {
|
||||
it(`should ${expectation ? `append` : 'throw an error when appending'}config options to ${fqbn}`, () => {
|
||||
if (!expectation) {
|
||||
try {
|
||||
ConfigOption.decorate(fqbn, configOptions);
|
||||
fail(`Expected a failure when decorating ${fqbn} with config options.`);
|
||||
} catch (e) {
|
||||
expect(e).to.be.instanceOf(ConfigOption.ConfigOptionError);
|
||||
}
|
||||
} else {
|
||||
expect(ConfigOption.decorate(fqbn, configOptions)).to.be.equal(expectation);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -1,4 +0,0 @@
|
||||
Known issues:
|
||||
- arduino-cli does not get stopped reliably upon app shutdown
|
||||
- startup time is not as fast we'd like to have it
|
||||
- in Electron on OSX, the application menu is incomplete on app startup (see https://github.com/theia-ide/theia/issues/5100)
|
Loading…
x
Reference in New Issue
Block a user