From 9058abb01597c963aab3897b2f963e2c936286f2 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Thu, 10 Mar 2022 15:48:55 +0100 Subject: [PATCH] Remove several unnecessary serial monitor classes --- .../serial/serial-connection-manager.ts | 360 ---------------- .../src/browser/serial/serial-model.ts | 163 ------- .../serial/serial-service-client-impl.ts | 48 --- .../src/common/protocol/serial-service.ts | 102 ----- .../node/serial/monitor-client-provider.ts | 26 -- .../src/node/serial/serial-service-impl.ts | 397 ------------------ .../src/test/node/serial-service-impl.test.ts | 167 -------- 7 files changed, 1263 deletions(-) delete mode 100644 arduino-ide-extension/src/browser/serial/serial-connection-manager.ts delete mode 100644 arduino-ide-extension/src/browser/serial/serial-model.ts delete mode 100644 arduino-ide-extension/src/browser/serial/serial-service-client-impl.ts delete mode 100644 arduino-ide-extension/src/common/protocol/serial-service.ts delete mode 100644 arduino-ide-extension/src/node/serial/monitor-client-provider.ts delete mode 100644 arduino-ide-extension/src/node/serial/serial-service-impl.ts delete mode 100644 arduino-ide-extension/src/test/node/serial-service-impl.test.ts diff --git a/arduino-ide-extension/src/browser/serial/serial-connection-manager.ts b/arduino-ide-extension/src/browser/serial/serial-connection-manager.ts deleted file mode 100644 index e3fb2476..00000000 --- a/arduino-ide-extension/src/browser/serial/serial-connection-manager.ts +++ /dev/null @@ -1,360 +0,0 @@ -import { injectable, inject } from 'inversify'; -import { Emitter, Event } from '@theia/core/lib/common/event'; -import { MessageService } from '@theia/core/lib/common/message-service'; -import { - SerialService, - SerialConfig, - SerialError, - Status, - SerialServiceClient, -} from '../../common/protocol/serial-service'; -import { BoardsServiceProvider } from '../boards/boards-service-provider'; -import { - Board, - BoardsService, -} from '../../common/protocol/boards-service'; -import { BoardsConfig } from '../boards/boards-config'; -import { SerialModel } from './serial-model'; -import { ThemeService } from '@theia/core/lib/browser/theming'; -import { CoreService } from '../../common/protocol'; -import { nls } from '@theia/core/lib/common/nls'; - -@injectable() -export class SerialConnectionManager { - protected config: Partial = { - board: undefined, - port: undefined, - baudRate: undefined, - }; - - protected readonly onConnectionChangedEmitter = new Emitter(); - - /** - * This emitter forwards all read events **if** the connection is established. - */ - protected readonly onReadEmitter = new Emitter<{ messages: string[] }>(); - - /** - * Array for storing previous serial errors received from the server, and based on the number of elements in this array, - * we adjust the reconnection delay. - * Super naive way: we wait `array.length * 1000` ms. Once we hit 10 errors, we do not try to reconnect and clean the array. - */ - protected serialErrors: SerialError[] = []; - protected reconnectTimeout?: number; - - /** - * When the websocket server is up on the backend, we save the port here, so that the client knows how to connect to it - * */ - protected wsPort?: number; - protected webSocket?: WebSocket; - - constructor( - @inject(SerialModel) protected readonly serialModel: SerialModel, - @inject(SerialService) protected readonly serialService: SerialService, - @inject(SerialServiceClient) - protected readonly serialServiceClient: SerialServiceClient, - @inject(BoardsService) protected readonly boardsService: BoardsService, - @inject(BoardsServiceProvider) - protected readonly boardsServiceProvider: BoardsServiceProvider, - @inject(MessageService) protected messageService: MessageService, - @inject(ThemeService) protected readonly themeService: ThemeService, - @inject(CoreService) protected readonly core: CoreService, - @inject(BoardsServiceProvider) - protected readonly boardsServiceClientImpl: BoardsServiceProvider - ) { - this.serialServiceClient.onWebSocketChanged( - this.handleWebSocketChanged.bind(this) - ); - this.serialServiceClient.onBaudRateChanged((baudRate) => { - if (this.serialModel.baudRate !== baudRate) { - this.serialModel.baudRate = baudRate; - } - }); - this.serialServiceClient.onLineEndingChanged((lineending) => { - if (this.serialModel.lineEnding !== lineending) { - this.serialModel.lineEnding = lineending; - } - }); - this.serialServiceClient.onInterpolateChanged((interpolate) => { - if (this.serialModel.interpolate !== interpolate) { - this.serialModel.interpolate = interpolate; - } - }); - - this.serialServiceClient.onError(this.handleError.bind(this)); - this.boardsServiceProvider.onBoardsConfigChanged( - this.handleBoardConfigChange.bind(this) - ); - - // Handles the `baudRate` changes by reconnecting if required. - this.serialModel.onChange(async ({ property }) => { - if ( - property === 'baudRate' && - (await this.serialService.isSerialPortOpen()) - ) { - const { boardsConfig } = this.boardsServiceProvider; - this.handleBoardConfigChange(boardsConfig); - } - - // update the current values in the backend and propagate to websocket clients - this.serialService.updateWsConfigParam({ - ...(property === 'lineEnding' && { - currentLineEnding: this.serialModel.lineEnding, - }), - ...(property === 'interpolate' && { - interpolate: this.serialModel.interpolate, - }), - }); - }); - - this.themeService.onDidColorThemeChange((theme) => { - this.serialService.updateWsConfigParam({ - darkTheme: theme.newTheme.type === 'dark', - }); - }); - } - - /** - * Updated the config in the BE passing only the properties that has changed. - * BE will create a new connection if needed. - * - * @param newConfig the porperties of the config that has changed - */ - async setConfig(newConfig: Partial): Promise { - let configHasChanged = false; - Object.keys(this.config).forEach((key: keyof SerialConfig) => { - if (newConfig[key] !== this.config[key]) { - configHasChanged = true; - this.config = { ...this.config, [key]: newConfig[key] }; - } - }); - - if (configHasChanged) { - this.serialService.updateWsConfigParam({ - currentBaudrate: this.config.baudRate, - serialPort: this.config.port?.address, - }); - - if (isSerialConfig(this.config)) { - this.serialService.setSerialConfig(this.config); - } - } - } - - getConfig(): Partial { - return this.config; - } - - getWsPort(): number | undefined { - return this.wsPort; - } - - protected handleWebSocketChanged(wsPort: number): void { - this.wsPort = wsPort; - } - - get serialConfig(): SerialConfig | undefined { - return isSerialConfig(this.config) - ? (this.config as SerialConfig) - : undefined; - } - - async isBESerialConnected(): Promise { - return await this.serialService.isSerialPortOpen(); - } - - openWSToBE(): void { - if (!isSerialConfig(this.config)) { - this.messageService.error( - `Please select a board and a port to open the serial connection.` - ); - } - - if (!this.webSocket && this.wsPort) { - try { - this.webSocket = new WebSocket(`ws://localhost:${this.wsPort}`); - this.webSocket.onmessage = (res) => { - const messages = JSON.parse(res.data); - this.onReadEmitter.fire({ messages }); - }; - } catch { - this.messageService.error(`Unable to connect to websocket`); - } - } - } - - closeWStoBE(): void { - if (this.webSocket) { - try { - this.webSocket.close(); - this.webSocket = undefined; - } catch { - this.messageService.error(`Unable to close websocket`); - } - } - } - - /** - * Handles error on the SerialServiceClient and try to reconnect, eventually - */ - async handleError(error: SerialError): Promise { - if (!(await this.serialService.isSerialPortOpen())) return; - const { code, config } = error; - const { board, port } = config; - const options = { timeout: 3000 }; - switch (code) { - case SerialError.ErrorCodes.CLIENT_CANCEL: { - console.debug( - `Serial connection was canceled by client: ${Serial.Config.toString( - this.config - )}.` - ); - break; - } - case SerialError.ErrorCodes.DEVICE_BUSY: { - this.messageService.warn( - nls.localize( - 'arduino/serial/connectionBusy', - 'Connection failed. Serial port is busy: {0}', - port.address - ), - options - ); - this.serialErrors.push(error); - break; - } - case SerialError.ErrorCodes.DEVICE_NOT_CONFIGURED: { - this.messageService.info( - nls.localize( - 'arduino/serial/disconnected', - 'Disconnected {0} from {1}.', - Board.toString(board, { - useFqbn: false, - }), - port.address - ), - options - ); - break; - } - case undefined: { - this.messageService.error( - nls.localize( - 'arduino/serial/unexpectedError', - 'Unexpected error. Reconnecting {0} on port {1}.', - Board.toString(board), - port.address - ), - options - ); - console.error(JSON.stringify(error)); - break; - } - } - - if ((await this.serialService.clientsAttached()) > 0) { - if (this.serialErrors.length >= 10) { - this.messageService.warn( - nls.localize( - 'arduino/serial/failedReconnect', - 'Failed to reconnect {0} to serial port after 10 consecutive attempts. The {1} serial port is busy.', - Board.toString(board, { - useFqbn: false, - }), - port.address - ) - ); - this.serialErrors.length = 0; - } else { - const attempts = this.serialErrors.length || 1; - if (this.reconnectTimeout !== undefined) { - // Clear the previous timer. - window.clearTimeout(this.reconnectTimeout); - } - const timeout = attempts * 1000; - this.messageService.warn( - nls.localize( - 'arduino/serial/reconnect', - 'Reconnecting {0} to {1} in {2} seconds...', - Board.toString(board, { - useFqbn: false, - }), - port.address, - attempts.toString() - ) - ); - this.reconnectTimeout = window.setTimeout( - () => this.reconnectAfterUpload(), - timeout - ); - } - } - } - - async reconnectAfterUpload(): Promise { - try { - if (isSerialConfig(this.config)) { - await this.boardsServiceClientImpl.waitUntilAvailable( - Object.assign(this.config.board, { port: this.config.port }), - 10_000 - ); - this.serialService.connectSerialIfRequired(); - } - } catch (waitError) { - this.messageService.error( - nls.localize( - 'arduino/sketch/couldNotConnectToSerial', - 'Could not reconnect to serial port. {0}', - waitError.toString() - ) - ); - } - } - - /** - * Sends the data to the connected serial port. - * 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 (!(await this.serialService.isSerialPortOpen())) { - return Status.NOT_CONNECTED; - } - return new Promise((resolve) => { - this.serialService - .sendMessageToSerial(data + this.serialModel.lineEnding) - .then(() => resolve(Status.OK)); - }); - } - - get onConnectionChanged(): Event { - return this.onConnectionChangedEmitter.event; - } - - get onRead(): Event<{ messages: any }> { - return this.onReadEmitter.event; - } - - protected async handleBoardConfigChange( - boardsConfig: BoardsConfig.Config - ): Promise { - const { selectedBoard: board, selectedPort: port } = boardsConfig; - const { baudRate } = this.serialModel; - const newConfig: Partial = { board, port, baudRate }; - this.setConfig(newConfig); - } -} - -export namespace Serial { - export namespace Config { - export function toString(config: Partial): string { - if (!isSerialConfig(config)) return ''; - const { board, port } = config; - return `${Board.toString(board)} ${port.address}`; - } - } -} - -function isSerialConfig(config: Partial): config is SerialConfig { - return !!config.board && !!config.baudRate && !!config.port; -} diff --git a/arduino-ide-extension/src/browser/serial/serial-model.ts b/arduino-ide-extension/src/browser/serial/serial-model.ts deleted file mode 100644 index fc6e352e..00000000 --- a/arduino-ide-extension/src/browser/serial/serial-model.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { injectable, inject } from 'inversify'; -import { Emitter, Event } from '@theia/core/lib/common/event'; -import { SerialConfig } from '../../common/protocol'; -import { - FrontendApplicationContribution, - LocalStorageService, -} from '@theia/core/lib/browser'; -import { BoardsServiceProvider } from '../boards/boards-service-provider'; - -@injectable() -export class SerialModel implements FrontendApplicationContribution { - protected static STORAGE_ID = 'arduino-serial-model'; - - @inject(LocalStorageService) - protected readonly localStorageService: LocalStorageService; - - @inject(BoardsServiceProvider) - protected readonly boardsServiceClient: BoardsServiceProvider; - - protected readonly onChangeEmitter: Emitter< - SerialModel.State.Change - >; - protected _autoscroll: boolean; - protected _timestamp: boolean; - protected _baudRate: SerialConfig.BaudRate; - protected _lineEnding: SerialModel.EOL; - protected _interpolate: boolean; - - constructor() { - this._autoscroll = true; - this._timestamp = false; - this._baudRate = SerialConfig.BaudRate.DEFAULT; - this._lineEnding = SerialModel.EOL.DEFAULT; - this._interpolate = false; - this.onChangeEmitter = new Emitter< - SerialModel.State.Change - >(); - } - - onStart(): void { - this.localStorageService - .getData(SerialModel.STORAGE_ID) - .then((state) => { - if (state) { - this.restoreState(state); - } - }); - } - - get onChange(): Event> { - return this.onChangeEmitter.event; - } - - get autoscroll(): boolean { - return this._autoscroll; - } - - toggleAutoscroll(): void { - this._autoscroll = !this._autoscroll; - this.storeState(); - this.storeState().then(() => - this.onChangeEmitter.fire({ - property: 'autoscroll', - value: this._autoscroll, - }) - ); - } - - get timestamp(): boolean { - return this._timestamp; - } - - toggleTimestamp(): void { - this._timestamp = !this._timestamp; - this.storeState().then(() => - this.onChangeEmitter.fire({ - property: 'timestamp', - value: this._timestamp, - }) - ); - } - - get baudRate(): SerialConfig.BaudRate { - return this._baudRate; - } - - set baudRate(baudRate: SerialConfig.BaudRate) { - this._baudRate = baudRate; - this.storeState().then(() => - this.onChangeEmitter.fire({ - property: 'baudRate', - value: this._baudRate, - }) - ); - } - - get lineEnding(): SerialModel.EOL { - return this._lineEnding; - } - - set lineEnding(lineEnding: SerialModel.EOL) { - this._lineEnding = lineEnding; - this.storeState().then(() => - this.onChangeEmitter.fire({ - property: 'lineEnding', - value: this._lineEnding, - }) - ); - } - - get interpolate(): boolean { - return this._interpolate; - } - - set interpolate(i: boolean) { - this._interpolate = i; - this.storeState().then(() => - this.onChangeEmitter.fire({ - property: 'interpolate', - value: this._interpolate, - }) - ); - } - - protected restoreState(state: SerialModel.State): void { - this._autoscroll = state.autoscroll; - this._timestamp = state.timestamp; - this._baudRate = state.baudRate; - this._lineEnding = state.lineEnding; - this._interpolate = state.interpolate; - } - - protected async storeState(): Promise { - return this.localStorageService.setData(SerialModel.STORAGE_ID, { - autoscroll: this._autoscroll, - timestamp: this._timestamp, - baudRate: this._baudRate, - lineEnding: this._lineEnding, - interpolate: this._interpolate, - }); - } -} - -export namespace SerialModel { - export interface State { - autoscroll: boolean; - timestamp: boolean; - baudRate: SerialConfig.BaudRate; - lineEnding: EOL; - interpolate: boolean; - } - export namespace State { - export interface Change { - readonly property: K; - readonly value: State[K]; - } - } - - export type EOL = '' | '\n' | '\r' | '\r\n'; - export namespace EOL { - export const DEFAULT: EOL = '\n'; - } -} diff --git a/arduino-ide-extension/src/browser/serial/serial-service-client-impl.ts b/arduino-ide-extension/src/browser/serial/serial-service-client-impl.ts deleted file mode 100644 index 5a025fcf..00000000 --- a/arduino-ide-extension/src/browser/serial/serial-service-client-impl.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { injectable } from 'inversify'; -import { Emitter } from '@theia/core/lib/common/event'; -import { - SerialServiceClient, - SerialError, - SerialConfig, -} from '../../common/protocol/serial-service'; -import { SerialModel } from './serial-model'; - -@injectable() -export class SerialServiceClientImpl implements SerialServiceClient { - protected readonly onErrorEmitter = new Emitter(); - readonly onError = this.onErrorEmitter.event; - - protected readonly onWebSocketChangedEmitter = new Emitter(); - readonly onWebSocketChanged = this.onWebSocketChangedEmitter.event; - - protected readonly onBaudRateChangedEmitter = - new Emitter(); - readonly onBaudRateChanged = this.onBaudRateChangedEmitter.event; - - protected readonly onLineEndingChangedEmitter = - new Emitter(); - readonly onLineEndingChanged = this.onLineEndingChangedEmitter.event; - - protected readonly onInterpolateChangedEmitter = new Emitter(); - readonly onInterpolateChanged = this.onInterpolateChangedEmitter.event; - - notifyError(error: SerialError): void { - this.onErrorEmitter.fire(error); - } - - notifyWebSocketChanged(message: number): void { - this.onWebSocketChangedEmitter.fire(message); - } - - notifyBaudRateChanged(message: SerialConfig.BaudRate): void { - this.onBaudRateChangedEmitter.fire(message); - } - - notifyLineEndingChanged(message: SerialModel.EOL): void { - this.onLineEndingChangedEmitter.fire(message); - } - - notifyInterpolateChanged(message: boolean): void { - this.onInterpolateChangedEmitter.fire(message); - } -} diff --git a/arduino-ide-extension/src/common/protocol/serial-service.ts b/arduino-ide-extension/src/common/protocol/serial-service.ts deleted file mode 100644 index 0e77bb9c..00000000 --- a/arduino-ide-extension/src/common/protocol/serial-service.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory'; -import { Board, Port } from './boards-service'; -import { Event } from '@theia/core/lib/common/event'; -import { SerialPlotter } from '../../browser/serial/plotter/protocol'; -import { SerialModel } from '../../browser/serial/serial-model'; - -export interface Status {} -export type OK = Status; -export interface ErrorStatus extends Status { - readonly message: string; -} -export namespace Status { - export function isOK(status: Status & { message?: string }): status is OK { - return !!status && typeof status.message !== 'string'; - } - export const OK: OK = {}; - export const NOT_CONNECTED: ErrorStatus = { message: 'Not connected.' }; - export const ALREADY_CONNECTED: ErrorStatus = { - message: 'Already connected.', - }; - export const CONFIG_MISSING: ErrorStatus = { - message: 'Serial Config missing.', - }; -} - -export const SerialServicePath = '/services/serial'; -export const SerialService = Symbol('SerialService'); -export interface SerialService extends JsonRpcServer { - clientsAttached(): Promise; - setSerialConfig(config: SerialConfig): Promise; - sendMessageToSerial(message: string): Promise; - updateWsConfigParam(config: Partial): Promise; - isSerialPortOpen(): Promise; - connectSerialIfRequired(): Promise; - disconnect(reason?: SerialError): Promise; - uploadInProgress: boolean; -} - -export interface SerialConfig { - readonly board: Board; - readonly port: Port; - /** - * Defaults to [`SERIAL`](MonitorConfig#ConnectionType#SERIAL). - */ - readonly type?: SerialConfig.ConnectionType; - /** - * Defaults to `9600`. - */ - readonly baudRate?: SerialConfig.BaudRate; -} -export namespace SerialConfig { - export const BaudRates = [ - 300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, - ] as const; - export type BaudRate = typeof SerialConfig.BaudRates[number]; - export namespace BaudRate { - export const DEFAULT: BaudRate = 9600; - } - - export enum ConnectionType { - SERIAL = 0, - } -} - -export const SerialServiceClient = Symbol('SerialServiceClient'); -export interface SerialServiceClient { - onError: Event; - onWebSocketChanged: Event; - onLineEndingChanged: Event; - onBaudRateChanged: Event; - onInterpolateChanged: Event; - notifyError(event: SerialError): void; - notifyWebSocketChanged(message: number): void; - notifyLineEndingChanged(message: SerialModel.EOL): void; - notifyBaudRateChanged(message: SerialConfig.BaudRate): void; - notifyInterpolateChanged(message: boolean): void; -} - -export interface SerialError { - readonly message: string; - /** - * If no `code` is available, clients must reestablish the serial connection. - */ - readonly code: number | undefined; - readonly config: SerialConfig; -} -export namespace SerialError { - export namespace ErrorCodes { - /** - * The frontend has refreshed the browser, for instance. - */ - export const CLIENT_CANCEL = 1; - /** - * When detaching a physical device when the duplex channel is still opened. - */ - export const DEVICE_NOT_CONFIGURED = 2; - /** - * Another serial connection was opened on this port. For another electron-instance, Java IDE. - */ - export const DEVICE_BUSY = 3; - } -} diff --git a/arduino-ide-extension/src/node/serial/monitor-client-provider.ts b/arduino-ide-extension/src/node/serial/monitor-client-provider.ts deleted file mode 100644 index d73c98cf..00000000 --- a/arduino-ide-extension/src/node/serial/monitor-client-provider.ts +++ /dev/null @@ -1,26 +0,0 @@ -import * as grpc from '@grpc/grpc-js'; -import { injectable } from 'inversify'; -import { MonitorServiceClient } from '../cli-protocol/cc/arduino/cli/monitor/v1/monitor_grpc_pb'; -import * as monitorGrpcPb from '../cli-protocol/cc/arduino/cli/monitor/v1/monitor_grpc_pb'; -import { GrpcClientProvider } from '../grpc-client-provider'; - -@injectable() -export class MonitorClientProvider extends GrpcClientProvider { - createClient(port: string | number): MonitorServiceClient { - // https://github.com/agreatfool/grpc_tools_node_protoc_ts/blob/master/doc/grpcjs_support.md#usage - const MonitorServiceClient = grpc.makeClientConstructor( - // @ts-expect-error: ignore - monitorGrpcPb['cc.arduino.cli.monitor.v1.MonitorService'], - 'MonitorServiceService' - ) as any; - return new MonitorServiceClient( - `localhost:${port}`, - grpc.credentials.createInsecure(), - this.channelOptions - ); - } - - close(client: MonitorServiceClient): void { - client.close(); - } -} diff --git a/arduino-ide-extension/src/node/serial/serial-service-impl.ts b/arduino-ide-extension/src/node/serial/serial-service-impl.ts deleted file mode 100644 index 5b6475c3..00000000 --- a/arduino-ide-extension/src/node/serial/serial-service-impl.ts +++ /dev/null @@ -1,397 +0,0 @@ -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 { ILogger } from '@theia/core/lib/common/logger'; -import { - SerialService, - SerialServiceClient, - SerialConfig, - SerialError, - Status, -} from '../../common/protocol/serial-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 } from '../../common/protocol/boards-service'; -import { WebSocketProvider } from '../web-socket/web-socket-provider'; -import { SerialPlotter } from '../../browser/serial/plotter/protocol'; -import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol'; - -export const SerialServiceName = 'serial-service'; - -interface ErrorWithCode extends Error { - readonly code: number; -} -namespace ErrorWithCode { - export function toSerialError( - error: Error, - config: SerialConfig - ): SerialError { - const { message } = error; - let code = undefined; - if (is(error)) { - // TODO: const `mapping`. Use regex for the `message`. - const mapping = new Map(); - mapping.set( - '1 CANCELLED: Cancelled on client', - SerialError.ErrorCodes.CLIENT_CANCEL - ); - mapping.set( - '2 UNKNOWN: device not configured', - SerialError.ErrorCodes.DEVICE_NOT_CONFIGURED - ); - mapping.set( - '2 UNKNOWN: error opening serial connection: Serial port busy', - SerialError.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 SerialServiceImpl implements SerialService { - protected theiaFEClient?: SerialServiceClient; - protected serialConfig?: SerialConfig; - - protected serialConnection?: { - duplex: ClientDuplexStream; - config: SerialConfig; - }; - protected messages: string[] = []; - protected onMessageReceived: Disposable | null; - protected onWSClientsNumberChanged: Disposable | null; - - protected flushMessagesInterval: NodeJS.Timeout | null; - - uploadInProgress = false; - - constructor( - @inject(ILogger) - @named(SerialServiceName) - protected readonly logger: ILogger, - - @inject(MonitorClientProvider) - protected readonly serialClientProvider: MonitorClientProvider, - - @inject(WebSocketProvider) - protected readonly webSocketService: WebSocketService - ) { } - - async isSerialPortOpen(): Promise { - return !!this.serialConnection; - } - - setClient(client: SerialServiceClient | undefined): void { - this.theiaFEClient = client; - - this.theiaFEClient?.notifyWebSocketChanged( - this.webSocketService.getAddress().port - ); - - // listen for the number of websocket clients and create or dispose the serial connection - this.onWSClientsNumberChanged = - this.webSocketService.onClientsNumberChanged(async () => { - await this.connectSerialIfRequired(); - }); - } - - public async clientsAttached(): Promise { - return this.webSocketService.getConnectedClientsNumber.bind( - this.webSocketService - )(); - } - - public async connectSerialIfRequired(): Promise { - if (this.uploadInProgress) return; - const clients = await this.clientsAttached(); - clients > 0 ? await this.connect() : await this.disconnect(); - } - - dispose(): void { - this.logger.info('>>> Disposing serial service...'); - if (this.serialConnection) { - this.disconnect(); - } - this.logger.info('<<< Disposed serial service.'); - this.theiaFEClient = undefined; - } - - async setSerialConfig(config: SerialConfig): Promise { - this.serialConfig = config; - await this.disconnect(); - await this.connectSerialIfRequired(); - } - - async updateWsConfigParam( - config: Partial - ): Promise { - const msg: SerialPlotter.Protocol.Message = { - command: SerialPlotter.Protocol.Command.MIDDLEWARE_CONFIG_CHANGED, - data: config, - }; - this.webSocketService.sendMessage(JSON.stringify(msg)); - } - - private async connect(): Promise { - if (!this.serialConfig) { - return Status.CONFIG_MISSING; - } - - this.logger.info( - `>>> Creating serial connection for ${Board.toString( - this.serialConfig.board - )} on port ${this.serialConfig.port.address}...` - ); - - if (this.serialConnection) { - return Status.ALREADY_CONNECTED; - } - const client = await this.serialClientProvider.client(); - if (!client) { - return Status.NOT_CONNECTED; - } - if (client instanceof Error) { - return { message: client.message }; - } - const duplex = client.streamingOpen(); - this.serialConnection = { duplex, config: this.serialConfig }; - - const serialConfig = this.serialConfig; - - duplex.on( - 'error', - ((error: Error) => { - const serialError = ErrorWithCode.toSerialError(error, serialConfig); - if (serialError.code !== SerialError.ErrorCodes.CLIENT_CANCEL) { - this.disconnect(serialError).then(() => { - if (this.theiaFEClient) { - this.theiaFEClient.notifyError(serialError); - } - }); - } - if (serialError.code === undefined) { - // Log the original, unexpected error. - this.logger.error(error); - } - }).bind(this) - ); - - this.updateWsConfigParam({ connected: !!this.serialConnection }); - - const flushMessagesToFrontend = () => { - if (this.messages.length) { - this.webSocketService.sendMessage(JSON.stringify(this.messages)); - this.messages = []; - } - }; - - this.onMessageReceived = this.webSocketService.onMessageReceived( - (msg: string) => { - try { - const message: SerialPlotter.Protocol.Message = JSON.parse(msg); - - switch (message.command) { - case SerialPlotter.Protocol.Command.PLOTTER_SEND_MESSAGE: - this.sendMessageToSerial(message.data); - break; - - case SerialPlotter.Protocol.Command.PLOTTER_SET_BAUDRATE: - this.theiaFEClient?.notifyBaudRateChanged( - parseInt(message.data, 10) as SerialConfig.BaudRate - ); - break; - - case SerialPlotter.Protocol.Command.PLOTTER_SET_LINE_ENDING: - this.theiaFEClient?.notifyLineEndingChanged(message.data); - break; - - case SerialPlotter.Protocol.Command.PLOTTER_SET_INTERPOLATE: - this.theiaFEClient?.notifyInterpolateChanged(message.data); - break; - - default: - break; - } - } catch (error) { } - } - ); - - // empty the queue every 32ms (~30fps) - this.flushMessagesInterval = setInterval(flushMessagesToFrontend, 32); - - 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 } = this.serialConfig; - const req = new StreamingOpenRequest(); - const monitorConfig = new GrpcMonitorConfig(); - monitorConfig.setType(this.mapType(type)); - monitorConfig.setTarget(port.address); - if (this.serialConfig.baudRate !== undefined) { - monitorConfig.setAdditionalConfig( - Struct.fromJavaScript({ BaudRate: this.serialConfig.baudRate }) - ); - } - req.setConfig(monitorConfig); - - if (!this.serialConnection) { - return await this.disconnect(); - } - - const writeTimeout = new Promise((resolve) => { - setTimeout(async () => { - resolve(Status.NOT_CONNECTED); - }, 1000); - }); - - const writePromise = (serialConnection: any) => { - return new Promise((resolve) => { - serialConnection.duplex.write(req, () => { - const boardName = this.serialConfig?.board - ? Board.toString(this.serialConfig.board, { - useFqbn: false, - }) - : 'unknown board'; - - const portName = this.serialConfig?.port - ? this.serialConfig.port.address - : 'unknown port'; - this.logger.info( - `<<< Serial connection created for ${boardName} on port ${portName}.` - ); - resolve(Status.OK); - }); - }); - }; - - const status = await Promise.race([ - writeTimeout, - writePromise(this.serialConnection), - ]); - - if (status === Status.NOT_CONNECTED) { - this.disconnect(); - } - - return status; - } - - public async disconnect(reason?: SerialError): Promise { - return new Promise((resolve) => { - try { - if (this.onMessageReceived) { - this.onMessageReceived.dispose(); - this.onMessageReceived = null; - } - if (this.flushMessagesInterval) { - clearInterval(this.flushMessagesInterval); - this.flushMessagesInterval = null; - } - - if ( - !this.serialConnection && - reason && - reason.code === SerialError.ErrorCodes.CLIENT_CANCEL - ) { - resolve(Status.OK); - return; - } - this.logger.info('>>> Disposing serial connection...'); - if (!this.serialConnection) { - this.logger.warn('<<< Not connected. Nothing to dispose.'); - resolve(Status.NOT_CONNECTED); - return; - } - const { duplex, config } = this.serialConnection; - - this.logger.info( - `<<< Disposed serial connection for ${Board.toString(config.board, { - useFqbn: false, - })} on port ${config.port.address}.` - ); - - duplex.cancel(); - } finally { - this.serialConnection = undefined; - this.updateWsConfigParam({ connected: !!this.serialConnection }); - this.messages.length = 0; - - setTimeout(() => { - resolve(Status.OK); - }, 200); - } - }); - } - - async sendMessageToSerial(message: string): Promise { - if (!this.serialConnection) { - return Status.NOT_CONNECTED; - } - const req = new StreamingOpenRequest(); - req.setData(new TextEncoder().encode(message)); - return new Promise((resolve) => { - if (this.serialConnection) { - this.serialConnection.duplex.write(req, () => { - resolve(Status.OK); - }); - return; - } - this.disconnect().then(() => resolve(Status.NOT_CONNECTED)); - }); - } - - protected mapType( - type?: SerialConfig.ConnectionType - ): GrpcMonitorConfig.TargetType { - switch (type) { - case SerialConfig.ConnectionType.SERIAL: - return GrpcMonitorConfig.TargetType.TARGET_TYPE_SERIAL; - default: - return GrpcMonitorConfig.TargetType.TARGET_TYPE_SERIAL; - } - } -} - -// converts 'ab\nc\nd' => [ab\n,c\n,d] -function 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; -} diff --git a/arduino-ide-extension/src/test/node/serial-service-impl.test.ts b/arduino-ide-extension/src/test/node/serial-service-impl.test.ts deleted file mode 100644 index db77a8b8..00000000 --- a/arduino-ide-extension/src/test/node/serial-service-impl.test.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { SerialServiceImpl } from './../../node/serial/serial-service-impl'; -import { IMock, It, Mock } from 'typemoq'; -import { createSandbox } from 'sinon'; -import * as sinonChai from 'sinon-chai'; -import { expect, use } from 'chai'; -use(sinonChai); - -import { ILogger } from '@theia/core/lib/common/logger'; -import { MonitorClientProvider } from '../../node/serial/monitor-client-provider'; -import { WebSocketProvider } from '../../node/web-socket/web-socket-provider'; -import { MonitorServiceClient } from '../../node/cli-protocol/cc/arduino/cli/monitor/v1/monitor_grpc_pb'; -import { Status } from '../../common/protocol'; - -describe('SerialServiceImpl', () => { - let subject: SerialServiceImpl; - - let logger: IMock; - let serialClientProvider: IMock; - let webSocketService: IMock; - - beforeEach(() => { - logger = Mock.ofType(); - logger.setup((b) => b.info(It.isAnyString())); - logger.setup((b) => b.warn(It.isAnyString())); - logger.setup((b) => b.error(It.isAnyString())); - - serialClientProvider = Mock.ofType(); - webSocketService = Mock.ofType(); - - subject = new SerialServiceImpl( - logger.object, - serialClientProvider.object, - webSocketService.object - ); - }); - - context('when a serial connection is requested', () => { - const sandbox = createSandbox(); - beforeEach(() => { - subject.uploadInProgress = false; - sandbox.spy(subject, 'disconnect'); - sandbox.spy(subject, 'updateWsConfigParam'); - }); - - afterEach(function () { - sandbox.restore(); - }); - - context('and an upload is in progress', () => { - beforeEach(async () => { - subject.uploadInProgress = true; - }); - - it('should not change the connection status', async () => { - await subject.connectSerialIfRequired(); - expect(subject.disconnect).to.have.callCount(0); - }); - }); - - context('and there is no upload in progress', () => { - beforeEach(async () => { - subject.uploadInProgress = false; - }); - - context('and there are 0 attached ws clients', () => { - it('should disconnect', async () => { - await subject.connectSerialIfRequired(); - expect(subject.disconnect).to.have.been.calledOnce; - }); - }); - - context('and there are > 0 attached ws clients', () => { - beforeEach(() => { - webSocketService - .setup((b) => b.getConnectedClientsNumber()) - .returns(() => 1); - }); - - it('should not call the disconenct', async () => { - await subject.connectSerialIfRequired(); - expect(subject.disconnect).to.have.callCount(0); - }); - }); - }); - }); - - context('when a disconnection is requested', () => { - const sandbox = createSandbox(); - beforeEach(() => { }); - - afterEach(function () { - sandbox.restore(); - }); - - context('and a serialConnection is not set', () => { - it('should return a NOT_CONNECTED status', async () => { - const status = await subject.disconnect(); - expect(status).to.be.equal(Status.NOT_CONNECTED); - }); - }); - - context('and a serialConnection is set', async () => { - beforeEach(async () => { - sandbox.spy(subject, 'updateWsConfigParam'); - await subject.disconnect(); - }); - - it('should dispose the serialConnection', async () => { - const serialConnectionOpen = await subject.isSerialPortOpen(); - expect(serialConnectionOpen).to.be.false; - }); - - it('should call updateWsConfigParam with disconnected status', async () => { - expect(subject.updateWsConfigParam).to.be.calledWith({ - connected: false, - }); - }); - }); - }); - - context('when a new config is passed in', () => { - const sandbox = createSandbox(); - beforeEach(async () => { - subject.uploadInProgress = false; - webSocketService - .setup((b) => b.getConnectedClientsNumber()) - .returns(() => 1); - - serialClientProvider - .setup((b) => b.client()) - .returns(async () => { - return { - streamingOpen: () => { - return { - on: (str: string, cb: any) => { }, - write: (chunk: any, cb: any) => { - cb(); - }, - cancel: () => { }, - }; - }, - } as MonitorServiceClient; - }); - - sandbox.spy(subject, 'disconnect'); - - await subject.setSerialConfig({ - board: { name: 'test' }, - port: { id: 'test|test', address: 'test', addressLabel: 'test', protocol: 'test', protocolLabel: 'test' }, - }); - }); - - afterEach(function () { - sandbox.restore(); - subject.dispose(); - }); - - it('should disconnect from previous connection', async () => { - expect(subject.disconnect).to.be.called; - }); - - it('should create the serialConnection', async () => { - const serialConnectionOpen = await subject.isSerialPortOpen(); - expect(serialConnectionOpen).to.be.true; - }); - }); -});