Compare commits

...

35 Commits

Author SHA1 Message Date
David Simpson
9a16cf9e02 change MonitorService providers to injectable deps 2022-05-20 17:53:34 +02:00
Francesco Stasi
a4ff05a82b monitor service provider singleton 2022-05-19 16:56:37 +02:00
Alberto Iannaccone
0427759fdb refactor monitor settings interfaces 2022-05-19 11:19:13 +02:00
Francesco Stasi
80ade4c37e updated pseudo code 2022-05-19 10:59:20 +02:00
Francesco Stasi
355dec8aaa monitor settings provider stub 2022-05-19 10:40:01 +02:00
Alberto Iannaccone
7bf4ea0637 add MonitorSettingsProvider interface 2022-05-17 17:45:47 +02:00
Alberto Iannaccone
1982609c87 fix upload when monitor is open 2022-05-12 15:28:13 +02:00
Alberto Iannaccone
62eaeb1c74 update arduino-cli to 0.22.0 2022-05-10 15:54:04 +02:00
Alberto Iannaccone
9b58c9d0c8 delete duplex when connection is closed 2022-05-10 15:53:48 +02:00
Alberto Iannaccone
eff960bb7f fix monitor connection 2022-05-09 14:56:52 +02:00
Mark Sujew
fbe8fb421a Fix MonitorManagerProxy DI issue 2022-05-09 14:56:52 +02:00
Silvano Cerza
a8d803e7c3 Add missing binding 2022-05-09 14:56:52 +02:00
Silvano Cerza
397ca5665f coreClientProvider is now set when constructing MonitorService 2022-05-09 14:56:52 +02:00
Silvano Cerza
f9da9fc24b Delete unnecessary Symbol 2022-05-09 14:56:52 +02:00
Silvano Cerza
b97af32bb8 Fix backend logger bindings 2022-05-09 14:56:52 +02:00
Silvano Cerza
ce2f1c227a Updated MonitorWidget to use new monitor proxy 2022-05-09 14:56:52 +02:00
Silvano Cerza
7889f40834 Changed MonitorWidget and children to use new monitor proxy 2022-05-09 14:56:52 +02:00
Silvano Cerza
6b7b33356d Changed plotter contribution to use new manager proxy 2022-05-09 14:56:52 +02:00
Silvano Cerza
ad781f0bfc Remove unused file 2022-05-09 14:56:52 +02:00
Silvano Cerza
6cf61c498a More serial classes removal 2022-05-09 14:56:52 +02:00
Silvano Cerza
50239c5756 Add generic monitor settings storaging 2022-05-09 14:56:52 +02:00
Silvano Cerza
cbd5b4de1b WebSocketProvider is not injectable anymore 2022-05-09 14:56:52 +02:00
Silvano Cerza
bf958fd8cf Proxied more monitor methods to frontend 2022-05-09 14:56:52 +02:00
Silvano Cerza
ee265aec90 Changed how connection is handled on upload 2022-05-09 14:56:52 +02:00
Silvano Cerza
9058abb015 Remove several unnecessary serial monitor classes 2022-05-09 14:56:52 +02:00
Silvano Cerza
31b704cdb9 Moved settings to MonitorService 2022-05-09 14:56:52 +02:00
Silvano Cerza
61b8bdeec9 Add monitor proxy functions for the frontend 2022-05-09 14:56:52 +02:00
Silvano Cerza
c5695d3a76 Fixed WebSocketChange event signature 2022-05-09 14:56:52 +02:00
Silvano Cerza
480492a7c8 Enhance MonitorManager APIs 2022-05-09 14:56:52 +02:00
Silvano Cerza
2c95e7f033 Changed upload settings 2022-05-09 14:56:52 +02:00
Silvano Cerza
116b3d5984 Moved some interfaces 2022-05-09 14:56:52 +02:00
Silvano Cerza
750796d3a0 Rename WebSocketService to WebSocketProvider and uninjected it 2022-05-09 14:56:52 +02:00
Silvano Cerza
3133b01c4a Implement MonitorService to handle pluggable monitor lifetime 2022-05-09 14:56:52 +02:00
Silvano Cerza
ebab0b226f Scaffold interfaces and classes for pluggable monitors 2022-05-09 14:56:52 +02:00
Francesco Stasi
2b2ea72643 backend structure WIP 2022-05-09 14:56:52 +02:00
36 changed files with 1502 additions and 1389 deletions

View File

@@ -157,7 +157,7 @@
],
"arduino": {
"cli": {
"version": "0.21.0"
"version": "0.22.0"
},
"fwuploader": {
"version": "2.0.0"

View File

@@ -69,20 +69,12 @@ import { ScmContribution } from './theia/scm/scm-contribution';
import { SearchInWorkspaceFrontendContribution as TheiaSearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
import { SearchInWorkspaceFrontendContribution } from './theia/search-in-workspace/search-in-workspace-frontend-contribution';
import { LibraryListWidgetFrontendContribution } from './library/library-widget-frontend-contribution';
import { SerialServiceClientImpl } from './serial/serial-service-client-impl';
import {
SerialServicePath,
SerialService,
SerialServiceClient,
} from '../common/protocol/serial-service';
import {
ConfigService,
ConfigServicePath,
} from '../common/protocol/config-service';
import { MonitorWidget } from './serial/monitor/monitor-widget';
import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution';
import { SerialConnectionManager } from './serial/serial-connection-manager';
import { SerialModel } from './serial/serial-model';
import { TabBarDecoratorService as TheiaTabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
import { TabBarDecoratorService } from './theia/core/tab-bar-decorator';
import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browser';
@@ -160,7 +152,7 @@ import {
OutputChannelRegistryMainImpl as TheiaOutputChannelRegistryMainImpl,
OutputChannelRegistryMainImpl,
} from './theia/plugin-ext/output-channel-registry-main';
import { ExecutableService, ExecutableServicePath } from '../common/protocol';
import { ExecutableService, ExecutableServicePath, MonitorManagerProxy, MonitorManagerProxyClient, MonitorManagerProxyFactory, MonitorManagerProxyPath } from '../common/protocol';
import { MonacoTextModelService as TheiaMonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
import { MonacoTextModelService } from './theia/monaco/monaco-text-model-service';
import { ResponseServiceImpl } from './response-service-impl';
@@ -275,6 +267,8 @@ import {
IDEUpdaterDialogWidget,
} from './dialogs/ide-updater/ide-updater-dialog';
import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider';
import { MonitorModel } from './monitor-model';
import { MonitorManagerProxyClientImpl } from './monitor-manager-proxy-client-impl';
const ElementQueries = require('css-element-queries/src/ElementQueries');
@@ -407,29 +401,31 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
.inSingletonScope();
// Serial monitor
bind(SerialModel).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(SerialModel);
bind(MonitorWidget).toSelf();
bind(FrontendApplicationContribution).toService(MonitorModel);
bind(MonitorModel).toSelf().inSingletonScope();
bindViewContribution(bind, MonitorViewContribution);
bind(TabBarToolbarContribution).toService(MonitorViewContribution);
bind(WidgetFactory).toDynamicValue((context) => ({
id: MonitorWidget.ID,
createWidget: () => context.container.get(MonitorWidget),
}));
// Frontend binding for the serial service
bind(SerialService)
.toDynamicValue((context) => {
const connection = context.container.get(WebSocketConnectionProvider);
const client = context.container.get<SerialServiceClient>(
SerialServiceClient
createWidget: () => {
return new MonitorWidget(
context.container.get<MonitorModel>(MonitorModel),
context.container.get<MonitorManagerProxyClient>(MonitorManagerProxyClient),
context.container.get<BoardsServiceProvider>(BoardsServiceProvider),
);
return connection.createProxy(SerialServicePath, client);
})
.inSingletonScope();
bind(SerialConnectionManager).toSelf().inSingletonScope();
}
}));
// Serial service client to receive and delegate notifications from the backend.
bind(SerialServiceClient).to(SerialServiceClientImpl).inSingletonScope();
bind(MonitorManagerProxyFactory).toFactory((context) => () => context.container.get<MonitorManagerProxy>(MonitorManagerProxy))
bind(MonitorManagerProxy).toDynamicValue((context) =>
WebSocketConnectionProvider.createProxy(context.container, MonitorManagerProxyPath, context.container.get(MonitorManagerProxyClient))
).inSingletonScope();
// Monitor manager proxy client to receive and delegate pluggable monitors
// notifications from the backend
bind(MonitorManagerProxyClient).to(MonitorManagerProxyClientImpl).inSingletonScope();
bind(WorkspaceService).toSelf().inSingletonScope();
rebind(TheiaWorkspaceService).toService(WorkspaceService);

View File

@@ -3,7 +3,6 @@ import { OutputChannelManager } from '@theia/output/lib/browser/output-channel';
import { CoreService } from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus';
import { BoardsDataStore } from '../boards/boards-data-store';
import { SerialConnectionManager } from '../serial/serial-connection-manager';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import {
SketchContribution,
@@ -18,8 +17,6 @@ export class BurnBootloader extends SketchContribution {
@inject(CoreService)
protected readonly coreService: CoreService;
@inject(SerialConnectionManager)
protected readonly serialConnection: SerialConnectionManager;
@inject(BoardsDataStore)
protected readonly boardsDataStore: BoardsDataStore;
@@ -60,9 +57,15 @@ export class BurnBootloader extends SketchContribution {
this.preferences.get('arduino.upload.verify'),
this.preferences.get('arduino.upload.verbose'),
]);
const board = {
...boardsConfig.selectedBoard,
name: boardsConfig.selectedBoard?.name || '',
fqbn,
}
this.outputChannelManager.getChannel('Arduino').clear();
await this.coreService.burnBootloader({
fqbn,
board,
programmer,
port,
verify,
@@ -85,8 +88,6 @@ export class BurnBootloader extends SketchContribution {
errorMessage = e.toString();
}
this.messageService.error(errorMessage);
} finally {
await this.serialConnection.reconnectAfterUpload();
}
}
}

View File

@@ -4,7 +4,6 @@ import { BoardUserField, CoreService } from '../../common/protocol';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { BoardsDataStore } from '../boards/boards-data-store';
import { SerialConnectionManager } from '../serial/serial-connection-manager';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import {
SketchContribution,
@@ -22,9 +21,6 @@ export class UploadSketch extends SketchContribution {
@inject(CoreService)
protected readonly coreService: CoreService;
@inject(SerialConnectionManager)
protected readonly serialConnection: SerialConnectionManager;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@@ -226,6 +222,11 @@ export class UploadSketch extends SketchContribution {
this.sourceOverride(),
]);
const board = {
...boardsConfig.selectedBoard,
name: boardsConfig.selectedBoard?.name || '',
fqbn,
}
let options: CoreService.Upload.Options | undefined = undefined;
const sketchUri = sketch.uri;
const optimizeForDebug = this.editorMode.compileForDebug;
@@ -247,7 +248,7 @@ export class UploadSketch extends SketchContribution {
const programmer = selectedProgrammer;
options = {
sketchUri,
fqbn,
board,
optimizeForDebug,
programmer,
port,
@@ -259,7 +260,7 @@ export class UploadSketch extends SketchContribution {
} else {
options = {
sketchUri,
fqbn,
board,
optimizeForDebug,
port,
verbose,
@@ -289,8 +290,6 @@ export class UploadSketch extends SketchContribution {
} finally {
this.uploadInProgress = false;
this.onDidChangeEmitter.fire();
setTimeout(() => this.serialConnection.reconnectAfterUpload(), 5000);
}
}
}

View File

@@ -110,12 +110,17 @@ export class VerifySketch extends SketchContribution {
),
this.sourceOverride(),
]);
const board = {
...boardsConfig.selectedBoard,
name: boardsConfig.selectedBoard?.name || '',
fqbn,
}
const verbose = this.preferences.get('arduino.compile.verbose');
const compilerWarnings = this.preferences.get('arduino.compile.warnings');
this.outputChannelManager.getChannel('Arduino').clear();
await this.coreService.compile({
sketchUri: sketch.uri,
fqbn,
board,
optimizeForDebug: this.editorMode.compileForDebug,
verbose,
exportBinaries,

View File

@@ -1,5 +1,6 @@
import { nls } from '@theia/core/lib/common';
import * as React from 'react';
import { Port } from '../../../common/protocol';
import {
ArduinoFirmwareUploader,
FirmwareInfo,
@@ -20,7 +21,7 @@ export const FirmwareUploaderComponent = ({
availableBoards: AvailableBoard[];
firmwareUploader: ArduinoFirmwareUploader;
updatableFqbns: string[];
flashFirmware: (firmware: FirmwareInfo, port: string) => Promise<any>;
flashFirmware: (firmware: FirmwareInfo, port: Port) => Promise<any>;
isOpen: any;
}): React.ReactElement => {
// boolean states for buttons
@@ -81,7 +82,7 @@ export const FirmwareUploaderComponent = ({
const installStatus =
!!firmwareToFlash &&
!!selectedBoard?.port &&
(await flashFirmware(firmwareToFlash, selectedBoard?.port.address));
(await flashFirmware(firmwareToFlash, selectedBoard?.port));
setInstallFeedback((installStatus && 'ok') || 'fail');
} catch {

View File

@@ -15,6 +15,7 @@ import {
} from '../../../common/protocol/arduino-firmware-uploader';
import { FirmwareUploaderComponent } from './firmware-uploader-component';
import { UploadFirmware } from '../../contributions/upload-firmware';
import { Port } from '../../../common/protocol';
@injectable()
export class UploadFirmwareDialogWidget extends ReactWidget {
@@ -49,7 +50,7 @@ export class UploadFirmwareDialogWidget extends ReactWidget {
});
}
protected flashFirmware(firmware: FirmwareInfo, port: string): Promise<any> {
protected flashFirmware(firmware: FirmwareInfo, port: Port): Promise<any> {
this.busyCallback(true);
return this.arduinoFirmwareUploader
.flash(firmware, port)

View File

@@ -0,0 +1,127 @@
import { Emitter, MessageService } from '@theia/core';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Board, Port } from '../common/protocol';
import {
Monitor,
MonitorManagerProxyClient,
MonitorManagerProxyFactory,
} from '../common/protocol/monitor-service';
import {
PluggableMonitorSettings,
MonitorSettings,
} from '../node/monitor-settings/monitor-settings-provider';
@injectable()
export class MonitorManagerProxyClientImpl
implements MonitorManagerProxyClient
{
// When pluggable monitor messages are received from the backend
// this event is triggered.
// Ideally a frontend component is connected to this event
// to update the UI.
protected readonly onMessagesReceivedEmitter = new Emitter<{
messages: string[];
}>();
readonly onMessagesReceived = this.onMessagesReceivedEmitter.event;
protected readonly onWSConnectionChangedEmitter = new Emitter<boolean>();
readonly onWSConnectionChanged = this.onWSConnectionChangedEmitter.event;
// WebSocket used to handle pluggable monitor communication between
// frontend and backend.
private webSocket?: WebSocket;
private wsPort?: number;
getWebSocketPort(): number | undefined {
return this.wsPort;
}
constructor(
@inject(MessageService)
protected messageService: MessageService,
// This is necessary to call the backend methods from the frontend
@inject(MonitorManagerProxyFactory)
protected server: MonitorManagerProxyFactory
) {}
/**
* Connects a localhost WebSocket using the specified port.
* @param addressPort port of the WebSocket
*/
connect(addressPort: number): void {
if (this.webSocket) {
return;
}
try {
this.webSocket = new WebSocket(`ws://localhost:${addressPort}`);
this.onWSConnectionChangedEmitter.fire(true);
} catch {
this.messageService.error('Unable to connect to websocket');
return;
}
this.webSocket.onmessage = (res) => {
const messages = JSON.parse(res.data);
this.onMessagesReceivedEmitter.fire({ messages });
};
this.wsPort = addressPort;
}
/**
* Disconnects the WebSocket if connected.
*/
disconnect(): void {
try {
this.webSocket?.close();
this.webSocket = undefined;
this.onWSConnectionChangedEmitter.fire(false);
} catch {
this.messageService.error('Unable to close websocket');
}
}
async isWSConnected(): Promise<boolean> {
return !!this.webSocket;
}
async startMonitor(
board: Board,
port: Port,
settings?: PluggableMonitorSettings
): Promise<void> {
return this.server().startMonitor(board, port, settings);
}
getCurrentSettings(board: Board, port: Port): MonitorSettings {
return this.server().getCurrentSettings(board, port);
}
send(message: string): void {
if (!this.webSocket) {
return;
}
this.webSocket.send(
JSON.stringify({
command: Monitor.Command.SEND_MESSAGE,
data: message,
})
);
}
changeSettings(settings: MonitorSettings): void {
if (!this.webSocket) {
return;
}
this.webSocket.send(
JSON.stringify({
command: Monitor.Command.CHANGE_SETTINGS,
// TODO: This might be wrong, verify if it works
// SPOILER: It doesn't
data: settings,
})
);
}
}

View File

@@ -1,69 +1,78 @@
import { injectable, inject } from 'inversify';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { SerialConfig } from '../../common/protocol';
import { Emitter, Event } from '@theia/core';
import {
FrontendApplicationContribution,
LocalStorageService,
} from '@theia/core/lib/browser';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { inject, injectable } from '@theia/core/shared/inversify';
@injectable()
export class SerialModel implements FrontendApplicationContribution {
protected static STORAGE_ID = 'arduino-serial-model';
export class MonitorModel implements FrontendApplicationContribution {
protected static STORAGE_ID = 'arduino-monitor-model';
@inject(LocalStorageService)
protected readonly localStorageService: LocalStorageService;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
protected readonly onChangeEmitter: Emitter<
SerialModel.State.Change<keyof SerialModel.State>
MonitorModel.State.Change<keyof MonitorModel.State>
>;
protected _autoscroll: boolean;
protected _timestamp: boolean;
protected _baudRate: SerialConfig.BaudRate;
protected _lineEnding: SerialModel.EOL;
protected _lineEnding: MonitorModel.EOL;
protected _interpolate: boolean;
constructor() {
this._autoscroll = true;
this._timestamp = false;
this._baudRate = SerialConfig.BaudRate.DEFAULT;
this._lineEnding = SerialModel.EOL.DEFAULT;
this._interpolate = false;
this._lineEnding = MonitorModel.EOL.DEFAULT;
this.onChangeEmitter = new Emitter<
SerialModel.State.Change<keyof SerialModel.State>
MonitorModel.State.Change<keyof MonitorModel.State>
>();
}
onStart(): void {
this.localStorageService
.getData<SerialModel.State>(SerialModel.STORAGE_ID)
.then((state) => {
if (state) {
this.restoreState(state);
}
});
.getData<MonitorModel.State>(MonitorModel.STORAGE_ID)
.then(this.restoreState);
}
get onChange(): Event<SerialModel.State.Change<keyof SerialModel.State>> {
get onChange(): Event<MonitorModel.State.Change<keyof MonitorModel.State>> {
return this.onChangeEmitter.event;
}
protected restoreState(state: MonitorModel.State): void {
if (!state) {
return;
}
this._autoscroll = state.autoscroll;
this._timestamp = state.timestamp;
this._lineEnding = state.lineEnding;
this._interpolate = state.interpolate;
}
protected async storeState(): Promise<void> {
return this.localStorageService.setData(MonitorModel.STORAGE_ID, {
autoscroll: this._autoscroll,
timestamp: this._timestamp,
lineEnding: this._lineEnding,
interpolate: this._interpolate,
});
}
get autoscroll(): boolean {
return this._autoscroll;
}
toggleAutoscroll(): void {
this._autoscroll = !this._autoscroll;
this.storeState();
this.storeState().then(() =>
this.storeState().then(() => {
this.onChangeEmitter.fire({
property: 'autoscroll',
value: this._autoscroll,
})
);
value: this._timestamp,
});
});
}
get timestamp(): boolean {
@@ -80,25 +89,11 @@ export class SerialModel implements FrontendApplicationContribution {
);
}
get baudRate(): SerialConfig.BaudRate {
return this._baudRate;
}
set baudRate(baudRate: SerialConfig.BaudRate) {
this._baudRate = baudRate;
this.storeState().then(() =>
this.onChangeEmitter.fire({
property: 'baudRate',
value: this._baudRate,
})
);
}
get lineEnding(): SerialModel.EOL {
get lineEnding(): MonitorModel.EOL {
return this._lineEnding;
}
set lineEnding(lineEnding: SerialModel.EOL) {
set lineEnding(lineEnding: MonitorModel.EOL) {
this._lineEnding = lineEnding;
this.storeState().then(() =>
this.onChangeEmitter.fire({
@@ -121,31 +116,13 @@ export class SerialModel implements FrontendApplicationContribution {
})
);
}
protected restoreState(state: SerialModel.State): void {
this._autoscroll = state.autoscroll;
this._timestamp = state.timestamp;
this._baudRate = state.baudRate;
this._lineEnding = state.lineEnding;
this._interpolate = state.interpolate;
}
protected async storeState(): Promise<void> {
return this.localStorageService.setData(SerialModel.STORAGE_ID, {
autoscroll: this._autoscroll,
timestamp: this._timestamp,
baudRate: this._baudRate,
lineEnding: this._lineEnding,
interpolate: this._interpolate,
});
}
}
export namespace SerialModel {
// TODO: Move this to /common
export namespace MonitorModel {
export interface State {
autoscroll: boolean;
timestamp: boolean;
baudRate: SerialConfig.BaudRate;
lineEnding: EOL;
interpolate: boolean;
}

View File

@@ -8,9 +8,9 @@ import {
TabBarToolbarRegistry,
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { ArduinoToolbar } from '../../toolbar/arduino-toolbar';
import { SerialModel } from '../serial-model';
import { ArduinoMenus } from '../../menu/arduino-menus';
import { nls } from '@theia/core/lib/common';
import { MonitorModel } from '../../monitor-model';
export namespace SerialMonitor {
export namespace Commands {
@@ -48,7 +48,8 @@ export class MonitorViewContribution
static readonly TOGGLE_SERIAL_MONITOR_TOOLBAR =
MonitorWidget.ID + ':toggle-toolbar';
@inject(SerialModel) protected readonly model: SerialModel;
@inject(MonitorModel)
protected readonly model: MonitorModel;
constructor() {
super({

View File

@@ -9,14 +9,14 @@ import {
Widget,
MessageLoop,
} from '@theia/core/lib/browser/widgets';
import { SerialConfig } from '../../../common/protocol/serial-service';
import { ArduinoSelect } from '../../widgets/arduino-select';
import { SerialModel } from '../serial-model';
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';
import { nls } from '@theia/core/lib/common';
import { MonitorManagerProxyClient } from '../../../common/protocol';
import { MonitorModel } from '../../monitor-model';
import { MonitorSettings } from '../../../node/monitor-settings/monitor-settings-provider';
@injectable()
export class MonitorWidget extends ReactWidget {
@@ -26,15 +26,6 @@ export class MonitorWidget extends ReactWidget {
);
static readonly ID = 'serial-monitor';
@inject(SerialModel)
protected readonly serialModel: SerialModel;
@inject(SerialConnectionManager)
protected readonly serialConnection: SerialConnectionManager;
@inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider;
protected widgetHeight: number;
/**
@@ -48,7 +39,16 @@ export class MonitorWidget extends ReactWidget {
protected closing = false;
protected readonly clearOutputEmitter = new Emitter<void>();
constructor() {
constructor(
@inject(MonitorModel)
protected readonly monitorModel: MonitorModel,
@inject(MonitorManagerProxyClient)
protected readonly monitorManagerProxy: MonitorManagerProxyClient,
@inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider
) {
super();
this.id = MonitorWidget.ID;
this.title.label = MonitorWidget.LABEL;
@@ -57,17 +57,35 @@ export class MonitorWidget extends ReactWidget {
this.scrollOptions = undefined;
this.toDispose.push(this.clearOutputEmitter);
this.toDispose.push(
Disposable.create(() => this.serialConnection.closeWStoBE())
Disposable.create(() => this.monitorManagerProxy.disconnect())
);
// Start monitor right away if there is already a board/port combination selected
const { selectedBoard, selectedPort } =
this.boardsServiceProvider.boardsConfig;
if (selectedBoard && selectedBoard.fqbn && selectedPort) {
this.monitorManagerProxy.startMonitor(selectedBoard, selectedPort);
}
this.toDispose.push(
this.boardsServiceProvider.onBoardsConfigChanged(
async ({ selectedBoard, selectedPort }) => {
if (selectedBoard && selectedBoard.fqbn && selectedPort) {
await this.monitorManagerProxy.startMonitor(
selectedBoard,
selectedPort
);
this.update();
}
}
)
);
}
@postConstruct()
protected init(): void {
this.update();
this.toDispose.push(
this.serialConnection.onConnectionChanged(() => this.clearConsole())
);
this.toDispose.push(this.serialModel.onChange(() => this.update()));
this.toDispose.push(this.monitorModel.onChange(() => this.update()));
}
clearConsole(): void {
@@ -79,11 +97,6 @@ export class MonitorWidget extends ReactWidget {
super.dispose();
}
protected onAfterAttach(msg: Message): void {
super.onAfterAttach(msg);
this.serialConnection.openWSToBE();
}
onCloseRequest(msg: Message): void {
this.closing = true;
super.onCloseRequest(msg);
@@ -119,7 +132,7 @@ export class MonitorWidget extends ReactWidget {
};
protected get lineEndings(): OptionsType<
SerialMonitorOutput.SelectOption<SerialModel.EOL>
SerialMonitorOutput.SelectOption<MonitorModel.EOL>
> {
return [
{
@@ -144,32 +157,63 @@ export class MonitorWidget extends ReactWidget {
];
}
protected get baudRates(): OptionsType<
SerialMonitorOutput.SelectOption<SerialConfig.BaudRate>
> {
const baudRates: Array<SerialConfig.BaudRate> = [
300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200,
];
return baudRates.map((baudRate) => ({
label: baudRate + ' baud',
value: baudRate,
}));
private getCurrentSettings(): MonitorSettings {
const board = this.boardsServiceProvider.boardsConfig.selectedBoard;
const port = this.boardsServiceProvider.boardsConfig.selectedPort;
if (!board || !port) {
return {};
}
return this.monitorManagerProxy.getCurrentSettings(board, port);
}
//////////////////////////////////////////////////
////////////////////IMPORTANT/////////////////////
//////////////////////////////////////////////////
// baudRates and selectedBaudRates as of now are hardcoded
// like this to retrieve the baudrate settings from the ones
// received by the monitor.
// We're doing it like since the frontend as of now doesn't
// support a fully customizable list of options that would
// be require to support pluggable monitors completely.
// As soon as the frontend UI is updated to support
// any custom settings this methods MUST be removed and
// made generic.
//
// This breaks if the user tries to open a monitor that
// doesn't support the baudrate setting.
protected get baudRates(): string[] {
const { pluggableMonitorSettings } = this.getCurrentSettings();
if (!pluggableMonitorSettings || !pluggableMonitorSettings['baudrate']) {
return [];
}
const baudRateSettings = pluggableMonitorSettings['baudrate'];
return baudRateSettings.values;
}
protected get selectedBaudRate(): string {
const { pluggableMonitorSettings } = this.getCurrentSettings();
if (!pluggableMonitorSettings || !pluggableMonitorSettings['baudrate']) {
return '';
}
const baudRateSettings = pluggableMonitorSettings['baudrate'];
return baudRateSettings.selectedValue;
}
protected render(): React.ReactNode {
const { baudRates, lineEndings } = this;
const lineEnding =
lineEndings.find((item) => item.value === this.serialModel.lineEnding) ||
lineEndings.find((item) => item.value === this.monitorModel.lineEnding) ||
lineEndings[1]; // Defaults to `\n`.
const baudRate =
baudRates.find((item) => item.value === this.serialModel.baudRate) ||
baudRates[4]; // Defaults to `9600`.
const baudRate = baudRates.find((item) => item === this.selectedBaudRate);
return (
<div className="serial-monitor">
<div className="head">
<div className="send">
<SerialMonitorSendInput
serialConnection={this.serialConnection}
boardsServiceProvider={this.boardsServiceProvider}
monitorManagerProxy={this.monitorManagerProxy}
resolveFocus={this.onFocusResolved}
onSend={this.onSend}
/>
@@ -196,8 +240,8 @@ export class MonitorWidget extends ReactWidget {
</div>
<div className="body">
<SerialMonitorOutput
serialModel={this.serialModel}
serialConnection={this.serialConnection}
monitorModel={this.monitorModel}
monitorManagerProxy={this.monitorManagerProxy}
clearConsoleEvent={this.clearOutputEmitter.event}
height={Math.floor(this.widgetHeight - 50)}
/>
@@ -208,18 +252,21 @@ export class MonitorWidget extends ReactWidget {
protected readonly onSend = (value: string) => this.doSend(value);
protected async doSend(value: string): Promise<void> {
this.serialConnection.send(value);
this.monitorManagerProxy.send(value);
}
protected readonly onChangeLineEnding = (
option: SerialMonitorOutput.SelectOption<SerialModel.EOL>
option: SerialMonitorOutput.SelectOption<MonitorModel.EOL>
) => {
this.serialModel.lineEnding = option.value;
this.monitorModel.lineEnding = option.value;
};
protected readonly onChangeBaudRate = (
option: SerialMonitorOutput.SelectOption<SerialConfig.BaudRate>
) => {
this.serialModel.baudRate = option.value;
protected readonly onChangeBaudRate = (value: string) => {
const { pluggableMonitorSettings } = this.getCurrentSettings();
if (!pluggableMonitorSettings || !pluggableMonitorSettings['baudrate'])
return;
const baudRateSettings = pluggableMonitorSettings['baudrate'];
baudRateSettings.selectedValue = value;
this.monitorManagerProxy.changeSettings(pluggableMonitorSettings);
};
}

View File

@@ -3,12 +3,14 @@ import { Key, KeyCode } from '@theia/core/lib/browser/keys';
import { Board } from '../../../common/protocol/boards-service';
import { isOSX } from '@theia/core/lib/common/os';
import { DisposableCollection, nls } from '@theia/core/lib/common';
import { SerialConnectionManager } from '../serial-connection-manager';
import { SerialPlotter } from '../plotter/protocol';
import { MonitorManagerProxyClient } from '../../../common/protocol';
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
import { timeout } from '@theia/core/lib/common/promise-util';
export namespace SerialMonitorSendInput {
export interface Props {
readonly serialConnection: SerialConnectionManager;
readonly boardsServiceProvider: BoardsServiceProvider;
readonly monitorManagerProxy: MonitorManagerProxyClient;
readonly onSend: (text: string) => void;
readonly resolveFocus: (element: HTMLElement | undefined) => void;
}
@@ -26,28 +28,33 @@ export class SerialMonitorSendInput extends React.Component<
constructor(props: Readonly<SerialMonitorSendInput.Props>) {
super(props);
this.state = { text: '', connected: false };
this.state = { text: '', connected: true };
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.setState({ connected: true });
const checkWSConnection = new Promise<boolean>((resolve) => {
this.props.monitorManagerProxy.onWSConnectionChanged((connected) => {
this.setState({ connected });
resolve(true);
});
});
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 });
const checkWSTimeout = timeout(1000).then(() => false);
Promise.race<boolean>([checkWSConnection, checkWSTimeout]).then(
async (resolved) => {
if (!resolved) {
const connected =
await this.props.monitorManagerProxy.isWSConnected();
this.setState({ connected });
}
}),
]);
}
);
}
componentWillUnmount(): void {
@@ -60,7 +67,7 @@ export class SerialMonitorSendInput extends React.Component<
<input
ref={this.setRef}
type="text"
className={`theia-input ${this.state.connected ? '' : 'warning'}`}
className={`theia-input ${this.shouldShowWarning() ? 'warning' : ''}`}
placeholder={this.placeholder}
value={this.state.text}
onChange={this.onChange}
@@ -69,15 +76,22 @@ export class SerialMonitorSendInput extends React.Component<
);
}
protected shouldShowWarning(): boolean {
const board = this.props.boardsServiceProvider.boardsConfig.selectedBoard;
const port = this.props.boardsServiceProvider.boardsConfig.selectedPort;
return !this.state.connected || !board || !port;
}
protected get placeholder(): string {
const serialConfig = this.props.serialConnection.getConfig();
if (!this.state.connected || !serialConfig) {
if (this.shouldShowWarning()) {
return nls.localize(
'arduino/serial/notConnected',
'Not connected. Select a board and a port to connect automatically.'
);
}
const { board, port } = serialConfig;
const board = this.props.boardsServiceProvider.boardsConfig.selectedBoard;
const port = this.props.boardsServiceProvider.boardsConfig.selectedPort;
return nls.localize(
'arduino/serial/message',
"Message ({0} + Enter to send message to '{1}' on '{2}')",

View File

@@ -2,10 +2,10 @@ import * as React from 'react';
import { Event } from '@theia/core/lib/common/event';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { areEqual, FixedSizeList as List } from 'react-window';
import { SerialModel } from '../serial-model';
import { SerialConnectionManager } from '../serial-connection-manager';
import dateFormat = require('dateformat');
import { messagesToLines, truncateLines } from './monitor-utils';
import { MonitorManagerProxyClient } from '../../../common/protocol';
import { MonitorModel } from '../../monitor-model';
export type Line = { message: string; timestamp?: Date; lineLen: number };
@@ -24,7 +24,7 @@ export class SerialMonitorOutput extends React.Component<
this.listRef = React.createRef();
this.state = {
lines: [],
timestamp: this.props.serialModel.timestamp,
timestamp: this.props.monitorModel.timestamp,
charCount: 0,
};
}
@@ -58,14 +58,13 @@ export class SerialMonitorOutput extends React.Component<
componentDidMount(): void {
this.scrollToBottom();
this.toDisposeBeforeUnmount.pushAll([
this.props.serialConnection.onRead(({ messages }) => {
this.props.monitorManagerProxy.onMessagesReceived(({ messages }) => {
const [newLines, totalCharCount] = messagesToLines(
messages,
this.state.lines,
this.state.charCount
);
const [lines, charCount] = truncateLines(newLines, totalCharCount);
this.setState({
lines,
charCount,
@@ -75,9 +74,9 @@ export class SerialMonitorOutput extends React.Component<
this.props.clearConsoleEvent(() =>
this.setState({ lines: [], charCount: 0 })
),
this.props.serialModel.onChange(({ property }) => {
this.props.monitorModel.onChange(({ property }) => {
if (property === 'timestamp') {
const { timestamp } = this.props.serialModel;
const { timestamp } = this.props.monitorModel;
this.setState({ timestamp });
}
if (property === 'autoscroll') {
@@ -93,7 +92,7 @@ export class SerialMonitorOutput extends React.Component<
}
scrollToBottom = ((): void => {
if (this.listRef.current && this.props.serialModel.autoscroll) {
if (this.listRef.current && this.props.monitorModel.autoscroll) {
this.listRef.current.scrollToItem(this.state.lines.length, 'end');
}
}).bind(this);
@@ -128,8 +127,8 @@ const Row = React.memo(_Row, areEqual);
export namespace SerialMonitorOutput {
export interface Props {
readonly serialModel: SerialModel;
readonly serialConnection: SerialConnectionManager;
readonly monitorModel: MonitorModel;
readonly monitorManagerProxy: MonitorManagerProxyClient;
readonly clearConsoleEvent: Event<void>;
readonly height: number;
}

View File

@@ -6,15 +6,14 @@ import {
MaybePromise,
MenuModelRegistry,
} from '@theia/core';
import { SerialModel } from '../serial-model';
import { ArduinoMenus } from '../../menu/arduino-menus';
import { Contribution } from '../../contributions/contribution';
import { Endpoint, FrontendApplication } from '@theia/core/lib/browser';
import { ipcRenderer } from '@theia/electron/shared/electron';
import { SerialConfig } from '../../../common/protocol';
import { SerialConnectionManager } from '../serial-connection-manager';
import { MonitorManagerProxyClient } from '../../../common/protocol';
import { SerialPlotter } from './protocol';
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
import { MonitorModel } from '../../monitor-model';
const queryString = require('query-string');
export namespace SerialPlotterContribution {
@@ -33,14 +32,14 @@ export class PlotterFrontendContribution extends Contribution {
protected url: string;
protected wsPort: number;
@inject(SerialModel)
protected readonly model: SerialModel;
@inject(MonitorModel)
protected readonly model: MonitorModel;
@inject(ThemeService)
protected readonly themeService: ThemeService;
@inject(SerialConnectionManager)
protected readonly serialConnection: SerialConnectionManager;
@inject(MonitorManagerProxyClient)
protected readonly monitorManagerProxy: MonitorManagerProxyClient;
@inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider;
@@ -75,7 +74,7 @@ export class PlotterFrontendContribution extends Contribution {
this.window.focus();
return;
}
const wsPort = this.serialConnection.getWsPort();
const wsPort = this.monitorManagerProxy.getWebSocketPort();
if (wsPort) {
this.open(wsPort);
} else {
@@ -84,14 +83,28 @@ export class PlotterFrontendContribution extends Contribution {
}
protected async open(wsPort: number): Promise<void> {
const board = this.boardsServiceProvider.boardsConfig.selectedBoard;
const port = this.boardsServiceProvider.boardsConfig.selectedPort;
let baudrates: number[] = [];
let currentBaudrate = -1;
if (board && port) {
const { pluggableMonitorSettings } =
this.monitorManagerProxy.getCurrentSettings(board, port);
if (pluggableMonitorSettings && 'baudrate' in pluggableMonitorSettings) {
// Convert from string to numbers
baudrates = pluggableMonitorSettings['baudrate'].values.map((b) => +b);
currentBaudrate = +pluggableMonitorSettings['baudrate'].selectedValue;
}
}
const initConfig: Partial<SerialPlotter.Config> = {
baudrates: SerialConfig.BaudRates.map((b) => b),
currentBaudrate: this.model.baudRate,
baudrates,
currentBaudrate,
currentLineEnding: this.model.lineEnding,
darkTheme: this.themeService.getCurrentTheme().type === 'dark',
wsPort,
interpolate: this.model.interpolate,
connected: await this.serialConnection.isBESerialConnected(),
connected: await this.monitorManagerProxy.isWSConnected(),
serialPort: this.boardsServiceProvider.boardsConfig.selectedPort?.address,
};
const urlWithParams = queryString.stringifyUrl(

View File

@@ -1,360 +0,0 @@
import { injectable, inject } from 'inversify';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { MessageService } from '@theia/core/lib/common/message-service';
import {
SerialService,
SerialConfig,
SerialError,
Status,
SerialServiceClient,
} from '../../common/protocol/serial-service';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import {
Board,
BoardsService,
} from '../../common/protocol/boards-service';
import { BoardsConfig } from '../boards/boards-config';
import { SerialModel } from './serial-model';
import { ThemeService } from '@theia/core/lib/browser/theming';
import { CoreService } from '../../common/protocol';
import { nls } from '@theia/core/lib/common/nls';
@injectable()
export class SerialConnectionManager {
protected config: Partial<SerialConfig> = {
board: undefined,
port: undefined,
baudRate: undefined,
};
protected readonly onConnectionChangedEmitter = new Emitter<boolean>();
/**
* This emitter forwards all read events **if** the connection is established.
*/
protected readonly onReadEmitter = new Emitter<{ messages: string[] }>();
/**
* Array for storing previous serial errors received from the server, and based on the number of elements in this array,
* we adjust the reconnection delay.
* Super naive way: we wait `array.length * 1000` ms. Once we hit 10 errors, we do not try to reconnect and clean the array.
*/
protected serialErrors: SerialError[] = [];
protected reconnectTimeout?: number;
/**
* When the websocket server is up on the backend, we save the port here, so that the client knows how to connect to it
* */
protected wsPort?: number;
protected webSocket?: WebSocket;
constructor(
@inject(SerialModel) protected readonly serialModel: SerialModel,
@inject(SerialService) protected readonly serialService: SerialService,
@inject(SerialServiceClient)
protected readonly serialServiceClient: SerialServiceClient,
@inject(BoardsService) protected readonly boardsService: BoardsService,
@inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider,
@inject(MessageService) protected messageService: MessageService,
@inject(ThemeService) protected readonly themeService: ThemeService,
@inject(CoreService) protected readonly core: CoreService,
@inject(BoardsServiceProvider)
protected readonly boardsServiceClientImpl: BoardsServiceProvider
) {
this.serialServiceClient.onWebSocketChanged(
this.handleWebSocketChanged.bind(this)
);
this.serialServiceClient.onBaudRateChanged((baudRate) => {
if (this.serialModel.baudRate !== baudRate) {
this.serialModel.baudRate = baudRate;
}
});
this.serialServiceClient.onLineEndingChanged((lineending) => {
if (this.serialModel.lineEnding !== lineending) {
this.serialModel.lineEnding = lineending;
}
});
this.serialServiceClient.onInterpolateChanged((interpolate) => {
if (this.serialModel.interpolate !== interpolate) {
this.serialModel.interpolate = interpolate;
}
});
this.serialServiceClient.onError(this.handleError.bind(this));
this.boardsServiceProvider.onBoardsConfigChanged(
this.handleBoardConfigChange.bind(this)
);
// Handles the `baudRate` changes by reconnecting if required.
this.serialModel.onChange(async ({ property }) => {
if (
property === 'baudRate' &&
(await this.serialService.isSerialPortOpen())
) {
const { boardsConfig } = this.boardsServiceProvider;
this.handleBoardConfigChange(boardsConfig);
}
// update the current values in the backend and propagate to websocket clients
this.serialService.updateWsConfigParam({
...(property === 'lineEnding' && {
currentLineEnding: this.serialModel.lineEnding,
}),
...(property === 'interpolate' && {
interpolate: this.serialModel.interpolate,
}),
});
});
this.themeService.onDidColorThemeChange((theme) => {
this.serialService.updateWsConfigParam({
darkTheme: theme.newTheme.type === 'dark',
});
});
}
/**
* Updated the config in the BE passing only the properties that has changed.
* BE will create a new connection if needed.
*
* @param newConfig the porperties of the config that has changed
*/
async setConfig(newConfig: Partial<SerialConfig>): Promise<void> {
let configHasChanged = false;
Object.keys(this.config).forEach((key: keyof SerialConfig) => {
if (newConfig[key] !== this.config[key]) {
configHasChanged = true;
this.config = { ...this.config, [key]: newConfig[key] };
}
});
if (configHasChanged) {
this.serialService.updateWsConfigParam({
currentBaudrate: this.config.baudRate,
serialPort: this.config.port?.address,
});
if (isSerialConfig(this.config)) {
this.serialService.setSerialConfig(this.config);
}
}
}
getConfig(): Partial<SerialConfig> {
return this.config;
}
getWsPort(): number | undefined {
return this.wsPort;
}
protected handleWebSocketChanged(wsPort: number): void {
this.wsPort = wsPort;
}
get serialConfig(): SerialConfig | undefined {
return isSerialConfig(this.config)
? (this.config as SerialConfig)
: undefined;
}
async isBESerialConnected(): Promise<boolean> {
return await this.serialService.isSerialPortOpen();
}
openWSToBE(): void {
if (!isSerialConfig(this.config)) {
this.messageService.error(
`Please select a board and a port to open the serial connection.`
);
}
if (!this.webSocket && this.wsPort) {
try {
this.webSocket = new WebSocket(`ws://localhost:${this.wsPort}`);
this.webSocket.onmessage = (res) => {
const messages = JSON.parse(res.data);
this.onReadEmitter.fire({ messages });
};
} catch {
this.messageService.error(`Unable to connect to websocket`);
}
}
}
closeWStoBE(): void {
if (this.webSocket) {
try {
this.webSocket.close();
this.webSocket = undefined;
} catch {
this.messageService.error(`Unable to close websocket`);
}
}
}
/**
* Handles error on the SerialServiceClient and try to reconnect, eventually
*/
async handleError(error: SerialError): Promise<void> {
if (!(await this.serialService.isSerialPortOpen())) return;
const { code, config } = error;
const { board, port } = config;
const options = { timeout: 3000 };
switch (code) {
case SerialError.ErrorCodes.CLIENT_CANCEL: {
console.debug(
`Serial connection was canceled by client: ${Serial.Config.toString(
this.config
)}.`
);
break;
}
case SerialError.ErrorCodes.DEVICE_BUSY: {
this.messageService.warn(
nls.localize(
'arduino/serial/connectionBusy',
'Connection failed. Serial port is busy: {0}',
port.address
),
options
);
this.serialErrors.push(error);
break;
}
case SerialError.ErrorCodes.DEVICE_NOT_CONFIGURED: {
this.messageService.info(
nls.localize(
'arduino/serial/disconnected',
'Disconnected {0} from {1}.',
Board.toString(board, {
useFqbn: false,
}),
port.address
),
options
);
break;
}
case undefined: {
this.messageService.error(
nls.localize(
'arduino/serial/unexpectedError',
'Unexpected error. Reconnecting {0} on port {1}.',
Board.toString(board),
port.address
),
options
);
console.error(JSON.stringify(error));
break;
}
}
if ((await this.serialService.clientsAttached()) > 0) {
if (this.serialErrors.length >= 10) {
this.messageService.warn(
nls.localize(
'arduino/serial/failedReconnect',
'Failed to reconnect {0} to serial port after 10 consecutive attempts. The {1} serial port is busy.',
Board.toString(board, {
useFqbn: false,
}),
port.address
)
);
this.serialErrors.length = 0;
} else {
const attempts = this.serialErrors.length || 1;
if (this.reconnectTimeout !== undefined) {
// Clear the previous timer.
window.clearTimeout(this.reconnectTimeout);
}
const timeout = attempts * 1000;
this.messageService.warn(
nls.localize(
'arduino/serial/reconnect',
'Reconnecting {0} to {1} in {2} seconds...',
Board.toString(board, {
useFqbn: false,
}),
port.address,
attempts.toString()
)
);
this.reconnectTimeout = window.setTimeout(
() => this.reconnectAfterUpload(),
timeout
);
}
}
}
async reconnectAfterUpload(): Promise<void> {
try {
if (isSerialConfig(this.config)) {
await this.boardsServiceClientImpl.waitUntilAvailable(
Object.assign(this.config.board, { port: this.config.port }),
10_000
);
this.serialService.connectSerialIfRequired();
}
} catch (waitError) {
this.messageService.error(
nls.localize(
'arduino/sketch/couldNotConnectToSerial',
'Could not reconnect to serial port. {0}',
waitError.toString()
)
);
}
}
/**
* Sends the data to the connected serial port.
* The desired EOL is appended to `data`, you do not have to add it.
* It is a NOOP if connected.
*/
async send(data: string): Promise<Status> {
if (!(await this.serialService.isSerialPortOpen())) {
return Status.NOT_CONNECTED;
}
return new Promise<Status>((resolve) => {
this.serialService
.sendMessageToSerial(data + this.serialModel.lineEnding)
.then(() => resolve(Status.OK));
});
}
get onConnectionChanged(): Event<boolean> {
return this.onConnectionChangedEmitter.event;
}
get onRead(): Event<{ messages: any }> {
return this.onReadEmitter.event;
}
protected async handleBoardConfigChange(
boardsConfig: BoardsConfig.Config
): Promise<void> {
const { selectedBoard: board, selectedPort: port } = boardsConfig;
const { baudRate } = this.serialModel;
const newConfig: Partial<SerialConfig> = { board, port, baudRate };
this.setConfig(newConfig);
}
}
export namespace Serial {
export namespace Config {
export function toString(config: Partial<SerialConfig>): string {
if (!isSerialConfig(config)) return '';
const { board, port } = config;
return `${Board.toString(board)} ${port.address}`;
}
}
}
function isSerialConfig(config: Partial<SerialConfig>): config is SerialConfig {
return !!config.board && !!config.baudRate && !!config.port;
}

View File

@@ -1,48 +0,0 @@
import { injectable } from 'inversify';
import { Emitter } from '@theia/core/lib/common/event';
import {
SerialServiceClient,
SerialError,
SerialConfig,
} from '../../common/protocol/serial-service';
import { SerialModel } from './serial-model';
@injectable()
export class SerialServiceClientImpl implements SerialServiceClient {
protected readonly onErrorEmitter = new Emitter<SerialError>();
readonly onError = this.onErrorEmitter.event;
protected readonly onWebSocketChangedEmitter = new Emitter<number>();
readonly onWebSocketChanged = this.onWebSocketChangedEmitter.event;
protected readonly onBaudRateChangedEmitter =
new Emitter<SerialConfig.BaudRate>();
readonly onBaudRateChanged = this.onBaudRateChangedEmitter.event;
protected readonly onLineEndingChangedEmitter =
new Emitter<SerialModel.EOL>();
readonly onLineEndingChanged = this.onLineEndingChangedEmitter.event;
protected readonly onInterpolateChangedEmitter = new Emitter<boolean>();
readonly onInterpolateChanged = this.onInterpolateChangedEmitter.event;
notifyError(error: SerialError): void {
this.onErrorEmitter.fire(error);
}
notifyWebSocketChanged(message: number): void {
this.onWebSocketChangedEmitter.fire(message);
}
notifyBaudRateChanged(message: SerialConfig.BaudRate): void {
this.onBaudRateChangedEmitter.fire(message);
}
notifyLineEndingChanged(message: SerialModel.EOL): void {
this.onLineEndingChangedEmitter.fire(message);
}
notifyInterpolateChanged(message: boolean): void {
this.onInterpolateChangedEmitter.fire(message);
}
}

View File

@@ -1,3 +1,5 @@
import { Port } from "./boards-service";
export const ArduinoFirmwareUploaderPath =
'/services/arduino-firmware-uploader';
export const ArduinoFirmwareUploader = Symbol('ArduinoFirmwareUploader');
@@ -10,7 +12,7 @@ export type FirmwareInfo = {
};
export interface ArduinoFirmwareUploader {
list(fqbn?: string): Promise<FirmwareInfo[]>;
flash(firmware: FirmwareInfo, port: string): Promise<string>;
flash(firmware: FirmwareInfo, port: Port): Promise<string>;
uploadCertificates(command: string): Promise<any>;
updatableBoards(): Promise<string[]>;
availableFirmwares(fqbn: string): Promise<FirmwareInfo[]>;

View File

@@ -1,5 +1,5 @@
import { BoardUserField } from '.';
import { Port } from '../../common/protocol/boards-service';
import { Board, Port } from '../../common/protocol/boards-service';
import { Programmer } from './boards-service';
export const CompilerWarningLiterals = [
@@ -33,7 +33,7 @@ export namespace CoreService {
* `file` URI to the sketch folder.
*/
readonly sketchUri: string;
readonly fqbn?: string | undefined;
readonly board?: Board;
readonly optimizeForDebug: boolean;
readonly verbose: boolean;
readonly sourceOverride: Record<string, string>;
@@ -42,7 +42,7 @@ export namespace CoreService {
export namespace Upload {
export interface Options extends Compile.Options {
readonly port?: Port | undefined;
readonly port?: Port;
readonly programmer?: Programmer | undefined;
readonly verify: boolean;
readonly userFields: BoardUserField[];
@@ -51,8 +51,8 @@ export namespace CoreService {
export namespace Bootloader {
export interface Options {
readonly fqbn?: string | undefined;
readonly port?: Port | undefined;
readonly board?: Board;
readonly port?: Port;
readonly programmer?: Programmer | undefined;
readonly verbose: boolean;
readonly verify: boolean;

View File

@@ -6,10 +6,10 @@ export * from './core-service';
export * from './filesystem-ext';
export * from './installable';
export * from './library-service';
export * from './serial-service';
export * from './searchable';
export * from './sketches-service';
export * from './examples-service';
export * from './executable-service';
export * from './response-service';
export * from './notification-service';
export * from './monitor-service';

View File

@@ -0,0 +1,92 @@
import { Event, JsonRpcServer } from '@theia/core';
import {
PluggableMonitorSettings,
MonitorSettings,
} from '../../node/monitor-settings/monitor-settings-provider';
import { Board, Port } from './boards-service';
export const MonitorManagerProxyFactory = Symbol('MonitorManagerProxyFactory');
export type MonitorManagerProxyFactory = () => MonitorManagerProxy;
export const MonitorManagerProxyPath = '/services/monitor-manager-proxy';
export const MonitorManagerProxy = Symbol('MonitorManagerProxy');
export interface MonitorManagerProxy
extends JsonRpcServer<MonitorManagerProxyClient> {
startMonitor(
board: Board,
port: Port,
settings?: PluggableMonitorSettings
): Promise<void>;
changeMonitorSettings(
board: Board,
port: Port,
settings: PluggableMonitorSettings
): Promise<void>;
stopMonitor(board: Board, port: Port): Promise<void>;
getCurrentSettings(board: Board, port: Port): PluggableMonitorSettings;
}
export const MonitorManagerProxyClient = Symbol('MonitorManagerProxyClient');
export interface MonitorManagerProxyClient {
onMessagesReceived: Event<{ messages: string[] }>;
onWSConnectionChanged: Event<boolean>;
connect(addressPort: number): void;
disconnect(): void;
getWebSocketPort(): number | undefined;
isWSConnected(): Promise<boolean>;
startMonitor(
board: Board,
port: Port,
settings?: PluggableMonitorSettings
): Promise<void>;
getCurrentSettings(board: Board, port: Port): MonitorSettings;
send(message: string): void;
changeSettings(settings: MonitorSettings): void;
}
export interface PluggableMonitorSetting {
// The setting identifier
readonly id: string;
// A human-readable label of the setting (to be displayed on the GUI)
readonly label: string;
// The setting type (at the moment only "enum" is avaiable)
readonly type: string;
// The values allowed on "enum" types
readonly values: string[];
// The selected value
selectedValue: string;
}
export namespace Monitor {
export enum Command {
SEND_MESSAGE = 'MONITOR_SEND_MESSAGE',
CHANGE_SETTINGS = 'MONITOR_CHANGE_SETTINGS',
}
export type Message = {
command: Monitor.Command;
data: string;
};
}
export interface Status {}
export type OK = Status;
export interface ErrorStatus extends Status {
readonly message: string;
}
export namespace Status {
export function isOK(status: Status & { message?: string }): status is OK {
return !!status && typeof status.message !== 'string';
}
export const OK: OK = {};
export const NOT_CONNECTED: ErrorStatus = { message: 'Not connected.' };
export const ALREADY_CONNECTED: ErrorStatus = {
message: 'Already connected.',
};
export const CONFIG_MISSING: ErrorStatus = {
message: 'Serial Config missing.',
};
export const UPLOAD_IN_PROGRESS: ErrorStatus = {
message: 'Upload in progress.',
};
}

View File

@@ -1,102 +0,0 @@
import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory';
import { Board, Port } from './boards-service';
import { Event } from '@theia/core/lib/common/event';
import { SerialPlotter } from '../../browser/serial/plotter/protocol';
import { SerialModel } from '../../browser/serial/serial-model';
export interface Status {}
export type OK = Status;
export interface ErrorStatus extends Status {
readonly message: string;
}
export namespace Status {
export function isOK(status: Status & { message?: string }): status is OK {
return !!status && typeof status.message !== 'string';
}
export const OK: OK = {};
export const NOT_CONNECTED: ErrorStatus = { message: 'Not connected.' };
export const ALREADY_CONNECTED: ErrorStatus = {
message: 'Already connected.',
};
export const CONFIG_MISSING: ErrorStatus = {
message: 'Serial Config missing.',
};
}
export const SerialServicePath = '/services/serial';
export const SerialService = Symbol('SerialService');
export interface SerialService extends JsonRpcServer<SerialServiceClient> {
clientsAttached(): Promise<number>;
setSerialConfig(config: SerialConfig): Promise<void>;
sendMessageToSerial(message: string): Promise<Status>;
updateWsConfigParam(config: Partial<SerialPlotter.Config>): Promise<void>;
isSerialPortOpen(): Promise<boolean>;
connectSerialIfRequired(): Promise<void>;
disconnect(reason?: SerialError): Promise<Status>;
uploadInProgress: boolean;
}
export interface SerialConfig {
readonly board: Board;
readonly port: Port;
/**
* Defaults to [`SERIAL`](MonitorConfig#ConnectionType#SERIAL).
*/
readonly type?: SerialConfig.ConnectionType;
/**
* Defaults to `9600`.
*/
readonly baudRate?: SerialConfig.BaudRate;
}
export namespace SerialConfig {
export const BaudRates = [
300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200,
] as const;
export type BaudRate = typeof SerialConfig.BaudRates[number];
export namespace BaudRate {
export const DEFAULT: BaudRate = 9600;
}
export enum ConnectionType {
SERIAL = 0,
}
}
export const SerialServiceClient = Symbol('SerialServiceClient');
export interface SerialServiceClient {
onError: Event<SerialError>;
onWebSocketChanged: Event<number>;
onLineEndingChanged: Event<SerialModel.EOL>;
onBaudRateChanged: Event<SerialConfig.BaudRate>;
onInterpolateChanged: Event<boolean>;
notifyError(event: SerialError): void;
notifyWebSocketChanged(message: number): void;
notifyLineEndingChanged(message: SerialModel.EOL): void;
notifyBaudRateChanged(message: SerialConfig.BaudRate): void;
notifyInterpolateChanged(message: boolean): void;
}
export interface SerialError {
readonly message: string;
/**
* If no `code` is available, clients must reestablish the serial connection.
*/
readonly code: number | undefined;
readonly config: SerialConfig;
}
export namespace SerialError {
export namespace ErrorCodes {
/**
* The frontend has refreshed the browser, for instance.
*/
export const CLIENT_CANCEL = 1;
/**
* When detaching a physical device when the duplex channel is still opened.
*/
export const DEVICE_NOT_CONFIGURED = 2;
/**
* Another serial connection was opened on this port. For another electron-instance, Java IDE.
*/
export const DEVICE_BUSY = 3;
}
}

View File

@@ -3,10 +3,10 @@ import {
FirmwareInfo,
} from '../common/protocol/arduino-firmware-uploader';
import { injectable, inject, named } from 'inversify';
import { ExecutableService } from '../common/protocol';
import { SerialService } from '../common/protocol/serial-service';
import { ExecutableService, Port } from '../common/protocol';
import { getExecPath, spawnCommand } from './exec-util';
import { ILogger } from '@theia/core/lib/common/logger';
import { MonitorManager } from './monitor-manager';
@injectable()
export class ArduinoFirmwareUploaderImpl implements ArduinoFirmwareUploader {
@@ -19,8 +19,8 @@ export class ArduinoFirmwareUploaderImpl implements ArduinoFirmwareUploader {
@named('fwuploader')
protected readonly logger: ILogger;
@inject(SerialService)
protected readonly serialService: SerialService;
@inject(MonitorManager)
protected readonly monitorManager: MonitorManager;
protected onError(error: any): void {
this.logger.error(error);
@@ -69,26 +69,28 @@ export class ArduinoFirmwareUploaderImpl implements ArduinoFirmwareUploader {
return await this.list(fqbn);
}
async flash(firmware: FirmwareInfo, port: string): Promise<string> {
async flash(firmware: FirmwareInfo, port: Port): Promise<string> {
let output;
const board = {
name: firmware.board_name,
fqbn: firmware.board_fqbn,
}
try {
this.serialService.uploadInProgress = true;
await this.serialService.disconnect();
this.monitorManager.notifyUploadStarted(board, port);
output = await this.runCommand([
'firmware',
'flash',
'--fqbn',
firmware.board_fqbn,
'--address',
port,
port.address,
'--module',
`${firmware.module}@${firmware.firmware_version}`,
]);
} catch (e) {
throw e;
} finally {
this.serialService.uploadInProgress = false;
this.serialService.connectSerialIfRequired();
this.monitorManager.notifyUploadFinished(board, port);
return output;
}
}

View File

@@ -40,16 +40,7 @@ import {
ArduinoDaemon,
ArduinoDaemonPath,
} from '../common/protocol/arduino-daemon';
import {
SerialServiceImpl,
SerialServiceName,
} from './serial/serial-service-impl';
import {
SerialService,
SerialServicePath,
SerialServiceClient,
} from '../common/protocol/serial-service';
import { MonitorClientProvider } from './serial/monitor-client-provider';
import { ConfigServiceImpl } from './config-service-impl';
import { EnvVariablesServer as TheiaEnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { EnvVariablesServer } from './theia/env-variables/env-variables-server';
@@ -90,10 +81,24 @@ import {
} from '../common/protocol/authentication-service';
import { ArduinoFirmwareUploaderImpl } from './arduino-firmware-uploader-impl';
import { PlotterBackendContribution } from './plotter/plotter-backend-contribution';
import WebSocketServiceImpl from './web-socket/web-socket-service-impl';
import { WebSocketService } from './web-socket/web-socket-service';
import { ArduinoLocalizationContribution } from './arduino-localization-contribution';
import { LocalizationContribution } from '@theia/core/lib/node/i18n/localization-contribution';
import { MonitorManagerProxyImpl } from './monitor-manager-proxy-impl';
import { MonitorManager, MonitorManagerName } from './monitor-manager';
import {
MonitorManagerProxy,
MonitorManagerProxyClient,
MonitorManagerProxyPath,
} from '../common/protocol/monitor-service';
import { MonitorService, MonitorServiceName } from './monitor-service';
import { MonitorSettingsProvider } from './monitor-settings/monitor-settings-provider';
import { MonitorSettingsProviderImpl } from './monitor-settings/monitor-settings-provider-impl';
import {
MonitorServiceFactory,
MonitorServiceFactoryOptions,
} from './monitor-service-factory';
import WebSocketProviderImpl from './web-socket/web-socket-provider-impl';
import { WebSocketProvider } from './web-socket/web-socket-provider';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(BackendApplication).toSelf().inSingletonScope();
@@ -177,9 +182,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
})
);
// Shared WebSocketService for the backend. This will manage all websocket conenctions
bind(WebSocketService).to(WebSocketServiceImpl).inSingletonScope();
// Shared Arduino core client provider service for the backend.
bind(CoreClientProvider).toSelf().inSingletonScope();
@@ -205,19 +207,57 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
// #endregion Theia customizations
// a single MonitorManager is responsible for handling the actual connections to the pluggable monitors
bind(MonitorManager).toSelf().inSingletonScope();
// monitor service & factory bindings
bind(MonitorSettingsProviderImpl).toSelf().inSingletonScope();
bind(MonitorSettingsProvider).toService(MonitorSettingsProviderImpl);
bind(WebSocketProviderImpl).toSelf();
bind(WebSocketProvider).toService(WebSocketProviderImpl);
bind(MonitorServiceFactory).toFactory(
({ container }) =>
(options: MonitorServiceFactoryOptions) => {
const logger = container.get<ILogger>(ILogger);
const monitorSettingsProvider = container.get<MonitorSettingsProvider>(
MonitorSettingsProvider
);
const webSocketProvider =
container.get<WebSocketProvider>(WebSocketProvider);
const { board, port, coreClientProvider } = options;
return new MonitorService(
logger,
monitorSettingsProvider,
webSocketProvider,
board,
port,
coreClientProvider
);
}
);
// Serial client provider per connected frontend.
bind(ConnectionContainerModule).toConstantValue(
ConnectionContainerModule.create(({ bind, bindBackendService }) => {
bind(MonitorClientProvider).toSelf().inSingletonScope();
bind(SerialServiceImpl).toSelf().inSingletonScope();
bind(SerialService).toService(SerialServiceImpl);
bindBackendService<SerialService, SerialServiceClient>(
SerialServicePath,
SerialService,
(service, client) => {
service.setClient(client);
client.onDidCloseConnection(() => service.dispose());
return service;
bind(MonitorManagerProxyImpl).toSelf().inSingletonScope();
bind(MonitorManagerProxy).toService(MonitorManagerProxyImpl);
bindBackendService<MonitorManagerProxy, MonitorManagerProxyClient>(
MonitorManagerProxyPath,
MonitorManagerProxy,
(monitorMgrProxy, client) => {
monitorMgrProxy.setClient(client);
// when the client close the connection, the proxy is disposed.
// when the MonitorManagerProxy is disposed, it informs the MonitorManager
// telling him that it does not need an address/board anymore.
// the MonitorManager will then dispose the actual connection if there are no proxies using it
client.onDidCloseConnection(() => monitorMgrProxy.dispose());
return monitorMgrProxy;
}
);
})
@@ -307,14 +347,22 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
.inSingletonScope()
.whenTargetNamed('config');
// Logger for the serial service.
// Logger for the monitor manager and its services
bind(ILogger)
.toDynamicValue((ctx) => {
const parentLogger = ctx.container.get<ILogger>(ILogger);
return parentLogger.child(SerialServiceName);
return parentLogger.child(MonitorManagerName);
})
.inSingletonScope()
.whenTargetNamed(SerialServiceName);
.whenTargetNamed(MonitorManagerName);
bind(ILogger)
.toDynamicValue((ctx) => {
const parentLogger = ctx.container.get<ILogger>(ILogger);
return parentLogger.child(MonitorServiceName);
})
.inSingletonScope()
.whenTargetNamed(MonitorServiceName);
bind(DefaultGitInit).toSelf();
rebind(GitInit).toService(DefaultGitInit);

View File

@@ -24,7 +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';
import { MonitorManager } from './monitor-manager';
@injectable()
export class CoreServiceImpl extends CoreClientAware implements CoreService {
@@ -34,8 +34,8 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
@inject(NotificationServiceServer)
protected readonly notificationService: NotificationServiceServer;
@inject(SerialService)
protected readonly serialService: SerialService;
@inject(MonitorManager)
protected readonly monitorManager: MonitorManager;
protected uploading = false;
@@ -45,7 +45,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
compilerWarnings?: CompilerWarnings;
}
): Promise<void> {
const { sketchUri, fqbn, compilerWarnings } = options;
const { sketchUri, board, compilerWarnings } = options;
const sketchPath = FileUri.fsPath(sketchUri);
await this.coreClientProvider.initialized;
@@ -55,8 +55,8 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
const compileReq = new CompileRequest();
compileReq.setInstance(instance);
compileReq.setSketchPath(sketchPath);
if (fqbn) {
compileReq.setFqbn(fqbn);
if (board?.fqbn) {
compileReq.setFqbn(board.fqbn);
}
if (compilerWarnings) {
compileReq.setWarnings(compilerWarnings.toLowerCase());
@@ -139,11 +139,9 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
await this.compile(Object.assign(options, { exportBinaries: false }));
this.uploading = true;
this.serialService.uploadInProgress = true;
const { sketchUri, board, port, programmer } = options;
await this.monitorManager.notifyUploadStarted(board, port);
await this.serialService.disconnect();
const { sketchUri, fqbn, port, programmer } = options;
const sketchPath = FileUri.fsPath(sketchUri);
await this.coreClientProvider.initialized;
@@ -153,8 +151,8 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
const req = requestProvider();
req.setInstance(instance);
req.setSketchPath(sketchPath);
if (fqbn) {
req.setFqbn(fqbn);
if (board?.fqbn) {
req.setFqbn(board.fqbn);
}
const p = new Port();
if (port) {
@@ -209,23 +207,22 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
throw new Error(errorMessage);
} finally {
this.uploading = false;
this.serialService.uploadInProgress = false;
this.monitorManager.notifyUploadFinished(board, port);
}
}
async burnBootloader(options: CoreService.Bootloader.Options): Promise<void> {
this.uploading = true;
this.serialService.uploadInProgress = true;
await this.serialService.disconnect();
const { board, port, programmer } = options;
await this.monitorManager.notifyUploadStarted(board, port);
await this.coreClientProvider.initialized;
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const { fqbn, port, programmer } = options;
const burnReq = new BurnBootloaderRequest();
burnReq.setInstance(instance);
if (fqbn) {
burnReq.setFqbn(fqbn);
if (board?.fqbn) {
burnReq.setFqbn(board.fqbn);
}
const p = new Port();
if (port) {
@@ -267,7 +264,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
throw new Error(errorMessage);
} finally {
this.uploading = false;
this.serialService.uploadInProgress = false;
await this.monitorManager.notifyUploadFinished(board, port);
}
}

View File

@@ -0,0 +1,92 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import {
MonitorManagerProxy,
MonitorManagerProxyClient,
Status,
} from '../common/protocol';
import { Board, Port } from '../common/protocol';
import { MonitorManager } from './monitor-manager';
import { PluggableMonitorSettings } from './monitor-settings/monitor-settings-provider';
@injectable()
export class MonitorManagerProxyImpl implements MonitorManagerProxy {
protected client: MonitorManagerProxyClient;
constructor(
@inject(MonitorManager)
protected readonly manager: MonitorManager
) {}
dispose(): void {
this.client?.disconnect();
}
/**
* Start a pluggable monitor and/or change its settings.
* If settings are defined they'll be set before starting the monitor,
* otherwise default ones will be used by the monitor.
* @param board board connected to port
* @param port port to monitor
* @param settings map of supported configuration by the monitor
*/
async startMonitor(
board: Board,
port: Port,
settings?: PluggableMonitorSettings
): Promise<void> {
if (settings) {
await this.changeMonitorSettings(board, port, settings);
}
const status = await this.manager.startMonitor(board, port);
if (status === Status.ALREADY_CONNECTED || status === Status.OK) {
// Monitor started correctly, connect it with the frontend
this.client.connect(this.manager.getWebsocketAddressPort(board, port));
}
}
/**
* Changes the settings of a running pluggable monitor, if that monitor is not
* started this function is a noop.
* @param board board connected to port
* @param port port monitored
* @param settings map of supported configuration by the monitor
*/
async changeMonitorSettings(
board: Board,
port: Port,
settings: PluggableMonitorSettings
): Promise<void> {
if (!this.manager.isStarted(board, port)) {
// Monitor is not running, no need to change settings
return;
}
return this.manager.changeMonitorSettings(board, port, settings);
}
/**
* Stops a running pluggable monitor.
* @param board board connected to port
* @param port port monitored
*/
async stopMonitor(board: Board, port: Port): Promise<void> {
return this.manager.stopMonitor(board, port);
}
/**
* Returns the current settings by the pluggable monitor connected to specified
* by board/port combination.
* @param board board connected to port
* @param port port monitored
* @returns a map of MonitorSetting
*/
getCurrentSettings(board: Board, port: Port): PluggableMonitorSettings {
return this.manager.currentMonitorSettings(board, port);
}
setClient(client: MonitorManagerProxyClient | undefined): void {
if (!client) {
return;
}
this.client = client;
}
}

View File

@@ -0,0 +1,213 @@
import { ILogger } from '@theia/core';
import { inject, injectable, named } from '@theia/core/shared/inversify';
import { Board, Port, Status } from '../common/protocol';
import { CoreClientAware } from './core-client-provider';
import { MonitorService } from './monitor-service';
import { MonitorServiceFactory } from './monitor-service-factory';
import { PluggableMonitorSettings } from './monitor-settings/monitor-settings-provider';
type MonitorID = string;
export const MonitorManagerName = 'monitor-manager';
@injectable()
export class MonitorManager extends CoreClientAware {
// Map of monitor services that manage the running pluggable monitors.
// Each service handles the lifetime of one, and only one, monitor.
// If either the board or port managed changes, a new service must
// be started.
private monitorServices = new Map<MonitorID, MonitorService>();
@inject(MonitorServiceFactory)
private monitorServiceFactory: MonitorServiceFactory;
constructor(
@inject(ILogger)
@named(MonitorManagerName)
protected readonly logger: ILogger
) {
super();
}
/**
* Used to know if a monitor is started
* @param board board connected to port
* @param port port to monitor
* @returns true if the monitor is currently monitoring the board/port
* combination specifed, false in all other cases.
*/
isStarted(board: Board, port: Port): boolean {
const monitorID = this.monitorID(board, port);
const monitor = this.monitorServices.get(monitorID);
if (monitor) {
return monitor.isStarted();
}
return false;
}
/**
* Start a pluggable monitor that receives and sends messages
* to the specified board and port combination.
* @param board board connected to port
* @param port port to monitor
* @returns a Status object to know if the process has been
* started or if there have been errors.
*/
async startMonitor(board: Board, port: Port): Promise<Status> {
const monitorID = this.monitorID(board, port);
let monitor = this.monitorServices.get(monitorID);
if (!monitor) {
monitor = this.createMonitor(board, port);
}
return await monitor.start(monitorID);
}
/**
* Stop a pluggable monitor connected to the specified board/port
* combination. It's a noop if monitor is not running.
* @param board board connected to port
* @param port port monitored
*/
async stopMonitor(board: Board, port: Port): Promise<void> {
const monitorID = this.monitorID(board, port);
const monitor = this.monitorServices.get(monitorID);
if (!monitor) {
// There's no monitor to stop, bail
return;
}
return await monitor.stop();
}
/**
* Returns the port of the WebSocket used by the MonitorService
* that is handling the board/port combination
* @param board board connected to port
* @param port port to monitor
* @returns port of the MonitorService's WebSocket
*/
getWebsocketAddressPort(board: Board, port: Port): number {
const monitorID = this.monitorID(board, port);
const monitor = this.monitorServices.get(monitorID);
if (!monitor) {
return -1;
}
return monitor.getWebsocketAddressPort();
}
/**
* Notifies the monitor service of that board/port combination
* that an upload process started on that exact board/port combination.
* This must be done so that we can stop the monitor for the time being
* until the upload process finished.
* @param board board connected to port
* @param port port to monitor
*/
async notifyUploadStarted(board?: Board, port?: Port): Promise<void> {
if (!board || !port) {
// We have no way of knowing which monitor
// to retrieve if we don't have this information.
return;
}
const monitorID = this.monitorID(board, port);
const monitor = this.monitorServices.get(monitorID);
if (!monitor) {
// There's no monitor running there, bail
return;
}
monitor.setUploadInProgress(true);
return await monitor.pause();
}
/**
* Notifies the monitor service of that board/port combination
* that an upload process started on that exact board/port combination.
* @param board board connected to port
* @param port port to monitor
* @returns a Status object to know if the process has been
* started or if there have been errors.
*/
async notifyUploadFinished(board?: Board, port?: Port): Promise<Status> {
if (!board || !port) {
// We have no way of knowing which monitor
// to retrieve if we don't have this information.
return Status.NOT_CONNECTED;
}
const monitorID = this.monitorID(board, port);
const monitor = this.monitorServices.get(monitorID);
if (!monitor) {
// There's no monitor running there, bail
return Status.NOT_CONNECTED;
}
monitor.setUploadInProgress(false);
return await monitor.start(monitorID);
}
/**
* Changes the settings of a pluggable monitor even if it's running.
* If monitor is not running they're going to be used as soon as it's started.
* @param board board connected to port
* @param port port to monitor
* @param settings monitor settings to change
*/
changeMonitorSettings(
board: Board,
port: Port,
settings: PluggableMonitorSettings
) {
const monitorID = this.monitorID(board, port);
let monitor = this.monitorServices.get(monitorID);
if (!monitor) {
monitor = this.createMonitor(board, port);
monitor.changeSettings(settings);
}
}
/**
* Returns the settings currently used by the pluggable monitor
* that's communicating with the specified board/port combination.
* @param board board connected to port
* @param port port monitored
* @returns map of current monitor settings
*/
currentMonitorSettings(board: Board, port: Port): PluggableMonitorSettings {
const monitorID = this.monitorID(board, port);
const monitor = this.monitorServices.get(monitorID);
if (!monitor) {
return {};
}
return monitor.currentSettings();
}
/**
* Creates a MonitorService that handles the lifetime and the
* communication via WebSocket with the frontend.
* @param board board connected to specified port
* @param port port to monitor
* @returns a new instance of MonitorService ready to use.
*/
private createMonitor(board: Board, port: Port): MonitorService {
const monitorID = this.monitorID(board, port);
const monitor = this.monitorServiceFactory({
board,
port,
coreClientProvider: this.coreClientProvider,
});
this.monitorServices.set(monitorID, monitor);
monitor.onDispose(
(() => {
this.monitorServices.delete(monitorID);
}).bind(this)
);
return monitor;
}
/**
* Utility function to create a unique ID for a monitor service.
* @param board
* @param port
* @returns a unique monitor ID
*/
private monitorID(board: Board, port: Port): MonitorID {
return `${board.fqbn}-${port.address}-${port.protocol}`;
}
}

View File

@@ -0,0 +1,18 @@
import { Board, Port } from '../common/protocol';
import { CoreClientProvider } from './core-client-provider';
import { MonitorService } from './monitor-service';
export const MonitorServiceFactory = Symbol('MonitorServiceFactory');
export interface MonitorServiceFactory {
(options: {
board: Board;
port: Port;
coreClientProvider: CoreClientProvider;
}): MonitorService;
}
export interface MonitorServiceFactoryOptions {
board: Board;
port: Port;
coreClientProvider: CoreClientProvider;
}

View File

@@ -0,0 +1,425 @@
import { ClientDuplexStream } from '@grpc/grpc-js';
import { Disposable, Emitter, ILogger } from '@theia/core';
import { inject, named } from '@theia/core/shared/inversify';
import { Board, Port, Status, Monitor } from '../common/protocol';
import {
EnumerateMonitorPortSettingsRequest,
EnumerateMonitorPortSettingsResponse,
MonitorPortConfiguration,
MonitorPortSetting,
MonitorRequest,
MonitorResponse,
} from './cli-protocol/cc/arduino/cli/commands/v1/monitor_pb';
import { CoreClientAware, CoreClientProvider } from './core-client-provider';
import { WebSocketProvider } from './web-socket/web-socket-provider';
import { Port as gRPCPort } from 'arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/port_pb';
import {
PluggableMonitorSettings,
MonitorSettingsProvider,
} from './monitor-settings/monitor-settings-provider';
export const MonitorServiceName = 'monitor-service';
export class MonitorService extends CoreClientAware implements Disposable {
// Bidirectional gRPC stream used to receive and send data from the running
// pluggable monitor managed by the Arduino CLI.
protected duplex: ClientDuplexStream<MonitorRequest, MonitorResponse> | null;
// Settings used by the currently running pluggable monitor.
// They can be freely modified while running.
protected settings: PluggableMonitorSettings;
// List of messages received from the running pluggable monitor.
// These are flushed from time to time to the frontend.
protected messages: string[] = [];
// Handles messages received from the frontend via websocket.
protected onMessageReceived?: Disposable;
// Sends messages to the frontend from time to time.
protected flushMessagesInterval?: NodeJS.Timeout;
// Triggered each time the number of clients connected
// to the this service WebSocket changes.
protected onWSClientsNumberChanged?: Disposable;
// Used to notify that the monitor is being disposed
protected readonly onDisposeEmitter = new Emitter<void>();
readonly onDispose = this.onDisposeEmitter.event;
protected uploadInProgress = false;
constructor(
@inject(ILogger)
@named(MonitorServiceName)
protected readonly logger: ILogger,
@inject(MonitorSettingsProvider)
protected readonly monitorSettingsProvider: MonitorSettingsProvider,
@inject(WebSocketProvider)
protected readonly webSocketProvider: WebSocketProvider,
private readonly board: Board,
private readonly port: Port,
protected readonly coreClientProvider: CoreClientProvider
) {
super();
this.onWSClientsNumberChanged =
this.webSocketProvider.onClientsNumberChanged(async (clients: number) => {
if (clients === 0) {
// There are no more clients that want to receive
// data from this monitor, we can freely close
// and dispose it.
this.dispose();
}
});
}
setUploadInProgress(status: boolean): void {
this.uploadInProgress = status;
}
getWebsocketAddressPort(): number {
return this.webSocketProvider.getAddress().port;
}
dispose(): void {
this.stop();
this.onDisposeEmitter.fire();
}
/**
* isStarted is used to know if the currently running pluggable monitor is started.
* @returns true if pluggable monitor communication duplex is open,
* false in all other cases.
*/
isStarted(): boolean {
return !!this.duplex;
}
/**
* Start and connects a monitor using currently set board and port.
* If a monitor is already started or board fqbn, port address and/or protocol
* are missing nothing happens.
* @param id
* @returns a status to verify connection has been established.
*/
async start(monitorID: string): Promise<Status> {
if (this.duplex) {
return Status.ALREADY_CONNECTED;
}
if (!this.board?.fqbn || !this.port?.address || !this.port?.protocol) {
return Status.CONFIG_MISSING;
}
if (this.uploadInProgress) {
return Status.UPLOAD_IN_PROGRESS;
}
this.logger.info('starting monitor');
// get default monitor settings from the CLI
const defaultSettings = await this.portMonitorSettings(
this.port.protocol,
this.board.fqbn
);
// get actual settings from the settings provider
this.settings = await this.monitorSettingsProvider.getSettings(
monitorID,
defaultSettings
);
await this.coreClientProvider.initialized;
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
this.duplex = client.monitor();
this.duplex
.on('close', () => {
this.duplex = null;
this.logger.info(
`monitor to ${this.port?.address} using ${this.port?.protocol} closed by client`
);
})
.on('end', () => {
this.duplex = null;
this.logger.info(
`monitor to ${this.port?.address} using ${this.port?.protocol} closed by server`
);
})
.on('error', (err: Error) => {
this.logger.error(err);
// TODO
// this.theiaFEClient?.notifyError()
})
.on(
'data',
((res: MonitorResponse) => {
if (res.getError()) {
// TODO: Maybe disconnect
this.logger.error(res.getError());
return;
}
const data = res.getRxData();
const message =
typeof data === 'string'
? data
: new TextDecoder('utf8').decode(data);
this.messages.push(...splitLines(message));
}).bind(this)
);
const req = new MonitorRequest();
req.setInstance(instance);
if (this.board?.fqbn) {
req.setFqbn(this.board.fqbn);
}
if (this.port?.address && this.port?.protocol) {
const port = new gRPCPort();
port.setAddress(this.port.address);
port.setProtocol(this.port.protocol);
req.setPort(port);
}
const config = new MonitorPortConfiguration();
for (const id in this.settings) {
const s = new MonitorPortSetting();
s.setSettingId(id);
s.setValue(this.settings[id].selectedValue);
config.addSettings(s);
}
req.setPortConfiguration(config);
const connect = new Promise<Status>((resolve) => {
if (this.duplex?.write(req)) {
this.startMessagesHandlers();
this.logger.info(
`started monitor to ${this.port?.address} using ${this.port?.protocol}`
);
resolve(Status.OK);
return;
}
this.logger.warn(
`failed starting monitor to ${this.port?.address} using ${this.port?.protocol}`
);
resolve(Status.NOT_CONNECTED);
});
const connectTimeout = new Promise<Status>((resolve) => {
setTimeout(async () => {
this.logger.warn(
`timeout starting monitor to ${this.port?.address} using ${this.port?.protocol}`
);
resolve(Status.NOT_CONNECTED);
}, 1000);
});
// Try opening a monitor connection with a timeout
return await Promise.race([connect, connectTimeout]);
}
/**
* Pauses the currently running monitor, it still closes the gRPC connection
* with the underlying monitor process but it doesn't stop the message handlers
* currently running.
* This is mainly used to handle upload with the board/port combination
* the monitor is listening to.
* @returns
*/
async pause(): Promise<void> {
return new Promise(async (resolve) => {
if (!this.duplex) {
this.logger.warn(
`monitor to ${this.port?.address} using ${this.port?.protocol} already stopped`
);
return resolve();
}
// It's enough to close the connection with the client
// to stop the monitor process
this.duplex.end();
this.logger.info(
`stopped monitor to ${this.port?.address} using ${this.port?.protocol}`
);
this.duplex.on('end', resolve);
});
}
/**
* Stop the monitor currently running
*/
async stop(): Promise<void> {
return this.pause().finally(this.stopMessagesHandlers.bind(this));
}
/**
* Send a message to the running monitor, a well behaved monitor
* will then send that message to the board.
* We MUST NEVER send a message that wasn't a user's input to the board.
* @param message string sent to running monitor
* @returns a status to verify message has been sent.
*/
async send(message: string): Promise<Status> {
if (!this.duplex) {
return Status.NOT_CONNECTED;
}
await this.coreClientProvider.initialized;
const coreClient = await this.coreClient();
const { instance } = coreClient;
const req = new MonitorRequest();
req.setInstance(instance);
req.setTxData(new TextEncoder().encode(message));
return new Promise<Status>((resolve) => {
if (this.duplex) {
this.duplex?.write(req, () => {
resolve(Status.OK);
});
return;
}
this.stop().then(() => resolve(Status.NOT_CONNECTED));
});
}
/**
*
* @returns map of current monitor settings
*/
currentSettings(): PluggableMonitorSettings {
return this.settings;
}
// TODO: move this into MonitoSettingsProvider
/**
* Returns the possible configurations used to connect a monitor
* to the board specified by fqbn using the specified protocol
* @param protocol the protocol of the monitor we want get settings for
* @param fqbn the fqbn of the board we want to monitor
* @returns a map of all the settings supported by the monitor
*/
private async portMonitorSettings(
protocol: string,
fqbn: string
): Promise<PluggableMonitorSettings> {
await this.coreClientProvider.initialized;
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const req = new EnumerateMonitorPortSettingsRequest();
req.setInstance(instance);
req.setPortProtocol(protocol);
req.setFqbn(fqbn);
const res = await new Promise<EnumerateMonitorPortSettingsResponse>(
(resolve, reject) => {
client.enumerateMonitorPortSettings(req, (err, resp) => {
if (!!err) {
reject(err);
}
resolve(resp);
});
}
);
const settings: PluggableMonitorSettings = {};
for (const iterator of res.getSettingsList()) {
settings[iterator.getSettingId()] = {
id: iterator.getSettingId(),
label: iterator.getLabel(),
type: iterator.getType(),
values: iterator.getEnumValuesList(),
selectedValue: iterator.getValue(),
};
}
return settings;
}
/**
* Set monitor settings, if there is a running monitor they'll be sent
* to it, otherwise they'll be used when starting one.
* Only values in settings parameter will be change, other values won't
* be changed in any way.
* @param settings map of monitor settings to change
* @returns a status to verify settings have been sent.
*/
async changeSettings(settings: PluggableMonitorSettings): Promise<Status> {
const config = new MonitorPortConfiguration();
for (const id in settings) {
const s = new MonitorPortSetting();
s.setSettingId(id);
s.setValue(settings[id].selectedValue);
config.addSettings(s);
this.settings[id] = settings[id];
}
if (!this.duplex) {
return Status.NOT_CONNECTED;
}
await this.coreClientProvider.initialized;
const coreClient = await this.coreClient();
const { instance } = coreClient;
const req = new MonitorRequest();
req.setInstance(instance);
req.setPortConfiguration(config);
this.duplex.write(req);
return Status.OK;
}
/**
* Starts the necessary handlers to send and receive
* messages to and from the frontend and the running monitor
*/
private startMessagesHandlers(): void {
if (!this.flushMessagesInterval) {
const flushMessagesToFrontend = () => {
if (this.messages.length) {
this.webSocketProvider.sendMessage(JSON.stringify(this.messages));
this.messages = [];
}
};
this.flushMessagesInterval = setInterval(flushMessagesToFrontend, 32);
}
if (!this.onMessageReceived) {
this.onMessageReceived = this.webSocketProvider.onMessageReceived(
(msg: string) => {
const message: Monitor.Message = JSON.parse(msg);
switch (message.command) {
case Monitor.Command.SEND_MESSAGE:
this.send(message.data);
break;
case Monitor.Command.CHANGE_SETTINGS:
const settings: PluggableMonitorSettings = JSON.parse(
message.data
);
this.changeSettings(settings);
break;
}
}
);
}
}
/**
* Stops the necessary handlers to send and receive messages to
* and from the frontend and the running monitor
*/
private stopMessagesHandlers(): void {
if (this.flushMessagesInterval) {
clearInterval(this.flushMessagesInterval);
this.flushMessagesInterval = undefined;
}
if (this.onMessageReceived) {
this.onMessageReceived.dispose();
this.onMessageReceived = undefined;
}
}
}
/**
* Splits a string into an array without removing newline char.
* @param s string to split into lines
* @returns an lines array
*/
function splitLines(s: string): string[] {
return s.split(/(?<=\n)/);
}

View File

@@ -0,0 +1,145 @@
import * as fs from 'fs';
import { join } from 'path';
import { injectable, inject, postConstruct } from 'inversify';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { FileUri } from '@theia/core/lib/node/file-uri';
import { promisify } from 'util';
import {
PluggableMonitorSettings,
MonitorSettingsProvider,
} from './monitor-settings-provider';
import { Deferred } from '@theia/core/lib/common/promise-util';
const MONITOR_SETTINGS_FILE = 'pluggable-monitor-settings.json';
@injectable()
export class MonitorSettingsProviderImpl implements MonitorSettingsProvider {
@inject(EnvVariablesServer)
protected readonly envVariablesServer: EnvVariablesServer;
protected ready = new Deferred<void>();
// this is populated with all settings coming from the CLI. This should never be modified
// // as it is used to double check the monitorSettings attribute
// private monitorDefaultSettings: PluggableMonitorSettings;
// this contains actual values coming from the stored file and edited by the user
// this is a map with MonitorId as key and PluggableMonitorSetting as value
private monitorSettings: Record<string, PluggableMonitorSettings>;
private pluggableMonitorSettingsPath: string;
@postConstruct()
protected async init(): Promise<void> {
// get the monitor settings file path
const configDirUri = await this.envVariablesServer.getConfigDirUri();
this.pluggableMonitorSettingsPath = join(
FileUri.fsPath(configDirUri),
MONITOR_SETTINGS_FILE
);
// read existing settings
await this.readFile();
console.log(this.monitorSettings);
this.ready.resolve();
}
async getSettings(
monitorId: string,
defaultSettings: PluggableMonitorSettings
): Promise<PluggableMonitorSettings> {
// wait for the service to complete the init
await this.ready.promise;
const { matchingSettings } = this.longestPrefixMatch(monitorId);
return this.reconcileSettings(matchingSettings, defaultSettings);
}
async setSettings(
monitorId: string,
settings: PluggableMonitorSettings
): Promise<PluggableMonitorSettings> {
// wait for the service to complete the init
await this.ready.promise;
const newSettings = this.reconcileSettings(
settings,
this.monitorSettings[monitorId]
);
this.monitorSettings[monitorId] = newSettings;
await this.writeFile();
return newSettings;
}
private reconcileSettings(
newSettings: PluggableMonitorSettings,
defaultSettings: PluggableMonitorSettings
): PluggableMonitorSettings {
// TODO: implement
return newSettings;
}
private async readFile(): Promise<void> {
const rawJson = await promisify(fs.readFile)(
this.pluggableMonitorSettingsPath,
{
encoding: 'utf-8',
flag: 'a+', // a+ = append and read, creating the file if it doesn't exist
}
);
if (!rawJson) {
this.monitorSettings = {};
}
try {
this.monitorSettings = JSON.parse(rawJson);
} catch (error) {
console.error(
'Could not parse the pluggable monitor settings file. Using empty file.'
);
this.monitorSettings = {};
}
}
private async writeFile() {
await promisify(fs.writeFile)(
this.pluggableMonitorSettingsPath,
JSON.stringify(this.monitorSettings)
);
}
private longestPrefixMatch(id: string): {
matchingPrefix: string;
matchingSettings: PluggableMonitorSettings;
} {
const separator = '-';
const idTokens = id.split(separator);
let matchingPrefix = '';
let matchingSettings: PluggableMonitorSettings = {};
const monitorSettingsKeys = Object.keys(this.monitorSettings);
for (let i = 0; i < idTokens.length; i++) {
const prefix = idTokens.slice(0, i + 1).join(separator);
for (let k = 0; k < monitorSettingsKeys.length; k++) {
if (monitorSettingsKeys[k].startsWith(prefix)) {
matchingPrefix = prefix;
matchingSettings = this.monitorSettings[monitorSettingsKeys[k]];
break;
}
}
if (matchingPrefix.length) {
break;
}
}
return { matchingPrefix, matchingSettings };
}
}

View File

@@ -0,0 +1,20 @@
import { MonitorModel } from '../../browser/monitor-model';
import { PluggableMonitorSetting } from '../../common/protocol';
export type PluggableMonitorSettings = Record<string, PluggableMonitorSetting>;
export interface MonitorSettings {
pluggableMonitorSettings?: PluggableMonitorSettings;
monitorUISettings?: Partial<MonitorModel.State>;
}
export const MonitorSettingsProvider = Symbol('MonitorSettingsProvider');
export interface MonitorSettingsProvider {
getSettings(
monitorId: string,
defaultSettings: PluggableMonitorSettings
): Promise<PluggableMonitorSettings>;
setSettings(
monitorId: string,
settings: PluggableMonitorSettings
): Promise<PluggableMonitorSettings>;
}

View File

@@ -1,26 +0,0 @@
import * as grpc from '@grpc/grpc-js';
import { injectable } from 'inversify';
import { MonitorServiceClient } from '../cli-protocol/cc/arduino/cli/monitor/v1/monitor_grpc_pb';
import * as monitorGrpcPb from '../cli-protocol/cc/arduino/cli/monitor/v1/monitor_grpc_pb';
import { GrpcClientProvider } from '../grpc-client-provider';
@injectable()
export class MonitorClientProvider extends GrpcClientProvider<MonitorServiceClient> {
createClient(port: string | number): MonitorServiceClient {
// https://github.com/agreatfool/grpc_tools_node_protoc_ts/blob/master/doc/grpcjs_support.md#usage
const MonitorServiceClient = grpc.makeClientConstructor(
// @ts-expect-error: ignore
monitorGrpcPb['cc.arduino.cli.monitor.v1.MonitorService'],
'MonitorServiceService'
) as any;
return new MonitorServiceClient(
`localhost:${port}`,
grpc.credentials.createInsecure(),
this.channelOptions
);
}
close(client: MonitorServiceClient): void {
client.close();
}
}

View File

@@ -1,397 +0,0 @@
import { ClientDuplexStream } from '@grpc/grpc-js';
import { TextEncoder } from 'util';
import { injectable, inject, named } from 'inversify';
import { Struct } from 'google-protobuf/google/protobuf/struct_pb';
import { ILogger } from '@theia/core/lib/common/logger';
import {
SerialService,
SerialServiceClient,
SerialConfig,
SerialError,
Status,
} from '../../common/protocol/serial-service';
import {
StreamingOpenRequest,
StreamingOpenResponse,
MonitorConfig as GrpcMonitorConfig,
} from '../cli-protocol/cc/arduino/cli/monitor/v1/monitor_pb';
import { MonitorClientProvider } from './monitor-client-provider';
import { Board } from '../../common/protocol/boards-service';
import { WebSocketService } from '../web-socket/web-socket-service';
import { SerialPlotter } from '../../browser/serial/plotter/protocol';
import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol';
export const SerialServiceName = 'serial-service';
interface ErrorWithCode extends Error {
readonly code: number;
}
namespace ErrorWithCode {
export function toSerialError(
error: Error,
config: SerialConfig
): SerialError {
const { message } = error;
let code = undefined;
if (is(error)) {
// TODO: const `mapping`. Use regex for the `message`.
const mapping = new Map<string, number>();
mapping.set(
'1 CANCELLED: Cancelled on client',
SerialError.ErrorCodes.CLIENT_CANCEL
);
mapping.set(
'2 UNKNOWN: device not configured',
SerialError.ErrorCodes.DEVICE_NOT_CONFIGURED
);
mapping.set(
'2 UNKNOWN: error opening serial connection: Serial port busy',
SerialError.ErrorCodes.DEVICE_BUSY
);
code = mapping.get(message);
}
return {
message,
code,
config,
};
}
function is(error: Error & { code?: number }): error is ErrorWithCode {
return typeof error.code === 'number';
}
}
@injectable()
export class SerialServiceImpl implements SerialService {
protected theiaFEClient?: SerialServiceClient;
protected serialConfig?: SerialConfig;
protected serialConnection?: {
duplex: ClientDuplexStream<StreamingOpenRequest, StreamingOpenResponse>;
config: SerialConfig;
};
protected messages: string[] = [];
protected onMessageReceived: Disposable | null;
protected onWSClientsNumberChanged: Disposable | null;
protected flushMessagesInterval: NodeJS.Timeout | null;
uploadInProgress = false;
constructor(
@inject(ILogger)
@named(SerialServiceName)
protected readonly logger: ILogger,
@inject(MonitorClientProvider)
protected readonly serialClientProvider: MonitorClientProvider,
@inject(WebSocketService)
protected readonly webSocketService: WebSocketService
) { }
async isSerialPortOpen(): Promise<boolean> {
return !!this.serialConnection;
}
setClient(client: SerialServiceClient | undefined): void {
this.theiaFEClient = client;
this.theiaFEClient?.notifyWebSocketChanged(
this.webSocketService.getAddress().port
);
// listen for the number of websocket clients and create or dispose the serial connection
this.onWSClientsNumberChanged =
this.webSocketService.onClientsNumberChanged(async () => {
await this.connectSerialIfRequired();
});
}
public async clientsAttached(): Promise<number> {
return this.webSocketService.getConnectedClientsNumber.bind(
this.webSocketService
)();
}
public async connectSerialIfRequired(): Promise<void> {
if (this.uploadInProgress) return;
const clients = await this.clientsAttached();
clients > 0 ? await this.connect() : await this.disconnect();
}
dispose(): void {
this.logger.info('>>> Disposing serial service...');
if (this.serialConnection) {
this.disconnect();
}
this.logger.info('<<< Disposed serial service.');
this.theiaFEClient = undefined;
}
async setSerialConfig(config: SerialConfig): Promise<void> {
this.serialConfig = config;
await this.disconnect();
await this.connectSerialIfRequired();
}
async updateWsConfigParam(
config: Partial<SerialPlotter.Config>
): Promise<void> {
const msg: SerialPlotter.Protocol.Message = {
command: SerialPlotter.Protocol.Command.MIDDLEWARE_CONFIG_CHANGED,
data: config,
};
this.webSocketService.sendMessage(JSON.stringify(msg));
}
private async connect(): Promise<Status> {
if (!this.serialConfig) {
return Status.CONFIG_MISSING;
}
this.logger.info(
`>>> Creating serial connection for ${Board.toString(
this.serialConfig.board
)} on port ${this.serialConfig.port.address}...`
);
if (this.serialConnection) {
return Status.ALREADY_CONNECTED;
}
const client = await this.serialClientProvider.client();
if (!client) {
return Status.NOT_CONNECTED;
}
if (client instanceof Error) {
return { message: client.message };
}
const duplex = client.streamingOpen();
this.serialConnection = { duplex, config: this.serialConfig };
const serialConfig = this.serialConfig;
duplex.on(
'error',
((error: Error) => {
const serialError = ErrorWithCode.toSerialError(error, serialConfig);
if (serialError.code !== SerialError.ErrorCodes.CLIENT_CANCEL) {
this.disconnect(serialError).then(() => {
if (this.theiaFEClient) {
this.theiaFEClient.notifyError(serialError);
}
});
}
if (serialError.code === undefined) {
// Log the original, unexpected error.
this.logger.error(error);
}
}).bind(this)
);
this.updateWsConfigParam({ connected: !!this.serialConnection });
const flushMessagesToFrontend = () => {
if (this.messages.length) {
this.webSocketService.sendMessage(JSON.stringify(this.messages));
this.messages = [];
}
};
this.onMessageReceived = this.webSocketService.onMessageReceived(
(msg: string) => {
try {
const message: SerialPlotter.Protocol.Message = JSON.parse(msg);
switch (message.command) {
case SerialPlotter.Protocol.Command.PLOTTER_SEND_MESSAGE:
this.sendMessageToSerial(message.data);
break;
case SerialPlotter.Protocol.Command.PLOTTER_SET_BAUDRATE:
this.theiaFEClient?.notifyBaudRateChanged(
parseInt(message.data, 10) as SerialConfig.BaudRate
);
break;
case SerialPlotter.Protocol.Command.PLOTTER_SET_LINE_ENDING:
this.theiaFEClient?.notifyLineEndingChanged(message.data);
break;
case SerialPlotter.Protocol.Command.PLOTTER_SET_INTERPOLATE:
this.theiaFEClient?.notifyInterpolateChanged(message.data);
break;
default:
break;
}
} catch (error) { }
}
);
// empty the queue every 32ms (~30fps)
this.flushMessagesInterval = setInterval(flushMessagesToFrontend, 32);
duplex.on(
'data',
((resp: StreamingOpenResponse) => {
const raw = resp.getData();
const message =
typeof raw === 'string' ? raw : new TextDecoder('utf8').decode(raw);
// split the message if it contains more lines
const messages = stringToArray(message);
this.messages.push(...messages);
}).bind(this)
);
const { type, port } = this.serialConfig;
const req = new StreamingOpenRequest();
const monitorConfig = new GrpcMonitorConfig();
monitorConfig.setType(this.mapType(type));
monitorConfig.setTarget(port.address);
if (this.serialConfig.baudRate !== undefined) {
monitorConfig.setAdditionalConfig(
Struct.fromJavaScript({ BaudRate: this.serialConfig.baudRate })
);
}
req.setConfig(monitorConfig);
if (!this.serialConnection) {
return await this.disconnect();
}
const writeTimeout = new Promise<Status>((resolve) => {
setTimeout(async () => {
resolve(Status.NOT_CONNECTED);
}, 1000);
});
const writePromise = (serialConnection: any) => {
return new Promise<Status>((resolve) => {
serialConnection.duplex.write(req, () => {
const boardName = this.serialConfig?.board
? Board.toString(this.serialConfig.board, {
useFqbn: false,
})
: 'unknown board';
const portName = this.serialConfig?.port
? this.serialConfig.port.address
: 'unknown port';
this.logger.info(
`<<< Serial connection created for ${boardName} on port ${portName}.`
);
resolve(Status.OK);
});
});
};
const status = await Promise.race([
writeTimeout,
writePromise(this.serialConnection),
]);
if (status === Status.NOT_CONNECTED) {
this.disconnect();
}
return status;
}
public async disconnect(reason?: SerialError): Promise<Status> {
return new Promise<Status>((resolve) => {
try {
if (this.onMessageReceived) {
this.onMessageReceived.dispose();
this.onMessageReceived = null;
}
if (this.flushMessagesInterval) {
clearInterval(this.flushMessagesInterval);
this.flushMessagesInterval = null;
}
if (
!this.serialConnection &&
reason &&
reason.code === SerialError.ErrorCodes.CLIENT_CANCEL
) {
resolve(Status.OK);
return;
}
this.logger.info('>>> Disposing serial connection...');
if (!this.serialConnection) {
this.logger.warn('<<< Not connected. Nothing to dispose.');
resolve(Status.NOT_CONNECTED);
return;
}
const { duplex, config } = this.serialConnection;
this.logger.info(
`<<< Disposed serial connection for ${Board.toString(config.board, {
useFqbn: false,
})} on port ${config.port.address}.`
);
duplex.cancel();
} finally {
this.serialConnection = undefined;
this.updateWsConfigParam({ connected: !!this.serialConnection });
this.messages.length = 0;
setTimeout(() => {
resolve(Status.OK);
}, 200);
}
});
}
async sendMessageToSerial(message: string): Promise<Status> {
if (!this.serialConnection) {
return Status.NOT_CONNECTED;
}
const req = new StreamingOpenRequest();
req.setData(new TextEncoder().encode(message));
return new Promise<Status>((resolve) => {
if (this.serialConnection) {
this.serialConnection.duplex.write(req, () => {
resolve(Status.OK);
});
return;
}
this.disconnect().then(() => resolve(Status.NOT_CONNECTED));
});
}
protected mapType(
type?: SerialConfig.ConnectionType
): GrpcMonitorConfig.TargetType {
switch (type) {
case SerialConfig.ConnectionType.SERIAL:
return GrpcMonitorConfig.TargetType.TARGET_TYPE_SERIAL;
default:
return GrpcMonitorConfig.TargetType.TARGET_TYPE_SERIAL;
}
}
}
// converts 'ab\nc\nd' => [ab\n,c\n,d]
function stringToArray(string: string, separator = '\n') {
const retArray: string[] = [];
let prevChar = separator;
for (let i = 0; i < string.length; i++) {
const currChar = string[i];
if (prevChar === separator) {
retArray.push(currChar);
} else {
const lastWord = retArray[retArray.length - 1];
retArray[retArray.length - 1] = lastWord + currChar;
}
prevChar = currChar;
}
return retArray;
}

View File

@@ -1,10 +1,10 @@
import { Emitter } from '@theia/core';
import { injectable } from 'inversify';
import { injectable } from '@theia/core/shared/inversify';
import * as WebSocket from 'ws';
import { WebSocketService } from './web-socket-service';
import { WebSocketProvider } from './web-socket-provider';
@injectable()
export default class WebSocketServiceImpl implements WebSocketService {
export default class WebSocketProviderImpl implements WebSocketProvider {
protected wsClients: WebSocket[];
protected server: WebSocket.Server;

View File

@@ -1,8 +1,8 @@
import { Event } from '@theia/core/lib/common/event';
import * as WebSocket from 'ws';
export const WebSocketService = Symbol('WebSocketService');
export interface WebSocketService {
export const WebSocketProvider = Symbol('WebSocketProvider');
export interface WebSocketProvider {
getAddress(): WebSocket.AddressInfo;
sendMessage(message: string): void;
onMessageReceived: Event<string>;

View File

@@ -1,22 +0,0 @@
import { SerialConfig } from '../../../common/protocol/serial-service';
import { aBoard, anotherBoard, anotherPort, aPort } from './boards';
export const aSerialConfig: SerialConfig = {
board: aBoard,
port: aPort,
baudRate: 9600,
};
export const anotherSerialConfig: SerialConfig = {
board: anotherBoard,
port: anotherPort,
baudRate: 9600,
};
export class WebSocketMock {
readonly url: string;
constructor(url: string) {
this.url = url;
}
close() {}
}

View File

@@ -1,167 +0,0 @@
import { SerialServiceImpl } from './../../node/serial/serial-service-impl';
import { IMock, It, Mock } from 'typemoq';
import { createSandbox } from 'sinon';
import * as sinonChai from 'sinon-chai';
import { expect, use } from 'chai';
use(sinonChai);
import { ILogger } from '@theia/core/lib/common/logger';
import { MonitorClientProvider } from '../../node/serial/monitor-client-provider';
import { 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: { id: 'test|test', address: 'test', addressLabel: 'test', protocol: 'test', protocolLabel: 'test' },
});
});
afterEach(function () {
sandbox.restore();
subject.dispose();
});
it('should disconnect from previous connection', async () => {
expect(subject.disconnect).to.be.called;
});
it('should create the serialConnection', async () => {
const serialConnectionOpen = await subject.isSerialPortOpen();
expect(serialConnectionOpen).to.be.true;
});
});
});