mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-04-28 17:27:19 +00:00
361 lines
11 KiB
TypeScript
361 lines
11 KiB
TypeScript
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;
|
|
}
|