mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-04-19 12:57:17 +00:00
Serial Plotter implementation (#597)
* spawn new window where to instantiate serial plotter app * initialize serial monito web app * connect serial plotter app with websocket * use npm serial-plotter package * refactor monitor connection and fix some connection issues * fix clearConsole + refactor monitor connection * add serial unit tests * refactoring and cleaning code
This commit is contained in:
parent
9863dc2f90
commit
20f7712129
@ -30,17 +30,20 @@ The Core Service is responsible for building your sketches and uploading them to
|
||||
- compiling a sketch for a selected board type
|
||||
- uploading a sketch to a connected board
|
||||
|
||||
#### Monitor Service
|
||||
#### Serial Service
|
||||
|
||||
The Monitor Service allows getting information back from sketches running on your Arduino boards.
|
||||
The Serial Service allows getting information back from sketches running on your Arduino boards.
|
||||
|
||||
- [src/common/protocol/monitor-service.ts](./src/common/protocol/monitor-service.ts) implements the common classes and interfaces
|
||||
- [src/node/monitor/monitor-service-impl.ts](./src/node/monitor/monitor-service-impl.ts) implements the service backend:
|
||||
- [src/common/protocol/serial-service.ts](./src/common/protocol/serial-service.ts) implements the common classes and interfaces
|
||||
- [src/node/serial/serial-service-impl.ts](./src/node/serial/serial-service-impl.ts) implements the service backend:
|
||||
- connecting to / disconnecting from a board
|
||||
- receiving and sending data
|
||||
- [src/browser/monitor/monitor-widget.tsx](./src/browser/monitor/monitor-widget.tsx) implements the serial monitor front-end:
|
||||
- [src/browser/serial/serial-connection-manager.ts](./src/browser/serial/serial-connection-manager.ts) handles the serial connection in the frontend
|
||||
- [src/browser/serial/monitor/monitor-widget.tsx](./src/browser/serial/monitor/monitor-widget.tsx) implements the serial monitor front-end:
|
||||
- viewing the output from a connected board
|
||||
- entering data to send to the board
|
||||
- [src/browser/serial/plotter/plotter-frontend-contribution.ts](./src/browser/serial/plotter/plotter-frontend-contribution.ts) implements the serial plotter front-end:
|
||||
- opening a new window running the [Serial Plotter Web App](https://github.com/arduino/arduino-serial-plotter-webapp)
|
||||
|
||||
#### Config Service
|
||||
|
||||
|
@ -4,10 +4,11 @@
|
||||
"description": "An extension for Theia building the Arduino IDE",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"scripts": {
|
||||
"prepare": "yarn download-cli && yarn download-fwuploader && yarn download-ls && yarn clean && yarn download-examples && yarn build && yarn test",
|
||||
"prepare": "yarn download-cli && yarn download-fwuploader && yarn download-ls && yarn copy-serial-plotter && yarn clean && yarn download-examples && yarn build && yarn test",
|
||||
"clean": "rimraf lib",
|
||||
"download-cli": "node ./scripts/download-cli.js",
|
||||
"download-fwuploader": "node ./scripts/download-fwuploader.js",
|
||||
"copy-serial-plotter": "npx ncp ../node_modules/arduino-serial-plotter-webapp ./build/arduino-serial-plotter-webapp",
|
||||
"download-ls": "node ./scripts/download-ls.js",
|
||||
"download-examples": "node ./scripts/download-examples.js",
|
||||
"generate-protocol": "node ./scripts/generate-protocol.js",
|
||||
@ -18,11 +19,12 @@
|
||||
"test:watch": "mocha --watch --watch-files lib \"./lib/test/**/*.test.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"arduino-serial-plotter-webapp": "0.0.15",
|
||||
"@grpc/grpc-js": "^1.3.7",
|
||||
"@theia/application-package": "1.18.0",
|
||||
"@theia/core": "1.18.0",
|
||||
"@theia/editor": "1.18.0",
|
||||
"@theia/editor-preview": "1.18.0",
|
||||
"@theia/editor-preview": "1.18.0",
|
||||
"@theia/filesystem": "1.18.0",
|
||||
"@theia/git": "1.18.0",
|
||||
"@theia/keymaps": "1.18.0",
|
||||
@ -77,6 +79,7 @@
|
||||
"open": "^8.0.6",
|
||||
"p-queue": "^5.0.0",
|
||||
"ps-tree": "^1.2.0",
|
||||
"query-string": "^7.0.1",
|
||||
"react-disable": "^0.1.0",
|
||||
"react-select": "^3.0.4",
|
||||
"react-tabs": "^3.1.2",
|
||||
|
@ -3,16 +3,13 @@ import {
|
||||
MAIN_MENU_BAR,
|
||||
MenuContribution,
|
||||
MenuModelRegistry,
|
||||
SelectionService,
|
||||
ILogger,
|
||||
DisposableCollection,
|
||||
} from '@theia/core';
|
||||
import {
|
||||
ContextMenuRenderer,
|
||||
FrontendApplication,
|
||||
FrontendApplicationContribution,
|
||||
LocalStorageService,
|
||||
OpenerService,
|
||||
StatusBar,
|
||||
StatusBarAlignment,
|
||||
} from '@theia/core/lib/browser';
|
||||
@ -35,7 +32,6 @@ import {
|
||||
EditorManager,
|
||||
EditorOpenerOptions,
|
||||
} from '@theia/editor/lib/browser';
|
||||
import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog';
|
||||
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
|
||||
import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu';
|
||||
import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
|
||||
@ -47,33 +43,25 @@ import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-con
|
||||
import { inject, injectable, postConstruct } from 'inversify';
|
||||
import * as React from 'react';
|
||||
import { remote } from 'electron';
|
||||
import { MainMenuManager } from '../common/main-menu-manager';
|
||||
import {
|
||||
BoardsService,
|
||||
CoreService,
|
||||
Port,
|
||||
SketchesService,
|
||||
ExecutableService,
|
||||
Sketch,
|
||||
} from '../common/protocol';
|
||||
import { ArduinoDaemon } from '../common/protocol/arduino-daemon';
|
||||
import { ConfigService } from '../common/protocol/config-service';
|
||||
import { FileSystemExt } from '../common/protocol/filesystem-ext';
|
||||
import { ArduinoCommands } from './arduino-commands';
|
||||
import { BoardsConfig } from './boards/boards-config';
|
||||
import { BoardsConfigDialog } from './boards/boards-config-dialog';
|
||||
import { BoardsDataStore } from './boards/boards-data-store';
|
||||
import { BoardsServiceProvider } from './boards/boards-service-provider';
|
||||
import { BoardsToolBarItem } from './boards/boards-toolbar-item';
|
||||
import { EditorMode } from './editor-mode';
|
||||
import { ArduinoMenus } from './menu/arduino-menus';
|
||||
import { MonitorConnection } from './monitor/monitor-connection';
|
||||
import { MonitorViewContribution } from './monitor/monitor-view-contribution';
|
||||
import { WorkspaceService } from './theia/workspace/workspace-service';
|
||||
import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution';
|
||||
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
|
||||
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { ResponseService } from '../common/protocol/response-service';
|
||||
import { ArduinoPreferences } from './arduino-preferences';
|
||||
import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl';
|
||||
import { SaveAsSketch } from './contributions/save-as-sketch';
|
||||
@ -101,24 +89,12 @@ export class ArduinoFrontendContribution
|
||||
@inject(BoardsService)
|
||||
protected readonly boardsService: BoardsService;
|
||||
|
||||
@inject(CoreService)
|
||||
protected readonly coreService: CoreService;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
|
||||
|
||||
@inject(SelectionService)
|
||||
protected readonly selectionService: SelectionService;
|
||||
|
||||
@inject(EditorManager)
|
||||
protected readonly editorManager: EditorManager;
|
||||
|
||||
@inject(ContextMenuRenderer)
|
||||
protected readonly contextMenuRenderer: ContextMenuRenderer;
|
||||
|
||||
@inject(FileDialogService)
|
||||
protected readonly fileDialogService: FileDialogService;
|
||||
|
||||
@inject(FileService)
|
||||
protected readonly fileService: FileService;
|
||||
|
||||
@ -128,21 +104,12 @@ export class ArduinoFrontendContribution
|
||||
@inject(BoardsConfigDialog)
|
||||
protected readonly boardsConfigDialog: BoardsConfigDialog;
|
||||
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(StatusBar)
|
||||
protected readonly statusBar: StatusBar;
|
||||
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
@inject(MonitorConnection)
|
||||
protected readonly monitorConnection: MonitorConnection;
|
||||
|
||||
@inject(FileNavigatorContribution)
|
||||
protected readonly fileNavigatorContributions: FileNavigatorContribution;
|
||||
|
||||
@ -167,40 +134,21 @@ export class ArduinoFrontendContribution
|
||||
@inject(EditorMode)
|
||||
protected readonly editorMode: EditorMode;
|
||||
|
||||
@inject(ArduinoDaemon)
|
||||
protected readonly daemon: ArduinoDaemon;
|
||||
|
||||
@inject(OpenerService)
|
||||
protected readonly openerService: OpenerService;
|
||||
|
||||
@inject(ConfigService)
|
||||
protected readonly configService: ConfigService;
|
||||
|
||||
@inject(BoardsDataStore)
|
||||
protected readonly boardsDataStore: BoardsDataStore;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
protected readonly mainMenuManager: MainMenuManager;
|
||||
|
||||
@inject(FileSystemExt)
|
||||
protected readonly fileSystemExt: FileSystemExt;
|
||||
|
||||
@inject(HostedPluginSupport)
|
||||
protected hostedPluginSupport: HostedPluginSupport;
|
||||
|
||||
@inject(ExecutableService)
|
||||
protected executableService: ExecutableService;
|
||||
|
||||
@inject(ResponseService)
|
||||
protected readonly responseService: ResponseService;
|
||||
|
||||
@inject(ArduinoPreferences)
|
||||
protected readonly arduinoPreferences: ArduinoPreferences;
|
||||
|
||||
@inject(SketchesServiceClientImpl)
|
||||
protected readonly sketchServiceClient: SketchesServiceClientImpl;
|
||||
|
||||
@inject(FrontendApplicationStateService)
|
||||
protected readonly appStateService: FrontendApplicationStateService;
|
||||
|
||||
@inject(LocalStorageService)
|
||||
|
@ -69,20 +69,20 @@ import { ScmContribution } from './theia/scm/scm-contribution';
|
||||
import { SearchInWorkspaceFrontendContribution as TheiaSearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
|
||||
import { SearchInWorkspaceFrontendContribution } from './theia/search-in-workspace/search-in-workspace-frontend-contribution';
|
||||
import { LibraryListWidgetFrontendContribution } from './library/library-widget-frontend-contribution';
|
||||
import { MonitorServiceClientImpl } from './monitor/monitor-service-client-impl';
|
||||
import { SerialServiceClientImpl } from './serial/serial-service-client-impl';
|
||||
import {
|
||||
MonitorServicePath,
|
||||
MonitorService,
|
||||
MonitorServiceClient,
|
||||
} from '../common/protocol/monitor-service';
|
||||
SerialServicePath,
|
||||
SerialService,
|
||||
SerialServiceClient,
|
||||
} from '../common/protocol/serial-service';
|
||||
import {
|
||||
ConfigService,
|
||||
ConfigServicePath,
|
||||
} from '../common/protocol/config-service';
|
||||
import { MonitorWidget } from './monitor/monitor-widget';
|
||||
import { MonitorViewContribution } from './monitor/monitor-view-contribution';
|
||||
import { MonitorConnection } from './monitor/monitor-connection';
|
||||
import { MonitorModel } from './monitor/monitor-model';
|
||||
import { MonitorWidget } from './serial/monitor/monitor-widget';
|
||||
import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution';
|
||||
import { SerialConnectionManager } from './serial/serial-connection-manager';
|
||||
import { SerialModel } from './serial/serial-model';
|
||||
import { TabBarDecoratorService as TheiaTabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
|
||||
import { TabBarDecoratorService } from './theia/core/tab-bar-decorator';
|
||||
import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browser';
|
||||
@ -253,6 +253,7 @@ import {
|
||||
UploadCertificateDialogProps,
|
||||
UploadCertificateDialogWidget,
|
||||
} from './dialogs/certificate-uploader/certificate-uploader-dialog';
|
||||
import { PlotterFrontendContribution } from './serial/plotter/plotter-frontend-contribution';
|
||||
import { nls } from '@theia/core/lib/browser/nls';
|
||||
|
||||
const ElementQueries = require('css-element-queries/src/ElementQueries');
|
||||
@ -386,8 +387,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
.inSingletonScope();
|
||||
|
||||
// Serial monitor
|
||||
bind(MonitorModel).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(MonitorModel);
|
||||
bind(SerialModel).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(SerialModel);
|
||||
bind(MonitorWidget).toSelf();
|
||||
bindViewContribution(bind, MonitorViewContribution);
|
||||
bind(TabBarToolbarContribution).toService(MonitorViewContribution);
|
||||
@ -395,18 +396,19 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
id: MonitorWidget.ID,
|
||||
createWidget: () => context.container.get(MonitorWidget),
|
||||
}));
|
||||
// Frontend binding for the serial monitor service
|
||||
bind(MonitorService)
|
||||
// Frontend binding for the serial service
|
||||
bind(SerialService)
|
||||
.toDynamicValue((context) => {
|
||||
const connection = context.container.get(WebSocketConnectionProvider);
|
||||
const client =
|
||||
context.container.get<MonitorServiceClient>(MonitorServiceClient);
|
||||
return connection.createProxy(MonitorServicePath, client);
|
||||
context.container.get<SerialServiceClient>(SerialServiceClient);
|
||||
return connection.createProxy(SerialServicePath, client);
|
||||
})
|
||||
.inSingletonScope();
|
||||
bind(MonitorConnection).toSelf().inSingletonScope();
|
||||
// Serial monitor service client to receive and delegate notifications from the backend.
|
||||
bind(MonitorServiceClient).to(MonitorServiceClientImpl).inSingletonScope();
|
||||
bind(SerialConnectionManager).toSelf().inSingletonScope();
|
||||
|
||||
// Serial service client to receive and delegate notifications from the backend.
|
||||
bind(SerialServiceClient).to(SerialServiceClientImpl).inSingletonScope();
|
||||
|
||||
bind(WorkspaceService).toSelf().inSingletonScope();
|
||||
rebind(TheiaWorkspaceService).toService(WorkspaceService);
|
||||
@ -597,6 +599,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
Contribution.configure(bind, AddFile);
|
||||
Contribution.configure(bind, ArchiveSketch);
|
||||
Contribution.configure(bind, AddZipLibrary);
|
||||
Contribution.configure(bind, PlotterFrontendContribution);
|
||||
|
||||
bind(ResponseServiceImpl)
|
||||
.toSelf()
|
||||
|
@ -64,7 +64,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
* This even also fires, when the boards package was not available for the currently selected board,
|
||||
* and the user installs the board package. Note: installing a board package will set the `fqbn` of the
|
||||
* currently selected board.\
|
||||
* This even also emitted when the board package for the currently selected board was uninstalled.
|
||||
* This event is also emitted when the board package for the currently selected board was uninstalled.
|
||||
*/
|
||||
readonly onBoardsConfigChanged = this.onBoardsConfigChangedEmitter.event;
|
||||
readonly onAvailableBoardsChanged =
|
||||
|
@ -138,7 +138,11 @@ PID: ${PID}`;
|
||||
// The board specific items, and the rest, have order with `z`. We needed something between `0` and `z` with natural-order.
|
||||
this.menuModelRegistry.registerSubmenu(
|
||||
boardsSubmenuPath,
|
||||
nls.localize('arduino/board/board', 'Board{0}', !!boardsSubmenuLabel ? `: "${boardsSubmenuLabel}"` : ''),
|
||||
nls.localize(
|
||||
'arduino/board/board',
|
||||
'Board{0}',
|
||||
!!boardsSubmenuLabel ? `: "${boardsSubmenuLabel}"` : ''
|
||||
),
|
||||
{ order: '100' }
|
||||
);
|
||||
this.toDisposeBeforeMenuRebuild.push(
|
||||
@ -155,7 +159,11 @@ PID: ${PID}`;
|
||||
const portsSubmenuLabel = config.selectedPort?.address;
|
||||
this.menuModelRegistry.registerSubmenu(
|
||||
portsSubmenuPath,
|
||||
nls.localize('arduino/board/port', 'Port{0}', portsSubmenuLabel ? `: "${portsSubmenuLabel}"` : ''),
|
||||
nls.localize(
|
||||
'arduino/board/port',
|
||||
'Port{0}',
|
||||
portsSubmenuLabel ? `: "${portsSubmenuLabel}"` : ''
|
||||
),
|
||||
{ order: '101' }
|
||||
);
|
||||
this.toDisposeBeforeMenuRebuild.push(
|
||||
@ -193,9 +201,10 @@ PID: ${PID}`;
|
||||
|
||||
const packageLabel =
|
||||
packageName +
|
||||
`${manuallyInstalled
|
||||
? nls.localize('arduino/board/inSketchbook', ' (in Sketchbook)')
|
||||
: ''
|
||||
`${
|
||||
manuallyInstalled
|
||||
? nls.localize('arduino/board/inSketchbook', ' (in Sketchbook)')
|
||||
: ''
|
||||
}`;
|
||||
// Platform submenu
|
||||
const platformMenuPath = [...boardsPackagesGroup, packageId];
|
||||
@ -268,8 +277,9 @@ PID: ${PID}`;
|
||||
});
|
||||
}
|
||||
for (const { name, fqbn } of boards) {
|
||||
const id = `arduino-select-port--${address}${fqbn ? `--${fqbn}` : ''
|
||||
}`;
|
||||
const id = `arduino-select-port--${address}${
|
||||
fqbn ? `--${fqbn}` : ''
|
||||
}`;
|
||||
const command = { id };
|
||||
const handler = {
|
||||
execute: () => {
|
||||
|
@ -3,7 +3,7 @@ import { OutputChannelManager } from '@theia/output/lib/common/output-channel';
|
||||
import { CoreService } from '../../common/protocol';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { BoardsDataStore } from '../boards/boards-data-store';
|
||||
import { MonitorConnection } from '../monitor/monitor-connection';
|
||||
import { SerialConnectionManager } from '../serial/serial-connection-manager';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import {
|
||||
SketchContribution,
|
||||
@ -18,8 +18,8 @@ export class BurnBootloader extends SketchContribution {
|
||||
@inject(CoreService)
|
||||
protected readonly coreService: CoreService;
|
||||
|
||||
@inject(MonitorConnection)
|
||||
protected readonly monitorConnection: MonitorConnection;
|
||||
@inject(SerialConnectionManager)
|
||||
protected readonly serialConnection: SerialConnectionManager;
|
||||
|
||||
@inject(BoardsDataStore)
|
||||
protected readonly boardsDataStore: BoardsDataStore;
|
||||
@ -48,10 +48,7 @@ export class BurnBootloader extends SketchContribution {
|
||||
}
|
||||
|
||||
async burnBootloader(): Promise<void> {
|
||||
const monitorConfig = this.monitorConnection.monitorConfig;
|
||||
if (monitorConfig) {
|
||||
await this.monitorConnection.disconnect();
|
||||
}
|
||||
await this.serialConnection.disconnect();
|
||||
try {
|
||||
const { boardsConfig } = this.boardsServiceClientImpl;
|
||||
const port = boardsConfig.selectedPort;
|
||||
@ -84,8 +81,8 @@ export class BurnBootloader extends SketchContribution {
|
||||
} catch (e) {
|
||||
this.messageService.error(e.toString());
|
||||
} finally {
|
||||
if (monitorConfig) {
|
||||
await this.monitorConnection.connect(monitorConfig);
|
||||
if (this.serialConnection.isSerialOpen()) {
|
||||
await this.serialConnection.connect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import { CoreService } from '../../common/protocol';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import { BoardsDataStore } from '../boards/boards-data-store';
|
||||
import { MonitorConnection } from '../monitor/monitor-connection';
|
||||
import { SerialConnectionManager } from '../serial/serial-connection-manager';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import {
|
||||
SketchContribution,
|
||||
@ -21,8 +21,8 @@ export class UploadSketch extends SketchContribution {
|
||||
@inject(CoreService)
|
||||
protected readonly coreService: CoreService;
|
||||
|
||||
@inject(MonitorConnection)
|
||||
protected readonly monitorConnection: MonitorConnection;
|
||||
@inject(SerialConnectionManager)
|
||||
protected readonly serialConnection: SerialConnectionManager;
|
||||
|
||||
@inject(BoardsDataStore)
|
||||
protected readonly boardsDataStore: BoardsDataStore;
|
||||
@ -108,15 +108,7 @@ export class UploadSketch extends SketchContribution {
|
||||
if (!sketch) {
|
||||
return;
|
||||
}
|
||||
let shouldAutoConnect = false;
|
||||
const monitorConfig = this.monitorConnection.monitorConfig;
|
||||
if (monitorConfig) {
|
||||
await this.monitorConnection.disconnect();
|
||||
if (this.monitorConnection.autoConnect) {
|
||||
shouldAutoConnect = true;
|
||||
}
|
||||
this.monitorConnection.autoConnect = false;
|
||||
}
|
||||
await this.serialConnection.disconnect();
|
||||
try {
|
||||
const { boardsConfig } = this.boardsServiceClientImpl;
|
||||
const [fqbn, { selectedProgrammer }, verify, verbose, sourceOverride] =
|
||||
@ -175,24 +167,22 @@ export class UploadSketch extends SketchContribution {
|
||||
this.uploadInProgress = false;
|
||||
this.onDidChangeEmitter.fire();
|
||||
|
||||
if (monitorConfig) {
|
||||
const { board, port } = monitorConfig;
|
||||
if (
|
||||
this.serialConnection.isSerialOpen() &&
|
||||
this.serialConnection.serialConfig
|
||||
) {
|
||||
const { board, port } = this.serialConnection.serialConfig;
|
||||
try {
|
||||
await this.boardsServiceClientImpl.waitUntilAvailable(
|
||||
Object.assign(board, { port }),
|
||||
10_000
|
||||
);
|
||||
if (shouldAutoConnect) {
|
||||
// Enabling auto-connect will trigger a connect.
|
||||
this.monitorConnection.autoConnect = true;
|
||||
} else {
|
||||
await this.monitorConnection.connect(monitorConfig);
|
||||
}
|
||||
await this.serialConnection.connect();
|
||||
} catch (waitError) {
|
||||
this.messageService.error(
|
||||
nls.localize(
|
||||
'arduino/sketch/couldNotConnectToMonitor',
|
||||
'Could not reconnect to serial monitor. {0}',
|
||||
'arduino/sketch/couldNotConnectToSerial',
|
||||
'Could not reconnect to serial port. {0}',
|
||||
waitError.toString()
|
||||
)
|
||||
);
|
||||
|
@ -86,7 +86,7 @@ export namespace ArduinoMenus {
|
||||
|
||||
// -- Tools
|
||||
export const TOOLS = [...MAIN_MENU_BAR, '4_tools'];
|
||||
// `Auto Format`, `Archive Sketch`, `Manage Libraries...`, `Serial Monitor`
|
||||
// `Auto Format`, `Archive Sketch`, `Manage Libraries...`, `Serial Monitor`, Serial Plotter
|
||||
export const TOOLS__MAIN_GROUP = [...TOOLS, '0_main'];
|
||||
// `WiFi101 / WiFiNINA Firmware Updater`
|
||||
export const TOOLS__FIRMWARE_UPLOADER_GROUP = [
|
||||
|
@ -1,381 +0,0 @@
|
||||
import { injectable, inject, postConstruct } from 'inversify';
|
||||
import { deepClone } from '@theia/core/lib/common/objects';
|
||||
import { Emitter, Event } from '@theia/core/lib/common/event';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||
import {
|
||||
MonitorService,
|
||||
MonitorConfig,
|
||||
MonitorError,
|
||||
Status,
|
||||
MonitorServiceClient,
|
||||
} from '../../common/protocol/monitor-service';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import {
|
||||
Port,
|
||||
Board,
|
||||
BoardsService,
|
||||
AttachedBoardsChangeEvent,
|
||||
} from '../../common/protocol/boards-service';
|
||||
import { BoardsConfig } from '../boards/boards-config';
|
||||
import { MonitorModel } from './monitor-model';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { nls } from '@theia/core/lib/browser/nls';
|
||||
|
||||
@injectable()
|
||||
export class MonitorConnection {
|
||||
@inject(MonitorModel)
|
||||
protected readonly monitorModel: MonitorModel;
|
||||
|
||||
@inject(MonitorService)
|
||||
protected readonly monitorService: MonitorService;
|
||||
|
||||
@inject(MonitorServiceClient)
|
||||
protected readonly monitorServiceClient: MonitorServiceClient;
|
||||
|
||||
@inject(BoardsService)
|
||||
protected readonly boardsService: BoardsService;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
@inject(MessageService)
|
||||
protected messageService: MessageService;
|
||||
|
||||
@inject(FrontendApplicationStateService)
|
||||
protected readonly applicationState: FrontendApplicationStateService;
|
||||
|
||||
protected state: MonitorConnection.State | undefined;
|
||||
/**
|
||||
* Note: The idea is to toggle this property from the UI (`Monitor` view)
|
||||
* and the boards config and the boards attachment/detachment logic can be at on place, here.
|
||||
*/
|
||||
protected _autoConnect = false;
|
||||
protected readonly onConnectionChangedEmitter = new Emitter<
|
||||
MonitorConnection.State | undefined
|
||||
>();
|
||||
/**
|
||||
* This emitter forwards all read events **iff** the connection is established.
|
||||
*/
|
||||
protected readonly onReadEmitter = new Emitter<{ messages: string[] }>();
|
||||
|
||||
/**
|
||||
* Array for storing previous monitor errors received from the server, and based on the number of elements in this array,
|
||||
* 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 monitorErrors: MonitorError[] = [];
|
||||
protected reconnectTimeout?: number;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.monitorServiceClient.onMessage(this.handleMessage.bind(this));
|
||||
this.monitorServiceClient.onError(this.handleError.bind(this));
|
||||
this.boardsServiceProvider.onBoardsConfigChanged(
|
||||
this.handleBoardConfigChange.bind(this)
|
||||
);
|
||||
this.notificationCenter.onAttachedBoardsChanged(
|
||||
this.handleAttachedBoardsChanged.bind(this)
|
||||
);
|
||||
|
||||
// Handles the `baudRate` changes by reconnecting if required.
|
||||
this.monitorModel.onChange(({ property }) => {
|
||||
if (property === 'baudRate' && this.autoConnect && this.connected) {
|
||||
const { boardsConfig } = this.boardsServiceProvider;
|
||||
this.handleBoardConfigChange(boardsConfig);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async handleMessage(port: string): Promise<void> {
|
||||
const w = new WebSocket(`ws://localhost:${port}`);
|
||||
w.onmessage = (res) => {
|
||||
const messages = JSON.parse(res.data);
|
||||
this.onReadEmitter.fire({ messages });
|
||||
};
|
||||
}
|
||||
|
||||
get connected(): boolean {
|
||||
return !!this.state;
|
||||
}
|
||||
|
||||
get monitorConfig(): MonitorConfig | undefined {
|
||||
return this.state ? this.state.config : undefined;
|
||||
}
|
||||
|
||||
get autoConnect(): boolean {
|
||||
return this._autoConnect;
|
||||
}
|
||||
|
||||
set autoConnect(value: boolean) {
|
||||
const oldValue = this._autoConnect;
|
||||
this._autoConnect = value;
|
||||
// When we enable the auto-connect, we have to connect
|
||||
if (!oldValue && value) {
|
||||
// We have to make sure the previous boards config has been restored.
|
||||
// Otherwise, we might start the auto-connection without configured boards.
|
||||
this.applicationState.reachedState('started_contributions').then(() => {
|
||||
const { boardsConfig } = this.boardsServiceProvider;
|
||||
this.handleBoardConfigChange(boardsConfig);
|
||||
});
|
||||
} else if (oldValue && !value) {
|
||||
if (this.reconnectTimeout !== undefined) {
|
||||
window.clearTimeout(this.reconnectTimeout);
|
||||
this.monitorErrors.length = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleError(error: MonitorError): void {
|
||||
let shouldReconnect = false;
|
||||
if (this.state) {
|
||||
const { code, config } = error;
|
||||
const { board, port } = config;
|
||||
const options = { timeout: 3000 };
|
||||
switch (code) {
|
||||
case MonitorError.ErrorCodes.CLIENT_CANCEL: {
|
||||
console.debug(
|
||||
`Connection was canceled by client: ${MonitorConnection.State.toString(
|
||||
this.state
|
||||
)}.`
|
||||
);
|
||||
break;
|
||||
}
|
||||
case MonitorError.ErrorCodes.DEVICE_BUSY: {
|
||||
this.messageService.warn(
|
||||
nls.localize(
|
||||
'arduino/monitor/connectionBusy',
|
||||
'Connection failed. Serial port is busy: {0}',
|
||||
Port.toString(port)
|
||||
),
|
||||
options
|
||||
);
|
||||
shouldReconnect = this.autoConnect;
|
||||
this.monitorErrors.push(error);
|
||||
break;
|
||||
}
|
||||
case MonitorError.ErrorCodes.DEVICE_NOT_CONFIGURED: {
|
||||
this.messageService.info(
|
||||
nls.localize(
|
||||
'arduino/monitor/disconnected',
|
||||
'Disconnected {0} from {1}.',
|
||||
Board.toString(board, {
|
||||
useFqbn: false,
|
||||
}),
|
||||
Port.toString(port)
|
||||
),
|
||||
options
|
||||
);
|
||||
break;
|
||||
}
|
||||
case undefined: {
|
||||
this.messageService.error(
|
||||
nls.localize(
|
||||
'arduino/monitor/unexpectedError',
|
||||
'Unexpected error. Reconnecting {0} on port {1}.',
|
||||
Board.toString(board),
|
||||
Port.toString(port)
|
||||
),
|
||||
options
|
||||
);
|
||||
console.error(JSON.stringify(error));
|
||||
shouldReconnect = this.connected && this.autoConnect;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const oldState = this.state;
|
||||
this.state = undefined;
|
||||
this.onConnectionChangedEmitter.fire(this.state);
|
||||
if (shouldReconnect) {
|
||||
if (this.monitorErrors.length >= 10) {
|
||||
this.messageService.warn(
|
||||
nls.localize(
|
||||
'arduino/monitor/failedReconnect',
|
||||
'Failed to reconnect {0} to the the serial-monitor after 10 consecutive attempts. The {1} serial port is busy.',
|
||||
Board.toString(board, {
|
||||
useFqbn: false,
|
||||
}),
|
||||
Port.toString(port)
|
||||
)
|
||||
);
|
||||
this.monitorErrors.length = 0;
|
||||
} else {
|
||||
const attempts = this.monitorErrors.length || 1;
|
||||
if (this.reconnectTimeout !== undefined) {
|
||||
// Clear the previous timer.
|
||||
window.clearTimeout(this.reconnectTimeout);
|
||||
}
|
||||
const timeout = attempts * 1000;
|
||||
this.messageService.warn(
|
||||
nls.localize(
|
||||
'arduino/monitor/reconnect',
|
||||
'Reconnecting {0} to {1} in {2] seconds...',
|
||||
Board.toString(board, {
|
||||
useFqbn: false,
|
||||
}),
|
||||
Port.toString(port),
|
||||
attempts.toString()
|
||||
),
|
||||
{ timeout }
|
||||
);
|
||||
this.reconnectTimeout = window.setTimeout(
|
||||
() => this.connect(oldState.config),
|
||||
timeout
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
|
||||
if (this.autoConnect && this.connected) {
|
||||
const { boardsConfig } = this.boardsServiceProvider;
|
||||
if (
|
||||
this.boardsServiceProvider.canUploadTo(boardsConfig, {
|
||||
silent: false,
|
||||
})
|
||||
) {
|
||||
const { attached } = AttachedBoardsChangeEvent.diff(event);
|
||||
if (
|
||||
attached.boards.some(
|
||||
(board) =>
|
||||
!!board.port && BoardsConfig.Config.sameAs(boardsConfig, board)
|
||||
)
|
||||
) {
|
||||
const { selectedBoard: board, selectedPort: port } = boardsConfig;
|
||||
const { baudRate } = this.monitorModel;
|
||||
this.disconnect().then(() => this.connect({ board, port, baudRate }));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async connect(config: MonitorConfig): Promise<Status> {
|
||||
if (this.connected) {
|
||||
const disconnectStatus = await this.disconnect();
|
||||
if (!Status.isOK(disconnectStatus)) {
|
||||
return disconnectStatus;
|
||||
}
|
||||
}
|
||||
console.info(
|
||||
`>>> Creating serial monitor connection for ${Board.toString(
|
||||
config.board
|
||||
)} on port ${Port.toString(config.port)}...`
|
||||
);
|
||||
const connectStatus = await this.monitorService.connect(config);
|
||||
if (Status.isOK(connectStatus)) {
|
||||
this.state = { config };
|
||||
console.info(
|
||||
`<<< Serial monitor connection created for ${Board.toString(
|
||||
config.board,
|
||||
{ useFqbn: false }
|
||||
)} on port ${Port.toString(config.port)}.`
|
||||
);
|
||||
}
|
||||
this.onConnectionChangedEmitter.fire(this.state);
|
||||
return Status.isOK(connectStatus);
|
||||
}
|
||||
|
||||
async disconnect(): Promise<Status> {
|
||||
if (!this.connected) {
|
||||
return Status.OK;
|
||||
}
|
||||
const stateCopy = deepClone(this.state);
|
||||
if (!stateCopy) {
|
||||
return Status.OK;
|
||||
}
|
||||
console.log('>>> Disposing existing monitor connection...');
|
||||
const status = await this.monitorService.disconnect();
|
||||
if (Status.isOK(status)) {
|
||||
console.log(
|
||||
`<<< Disposed connection. Was: ${MonitorConnection.State.toString(
|
||||
stateCopy
|
||||
)}`
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
`<<< Could not dispose connection. Activate connection: ${MonitorConnection.State.toString(
|
||||
stateCopy
|
||||
)}`
|
||||
);
|
||||
}
|
||||
this.state = undefined;
|
||||
this.onConnectionChangedEmitter.fire(this.state);
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the data to the connected serial monitor.
|
||||
* 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 (!this.connected) {
|
||||
return Status.NOT_CONNECTED;
|
||||
}
|
||||
return new Promise<Status>((resolve) => {
|
||||
this.monitorService
|
||||
.send(data + this.monitorModel.lineEnding)
|
||||
.then(() => resolve(Status.OK));
|
||||
});
|
||||
}
|
||||
|
||||
get onConnectionChanged(): Event<MonitorConnection.State | undefined> {
|
||||
return this.onConnectionChangedEmitter.event;
|
||||
}
|
||||
|
||||
get onRead(): Event<{ messages: string[] }> {
|
||||
return this.onReadEmitter.event;
|
||||
}
|
||||
|
||||
protected async handleBoardConfigChange(
|
||||
boardsConfig: BoardsConfig.Config
|
||||
): Promise<void> {
|
||||
if (this.autoConnect) {
|
||||
if (
|
||||
this.boardsServiceProvider.canUploadTo(boardsConfig, {
|
||||
silent: false,
|
||||
})
|
||||
) {
|
||||
// Instead of calling `getAttachedBoards` and filtering for `AttachedSerialBoard` we have to check the available ports.
|
||||
// The connected board might be unknown. See: https://github.com/arduino/arduino-pro-ide/issues/127#issuecomment-563251881
|
||||
this.boardsService.getAvailablePorts().then((ports) => {
|
||||
if (
|
||||
ports.some((port) => Port.equals(port, boardsConfig.selectedPort))
|
||||
) {
|
||||
new Promise<void>((resolve) => {
|
||||
// First, disconnect if connected.
|
||||
if (this.connected) {
|
||||
this.disconnect().then(() => resolve());
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
}).then(() => {
|
||||
// Then (re-)connect.
|
||||
const { selectedBoard: board, selectedPort: port } = boardsConfig;
|
||||
const { baudRate } = this.monitorModel;
|
||||
this.connect({ board, port, baudRate });
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace MonitorConnection {
|
||||
export interface State {
|
||||
readonly config: MonitorConfig;
|
||||
}
|
||||
|
||||
export namespace State {
|
||||
export function toString(state: State): string {
|
||||
const { config } = state;
|
||||
const { board, port } = config;
|
||||
return `${Board.toString(board)} ${Port.toString(port)}`;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import {
|
||||
MonitorServiceClient,
|
||||
MonitorError,
|
||||
} from '../../common/protocol/monitor-service';
|
||||
|
||||
@injectable()
|
||||
export class MonitorServiceClientImpl implements MonitorServiceClient {
|
||||
protected readonly onErrorEmitter = new Emitter<MonitorError>();
|
||||
readonly onError = this.onErrorEmitter.event;
|
||||
|
||||
protected readonly onMessageEmitter = new Emitter<string>();
|
||||
readonly onMessage = this.onMessageEmitter.event;
|
||||
|
||||
notifyError(error: MonitorError): void {
|
||||
this.onErrorEmitter.fire(error);
|
||||
}
|
||||
|
||||
notifyMessage(message: string): void {
|
||||
this.onMessageEmitter.fire(message);
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ export function messagesToLines(
|
||||
const linesToAdd: Line[] = prevLines.length
|
||||
? [prevLines[prevLines.length - 1]]
|
||||
: [{ message: '', lineLen: 0 }];
|
||||
if (!(Symbol.iterator in Object(messages))) return [prevLines, charCount];
|
||||
|
||||
for (const message of messages) {
|
||||
const messageLen = message.length;
|
@ -7,9 +7,9 @@ import {
|
||||
TabBarToolbarContribution,
|
||||
TabBarToolbarRegistry,
|
||||
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import { MonitorModel } from './monitor-model';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ArduinoToolbar } from '../../toolbar/arduino-toolbar';
|
||||
import { SerialModel } from '../serial-model';
|
||||
import { ArduinoMenus } from '../../menu/arduino-menus';
|
||||
import { nls } from '@theia/core/lib/browser/nls';
|
||||
|
||||
export namespace SerialMonitor {
|
||||
@ -19,14 +19,14 @@ export namespace SerialMonitor {
|
||||
id: 'serial-monitor-autoscroll',
|
||||
label: 'Autoscroll',
|
||||
},
|
||||
'arduino/monitor/autoscroll'
|
||||
'arduino/serial/autoscroll'
|
||||
);
|
||||
export const TIMESTAMP = Command.toLocalizedCommand(
|
||||
{
|
||||
id: 'serial-monitor-timestamp',
|
||||
label: 'Timestamp',
|
||||
},
|
||||
'arduino/monitor/timestamp'
|
||||
'arduino/serial/timestamp'
|
||||
);
|
||||
export const CLEAR_OUTPUT = Command.toLocalizedCommand(
|
||||
{
|
||||
@ -48,7 +48,7 @@ export class MonitorViewContribution
|
||||
static readonly TOGGLE_SERIAL_MONITOR_TOOLBAR =
|
||||
MonitorWidget.ID + ':toggle-toolbar';
|
||||
|
||||
@inject(MonitorModel) protected readonly model: MonitorModel;
|
||||
@inject(SerialModel) protected readonly model: SerialModel;
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
@ -156,7 +156,7 @@ export class MonitorViewContribution
|
||||
<React.Fragment key="line-ending-toolbar-item">
|
||||
<div
|
||||
title={nls.localize(
|
||||
'arduino/monitor/toggleTimestamp',
|
||||
'arduino/serial/toggleTimestamp',
|
||||
'Toggle Timestamp'
|
||||
)}
|
||||
className={`item enabled fa fa-clock-o arduino-monitor ${
|
@ -9,27 +9,31 @@ import {
|
||||
Widget,
|
||||
MessageLoop,
|
||||
} from '@theia/core/lib/browser/widgets';
|
||||
import { MonitorConfig } from '../../common/protocol/monitor-service';
|
||||
import { ArduinoSelect } from '../widgets/arduino-select';
|
||||
import { MonitorModel } from './monitor-model';
|
||||
import { MonitorConnection } from './monitor-connection';
|
||||
import { SerialConfig } from '../../../common/protocol/serial-service';
|
||||
import { ArduinoSelect } from '../../widgets/arduino-select';
|
||||
import { SerialModel } from '../serial-model';
|
||||
import { Serial, SerialConnectionManager } from '../serial-connection-manager';
|
||||
import { SerialMonitorSendInput } from './serial-monitor-send-input';
|
||||
import { SerialMonitorOutput } from './serial-monitor-send-output';
|
||||
import { nls } from '@theia/core/lib/browser/nls';
|
||||
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
|
||||
|
||||
@injectable()
|
||||
export class MonitorWidget extends ReactWidget {
|
||||
static readonly LABEL = nls.localize(
|
||||
'arduino/monitor/title',
|
||||
'arduino/common/serialMonitor',
|
||||
'Serial Monitor'
|
||||
);
|
||||
static readonly ID = 'serial-monitor';
|
||||
|
||||
@inject(MonitorModel)
|
||||
protected readonly monitorModel: MonitorModel;
|
||||
@inject(SerialModel)
|
||||
protected readonly serialModel: SerialModel;
|
||||
|
||||
@inject(MonitorConnection)
|
||||
protected readonly monitorConnection: MonitorConnection;
|
||||
@inject(SerialConnectionManager)
|
||||
protected readonly serialConnection: SerialConnectionManager;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
||||
protected widgetHeight: number;
|
||||
|
||||
@ -53,12 +57,9 @@ export class MonitorWidget extends ReactWidget {
|
||||
this.scrollOptions = undefined;
|
||||
this.toDispose.push(this.clearOutputEmitter);
|
||||
this.toDispose.push(
|
||||
Disposable.create(() => {
|
||||
this.monitorConnection.autoConnect = false;
|
||||
if (this.monitorConnection.connected) {
|
||||
this.monitorConnection.disconnect();
|
||||
}
|
||||
})
|
||||
Disposable.create(() =>
|
||||
this.serialConnection.closeSerial(Serial.Type.Monitor)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -66,8 +67,9 @@ export class MonitorWidget extends ReactWidget {
|
||||
protected init(): void {
|
||||
this.update();
|
||||
this.toDispose.push(
|
||||
this.monitorConnection.onConnectionChanged(() => this.clearConsole())
|
||||
this.serialConnection.onConnectionChanged(() => this.clearConsole())
|
||||
);
|
||||
this.toDispose.push(this.serialModel.onChange(() => this.update()));
|
||||
}
|
||||
|
||||
clearConsole(): void {
|
||||
@ -81,7 +83,7 @@ export class MonitorWidget extends ReactWidget {
|
||||
|
||||
protected onAfterAttach(msg: Message): void {
|
||||
super.onAfterAttach(msg);
|
||||
this.monitorConnection.autoConnect = true;
|
||||
this.serialConnection.openSerial(Serial.Type.Monitor);
|
||||
}
|
||||
|
||||
onCloseRequest(msg: Message): void {
|
||||
@ -119,27 +121,24 @@ export class MonitorWidget extends ReactWidget {
|
||||
};
|
||||
|
||||
protected get lineEndings(): OptionsType<
|
||||
SerialMonitorOutput.SelectOption<MonitorModel.EOL>
|
||||
SerialMonitorOutput.SelectOption<SerialModel.EOL>
|
||||
> {
|
||||
return [
|
||||
{
|
||||
label: nls.localize('arduino/monitor/noLineEndings', 'No Line Ending'),
|
||||
label: nls.localize('arduino/serial/noLineEndings', 'No Line Ending'),
|
||||
value: '',
|
||||
},
|
||||
{
|
||||
label: nls.localize('arduino/monitor/newLine', 'New Line'),
|
||||
label: nls.localize('arduino/serial/newLine', 'New Line'),
|
||||
value: '\n',
|
||||
},
|
||||
{
|
||||
label: nls.localize(
|
||||
'arduino/monitor/carriageReturn',
|
||||
'Carriage Return'
|
||||
),
|
||||
label: nls.localize('arduino/serial/carriageReturn', 'Carriage Return'),
|
||||
value: '\r',
|
||||
},
|
||||
{
|
||||
label: nls.localize(
|
||||
'arduino/monitor/newLineCarriageReturn',
|
||||
'arduino/serial/newLineCarriageReturn',
|
||||
'Both NL & CR'
|
||||
),
|
||||
value: '\r\n',
|
||||
@ -148,9 +147,9 @@ export class MonitorWidget extends ReactWidget {
|
||||
}
|
||||
|
||||
protected get baudRates(): OptionsType<
|
||||
SerialMonitorOutput.SelectOption<MonitorConfig.BaudRate>
|
||||
SerialMonitorOutput.SelectOption<SerialConfig.BaudRate>
|
||||
> {
|
||||
const baudRates: Array<MonitorConfig.BaudRate> = [
|
||||
const baudRates: Array<SerialConfig.BaudRate> = [
|
||||
300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200,
|
||||
];
|
||||
return baudRates.map((baudRate) => ({
|
||||
@ -162,17 +161,17 @@ export class MonitorWidget extends ReactWidget {
|
||||
protected render(): React.ReactNode {
|
||||
const { baudRates, lineEndings } = this;
|
||||
const lineEnding =
|
||||
lineEndings.find((item) => item.value === this.monitorModel.lineEnding) ||
|
||||
lineEndings.find((item) => item.value === this.serialModel.lineEnding) ||
|
||||
lineEndings[1]; // Defaults to `\n`.
|
||||
const baudRate =
|
||||
baudRates.find((item) => item.value === this.monitorModel.baudRate) ||
|
||||
baudRates.find((item) => item.value === this.serialModel.baudRate) ||
|
||||
baudRates[4]; // Defaults to `9600`.
|
||||
return (
|
||||
<div className="serial-monitor">
|
||||
<div className="head">
|
||||
<div className="send">
|
||||
<SerialMonitorSendInput
|
||||
monitorConfig={this.monitorConnection.monitorConfig}
|
||||
serialConfig={this.serialConnection.serialConfig}
|
||||
resolveFocus={this.onFocusResolved}
|
||||
onSend={this.onSend}
|
||||
/>
|
||||
@ -182,7 +181,7 @@ export class MonitorWidget extends ReactWidget {
|
||||
<ArduinoSelect
|
||||
maxMenuHeight={this.widgetHeight - 40}
|
||||
options={lineEndings}
|
||||
defaultValue={lineEnding}
|
||||
value={lineEnding}
|
||||
onChange={this.onChangeLineEnding}
|
||||
/>
|
||||
</div>
|
||||
@ -191,7 +190,7 @@ export class MonitorWidget extends ReactWidget {
|
||||
className="select"
|
||||
maxMenuHeight={this.widgetHeight - 40}
|
||||
options={baudRates}
|
||||
defaultValue={baudRate}
|
||||
value={baudRate}
|
||||
onChange={this.onChangeBaudRate}
|
||||
/>
|
||||
</div>
|
||||
@ -199,8 +198,8 @@ export class MonitorWidget extends ReactWidget {
|
||||
</div>
|
||||
<div className="body">
|
||||
<SerialMonitorOutput
|
||||
monitorModel={this.monitorModel}
|
||||
monitorConnection={this.monitorConnection}
|
||||
serialModel={this.serialModel}
|
||||
serialConnection={this.serialConnection}
|
||||
clearConsoleEvent={this.clearOutputEmitter.event}
|
||||
height={Math.floor(this.widgetHeight - 50)}
|
||||
/>
|
||||
@ -211,18 +210,18 @@ export class MonitorWidget extends ReactWidget {
|
||||
|
||||
protected readonly onSend = (value: string) => this.doSend(value);
|
||||
protected async doSend(value: string): Promise<void> {
|
||||
this.monitorConnection.send(value);
|
||||
this.serialConnection.send(value);
|
||||
}
|
||||
|
||||
protected readonly onChangeLineEnding = (
|
||||
option: SerialMonitorOutput.SelectOption<MonitorModel.EOL>
|
||||
option: SerialMonitorOutput.SelectOption<SerialModel.EOL>
|
||||
) => {
|
||||
this.monitorModel.lineEnding = option.value;
|
||||
this.serialModel.lineEnding = option.value;
|
||||
};
|
||||
|
||||
protected readonly onChangeBaudRate = (
|
||||
option: SerialMonitorOutput.SelectOption<MonitorConfig.BaudRate>
|
||||
option: SerialMonitorOutput.SelectOption<SerialConfig.BaudRate>
|
||||
) => {
|
||||
this.monitorModel.baudRate = option.value;
|
||||
this.serialModel.baudRate = option.value;
|
||||
};
|
||||
}
|
@ -1,13 +1,13 @@
|
||||
import * as React from 'react';
|
||||
import { Key, KeyCode } from '@theia/core/lib/browser/keys';
|
||||
import { Board, Port } from '../../common/protocol/boards-service';
|
||||
import { MonitorConfig } from '../../common/protocol/monitor-service';
|
||||
import { Board, Port } from '../../../common/protocol/boards-service';
|
||||
import { SerialConfig } from '../../../common/protocol/serial-service';
|
||||
import { isOSX } from '@theia/core/lib/common/os';
|
||||
import { nls } from '@theia/core/lib/browser/nls';
|
||||
|
||||
export namespace SerialMonitorSendInput {
|
||||
export interface Props {
|
||||
readonly monitorConfig?: MonitorConfig;
|
||||
readonly serialConfig?: SerialConfig;
|
||||
readonly onSend: (text: string) => void;
|
||||
readonly resolveFocus: (element: HTMLElement | undefined) => void;
|
||||
}
|
||||
@ -33,7 +33,7 @@ export class SerialMonitorSendInput extends React.Component<
|
||||
<input
|
||||
ref={this.setRef}
|
||||
type="text"
|
||||
className={`theia-input ${this.props.monitorConfig ? '' : 'warning'}`}
|
||||
className={`theia-input ${this.props.serialConfig ? '' : 'warning'}`}
|
||||
placeholder={this.placeholder}
|
||||
value={this.state.text}
|
||||
onChange={this.onChange}
|
||||
@ -43,16 +43,16 @@ export class SerialMonitorSendInput extends React.Component<
|
||||
}
|
||||
|
||||
protected get placeholder(): string {
|
||||
const { monitorConfig } = this.props;
|
||||
if (!monitorConfig) {
|
||||
const { serialConfig } = this.props;
|
||||
if (!serialConfig) {
|
||||
return nls.localize(
|
||||
'arduino/monitor/notConnected',
|
||||
'arduino/serial/notConnected',
|
||||
'Not connected. Select a board and a port to connect automatically.'
|
||||
);
|
||||
}
|
||||
const { board, port } = monitorConfig;
|
||||
const { board, port } = serialConfig;
|
||||
return nls.localize(
|
||||
'arduino/monitor/message',
|
||||
'arduino/serial/message',
|
||||
"Message ({0} + Enter to send message to '{1}' on '{2}'",
|
||||
isOSX ? '⌘' : nls.localize('vscode/keybindingLabels/ctrlKey', 'Ctrl'),
|
||||
Board.toString(board, {
|
@ -2,8 +2,8 @@ import * as React from 'react';
|
||||
import { Event } from '@theia/core/lib/common/event';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { areEqual, FixedSizeList as List } from 'react-window';
|
||||
import { MonitorModel } from './monitor-model';
|
||||
import { MonitorConnection } from './monitor-connection';
|
||||
import { SerialModel } from '../serial-model';
|
||||
import { SerialConnectionManager } from '../serial-connection-manager';
|
||||
import dateFormat = require('dateformat');
|
||||
import { messagesToLines, truncateLines } from './monitor-utils';
|
||||
|
||||
@ -24,7 +24,7 @@ export class SerialMonitorOutput extends React.Component<
|
||||
this.listRef = React.createRef();
|
||||
this.state = {
|
||||
lines: [],
|
||||
timestamp: this.props.monitorModel.timestamp,
|
||||
timestamp: this.props.serialModel.timestamp,
|
||||
charCount: 0,
|
||||
};
|
||||
}
|
||||
@ -57,7 +57,7 @@ export class SerialMonitorOutput extends React.Component<
|
||||
componentDidMount(): void {
|
||||
this.scrollToBottom();
|
||||
this.toDisposeBeforeUnmount.pushAll([
|
||||
this.props.monitorConnection.onRead(({ messages }) => {
|
||||
this.props.serialConnection.onRead(({ messages }) => {
|
||||
const [newLines, totalCharCount] = messagesToLines(
|
||||
messages,
|
||||
this.state.lines,
|
||||
@ -74,9 +74,9 @@ export class SerialMonitorOutput extends React.Component<
|
||||
this.props.clearConsoleEvent(() =>
|
||||
this.setState({ lines: [], charCount: 0 })
|
||||
),
|
||||
this.props.monitorModel.onChange(({ property }) => {
|
||||
this.props.serialModel.onChange(({ property }) => {
|
||||
if (property === 'timestamp') {
|
||||
const { timestamp } = this.props.monitorModel;
|
||||
const { timestamp } = this.props.serialModel;
|
||||
this.setState({ timestamp });
|
||||
}
|
||||
if (property === 'autoscroll') {
|
||||
@ -92,7 +92,7 @@ export class SerialMonitorOutput extends React.Component<
|
||||
}
|
||||
|
||||
scrollToBottom = ((): void => {
|
||||
if (this.listRef.current && this.props.monitorModel.autoscroll) {
|
||||
if (this.listRef.current && this.props.serialModel.autoscroll) {
|
||||
this.listRef.current.scrollToItem(this.state.lines.length, 'end');
|
||||
}
|
||||
}).bind(this);
|
||||
@ -125,8 +125,8 @@ const Row = React.memo(_Row, areEqual);
|
||||
|
||||
export namespace SerialMonitorOutput {
|
||||
export interface Props {
|
||||
readonly monitorModel: MonitorModel;
|
||||
readonly monitorConnection: MonitorConnection;
|
||||
readonly serialModel: SerialModel;
|
||||
readonly serialConnection: SerialConnectionManager;
|
||||
readonly clearConsoleEvent: Event<void>;
|
||||
readonly height: number;
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
import { ThemeService } from '@theia/core/lib/browser/theming';
|
||||
import { injectable, inject } from 'inversify';
|
||||
import {
|
||||
Command,
|
||||
CommandRegistry,
|
||||
MaybePromise,
|
||||
MenuModelRegistry,
|
||||
} from '@theia/core';
|
||||
import { SerialModel } from '../serial-model';
|
||||
import { ArduinoMenus } from '../../menu/arduino-menus';
|
||||
import { Contribution } from '../../contributions/contribution';
|
||||
import { Endpoint, FrontendApplication } from '@theia/core/lib/browser';
|
||||
import { ipcRenderer } from '@theia/core/shared/electron';
|
||||
import { SerialConfig, Status } from '../../../common/protocol';
|
||||
import { Serial, SerialConnectionManager } from '../serial-connection-manager';
|
||||
import { SerialPlotter } from './protocol';
|
||||
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
|
||||
const queryString = require('query-string');
|
||||
|
||||
export namespace SerialPlotterContribution {
|
||||
export namespace Commands {
|
||||
export const OPEN: Command = {
|
||||
id: 'serial-plotter-open',
|
||||
label: 'Serial Plotter',
|
||||
category: 'Arduino',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class PlotterFrontendContribution extends Contribution {
|
||||
protected window: Window | null;
|
||||
protected url: string;
|
||||
protected wsPort: number;
|
||||
|
||||
@inject(SerialModel)
|
||||
protected readonly model: SerialModel;
|
||||
|
||||
@inject(ThemeService)
|
||||
protected readonly themeService: ThemeService;
|
||||
|
||||
@inject(SerialConnectionManager)
|
||||
protected readonly serialConnection: SerialConnectionManager;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
||||
onStart(app: FrontendApplication): MaybePromise<void> {
|
||||
this.url = new Endpoint({ path: '/plotter' }).getRestUrl().toString();
|
||||
|
||||
ipcRenderer.on('CLOSE_CHILD_WINDOW', async () => {
|
||||
if (!!this.window) {
|
||||
this.window = null;
|
||||
await this.serialConnection.closeSerial(Serial.Type.Plotter);
|
||||
}
|
||||
});
|
||||
|
||||
return super.onStart(app);
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(SerialPlotterContribution.Commands.OPEN, {
|
||||
execute: this.connect.bind(this),
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(menus: MenuModelRegistry): void {
|
||||
menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
|
||||
commandId: SerialPlotterContribution.Commands.OPEN.id,
|
||||
label: SerialPlotterContribution.Commands.OPEN.label,
|
||||
order: '7',
|
||||
});
|
||||
}
|
||||
|
||||
async connect(): Promise<void> {
|
||||
if (!!this.window) {
|
||||
this.window.focus();
|
||||
return;
|
||||
}
|
||||
const status = await this.serialConnection.openSerial(Serial.Type.Plotter);
|
||||
const wsPort = this.serialConnection.getWsPort();
|
||||
if (Status.isOK(status) && wsPort) {
|
||||
this.open(wsPort);
|
||||
} else {
|
||||
this.serialConnection.closeSerial(Serial.Type.Plotter);
|
||||
this.messageService.error(`Couldn't open serial plotter`);
|
||||
}
|
||||
}
|
||||
|
||||
protected open(wsPort: number): void {
|
||||
const initConfig: Partial<SerialPlotter.Config> = {
|
||||
baudrates: SerialConfig.BaudRates.map((b) => b),
|
||||
currentBaudrate: this.model.baudRate,
|
||||
currentLineEnding: this.model.lineEnding,
|
||||
darkTheme: this.themeService.getCurrentTheme().type === 'dark',
|
||||
wsPort,
|
||||
interpolate: this.model.interpolate,
|
||||
connected: this.serialConnection.connected,
|
||||
serialPort: this.boardsServiceProvider.boardsConfig.selectedPort?.address,
|
||||
};
|
||||
const urlWithParams = queryString.stringifyUrl(
|
||||
{
|
||||
url: this.url,
|
||||
query: initConfig,
|
||||
},
|
||||
{ arrayFormat: 'comma' }
|
||||
);
|
||||
this.window = window.open(urlWithParams, 'serialPlotter');
|
||||
}
|
||||
}
|
26
arduino-ide-extension/src/browser/serial/plotter/protocol.ts
Normal file
26
arduino-ide-extension/src/browser/serial/plotter/protocol.ts
Normal file
@ -0,0 +1,26 @@
|
||||
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;
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,479 @@
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { deepClone } from '@theia/core/lib/common/objects';
|
||||
import { Emitter, Event } from '@theia/core/lib/common/event';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import {
|
||||
SerialService,
|
||||
SerialConfig,
|
||||
SerialError,
|
||||
Status,
|
||||
SerialServiceClient,
|
||||
} from '../../common/protocol/serial-service';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import {
|
||||
Port,
|
||||
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 { nls } from '@theia/core/lib/browser/nls';
|
||||
import { CoreService } from '../../common/protocol';
|
||||
|
||||
@injectable()
|
||||
export class SerialConnectionManager {
|
||||
protected _state: Serial.State = [];
|
||||
protected _connected = false;
|
||||
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
|
||||
) {
|
||||
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(({ property }) => {
|
||||
if (property === 'baudRate' && this.connected) {
|
||||
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',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the config passing only the properties that has changed. If some has changed and the serial is open,
|
||||
* we try to reconnect
|
||||
*
|
||||
* @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.isSerialOpen() &&
|
||||
!(await this.core.isUploading())
|
||||
) {
|
||||
this.serialService.updateWsConfigParam({
|
||||
currentBaudrate: this.config.baudRate,
|
||||
serialPort: this.config.port?.address,
|
||||
});
|
||||
await this.disconnect();
|
||||
await this.connect();
|
||||
}
|
||||
}
|
||||
|
||||
getConfig(): Partial<SerialConfig> {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
getWsPort(): number | undefined {
|
||||
return this.wsPort;
|
||||
}
|
||||
|
||||
isWebSocketConnected(): boolean {
|
||||
return !!this.webSocket?.url;
|
||||
}
|
||||
|
||||
protected handleWebSocketChanged(wsPort: number): void {
|
||||
this.wsPort = wsPort;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the serial is open and the frontend is connected to the serial, we create the websocket here
|
||||
*/
|
||||
protected createWsConnection(): boolean {
|
||||
if (this.wsPort) {
|
||||
try {
|
||||
this.webSocket = new WebSocket(`ws://localhost:${this.wsPort}`);
|
||||
this.webSocket.onmessage = (res) => {
|
||||
const messages = JSON.parse(res.data);
|
||||
this.onReadEmitter.fire({ messages });
|
||||
};
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the types of connections needed by the client.
|
||||
*
|
||||
* @param newState The array containing the list of desired connections.
|
||||
* If the previuos state was empty and 'newState' is not, it tries to reconnect to the serial service
|
||||
* If the provios state was NOT empty and now it is, it disconnects to the serial service
|
||||
* @returns The status of the operation
|
||||
*/
|
||||
protected async setState(newState: Serial.State): Promise<Status> {
|
||||
const oldState = deepClone(this._state);
|
||||
let status = Status.OK;
|
||||
|
||||
if (this.isSerialOpen(oldState) && !this.isSerialOpen(newState)) {
|
||||
status = await this.disconnect();
|
||||
} else if (!this.isSerialOpen(oldState) && this.isSerialOpen(newState)) {
|
||||
if (await this.core.isUploading()) {
|
||||
this.messageService.error(`Cannot open serial port when uploading`);
|
||||
return Status.NOT_CONNECTED;
|
||||
}
|
||||
status = await this.connect();
|
||||
}
|
||||
this._state = newState;
|
||||
return status;
|
||||
}
|
||||
|
||||
protected get state(): Serial.State {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
isSerialOpen(state?: Serial.State): boolean {
|
||||
return (state ? state : this._state).length > 0;
|
||||
}
|
||||
|
||||
get serialConfig(): SerialConfig | undefined {
|
||||
return isSerialConfig(this.config)
|
||||
? (this.config as SerialConfig)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
get connected(): boolean {
|
||||
return this._connected;
|
||||
}
|
||||
|
||||
set connected(c: boolean) {
|
||||
this._connected = c;
|
||||
this.serialService.updateWsConfigParam({ connected: c });
|
||||
this.onConnectionChangedEmitter.fire(this._connected);
|
||||
}
|
||||
/**
|
||||
* Called when a client opens the serial from the GUI
|
||||
*
|
||||
* @param type could be either 'Monitor' or 'Plotter'. If it's 'Monitor' we also connect to the websocket and
|
||||
* listen to the message events
|
||||
* @returns the status of the operation
|
||||
*/
|
||||
async openSerial(type: Serial.Type): Promise<Status> {
|
||||
if (!isSerialConfig(this.config)) {
|
||||
this.messageService.error(
|
||||
`Please select a board and a port to open the serial connection.`
|
||||
);
|
||||
return Status.NOT_CONNECTED;
|
||||
}
|
||||
if (this.state.includes(type)) return Status.OK;
|
||||
const newState = deepClone(this.state);
|
||||
newState.push(type);
|
||||
const status = await this.setState(newState);
|
||||
if (Status.isOK(status) && type === Serial.Type.Monitor)
|
||||
this.createWsConnection();
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a client closes the serial from the GUI
|
||||
*
|
||||
* @param type could be either 'Monitor' or 'Plotter'. If it's 'Monitor' we close the websocket connection
|
||||
* @returns the status of the operation
|
||||
*/
|
||||
async closeSerial(type: Serial.Type): Promise<Status> {
|
||||
const index = this.state.indexOf(type);
|
||||
let status = Status.OK;
|
||||
if (index >= 0) {
|
||||
const newState = deepClone(this.state);
|
||||
newState.splice(index, 1);
|
||||
status = await this.setState(newState);
|
||||
if (
|
||||
Status.isOK(status) &&
|
||||
type === Serial.Type.Monitor &&
|
||||
this.webSocket
|
||||
) {
|
||||
this.webSocket.close();
|
||||
this.webSocket = undefined;
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles error on the SerialServiceClient and try to reconnect, eventually
|
||||
*/
|
||||
handleError(error: SerialError): void {
|
||||
if (!this.connected) 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.toString(port)
|
||||
),
|
||||
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.toString(port)
|
||||
),
|
||||
options
|
||||
);
|
||||
break;
|
||||
}
|
||||
case undefined: {
|
||||
this.messageService.error(
|
||||
nls.localize(
|
||||
'arduino/serial/unexpectedError',
|
||||
'Unexpected error. Reconnecting {0} on port {1}.',
|
||||
Board.toString(board),
|
||||
Port.toString(port)
|
||||
),
|
||||
options
|
||||
);
|
||||
console.error(JSON.stringify(error));
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.connected = false;
|
||||
|
||||
if (this.isSerialOpen()) {
|
||||
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.toString(port)
|
||||
)
|
||||
);
|
||||
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.toString(port),
|
||||
attempts.toString()
|
||||
)
|
||||
);
|
||||
this.reconnectTimeout = window.setTimeout(
|
||||
() => this.connect(),
|
||||
timeout
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async connect(): Promise<Status> {
|
||||
if (this.connected) return Status.ALREADY_CONNECTED;
|
||||
if (!isSerialConfig(this.config)) return Status.NOT_CONNECTED;
|
||||
|
||||
console.info(
|
||||
`>>> Creating serial connection for ${Board.toString(
|
||||
this.config.board
|
||||
)} on port ${Port.toString(this.config.port)}...`
|
||||
);
|
||||
const connectStatus = await this.serialService.connect(this.config);
|
||||
if (Status.isOK(connectStatus)) {
|
||||
this.connected = true;
|
||||
console.info(
|
||||
`<<< Serial connection created for ${Board.toString(this.config.board, {
|
||||
useFqbn: false,
|
||||
})} on port ${Port.toString(this.config.port)}.`
|
||||
);
|
||||
}
|
||||
|
||||
return Status.isOK(connectStatus);
|
||||
}
|
||||
|
||||
async disconnect(): Promise<Status> {
|
||||
if (!this.connected) {
|
||||
return Status.OK;
|
||||
}
|
||||
|
||||
console.log('>>> Disposing existing serial connection...');
|
||||
const status = await this.serialService.disconnect();
|
||||
if (Status.isOK(status)) {
|
||||
this.connected = false;
|
||||
console.log(
|
||||
`<<< Disposed serial connection. Was: ${Serial.Config.toString(
|
||||
this.config
|
||||
)}`
|
||||
);
|
||||
this.wsPort = undefined;
|
||||
} else {
|
||||
console.warn(
|
||||
`<<< Could not dispose serial connection. Activate connection: ${Serial.Config.toString(
|
||||
this.config
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (!this.connected) {
|
||||
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: string[] }> {
|
||||
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 enum Type {
|
||||
Monitor = 'Monitor',
|
||||
Plotter = 'Plotter',
|
||||
}
|
||||
|
||||
/**
|
||||
* The state represents which types of connections are needed by the client, and it should match whether the Serial Monitor
|
||||
* or the Serial Plotter are open or not in the GUI. It's an array cause it's possible to have both, none or only one of
|
||||
* them open
|
||||
*/
|
||||
export type State = Serial.Type[];
|
||||
|
||||
export namespace Config {
|
||||
export function toString(config: Partial<SerialConfig>): string {
|
||||
if (!isSerialConfig(config)) return '';
|
||||
const { board, port } = config;
|
||||
return `${Board.toString(board)} ${Port.toString(port)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isSerialConfig(config: Partial<SerialConfig>): config is SerialConfig {
|
||||
return !!config.board && !!config.baudRate && !!config.port;
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { Emitter, Event } from '@theia/core/lib/common/event';
|
||||
import { MonitorConfig } from '../../common/protocol/monitor-service';
|
||||
import { SerialConfig } from '../../common/protocol';
|
||||
import {
|
||||
FrontendApplicationContribution,
|
||||
LocalStorageService,
|
||||
@ -8,8 +8,8 @@ import {
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
|
||||
@injectable()
|
||||
export class MonitorModel implements FrontendApplicationContribution {
|
||||
protected static STORAGE_ID = 'arduino-monitor-model';
|
||||
export class SerialModel implements FrontendApplicationContribution {
|
||||
protected static STORAGE_ID = 'arduino-serial-model';
|
||||
|
||||
@inject(LocalStorageService)
|
||||
protected readonly localStorageService: LocalStorageService;
|
||||
@ -18,26 +18,28 @@ export class MonitorModel implements FrontendApplicationContribution {
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
|
||||
protected readonly onChangeEmitter: Emitter<
|
||||
MonitorModel.State.Change<keyof MonitorModel.State>
|
||||
SerialModel.State.Change<keyof SerialModel.State>
|
||||
>;
|
||||
protected _autoscroll: boolean;
|
||||
protected _timestamp: boolean;
|
||||
protected _baudRate: MonitorConfig.BaudRate;
|
||||
protected _lineEnding: MonitorModel.EOL;
|
||||
protected _baudRate: SerialConfig.BaudRate;
|
||||
protected _lineEnding: SerialModel.EOL;
|
||||
protected _interpolate: boolean;
|
||||
|
||||
constructor() {
|
||||
this._autoscroll = true;
|
||||
this._timestamp = false;
|
||||
this._baudRate = MonitorConfig.BaudRate.DEFAULT;
|
||||
this._lineEnding = MonitorModel.EOL.DEFAULT;
|
||||
this._baudRate = SerialConfig.BaudRate.DEFAULT;
|
||||
this._lineEnding = SerialModel.EOL.DEFAULT;
|
||||
this._interpolate = false;
|
||||
this.onChangeEmitter = new Emitter<
|
||||
MonitorModel.State.Change<keyof MonitorModel.State>
|
||||
SerialModel.State.Change<keyof SerialModel.State>
|
||||
>();
|
||||
}
|
||||
|
||||
onStart(): void {
|
||||
this.localStorageService
|
||||
.getData<MonitorModel.State>(MonitorModel.STORAGE_ID)
|
||||
.getData<SerialModel.State>(SerialModel.STORAGE_ID)
|
||||
.then((state) => {
|
||||
if (state) {
|
||||
this.restoreState(state);
|
||||
@ -45,7 +47,7 @@ export class MonitorModel implements FrontendApplicationContribution {
|
||||
});
|
||||
}
|
||||
|
||||
get onChange(): Event<MonitorModel.State.Change<keyof MonitorModel.State>> {
|
||||
get onChange(): Event<SerialModel.State.Change<keyof SerialModel.State>> {
|
||||
return this.onChangeEmitter.event;
|
||||
}
|
||||
|
||||
@ -78,11 +80,11 @@ export class MonitorModel implements FrontendApplicationContribution {
|
||||
);
|
||||
}
|
||||
|
||||
get baudRate(): MonitorConfig.BaudRate {
|
||||
get baudRate(): SerialConfig.BaudRate {
|
||||
return this._baudRate;
|
||||
}
|
||||
|
||||
set baudRate(baudRate: MonitorConfig.BaudRate) {
|
||||
set baudRate(baudRate: SerialConfig.BaudRate) {
|
||||
this._baudRate = baudRate;
|
||||
this.storeState().then(() =>
|
||||
this.onChangeEmitter.fire({
|
||||
@ -92,11 +94,11 @@ export class MonitorModel implements FrontendApplicationContribution {
|
||||
);
|
||||
}
|
||||
|
||||
get lineEnding(): MonitorModel.EOL {
|
||||
get lineEnding(): SerialModel.EOL {
|
||||
return this._lineEnding;
|
||||
}
|
||||
|
||||
set lineEnding(lineEnding: MonitorModel.EOL) {
|
||||
set lineEnding(lineEnding: SerialModel.EOL) {
|
||||
this._lineEnding = lineEnding;
|
||||
this.storeState().then(() =>
|
||||
this.onChangeEmitter.fire({
|
||||
@ -106,29 +108,46 @@ export class MonitorModel implements FrontendApplicationContribution {
|
||||
);
|
||||
}
|
||||
|
||||
protected restoreState(state: MonitorModel.State): void {
|
||||
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(MonitorModel.STORAGE_ID, {
|
||||
return this.localStorageService.setData(SerialModel.STORAGE_ID, {
|
||||
autoscroll: this._autoscroll,
|
||||
timestamp: this._timestamp,
|
||||
baudRate: this._baudRate,
|
||||
lineEnding: this._lineEnding,
|
||||
interpolate: this._interpolate,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export namespace MonitorModel {
|
||||
export namespace SerialModel {
|
||||
export interface State {
|
||||
autoscroll: boolean;
|
||||
timestamp: boolean;
|
||||
baudRate: MonitorConfig.BaudRate;
|
||||
baudRate: SerialConfig.BaudRate;
|
||||
lineEnding: EOL;
|
||||
interpolate: boolean;
|
||||
}
|
||||
export namespace State {
|
||||
export interface Change<K extends keyof State> {
|
@ -0,0 +1,48 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import {
|
||||
SerialServiceClient,
|
||||
SerialError,
|
||||
SerialConfig,
|
||||
} from '../../common/protocol/serial-service';
|
||||
import { SerialModel } from './serial-model';
|
||||
|
||||
@injectable()
|
||||
export class SerialServiceClientImpl implements SerialServiceClient {
|
||||
protected readonly onErrorEmitter = new Emitter<SerialError>();
|
||||
readonly onError = this.onErrorEmitter.event;
|
||||
|
||||
protected readonly onWebSocketChangedEmitter = new Emitter<number>();
|
||||
readonly onWebSocketChanged = this.onWebSocketChangedEmitter.event;
|
||||
|
||||
protected readonly onBaudRateChangedEmitter =
|
||||
new Emitter<SerialConfig.BaudRate>();
|
||||
readonly onBaudRateChanged = this.onBaudRateChangedEmitter.event;
|
||||
|
||||
protected readonly onLineEndingChangedEmitter =
|
||||
new Emitter<SerialModel.EOL>();
|
||||
readonly onLineEndingChanged = this.onLineEndingChangedEmitter.event;
|
||||
|
||||
protected readonly onInterpolateChangedEmitter = new Emitter<boolean>();
|
||||
readonly onInterpolateChanged = this.onInterpolateChangedEmitter.event;
|
||||
|
||||
notifyError(error: SerialError): void {
|
||||
this.onErrorEmitter.fire(error);
|
||||
}
|
||||
|
||||
notifyWebSocketChanged(message: number): void {
|
||||
this.onWebSocketChangedEmitter.fire(message);
|
||||
}
|
||||
|
||||
notifyBaudRateChanged(message: SerialConfig.BaudRate): void {
|
||||
this.onBaudRateChangedEmitter.fire(message);
|
||||
}
|
||||
|
||||
notifyLineEndingChanged(message: SerialModel.EOL): void {
|
||||
this.onLineEndingChangedEmitter.fire(message);
|
||||
}
|
||||
|
||||
notifyInterpolateChanged(message: boolean): void {
|
||||
this.onInterpolateChangedEmitter.fire(message);
|
||||
}
|
||||
}
|
@ -22,6 +22,7 @@ export interface CoreService {
|
||||
upload(options: CoreService.Upload.Options): Promise<void>;
|
||||
uploadUsingProgrammer(options: CoreService.Upload.Options): Promise<void>;
|
||||
burnBootloader(options: CoreService.Bootloader.Options): Promise<void>;
|
||||
isUploading(): Promise<boolean>;
|
||||
}
|
||||
|
||||
export namespace CoreService {
|
||||
|
@ -6,7 +6,7 @@ export * from './core-service';
|
||||
export * from './filesystem-ext';
|
||||
export * from './installable';
|
||||
export * from './library-service';
|
||||
export * from './monitor-service';
|
||||
export * from './serial-service';
|
||||
export * from './searchable';
|
||||
export * from './sketches-service';
|
||||
export * from './examples-service';
|
||||
|
@ -1,92 +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';
|
||||
|
||||
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 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 MonitorServicePath = '/services/serial-monitor';
|
||||
export const MonitorService = Symbol('MonitorService');
|
||||
export interface MonitorService extends JsonRpcServer<MonitorServiceClient> {
|
||||
connect(config: MonitorConfig): Promise<Status>;
|
||||
disconnect(): Promise<Status>;
|
||||
send(message: string): Promise<Status>;
|
||||
}
|
||||
|
||||
export interface MonitorConfig {
|
||||
readonly board: Board;
|
||||
readonly port: Port;
|
||||
/**
|
||||
* Defaults to [`SERIAL`](MonitorConfig#ConnectionType#SERIAL).
|
||||
*/
|
||||
readonly type?: MonitorConfig.ConnectionType;
|
||||
/**
|
||||
* Defaults to `9600`.
|
||||
*/
|
||||
readonly baudRate?: MonitorConfig.BaudRate;
|
||||
}
|
||||
export namespace MonitorConfig {
|
||||
export type BaudRate =
|
||||
| 300
|
||||
| 1200
|
||||
| 2400
|
||||
| 4800
|
||||
| 9600
|
||||
| 19200
|
||||
| 38400
|
||||
| 57600
|
||||
| 115200;
|
||||
export namespace BaudRate {
|
||||
export const DEFAULT: BaudRate = 9600;
|
||||
}
|
||||
|
||||
export enum ConnectionType {
|
||||
SERIAL = 0,
|
||||
}
|
||||
}
|
||||
|
||||
export const MonitorServiceClient = Symbol('MonitorServiceClient');
|
||||
export interface MonitorServiceClient {
|
||||
onError: Event<MonitorError>;
|
||||
onMessage: Event<string>;
|
||||
notifyError(event: MonitorError): void;
|
||||
notifyMessage(message: string): void;
|
||||
}
|
||||
|
||||
export interface MonitorError {
|
||||
readonly message: string;
|
||||
/**
|
||||
* If no `code` is available, clients must reestablish the serial-monitor connection.
|
||||
*/
|
||||
readonly code: number | undefined;
|
||||
readonly config: MonitorConfig;
|
||||
}
|
||||
export namespace MonitorError {
|
||||
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 monitor was opened on this port. For another electron-instance, Java IDE.
|
||||
*/
|
||||
export const DEVICE_BUSY = 3;
|
||||
}
|
||||
}
|
95
arduino-ide-extension/src/common/protocol/serial-service.ts
Normal file
95
arduino-ide-extension/src/common/protocol/serial-service.ts
Normal file
@ -0,0 +1,95 @@
|
||||
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 SerialServicePath = '/services/serial';
|
||||
export const SerialService = Symbol('SerialService');
|
||||
export interface SerialService extends JsonRpcServer<SerialServiceClient> {
|
||||
connect(config: SerialConfig): Promise<Status>;
|
||||
disconnect(): Promise<Status>;
|
||||
sendMessageToSerial(message: string): Promise<Status>;
|
||||
updateWsConfigParam(config: Partial<SerialPlotter.Config>): Promise<void>;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -18,6 +18,8 @@ import {
|
||||
} from '@theia/core/lib/electron-main/electron-main-application';
|
||||
import { SplashServiceImpl } from '../splash/splash-service-impl';
|
||||
|
||||
app.commandLine.appendSwitch('disable-http-cache');
|
||||
|
||||
@injectable()
|
||||
export class ElectronMainApplication extends TheiaElectronMainApplication {
|
||||
protected _windows: BrowserWindow[] = [];
|
||||
@ -88,6 +90,35 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
|
||||
this.splashService.onCloseRequested
|
||||
);
|
||||
}
|
||||
|
||||
electronWindow.webContents.on(
|
||||
'new-window',
|
||||
(event, url, frameName, disposition, options, additionalFeatures) => {
|
||||
if (frameName === 'serialPlotter') {
|
||||
event.preventDefault();
|
||||
Object.assign(options, {
|
||||
width: 800,
|
||||
minWidth: 620,
|
||||
height: 500,
|
||||
minHeight: 320,
|
||||
x: 100,
|
||||
y: 100,
|
||||
webPreferences: {
|
||||
devTools: true,
|
||||
nativeWindowOpen: true,
|
||||
openerId: electronWindow?.webContents.id,
|
||||
},
|
||||
});
|
||||
event.newGuest = new BrowserWindow(options);
|
||||
event.newGuest.setMenu(null);
|
||||
event.newGuest?.on('closed', (e: any) => {
|
||||
electronWindow?.webContents.send('CLOSE_CHILD_WINDOW');
|
||||
});
|
||||
event.newGuest?.loadURL(url);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this._windows.push(electronWindow);
|
||||
electronWindow.on('closed', () => {
|
||||
if (electronWindow) {
|
||||
|
@ -40,13 +40,16 @@ import {
|
||||
ArduinoDaemon,
|
||||
ArduinoDaemonPath,
|
||||
} from '../common/protocol/arduino-daemon';
|
||||
import { MonitorServiceImpl } from './monitor/monitor-service-impl';
|
||||
import {
|
||||
MonitorService,
|
||||
MonitorServicePath,
|
||||
MonitorServiceClient,
|
||||
} from '../common/protocol/monitor-service';
|
||||
import { MonitorClientProvider } from './monitor/monitor-client-provider';
|
||||
SerialServiceImpl,
|
||||
SerialServiceName,
|
||||
} from './serial/serial-service-impl';
|
||||
import {
|
||||
SerialService,
|
||||
SerialServicePath,
|
||||
SerialServiceClient,
|
||||
} from '../common/protocol/serial-service';
|
||||
import { MonitorClientProvider } from './serial/monitor-client-provider';
|
||||
import { ConfigServiceImpl } from './config-service-impl';
|
||||
import { EnvVariablesServer as TheiaEnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||
import { EnvVariablesServer } from './theia/env-variables/env-variables-server';
|
||||
@ -86,6 +89,9 @@ import {
|
||||
AuthenticationServicePath,
|
||||
} from '../common/protocol/authentication-service';
|
||||
import { ArduinoFirmwareUploaderImpl } from './arduino-firmware-uploader-impl';
|
||||
import { PlotterBackendContribution } from './plotter/plotter-backend-contribution';
|
||||
import WebSocketServiceImpl from './web-socket/web-socket-service-impl';
|
||||
import { WebSocketService } from './web-socket/web-socket-service';
|
||||
|
||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(BackendApplication).toSelf().inSingletonScope();
|
||||
@ -169,6 +175,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
})
|
||||
);
|
||||
|
||||
// Shared WebSocketService for the backend. This will manage all websocket conenctions
|
||||
bind(WebSocketService).to(WebSocketServiceImpl).inSingletonScope();
|
||||
|
||||
// Shared Arduino core client provider service for the backend.
|
||||
bind(CoreClientProvider).toSelf().inSingletonScope();
|
||||
|
||||
@ -198,11 +207,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(ConnectionContainerModule).toConstantValue(
|
||||
ConnectionContainerModule.create(({ bind, bindBackendService }) => {
|
||||
bind(MonitorClientProvider).toSelf().inSingletonScope();
|
||||
bind(MonitorServiceImpl).toSelf().inSingletonScope();
|
||||
bind(MonitorService).toService(MonitorServiceImpl);
|
||||
bindBackendService<MonitorService, MonitorServiceClient>(
|
||||
MonitorServicePath,
|
||||
MonitorService,
|
||||
bind(SerialServiceImpl).toSelf().inSingletonScope();
|
||||
bind(SerialService).toService(SerialServiceImpl);
|
||||
bindBackendService<SerialService, SerialServiceClient>(
|
||||
SerialServicePath,
|
||||
SerialService,
|
||||
(service, client) => {
|
||||
service.setClient(client);
|
||||
client.onDidCloseConnection(() => service.dispose());
|
||||
@ -299,14 +308,14 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
.inSingletonScope()
|
||||
.whenTargetNamed('config');
|
||||
|
||||
// Logger for the monitor service.
|
||||
// Logger for the serial service.
|
||||
bind(ILogger)
|
||||
.toDynamicValue((ctx) => {
|
||||
const parentLogger = ctx.container.get<ILogger>(ILogger);
|
||||
return parentLogger.child('monitor-service');
|
||||
return parentLogger.child(SerialServiceName);
|
||||
})
|
||||
.inSingletonScope()
|
||||
.whenTargetNamed('monitor-service');
|
||||
.whenTargetNamed(SerialServiceName);
|
||||
|
||||
bind(DefaultGitInit).toSelf();
|
||||
rebind(GitInit).toService(DefaultGitInit);
|
||||
@ -331,4 +340,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
)
|
||||
)
|
||||
.inSingletonScope();
|
||||
|
||||
bind(PlotterBackendContribution).toSelf().inSingletonScope();
|
||||
bind(BackendApplicationContribution).toService(PlotterBackendContribution);
|
||||
});
|
||||
|
@ -32,6 +32,8 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
||||
@inject(NotificationServiceServer)
|
||||
protected readonly notificationService: NotificationServiceServer;
|
||||
|
||||
protected uploading = false;
|
||||
|
||||
async compile(
|
||||
options: CoreService.Compile.Options & {
|
||||
exportBinaries?: boolean;
|
||||
@ -110,6 +112,10 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
||||
);
|
||||
}
|
||||
|
||||
isUploading(): Promise<boolean> {
|
||||
return Promise.resolve(this.uploading);
|
||||
}
|
||||
|
||||
protected async doUpload(
|
||||
options: CoreService.Upload.Options,
|
||||
requestProvider: () => UploadRequest | UploadUsingProgrammerRequest,
|
||||
@ -120,6 +126,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
||||
) => ClientReadableStream<UploadResponse | UploadUsingProgrammerResponse>,
|
||||
task = 'upload'
|
||||
): Promise<void> {
|
||||
this.uploading = true;
|
||||
await this.compile(Object.assign(options, { exportBinaries: false }));
|
||||
const { sketchUri, fqbn, port, programmer } = options;
|
||||
const sketchPath = FileUri.fsPath(sketchUri);
|
||||
@ -173,6 +180,8 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
|
||||
severity: 'error',
|
||||
});
|
||||
throw e;
|
||||
} finally {
|
||||
this.uploading = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,29 @@
|
||||
import * as express from 'express';
|
||||
import { injectable } from 'inversify';
|
||||
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
|
||||
import path = require('path');
|
||||
|
||||
@injectable()
|
||||
export class PlotterBackendContribution
|
||||
implements BackendApplicationContribution
|
||||
{
|
||||
async initialize(): Promise<void> {}
|
||||
|
||||
configure(app: express.Application): void {
|
||||
const relativePath = [
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'build',
|
||||
'arduino-serial-plotter-webapp',
|
||||
'build',
|
||||
];
|
||||
app.use(express.static(path.join(__dirname, ...relativePath)));
|
||||
app.get('/plotter', (req, res) => {
|
||||
console.log(
|
||||
`Serving serial plotter on http://${req.headers.host}${req.url}`
|
||||
);
|
||||
res.sendFile(path.join(__dirname, ...relativePath, 'index.html'));
|
||||
});
|
||||
}
|
||||
}
|
@ -2,15 +2,14 @@ import { ClientDuplexStream } from '@grpc/grpc-js';
|
||||
import { TextEncoder } from 'util';
|
||||
import { injectable, inject, named } from 'inversify';
|
||||
import { Struct } from 'google-protobuf/google/protobuf/struct_pb';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import {
|
||||
MonitorService,
|
||||
MonitorServiceClient,
|
||||
MonitorConfig,
|
||||
MonitorError,
|
||||
SerialService,
|
||||
SerialServiceClient,
|
||||
SerialConfig,
|
||||
SerialError,
|
||||
Status,
|
||||
} from '../../common/protocol/monitor-service';
|
||||
} from '../../common/protocol/serial-service';
|
||||
import {
|
||||
StreamingOpenRequest,
|
||||
StreamingOpenResponse,
|
||||
@ -18,16 +17,20 @@ import {
|
||||
} from '../cli-protocol/cc/arduino/cli/monitor/v1/monitor_pb';
|
||||
import { MonitorClientProvider } from './monitor-client-provider';
|
||||
import { Board, Port } from '../../common/protocol/boards-service';
|
||||
import * as WebSocket from 'ws';
|
||||
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 toMonitorError(
|
||||
export function toSerialError(
|
||||
error: Error,
|
||||
config: MonitorConfig
|
||||
): MonitorError {
|
||||
config: SerialConfig
|
||||
): SerialError {
|
||||
const { message } = error;
|
||||
let code = undefined;
|
||||
if (is(error)) {
|
||||
@ -35,15 +38,15 @@ namespace ErrorWithCode {
|
||||
const mapping = new Map<string, number>();
|
||||
mapping.set(
|
||||
'1 CANCELLED: Cancelled on client',
|
||||
MonitorError.ErrorCodes.CLIENT_CANCEL
|
||||
SerialError.ErrorCodes.CLIENT_CANCEL
|
||||
);
|
||||
mapping.set(
|
||||
'2 UNKNOWN: device not configured',
|
||||
MonitorError.ErrorCodes.DEVICE_NOT_CONFIGURED
|
||||
SerialError.ErrorCodes.DEVICE_NOT_CONFIGURED
|
||||
);
|
||||
mapping.set(
|
||||
'2 UNKNOWN: error opening serial monitor: Serial port busy',
|
||||
MonitorError.ErrorCodes.DEVICE_BUSY
|
||||
'2 UNKNOWN: error opening serial connection: Serial port busy',
|
||||
SerialError.ErrorCodes.DEVICE_BUSY
|
||||
);
|
||||
code = mapping.get(message);
|
||||
}
|
||||
@ -59,45 +62,59 @@ namespace ErrorWithCode {
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class MonitorServiceImpl implements MonitorService {
|
||||
export class SerialServiceImpl implements SerialService {
|
||||
@named(SerialServiceName)
|
||||
@inject(ILogger)
|
||||
@named('monitor-service')
|
||||
protected readonly logger: ILogger;
|
||||
|
||||
@inject(MonitorClientProvider)
|
||||
protected readonly monitorClientProvider: MonitorClientProvider;
|
||||
protected readonly serialClientProvider: MonitorClientProvider;
|
||||
|
||||
protected client?: MonitorServiceClient;
|
||||
protected connection?: {
|
||||
@inject(WebSocketService)
|
||||
protected readonly webSocketService: WebSocketService;
|
||||
|
||||
protected client?: SerialServiceClient;
|
||||
protected serialConnection?: {
|
||||
duplex: ClientDuplexStream<StreamingOpenRequest, StreamingOpenResponse>;
|
||||
config: MonitorConfig;
|
||||
config: SerialConfig;
|
||||
};
|
||||
protected messages: string[] = [];
|
||||
protected onMessageDidReadEmitter = new Emitter<void>();
|
||||
protected onMessageReceived: Disposable | null;
|
||||
protected flushMessagesInterval: NodeJS.Timeout | null;
|
||||
|
||||
setClient(client: MonitorServiceClient | undefined): void {
|
||||
setClient(client: SerialServiceClient | undefined): void {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.logger.info('>>> Disposing monitor service...');
|
||||
if (this.connection) {
|
||||
this.logger.info('>>> Disposing serial service...');
|
||||
if (this.serialConnection) {
|
||||
this.disconnect();
|
||||
}
|
||||
this.logger.info('<<< Disposed monitor service.');
|
||||
this.logger.info('<<< Disposed serial service.');
|
||||
this.client = undefined;
|
||||
}
|
||||
|
||||
async connect(config: MonitorConfig): Promise<Status> {
|
||||
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));
|
||||
}
|
||||
|
||||
async connect(config: SerialConfig): Promise<Status> {
|
||||
this.logger.info(
|
||||
`>>> Creating serial monitor connection for ${Board.toString(
|
||||
`>>> Creating serial connection for ${Board.toString(
|
||||
config.board
|
||||
)} on port ${Port.toString(config.port)}...`
|
||||
);
|
||||
if (this.connection) {
|
||||
if (this.serialConnection) {
|
||||
return Status.ALREADY_CONNECTED;
|
||||
}
|
||||
const client = await this.monitorClientProvider.client();
|
||||
const client = await this.serialClientProvider.client();
|
||||
if (!client) {
|
||||
return Status.NOT_CONNECTED;
|
||||
}
|
||||
@ -105,17 +122,17 @@ export class MonitorServiceImpl implements MonitorService {
|
||||
return { message: client.message };
|
||||
}
|
||||
const duplex = client.streamingOpen();
|
||||
this.connection = { duplex, config };
|
||||
this.serialConnection = { duplex, config };
|
||||
|
||||
duplex.on(
|
||||
'error',
|
||||
((error: Error) => {
|
||||
const monitorError = ErrorWithCode.toMonitorError(error, config);
|
||||
this.disconnect(monitorError).then(() => {
|
||||
const serialError = ErrorWithCode.toSerialError(error, config);
|
||||
this.disconnect(serialError).then(() => {
|
||||
if (this.client) {
|
||||
this.client.notifyError(monitorError);
|
||||
this.client.notifyError(serialError);
|
||||
}
|
||||
if (monitorError.code === undefined) {
|
||||
if (serialError.code === undefined) {
|
||||
// Log the original, unexpected error.
|
||||
this.logger.error(error);
|
||||
}
|
||||
@ -123,23 +140,50 @@ export class MonitorServiceImpl implements MonitorService {
|
||||
}).bind(this)
|
||||
);
|
||||
|
||||
const ws = new WebSocket.Server({ port: 0 });
|
||||
const address: any = ws.address();
|
||||
this.client?.notifyMessage(address.port);
|
||||
let wsConn: WebSocket | null = null;
|
||||
ws.on('connection', (ws) => {
|
||||
wsConn = ws;
|
||||
});
|
||||
this.client?.notifyWebSocketChanged(
|
||||
this.webSocketService.getAddress().port
|
||||
);
|
||||
|
||||
const flushMessagesToFrontend = () => {
|
||||
if (this.messages.length) {
|
||||
wsConn?.send(JSON.stringify(this.messages));
|
||||
this.webSocketService.sendMessage(JSON.stringify(this.messages));
|
||||
this.messages = [];
|
||||
}
|
||||
};
|
||||
|
||||
// empty the queue every 16ms (~60fps)
|
||||
setInterval(flushMessagesToFrontend, 32);
|
||||
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.client?.notifyBaudRateChanged(
|
||||
parseInt(message.data, 10) as SerialConfig.BaudRate
|
||||
);
|
||||
break;
|
||||
|
||||
case SerialPlotter.Protocol.Command.PLOTTER_SET_LINE_ENDING:
|
||||
this.client?.notifyLineEndingChanged(message.data);
|
||||
break;
|
||||
|
||||
case SerialPlotter.Protocol.Command.PLOTTER_SET_INTERPOLATE:
|
||||
this.client?.notifyInterpolateChanged(message.data);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
);
|
||||
|
||||
// empty the queue every 32ms (~30fps)
|
||||
this.flushMessagesInterval = setInterval(flushMessagesToFrontend, 32);
|
||||
|
||||
// converts 'ab\nc\nd' => [ab\n,c\n,d]
|
||||
const stringToArray = (string: string, separator = '\n') => {
|
||||
@ -188,13 +232,12 @@ export class MonitorServiceImpl implements MonitorService {
|
||||
req.setConfig(monitorConfig);
|
||||
|
||||
return new Promise<Status>((resolve) => {
|
||||
if (this.connection) {
|
||||
this.connection.duplex.write(req, () => {
|
||||
if (this.serialConnection) {
|
||||
this.serialConnection.duplex.write(req, () => {
|
||||
this.logger.info(
|
||||
`<<< Serial monitor connection created for ${Board.toString(
|
||||
config.board,
|
||||
{ useFqbn: false }
|
||||
)} on port ${Port.toString(config.port)}.`
|
||||
`<<< Serial connection created for ${Board.toString(config.board, {
|
||||
useFqbn: false,
|
||||
})} on port ${Port.toString(config.port)}.`
|
||||
);
|
||||
resolve(Status.OK);
|
||||
});
|
||||
@ -204,43 +247,52 @@ export class MonitorServiceImpl implements MonitorService {
|
||||
});
|
||||
}
|
||||
|
||||
async disconnect(reason?: MonitorError): Promise<Status> {
|
||||
async disconnect(reason?: SerialError): Promise<Status> {
|
||||
try {
|
||||
if (this.onMessageReceived) {
|
||||
this.onMessageReceived.dispose();
|
||||
this.onMessageReceived = null;
|
||||
}
|
||||
if (this.flushMessagesInterval) {
|
||||
clearInterval(this.flushMessagesInterval);
|
||||
this.flushMessagesInterval = null;
|
||||
}
|
||||
|
||||
if (
|
||||
!this.connection &&
|
||||
!this.serialConnection &&
|
||||
reason &&
|
||||
reason.code === MonitorError.ErrorCodes.CLIENT_CANCEL
|
||||
reason.code === SerialError.ErrorCodes.CLIENT_CANCEL
|
||||
) {
|
||||
return Status.OK;
|
||||
}
|
||||
this.logger.info('>>> Disposing monitor connection...');
|
||||
if (!this.connection) {
|
||||
this.logger.info('>>> Disposing serial connection...');
|
||||
if (!this.serialConnection) {
|
||||
this.logger.warn('<<< Not connected. Nothing to dispose.');
|
||||
return Status.NOT_CONNECTED;
|
||||
}
|
||||
const { duplex, config } = this.connection;
|
||||
const { duplex, config } = this.serialConnection;
|
||||
duplex.cancel();
|
||||
this.logger.info(
|
||||
`<<< Disposed monitor connection for ${Board.toString(config.board, {
|
||||
`<<< Disposed serial connection for ${Board.toString(config.board, {
|
||||
useFqbn: false,
|
||||
})} on port ${Port.toString(config.port)}.`
|
||||
);
|
||||
this.connection = undefined;
|
||||
this.serialConnection = undefined;
|
||||
return Status.OK;
|
||||
} finally {
|
||||
this.messages.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
async send(message: string): Promise<Status> {
|
||||
if (!this.connection) {
|
||||
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.connection) {
|
||||
this.connection.duplex.write(req, () => {
|
||||
if (this.serialConnection) {
|
||||
this.serialConnection.duplex.write(req, () => {
|
||||
resolve(Status.OK);
|
||||
});
|
||||
return;
|
||||
@ -250,10 +302,10 @@ export class MonitorServiceImpl implements MonitorService {
|
||||
}
|
||||
|
||||
protected mapType(
|
||||
type?: MonitorConfig.ConnectionType
|
||||
type?: SerialConfig.ConnectionType
|
||||
): GrpcMonitorConfig.TargetType {
|
||||
switch (type) {
|
||||
case MonitorConfig.ConnectionType.SERIAL:
|
||||
case SerialConfig.ConnectionType.SERIAL:
|
||||
return GrpcMonitorConfig.TargetType.TARGET_TYPE_SERIAL;
|
||||
default:
|
||||
return GrpcMonitorConfig.TargetType.TARGET_TYPE_SERIAL;
|
@ -0,0 +1,46 @@
|
||||
import { Emitter } from '@theia/core';
|
||||
import { injectable } from 'inversify';
|
||||
import * as WebSocket from 'ws';
|
||||
import { WebSocketService } from './web-socket-service';
|
||||
|
||||
@injectable()
|
||||
export default class WebSocketServiceImpl implements WebSocketService {
|
||||
protected wsClients: WebSocket[];
|
||||
protected server: WebSocket.Server;
|
||||
|
||||
protected readonly onMessage = new Emitter<string>();
|
||||
public readonly onMessageReceived = this.onMessage.event;
|
||||
|
||||
constructor() {
|
||||
this.wsClients = [];
|
||||
this.server = new WebSocket.Server({ port: 0 });
|
||||
|
||||
const addClient = this.addClient.bind(this);
|
||||
this.server.on('connection', addClient);
|
||||
}
|
||||
|
||||
private addClient(ws: WebSocket): void {
|
||||
this.wsClients.push(ws);
|
||||
ws.onclose = () => {
|
||||
this.wsClients.splice(this.wsClients.indexOf(ws), 1);
|
||||
};
|
||||
|
||||
ws.onmessage = (res) => {
|
||||
this.onMessage.fire(res.data.toString());
|
||||
};
|
||||
}
|
||||
|
||||
getAddress(): WebSocket.AddressInfo {
|
||||
return this.server.address() as WebSocket.AddressInfo;
|
||||
}
|
||||
|
||||
sendMessage(message: string): void {
|
||||
this.wsClients.forEach((w) => {
|
||||
try {
|
||||
w.send(message);
|
||||
} catch {
|
||||
w.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import { Event } from '@theia/core/lib/common/event';
|
||||
import * as WebSocket from 'ws';
|
||||
|
||||
export const WebSocketService = Symbol('WebSocketService');
|
||||
export interface WebSocketService {
|
||||
getAddress(): WebSocket.AddressInfo;
|
||||
sendMessage(message: string): void;
|
||||
onMessageReceived: Event<string>;
|
||||
}
|
@ -11,52 +11,19 @@ import { MessageService } from '@theia/core';
|
||||
import { BoardsServiceProvider } from '../../browser/boards/boards-service-provider';
|
||||
import { BoardsListWidgetFrontendContribution } from '../../browser/boards/boards-widget-frontend-contribution';
|
||||
import {
|
||||
Board,
|
||||
BoardsPackage,
|
||||
BoardsService,
|
||||
Port,
|
||||
ResponseServiceArduino,
|
||||
} from '../../common/protocol';
|
||||
import { IMock, It, Mock, Times } from 'typemoq';
|
||||
import { Container, ContainerModule } from 'inversify';
|
||||
import { BoardsAutoInstaller } from '../../browser/boards/boards-auto-installer';
|
||||
import { BoardsConfig } from '../../browser/boards/boards-config';
|
||||
import { tick } from '../utils';
|
||||
import { ListWidget } from '../../browser/widgets/component-list/list-widget';
|
||||
import { aBoardConfig, anInstalledPackage, aPackage } from './fixtures/boards';
|
||||
|
||||
disableJSDOM();
|
||||
|
||||
const aBoard: Board = {
|
||||
fqbn: 'some:board:fqbn',
|
||||
name: 'Some Arduino Board',
|
||||
port: { address: '/lol/port1234', protocol: 'serial' },
|
||||
};
|
||||
const aPort: Port = {
|
||||
address: aBoard.port!.address,
|
||||
protocol: aBoard.port!.protocol,
|
||||
};
|
||||
const aBoardConfig: BoardsConfig.Config = {
|
||||
selectedBoard: aBoard,
|
||||
selectedPort: aPort,
|
||||
};
|
||||
const aPackage: BoardsPackage = {
|
||||
author: 'someAuthor',
|
||||
availableVersions: ['some.ver.sion', 'some.other.version'],
|
||||
boards: [aBoard],
|
||||
deprecated: false,
|
||||
description: 'Some Arduino Board, Some Other Arduino Board',
|
||||
id: 'some:arduinoCoreId',
|
||||
installable: true,
|
||||
moreInfoLink: 'http://www.some-url.lol/',
|
||||
name: 'Some Arduino Package',
|
||||
summary: 'Boards included in this package:',
|
||||
};
|
||||
|
||||
const anInstalledPackage: BoardsPackage = {
|
||||
...aPackage,
|
||||
installedVersion: 'some.ver.sion',
|
||||
};
|
||||
|
||||
describe('BoardsAutoInstaller', () => {
|
||||
let subject: BoardsAutoInstaller;
|
||||
let messageService: IMock<MessageService>;
|
||||
|
47
arduino-ide-extension/src/test/browser/fixtures/boards.ts
Normal file
47
arduino-ide-extension/src/test/browser/fixtures/boards.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { BoardsConfig } from '../../../browser/boards/boards-config';
|
||||
import { Board, BoardsPackage, Port } from '../../../common/protocol';
|
||||
|
||||
export const aBoard: Board = {
|
||||
fqbn: 'some:board:fqbn',
|
||||
name: 'Some Arduino Board',
|
||||
port: { address: '/lol/port1234', protocol: 'serial' },
|
||||
};
|
||||
export const aPort: Port = {
|
||||
address: aBoard.port!.address,
|
||||
protocol: aBoard.port!.protocol,
|
||||
};
|
||||
export const aBoardConfig: BoardsConfig.Config = {
|
||||
selectedBoard: aBoard,
|
||||
selectedPort: aPort,
|
||||
};
|
||||
export const anotherBoard: Board = {
|
||||
fqbn: 'another:board:fqbn',
|
||||
name: 'Another Arduino Board',
|
||||
port: { address: '/kek/port5678', protocol: 'serial' },
|
||||
};
|
||||
export const anotherPort: Port = {
|
||||
address: anotherBoard.port!.address,
|
||||
protocol: anotherBoard.port!.protocol,
|
||||
};
|
||||
export const anotherBoardConfig: BoardsConfig.Config = {
|
||||
selectedBoard: anotherBoard,
|
||||
selectedPort: anotherPort,
|
||||
};
|
||||
|
||||
export const aPackage: BoardsPackage = {
|
||||
author: 'someAuthor',
|
||||
availableVersions: ['some.ver.sion', 'some.other.version'],
|
||||
boards: [aBoard],
|
||||
deprecated: false,
|
||||
description: 'Some Arduino Board, Some Other Arduino Board',
|
||||
id: 'some:arduinoCoreId',
|
||||
installable: true,
|
||||
moreInfoLink: 'http://www.some-url.lol/',
|
||||
name: 'Some Arduino Package',
|
||||
summary: 'Boards included in this package:',
|
||||
};
|
||||
|
||||
export const anInstalledPackage: BoardsPackage = {
|
||||
...aPackage,
|
||||
installedVersion: 'some.ver.sion',
|
||||
};
|
22
arduino-ide-extension/src/test/browser/fixtures/serial.ts
Normal file
22
arduino-ide-extension/src/test/browser/fixtures/serial.ts
Normal file
@ -0,0 +1,22 @@
|
||||
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() {}
|
||||
}
|
@ -2,8 +2,8 @@ import { expect } from 'chai';
|
||||
import {
|
||||
messagesToLines,
|
||||
truncateLines,
|
||||
} from '../../browser/monitor/monitor-utils';
|
||||
import { Line } from '../../browser/monitor/serial-monitor-send-output';
|
||||
} from '../../browser/serial/monitor/monitor-utils';
|
||||
import { Line } from '../../browser/serial/monitor/serial-monitor-send-output';
|
||||
import { set, reset } from 'mockdate';
|
||||
|
||||
type TestLine = {
|
||||
|
@ -0,0 +1,375 @@
|
||||
import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
|
||||
const disableJSDOM = enableJSDOM();
|
||||
|
||||
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
||||
import { ApplicationProps } from '@theia/application-package/lib/application-props';
|
||||
FrontendApplicationConfigProvider.set({
|
||||
...ApplicationProps.DEFAULT.frontend.config,
|
||||
});
|
||||
|
||||
import { MessageService } from '@theia/core';
|
||||
import { BoardsServiceProvider } from '../../browser/boards/boards-service-provider';
|
||||
import {
|
||||
BoardsService,
|
||||
CoreService,
|
||||
SerialService,
|
||||
SerialServiceClient,
|
||||
Status,
|
||||
} from '../../common/protocol';
|
||||
import { IMock, It, Mock, Times } from 'typemoq';
|
||||
import {
|
||||
Serial,
|
||||
SerialConnectionManager,
|
||||
} from '../../browser/serial/serial-connection-manager';
|
||||
import { ThemeService } from '@theia/core/lib/browser/theming';
|
||||
import { SerialModel } from '../../browser/serial/serial-model';
|
||||
import {
|
||||
aBoardConfig,
|
||||
anotherBoardConfig,
|
||||
anotherPort,
|
||||
aPort,
|
||||
} from './fixtures/boards';
|
||||
import { BoardsConfig } from '../../browser/boards/boards-config';
|
||||
import {
|
||||
anotherSerialConfig,
|
||||
aSerialConfig,
|
||||
WebSocketMock,
|
||||
} from './fixtures/serial';
|
||||
import { expect } from 'chai';
|
||||
import { tick } from '../utils';
|
||||
|
||||
disableJSDOM();
|
||||
|
||||
global.WebSocket = WebSocketMock as any;
|
||||
|
||||
describe.only('SerialConnectionManager', () => {
|
||||
let subject: SerialConnectionManager;
|
||||
|
||||
let serialModel: IMock<SerialModel>;
|
||||
let serialService: IMock<SerialService>;
|
||||
let serialServiceClient: IMock<SerialServiceClient>;
|
||||
let boardsService: IMock<BoardsService>;
|
||||
let boardsServiceProvider: IMock<BoardsServiceProvider>;
|
||||
let messageService: IMock<MessageService>;
|
||||
let themeService: IMock<ThemeService>;
|
||||
let core: IMock<CoreService>;
|
||||
|
||||
let handleBoardConfigChange: (
|
||||
boardsConfig: BoardsConfig.Config
|
||||
) => Promise<void>;
|
||||
let handleWebSocketChanged: (wsPort: number) => void;
|
||||
const wsPort = 1234;
|
||||
|
||||
beforeEach(() => {
|
||||
serialModel = Mock.ofType<SerialModel>();
|
||||
serialService = Mock.ofType<SerialService>();
|
||||
serialServiceClient = Mock.ofType<SerialServiceClient>();
|
||||
boardsService = Mock.ofType<BoardsService>();
|
||||
boardsServiceProvider = Mock.ofType<BoardsServiceProvider>();
|
||||
messageService = Mock.ofType<MessageService>();
|
||||
themeService = Mock.ofType<ThemeService>();
|
||||
core = Mock.ofType<CoreService>();
|
||||
|
||||
boardsServiceProvider
|
||||
.setup((b) => b.boardsConfig)
|
||||
.returns(() => aBoardConfig);
|
||||
|
||||
boardsServiceProvider
|
||||
.setup((b) => b.onBoardsConfigChanged(It.isAny()))
|
||||
.returns((h) => {
|
||||
handleBoardConfigChange = h;
|
||||
return { dispose: () => {} };
|
||||
});
|
||||
|
||||
boardsServiceProvider
|
||||
.setup((b) => b.canUploadTo(It.isAny(), It.isValue({ silent: false })))
|
||||
.returns(() => true);
|
||||
|
||||
boardsService
|
||||
.setup((b) => b.getAvailablePorts())
|
||||
.returns(() => Promise.resolve([aPort, anotherPort]));
|
||||
|
||||
serialModel
|
||||
.setup((m) => m.baudRate)
|
||||
.returns(() => aSerialConfig.baudRate || 9600);
|
||||
|
||||
serialServiceClient
|
||||
.setup((m) => m.onWebSocketChanged(It.isAny()))
|
||||
.returns((h) => {
|
||||
handleWebSocketChanged = h;
|
||||
return { dispose: () => {} };
|
||||
});
|
||||
|
||||
serialService
|
||||
.setup((m) => m.disconnect())
|
||||
.returns(() => Promise.resolve(Status.OK));
|
||||
|
||||
core.setup((u) => u.isUploading()).returns(() => Promise.resolve(false));
|
||||
|
||||
subject = new SerialConnectionManager(
|
||||
serialModel.object,
|
||||
serialService.object,
|
||||
serialServiceClient.object,
|
||||
boardsService.object,
|
||||
boardsServiceProvider.object,
|
||||
messageService.object,
|
||||
themeService.object,
|
||||
core.object
|
||||
);
|
||||
});
|
||||
|
||||
context('when no serial config is set', () => {
|
||||
context('and the serial is NOT open', () => {
|
||||
context('and it tries to open the serial plotter', () => {
|
||||
it('should not try to connect and show an error', async () => {
|
||||
await subject.openSerial(Serial.Type.Plotter);
|
||||
messageService.verify((m) => m.error(It.isAnyString()), Times.once());
|
||||
serialService.verify((m) => m.disconnect(), Times.never());
|
||||
serialService.verify((m) => m.connect(It.isAny()), Times.never());
|
||||
});
|
||||
});
|
||||
context('and a serial config is set', () => {
|
||||
it('should not try to reconnect', async () => {
|
||||
await handleBoardConfigChange(aBoardConfig);
|
||||
serialService.verify((m) => m.disconnect(), Times.never());
|
||||
serialService.verify((m) => m.connect(It.isAny()), Times.never());
|
||||
expect(subject.getConfig()).to.deep.equal(aSerialConfig);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
context('when a serial config is set', () => {
|
||||
beforeEach(() => {
|
||||
subject.setConfig(aSerialConfig);
|
||||
});
|
||||
context('and the serial is NOT open', () => {
|
||||
context('and it tries to disconnect', () => {
|
||||
it('should do nothing', async () => {
|
||||
const status = await subject.disconnect();
|
||||
expect(status).to.be.ok;
|
||||
expect(subject.connected).to.be.false;
|
||||
});
|
||||
});
|
||||
context('and the config changes', () => {
|
||||
beforeEach(() => {
|
||||
subject.setConfig(anotherSerialConfig);
|
||||
});
|
||||
it('should not try to reconnect', async () => {
|
||||
await tick();
|
||||
messageService.verify(
|
||||
(m) => m.error(It.isAnyString()),
|
||||
Times.never()
|
||||
);
|
||||
serialService.verify((m) => m.disconnect(), Times.never());
|
||||
serialService.verify((m) => m.connect(It.isAny()), Times.never());
|
||||
});
|
||||
});
|
||||
context(
|
||||
'and the connection to the serial succeeds with the config',
|
||||
() => {
|
||||
beforeEach(() => {
|
||||
serialService
|
||||
.setup((m) => m.connect(It.isValue(aSerialConfig)))
|
||||
.returns(() => {
|
||||
handleWebSocketChanged(wsPort);
|
||||
return Promise.resolve(Status.OK);
|
||||
});
|
||||
});
|
||||
context('and it tries to open the serial plotter', () => {
|
||||
let status: Status;
|
||||
beforeEach(async () => {
|
||||
status = await subject.openSerial(Serial.Type.Plotter);
|
||||
});
|
||||
it('should successfully connect to the serial', async () => {
|
||||
messageService.verify(
|
||||
(m) => m.error(It.isAnyString()),
|
||||
Times.never()
|
||||
);
|
||||
serialService.verify((m) => m.disconnect(), Times.never());
|
||||
serialService.verify((m) => m.connect(It.isAny()), Times.once());
|
||||
expect(status).to.be.ok;
|
||||
expect(subject.connected).to.be.true;
|
||||
expect(subject.getWsPort()).to.equal(wsPort);
|
||||
expect(subject.isSerialOpen()).to.be.true;
|
||||
expect(subject.isWebSocketConnected()).to.be.false;
|
||||
});
|
||||
context('and it tries to open the serial monitor', () => {
|
||||
let status: Status;
|
||||
beforeEach(async () => {
|
||||
status = await subject.openSerial(Serial.Type.Monitor);
|
||||
});
|
||||
it('should open it using the same serial connection', () => {
|
||||
messageService.verify(
|
||||
(m) => m.error(It.isAnyString()),
|
||||
Times.never()
|
||||
);
|
||||
serialService.verify((m) => m.disconnect(), Times.never());
|
||||
serialService.verify(
|
||||
(m) => m.connect(It.isAny()),
|
||||
Times.once()
|
||||
);
|
||||
expect(status).to.be.ok;
|
||||
expect(subject.connected).to.be.true;
|
||||
expect(subject.isSerialOpen()).to.be.true;
|
||||
});
|
||||
it('should create a websocket connection', () => {
|
||||
expect(subject.getWsPort()).to.equal(wsPort);
|
||||
expect(subject.isWebSocketConnected()).to.be.true;
|
||||
});
|
||||
context('and then it closes the serial plotter', () => {
|
||||
beforeEach(async () => {
|
||||
status = await subject.closeSerial(Serial.Type.Plotter);
|
||||
});
|
||||
it('should close the plotter without disconnecting from the serial', () => {
|
||||
messageService.verify(
|
||||
(m) => m.error(It.isAnyString()),
|
||||
Times.never()
|
||||
);
|
||||
serialService.verify((m) => m.disconnect(), Times.never());
|
||||
serialService.verify(
|
||||
(m) => m.connect(It.isAny()),
|
||||
Times.once()
|
||||
);
|
||||
expect(status).to.be.ok;
|
||||
expect(subject.connected).to.be.true;
|
||||
expect(subject.isSerialOpen()).to.be.true;
|
||||
expect(subject.getWsPort()).to.equal(wsPort);
|
||||
});
|
||||
it('should not close the websocket connection', () => {
|
||||
expect(subject.isWebSocketConnected()).to.be.true;
|
||||
});
|
||||
});
|
||||
context('and then it closes the serial monitor', () => {
|
||||
beforeEach(async () => {
|
||||
status = await subject.closeSerial(Serial.Type.Monitor);
|
||||
});
|
||||
it('should close the monitor without disconnecting from the serial', () => {
|
||||
messageService.verify(
|
||||
(m) => m.error(It.isAnyString()),
|
||||
Times.never()
|
||||
);
|
||||
serialService.verify((m) => m.disconnect(), Times.never());
|
||||
serialService.verify(
|
||||
(m) => m.connect(It.isAny()),
|
||||
Times.once()
|
||||
);
|
||||
expect(status).to.be.ok;
|
||||
expect(subject.connected).to.be.true;
|
||||
expect(subject.getWsPort()).to.equal(wsPort);
|
||||
expect(subject.isSerialOpen()).to.be.true;
|
||||
});
|
||||
it('should close the websocket connection', () => {
|
||||
expect(subject.isWebSocketConnected()).to.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
context('and then it closes the serial plotter', () => {
|
||||
beforeEach(async () => {
|
||||
status = await subject.closeSerial(Serial.Type.Plotter);
|
||||
});
|
||||
it('should successfully disconnect from the serial', () => {
|
||||
messageService.verify(
|
||||
(m) => m.error(It.isAnyString()),
|
||||
Times.never()
|
||||
);
|
||||
serialService.verify((m) => m.disconnect(), Times.once());
|
||||
serialService.verify(
|
||||
(m) => m.connect(It.isAny()),
|
||||
Times.once()
|
||||
);
|
||||
expect(status).to.be.ok;
|
||||
expect(subject.connected).to.be.false;
|
||||
expect(subject.getWsPort()).to.be.undefined;
|
||||
expect(subject.isSerialOpen()).to.be.false;
|
||||
expect(subject.isWebSocketConnected()).to.be.false;
|
||||
});
|
||||
});
|
||||
context('and the config changes', () => {
|
||||
beforeEach(() => {
|
||||
subject.setConfig(anotherSerialConfig);
|
||||
});
|
||||
it('should try to reconnect', async () => {
|
||||
await tick();
|
||||
messageService.verify(
|
||||
(m) => m.error(It.isAnyString()),
|
||||
Times.never()
|
||||
);
|
||||
serialService.verify((m) => m.disconnect(), Times.once());
|
||||
serialService.verify(
|
||||
(m) => m.connect(It.isAny()),
|
||||
Times.exactly(2)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
context(
|
||||
'and the connection to the serial does NOT succeed with the config',
|
||||
() => {
|
||||
beforeEach(() => {
|
||||
serialService
|
||||
.setup((m) => m.connect(It.isValue(aSerialConfig)))
|
||||
.returns(() => {
|
||||
return Promise.resolve(Status.NOT_CONNECTED);
|
||||
});
|
||||
serialService
|
||||
.setup((m) => m.connect(It.isValue(anotherSerialConfig)))
|
||||
.returns(() => {
|
||||
handleWebSocketChanged(wsPort);
|
||||
return Promise.resolve(Status.OK);
|
||||
});
|
||||
});
|
||||
context('and it tries to open the serial plotter', () => {
|
||||
let status: Status;
|
||||
beforeEach(async () => {
|
||||
status = await subject.openSerial(Serial.Type.Plotter);
|
||||
});
|
||||
|
||||
it('should fail to connect to the serial', async () => {
|
||||
messageService.verify(
|
||||
(m) => m.error(It.isAnyString()),
|
||||
Times.never()
|
||||
);
|
||||
serialService.verify((m) => m.disconnect(), Times.never());
|
||||
serialService.verify(
|
||||
(m) => m.connect(It.isValue(aSerialConfig)),
|
||||
Times.once()
|
||||
);
|
||||
expect(status).to.be.false;
|
||||
expect(subject.connected).to.be.false;
|
||||
expect(subject.getWsPort()).to.be.undefined;
|
||||
expect(subject.isSerialOpen()).to.be.true;
|
||||
});
|
||||
|
||||
context(
|
||||
'and the board config changes with an acceptable one',
|
||||
() => {
|
||||
beforeEach(async () => {
|
||||
await handleBoardConfigChange(anotherBoardConfig);
|
||||
});
|
||||
|
||||
it('should successfully connect to the serial', async () => {
|
||||
await tick();
|
||||
messageService.verify(
|
||||
(m) => m.error(It.isAnyString()),
|
||||
Times.never()
|
||||
);
|
||||
serialService.verify((m) => m.disconnect(), Times.never());
|
||||
serialService.verify(
|
||||
(m) => m.connect(It.isValue(anotherSerialConfig)),
|
||||
Times.once()
|
||||
);
|
||||
expect(subject.connected).to.be.true;
|
||||
expect(subject.getWsPort()).to.equal(wsPort);
|
||||
expect(subject.isSerialOpen()).to.be.true;
|
||||
expect(subject.isWebSocketConnected()).to.be.false;
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
17
i18n/en.json
17
i18n/en.json
@ -179,7 +179,7 @@
|
||||
"upload": "Upload",
|
||||
"uploadUsingProgrammer": "Upload Using Programmer",
|
||||
"doneUploading": "Done uploading.",
|
||||
"couldNotConnectToMonitor": "Could not reconnect to serial monitor. {0}",
|
||||
"couldNotConnectToSerial": "Could not reconnect to serial port. {0}",
|
||||
"verifyOrCompile": "Verify/Compile",
|
||||
"exportBinary": "Export Compiled Binary",
|
||||
"verify": "Verify",
|
||||
@ -253,22 +253,21 @@
|
||||
"dialog": {
|
||||
"dontAskAgain": "Don't ask again"
|
||||
},
|
||||
"monitor": {
|
||||
"connectionBusy": "Connection failed. Serial port is busy: {0}",
|
||||
"disconnected": "Disconnected {0} from {1}.",
|
||||
"unexpectedError": "Unexpected error. Reconnecting {0} on port {1}.",
|
||||
"failedReconnect": "Failed to reconnect {0} to the the serial-monitor after 10 consecutive attempts. The {1} serial port is busy.",
|
||||
"reconnect": "Reconnecting {0} to {1} in {2] seconds...",
|
||||
"serial": {
|
||||
"toggleTimestamp": "Toggle Timestamp",
|
||||
"autoscroll": "Autoscroll",
|
||||
"timestamp": "Timestamp",
|
||||
"title": "Serial Monitor",
|
||||
"noLineEndings": "No Line Ending",
|
||||
"newLine": "New Line",
|
||||
"carriageReturn": "Carriage Return",
|
||||
"newLineCarriageReturn": "Both NL & CR",
|
||||
"notConnected": "Not connected. Select a board and a port to connect automatically.",
|
||||
"message": "Message ({0} + Enter to send message to '{1}' on '{2}'"
|
||||
"message": "Message ({0} + Enter to send message to '{1}' on '{2}'",
|
||||
"connectionBusy": "Connection failed. Serial port is busy: {0}",
|
||||
"disconnected": "Disconnected {0} from {1}.",
|
||||
"unexpectedError": "Unexpected error. Reconnecting {0} on port {1}.",
|
||||
"failedReconnect": "Failed to reconnect {0} to serial port after 10 consecutive attempts. The {1} serial port is busy.",
|
||||
"reconnect": "Reconnecting {0} to {1} in {2] seconds..."
|
||||
},
|
||||
"component": {
|
||||
"uninstall": "Uninstall",
|
||||
|
15
yarn.lock
15
yarn.lock
@ -4109,6 +4109,11 @@ archive-type@^4.0.0:
|
||||
dependencies:
|
||||
file-type "^4.2.0"
|
||||
|
||||
arduino-serial-plotter-webapp@0.0.13:
|
||||
version "0.0.13"
|
||||
resolved "https://registry.yarnpkg.com/arduino-serial-plotter-webapp/-/arduino-serial-plotter-webapp-0.0.13.tgz#b8d943a39f2c218bca36bb81bb6c5cabe4695ad7"
|
||||
integrity sha512-Rn1shl6c1pUt1vtcdsAzhHIlHuHAmC829z0nR4JW4mYdYA+1MEY2VbbhfDf/tXiAFm8XAGfH63f//h1t99eGWQ==
|
||||
|
||||
are-we-there-yet@~1.1.2:
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
|
||||
@ -11945,6 +11950,16 @@ query-string@^6.13.8:
|
||||
split-on-first "^1.0.0"
|
||||
strict-uri-encode "^2.0.0"
|
||||
|
||||
query-string@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.0.1.tgz#45bd149cf586aaa582dffc7ec7a8ad97dd02f75d"
|
||||
integrity sha512-uIw3iRvHnk9to1blJCG3BTc+Ro56CBowJXKmNNAm3RulvPBzWLRqKSiiDk+IplJhsydwtuNMHi8UGQFcCLVfkA==
|
||||
dependencies:
|
||||
decode-uri-component "^0.2.0"
|
||||
filter-obj "^1.1.0"
|
||||
split-on-first "^1.0.0"
|
||||
strict-uri-encode "^2.0.0"
|
||||
|
||||
querystring@0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
|
||||
|
Loading…
x
Reference in New Issue
Block a user