arduino-ide/arduino-ide-extension/src/node/boards-service-impl.ts
Akos Kitta 2f33038695 No disconnect/reconnect when DNDing the widget.
- Updated to next Theia,
 - Added elecron launch config,
 - Yet another syling for the input + selects,
 - Close monitor connection on widget close not detach.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:28 +01:00

371 lines
16 KiB
TypeScript

import * as PQueue from 'p-queue';
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 } from '../common/protocol/boards-service';
import {
PlatformSearchReq,
PlatformSearchResp,
PlatformInstallReq,
PlatformInstallResp,
PlatformListReq,
PlatformListResp,
Platform,
PlatformUninstallReq,
PlatformUninstallResp
} from './cli-protocol/commands/core_pb';
import { CoreClientProvider } from './core-client-provider';
import { BoardListReq, BoardListResp } from './cli-protocol/commands/board_pb';
import { ToolOutputServiceServer } from '../common/protocol/tool-output-service';
import { Installable } from '../common/protocol/installable';
@injectable()
export class BoardsServiceImpl implements BoardsService {
@inject(ILogger)
protected logger: ILogger;
@inject(ILogger)
@named('discovery')
protected discoveryLogger: ILogger;
@inject(CoreClientProvider)
protected readonly coreClientProvider: CoreClientProvider;
@inject(ToolOutputServiceServer)
protected readonly toolOutputService: ToolOutputServiceServer;
protected discoveryInitialized = false;
protected discoveryTimer: NodeJS.Timer | undefined;
/**
* Poor man's serial discovery:
* 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 started = new Deferred<void>();
protected client: BoardsServiceClient | undefined;
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
@postConstruct()
protected async init(): Promise<void> {
this.discoveryTimer = setInterval(() => {
this.discoveryLogger.trace('Discovering attached boards and available ports...');
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.discoveryLogger.info(`${message} - Discovered boards: ${JSON.stringify(newBoards)} and available ports: ${JSON.stringify(newPorts)}`);
if (this.client) {
this.client.notifyAttachedBoardsChanged({
oldState: {
boards: oldBoards,
ports: oldPorts
},
newState: {
boards: newBoards,
ports: newPorts
}
});
}
}
const sortedBoards = boards.sort(Board.compare);
const sortedPorts = ports.sort(Port.compare);
this.discoveryLogger.trace(`Discovery done. Boards: ${JSON.stringify(sortedBoards)}. Ports: ${sortedPorts}`);
if (!this.discoveryInitialized) {
update([], sortedBoards, [], sortedPorts, 'Initialized attached boards and available ports.');
this.discoveryInitialized = true;
this.started.resolve();
} else {
Promise.all([
this.getAttachedBoards(),
this.getAvailablePorts()
]).then(([{ boards: currentBoards }, { ports: 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.');
return;
}
// `currentBoards` is already sorted.
for (let i = 0; i < sortedBoards.length; i++) {
if (Board.compare(sortedBoards[i], currentBoards[i]) !== 0) {
update(currentBoards, sortedBoards, currentPorts, sortedPorts, 'Updated discovered boards.');
return;
}
}
for (let i = 0; i < sortedPorts.length; i++) {
if (Port.compare(sortedPorts[i], currentPorts[i]) !== 0) {
update(currentBoards, sortedBoards, currentPorts, sortedPorts, 'Updated discovered boards.');
return;
}
}
this.discoveryLogger.trace('No new boards were discovered.');
});
}
});
}, 1000);
}
setClient(client: BoardsServiceClient | undefined): void {
this.client = client;
}
dispose(): void {
this.logger.info('>>> Disposing boards service...');
this.queue.pause();
this.queue.clear();
if (this.discoveryTimer !== undefined) {
clearInterval(this.discoveryTimer);
}
this.logger.info('<<< Disposed boards service.');
this.client = undefined;
}
async getAttachedBoards(): Promise<{ boards: Board[] }> {
await this.started.promise;
return this.attachedBoards;
}
async getAvailablePorts(): Promise<{ ports: Port[] }> {
await this.started.promise;
return this.availablePorts;
}
private async doGetAttachedBoardsAndAvailablePorts(): Promise<{ boards: Board[], ports: Port[] }> {
return this.queue.add(() => {
return new Promise<{ boards: Board[], ports: Port[] }>(async resolve => {
const coreClient = await this.coreClientProvider.getClient();
const boards: Board[] = [];
const ports: Port[] = [];
if (!coreClient) {
resolve({ boards, ports });
return;
}
const { client, instance } = coreClient;
const req = new BoardListReq();
req.setInstance(instance);
const resp = await new Promise<BoardListResp>((resolve, reject) => client.boardList(req, (err, resp) => (!!err ? reject : resolve)(!!err ? err : resp)));
const portsList = resp.getPortsList();
// TODO: remove unknown board mocking!
// You also have to manually import `DetectedPort`.
// const unknownPortList = new DetectedPort();
// unknownPortList.setAddress(platform() === 'win32' ? 'COM3' : platform() === 'darwin' ? '/dev/cu.usbmodem94401' : '/dev/ttyACM0');
// unknownPortList.setProtocol('serial');
// unknownPortList.setProtocolLabel('Serial Port (USB)');
// portsList.push(unknownPortList);
for (const portList of portsList) {
const protocol = Port.Protocol.toProtocol(portList.getProtocol());
const address = portList.getAddress();
// Available ports can exist with unknown attached boards.
// The `BoardListResp` looks like this for a known attached board:
// [
// {
// "address": "COM10",
// "protocol": "serial",
// "protocol_label": "Serial Port (USB)",
// "boards": [
// {
// "name": "Arduino MKR1000",
// "FQBN": "arduino:samd:mkr1000"
// }
// ]
// }
// ]
// And the `BoardListResp` looks like this for an unknown board:
// [
// {
// "address": "COM9",
// "protocol": "serial",
// "protocol_label": "Serial Port (USB)",
// }
// ]
ports.push({ protocol, address });
for (const board of portList.getBoardsList()) {
const name = board.getName() || 'unknown';
const 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}.`);
}
}
}
// TODO: remove mock board!
// boards.push(...[
// <AttachedSerialBoard>{ name: 'Arduino/Genuino Uno', fqbn: 'arduino:avr:uno', port: '/dev/cu.usbmodem14201' },
// <AttachedSerialBoard>{ name: 'Arduino/Genuino Uno', fqbn: 'arduino:avr:uno', port: '/dev/cu.usbmodem142xx' },
// ]);
resolve({ boards, ports });
})
});
}
async search(options: { query?: string }): Promise<{ items: BoardPackage[] }> {
const coreClient = await this.coreClientProvider.getClient();
if (!coreClient) {
return { items: [] };
}
const { client, instance } = coreClient;
const installedPlatformsReq = new PlatformListReq();
installedPlatformsReq.setInstance(instance);
const installedPlatformsResp = await new Promise<PlatformListResp>((resolve, reject) =>
client.platformList(installedPlatformsReq, (err, resp) => (!!err ? reject : resolve)(!!err ? err : resp))
);
const installedPlatforms = installedPlatformsResp.getInstalledPlatformList();
const req = new PlatformSearchReq();
req.setSearchArgs(options.query || "");
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 toPackage = (platform: Platform) => {
let installedVersion: string | undefined;
const matchingPlatform = installedPlatforms.find(ip => ip.getId() === platform.getId());
if (!!matchingPlatform) {
installedVersion = matchingPlatform.getInstalled();
}
return {
id: platform.getId(),
name: platform.getName(),
author: platform.getMaintainer(),
availableVersions: [platform.getLatest()],
description: platform.getBoardsList().map(b => b.getName()).join(", "),
installable: true,
summary: "Boards included in this package:",
installedVersion,
boards: platform.getBoardsList().map(b => <Board>{ name: b.getName(), fqbn: b.getFqbn() }),
moreInfoLink: platform.getWebsite()
}
}
// We must group the cores by ID, and sort platforms by, first the installed version, then version alphabetical order.
// Otherwise we lose the FQBN information.
const groupedById: Map<string, Platform[]> = new Map();
for (const platform of resp.getSearchOutputList()) {
const id = platform.getId();
if (groupedById.has(id)) {
groupedById.get(id)!.push(platform);
} else {
groupedById.set(id, [platform]);
}
}
const installedAwareVersionComparator = (left: Platform, right: Platform) => {
// XXX: we cannot rely on `platform.getInstalled()`, it is always an empty string.
const leftInstalled = !!installedPlatforms.find(ip => ip.getId() === left.getId() && ip.getInstalled() === left.getLatest());
const rightInstalled = !!installedPlatforms.find(ip => ip.getId() === right.getId() && ip.getInstalled() === right.getLatest());
if (leftInstalled && !rightInstalled) {
return -1;
}
if (!leftInstalled && rightInstalled) {
return 1;
}
return Installable.Version.COMPARATOR(right.getLatest(), left.getLatest()); // Higher version comes first.
}
for (const id of groupedById.keys()) {
groupedById.get(id)!.sort(installedAwareVersionComparator);
}
for (const id of groupedById.keys()) {
for (const platform of groupedById.get(id)!) {
const id = platform.getId();
const pkg = packages.get(id);
if (pkg) {
pkg.availableVersions.push(platform.getLatest());
pkg.availableVersions.sort(Installable.Version.COMPARATOR);
} else {
packages.set(id, toPackage(platform));
}
}
}
return { items: [...packages.values()] };
}
async install(options: { item: BoardPackage, version?: Installable.Version }): Promise<void> {
const pkg = options.item;
const version = !!options.version ? options.version : pkg.availableVersions[0];
const coreClient = await this.coreClientProvider.getClient();
if (!coreClient) {
return;
}
const { client, instance } = coreClient;
const [platform, boardName] = pkg.id.split(":");
const req = new PlatformInstallReq();
req.setInstance(instance);
req.setArchitecture(boardName);
req.setPlatformPackage(platform);
req.setVersion(version);
console.info("Starting board installation", pkg);
const resp = client.platformInstall(req);
resp.on('data', (r: PlatformInstallResp) => {
const prog = r.getProgress();
if (prog && prog.getFile()) {
this.toolOutputService.publishNewOutput("board download", `downloading ${prog.getFile()}\n`)
}
});
await new Promise<void>((resolve, reject) => {
resp.on('end', resolve);
resp.on('error', reject);
});
if (this.client) {
this.client.notifyBoardInstalled({ pkg });
}
console.info("Board installation done", pkg);
}
async uninstall(options: { item: BoardPackage }): Promise<void> {
const pkg = options.item;
const coreClient = await this.coreClientProvider.getClient();
if (!coreClient) {
return;
}
const { client, instance } = coreClient;
const [platform, boardName] = pkg.id.split(":");
const req = new PlatformUninstallReq();
req.setInstance(instance);
req.setArchitecture(boardName);
req.setPlatformPackage(platform);
console.info("Starting board uninstallation", pkg);
let logged = false;
const resp = client.platformUninstall(req);
resp.on('data', (_: PlatformUninstallResp) => {
if (!logged) {
this.toolOutputService.publishNewOutput("board uninstall", `uninstalling ${pkg.id}\n`)
logged = true;
}
})
await new Promise<void>((resolve, reject) => {
resp.on('end', resolve);
resp.on('error', reject);
});
if (this.client) {
this.client.notifyBoardUninstalled({ pkg });
}
console.info("Board uninstallation done", pkg);
}
}