mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-04-19 12:57:17 +00:00
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:
parent
88397931c5
commit
767b09d2f1
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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'
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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 '';
|
||||
|
@ -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 {
|
||||
|
@ -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, {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -6,4 +6,6 @@ export interface WebSocketService {
|
||||
getAddress(): WebSocket.AddressInfo;
|
||||
sendMessage(message: string): void;
|
||||
onMessageReceived: Event<string>;
|
||||
onClientsNumberChanged: Event<number>;
|
||||
getConnectedClientsNumber(): number;
|
||||
}
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
167
arduino-ide-extension/src/test/node/serial-service-impl.test.ts
Normal file
167
arduino-ide-extension/src/test/node/serial-service-impl.test.ts
Normal 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;
|
||||
});
|
||||
});
|
||||
});
|
@ -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",
|
||||
|
@ -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/"
|
||||
|
97
yarn.lock
97
yarn.lock
@ -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==
|
||||
|
Loading…
x
Reference in New Issue
Block a user