From 85bf50213d96a07617401bb6fef2758418c45a4f Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Wed, 4 Dec 2019 19:34:05 +0100 Subject: [PATCH] Removed more logic from the widget. Signed-off-by: Akos Kitta --- .../boards/boards-service-client-impl.ts | 48 +- .../src/browser/monitor/monitor-connection.ts | 104 +++- .../src/browser/monitor/monitor-model.ts | 6 +- .../src/browser/monitor/monitor-widget.tsx | 481 ++++++++---------- 4 files changed, 350 insertions(+), 289 deletions(-) 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 e6cb90c5..96739719 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 @@ -5,6 +5,7 @@ import { LocalStorageService } from '@theia/core/lib/browser/storage-service'; import { RecursiveRequired } from '../../common/types'; import { BoardsServiceClient, AttachedBoardsChangeEvent, BoardInstalledEvent, AttachedSerialBoard, Board, Port, BoardUninstalledEvent } from '../../common/protocol/boards-service'; import { BoardsConfig } from './boards-config'; +import { MessageService } from '@theia/core'; @injectable() export class BoardsServiceClientImpl implements BoardsServiceClient { @@ -12,6 +13,9 @@ export class BoardsServiceClientImpl implements BoardsServiceClient { @inject(ILogger) protected logger: ILogger; + @inject(MessageService) + protected messageService: MessageService; + @inject(LocalStorageService) protected storageService: LocalStorageService; @@ -110,15 +114,51 @@ export class BoardsServiceClientImpl implements BoardsServiceClient { /** * `true` if the `config.selectedBoard` is defined; hence can compile against the board. Otherwise, `false`. */ - canVerify(config: BoardsConfig.Config | undefined = this.boardsConfig): config is BoardsConfig.Config & { selectedBoard: Board } { - return !!config && !!config.selectedBoard; + canVerify( + config: BoardsConfig.Config | undefined = this.boardsConfig, + options: { silent: boolean } = { silent: true }): config is BoardsConfig.Config & { selectedBoard: Board } { + + if (!config) { + return false; + } + + if (!config.selectedBoard) { + if (!options.silent) { + this.messageService.warn('No boards selected.'); + } + return false; + } + + return true; } /** * `true` if the `canVerify` and the `config.selectedPort` is also set with FQBN, hence can upload to board. Otherwise, `false`. */ - canUploadTo(config: BoardsConfig.Config | undefined = this.boardsConfig): config is RecursiveRequired { - return this.canVerify(config) && !!config.selectedPort && !!config.selectedBoard.fqbn; + canUploadTo( + config: BoardsConfig.Config | undefined = this.boardsConfig, + options: { silent: boolean } = { silent: true }): config is RecursiveRequired { + + if (!this.canVerify(config, options)) { + return false; + } + + const { name } = config.selectedBoard; + if (!config.selectedPort) { + if (!options.silent) { + this.messageService.warn(`No ports selected for board: '${name}'.`); + } + return false; + } + + if (!config.selectedBoard.fqbn) { + if (!options.silent) { + this.messageService.warn(`The FQBN is not available for the selected board ${name}. Do you have the corresponding core installed?`); + } + return false; + } + + return true; } protected saveState(): Promise { diff --git a/arduino-ide-extension/src/browser/monitor/monitor-connection.ts b/arduino-ide-extension/src/browser/monitor/monitor-connection.ts index 7247de64..45ec5f8f 100644 --- a/arduino-ide-extension/src/browser/monitor/monitor-connection.ts +++ b/arduino-ide-extension/src/browser/monitor/monitor-connection.ts @@ -4,18 +4,26 @@ import { Emitter, Event } from '@theia/core/lib/common/event'; import { MessageService } from '@theia/core/lib/common/message-service'; import { MonitorService, MonitorConfig, MonitorError, Status } from '../../common/protocol/monitor-service'; import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl'; -import { Port, Board } from '../../common/protocol/boards-service'; +import { Port, Board, BoardsService, AttachedSerialBoard, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service'; import { MonitorServiceClientImpl } from './monitor-service-client-impl'; +import { BoardsConfig } from '../boards/boards-config'; +import { MonitorModel } from './monitor-model'; @injectable() export class MonitorConnection { + @inject(MonitorModel) + protected readonly monitorModel: MonitorModel; + @inject(MonitorService) protected readonly monitorService: MonitorService; @inject(MonitorServiceClientImpl) protected readonly monitorServiceClient: MonitorServiceClientImpl; + @inject(BoardsService) + protected readonly boardsService: BoardsService; + @inject(BoardsServiceClientImpl) protected boardsServiceClient: BoardsServiceClientImpl; @@ -26,9 +34,14 @@ export class MonitorConnection { // protected readonly connectionStatusService: ConnectionStatusService; protected state: MonitorConnection.State | undefined; - protected readonly onConnectionChangedEmitter = new Emitter(); + /** + * Note: The idea is to toggle this property from the UI (`Monitor` view) + * and the boards config and the boards attachment/detachment logic can be at on place, here. + */ + protected _autoConnect: boolean = false; + protected readonly onConnectionChangedEmitter = new Emitter(); - readonly onConnectionChanged: Event = this.onConnectionChangedEmitter.event; + readonly onConnectionChanged: Event = this.onConnectionChangedEmitter.event; @postConstruct() protected init(): void { @@ -55,16 +68,39 @@ export class MonitorConnection { const { board, port } = config; this.messageService.error(`Unexpected error. Reconnecting ${Board.toString(board)} on port ${Port.toString(port)}.`); console.error(JSON.stringify(error)); - shouldReconnect = true; + shouldReconnect = this.connected; } } const oldState = this.state; this.state = undefined; + this.onConnectionChangedEmitter.fire(this.state); if (shouldReconnect) { await this.connect(oldState.config); } } }); + this.boardsServiceClient.onBoardsConfigChanged(this.handleBoardConfigChange.bind(this)); + this.boardsServiceClient.onBoardsChanged(event => { + if (this.autoConnect && this.connected) { + const { boardsConfig } = this.boardsServiceClient; + if (this.boardsServiceClient.canUploadTo(boardsConfig, { silent: false })) { + const { attached } = AttachedBoardsChangeEvent.diff(event); + if (attached.boards.some(board => AttachedSerialBoard.is(board) && BoardsConfig.Config.sameAs(boardsConfig, board))) { + const { selectedBoard: board, selectedPort: port } = boardsConfig; + const { baudRate } = this.monitorModel; + this.disconnect() + .then(() => this.connect({ board, port, baudRate })); + } + } + } + }); + // Handles the `baudRate` changes by reconnecting if required. + this.monitorModel.onChange(() => { + if (this.autoConnect && this.connected) { + const { boardsConfig } = this.boardsServiceClient; + this.handleBoardConfigChange(boardsConfig); + } + }) } get connected(): boolean { @@ -75,8 +111,22 @@ export class MonitorConnection { return this.state ? this.state.config : undefined; } + get autoConnect(): boolean { + return this._autoConnect; + } + + set autoConnect(value: boolean) { + const oldValue = this._autoConnect; + this._autoConnect = value; + // When we enable the auto-connect, we have to connect + if (!oldValue && value) { + const { boardsConfig } = this.boardsServiceClient; + this.handleBoardConfigChange(boardsConfig); + } + } + async connect(config: MonitorConfig): Promise { - if (this.state) { + if (this.connected) { const disconnectStatus = await this.disconnect(); if (!Status.isOK(disconnectStatus)) { return disconnectStatus; @@ -85,13 +135,13 @@ export class MonitorConnection { const connectStatus = await this.monitorService.connect(config); if (Status.isOK(connectStatus)) { this.state = { config }; - this.onConnectionChangedEmitter.fire(true); } + this.onConnectionChangedEmitter.fire(this.state); return Status.isOK(connectStatus); } async disconnect(): Promise { - if (!this.state) { + if (!this.state) { // XXX: we user `this.state` instead of `this.connected` to make the type checker happy. return Status.OK; } console.log('>>> Disposing existing monitor connection before establishing a new one...'); @@ -102,10 +152,48 @@ export class MonitorConnection { console.warn(`<<< Could not dispose connection. Activate connection: ${MonitorConnection.State.toString(this.state)}`); } this.state = undefined; - this.onConnectionChangedEmitter.fire(false); + this.onConnectionChangedEmitter.fire(this.state); return status; } + /** + * Sends the data to the connected serial monitor. + * The desired EOL is appended to `data`, you do not have to add it. + * It is a NOOP if connected. + */ + async send(data: string): Promise { + if (!this.connected) { + return Status.NOT_CONNECTED; + } + return new Promise(resolve => { + this.monitorService.send(data + this.monitorModel.lineEnding) + .then(() => resolve(Status.OK)); + }); + } + + protected async handleBoardConfigChange(boardsConfig: BoardsConfig.Config): Promise { + if (this.autoConnect) { + if (this.boardsServiceClient.canUploadTo(boardsConfig, { silent: false })) { + this.boardsService.getAttachedBoards().then(({ boards }) => { + if (boards.filter(AttachedSerialBoard.is).some(board => BoardsConfig.Config.sameAs(boardsConfig, board))) { + new Promise(resolve => { + // First, disconnect if connected. + if (this.connected) { + this.disconnect().then(() => resolve()); + } + resolve(); + }).then(() => { + // Then (re-)connect. + const { selectedBoard: board, selectedPort: port } = boardsConfig; + const { baudRate } = this.monitorModel; + this.connect({ board, port, baudRate }); + }); + } + }); + } + } + } + } export namespace MonitorConnection { diff --git a/arduino-ide-extension/src/browser/monitor/monitor-model.ts b/arduino-ide-extension/src/browser/monitor/monitor-model.ts index 5c6be3fa..79cb1de6 100644 --- a/arduino-ide-extension/src/browser/monitor/monitor-model.ts +++ b/arduino-ide-extension/src/browser/monitor/monitor-model.ts @@ -2,6 +2,7 @@ import { injectable, inject } from 'inversify'; import { Emitter, Event } from '@theia/core/lib/common/event'; import { MonitorConfig } from '../../common/protocol/monitor-service'; import { FrontendApplicationContribution, LocalStorageService } from '@theia/core/lib/browser'; +import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl'; @injectable() export class MonitorModel implements FrontendApplicationContribution { @@ -11,6 +12,9 @@ export class MonitorModel implements FrontendApplicationContribution { @inject(LocalStorageService) protected readonly localStorageService: LocalStorageService; + @inject(BoardsServiceClientImpl) + protected readonly boardsServiceClient: BoardsServiceClientImpl; + protected readonly onChangeEmitter: Emitter; protected _autoscroll: boolean; protected _timestamp: boolean; @@ -70,7 +74,7 @@ export class MonitorModel implements FrontendApplicationContribution { set lineEnding(lineEnding: MonitorModel.EOL) { this._lineEnding = lineEnding; - this.storeState().then(() => this.onChangeEmitter.fire(undefined)); + this.storeState(); } protected restoreState(state: MonitorModel.State) { diff --git a/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx b/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx index 3a903dc1..130a5b21 100644 --- a/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx +++ b/arduino-ide-extension/src/browser/monitor/monitor-widget.tsx @@ -5,15 +5,213 @@ import { ThemeConfig } from 'react-select/src/theme'; import { OptionsType } from 'react-select/src/types'; import Select from 'react-select'; import { Styles } from 'react-select/src/styles'; -import { MessageService } from '@theia/core/lib/common/message-service'; -import { ReactWidget, Message, Widget } from '@theia/core/lib/browser'; -import { MonitorServiceClientImpl } from './monitor-service-client-impl'; -import { MonitorConfig, MonitorService } from '../../common/protocol/monitor-service'; -import { AttachedSerialBoard, BoardsService, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service'; -import { BoardsConfig } from '../boards/boards-config'; -import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl'; +import { ReactWidget, Message, Widget } from '@theia/core/lib/browser/widgets'; +import { MonitorConfig } from '../../common/protocol/monitor-service'; import { MonitorModel } from './monitor-model'; import { MonitorConnection } from './monitor-connection'; +import { MonitorServiceClientImpl } from './monitor-service-client-impl'; + +@injectable() +export class MonitorWidget extends ReactWidget { + + static readonly ID = 'serial-monitor'; + + @inject(MonitorModel) + protected readonly model: MonitorModel; + + @inject(MonitorConnection) + protected readonly monitorConnection: MonitorConnection; + + @inject(MonitorServiceClientImpl) + protected readonly monitorServiceClient: MonitorServiceClientImpl; + + protected lines: string[]; + protected chunk: string; + protected widgetHeight: number; + + /** + * Do not touch or use it. It is for setting the focus on the `input` after the widget activation. + */ + protected focusNode: HTMLElement | undefined; + + constructor() { + super(); + + this.id = MonitorWidget.ID; + this.title.label = 'Serial Monitor'; + this.title.iconClass = 'arduino-serial-monitor-tab-icon'; + + this.lines = []; + this.chunk = ''; + this.scrollOptions = undefined; + } + + @postConstruct() + protected init(): void { + this.toDisposeOnDetach.pushAll([ + this.monitorServiceClient.onRead(({ data }) => { + this.chunk += data; + const eolIndex = this.chunk.indexOf('\n'); + if (eolIndex !== -1) { + const line = this.chunk.substring(0, eolIndex + 1); + this.chunk = this.chunk.slice(eolIndex + 1); + this.lines.push(`${this.model.timestamp ? `${dateFormat(new Date(), 'H:M:ss.l')} -> ` : ''}${line}`); + this.update(); + } + }), + this.monitorConnection.onConnectionChanged(state => { + if (!state) { + this.clearConsole(); + } + }) + ]); + this.update(); + } + + clearConsole(): void { + this.chunk = ''; + this.lines = []; + this.update(); + } + + onBeforeAttach(msg: Message): void { + super.onBeforeAttach(msg); + this.clearConsole(); + } + + protected onAfterAttach(msg: Message): void { + super.onAfterAttach(msg); + this.monitorConnection.autoConnect = true; + } + + protected onBeforeDetach(msg: Message): void { + super.onBeforeDetach(msg); + this.monitorConnection.autoConnect = false; + } + + protected onResize(msg: Widget.ResizeMessage): void { + super.onResize(msg); + this.widgetHeight = msg.height; + this.update(); + } + + protected get lineEndings(): OptionsType> { + return [ + { + label: 'No Line Ending', + value: '' + }, + { + label: 'Newline', + value: '\n' + }, + { + label: 'Carriage Return', + value: '\r' + }, + { + label: 'Both NL & CR', + value: '\r\n' + } + ] + } + + protected get baudRates(): OptionsType> { + const baudRates: Array = [300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200]; + return baudRates.map(baudRate => ({ label: baudRate + ' baud', value: baudRate })); + } + + protected render(): React.ReactNode { + const { baudRates, lineEndings } = this; + const lineEnding = lineEndings.find(item => item.value === this.model.lineEnding) || lineEndings[1]; // Defaults to `\n`. + const baudRate = baudRates.find(item => item.value === this.model.baudRate) || baudRates[4]; // Defaults to `9600`. + return
+
+
+ +
+
+ {this.renderSelectField('arduino-serial-monitor-line-endings', lineEndings, lineEnding, this.onChangeLineEnding)} + {this.renderSelectField('arduino-serial-monitor-baud-rates', baudRates, baudRate, this.onChangeBaudRate)} +
+
+
+ +
+
; + } + + protected readonly onSend = (value: string) => this.doSend(value); + protected async doSend(value: string): Promise { + this.monitorConnection.send(value); + } + + protected readonly onChangeLineEnding = (option: SelectOption) => { + this.model.lineEnding = option.value; + } + + protected readonly onChangeBaudRate = async (option: SelectOption) => { + await this.monitorConnection.disconnect(); + this.model.baudRate = option.value; + } + + protected renderSelectField( + id: string, + options: OptionsType>, + defaultValue: SelectOption, + onChange: (option: SelectOption) => void): React.ReactNode { + + const height = 25; + const styles: Styles = { + control: (styles, state) => ({ + ...styles, + width: 200, + color: 'var(--theia-ui-font-color1)' + }), + dropdownIndicator: styles => ({ + ...styles, + padding: 0 + }), + indicatorSeparator: () => ({ + display: 'none' + }), + indicatorsContainer: () => ({ + padding: '0px 5px' + }), + menu: styles => ({ + ...styles, + marginTop: 0 + }) + }; + const theme: ThemeConfig = theme => ({ + ...theme, + borderRadius: 0, + spacing: { + controlHeight: height, + baseUnit: 2, + menuGutter: 4 + } + }); + const DropdownIndicator = () => { + return ( + + ); + }; + return - } -}