mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-11-22 00:26:09 +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:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user