mirror of
				https://github.com/arduino/arduino-ide.git
				synced 2025-11-03 23:48:32 +00:00 
			
		
		
		
	Compare commits
	
		
			35 Commits
		
	
	
		
			2.0.1
			...
			pluggable-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					9a16cf9e02 | ||
| 
						 | 
					a4ff05a82b | ||
| 
						 | 
					0427759fdb | ||
| 
						 | 
					80ade4c37e | ||
| 
						 | 
					355dec8aaa | ||
| 
						 | 
					7bf4ea0637 | ||
| 
						 | 
					1982609c87 | ||
| 
						 | 
					62eaeb1c74 | ||
| 
						 | 
					9b58c9d0c8 | ||
| 
						 | 
					eff960bb7f | ||
| 
						 | 
					fbe8fb421a | ||
| 
						 | 
					a8d803e7c3 | ||
| 
						 | 
					397ca5665f | ||
| 
						 | 
					f9da9fc24b | ||
| 
						 | 
					b97af32bb8 | ||
| 
						 | 
					ce2f1c227a | ||
| 
						 | 
					7889f40834 | ||
| 
						 | 
					6b7b33356d | ||
| 
						 | 
					ad781f0bfc | ||
| 
						 | 
					6cf61c498a | ||
| 
						 | 
					50239c5756 | ||
| 
						 | 
					cbd5b4de1b | ||
| 
						 | 
					bf958fd8cf | ||
| 
						 | 
					ee265aec90 | ||
| 
						 | 
					9058abb015 | ||
| 
						 | 
					31b704cdb9 | ||
| 
						 | 
					61b8bdeec9 | ||
| 
						 | 
					c5695d3a76 | ||
| 
						 | 
					480492a7c8 | ||
| 
						 | 
					2c95e7f033 | ||
| 
						 | 
					116b3d5984 | ||
| 
						 | 
					750796d3a0 | ||
| 
						 | 
					3133b01c4a | ||
| 
						 | 
					ebab0b226f | ||
| 
						 | 
					2b2ea72643 | 
@@ -157,7 +157,7 @@
 | 
			
		||||
  ],
 | 
			
		||||
  "arduino": {
 | 
			
		||||
    "cli": {
 | 
			
		||||
      "version": "0.21.0"
 | 
			
		||||
      "version": "0.22.0"
 | 
			
		||||
    },
 | 
			
		||||
    "fwuploader": {
 | 
			
		||||
      "version": "2.0.0"
 | 
			
		||||
 
 | 
			
		||||
@@ -69,20 +69,12 @@ import { ScmContribution } from './theia/scm/scm-contribution';
 | 
			
		||||
import { SearchInWorkspaceFrontendContribution as TheiaSearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
 | 
			
		||||
import { SearchInWorkspaceFrontendContribution } from './theia/search-in-workspace/search-in-workspace-frontend-contribution';
 | 
			
		||||
import { LibraryListWidgetFrontendContribution } from './library/library-widget-frontend-contribution';
 | 
			
		||||
import { SerialServiceClientImpl } from './serial/serial-service-client-impl';
 | 
			
		||||
import {
 | 
			
		||||
  SerialServicePath,
 | 
			
		||||
  SerialService,
 | 
			
		||||
  SerialServiceClient,
 | 
			
		||||
} from '../common/protocol/serial-service';
 | 
			
		||||
import {
 | 
			
		||||
  ConfigService,
 | 
			
		||||
  ConfigServicePath,
 | 
			
		||||
} from '../common/protocol/config-service';
 | 
			
		||||
import { MonitorWidget } from './serial/monitor/monitor-widget';
 | 
			
		||||
import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution';
 | 
			
		||||
import { SerialConnectionManager } from './serial/serial-connection-manager';
 | 
			
		||||
import { SerialModel } from './serial/serial-model';
 | 
			
		||||
import { TabBarDecoratorService as TheiaTabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
 | 
			
		||||
import { TabBarDecoratorService } from './theia/core/tab-bar-decorator';
 | 
			
		||||
import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browser';
 | 
			
		||||
@@ -160,7 +152,7 @@ import {
 | 
			
		||||
  OutputChannelRegistryMainImpl as TheiaOutputChannelRegistryMainImpl,
 | 
			
		||||
  OutputChannelRegistryMainImpl,
 | 
			
		||||
} from './theia/plugin-ext/output-channel-registry-main';
 | 
			
		||||
import { ExecutableService, ExecutableServicePath } from '../common/protocol';
 | 
			
		||||
import { ExecutableService, ExecutableServicePath, MonitorManagerProxy, MonitorManagerProxyClient, MonitorManagerProxyFactory, MonitorManagerProxyPath } from '../common/protocol';
 | 
			
		||||
import { MonacoTextModelService as TheiaMonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
 | 
			
		||||
import { MonacoTextModelService } from './theia/monaco/monaco-text-model-service';
 | 
			
		||||
import { ResponseServiceImpl } from './response-service-impl';
 | 
			
		||||
@@ -275,6 +267,8 @@ import {
 | 
			
		||||
  IDEUpdaterDialogWidget,
 | 
			
		||||
} from './dialogs/ide-updater/ide-updater-dialog';
 | 
			
		||||
import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider';
 | 
			
		||||
import { MonitorModel } from './monitor-model';
 | 
			
		||||
import { MonitorManagerProxyClientImpl } from './monitor-manager-proxy-client-impl';
 | 
			
		||||
 | 
			
		||||
const ElementQueries = require('css-element-queries/src/ElementQueries');
 | 
			
		||||
 | 
			
		||||
@@ -407,29 +401,31 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
 | 
			
		||||
    .inSingletonScope();
 | 
			
		||||
 | 
			
		||||
  // Serial monitor
 | 
			
		||||
  bind(SerialModel).toSelf().inSingletonScope();
 | 
			
		||||
  bind(FrontendApplicationContribution).toService(SerialModel);
 | 
			
		||||
  bind(MonitorWidget).toSelf();
 | 
			
		||||
  bind(FrontendApplicationContribution).toService(MonitorModel);
 | 
			
		||||
  bind(MonitorModel).toSelf().inSingletonScope();
 | 
			
		||||
  bindViewContribution(bind, MonitorViewContribution);
 | 
			
		||||
  bind(TabBarToolbarContribution).toService(MonitorViewContribution);
 | 
			
		||||
  bind(WidgetFactory).toDynamicValue((context) => ({
 | 
			
		||||
    id: MonitorWidget.ID,
 | 
			
		||||
    createWidget: () => context.container.get(MonitorWidget),
 | 
			
		||||
  }));
 | 
			
		||||
  // Frontend binding for the serial service
 | 
			
		||||
  bind(SerialService)
 | 
			
		||||
    .toDynamicValue((context) => {
 | 
			
		||||
      const connection = context.container.get(WebSocketConnectionProvider);
 | 
			
		||||
      const client = context.container.get<SerialServiceClient>(
 | 
			
		||||
        SerialServiceClient
 | 
			
		||||
    createWidget: () => {
 | 
			
		||||
      return new MonitorWidget(
 | 
			
		||||
        context.container.get<MonitorModel>(MonitorModel),
 | 
			
		||||
        context.container.get<MonitorManagerProxyClient>(MonitorManagerProxyClient),
 | 
			
		||||
        context.container.get<BoardsServiceProvider>(BoardsServiceProvider),
 | 
			
		||||
      );
 | 
			
		||||
      return connection.createProxy(SerialServicePath, client);
 | 
			
		||||
    })
 | 
			
		||||
    .inSingletonScope();
 | 
			
		||||
  bind(SerialConnectionManager).toSelf().inSingletonScope();
 | 
			
		||||
    }
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  // Serial service client to receive and delegate notifications from the backend.
 | 
			
		||||
  bind(SerialServiceClient).to(SerialServiceClientImpl).inSingletonScope();
 | 
			
		||||
  bind(MonitorManagerProxyFactory).toFactory((context) => () => context.container.get<MonitorManagerProxy>(MonitorManagerProxy))
 | 
			
		||||
 | 
			
		||||
  bind(MonitorManagerProxy).toDynamicValue((context) =>
 | 
			
		||||
    WebSocketConnectionProvider.createProxy(context.container, MonitorManagerProxyPath, context.container.get(MonitorManagerProxyClient))
 | 
			
		||||
  ).inSingletonScope();
 | 
			
		||||
 | 
			
		||||
  // Monitor manager proxy client to receive and delegate pluggable monitors
 | 
			
		||||
  // notifications from the backend
 | 
			
		||||
  bind(MonitorManagerProxyClient).to(MonitorManagerProxyClientImpl).inSingletonScope();
 | 
			
		||||
 | 
			
		||||
  bind(WorkspaceService).toSelf().inSingletonScope();
 | 
			
		||||
  rebind(TheiaWorkspaceService).toService(WorkspaceService);
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,6 @@ import { OutputChannelManager } from '@theia/output/lib/browser/output-channel';
 | 
			
		||||
import { CoreService } from '../../common/protocol';
 | 
			
		||||
import { ArduinoMenus } from '../menu/arduino-menus';
 | 
			
		||||
import { BoardsDataStore } from '../boards/boards-data-store';
 | 
			
		||||
import { SerialConnectionManager } from '../serial/serial-connection-manager';
 | 
			
		||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
 | 
			
		||||
import {
 | 
			
		||||
  SketchContribution,
 | 
			
		||||
@@ -18,8 +17,6 @@ export class BurnBootloader extends SketchContribution {
 | 
			
		||||
  @inject(CoreService)
 | 
			
		||||
  protected readonly coreService: CoreService;
 | 
			
		||||
 | 
			
		||||
  @inject(SerialConnectionManager)
 | 
			
		||||
  protected readonly serialConnection: SerialConnectionManager;
 | 
			
		||||
 | 
			
		||||
  @inject(BoardsDataStore)
 | 
			
		||||
  protected readonly boardsDataStore: BoardsDataStore;
 | 
			
		||||
@@ -60,9 +57,15 @@ export class BurnBootloader extends SketchContribution {
 | 
			
		||||
          this.preferences.get('arduino.upload.verify'),
 | 
			
		||||
          this.preferences.get('arduino.upload.verbose'),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
      const board = {
 | 
			
		||||
        ...boardsConfig.selectedBoard,
 | 
			
		||||
        name: boardsConfig.selectedBoard?.name || '',
 | 
			
		||||
        fqbn,
 | 
			
		||||
      }
 | 
			
		||||
      this.outputChannelManager.getChannel('Arduino').clear();
 | 
			
		||||
      await this.coreService.burnBootloader({
 | 
			
		||||
        fqbn,
 | 
			
		||||
        board,
 | 
			
		||||
        programmer,
 | 
			
		||||
        port,
 | 
			
		||||
        verify,
 | 
			
		||||
@@ -85,8 +88,6 @@ export class BurnBootloader extends SketchContribution {
 | 
			
		||||
        errorMessage = e.toString();
 | 
			
		||||
      }
 | 
			
		||||
      this.messageService.error(errorMessage);
 | 
			
		||||
    } finally {
 | 
			
		||||
      await this.serialConnection.reconnectAfterUpload();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@ import { BoardUserField, CoreService } from '../../common/protocol';
 | 
			
		||||
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
 | 
			
		||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
 | 
			
		||||
import { BoardsDataStore } from '../boards/boards-data-store';
 | 
			
		||||
import { SerialConnectionManager } from '../serial/serial-connection-manager';
 | 
			
		||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
 | 
			
		||||
import {
 | 
			
		||||
  SketchContribution,
 | 
			
		||||
@@ -22,9 +21,6 @@ export class UploadSketch extends SketchContribution {
 | 
			
		||||
  @inject(CoreService)
 | 
			
		||||
  protected readonly coreService: CoreService;
 | 
			
		||||
 | 
			
		||||
  @inject(SerialConnectionManager)
 | 
			
		||||
  protected readonly serialConnection: SerialConnectionManager;
 | 
			
		||||
 | 
			
		||||
  @inject(MenuModelRegistry)
 | 
			
		||||
  protected readonly menuRegistry: MenuModelRegistry;
 | 
			
		||||
 | 
			
		||||
@@ -226,6 +222,11 @@ export class UploadSketch extends SketchContribution {
 | 
			
		||||
          this.sourceOverride(),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
      const board = {
 | 
			
		||||
        ...boardsConfig.selectedBoard,
 | 
			
		||||
        name: boardsConfig.selectedBoard?.name || '',
 | 
			
		||||
        fqbn,
 | 
			
		||||
      }
 | 
			
		||||
      let options: CoreService.Upload.Options | undefined = undefined;
 | 
			
		||||
      const sketchUri = sketch.uri;
 | 
			
		||||
      const optimizeForDebug = this.editorMode.compileForDebug;
 | 
			
		||||
@@ -247,7 +248,7 @@ export class UploadSketch extends SketchContribution {
 | 
			
		||||
        const programmer = selectedProgrammer;
 | 
			
		||||
        options = {
 | 
			
		||||
          sketchUri,
 | 
			
		||||
          fqbn,
 | 
			
		||||
          board,
 | 
			
		||||
          optimizeForDebug,
 | 
			
		||||
          programmer,
 | 
			
		||||
          port,
 | 
			
		||||
@@ -259,7 +260,7 @@ export class UploadSketch extends SketchContribution {
 | 
			
		||||
      } else {
 | 
			
		||||
        options = {
 | 
			
		||||
          sketchUri,
 | 
			
		||||
          fqbn,
 | 
			
		||||
          board,
 | 
			
		||||
          optimizeForDebug,
 | 
			
		||||
          port,
 | 
			
		||||
          verbose,
 | 
			
		||||
@@ -289,8 +290,6 @@ export class UploadSketch extends SketchContribution {
 | 
			
		||||
    } finally {
 | 
			
		||||
      this.uploadInProgress = false;
 | 
			
		||||
      this.onDidChangeEmitter.fire();
 | 
			
		||||
 | 
			
		||||
      setTimeout(() => this.serialConnection.reconnectAfterUpload(), 5000);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -110,12 +110,17 @@ export class VerifySketch extends SketchContribution {
 | 
			
		||||
        ),
 | 
			
		||||
        this.sourceOverride(),
 | 
			
		||||
      ]);
 | 
			
		||||
      const board = {
 | 
			
		||||
        ...boardsConfig.selectedBoard,
 | 
			
		||||
        name: boardsConfig.selectedBoard?.name || '',
 | 
			
		||||
        fqbn,
 | 
			
		||||
      }
 | 
			
		||||
      const verbose = this.preferences.get('arduino.compile.verbose');
 | 
			
		||||
      const compilerWarnings = this.preferences.get('arduino.compile.warnings');
 | 
			
		||||
      this.outputChannelManager.getChannel('Arduino').clear();
 | 
			
		||||
      await this.coreService.compile({
 | 
			
		||||
        sketchUri: sketch.uri,
 | 
			
		||||
        fqbn,
 | 
			
		||||
        board,
 | 
			
		||||
        optimizeForDebug: this.editorMode.compileForDebug,
 | 
			
		||||
        verbose,
 | 
			
		||||
        exportBinaries,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import { nls } from '@theia/core/lib/common';
 | 
			
		||||
import * as React from 'react';
 | 
			
		||||
import { Port } from '../../../common/protocol';
 | 
			
		||||
import {
 | 
			
		||||
  ArduinoFirmwareUploader,
 | 
			
		||||
  FirmwareInfo,
 | 
			
		||||
@@ -20,7 +21,7 @@ export const FirmwareUploaderComponent = ({
 | 
			
		||||
  availableBoards: AvailableBoard[];
 | 
			
		||||
  firmwareUploader: ArduinoFirmwareUploader;
 | 
			
		||||
  updatableFqbns: string[];
 | 
			
		||||
  flashFirmware: (firmware: FirmwareInfo, port: string) => Promise<any>;
 | 
			
		||||
  flashFirmware: (firmware: FirmwareInfo, port: Port) => Promise<any>;
 | 
			
		||||
  isOpen: any;
 | 
			
		||||
}): React.ReactElement => {
 | 
			
		||||
  // boolean states for buttons
 | 
			
		||||
@@ -81,7 +82,7 @@ export const FirmwareUploaderComponent = ({
 | 
			
		||||
      const installStatus =
 | 
			
		||||
        !!firmwareToFlash &&
 | 
			
		||||
        !!selectedBoard?.port &&
 | 
			
		||||
        (await flashFirmware(firmwareToFlash, selectedBoard?.port.address));
 | 
			
		||||
        (await flashFirmware(firmwareToFlash, selectedBoard?.port));
 | 
			
		||||
 | 
			
		||||
      setInstallFeedback((installStatus && 'ok') || 'fail');
 | 
			
		||||
    } catch {
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@ import {
 | 
			
		||||
} from '../../../common/protocol/arduino-firmware-uploader';
 | 
			
		||||
import { FirmwareUploaderComponent } from './firmware-uploader-component';
 | 
			
		||||
import { UploadFirmware } from '../../contributions/upload-firmware';
 | 
			
		||||
import { Port } from '../../../common/protocol';
 | 
			
		||||
 | 
			
		||||
@injectable()
 | 
			
		||||
export class UploadFirmwareDialogWidget extends ReactWidget {
 | 
			
		||||
@@ -49,7 +50,7 @@ export class UploadFirmwareDialogWidget extends ReactWidget {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected flashFirmware(firmware: FirmwareInfo, port: string): Promise<any> {
 | 
			
		||||
  protected flashFirmware(firmware: FirmwareInfo, port: Port): Promise<any> {
 | 
			
		||||
    this.busyCallback(true);
 | 
			
		||||
    return this.arduinoFirmwareUploader
 | 
			
		||||
      .flash(firmware, port)
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,127 @@
 | 
			
		||||
import { Emitter, MessageService } from '@theia/core';
 | 
			
		||||
import { inject, injectable } from '@theia/core/shared/inversify';
 | 
			
		||||
import { Board, Port } from '../common/protocol';
 | 
			
		||||
import {
 | 
			
		||||
  Monitor,
 | 
			
		||||
  MonitorManagerProxyClient,
 | 
			
		||||
  MonitorManagerProxyFactory,
 | 
			
		||||
} from '../common/protocol/monitor-service';
 | 
			
		||||
import {
 | 
			
		||||
  PluggableMonitorSettings,
 | 
			
		||||
  MonitorSettings,
 | 
			
		||||
} from '../node/monitor-settings/monitor-settings-provider';
 | 
			
		||||
 | 
			
		||||
@injectable()
 | 
			
		||||
export class MonitorManagerProxyClientImpl
 | 
			
		||||
  implements MonitorManagerProxyClient
 | 
			
		||||
{
 | 
			
		||||
  // When pluggable monitor messages are received from the backend
 | 
			
		||||
  // this event is triggered.
 | 
			
		||||
  // Ideally a frontend component is connected to this event
 | 
			
		||||
  // to update the UI.
 | 
			
		||||
  protected readonly onMessagesReceivedEmitter = new Emitter<{
 | 
			
		||||
    messages: string[];
 | 
			
		||||
  }>();
 | 
			
		||||
  readonly onMessagesReceived = this.onMessagesReceivedEmitter.event;
 | 
			
		||||
 | 
			
		||||
  protected readonly onWSConnectionChangedEmitter = new Emitter<boolean>();
 | 
			
		||||
  readonly onWSConnectionChanged = this.onWSConnectionChangedEmitter.event;
 | 
			
		||||
 | 
			
		||||
  // WebSocket used to handle pluggable monitor communication between
 | 
			
		||||
  // frontend and backend.
 | 
			
		||||
  private webSocket?: WebSocket;
 | 
			
		||||
  private wsPort?: number;
 | 
			
		||||
 | 
			
		||||
  getWebSocketPort(): number | undefined {
 | 
			
		||||
    return this.wsPort;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    @inject(MessageService)
 | 
			
		||||
    protected messageService: MessageService,
 | 
			
		||||
 | 
			
		||||
    // This is necessary to call the backend methods from the frontend
 | 
			
		||||
    @inject(MonitorManagerProxyFactory)
 | 
			
		||||
    protected server: MonitorManagerProxyFactory
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Connects a localhost WebSocket using the specified port.
 | 
			
		||||
   * @param addressPort port of the WebSocket
 | 
			
		||||
   */
 | 
			
		||||
  connect(addressPort: number): void {
 | 
			
		||||
    if (this.webSocket) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      this.webSocket = new WebSocket(`ws://localhost:${addressPort}`);
 | 
			
		||||
      this.onWSConnectionChangedEmitter.fire(true);
 | 
			
		||||
    } catch {
 | 
			
		||||
      this.messageService.error('Unable to connect to websocket');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.webSocket.onmessage = (res) => {
 | 
			
		||||
      const messages = JSON.parse(res.data);
 | 
			
		||||
      this.onMessagesReceivedEmitter.fire({ messages });
 | 
			
		||||
    };
 | 
			
		||||
    this.wsPort = addressPort;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Disconnects the WebSocket if connected.
 | 
			
		||||
   */
 | 
			
		||||
  disconnect(): void {
 | 
			
		||||
    try {
 | 
			
		||||
      this.webSocket?.close();
 | 
			
		||||
      this.webSocket = undefined;
 | 
			
		||||
      this.onWSConnectionChangedEmitter.fire(false);
 | 
			
		||||
    } catch {
 | 
			
		||||
      this.messageService.error('Unable to close websocket');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async isWSConnected(): Promise<boolean> {
 | 
			
		||||
    return !!this.webSocket;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async startMonitor(
 | 
			
		||||
    board: Board,
 | 
			
		||||
    port: Port,
 | 
			
		||||
    settings?: PluggableMonitorSettings
 | 
			
		||||
  ): Promise<void> {
 | 
			
		||||
    return this.server().startMonitor(board, port, settings);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getCurrentSettings(board: Board, port: Port): MonitorSettings {
 | 
			
		||||
    return this.server().getCurrentSettings(board, port);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  send(message: string): void {
 | 
			
		||||
    if (!this.webSocket) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.webSocket.send(
 | 
			
		||||
      JSON.stringify({
 | 
			
		||||
        command: Monitor.Command.SEND_MESSAGE,
 | 
			
		||||
        data: message,
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  changeSettings(settings: MonitorSettings): void {
 | 
			
		||||
    if (!this.webSocket) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.webSocket.send(
 | 
			
		||||
      JSON.stringify({
 | 
			
		||||
        command: Monitor.Command.CHANGE_SETTINGS,
 | 
			
		||||
        // TODO: This might be wrong, verify if it works
 | 
			
		||||
        // SPOILER: It doesn't
 | 
			
		||||
        data: settings,
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,69 +1,78 @@
 | 
			
		||||
import { injectable, inject } from 'inversify';
 | 
			
		||||
import { Emitter, Event } from '@theia/core/lib/common/event';
 | 
			
		||||
import { SerialConfig } from '../../common/protocol';
 | 
			
		||||
import { Emitter, Event } from '@theia/core';
 | 
			
		||||
import {
 | 
			
		||||
  FrontendApplicationContribution,
 | 
			
		||||
  LocalStorageService,
 | 
			
		||||
} from '@theia/core/lib/browser';
 | 
			
		||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
 | 
			
		||||
import { inject, injectable } from '@theia/core/shared/inversify';
 | 
			
		||||
 | 
			
		||||
@injectable()
 | 
			
		||||
export class SerialModel implements FrontendApplicationContribution {
 | 
			
		||||
  protected static STORAGE_ID = 'arduino-serial-model';
 | 
			
		||||
export class MonitorModel implements FrontendApplicationContribution {
 | 
			
		||||
  protected static STORAGE_ID = 'arduino-monitor-model';
 | 
			
		||||
 | 
			
		||||
  @inject(LocalStorageService)
 | 
			
		||||
  protected readonly localStorageService: LocalStorageService;
 | 
			
		||||
 | 
			
		||||
  @inject(BoardsServiceProvider)
 | 
			
		||||
  protected readonly boardsServiceClient: BoardsServiceProvider;
 | 
			
		||||
 | 
			
		||||
  protected readonly onChangeEmitter: Emitter<
 | 
			
		||||
    SerialModel.State.Change<keyof SerialModel.State>
 | 
			
		||||
    MonitorModel.State.Change<keyof MonitorModel.State>
 | 
			
		||||
  >;
 | 
			
		||||
 | 
			
		||||
  protected _autoscroll: boolean;
 | 
			
		||||
  protected _timestamp: boolean;
 | 
			
		||||
  protected _baudRate: SerialConfig.BaudRate;
 | 
			
		||||
  protected _lineEnding: SerialModel.EOL;
 | 
			
		||||
  protected _lineEnding: MonitorModel.EOL;
 | 
			
		||||
  protected _interpolate: boolean;
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this._autoscroll = true;
 | 
			
		||||
    this._timestamp = false;
 | 
			
		||||
    this._baudRate = SerialConfig.BaudRate.DEFAULT;
 | 
			
		||||
    this._lineEnding = SerialModel.EOL.DEFAULT;
 | 
			
		||||
    this._interpolate = false;
 | 
			
		||||
    this._lineEnding = MonitorModel.EOL.DEFAULT;
 | 
			
		||||
 | 
			
		||||
    this.onChangeEmitter = new Emitter<
 | 
			
		||||
      SerialModel.State.Change<keyof SerialModel.State>
 | 
			
		||||
      MonitorModel.State.Change<keyof MonitorModel.State>
 | 
			
		||||
    >();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onStart(): void {
 | 
			
		||||
    this.localStorageService
 | 
			
		||||
      .getData<SerialModel.State>(SerialModel.STORAGE_ID)
 | 
			
		||||
      .then((state) => {
 | 
			
		||||
        if (state) {
 | 
			
		||||
          this.restoreState(state);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      .getData<MonitorModel.State>(MonitorModel.STORAGE_ID)
 | 
			
		||||
      .then(this.restoreState);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get onChange(): Event<SerialModel.State.Change<keyof SerialModel.State>> {
 | 
			
		||||
  get onChange(): Event<MonitorModel.State.Change<keyof MonitorModel.State>> {
 | 
			
		||||
    return this.onChangeEmitter.event;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected restoreState(state: MonitorModel.State): void {
 | 
			
		||||
    if (!state) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this._autoscroll = state.autoscroll;
 | 
			
		||||
    this._timestamp = state.timestamp;
 | 
			
		||||
    this._lineEnding = state.lineEnding;
 | 
			
		||||
    this._interpolate = state.interpolate;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected async storeState(): Promise<void> {
 | 
			
		||||
    return this.localStorageService.setData(MonitorModel.STORAGE_ID, {
 | 
			
		||||
      autoscroll: this._autoscroll,
 | 
			
		||||
      timestamp: this._timestamp,
 | 
			
		||||
      lineEnding: this._lineEnding,
 | 
			
		||||
      interpolate: this._interpolate,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get autoscroll(): boolean {
 | 
			
		||||
    return this._autoscroll;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  toggleAutoscroll(): void {
 | 
			
		||||
    this._autoscroll = !this._autoscroll;
 | 
			
		||||
    this.storeState();
 | 
			
		||||
    this.storeState().then(() =>
 | 
			
		||||
    this.storeState().then(() => {
 | 
			
		||||
      this.onChangeEmitter.fire({
 | 
			
		||||
        property: 'autoscroll',
 | 
			
		||||
        value: this._autoscroll,
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
        value: this._timestamp,
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get timestamp(): boolean {
 | 
			
		||||
@@ -80,25 +89,11 @@ export class SerialModel implements FrontendApplicationContribution {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get baudRate(): SerialConfig.BaudRate {
 | 
			
		||||
    return this._baudRate;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  set baudRate(baudRate: SerialConfig.BaudRate) {
 | 
			
		||||
    this._baudRate = baudRate;
 | 
			
		||||
    this.storeState().then(() =>
 | 
			
		||||
      this.onChangeEmitter.fire({
 | 
			
		||||
        property: 'baudRate',
 | 
			
		||||
        value: this._baudRate,
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get lineEnding(): SerialModel.EOL {
 | 
			
		||||
  get lineEnding(): MonitorModel.EOL {
 | 
			
		||||
    return this._lineEnding;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  set lineEnding(lineEnding: SerialModel.EOL) {
 | 
			
		||||
  set lineEnding(lineEnding: MonitorModel.EOL) {
 | 
			
		||||
    this._lineEnding = lineEnding;
 | 
			
		||||
    this.storeState().then(() =>
 | 
			
		||||
      this.onChangeEmitter.fire({
 | 
			
		||||
@@ -121,31 +116,13 @@ export class SerialModel implements FrontendApplicationContribution {
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected restoreState(state: SerialModel.State): void {
 | 
			
		||||
    this._autoscroll = state.autoscroll;
 | 
			
		||||
    this._timestamp = state.timestamp;
 | 
			
		||||
    this._baudRate = state.baudRate;
 | 
			
		||||
    this._lineEnding = state.lineEnding;
 | 
			
		||||
    this._interpolate = state.interpolate;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected async storeState(): Promise<void> {
 | 
			
		||||
    return this.localStorageService.setData(SerialModel.STORAGE_ID, {
 | 
			
		||||
      autoscroll: this._autoscroll,
 | 
			
		||||
      timestamp: this._timestamp,
 | 
			
		||||
      baudRate: this._baudRate,
 | 
			
		||||
      lineEnding: this._lineEnding,
 | 
			
		||||
      interpolate: this._interpolate,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export namespace SerialModel {
 | 
			
		||||
// TODO: Move this to /common
 | 
			
		||||
export namespace MonitorModel {
 | 
			
		||||
  export interface State {
 | 
			
		||||
    autoscroll: boolean;
 | 
			
		||||
    timestamp: boolean;
 | 
			
		||||
    baudRate: SerialConfig.BaudRate;
 | 
			
		||||
    lineEnding: EOL;
 | 
			
		||||
    interpolate: boolean;
 | 
			
		||||
  }
 | 
			
		||||
@@ -8,9 +8,9 @@ import {
 | 
			
		||||
  TabBarToolbarRegistry,
 | 
			
		||||
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
 | 
			
		||||
import { ArduinoToolbar } from '../../toolbar/arduino-toolbar';
 | 
			
		||||
import { SerialModel } from '../serial-model';
 | 
			
		||||
import { ArduinoMenus } from '../../menu/arduino-menus';
 | 
			
		||||
import { nls } from '@theia/core/lib/common';
 | 
			
		||||
import { MonitorModel } from '../../monitor-model';
 | 
			
		||||
 | 
			
		||||
export namespace SerialMonitor {
 | 
			
		||||
  export namespace Commands {
 | 
			
		||||
@@ -48,7 +48,8 @@ export class MonitorViewContribution
 | 
			
		||||
  static readonly TOGGLE_SERIAL_MONITOR_TOOLBAR =
 | 
			
		||||
    MonitorWidget.ID + ':toggle-toolbar';
 | 
			
		||||
 | 
			
		||||
  @inject(SerialModel) protected readonly model: SerialModel;
 | 
			
		||||
  @inject(MonitorModel)
 | 
			
		||||
  protected readonly model: MonitorModel;
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    super({
 | 
			
		||||
 
 | 
			
		||||
@@ -9,14 +9,14 @@ import {
 | 
			
		||||
  Widget,
 | 
			
		||||
  MessageLoop,
 | 
			
		||||
} from '@theia/core/lib/browser/widgets';
 | 
			
		||||
import { SerialConfig } from '../../../common/protocol/serial-service';
 | 
			
		||||
import { ArduinoSelect } from '../../widgets/arduino-select';
 | 
			
		||||
import { SerialModel } from '../serial-model';
 | 
			
		||||
import { SerialConnectionManager } from '../serial-connection-manager';
 | 
			
		||||
import { SerialMonitorSendInput } from './serial-monitor-send-input';
 | 
			
		||||
import { SerialMonitorOutput } from './serial-monitor-send-output';
 | 
			
		||||
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
 | 
			
		||||
import { nls } from '@theia/core/lib/common';
 | 
			
		||||
import { MonitorManagerProxyClient } from '../../../common/protocol';
 | 
			
		||||
import { MonitorModel } from '../../monitor-model';
 | 
			
		||||
import { MonitorSettings } from '../../../node/monitor-settings/monitor-settings-provider';
 | 
			
		||||
 | 
			
		||||
@injectable()
 | 
			
		||||
export class MonitorWidget extends ReactWidget {
 | 
			
		||||
@@ -26,15 +26,6 @@ export class MonitorWidget extends ReactWidget {
 | 
			
		||||
  );
 | 
			
		||||
  static readonly ID = 'serial-monitor';
 | 
			
		||||
 | 
			
		||||
  @inject(SerialModel)
 | 
			
		||||
  protected readonly serialModel: SerialModel;
 | 
			
		||||
 | 
			
		||||
  @inject(SerialConnectionManager)
 | 
			
		||||
  protected readonly serialConnection: SerialConnectionManager;
 | 
			
		||||
 | 
			
		||||
  @inject(BoardsServiceProvider)
 | 
			
		||||
  protected readonly boardsServiceProvider: BoardsServiceProvider;
 | 
			
		||||
 | 
			
		||||
  protected widgetHeight: number;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@@ -48,7 +39,16 @@ export class MonitorWidget extends ReactWidget {
 | 
			
		||||
  protected closing = false;
 | 
			
		||||
  protected readonly clearOutputEmitter = new Emitter<void>();
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
  constructor(
 | 
			
		||||
    @inject(MonitorModel)
 | 
			
		||||
    protected readonly monitorModel: MonitorModel,
 | 
			
		||||
 | 
			
		||||
    @inject(MonitorManagerProxyClient)
 | 
			
		||||
    protected readonly monitorManagerProxy: MonitorManagerProxyClient,
 | 
			
		||||
 | 
			
		||||
    @inject(BoardsServiceProvider)
 | 
			
		||||
    protected readonly boardsServiceProvider: BoardsServiceProvider
 | 
			
		||||
  ) {
 | 
			
		||||
    super();
 | 
			
		||||
    this.id = MonitorWidget.ID;
 | 
			
		||||
    this.title.label = MonitorWidget.LABEL;
 | 
			
		||||
@@ -57,17 +57,35 @@ export class MonitorWidget extends ReactWidget {
 | 
			
		||||
    this.scrollOptions = undefined;
 | 
			
		||||
    this.toDispose.push(this.clearOutputEmitter);
 | 
			
		||||
    this.toDispose.push(
 | 
			
		||||
      Disposable.create(() => this.serialConnection.closeWStoBE())
 | 
			
		||||
      Disposable.create(() => this.monitorManagerProxy.disconnect())
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Start monitor right away if there is already a board/port combination selected
 | 
			
		||||
    const { selectedBoard, selectedPort } =
 | 
			
		||||
      this.boardsServiceProvider.boardsConfig;
 | 
			
		||||
    if (selectedBoard && selectedBoard.fqbn && selectedPort) {
 | 
			
		||||
      this.monitorManagerProxy.startMonitor(selectedBoard, selectedPort);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.toDispose.push(
 | 
			
		||||
      this.boardsServiceProvider.onBoardsConfigChanged(
 | 
			
		||||
        async ({ selectedBoard, selectedPort }) => {
 | 
			
		||||
          if (selectedBoard && selectedBoard.fqbn && selectedPort) {
 | 
			
		||||
            await this.monitorManagerProxy.startMonitor(
 | 
			
		||||
              selectedBoard,
 | 
			
		||||
              selectedPort
 | 
			
		||||
            );
 | 
			
		||||
            this.update();
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      )
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @postConstruct()
 | 
			
		||||
  protected init(): void {
 | 
			
		||||
    this.update();
 | 
			
		||||
    this.toDispose.push(
 | 
			
		||||
      this.serialConnection.onConnectionChanged(() => this.clearConsole())
 | 
			
		||||
    );
 | 
			
		||||
    this.toDispose.push(this.serialModel.onChange(() => this.update()));
 | 
			
		||||
    this.toDispose.push(this.monitorModel.onChange(() => this.update()));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  clearConsole(): void {
 | 
			
		||||
@@ -79,11 +97,6 @@ export class MonitorWidget extends ReactWidget {
 | 
			
		||||
    super.dispose();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected onAfterAttach(msg: Message): void {
 | 
			
		||||
    super.onAfterAttach(msg);
 | 
			
		||||
    this.serialConnection.openWSToBE();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onCloseRequest(msg: Message): void {
 | 
			
		||||
    this.closing = true;
 | 
			
		||||
    super.onCloseRequest(msg);
 | 
			
		||||
@@ -119,7 +132,7 @@ export class MonitorWidget extends ReactWidget {
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  protected get lineEndings(): OptionsType<
 | 
			
		||||
    SerialMonitorOutput.SelectOption<SerialModel.EOL>
 | 
			
		||||
    SerialMonitorOutput.SelectOption<MonitorModel.EOL>
 | 
			
		||||
  > {
 | 
			
		||||
    return [
 | 
			
		||||
      {
 | 
			
		||||
@@ -144,32 +157,63 @@ export class MonitorWidget extends ReactWidget {
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected get baudRates(): OptionsType<
 | 
			
		||||
    SerialMonitorOutput.SelectOption<SerialConfig.BaudRate>
 | 
			
		||||
  > {
 | 
			
		||||
    const baudRates: Array<SerialConfig.BaudRate> = [
 | 
			
		||||
      300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200,
 | 
			
		||||
    ];
 | 
			
		||||
    return baudRates.map((baudRate) => ({
 | 
			
		||||
      label: baudRate + ' baud',
 | 
			
		||||
      value: baudRate,
 | 
			
		||||
    }));
 | 
			
		||||
  private getCurrentSettings(): MonitorSettings {
 | 
			
		||||
    const board = this.boardsServiceProvider.boardsConfig.selectedBoard;
 | 
			
		||||
    const port = this.boardsServiceProvider.boardsConfig.selectedPort;
 | 
			
		||||
    if (!board || !port) {
 | 
			
		||||
      return {};
 | 
			
		||||
    }
 | 
			
		||||
    return this.monitorManagerProxy.getCurrentSettings(board, port);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  //////////////////////////////////////////////////
 | 
			
		||||
  ////////////////////IMPORTANT/////////////////////
 | 
			
		||||
  //////////////////////////////////////////////////
 | 
			
		||||
  // baudRates and selectedBaudRates as of now are hardcoded
 | 
			
		||||
  // like this to retrieve the baudrate settings from the ones
 | 
			
		||||
  // received by the monitor.
 | 
			
		||||
  // We're doing it like since the frontend as of now doesn't
 | 
			
		||||
  // support a fully customizable list of options that would
 | 
			
		||||
  // be require to support pluggable monitors completely.
 | 
			
		||||
  // As soon as the frontend UI is updated to support
 | 
			
		||||
  // any custom settings this methods MUST be removed and
 | 
			
		||||
  // made generic.
 | 
			
		||||
  //
 | 
			
		||||
  // This breaks if the user tries to open a monitor that
 | 
			
		||||
  // doesn't support the baudrate setting.
 | 
			
		||||
  protected get baudRates(): string[] {
 | 
			
		||||
    const { pluggableMonitorSettings } = this.getCurrentSettings();
 | 
			
		||||
    if (!pluggableMonitorSettings || !pluggableMonitorSettings['baudrate']) {
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const baudRateSettings = pluggableMonitorSettings['baudrate'];
 | 
			
		||||
 | 
			
		||||
    return baudRateSettings.values;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected get selectedBaudRate(): string {
 | 
			
		||||
    const { pluggableMonitorSettings } = this.getCurrentSettings();
 | 
			
		||||
    if (!pluggableMonitorSettings || !pluggableMonitorSettings['baudrate']) {
 | 
			
		||||
      return '';
 | 
			
		||||
    }
 | 
			
		||||
    const baudRateSettings = pluggableMonitorSettings['baudrate'];
 | 
			
		||||
    return baudRateSettings.selectedValue;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected render(): React.ReactNode {
 | 
			
		||||
    const { baudRates, lineEndings } = this;
 | 
			
		||||
    const lineEnding =
 | 
			
		||||
      lineEndings.find((item) => item.value === this.serialModel.lineEnding) ||
 | 
			
		||||
      lineEndings.find((item) => item.value === this.monitorModel.lineEnding) ||
 | 
			
		||||
      lineEndings[1]; // Defaults to `\n`.
 | 
			
		||||
    const baudRate =
 | 
			
		||||
      baudRates.find((item) => item.value === this.serialModel.baudRate) ||
 | 
			
		||||
      baudRates[4]; // Defaults to `9600`.
 | 
			
		||||
    const baudRate = baudRates.find((item) => item === this.selectedBaudRate);
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="serial-monitor">
 | 
			
		||||
        <div className="head">
 | 
			
		||||
          <div className="send">
 | 
			
		||||
            <SerialMonitorSendInput
 | 
			
		||||
              serialConnection={this.serialConnection}
 | 
			
		||||
              boardsServiceProvider={this.boardsServiceProvider}
 | 
			
		||||
              monitorManagerProxy={this.monitorManagerProxy}
 | 
			
		||||
              resolveFocus={this.onFocusResolved}
 | 
			
		||||
              onSend={this.onSend}
 | 
			
		||||
            />
 | 
			
		||||
@@ -196,8 +240,8 @@ export class MonitorWidget extends ReactWidget {
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="body">
 | 
			
		||||
          <SerialMonitorOutput
 | 
			
		||||
            serialModel={this.serialModel}
 | 
			
		||||
            serialConnection={this.serialConnection}
 | 
			
		||||
            monitorModel={this.monitorModel}
 | 
			
		||||
            monitorManagerProxy={this.monitorManagerProxy}
 | 
			
		||||
            clearConsoleEvent={this.clearOutputEmitter.event}
 | 
			
		||||
            height={Math.floor(this.widgetHeight - 50)}
 | 
			
		||||
          />
 | 
			
		||||
@@ -208,18 +252,21 @@ export class MonitorWidget extends ReactWidget {
 | 
			
		||||
 | 
			
		||||
  protected readonly onSend = (value: string) => this.doSend(value);
 | 
			
		||||
  protected async doSend(value: string): Promise<void> {
 | 
			
		||||
    this.serialConnection.send(value);
 | 
			
		||||
    this.monitorManagerProxy.send(value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected readonly onChangeLineEnding = (
 | 
			
		||||
    option: SerialMonitorOutput.SelectOption<SerialModel.EOL>
 | 
			
		||||
    option: SerialMonitorOutput.SelectOption<MonitorModel.EOL>
 | 
			
		||||
  ) => {
 | 
			
		||||
    this.serialModel.lineEnding = option.value;
 | 
			
		||||
    this.monitorModel.lineEnding = option.value;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  protected readonly onChangeBaudRate = (
 | 
			
		||||
    option: SerialMonitorOutput.SelectOption<SerialConfig.BaudRate>
 | 
			
		||||
  ) => {
 | 
			
		||||
    this.serialModel.baudRate = option.value;
 | 
			
		||||
  protected readonly onChangeBaudRate = (value: string) => {
 | 
			
		||||
    const { pluggableMonitorSettings } = this.getCurrentSettings();
 | 
			
		||||
    if (!pluggableMonitorSettings || !pluggableMonitorSettings['baudrate'])
 | 
			
		||||
      return;
 | 
			
		||||
    const baudRateSettings = pluggableMonitorSettings['baudrate'];
 | 
			
		||||
    baudRateSettings.selectedValue = value;
 | 
			
		||||
    this.monitorManagerProxy.changeSettings(pluggableMonitorSettings);
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,12 +3,14 @@ import { Key, KeyCode } from '@theia/core/lib/browser/keys';
 | 
			
		||||
import { Board } from '../../../common/protocol/boards-service';
 | 
			
		||||
import { isOSX } from '@theia/core/lib/common/os';
 | 
			
		||||
import { DisposableCollection, nls } from '@theia/core/lib/common';
 | 
			
		||||
import { SerialConnectionManager } from '../serial-connection-manager';
 | 
			
		||||
import { SerialPlotter } from '../plotter/protocol';
 | 
			
		||||
import { MonitorManagerProxyClient } from '../../../common/protocol';
 | 
			
		||||
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
 | 
			
		||||
import { timeout } from '@theia/core/lib/common/promise-util';
 | 
			
		||||
 | 
			
		||||
export namespace SerialMonitorSendInput {
 | 
			
		||||
  export interface Props {
 | 
			
		||||
    readonly serialConnection: SerialConnectionManager;
 | 
			
		||||
    readonly boardsServiceProvider: BoardsServiceProvider;
 | 
			
		||||
    readonly monitorManagerProxy: MonitorManagerProxyClient;
 | 
			
		||||
    readonly onSend: (text: string) => void;
 | 
			
		||||
    readonly resolveFocus: (element: HTMLElement | undefined) => void;
 | 
			
		||||
  }
 | 
			
		||||
@@ -26,28 +28,33 @@ export class SerialMonitorSendInput extends React.Component<
 | 
			
		||||
 | 
			
		||||
  constructor(props: Readonly<SerialMonitorSendInput.Props>) {
 | 
			
		||||
    super(props);
 | 
			
		||||
    this.state = { text: '', connected: false };
 | 
			
		||||
    this.state = { text: '', connected: true };
 | 
			
		||||
    this.onChange = this.onChange.bind(this);
 | 
			
		||||
    this.onSend = this.onSend.bind(this);
 | 
			
		||||
    this.onKeyDown = this.onKeyDown.bind(this);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidMount(): void {
 | 
			
		||||
    this.props.serialConnection.isBESerialConnected().then((connected) => {
 | 
			
		||||
      this.setState({ connected });
 | 
			
		||||
    this.setState({ connected: true });
 | 
			
		||||
 | 
			
		||||
    const checkWSConnection = new Promise<boolean>((resolve) => {
 | 
			
		||||
      this.props.monitorManagerProxy.onWSConnectionChanged((connected) => {
 | 
			
		||||
        this.setState({ connected });
 | 
			
		||||
        resolve(true);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.toDisposeBeforeUnmount.pushAll([
 | 
			
		||||
      this.props.serialConnection.onRead(({ messages }) => {
 | 
			
		||||
        if (
 | 
			
		||||
          messages.command ===
 | 
			
		||||
            SerialPlotter.Protocol.Command.MIDDLEWARE_CONFIG_CHANGED &&
 | 
			
		||||
          'connected' in messages.data
 | 
			
		||||
        ) {
 | 
			
		||||
          this.setState({ connected: messages.data.connected });
 | 
			
		||||
    const checkWSTimeout = timeout(1000).then(() => false);
 | 
			
		||||
 | 
			
		||||
    Promise.race<boolean>([checkWSConnection, checkWSTimeout]).then(
 | 
			
		||||
      async (resolved) => {
 | 
			
		||||
        if (!resolved) {
 | 
			
		||||
          const connected =
 | 
			
		||||
            await this.props.monitorManagerProxy.isWSConnected();
 | 
			
		||||
          this.setState({ connected });
 | 
			
		||||
        }
 | 
			
		||||
      }),
 | 
			
		||||
    ]);
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentWillUnmount(): void {
 | 
			
		||||
@@ -60,7 +67,7 @@ export class SerialMonitorSendInput extends React.Component<
 | 
			
		||||
      <input
 | 
			
		||||
        ref={this.setRef}
 | 
			
		||||
        type="text"
 | 
			
		||||
        className={`theia-input ${this.state.connected ? '' : 'warning'}`}
 | 
			
		||||
        className={`theia-input ${this.shouldShowWarning() ? 'warning' : ''}`}
 | 
			
		||||
        placeholder={this.placeholder}
 | 
			
		||||
        value={this.state.text}
 | 
			
		||||
        onChange={this.onChange}
 | 
			
		||||
@@ -69,15 +76,22 @@ export class SerialMonitorSendInput extends React.Component<
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected shouldShowWarning(): boolean {
 | 
			
		||||
    const board = this.props.boardsServiceProvider.boardsConfig.selectedBoard;
 | 
			
		||||
    const port = this.props.boardsServiceProvider.boardsConfig.selectedPort;
 | 
			
		||||
    return !this.state.connected || !board || !port;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected get placeholder(): string {
 | 
			
		||||
    const serialConfig = this.props.serialConnection.getConfig();
 | 
			
		||||
    if (!this.state.connected || !serialConfig) {
 | 
			
		||||
    if (this.shouldShowWarning()) {
 | 
			
		||||
      return nls.localize(
 | 
			
		||||
        'arduino/serial/notConnected',
 | 
			
		||||
        'Not connected. Select a board and a port to connect automatically.'
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    const { board, port } = serialConfig;
 | 
			
		||||
 | 
			
		||||
    const board = this.props.boardsServiceProvider.boardsConfig.selectedBoard;
 | 
			
		||||
    const port = this.props.boardsServiceProvider.boardsConfig.selectedPort;
 | 
			
		||||
    return nls.localize(
 | 
			
		||||
      'arduino/serial/message',
 | 
			
		||||
      "Message ({0} + Enter to send message to '{1}' on '{2}')",
 | 
			
		||||
 
 | 
			
		||||
@@ -2,10 +2,10 @@ import * as React from 'react';
 | 
			
		||||
import { Event } from '@theia/core/lib/common/event';
 | 
			
		||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
 | 
			
		||||
import { areEqual, FixedSizeList as List } from 'react-window';
 | 
			
		||||
import { SerialModel } from '../serial-model';
 | 
			
		||||
import { SerialConnectionManager } from '../serial-connection-manager';
 | 
			
		||||
import dateFormat = require('dateformat');
 | 
			
		||||
import { messagesToLines, truncateLines } from './monitor-utils';
 | 
			
		||||
import { MonitorManagerProxyClient } from '../../../common/protocol';
 | 
			
		||||
import { MonitorModel } from '../../monitor-model';
 | 
			
		||||
 | 
			
		||||
export type Line = { message: string; timestamp?: Date; lineLen: number };
 | 
			
		||||
 | 
			
		||||
@@ -24,7 +24,7 @@ export class SerialMonitorOutput extends React.Component<
 | 
			
		||||
    this.listRef = React.createRef();
 | 
			
		||||
    this.state = {
 | 
			
		||||
      lines: [],
 | 
			
		||||
      timestamp: this.props.serialModel.timestamp,
 | 
			
		||||
      timestamp: this.props.monitorModel.timestamp,
 | 
			
		||||
      charCount: 0,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
@@ -58,14 +58,13 @@ export class SerialMonitorOutput extends React.Component<
 | 
			
		||||
  componentDidMount(): void {
 | 
			
		||||
    this.scrollToBottom();
 | 
			
		||||
    this.toDisposeBeforeUnmount.pushAll([
 | 
			
		||||
      this.props.serialConnection.onRead(({ messages }) => {
 | 
			
		||||
      this.props.monitorManagerProxy.onMessagesReceived(({ messages }) => {
 | 
			
		||||
        const [newLines, totalCharCount] = messagesToLines(
 | 
			
		||||
          messages,
 | 
			
		||||
          this.state.lines,
 | 
			
		||||
          this.state.charCount
 | 
			
		||||
        );
 | 
			
		||||
        const [lines, charCount] = truncateLines(newLines, totalCharCount);
 | 
			
		||||
 | 
			
		||||
        this.setState({
 | 
			
		||||
          lines,
 | 
			
		||||
          charCount,
 | 
			
		||||
@@ -75,9 +74,9 @@ export class SerialMonitorOutput extends React.Component<
 | 
			
		||||
      this.props.clearConsoleEvent(() =>
 | 
			
		||||
        this.setState({ lines: [], charCount: 0 })
 | 
			
		||||
      ),
 | 
			
		||||
      this.props.serialModel.onChange(({ property }) => {
 | 
			
		||||
      this.props.monitorModel.onChange(({ property }) => {
 | 
			
		||||
        if (property === 'timestamp') {
 | 
			
		||||
          const { timestamp } = this.props.serialModel;
 | 
			
		||||
          const { timestamp } = this.props.monitorModel;
 | 
			
		||||
          this.setState({ timestamp });
 | 
			
		||||
        }
 | 
			
		||||
        if (property === 'autoscroll') {
 | 
			
		||||
@@ -93,7 +92,7 @@ export class SerialMonitorOutput extends React.Component<
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  scrollToBottom = ((): void => {
 | 
			
		||||
    if (this.listRef.current && this.props.serialModel.autoscroll) {
 | 
			
		||||
    if (this.listRef.current && this.props.monitorModel.autoscroll) {
 | 
			
		||||
      this.listRef.current.scrollToItem(this.state.lines.length, 'end');
 | 
			
		||||
    }
 | 
			
		||||
  }).bind(this);
 | 
			
		||||
@@ -128,8 +127,8 @@ const Row = React.memo(_Row, areEqual);
 | 
			
		||||
 | 
			
		||||
export namespace SerialMonitorOutput {
 | 
			
		||||
  export interface Props {
 | 
			
		||||
    readonly serialModel: SerialModel;
 | 
			
		||||
    readonly serialConnection: SerialConnectionManager;
 | 
			
		||||
    readonly monitorModel: MonitorModel;
 | 
			
		||||
    readonly monitorManagerProxy: MonitorManagerProxyClient;
 | 
			
		||||
    readonly clearConsoleEvent: Event<void>;
 | 
			
		||||
    readonly height: number;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -6,15 +6,14 @@ import {
 | 
			
		||||
  MaybePromise,
 | 
			
		||||
  MenuModelRegistry,
 | 
			
		||||
} from '@theia/core';
 | 
			
		||||
import { SerialModel } from '../serial-model';
 | 
			
		||||
import { ArduinoMenus } from '../../menu/arduino-menus';
 | 
			
		||||
import { Contribution } from '../../contributions/contribution';
 | 
			
		||||
import { Endpoint, FrontendApplication } from '@theia/core/lib/browser';
 | 
			
		||||
import { ipcRenderer } from '@theia/electron/shared/electron';
 | 
			
		||||
import { SerialConfig } from '../../../common/protocol';
 | 
			
		||||
import { SerialConnectionManager } from '../serial-connection-manager';
 | 
			
		||||
import { MonitorManagerProxyClient } from '../../../common/protocol';
 | 
			
		||||
import { SerialPlotter } from './protocol';
 | 
			
		||||
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
 | 
			
		||||
import { MonitorModel } from '../../monitor-model';
 | 
			
		||||
const queryString = require('query-string');
 | 
			
		||||
 | 
			
		||||
export namespace SerialPlotterContribution {
 | 
			
		||||
@@ -33,14 +32,14 @@ export class PlotterFrontendContribution extends Contribution {
 | 
			
		||||
  protected url: string;
 | 
			
		||||
  protected wsPort: number;
 | 
			
		||||
 | 
			
		||||
  @inject(SerialModel)
 | 
			
		||||
  protected readonly model: SerialModel;
 | 
			
		||||
  @inject(MonitorModel)
 | 
			
		||||
  protected readonly model: MonitorModel;
 | 
			
		||||
 | 
			
		||||
  @inject(ThemeService)
 | 
			
		||||
  protected readonly themeService: ThemeService;
 | 
			
		||||
 | 
			
		||||
  @inject(SerialConnectionManager)
 | 
			
		||||
  protected readonly serialConnection: SerialConnectionManager;
 | 
			
		||||
  @inject(MonitorManagerProxyClient)
 | 
			
		||||
  protected readonly monitorManagerProxy: MonitorManagerProxyClient;
 | 
			
		||||
 | 
			
		||||
  @inject(BoardsServiceProvider)
 | 
			
		||||
  protected readonly boardsServiceProvider: BoardsServiceProvider;
 | 
			
		||||
@@ -75,7 +74,7 @@ export class PlotterFrontendContribution extends Contribution {
 | 
			
		||||
      this.window.focus();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const wsPort = this.serialConnection.getWsPort();
 | 
			
		||||
    const wsPort = this.monitorManagerProxy.getWebSocketPort();
 | 
			
		||||
    if (wsPort) {
 | 
			
		||||
      this.open(wsPort);
 | 
			
		||||
    } else {
 | 
			
		||||
@@ -84,14 +83,28 @@ export class PlotterFrontendContribution extends Contribution {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected async open(wsPort: number): Promise<void> {
 | 
			
		||||
    const board = this.boardsServiceProvider.boardsConfig.selectedBoard;
 | 
			
		||||
    const port = this.boardsServiceProvider.boardsConfig.selectedPort;
 | 
			
		||||
    let baudrates: number[] = [];
 | 
			
		||||
    let currentBaudrate = -1;
 | 
			
		||||
    if (board && port) {
 | 
			
		||||
      const { pluggableMonitorSettings } =
 | 
			
		||||
        this.monitorManagerProxy.getCurrentSettings(board, port);
 | 
			
		||||
      if (pluggableMonitorSettings && 'baudrate' in pluggableMonitorSettings) {
 | 
			
		||||
        // Convert from string to numbers
 | 
			
		||||
        baudrates = pluggableMonitorSettings['baudrate'].values.map((b) => +b);
 | 
			
		||||
        currentBaudrate = +pluggableMonitorSettings['baudrate'].selectedValue;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const initConfig: Partial<SerialPlotter.Config> = {
 | 
			
		||||
      baudrates: SerialConfig.BaudRates.map((b) => b),
 | 
			
		||||
      currentBaudrate: this.model.baudRate,
 | 
			
		||||
      baudrates,
 | 
			
		||||
      currentBaudrate,
 | 
			
		||||
      currentLineEnding: this.model.lineEnding,
 | 
			
		||||
      darkTheme: this.themeService.getCurrentTheme().type === 'dark',
 | 
			
		||||
      wsPort,
 | 
			
		||||
      interpolate: this.model.interpolate,
 | 
			
		||||
      connected: await this.serialConnection.isBESerialConnected(),
 | 
			
		||||
      connected: await this.monitorManagerProxy.isWSConnected(),
 | 
			
		||||
      serialPort: this.boardsServiceProvider.boardsConfig.selectedPort?.address,
 | 
			
		||||
    };
 | 
			
		||||
    const urlWithParams = queryString.stringifyUrl(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,360 +0,0 @@
 | 
			
		||||
import { injectable, inject } from 'inversify';
 | 
			
		||||
import { Emitter, Event } from '@theia/core/lib/common/event';
 | 
			
		||||
import { MessageService } from '@theia/core/lib/common/message-service';
 | 
			
		||||
import {
 | 
			
		||||
  SerialService,
 | 
			
		||||
  SerialConfig,
 | 
			
		||||
  SerialError,
 | 
			
		||||
  Status,
 | 
			
		||||
  SerialServiceClient,
 | 
			
		||||
} from '../../common/protocol/serial-service';
 | 
			
		||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
 | 
			
		||||
import {
 | 
			
		||||
  Board,
 | 
			
		||||
  BoardsService,
 | 
			
		||||
} from '../../common/protocol/boards-service';
 | 
			
		||||
import { BoardsConfig } from '../boards/boards-config';
 | 
			
		||||
import { SerialModel } from './serial-model';
 | 
			
		||||
import { ThemeService } from '@theia/core/lib/browser/theming';
 | 
			
		||||
import { CoreService } from '../../common/protocol';
 | 
			
		||||
import { nls } from '@theia/core/lib/common/nls';
 | 
			
		||||
 | 
			
		||||
@injectable()
 | 
			
		||||
export class SerialConnectionManager {
 | 
			
		||||
  protected config: Partial<SerialConfig> = {
 | 
			
		||||
    board: undefined,
 | 
			
		||||
    port: undefined,
 | 
			
		||||
    baudRate: undefined,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  protected readonly onConnectionChangedEmitter = new Emitter<boolean>();
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * This emitter forwards all read events **if** the connection is established.
 | 
			
		||||
   */
 | 
			
		||||
  protected readonly onReadEmitter = new Emitter<{ messages: string[] }>();
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Array for storing previous serial errors received from the server, and based on the number of elements in this array,
 | 
			
		||||
   * we adjust the reconnection delay.
 | 
			
		||||
   * Super naive way: we wait `array.length * 1000` ms. Once we hit 10 errors, we do not try to reconnect and clean the array.
 | 
			
		||||
   */
 | 
			
		||||
  protected serialErrors: SerialError[] = [];
 | 
			
		||||
  protected reconnectTimeout?: number;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * When the websocket server is up on the backend, we save the port here, so that the client knows how to connect to it
 | 
			
		||||
   * */
 | 
			
		||||
  protected wsPort?: number;
 | 
			
		||||
  protected webSocket?: WebSocket;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    @inject(SerialModel) protected readonly serialModel: SerialModel,
 | 
			
		||||
    @inject(SerialService) protected readonly serialService: SerialService,
 | 
			
		||||
    @inject(SerialServiceClient)
 | 
			
		||||
    protected readonly serialServiceClient: SerialServiceClient,
 | 
			
		||||
    @inject(BoardsService) protected readonly boardsService: BoardsService,
 | 
			
		||||
    @inject(BoardsServiceProvider)
 | 
			
		||||
    protected readonly boardsServiceProvider: BoardsServiceProvider,
 | 
			
		||||
    @inject(MessageService) protected messageService: MessageService,
 | 
			
		||||
    @inject(ThemeService) protected readonly themeService: ThemeService,
 | 
			
		||||
    @inject(CoreService) protected readonly core: CoreService,
 | 
			
		||||
    @inject(BoardsServiceProvider)
 | 
			
		||||
    protected readonly boardsServiceClientImpl: BoardsServiceProvider
 | 
			
		||||
  ) {
 | 
			
		||||
    this.serialServiceClient.onWebSocketChanged(
 | 
			
		||||
      this.handleWebSocketChanged.bind(this)
 | 
			
		||||
    );
 | 
			
		||||
    this.serialServiceClient.onBaudRateChanged((baudRate) => {
 | 
			
		||||
      if (this.serialModel.baudRate !== baudRate) {
 | 
			
		||||
        this.serialModel.baudRate = baudRate;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    this.serialServiceClient.onLineEndingChanged((lineending) => {
 | 
			
		||||
      if (this.serialModel.lineEnding !== lineending) {
 | 
			
		||||
        this.serialModel.lineEnding = lineending;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    this.serialServiceClient.onInterpolateChanged((interpolate) => {
 | 
			
		||||
      if (this.serialModel.interpolate !== interpolate) {
 | 
			
		||||
        this.serialModel.interpolate = interpolate;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.serialServiceClient.onError(this.handleError.bind(this));
 | 
			
		||||
    this.boardsServiceProvider.onBoardsConfigChanged(
 | 
			
		||||
      this.handleBoardConfigChange.bind(this)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Handles the `baudRate` changes by reconnecting if required.
 | 
			
		||||
    this.serialModel.onChange(async ({ property }) => {
 | 
			
		||||
      if (
 | 
			
		||||
        property === 'baudRate' &&
 | 
			
		||||
        (await this.serialService.isSerialPortOpen())
 | 
			
		||||
      ) {
 | 
			
		||||
        const { boardsConfig } = this.boardsServiceProvider;
 | 
			
		||||
        this.handleBoardConfigChange(boardsConfig);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // update the current values in the backend and propagate to websocket clients
 | 
			
		||||
      this.serialService.updateWsConfigParam({
 | 
			
		||||
        ...(property === 'lineEnding' && {
 | 
			
		||||
          currentLineEnding: this.serialModel.lineEnding,
 | 
			
		||||
        }),
 | 
			
		||||
        ...(property === 'interpolate' && {
 | 
			
		||||
          interpolate: this.serialModel.interpolate,
 | 
			
		||||
        }),
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.themeService.onDidColorThemeChange((theme) => {
 | 
			
		||||
      this.serialService.updateWsConfigParam({
 | 
			
		||||
        darkTheme: theme.newTheme.type === 'dark',
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Updated the config in the BE passing only the properties that has changed.
 | 
			
		||||
   * BE will create a new connection if needed.
 | 
			
		||||
   *
 | 
			
		||||
   * @param newConfig the porperties of the config that has changed
 | 
			
		||||
   */
 | 
			
		||||
  async setConfig(newConfig: Partial<SerialConfig>): Promise<void> {
 | 
			
		||||
    let configHasChanged = false;
 | 
			
		||||
    Object.keys(this.config).forEach((key: keyof SerialConfig) => {
 | 
			
		||||
      if (newConfig[key] !== this.config[key]) {
 | 
			
		||||
        configHasChanged = true;
 | 
			
		||||
        this.config = { ...this.config, [key]: newConfig[key] };
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (configHasChanged) {
 | 
			
		||||
      this.serialService.updateWsConfigParam({
 | 
			
		||||
        currentBaudrate: this.config.baudRate,
 | 
			
		||||
        serialPort: this.config.port?.address,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (isSerialConfig(this.config)) {
 | 
			
		||||
        this.serialService.setSerialConfig(this.config);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getConfig(): Partial<SerialConfig> {
 | 
			
		||||
    return this.config;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getWsPort(): number | undefined {
 | 
			
		||||
    return this.wsPort;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected handleWebSocketChanged(wsPort: number): void {
 | 
			
		||||
    this.wsPort = wsPort;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get serialConfig(): SerialConfig | undefined {
 | 
			
		||||
    return isSerialConfig(this.config)
 | 
			
		||||
      ? (this.config as SerialConfig)
 | 
			
		||||
      : undefined;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async isBESerialConnected(): Promise<boolean> {
 | 
			
		||||
    return await this.serialService.isSerialPortOpen();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  openWSToBE(): void {
 | 
			
		||||
    if (!isSerialConfig(this.config)) {
 | 
			
		||||
      this.messageService.error(
 | 
			
		||||
        `Please select a board and a port to open the serial connection.`
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!this.webSocket && this.wsPort) {
 | 
			
		||||
      try {
 | 
			
		||||
        this.webSocket = new WebSocket(`ws://localhost:${this.wsPort}`);
 | 
			
		||||
        this.webSocket.onmessage = (res) => {
 | 
			
		||||
          const messages = JSON.parse(res.data);
 | 
			
		||||
          this.onReadEmitter.fire({ messages });
 | 
			
		||||
        };
 | 
			
		||||
      } catch {
 | 
			
		||||
        this.messageService.error(`Unable to connect to websocket`);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  closeWStoBE(): void {
 | 
			
		||||
    if (this.webSocket) {
 | 
			
		||||
      try {
 | 
			
		||||
        this.webSocket.close();
 | 
			
		||||
        this.webSocket = undefined;
 | 
			
		||||
      } catch {
 | 
			
		||||
        this.messageService.error(`Unable to close websocket`);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Handles error on the SerialServiceClient and try to reconnect, eventually
 | 
			
		||||
   */
 | 
			
		||||
  async handleError(error: SerialError): Promise<void> {
 | 
			
		||||
    if (!(await this.serialService.isSerialPortOpen())) return;
 | 
			
		||||
    const { code, config } = error;
 | 
			
		||||
    const { board, port } = config;
 | 
			
		||||
    const options = { timeout: 3000 };
 | 
			
		||||
    switch (code) {
 | 
			
		||||
      case SerialError.ErrorCodes.CLIENT_CANCEL: {
 | 
			
		||||
        console.debug(
 | 
			
		||||
          `Serial connection was canceled by client: ${Serial.Config.toString(
 | 
			
		||||
            this.config
 | 
			
		||||
          )}.`
 | 
			
		||||
        );
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      case SerialError.ErrorCodes.DEVICE_BUSY: {
 | 
			
		||||
        this.messageService.warn(
 | 
			
		||||
          nls.localize(
 | 
			
		||||
            'arduino/serial/connectionBusy',
 | 
			
		||||
            'Connection failed. Serial port is busy: {0}',
 | 
			
		||||
            port.address
 | 
			
		||||
          ),
 | 
			
		||||
          options
 | 
			
		||||
        );
 | 
			
		||||
        this.serialErrors.push(error);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      case SerialError.ErrorCodes.DEVICE_NOT_CONFIGURED: {
 | 
			
		||||
        this.messageService.info(
 | 
			
		||||
          nls.localize(
 | 
			
		||||
            'arduino/serial/disconnected',
 | 
			
		||||
            'Disconnected {0} from {1}.',
 | 
			
		||||
            Board.toString(board, {
 | 
			
		||||
              useFqbn: false,
 | 
			
		||||
            }),
 | 
			
		||||
            port.address
 | 
			
		||||
          ),
 | 
			
		||||
          options
 | 
			
		||||
        );
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      case undefined: {
 | 
			
		||||
        this.messageService.error(
 | 
			
		||||
          nls.localize(
 | 
			
		||||
            'arduino/serial/unexpectedError',
 | 
			
		||||
            'Unexpected error. Reconnecting {0} on port {1}.',
 | 
			
		||||
            Board.toString(board),
 | 
			
		||||
            port.address
 | 
			
		||||
          ),
 | 
			
		||||
          options
 | 
			
		||||
        );
 | 
			
		||||
        console.error(JSON.stringify(error));
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if ((await this.serialService.clientsAttached()) > 0) {
 | 
			
		||||
      if (this.serialErrors.length >= 10) {
 | 
			
		||||
        this.messageService.warn(
 | 
			
		||||
          nls.localize(
 | 
			
		||||
            'arduino/serial/failedReconnect',
 | 
			
		||||
            'Failed to reconnect {0} to serial port after 10 consecutive attempts. The {1} serial port is busy.',
 | 
			
		||||
            Board.toString(board, {
 | 
			
		||||
              useFqbn: false,
 | 
			
		||||
            }),
 | 
			
		||||
            port.address
 | 
			
		||||
          )
 | 
			
		||||
        );
 | 
			
		||||
        this.serialErrors.length = 0;
 | 
			
		||||
      } else {
 | 
			
		||||
        const attempts = this.serialErrors.length || 1;
 | 
			
		||||
        if (this.reconnectTimeout !== undefined) {
 | 
			
		||||
          // Clear the previous timer.
 | 
			
		||||
          window.clearTimeout(this.reconnectTimeout);
 | 
			
		||||
        }
 | 
			
		||||
        const timeout = attempts * 1000;
 | 
			
		||||
        this.messageService.warn(
 | 
			
		||||
          nls.localize(
 | 
			
		||||
            'arduino/serial/reconnect',
 | 
			
		||||
            'Reconnecting {0} to {1} in {2} seconds...',
 | 
			
		||||
            Board.toString(board, {
 | 
			
		||||
              useFqbn: false,
 | 
			
		||||
            }),
 | 
			
		||||
            port.address,
 | 
			
		||||
            attempts.toString()
 | 
			
		||||
          )
 | 
			
		||||
        );
 | 
			
		||||
        this.reconnectTimeout = window.setTimeout(
 | 
			
		||||
          () => this.reconnectAfterUpload(),
 | 
			
		||||
          timeout
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async reconnectAfterUpload(): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      if (isSerialConfig(this.config)) {
 | 
			
		||||
        await this.boardsServiceClientImpl.waitUntilAvailable(
 | 
			
		||||
          Object.assign(this.config.board, { port: this.config.port }),
 | 
			
		||||
          10_000
 | 
			
		||||
        );
 | 
			
		||||
        this.serialService.connectSerialIfRequired();
 | 
			
		||||
      }
 | 
			
		||||
    } catch (waitError) {
 | 
			
		||||
      this.messageService.error(
 | 
			
		||||
        nls.localize(
 | 
			
		||||
          'arduino/sketch/couldNotConnectToSerial',
 | 
			
		||||
          'Could not reconnect to serial port. {0}',
 | 
			
		||||
          waitError.toString()
 | 
			
		||||
        )
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Sends the data to the connected serial port.
 | 
			
		||||
   * The desired EOL is appended to `data`, you do not have to add it.
 | 
			
		||||
   * It is a NOOP if connected.
 | 
			
		||||
   */
 | 
			
		||||
  async send(data: string): Promise<Status> {
 | 
			
		||||
    if (!(await this.serialService.isSerialPortOpen())) {
 | 
			
		||||
      return Status.NOT_CONNECTED;
 | 
			
		||||
    }
 | 
			
		||||
    return new Promise<Status>((resolve) => {
 | 
			
		||||
      this.serialService
 | 
			
		||||
        .sendMessageToSerial(data + this.serialModel.lineEnding)
 | 
			
		||||
        .then(() => resolve(Status.OK));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get onConnectionChanged(): Event<boolean> {
 | 
			
		||||
    return this.onConnectionChangedEmitter.event;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get onRead(): Event<{ messages: any }> {
 | 
			
		||||
    return this.onReadEmitter.event;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected async handleBoardConfigChange(
 | 
			
		||||
    boardsConfig: BoardsConfig.Config
 | 
			
		||||
  ): Promise<void> {
 | 
			
		||||
    const { selectedBoard: board, selectedPort: port } = boardsConfig;
 | 
			
		||||
    const { baudRate } = this.serialModel;
 | 
			
		||||
    const newConfig: Partial<SerialConfig> = { board, port, baudRate };
 | 
			
		||||
    this.setConfig(newConfig);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export namespace Serial {
 | 
			
		||||
  export namespace Config {
 | 
			
		||||
    export function toString(config: Partial<SerialConfig>): string {
 | 
			
		||||
      if (!isSerialConfig(config)) return '';
 | 
			
		||||
      const { board, port } = config;
 | 
			
		||||
      return `${Board.toString(board)} ${port.address}`;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isSerialConfig(config: Partial<SerialConfig>): config is SerialConfig {
 | 
			
		||||
  return !!config.board && !!config.baudRate && !!config.port;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,48 +0,0 @@
 | 
			
		||||
import { injectable } from 'inversify';
 | 
			
		||||
import { Emitter } from '@theia/core/lib/common/event';
 | 
			
		||||
import {
 | 
			
		||||
  SerialServiceClient,
 | 
			
		||||
  SerialError,
 | 
			
		||||
  SerialConfig,
 | 
			
		||||
} from '../../common/protocol/serial-service';
 | 
			
		||||
import { SerialModel } from './serial-model';
 | 
			
		||||
 | 
			
		||||
@injectable()
 | 
			
		||||
export class SerialServiceClientImpl implements SerialServiceClient {
 | 
			
		||||
  protected readonly onErrorEmitter = new Emitter<SerialError>();
 | 
			
		||||
  readonly onError = this.onErrorEmitter.event;
 | 
			
		||||
 | 
			
		||||
  protected readonly onWebSocketChangedEmitter = new Emitter<number>();
 | 
			
		||||
  readonly onWebSocketChanged = this.onWebSocketChangedEmitter.event;
 | 
			
		||||
 | 
			
		||||
  protected readonly onBaudRateChangedEmitter =
 | 
			
		||||
    new Emitter<SerialConfig.BaudRate>();
 | 
			
		||||
  readonly onBaudRateChanged = this.onBaudRateChangedEmitter.event;
 | 
			
		||||
 | 
			
		||||
  protected readonly onLineEndingChangedEmitter =
 | 
			
		||||
    new Emitter<SerialModel.EOL>();
 | 
			
		||||
  readonly onLineEndingChanged = this.onLineEndingChangedEmitter.event;
 | 
			
		||||
 | 
			
		||||
  protected readonly onInterpolateChangedEmitter = new Emitter<boolean>();
 | 
			
		||||
  readonly onInterpolateChanged = this.onInterpolateChangedEmitter.event;
 | 
			
		||||
 | 
			
		||||
  notifyError(error: SerialError): void {
 | 
			
		||||
    this.onErrorEmitter.fire(error);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  notifyWebSocketChanged(message: number): void {
 | 
			
		||||
    this.onWebSocketChangedEmitter.fire(message);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  notifyBaudRateChanged(message: SerialConfig.BaudRate): void {
 | 
			
		||||
    this.onBaudRateChangedEmitter.fire(message);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  notifyLineEndingChanged(message: SerialModel.EOL): void {
 | 
			
		||||
    this.onLineEndingChangedEmitter.fire(message);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  notifyInterpolateChanged(message: boolean): void {
 | 
			
		||||
    this.onInterpolateChangedEmitter.fire(message);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
import { Port } from "./boards-service";
 | 
			
		||||
 | 
			
		||||
export const ArduinoFirmwareUploaderPath =
 | 
			
		||||
  '/services/arduino-firmware-uploader';
 | 
			
		||||
export const ArduinoFirmwareUploader = Symbol('ArduinoFirmwareUploader');
 | 
			
		||||
@@ -10,7 +12,7 @@ export type FirmwareInfo = {
 | 
			
		||||
};
 | 
			
		||||
export interface ArduinoFirmwareUploader {
 | 
			
		||||
  list(fqbn?: string): Promise<FirmwareInfo[]>;
 | 
			
		||||
  flash(firmware: FirmwareInfo, port: string): Promise<string>;
 | 
			
		||||
  flash(firmware: FirmwareInfo, port: Port): Promise<string>;
 | 
			
		||||
  uploadCertificates(command: string): Promise<any>;
 | 
			
		||||
  updatableBoards(): Promise<string[]>;
 | 
			
		||||
  availableFirmwares(fqbn: string): Promise<FirmwareInfo[]>;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { BoardUserField } from '.';
 | 
			
		||||
import { Port } from '../../common/protocol/boards-service';
 | 
			
		||||
import { Board, Port } from '../../common/protocol/boards-service';
 | 
			
		||||
import { Programmer } from './boards-service';
 | 
			
		||||
 | 
			
		||||
export const CompilerWarningLiterals = [
 | 
			
		||||
@@ -33,7 +33,7 @@ export namespace CoreService {
 | 
			
		||||
       * `file` URI to the sketch folder.
 | 
			
		||||
       */
 | 
			
		||||
      readonly sketchUri: string;
 | 
			
		||||
      readonly fqbn?: string | undefined;
 | 
			
		||||
      readonly board?: Board;
 | 
			
		||||
      readonly optimizeForDebug: boolean;
 | 
			
		||||
      readonly verbose: boolean;
 | 
			
		||||
      readonly sourceOverride: Record<string, string>;
 | 
			
		||||
@@ -42,7 +42,7 @@ export namespace CoreService {
 | 
			
		||||
 | 
			
		||||
  export namespace Upload {
 | 
			
		||||
    export interface Options extends Compile.Options {
 | 
			
		||||
      readonly port?: Port | undefined;
 | 
			
		||||
      readonly port?: Port;
 | 
			
		||||
      readonly programmer?: Programmer | undefined;
 | 
			
		||||
      readonly verify: boolean;
 | 
			
		||||
      readonly userFields: BoardUserField[];
 | 
			
		||||
@@ -51,8 +51,8 @@ export namespace CoreService {
 | 
			
		||||
 | 
			
		||||
  export namespace Bootloader {
 | 
			
		||||
    export interface Options {
 | 
			
		||||
      readonly fqbn?: string | undefined;
 | 
			
		||||
      readonly port?: Port | undefined;
 | 
			
		||||
      readonly board?: Board;
 | 
			
		||||
      readonly port?: Port;
 | 
			
		||||
      readonly programmer?: Programmer | undefined;
 | 
			
		||||
      readonly verbose: boolean;
 | 
			
		||||
      readonly verify: boolean;
 | 
			
		||||
 
 | 
			
		||||
@@ -6,10 +6,10 @@ export * from './core-service';
 | 
			
		||||
export * from './filesystem-ext';
 | 
			
		||||
export * from './installable';
 | 
			
		||||
export * from './library-service';
 | 
			
		||||
export * from './serial-service';
 | 
			
		||||
export * from './searchable';
 | 
			
		||||
export * from './sketches-service';
 | 
			
		||||
export * from './examples-service';
 | 
			
		||||
export * from './executable-service';
 | 
			
		||||
export * from './response-service';
 | 
			
		||||
export * from './notification-service';
 | 
			
		||||
export * from './monitor-service';
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										92
									
								
								arduino-ide-extension/src/common/protocol/monitor-service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								arduino-ide-extension/src/common/protocol/monitor-service.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,92 @@
 | 
			
		||||
import { Event, JsonRpcServer } from '@theia/core';
 | 
			
		||||
import {
 | 
			
		||||
  PluggableMonitorSettings,
 | 
			
		||||
  MonitorSettings,
 | 
			
		||||
} from '../../node/monitor-settings/monitor-settings-provider';
 | 
			
		||||
import { Board, Port } from './boards-service';
 | 
			
		||||
 | 
			
		||||
export const MonitorManagerProxyFactory = Symbol('MonitorManagerProxyFactory');
 | 
			
		||||
export type MonitorManagerProxyFactory = () => MonitorManagerProxy;
 | 
			
		||||
 | 
			
		||||
export const MonitorManagerProxyPath = '/services/monitor-manager-proxy';
 | 
			
		||||
export const MonitorManagerProxy = Symbol('MonitorManagerProxy');
 | 
			
		||||
export interface MonitorManagerProxy
 | 
			
		||||
  extends JsonRpcServer<MonitorManagerProxyClient> {
 | 
			
		||||
  startMonitor(
 | 
			
		||||
    board: Board,
 | 
			
		||||
    port: Port,
 | 
			
		||||
    settings?: PluggableMonitorSettings
 | 
			
		||||
  ): Promise<void>;
 | 
			
		||||
  changeMonitorSettings(
 | 
			
		||||
    board: Board,
 | 
			
		||||
    port: Port,
 | 
			
		||||
    settings: PluggableMonitorSettings
 | 
			
		||||
  ): Promise<void>;
 | 
			
		||||
  stopMonitor(board: Board, port: Port): Promise<void>;
 | 
			
		||||
  getCurrentSettings(board: Board, port: Port): PluggableMonitorSettings;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const MonitorManagerProxyClient = Symbol('MonitorManagerProxyClient');
 | 
			
		||||
export interface MonitorManagerProxyClient {
 | 
			
		||||
  onMessagesReceived: Event<{ messages: string[] }>;
 | 
			
		||||
  onWSConnectionChanged: Event<boolean>;
 | 
			
		||||
  connect(addressPort: number): void;
 | 
			
		||||
  disconnect(): void;
 | 
			
		||||
  getWebSocketPort(): number | undefined;
 | 
			
		||||
  isWSConnected(): Promise<boolean>;
 | 
			
		||||
  startMonitor(
 | 
			
		||||
    board: Board,
 | 
			
		||||
    port: Port,
 | 
			
		||||
    settings?: PluggableMonitorSettings
 | 
			
		||||
  ): Promise<void>;
 | 
			
		||||
  getCurrentSettings(board: Board, port: Port): MonitorSettings;
 | 
			
		||||
  send(message: string): void;
 | 
			
		||||
  changeSettings(settings: MonitorSettings): void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface PluggableMonitorSetting {
 | 
			
		||||
  // The setting identifier
 | 
			
		||||
  readonly id: string;
 | 
			
		||||
  // A human-readable label of the setting (to be displayed on the GUI)
 | 
			
		||||
  readonly label: string;
 | 
			
		||||
  // The setting type (at the moment only "enum" is avaiable)
 | 
			
		||||
  readonly type: string;
 | 
			
		||||
  // The values allowed on "enum" types
 | 
			
		||||
  readonly values: string[];
 | 
			
		||||
  // The selected value
 | 
			
		||||
  selectedValue: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export namespace Monitor {
 | 
			
		||||
  export enum Command {
 | 
			
		||||
    SEND_MESSAGE = 'MONITOR_SEND_MESSAGE',
 | 
			
		||||
    CHANGE_SETTINGS = 'MONITOR_CHANGE_SETTINGS',
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export type Message = {
 | 
			
		||||
    command: Monitor.Command;
 | 
			
		||||
    data: string;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Status {}
 | 
			
		||||
export type OK = Status;
 | 
			
		||||
export interface ErrorStatus extends Status {
 | 
			
		||||
  readonly message: string;
 | 
			
		||||
}
 | 
			
		||||
export namespace Status {
 | 
			
		||||
  export function isOK(status: Status & { message?: string }): status is OK {
 | 
			
		||||
    return !!status && typeof status.message !== 'string';
 | 
			
		||||
  }
 | 
			
		||||
  export const OK: OK = {};
 | 
			
		||||
  export const NOT_CONNECTED: ErrorStatus = { message: 'Not connected.' };
 | 
			
		||||
  export const ALREADY_CONNECTED: ErrorStatus = {
 | 
			
		||||
    message: 'Already connected.',
 | 
			
		||||
  };
 | 
			
		||||
  export const CONFIG_MISSING: ErrorStatus = {
 | 
			
		||||
    message: 'Serial Config missing.',
 | 
			
		||||
  };
 | 
			
		||||
  export const UPLOAD_IN_PROGRESS: ErrorStatus = {
 | 
			
		||||
    message: 'Upload in progress.',
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
@@ -1,102 +0,0 @@
 | 
			
		||||
import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory';
 | 
			
		||||
import { Board, Port } from './boards-service';
 | 
			
		||||
import { Event } from '@theia/core/lib/common/event';
 | 
			
		||||
import { SerialPlotter } from '../../browser/serial/plotter/protocol';
 | 
			
		||||
import { SerialModel } from '../../browser/serial/serial-model';
 | 
			
		||||
 | 
			
		||||
export interface Status {}
 | 
			
		||||
export type OK = Status;
 | 
			
		||||
export interface ErrorStatus extends Status {
 | 
			
		||||
  readonly message: string;
 | 
			
		||||
}
 | 
			
		||||
export namespace Status {
 | 
			
		||||
  export function isOK(status: Status & { message?: string }): status is OK {
 | 
			
		||||
    return !!status && typeof status.message !== 'string';
 | 
			
		||||
  }
 | 
			
		||||
  export const OK: OK = {};
 | 
			
		||||
  export const NOT_CONNECTED: ErrorStatus = { message: 'Not connected.' };
 | 
			
		||||
  export const ALREADY_CONNECTED: ErrorStatus = {
 | 
			
		||||
    message: 'Already connected.',
 | 
			
		||||
  };
 | 
			
		||||
  export const CONFIG_MISSING: ErrorStatus = {
 | 
			
		||||
    message: 'Serial Config missing.',
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const SerialServicePath = '/services/serial';
 | 
			
		||||
export const SerialService = Symbol('SerialService');
 | 
			
		||||
export interface SerialService extends JsonRpcServer<SerialServiceClient> {
 | 
			
		||||
  clientsAttached(): Promise<number>;
 | 
			
		||||
  setSerialConfig(config: SerialConfig): Promise<void>;
 | 
			
		||||
  sendMessageToSerial(message: string): Promise<Status>;
 | 
			
		||||
  updateWsConfigParam(config: Partial<SerialPlotter.Config>): Promise<void>;
 | 
			
		||||
  isSerialPortOpen(): Promise<boolean>;
 | 
			
		||||
  connectSerialIfRequired(): Promise<void>;
 | 
			
		||||
  disconnect(reason?: SerialError): Promise<Status>;
 | 
			
		||||
  uploadInProgress: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface SerialConfig {
 | 
			
		||||
  readonly board: Board;
 | 
			
		||||
  readonly port: Port;
 | 
			
		||||
  /**
 | 
			
		||||
   * Defaults to [`SERIAL`](MonitorConfig#ConnectionType#SERIAL).
 | 
			
		||||
   */
 | 
			
		||||
  readonly type?: SerialConfig.ConnectionType;
 | 
			
		||||
  /**
 | 
			
		||||
   * Defaults to `9600`.
 | 
			
		||||
   */
 | 
			
		||||
  readonly baudRate?: SerialConfig.BaudRate;
 | 
			
		||||
}
 | 
			
		||||
export namespace SerialConfig {
 | 
			
		||||
  export const BaudRates = [
 | 
			
		||||
    300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200,
 | 
			
		||||
  ] as const;
 | 
			
		||||
  export type BaudRate = typeof SerialConfig.BaudRates[number];
 | 
			
		||||
  export namespace BaudRate {
 | 
			
		||||
    export const DEFAULT: BaudRate = 9600;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export enum ConnectionType {
 | 
			
		||||
    SERIAL = 0,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const SerialServiceClient = Symbol('SerialServiceClient');
 | 
			
		||||
export interface SerialServiceClient {
 | 
			
		||||
  onError: Event<SerialError>;
 | 
			
		||||
  onWebSocketChanged: Event<number>;
 | 
			
		||||
  onLineEndingChanged: Event<SerialModel.EOL>;
 | 
			
		||||
  onBaudRateChanged: Event<SerialConfig.BaudRate>;
 | 
			
		||||
  onInterpolateChanged: Event<boolean>;
 | 
			
		||||
  notifyError(event: SerialError): void;
 | 
			
		||||
  notifyWebSocketChanged(message: number): void;
 | 
			
		||||
  notifyLineEndingChanged(message: SerialModel.EOL): void;
 | 
			
		||||
  notifyBaudRateChanged(message: SerialConfig.BaudRate): void;
 | 
			
		||||
  notifyInterpolateChanged(message: boolean): void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface SerialError {
 | 
			
		||||
  readonly message: string;
 | 
			
		||||
  /**
 | 
			
		||||
   * If no `code` is available, clients must reestablish the serial connection.
 | 
			
		||||
   */
 | 
			
		||||
  readonly code: number | undefined;
 | 
			
		||||
  readonly config: SerialConfig;
 | 
			
		||||
}
 | 
			
		||||
export namespace SerialError {
 | 
			
		||||
  export namespace ErrorCodes {
 | 
			
		||||
    /**
 | 
			
		||||
     * The frontend has refreshed the browser, for instance.
 | 
			
		||||
     */
 | 
			
		||||
    export const CLIENT_CANCEL = 1;
 | 
			
		||||
    /**
 | 
			
		||||
     * When detaching a physical device when the duplex channel is still opened.
 | 
			
		||||
     */
 | 
			
		||||
    export const DEVICE_NOT_CONFIGURED = 2;
 | 
			
		||||
    /**
 | 
			
		||||
     * Another serial connection was opened on this port. For another electron-instance, Java IDE.
 | 
			
		||||
     */
 | 
			
		||||
    export const DEVICE_BUSY = 3;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -3,10 +3,10 @@ import {
 | 
			
		||||
  FirmwareInfo,
 | 
			
		||||
} from '../common/protocol/arduino-firmware-uploader';
 | 
			
		||||
import { injectable, inject, named } from 'inversify';
 | 
			
		||||
import { ExecutableService } from '../common/protocol';
 | 
			
		||||
import { SerialService } from '../common/protocol/serial-service';
 | 
			
		||||
import { ExecutableService, Port } from '../common/protocol';
 | 
			
		||||
import { getExecPath, spawnCommand } from './exec-util';
 | 
			
		||||
import { ILogger } from '@theia/core/lib/common/logger';
 | 
			
		||||
import { MonitorManager } from './monitor-manager';
 | 
			
		||||
 | 
			
		||||
@injectable()
 | 
			
		||||
export class ArduinoFirmwareUploaderImpl implements ArduinoFirmwareUploader {
 | 
			
		||||
@@ -19,8 +19,8 @@ export class ArduinoFirmwareUploaderImpl implements ArduinoFirmwareUploader {
 | 
			
		||||
  @named('fwuploader')
 | 
			
		||||
  protected readonly logger: ILogger;
 | 
			
		||||
 | 
			
		||||
  @inject(SerialService)
 | 
			
		||||
  protected readonly serialService: SerialService;
 | 
			
		||||
  @inject(MonitorManager)
 | 
			
		||||
  protected readonly monitorManager: MonitorManager;
 | 
			
		||||
 | 
			
		||||
  protected onError(error: any): void {
 | 
			
		||||
    this.logger.error(error);
 | 
			
		||||
@@ -69,26 +69,28 @@ export class ArduinoFirmwareUploaderImpl implements ArduinoFirmwareUploader {
 | 
			
		||||
    return await this.list(fqbn);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async flash(firmware: FirmwareInfo, port: string): Promise<string> {
 | 
			
		||||
  async flash(firmware: FirmwareInfo, port: Port): Promise<string> {
 | 
			
		||||
    let output;
 | 
			
		||||
    const board = {
 | 
			
		||||
      name: firmware.board_name,
 | 
			
		||||
      fqbn: firmware.board_fqbn,
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      this.serialService.uploadInProgress = true;
 | 
			
		||||
      await this.serialService.disconnect();
 | 
			
		||||
      this.monitorManager.notifyUploadStarted(board, port);
 | 
			
		||||
      output = await this.runCommand([
 | 
			
		||||
        'firmware',
 | 
			
		||||
        'flash',
 | 
			
		||||
        '--fqbn',
 | 
			
		||||
        firmware.board_fqbn,
 | 
			
		||||
        '--address',
 | 
			
		||||
        port,
 | 
			
		||||
        port.address,
 | 
			
		||||
        '--module',
 | 
			
		||||
        `${firmware.module}@${firmware.firmware_version}`,
 | 
			
		||||
      ]);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      throw e;
 | 
			
		||||
    } finally {
 | 
			
		||||
      this.serialService.uploadInProgress = false;
 | 
			
		||||
      this.serialService.connectSerialIfRequired();
 | 
			
		||||
      this.monitorManager.notifyUploadFinished(board, port);
 | 
			
		||||
      return output;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -40,16 +40,7 @@ import {
 | 
			
		||||
  ArduinoDaemon,
 | 
			
		||||
  ArduinoDaemonPath,
 | 
			
		||||
} from '../common/protocol/arduino-daemon';
 | 
			
		||||
import {
 | 
			
		||||
  SerialServiceImpl,
 | 
			
		||||
  SerialServiceName,
 | 
			
		||||
} from './serial/serial-service-impl';
 | 
			
		||||
import {
 | 
			
		||||
  SerialService,
 | 
			
		||||
  SerialServicePath,
 | 
			
		||||
  SerialServiceClient,
 | 
			
		||||
} from '../common/protocol/serial-service';
 | 
			
		||||
import { MonitorClientProvider } from './serial/monitor-client-provider';
 | 
			
		||||
 | 
			
		||||
import { ConfigServiceImpl } from './config-service-impl';
 | 
			
		||||
import { EnvVariablesServer as TheiaEnvVariablesServer } from '@theia/core/lib/common/env-variables';
 | 
			
		||||
import { EnvVariablesServer } from './theia/env-variables/env-variables-server';
 | 
			
		||||
@@ -90,10 +81,24 @@ import {
 | 
			
		||||
} from '../common/protocol/authentication-service';
 | 
			
		||||
import { ArduinoFirmwareUploaderImpl } from './arduino-firmware-uploader-impl';
 | 
			
		||||
import { PlotterBackendContribution } from './plotter/plotter-backend-contribution';
 | 
			
		||||
import WebSocketServiceImpl from './web-socket/web-socket-service-impl';
 | 
			
		||||
import { WebSocketService } from './web-socket/web-socket-service';
 | 
			
		||||
import { ArduinoLocalizationContribution } from './arduino-localization-contribution';
 | 
			
		||||
import { LocalizationContribution } from '@theia/core/lib/node/i18n/localization-contribution';
 | 
			
		||||
import { MonitorManagerProxyImpl } from './monitor-manager-proxy-impl';
 | 
			
		||||
import { MonitorManager, MonitorManagerName } from './monitor-manager';
 | 
			
		||||
import {
 | 
			
		||||
  MonitorManagerProxy,
 | 
			
		||||
  MonitorManagerProxyClient,
 | 
			
		||||
  MonitorManagerProxyPath,
 | 
			
		||||
} from '../common/protocol/monitor-service';
 | 
			
		||||
import { MonitorService, MonitorServiceName } from './monitor-service';
 | 
			
		||||
import { MonitorSettingsProvider } from './monitor-settings/monitor-settings-provider';
 | 
			
		||||
import { MonitorSettingsProviderImpl } from './monitor-settings/monitor-settings-provider-impl';
 | 
			
		||||
import {
 | 
			
		||||
  MonitorServiceFactory,
 | 
			
		||||
  MonitorServiceFactoryOptions,
 | 
			
		||||
} from './monitor-service-factory';
 | 
			
		||||
import WebSocketProviderImpl from './web-socket/web-socket-provider-impl';
 | 
			
		||||
import { WebSocketProvider } from './web-socket/web-socket-provider';
 | 
			
		||||
 | 
			
		||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
 | 
			
		||||
  bind(BackendApplication).toSelf().inSingletonScope();
 | 
			
		||||
@@ -177,9 +182,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
 | 
			
		||||
    })
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  // Shared WebSocketService for the backend. This will manage all websocket conenctions
 | 
			
		||||
  bind(WebSocketService).to(WebSocketServiceImpl).inSingletonScope();
 | 
			
		||||
 | 
			
		||||
  // Shared Arduino core client provider service for the backend.
 | 
			
		||||
  bind(CoreClientProvider).toSelf().inSingletonScope();
 | 
			
		||||
 | 
			
		||||
@@ -205,19 +207,57 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
 | 
			
		||||
 | 
			
		||||
  // #endregion Theia customizations
 | 
			
		||||
 | 
			
		||||
  // a single MonitorManager is responsible for handling the actual connections to the pluggable monitors
 | 
			
		||||
  bind(MonitorManager).toSelf().inSingletonScope();
 | 
			
		||||
 | 
			
		||||
  // monitor service & factory bindings
 | 
			
		||||
  bind(MonitorSettingsProviderImpl).toSelf().inSingletonScope();
 | 
			
		||||
  bind(MonitorSettingsProvider).toService(MonitorSettingsProviderImpl);
 | 
			
		||||
 | 
			
		||||
  bind(WebSocketProviderImpl).toSelf();
 | 
			
		||||
  bind(WebSocketProvider).toService(WebSocketProviderImpl);
 | 
			
		||||
 | 
			
		||||
  bind(MonitorServiceFactory).toFactory(
 | 
			
		||||
    ({ container }) =>
 | 
			
		||||
      (options: MonitorServiceFactoryOptions) => {
 | 
			
		||||
        const logger = container.get<ILogger>(ILogger);
 | 
			
		||||
 | 
			
		||||
        const monitorSettingsProvider = container.get<MonitorSettingsProvider>(
 | 
			
		||||
          MonitorSettingsProvider
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const webSocketProvider =
 | 
			
		||||
          container.get<WebSocketProvider>(WebSocketProvider);
 | 
			
		||||
 | 
			
		||||
        const { board, port, coreClientProvider } = options;
 | 
			
		||||
 | 
			
		||||
        return new MonitorService(
 | 
			
		||||
          logger,
 | 
			
		||||
          monitorSettingsProvider,
 | 
			
		||||
          webSocketProvider,
 | 
			
		||||
          board,
 | 
			
		||||
          port,
 | 
			
		||||
          coreClientProvider
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  // Serial client provider per connected frontend.
 | 
			
		||||
  bind(ConnectionContainerModule).toConstantValue(
 | 
			
		||||
    ConnectionContainerModule.create(({ bind, bindBackendService }) => {
 | 
			
		||||
      bind(MonitorClientProvider).toSelf().inSingletonScope();
 | 
			
		||||
      bind(SerialServiceImpl).toSelf().inSingletonScope();
 | 
			
		||||
      bind(SerialService).toService(SerialServiceImpl);
 | 
			
		||||
      bindBackendService<SerialService, SerialServiceClient>(
 | 
			
		||||
        SerialServicePath,
 | 
			
		||||
        SerialService,
 | 
			
		||||
        (service, client) => {
 | 
			
		||||
          service.setClient(client);
 | 
			
		||||
          client.onDidCloseConnection(() => service.dispose());
 | 
			
		||||
          return service;
 | 
			
		||||
      bind(MonitorManagerProxyImpl).toSelf().inSingletonScope();
 | 
			
		||||
      bind(MonitorManagerProxy).toService(MonitorManagerProxyImpl);
 | 
			
		||||
      bindBackendService<MonitorManagerProxy, MonitorManagerProxyClient>(
 | 
			
		||||
        MonitorManagerProxyPath,
 | 
			
		||||
        MonitorManagerProxy,
 | 
			
		||||
        (monitorMgrProxy, client) => {
 | 
			
		||||
          monitorMgrProxy.setClient(client);
 | 
			
		||||
          // when the client close the connection, the proxy is disposed.
 | 
			
		||||
          // when the MonitorManagerProxy is disposed, it informs the MonitorManager
 | 
			
		||||
          // telling him that it does not need an address/board anymore.
 | 
			
		||||
          // the MonitorManager will then dispose the actual connection if there are no proxies using it
 | 
			
		||||
          client.onDidCloseConnection(() => monitorMgrProxy.dispose());
 | 
			
		||||
          return monitorMgrProxy;
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
    })
 | 
			
		||||
@@ -307,14 +347,22 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
 | 
			
		||||
    .inSingletonScope()
 | 
			
		||||
    .whenTargetNamed('config');
 | 
			
		||||
 | 
			
		||||
  // Logger for the serial service.
 | 
			
		||||
  // Logger for the monitor manager and its services
 | 
			
		||||
  bind(ILogger)
 | 
			
		||||
    .toDynamicValue((ctx) => {
 | 
			
		||||
      const parentLogger = ctx.container.get<ILogger>(ILogger);
 | 
			
		||||
      return parentLogger.child(SerialServiceName);
 | 
			
		||||
      return parentLogger.child(MonitorManagerName);
 | 
			
		||||
    })
 | 
			
		||||
    .inSingletonScope()
 | 
			
		||||
    .whenTargetNamed(SerialServiceName);
 | 
			
		||||
    .whenTargetNamed(MonitorManagerName);
 | 
			
		||||
 | 
			
		||||
  bind(ILogger)
 | 
			
		||||
    .toDynamicValue((ctx) => {
 | 
			
		||||
      const parentLogger = ctx.container.get<ILogger>(ILogger);
 | 
			
		||||
      return parentLogger.child(MonitorServiceName);
 | 
			
		||||
    })
 | 
			
		||||
    .inSingletonScope()
 | 
			
		||||
    .whenTargetNamed(MonitorServiceName);
 | 
			
		||||
 | 
			
		||||
  bind(DefaultGitInit).toSelf();
 | 
			
		||||
  rebind(GitInit).toService(DefaultGitInit);
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ import { ArduinoCoreServiceClient } from './cli-protocol/cc/arduino/cli/commands
 | 
			
		||||
import { firstToUpperCase, firstToLowerCase } from '../common/utils';
 | 
			
		||||
import { Port } from './cli-protocol/cc/arduino/cli/commands/v1/port_pb';
 | 
			
		||||
import { nls } from '@theia/core';
 | 
			
		||||
import { SerialService } from './../common/protocol/serial-service';
 | 
			
		||||
import { MonitorManager } from './monitor-manager';
 | 
			
		||||
 | 
			
		||||
@injectable()
 | 
			
		||||
export class CoreServiceImpl extends CoreClientAware implements CoreService {
 | 
			
		||||
@@ -34,8 +34,8 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
 | 
			
		||||
  @inject(NotificationServiceServer)
 | 
			
		||||
  protected readonly notificationService: NotificationServiceServer;
 | 
			
		||||
 | 
			
		||||
  @inject(SerialService)
 | 
			
		||||
  protected readonly serialService: SerialService;
 | 
			
		||||
  @inject(MonitorManager)
 | 
			
		||||
  protected readonly monitorManager: MonitorManager;
 | 
			
		||||
 | 
			
		||||
  protected uploading = false;
 | 
			
		||||
 | 
			
		||||
@@ -45,7 +45,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
 | 
			
		||||
      compilerWarnings?: CompilerWarnings;
 | 
			
		||||
    }
 | 
			
		||||
  ): Promise<void> {
 | 
			
		||||
    const { sketchUri, fqbn, compilerWarnings } = options;
 | 
			
		||||
    const { sketchUri, board, compilerWarnings } = options;
 | 
			
		||||
    const sketchPath = FileUri.fsPath(sketchUri);
 | 
			
		||||
 | 
			
		||||
    await this.coreClientProvider.initialized;
 | 
			
		||||
@@ -55,8 +55,8 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
 | 
			
		||||
    const compileReq = new CompileRequest();
 | 
			
		||||
    compileReq.setInstance(instance);
 | 
			
		||||
    compileReq.setSketchPath(sketchPath);
 | 
			
		||||
    if (fqbn) {
 | 
			
		||||
      compileReq.setFqbn(fqbn);
 | 
			
		||||
    if (board?.fqbn) {
 | 
			
		||||
      compileReq.setFqbn(board.fqbn);
 | 
			
		||||
    }
 | 
			
		||||
    if (compilerWarnings) {
 | 
			
		||||
      compileReq.setWarnings(compilerWarnings.toLowerCase());
 | 
			
		||||
@@ -139,11 +139,9 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
 | 
			
		||||
    await this.compile(Object.assign(options, { exportBinaries: false }));
 | 
			
		||||
 | 
			
		||||
    this.uploading = true;
 | 
			
		||||
    this.serialService.uploadInProgress = true;
 | 
			
		||||
    const { sketchUri, board, port, programmer } = options;
 | 
			
		||||
    await this.monitorManager.notifyUploadStarted(board, port);
 | 
			
		||||
 | 
			
		||||
    await this.serialService.disconnect();
 | 
			
		||||
 | 
			
		||||
    const { sketchUri, fqbn, port, programmer } = options;
 | 
			
		||||
    const sketchPath = FileUri.fsPath(sketchUri);
 | 
			
		||||
 | 
			
		||||
    await this.coreClientProvider.initialized;
 | 
			
		||||
@@ -153,8 +151,8 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
 | 
			
		||||
    const req = requestProvider();
 | 
			
		||||
    req.setInstance(instance);
 | 
			
		||||
    req.setSketchPath(sketchPath);
 | 
			
		||||
    if (fqbn) {
 | 
			
		||||
      req.setFqbn(fqbn);
 | 
			
		||||
    if (board?.fqbn) {
 | 
			
		||||
      req.setFqbn(board.fqbn);
 | 
			
		||||
    }
 | 
			
		||||
    const p = new Port();
 | 
			
		||||
    if (port) {
 | 
			
		||||
@@ -209,23 +207,22 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
 | 
			
		||||
      throw new Error(errorMessage);
 | 
			
		||||
    } finally {
 | 
			
		||||
      this.uploading = false;
 | 
			
		||||
      this.serialService.uploadInProgress = false;
 | 
			
		||||
      this.monitorManager.notifyUploadFinished(board, port);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async burnBootloader(options: CoreService.Bootloader.Options): Promise<void> {
 | 
			
		||||
    this.uploading = true;
 | 
			
		||||
    this.serialService.uploadInProgress = true;
 | 
			
		||||
    await this.serialService.disconnect();
 | 
			
		||||
    const { board, port, programmer } = options;
 | 
			
		||||
    await this.monitorManager.notifyUploadStarted(board, port);
 | 
			
		||||
 | 
			
		||||
    await this.coreClientProvider.initialized;
 | 
			
		||||
    const coreClient = await this.coreClient();
 | 
			
		||||
    const { client, instance } = coreClient;
 | 
			
		||||
    const { fqbn, port, programmer } = options;
 | 
			
		||||
    const burnReq = new BurnBootloaderRequest();
 | 
			
		||||
    burnReq.setInstance(instance);
 | 
			
		||||
    if (fqbn) {
 | 
			
		||||
      burnReq.setFqbn(fqbn);
 | 
			
		||||
    if (board?.fqbn) {
 | 
			
		||||
      burnReq.setFqbn(board.fqbn);
 | 
			
		||||
    }
 | 
			
		||||
    const p = new Port();
 | 
			
		||||
    if (port) {
 | 
			
		||||
@@ -267,7 +264,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
 | 
			
		||||
      throw new Error(errorMessage);
 | 
			
		||||
    } finally {
 | 
			
		||||
      this.uploading = false;
 | 
			
		||||
      this.serialService.uploadInProgress = false;
 | 
			
		||||
      await this.monitorManager.notifyUploadFinished(board, port);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										92
									
								
								arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,92 @@
 | 
			
		||||
import { inject, injectable } from '@theia/core/shared/inversify';
 | 
			
		||||
import {
 | 
			
		||||
  MonitorManagerProxy,
 | 
			
		||||
  MonitorManagerProxyClient,
 | 
			
		||||
  Status,
 | 
			
		||||
} from '../common/protocol';
 | 
			
		||||
import { Board, Port } from '../common/protocol';
 | 
			
		||||
import { MonitorManager } from './monitor-manager';
 | 
			
		||||
import { PluggableMonitorSettings } from './monitor-settings/monitor-settings-provider';
 | 
			
		||||
 | 
			
		||||
@injectable()
 | 
			
		||||
export class MonitorManagerProxyImpl implements MonitorManagerProxy {
 | 
			
		||||
  protected client: MonitorManagerProxyClient;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    @inject(MonitorManager)
 | 
			
		||||
    protected readonly manager: MonitorManager
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  dispose(): void {
 | 
			
		||||
    this.client?.disconnect();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Start a pluggable monitor and/or change its settings.
 | 
			
		||||
   * If settings are defined they'll be set before starting the monitor,
 | 
			
		||||
   * otherwise default ones will be used by the monitor.
 | 
			
		||||
   * @param board board connected to port
 | 
			
		||||
   * @param port port to monitor
 | 
			
		||||
   * @param settings map of supported configuration by the monitor
 | 
			
		||||
   */
 | 
			
		||||
  async startMonitor(
 | 
			
		||||
    board: Board,
 | 
			
		||||
    port: Port,
 | 
			
		||||
    settings?: PluggableMonitorSettings
 | 
			
		||||
  ): Promise<void> {
 | 
			
		||||
    if (settings) {
 | 
			
		||||
      await this.changeMonitorSettings(board, port, settings);
 | 
			
		||||
    }
 | 
			
		||||
    const status = await this.manager.startMonitor(board, port);
 | 
			
		||||
    if (status === Status.ALREADY_CONNECTED || status === Status.OK) {
 | 
			
		||||
      // Monitor started correctly, connect it with the frontend
 | 
			
		||||
      this.client.connect(this.manager.getWebsocketAddressPort(board, port));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Changes the settings of a running pluggable monitor, if that monitor is not
 | 
			
		||||
   * started this function is a noop.
 | 
			
		||||
   * @param board board connected to port
 | 
			
		||||
   * @param port port monitored
 | 
			
		||||
   * @param settings map of supported configuration by the monitor
 | 
			
		||||
   */
 | 
			
		||||
  async changeMonitorSettings(
 | 
			
		||||
    board: Board,
 | 
			
		||||
    port: Port,
 | 
			
		||||
    settings: PluggableMonitorSettings
 | 
			
		||||
  ): Promise<void> {
 | 
			
		||||
    if (!this.manager.isStarted(board, port)) {
 | 
			
		||||
      // Monitor is not running, no need to change settings
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    return this.manager.changeMonitorSettings(board, port, settings);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Stops a running pluggable monitor.
 | 
			
		||||
   * @param board board connected to port
 | 
			
		||||
   * @param port port monitored
 | 
			
		||||
   */
 | 
			
		||||
  async stopMonitor(board: Board, port: Port): Promise<void> {
 | 
			
		||||
    return this.manager.stopMonitor(board, port);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the current settings by the pluggable monitor connected to specified
 | 
			
		||||
   * by board/port combination.
 | 
			
		||||
   * @param board board connected to port
 | 
			
		||||
   * @param port port monitored
 | 
			
		||||
   * @returns a map of MonitorSetting
 | 
			
		||||
   */
 | 
			
		||||
  getCurrentSettings(board: Board, port: Port): PluggableMonitorSettings {
 | 
			
		||||
    return this.manager.currentMonitorSettings(board, port);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setClient(client: MonitorManagerProxyClient | undefined): void {
 | 
			
		||||
    if (!client) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this.client = client;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										213
									
								
								arduino-ide-extension/src/node/monitor-manager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										213
									
								
								arduino-ide-extension/src/node/monitor-manager.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,213 @@
 | 
			
		||||
import { ILogger } from '@theia/core';
 | 
			
		||||
import { inject, injectable, named } from '@theia/core/shared/inversify';
 | 
			
		||||
import { Board, Port, Status } from '../common/protocol';
 | 
			
		||||
import { CoreClientAware } from './core-client-provider';
 | 
			
		||||
import { MonitorService } from './monitor-service';
 | 
			
		||||
import { MonitorServiceFactory } from './monitor-service-factory';
 | 
			
		||||
import { PluggableMonitorSettings } from './monitor-settings/monitor-settings-provider';
 | 
			
		||||
 | 
			
		||||
type MonitorID = string;
 | 
			
		||||
 | 
			
		||||
export const MonitorManagerName = 'monitor-manager';
 | 
			
		||||
 | 
			
		||||
@injectable()
 | 
			
		||||
export class MonitorManager extends CoreClientAware {
 | 
			
		||||
  // Map of monitor services that manage the running pluggable monitors.
 | 
			
		||||
  // Each service handles the lifetime of one, and only one, monitor.
 | 
			
		||||
  // If either the board or port managed changes, a new service must
 | 
			
		||||
  // be started.
 | 
			
		||||
  private monitorServices = new Map<MonitorID, MonitorService>();
 | 
			
		||||
 | 
			
		||||
  @inject(MonitorServiceFactory)
 | 
			
		||||
  private monitorServiceFactory: MonitorServiceFactory;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    @inject(ILogger)
 | 
			
		||||
    @named(MonitorManagerName)
 | 
			
		||||
    protected readonly logger: ILogger
 | 
			
		||||
  ) {
 | 
			
		||||
    super();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Used to know if a monitor is started
 | 
			
		||||
   * @param board board connected to port
 | 
			
		||||
   * @param port port to monitor
 | 
			
		||||
   * @returns true if the monitor is currently monitoring the board/port
 | 
			
		||||
   * combination specifed, false in all other cases.
 | 
			
		||||
   */
 | 
			
		||||
  isStarted(board: Board, port: Port): boolean {
 | 
			
		||||
    const monitorID = this.monitorID(board, port);
 | 
			
		||||
    const monitor = this.monitorServices.get(monitorID);
 | 
			
		||||
    if (monitor) {
 | 
			
		||||
      return monitor.isStarted();
 | 
			
		||||
    }
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Start a pluggable monitor that receives and sends messages
 | 
			
		||||
   * to the specified board and port combination.
 | 
			
		||||
   * @param board board connected to port
 | 
			
		||||
   * @param port port to monitor
 | 
			
		||||
   * @returns a Status object to know if the process has been
 | 
			
		||||
   * started or if there have been errors.
 | 
			
		||||
   */
 | 
			
		||||
  async startMonitor(board: Board, port: Port): Promise<Status> {
 | 
			
		||||
    const monitorID = this.monitorID(board, port);
 | 
			
		||||
    let monitor = this.monitorServices.get(monitorID);
 | 
			
		||||
    if (!monitor) {
 | 
			
		||||
      monitor = this.createMonitor(board, port);
 | 
			
		||||
    }
 | 
			
		||||
    return await monitor.start(monitorID);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Stop a pluggable monitor connected to the specified board/port
 | 
			
		||||
   * combination. It's a noop if monitor is not running.
 | 
			
		||||
   * @param board board connected to port
 | 
			
		||||
   * @param port port monitored
 | 
			
		||||
   */
 | 
			
		||||
  async stopMonitor(board: Board, port: Port): Promise<void> {
 | 
			
		||||
    const monitorID = this.monitorID(board, port);
 | 
			
		||||
    const monitor = this.monitorServices.get(monitorID);
 | 
			
		||||
    if (!monitor) {
 | 
			
		||||
      // There's no monitor to stop, bail
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    return await monitor.stop();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the port of the WebSocket used by the MonitorService
 | 
			
		||||
   * that is handling the board/port combination
 | 
			
		||||
   * @param board board connected to port
 | 
			
		||||
   * @param port port to monitor
 | 
			
		||||
   * @returns port of the MonitorService's WebSocket
 | 
			
		||||
   */
 | 
			
		||||
  getWebsocketAddressPort(board: Board, port: Port): number {
 | 
			
		||||
    const monitorID = this.monitorID(board, port);
 | 
			
		||||
    const monitor = this.monitorServices.get(monitorID);
 | 
			
		||||
    if (!monitor) {
 | 
			
		||||
      return -1;
 | 
			
		||||
    }
 | 
			
		||||
    return monitor.getWebsocketAddressPort();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Notifies the monitor service of that board/port combination
 | 
			
		||||
   * that an upload process started on that exact board/port combination.
 | 
			
		||||
   * This must be done so that we can stop the monitor for the time being
 | 
			
		||||
   * until the upload process finished.
 | 
			
		||||
   * @param board board connected to port
 | 
			
		||||
   * @param port port to monitor
 | 
			
		||||
   */
 | 
			
		||||
  async notifyUploadStarted(board?: Board, port?: Port): Promise<void> {
 | 
			
		||||
    if (!board || !port) {
 | 
			
		||||
      // We have no way of knowing which monitor
 | 
			
		||||
      // to retrieve if we don't have this information.
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const monitorID = this.monitorID(board, port);
 | 
			
		||||
    const monitor = this.monitorServices.get(monitorID);
 | 
			
		||||
    if (!monitor) {
 | 
			
		||||
      // There's no monitor running there, bail
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    monitor.setUploadInProgress(true);
 | 
			
		||||
    return await monitor.pause();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Notifies the monitor service of that board/port combination
 | 
			
		||||
   * that an upload process started on that exact board/port combination.
 | 
			
		||||
   * @param board board connected to port
 | 
			
		||||
   * @param port port to monitor
 | 
			
		||||
   * @returns a Status object to know if the process has been
 | 
			
		||||
   * started or if there have been errors.
 | 
			
		||||
   */
 | 
			
		||||
  async notifyUploadFinished(board?: Board, port?: Port): Promise<Status> {
 | 
			
		||||
    if (!board || !port) {
 | 
			
		||||
      // We have no way of knowing which monitor
 | 
			
		||||
      // to retrieve if we don't have this information.
 | 
			
		||||
      return Status.NOT_CONNECTED;
 | 
			
		||||
    }
 | 
			
		||||
    const monitorID = this.monitorID(board, port);
 | 
			
		||||
    const monitor = this.monitorServices.get(monitorID);
 | 
			
		||||
    if (!monitor) {
 | 
			
		||||
      // There's no monitor running there, bail
 | 
			
		||||
      return Status.NOT_CONNECTED;
 | 
			
		||||
    }
 | 
			
		||||
    monitor.setUploadInProgress(false);
 | 
			
		||||
    return await monitor.start(monitorID);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Changes the settings of a pluggable monitor even if it's running.
 | 
			
		||||
   * If monitor is not running they're going to be used as soon as it's started.
 | 
			
		||||
   * @param board board connected to port
 | 
			
		||||
   * @param port port to monitor
 | 
			
		||||
   * @param settings monitor settings to change
 | 
			
		||||
   */
 | 
			
		||||
  changeMonitorSettings(
 | 
			
		||||
    board: Board,
 | 
			
		||||
    port: Port,
 | 
			
		||||
    settings: PluggableMonitorSettings
 | 
			
		||||
  ) {
 | 
			
		||||
    const monitorID = this.monitorID(board, port);
 | 
			
		||||
    let monitor = this.monitorServices.get(monitorID);
 | 
			
		||||
    if (!monitor) {
 | 
			
		||||
      monitor = this.createMonitor(board, port);
 | 
			
		||||
      monitor.changeSettings(settings);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the settings currently used by the pluggable monitor
 | 
			
		||||
   * that's communicating with the specified board/port combination.
 | 
			
		||||
   * @param board board connected to port
 | 
			
		||||
   * @param port port monitored
 | 
			
		||||
   * @returns map of current monitor settings
 | 
			
		||||
   */
 | 
			
		||||
  currentMonitorSettings(board: Board, port: Port): PluggableMonitorSettings {
 | 
			
		||||
    const monitorID = this.monitorID(board, port);
 | 
			
		||||
    const monitor = this.monitorServices.get(monitorID);
 | 
			
		||||
    if (!monitor) {
 | 
			
		||||
      return {};
 | 
			
		||||
    }
 | 
			
		||||
    return monitor.currentSettings();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates a MonitorService that handles the lifetime and the
 | 
			
		||||
   * communication via WebSocket with the frontend.
 | 
			
		||||
   * @param board board connected to specified port
 | 
			
		||||
   * @param port port to monitor
 | 
			
		||||
   * @returns a new instance of MonitorService ready to use.
 | 
			
		||||
   */
 | 
			
		||||
  private createMonitor(board: Board, port: Port): MonitorService {
 | 
			
		||||
    const monitorID = this.monitorID(board, port);
 | 
			
		||||
    const monitor = this.monitorServiceFactory({
 | 
			
		||||
      board,
 | 
			
		||||
      port,
 | 
			
		||||
      coreClientProvider: this.coreClientProvider,
 | 
			
		||||
    });
 | 
			
		||||
    this.monitorServices.set(monitorID, monitor);
 | 
			
		||||
    monitor.onDispose(
 | 
			
		||||
      (() => {
 | 
			
		||||
        this.monitorServices.delete(monitorID);
 | 
			
		||||
      }).bind(this)
 | 
			
		||||
    );
 | 
			
		||||
    return monitor;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Utility function to create a unique ID for a monitor service.
 | 
			
		||||
   * @param board
 | 
			
		||||
   * @param port
 | 
			
		||||
   * @returns a unique monitor ID
 | 
			
		||||
   */
 | 
			
		||||
  private monitorID(board: Board, port: Port): MonitorID {
 | 
			
		||||
    return `${board.fqbn}-${port.address}-${port.protocol}`;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								arduino-ide-extension/src/node/monitor-service-factory.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								arduino-ide-extension/src/node/monitor-service-factory.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
import { Board, Port } from '../common/protocol';
 | 
			
		||||
import { CoreClientProvider } from './core-client-provider';
 | 
			
		||||
import { MonitorService } from './monitor-service';
 | 
			
		||||
 | 
			
		||||
export const MonitorServiceFactory = Symbol('MonitorServiceFactory');
 | 
			
		||||
export interface MonitorServiceFactory {
 | 
			
		||||
  (options: {
 | 
			
		||||
    board: Board;
 | 
			
		||||
    port: Port;
 | 
			
		||||
    coreClientProvider: CoreClientProvider;
 | 
			
		||||
  }): MonitorService;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface MonitorServiceFactoryOptions {
 | 
			
		||||
  board: Board;
 | 
			
		||||
  port: Port;
 | 
			
		||||
  coreClientProvider: CoreClientProvider;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										425
									
								
								arduino-ide-extension/src/node/monitor-service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										425
									
								
								arduino-ide-extension/src/node/monitor-service.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,425 @@
 | 
			
		||||
import { ClientDuplexStream } from '@grpc/grpc-js';
 | 
			
		||||
import { Disposable, Emitter, ILogger } from '@theia/core';
 | 
			
		||||
import { inject, named } from '@theia/core/shared/inversify';
 | 
			
		||||
import { Board, Port, Status, Monitor } from '../common/protocol';
 | 
			
		||||
import {
 | 
			
		||||
  EnumerateMonitorPortSettingsRequest,
 | 
			
		||||
  EnumerateMonitorPortSettingsResponse,
 | 
			
		||||
  MonitorPortConfiguration,
 | 
			
		||||
  MonitorPortSetting,
 | 
			
		||||
  MonitorRequest,
 | 
			
		||||
  MonitorResponse,
 | 
			
		||||
} from './cli-protocol/cc/arduino/cli/commands/v1/monitor_pb';
 | 
			
		||||
import { CoreClientAware, CoreClientProvider } from './core-client-provider';
 | 
			
		||||
import { WebSocketProvider } from './web-socket/web-socket-provider';
 | 
			
		||||
import { Port as gRPCPort } from 'arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/port_pb';
 | 
			
		||||
import {
 | 
			
		||||
  PluggableMonitorSettings,
 | 
			
		||||
  MonitorSettingsProvider,
 | 
			
		||||
} from './monitor-settings/monitor-settings-provider';
 | 
			
		||||
 | 
			
		||||
export const MonitorServiceName = 'monitor-service';
 | 
			
		||||
 | 
			
		||||
export class MonitorService extends CoreClientAware implements Disposable {
 | 
			
		||||
  // Bidirectional gRPC stream used to receive and send data from the running
 | 
			
		||||
  // pluggable monitor managed by the Arduino CLI.
 | 
			
		||||
  protected duplex: ClientDuplexStream<MonitorRequest, MonitorResponse> | null;
 | 
			
		||||
 | 
			
		||||
  // Settings used by the currently running pluggable monitor.
 | 
			
		||||
  // They can be freely modified while running.
 | 
			
		||||
  protected settings: PluggableMonitorSettings;
 | 
			
		||||
 | 
			
		||||
  // List of messages received from the running pluggable monitor.
 | 
			
		||||
  // These are flushed from time to time to the frontend.
 | 
			
		||||
  protected messages: string[] = [];
 | 
			
		||||
 | 
			
		||||
  // Handles messages received from the frontend via websocket.
 | 
			
		||||
  protected onMessageReceived?: Disposable;
 | 
			
		||||
 | 
			
		||||
  // Sends messages to the frontend from time to time.
 | 
			
		||||
  protected flushMessagesInterval?: NodeJS.Timeout;
 | 
			
		||||
 | 
			
		||||
  // Triggered each time the number of clients connected
 | 
			
		||||
  // to the this service WebSocket changes.
 | 
			
		||||
  protected onWSClientsNumberChanged?: Disposable;
 | 
			
		||||
 | 
			
		||||
  // Used to notify that the monitor is being disposed
 | 
			
		||||
  protected readonly onDisposeEmitter = new Emitter<void>();
 | 
			
		||||
  readonly onDispose = this.onDisposeEmitter.event;
 | 
			
		||||
 | 
			
		||||
  protected uploadInProgress = false;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    @inject(ILogger)
 | 
			
		||||
    @named(MonitorServiceName)
 | 
			
		||||
    protected readonly logger: ILogger,
 | 
			
		||||
    @inject(MonitorSettingsProvider)
 | 
			
		||||
    protected readonly monitorSettingsProvider: MonitorSettingsProvider,
 | 
			
		||||
    @inject(WebSocketProvider)
 | 
			
		||||
    protected readonly webSocketProvider: WebSocketProvider,
 | 
			
		||||
 | 
			
		||||
    private readonly board: Board,
 | 
			
		||||
    private readonly port: Port,
 | 
			
		||||
    protected readonly coreClientProvider: CoreClientProvider
 | 
			
		||||
  ) {
 | 
			
		||||
    super();
 | 
			
		||||
 | 
			
		||||
    this.onWSClientsNumberChanged =
 | 
			
		||||
      this.webSocketProvider.onClientsNumberChanged(async (clients: number) => {
 | 
			
		||||
        if (clients === 0) {
 | 
			
		||||
          // There are no more clients that want to receive
 | 
			
		||||
          // data from this monitor, we can freely close
 | 
			
		||||
          // and dispose it.
 | 
			
		||||
          this.dispose();
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setUploadInProgress(status: boolean): void {
 | 
			
		||||
    this.uploadInProgress = status;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getWebsocketAddressPort(): number {
 | 
			
		||||
    return this.webSocketProvider.getAddress().port;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  dispose(): void {
 | 
			
		||||
    this.stop();
 | 
			
		||||
    this.onDisposeEmitter.fire();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * isStarted is used to know if the currently running pluggable monitor is started.
 | 
			
		||||
   * @returns true if pluggable monitor communication duplex is open,
 | 
			
		||||
   * false in all other cases.
 | 
			
		||||
   */
 | 
			
		||||
  isStarted(): boolean {
 | 
			
		||||
    return !!this.duplex;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Start and connects a monitor using currently set board and port.
 | 
			
		||||
   * If a monitor is already started or board fqbn, port address and/or protocol
 | 
			
		||||
   * are missing nothing happens.
 | 
			
		||||
   * @param id
 | 
			
		||||
   * @returns a status to verify connection has been established.
 | 
			
		||||
   */
 | 
			
		||||
  async start(monitorID: string): Promise<Status> {
 | 
			
		||||
    if (this.duplex) {
 | 
			
		||||
      return Status.ALREADY_CONNECTED;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!this.board?.fqbn || !this.port?.address || !this.port?.protocol) {
 | 
			
		||||
      return Status.CONFIG_MISSING;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.uploadInProgress) {
 | 
			
		||||
      return Status.UPLOAD_IN_PROGRESS;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.logger.info('starting monitor');
 | 
			
		||||
 | 
			
		||||
    // get default monitor settings from the CLI
 | 
			
		||||
    const defaultSettings = await this.portMonitorSettings(
 | 
			
		||||
      this.port.protocol,
 | 
			
		||||
      this.board.fqbn
 | 
			
		||||
    );
 | 
			
		||||
    // get actual settings from the settings provider
 | 
			
		||||
    this.settings = await this.monitorSettingsProvider.getSettings(
 | 
			
		||||
      monitorID,
 | 
			
		||||
      defaultSettings
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    await this.coreClientProvider.initialized;
 | 
			
		||||
    const coreClient = await this.coreClient();
 | 
			
		||||
    const { client, instance } = coreClient;
 | 
			
		||||
    this.duplex = client.monitor();
 | 
			
		||||
    this.duplex
 | 
			
		||||
      .on('close', () => {
 | 
			
		||||
        this.duplex = null;
 | 
			
		||||
        this.logger.info(
 | 
			
		||||
          `monitor to ${this.port?.address} using ${this.port?.protocol} closed by client`
 | 
			
		||||
        );
 | 
			
		||||
      })
 | 
			
		||||
      .on('end', () => {
 | 
			
		||||
        this.duplex = null;
 | 
			
		||||
        this.logger.info(
 | 
			
		||||
          `monitor to ${this.port?.address} using ${this.port?.protocol} closed by server`
 | 
			
		||||
        );
 | 
			
		||||
      })
 | 
			
		||||
      .on('error', (err: Error) => {
 | 
			
		||||
        this.logger.error(err);
 | 
			
		||||
        // TODO
 | 
			
		||||
        // this.theiaFEClient?.notifyError()
 | 
			
		||||
      })
 | 
			
		||||
      .on(
 | 
			
		||||
        'data',
 | 
			
		||||
        ((res: MonitorResponse) => {
 | 
			
		||||
          if (res.getError()) {
 | 
			
		||||
            // TODO: Maybe disconnect
 | 
			
		||||
            this.logger.error(res.getError());
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
          const data = res.getRxData();
 | 
			
		||||
          const message =
 | 
			
		||||
            typeof data === 'string'
 | 
			
		||||
              ? data
 | 
			
		||||
              : new TextDecoder('utf8').decode(data);
 | 
			
		||||
          this.messages.push(...splitLines(message));
 | 
			
		||||
        }).bind(this)
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    const req = new MonitorRequest();
 | 
			
		||||
    req.setInstance(instance);
 | 
			
		||||
    if (this.board?.fqbn) {
 | 
			
		||||
      req.setFqbn(this.board.fqbn);
 | 
			
		||||
    }
 | 
			
		||||
    if (this.port?.address && this.port?.protocol) {
 | 
			
		||||
      const port = new gRPCPort();
 | 
			
		||||
      port.setAddress(this.port.address);
 | 
			
		||||
      port.setProtocol(this.port.protocol);
 | 
			
		||||
      req.setPort(port);
 | 
			
		||||
    }
 | 
			
		||||
    const config = new MonitorPortConfiguration();
 | 
			
		||||
    for (const id in this.settings) {
 | 
			
		||||
      const s = new MonitorPortSetting();
 | 
			
		||||
      s.setSettingId(id);
 | 
			
		||||
      s.setValue(this.settings[id].selectedValue);
 | 
			
		||||
      config.addSettings(s);
 | 
			
		||||
    }
 | 
			
		||||
    req.setPortConfiguration(config);
 | 
			
		||||
 | 
			
		||||
    const connect = new Promise<Status>((resolve) => {
 | 
			
		||||
      if (this.duplex?.write(req)) {
 | 
			
		||||
        this.startMessagesHandlers();
 | 
			
		||||
        this.logger.info(
 | 
			
		||||
          `started monitor to ${this.port?.address} using ${this.port?.protocol}`
 | 
			
		||||
        );
 | 
			
		||||
        resolve(Status.OK);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      this.logger.warn(
 | 
			
		||||
        `failed starting monitor to ${this.port?.address} using ${this.port?.protocol}`
 | 
			
		||||
      );
 | 
			
		||||
      resolve(Status.NOT_CONNECTED);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const connectTimeout = new Promise<Status>((resolve) => {
 | 
			
		||||
      setTimeout(async () => {
 | 
			
		||||
        this.logger.warn(
 | 
			
		||||
          `timeout starting monitor to ${this.port?.address} using ${this.port?.protocol}`
 | 
			
		||||
        );
 | 
			
		||||
        resolve(Status.NOT_CONNECTED);
 | 
			
		||||
      }, 1000);
 | 
			
		||||
    });
 | 
			
		||||
    // Try opening a monitor connection with a timeout
 | 
			
		||||
    return await Promise.race([connect, connectTimeout]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Pauses the currently running monitor, it still closes the gRPC connection
 | 
			
		||||
   * with the underlying monitor process but it doesn't stop the message handlers
 | 
			
		||||
   * currently running.
 | 
			
		||||
   * This is mainly used to handle upload with the board/port combination
 | 
			
		||||
   * the monitor is listening to.
 | 
			
		||||
   * @returns
 | 
			
		||||
   */
 | 
			
		||||
  async pause(): Promise<void> {
 | 
			
		||||
    return new Promise(async (resolve) => {
 | 
			
		||||
      if (!this.duplex) {
 | 
			
		||||
        this.logger.warn(
 | 
			
		||||
          `monitor to ${this.port?.address} using ${this.port?.protocol} already stopped`
 | 
			
		||||
        );
 | 
			
		||||
        return resolve();
 | 
			
		||||
      }
 | 
			
		||||
      // It's enough to close the connection with the client
 | 
			
		||||
      // to stop the monitor process
 | 
			
		||||
      this.duplex.end();
 | 
			
		||||
      this.logger.info(
 | 
			
		||||
        `stopped monitor to ${this.port?.address} using ${this.port?.protocol}`
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      this.duplex.on('end', resolve);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Stop the monitor currently running
 | 
			
		||||
   */
 | 
			
		||||
  async stop(): Promise<void> {
 | 
			
		||||
    return this.pause().finally(this.stopMessagesHandlers.bind(this));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Send a message to the running monitor, a well behaved monitor
 | 
			
		||||
   * will then send that message to the board.
 | 
			
		||||
   * We MUST NEVER send a message that wasn't a user's input to the board.
 | 
			
		||||
   * @param message string sent to running monitor
 | 
			
		||||
   * @returns a status to verify message has been sent.
 | 
			
		||||
   */
 | 
			
		||||
  async send(message: string): Promise<Status> {
 | 
			
		||||
    if (!this.duplex) {
 | 
			
		||||
      return Status.NOT_CONNECTED;
 | 
			
		||||
    }
 | 
			
		||||
    await this.coreClientProvider.initialized;
 | 
			
		||||
    const coreClient = await this.coreClient();
 | 
			
		||||
    const { instance } = coreClient;
 | 
			
		||||
 | 
			
		||||
    const req = new MonitorRequest();
 | 
			
		||||
    req.setInstance(instance);
 | 
			
		||||
    req.setTxData(new TextEncoder().encode(message));
 | 
			
		||||
    return new Promise<Status>((resolve) => {
 | 
			
		||||
      if (this.duplex) {
 | 
			
		||||
        this.duplex?.write(req, () => {
 | 
			
		||||
          resolve(Status.OK);
 | 
			
		||||
        });
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      this.stop().then(() => resolve(Status.NOT_CONNECTED));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   *
 | 
			
		||||
   * @returns map of current monitor settings
 | 
			
		||||
   */
 | 
			
		||||
  currentSettings(): PluggableMonitorSettings {
 | 
			
		||||
    return this.settings;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // TODO: move this into MonitoSettingsProvider
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the possible configurations used to connect a monitor
 | 
			
		||||
   * to the board specified by fqbn using the specified protocol
 | 
			
		||||
   * @param protocol the protocol of the monitor we want get settings for
 | 
			
		||||
   * @param fqbn the fqbn of the board we want to monitor
 | 
			
		||||
   * @returns a map of all the settings supported by the monitor
 | 
			
		||||
   */
 | 
			
		||||
  private async portMonitorSettings(
 | 
			
		||||
    protocol: string,
 | 
			
		||||
    fqbn: string
 | 
			
		||||
  ): Promise<PluggableMonitorSettings> {
 | 
			
		||||
    await this.coreClientProvider.initialized;
 | 
			
		||||
    const coreClient = await this.coreClient();
 | 
			
		||||
    const { client, instance } = coreClient;
 | 
			
		||||
    const req = new EnumerateMonitorPortSettingsRequest();
 | 
			
		||||
    req.setInstance(instance);
 | 
			
		||||
    req.setPortProtocol(protocol);
 | 
			
		||||
    req.setFqbn(fqbn);
 | 
			
		||||
 | 
			
		||||
    const res = await new Promise<EnumerateMonitorPortSettingsResponse>(
 | 
			
		||||
      (resolve, reject) => {
 | 
			
		||||
        client.enumerateMonitorPortSettings(req, (err, resp) => {
 | 
			
		||||
          if (!!err) {
 | 
			
		||||
            reject(err);
 | 
			
		||||
          }
 | 
			
		||||
          resolve(resp);
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const settings: PluggableMonitorSettings = {};
 | 
			
		||||
    for (const iterator of res.getSettingsList()) {
 | 
			
		||||
      settings[iterator.getSettingId()] = {
 | 
			
		||||
        id: iterator.getSettingId(),
 | 
			
		||||
        label: iterator.getLabel(),
 | 
			
		||||
        type: iterator.getType(),
 | 
			
		||||
        values: iterator.getEnumValuesList(),
 | 
			
		||||
        selectedValue: iterator.getValue(),
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
    return settings;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Set monitor settings, if there is a running monitor they'll be sent
 | 
			
		||||
   * to it, otherwise they'll be used when starting one.
 | 
			
		||||
   * Only values in settings parameter will be change, other values won't
 | 
			
		||||
   * be changed in any way.
 | 
			
		||||
   * @param settings map of monitor settings to change
 | 
			
		||||
   * @returns a status to verify settings have been sent.
 | 
			
		||||
   */
 | 
			
		||||
  async changeSettings(settings: PluggableMonitorSettings): Promise<Status> {
 | 
			
		||||
    const config = new MonitorPortConfiguration();
 | 
			
		||||
    for (const id in settings) {
 | 
			
		||||
      const s = new MonitorPortSetting();
 | 
			
		||||
      s.setSettingId(id);
 | 
			
		||||
      s.setValue(settings[id].selectedValue);
 | 
			
		||||
      config.addSettings(s);
 | 
			
		||||
      this.settings[id] = settings[id];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!this.duplex) {
 | 
			
		||||
      return Status.NOT_CONNECTED;
 | 
			
		||||
    }
 | 
			
		||||
    await this.coreClientProvider.initialized;
 | 
			
		||||
    const coreClient = await this.coreClient();
 | 
			
		||||
    const { instance } = coreClient;
 | 
			
		||||
 | 
			
		||||
    const req = new MonitorRequest();
 | 
			
		||||
    req.setInstance(instance);
 | 
			
		||||
    req.setPortConfiguration(config);
 | 
			
		||||
    this.duplex.write(req);
 | 
			
		||||
    return Status.OK;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Starts the necessary handlers to send and receive
 | 
			
		||||
   * messages to and from the frontend and the running monitor
 | 
			
		||||
   */
 | 
			
		||||
  private startMessagesHandlers(): void {
 | 
			
		||||
    if (!this.flushMessagesInterval) {
 | 
			
		||||
      const flushMessagesToFrontend = () => {
 | 
			
		||||
        if (this.messages.length) {
 | 
			
		||||
          this.webSocketProvider.sendMessage(JSON.stringify(this.messages));
 | 
			
		||||
          this.messages = [];
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
      this.flushMessagesInterval = setInterval(flushMessagesToFrontend, 32);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!this.onMessageReceived) {
 | 
			
		||||
      this.onMessageReceived = this.webSocketProvider.onMessageReceived(
 | 
			
		||||
        (msg: string) => {
 | 
			
		||||
          const message: Monitor.Message = JSON.parse(msg);
 | 
			
		||||
 | 
			
		||||
          switch (message.command) {
 | 
			
		||||
            case Monitor.Command.SEND_MESSAGE:
 | 
			
		||||
              this.send(message.data);
 | 
			
		||||
              break;
 | 
			
		||||
            case Monitor.Command.CHANGE_SETTINGS:
 | 
			
		||||
              const settings: PluggableMonitorSettings = JSON.parse(
 | 
			
		||||
                message.data
 | 
			
		||||
              );
 | 
			
		||||
              this.changeSettings(settings);
 | 
			
		||||
              break;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Stops the necessary handlers to send and receive messages to
 | 
			
		||||
   * and from the frontend and the running monitor
 | 
			
		||||
   */
 | 
			
		||||
  private stopMessagesHandlers(): void {
 | 
			
		||||
    if (this.flushMessagesInterval) {
 | 
			
		||||
      clearInterval(this.flushMessagesInterval);
 | 
			
		||||
      this.flushMessagesInterval = undefined;
 | 
			
		||||
    }
 | 
			
		||||
    if (this.onMessageReceived) {
 | 
			
		||||
      this.onMessageReceived.dispose();
 | 
			
		||||
      this.onMessageReceived = undefined;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Splits a string into an array without removing newline char.
 | 
			
		||||
 * @param s string to split into lines
 | 
			
		||||
 * @returns an lines array
 | 
			
		||||
 */
 | 
			
		||||
function splitLines(s: string): string[] {
 | 
			
		||||
  return s.split(/(?<=\n)/);
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,145 @@
 | 
			
		||||
import * as fs from 'fs';
 | 
			
		||||
import { join } from 'path';
 | 
			
		||||
import { injectable, inject, postConstruct } from 'inversify';
 | 
			
		||||
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
 | 
			
		||||
import { FileUri } from '@theia/core/lib/node/file-uri';
 | 
			
		||||
import { promisify } from 'util';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  PluggableMonitorSettings,
 | 
			
		||||
  MonitorSettingsProvider,
 | 
			
		||||
} from './monitor-settings-provider';
 | 
			
		||||
import { Deferred } from '@theia/core/lib/common/promise-util';
 | 
			
		||||
 | 
			
		||||
const MONITOR_SETTINGS_FILE = 'pluggable-monitor-settings.json';
 | 
			
		||||
 | 
			
		||||
@injectable()
 | 
			
		||||
export class MonitorSettingsProviderImpl implements MonitorSettingsProvider {
 | 
			
		||||
  @inject(EnvVariablesServer)
 | 
			
		||||
  protected readonly envVariablesServer: EnvVariablesServer;
 | 
			
		||||
 | 
			
		||||
  protected ready = new Deferred<void>();
 | 
			
		||||
 | 
			
		||||
  // this is populated with all settings coming from the CLI. This should never be modified
 | 
			
		||||
  // // as it is used to double check the monitorSettings attribute
 | 
			
		||||
  // private monitorDefaultSettings: PluggableMonitorSettings;
 | 
			
		||||
 | 
			
		||||
  // this contains actual values coming from the stored file and edited by the user
 | 
			
		||||
  // this is a map with MonitorId as key and PluggableMonitorSetting as value
 | 
			
		||||
  private monitorSettings: Record<string, PluggableMonitorSettings>;
 | 
			
		||||
 | 
			
		||||
  private pluggableMonitorSettingsPath: string;
 | 
			
		||||
 | 
			
		||||
  @postConstruct()
 | 
			
		||||
  protected async init(): Promise<void> {
 | 
			
		||||
    // get the monitor settings file path
 | 
			
		||||
    const configDirUri = await this.envVariablesServer.getConfigDirUri();
 | 
			
		||||
    this.pluggableMonitorSettingsPath = join(
 | 
			
		||||
      FileUri.fsPath(configDirUri),
 | 
			
		||||
      MONITOR_SETTINGS_FILE
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // read existing settings
 | 
			
		||||
    await this.readFile();
 | 
			
		||||
 | 
			
		||||
    console.log(this.monitorSettings);
 | 
			
		||||
    this.ready.resolve();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async getSettings(
 | 
			
		||||
    monitorId: string,
 | 
			
		||||
    defaultSettings: PluggableMonitorSettings
 | 
			
		||||
  ): Promise<PluggableMonitorSettings> {
 | 
			
		||||
    // wait for the service to complete the init
 | 
			
		||||
    await this.ready.promise;
 | 
			
		||||
 | 
			
		||||
    const { matchingSettings } = this.longestPrefixMatch(monitorId);
 | 
			
		||||
 | 
			
		||||
    return this.reconcileSettings(matchingSettings, defaultSettings);
 | 
			
		||||
  }
 | 
			
		||||
  async setSettings(
 | 
			
		||||
    monitorId: string,
 | 
			
		||||
    settings: PluggableMonitorSettings
 | 
			
		||||
  ): Promise<PluggableMonitorSettings> {
 | 
			
		||||
    // wait for the service to complete the init
 | 
			
		||||
    await this.ready.promise;
 | 
			
		||||
 | 
			
		||||
    const newSettings = this.reconcileSettings(
 | 
			
		||||
      settings,
 | 
			
		||||
      this.monitorSettings[monitorId]
 | 
			
		||||
    );
 | 
			
		||||
    this.monitorSettings[monitorId] = newSettings;
 | 
			
		||||
 | 
			
		||||
    await this.writeFile();
 | 
			
		||||
    return newSettings;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private reconcileSettings(
 | 
			
		||||
    newSettings: PluggableMonitorSettings,
 | 
			
		||||
    defaultSettings: PluggableMonitorSettings
 | 
			
		||||
  ): PluggableMonitorSettings {
 | 
			
		||||
    // TODO: implement
 | 
			
		||||
    return newSettings;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async readFile(): Promise<void> {
 | 
			
		||||
    const rawJson = await promisify(fs.readFile)(
 | 
			
		||||
      this.pluggableMonitorSettingsPath,
 | 
			
		||||
      {
 | 
			
		||||
        encoding: 'utf-8',
 | 
			
		||||
        flag: 'a+', // a+ = append and read, creating the file if it doesn't exist
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (!rawJson) {
 | 
			
		||||
      this.monitorSettings = {};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      this.monitorSettings = JSON.parse(rawJson);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error(
 | 
			
		||||
        'Could not parse the pluggable monitor settings file. Using empty file.'
 | 
			
		||||
      );
 | 
			
		||||
      this.monitorSettings = {};
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async writeFile() {
 | 
			
		||||
    await promisify(fs.writeFile)(
 | 
			
		||||
      this.pluggableMonitorSettingsPath,
 | 
			
		||||
      JSON.stringify(this.monitorSettings)
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private longestPrefixMatch(id: string): {
 | 
			
		||||
    matchingPrefix: string;
 | 
			
		||||
    matchingSettings: PluggableMonitorSettings;
 | 
			
		||||
  } {
 | 
			
		||||
    const separator = '-';
 | 
			
		||||
    const idTokens = id.split(separator);
 | 
			
		||||
 | 
			
		||||
    let matchingPrefix = '';
 | 
			
		||||
    let matchingSettings: PluggableMonitorSettings = {};
 | 
			
		||||
 | 
			
		||||
    const monitorSettingsKeys = Object.keys(this.monitorSettings);
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < idTokens.length; i++) {
 | 
			
		||||
      const prefix = idTokens.slice(0, i + 1).join(separator);
 | 
			
		||||
 | 
			
		||||
      for (let k = 0; k < monitorSettingsKeys.length; k++) {
 | 
			
		||||
        if (monitorSettingsKeys[k].startsWith(prefix)) {
 | 
			
		||||
          matchingPrefix = prefix;
 | 
			
		||||
          matchingSettings = this.monitorSettings[monitorSettingsKeys[k]];
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (matchingPrefix.length) {
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return { matchingPrefix, matchingSettings };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,20 @@
 | 
			
		||||
import { MonitorModel } from '../../browser/monitor-model';
 | 
			
		||||
import { PluggableMonitorSetting } from '../../common/protocol';
 | 
			
		||||
 | 
			
		||||
export type PluggableMonitorSettings = Record<string, PluggableMonitorSetting>;
 | 
			
		||||
export interface MonitorSettings {
 | 
			
		||||
  pluggableMonitorSettings?: PluggableMonitorSettings;
 | 
			
		||||
  monitorUISettings?: Partial<MonitorModel.State>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const MonitorSettingsProvider = Symbol('MonitorSettingsProvider');
 | 
			
		||||
export interface MonitorSettingsProvider {
 | 
			
		||||
  getSettings(
 | 
			
		||||
    monitorId: string,
 | 
			
		||||
    defaultSettings: PluggableMonitorSettings
 | 
			
		||||
  ): Promise<PluggableMonitorSettings>;
 | 
			
		||||
  setSettings(
 | 
			
		||||
    monitorId: string,
 | 
			
		||||
    settings: PluggableMonitorSettings
 | 
			
		||||
  ): Promise<PluggableMonitorSettings>;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,26 +0,0 @@
 | 
			
		||||
import * as grpc from '@grpc/grpc-js';
 | 
			
		||||
import { injectable } from 'inversify';
 | 
			
		||||
import { MonitorServiceClient } from '../cli-protocol/cc/arduino/cli/monitor/v1/monitor_grpc_pb';
 | 
			
		||||
import * as monitorGrpcPb from '../cli-protocol/cc/arduino/cli/monitor/v1/monitor_grpc_pb';
 | 
			
		||||
import { GrpcClientProvider } from '../grpc-client-provider';
 | 
			
		||||
 | 
			
		||||
@injectable()
 | 
			
		||||
export class MonitorClientProvider extends GrpcClientProvider<MonitorServiceClient> {
 | 
			
		||||
  createClient(port: string | number): MonitorServiceClient {
 | 
			
		||||
    // https://github.com/agreatfool/grpc_tools_node_protoc_ts/blob/master/doc/grpcjs_support.md#usage
 | 
			
		||||
    const MonitorServiceClient = grpc.makeClientConstructor(
 | 
			
		||||
      // @ts-expect-error: ignore
 | 
			
		||||
      monitorGrpcPb['cc.arduino.cli.monitor.v1.MonitorService'],
 | 
			
		||||
      'MonitorServiceService'
 | 
			
		||||
    ) as any;
 | 
			
		||||
    return new MonitorServiceClient(
 | 
			
		||||
      `localhost:${port}`,
 | 
			
		||||
      grpc.credentials.createInsecure(),
 | 
			
		||||
      this.channelOptions
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  close(client: MonitorServiceClient): void {
 | 
			
		||||
    client.close();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,397 +0,0 @@
 | 
			
		||||
import { ClientDuplexStream } from '@grpc/grpc-js';
 | 
			
		||||
import { TextEncoder } from 'util';
 | 
			
		||||
import { injectable, inject, named } from 'inversify';
 | 
			
		||||
import { Struct } from 'google-protobuf/google/protobuf/struct_pb';
 | 
			
		||||
import { ILogger } from '@theia/core/lib/common/logger';
 | 
			
		||||
import {
 | 
			
		||||
  SerialService,
 | 
			
		||||
  SerialServiceClient,
 | 
			
		||||
  SerialConfig,
 | 
			
		||||
  SerialError,
 | 
			
		||||
  Status,
 | 
			
		||||
} from '../../common/protocol/serial-service';
 | 
			
		||||
import {
 | 
			
		||||
  StreamingOpenRequest,
 | 
			
		||||
  StreamingOpenResponse,
 | 
			
		||||
  MonitorConfig as GrpcMonitorConfig,
 | 
			
		||||
} from '../cli-protocol/cc/arduino/cli/monitor/v1/monitor_pb';
 | 
			
		||||
import { MonitorClientProvider } from './monitor-client-provider';
 | 
			
		||||
import { Board } from '../../common/protocol/boards-service';
 | 
			
		||||
import { WebSocketService } from '../web-socket/web-socket-service';
 | 
			
		||||
import { SerialPlotter } from '../../browser/serial/plotter/protocol';
 | 
			
		||||
import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol';
 | 
			
		||||
 | 
			
		||||
export const SerialServiceName = 'serial-service';
 | 
			
		||||
 | 
			
		||||
interface ErrorWithCode extends Error {
 | 
			
		||||
  readonly code: number;
 | 
			
		||||
}
 | 
			
		||||
namespace ErrorWithCode {
 | 
			
		||||
  export function toSerialError(
 | 
			
		||||
    error: Error,
 | 
			
		||||
    config: SerialConfig
 | 
			
		||||
  ): SerialError {
 | 
			
		||||
    const { message } = error;
 | 
			
		||||
    let code = undefined;
 | 
			
		||||
    if (is(error)) {
 | 
			
		||||
      // TODO: const `mapping`. Use regex for the `message`.
 | 
			
		||||
      const mapping = new Map<string, number>();
 | 
			
		||||
      mapping.set(
 | 
			
		||||
        '1 CANCELLED: Cancelled on client',
 | 
			
		||||
        SerialError.ErrorCodes.CLIENT_CANCEL
 | 
			
		||||
      );
 | 
			
		||||
      mapping.set(
 | 
			
		||||
        '2 UNKNOWN: device not configured',
 | 
			
		||||
        SerialError.ErrorCodes.DEVICE_NOT_CONFIGURED
 | 
			
		||||
      );
 | 
			
		||||
      mapping.set(
 | 
			
		||||
        '2 UNKNOWN: error opening serial connection: Serial port busy',
 | 
			
		||||
        SerialError.ErrorCodes.DEVICE_BUSY
 | 
			
		||||
      );
 | 
			
		||||
      code = mapping.get(message);
 | 
			
		||||
    }
 | 
			
		||||
    return {
 | 
			
		||||
      message,
 | 
			
		||||
      code,
 | 
			
		||||
      config,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
  function is(error: Error & { code?: number }): error is ErrorWithCode {
 | 
			
		||||
    return typeof error.code === 'number';
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@injectable()
 | 
			
		||||
export class SerialServiceImpl implements SerialService {
 | 
			
		||||
  protected theiaFEClient?: SerialServiceClient;
 | 
			
		||||
  protected serialConfig?: SerialConfig;
 | 
			
		||||
 | 
			
		||||
  protected serialConnection?: {
 | 
			
		||||
    duplex: ClientDuplexStream<StreamingOpenRequest, StreamingOpenResponse>;
 | 
			
		||||
    config: SerialConfig;
 | 
			
		||||
  };
 | 
			
		||||
  protected messages: string[] = [];
 | 
			
		||||
  protected onMessageReceived: Disposable | null;
 | 
			
		||||
  protected onWSClientsNumberChanged: Disposable | null;
 | 
			
		||||
 | 
			
		||||
  protected flushMessagesInterval: NodeJS.Timeout | null;
 | 
			
		||||
 | 
			
		||||
  uploadInProgress = false;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    @inject(ILogger)
 | 
			
		||||
    @named(SerialServiceName)
 | 
			
		||||
    protected readonly logger: ILogger,
 | 
			
		||||
 | 
			
		||||
    @inject(MonitorClientProvider)
 | 
			
		||||
    protected readonly serialClientProvider: MonitorClientProvider,
 | 
			
		||||
 | 
			
		||||
    @inject(WebSocketService)
 | 
			
		||||
    protected readonly webSocketService: WebSocketService
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  async isSerialPortOpen(): Promise<boolean> {
 | 
			
		||||
    return !!this.serialConnection;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setClient(client: SerialServiceClient | undefined): void {
 | 
			
		||||
    this.theiaFEClient = client;
 | 
			
		||||
 | 
			
		||||
    this.theiaFEClient?.notifyWebSocketChanged(
 | 
			
		||||
      this.webSocketService.getAddress().port
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // listen for the number of websocket clients and create or dispose the serial connection
 | 
			
		||||
    this.onWSClientsNumberChanged =
 | 
			
		||||
      this.webSocketService.onClientsNumberChanged(async () => {
 | 
			
		||||
        await this.connectSerialIfRequired();
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async clientsAttached(): Promise<number> {
 | 
			
		||||
    return this.webSocketService.getConnectedClientsNumber.bind(
 | 
			
		||||
      this.webSocketService
 | 
			
		||||
    )();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async connectSerialIfRequired(): Promise<void> {
 | 
			
		||||
    if (this.uploadInProgress) return;
 | 
			
		||||
    const clients = await this.clientsAttached();
 | 
			
		||||
    clients > 0 ? await this.connect() : await this.disconnect();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  dispose(): void {
 | 
			
		||||
    this.logger.info('>>> Disposing serial service...');
 | 
			
		||||
    if (this.serialConnection) {
 | 
			
		||||
      this.disconnect();
 | 
			
		||||
    }
 | 
			
		||||
    this.logger.info('<<< Disposed serial service.');
 | 
			
		||||
    this.theiaFEClient = undefined;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async setSerialConfig(config: SerialConfig): Promise<void> {
 | 
			
		||||
    this.serialConfig = config;
 | 
			
		||||
    await this.disconnect();
 | 
			
		||||
    await this.connectSerialIfRequired();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async updateWsConfigParam(
 | 
			
		||||
    config: Partial<SerialPlotter.Config>
 | 
			
		||||
  ): Promise<void> {
 | 
			
		||||
    const msg: SerialPlotter.Protocol.Message = {
 | 
			
		||||
      command: SerialPlotter.Protocol.Command.MIDDLEWARE_CONFIG_CHANGED,
 | 
			
		||||
      data: config,
 | 
			
		||||
    };
 | 
			
		||||
    this.webSocketService.sendMessage(JSON.stringify(msg));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async connect(): Promise<Status> {
 | 
			
		||||
    if (!this.serialConfig) {
 | 
			
		||||
      return Status.CONFIG_MISSING;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.logger.info(
 | 
			
		||||
      `>>> Creating serial connection for ${Board.toString(
 | 
			
		||||
        this.serialConfig.board
 | 
			
		||||
      )} on port ${this.serialConfig.port.address}...`
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (this.serialConnection) {
 | 
			
		||||
      return Status.ALREADY_CONNECTED;
 | 
			
		||||
    }
 | 
			
		||||
    const client = await this.serialClientProvider.client();
 | 
			
		||||
    if (!client) {
 | 
			
		||||
      return Status.NOT_CONNECTED;
 | 
			
		||||
    }
 | 
			
		||||
    if (client instanceof Error) {
 | 
			
		||||
      return { message: client.message };
 | 
			
		||||
    }
 | 
			
		||||
    const duplex = client.streamingOpen();
 | 
			
		||||
    this.serialConnection = { duplex, config: this.serialConfig };
 | 
			
		||||
 | 
			
		||||
    const serialConfig = this.serialConfig;
 | 
			
		||||
 | 
			
		||||
    duplex.on(
 | 
			
		||||
      'error',
 | 
			
		||||
      ((error: Error) => {
 | 
			
		||||
        const serialError = ErrorWithCode.toSerialError(error, serialConfig);
 | 
			
		||||
        if (serialError.code !== SerialError.ErrorCodes.CLIENT_CANCEL) {
 | 
			
		||||
          this.disconnect(serialError).then(() => {
 | 
			
		||||
            if (this.theiaFEClient) {
 | 
			
		||||
              this.theiaFEClient.notifyError(serialError);
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
        if (serialError.code === undefined) {
 | 
			
		||||
          // Log the original, unexpected error.
 | 
			
		||||
          this.logger.error(error);
 | 
			
		||||
        }
 | 
			
		||||
      }).bind(this)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.updateWsConfigParam({ connected: !!this.serialConnection });
 | 
			
		||||
 | 
			
		||||
    const flushMessagesToFrontend = () => {
 | 
			
		||||
      if (this.messages.length) {
 | 
			
		||||
        this.webSocketService.sendMessage(JSON.stringify(this.messages));
 | 
			
		||||
        this.messages = [];
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.onMessageReceived = this.webSocketService.onMessageReceived(
 | 
			
		||||
      (msg: string) => {
 | 
			
		||||
        try {
 | 
			
		||||
          const message: SerialPlotter.Protocol.Message = JSON.parse(msg);
 | 
			
		||||
 | 
			
		||||
          switch (message.command) {
 | 
			
		||||
            case SerialPlotter.Protocol.Command.PLOTTER_SEND_MESSAGE:
 | 
			
		||||
              this.sendMessageToSerial(message.data);
 | 
			
		||||
              break;
 | 
			
		||||
 | 
			
		||||
            case SerialPlotter.Protocol.Command.PLOTTER_SET_BAUDRATE:
 | 
			
		||||
              this.theiaFEClient?.notifyBaudRateChanged(
 | 
			
		||||
                parseInt(message.data, 10) as SerialConfig.BaudRate
 | 
			
		||||
              );
 | 
			
		||||
              break;
 | 
			
		||||
 | 
			
		||||
            case SerialPlotter.Protocol.Command.PLOTTER_SET_LINE_ENDING:
 | 
			
		||||
              this.theiaFEClient?.notifyLineEndingChanged(message.data);
 | 
			
		||||
              break;
 | 
			
		||||
 | 
			
		||||
            case SerialPlotter.Protocol.Command.PLOTTER_SET_INTERPOLATE:
 | 
			
		||||
              this.theiaFEClient?.notifyInterpolateChanged(message.data);
 | 
			
		||||
              break;
 | 
			
		||||
 | 
			
		||||
            default:
 | 
			
		||||
              break;
 | 
			
		||||
          }
 | 
			
		||||
        } catch (error) { }
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // empty the queue every 32ms (~30fps)
 | 
			
		||||
    this.flushMessagesInterval = setInterval(flushMessagesToFrontend, 32);
 | 
			
		||||
 | 
			
		||||
    duplex.on(
 | 
			
		||||
      'data',
 | 
			
		||||
      ((resp: StreamingOpenResponse) => {
 | 
			
		||||
        const raw = resp.getData();
 | 
			
		||||
        const message =
 | 
			
		||||
          typeof raw === 'string' ? raw : new TextDecoder('utf8').decode(raw);
 | 
			
		||||
 | 
			
		||||
        // split the message if it contains more lines
 | 
			
		||||
        const messages = stringToArray(message);
 | 
			
		||||
        this.messages.push(...messages);
 | 
			
		||||
      }).bind(this)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const { type, port } = this.serialConfig;
 | 
			
		||||
    const req = new StreamingOpenRequest();
 | 
			
		||||
    const monitorConfig = new GrpcMonitorConfig();
 | 
			
		||||
    monitorConfig.setType(this.mapType(type));
 | 
			
		||||
    monitorConfig.setTarget(port.address);
 | 
			
		||||
    if (this.serialConfig.baudRate !== undefined) {
 | 
			
		||||
      monitorConfig.setAdditionalConfig(
 | 
			
		||||
        Struct.fromJavaScript({ BaudRate: this.serialConfig.baudRate })
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    req.setConfig(monitorConfig);
 | 
			
		||||
 | 
			
		||||
    if (!this.serialConnection) {
 | 
			
		||||
      return await this.disconnect();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const writeTimeout = new Promise<Status>((resolve) => {
 | 
			
		||||
      setTimeout(async () => {
 | 
			
		||||
        resolve(Status.NOT_CONNECTED);
 | 
			
		||||
      }, 1000);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const writePromise = (serialConnection: any) => {
 | 
			
		||||
      return new Promise<Status>((resolve) => {
 | 
			
		||||
        serialConnection.duplex.write(req, () => {
 | 
			
		||||
          const boardName = this.serialConfig?.board
 | 
			
		||||
            ? Board.toString(this.serialConfig.board, {
 | 
			
		||||
              useFqbn: false,
 | 
			
		||||
            })
 | 
			
		||||
            : 'unknown board';
 | 
			
		||||
 | 
			
		||||
          const portName = this.serialConfig?.port
 | 
			
		||||
            ? this.serialConfig.port.address
 | 
			
		||||
            : 'unknown port';
 | 
			
		||||
          this.logger.info(
 | 
			
		||||
            `<<< Serial connection created for ${boardName} on port ${portName}.`
 | 
			
		||||
          );
 | 
			
		||||
          resolve(Status.OK);
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const status = await Promise.race([
 | 
			
		||||
      writeTimeout,
 | 
			
		||||
      writePromise(this.serialConnection),
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    if (status === Status.NOT_CONNECTED) {
 | 
			
		||||
      this.disconnect();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return status;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async disconnect(reason?: SerialError): Promise<Status> {
 | 
			
		||||
    return new Promise<Status>((resolve) => {
 | 
			
		||||
      try {
 | 
			
		||||
        if (this.onMessageReceived) {
 | 
			
		||||
          this.onMessageReceived.dispose();
 | 
			
		||||
          this.onMessageReceived = null;
 | 
			
		||||
        }
 | 
			
		||||
        if (this.flushMessagesInterval) {
 | 
			
		||||
          clearInterval(this.flushMessagesInterval);
 | 
			
		||||
          this.flushMessagesInterval = null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (
 | 
			
		||||
          !this.serialConnection &&
 | 
			
		||||
          reason &&
 | 
			
		||||
          reason.code === SerialError.ErrorCodes.CLIENT_CANCEL
 | 
			
		||||
        ) {
 | 
			
		||||
          resolve(Status.OK);
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        this.logger.info('>>> Disposing serial connection...');
 | 
			
		||||
        if (!this.serialConnection) {
 | 
			
		||||
          this.logger.warn('<<< Not connected. Nothing to dispose.');
 | 
			
		||||
          resolve(Status.NOT_CONNECTED);
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        const { duplex, config } = this.serialConnection;
 | 
			
		||||
 | 
			
		||||
        this.logger.info(
 | 
			
		||||
          `<<< Disposed serial connection for ${Board.toString(config.board, {
 | 
			
		||||
            useFqbn: false,
 | 
			
		||||
          })} on port ${config.port.address}.`
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        duplex.cancel();
 | 
			
		||||
      } finally {
 | 
			
		||||
        this.serialConnection = undefined;
 | 
			
		||||
        this.updateWsConfigParam({ connected: !!this.serialConnection });
 | 
			
		||||
        this.messages.length = 0;
 | 
			
		||||
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          resolve(Status.OK);
 | 
			
		||||
        }, 200);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async sendMessageToSerial(message: string): Promise<Status> {
 | 
			
		||||
    if (!this.serialConnection) {
 | 
			
		||||
      return Status.NOT_CONNECTED;
 | 
			
		||||
    }
 | 
			
		||||
    const req = new StreamingOpenRequest();
 | 
			
		||||
    req.setData(new TextEncoder().encode(message));
 | 
			
		||||
    return new Promise<Status>((resolve) => {
 | 
			
		||||
      if (this.serialConnection) {
 | 
			
		||||
        this.serialConnection.duplex.write(req, () => {
 | 
			
		||||
          resolve(Status.OK);
 | 
			
		||||
        });
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      this.disconnect().then(() => resolve(Status.NOT_CONNECTED));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected mapType(
 | 
			
		||||
    type?: SerialConfig.ConnectionType
 | 
			
		||||
  ): GrpcMonitorConfig.TargetType {
 | 
			
		||||
    switch (type) {
 | 
			
		||||
      case SerialConfig.ConnectionType.SERIAL:
 | 
			
		||||
        return GrpcMonitorConfig.TargetType.TARGET_TYPE_SERIAL;
 | 
			
		||||
      default:
 | 
			
		||||
        return GrpcMonitorConfig.TargetType.TARGET_TYPE_SERIAL;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// converts 'ab\nc\nd' => [ab\n,c\n,d]
 | 
			
		||||
function stringToArray(string: string, separator = '\n') {
 | 
			
		||||
  const retArray: string[] = [];
 | 
			
		||||
 | 
			
		||||
  let prevChar = separator;
 | 
			
		||||
 | 
			
		||||
  for (let i = 0; i < string.length; i++) {
 | 
			
		||||
    const currChar = string[i];
 | 
			
		||||
 | 
			
		||||
    if (prevChar === separator) {
 | 
			
		||||
      retArray.push(currChar);
 | 
			
		||||
    } else {
 | 
			
		||||
      const lastWord = retArray[retArray.length - 1];
 | 
			
		||||
      retArray[retArray.length - 1] = lastWord + currChar;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    prevChar = currChar;
 | 
			
		||||
  }
 | 
			
		||||
  return retArray;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
import { Emitter } from '@theia/core';
 | 
			
		||||
import { injectable } from 'inversify';
 | 
			
		||||
import { injectable } from '@theia/core/shared/inversify';
 | 
			
		||||
import * as WebSocket from 'ws';
 | 
			
		||||
import { WebSocketService } from './web-socket-service';
 | 
			
		||||
import { WebSocketProvider } from './web-socket-provider';
 | 
			
		||||
 | 
			
		||||
@injectable()
 | 
			
		||||
export default class WebSocketServiceImpl implements WebSocketService {
 | 
			
		||||
export default class WebSocketProviderImpl implements WebSocketProvider {
 | 
			
		||||
  protected wsClients: WebSocket[];
 | 
			
		||||
  protected server: WebSocket.Server;
 | 
			
		||||
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
import { Event } from '@theia/core/lib/common/event';
 | 
			
		||||
import * as WebSocket from 'ws';
 | 
			
		||||
 | 
			
		||||
export const WebSocketService = Symbol('WebSocketService');
 | 
			
		||||
export interface WebSocketService {
 | 
			
		||||
export const WebSocketProvider = Symbol('WebSocketProvider');
 | 
			
		||||
export interface WebSocketProvider {
 | 
			
		||||
  getAddress(): WebSocket.AddressInfo;
 | 
			
		||||
  sendMessage(message: string): void;
 | 
			
		||||
  onMessageReceived: Event<string>;
 | 
			
		||||
@@ -1,22 +0,0 @@
 | 
			
		||||
import { SerialConfig } from '../../../common/protocol/serial-service';
 | 
			
		||||
import { aBoard, anotherBoard, anotherPort, aPort } from './boards';
 | 
			
		||||
 | 
			
		||||
export const aSerialConfig: SerialConfig = {
 | 
			
		||||
  board: aBoard,
 | 
			
		||||
  port: aPort,
 | 
			
		||||
  baudRate: 9600,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const anotherSerialConfig: SerialConfig = {
 | 
			
		||||
  board: anotherBoard,
 | 
			
		||||
  port: anotherPort,
 | 
			
		||||
  baudRate: 9600,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export class WebSocketMock {
 | 
			
		||||
  readonly url: string;
 | 
			
		||||
  constructor(url: string) {
 | 
			
		||||
    this.url = url;
 | 
			
		||||
  }
 | 
			
		||||
  close() {}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,167 +0,0 @@
 | 
			
		||||
import { SerialServiceImpl } from './../../node/serial/serial-service-impl';
 | 
			
		||||
import { IMock, It, Mock } from 'typemoq';
 | 
			
		||||
import { createSandbox } from 'sinon';
 | 
			
		||||
import * as sinonChai from 'sinon-chai';
 | 
			
		||||
import { expect, use } from 'chai';
 | 
			
		||||
use(sinonChai);
 | 
			
		||||
 | 
			
		||||
import { ILogger } from '@theia/core/lib/common/logger';
 | 
			
		||||
import { MonitorClientProvider } from '../../node/serial/monitor-client-provider';
 | 
			
		||||
import { WebSocketService } from '../../node/web-socket/web-socket-service';
 | 
			
		||||
import { MonitorServiceClient } from '../../node/cli-protocol/cc/arduino/cli/monitor/v1/monitor_grpc_pb';
 | 
			
		||||
import { Status } from '../../common/protocol';
 | 
			
		||||
 | 
			
		||||
describe('SerialServiceImpl', () => {
 | 
			
		||||
  let subject: SerialServiceImpl;
 | 
			
		||||
 | 
			
		||||
  let logger: IMock<ILogger>;
 | 
			
		||||
  let serialClientProvider: IMock<MonitorClientProvider>;
 | 
			
		||||
  let webSocketService: IMock<WebSocketService>;
 | 
			
		||||
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    logger = Mock.ofType<ILogger>();
 | 
			
		||||
    logger.setup((b) => b.info(It.isAnyString()));
 | 
			
		||||
    logger.setup((b) => b.warn(It.isAnyString()));
 | 
			
		||||
    logger.setup((b) => b.error(It.isAnyString()));
 | 
			
		||||
 | 
			
		||||
    serialClientProvider = Mock.ofType<MonitorClientProvider>();
 | 
			
		||||
    webSocketService = Mock.ofType<WebSocketService>();
 | 
			
		||||
 | 
			
		||||
    subject = new SerialServiceImpl(
 | 
			
		||||
      logger.object,
 | 
			
		||||
      serialClientProvider.object,
 | 
			
		||||
      webSocketService.object
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  context('when a serial connection is requested', () => {
 | 
			
		||||
    const sandbox = createSandbox();
 | 
			
		||||
    beforeEach(() => {
 | 
			
		||||
      subject.uploadInProgress = false;
 | 
			
		||||
      sandbox.spy(subject, 'disconnect');
 | 
			
		||||
      sandbox.spy(subject, 'updateWsConfigParam');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(function () {
 | 
			
		||||
      sandbox.restore();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    context('and an upload is in progress', () => {
 | 
			
		||||
      beforeEach(async () => {
 | 
			
		||||
        subject.uploadInProgress = true;
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('should not change the connection status', async () => {
 | 
			
		||||
        await subject.connectSerialIfRequired();
 | 
			
		||||
        expect(subject.disconnect).to.have.callCount(0);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    context('and there is no upload in progress', () => {
 | 
			
		||||
      beforeEach(async () => {
 | 
			
		||||
        subject.uploadInProgress = false;
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      context('and there are 0 attached ws clients', () => {
 | 
			
		||||
        it('should disconnect', async () => {
 | 
			
		||||
          await subject.connectSerialIfRequired();
 | 
			
		||||
          expect(subject.disconnect).to.have.been.calledOnce;
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      context('and there are > 0 attached ws clients', () => {
 | 
			
		||||
        beforeEach(() => {
 | 
			
		||||
          webSocketService
 | 
			
		||||
            .setup((b) => b.getConnectedClientsNumber())
 | 
			
		||||
            .returns(() => 1);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('should not call the disconenct', async () => {
 | 
			
		||||
          await subject.connectSerialIfRequired();
 | 
			
		||||
          expect(subject.disconnect).to.have.callCount(0);
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  context('when a disconnection is requested', () => {
 | 
			
		||||
    const sandbox = createSandbox();
 | 
			
		||||
    beforeEach(() => { });
 | 
			
		||||
 | 
			
		||||
    afterEach(function () {
 | 
			
		||||
      sandbox.restore();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    context('and a serialConnection is not set', () => {
 | 
			
		||||
      it('should return a NOT_CONNECTED status', async () => {
 | 
			
		||||
        const status = await subject.disconnect();
 | 
			
		||||
        expect(status).to.be.equal(Status.NOT_CONNECTED);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    context('and a serialConnection is set', async () => {
 | 
			
		||||
      beforeEach(async () => {
 | 
			
		||||
        sandbox.spy(subject, 'updateWsConfigParam');
 | 
			
		||||
        await subject.disconnect();
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('should dispose the serialConnection', async () => {
 | 
			
		||||
        const serialConnectionOpen = await subject.isSerialPortOpen();
 | 
			
		||||
        expect(serialConnectionOpen).to.be.false;
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('should call updateWsConfigParam with disconnected status', async () => {
 | 
			
		||||
        expect(subject.updateWsConfigParam).to.be.calledWith({
 | 
			
		||||
          connected: false,
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  context('when a new config is passed in', () => {
 | 
			
		||||
    const sandbox = createSandbox();
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      subject.uploadInProgress = false;
 | 
			
		||||
      webSocketService
 | 
			
		||||
        .setup((b) => b.getConnectedClientsNumber())
 | 
			
		||||
        .returns(() => 1);
 | 
			
		||||
 | 
			
		||||
      serialClientProvider
 | 
			
		||||
        .setup((b) => b.client())
 | 
			
		||||
        .returns(async () => {
 | 
			
		||||
          return {
 | 
			
		||||
            streamingOpen: () => {
 | 
			
		||||
              return {
 | 
			
		||||
                on: (str: string, cb: any) => { },
 | 
			
		||||
                write: (chunk: any, cb: any) => {
 | 
			
		||||
                  cb();
 | 
			
		||||
                },
 | 
			
		||||
                cancel: () => { },
 | 
			
		||||
              };
 | 
			
		||||
            },
 | 
			
		||||
          } as MonitorServiceClient;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
      sandbox.spy(subject, 'disconnect');
 | 
			
		||||
 | 
			
		||||
      await subject.setSerialConfig({
 | 
			
		||||
        board: { name: 'test' },
 | 
			
		||||
        port: { id: 'test|test', address: 'test', addressLabel: 'test', protocol: 'test', protocolLabel: 'test' },
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(function () {
 | 
			
		||||
      sandbox.restore();
 | 
			
		||||
      subject.dispose();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should disconnect from previous connection', async () => {
 | 
			
		||||
      expect(subject.disconnect).to.be.called;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should create the serialConnection', async () => {
 | 
			
		||||
      const serialConnectionOpen = await subject.isSerialPortOpen();
 | 
			
		||||
      expect(serialConnectionOpen).to.be.true;
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
		Reference in New Issue
	
	Block a user