mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-12-01 12:57:15 +00:00
Improve Serial Monitor Performances (#524)
Co-authored-by: Alberto Iannaccone <a.iannaccone@arduino.cc>
This commit is contained in:
committed by
Francesco Stasi
parent
7f8b227c39
commit
54a67fc67c
@@ -400,24 +400,14 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(MonitorService)
|
||||
.toDynamicValue((context) => {
|
||||
const connection = context.container.get(WebSocketConnectionProvider);
|
||||
const client = context.container.get(MonitorServiceClientImpl);
|
||||
const client =
|
||||
context.container.get<MonitorServiceClient>(MonitorServiceClient);
|
||||
return connection.createProxy(MonitorServicePath, client);
|
||||
})
|
||||
.inSingletonScope();
|
||||
bind(MonitorConnection).toSelf().inSingletonScope();
|
||||
// Serial monitor service client to receive and delegate notifications from the backend.
|
||||
bind(MonitorServiceClientImpl).toSelf().inSingletonScope();
|
||||
bind(MonitorServiceClient)
|
||||
.toDynamicValue((context) => {
|
||||
const client = context.container.get(MonitorServiceClientImpl);
|
||||
WebSocketConnectionProvider.createProxy(
|
||||
context.container,
|
||||
MonitorServicePath,
|
||||
client
|
||||
);
|
||||
return client;
|
||||
})
|
||||
.inSingletonScope();
|
||||
bind(MonitorServiceClient).to(MonitorServiceClientImpl).inSingletonScope();
|
||||
|
||||
bind(WorkspaceService).toSelf().inSingletonScope();
|
||||
rebind(TheiaWorkspaceService).toService(WorkspaceService);
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
MonitorConfig,
|
||||
MonitorError,
|
||||
Status,
|
||||
MonitorServiceClient,
|
||||
} from '../../common/protocol/monitor-service';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import {
|
||||
@@ -16,7 +17,6 @@ import {
|
||||
BoardsService,
|
||||
AttachedBoardsChangeEvent,
|
||||
} from '../../common/protocol/boards-service';
|
||||
import { MonitorServiceClientImpl } from './monitor-service-client-impl';
|
||||
import { BoardsConfig } from '../boards/boards-config';
|
||||
import { MonitorModel } from './monitor-model';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
@@ -29,8 +29,8 @@ export class MonitorConnection {
|
||||
@inject(MonitorService)
|
||||
protected readonly monitorService: MonitorService;
|
||||
|
||||
@inject(MonitorServiceClientImpl)
|
||||
protected readonly monitorServiceClient: MonitorServiceClientImpl;
|
||||
@inject(MonitorServiceClient)
|
||||
protected readonly monitorServiceClient: MonitorServiceClient;
|
||||
|
||||
@inject(BoardsService)
|
||||
protected readonly boardsService: BoardsService;
|
||||
@@ -59,7 +59,7 @@ export class MonitorConnection {
|
||||
/**
|
||||
* This emitter forwards all read events **iff** the connection is established.
|
||||
*/
|
||||
protected readonly onReadEmitter = new Emitter<{ message: string }>();
|
||||
protected readonly onReadEmitter = new Emitter<{ messages: string[] }>();
|
||||
|
||||
/**
|
||||
* Array for storing previous monitor errors received from the server, and based on the number of elements in this array,
|
||||
@@ -71,112 +71,15 @@ export class MonitorConnection {
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.monitorServiceClient.onError(async (error) => {
|
||||
let shouldReconnect = false;
|
||||
if (this.state) {
|
||||
const { code, config } = error;
|
||||
const { board, port } = config;
|
||||
const options = { timeout: 3000 };
|
||||
switch (code) {
|
||||
case MonitorError.ErrorCodes.CLIENT_CANCEL: {
|
||||
console.debug(
|
||||
`Connection was canceled by client: ${MonitorConnection.State.toString(
|
||||
this.state
|
||||
)}.`
|
||||
);
|
||||
break;
|
||||
}
|
||||
case MonitorError.ErrorCodes.DEVICE_BUSY: {
|
||||
this.messageService.warn(
|
||||
`Connection failed. Serial port is busy: ${Port.toString(port)}.`,
|
||||
options
|
||||
);
|
||||
shouldReconnect = this.autoConnect;
|
||||
this.monitorErrors.push(error);
|
||||
break;
|
||||
}
|
||||
case MonitorError.ErrorCodes.DEVICE_NOT_CONFIGURED: {
|
||||
this.messageService.info(
|
||||
`Disconnected ${Board.toString(board, {
|
||||
useFqbn: false,
|
||||
})} from ${Port.toString(port)}.`,
|
||||
options
|
||||
);
|
||||
break;
|
||||
}
|
||||
case undefined: {
|
||||
this.messageService.error(
|
||||
`Unexpected error. Reconnecting ${Board.toString(
|
||||
board
|
||||
)} on port ${Port.toString(port)}.`,
|
||||
options
|
||||
);
|
||||
console.error(JSON.stringify(error));
|
||||
shouldReconnect = this.connected && this.autoConnect;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const oldState = this.state;
|
||||
this.state = undefined;
|
||||
this.onConnectionChangedEmitter.fire(this.state);
|
||||
if (shouldReconnect) {
|
||||
if (this.monitorErrors.length >= 10) {
|
||||
this.messageService.warn(
|
||||
`Failed to reconnect ${Board.toString(board, {
|
||||
useFqbn: false,
|
||||
})} to the the serial-monitor after 10 consecutive attempts. The ${Port.toString(
|
||||
port
|
||||
)} serial port is busy. after 10 consecutive attempts.`
|
||||
);
|
||||
this.monitorErrors.length = 0;
|
||||
} else {
|
||||
const attempts = this.monitorErrors.length || 1;
|
||||
if (this.reconnectTimeout !== undefined) {
|
||||
// Clear the previous timer.
|
||||
window.clearTimeout(this.reconnectTimeout);
|
||||
}
|
||||
const timeout = attempts * 1000;
|
||||
this.messageService.warn(
|
||||
`Reconnecting ${Board.toString(board, {
|
||||
useFqbn: false,
|
||||
})} to ${Port.toString(port)} in ${attempts} seconds...`,
|
||||
{ timeout }
|
||||
);
|
||||
this.reconnectTimeout = window.setTimeout(
|
||||
() => this.connect(oldState.config),
|
||||
timeout
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
this.monitorServiceClient.onMessage(this.handleMessage.bind(this));
|
||||
this.monitorServiceClient.onError(this.handleError.bind(this));
|
||||
this.boardsServiceProvider.onBoardsConfigChanged(
|
||||
this.handleBoardConfigChange.bind(this)
|
||||
);
|
||||
this.notificationCenter.onAttachedBoardsChanged((event) => {
|
||||
if (this.autoConnect && this.connected) {
|
||||
const { boardsConfig } = this.boardsServiceProvider;
|
||||
if (
|
||||
this.boardsServiceProvider.canUploadTo(boardsConfig, {
|
||||
silent: false,
|
||||
})
|
||||
) {
|
||||
const { attached } = AttachedBoardsChangeEvent.diff(event);
|
||||
if (
|
||||
attached.boards.some(
|
||||
(board) =>
|
||||
!!board.port && BoardsConfig.Config.sameAs(boardsConfig, board)
|
||||
)
|
||||
) {
|
||||
const { selectedBoard: board, selectedPort: port } = boardsConfig;
|
||||
const { baudRate } = this.monitorModel;
|
||||
this.disconnect().then(() =>
|
||||
this.connect({ board, port, baudRate })
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
this.notificationCenter.onAttachedBoardsChanged(
|
||||
this.handleAttachedBoardsChanged.bind(this)
|
||||
);
|
||||
|
||||
// Handles the `baudRate` changes by reconnecting if required.
|
||||
this.monitorModel.onChange(({ property }) => {
|
||||
if (property === 'baudRate' && this.autoConnect && this.connected) {
|
||||
@@ -186,6 +89,14 @@ export class MonitorConnection {
|
||||
});
|
||||
}
|
||||
|
||||
async handleMessage(port: string): Promise<void> {
|
||||
const w = new WebSocket(`ws://localhost:${port}`);
|
||||
w.onmessage = (res) => {
|
||||
const messages = JSON.parse(res.data);
|
||||
this.onReadEmitter.fire({ messages });
|
||||
};
|
||||
}
|
||||
|
||||
get connected(): boolean {
|
||||
return !!this.state;
|
||||
}
|
||||
@@ -217,6 +128,109 @@ export class MonitorConnection {
|
||||
}
|
||||
}
|
||||
|
||||
handleError(error: MonitorError): void {
|
||||
let shouldReconnect = false;
|
||||
if (this.state) {
|
||||
const { code, config } = error;
|
||||
const { board, port } = config;
|
||||
const options = { timeout: 3000 };
|
||||
switch (code) {
|
||||
case MonitorError.ErrorCodes.CLIENT_CANCEL: {
|
||||
console.debug(
|
||||
`Connection was canceled by client: ${MonitorConnection.State.toString(
|
||||
this.state
|
||||
)}.`
|
||||
);
|
||||
break;
|
||||
}
|
||||
case MonitorError.ErrorCodes.DEVICE_BUSY: {
|
||||
this.messageService.warn(
|
||||
`Connection failed. Serial port is busy: ${Port.toString(port)}.`,
|
||||
options
|
||||
);
|
||||
shouldReconnect = this.autoConnect;
|
||||
this.monitorErrors.push(error);
|
||||
break;
|
||||
}
|
||||
case MonitorError.ErrorCodes.DEVICE_NOT_CONFIGURED: {
|
||||
this.messageService.info(
|
||||
`Disconnected ${Board.toString(board, {
|
||||
useFqbn: false,
|
||||
})} from ${Port.toString(port)}.`,
|
||||
options
|
||||
);
|
||||
break;
|
||||
}
|
||||
case undefined: {
|
||||
this.messageService.error(
|
||||
`Unexpected error. Reconnecting ${Board.toString(
|
||||
board
|
||||
)} on port ${Port.toString(port)}.`,
|
||||
options
|
||||
);
|
||||
console.error(JSON.stringify(error));
|
||||
shouldReconnect = this.connected && this.autoConnect;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const oldState = this.state;
|
||||
this.state = undefined;
|
||||
this.onConnectionChangedEmitter.fire(this.state);
|
||||
if (shouldReconnect) {
|
||||
if (this.monitorErrors.length >= 10) {
|
||||
this.messageService.warn(
|
||||
`Failed to reconnect ${Board.toString(board, {
|
||||
useFqbn: false,
|
||||
})} to the the serial-monitor after 10 consecutive attempts. The ${Port.toString(
|
||||
port
|
||||
)} serial port is busy. after 10 consecutive attempts.`
|
||||
);
|
||||
this.monitorErrors.length = 0;
|
||||
} else {
|
||||
const attempts = this.monitorErrors.length || 1;
|
||||
if (this.reconnectTimeout !== undefined) {
|
||||
// Clear the previous timer.
|
||||
window.clearTimeout(this.reconnectTimeout);
|
||||
}
|
||||
const timeout = attempts * 1000;
|
||||
this.messageService.warn(
|
||||
`Reconnecting ${Board.toString(board, {
|
||||
useFqbn: false,
|
||||
})} to ${Port.toString(port)} in ${attempts} seconds...`,
|
||||
{ timeout }
|
||||
);
|
||||
this.reconnectTimeout = window.setTimeout(
|
||||
() => this.connect(oldState.config),
|
||||
timeout
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
|
||||
if (this.autoConnect && this.connected) {
|
||||
const { boardsConfig } = this.boardsServiceProvider;
|
||||
if (
|
||||
this.boardsServiceProvider.canUploadTo(boardsConfig, {
|
||||
silent: false,
|
||||
})
|
||||
) {
|
||||
const { attached } = AttachedBoardsChangeEvent.diff(event);
|
||||
if (
|
||||
attached.boards.some(
|
||||
(board) =>
|
||||
!!board.port && BoardsConfig.Config.sameAs(boardsConfig, board)
|
||||
)
|
||||
) {
|
||||
const { selectedBoard: board, selectedPort: port } = boardsConfig;
|
||||
const { baudRate } = this.monitorModel;
|
||||
this.disconnect().then(() => this.connect({ board, port, baudRate }));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async connect(config: MonitorConfig): Promise<Status> {
|
||||
if (this.connected) {
|
||||
const disconnectStatus = await this.disconnect();
|
||||
@@ -231,15 +245,6 @@ export class MonitorConnection {
|
||||
);
|
||||
const connectStatus = await this.monitorService.connect(config);
|
||||
if (Status.isOK(connectStatus)) {
|
||||
const requestMessage = () => {
|
||||
this.monitorService.request().then(({ message }) => {
|
||||
if (this.connected) {
|
||||
this.onReadEmitter.fire({ message });
|
||||
requestMessage();
|
||||
}
|
||||
});
|
||||
};
|
||||
requestMessage();
|
||||
this.state = { config };
|
||||
console.info(
|
||||
`<<< Serial monitor connection created for ${Board.toString(
|
||||
@@ -300,7 +305,7 @@ export class MonitorConnection {
|
||||
return this.onConnectionChangedEmitter.event;
|
||||
}
|
||||
|
||||
get onRead(): Event<{ message: string }> {
|
||||
get onRead(): Event<{ messages: string[] }> {
|
||||
return this.onReadEmitter.event;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,14 @@ export class MonitorServiceClientImpl implements MonitorServiceClient {
|
||||
protected readonly onErrorEmitter = new Emitter<MonitorError>();
|
||||
readonly onError = this.onErrorEmitter.event;
|
||||
|
||||
protected readonly onMessageEmitter = new Emitter<string>();
|
||||
readonly onMessage = this.onMessageEmitter.event;
|
||||
|
||||
notifyError(error: MonitorError): void {
|
||||
this.onErrorEmitter.fire(error);
|
||||
}
|
||||
|
||||
notifyMessage(message: string): void {
|
||||
this.onMessageEmitter.fire(message);
|
||||
}
|
||||
}
|
||||
|
||||
68
arduino-ide-extension/src/browser/monitor/monitor-utils.ts
Normal file
68
arduino-ide-extension/src/browser/monitor/monitor-utils.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { Line, SerialMonitorOutput } from './serial-monitor-send-output';
|
||||
|
||||
export function messagesToLines(
|
||||
messages: string[],
|
||||
prevLines: Line[] = [],
|
||||
charCount = 0,
|
||||
separator = '\n'
|
||||
): [Line[], number] {
|
||||
const linesToAdd: Line[] = prevLines.length
|
||||
? [prevLines[prevLines.length - 1]]
|
||||
: [{ message: '', lineLen: 0 }];
|
||||
|
||||
for (const message of messages) {
|
||||
const messageLen = message.length;
|
||||
charCount += messageLen;
|
||||
const lastLine = linesToAdd[linesToAdd.length - 1];
|
||||
|
||||
// if the previous messages ends with "separator" add a new line
|
||||
if (lastLine.message.charAt(lastLine.message.length - 1) === separator) {
|
||||
linesToAdd.push({
|
||||
message,
|
||||
timestamp: new Date(),
|
||||
lineLen: messageLen,
|
||||
});
|
||||
} else {
|
||||
// concatenate to the last line
|
||||
linesToAdd[linesToAdd.length - 1].message += message;
|
||||
linesToAdd[linesToAdd.length - 1].lineLen += messageLen;
|
||||
if (!linesToAdd[linesToAdd.length - 1].timestamp) {
|
||||
linesToAdd[linesToAdd.length - 1].timestamp = new Date();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prevLines.splice(prevLines.length - 1, 1, ...linesToAdd);
|
||||
return [prevLines, charCount];
|
||||
}
|
||||
|
||||
export function truncateLines(
|
||||
lines: Line[],
|
||||
charCount: number,
|
||||
maxCharacters: number = SerialMonitorOutput.MAX_CHARACTERS
|
||||
): [Line[], number] {
|
||||
let charsToDelete = charCount - maxCharacters;
|
||||
let lineIndex = 0;
|
||||
while (charsToDelete > 0 || lineIndex > 0) {
|
||||
const firstLineLength = lines[lineIndex]?.lineLen;
|
||||
|
||||
if (charsToDelete >= firstLineLength) {
|
||||
// every time a full line to delete is found, move the index.
|
||||
lineIndex++;
|
||||
charsToDelete -= firstLineLength;
|
||||
charCount -= firstLineLength;
|
||||
continue;
|
||||
}
|
||||
|
||||
// delete all previous lines
|
||||
lines.splice(0, lineIndex);
|
||||
lineIndex = 0;
|
||||
|
||||
const newFirstLine = lines[0]?.message?.substring(charsToDelete);
|
||||
const deletedCharsCount = firstLineLength - newFirstLine.length;
|
||||
charCount -= deletedCharsCount;
|
||||
charsToDelete -= deletedCharsCount;
|
||||
lines[0].message = newFirstLine;
|
||||
}
|
||||
return [lines, charCount];
|
||||
}
|
||||
@@ -1,26 +1,20 @@
|
||||
import * as React from 'react';
|
||||
import * as dateFormat from 'dateformat';
|
||||
import { postConstruct, injectable, inject } from 'inversify';
|
||||
import { OptionsType } from 'react-select/src/types';
|
||||
import { isOSX } from '@theia/core/lib/common/os';
|
||||
import { Event, Emitter } from '@theia/core/lib/common/event';
|
||||
import { Key, KeyCode } from '@theia/core/lib/browser/keys';
|
||||
import {
|
||||
DisposableCollection,
|
||||
Disposable,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { Disposable } from '@theia/core/lib/common/disposable';
|
||||
import {
|
||||
ReactWidget,
|
||||
Message,
|
||||
Widget,
|
||||
MessageLoop,
|
||||
} from '@theia/core/lib/browser/widgets';
|
||||
import { Board, Port } from '../../common/protocol/boards-service';
|
||||
import { MonitorConfig } from '../../common/protocol/monitor-service';
|
||||
import { ArduinoSelect } from '../widgets/arduino-select';
|
||||
import { MonitorModel } from './monitor-model';
|
||||
import { MonitorConnection } from './monitor-connection';
|
||||
import { MonitorServiceClientImpl } from './monitor-service-client-impl';
|
||||
import { SerialMonitorSendInput } from './serial-monitor-send-input';
|
||||
import { SerialMonitorOutput } from './serial-monitor-send-output';
|
||||
|
||||
@injectable()
|
||||
export class MonitorWidget extends ReactWidget {
|
||||
@@ -32,9 +26,6 @@ export class MonitorWidget extends ReactWidget {
|
||||
@inject(MonitorConnection)
|
||||
protected readonly monitorConnection: MonitorConnection;
|
||||
|
||||
@inject(MonitorServiceClientImpl)
|
||||
protected readonly monitorServiceClient: MonitorServiceClientImpl;
|
||||
|
||||
protected widgetHeight: number;
|
||||
|
||||
/**
|
||||
@@ -122,7 +113,9 @@ export class MonitorWidget extends ReactWidget {
|
||||
);
|
||||
};
|
||||
|
||||
protected get lineEndings(): OptionsType<SelectOption<MonitorModel.EOL>> {
|
||||
protected get lineEndings(): OptionsType<
|
||||
SerialMonitorOutput.SelectOption<MonitorModel.EOL>
|
||||
> {
|
||||
return [
|
||||
{
|
||||
label: 'No Line Ending',
|
||||
@@ -143,7 +136,9 @@ export class MonitorWidget extends ReactWidget {
|
||||
];
|
||||
}
|
||||
|
||||
protected get baudRates(): OptionsType<SelectOption<MonitorConfig.BaudRate>> {
|
||||
protected get baudRates(): OptionsType<
|
||||
SerialMonitorOutput.SelectOption<MonitorConfig.BaudRate>
|
||||
> {
|
||||
const baudRates: Array<MonitorConfig.BaudRate> = [
|
||||
300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200,
|
||||
];
|
||||
@@ -196,6 +191,7 @@ export class MonitorWidget extends ReactWidget {
|
||||
monitorModel={this.monitorModel}
|
||||
monitorConnection={this.monitorConnection}
|
||||
clearConsoleEvent={this.clearOutputEmitter.event}
|
||||
height={Math.floor(this.widgetHeight - 50)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -208,187 +204,14 @@ export class MonitorWidget extends ReactWidget {
|
||||
}
|
||||
|
||||
protected readonly onChangeLineEnding = (
|
||||
option: SelectOption<MonitorModel.EOL>
|
||||
option: SerialMonitorOutput.SelectOption<MonitorModel.EOL>
|
||||
) => {
|
||||
this.monitorModel.lineEnding = option.value;
|
||||
};
|
||||
|
||||
protected readonly onChangeBaudRate = (
|
||||
option: SelectOption<MonitorConfig.BaudRate>
|
||||
option: SerialMonitorOutput.SelectOption<MonitorConfig.BaudRate>
|
||||
) => {
|
||||
this.monitorModel.baudRate = option.value;
|
||||
};
|
||||
}
|
||||
|
||||
export namespace SerialMonitorSendInput {
|
||||
export interface Props {
|
||||
readonly monitorConfig?: MonitorConfig;
|
||||
readonly onSend: (text: string) => void;
|
||||
readonly resolveFocus: (element: HTMLElement | undefined) => void;
|
||||
}
|
||||
export interface State {
|
||||
text: string;
|
||||
}
|
||||
}
|
||||
|
||||
export class SerialMonitorSendInput extends React.Component<
|
||||
SerialMonitorSendInput.Props,
|
||||
SerialMonitorSendInput.State
|
||||
> {
|
||||
constructor(props: Readonly<SerialMonitorSendInput.Props>) {
|
||||
super(props);
|
||||
this.state = { text: '' };
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onSend = this.onSend.bind(this);
|
||||
this.onKeyDown = this.onKeyDown.bind(this);
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
return (
|
||||
<input
|
||||
ref={this.setRef}
|
||||
type="text"
|
||||
className={`theia-input ${this.props.monitorConfig ? '' : 'warning'}`}
|
||||
placeholder={this.placeholder}
|
||||
value={this.state.text}
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
protected get placeholder(): string {
|
||||
const { monitorConfig } = this.props;
|
||||
if (!monitorConfig) {
|
||||
return 'Not connected. Select a board and a port to connect automatically.';
|
||||
}
|
||||
const { board, port } = monitorConfig;
|
||||
return `Message (${
|
||||
isOSX ? '⌘' : 'Ctrl'
|
||||
}+Enter to send message to '${Board.toString(board, {
|
||||
useFqbn: false,
|
||||
})}' on '${Port.toString(port)}')`;
|
||||
}
|
||||
|
||||
protected setRef = (element: HTMLElement | null) => {
|
||||
if (this.props.resolveFocus) {
|
||||
this.props.resolveFocus(element || undefined);
|
||||
}
|
||||
};
|
||||
|
||||
protected onChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||
this.setState({ text: event.target.value });
|
||||
}
|
||||
|
||||
protected onSend(): void {
|
||||
this.props.onSend(this.state.text);
|
||||
this.setState({ text: '' });
|
||||
}
|
||||
|
||||
protected onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
|
||||
const keyCode = KeyCode.createKeyCode(event.nativeEvent);
|
||||
if (keyCode) {
|
||||
const { key, meta, ctrl } = keyCode;
|
||||
if (key === Key.ENTER && ((isOSX && meta) || (!isOSX && ctrl))) {
|
||||
this.onSend();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace SerialMonitorOutput {
|
||||
export interface Props {
|
||||
readonly monitorModel: MonitorModel;
|
||||
readonly monitorConnection: MonitorConnection;
|
||||
readonly clearConsoleEvent: Event<void>;
|
||||
}
|
||||
export interface State {
|
||||
content: string;
|
||||
timestamp: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export class SerialMonitorOutput extends React.Component<
|
||||
SerialMonitorOutput.Props,
|
||||
SerialMonitorOutput.State
|
||||
> {
|
||||
/**
|
||||
* Do not touch it. It is used to be able to "follow" the serial monitor log.
|
||||
*/
|
||||
protected anchor: HTMLElement | null;
|
||||
protected toDisposeBeforeUnmount = new DisposableCollection();
|
||||
|
||||
constructor(props: Readonly<SerialMonitorOutput.Props>) {
|
||||
super(props);
|
||||
this.state = {
|
||||
content: '',
|
||||
timestamp: this.props.monitorModel.timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div style={{ whiteSpace: 'pre', fontFamily: 'monospace' }}>
|
||||
{this.state.content}
|
||||
</div>
|
||||
<div
|
||||
style={{ float: 'left', clear: 'both' }}
|
||||
ref={(element) => {
|
||||
this.anchor = element;
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.scrollToBottom();
|
||||
this.toDisposeBeforeUnmount.pushAll([
|
||||
this.props.monitorConnection.onRead(({ message }) => {
|
||||
const rawLines = message.split('\n');
|
||||
const lines: string[] = [];
|
||||
const timestamp = () =>
|
||||
this.state.timestamp
|
||||
? `${dateFormat(new Date(), 'H:M:ss.l')} -> `
|
||||
: '';
|
||||
for (let i = 0; i < rawLines.length; i++) {
|
||||
if (i === 0 && this.state.content.length !== 0) {
|
||||
lines.push(rawLines[i]);
|
||||
} else {
|
||||
lines.push(timestamp() + rawLines[i]);
|
||||
}
|
||||
}
|
||||
const content = this.state.content + lines.join('\n');
|
||||
this.setState({ content });
|
||||
}),
|
||||
this.props.clearConsoleEvent(() => this.setState({ content: '' })),
|
||||
this.props.monitorModel.onChange(({ property }) => {
|
||||
if (property === 'timestamp') {
|
||||
const { timestamp } = this.props.monitorModel;
|
||||
this.setState({ timestamp });
|
||||
}
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
componentDidUpdate(): void {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
// TODO: "Your preferred browser's local storage is almost full." Discard `content` before saving layout?
|
||||
this.toDisposeBeforeUnmount.dispose();
|
||||
}
|
||||
|
||||
protected scrollToBottom(): void {
|
||||
if (this.props.monitorModel.autoscroll && this.anchor) {
|
||||
this.anchor.scrollIntoView();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface SelectOption<T> {
|
||||
readonly label: string;
|
||||
readonly value: T;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import * as React from 'react';
|
||||
import { Key, KeyCode } from '@theia/core/lib/browser/keys';
|
||||
import { Board, Port } from '../../common/protocol/boards-service';
|
||||
import { MonitorConfig } from '../../common/protocol/monitor-service';
|
||||
import { isOSX } from '@theia/core/lib/common/os';
|
||||
|
||||
export namespace SerialMonitorSendInput {
|
||||
export interface Props {
|
||||
readonly monitorConfig?: MonitorConfig;
|
||||
readonly onSend: (text: string) => void;
|
||||
readonly resolveFocus: (element: HTMLElement | undefined) => void;
|
||||
}
|
||||
export interface State {
|
||||
text: string;
|
||||
}
|
||||
}
|
||||
|
||||
export class SerialMonitorSendInput extends React.Component<
|
||||
SerialMonitorSendInput.Props,
|
||||
SerialMonitorSendInput.State
|
||||
> {
|
||||
constructor(props: Readonly<SerialMonitorSendInput.Props>) {
|
||||
super(props);
|
||||
this.state = { text: '' };
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onSend = this.onSend.bind(this);
|
||||
this.onKeyDown = this.onKeyDown.bind(this);
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
return (
|
||||
<input
|
||||
ref={this.setRef}
|
||||
type="text"
|
||||
className={`theia-input ${this.props.monitorConfig ? '' : 'warning'}`}
|
||||
placeholder={this.placeholder}
|
||||
value={this.state.text}
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
protected get placeholder(): string {
|
||||
const { monitorConfig } = this.props;
|
||||
if (!monitorConfig) {
|
||||
return 'Not connected. Select a board and a port to connect automatically.';
|
||||
}
|
||||
const { board, port } = monitorConfig;
|
||||
return `Message (${
|
||||
isOSX ? '⌘' : 'Ctrl'
|
||||
}+Enter to send message to '${Board.toString(board, {
|
||||
useFqbn: false,
|
||||
})}' on '${Port.toString(port)}')`;
|
||||
}
|
||||
|
||||
protected setRef = (element: HTMLElement | null) => {
|
||||
if (this.props.resolveFocus) {
|
||||
this.props.resolveFocus(element || undefined);
|
||||
}
|
||||
};
|
||||
|
||||
protected onChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||
this.setState({ text: event.target.value });
|
||||
}
|
||||
|
||||
protected onSend(): void {
|
||||
this.props.onSend(this.state.text);
|
||||
this.setState({ text: '' });
|
||||
}
|
||||
|
||||
protected onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
|
||||
const keyCode = KeyCode.createKeyCode(event.nativeEvent);
|
||||
if (keyCode) {
|
||||
const { key, meta, ctrl } = keyCode;
|
||||
if (key === Key.ENTER && ((isOSX && meta) || (!isOSX && ctrl))) {
|
||||
this.onSend();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import * as React from 'react';
|
||||
import { Event } from '@theia/core/lib/common/event';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { areEqual, FixedSizeList as List } from 'react-window';
|
||||
import { MonitorModel } from './monitor-model';
|
||||
import { MonitorConnection } from './monitor-connection';
|
||||
import dateFormat = require('dateformat');
|
||||
import { messagesToLines, truncateLines } from './monitor-utils';
|
||||
|
||||
export type Line = { message: string; timestamp?: Date; lineLen: number };
|
||||
|
||||
export class SerialMonitorOutput extends React.Component<
|
||||
SerialMonitorOutput.Props,
|
||||
SerialMonitorOutput.State
|
||||
> {
|
||||
/**
|
||||
* Do not touch it. It is used to be able to "follow" the serial monitor log.
|
||||
*/
|
||||
protected toDisposeBeforeUnmount = new DisposableCollection();
|
||||
private listRef: React.RefObject<any>;
|
||||
|
||||
constructor(props: Readonly<SerialMonitorOutput.Props>) {
|
||||
super(props);
|
||||
this.listRef = React.createRef();
|
||||
this.state = {
|
||||
lines: [],
|
||||
timestamp: this.props.monitorModel.timestamp,
|
||||
charCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
return (
|
||||
<List
|
||||
className="serial-monitor-messages"
|
||||
height={this.props.height}
|
||||
itemData={
|
||||
{
|
||||
lines: this.state.lines,
|
||||
timestamp: this.state.timestamp,
|
||||
} as any
|
||||
}
|
||||
itemCount={this.state.lines.length}
|
||||
itemSize={18}
|
||||
width={'100%'}
|
||||
ref={this.listRef}
|
||||
>
|
||||
{Row}
|
||||
</List>
|
||||
);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.scrollToBottom();
|
||||
this.toDisposeBeforeUnmount.pushAll([
|
||||
this.props.monitorConnection.onRead(({ messages }) => {
|
||||
const [newLines, totalCharCount] = messagesToLines(
|
||||
messages,
|
||||
this.state.lines,
|
||||
this.state.charCount
|
||||
);
|
||||
const [lines, charCount] = truncateLines(newLines, totalCharCount);
|
||||
|
||||
this.setState({
|
||||
lines,
|
||||
charCount,
|
||||
});
|
||||
this.scrollToBottom();
|
||||
}),
|
||||
this.props.clearConsoleEvent(() => this.setState({ lines: [] })),
|
||||
this.props.monitorModel.onChange(({ property }) => {
|
||||
if (property === 'timestamp') {
|
||||
const { timestamp } = this.props.monitorModel;
|
||||
this.setState({ timestamp });
|
||||
}
|
||||
if (property === 'autoscroll') {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
// TODO: "Your preferred browser's local storage is almost full." Discard `content` before saving layout?
|
||||
this.toDisposeBeforeUnmount.dispose();
|
||||
}
|
||||
|
||||
scrollToBottom = ((): void => {
|
||||
if (this.listRef.current && this.props.monitorModel.autoscroll) {
|
||||
this.listRef.current.scrollToItem(this.state.lines.length, 'end');
|
||||
}
|
||||
}).bind(this);
|
||||
}
|
||||
|
||||
const _Row = ({
|
||||
index,
|
||||
style,
|
||||
data,
|
||||
}: {
|
||||
index: number;
|
||||
style: any;
|
||||
data: { lines: Line[]; timestamp: boolean };
|
||||
}) => {
|
||||
const timestamp =
|
||||
(data.timestamp &&
|
||||
`${dateFormat(data.lines[index].timestamp, 'H:M:ss.l')} -> `) ||
|
||||
'';
|
||||
return (
|
||||
(data.lines[index].lineLen && (
|
||||
<div style={style}>
|
||||
{timestamp}
|
||||
{data.lines[index].message}
|
||||
</div>
|
||||
)) ||
|
||||
null
|
||||
);
|
||||
};
|
||||
const Row = React.memo(_Row, areEqual);
|
||||
|
||||
export namespace SerialMonitorOutput {
|
||||
export interface Props {
|
||||
readonly monitorModel: MonitorModel;
|
||||
readonly monitorConnection: MonitorConnection;
|
||||
readonly clearConsoleEvent: Event<void>;
|
||||
readonly height: number;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
lines: Line[];
|
||||
timestamp: boolean;
|
||||
charCount: number;
|
||||
}
|
||||
|
||||
export interface SelectOption<T> {
|
||||
readonly label: string;
|
||||
readonly value: T;
|
||||
}
|
||||
|
||||
export const MAX_CHARACTERS = 1_000_000;
|
||||
}
|
||||
@@ -9,6 +9,11 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.serial-monitor-messages {
|
||||
white-space: 'pre';
|
||||
font-family: monospace
|
||||
}
|
||||
|
||||
.serial-monitor .head {
|
||||
display: flex;
|
||||
padding: 5px;
|
||||
|
||||
Reference in New Issue
Block a user