diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index 50d34ff6..f2b86f57 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -30,6 +30,7 @@ "react-select": "^3.0.4", "p-queue": "^5.0.0", "ps-tree": "^1.2.0", + "string-natural-compare": "^2.0.3", "tree-kill": "^1.2.1", "upath": "^1.1.2", "which": "^1.3.1" diff --git a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx index 7a790c4b..a0329334 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx +++ b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx @@ -165,7 +165,10 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C this.registerSketchesInMenu(this.menuRegistry); - this.boardsService.getAttachedBoards().then(({ boards }) => this.boardsServiceClient.tryReconnect(boards)); + Promise.all([ + this.boardsService.getAttachedBoards(), + this.boardsService.getAvailablePorts() + ]).then(([{ boards }, { ports }]) => this.boardsServiceClient.tryReconnect(boards, ports)); } registerToolbarItems(registry: TabBarToolbarRegistry): void { @@ -270,7 +273,7 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C if (!selectedPort) { throw new Error('No ports selected. Please select a port.'); } - await this.coreService.upload({ uri: uri.toString(), board: boardsConfig.selectedBoard, port: selectedPort }); + await this.coreService.upload({ uri: uri.toString(), board: boardsConfig.selectedBoard, port: selectedPort.address }); } catch (e) { await this.messageService.error(e.toString()); } finally { diff --git a/arduino-ide-extension/src/browser/boards/boards-config.tsx b/arduino-ide-extension/src/browser/boards/boards-config.tsx index 3e294eb0..fef02883 100644 --- a/arduino-ide-extension/src/browser/boards/boards-config.tsx +++ b/arduino-ide-extension/src/browser/boards/boards-config.tsx @@ -1,13 +1,13 @@ import * as React from 'react'; import { DisposableCollection } from '@theia/core'; -import { BoardsService, Board, AttachedSerialBoard, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service'; +import { BoardsService, Board, Port, AttachedSerialBoard, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service'; import { BoardsServiceClientImpl } from './boards-service-client-impl'; export namespace BoardsConfig { export interface Config { selectedBoard?: Board; - selectedPort?: string; + selectedPort?: Port; } export interface Props { @@ -19,7 +19,8 @@ export namespace BoardsConfig { export interface State extends Config { searchResults: Array; - knownPorts: string[]; + knownPorts: Port[]; + showAllPorts: boolean; } } @@ -47,7 +48,7 @@ export abstract class Item extends React.Component<{ {label} {!detail ? '' :
{detail}
} - {!selected ? '' :
} + {!selected ? '' :
} ; } @@ -68,16 +69,17 @@ export class BoardsConfig extends React.Component this.updatePorts(boards)); + this.props.boardsService.getAvailablePorts().then(({ ports }) => this.updatePorts(ports)); const { boardsServiceClient: client } = this.props; this.toDispose.pushAll([ - client.onBoardsChanged(event => this.updatePorts(event.newState.boards, AttachedBoardsChangeEvent.diff(event).detached)), + client.onBoardsChanged(event => this.updatePorts(event.newState.ports, AttachedBoardsChangeEvent.diff(event).detached.ports)), client.onBoardsConfigChanged(({ selectedBoard, selectedPort }) => { this.setState({ selectedBoard, selectedPort }, () => this.fireConfigChanged()); }) @@ -101,11 +103,11 @@ export class BoardsConfig extends React.Component this.setState({ searchResults })); } - protected updatePorts = (boards: Board[] = [], detachedBoards: Board[] = []) => { - this.queryPorts(Promise.resolve({ boards })).then(({ knownPorts }) => { + protected updatePorts = (ports: Port[] = [], removedPorts: Port[] = []) => { + this.queryPorts(Promise.resolve({ ports })).then(({ knownPorts }) => { let { selectedPort } = this.state; - const removedPorts = detachedBoards.filter(AttachedSerialBoard.is).map(({ port }) => port); - if (!!selectedPort && removedPorts.indexOf(selectedPort) !== -1) { + // If the currently selected port is not available anymore, unset the selected port. + if (removedPorts.some(port => Port.equals(port, selectedPort))) { selectedPort = undefined; } this.setState({ knownPorts, selectedPort }, () => this.fireConfigChanged()); @@ -130,18 +132,24 @@ export class BoardsConfig extends React.Component = this.attachedBoards) => { - return new Promise<{ knownPorts: string[] }>(resolve => { - attachedBoards - .then(({ boards }) => boards - .filter(AttachedSerialBoard.is) - .map(({ port }) => port) - .sort()) + protected get availablePorts(): Promise<{ ports: 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 selectPort = (selectedPort: string | undefined) => { + protected toggleFilterPorts = () => { + this.setState({ showAllPorts: !this.state.showAllPorts }); + } + + protected selectPort = (selectedPort: Port | undefined) => { this.setState({ selectedPort }, () => this.fireConfigChanged()); } @@ -156,17 +164,20 @@ export class BoardsConfig extends React.Component {this.renderContainer('boards', this.renderBoards.bind(this))} - {this.renderContainer('ports', this.renderPorts.bind(this))} + {this.renderContainer('ports', this.renderPorts.bind(this), this.renderPortsFooter.bind(this))} ; } - protected renderContainer(title: string, contentRenderer: () => React.ReactNode): React.ReactNode { + protected renderContainer(title: string, contentRenderer: () => React.ReactNode, footerRenderer?: () => React.ReactNode): React.ReactNode { return
{title}
{contentRenderer()} +
+ {(footerRenderer ? footerRenderer() : '')} +
; } @@ -214,7 +225,9 @@ export class BoardsConfig extends React.Component true : Port.isBoardPort; + const ports = this.state.knownPorts.filter(filter); + return !ports.length ? (
No ports discovered @@ -222,17 +235,31 @@ export class BoardsConfig extends React.Component - {this.state.knownPorts.map(port => - key={port} + {ports.map(port => + key={Port.toString(port)} item={port} - label={port} - selected={this.state.selectedPort === port} + label={Port.toString(port)} + selected={Port.equals(this.state.selectedPort, port)} onClick={this.selectPort} />)}
); } + protected renderPortsFooter(): React.ReactNode { + return
+ +
; + } + } export namespace BoardsConfig { @@ -244,7 +271,7 @@ export namespace BoardsConfig { if (AttachedSerialBoard.is(other)) { return !!selectedBoard && Board.equals(other, selectedBoard) - && selectedPort === other.port; + && Port.sameAs(selectedPort, other.port); } return sameAs(config, other); } @@ -260,7 +287,7 @@ export namespace BoardsConfig { return options.default; } const { name } = selectedBoard; - return `${name}${port ? ' at ' + port : ''}`; + return `${name}${port ? ' at ' + Port.toString(port) : ''}`; } } diff --git a/arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts b/arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts index b11476b3..e2ac6a80 100644 --- a/arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts +++ b/arduino-ide-extension/src/browser/boards/boards-service-client-impl.ts @@ -3,9 +3,8 @@ import { Emitter } from '@theia/core/lib/common/event'; import { ILogger } from '@theia/core/lib/common/logger'; import { LocalStorageService } from '@theia/core/lib/browser/storage-service'; import { RecursiveRequired } from '../../common/types'; -import { BoardsServiceClient, AttachedBoardsChangeEvent, BoardInstalledEvent, AttachedSerialBoard, Board } from '../../common/protocol/boards-service'; +import { BoardsServiceClient, AttachedBoardsChangeEvent, BoardInstalledEvent, AttachedSerialBoard, Board, Port } from '../../common/protocol/boards-service'; import { BoardsConfig } from './boards-config'; -import { MaybePromise } from '@theia/core'; @injectable() export class BoardsServiceClientImpl implements BoardsServiceClient { @@ -40,29 +39,27 @@ export class BoardsServiceClientImpl implements BoardsServiceClient { } notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void { - this.logger.info('Attached boards changed: ', JSON.stringify(event)); + this.logger.info('Attached boards and available ports changed: ', JSON.stringify(event)); const { detached, attached } = AttachedBoardsChangeEvent.diff(event); - const detachedBoards = detached.filter(AttachedSerialBoard.is).map(({ port }) => port); const { selectedPort, selectedBoard } = this.boardsConfig; this.onAttachedBoardsChangedEmitter.fire(event); - // Dynamically unset the port if the selected board was an attached one and we detached it. - if (!!selectedPort && detachedBoards.indexOf(selectedPort) !== -1) { + // 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); + this.tryReconnect(attached.boards, attached.ports); } - async tryReconnect(attachedBoards: MaybePromise>): Promise { - const boards = await attachedBoards; + async tryReconnect(attachedBoards: Board[], availablePorts: Port[]): Promise { if (this.latestValidBoardsConfig && !this.canUploadTo(this.boardsConfig)) { - for (const board of boards.filter(AttachedSerialBoard.is)) { + for (const board of attachedBoards.filter(AttachedSerialBoard.is)) { if (this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn && this.latestValidBoardsConfig.selectedBoard.name === board.name - && this.latestValidBoardsConfig.selectedPort === board.port) { + && Port.sameAs(this.latestValidBoardsConfig.selectedPort, board.port)) { this.boardsConfig = this.latestValidBoardsConfig; return true; @@ -70,13 +67,13 @@ export class BoardsServiceClientImpl implements BoardsServiceClient { } // 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 boards.filter(AttachedSerialBoard.is)) { + for (const board of attachedBoards.filter(AttachedSerialBoard.is)) { if (this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn && this.latestValidBoardsConfig.selectedBoard.name === board.name) { this.boardsConfig = { ...this.latestValidBoardsConfig, - selectedPort: board.port + selectedPort: availablePorts.find(port => Port.sameAs(port, board.port)) }; return true; } diff --git a/arduino-ide-extension/src/browser/boards/boards-toolbar-item.tsx b/arduino-ide-extension/src/browser/boards/boards-toolbar-item.tsx index aca66750..7afea70e 100644 --- a/arduino-ide-extension/src/browser/boards/boards-toolbar-item.tsx +++ b/arduino-ide-extension/src/browser/boards/boards-toolbar-item.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { CommandRegistry, DisposableCollection } from '@theia/core'; -import { BoardsService, Board, AttachedSerialBoard } from '../../common/protocol/boards-service'; +import { BoardsService, Board, AttachedSerialBoard, Port } from '../../common/protocol/boards-service'; import { ArduinoCommands } from '../arduino-commands'; import { BoardsServiceClientImpl } from './boards-service-client-impl'; import { BoardsConfig } from './boards-config'; @@ -88,6 +88,7 @@ export namespace BoardsToolBarItem { export interface State { boardsConfig: BoardsConfig.Config; attachedBoards: Board[]; + availablePorts: Port[]; coords: BoardsDropDownListCoords | 'hidden'; } } @@ -104,6 +105,7 @@ export class BoardsToolBarItem extends React.Component this.setState({ boardsConfig })), - client.onBoardsChanged(({ newState }) => this.setState({ attachedBoards: newState.boards })) + client.onBoardsChanged(({ newState }) => this.setState({ attachedBoards: newState.boards, availablePorts: newState.ports })) ]); - boardService.getAttachedBoards().then(({ boards: attachedBoards }) => { - this.setState({ attachedBoards }) + Promise.all([ + boardService.getAttachedBoards(), + boardService.getAvailablePorts() + ]).then(([{boards: attachedBoards}, { ports: availablePorts }]) => { + this.setState({ attachedBoards, availablePorts }) }); } @@ -149,29 +154,32 @@ export class BoardsToolBarItem extends React.Component 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: board.port + onClick: () => { + this.props.boardsServiceClient.boardsConfig = { + selectedBoard: board, + selectedPort: availablePorts.find(port => Port.sameAs(port, board.port)) + } } })); return
-
+
- {boardsConfigText} + {title}
diff --git a/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx b/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx index 84425bff..f9e5e128 100644 --- a/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx +++ b/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx @@ -268,7 +268,7 @@ export class MonitorWidget extends ReactWidget implements StatefulWidget { return { baudRate, board: selectedBoard, - port: selectedPort + port: selectedPort.address } } diff --git a/arduino-ide-extension/src/browser/style/board-select-dialog.css b/arduino-ide-extension/src/browser/style/board-select-dialog.css index f4a3b02b..76422226 100644 --- a/arduino-ide-extension/src/browser/style/board-select-dialog.css +++ b/arduino-ide-extension/src/browser/style/board-select-dialog.css @@ -73,11 +73,18 @@ div#select-board-dialog .selectBoardContainer .body .list .item.selected i{ text-transform: uppercase; } +#select-board-dialog .selectBoardContainer .body .container .content .footer { + padding: 10px 5px 10px 0px; +} + #select-board-dialog .selectBoardContainer .body .container .content .loading { font-size: var(--theia-ui-font-size1); color: #7f8c8d; padding: 10px 5px 10px 10px; text-transform: uppercase; + /* The max, min-height comes from `.body .list` 265px + 47px top padding - 2 * 10px top padding */ + max-height: 292px; + min-height: 292px; } #select-board-dialog .selectBoardContainer .body .list .item { diff --git a/arduino-ide-extension/src/common/protocol/boards-service.ts b/arduino-ide-extension/src/common/protocol/boards-service.ts index 9a2fd518..5c0a662c 100644 --- a/arduino-ide-extension/src/common/protocol/boards-service.ts +++ b/arduino-ide-extension/src/common/protocol/boards-service.ts @@ -1,23 +1,42 @@ -import { JsonRpcServer } from '@theia/core'; +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 { ArduinoComponent } from './arduino-component'; +const naturalCompare: (left: string, right: string) => number = require('string-natural-compare').caseInsensitive; export interface AttachedBoardsChangeEvent { - readonly oldState: Readonly<{ boards: Board[] }>; - readonly newState: Readonly<{ boards: Board[] }>; + readonly oldState: Readonly<{ boards: Board[], ports: Port[] }>; + readonly newState: Readonly<{ boards: Board[], ports: Port[] }>; } export namespace AttachedBoardsChangeEvent { - export function diff(event: AttachedBoardsChangeEvent): Readonly<{ attached: Board[], detached: Board[] }> { + export function diff(event: AttachedBoardsChangeEvent): Readonly<{ + attached: { + boards: Board[], + ports: Port[] + }, + detached: { + boards: Board[], + ports: Port[] + } + }> { const diff = (left: T[], right: T[]) => { return left.filter(item => right.indexOf(item) === -1); } const { boards: newBoards } = event.newState; const { boards: oldBoards } = event.oldState; + const { ports: newPorts } = event.newState; + const { ports: oldPorts } = event.oldState; return { - detached: diff(oldBoards, newBoards), - attached: diff(newBoards, oldBoards) + detached: { + boards: diff(oldBoards, newBoards), + ports: diff(oldPorts, newPorts) + }, + attached: { + boards: diff(newBoards, oldBoards), + ports: diff(newPorts, oldPorts) + } }; } @@ -37,6 +56,114 @@ export const BoardsServicePath = '/services/boards-service'; export const BoardsService = Symbol('BoardsService'); export interface BoardsService extends Installable, Searchable, JsonRpcServer { getAttachedBoards(): Promise<{ boards: Board[] }>; + getAvailablePorts(): Promise<{ ports: Port[] }>; +} + +export interface Port { + readonly address: string; + readonly protocol: Port.Protocol; + /** + * Optional label for the protocol. For example: `Serial Port (USB)`. + */ + readonly label?: string; +} +export namespace Port { + + export type Protocol = 'serial' | 'network' | 'unknown'; + export namespace Protocol { + export function toProtocol(protocol: string | undefined): Protocol { + if (protocol === 'serial') { + return 'serial'; + } else if (protocol === 'network') { + return 'network'; + } else { + return 'unknown'; + } + } + } + + export function toString(port: Port, options: { useLabel: boolean } = { useLabel: false }): string { + if (options.useLabel && port.label) { + return `${port.address} ${port.label}` + } + return port.address; + } + + export function compare(left: Port, right: Port): number { + // Board ports have higher priorities, they come first. + if (isBoardPort(left) && !isBoardPort(right)) { + return -1; + } + if (!isBoardPort(left) && isBoardPort(right)) { + return 1; + } + let result = left.protocol.toLocaleLowerCase().localeCompare(right.protocol.toLocaleLowerCase()); + if (result !== 0) { + return result; + } + result = naturalCompare(left.address, right.address); + if (result !== 0) { + return result; + } + return (left.label || '').localeCompare(right.label || ''); + } + + export function equals(left: Port | undefined, right: Port | undefined): boolean { + if (left && right) { + return left.address === right.address + && left.protocol === right.protocol + && (left.label || '') === (right.label || ''); + } + return left === right; + } + + // Based on: https://github.com/arduino/Arduino/blob/93581b03d723e55c60caedb4729ffc6ea808fe78/arduino-core/src/processing/app/SerialPortList.java#L48-L74 + export function isBoardPort(port: Port): boolean { + const address = port.address.toLocaleLowerCase(); + if (isWindows) { + // `COM1` seems to be the default serial port on Windows. + return address !== 'COM1'.toLocaleLowerCase(); + } + // On macOS and Linux, the port should start with `/dev/`. + if (!address.startsWith('/dev/')) { + return false + } + if (isOSX) { + // Example: `/dev/cu.usbmodem14401` + if (/(tty|cu)\..*/.test(address.substring('/dev/'.length))) { + return [ + '/dev/cu.MALS', + '/dev/cu.SOC', + '/dev/cu.Bluetooth-Incoming-Port' + ].map(a => a.toLocaleLowerCase()).every(a => a !== address); + } + } + + // Example: `/dev/ttyACM0` + if (/(ttyS|ttyUSB|ttyACM|ttyAMA|rfcomm|ttyO)[0-9]{1,3}/.test(address.substring('/dev/'.length))) { + // Default ports were `/dev/ttyS0` -> `/dev/ttyS31` on Ubuntu 16.04.2. + if (address.startsWith('/dev/ttyS')) { + const index = Number.parseInt(address.substring('/dev/ttyS'.length), 10); + if (!Number.isNaN(index) && 0 <= index && 31 >= index) { + return false; + } + } + return true; + } + + return false; + } + + export function sameAs(left: Port | undefined, right: string | undefined) { + if (left && right) { + if (left.protocol !== 'serial') { + console.log(`Unexpected protocol for port: ${JSON.stringify(left)}. Ignoring protocol, comparing addresses with ${right}.`); + } + return left.address === right; + } + return false; + } + } export interface BoardPackage extends ArduinoComponent { diff --git a/arduino-ide-extension/src/node/boards-service-impl.ts b/arduino-ide-extension/src/node/boards-service-impl.ts index 737b09eb..455353de 100644 --- a/arduino-ide-extension/src/node/boards-service-impl.ts +++ b/arduino-ide-extension/src/node/boards-service-impl.ts @@ -1,7 +1,7 @@ import * as PQueue from 'p-queue'; import { injectable, inject, postConstruct, named } from 'inversify'; import { ILogger } from '@theia/core/lib/common/logger'; -import { BoardsService, AttachedSerialBoard, BoardPackage, Board, AttachedNetworkBoard, BoardsServiceClient } from '../common/protocol/boards-service'; +import { BoardsService, AttachedSerialBoard, BoardPackage, Board, AttachedNetworkBoard, BoardsServiceClient, Port } from '../common/protocol/boards-service'; import { PlatformSearchReq, PlatformSearchResp, PlatformInstallReq, PlatformInstallResp, PlatformListReq, PlatformListResp } from './cli-protocol/commands/core_pb'; import { CoreClientProvider } from './core-client-provider'; import { BoardListReq, BoardListResp } from './cli-protocol/commands/board_pb'; @@ -20,7 +20,6 @@ export class BoardsServiceImpl implements BoardsService { @inject(ToolOutputServiceServer) protected readonly toolOutputService: ToolOutputServiceServer; - protected selectedBoard: Board | undefined; protected discoveryInitialized = false; protected discoveryTimer: NodeJS.Timeout | undefined; /** @@ -29,44 +28,58 @@ export class BoardsServiceImpl implements BoardsService { * This state is updated via periodical polls. */ protected _attachedBoards: { boards: Board[] } = { boards: [] }; + protected _availablePorts: { ports: Port[] } = { ports: [] }; protected client: BoardsServiceClient | undefined; protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 }); @postConstruct() protected async init(): Promise { this.discoveryTimer = setInterval(() => { - this.discoveryLogger.trace('Discovering attached boards...'); - this.doGetAttachedBoards().then(({ boards }) => { - const update = (oldState: Board[], newState: Board[], message: string) => { - this._attachedBoards = { boards: newState }; - this.discoveryLogger.info(`${message} - Discovered boards: ${JSON.stringify(newState)}`); + 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: oldState + boards: oldBoards, + ports: oldPorts }, newState: { - boards: newState + boards: newBoards, + ports: newPorts } }); } } const sortedBoards = boards.sort(Board.compare); - this.discoveryLogger.trace(`Discovery done. ${JSON.stringify(sortedBoards)}`); + const sortedPorts = ports.sort(Port.compare); + this.discoveryLogger.trace(`Discovery done. Boards: ${JSON.stringify(sortedBoards)}. Ports: ${sortedPorts}`); if (!this.discoveryInitialized) { - update([], sortedBoards, 'Initialized attached boards.'); + update([], sortedBoards, [], sortedPorts, 'Initialized attached boards and available ports.'); this.discoveryInitialized = true; } else { - this.getAttachedBoards().then(({ boards: currentBoards }) => { + 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) { - update(currentBoards, sortedBoards, 'Updated discovered boards.'); + 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, 'Updated discovered boards.'); + 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; } } @@ -91,13 +104,18 @@ export class BoardsServiceImpl implements BoardsService { return this._attachedBoards; } - private async doGetAttachedBoards(): Promise<{ boards: Board[] }> { + async getAvailablePorts(): Promise<{ ports: Port[] }> { + return this._availablePorts; + } + + private async doGetAttachedBoardsAndAvailablePorts(): Promise<{ boards: Board[], ports: Port[] }> { return this.queue.add(() => { - return new Promise<{ boards: Board[] }>(async resolve => { + 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 }); + resolve({ boards, ports }); return; } @@ -105,10 +123,43 @@ export class BoardsServiceImpl implements BoardsService { const req = new BoardListReq(); req.setInstance(instance); const resp = await new Promise((resolve, reject) => client.boardList(req, (err, resp) => (!!err ? reject : resolve)(!!err ? err : resp))); - for (const portsList of resp.getPortsList()) { - const protocol = portsList.getProtocol(); - const address = portsList.getAddress(); - for (const board of portsList.getBoardsList()) { + 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; @@ -118,13 +169,15 @@ export class BoardsServiceImpl implements BoardsService { fqbn, port }); - } else { // We assume, it is a `network` board. + } else if (protocol === 'network') { // We assume, it is a `network` board. boards.push({ name, fqbn, address, port }); + } else { + console.warn(`Unknown protocol for port: ${address}.`); } } } @@ -133,11 +186,13 @@ export class BoardsServiceImpl implements BoardsService { // { name: 'Arduino/Genuino Uno', fqbn: 'arduino:avr:uno', port: '/dev/cu.usbmodem14201' }, // { name: 'Arduino/Genuino Uno', fqbn: 'arduino:avr:uno', port: '/dev/cu.usbmodem142xx' }, // ]); - resolve({ boards }); + resolve({ boards, ports }); }) }); } + + async search(options: { query?: string }): Promise<{ items: BoardPackage[] }> { const coreClient = await this.coreClientProvider.getClient(); if (!coreClient) { diff --git a/browser-app/package.json b/browser-app/package.json index 9bd2ec75..b88ee956 100644 --- a/browser-app/package.json +++ b/browser-app/package.json @@ -25,7 +25,7 @@ }, "scripts": { "prepare": "theia build --mode development", - "start": "theia start --root-dir=../workspace", + "start": "theia start", "watch": "theia build --watch --mode development" }, "theia": { diff --git a/electron-app/package.json b/electron-app/package.json index 1b039aa7..9599d793 100644 --- a/electron-app/package.json +++ b/electron-app/package.json @@ -27,7 +27,7 @@ }, "scripts": { "prepare": "theia build --mode development", - "start": "theia start --root-dir=../workspace", + "start": "theia start", "watch": "theia build --watch --mode development" }, "theia": { diff --git a/yarn.lock b/yarn.lock index a1a9a497..a87e9e0f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11924,6 +11924,11 @@ string-argv@^0.1.1: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.1.2.tgz#c5b7bc03fb2b11983ba3a72333dd0559e77e4738" integrity sha512-mBqPGEOMNJKXRo7z0keX0wlAhbBAjilUdPW13nN0PecVryZxdHIeM7TqbsSUA7VYuS00HGC6mojP7DlQzfa9ZA== +string-natural-compare@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-2.0.3.tgz#9dbe1dd65490a5fe14f7a5c9bc686fc67cb9c6e4" + integrity sha512-4Kcl12rNjc+6EKhY8QyDVuQTAlMWwRiNbsxnVwBUKFr7dYPQuXVrtNU4sEkjF9LHY0AY6uVbB3ktbkIH4LC+BQ== + string-template@~0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add"