1
0
mirror of https://github.com/arduino/arduino-ide.git synced 2025-05-11 15:38:43 +00:00
Francesco Stasi 54a67fc67c Improve Serial Monitor Performances ()
Co-authored-by: Alberto Iannaccone <a.iannaccone@arduino.cc>
2021-10-06 09:21:06 +02:00

263 lines
7.5 KiB
TypeScript

import { ClientDuplexStream } from '@grpc/grpc-js';
import { TextEncoder } from 'util';
import { injectable, inject, named } from 'inversify';
import { Struct } from 'google-protobuf/google/protobuf/struct_pb';
import { Emitter } from '@theia/core/lib/common/event';
import { ILogger } from '@theia/core/lib/common/logger';
import {
MonitorService,
MonitorServiceClient,
MonitorConfig,
MonitorError,
Status,
} from '../../common/protocol/monitor-service';
import {
StreamingOpenRequest,
StreamingOpenResponse,
MonitorConfig as GrpcMonitorConfig,
} from '../cli-protocol/cc/arduino/cli/monitor/v1/monitor_pb';
import { MonitorClientProvider } from './monitor-client-provider';
import { Board, Port } from '../../common/protocol/boards-service';
import * as WebSocket from 'ws';
interface ErrorWithCode extends Error {
readonly code: number;
}
namespace ErrorWithCode {
export function toMonitorError(
error: Error,
config: MonitorConfig
): MonitorError {
const { message } = error;
let code = undefined;
if (is(error)) {
// TODO: const `mapping`. Use regex for the `message`.
const mapping = new Map<string, number>();
mapping.set(
'1 CANCELLED: Cancelled on client',
MonitorError.ErrorCodes.CLIENT_CANCEL
);
mapping.set(
'2 UNKNOWN: device not configured',
MonitorError.ErrorCodes.DEVICE_NOT_CONFIGURED
);
mapping.set(
'2 UNKNOWN: error opening serial monitor: Serial port busy',
MonitorError.ErrorCodes.DEVICE_BUSY
);
code = mapping.get(message);
}
return {
message,
code,
config,
};
}
function is(error: Error & { code?: number }): error is ErrorWithCode {
return typeof error.code === 'number';
}
}
@injectable()
export class MonitorServiceImpl implements MonitorService {
@inject(ILogger)
@named('monitor-service')
protected readonly logger: ILogger;
@inject(MonitorClientProvider)
protected readonly monitorClientProvider: MonitorClientProvider;
protected client?: MonitorServiceClient;
protected connection?: {
duplex: ClientDuplexStream<StreamingOpenRequest, StreamingOpenResponse>;
config: MonitorConfig;
};
protected messages: string[] = [];
protected onMessageDidReadEmitter = new Emitter<void>();
setClient(client: MonitorServiceClient | undefined): void {
this.client = client;
}
dispose(): void {
this.logger.info('>>> Disposing monitor service...');
if (this.connection) {
this.disconnect();
}
this.logger.info('<<< Disposed monitor service.');
this.client = undefined;
}
async connect(config: MonitorConfig): Promise<Status> {
this.logger.info(
`>>> Creating serial monitor connection for ${Board.toString(
config.board
)} on port ${Port.toString(config.port)}...`
);
if (this.connection) {
return Status.ALREADY_CONNECTED;
}
const client = await this.monitorClientProvider.client();
if (!client) {
return Status.NOT_CONNECTED;
}
if (client instanceof Error) {
return { message: client.message };
}
const duplex = client.streamingOpen();
this.connection = { duplex, config };
duplex.on(
'error',
((error: Error) => {
const monitorError = ErrorWithCode.toMonitorError(error, config);
this.disconnect(monitorError).then(() => {
if (this.client) {
this.client.notifyError(monitorError);
}
if (monitorError.code === undefined) {
// Log the original, unexpected error.
this.logger.error(error);
}
});
}).bind(this)
);
const ws = new WebSocket.Server({ port: 0 });
const address: any = ws.address();
this.client?.notifyMessage(address.port);
let wsConn: WebSocket | null = null;
ws.on('connection', (ws) => {
wsConn = ws;
});
const flushMessagesToFrontend = () => {
if (this.messages.length) {
wsConn?.send(JSON.stringify(this.messages));
this.messages = [];
}
};
// empty the queue every 16ms (~60fps)
setInterval(flushMessagesToFrontend, 32);
// converts 'ab\nc\nd' => [ab\n,c\n,d]
const stringToArray = (string: string, separator = '\n') => {
const retArray: string[] = [];
let prevChar = separator;
for (let i = 0; i < string.length; i++) {
const currChar = string[i];
if (prevChar === separator) {
retArray.push(currChar);
} else {
const lastWord = retArray[retArray.length - 1];
retArray[retArray.length - 1] = lastWord + currChar;
}
prevChar = currChar;
}
return retArray;
};
duplex.on(
'data',
((resp: StreamingOpenResponse) => {
const raw = resp.getData();
const message =
typeof raw === 'string' ? raw : new TextDecoder('utf8').decode(raw);
// split the message if it contains more lines
const messages = stringToArray(message);
this.messages.push(...messages);
}).bind(this)
);
const { type, port } = config;
const req = new StreamingOpenRequest();
const monitorConfig = new GrpcMonitorConfig();
monitorConfig.setType(this.mapType(type));
monitorConfig.setTarget(port.address);
if (config.baudRate !== undefined) {
monitorConfig.setAdditionalConfig(
Struct.fromJavaScript({ BaudRate: config.baudRate })
);
}
req.setConfig(monitorConfig);
return new Promise<Status>((resolve) => {
if (this.connection) {
this.connection.duplex.write(req, () => {
this.logger.info(
`<<< Serial monitor connection created for ${Board.toString(
config.board,
{ useFqbn: false }
)} on port ${Port.toString(config.port)}.`
);
resolve(Status.OK);
});
return;
}
this.disconnect().then(() => resolve(Status.NOT_CONNECTED));
});
}
async disconnect(reason?: MonitorError): Promise<Status> {
try {
if (
!this.connection &&
reason &&
reason.code === MonitorError.ErrorCodes.CLIENT_CANCEL
) {
return Status.OK;
}
this.logger.info('>>> Disposing monitor connection...');
if (!this.connection) {
this.logger.warn('<<< Not connected. Nothing to dispose.');
return Status.NOT_CONNECTED;
}
const { duplex, config } = this.connection;
duplex.cancel();
this.logger.info(
`<<< Disposed monitor connection for ${Board.toString(config.board, {
useFqbn: false,
})} on port ${Port.toString(config.port)}.`
);
this.connection = undefined;
return Status.OK;
} finally {
this.messages.length = 0;
}
}
async send(message: string): Promise<Status> {
if (!this.connection) {
return Status.NOT_CONNECTED;
}
const req = new StreamingOpenRequest();
req.setData(new TextEncoder().encode(message));
return new Promise<Status>((resolve) => {
if (this.connection) {
this.connection.duplex.write(req, () => {
resolve(Status.OK);
});
return;
}
this.disconnect().then(() => resolve(Status.NOT_CONNECTED));
});
}
protected mapType(
type?: MonitorConfig.ConnectionType
): GrpcMonitorConfig.TargetType {
switch (type) {
case MonitorConfig.ConnectionType.SERIAL:
return GrpcMonitorConfig.TargetType.TARGET_TYPE_SERIAL;
default:
return GrpcMonitorConfig.TargetType.TARGET_TYPE_SERIAL;
}
}
}