mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-07-30 14:46:34 +00:00
Remove several unnecessary serial monitor classes
This commit is contained in:
parent
31b704cdb9
commit
9058abb015
@ -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<SerialConfig> = {
|
|
||||||
board: undefined,
|
|
||||||
port: undefined,
|
|
||||||
baudRate: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
protected readonly onConnectionChangedEmitter = new Emitter<boolean>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<SerialConfig>): Promise<void> {
|
|
||||||
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<SerialConfig> {
|
|
||||||
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<boolean> {
|
|
||||||
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<void> {
|
|
||||||
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<void> {
|
|
||||||
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<Status> {
|
|
||||||
if (!(await this.serialService.isSerialPortOpen())) {
|
|
||||||
return Status.NOT_CONNECTED;
|
|
||||||
}
|
|
||||||
return new Promise<Status>((resolve) => {
|
|
||||||
this.serialService
|
|
||||||
.sendMessageToSerial(data + this.serialModel.lineEnding)
|
|
||||||
.then(() => resolve(Status.OK));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get onConnectionChanged(): Event<boolean> {
|
|
||||||
return this.onConnectionChangedEmitter.event;
|
|
||||||
}
|
|
||||||
|
|
||||||
get onRead(): Event<{ messages: any }> {
|
|
||||||
return this.onReadEmitter.event;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async handleBoardConfigChange(
|
|
||||||
boardsConfig: BoardsConfig.Config
|
|
||||||
): Promise<void> {
|
|
||||||
const { selectedBoard: board, selectedPort: port } = boardsConfig;
|
|
||||||
const { baudRate } = this.serialModel;
|
|
||||||
const newConfig: Partial<SerialConfig> = { board, port, baudRate };
|
|
||||||
this.setConfig(newConfig);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export namespace Serial {
|
|
||||||
export namespace Config {
|
|
||||||
export function toString(config: Partial<SerialConfig>): string {
|
|
||||||
if (!isSerialConfig(config)) return '';
|
|
||||||
const { board, port } = config;
|
|
||||||
return `${Board.toString(board)} ${port.address}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSerialConfig(config: Partial<SerialConfig>): config is SerialConfig {
|
|
||||||
return !!config.board && !!config.baudRate && !!config.port;
|
|
||||||
}
|
|
@ -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<keyof SerialModel.State>
|
|
||||||
>;
|
|
||||||
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<keyof SerialModel.State>
|
|
||||||
>();
|
|
||||||
}
|
|
||||||
|
|
||||||
onStart(): void {
|
|
||||||
this.localStorageService
|
|
||||||
.getData<SerialModel.State>(SerialModel.STORAGE_ID)
|
|
||||||
.then((state) => {
|
|
||||||
if (state) {
|
|
||||||
this.restoreState(state);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get onChange(): Event<SerialModel.State.Change<keyof SerialModel.State>> {
|
|
||||||
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<void> {
|
|
||||||
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<K extends keyof State> {
|
|
||||||
readonly property: K;
|
|
||||||
readonly value: State[K];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EOL = '' | '\n' | '\r' | '\r\n';
|
|
||||||
export namespace EOL {
|
|
||||||
export const DEFAULT: EOL = '\n';
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<SerialError>();
|
|
||||||
readonly onError = this.onErrorEmitter.event;
|
|
||||||
|
|
||||||
protected readonly onWebSocketChangedEmitter = new Emitter<number>();
|
|
||||||
readonly onWebSocketChanged = this.onWebSocketChangedEmitter.event;
|
|
||||||
|
|
||||||
protected readonly onBaudRateChangedEmitter =
|
|
||||||
new Emitter<SerialConfig.BaudRate>();
|
|
||||||
readonly onBaudRateChanged = this.onBaudRateChangedEmitter.event;
|
|
||||||
|
|
||||||
protected readonly onLineEndingChangedEmitter =
|
|
||||||
new Emitter<SerialModel.EOL>();
|
|
||||||
readonly onLineEndingChanged = this.onLineEndingChangedEmitter.event;
|
|
||||||
|
|
||||||
protected readonly onInterpolateChangedEmitter = new Emitter<boolean>();
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<SerialServiceClient> {
|
|
||||||
clientsAttached(): Promise<number>;
|
|
||||||
setSerialConfig(config: SerialConfig): Promise<void>;
|
|
||||||
sendMessageToSerial(message: string): Promise<Status>;
|
|
||||||
updateWsConfigParam(config: Partial<SerialPlotter.Config>): Promise<void>;
|
|
||||||
isSerialPortOpen(): Promise<boolean>;
|
|
||||||
connectSerialIfRequired(): Promise<void>;
|
|
||||||
disconnect(reason?: SerialError): Promise<Status>;
|
|
||||||
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<SerialError>;
|
|
||||||
onWebSocketChanged: Event<number>;
|
|
||||||
onLineEndingChanged: Event<SerialModel.EOL>;
|
|
||||||
onBaudRateChanged: Event<SerialConfig.BaudRate>;
|
|
||||||
onInterpolateChanged: Event<boolean>;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<MonitorServiceClient> {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<string, number>();
|
|
||||||
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<StreamingOpenRequest, StreamingOpenResponse>;
|
|
||||||
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<boolean> {
|
|
||||||
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<number> {
|
|
||||||
return this.webSocketService.getConnectedClientsNumber.bind(
|
|
||||||
this.webSocketService
|
|
||||||
)();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async connectSerialIfRequired(): Promise<void> {
|
|
||||||
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<void> {
|
|
||||||
this.serialConfig = config;
|
|
||||||
await this.disconnect();
|
|
||||||
await this.connectSerialIfRequired();
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateWsConfigParam(
|
|
||||||
config: Partial<SerialPlotter.Config>
|
|
||||||
): Promise<void> {
|
|
||||||
const msg: SerialPlotter.Protocol.Message = {
|
|
||||||
command: SerialPlotter.Protocol.Command.MIDDLEWARE_CONFIG_CHANGED,
|
|
||||||
data: config,
|
|
||||||
};
|
|
||||||
this.webSocketService.sendMessage(JSON.stringify(msg));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async connect(): Promise<Status> {
|
|
||||||
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<Status>((resolve) => {
|
|
||||||
setTimeout(async () => {
|
|
||||||
resolve(Status.NOT_CONNECTED);
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
const writePromise = (serialConnection: any) => {
|
|
||||||
return new Promise<Status>((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<Status> {
|
|
||||||
return new Promise<Status>((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<Status> {
|
|
||||||
if (!this.serialConnection) {
|
|
||||||
return Status.NOT_CONNECTED;
|
|
||||||
}
|
|
||||||
const req = new StreamingOpenRequest();
|
|
||||||
req.setData(new TextEncoder().encode(message));
|
|
||||||
return new Promise<Status>((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;
|
|
||||||
}
|
|
@ -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<ILogger>;
|
|
||||||
let serialClientProvider: IMock<MonitorClientProvider>;
|
|
||||||
let webSocketService: IMock<WebSocketProvider>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
logger = Mock.ofType<ILogger>();
|
|
||||||
logger.setup((b) => b.info(It.isAnyString()));
|
|
||||||
logger.setup((b) => b.warn(It.isAnyString()));
|
|
||||||
logger.setup((b) => b.error(It.isAnyString()));
|
|
||||||
|
|
||||||
serialClientProvider = Mock.ofType<MonitorClientProvider>();
|
|
||||||
webSocketService = Mock.ofType<WebSocketProvider>();
|
|
||||||
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
x
Reference in New Issue
Block a user