Akos Kitta 69ae38effa
feat: simplify board and port handling (#2165)
Use Arduino CLI revision `38479dc`

Closes #43
Closes #82
Closes #1319
Closes #1366
Closes #2143
Closes #2158

Ref: 38479dc706

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2023-08-18 14:42:50 +02:00

224 lines
6.1 KiB
TypeScript

import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { LocalStorageService } from '@theia/core/lib/browser/storage-service';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { ILogger } from '@theia/core/lib/common/logger';
import { deepClone } from '@theia/core/lib/common/objects';
import { inject, injectable, named } from '@theia/core/shared/inversify';
import {
BoardDetails,
BoardsService,
ConfigOption,
Programmer,
} from '../../common/protocol';
import { notEmpty } from '../../common/utils';
import { NotificationCenter } from '../notification-center';
@injectable()
export class BoardsDataStore implements FrontendApplicationContribution {
@inject(ILogger)
@named('store')
private readonly logger: ILogger;
@inject(BoardsService)
private readonly boardsService: BoardsService;
@inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter;
@inject(LocalStorageService)
private readonly storageService: LocalStorageService;
private readonly onChangedEmitter = new Emitter<string[]>();
private readonly toDispose = new DisposableCollection(this.onChangedEmitter);
onStart(): void {
this.toDispose.push(
this.notificationCenter.onPlatformDidInstall(async ({ item }) => {
const dataDidChangePerFqbn: string[] = [];
for (const fqbn of item.boards
.map(({ fqbn }) => fqbn)
.filter(notEmpty)
.filter((fqbn) => !!fqbn)) {
const key = this.getStorageKey(fqbn);
let data = await this.storageService.getData<ConfigOption[]>(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);
dataDidChangePerFqbn.push(fqbn);
}
}
}
}
if (dataDidChangePerFqbn.length) {
this.fireChanged(...dataDidChangePerFqbn);
}
})
);
}
onStop(): void {
this.toDispose.dispose();
}
get onChanged(): Event<string[]> {
return this.onChangedEmitter.event;
}
async appendConfigToFqbn(
fqbn: string | undefined
): Promise<string | undefined> {
if (!fqbn) {
return undefined;
}
const { configOptions } = await this.getData(fqbn);
return ConfigOption.decorate(fqbn, configOptions);
}
async getData(fqbn: string | undefined): Promise<BoardsDataStore.Data> {
if (!fqbn) {
return BoardsDataStore.Data.EMPTY;
}
const key = this.getStorageKey(fqbn);
let data = await this.storageService.getData<
BoardsDataStore.Data | undefined
>(key, undefined);
if (BoardsDataStore.Data.is(data)) {
return data;
}
const boardDetails = await this.getBoardDetailsSafe(fqbn);
if (!boardDetails) {
return BoardsDataStore.Data.EMPTY;
}
data = {
configOptions: boardDetails.configOptions,
programmers: boardDetails.programmers,
};
await this.storageService.setData(key, data);
return data;
}
async selectProgrammer({
fqbn,
selectedProgrammer,
}: {
fqbn: string;
selectedProgrammer: Programmer;
}): Promise<boolean> {
const data = deepClone(await this.getData(fqbn));
const { programmers } = data;
if (!programmers.find((p) => Programmer.equals(selectedProgrammer, p))) {
return false;
}
await this.setData({
fqbn,
data: { ...data, selectedProgrammer },
});
this.fireChanged(fqbn);
return true;
}
async selectConfigOption({
fqbn,
option,
selectedValue,
}: {
fqbn: string;
option: string;
selectedValue: string;
}): Promise<boolean> {
const data = deepClone(await this.getData(fqbn));
const { configOptions } = data;
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;
}
await this.setData({ fqbn, data });
this.fireChanged(fqbn);
return true;
}
protected async setData({
fqbn,
data,
}: {
fqbn: string;
data: BoardsDataStore.Data;
}): Promise<void> {
const key = this.getStorageKey(fqbn);
return this.storageService.setData(key, data);
}
protected getStorageKey(fqbn: string): string {
return `.arduinoIDE-configOptions-${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(...fqbn: string[]): void {
this.onChangedEmitter.fire(fqbn);
}
}
export namespace BoardsDataStore {
export interface Data {
readonly configOptions: ConfigOption[];
readonly programmers: Programmer[];
readonly selectedProgrammer?: Programmer;
}
export namespace Data {
export const EMPTY: Data = {
configOptions: [],
programmers: [],
};
export function is(arg: any): arg is Data {
return (
!!arg &&
'configOptions' in arg &&
Array.isArray(arg['configOptions']) &&
'programmers' in arg &&
Array.isArray(arg['programmers'])
);
}
}
}