Skip to content
6 changes: 5 additions & 1 deletion arduino-ide-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"test:watch": "mocha --watch --watch-files lib \"./lib/test/**/*.test.js\""
},
"dependencies": {
"@grpc/grpc-js": "^1.1.1",
"@grpc/grpc-js": "^1.3.7",
"@theia/application-package": "next",
"@theia/core": "next",
"@theia/editor": "next",
Expand Down Expand Up @@ -64,6 +64,7 @@
"fuzzy": "^0.1.3",
"glob": "^7.1.6",
"google-protobuf": "^3.11.4",
"grpc": "^1.24.11",
"hash.js": "^1.1.7",
"is-valid-path": "^0.1.1",
"js-yaml": "^3.13.1",
Expand All @@ -78,6 +79,7 @@
"react-disable": "^0.1.0",
"react-select": "^3.0.4",
"react-tabs": "^3.1.2",
"react-window": "^1.8.6",
"semver": "^7.3.2",
"string-natural-compare": "^2.0.3",
"temp": "^0.9.1",
Expand All @@ -89,6 +91,7 @@
"@types/chai": "^4.2.7",
"@types/chai-string": "^1.4.2",
"@types/mocha": "^5.2.7",
"@types/react-window": "^1.8.5",
"chai": "^4.2.0",
"chai-string": "^1.5.0",
"decompress": "^4.2.0",
Expand All @@ -97,6 +100,7 @@
"download": "^7.1.0",
"grpc_tools_node_protoc_ts": "^4.1.0",
"mocha": "^7.0.0",
"mockdate": "^3.0.5",
"moment": "^2.24.0",
"protoc": "^1.0.4",
"shelljs": "^0.8.3",
Expand Down
27 changes: 14 additions & 13 deletions arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,27 +397,28 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
createWidget: () => context.container.get(MonitorWidget),
}));
// Frontend binding for the serial monitor service
// bind(MonitorServiceClient)
// .toDynamicValue((context) => {
// const client = context.container.get(MonitorServiceClientImpl);
// WebSocketConnectionProvider.createProxy(
// context.container,
// MonitorServicePath,
// client
// );
// return client;
// })
// .inSingletonScope();
bind(MonitorService)
.toDynamicValue((context) => {
const connection = context.container.get(WebSocketConnectionProvider);
const client = context.container.get(MonitorServiceClientImpl);
const client =
context.container.get<MonitorServiceClient>(MonitorServiceClient);
return connection.createProxy(MonitorServicePath, client);
})
.inSingletonScope();
bind(MonitorConnection).toSelf().inSingletonScope();
// Serial monitor service client to receive and delegate notifications from the backend.
bind(MonitorServiceClientImpl).toSelf().inSingletonScope();
bind(MonitorServiceClient)
.toDynamicValue((context) => {
const client = context.container.get(MonitorServiceClientImpl);
WebSocketConnectionProvider.createProxy(
context.container,
MonitorServicePath,
client
);
return client;
})
.inSingletonScope();
bind(MonitorServiceClient).to(MonitorServiceClientImpl).inSingletonScope();

bind(WorkspaceService).toSelf().inSingletonScope();
rebind(TheiaWorkspaceService).toService(WorkspaceService);
Expand Down
243 changes: 132 additions & 111 deletions arduino-ide-extension/src/browser/monitor/monitor-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
MonitorConfig,
MonitorError,
Status,
MonitorServiceClient,
} from '../../common/protocol/monitor-service';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import {
Expand All @@ -16,7 +17,6 @@ import {
BoardsService,
AttachedBoardsChangeEvent,
} from '../../common/protocol/boards-service';
import { MonitorServiceClientImpl } from './monitor-service-client-impl';
import { BoardsConfig } from '../boards/boards-config';
import { MonitorModel } from './monitor-model';
import { NotificationCenter } from '../notification-center';
Expand All @@ -29,8 +29,8 @@ export class MonitorConnection {
@inject(MonitorService)
protected readonly monitorService: MonitorService;

@inject(MonitorServiceClientImpl)
protected readonly monitorServiceClient: MonitorServiceClientImpl;
@inject(MonitorServiceClient)
protected readonly monitorServiceClient: MonitorServiceClient;

@inject(BoardsService)
protected readonly boardsService: BoardsService;
Expand Down Expand Up @@ -59,7 +59,7 @@ export class MonitorConnection {
/**
* This emitter forwards all read events **iff** the connection is established.
*/
protected readonly onReadEmitter = new Emitter<{ message: string }>();
protected readonly onReadEmitter = new Emitter<{ messages: string[] }>();

/**
* Array for storing previous monitor errors received from the server, and based on the number of elements in this array,
Expand All @@ -71,112 +71,15 @@ export class MonitorConnection {

@postConstruct()
protected init(): void {
this.monitorServiceClient.onError(async (error) => {
let shouldReconnect = false;
if (this.state) {
const { code, config } = error;
const { board, port } = config;
const options = { timeout: 3000 };
switch (code) {
case MonitorError.ErrorCodes.CLIENT_CANCEL: {
console.debug(
`Connection was canceled by client: ${MonitorConnection.State.toString(
this.state
)}.`
);
break;
}
case MonitorError.ErrorCodes.DEVICE_BUSY: {
this.messageService.warn(
`Connection failed. Serial port is busy: ${Port.toString(port)}.`,
options
);
shouldReconnect = this.autoConnect;
this.monitorErrors.push(error);
break;
}
case MonitorError.ErrorCodes.DEVICE_NOT_CONFIGURED: {
this.messageService.info(
`Disconnected ${Board.toString(board, {
useFqbn: false,
})} from ${Port.toString(port)}.`,
options
);
break;
}
case undefined: {
this.messageService.error(
`Unexpected error. Reconnecting ${Board.toString(
board
)} on port ${Port.toString(port)}.`,
options
);
console.error(JSON.stringify(error));
shouldReconnect = this.connected && this.autoConnect;
break;
}
}
const oldState = this.state;
this.state = undefined;
this.onConnectionChangedEmitter.fire(this.state);
if (shouldReconnect) {
if (this.monitorErrors.length >= 10) {
this.messageService.warn(
`Failed to reconnect ${Board.toString(board, {
useFqbn: false,
})} to the the serial-monitor after 10 consecutive attempts. The ${Port.toString(
port
)} serial port is busy. after 10 consecutive attempts.`
);
this.monitorErrors.length = 0;
} else {
const attempts = this.monitorErrors.length || 1;
if (this.reconnectTimeout !== undefined) {
// Clear the previous timer.
window.clearTimeout(this.reconnectTimeout);
}
const timeout = attempts * 1000;
this.messageService.warn(
`Reconnecting ${Board.toString(board, {
useFqbn: false,
})} to ${Port.toString(port)} in ${attempts} seconds...`,
{ timeout }
);
this.reconnectTimeout = window.setTimeout(
() => this.connect(oldState.config),
timeout
);
}
}
}
});
this.monitorServiceClient.onMessage(this.handleMessage.bind(this));
this.monitorServiceClient.onError(this.handleError.bind(this));
this.boardsServiceProvider.onBoardsConfigChanged(
this.handleBoardConfigChange.bind(this)
);
this.notificationCenter.onAttachedBoardsChanged((event) => {
if (this.autoConnect && this.connected) {
const { boardsConfig } = this.boardsServiceProvider;
if (
this.boardsServiceProvider.canUploadTo(boardsConfig, {
silent: false,
})
) {
const { attached } = AttachedBoardsChangeEvent.diff(event);
if (
attached.boards.some(
(board) =>
!!board.port && BoardsConfig.Config.sameAs(boardsConfig, board)
)
) {
const { selectedBoard: board, selectedPort: port } = boardsConfig;
const { baudRate } = this.monitorModel;
this.disconnect().then(() =>
this.connect({ board, port, baudRate })
);
}
}
}
});
this.notificationCenter.onAttachedBoardsChanged(
this.handleAttachedBoardsChanged.bind(this)
);

// Handles the `baudRate` changes by reconnecting if required.
this.monitorModel.onChange(({ property }) => {
if (property === 'baudRate' && this.autoConnect && this.connected) {
Expand All @@ -186,6 +89,14 @@ export class MonitorConnection {
});
}

async handleMessage(port: string): Promise<void> {
const w = new WebSocket(`ws://localhost:${port}`);
w.onmessage = (res) => {
const messages = JSON.parse(res.data);
this.onReadEmitter.fire({ messages });
};
}

get connected(): boolean {
return !!this.state;
}
Expand Down Expand Up @@ -217,6 +128,109 @@ export class MonitorConnection {
}
}

handleError(error: MonitorError): void {
let shouldReconnect = false;
if (this.state) {
const { code, config } = error;
const { board, port } = config;
const options = { timeout: 3000 };
switch (code) {
case MonitorError.ErrorCodes.CLIENT_CANCEL: {
console.debug(
`Connection was canceled by client: ${MonitorConnection.State.toString(
this.state
)}.`
);
break;
}
case MonitorError.ErrorCodes.DEVICE_BUSY: {
this.messageService.warn(
`Connection failed. Serial port is busy: ${Port.toString(port)}.`,
options
);
shouldReconnect = this.autoConnect;
this.monitorErrors.push(error);
break;
}
case MonitorError.ErrorCodes.DEVICE_NOT_CONFIGURED: {
this.messageService.info(
`Disconnected ${Board.toString(board, {
useFqbn: false,
})} from ${Port.toString(port)}.`,
options
);
break;
}
case undefined: {
this.messageService.error(
`Unexpected error. Reconnecting ${Board.toString(
board
)} on port ${Port.toString(port)}.`,
options
);
console.error(JSON.stringify(error));
shouldReconnect = this.connected && this.autoConnect;
break;
}
}
const oldState = this.state;
this.state = undefined;
this.onConnectionChangedEmitter.fire(this.state);
if (shouldReconnect) {
if (this.monitorErrors.length >= 10) {
this.messageService.warn(
`Failed to reconnect ${Board.toString(board, {
useFqbn: false,
})} to the the serial-monitor after 10 consecutive attempts. The ${Port.toString(
port
)} serial port is busy. after 10 consecutive attempts.`
);
this.monitorErrors.length = 0;
} else {
const attempts = this.monitorErrors.length || 1;
if (this.reconnectTimeout !== undefined) {
// Clear the previous timer.
window.clearTimeout(this.reconnectTimeout);
}
const timeout = attempts * 1000;
this.messageService.warn(
`Reconnecting ${Board.toString(board, {
useFqbn: false,
})} to ${Port.toString(port)} in ${attempts} seconds...`,
{ timeout }
);
this.reconnectTimeout = window.setTimeout(
() => this.connect(oldState.config),
timeout
);
}
}
}
}

handleAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
if (this.autoConnect && this.connected) {
const { boardsConfig } = this.boardsServiceProvider;
if (
this.boardsServiceProvider.canUploadTo(boardsConfig, {
silent: false,
})
) {
const { attached } = AttachedBoardsChangeEvent.diff(event);
if (
attached.boards.some(
(board) =>
!!board.port && BoardsConfig.Config.sameAs(boardsConfig, board)
)
) {
const { selectedBoard: board, selectedPort: port } = boardsConfig;
const { baudRate } = this.monitorModel;
this.disconnect().then(() => this.connect({ board, port, baudRate }));
}
}
}
}

async connect(config: MonitorConfig): Promise<Status> {
if (this.connected) {
const disconnectStatus = await this.disconnect();
Expand All @@ -231,15 +245,22 @@ export class MonitorConnection {
);
const connectStatus = await this.monitorService.connect(config);
if (Status.isOK(connectStatus)) {
let j = 0;
const requestMessage = () => {
this.monitorService.request().then(({ message }) => {
this.monitorService.request().then(({ messages }) => {
if (this.connected) {
this.onReadEmitter.fire({ message });
// this.onReadEmitter.fire({ messages });
j += messages.length;
if (j > 1000) {
j = 0;
// console.log(`read more than 1000 messages`);
}

requestMessage();
}
});
};
requestMessage();
// requestMessage();
this.state = { config };
console.info(
`<<< Serial monitor connection created for ${Board.toString(
Expand Down Expand Up @@ -300,7 +321,7 @@ export class MonitorConnection {
return this.onConnectionChangedEmitter.event;
}

get onRead(): Event<{ message: string }> {
get onRead(): Event<{ messages: string[] }> {
return this.onReadEmitter.event;
}

Expand Down
Loading