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:
Akos Kitta
2020-03-03 17:03:11 +01:00
parent 5c16f8d6c9
commit 12f2aa35ff
43 changed files with 1905 additions and 895 deletions

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -1,10 +0,0 @@
export interface Detailable<T> {
detail(options: Detailable.Options): Promise<{ item?: T }>;
}
export namespace Detailable {
export interface Options {
readonly id: string;
}
}

View File

@@ -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;
}
}
}