Fix upload and serial (#661)

* get serial connection status from BE

* handle serial connect in the BE

* allow breakpoints on vscode (windows)

* Timeout on config change to prevent serial busy

* serial-service tests
This commit is contained in:
Francesco Stasi 2021-12-07 17:38:43 +01:00 committed by GitHub
parent 88397931c5
commit 767b09d2f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 576 additions and 756 deletions

4
.vscode/launch.json vendored
View File

@ -8,10 +8,6 @@
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd",
"env": {
"NODE_ENV": "development",
"NODE_PRESERVE_SYMLINKS": "1"
}
},
"cwd": "${workspaceFolder}/electron-app",
"protocol": "inspector",

View File

@ -19,12 +19,11 @@
"test:watch": "mocha --watch --watch-files lib \"./lib/test/**/*.test.js\""
},
"dependencies": {
"arduino-serial-plotter-webapp": "0.0.15",
"@grpc/grpc-js": "^1.3.7",
"@theia/application-package": "1.19.0",
"@theia/core": "1.19.0",
"@theia/editor": "1.19.0",
"@theia/editor-preview": "1.19.0",
"@theia/editor-preview": "1.19.0",
"@theia/filesystem": "1.19.0",
"@theia/git": "1.19.0",
"@theia/keymaps": "1.19.0",
@ -53,10 +52,10 @@
"@types/ps-tree": "^1.1.0",
"@types/react-select": "^3.0.0",
"@types/react-tabs": "^2.3.2",
"@types/sinon": "^7.5.2",
"@types/temp": "^0.8.34",
"@types/which": "^1.3.1",
"ajv": "^6.5.3",
"arduino-serial-plotter-webapp": "0.0.15",
"async-mutex": "^0.3.0",
"atob": "^2.1.2",
"auth0-js": "^9.14.0",
@ -97,6 +96,8 @@
"@types/chai-string": "^1.4.2",
"@types/mocha": "^5.2.7",
"@types/react-window": "^1.8.5",
"@types/sinon": "^10.0.6",
"@types/sinon-chai": "^3.2.6",
"chai": "^4.2.0",
"chai-string": "^1.5.0",
"decompress": "^4.2.0",
@ -109,7 +110,8 @@
"moment": "^2.24.0",
"protoc": "^1.0.4",
"shelljs": "^0.8.3",
"sinon": "^9.0.1",
"sinon": "^12.0.1",
"sinon-chai": "^3.7.0",
"typemoq": "^2.1.0",
"uuid": "^3.2.1",
"yargs": "^11.1.0"

View File

@ -48,7 +48,6 @@ export class BurnBootloader extends SketchContribution {
}
async burnBootloader(): Promise<void> {
await this.serialConnection.disconnect();
try {
const { boardsConfig } = this.boardsServiceClientImpl;
const port = boardsConfig.selectedPort;
@ -87,9 +86,7 @@ export class BurnBootloader extends SketchContribution {
}
this.messageService.error(errorMessage);
} finally {
if (this.serialConnection.isSerialOpen()) {
await this.serialConnection.connect();
}
await this.serialConnection.reconnectAfterUpload();
}
}
}

View File

@ -210,7 +210,7 @@ export class UploadSketch extends SketchContribution {
if (!sketch) {
return;
}
await this.serialConnection.disconnect();
try {
const { boardsConfig } = this.boardsServiceClientImpl;
const [fqbn, { selectedProgrammer }, verify, verbose, sourceOverride] =
@ -288,27 +288,7 @@ export class UploadSketch extends SketchContribution {
this.uploadInProgress = false;
this.onDidChangeEmitter.fire();
if (
this.serialConnection.isSerialOpen() &&
this.serialConnection.serialConfig
) {
const { board, port } = this.serialConnection.serialConfig;
try {
await this.boardsServiceClientImpl.waitUntilAvailable(
Object.assign(board, { port }),
10_000
);
await this.serialConnection.connect();
} catch (waitError) {
this.messageService.error(
nls.localize(
'arduino/sketch/couldNotConnectToSerial',
'Could not reconnect to serial port. {0}',
waitError.toString()
)
);
}
}
setTimeout(() => this.serialConnection.reconnectAfterUpload(), 5000);
}
}
}

View File

@ -12,7 +12,7 @@ import {
import { SerialConfig } from '../../../common/protocol/serial-service';
import { ArduinoSelect } from '../../widgets/arduino-select';
import { SerialModel } from '../serial-model';
import { Serial, SerialConnectionManager } from '../serial-connection-manager';
import { SerialConnectionManager } from '../serial-connection-manager';
import { SerialMonitorSendInput } from './serial-monitor-send-input';
import { SerialMonitorOutput } from './serial-monitor-send-output';
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
@ -57,9 +57,7 @@ export class MonitorWidget extends ReactWidget {
this.scrollOptions = undefined;
this.toDispose.push(this.clearOutputEmitter);
this.toDispose.push(
Disposable.create(() =>
this.serialConnection.closeSerial(Serial.Type.Monitor)
)
Disposable.create(() => this.serialConnection.closeWStoBE())
);
}
@ -83,7 +81,7 @@ export class MonitorWidget extends ReactWidget {
protected onAfterAttach(msg: Message): void {
super.onAfterAttach(msg);
this.serialConnection.openSerial(Serial.Type.Monitor);
this.serialConnection.openWSToBE();
}
onCloseRequest(msg: Message): void {
@ -171,7 +169,7 @@ export class MonitorWidget extends ReactWidget {
<div className="head">
<div className="send">
<SerialMonitorSendInput
serialConfig={this.serialConnection.serialConfig}
serialConnection={this.serialConnection}
resolveFocus={this.onFocusResolved}
onSend={this.onSend}
/>

View File

@ -1,18 +1,20 @@
import * as React from 'react';
import { Key, KeyCode } from '@theia/core/lib/browser/keys';
import { Board, Port } from '../../../common/protocol/boards-service';
import { SerialConfig } from '../../../common/protocol/serial-service';
import { isOSX } from '@theia/core/lib/common/os';
import { nls } from '@theia/core/lib/common';
import { DisposableCollection, nls } from '@theia/core/lib/common';
import { SerialConnectionManager } from '../serial-connection-manager';
import { SerialPlotter } from '../plotter/protocol';
export namespace SerialMonitorSendInput {
export interface Props {
readonly serialConfig?: SerialConfig;
readonly serialConnection: SerialConnectionManager;
readonly onSend: (text: string) => void;
readonly resolveFocus: (element: HTMLElement | undefined) => void;
}
export interface State {
text: string;
connected: boolean;
}
}
@ -20,20 +22,45 @@ export class SerialMonitorSendInput extends React.Component<
SerialMonitorSendInput.Props,
SerialMonitorSendInput.State
> {
protected toDisposeBeforeUnmount = new DisposableCollection();
constructor(props: Readonly<SerialMonitorSendInput.Props>) {
super(props);
this.state = { text: '' };
this.state = { text: '', connected: false };
this.onChange = this.onChange.bind(this);
this.onSend = this.onSend.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
}
componentDidMount(): void {
this.props.serialConnection.isBESerialConnected().then((connected) => {
this.setState({ connected });
});
this.toDisposeBeforeUnmount.pushAll([
this.props.serialConnection.onRead(({ messages }) => {
if (
messages.command ===
SerialPlotter.Protocol.Command.MIDDLEWARE_CONFIG_CHANGED &&
'connected' in messages.data
) {
this.setState({ connected: messages.data.connected });
}
}),
]);
}
componentWillUnmount(): void {
// TODO: "Your preferred browser's local storage is almost full." Discard `content` before saving layout?
this.toDisposeBeforeUnmount.dispose();
}
render(): React.ReactNode {
return (
<input
ref={this.setRef}
type="text"
className={`theia-input ${this.props.serialConfig ? '' : 'warning'}`}
className={`theia-input ${this.state.connected ? '' : 'warning'}`}
placeholder={this.placeholder}
value={this.state.text}
onChange={this.onChange}
@ -43,8 +70,8 @@ export class SerialMonitorSendInput extends React.Component<
}
protected get placeholder(): string {
const { serialConfig } = this.props;
if (!serialConfig) {
const serialConfig = this.props.serialConnection.getConfig();
if (!this.state.connected || !serialConfig) {
return nls.localize(
'arduino/serial/notConnected',
'Not connected. Select a board and a port to connect automatically.'
@ -55,10 +82,12 @@ export class SerialMonitorSendInput extends React.Component<
'arduino/serial/message',
"Message ({0} + Enter to send message to '{1}' on '{2}'",
isOSX ? '⌘' : nls.localize('vscode/keybindingLabels/ctrlKey', 'Ctrl'),
Board.toString(board, {
useFqbn: false,
}),
Port.toString(port)
board
? Board.toString(board, {
useFqbn: false,
})
: 'unknown',
port ? Port.toString(port) : 'unknown'
);
}

View File

@ -11,8 +11,8 @@ import { ArduinoMenus } from '../../menu/arduino-menus';
import { Contribution } from '../../contributions/contribution';
import { Endpoint, FrontendApplication } from '@theia/core/lib/browser';
import { ipcRenderer } from '@theia/core/shared/electron';
import { SerialConfig, Status } from '../../../common/protocol';
import { Serial, SerialConnectionManager } from '../serial-connection-manager';
import { SerialConfig } from '../../../common/protocol';
import { SerialConnectionManager } from '../serial-connection-manager';
import { SerialPlotter } from './protocol';
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
const queryString = require('query-string');
@ -51,10 +51,8 @@ export class PlotterFrontendContribution extends Contribution {
ipcRenderer.on('CLOSE_CHILD_WINDOW', async () => {
if (!!this.window) {
this.window = null;
await this.serialConnection.closeSerial(Serial.Type.Plotter);
}
});
return super.onStart(app);
}
@ -77,17 +75,15 @@ export class PlotterFrontendContribution extends Contribution {
this.window.focus();
return;
}
const status = await this.serialConnection.openSerial(Serial.Type.Plotter);
const wsPort = this.serialConnection.getWsPort();
if (Status.isOK(status) && wsPort) {
if (wsPort) {
this.open(wsPort);
} else {
this.serialConnection.closeSerial(Serial.Type.Plotter);
this.messageService.error(`Couldn't open serial plotter`);
}
}
protected open(wsPort: number): void {
protected async open(wsPort: number): Promise<void> {
const initConfig: Partial<SerialPlotter.Config> = {
baudrates: SerialConfig.BaudRates.map((b) => b),
currentBaudrate: this.model.baudRate,
@ -95,7 +91,7 @@ export class PlotterFrontendContribution extends Contribution {
darkTheme: this.themeService.getCurrentTheme().type === 'dark',
wsPort,
interpolate: this.model.interpolate,
connected: this.serialConnection.connected,
connected: await this.serialConnection.isBESerialConnected(),
serialPort: this.boardsServiceProvider.boardsConfig.selectedPort?.address,
};
const urlWithParams = queryString.stringifyUrl(

View File

@ -1,5 +1,4 @@
import { injectable, inject } from 'inversify';
import { deepClone } from '@theia/core/lib/common/objects';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { MessageService } from '@theia/core/lib/common/message-service';
import {
@ -23,8 +22,6 @@ import { nls } from '@theia/core/lib/common/nls';
@injectable()
export class SerialConnectionManager {
protected _state: Serial.State = [];
protected _connected = false;
protected config: Partial<SerialConfig> = {
board: undefined,
port: undefined,
@ -62,7 +59,9 @@ export class SerialConnectionManager {
protected readonly boardsServiceProvider: BoardsServiceProvider,
@inject(MessageService) protected messageService: MessageService,
@inject(ThemeService) protected readonly themeService: ThemeService,
@inject(CoreService) protected readonly core: CoreService
@inject(CoreService) protected readonly core: CoreService,
@inject(BoardsServiceProvider)
protected readonly boardsServiceClientImpl: BoardsServiceProvider
) {
this.serialServiceClient.onWebSocketChanged(
this.handleWebSocketChanged.bind(this)
@ -89,8 +88,11 @@ export class SerialConnectionManager {
);
// Handles the `baudRate` changes by reconnecting if required.
this.serialModel.onChange(({ property }) => {
if (property === 'baudRate' && this.connected) {
this.serialModel.onChange(async ({ property }) => {
if (
property === 'baudRate' &&
(await this.serialService.isSerialPortOpen())
) {
const { boardsConfig } = this.boardsServiceProvider;
this.handleBoardConfigChange(boardsConfig);
}
@ -114,8 +116,8 @@ export class SerialConnectionManager {
}
/**
* Set the config passing only the properties that has changed. If some has changed and the serial is open,
* we try to reconnect
* 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
*/
@ -127,17 +129,16 @@ export class SerialConnectionManager {
this.config = { ...this.config, [key]: newConfig[key] };
}
});
if (
configHasChanged &&
this.isSerialOpen() &&
!(await this.core.isUploading())
) {
if (configHasChanged) {
this.serialService.updateWsConfigParam({
currentBaudrate: this.config.baudRate,
serialPort: this.config.port?.address,
});
await this.disconnect();
await this.connect();
if (isSerialConfig(this.config)) {
this.serialService.setSerialConfig(this.config);
}
}
}
@ -149,134 +150,56 @@ export class SerialConnectionManager {
return this.wsPort;
}
isWebSocketConnected(): boolean {
return !!this.webSocket?.url;
}
protected handleWebSocketChanged(wsPort: number): void {
this.wsPort = wsPort;
}
/**
* When the serial is open and the frontend is connected to the serial, we create the websocket here
*/
protected createWsConnection(): boolean {
if (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 });
};
return true;
} catch {
return false;
}
}
return false;
}
/**
* Sets the types of connections needed by the client.
*
* @param newState The array containing the list of desired connections.
* If the previuos state was empty and 'newState' is not, it tries to reconnect to the serial service
* If the provios state was NOT empty and now it is, it disconnects to the serial service
* @returns The status of the operation
*/
protected async setState(newState: Serial.State): Promise<Status> {
const oldState = deepClone(this._state);
let status = Status.OK;
if (this.isSerialOpen(oldState) && !this.isSerialOpen(newState)) {
status = await this.disconnect();
} else if (!this.isSerialOpen(oldState) && this.isSerialOpen(newState)) {
if (await this.core.isUploading()) {
this.messageService.error(`Cannot open serial port when uploading`);
return Status.NOT_CONNECTED;
}
status = await this.connect();
}
this._state = newState;
return status;
}
protected get state(): Serial.State {
return this._state;
}
isSerialOpen(state?: Serial.State): boolean {
return (state ? state : this._state).length > 0;
}
get serialConfig(): SerialConfig | undefined {
return isSerialConfig(this.config)
? (this.config as SerialConfig)
: undefined;
}
get connected(): boolean {
return this._connected;
async isBESerialConnected(): Promise<boolean> {
return await this.serialService.isSerialPortOpen();
}
set connected(c: boolean) {
this._connected = c;
this.serialService.updateWsConfigParam({ connected: c });
this.onConnectionChangedEmitter.fire(this._connected);
}
/**
* Called when a client opens the serial from the GUI
*
* @param type could be either 'Monitor' or 'Plotter'. If it's 'Monitor' we also connect to the websocket and
* listen to the message events
* @returns the status of the operation
*/
async openSerial(type: Serial.Type): Promise<Status> {
openWSToBE(): void {
if (!isSerialConfig(this.config)) {
this.messageService.error(
`Please select a board and a port to open the serial connection.`
);
return Status.NOT_CONNECTED;
}
if (this.state.includes(type)) return Status.OK;
const newState = deepClone(this.state);
newState.push(type);
const status = await this.setState(newState);
if (Status.isOK(status) && type === Serial.Type.Monitor)
this.createWsConnection();
return status;
}
/**
* Called when a client closes the serial from the GUI
*
* @param type could be either 'Monitor' or 'Plotter'. If it's 'Monitor' we close the websocket connection
* @returns the status of the operation
*/
async closeSerial(type: Serial.Type): Promise<Status> {
const index = this.state.indexOf(type);
let status = Status.OK;
if (index >= 0) {
const newState = deepClone(this.state);
newState.splice(index, 1);
status = await this.setState(newState);
if (
Status.isOK(status) &&
type === Serial.Type.Monitor &&
this.webSocket
) {
this.webSocket.close();
this.webSocket = undefined;
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`);
}
}
return status;
}
/**
* Handles error on the SerialServiceClient and try to reconnect, eventually
*/
handleError(error: SerialError): void {
if (!this.connected) return;
async handleError(error: SerialError): Promise<void> {
if (!(await this.serialService.isSerialPortOpen())) return;
const { code, config } = error;
const { board, port } = config;
const options = { timeout: 3000 };
@ -329,9 +252,8 @@ export class SerialConnectionManager {
break;
}
}
this.connected = false;
if (this.isSerialOpen()) {
if ((await this.serialService.clientsAttached()) > 0) {
if (this.serialErrors.length >= 10) {
this.messageService.warn(
nls.localize(
@ -363,59 +285,31 @@ export class SerialConnectionManager {
)
);
this.reconnectTimeout = window.setTimeout(
() => this.connect(),
() => this.reconnectAfterUpload(),
timeout
);
}
}
}
async connect(): Promise<Status> {
if (this.connected) return Status.ALREADY_CONNECTED;
if (!isSerialConfig(this.config)) return Status.NOT_CONNECTED;
console.info(
`>>> Creating serial connection for ${Board.toString(
this.config.board
)} on port ${Port.toString(this.config.port)}...`
);
const connectStatus = await this.serialService.connect(this.config);
if (Status.isOK(connectStatus)) {
this.connected = true;
console.info(
`<<< Serial connection created for ${Board.toString(this.config.board, {
useFqbn: false,
})} on port ${Port.toString(this.config.port)}.`
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()
)
);
}
return Status.isOK(connectStatus);
}
async disconnect(): Promise<Status> {
if (!this.connected) {
return Status.OK;
}
console.log('>>> Disposing existing serial connection...');
const status = await this.serialService.disconnect();
if (Status.isOK(status)) {
this.connected = false;
console.log(
`<<< Disposed serial connection. Was: ${Serial.Config.toString(
this.config
)}`
);
this.wsPort = undefined;
} else {
console.warn(
`<<< Could not dispose serial connection. Activate connection: ${Serial.Config.toString(
this.config
)}`
);
}
return status;
}
/**
@ -424,7 +318,7 @@ export class SerialConnectionManager {
* It is a NOOP if connected.
*/
async send(data: string): Promise<Status> {
if (!this.connected) {
if (!(await this.serialService.isSerialPortOpen())) {
return Status.NOT_CONNECTED;
}
return new Promise<Status>((resolve) => {
@ -438,7 +332,7 @@ export class SerialConnectionManager {
return this.onConnectionChangedEmitter.event;
}
get onRead(): Event<{ messages: string[] }> {
get onRead(): Event<{ messages: any }> {
return this.onReadEmitter.event;
}
@ -453,18 +347,6 @@ export class SerialConnectionManager {
}
export namespace Serial {
export enum Type {
Monitor = 'Monitor',
Plotter = 'Plotter',
}
/**
* The state represents which types of connections are needed by the client, and it should match whether the Serial Monitor
* or the Serial Plotter are open or not in the GUI. It's an array cause it's possible to have both, none or only one of
* them open
*/
export type State = Serial.Type[];
export namespace Config {
export function toString(config: Partial<SerialConfig>): string {
if (!isSerialConfig(config)) return '';

View File

@ -18,15 +18,22 @@ export namespace Status {
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> {
connect(config: SerialConfig): Promise<Status>;
disconnect(): Promise<Status>;
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 {

View File

@ -108,7 +108,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
electronWindow.webContents.on(
'new-window',
(event, url, frameName, disposition, options, additionalFeatures) => {
(event, url, frameName, disposition, options) => {
if (frameName === 'serialPlotter') {
event.preventDefault();
Object.assign(options, {

View File

@ -24,6 +24,7 @@ import { ArduinoCoreServiceClient } from './cli-protocol/cc/arduino/cli/commands
import { firstToUpperCase, firstToLowerCase } from '../common/utils';
import { Port } from './cli-protocol/cc/arduino/cli/commands/v1/port_pb';
import { nls } from '@theia/core';
import { SerialService } from './../common/protocol/serial-service';
@injectable()
export class CoreServiceImpl extends CoreClientAware implements CoreService {
@ -33,6 +34,9 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
@inject(NotificationServiceServer)
protected readonly notificationService: NotificationServiceServer;
@inject(SerialService)
protected readonly serialService: SerialService;
protected uploading = false;
async compile(
@ -132,8 +136,13 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
) => ClientReadableStream<UploadResponse | UploadUsingProgrammerResponse>,
task = 'upload'
): Promise<void> {
this.uploading = true;
await this.compile(Object.assign(options, { exportBinaries: false }));
this.uploading = true;
this.serialService.uploadInProgress = true;
await this.serialService.disconnect();
const { sketchUri, fqbn, port, programmer } = options;
const sketchPath = FileUri.fsPath(sketchUri);
@ -160,7 +169,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
req.setVerbose(options.verbose);
req.setVerify(options.verify);
options.userFields.forEach(e => {
options.userFields.forEach((e) => {
req.getUserFieldsMap().set(e.name, e.value);
});
@ -190,7 +199,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
'arduino/upload/error',
'{0} error: {1}',
firstToUpperCase(task),
e.details,
e.details
);
this.responseService.appendToOutput({
chunk: `${errorMessage}\n`,
@ -199,10 +208,15 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
throw new Error(errorMessage);
} finally {
this.uploading = false;
this.serialService.uploadInProgress = false;
}
}
async burnBootloader(options: CoreService.Bootloader.Options): Promise<void> {
this.uploading = true;
this.serialService.uploadInProgress = true;
await this.serialService.disconnect();
await this.coreClientProvider.initialized;
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
@ -242,13 +256,16 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
const errorMessage = nls.localize(
'arduino/burnBootloader/error',
'Error while burning the bootloader: {0}',
e.details,
e.details
);
this.responseService.appendToOutput({
chunk: `${errorMessage}\n`,
severity: 'error',
});
throw new Error(errorMessage);
} finally {
this.uploading = false;
this.serialService.uploadInProgress = false;
}
}

View File

@ -63,27 +63,61 @@ namespace ErrorWithCode {
@injectable()
export class SerialServiceImpl implements SerialService {
@named(SerialServiceName)
@inject(ILogger)
protected readonly logger: ILogger;
protected theiaFEClient?: SerialServiceClient;
protected serialConfig?: SerialConfig;
@inject(MonitorClientProvider)
protected readonly serialClientProvider: MonitorClientProvider;
@inject(WebSocketService)
protected readonly webSocketService: WebSocketService;
protected client?: SerialServiceClient;
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(WebSocketService)
protected readonly webSocketService: WebSocketService
) {}
async isSerialPortOpen(): Promise<boolean> {
return !!this.serialConnection;
}
setClient(client: SerialServiceClient | undefined): void {
this.client = client;
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 {
@ -92,7 +126,13 @@ export class SerialServiceImpl implements SerialService {
this.disconnect();
}
this.logger.info('<<< Disposed serial service.');
this.client = undefined;
this.theiaFEClient = undefined;
}
async setSerialConfig(config: SerialConfig): Promise<void> {
this.serialConfig = config;
await this.disconnect();
await this.connectSerialIfRequired();
}
async updateWsConfigParam(
@ -105,12 +145,17 @@ export class SerialServiceImpl implements SerialService {
this.webSocketService.sendMessage(JSON.stringify(msg));
}
async connect(config: SerialConfig): Promise<Status> {
private async connect(): Promise<Status> {
if (!this.serialConfig) {
return Status.CONFIG_MISSING;
}
this.logger.info(
`>>> Creating serial connection for ${Board.toString(
config.board
)} on port ${Port.toString(config.port)}...`
this.serialConfig.board
)} on port ${Port.toString(this.serialConfig.port)}...`
);
if (this.serialConnection) {
return Status.ALREADY_CONNECTED;
}
@ -122,27 +167,29 @@ export class SerialServiceImpl implements SerialService {
return { message: client.message };
}
const duplex = client.streamingOpen();
this.serialConnection = { duplex, config };
this.serialConnection = { duplex, config: this.serialConfig };
const serialConfig = this.serialConfig;
duplex.on(
'error',
((error: Error) => {
const serialError = ErrorWithCode.toSerialError(error, config);
this.disconnect(serialError).then(() => {
if (this.client) {
this.client.notifyError(serialError);
}
if (serialError.code === undefined) {
// Log the original, unexpected error.
this.logger.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.client?.notifyWebSocketChanged(
this.webSocketService.getAddress().port
);
this.updateWsConfigParam({ connected: !!this.serialConnection });
const flushMessagesToFrontend = () => {
if (this.messages.length) {
@ -162,17 +209,17 @@ export class SerialServiceImpl implements SerialService {
break;
case SerialPlotter.Protocol.Command.PLOTTER_SET_BAUDRATE:
this.client?.notifyBaudRateChanged(
this.theiaFEClient?.notifyBaudRateChanged(
parseInt(message.data, 10) as SerialConfig.BaudRate
);
break;
case SerialPlotter.Protocol.Command.PLOTTER_SET_LINE_ENDING:
this.client?.notifyLineEndingChanged(message.data);
this.theiaFEClient?.notifyLineEndingChanged(message.data);
break;
case SerialPlotter.Protocol.Command.PLOTTER_SET_INTERPOLATE:
this.client?.notifyInterpolateChanged(message.data);
this.theiaFEClient?.notifyInterpolateChanged(message.data);
break;
default:
@ -185,27 +232,6 @@ export class SerialServiceImpl implements SerialService {
// empty the queue every 32ms (~30fps)
this.flushMessagesInterval = setInterval(flushMessagesToFrontend, 32);
// converts 'ab\nc\nd' => [ab\n,c\n,d]
const stringToArray = (string: string, separator = '\n') => {
const retArray: string[] = [];
let prevChar = separator;
for (let i = 0; i < string.length; i++) {
const currChar = string[i];
if (prevChar === separator) {
retArray.push(currChar);
} else {
const lastWord = retArray[retArray.length - 1];
retArray[retArray.length - 1] = lastWord + currChar;
}
prevChar = currChar;
}
return retArray;
};
duplex.on(
'data',
((resp: StreamingOpenResponse) => {
@ -219,69 +245,105 @@ export class SerialServiceImpl implements SerialService {
}).bind(this)
);
const { type, port } = config;
const { type, port } = this.serialConfig;
const req = new StreamingOpenRequest();
const monitorConfig = new GrpcMonitorConfig();
monitorConfig.setType(this.mapType(type));
monitorConfig.setTarget(port.address);
if (config.baudRate !== undefined) {
if (this.serialConfig.baudRate !== undefined) {
monitorConfig.setAdditionalConfig(
Struct.fromJavaScript({ BaudRate: config.baudRate })
Struct.fromJavaScript({ BaudRate: this.serialConfig.baudRate })
);
}
req.setConfig(monitorConfig);
return new Promise<Status>((resolve) => {
if (this.serialConnection) {
this.serialConnection.duplex.write(req, () => {
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
? Port.toString(this.serialConfig.port)
: 'unknown port';
this.logger.info(
`<<< Serial connection created for ${Board.toString(config.board, {
useFqbn: false,
})} on port ${Port.toString(config.port)}.`
`<<< Serial connection created for ${boardName} on port ${portName}.`
);
resolve(Status.OK);
});
return;
}
this.disconnect().then(() => resolve(Status.NOT_CONNECTED));
});
});
};
const status = await Promise.race([
writeTimeout,
writePromise(this.serialConnection),
]);
if (status === Status.NOT_CONNECTED) {
this.disconnect();
}
return status;
}
async disconnect(reason?: SerialError): Promise<Status> {
try {
if (this.onMessageReceived) {
this.onMessageReceived.dispose();
this.onMessageReceived = null;
}
if (this.flushMessagesInterval) {
clearInterval(this.flushMessagesInterval);
this.flushMessagesInterval = null;
}
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
) {
return Status.OK;
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 ${Port.toString(config.port)}.`
);
duplex.cancel();
} finally {
this.serialConnection = undefined;
this.updateWsConfigParam({ connected: !!this.serialConnection });
this.messages.length = 0;
setTimeout(() => {
resolve(Status.OK);
}, 200);
}
this.logger.info('>>> Disposing serial connection...');
if (!this.serialConnection) {
this.logger.warn('<<< Not connected. Nothing to dispose.');
return Status.NOT_CONNECTED;
}
const { duplex, config } = this.serialConnection;
duplex.cancel();
this.logger.info(
`<<< Disposed serial connection for ${Board.toString(config.board, {
useFqbn: false,
})} on port ${Port.toString(config.port)}.`
);
this.serialConnection = undefined;
return Status.OK;
} finally {
this.messages.length = 0;
}
});
}
async sendMessageToSerial(message: string): Promise<Status> {
@ -312,3 +374,24 @@ export class SerialServiceImpl implements SerialService {
}
}
}
// 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;
}

View File

@ -11,6 +11,9 @@ export default class WebSocketServiceImpl implements WebSocketService {
protected readonly onMessage = new Emitter<string>();
public readonly onMessageReceived = this.onMessage.event;
protected readonly onConnectedClients = new Emitter<number>();
public readonly onClientsNumberChanged = this.onConnectedClients.event;
constructor() {
this.wsClients = [];
this.server = new WebSocket.Server({ port: 0 });
@ -21,8 +24,11 @@ export default class WebSocketServiceImpl implements WebSocketService {
private addClient(ws: WebSocket): void {
this.wsClients.push(ws);
this.onConnectedClients.fire(this.wsClients.length);
ws.onclose = () => {
this.wsClients.splice(this.wsClients.indexOf(ws), 1);
this.onConnectedClients.fire(this.wsClients.length);
};
ws.onmessage = (res) => {
@ -30,6 +36,10 @@ export default class WebSocketServiceImpl implements WebSocketService {
};
}
getConnectedClientsNumber(): number {
return this.wsClients.length;
}
getAddress(): WebSocket.AddressInfo {
return this.server.address() as WebSocket.AddressInfo;
}

View File

@ -6,4 +6,6 @@ export interface WebSocketService {
getAddress(): WebSocket.AddressInfo;
sendMessage(message: string): void;
onMessageReceived: Event<string>;
onClientsNumberChanged: Event<number>;
getConnectedClientsNumber(): number;
}

View File

@ -1,375 +0,0 @@
import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
const disableJSDOM = enableJSDOM();
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
import { ApplicationProps } from '@theia/application-package/lib/application-props';
FrontendApplicationConfigProvider.set({
...ApplicationProps.DEFAULT.frontend.config,
});
import { MessageService } from '@theia/core';
import { BoardsServiceProvider } from '../../browser/boards/boards-service-provider';
import {
BoardsService,
CoreService,
SerialService,
SerialServiceClient,
Status,
} from '../../common/protocol';
import { IMock, It, Mock, Times } from 'typemoq';
import {
Serial,
SerialConnectionManager,
} from '../../browser/serial/serial-connection-manager';
import { ThemeService } from '@theia/core/lib/browser/theming';
import { SerialModel } from '../../browser/serial/serial-model';
import {
aBoardConfig,
anotherBoardConfig,
anotherPort,
aPort,
} from './fixtures/boards';
import { BoardsConfig } from '../../browser/boards/boards-config';
import {
anotherSerialConfig,
aSerialConfig,
WebSocketMock,
} from './fixtures/serial';
import { expect } from 'chai';
import { tick } from '../utils';
disableJSDOM();
global.WebSocket = WebSocketMock as any;
describe.only('SerialConnectionManager', () => {
let subject: SerialConnectionManager;
let serialModel: IMock<SerialModel>;
let serialService: IMock<SerialService>;
let serialServiceClient: IMock<SerialServiceClient>;
let boardsService: IMock<BoardsService>;
let boardsServiceProvider: IMock<BoardsServiceProvider>;
let messageService: IMock<MessageService>;
let themeService: IMock<ThemeService>;
let core: IMock<CoreService>;
let handleBoardConfigChange: (
boardsConfig: BoardsConfig.Config
) => Promise<void>;
let handleWebSocketChanged: (wsPort: number) => void;
const wsPort = 1234;
beforeEach(() => {
serialModel = Mock.ofType<SerialModel>();
serialService = Mock.ofType<SerialService>();
serialServiceClient = Mock.ofType<SerialServiceClient>();
boardsService = Mock.ofType<BoardsService>();
boardsServiceProvider = Mock.ofType<BoardsServiceProvider>();
messageService = Mock.ofType<MessageService>();
themeService = Mock.ofType<ThemeService>();
core = Mock.ofType<CoreService>();
boardsServiceProvider
.setup((b) => b.boardsConfig)
.returns(() => aBoardConfig);
boardsServiceProvider
.setup((b) => b.onBoardsConfigChanged(It.isAny()))
.returns((h) => {
handleBoardConfigChange = h;
return { dispose: () => {} };
});
boardsServiceProvider
.setup((b) => b.canUploadTo(It.isAny(), It.isValue({ silent: false })))
.returns(() => true);
boardsService
.setup((b) => b.getAvailablePorts())
.returns(() => Promise.resolve([aPort, anotherPort]));
serialModel
.setup((m) => m.baudRate)
.returns(() => aSerialConfig.baudRate || 9600);
serialServiceClient
.setup((m) => m.onWebSocketChanged(It.isAny()))
.returns((h) => {
handleWebSocketChanged = h;
return { dispose: () => {} };
});
serialService
.setup((m) => m.disconnect())
.returns(() => Promise.resolve(Status.OK));
core.setup((u) => u.isUploading()).returns(() => Promise.resolve(false));
subject = new SerialConnectionManager(
serialModel.object,
serialService.object,
serialServiceClient.object,
boardsService.object,
boardsServiceProvider.object,
messageService.object,
themeService.object,
core.object
);
});
context('when no serial config is set', () => {
context('and the serial is NOT open', () => {
context('and it tries to open the serial plotter', () => {
it('should not try to connect and show an error', async () => {
await subject.openSerial(Serial.Type.Plotter);
messageService.verify((m) => m.error(It.isAnyString()), Times.once());
serialService.verify((m) => m.disconnect(), Times.never());
serialService.verify((m) => m.connect(It.isAny()), Times.never());
});
});
context('and a serial config is set', () => {
it('should not try to reconnect', async () => {
await handleBoardConfigChange(aBoardConfig);
serialService.verify((m) => m.disconnect(), Times.never());
serialService.verify((m) => m.connect(It.isAny()), Times.never());
expect(subject.getConfig()).to.deep.equal(aSerialConfig);
});
});
});
});
context('when a serial config is set', () => {
beforeEach(() => {
subject.setConfig(aSerialConfig);
});
context('and the serial is NOT open', () => {
context('and it tries to disconnect', () => {
it('should do nothing', async () => {
const status = await subject.disconnect();
expect(status).to.be.ok;
expect(subject.connected).to.be.false;
});
});
context('and the config changes', () => {
beforeEach(() => {
subject.setConfig(anotherSerialConfig);
});
it('should not try to reconnect', async () => {
await tick();
messageService.verify(
(m) => m.error(It.isAnyString()),
Times.never()
);
serialService.verify((m) => m.disconnect(), Times.never());
serialService.verify((m) => m.connect(It.isAny()), Times.never());
});
});
context(
'and the connection to the serial succeeds with the config',
() => {
beforeEach(() => {
serialService
.setup((m) => m.connect(It.isValue(aSerialConfig)))
.returns(() => {
handleWebSocketChanged(wsPort);
return Promise.resolve(Status.OK);
});
});
context('and it tries to open the serial plotter', () => {
let status: Status;
beforeEach(async () => {
status = await subject.openSerial(Serial.Type.Plotter);
});
it('should successfully connect to the serial', async () => {
messageService.verify(
(m) => m.error(It.isAnyString()),
Times.never()
);
serialService.verify((m) => m.disconnect(), Times.never());
serialService.verify((m) => m.connect(It.isAny()), Times.once());
expect(status).to.be.ok;
expect(subject.connected).to.be.true;
expect(subject.getWsPort()).to.equal(wsPort);
expect(subject.isSerialOpen()).to.be.true;
expect(subject.isWebSocketConnected()).to.be.false;
});
context('and it tries to open the serial monitor', () => {
let status: Status;
beforeEach(async () => {
status = await subject.openSerial(Serial.Type.Monitor);
});
it('should open it using the same serial connection', () => {
messageService.verify(
(m) => m.error(It.isAnyString()),
Times.never()
);
serialService.verify((m) => m.disconnect(), Times.never());
serialService.verify(
(m) => m.connect(It.isAny()),
Times.once()
);
expect(status).to.be.ok;
expect(subject.connected).to.be.true;
expect(subject.isSerialOpen()).to.be.true;
});
it('should create a websocket connection', () => {
expect(subject.getWsPort()).to.equal(wsPort);
expect(subject.isWebSocketConnected()).to.be.true;
});
context('and then it closes the serial plotter', () => {
beforeEach(async () => {
status = await subject.closeSerial(Serial.Type.Plotter);
});
it('should close the plotter without disconnecting from the serial', () => {
messageService.verify(
(m) => m.error(It.isAnyString()),
Times.never()
);
serialService.verify((m) => m.disconnect(), Times.never());
serialService.verify(
(m) => m.connect(It.isAny()),
Times.once()
);
expect(status).to.be.ok;
expect(subject.connected).to.be.true;
expect(subject.isSerialOpen()).to.be.true;
expect(subject.getWsPort()).to.equal(wsPort);
});
it('should not close the websocket connection', () => {
expect(subject.isWebSocketConnected()).to.be.true;
});
});
context('and then it closes the serial monitor', () => {
beforeEach(async () => {
status = await subject.closeSerial(Serial.Type.Monitor);
});
it('should close the monitor without disconnecting from the serial', () => {
messageService.verify(
(m) => m.error(It.isAnyString()),
Times.never()
);
serialService.verify((m) => m.disconnect(), Times.never());
serialService.verify(
(m) => m.connect(It.isAny()),
Times.once()
);
expect(status).to.be.ok;
expect(subject.connected).to.be.true;
expect(subject.getWsPort()).to.equal(wsPort);
expect(subject.isSerialOpen()).to.be.true;
});
it('should close the websocket connection', () => {
expect(subject.isWebSocketConnected()).to.be.false;
});
});
});
context('and then it closes the serial plotter', () => {
beforeEach(async () => {
status = await subject.closeSerial(Serial.Type.Plotter);
});
it('should successfully disconnect from the serial', () => {
messageService.verify(
(m) => m.error(It.isAnyString()),
Times.never()
);
serialService.verify((m) => m.disconnect(), Times.once());
serialService.verify(
(m) => m.connect(It.isAny()),
Times.once()
);
expect(status).to.be.ok;
expect(subject.connected).to.be.false;
expect(subject.getWsPort()).to.be.undefined;
expect(subject.isSerialOpen()).to.be.false;
expect(subject.isWebSocketConnected()).to.be.false;
});
});
context('and the config changes', () => {
beforeEach(() => {
subject.setConfig(anotherSerialConfig);
});
it('should try to reconnect', async () => {
await tick();
messageService.verify(
(m) => m.error(It.isAnyString()),
Times.never()
);
serialService.verify((m) => m.disconnect(), Times.once());
serialService.verify(
(m) => m.connect(It.isAny()),
Times.exactly(2)
);
});
});
});
}
);
context(
'and the connection to the serial does NOT succeed with the config',
() => {
beforeEach(() => {
serialService
.setup((m) => m.connect(It.isValue(aSerialConfig)))
.returns(() => {
return Promise.resolve(Status.NOT_CONNECTED);
});
serialService
.setup((m) => m.connect(It.isValue(anotherSerialConfig)))
.returns(() => {
handleWebSocketChanged(wsPort);
return Promise.resolve(Status.OK);
});
});
context('and it tries to open the serial plotter', () => {
let status: Status;
beforeEach(async () => {
status = await subject.openSerial(Serial.Type.Plotter);
});
it('should fail to connect to the serial', async () => {
messageService.verify(
(m) => m.error(It.isAnyString()),
Times.never()
);
serialService.verify((m) => m.disconnect(), Times.never());
serialService.verify(
(m) => m.connect(It.isValue(aSerialConfig)),
Times.once()
);
expect(status).to.be.false;
expect(subject.connected).to.be.false;
expect(subject.getWsPort()).to.be.undefined;
expect(subject.isSerialOpen()).to.be.true;
});
context(
'and the board config changes with an acceptable one',
() => {
beforeEach(async () => {
await handleBoardConfigChange(anotherBoardConfig);
});
it('should successfully connect to the serial', async () => {
await tick();
messageService.verify(
(m) => m.error(It.isAnyString()),
Times.never()
);
serialService.verify((m) => m.disconnect(), Times.never());
serialService.verify(
(m) => m.connect(It.isValue(anotherSerialConfig)),
Times.once()
);
expect(subject.connected).to.be.true;
expect(subject.getWsPort()).to.equal(wsPort);
expect(subject.isSerialOpen()).to.be.true;
expect(subject.isWebSocketConnected()).to.be.false;
});
}
);
});
}
);
});
});
});

View File

@ -0,0 +1,167 @@
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 { WebSocketService } from '../../node/web-socket/web-socket-service';
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<WebSocketService>;
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<WebSocketService>();
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: { address: 'test', protocol: '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;
});
});
});

View File

@ -181,12 +181,12 @@
"uploadUsingProgrammer": "Upload Using Programmer",
"userFieldsNotFoundError": "Can't find user fields for connected board",
"doneUploading": "Done uploading.",
"couldNotConnectToSerial": "Could not reconnect to serial port. {0}",
"configureAndUpload": "Configure And Upload",
"verifyOrCompile": "Verify/Compile",
"exportBinary": "Export Compiled Binary",
"verify": "Verify",
"doneCompiling": "Done compiling.",
"couldNotConnectToSerial": "Could not reconnect to serial port. {0}",
"openSketchInNewWindow": "Open Sketch in New Window",
"openFolder": "Open Folder",
"titleLocalSketchbook": "Local Sketchbook",

View File

@ -43,7 +43,7 @@
"test": "lerna run test",
"download:plugins": "theia download:plugins",
"update:version": "node ./scripts/update-version.js",
"i18n:generate": "theia nls-extract -e vscode -f '+(arduino-ide-extension|browser-app|electron|electron-app|plugins)/**/*.ts?(x)' -o ./i18n/en.json",
"i18n:generate": "theia nls-extract -e vscode -f \"+(arduino-ide-extension|browser-app|electron|electron-app|plugins)/**/*.ts?(x)\" -o ./i18n/en.json",
"i18n:check": "yarn i18n:generate && git add -N ./i18n && git diff --exit-code ./i18n",
"i18n:push": "node ./scripts/i18n/transifex-push.js ./i18n/en.json",
"i18n:pull": "node ./scripts/i18n/transifex-pull.js ./i18n/"

View File

@ -2064,24 +2064,38 @@
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==
"@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.8.1":
"@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0":
version "1.8.2"
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.2.tgz#858f5c4b48d80778fde4b9d541f27edc0d56488b"
integrity sha512-sruwd86RJHdsVf/AtBoijDmUqJp3B6hF/DGC23C+JaegnDHaZyewCjoVGTdg3J0uz3Zs7NnIT05OBOmML72lQw==
dependencies:
type-detect "4.0.8"
"@sinonjs/fake-timers@^6.0.0", "@sinonjs/fake-timers@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40"
integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==
"@sinonjs/commons@^1.8.3":
version "1.8.3"
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d"
integrity sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==
dependencies:
type-detect "4.0.8"
"@sinonjs/fake-timers@^7.0.4", "@sinonjs/fake-timers@^7.1.0":
version "7.1.2"
resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz#2524eae70c4910edccf99b2f4e6efc5894aff7b5"
integrity sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==
dependencies:
"@sinonjs/commons" "^1.7.0"
"@sinonjs/samsam@^5.3.1":
version "5.3.1"
resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-5.3.1.tgz#375a45fe6ed4e92fca2fb920e007c48232a6507f"
integrity sha512-1Hc0b1TtyfBu8ixF/tpfSHTVWKwCBLY4QJbkgnE7HcwyvT2xArDxb4K7dMgqRm3szI+LJbzmW/s4xxEhv6hwDg==
"@sinonjs/fake-timers@^8.1.0":
version "8.1.0"
resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz#3fdc2b6cb58935b21bfb8d1625eb1300484316e7"
integrity sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==
dependencies:
"@sinonjs/commons" "^1.7.0"
"@sinonjs/samsam@^6.0.2":
version "6.0.2"
resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-6.0.2.tgz#a0117d823260f282c04bff5f8704bdc2ac6910bb"
integrity sha512-jxPRPp9n93ci7b8hMfJOFDPRLFYadN6FSpeROFTR4UNF4i5b+EK6m4QXPO46BDhFgRy1JuS87zAnFOzCUwMJcQ==
dependencies:
"@sinonjs/commons" "^1.6.0"
lodash.get "^4.4.2"
@ -3246,16 +3260,26 @@
"@types/mime" "^1"
"@types/node" "*"
"@types/sinon-chai@^3.2.6":
version "3.2.6"
resolved "https://registry.yarnpkg.com/@types/sinon-chai/-/sinon-chai-3.2.6.tgz#3504a744e2108646394766fb1339f52ea5d6bd0f"
integrity sha512-Z57LprQ+yOQNu9d6mWdHNvnmncPXzDWGSeLj+8L075/QahToapC4Q13zAFRVKV4clyBmdJ5gz4xBfVkOso5lXw==
dependencies:
"@types/chai" "*"
"@types/sinon" "*"
"@types/sinon@*", "@types/sinon@^10.0.6":
version "10.0.6"
resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.6.tgz#bc3faff5154e6ecb69b797d311b7cf0c1b523a1d"
integrity sha512-6EF+wzMWvBNeGrfP3Nx60hhx+FfwSg1JJBLAAP/IdIUq0EYkqCYf70VT3PhuhPX9eLD+Dp+lNdpb/ZeHG8Yezg==
dependencies:
"@sinonjs/fake-timers" "^7.1.0"
"@types/sinon@^2.3.5":
version "2.3.7"
resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-2.3.7.tgz#e92c2fed3297eae078d78d1da032b26788b4af86"
integrity sha512-w+LjztaZbgZWgt/y/VMP5BUAWLtSyoIJhXyW279hehLPyubDoBNwvhcj3WaSptcekuKYeTCVxrq60rdLc6ImJA==
"@types/sinon@^7.5.2":
version "7.5.2"
resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.5.2.tgz#5e2f1d120f07b9cda07e5dedd4f3bf8888fccdb9"
integrity sha512-T+m89VdXj/eidZyejvmoP9jivXgBDdkOSBVQjU9kF349NEx10QdPNGxHeZUaj1IlJ32/ewdyXJjnJxyxJroYwg==
"@types/tar-fs@^1.16.1":
version "1.16.3"
resolved "https://registry.yarnpkg.com/@types/tar-fs/-/tar-fs-1.16.3.tgz#425b2b817c405d13d051f36ec6ec6ebd25e31069"
@ -6075,10 +6099,10 @@ diff@3.5.0, diff@^3.4.0:
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==
diff@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
diff@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b"
integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==
dir-glob@^2.0.0, dir-glob@^2.2.2:
version "2.2.2"
@ -10214,13 +10238,13 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
nise@^4.0.4:
version "4.1.0"
resolved "https://registry.yarnpkg.com/nise/-/nise-4.1.0.tgz#8fb75a26e90b99202fa1e63f448f58efbcdedaf6"
integrity sha512-eQMEmGN/8arp0xsvGoQ+B1qvSkR73B1nWSCh7nOt5neMCtwcQVYQGdzQMhcNscktTsWB54xnlSQFzOAPJD8nXA==
nise@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.0.tgz#713ef3ed138252daef20ec035ab62b7a28be645c"
integrity sha512-W5WlHu+wvo3PaKLsJJkgPup2LrsXCcm7AWwyNZkUnn5rwPkuPBi3Iwk5SQtN0mv+K65k7nKKjwNQ30wg3wLAQQ==
dependencies:
"@sinonjs/commons" "^1.7.0"
"@sinonjs/fake-timers" "^6.0.0"
"@sinonjs/fake-timers" "^7.0.4"
"@sinonjs/text-encoding" "^0.7.1"
just-extend "^4.0.2"
path-to-regexp "^1.7.0"
@ -12881,17 +12905,22 @@ simple-get@^3.0.3:
once "^1.3.1"
simple-concat "^1.0.0"
sinon@^9.0.1:
version "9.2.4"
resolved "https://registry.yarnpkg.com/sinon/-/sinon-9.2.4.tgz#e55af4d3b174a4443a8762fa8421c2976683752b"
integrity sha512-zljcULZQsJxVra28qIAL6ow1Z9tpattkCTEJR4RBP3TGc00FcttsP5pK284Nas5WjMZU5Yzy3kAIp3B3KRf5Yg==
sinon-chai@^3.7.0:
version "3.7.0"
resolved "https://registry.yarnpkg.com/sinon-chai/-/sinon-chai-3.7.0.tgz#cfb7dec1c50990ed18c153f1840721cf13139783"
integrity sha512-mf5NURdUaSdnatJx3uhoBOrY9dtL19fiOtAdT1Azxg3+lNJFiuN0uzaU3xX1LeAfL17kHQhTAJgpsfhbMJMY2g==
sinon@^12.0.1:
version "12.0.1"
resolved "https://registry.yarnpkg.com/sinon/-/sinon-12.0.1.tgz#331eef87298752e1b88a662b699f98e403c859e9"
integrity sha512-iGu29Xhym33ydkAT+aNQFBINakjq69kKO6ByPvTsm3yyIACfyQttRTP03aBP/I8GfhFmLzrnKwNNkr0ORb1udg==
dependencies:
"@sinonjs/commons" "^1.8.1"
"@sinonjs/fake-timers" "^6.0.1"
"@sinonjs/samsam" "^5.3.1"
diff "^4.0.2"
nise "^4.0.4"
supports-color "^7.1.0"
"@sinonjs/commons" "^1.8.3"
"@sinonjs/fake-timers" "^8.1.0"
"@sinonjs/samsam" "^6.0.2"
diff "^5.0.0"
nise "^5.1.0"
supports-color "^7.2.0"
slash@^1.0.0:
version "1.0.0"
@ -13511,7 +13540,7 @@ supports-color@^5.3.0, supports-color@^5.4.0:
dependencies:
has-flag "^3.0.0"
supports-color@^7.1.0:
supports-color@^7.1.0, supports-color@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==