mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-07-08 20:06:32 +00:00
Pluggable monitor (#982)
* backend structure WIP * Scaffold interfaces and classes for pluggable monitors * Implement MonitorService to handle pluggable monitor lifetime * Rename WebSocketService to WebSocketProvider and uninjected it * Moved some interfaces * Changed upload settings * Enhance MonitorManager APIs * Fixed WebSocketChange event signature * Add monitor proxy functions for the frontend * Moved settings to MonitorService * Remove several unnecessary serial monitor classes * Changed how connection is handled on upload * Proxied more monitor methods to frontend * WebSocketProvider is not injectable anymore * Add generic monitor settings storaging * More serial classes removal * Remove unused file * Changed plotter contribution to use new manager proxy * Changed MonitorWidget and children to use new monitor proxy * Updated MonitorWidget to use new monitor proxy * Fix backend logger bindings * Delete unnecessary Symbol * coreClientProvider is now set when constructing MonitorService * Add missing binding * Fix `MonitorManagerProxy` DI issue * fix monitor connection * delete duplex when connection is closed * update arduino-cli to 0.22.0 * fix upload when monitor is open * add MonitorSettingsProvider interface * monitor settings provider stub * updated pseudo code * refactor monitor settings interfaces * monitor service provider singleton * add unit tests * change MonitorService providers to injectable deps * fix monitor settings client communication * refactor monitor commands protocol * use monitor settings provider properly * add settings to monitor model * add settings to monitor model * reset serial monitor when port changes * fix serial plotter opening * refine monitor connection settings * fix hanging web socket connections * add serial plotter reset command * send port to web socket clients * monitor service wait for success serial port open * fix reset loop * update serial plotter version * update arduino-cli version to 0.23.0-rc1 and regenerate grpc protocol * remove useless plotter protocol file * localize web socket errors * clean-up code * update translation file * Fix duplicated editor tabs (#1012) * Save dialog for closing temporary sketch and unsaved files (#893) * Use normal `OnWillStop` event * Align `CLOSE` command to rest of app * Fixed FS path vs encoded URL comparision when handling stop request. Ref: https://github.com/eclipse-theia/theia/issues/11226 Signed-off-by: Akos Kitta <a.kitta@arduino.cc> * Fixed the translations. Signed-off-by: Akos Kitta <a.kitta@arduino.cc> * Fixed the translations again. Removed `electron` from the `nls-extract`. It does not contain app code. Signed-off-by: Akos Kitta <a.kitta@arduino.cc> * Aligned the stop handler code to Theia. Signed-off-by: Akos Kitta <a.kitta@arduino.cc> Co-authored-by: Akos Kitta <a.kitta@arduino.cc> * fix serial monitor send line ending * refactor monitor-service poll for test/readability * localize web socket errors * update translation file * Fix duplicated editor tabs (#1012) * i18n:check rerun * Speed up IDE startup time. Signed-off-by: Akos Kitta <a.kitta@arduino.cc> * override coreClientProvider in monitor-service * cleanup merged code Co-authored-by: Francesco Stasi <f.stasi@me.com> Co-authored-by: Silvano Cerza <silvanocerza@gmail.com> Co-authored-by: Mark Sujew <mark.sujew@typefox.io> Co-authored-by: David Simpson <45690499+davegarthsimpson@users.noreply.github.com> Co-authored-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
parent
4c55807392
commit
df8658eff9
@ -57,7 +57,7 @@
|
|||||||
"@types/temp": "^0.8.34",
|
"@types/temp": "^0.8.34",
|
||||||
"@types/which": "^1.3.1",
|
"@types/which": "^1.3.1",
|
||||||
"ajv": "^6.5.3",
|
"ajv": "^6.5.3",
|
||||||
"arduino-serial-plotter-webapp": "0.0.17",
|
"arduino-serial-plotter-webapp": "0.1.0",
|
||||||
"async-mutex": "^0.3.0",
|
"async-mutex": "^0.3.0",
|
||||||
"atob": "^2.1.2",
|
"atob": "^2.1.2",
|
||||||
"auth0-js": "^9.14.0",
|
"auth0-js": "^9.14.0",
|
||||||
|
@ -68,20 +68,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 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 { SearchInWorkspaceFrontendContribution } from './theia/search-in-workspace/search-in-workspace-frontend-contribution';
|
||||||
import { LibraryListWidgetFrontendContribution } from './library/library-widget-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 {
|
import {
|
||||||
ConfigService,
|
ConfigService,
|
||||||
ConfigServicePath,
|
ConfigServicePath,
|
||||||
} from '../common/protocol/config-service';
|
} from '../common/protocol/config-service';
|
||||||
import { MonitorWidget } from './serial/monitor/monitor-widget';
|
import { MonitorWidget } from './serial/monitor/monitor-widget';
|
||||||
import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution';
|
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 as TheiaTabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
|
||||||
import { TabBarDecoratorService } from './theia/core/tab-bar-decorator';
|
import { TabBarDecoratorService } from './theia/core/tab-bar-decorator';
|
||||||
import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browser';
|
import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browser';
|
||||||
@ -158,7 +150,14 @@ import {
|
|||||||
OutputChannelRegistryMainImpl as TheiaOutputChannelRegistryMainImpl,
|
OutputChannelRegistryMainImpl as TheiaOutputChannelRegistryMainImpl,
|
||||||
OutputChannelRegistryMainImpl,
|
OutputChannelRegistryMainImpl,
|
||||||
} from './theia/plugin-ext/output-channel-registry-main';
|
} 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 as TheiaMonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
|
||||||
import { MonacoTextModelService } from './theia/monaco/monaco-text-model-service';
|
import { MonacoTextModelService } from './theia/monaco/monaco-text-model-service';
|
||||||
import { ResponseServiceImpl } from './response-service-impl';
|
import { ResponseServiceImpl } from './response-service-impl';
|
||||||
@ -273,6 +272,8 @@ import {
|
|||||||
IDEUpdaterDialogWidget,
|
IDEUpdaterDialogWidget,
|
||||||
} from './dialogs/ide-updater/ide-updater-dialog';
|
} from './dialogs/ide-updater/ide-updater-dialog';
|
||||||
import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider';
|
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';
|
||||||
import { EditorManager as TheiaEditorManager } from '@theia/editor/lib/browser/editor-manager';
|
import { EditorManager as TheiaEditorManager } from '@theia/editor/lib/browser/editor-manager';
|
||||||
import { EditorManager } from './theia/editor/editor-manager';
|
import { EditorManager } from './theia/editor/editor-manager';
|
||||||
import { HostedPluginEvents } from './hosted-plugin-events';
|
import { HostedPluginEvents } from './hosted-plugin-events';
|
||||||
@ -424,29 +425,44 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
.inSingletonScope();
|
.inSingletonScope();
|
||||||
|
|
||||||
// Serial monitor
|
// Serial monitor
|
||||||
bind(SerialModel).toSelf().inSingletonScope();
|
|
||||||
bind(FrontendApplicationContribution).toService(SerialModel);
|
|
||||||
bind(MonitorWidget).toSelf();
|
bind(MonitorWidget).toSelf();
|
||||||
|
bind(FrontendApplicationContribution).toService(MonitorModel);
|
||||||
|
bind(MonitorModel).toSelf().inSingletonScope();
|
||||||
bindViewContribution(bind, MonitorViewContribution);
|
bindViewContribution(bind, MonitorViewContribution);
|
||||||
bind(TabBarToolbarContribution).toService(MonitorViewContribution);
|
bind(TabBarToolbarContribution).toService(MonitorViewContribution);
|
||||||
bind(WidgetFactory).toDynamicValue((context) => ({
|
bind(WidgetFactory).toDynamicValue((context) => ({
|
||||||
id: MonitorWidget.ID,
|
id: MonitorWidget.ID,
|
||||||
createWidget: () => context.container.get(MonitorWidget),
|
createWidget: () => {
|
||||||
}));
|
return new MonitorWidget(
|
||||||
// Frontend binding for the serial service
|
context.container.get<MonitorModel>(MonitorModel),
|
||||||
bind(SerialService)
|
context.container.get<MonitorManagerProxyClient>(
|
||||||
.toDynamicValue((context) => {
|
MonitorManagerProxyClient
|
||||||
const connection = context.container.get(WebSocketConnectionProvider);
|
),
|
||||||
const client = context.container.get<SerialServiceClient>(
|
context.container.get<BoardsServiceProvider>(BoardsServiceProvider)
|
||||||
SerialServiceClient
|
|
||||||
);
|
);
|
||||||
return connection.createProxy(SerialServicePath, client);
|
},
|
||||||
})
|
}));
|
||||||
.inSingletonScope();
|
|
||||||
bind(SerialConnectionManager).toSelf().inSingletonScope();
|
|
||||||
|
|
||||||
// Serial service client to receive and delegate notifications from the backend.
|
bind(MonitorManagerProxyFactory).toFactory(
|
||||||
bind(SerialServiceClient).to(SerialServiceClientImpl).inSingletonScope();
|
(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();
|
bind(WorkspaceService).toSelf().inSingletonScope();
|
||||||
rebind(TheiaWorkspaceService).toService(WorkspaceService);
|
rebind(TheiaWorkspaceService).toService(WorkspaceService);
|
||||||
@ -502,11 +518,12 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
.inSingletonScope();
|
.inSingletonScope();
|
||||||
rebind(TheiaEditorWidgetFactory).to(EditorWidgetFactory).inSingletonScope();
|
rebind(TheiaEditorWidgetFactory).to(EditorWidgetFactory).inSingletonScope();
|
||||||
rebind(TabBarToolbarFactory).toFactory(
|
rebind(TabBarToolbarFactory).toFactory(
|
||||||
({ container: parentContainer }) => () => {
|
({ container: parentContainer }) =>
|
||||||
const container = parentContainer.createChild();
|
() => {
|
||||||
container.bind(TabBarToolbar).toSelf().inSingletonScope();
|
const container = parentContainer.createChild();
|
||||||
return container.get(TabBarToolbar);
|
container.bind(TabBarToolbar).toSelf().inSingletonScope();
|
||||||
}
|
return container.get(TabBarToolbar);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
bind(OutputWidget).toSelf().inSingletonScope();
|
bind(OutputWidget).toSelf().inSingletonScope();
|
||||||
rebind(TheiaOutputWidget).toService(OutputWidget);
|
rebind(TheiaOutputWidget).toService(OutputWidget);
|
||||||
@ -523,7 +540,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
|
|
||||||
bind(SearchInWorkspaceWidget).toSelf();
|
bind(SearchInWorkspaceWidget).toSelf();
|
||||||
rebind(TheiaSearchInWorkspaceWidget).toService(SearchInWorkspaceWidget);
|
rebind(TheiaSearchInWorkspaceWidget).toService(SearchInWorkspaceWidget);
|
||||||
|
|
||||||
rebind(TheiaEditorManager).to(EditorManager);
|
rebind(TheiaEditorManager).to(EditorManager);
|
||||||
|
|
||||||
// replace search icon
|
// replace search icon
|
||||||
@ -560,9 +577,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
bind(ProblemManager).toSelf().inSingletonScope();
|
bind(ProblemManager).toSelf().inSingletonScope();
|
||||||
rebind(TheiaProblemManager).toService(ProblemManager);
|
rebind(TheiaProblemManager).toService(ProblemManager);
|
||||||
|
|
||||||
// Customized layout restorer that can restore the state in async way: https://github.com/eclipse-theia/theia/issues/6579
|
// Customized layout restorer that can restore the state in async way: https://github.com/eclipse-theia/theia/issues/6579
|
||||||
bind(ShellLayoutRestorer).toSelf().inSingletonScope();
|
bind(ShellLayoutRestorer).toSelf().inSingletonScope();
|
||||||
rebind(TheiaShellLayoutRestorer).toService(ShellLayoutRestorer);
|
rebind(TheiaShellLayoutRestorer).toService(ShellLayoutRestorer);
|
||||||
|
|
||||||
// No dropdown for the _Output_ view.
|
// No dropdown for the _Output_ view.
|
||||||
bind(OutputToolbarContribution).toSelf().inSingletonScope();
|
bind(OutputToolbarContribution).toSelf().inSingletonScope();
|
||||||
@ -687,15 +704,13 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
|
|
||||||
// Enable the dirty indicator on uncloseable widgets.
|
// Enable the dirty indicator on uncloseable widgets.
|
||||||
rebind(TabBarRendererFactory).toFactory((context) => () => {
|
rebind(TabBarRendererFactory).toFactory((context) => () => {
|
||||||
const contextMenuRenderer = context.container.get<ContextMenuRenderer>(
|
const contextMenuRenderer =
|
||||||
ContextMenuRenderer
|
context.container.get<ContextMenuRenderer>(ContextMenuRenderer);
|
||||||
);
|
|
||||||
const decoratorService = context.container.get<TabBarDecoratorService>(
|
const decoratorService = context.container.get<TabBarDecoratorService>(
|
||||||
TabBarDecoratorService
|
TabBarDecoratorService
|
||||||
);
|
);
|
||||||
const iconThemeService = context.container.get<IconThemeService>(
|
const iconThemeService =
|
||||||
IconThemeService
|
context.container.get<IconThemeService>(IconThemeService);
|
||||||
);
|
|
||||||
return new TabBarRenderer(
|
return new TabBarRenderer(
|
||||||
contextMenuRenderer,
|
contextMenuRenderer,
|
||||||
decoratorService,
|
decoratorService,
|
||||||
|
@ -3,7 +3,6 @@ import { OutputChannelManager } from '@theia/output/lib/browser/output-channel';
|
|||||||
import { CoreService } from '../../common/protocol';
|
import { CoreService } from '../../common/protocol';
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
import { BoardsDataStore } from '../boards/boards-data-store';
|
import { BoardsDataStore } from '../boards/boards-data-store';
|
||||||
import { SerialConnectionManager } from '../serial/serial-connection-manager';
|
|
||||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||||
import {
|
import {
|
||||||
SketchContribution,
|
SketchContribution,
|
||||||
@ -18,8 +17,6 @@ export class BurnBootloader extends SketchContribution {
|
|||||||
@inject(CoreService)
|
@inject(CoreService)
|
||||||
protected readonly coreService: CoreService;
|
protected readonly coreService: CoreService;
|
||||||
|
|
||||||
@inject(SerialConnectionManager)
|
|
||||||
protected readonly serialConnection: SerialConnectionManager;
|
|
||||||
|
|
||||||
@inject(BoardsDataStore)
|
@inject(BoardsDataStore)
|
||||||
protected readonly boardsDataStore: 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.verify'),
|
||||||
this.preferences.get('arduino.upload.verbose'),
|
this.preferences.get('arduino.upload.verbose'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const board = {
|
||||||
|
...boardsConfig.selectedBoard,
|
||||||
|
name: boardsConfig.selectedBoard?.name || '',
|
||||||
|
fqbn,
|
||||||
|
}
|
||||||
this.outputChannelManager.getChannel('Arduino').clear();
|
this.outputChannelManager.getChannel('Arduino').clear();
|
||||||
await this.coreService.burnBootloader({
|
await this.coreService.burnBootloader({
|
||||||
fqbn,
|
board,
|
||||||
programmer,
|
programmer,
|
||||||
port,
|
port,
|
||||||
verify,
|
verify,
|
||||||
@ -85,8 +88,6 @@ export class BurnBootloader extends SketchContribution {
|
|||||||
errorMessage = e.toString();
|
errorMessage = e.toString();
|
||||||
}
|
}
|
||||||
this.messageService.error(errorMessage);
|
this.messageService.error(errorMessage);
|
||||||
} finally {
|
|
||||||
await this.serialConnection.reconnectAfterUpload();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import { BoardUserField, CoreService } from '../../common/protocol';
|
|||||||
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
|
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
|
||||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||||
import { BoardsDataStore } from '../boards/boards-data-store';
|
import { BoardsDataStore } from '../boards/boards-data-store';
|
||||||
import { SerialConnectionManager } from '../serial/serial-connection-manager';
|
|
||||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||||
import {
|
import {
|
||||||
SketchContribution,
|
SketchContribution,
|
||||||
@ -23,9 +22,6 @@ export class UploadSketch extends SketchContribution {
|
|||||||
@inject(CoreService)
|
@inject(CoreService)
|
||||||
protected readonly coreService: CoreService;
|
protected readonly coreService: CoreService;
|
||||||
|
|
||||||
@inject(SerialConnectionManager)
|
|
||||||
protected readonly serialConnection: SerialConnectionManager;
|
|
||||||
|
|
||||||
@inject(MenuModelRegistry)
|
@inject(MenuModelRegistry)
|
||||||
protected readonly menuRegistry: MenuModelRegistry;
|
protected readonly menuRegistry: MenuModelRegistry;
|
||||||
|
|
||||||
@ -227,6 +223,11 @@ export class UploadSketch extends SketchContribution {
|
|||||||
this.sourceOverride(),
|
this.sourceOverride(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const board = {
|
||||||
|
...boardsConfig.selectedBoard,
|
||||||
|
name: boardsConfig.selectedBoard?.name || '',
|
||||||
|
fqbn,
|
||||||
|
}
|
||||||
let options: CoreService.Upload.Options | undefined = undefined;
|
let options: CoreService.Upload.Options | undefined = undefined;
|
||||||
const sketchUri = sketch.uri;
|
const sketchUri = sketch.uri;
|
||||||
const optimizeForDebug = this.editorMode.compileForDebug;
|
const optimizeForDebug = this.editorMode.compileForDebug;
|
||||||
@ -248,7 +249,7 @@ export class UploadSketch extends SketchContribution {
|
|||||||
const programmer = selectedProgrammer;
|
const programmer = selectedProgrammer;
|
||||||
options = {
|
options = {
|
||||||
sketchUri,
|
sketchUri,
|
||||||
fqbn,
|
board,
|
||||||
optimizeForDebug,
|
optimizeForDebug,
|
||||||
programmer,
|
programmer,
|
||||||
port,
|
port,
|
||||||
@ -260,7 +261,7 @@ export class UploadSketch extends SketchContribution {
|
|||||||
} else {
|
} else {
|
||||||
options = {
|
options = {
|
||||||
sketchUri,
|
sketchUri,
|
||||||
fqbn,
|
board,
|
||||||
optimizeForDebug,
|
optimizeForDebug,
|
||||||
port,
|
port,
|
||||||
verbose,
|
verbose,
|
||||||
@ -290,8 +291,6 @@ export class UploadSketch extends SketchContribution {
|
|||||||
} finally {
|
} finally {
|
||||||
this.uploadInProgress = false;
|
this.uploadInProgress = false;
|
||||||
this.onDidChangeEmitter.fire();
|
this.onDidChangeEmitter.fire();
|
||||||
|
|
||||||
setTimeout(() => this.serialConnection.reconnectAfterUpload(), 5000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,12 +111,17 @@ export class VerifySketch extends SketchContribution {
|
|||||||
),
|
),
|
||||||
this.sourceOverride(),
|
this.sourceOverride(),
|
||||||
]);
|
]);
|
||||||
|
const board = {
|
||||||
|
...boardsConfig.selectedBoard,
|
||||||
|
name: boardsConfig.selectedBoard?.name || '',
|
||||||
|
fqbn,
|
||||||
|
}
|
||||||
const verbose = this.preferences.get('arduino.compile.verbose');
|
const verbose = this.preferences.get('arduino.compile.verbose');
|
||||||
const compilerWarnings = this.preferences.get('arduino.compile.warnings');
|
const compilerWarnings = this.preferences.get('arduino.compile.warnings');
|
||||||
this.outputChannelManager.getChannel('Arduino').clear();
|
this.outputChannelManager.getChannel('Arduino').clear();
|
||||||
await this.coreService.compile({
|
await this.coreService.compile({
|
||||||
sketchUri: sketch.uri,
|
sketchUri: sketch.uri,
|
||||||
fqbn,
|
board,
|
||||||
optimizeForDebug: this.editorMode.compileForDebug,
|
optimizeForDebug: this.editorMode.compileForDebug,
|
||||||
verbose,
|
verbose,
|
||||||
exportBinaries,
|
exportBinaries,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { nls } from '@theia/core/lib/common';
|
import { nls } from '@theia/core/lib/common';
|
||||||
import * as React from '@theia/core/shared/react';
|
import * as React from '@theia/core/shared/react';
|
||||||
|
import { Port } from '../../../common/protocol';
|
||||||
import {
|
import {
|
||||||
ArduinoFirmwareUploader,
|
ArduinoFirmwareUploader,
|
||||||
FirmwareInfo,
|
FirmwareInfo,
|
||||||
@ -20,7 +21,7 @@ export const FirmwareUploaderComponent = ({
|
|||||||
availableBoards: AvailableBoard[];
|
availableBoards: AvailableBoard[];
|
||||||
firmwareUploader: ArduinoFirmwareUploader;
|
firmwareUploader: ArduinoFirmwareUploader;
|
||||||
updatableFqbns: string[];
|
updatableFqbns: string[];
|
||||||
flashFirmware: (firmware: FirmwareInfo, port: string) => Promise<any>;
|
flashFirmware: (firmware: FirmwareInfo, port: Port) => Promise<any>;
|
||||||
isOpen: any;
|
isOpen: any;
|
||||||
}): React.ReactElement => {
|
}): React.ReactElement => {
|
||||||
// boolean states for buttons
|
// boolean states for buttons
|
||||||
@ -81,7 +82,7 @@ export const FirmwareUploaderComponent = ({
|
|||||||
const installStatus =
|
const installStatus =
|
||||||
!!firmwareToFlash &&
|
!!firmwareToFlash &&
|
||||||
!!selectedBoard?.port &&
|
!!selectedBoard?.port &&
|
||||||
(await flashFirmware(firmwareToFlash, selectedBoard?.port.address));
|
(await flashFirmware(firmwareToFlash, selectedBoard?.port));
|
||||||
|
|
||||||
setInstallFeedback((installStatus && 'ok') || 'fail');
|
setInstallFeedback((installStatus && 'ok') || 'fail');
|
||||||
} catch {
|
} catch {
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import * as React from '@theia/core/shared/react';
|
import * as React from '@theia/core/shared/react';
|
||||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
import {
|
||||||
|
inject,
|
||||||
|
injectable,
|
||||||
|
postConstruct,
|
||||||
|
} from '@theia/core/shared/inversify';
|
||||||
import { DialogProps } from '@theia/core/lib/browser/dialogs';
|
import { DialogProps } from '@theia/core/lib/browser/dialogs';
|
||||||
import { AbstractDialog } from '../../theia/dialogs/dialogs';
|
import { AbstractDialog } from '../../theia/dialogs/dialogs';
|
||||||
import { Widget } from '@theia/core/shared/@phosphor/widgets';
|
import { Widget } from '@theia/core/shared/@phosphor/widgets';
|
||||||
@ -15,6 +19,7 @@ import {
|
|||||||
} from '../../../common/protocol/arduino-firmware-uploader';
|
} from '../../../common/protocol/arduino-firmware-uploader';
|
||||||
import { FirmwareUploaderComponent } from './firmware-uploader-component';
|
import { FirmwareUploaderComponent } from './firmware-uploader-component';
|
||||||
import { UploadFirmware } from '../../contributions/upload-firmware';
|
import { UploadFirmware } from '../../contributions/upload-firmware';
|
||||||
|
import { Port } from '../../../common/protocol';
|
||||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
@ -54,7 +59,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);
|
this.busyCallback(true);
|
||||||
return this.arduinoFirmwareUploader
|
return this.arduinoFirmwareUploader
|
||||||
.flash(firmware, port)
|
.flash(firmware, port)
|
||||||
|
@ -0,0 +1,199 @@
|
|||||||
|
import {
|
||||||
|
CommandRegistry,
|
||||||
|
Disposable,
|
||||||
|
Emitter,
|
||||||
|
MessageService,
|
||||||
|
nls,
|
||||||
|
} 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';
|
||||||
|
import { BoardsConfig } from './boards/boards-config';
|
||||||
|
import { BoardsServiceProvider } from './boards/boards-service-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 onMonitorSettingsDidChangeEmitter =
|
||||||
|
new Emitter<MonitorSettings>();
|
||||||
|
readonly onMonitorSettingsDidChange =
|
||||||
|
this.onMonitorSettingsDidChangeEmitter.event;
|
||||||
|
|
||||||
|
protected readonly onMonitorShouldResetEmitter = new Emitter();
|
||||||
|
readonly onMonitorShouldReset = this.onMonitorShouldResetEmitter.event;
|
||||||
|
|
||||||
|
// WebSocket used to handle pluggable monitor communication between
|
||||||
|
// frontend and backend.
|
||||||
|
private webSocket?: WebSocket;
|
||||||
|
private wsPort?: number;
|
||||||
|
private lastConnectedBoard: BoardsConfig.Config;
|
||||||
|
private onBoardsConfigChanged: Disposable | undefined;
|
||||||
|
|
||||||
|
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,
|
||||||
|
|
||||||
|
@inject(CommandRegistry)
|
||||||
|
protected readonly commandRegistry: CommandRegistry,
|
||||||
|
|
||||||
|
@inject(BoardsServiceProvider)
|
||||||
|
protected readonly boardsServiceProvider: BoardsServiceProvider
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connects a localhost WebSocket using the specified port.
|
||||||
|
* @param addressPort port of the WebSocket
|
||||||
|
*/
|
||||||
|
async connect(addressPort: number): Promise<void> {
|
||||||
|
if (!!this.webSocket) {
|
||||||
|
if (this.wsPort === addressPort) return;
|
||||||
|
else this.disconnect();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.webSocket = new WebSocket(`ws://localhost:${addressPort}`);
|
||||||
|
} catch {
|
||||||
|
this.messageService.error(
|
||||||
|
nls.localize(
|
||||||
|
'arduino/monitor/unableToConnectToWebSocket',
|
||||||
|
'Unable to connect to websocket'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.webSocket.onmessage = (message) => {
|
||||||
|
const parsedMessage = JSON.parse(message.data);
|
||||||
|
if (Array.isArray(parsedMessage))
|
||||||
|
this.onMessagesReceivedEmitter.fire({ messages: parsedMessage });
|
||||||
|
else if (
|
||||||
|
parsedMessage.command ===
|
||||||
|
Monitor.MiddlewareCommand.ON_SETTINGS_DID_CHANGE
|
||||||
|
) {
|
||||||
|
this.onMonitorSettingsDidChangeEmitter.fire(parsedMessage.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.wsPort = addressPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnects the WebSocket if connected.
|
||||||
|
*/
|
||||||
|
disconnect(): void {
|
||||||
|
if (!this.webSocket) return;
|
||||||
|
this.onBoardsConfigChanged?.dispose();
|
||||||
|
this.onBoardsConfigChanged = undefined;
|
||||||
|
try {
|
||||||
|
this.webSocket?.close();
|
||||||
|
this.webSocket = undefined;
|
||||||
|
} catch {
|
||||||
|
this.messageService.error(
|
||||||
|
nls.localize(
|
||||||
|
'arduino/monitor/unableToCloseWebSocket',
|
||||||
|
'Unable to close websocket'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async isWSConnected(): Promise<boolean> {
|
||||||
|
return !!this.webSocket;
|
||||||
|
}
|
||||||
|
|
||||||
|
async startMonitor(settings?: PluggableMonitorSettings): Promise<void> {
|
||||||
|
this.lastConnectedBoard = {
|
||||||
|
selectedBoard: this.boardsServiceProvider.boardsConfig.selectedBoard,
|
||||||
|
selectedPort: this.boardsServiceProvider.boardsConfig.selectedPort,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!this.onBoardsConfigChanged) {
|
||||||
|
this.onBoardsConfigChanged =
|
||||||
|
this.boardsServiceProvider.onBoardsConfigChanged(
|
||||||
|
async ({ selectedBoard, selectedPort }) => {
|
||||||
|
if (
|
||||||
|
typeof selectedBoard === 'undefined' ||
|
||||||
|
typeof selectedPort === 'undefined'
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// a board is plugged and it's different from the old connected board
|
||||||
|
if (
|
||||||
|
selectedBoard?.fqbn !==
|
||||||
|
this.lastConnectedBoard?.selectedBoard?.fqbn ||
|
||||||
|
selectedPort?.id !== this.lastConnectedBoard?.selectedPort?.id
|
||||||
|
) {
|
||||||
|
this.onMonitorShouldResetEmitter.fire(null);
|
||||||
|
this.lastConnectedBoard = {
|
||||||
|
selectedBoard: selectedBoard,
|
||||||
|
selectedPort: selectedPort,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// a board is plugged and it's the same as prev, rerun "this.startMonitor" to
|
||||||
|
// recreate the listener callback
|
||||||
|
this.startMonitor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { selectedBoard, selectedPort } =
|
||||||
|
this.boardsServiceProvider.boardsConfig;
|
||||||
|
if (!selectedBoard || !selectedBoard.fqbn || !selectedPort) return;
|
||||||
|
await this.server().startMonitor(selectedBoard, selectedPort, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentSettings(board: Board, port: Port): Promise<MonitorSettings> {
|
||||||
|
return this.server().getCurrentSettings(board, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
send(message: string): void {
|
||||||
|
if (!this.webSocket) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.webSocket.send(
|
||||||
|
JSON.stringify({
|
||||||
|
command: Monitor.ClientCommand.SEND_MESSAGE,
|
||||||
|
data: message,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
changeSettings(settings: MonitorSettings): void {
|
||||||
|
if (!this.webSocket) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.webSocket.send(
|
||||||
|
JSON.stringify({
|
||||||
|
command: Monitor.ClientCommand.CHANGE_SETTINGS,
|
||||||
|
data: settings,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
278
arduino-ide-extension/src/browser/monitor-model.ts
Normal file
278
arduino-ide-extension/src/browser/monitor-model.ts
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
import { Emitter, Event } from '@theia/core';
|
||||||
|
import {
|
||||||
|
FrontendApplicationContribution,
|
||||||
|
LocalStorageService,
|
||||||
|
} from '@theia/core/lib/browser';
|
||||||
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
|
import { MonitorManagerProxyClient } from '../common/protocol';
|
||||||
|
import { isNullOrUndefined } from '../common/utils';
|
||||||
|
import { MonitorSettings } from '../node/monitor-settings/monitor-settings-provider';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class MonitorModel implements FrontendApplicationContribution {
|
||||||
|
protected static STORAGE_ID = 'arduino-monitor-model';
|
||||||
|
|
||||||
|
@inject(LocalStorageService)
|
||||||
|
protected readonly localStorageService: LocalStorageService;
|
||||||
|
|
||||||
|
@inject(MonitorManagerProxyClient)
|
||||||
|
protected readonly monitorManagerProxy: MonitorManagerProxyClient;
|
||||||
|
|
||||||
|
protected readonly onChangeEmitter: Emitter<
|
||||||
|
MonitorModel.State.Change<keyof MonitorModel.State>
|
||||||
|
>;
|
||||||
|
|
||||||
|
protected _autoscroll: boolean;
|
||||||
|
protected _timestamp: boolean;
|
||||||
|
protected _lineEnding: MonitorModel.EOL;
|
||||||
|
protected _interpolate: boolean;
|
||||||
|
protected _darkTheme: boolean;
|
||||||
|
protected _wsPort: number;
|
||||||
|
protected _serialPort: string;
|
||||||
|
protected _connected: boolean;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._autoscroll = true;
|
||||||
|
this._timestamp = false;
|
||||||
|
this._interpolate = false;
|
||||||
|
this._lineEnding = MonitorModel.EOL.DEFAULT;
|
||||||
|
this._darkTheme = false;
|
||||||
|
this._wsPort = 0;
|
||||||
|
this._serialPort = '';
|
||||||
|
this._connected = true;
|
||||||
|
|
||||||
|
this.onChangeEmitter = new Emitter<
|
||||||
|
MonitorModel.State.Change<keyof MonitorModel.State>
|
||||||
|
>();
|
||||||
|
}
|
||||||
|
|
||||||
|
onStart(): void {
|
||||||
|
this.localStorageService
|
||||||
|
.getData<MonitorModel.State>(MonitorModel.STORAGE_ID)
|
||||||
|
.then(this.restoreState.bind(this));
|
||||||
|
|
||||||
|
this.monitorManagerProxy.onMonitorSettingsDidChange(
|
||||||
|
this.onMonitorSettingsDidChange.bind(this)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
this._serialPort = state.serialPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async storeState(): Promise<void> {
|
||||||
|
return this.localStorageService.setData(MonitorModel.STORAGE_ID, {
|
||||||
|
autoscroll: this._autoscroll,
|
||||||
|
timestamp: this._timestamp,
|
||||||
|
lineEnding: this._lineEnding,
|
||||||
|
interpolate: this._interpolate,
|
||||||
|
serialPort: this._serialPort,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get autoscroll(): boolean {
|
||||||
|
return this._autoscroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
set autoscroll(autoscroll: boolean) {
|
||||||
|
if (autoscroll === this._autoscroll) return;
|
||||||
|
this._autoscroll = autoscroll;
|
||||||
|
this.monitorManagerProxy.changeSettings({
|
||||||
|
monitorUISettings: { autoscroll },
|
||||||
|
});
|
||||||
|
this.storeState().then(() => {
|
||||||
|
this.onChangeEmitter.fire({
|
||||||
|
property: 'autoscroll',
|
||||||
|
value: this._autoscroll,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleAutoscroll(): void {
|
||||||
|
this.autoscroll = !this._autoscroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
get timestamp(): boolean {
|
||||||
|
return this._timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
set timestamp(timestamp: boolean) {
|
||||||
|
if (timestamp === this._timestamp) return;
|
||||||
|
this._timestamp = timestamp;
|
||||||
|
this.monitorManagerProxy.changeSettings({
|
||||||
|
monitorUISettings: { timestamp },
|
||||||
|
});
|
||||||
|
this.storeState().then(() =>
|
||||||
|
this.onChangeEmitter.fire({
|
||||||
|
property: 'timestamp',
|
||||||
|
value: this._timestamp,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleTimestamp(): void {
|
||||||
|
this.timestamp = !this._timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
get lineEnding(): MonitorModel.EOL {
|
||||||
|
return this._lineEnding;
|
||||||
|
}
|
||||||
|
|
||||||
|
set lineEnding(lineEnding: MonitorModel.EOL) {
|
||||||
|
if (lineEnding === this._lineEnding) return;
|
||||||
|
this._lineEnding = lineEnding;
|
||||||
|
this.monitorManagerProxy.changeSettings({
|
||||||
|
monitorUISettings: { lineEnding },
|
||||||
|
});
|
||||||
|
this.storeState().then(() =>
|
||||||
|
this.onChangeEmitter.fire({
|
||||||
|
property: 'lineEnding',
|
||||||
|
value: this._lineEnding,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get interpolate(): boolean {
|
||||||
|
return this._interpolate;
|
||||||
|
}
|
||||||
|
|
||||||
|
set interpolate(interpolate: boolean) {
|
||||||
|
if (interpolate === this._interpolate) return;
|
||||||
|
this._interpolate = interpolate;
|
||||||
|
this.monitorManagerProxy.changeSettings({
|
||||||
|
monitorUISettings: { interpolate },
|
||||||
|
});
|
||||||
|
this.storeState().then(() =>
|
||||||
|
this.onChangeEmitter.fire({
|
||||||
|
property: 'interpolate',
|
||||||
|
value: this._interpolate,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get darkTheme(): boolean {
|
||||||
|
return this._darkTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
set darkTheme(darkTheme: boolean) {
|
||||||
|
if (darkTheme === this._darkTheme) return;
|
||||||
|
this._darkTheme = darkTheme;
|
||||||
|
this.monitorManagerProxy.changeSettings({
|
||||||
|
monitorUISettings: { darkTheme },
|
||||||
|
});
|
||||||
|
this.onChangeEmitter.fire({
|
||||||
|
property: 'darkTheme',
|
||||||
|
value: this._darkTheme,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get wsPort(): number {
|
||||||
|
return this._wsPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
set wsPort(wsPort: number) {
|
||||||
|
if (wsPort === this._wsPort) return;
|
||||||
|
this._wsPort = wsPort;
|
||||||
|
this.monitorManagerProxy.changeSettings({
|
||||||
|
monitorUISettings: { wsPort },
|
||||||
|
});
|
||||||
|
this.onChangeEmitter.fire({
|
||||||
|
property: 'wsPort',
|
||||||
|
value: this._wsPort,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get serialPort(): string {
|
||||||
|
return this._serialPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
set serialPort(serialPort: string) {
|
||||||
|
if (serialPort === this._serialPort) return;
|
||||||
|
this._serialPort = serialPort;
|
||||||
|
this.monitorManagerProxy.changeSettings({
|
||||||
|
monitorUISettings: { serialPort },
|
||||||
|
});
|
||||||
|
this.storeState().then(() =>
|
||||||
|
this.onChangeEmitter.fire({
|
||||||
|
property: 'serialPort',
|
||||||
|
value: this._serialPort,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get connected(): boolean {
|
||||||
|
return this._connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
set connected(connected: boolean) {
|
||||||
|
if (connected === this._connected) return;
|
||||||
|
this._connected = connected;
|
||||||
|
this.monitorManagerProxy.changeSettings({
|
||||||
|
monitorUISettings: { connected },
|
||||||
|
});
|
||||||
|
this.onChangeEmitter.fire({
|
||||||
|
property: 'connected',
|
||||||
|
value: this._connected,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onMonitorSettingsDidChange = (settings: MonitorSettings): void => {
|
||||||
|
const { monitorUISettings } = settings;
|
||||||
|
if (!monitorUISettings) return;
|
||||||
|
const {
|
||||||
|
autoscroll,
|
||||||
|
interpolate,
|
||||||
|
lineEnding,
|
||||||
|
timestamp,
|
||||||
|
darkTheme,
|
||||||
|
wsPort,
|
||||||
|
serialPort,
|
||||||
|
connected,
|
||||||
|
} = monitorUISettings;
|
||||||
|
|
||||||
|
if (!isNullOrUndefined(autoscroll)) this.autoscroll = autoscroll;
|
||||||
|
if (!isNullOrUndefined(interpolate)) this.interpolate = interpolate;
|
||||||
|
if (!isNullOrUndefined(lineEnding)) this.lineEnding = lineEnding;
|
||||||
|
if (!isNullOrUndefined(timestamp)) this.timestamp = timestamp;
|
||||||
|
if (!isNullOrUndefined(darkTheme)) this.darkTheme = darkTheme;
|
||||||
|
if (!isNullOrUndefined(wsPort)) this.wsPort = wsPort;
|
||||||
|
if (!isNullOrUndefined(serialPort)) this.serialPort = serialPort;
|
||||||
|
if (!isNullOrUndefined(connected)) this.connected = connected;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Move this to /common
|
||||||
|
export namespace MonitorModel {
|
||||||
|
export interface State {
|
||||||
|
autoscroll: boolean;
|
||||||
|
timestamp: boolean;
|
||||||
|
lineEnding: EOL;
|
||||||
|
interpolate: boolean;
|
||||||
|
darkTheme: boolean;
|
||||||
|
wsPort: number;
|
||||||
|
serialPort: string;
|
||||||
|
connected: boolean;
|
||||||
|
}
|
||||||
|
export namespace State {
|
||||||
|
export interface Change<K extends keyof State> {
|
||||||
|
readonly property: K;
|
||||||
|
readonly value: State[K];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EOL = '' | '\n' | '\r' | '\r\n';
|
||||||
|
export namespace EOL {
|
||||||
|
export const DEFAULT: EOL = '\n';
|
||||||
|
}
|
||||||
|
}
|
@ -8,9 +8,10 @@ import {
|
|||||||
TabBarToolbarRegistry,
|
TabBarToolbarRegistry,
|
||||||
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||||
import { ArduinoToolbar } from '../../toolbar/arduino-toolbar';
|
import { ArduinoToolbar } from '../../toolbar/arduino-toolbar';
|
||||||
import { SerialModel } from '../serial-model';
|
|
||||||
import { ArduinoMenus } from '../../menu/arduino-menus';
|
import { ArduinoMenus } from '../../menu/arduino-menus';
|
||||||
import { nls } from '@theia/core/lib/common';
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
import { MonitorModel } from '../../monitor-model';
|
||||||
|
import { MonitorManagerProxyClient } from '../../../common/protocol';
|
||||||
|
|
||||||
export namespace SerialMonitor {
|
export namespace SerialMonitor {
|
||||||
export namespace Commands {
|
export namespace Commands {
|
||||||
@ -47,10 +48,15 @@ export class MonitorViewContribution
|
|||||||
static readonly TOGGLE_SERIAL_MONITOR = MonitorWidget.ID + ':toggle';
|
static readonly TOGGLE_SERIAL_MONITOR = MonitorWidget.ID + ':toggle';
|
||||||
static readonly TOGGLE_SERIAL_MONITOR_TOOLBAR =
|
static readonly TOGGLE_SERIAL_MONITOR_TOOLBAR =
|
||||||
MonitorWidget.ID + ':toggle-toolbar';
|
MonitorWidget.ID + ':toggle-toolbar';
|
||||||
|
static readonly RESET_SERIAL_MONITOR = MonitorWidget.ID + ':reset';
|
||||||
|
|
||||||
@inject(SerialModel) protected readonly model: SerialModel;
|
constructor(
|
||||||
|
@inject(MonitorModel)
|
||||||
|
protected readonly model: MonitorModel,
|
||||||
|
|
||||||
constructor() {
|
@inject(MonitorManagerProxyClient)
|
||||||
|
protected readonly monitorManagerProxy: MonitorManagerProxyClient
|
||||||
|
) {
|
||||||
super({
|
super({
|
||||||
widgetId: MonitorWidget.ID,
|
widgetId: MonitorWidget.ID,
|
||||||
widgetName: MonitorWidget.LABEL,
|
widgetName: MonitorWidget.LABEL,
|
||||||
@ -60,6 +66,7 @@ export class MonitorViewContribution
|
|||||||
toggleCommandId: MonitorViewContribution.TOGGLE_SERIAL_MONITOR,
|
toggleCommandId: MonitorViewContribution.TOGGLE_SERIAL_MONITOR,
|
||||||
toggleKeybinding: 'CtrlCmd+Shift+M',
|
toggleKeybinding: 'CtrlCmd+Shift+M',
|
||||||
});
|
});
|
||||||
|
this.monitorManagerProxy.onMonitorShouldReset(() => this.reset());
|
||||||
}
|
}
|
||||||
|
|
||||||
override registerMenus(menus: MenuModelRegistry): void {
|
override registerMenus(menus: MenuModelRegistry): void {
|
||||||
@ -118,6 +125,10 @@ export class MonitorViewContribution
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
commands.registerCommand(
|
||||||
|
{ id: MonitorViewContribution.RESET_SERIAL_MONITOR },
|
||||||
|
{ execute: () => this.reset() }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async toggle(): Promise<void> {
|
protected async toggle(): Promise<void> {
|
||||||
@ -129,6 +140,14 @@ export class MonitorViewContribution
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async reset(): Promise<void> {
|
||||||
|
const widget = this.tryGetWidget();
|
||||||
|
if (widget) {
|
||||||
|
widget.dispose();
|
||||||
|
await this.openView({ activate: true, reveal: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected renderAutoScrollButton(): React.ReactNode {
|
protected renderAutoScrollButton(): React.ReactNode {
|
||||||
return (
|
return (
|
||||||
<React.Fragment key="autoscroll-toolbar-item">
|
<React.Fragment key="autoscroll-toolbar-item">
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import * as React from '@theia/core/shared/react';
|
import * as React from '@theia/core/shared/react';
|
||||||
import { postConstruct, injectable, inject } from '@theia/core/shared/inversify';
|
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||||
import { OptionsType } from 'react-select/src/types';
|
import { OptionsType } from 'react-select/src/types';
|
||||||
import { Emitter } from '@theia/core/lib/common/event';
|
import { Emitter } from '@theia/core/lib/common/event';
|
||||||
import { Disposable } from '@theia/core/lib/common/disposable';
|
import { Disposable } from '@theia/core/lib/common/disposable';
|
||||||
@ -9,14 +9,14 @@ import {
|
|||||||
Widget,
|
Widget,
|
||||||
MessageLoop,
|
MessageLoop,
|
||||||
} from '@theia/core/lib/browser/widgets';
|
} from '@theia/core/lib/browser/widgets';
|
||||||
import { SerialConfig } from '../../../common/protocol/serial-service';
|
|
||||||
import { ArduinoSelect } from '../../widgets/arduino-select';
|
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 { SerialMonitorSendInput } from './serial-monitor-send-input';
|
||||||
import { SerialMonitorOutput } from './serial-monitor-send-output';
|
import { SerialMonitorOutput } from './serial-monitor-send-output';
|
||||||
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
|
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
|
||||||
import { nls } from '@theia/core/lib/common';
|
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()
|
@injectable()
|
||||||
export class MonitorWidget extends ReactWidget {
|
export class MonitorWidget extends ReactWidget {
|
||||||
@ -26,14 +26,7 @@ export class MonitorWidget extends ReactWidget {
|
|||||||
);
|
);
|
||||||
static readonly ID = 'serial-monitor';
|
static readonly ID = 'serial-monitor';
|
||||||
|
|
||||||
@inject(SerialModel)
|
protected settings: MonitorSettings = {};
|
||||||
protected readonly serialModel: SerialModel;
|
|
||||||
|
|
||||||
@inject(SerialConnectionManager)
|
|
||||||
protected readonly serialConnection: SerialConnectionManager;
|
|
||||||
|
|
||||||
@inject(BoardsServiceProvider)
|
|
||||||
protected readonly boardsServiceProvider: BoardsServiceProvider;
|
|
||||||
|
|
||||||
protected widgetHeight: number;
|
protected widgetHeight: number;
|
||||||
|
|
||||||
@ -48,7 +41,16 @@ export class MonitorWidget extends ReactWidget {
|
|||||||
protected closing = false;
|
protected closing = false;
|
||||||
protected readonly clearOutputEmitter = new Emitter<void>();
|
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();
|
super();
|
||||||
this.id = MonitorWidget.ID;
|
this.id = MonitorWidget.ID;
|
||||||
this.title.label = MonitorWidget.LABEL;
|
this.title.label = MonitorWidget.LABEL;
|
||||||
@ -57,17 +59,30 @@ export class MonitorWidget extends ReactWidget {
|
|||||||
this.scrollOptions = undefined;
|
this.scrollOptions = undefined;
|
||||||
this.toDispose.push(this.clearOutputEmitter);
|
this.toDispose.push(this.clearOutputEmitter);
|
||||||
this.toDispose.push(
|
this.toDispose.push(
|
||||||
Disposable.create(() => this.serialConnection.closeWStoBE())
|
Disposable.create(() => this.monitorManagerProxy.disconnect())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@postConstruct()
|
protected override onBeforeAttach(msg: Message): void {
|
||||||
protected init(): void {
|
|
||||||
this.update();
|
this.update();
|
||||||
this.toDispose.push(
|
this.toDispose.push(this.monitorModel.onChange(() => this.update()));
|
||||||
this.serialConnection.onConnectionChanged(() => this.clearConsole())
|
this.getCurrentSettings().then(this.onMonitorSettingsDidChange.bind(this));
|
||||||
|
this.monitorManagerProxy.onMonitorSettingsDidChange(
|
||||||
|
this.onMonitorSettingsDidChange.bind(this)
|
||||||
);
|
);
|
||||||
this.toDispose.push(this.serialModel.onChange(() => this.update()));
|
|
||||||
|
this.monitorManagerProxy.startMonitor();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMonitorSettingsDidChange(settings: MonitorSettings): void {
|
||||||
|
this.settings = {
|
||||||
|
...this.settings,
|
||||||
|
pluggableMonitorSettings: {
|
||||||
|
...this.settings.pluggableMonitorSettings,
|
||||||
|
...settings.pluggableMonitorSettings,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
clearConsole(): void {
|
clearConsole(): void {
|
||||||
@ -79,11 +94,6 @@ export class MonitorWidget extends ReactWidget {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override onAfterAttach(msg: Message): void {
|
|
||||||
super.onAfterAttach(msg);
|
|
||||||
this.serialConnection.openWSToBE();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override onCloseRequest(msg: Message): void {
|
protected override onCloseRequest(msg: Message): void {
|
||||||
this.closing = true;
|
this.closing = true;
|
||||||
super.onCloseRequest(msg);
|
super.onCloseRequest(msg);
|
||||||
@ -119,7 +129,7 @@ export class MonitorWidget extends ReactWidget {
|
|||||||
};
|
};
|
||||||
|
|
||||||
protected get lineEndings(): OptionsType<
|
protected get lineEndings(): OptionsType<
|
||||||
SerialMonitorOutput.SelectOption<SerialModel.EOL>
|
SerialMonitorOutput.SelectOption<MonitorModel.EOL>
|
||||||
> {
|
> {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -144,32 +154,40 @@ export class MonitorWidget extends ReactWidget {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected get baudRates(): OptionsType<
|
private getCurrentSettings(): Promise<MonitorSettings> {
|
||||||
SerialMonitorOutput.SelectOption<SerialConfig.BaudRate>
|
const board = this.boardsServiceProvider.boardsConfig.selectedBoard;
|
||||||
> {
|
const port = this.boardsServiceProvider.boardsConfig.selectedPort;
|
||||||
const baudRates: Array<SerialConfig.BaudRate> = [
|
if (!board || !port) {
|
||||||
300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200,
|
return Promise.resolve(this.settings || {});
|
||||||
];
|
}
|
||||||
return baudRates.map((baudRate) => ({
|
return this.monitorManagerProxy.getCurrentSettings(board, port);
|
||||||
label: baudRate + ' baud',
|
|
||||||
value: baudRate,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): React.ReactNode {
|
protected render(): React.ReactNode {
|
||||||
const { baudRates, lineEndings } = this;
|
const baudrate = this.settings?.pluggableMonitorSettings
|
||||||
|
? this.settings.pluggableMonitorSettings.baudrate
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const baudrateOptions = baudrate?.values.map((b) => ({
|
||||||
|
label: b + ' baud',
|
||||||
|
value: b,
|
||||||
|
}));
|
||||||
|
const baudrateSelectedOption = baudrateOptions?.find(
|
||||||
|
(b) => b.value === baudrate?.selectedValue
|
||||||
|
);
|
||||||
|
|
||||||
const lineEnding =
|
const lineEnding =
|
||||||
lineEndings.find((item) => item.value === this.serialModel.lineEnding) ||
|
this.lineEndings.find(
|
||||||
lineEndings[1]; // Defaults to `\n`.
|
(item) => item.value === this.monitorModel.lineEnding
|
||||||
const baudRate =
|
) || this.lineEndings[1]; // Defaults to `\n`.
|
||||||
baudRates.find((item) => item.value === this.serialModel.baudRate) ||
|
|
||||||
baudRates[4]; // Defaults to `9600`.
|
|
||||||
return (
|
return (
|
||||||
<div className="serial-monitor">
|
<div className="serial-monitor">
|
||||||
<div className="head">
|
<div className="head">
|
||||||
<div className="send">
|
<div className="send">
|
||||||
<SerialMonitorSendInput
|
<SerialMonitorSendInput
|
||||||
serialConnection={this.serialConnection}
|
boardsServiceProvider={this.boardsServiceProvider}
|
||||||
|
monitorModel={this.monitorModel}
|
||||||
resolveFocus={this.onFocusResolved}
|
resolveFocus={this.onFocusResolved}
|
||||||
onSend={this.onSend}
|
onSend={this.onSend}
|
||||||
/>
|
/>
|
||||||
@ -178,26 +196,28 @@ export class MonitorWidget extends ReactWidget {
|
|||||||
<div className="select">
|
<div className="select">
|
||||||
<ArduinoSelect
|
<ArduinoSelect
|
||||||
maxMenuHeight={this.widgetHeight - 40}
|
maxMenuHeight={this.widgetHeight - 40}
|
||||||
options={lineEndings}
|
options={this.lineEndings}
|
||||||
value={lineEnding}
|
value={lineEnding}
|
||||||
onChange={this.onChangeLineEnding}
|
onChange={this.onChangeLineEnding}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="select">
|
{baudrateOptions && baudrateSelectedOption && (
|
||||||
<ArduinoSelect
|
<div className="select">
|
||||||
className="select"
|
<ArduinoSelect
|
||||||
maxMenuHeight={this.widgetHeight - 40}
|
className="select"
|
||||||
options={baudRates}
|
maxMenuHeight={this.widgetHeight - 40}
|
||||||
value={baudRate}
|
options={baudrateOptions}
|
||||||
onChange={this.onChangeBaudRate}
|
value={baudrateSelectedOption}
|
||||||
/>
|
onChange={this.onChangeBaudRate}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="body">
|
<div className="body">
|
||||||
<SerialMonitorOutput
|
<SerialMonitorOutput
|
||||||
serialModel={this.serialModel}
|
monitorModel={this.monitorModel}
|
||||||
serialConnection={this.serialConnection}
|
monitorManagerProxy={this.monitorManagerProxy}
|
||||||
clearConsoleEvent={this.clearOutputEmitter.event}
|
clearConsoleEvent={this.clearOutputEmitter.event}
|
||||||
height={Math.floor(this.widgetHeight - 50)}
|
height={Math.floor(this.widgetHeight - 50)}
|
||||||
/>
|
/>
|
||||||
@ -208,18 +228,26 @@ export class MonitorWidget extends ReactWidget {
|
|||||||
|
|
||||||
protected readonly onSend = (value: string) => this.doSend(value);
|
protected readonly onSend = (value: string) => this.doSend(value);
|
||||||
protected async doSend(value: string): Promise<void> {
|
protected async doSend(value: string): Promise<void> {
|
||||||
this.serialConnection.send(value);
|
this.monitorManagerProxy.send(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readonly onChangeLineEnding = (
|
protected readonly onChangeLineEnding = (
|
||||||
option: SerialMonitorOutput.SelectOption<SerialModel.EOL>
|
option: SerialMonitorOutput.SelectOption<MonitorModel.EOL>
|
||||||
) => {
|
): void => {
|
||||||
this.serialModel.lineEnding = option.value;
|
this.monitorModel.lineEnding = option.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
protected readonly onChangeBaudRate = (
|
protected readonly onChangeBaudRate = ({
|
||||||
option: SerialMonitorOutput.SelectOption<SerialConfig.BaudRate>
|
value,
|
||||||
) => {
|
}: {
|
||||||
this.serialModel.baudRate = option.value;
|
value: string;
|
||||||
|
}): void => {
|
||||||
|
this.getCurrentSettings().then(({ pluggableMonitorSettings }) => {
|
||||||
|
if (!pluggableMonitorSettings || !pluggableMonitorSettings['baudrate'])
|
||||||
|
return;
|
||||||
|
const baudRateSettings = pluggableMonitorSettings['baudrate'];
|
||||||
|
baudRateSettings.selectedValue = value;
|
||||||
|
this.monitorManagerProxy.changeSettings({ pluggableMonitorSettings });
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,13 @@ import { Key, KeyCode } from '@theia/core/lib/browser/keys';
|
|||||||
import { Board } from '../../../common/protocol/boards-service';
|
import { Board } from '../../../common/protocol/boards-service';
|
||||||
import { isOSX } from '@theia/core/lib/common/os';
|
import { isOSX } from '@theia/core/lib/common/os';
|
||||||
import { DisposableCollection, nls } from '@theia/core/lib/common';
|
import { DisposableCollection, nls } from '@theia/core/lib/common';
|
||||||
import { SerialConnectionManager } from '../serial-connection-manager';
|
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
|
||||||
import { SerialPlotter } from '../plotter/protocol';
|
import { MonitorModel } from '../../monitor-model';
|
||||||
|
|
||||||
export namespace SerialMonitorSendInput {
|
export namespace SerialMonitorSendInput {
|
||||||
export interface Props {
|
export interface Props {
|
||||||
readonly serialConnection: SerialConnectionManager;
|
readonly boardsServiceProvider: BoardsServiceProvider;
|
||||||
|
readonly monitorModel: MonitorModel;
|
||||||
readonly onSend: (text: string) => void;
|
readonly onSend: (text: string) => void;
|
||||||
readonly resolveFocus: (element: HTMLElement | undefined) => void;
|
readonly resolveFocus: (element: HTMLElement | undefined) => void;
|
||||||
}
|
}
|
||||||
@ -26,28 +27,20 @@ export class SerialMonitorSendInput extends React.Component<
|
|||||||
|
|
||||||
constructor(props: Readonly<SerialMonitorSendInput.Props>) {
|
constructor(props: Readonly<SerialMonitorSendInput.Props>) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { text: '', connected: false };
|
this.state = { text: '', connected: true };
|
||||||
this.onChange = this.onChange.bind(this);
|
this.onChange = this.onChange.bind(this);
|
||||||
this.onSend = this.onSend.bind(this);
|
this.onSend = this.onSend.bind(this);
|
||||||
this.onKeyDown = this.onKeyDown.bind(this);
|
this.onKeyDown = this.onKeyDown.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
override componentDidMount(): void {
|
override componentDidMount(): void {
|
||||||
this.props.serialConnection.isBESerialConnected().then((connected) => {
|
this.setState({ connected: this.props.monitorModel.connected });
|
||||||
this.setState({ connected });
|
this.toDisposeBeforeUnmount.push(
|
||||||
});
|
this.props.monitorModel.onChange(({ property }) => {
|
||||||
|
if (property === 'connected')
|
||||||
this.toDisposeBeforeUnmount.pushAll([
|
this.setState({ connected: this.props.monitorModel.connected });
|
||||||
this.props.serialConnection.onRead(({ messages }) => {
|
})
|
||||||
if (
|
);
|
||||||
messages.command ===
|
|
||||||
SerialPlotter.Protocol.Command.MIDDLEWARE_CONFIG_CHANGED &&
|
|
||||||
'connected' in messages.data
|
|
||||||
) {
|
|
||||||
this.setState({ connected: messages.data.connected });
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override componentWillUnmount(): void {
|
override componentWillUnmount(): void {
|
||||||
@ -60,7 +53,7 @@ export class SerialMonitorSendInput extends React.Component<
|
|||||||
<input
|
<input
|
||||||
ref={this.setRef}
|
ref={this.setRef}
|
||||||
type="text"
|
type="text"
|
||||||
className={`theia-input ${this.state.connected ? '' : 'warning'}`}
|
className={`theia-input ${this.shouldShowWarning() ? 'warning' : ''}`}
|
||||||
placeholder={this.placeholder}
|
placeholder={this.placeholder}
|
||||||
value={this.state.text}
|
value={this.state.text}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
@ -69,15 +62,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 {
|
protected get placeholder(): string {
|
||||||
const serialConfig = this.props.serialConnection.getConfig();
|
if (this.shouldShowWarning()) {
|
||||||
if (!this.state.connected || !serialConfig) {
|
|
||||||
return nls.localize(
|
return nls.localize(
|
||||||
'arduino/serial/notConnected',
|
'arduino/serial/notConnected',
|
||||||
'Not connected. Select a board and a port to connect automatically.'
|
'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(
|
return nls.localize(
|
||||||
'arduino/serial/message',
|
'arduino/serial/message',
|
||||||
"Message ({0} + Enter to send message to '{1}' on '{2}')",
|
"Message ({0} + Enter to send message to '{1}' on '{2}')",
|
||||||
@ -102,7 +102,7 @@ export class SerialMonitorSendInput extends React.Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected onSend(): void {
|
protected onSend(): void {
|
||||||
this.props.onSend(this.state.text);
|
this.props.onSend(this.state.text + this.props.monitorModel.lineEnding);
|
||||||
this.setState({ text: '' });
|
this.setState({ text: '' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,10 +2,10 @@ import * as React from '@theia/core/shared/react';
|
|||||||
import { Event } from '@theia/core/lib/common/event';
|
import { Event } from '@theia/core/lib/common/event';
|
||||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||||
import { areEqual, FixedSizeList as List } from 'react-window';
|
import { areEqual, FixedSizeList as List } from 'react-window';
|
||||||
import { SerialModel } from '../serial-model';
|
|
||||||
import { SerialConnectionManager } from '../serial-connection-manager';
|
|
||||||
import dateFormat = require('dateformat');
|
import dateFormat = require('dateformat');
|
||||||
import { messagesToLines, truncateLines } from './monitor-utils';
|
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 };
|
export type Line = { message: string; timestamp?: Date; lineLen: number };
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ export class SerialMonitorOutput extends React.Component<
|
|||||||
this.listRef = React.createRef();
|
this.listRef = React.createRef();
|
||||||
this.state = {
|
this.state = {
|
||||||
lines: [],
|
lines: [],
|
||||||
timestamp: this.props.serialModel.timestamp,
|
timestamp: this.props.monitorModel.timestamp,
|
||||||
charCount: 0,
|
charCount: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -58,14 +58,13 @@ export class SerialMonitorOutput extends React.Component<
|
|||||||
override componentDidMount(): void {
|
override componentDidMount(): void {
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
this.toDisposeBeforeUnmount.pushAll([
|
this.toDisposeBeforeUnmount.pushAll([
|
||||||
this.props.serialConnection.onRead(({ messages }) => {
|
this.props.monitorManagerProxy.onMessagesReceived(({ messages }) => {
|
||||||
const [newLines, totalCharCount] = messagesToLines(
|
const [newLines, totalCharCount] = messagesToLines(
|
||||||
messages,
|
messages,
|
||||||
this.state.lines,
|
this.state.lines,
|
||||||
this.state.charCount
|
this.state.charCount
|
||||||
);
|
);
|
||||||
const [lines, charCount] = truncateLines(newLines, totalCharCount);
|
const [lines, charCount] = truncateLines(newLines, totalCharCount);
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
lines,
|
lines,
|
||||||
charCount,
|
charCount,
|
||||||
@ -75,9 +74,9 @@ export class SerialMonitorOutput extends React.Component<
|
|||||||
this.props.clearConsoleEvent(() =>
|
this.props.clearConsoleEvent(() =>
|
||||||
this.setState({ lines: [], charCount: 0 })
|
this.setState({ lines: [], charCount: 0 })
|
||||||
),
|
),
|
||||||
this.props.serialModel.onChange(({ property }) => {
|
this.props.monitorModel.onChange(({ property }) => {
|
||||||
if (property === 'timestamp') {
|
if (property === 'timestamp') {
|
||||||
const { timestamp } = this.props.serialModel;
|
const { timestamp } = this.props.monitorModel;
|
||||||
this.setState({ timestamp });
|
this.setState({ timestamp });
|
||||||
}
|
}
|
||||||
if (property === 'autoscroll') {
|
if (property === 'autoscroll') {
|
||||||
@ -93,7 +92,7 @@ export class SerialMonitorOutput extends React.Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
scrollToBottom = ((): void => {
|
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');
|
this.listRef.current.scrollToItem(this.state.lines.length, 'end');
|
||||||
}
|
}
|
||||||
}).bind(this);
|
}).bind(this);
|
||||||
@ -128,8 +127,8 @@ const Row = React.memo(_Row, areEqual);
|
|||||||
|
|
||||||
export namespace SerialMonitorOutput {
|
export namespace SerialMonitorOutput {
|
||||||
export interface Props {
|
export interface Props {
|
||||||
readonly serialModel: SerialModel;
|
readonly monitorModel: MonitorModel;
|
||||||
readonly serialConnection: SerialConnectionManager;
|
readonly monitorManagerProxy: MonitorManagerProxyClient;
|
||||||
readonly clearConsoleEvent: Event<void>;
|
readonly clearConsoleEvent: Event<void>;
|
||||||
readonly height: number;
|
readonly height: number;
|
||||||
}
|
}
|
||||||
|
@ -6,15 +6,14 @@ import {
|
|||||||
MaybePromise,
|
MaybePromise,
|
||||||
MenuModelRegistry,
|
MenuModelRegistry,
|
||||||
} from '@theia/core';
|
} from '@theia/core';
|
||||||
import { SerialModel } from '../serial-model';
|
|
||||||
import { ArduinoMenus } from '../../menu/arduino-menus';
|
import { ArduinoMenus } from '../../menu/arduino-menus';
|
||||||
import { Contribution } from '../../contributions/contribution';
|
import { Contribution } from '../../contributions/contribution';
|
||||||
import { Endpoint, FrontendApplication } from '@theia/core/lib/browser';
|
import { Endpoint, FrontendApplication } from '@theia/core/lib/browser';
|
||||||
import { ipcRenderer } from '@theia/electron/shared/electron';
|
import { ipcRenderer } from '@theia/electron/shared/electron';
|
||||||
import { SerialConfig } from '../../../common/protocol';
|
import { MonitorManagerProxyClient } from '../../../common/protocol';
|
||||||
import { SerialConnectionManager } from '../serial-connection-manager';
|
|
||||||
import { SerialPlotter } from './protocol';
|
|
||||||
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
|
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
|
||||||
|
import { MonitorModel } from '../../monitor-model';
|
||||||
|
|
||||||
const queryString = require('query-string');
|
const queryString = require('query-string');
|
||||||
|
|
||||||
export namespace SerialPlotterContribution {
|
export namespace SerialPlotterContribution {
|
||||||
@ -24,6 +23,11 @@ export namespace SerialPlotterContribution {
|
|||||||
label: 'Serial Plotter',
|
label: 'Serial Plotter',
|
||||||
category: 'Arduino',
|
category: 'Arduino',
|
||||||
};
|
};
|
||||||
|
export const RESET: Command = {
|
||||||
|
id: 'serial-plotter-reset',
|
||||||
|
label: 'Reset Serial Plotter',
|
||||||
|
category: 'Arduino',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,14 +37,14 @@ export class PlotterFrontendContribution extends Contribution {
|
|||||||
protected url: string;
|
protected url: string;
|
||||||
protected wsPort: number;
|
protected wsPort: number;
|
||||||
|
|
||||||
@inject(SerialModel)
|
@inject(MonitorModel)
|
||||||
protected readonly model: SerialModel;
|
protected readonly model: MonitorModel;
|
||||||
|
|
||||||
@inject(ThemeService)
|
@inject(ThemeService)
|
||||||
protected readonly themeService: ThemeService;
|
protected readonly themeService: ThemeService;
|
||||||
|
|
||||||
@inject(SerialConnectionManager)
|
@inject(MonitorManagerProxyClient)
|
||||||
protected readonly serialConnection: SerialConnectionManager;
|
protected readonly monitorManagerProxy: MonitorManagerProxyClient;
|
||||||
|
|
||||||
@inject(BoardsServiceProvider)
|
@inject(BoardsServiceProvider)
|
||||||
protected readonly boardsServiceProvider: BoardsServiceProvider;
|
protected readonly boardsServiceProvider: BoardsServiceProvider;
|
||||||
@ -53,12 +57,17 @@ export class PlotterFrontendContribution extends Contribution {
|
|||||||
this.window = null;
|
this.window = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this.monitorManagerProxy.onMonitorShouldReset(() => this.reset());
|
||||||
|
|
||||||
return super.onStart(app);
|
return super.onStart(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
override registerCommands(registry: CommandRegistry): void {
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
registry.registerCommand(SerialPlotterContribution.Commands.OPEN, {
|
registry.registerCommand(SerialPlotterContribution.Commands.OPEN, {
|
||||||
execute: this.connect.bind(this),
|
execute: this.startPlotter.bind(this),
|
||||||
|
});
|
||||||
|
registry.registerCommand(SerialPlotterContribution.Commands.RESET, {
|
||||||
|
execute: () => this.reset(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,12 +79,13 @@ export class PlotterFrontendContribution extends Contribution {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect(): Promise<void> {
|
async startPlotter(): Promise<void> {
|
||||||
|
await this.monitorManagerProxy.startMonitor();
|
||||||
if (!!this.window) {
|
if (!!this.window) {
|
||||||
this.window.focus();
|
this.window.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const wsPort = this.serialConnection.getWsPort();
|
const wsPort = this.monitorManagerProxy.getWebSocketPort();
|
||||||
if (wsPort) {
|
if (wsPort) {
|
||||||
this.open(wsPort);
|
this.open(wsPort);
|
||||||
} else {
|
} else {
|
||||||
@ -84,15 +94,10 @@ export class PlotterFrontendContribution extends Contribution {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async open(wsPort: number): Promise<void> {
|
protected async open(wsPort: number): Promise<void> {
|
||||||
const initConfig: Partial<SerialPlotter.Config> = {
|
const initConfig = {
|
||||||
baudrates: SerialConfig.BaudRates.map((b) => b),
|
|
||||||
currentBaudrate: this.model.baudRate,
|
|
||||||
currentLineEnding: this.model.lineEnding,
|
|
||||||
darkTheme: this.themeService.getCurrentTheme().type === 'dark',
|
darkTheme: this.themeService.getCurrentTheme().type === 'dark',
|
||||||
wsPort,
|
wsPort,
|
||||||
interpolate: this.model.interpolate,
|
serialPort: this.model.serialPort,
|
||||||
connected: await this.serialConnection.isBESerialConnected(),
|
|
||||||
serialPort: this.boardsServiceProvider.boardsConfig.selectedPort?.address,
|
|
||||||
};
|
};
|
||||||
const urlWithParams = queryString.stringifyUrl(
|
const urlWithParams = queryString.stringifyUrl(
|
||||||
{
|
{
|
||||||
@ -103,4 +108,11 @@ export class PlotterFrontendContribution extends Contribution {
|
|||||||
);
|
);
|
||||||
this.window = window.open(urlWithParams, 'serialPlotter');
|
this.window = window.open(urlWithParams, 'serialPlotter');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async reset(): Promise<void> {
|
||||||
|
if (!!this.window) {
|
||||||
|
this.window.close();
|
||||||
|
await this.startPlotter();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
export namespace SerialPlotter {
|
|
||||||
export type Config = {
|
|
||||||
currentBaudrate: number;
|
|
||||||
baudrates: number[];
|
|
||||||
currentLineEnding: string;
|
|
||||||
darkTheme: boolean;
|
|
||||||
wsPort: number;
|
|
||||||
interpolate: boolean;
|
|
||||||
serialPort: string;
|
|
||||||
connected: boolean;
|
|
||||||
generate?: boolean;
|
|
||||||
};
|
|
||||||
export namespace Protocol {
|
|
||||||
export enum Command {
|
|
||||||
PLOTTER_SET_BAUDRATE = 'PLOTTER_SET_BAUDRATE',
|
|
||||||
PLOTTER_SET_LINE_ENDING = 'PLOTTER_SET_LINE_ENDING',
|
|
||||||
PLOTTER_SET_INTERPOLATE = 'PLOTTER_SET_INTERPOLATE',
|
|
||||||
PLOTTER_SEND_MESSAGE = 'PLOTTER_SEND_MESSAGE',
|
|
||||||
MIDDLEWARE_CONFIG_CHANGED = 'MIDDLEWARE_CONFIG_CHANGED',
|
|
||||||
}
|
|
||||||
export type Message = {
|
|
||||||
command: SerialPlotter.Protocol.Command;
|
|
||||||
data?: any;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,360 +0,0 @@
|
|||||||
import { injectable, inject } from '@theia/core/shared/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;
|
|
||||||
}
|
|
@ -1,163 +0,0 @@
|
|||||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
|
||||||
import { Emitter, Event } from '@theia/core/lib/common/event';
|
|
||||||
import { SerialConfig } from '../../common/protocol';
|
|
||||||
import {
|
|
||||||
FrontendApplicationContribution,
|
|
||||||
LocalStorageService,
|
|
||||||
} from '@theia/core/lib/browser';
|
|
||||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
|
||||||
|
|
||||||
@injectable()
|
|
||||||
export class SerialModel implements FrontendApplicationContribution {
|
|
||||||
protected static STORAGE_ID = 'arduino-serial-model';
|
|
||||||
|
|
||||||
@inject(LocalStorageService)
|
|
||||||
protected readonly localStorageService: LocalStorageService;
|
|
||||||
|
|
||||||
@inject(BoardsServiceProvider)
|
|
||||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
|
||||||
|
|
||||||
protected readonly onChangeEmitter: Emitter<
|
|
||||||
SerialModel.State.Change<keyof SerialModel.State>
|
|
||||||
>;
|
|
||||||
protected _autoscroll: boolean;
|
|
||||||
protected _timestamp: boolean;
|
|
||||||
protected _baudRate: SerialConfig.BaudRate;
|
|
||||||
protected _lineEnding: SerialModel.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.onChangeEmitter = new Emitter<
|
|
||||||
SerialModel.State.Change<keyof SerialModel.State>
|
|
||||||
>();
|
|
||||||
}
|
|
||||||
|
|
||||||
onStart(): void {
|
|
||||||
this.localStorageService
|
|
||||||
.getData<SerialModel.State>(SerialModel.STORAGE_ID)
|
|
||||||
.then((state) => {
|
|
||||||
if (state) {
|
|
||||||
this.restoreState(state);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get onChange(): Event<SerialModel.State.Change<keyof SerialModel.State>> {
|
|
||||||
return this.onChangeEmitter.event;
|
|
||||||
}
|
|
||||||
|
|
||||||
get autoscroll(): boolean {
|
|
||||||
return this._autoscroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleAutoscroll(): void {
|
|
||||||
this._autoscroll = !this._autoscroll;
|
|
||||||
this.storeState();
|
|
||||||
this.storeState().then(() =>
|
|
||||||
this.onChangeEmitter.fire({
|
|
||||||
property: 'autoscroll',
|
|
||||||
value: this._autoscroll,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get timestamp(): boolean {
|
|
||||||
return this._timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleTimestamp(): void {
|
|
||||||
this._timestamp = !this._timestamp;
|
|
||||||
this.storeState().then(() =>
|
|
||||||
this.onChangeEmitter.fire({
|
|
||||||
property: 'timestamp',
|
|
||||||
value: this._timestamp,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
return this._lineEnding;
|
|
||||||
}
|
|
||||||
|
|
||||||
set lineEnding(lineEnding: SerialModel.EOL) {
|
|
||||||
this._lineEnding = lineEnding;
|
|
||||||
this.storeState().then(() =>
|
|
||||||
this.onChangeEmitter.fire({
|
|
||||||
property: 'lineEnding',
|
|
||||||
value: this._lineEnding,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get interpolate(): boolean {
|
|
||||||
return this._interpolate;
|
|
||||||
}
|
|
||||||
|
|
||||||
set interpolate(i: boolean) {
|
|
||||||
this._interpolate = i;
|
|
||||||
this.storeState().then(() =>
|
|
||||||
this.onChangeEmitter.fire({
|
|
||||||
property: 'interpolate',
|
|
||||||
value: this._interpolate,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
export interface State {
|
|
||||||
autoscroll: boolean;
|
|
||||||
timestamp: boolean;
|
|
||||||
baudRate: SerialConfig.BaudRate;
|
|
||||||
lineEnding: EOL;
|
|
||||||
interpolate: boolean;
|
|
||||||
}
|
|
||||||
export namespace State {
|
|
||||||
export interface Change<K extends keyof State> {
|
|
||||||
readonly property: K;
|
|
||||||
readonly value: State[K];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EOL = '' | '\n' | '\r' | '\r\n';
|
|
||||||
export namespace EOL {
|
|
||||||
export const DEFAULT: EOL = '\n';
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
import { injectable } from '@theia/core/shared/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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Port } from "./boards-service";
|
||||||
|
|
||||||
export const ArduinoFirmwareUploaderPath =
|
export const ArduinoFirmwareUploaderPath =
|
||||||
'/services/arduino-firmware-uploader';
|
'/services/arduino-firmware-uploader';
|
||||||
export const ArduinoFirmwareUploader = Symbol('ArduinoFirmwareUploader');
|
export const ArduinoFirmwareUploader = Symbol('ArduinoFirmwareUploader');
|
||||||
@ -10,7 +12,7 @@ export type FirmwareInfo = {
|
|||||||
};
|
};
|
||||||
export interface ArduinoFirmwareUploader {
|
export interface ArduinoFirmwareUploader {
|
||||||
list(fqbn?: string): Promise<FirmwareInfo[]>;
|
list(fqbn?: string): Promise<FirmwareInfo[]>;
|
||||||
flash(firmware: FirmwareInfo, port: string): Promise<string>;
|
flash(firmware: FirmwareInfo, port: Port): Promise<string>;
|
||||||
uploadCertificates(command: string): Promise<any>;
|
uploadCertificates(command: string): Promise<any>;
|
||||||
updatableBoards(): Promise<string[]>;
|
updatableBoards(): Promise<string[]>;
|
||||||
availableFirmwares(fqbn: string): Promise<FirmwareInfo[]>;
|
availableFirmwares(fqbn: string): Promise<FirmwareInfo[]>;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { BoardUserField } from '.';
|
import { BoardUserField } from '.';
|
||||||
import { Port } from '../../common/protocol/boards-service';
|
import { Board, Port } from '../../common/protocol/boards-service';
|
||||||
import { Programmer } from './boards-service';
|
import { Programmer } from './boards-service';
|
||||||
|
|
||||||
export const CompilerWarningLiterals = [
|
export const CompilerWarningLiterals = [
|
||||||
@ -33,7 +33,7 @@ export namespace CoreService {
|
|||||||
* `file` URI to the sketch folder.
|
* `file` URI to the sketch folder.
|
||||||
*/
|
*/
|
||||||
readonly sketchUri: string;
|
readonly sketchUri: string;
|
||||||
readonly fqbn?: string | undefined;
|
readonly board?: Board;
|
||||||
readonly optimizeForDebug: boolean;
|
readonly optimizeForDebug: boolean;
|
||||||
readonly verbose: boolean;
|
readonly verbose: boolean;
|
||||||
readonly sourceOverride: Record<string, string>;
|
readonly sourceOverride: Record<string, string>;
|
||||||
@ -42,7 +42,7 @@ export namespace CoreService {
|
|||||||
|
|
||||||
export namespace Upload {
|
export namespace Upload {
|
||||||
export interface Options extends Compile.Options {
|
export interface Options extends Compile.Options {
|
||||||
readonly port?: Port | undefined;
|
readonly port?: Port;
|
||||||
readonly programmer?: Programmer | undefined;
|
readonly programmer?: Programmer | undefined;
|
||||||
readonly verify: boolean;
|
readonly verify: boolean;
|
||||||
readonly userFields: BoardUserField[];
|
readonly userFields: BoardUserField[];
|
||||||
@ -51,8 +51,8 @@ export namespace CoreService {
|
|||||||
|
|
||||||
export namespace Bootloader {
|
export namespace Bootloader {
|
||||||
export interface Options {
|
export interface Options {
|
||||||
readonly fqbn?: string | undefined;
|
readonly board?: Board;
|
||||||
readonly port?: Port | undefined;
|
readonly port?: Port;
|
||||||
readonly programmer?: Programmer | undefined;
|
readonly programmer?: Programmer | undefined;
|
||||||
readonly verbose: boolean;
|
readonly verbose: boolean;
|
||||||
readonly verify: boolean;
|
readonly verify: boolean;
|
||||||
|
@ -6,10 +6,10 @@ export * from './core-service';
|
|||||||
export * from './filesystem-ext';
|
export * from './filesystem-ext';
|
||||||
export * from './installable';
|
export * from './installable';
|
||||||
export * from './library-service';
|
export * from './library-service';
|
||||||
export * from './serial-service';
|
|
||||||
export * from './searchable';
|
export * from './searchable';
|
||||||
export * from './sketches-service';
|
export * from './sketches-service';
|
||||||
export * from './examples-service';
|
export * from './examples-service';
|
||||||
export * from './executable-service';
|
export * from './executable-service';
|
||||||
export * from './response-service';
|
export * from './response-service';
|
||||||
export * from './notification-service';
|
export * from './notification-service';
|
||||||
|
export * from './monitor-service';
|
||||||
|
95
arduino-ide-extension/src/common/protocol/monitor-service.ts
Normal file
95
arduino-ide-extension/src/common/protocol/monitor-service.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
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: MonitorSettings
|
||||||
|
): Promise<void>;
|
||||||
|
stopMonitor(board: Board, port: Port): Promise<void>;
|
||||||
|
getCurrentSettings(board: Board, port: Port): Promise<MonitorSettings>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MonitorManagerProxyClient = Symbol('MonitorManagerProxyClient');
|
||||||
|
export interface MonitorManagerProxyClient {
|
||||||
|
onMessagesReceived: Event<{ messages: string[] }>;
|
||||||
|
onMonitorSettingsDidChange: Event<MonitorSettings>;
|
||||||
|
onMonitorShouldReset: Event<void>;
|
||||||
|
connect(addressPort: number): void;
|
||||||
|
disconnect(): void;
|
||||||
|
getWebSocketPort(): number | undefined;
|
||||||
|
isWSConnected(): Promise<boolean>;
|
||||||
|
startMonitor(settings?: PluggableMonitorSettings): Promise<void>;
|
||||||
|
getCurrentSettings(board: Board, port: Port): Promise<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 {
|
||||||
|
// Commands sent by the clients to the web socket server
|
||||||
|
export enum ClientCommand {
|
||||||
|
SEND_MESSAGE = 'SEND_MESSAGE',
|
||||||
|
CHANGE_SETTINGS = 'CHANGE_SETTINGS',
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commands sent by the backend to the clients
|
||||||
|
export enum MiddlewareCommand {
|
||||||
|
ON_SETTINGS_DID_CHANGE = 'ON_SETTINGS_DID_CHANGE',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Message = {
|
||||||
|
command: Monitor.ClientCommand | Monitor.MiddlewareCommand;
|
||||||
|
data: string | MonitorSettings;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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.',
|
||||||
|
};
|
||||||
|
}
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -12,3 +12,7 @@ export function firstToLowerCase(what: string): string {
|
|||||||
export function firstToUpperCase(what: string): string {
|
export function firstToUpperCase(what: string): string {
|
||||||
return what.charAt(0).toUpperCase() + what.slice(1);
|
return what.charAt(0).toUpperCase() + what.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isNullOrUndefined(what: any): what is undefined | null {
|
||||||
|
return what === undefined || what === null;
|
||||||
|
}
|
||||||
|
@ -3,10 +3,10 @@ import {
|
|||||||
FirmwareInfo,
|
FirmwareInfo,
|
||||||
} from '../common/protocol/arduino-firmware-uploader';
|
} from '../common/protocol/arduino-firmware-uploader';
|
||||||
import { injectable, inject, named } from '@theia/core/shared/inversify';
|
import { injectable, inject, named } from '@theia/core/shared/inversify';
|
||||||
import { ExecutableService } from '../common/protocol';
|
import { ExecutableService, Port } from '../common/protocol';
|
||||||
import { SerialService } from '../common/protocol/serial-service';
|
|
||||||
import { getExecPath, spawnCommand } from './exec-util';
|
import { getExecPath, spawnCommand } from './exec-util';
|
||||||
import { ILogger } from '@theia/core/lib/common/logger';
|
import { ILogger } from '@theia/core/lib/common/logger';
|
||||||
|
import { MonitorManager } from './monitor-manager';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class ArduinoFirmwareUploaderImpl implements ArduinoFirmwareUploader {
|
export class ArduinoFirmwareUploaderImpl implements ArduinoFirmwareUploader {
|
||||||
@ -19,8 +19,8 @@ export class ArduinoFirmwareUploaderImpl implements ArduinoFirmwareUploader {
|
|||||||
@named('fwuploader')
|
@named('fwuploader')
|
||||||
protected readonly logger: ILogger;
|
protected readonly logger: ILogger;
|
||||||
|
|
||||||
@inject(SerialService)
|
@inject(MonitorManager)
|
||||||
protected readonly serialService: SerialService;
|
protected readonly monitorManager: MonitorManager;
|
||||||
|
|
||||||
protected onError(error: any): void {
|
protected onError(error: any): void {
|
||||||
this.logger.error(error);
|
this.logger.error(error);
|
||||||
@ -69,26 +69,28 @@ export class ArduinoFirmwareUploaderImpl implements ArduinoFirmwareUploader {
|
|||||||
return await this.list(fqbn);
|
return await this.list(fqbn);
|
||||||
}
|
}
|
||||||
|
|
||||||
async flash(firmware: FirmwareInfo, port: string): Promise<string> {
|
async flash(firmware: FirmwareInfo, port: Port): Promise<string> {
|
||||||
let output;
|
let output;
|
||||||
|
const board = {
|
||||||
|
name: firmware.board_name,
|
||||||
|
fqbn: firmware.board_fqbn,
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
this.serialService.uploadInProgress = true;
|
this.monitorManager.notifyUploadStarted(board, port);
|
||||||
await this.serialService.disconnect();
|
|
||||||
output = await this.runCommand([
|
output = await this.runCommand([
|
||||||
'firmware',
|
'firmware',
|
||||||
'flash',
|
'flash',
|
||||||
'--fqbn',
|
'--fqbn',
|
||||||
firmware.board_fqbn,
|
firmware.board_fqbn,
|
||||||
'--address',
|
'--address',
|
||||||
port,
|
port.address,
|
||||||
'--module',
|
'--module',
|
||||||
`${firmware.module}@${firmware.firmware_version}`,
|
`${firmware.module}@${firmware.firmware_version}`,
|
||||||
]);
|
]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw e;
|
throw e;
|
||||||
} finally {
|
} finally {
|
||||||
this.serialService.uploadInProgress = false;
|
this.monitorManager.notifyUploadFinished(board, port);
|
||||||
this.serialService.connectSerialIfRequired();
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,16 +40,7 @@ import {
|
|||||||
ArduinoDaemon,
|
ArduinoDaemon,
|
||||||
ArduinoDaemonPath,
|
ArduinoDaemonPath,
|
||||||
} from '../common/protocol/arduino-daemon';
|
} 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 { ConfigServiceImpl } from './config-service-impl';
|
||||||
import { EnvVariablesServer as TheiaEnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
import { EnvVariablesServer as TheiaEnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||||
import { EnvVariablesServer } from './theia/env-variables/env-variables-server';
|
import { EnvVariablesServer } from './theia/env-variables/env-variables-server';
|
||||||
@ -91,10 +82,24 @@ import {
|
|||||||
} from '../common/protocol/authentication-service';
|
} from '../common/protocol/authentication-service';
|
||||||
import { ArduinoFirmwareUploaderImpl } from './arduino-firmware-uploader-impl';
|
import { ArduinoFirmwareUploaderImpl } from './arduino-firmware-uploader-impl';
|
||||||
import { PlotterBackendContribution } from './plotter/plotter-backend-contribution';
|
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 { ArduinoLocalizationContribution } from './arduino-localization-contribution';
|
||||||
import { LocalizationContribution } from '@theia/core/lib/node/i18n/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';
|
||||||
import { ClangFormatter } from './clang-formatter';
|
import { ClangFormatter } from './clang-formatter';
|
||||||
import { FormatterPath } from '../common/protocol/formatter';
|
import { FormatterPath } from '../common/protocol/formatter';
|
||||||
|
|
||||||
@ -193,9 +198,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.
|
// Shared Arduino core client provider service for the backend.
|
||||||
bind(CoreClientProvider).toSelf().inSingletonScope();
|
bind(CoreClientProvider).toSelf().inSingletonScope();
|
||||||
|
|
||||||
@ -221,19 +223,58 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
|
|
||||||
// #endregion Theia customizations
|
// #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, monitorID } = options;
|
||||||
|
|
||||||
|
return new MonitorService(
|
||||||
|
logger,
|
||||||
|
monitorSettingsProvider,
|
||||||
|
webSocketProvider,
|
||||||
|
board,
|
||||||
|
port,
|
||||||
|
monitorID,
|
||||||
|
coreClientProvider
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Serial client provider per connected frontend.
|
// Serial client provider per connected frontend.
|
||||||
bind(ConnectionContainerModule).toConstantValue(
|
bind(ConnectionContainerModule).toConstantValue(
|
||||||
ConnectionContainerModule.create(({ bind, bindBackendService }) => {
|
ConnectionContainerModule.create(({ bind, bindBackendService }) => {
|
||||||
bind(MonitorClientProvider).toSelf().inSingletonScope();
|
bind(MonitorManagerProxyImpl).toSelf().inSingletonScope();
|
||||||
bind(SerialServiceImpl).toSelf().inSingletonScope();
|
bind(MonitorManagerProxy).toService(MonitorManagerProxyImpl);
|
||||||
bind(SerialService).toService(SerialServiceImpl);
|
bindBackendService<MonitorManagerProxy, MonitorManagerProxyClient>(
|
||||||
bindBackendService<SerialService, SerialServiceClient>(
|
MonitorManagerProxyPath,
|
||||||
SerialServicePath,
|
MonitorManagerProxy,
|
||||||
SerialService,
|
(monitorMgrProxy, client) => {
|
||||||
(service, client) => {
|
monitorMgrProxy.setClient(client);
|
||||||
service.setClient(client);
|
// when the client close the connection, the proxy is disposed.
|
||||||
client.onDidCloseConnection(() => service.dispose());
|
// when the MonitorManagerProxy is disposed, it informs the MonitorManager
|
||||||
return service;
|
// 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;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@ -323,14 +364,22 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
.inSingletonScope()
|
.inSingletonScope()
|
||||||
.whenTargetNamed('config');
|
.whenTargetNamed('config');
|
||||||
|
|
||||||
// Logger for the serial service.
|
// Logger for the monitor manager and its services
|
||||||
bind(ILogger)
|
bind(ILogger)
|
||||||
.toDynamicValue((ctx) => {
|
.toDynamicValue((ctx) => {
|
||||||
const parentLogger = ctx.container.get<ILogger>(ILogger);
|
const parentLogger = ctx.container.get<ILogger>(ILogger);
|
||||||
return parentLogger.child(SerialServiceName);
|
return parentLogger.child(MonitorManagerName);
|
||||||
})
|
})
|
||||||
.inSingletonScope()
|
.inSingletonScope()
|
||||||
.whenTargetNamed(SerialServiceName);
|
.whenTargetNamed(MonitorManagerName);
|
||||||
|
|
||||||
|
bind(ILogger)
|
||||||
|
.toDynamicValue((ctx) => {
|
||||||
|
const parentLogger = ctx.container.get<ILogger>(ILogger);
|
||||||
|
return parentLogger.child(MonitorServiceName);
|
||||||
|
})
|
||||||
|
.inSingletonScope()
|
||||||
|
.whenTargetNamed(MonitorServiceName);
|
||||||
|
|
||||||
// Remote sketchbook bindings
|
// Remote sketchbook bindings
|
||||||
bind(AuthenticationServiceImpl).toSelf().inSingletonScope();
|
bind(AuthenticationServiceImpl).toSelf().inSingletonScope();
|
||||||
|
@ -24,7 +24,7 @@ import { ArduinoCoreServiceClient } from './cli-protocol/cc/arduino/cli/commands
|
|||||||
import { firstToUpperCase, firstToLowerCase } from '../common/utils';
|
import { firstToUpperCase, firstToLowerCase } from '../common/utils';
|
||||||
import { Port } from './cli-protocol/cc/arduino/cli/commands/v1/port_pb';
|
import { Port } from './cli-protocol/cc/arduino/cli/commands/v1/port_pb';
|
||||||
import { nls } from '@theia/core';
|
import { nls } from '@theia/core';
|
||||||
import { SerialService } from './../common/protocol/serial-service';
|
import { MonitorManager } from './monitor-manager';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
||||||
@ -34,8 +34,8 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
|||||||
@inject(NotificationServiceServer)
|
@inject(NotificationServiceServer)
|
||||||
protected readonly notificationService: NotificationServiceServer;
|
protected readonly notificationService: NotificationServiceServer;
|
||||||
|
|
||||||
@inject(SerialService)
|
@inject(MonitorManager)
|
||||||
protected readonly serialService: SerialService;
|
protected readonly monitorManager: MonitorManager;
|
||||||
|
|
||||||
protected uploading = false;
|
protected uploading = false;
|
||||||
|
|
||||||
@ -45,7 +45,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
|||||||
compilerWarnings?: CompilerWarnings;
|
compilerWarnings?: CompilerWarnings;
|
||||||
}
|
}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { sketchUri, fqbn, compilerWarnings } = options;
|
const { sketchUri, board, compilerWarnings } = options;
|
||||||
const sketchPath = FileUri.fsPath(sketchUri);
|
const sketchPath = FileUri.fsPath(sketchUri);
|
||||||
|
|
||||||
await this.coreClientProvider.initialized;
|
await this.coreClientProvider.initialized;
|
||||||
@ -55,8 +55,8 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
|||||||
const compileReq = new CompileRequest();
|
const compileReq = new CompileRequest();
|
||||||
compileReq.setInstance(instance);
|
compileReq.setInstance(instance);
|
||||||
compileReq.setSketchPath(sketchPath);
|
compileReq.setSketchPath(sketchPath);
|
||||||
if (fqbn) {
|
if (board?.fqbn) {
|
||||||
compileReq.setFqbn(fqbn);
|
compileReq.setFqbn(board.fqbn);
|
||||||
}
|
}
|
||||||
if (compilerWarnings) {
|
if (compilerWarnings) {
|
||||||
compileReq.setWarnings(compilerWarnings.toLowerCase());
|
compileReq.setWarnings(compilerWarnings.toLowerCase());
|
||||||
@ -139,11 +139,9 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
|||||||
await this.compile(Object.assign(options, { exportBinaries: false }));
|
await this.compile(Object.assign(options, { exportBinaries: false }));
|
||||||
|
|
||||||
this.uploading = true;
|
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);
|
const sketchPath = FileUri.fsPath(sketchUri);
|
||||||
|
|
||||||
await this.coreClientProvider.initialized;
|
await this.coreClientProvider.initialized;
|
||||||
@ -153,8 +151,8 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
|||||||
const req = requestProvider();
|
const req = requestProvider();
|
||||||
req.setInstance(instance);
|
req.setInstance(instance);
|
||||||
req.setSketchPath(sketchPath);
|
req.setSketchPath(sketchPath);
|
||||||
if (fqbn) {
|
if (board?.fqbn) {
|
||||||
req.setFqbn(fqbn);
|
req.setFqbn(board.fqbn);
|
||||||
}
|
}
|
||||||
const p = new Port();
|
const p = new Port();
|
||||||
if (port) {
|
if (port) {
|
||||||
@ -209,23 +207,22 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
|||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
} finally {
|
} finally {
|
||||||
this.uploading = false;
|
this.uploading = false;
|
||||||
this.serialService.uploadInProgress = false;
|
this.monitorManager.notifyUploadFinished(board, port);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async burnBootloader(options: CoreService.Bootloader.Options): Promise<void> {
|
async burnBootloader(options: CoreService.Bootloader.Options): Promise<void> {
|
||||||
this.uploading = true;
|
this.uploading = true;
|
||||||
this.serialService.uploadInProgress = true;
|
const { board, port, programmer } = options;
|
||||||
await this.serialService.disconnect();
|
await this.monitorManager.notifyUploadStarted(board, port);
|
||||||
|
|
||||||
await this.coreClientProvider.initialized;
|
await this.coreClientProvider.initialized;
|
||||||
const coreClient = await this.coreClient();
|
const coreClient = await this.coreClient();
|
||||||
const { client, instance } = coreClient;
|
const { client, instance } = coreClient;
|
||||||
const { fqbn, port, programmer } = options;
|
|
||||||
const burnReq = new BurnBootloaderRequest();
|
const burnReq = new BurnBootloaderRequest();
|
||||||
burnReq.setInstance(instance);
|
burnReq.setInstance(instance);
|
||||||
if (fqbn) {
|
if (board?.fqbn) {
|
||||||
burnReq.setFqbn(fqbn);
|
burnReq.setFqbn(board.fqbn);
|
||||||
}
|
}
|
||||||
const p = new Port();
|
const p = new Port();
|
||||||
if (port) {
|
if (port) {
|
||||||
@ -267,7 +264,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
|||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
} finally {
|
} finally {
|
||||||
this.uploading = false;
|
this.uploading = false;
|
||||||
this.serialService.uploadInProgress = false;
|
await this.monitorManager.notifyUploadFinished(board, port);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
95
arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts
Normal file
95
arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
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 {
|
||||||
|
MonitorSettings,
|
||||||
|
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): Promise<MonitorSettings> {
|
||||||
|
return this.manager.currentMonitorSettings(board, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
setClient(client: MonitorManagerProxyClient | undefined): void {
|
||||||
|
if (!client) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
}
|
220
arduino-ide-extension/src/node/monitor-manager.ts
Normal file
220
arduino-ide-extension/src/node/monitor-manager.ts
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
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 {
|
||||||
|
MonitorSettings,
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
async currentMonitorSettings(
|
||||||
|
board: Board,
|
||||||
|
port: Port
|
||||||
|
): Promise<MonitorSettings> {
|
||||||
|
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,
|
||||||
|
monitorID,
|
||||||
|
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}`;
|
||||||
|
}
|
||||||
|
}
|
20
arduino-ide-extension/src/node/monitor-service-factory.ts
Normal file
20
arduino-ide-extension/src/node/monitor-service-factory.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
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;
|
||||||
|
monitorID: string;
|
||||||
|
coreClientProvider: CoreClientProvider;
|
||||||
|
}): MonitorService;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MonitorServiceFactoryOptions {
|
||||||
|
board: Board;
|
||||||
|
port: Port;
|
||||||
|
monitorID: string;
|
||||||
|
coreClientProvider: CoreClientProvider;
|
||||||
|
}
|
606
arduino-ide-extension/src/node/monitor-service.ts
Normal file
606
arduino-ide-extension/src/node/monitor-service.ts
Normal file
@ -0,0 +1,606 @@
|
|||||||
|
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 {
|
||||||
|
MonitorSettings,
|
||||||
|
PluggableMonitorSettings,
|
||||||
|
MonitorSettingsProvider,
|
||||||
|
} from './monitor-settings/monitor-settings-provider';
|
||||||
|
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||||
|
|
||||||
|
export const MonitorServiceName = 'monitor-service';
|
||||||
|
type DuplexHandlerKeys =
|
||||||
|
| 'close'
|
||||||
|
| 'end'
|
||||||
|
| 'error'
|
||||||
|
| 'data'
|
||||||
|
| 'status'
|
||||||
|
| 'metadata';
|
||||||
|
interface DuplexHandler {
|
||||||
|
key: DuplexHandlerKeys;
|
||||||
|
callback: (...args: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
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: MonitorSettings = {};
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
protected _initialized = new Deferred<void>();
|
||||||
|
protected creating: Deferred<Status>;
|
||||||
|
|
||||||
|
MAX_WRITE_TO_STREAM_TRIES = 10;
|
||||||
|
WRITE_TO_STREAM_TIMEOUT_MS = 30000;
|
||||||
|
|
||||||
|
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,
|
||||||
|
private readonly monitorID: string,
|
||||||
|
protected override 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();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.updateClientsSettings(this.settings);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.portMonitorSettings(port.protocol, board.fqbn!).then(
|
||||||
|
async (settings) => {
|
||||||
|
this.settings = {
|
||||||
|
...this.settings,
|
||||||
|
pluggableMonitorSettings:
|
||||||
|
await this.monitorSettingsProvider.getSettings(
|
||||||
|
this.monitorID,
|
||||||
|
settings
|
||||||
|
),
|
||||||
|
};
|
||||||
|
this._initialized.resolve();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get initialized(): Promise<void> {
|
||||||
|
return this._initialized.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUploadInProgress(status: boolean): void {
|
||||||
|
this.uploadInProgress = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
getWebsocketAddressPort(): number {
|
||||||
|
return this.webSocketProvider.getAddress().port;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose(): void {
|
||||||
|
this.stop();
|
||||||
|
this.onDisposeEmitter.fire();
|
||||||
|
this.onWSClientsNumberChanged?.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
* @returns a status to verify connection has been established.
|
||||||
|
*/
|
||||||
|
async start(): Promise<Status> {
|
||||||
|
if (this.creating?.state === 'unresolved') return this.creating.promise;
|
||||||
|
this.creating = new Deferred();
|
||||||
|
if (this.duplex) {
|
||||||
|
this.updateClientsSettings({
|
||||||
|
monitorUISettings: { connected: true, serialPort: this.port.address },
|
||||||
|
});
|
||||||
|
this.creating.resolve(Status.ALREADY_CONNECTED);
|
||||||
|
return this.creating.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.board?.fqbn || !this.port?.address || !this.port?.protocol) {
|
||||||
|
this.updateClientsSettings({ monitorUISettings: { connected: false } });
|
||||||
|
|
||||||
|
this.creating.resolve(Status.CONFIG_MISSING);
|
||||||
|
return this.creating.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.uploadInProgress) {
|
||||||
|
this.updateClientsSettings({
|
||||||
|
monitorUISettings: { connected: false, serialPort: this.port.address },
|
||||||
|
});
|
||||||
|
|
||||||
|
this.creating.resolve(Status.UPLOAD_IN_PROGRESS);
|
||||||
|
return this.creating.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
...this.settings,
|
||||||
|
pluggableMonitorSettings: {
|
||||||
|
...this.settings.pluggableMonitorSettings,
|
||||||
|
...(await this.monitorSettingsProvider.getSettings(
|
||||||
|
this.monitorID,
|
||||||
|
defaultSettings
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.coreClientProvider.initialized;
|
||||||
|
const coreClient = await this.coreClient();
|
||||||
|
|
||||||
|
const { instance } = coreClient;
|
||||||
|
const monitorRequest = new MonitorRequest();
|
||||||
|
monitorRequest.setInstance(instance);
|
||||||
|
if (this.board?.fqbn) {
|
||||||
|
monitorRequest.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);
|
||||||
|
monitorRequest.setPort(port);
|
||||||
|
}
|
||||||
|
const config = new MonitorPortConfiguration();
|
||||||
|
for (const id in this.settings.pluggableMonitorSettings) {
|
||||||
|
const s = new MonitorPortSetting();
|
||||||
|
s.setSettingId(id);
|
||||||
|
s.setValue(this.settings.pluggableMonitorSettings[id].selectedValue);
|
||||||
|
config.addSettings(s);
|
||||||
|
}
|
||||||
|
monitorRequest.setPortConfiguration(config);
|
||||||
|
|
||||||
|
const wroteToStreamSuccessfully = await this.pollWriteToStream(
|
||||||
|
monitorRequest
|
||||||
|
);
|
||||||
|
if (wroteToStreamSuccessfully) {
|
||||||
|
this.startMessagesHandlers();
|
||||||
|
this.logger.info(
|
||||||
|
`started monitor to ${this.port?.address} using ${this.port?.protocol}`
|
||||||
|
);
|
||||||
|
this.updateClientsSettings({
|
||||||
|
monitorUISettings: { connected: true, serialPort: this.port.address },
|
||||||
|
});
|
||||||
|
this.creating.resolve(Status.OK);
|
||||||
|
return this.creating.promise;
|
||||||
|
} else {
|
||||||
|
this.logger.warn(
|
||||||
|
`failed starting monitor to ${this.port?.address} using ${this.port?.protocol}`
|
||||||
|
);
|
||||||
|
this.creating.resolve(Status.NOT_CONNECTED);
|
||||||
|
return this.creating.promise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createDuplex(): Promise<
|
||||||
|
ClientDuplexStream<MonitorRequest, MonitorResponse>
|
||||||
|
> {
|
||||||
|
const coreClient = await this.coreClient();
|
||||||
|
return coreClient.client.monitor();
|
||||||
|
}
|
||||||
|
|
||||||
|
setDuplexHandlers(
|
||||||
|
duplex: ClientDuplexStream<MonitorRequest, MonitorResponse>,
|
||||||
|
additionalHandlers: DuplexHandler[]
|
||||||
|
): void {
|
||||||
|
// default handlers
|
||||||
|
duplex
|
||||||
|
.on('close', () => {
|
||||||
|
this.duplex = null;
|
||||||
|
this.updateClientsSettings({
|
||||||
|
monitorUISettings: { connected: false },
|
||||||
|
});
|
||||||
|
this.logger.info(
|
||||||
|
`monitor to ${this.port?.address} using ${this.port?.protocol} closed by client`
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.on('end', () => {
|
||||||
|
this.duplex = null;
|
||||||
|
this.updateClientsSettings({
|
||||||
|
monitorUISettings: { connected: false },
|
||||||
|
});
|
||||||
|
this.logger.info(
|
||||||
|
`monitor to ${this.port?.address} using ${this.port?.protocol} closed by server`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const handler of additionalHandlers) {
|
||||||
|
duplex.on(handler.key, handler.callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pollWriteToStream(request: MonitorRequest): Promise<boolean> {
|
||||||
|
let attemptsRemaining = this.MAX_WRITE_TO_STREAM_TRIES;
|
||||||
|
const writeTimeoutMs = this.WRITE_TO_STREAM_TIMEOUT_MS;
|
||||||
|
|
||||||
|
const createWriteToStreamExecutor =
|
||||||
|
(duplex: ClientDuplexStream<MonitorRequest, MonitorResponse>) =>
|
||||||
|
(resolve: (value: boolean) => void, reject: () => void) => {
|
||||||
|
const resolvingDuplexHandlers: DuplexHandler[] = [
|
||||||
|
{
|
||||||
|
key: 'error',
|
||||||
|
callback: async (err: Error) => {
|
||||||
|
this.logger.error(err);
|
||||||
|
resolve(false);
|
||||||
|
// TODO
|
||||||
|
// this.theiaFEClient?.notifyError()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'data',
|
||||||
|
callback: async (monitorResponse: MonitorResponse) => {
|
||||||
|
if (monitorResponse.getError()) {
|
||||||
|
// TODO: Maybe disconnect
|
||||||
|
this.logger.error(monitorResponse.getError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (monitorResponse.getSuccess()) {
|
||||||
|
resolve(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = monitorResponse.getRxData();
|
||||||
|
const message =
|
||||||
|
typeof data === 'string'
|
||||||
|
? data
|
||||||
|
: new TextDecoder('utf8').decode(data);
|
||||||
|
this.messages.push(...splitLines(message));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
this.setDuplexHandlers(duplex, resolvingDuplexHandlers);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
reject();
|
||||||
|
}, writeTimeoutMs);
|
||||||
|
duplex.write(request);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pollWriteToStream = new Promise<boolean>((resolve) => {
|
||||||
|
const startPolling = async () => {
|
||||||
|
// here we create a new duplex but we don't yet
|
||||||
|
// set "this.duplex", nor do we use "this.duplex" in our poll
|
||||||
|
// as duplex 'end' / 'close' events (which we do not "await")
|
||||||
|
// will set "this.duplex" to null
|
||||||
|
const createdDuplex = await this.createDuplex();
|
||||||
|
|
||||||
|
let pollingIsSuccessful;
|
||||||
|
// attempt a "writeToStream" and "await" CLI response: success (true) or error (false)
|
||||||
|
// if we get neither within WRITE_TO_STREAM_TIMEOUT_MS or an error we get undefined
|
||||||
|
try {
|
||||||
|
const writeToStream = createWriteToStreamExecutor(createdDuplex);
|
||||||
|
pollingIsSuccessful = await new Promise(writeToStream);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLI confirmed port opened successfully
|
||||||
|
if (pollingIsSuccessful) {
|
||||||
|
this.duplex = createdDuplex;
|
||||||
|
resolve(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if "pollingIsSuccessful" is false
|
||||||
|
// the CLI gave us an error, lets try again
|
||||||
|
// after waiting 2 seconds if we've not already
|
||||||
|
// reached MAX_WRITE_TO_STREAM_TRIES
|
||||||
|
if (pollingIsSuccessful === false) {
|
||||||
|
attemptsRemaining -= 1;
|
||||||
|
if (attemptsRemaining > 0) {
|
||||||
|
setTimeout(startPolling, 2000);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
resolve(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// "pollingIsSuccessful" remains undefined:
|
||||||
|
// we got no response from the CLI within 30 seconds
|
||||||
|
// resolve to false and end the duplex connection
|
||||||
|
resolve(false);
|
||||||
|
createdDuplex.end();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
startPolling();
|
||||||
|
});
|
||||||
|
|
||||||
|
return pollWriteToStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
async currentSettings(): Promise<MonitorSettings> {
|
||||||
|
await this.initialized;
|
||||||
|
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: MonitorSettings): Promise<Status> {
|
||||||
|
const config = new MonitorPortConfiguration();
|
||||||
|
const { pluggableMonitorSettings } = settings;
|
||||||
|
const reconciledSettings = await this.monitorSettingsProvider.setSettings(
|
||||||
|
this.monitorID,
|
||||||
|
pluggableMonitorSettings || {}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (reconciledSettings) {
|
||||||
|
for (const id in reconciledSettings) {
|
||||||
|
const s = new MonitorPortSetting();
|
||||||
|
s.setSettingId(id);
|
||||||
|
s.setValue(reconciledSettings[id].selectedValue);
|
||||||
|
config.addSettings(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateClientsSettings({
|
||||||
|
monitorUISettings: {
|
||||||
|
...settings.monitorUISettings,
|
||||||
|
connected: !!this.duplex,
|
||||||
|
serialPort: this.port.address,
|
||||||
|
},
|
||||||
|
pluggableMonitorSettings: reconciledSettings,
|
||||||
|
});
|
||||||
|
|
||||||
|
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.ClientCommand.SEND_MESSAGE:
|
||||||
|
this.send(message.data as string);
|
||||||
|
break;
|
||||||
|
case Monitor.ClientCommand.CHANGE_SETTINGS:
|
||||||
|
this.changeSettings(message.data as MonitorSettings);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateClientsSettings(settings: MonitorSettings): void {
|
||||||
|
this.settings = { ...this.settings, ...settings };
|
||||||
|
const command: Monitor.Message = {
|
||||||
|
command: Monitor.MiddlewareCommand.ON_SETTINGS_DID_CHANGE,
|
||||||
|
data: settings,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.webSocketProvider.sendMessage(JSON.stringify(command));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)/);
|
||||||
|
}
|
@ -0,0 +1,130 @@
|
|||||||
|
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';
|
||||||
|
import {
|
||||||
|
longestPrefixMatch,
|
||||||
|
reconcileSettings,
|
||||||
|
} from './monitor-settings-utils';
|
||||||
|
import { ILogger } from '@theia/core';
|
||||||
|
|
||||||
|
const MONITOR_SETTINGS_FILE = 'pluggable-monitor-settings.json';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class MonitorSettingsProviderImpl implements MonitorSettingsProvider {
|
||||||
|
@inject(EnvVariablesServer)
|
||||||
|
protected readonly envVariablesServer: EnvVariablesServer;
|
||||||
|
|
||||||
|
@inject(ILogger)
|
||||||
|
protected logger: ILogger;
|
||||||
|
|
||||||
|
// deferred used to guarantee file operations are performed after the service is initialized
|
||||||
|
protected ready = new Deferred<void>();
|
||||||
|
|
||||||
|
// 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>;
|
||||||
|
|
||||||
|
// this is the path to the pluggable monitor settings file, set during init
|
||||||
|
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.readSettingsFromFS();
|
||||||
|
|
||||||
|
// init is done, resolve the deferred and unblock any call that was waiting for it
|
||||||
|
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);
|
||||||
|
|
||||||
|
this.monitorSettings[monitorId] = this.reconcileSettings(
|
||||||
|
matchingSettings,
|
||||||
|
defaultSettings
|
||||||
|
);
|
||||||
|
return this.monitorSettings[monitorId];
|
||||||
|
}
|
||||||
|
|
||||||
|
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.writeSettingsToFS();
|
||||||
|
return newSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
private reconcileSettings(
|
||||||
|
newSettings: PluggableMonitorSettings,
|
||||||
|
defaultSettings: PluggableMonitorSettings
|
||||||
|
): PluggableMonitorSettings {
|
||||||
|
return reconcileSettings(newSettings, defaultSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async readSettingsFromFS(): 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) {
|
||||||
|
this.logger.error(
|
||||||
|
'Could not parse the pluggable monitor settings file. Using empty file.'
|
||||||
|
);
|
||||||
|
this.monitorSettings = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async writeSettingsToFS(): Promise<void> {
|
||||||
|
await promisify(fs.writeFile)(
|
||||||
|
this.pluggableMonitorSettingsPath,
|
||||||
|
JSON.stringify(this.monitorSettings)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private longestPrefixMatch(id: string): {
|
||||||
|
matchingPrefix: string;
|
||||||
|
matchingSettings: PluggableMonitorSettings;
|
||||||
|
} {
|
||||||
|
return longestPrefixMatch(id, this.monitorSettings);
|
||||||
|
}
|
||||||
|
}
|
@ -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>;
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
import { PluggableMonitorSettings } from './monitor-settings-provider';
|
||||||
|
|
||||||
|
export function longestPrefixMatch(
|
||||||
|
id: string,
|
||||||
|
monitorSettings: Record<string, PluggableMonitorSettings>
|
||||||
|
): {
|
||||||
|
matchingPrefix: string;
|
||||||
|
matchingSettings: PluggableMonitorSettings;
|
||||||
|
} {
|
||||||
|
const separator = '-';
|
||||||
|
const idTokens = id.split(separator);
|
||||||
|
|
||||||
|
let matchingPrefix = '';
|
||||||
|
let matchingSettings: PluggableMonitorSettings = {};
|
||||||
|
|
||||||
|
const monitorSettingsKeys = Object.keys(monitorSettings);
|
||||||
|
|
||||||
|
for (let i = idTokens.length - 1; i >= 0; 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 = monitorSettings[monitorSettingsKeys[k]];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchingPrefix.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { matchingPrefix, matchingSettings };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reconcileSettings(
|
||||||
|
newSettings: PluggableMonitorSettings,
|
||||||
|
defaultSettings: PluggableMonitorSettings
|
||||||
|
): PluggableMonitorSettings {
|
||||||
|
// create a map with all the keys, merged together
|
||||||
|
const mergedSettingsKeys = Object.keys({
|
||||||
|
...defaultSettings,
|
||||||
|
...newSettings,
|
||||||
|
});
|
||||||
|
|
||||||
|
// for every key in the settings, we need to check if it exist in the default
|
||||||
|
for (const key of mergedSettingsKeys) {
|
||||||
|
// remove from the newSettings if it was not found in the default
|
||||||
|
if (defaultSettings[key] === undefined) {
|
||||||
|
delete newSettings[key];
|
||||||
|
}
|
||||||
|
// add to the newSettings if it was missing
|
||||||
|
else if (newSettings[key] === undefined) {
|
||||||
|
newSettings[key] = defaultSettings[key];
|
||||||
|
}
|
||||||
|
// if the key is found in both, reconcile the settings
|
||||||
|
else {
|
||||||
|
// save the value set by the user
|
||||||
|
const value = newSettings[key].selectedValue;
|
||||||
|
|
||||||
|
// settings needs to be overwritten with the defaults
|
||||||
|
newSettings[key] = defaultSettings[key];
|
||||||
|
|
||||||
|
// if there are no valid values defined, assume the one selected by the user is valid
|
||||||
|
// also use the value if it is a valid setting defined in the values
|
||||||
|
if (
|
||||||
|
!Array.isArray(newSettings[key].values) ||
|
||||||
|
newSettings[key].values.length === 0 ||
|
||||||
|
newSettings[key].values.includes(value)
|
||||||
|
) {
|
||||||
|
newSettings[key].selectedValue = value;
|
||||||
|
} else {
|
||||||
|
// if there are valid values but the user selected one that is not valid, fallback to the first valid one
|
||||||
|
newSettings[key].selectedValue = newSettings[key].values[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newSettings;
|
||||||
|
}
|
@ -1,26 +0,0 @@
|
|||||||
import * as grpc from '@grpc/grpc-js';
|
|
||||||
import { injectable } from '@theia/core/shared/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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,397 +0,0 @@
|
|||||||
import { ClientDuplexStream } from '@grpc/grpc-js';
|
|
||||||
import { TextEncoder } from 'util';
|
|
||||||
import { injectable, inject, named } from '@theia/core/shared/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;
|
|
||||||
}
|
|
@ -1,10 +1,10 @@
|
|||||||
import { Emitter } from '@theia/core';
|
import { Emitter } from '@theia/core';
|
||||||
import { injectable } from '@theia/core/shared/inversify';
|
import { injectable } from '@theia/core/shared/inversify';
|
||||||
import * as WebSocket from 'ws';
|
import * as WebSocket from 'ws';
|
||||||
import { WebSocketService } from './web-socket-service';
|
import { WebSocketProvider } from './web-socket-provider';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export default class WebSocketServiceImpl implements WebSocketService {
|
export default class WebSocketProviderImpl implements WebSocketProvider {
|
||||||
protected wsClients: WebSocket[];
|
protected wsClients: WebSocket[];
|
||||||
protected server: WebSocket.Server;
|
protected server: WebSocket.Server;
|
||||||
|
|
@ -1,8 +1,8 @@
|
|||||||
import { Event } from '@theia/core/lib/common/event';
|
import { Event } from '@theia/core/lib/common/event';
|
||||||
import * as WebSocket from 'ws';
|
import * as WebSocket from 'ws';
|
||||||
|
|
||||||
export const WebSocketService = Symbol('WebSocketService');
|
export const WebSocketProvider = Symbol('WebSocketProvider');
|
||||||
export interface WebSocketService {
|
export interface WebSocketProvider {
|
||||||
getAddress(): WebSocket.AddressInfo;
|
getAddress(): WebSocket.AddressInfo;
|
||||||
sendMessage(message: string): void;
|
sendMessage(message: string): void;
|
||||||
onMessageReceived: Event<string>;
|
onMessageReceived: Event<string>;
|
@ -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() {}
|
|
||||||
}
|
|
@ -0,0 +1,193 @@
|
|||||||
|
import { expect } from 'chai';
|
||||||
|
import {
|
||||||
|
longestPrefixMatch,
|
||||||
|
reconcileSettings,
|
||||||
|
} from '../../node/monitor-settings/monitor-settings-utils';
|
||||||
|
import { PluggableMonitorSettings } from '../../node/monitor-settings/monitor-settings-provider';
|
||||||
|
|
||||||
|
type DeepWriteable<T> = { -readonly [P in keyof T]: DeepWriteable<T[P]> };
|
||||||
|
|
||||||
|
describe('longestPrefixMatch', () => {
|
||||||
|
const settings = {
|
||||||
|
'arduino:avr:uno-port1-protocol1': {
|
||||||
|
name: 'Arduino Uno',
|
||||||
|
},
|
||||||
|
'arduino:avr:due-port1-protocol2': {
|
||||||
|
name: 'Arduino Due',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should return the exact prefix when found', async () => {
|
||||||
|
const prefix = 'arduino:avr:uno-port1-protocol1';
|
||||||
|
|
||||||
|
const { matchingPrefix } = longestPrefixMatch(
|
||||||
|
prefix,
|
||||||
|
settings as unknown as Record<string, PluggableMonitorSettings>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(matchingPrefix).to.equal(prefix);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the exact object when the prefix match', async () => {
|
||||||
|
const prefix = 'arduino:avr:uno-port1-protocol1';
|
||||||
|
|
||||||
|
const { matchingSettings } = longestPrefixMatch(
|
||||||
|
prefix,
|
||||||
|
settings as unknown as Record<string, PluggableMonitorSettings>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(matchingSettings).to.have.property('name').to.equal('Arduino Uno');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a partial matching prefix when a similar object is found', async () => {
|
||||||
|
const prefix = 'arduino:avr:due-port2-protocol2';
|
||||||
|
|
||||||
|
const { matchingPrefix } = longestPrefixMatch(
|
||||||
|
prefix,
|
||||||
|
settings as unknown as Record<string, PluggableMonitorSettings>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(matchingPrefix).to.equal('arduino:avr:due');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the closest object when the prefix partially match', async () => {
|
||||||
|
const prefix = 'arduino:avr:uno-port1-protocol2';
|
||||||
|
|
||||||
|
const { matchingSettings } = longestPrefixMatch(
|
||||||
|
prefix,
|
||||||
|
settings as unknown as Record<string, PluggableMonitorSettings>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(matchingSettings).to.have.property('name').to.equal('Arduino Uno');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty matching prefix when no similar object is found', async () => {
|
||||||
|
const prefix = 'arduino:avr:tre-port2-protocol2';
|
||||||
|
|
||||||
|
const { matchingPrefix } = longestPrefixMatch(
|
||||||
|
prefix,
|
||||||
|
settings as unknown as Record<string, PluggableMonitorSettings>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(matchingPrefix).to.equal('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty object when no similar object is found', async () => {
|
||||||
|
const prefix = 'arduino:avr:tre-port1-protocol2';
|
||||||
|
|
||||||
|
const { matchingSettings } = longestPrefixMatch(
|
||||||
|
prefix,
|
||||||
|
settings as unknown as Record<string, PluggableMonitorSettings>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(matchingSettings).to.be.empty;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('reconcileSettings', () => {
|
||||||
|
const defaultSettings = {
|
||||||
|
setting1: {
|
||||||
|
id: 'setting1',
|
||||||
|
label: 'Label setting1',
|
||||||
|
type: 'enum',
|
||||||
|
values: ['a', 'b', 'c'],
|
||||||
|
selectedValue: 'b',
|
||||||
|
},
|
||||||
|
setting2: {
|
||||||
|
id: 'setting2',
|
||||||
|
label: 'Label setting2',
|
||||||
|
type: 'enum',
|
||||||
|
values: ['a', 'b', 'c'],
|
||||||
|
selectedValue: 'b',
|
||||||
|
},
|
||||||
|
setting3: {
|
||||||
|
id: 'setting3',
|
||||||
|
label: 'Label setting3',
|
||||||
|
type: 'enum',
|
||||||
|
values: ['a', 'b', 'c'],
|
||||||
|
selectedValue: 'b',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should return default settings if new settings are missing', async () => {
|
||||||
|
const newSettings: PluggableMonitorSettings = {};
|
||||||
|
|
||||||
|
const reconciledSettings = reconcileSettings(newSettings, defaultSettings);
|
||||||
|
|
||||||
|
expect(reconciledSettings).to.deep.equal(defaultSettings);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add missing attributes copying it from the default settings', async () => {
|
||||||
|
const newSettings: PluggableMonitorSettings = JSON.parse(
|
||||||
|
JSON.stringify(defaultSettings)
|
||||||
|
);
|
||||||
|
delete newSettings.setting2;
|
||||||
|
|
||||||
|
const reconciledSettings = reconcileSettings(newSettings, defaultSettings);
|
||||||
|
|
||||||
|
expect(reconciledSettings).to.have.property('setting2');
|
||||||
|
});
|
||||||
|
it('should remove wrong settings attributes using the default settings as a reference', async () => {
|
||||||
|
const newSettings: PluggableMonitorSettings = JSON.parse(
|
||||||
|
JSON.stringify(defaultSettings)
|
||||||
|
);
|
||||||
|
newSettings['setting4'] = defaultSettings.setting3;
|
||||||
|
|
||||||
|
const reconciledSettings = reconcileSettings(newSettings, defaultSettings);
|
||||||
|
|
||||||
|
expect(reconciledSettings).not.to.have.property('setting4');
|
||||||
|
});
|
||||||
|
it('should reset non-value fields to those defiend in the default settings', async () => {
|
||||||
|
const newSettings: DeepWriteable<PluggableMonitorSettings> = JSON.parse(
|
||||||
|
JSON.stringify(defaultSettings)
|
||||||
|
);
|
||||||
|
newSettings['setting2'].id = 'fake id';
|
||||||
|
|
||||||
|
const reconciledSettings = reconcileSettings(newSettings, defaultSettings);
|
||||||
|
|
||||||
|
expect(reconciledSettings.setting2)
|
||||||
|
.to.have.property('id')
|
||||||
|
.equal('setting2');
|
||||||
|
});
|
||||||
|
it('should accept a selectedValue if it is a valid one', async () => {
|
||||||
|
const newSettings: PluggableMonitorSettings = JSON.parse(
|
||||||
|
JSON.stringify(defaultSettings)
|
||||||
|
);
|
||||||
|
newSettings.setting2.selectedValue = 'c';
|
||||||
|
|
||||||
|
const reconciledSettings = reconcileSettings(newSettings, defaultSettings);
|
||||||
|
|
||||||
|
expect(reconciledSettings.setting2)
|
||||||
|
.to.have.property('selectedValue')
|
||||||
|
.to.equal('c');
|
||||||
|
});
|
||||||
|
it('should fall a back to the first valid setting when the selectedValue is not valid', async () => {
|
||||||
|
const newSettings: PluggableMonitorSettings = JSON.parse(
|
||||||
|
JSON.stringify(defaultSettings)
|
||||||
|
);
|
||||||
|
newSettings.setting2.selectedValue = 'z';
|
||||||
|
|
||||||
|
const reconciledSettings = reconcileSettings(newSettings, defaultSettings);
|
||||||
|
|
||||||
|
expect(reconciledSettings.setting2)
|
||||||
|
.to.have.property('selectedValue')
|
||||||
|
.to.equal('a');
|
||||||
|
});
|
||||||
|
it('should accept any value if default values are not set', async () => {
|
||||||
|
const wrongDefaults: DeepWriteable<PluggableMonitorSettings> = JSON.parse(
|
||||||
|
JSON.stringify(defaultSettings)
|
||||||
|
);
|
||||||
|
wrongDefaults.setting2.values = [];
|
||||||
|
|
||||||
|
const newSettings: PluggableMonitorSettings = JSON.parse(
|
||||||
|
JSON.stringify(wrongDefaults)
|
||||||
|
);
|
||||||
|
newSettings.setting2.selectedValue = 'z';
|
||||||
|
|
||||||
|
const reconciledSettings = reconcileSettings(newSettings, wrongDefaults);
|
||||||
|
|
||||||
|
expect(reconciledSettings.setting2)
|
||||||
|
.to.have.property('selectedValue')
|
||||||
|
.to.equal('z');
|
||||||
|
});
|
||||||
|
});
|
@ -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;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
12
i18n/en.json
12
i18n/en.json
@ -215,6 +215,10 @@
|
|||||||
"sketch": "Sketch",
|
"sketch": "Sketch",
|
||||||
"tools": "Tools"
|
"tools": "Tools"
|
||||||
},
|
},
|
||||||
|
"monitor": {
|
||||||
|
"unableToCloseWebSocket": "Unable to close websocket",
|
||||||
|
"unableToConnectToWebSocket": "Unable to connect to websocket"
|
||||||
|
},
|
||||||
"preferences": {
|
"preferences": {
|
||||||
"additionalManagerURLs": "Additional Boards Manager URLs",
|
"additionalManagerURLs": "Additional Boards Manager URLs",
|
||||||
"auth.audience": "The OAuth2 audience.",
|
"auth.audience": "The OAuth2 audience.",
|
||||||
@ -264,25 +268,19 @@
|
|||||||
"serial": {
|
"serial": {
|
||||||
"autoscroll": "Autoscroll",
|
"autoscroll": "Autoscroll",
|
||||||
"carriageReturn": "Carriage Return",
|
"carriageReturn": "Carriage Return",
|
||||||
"connectionBusy": "Connection failed. Serial port is busy: {0}",
|
|
||||||
"disconnected": "Disconnected {0} from {1}.",
|
|
||||||
"failedReconnect": "Failed to reconnect {0} to serial port after 10 consecutive attempts. The {1} serial port is busy.",
|
|
||||||
"message": "Message ({0} + Enter to send message to '{1}' on '{2}')",
|
"message": "Message ({0} + Enter to send message to '{1}' on '{2}')",
|
||||||
"newLine": "New Line",
|
"newLine": "New Line",
|
||||||
"newLineCarriageReturn": "Both NL & CR",
|
"newLineCarriageReturn": "Both NL & CR",
|
||||||
"noLineEndings": "No Line Ending",
|
"noLineEndings": "No Line Ending",
|
||||||
"notConnected": "Not connected. Select a board and a port to connect automatically.",
|
"notConnected": "Not connected. Select a board and a port to connect automatically.",
|
||||||
"reconnect": "Reconnecting {0} to {1} in {2} seconds...",
|
|
||||||
"timestamp": "Timestamp",
|
"timestamp": "Timestamp",
|
||||||
"toggleTimestamp": "Toggle Timestamp",
|
"toggleTimestamp": "Toggle Timestamp"
|
||||||
"unexpectedError": "Unexpected error. Reconnecting {0} on port {1}."
|
|
||||||
},
|
},
|
||||||
"sketch": {
|
"sketch": {
|
||||||
"archiveSketch": "Archive Sketch",
|
"archiveSketch": "Archive Sketch",
|
||||||
"cantOpen": "A folder named \"{0}\" already exists. Can't open sketch.",
|
"cantOpen": "A folder named \"{0}\" already exists. Can't open sketch.",
|
||||||
"close": "Are you sure you want to close the sketch?",
|
"close": "Are you sure you want to close the sketch?",
|
||||||
"configureAndUpload": "Configure And Upload",
|
"configureAndUpload": "Configure And Upload",
|
||||||
"couldNotConnectToSerial": "Could not reconnect to serial port. {0}",
|
|
||||||
"createdArchive": "Created archive '{0}'.",
|
"createdArchive": "Created archive '{0}'.",
|
||||||
"doneCompiling": "Done compiling.",
|
"doneCompiling": "Done compiling.",
|
||||||
"doneUploading": "Done uploading.",
|
"doneUploading": "Done uploading.",
|
||||||
|
@ -4271,10 +4271,10 @@ archive-type@^4.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
file-type "^4.2.0"
|
file-type "^4.2.0"
|
||||||
|
|
||||||
arduino-serial-plotter-webapp@0.0.17:
|
arduino-serial-plotter-webapp@0.1.0:
|
||||||
version "0.0.17"
|
version "0.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/arduino-serial-plotter-webapp/-/arduino-serial-plotter-webapp-0.0.17.tgz#9a304df2a2fc95d9ec812b0d56288643292dd151"
|
resolved "https://registry.yarnpkg.com/arduino-serial-plotter-webapp/-/arduino-serial-plotter-webapp-0.1.0.tgz#fa631483a93a12acd89d7bbe0487a3c0e57fac9f"
|
||||||
integrity sha512-JGXFm2uJ+izzhk45ayq1ioXJOi5IZyK9De9fjCHCJKvc3BSGqBToZmRr3r1W5GPMfO88ySrGn9pfzZQtgI8Isg==
|
integrity sha512-0gHDGDz6guIC7Y8JXHaUad0RoueG2A+ykKNY1yo59+hWGbkM37hdRy4GKLsOkn0NMqU1TjnWmQHaSmYJjD1cAQ==
|
||||||
|
|
||||||
are-we-there-yet@^2.0.0:
|
are-we-there-yet@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user