diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index 2f55a294..96261935 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -23,10 +23,15 @@ "@theia/terminal": "next", "@theia/workspace": "next", "@types/google-protobuf": "^3.7.1", + "@types/dateformat": "^3.0.1", + "@types/chance": "1.0.7", "@types/ps-tree": "^1.1.0", "@types/react-select": "^3.0.0", "@types/which": "^1.3.1", + "chance": "^1.1.3", "css-element-queries": "^1.2.0", + "dateformat": "^3.0.3", + "google-protobuf": "^3.11.0", "p-queue": "^5.0.0", "ps-tree": "^1.2.0", "react-select": "^3.0.4", diff --git a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx index 71095906..64cd8315 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx +++ b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx @@ -41,7 +41,6 @@ import { MaybePromise } from '@theia/core/lib/common/types'; import { BoardsConfigDialog } from './boards/boards-config-dialog'; import { BoardsToolBarItem } from './boards/boards-toolbar-item'; import { BoardsConfig } from './boards/boards-config'; -import { MonitorService } from '../common/protocol/monitor-service'; import { ConfigService } from '../common/protocol/config-service'; import { MonitorConnection } from './monitor/monitor-connection'; import { MonitorViewContribution } from './monitor/monitor-view-contribution'; @@ -79,9 +78,6 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut @inject(CoreService) protected readonly coreService: CoreService; - @inject(MonitorService) - protected readonly monitorService: MonitorService; - @inject(WorkspaceServiceExt) protected readonly workspaceServiceExt: WorkspaceServiceExt; @@ -336,7 +332,9 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut } const connectionConfig = this.monitorConnection.connectionConfig; - await this.monitorConnection.disconnect(); + if (connectionConfig) { + await this.monitorConnection.disconnect(); + } try { const { boardsConfig } = this.boardsServiceClient; diff --git a/arduino-ide-extension/src/browser/monitor/monitor-connection.ts b/arduino-ide-extension/src/browser/monitor/monitor-connection.ts index ce08ee33..f445fa9f 100644 --- a/arduino-ide-extension/src/browser/monitor/monitor-connection.ts +++ b/arduino-ide-extension/src/browser/monitor/monitor-connection.ts @@ -1,6 +1,11 @@ -import { injectable, inject } from "inversify"; -import { MonitorService, ConnectionConfig } from "../../common/protocol/monitor-service"; -import { Emitter, Event } from "@theia/core"; +import { injectable, inject, postConstruct } from 'inversify'; +import { Emitter, Event } from '@theia/core/lib/common/event'; +// import { ConnectionStatusService } from '@theia/core/lib/browser/connection-status-service'; +import { MessageService } from '@theia/core/lib/common/message-service'; +import { MonitorService, MonitorConfig, MonitorError } from '../../common/protocol/monitor-service'; +import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl'; +import { Port, Board } from '../../common/protocol/boards-service'; +import { MonitorServiceClientImpl } from './monitor-service-client-impl'; @injectable() export class MonitorConnection { @@ -8,46 +13,102 @@ export class MonitorConnection { @inject(MonitorService) protected readonly monitorService: MonitorService; - connectionId: string | undefined; + @inject(MonitorServiceClientImpl) + protected readonly monitorServiceClient: MonitorServiceClientImpl; - protected _connectionConfig: ConnectionConfig | undefined; + @inject(BoardsServiceClientImpl) + protected boardsServiceClient: BoardsServiceClientImpl; + @inject(MessageService) + protected messageService: MessageService; + + // @inject(ConnectionStatusService) + // protected readonly connectionStatusService: ConnectionStatusService; + + protected state: MonitorConnection.State | undefined; protected readonly onConnectionChangedEmitter = new Emitter(); + readonly onConnectionChanged: Event = this.onConnectionChangedEmitter.event; - get connectionConfig(): ConnectionConfig | undefined { - return this._connectionConfig; + @postConstruct() + protected init(): void { + this.monitorServiceClient.onError(error => { + if (this.state) { + const { code, connectionId, config } = error; + if (this.state.connectionId === connectionId) { + switch (code) { + case MonitorError.ErrorCodes.CLIENT_CANCEL: { + console.log(`Connection was canceled by client: ${MonitorConnection.State.toString(this.state)}.`); + break; + } + case MonitorError.ErrorCodes.DEVICE_BUSY: { + const { port } = config; + this.messageService.warn(`Connection failed. Serial port is busy: ${Port.toString(port)}.`); + break; + } + case MonitorError.ErrorCodes.DEVICE_NOT_CONFIGURED: { + const { port } = config; + this.messageService.info(`Disconnected from ${Port.toString(port)}.`); + break; + } + } + this.state = undefined; + } else { + console.warn(`Received an error from unexpected connection: ${MonitorConnection.State.toString({ connectionId, config })}.`); + } + } + }); } - async connect(config: ConnectionConfig): Promise { - if (this.connectionId) { - await this.disconnect(); + get connectionId(): string | undefined { + return this.state ? this.state.connectionId : undefined; + } + + get connectionConfig(): MonitorConfig | undefined { + return this.state ? this.state.config : undefined; + } + + async connect(config: MonitorConfig): Promise { + if (this.state) { + throw new Error(`Already connected to ${MonitorConnection.State.toString(this.state)}.`); } const { connectionId } = await this.monitorService.connect(config); - this.connectionId = connectionId; - this._connectionConfig = config; - - this.onConnectionChangedEmitter.fire(this.connectionId); - + this.state = { connectionId, config }; + this.onConnectionChangedEmitter.fire(connectionId); return connectionId; } async disconnect(): Promise { - let result = true; - const connections = await this.monitorService.getConnectionIds(); - if (this.connectionId && connections.findIndex(id => id === this.connectionId) >= 0) { - console.log('>>> Disposing existing monitor connection before establishing a new one...'); - result = await this.monitorService.disconnect(this.connectionId); - if (!result) { - // TODO: better!!! - console.error(`Could not close connection: ${this.connectionId}. Check the backend logs.`); - } else { - console.log(`<<< Disposed ${this.connectionId} connection.`); - this.connectionId = undefined; - this._connectionConfig = undefined; - this.onConnectionChangedEmitter.fire(this.connectionId); - } + if (!this.state) { + throw new Error('Not connected. Nothing to disconnect.'); + } + console.log('>>> Disposing existing monitor connection before establishing a new one...'); + const result = await this.monitorService.disconnect(this.state.connectionId); + if (result) { + console.log(`<<< Disposed connection. Was: ${MonitorConnection.State.toString(this.state)}`); + this.state = undefined; + this.onConnectionChangedEmitter.fire(undefined); + } else { + console.warn(`<<< Could not dispose connection. Activate connection: ${MonitorConnection.State.toString(this.state)}`); } return result; } -} \ No newline at end of file + +} + +export namespace MonitorConnection { + + export interface State { + readonly connectionId: string; + readonly config: MonitorConfig; + } + + export namespace State { + export function toString(state: State): string { + const { connectionId, config } = state; + const { board, port } = config; + return `${Board.toString(board)} ${Port.toString(port)} [ID: ${connectionId}]`; + } + } + +} diff --git a/arduino-ide-extension/src/browser/monitor/monitor-model.ts b/arduino-ide-extension/src/browser/monitor/monitor-model.ts index d364b435..22ec5d4e 100644 --- a/arduino-ide-extension/src/browser/monitor/monitor-model.ts +++ b/arduino-ide-extension/src/browser/monitor/monitor-model.ts @@ -1,58 +1,93 @@ -import { injectable } from "inversify"; -import { Emitter } from "@theia/core"; - -export namespace MonitorModel { - export interface Data { - autoscroll: boolean, - timestamp: boolean, - baudRate: number, - lineEnding: string - } -} +import { injectable } from 'inversify'; +import { Emitter, Event } from '@theia/core/lib/common/event'; +import { MonitorConfig } from '../../common/protocol/monitor-service'; @injectable() export class MonitorModel { - protected readonly onChangeEmitter = new Emitter(); + protected readonly onChangeEmitter: Emitter; + protected _autoscroll: boolean; + protected _timestamp: boolean; + protected _baudRate: MonitorConfig.BaudRate; + protected _lineEnding: MonitorModel.EOL; - readonly onChange = this.onChangeEmitter.event; + constructor() { + this._autoscroll = true; + this._timestamp = false; + this._baudRate = MonitorConfig.BaudRate.DEFAULT; + this._lineEnding = MonitorModel.EOL.DEFAULT; + this.onChangeEmitter = new Emitter(); + } - protected _autoscroll: boolean = true; - protected _timestamp: boolean = false; - baudRate: number; - lineEnding: string = '\n'; + get onChange(): Event { + return this.onChangeEmitter.event; + } get autoscroll(): boolean { return this._autoscroll; } + toggleAutoscroll(): void { + this._autoscroll = !this._autoscroll; + } + get timestamp(): boolean { return this._timestamp; } - toggleAutoscroll(): void { - this._autoscroll = !this._autoscroll; - this.onChangeEmitter.fire(undefined); - } - toggleTimestamp(): void { this._timestamp = !this._timestamp; + } + + get baudRate(): MonitorConfig.BaudRate { + return this._baudRate; + } + + set baudRate(baudRate: MonitorConfig.BaudRate) { + this._baudRate = baudRate; this.onChangeEmitter.fire(undefined); } - restore(model: MonitorModel.Data) { - this._autoscroll = model.autoscroll; - this._timestamp = model.timestamp; - this.baudRate = model.baudRate; - this.lineEnding = model.lineEnding; + get lineEnding(): MonitorModel.EOL { + return this._lineEnding; } - store(): MonitorModel.Data { + set lineEnding(lineEnding: MonitorModel.EOL) { + this._lineEnding = lineEnding; + this.onChangeEmitter.fire(undefined); + } + + restore(state: MonitorModel.State) { + this._autoscroll = state.autoscroll; + this._timestamp = state.timestamp; + this._baudRate = state.baudRate; + this._lineEnding = state.lineEnding; + this.onChangeEmitter.fire(undefined); + } + + store(): MonitorModel.State { return { autoscroll: this._autoscroll, timestamp: this._timestamp, - baudRate: this.baudRate, - lineEnding: this.lineEnding + baudRate: this._baudRate, + lineEnding: this._lineEnding } } -} \ No newline at end of file + +} + +export namespace MonitorModel { + + export interface State { + autoscroll: boolean; + timestamp: boolean; + baudRate: MonitorConfig.BaudRate; + lineEnding: EOL; + } + + export type EOL = '' | '\n' | '\r' | '\r\n'; + export namespace EOL { + export const DEFAULT: EOL = '\n'; + } + +} diff --git a/arduino-ide-extension/src/browser/monitor/monitor-service-client-impl.ts b/arduino-ide-extension/src/browser/monitor/monitor-service-client-impl.ts index 28ea0b2c..3e6367b7 100644 --- a/arduino-ide-extension/src/browser/monitor/monitor-service-client-impl.ts +++ b/arduino-ide-extension/src/browser/monitor/monitor-service-client-impl.ts @@ -13,7 +13,7 @@ export class MonitorServiceClientImpl implements MonitorServiceClient { notifyRead(event: MonitorReadEvent): void { this.onReadEmitter.fire(event); const { connectionId, data } = event; - console.log(`Received data from ${connectionId}: ${data}`); + console.debug(`Received data from ${connectionId}: ${data}`); } notifyError(error: MonitorError): void { diff --git a/arduino-ide-extension/src/browser/monitor/monitor-view-contribution.tsx b/arduino-ide-extension/src/browser/monitor/monitor-view-contribution.tsx index 26bc93ad..c5beaebf 100644 --- a/arduino-ide-extension/src/browser/monitor/monitor-view-contribution.tsx +++ b/arduino-ide-extension/src/browser/monitor/monitor-view-contribution.tsx @@ -80,7 +80,7 @@ export class MonitorViewContribution extends AbstractViewContribution widget instanceof MonitorWidget, execute: widget => { if (widget instanceof MonitorWidget) { - widget.clear(); + widget.clearConsole(); } } }); @@ -124,4 +124,5 @@ export class MonitorViewContribution extends AbstractViewContribution void + readonly onSend: (text: string) => void } - export interface State { value: string; } @@ -44,16 +44,17 @@ export class SerialMonitorSendField extends React.Component + this.inputField = ref} + type='text' id='serial-monitor-send' + autoComplete='off' + value={this.state.value} + onChange={this.handleChange} /> + + {/*
- this.inputField = ref} - type='text' id='serial-monitor-send' - autoComplete='off' - value={this.state.value} - onChange={this.handleChange} /> - -
+ */} } @@ -61,7 +62,7 @@ export class SerialMonitorSendField extends React.Component) { + protected handleSubmit(event: React.MouseEvent) { this.props.onSend(this.state.value); this.setState({ value: '' }); event.preventDefault(); @@ -70,55 +71,43 @@ export class SerialMonitorSendField extends React.Component { - protected theEnd: HTMLDivElement | null; + + protected anchor: HTMLElement | null; render() { - let result = ''; - - const style: React.CSSProperties = { - whiteSpace: 'pre', - fontFamily: 'monospace', - }; - - for (const text of this.props.lines) { - result += text; - } return -
{result}
-
{ this.theEnd = el; }}> +
+ {this.props.lines.join('')}
+
{ this.anchor = element; }} /> ; } - protected scrollToBottom() { - if (this.theEnd) { - this.theEnd.scrollIntoView(); - } - } - componentDidMount() { - if (this.props.model.autoscroll) { - this.scrollToBottom(); - } + this.scrollToBottom(); } componentDidUpdate() { - if (this.props.model.autoscroll) { - this.scrollToBottom(); + this.scrollToBottom(); + } + + protected scrollToBottom() { + if (this.props.model.autoscroll && this.anchor) { + this.anchor.scrollIntoView(); } } + } -export interface SelectOption { - label: string; - value: string | number; +export interface SelectOption { + readonly label: string; + readonly value: T; } @injectable() @@ -126,22 +115,37 @@ export class MonitorWidget extends ReactWidget implements StatefulWidget { static readonly ID = 'serial-monitor'; - protected lines: string[]; - protected tempData: string; + @inject(MonitorServiceClientImpl) + protected readonly serviceClient: MonitorServiceClientImpl; + @inject(MonitorConnection) + protected readonly connection: MonitorConnection; + + @inject(MonitorService) + protected readonly monitorService: MonitorService; + + @inject(BoardsServiceClientImpl) + protected readonly boardsServiceClient: BoardsServiceClientImpl; + + @inject(MessageService) + protected readonly messageService: MessageService; + + @inject(BoardsService) + protected readonly boardsService: BoardsService; + + @inject(MonitorModel) + protected readonly model: MonitorModel; + + protected lines: string[]; + protected chunk: string; protected widgetHeight: number; - protected continuePreviousConnection: boolean; + /** + * Do not touch or use it. It is for setting the focus on the `input` after the widget activation. + */ + protected focusNode: HTMLElement | undefined; - constructor( - @inject(MonitorServiceClientImpl) protected readonly serviceClient: MonitorServiceClientImpl, - @inject(MonitorConnection) protected readonly connection: MonitorConnection, - @inject(MonitorService) protected readonly monitorService: MonitorService, - @inject(BoardsServiceClientImpl) protected readonly boardsServiceClient: BoardsServiceClientImpl, - @inject(MessageService) protected readonly messageService: MessageService, - @inject(BoardsService) protected readonly boardsService: BoardsService, - @inject(MonitorModel) protected readonly model: MonitorModel - ) { + constructor() { super(); this.id = MonitorWidget.ID; @@ -149,104 +153,82 @@ export class MonitorWidget extends ReactWidget implements StatefulWidget { this.title.iconClass = 'arduino-serial-monitor-tab-icon'; this.lines = []; - this.tempData = ''; - + this.chunk = ''; this.scrollOptions = undefined; - - this.toDisposeOnDetach.push(serviceClient.onRead(({ data, connectionId }) => { - this.tempData += data; - if (this.tempData.endsWith('\n')) { - if (this.model.timestamp) { - const nu = new Date(); - const h = (100 + nu.getHours()).toString().substr(1) - const min = (100 + nu.getMinutes()).toString().substr(1) - const sec = (100 + nu.getSeconds()).toString().substr(1) - const ms = (1000 + nu.getMilliseconds()).toString().substr(1); - this.tempData = `${h}:${min}:${sec}.${ms} -> ` + this.tempData; - } - this.lines.push(this.tempData); - this.tempData = ''; - this.update(); - } - })); - // TODO onError } @postConstruct() protected init(): void { + this.toDisposeOnDetach.pushAll([ + this.serviceClient.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.boardsServiceClient.onBoardsConfigChanged(config => { + const { selectedBoard, selectedPort } = config; + if (selectedBoard && selectedPort) { + this.boardsService.getAttachedBoards().then(({ boards }) => { + if (boards.filter(AttachedSerialBoard.is).some(board => BoardsConfig.Config.sameAs(config, board))) { + this.connect(); + } + }); + } + })]); this.update(); } - clear(): void { + clearConsole(): void { + this.chunk = ''; this.lines = []; this.update(); } - storeState(): MonitorModel.Data { + storeState(): MonitorModel.State { return this.model.store(); } - restoreState(oldState: MonitorModel.Data): void { + restoreState(oldState: MonitorModel.State): void { this.model.restore(oldState); } - protected onAfterAttach(msg: Message) { + onBeforeAttach(msg: Message): void { + super.onBeforeAttach(msg); + this.clearConsole(); + } + + protected onAfterAttach(msg: Message): void { super.onAfterAttach(msg); - this.clear(); this.connect(); - this.toDisposeOnDetach.push( - this.boardsServiceClient.onBoardsChanged(async states => { - const currentConnectionConfig = this.connection.connectionConfig; - const connectedBoard = states.newState.boards - .filter(AttachedSerialBoard.is) - .find(board => { - const potentiallyConnected = currentConnectionConfig && currentConnectionConfig.board; - if (AttachedSerialBoard.is(potentiallyConnected)) { - return Board.equals(board, potentiallyConnected) && board.port === potentiallyConnected.port; - } - return false; - }); - if (connectedBoard && currentConnectionConfig) { - this.continuePreviousConnection = true; - this.connection.connect(currentConnectionConfig); - } - }) - ); - this.toDisposeOnDetach.push( - this.boardsServiceClient.onBoardsConfigChanged(async boardConfig => { - this.connect(); - }) - ) - - this.toDisposeOnDetach.push(this.connection.onConnectionChanged(() => { - if (!this.continuePreviousConnection) { - this.clear(); - } else { - this.continuePreviousConnection = false; - } - })); } - protected onBeforeDetach(msg: Message) { + protected onBeforeDetach(msg: Message): void { super.onBeforeDetach(msg); - this.connection.disconnect(); + if (this.connection.connectionId) { + this.connection.disconnect(); + } } - protected onResize(msg: Widget.ResizeMessage) { + protected onResize(msg: Widget.ResizeMessage): void { super.onResize(msg); this.widgetHeight = msg.height; this.update(); } - protected async connect() { + protected async connect(): Promise { const config = await this.getConnectionConfig(); if (config) { this.connection.connect(config); } } - protected async getConnectionConfig(): Promise { + protected async getConnectionConfig(): Promise { const baudRate = this.model.baudRate; const { boardsConfig } = this.boardsServiceClient; const { selectedBoard, selectedPort } = boardsConfig; @@ -268,11 +250,11 @@ export class MonitorWidget extends ReactWidget implements StatefulWidget { return { baudRate, board: selectedBoard, - port: selectedPort.address + port: selectedPort } } - protected getLineEndings(): OptionsType { + protected get lineEndings(): OptionsType> { return [ { label: 'No Line Ending', @@ -293,32 +275,29 @@ export class MonitorWidget extends ReactWidget implements StatefulWidget { ] } - protected getBaudRates(): OptionsType { - const baudRates = [300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200]; - return baudRates.map(baudRate => ({ label: baudRate + ' baud', value: baudRate })) + 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 le = this.getLineEndings(); - const br = this.getBaudRates(); - const leVal = this.model.lineEnding && le.find(val => val.value === this.model.lineEnding); - const brVal = this.model.baudRate && br.find(val => val.value === this.model.baudRate); - return -
-
-
- -
-
- {this.renderSelectField('arduino-serial-monitor-line-endings', le, leVal || le[1], this.onChangeLineEnding)} - {this.renderSelectField('arduino-serial-monitor-baud-rates', br, brVal || br[4], this.onChangeBaudRate)} -
+ 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); @@ -329,40 +308,45 @@ export class MonitorWidget extends ReactWidget implements StatefulWidget { } } - protected readonly onChangeLineEnding = (le: SelectOption) => { - this.model.lineEnding = typeof le.value === 'string' ? le.value : '\n'; + protected readonly onChangeLineEnding = (option: SelectOption) => { + this.model.lineEnding = typeof option.value === 'string' ? option.value : MonitorModel.EOL.DEFAULT; } - protected readonly onChangeBaudRate = async (br: SelectOption) => { + protected readonly onChangeBaudRate = async (option: SelectOption) => { await this.connection.disconnect(); - this.model.baudRate = typeof br.value === 'number' ? br.value : 9600; - this.clear(); + this.model.baudRate = typeof option.value === 'number' ? option.value : MonitorConfig.BaudRate.DEFAULT; + this.clearConsole(); const config = await this.getConnectionConfig(); if (config) { await this.connection.connect(config); } } - protected renderSelectField(id: string, options: OptionsType, defaultVal: SelectOption, onChange: (v: SelectOption) => void): React.ReactNode { + protected renderSelectField( + id: string, + options: OptionsType>, + defaultValue: SelectOption, + onChange: (option: SelectOption) => void): React.ReactNode { + const height = 25; - const selectStyles: Styles = { - control: (provided, state) => ({ - ...provided, + const styles: Styles = { + control: (styles, state) => ({ + ...styles, width: 200, - border: 'none' + color: 'var(--theia-ui-font-color1)' }), - dropdownIndicator: (p, s) => ({ - ...p, + dropdownIndicator: styles => ({ + ...styles, padding: 0 }), - indicatorSeparator: (p, s) => ({ + indicatorSeparator: () => ({ display: 'none' }), - indicatorsContainer: (p, s) => ({ - padding: '0 5px' + indicatorsContainer: () => ({ + padding: '0px 5px' }), - menu: (p, s) => ({ - ...p, + menu: styles => ({ + ...styles, marginTop: 0 }) }; @@ -375,24 +359,22 @@ export class MonitorWidget extends ReactWidget implements StatefulWidget { menuGutter: 4 } }); - const DropdownIndicator = ( - props: React.Props - ) => { + const DropdownIndicator = () => { return ( ); }; return