From 2b2ea7264348621d895a7ca082e2d8d2d6f84603 Mon Sep 17 00:00:00 2001 From: Francesco Stasi Date: Wed, 2 Mar 2022 17:55:11 +0100 Subject: [PATCH 01/35] backend structure WIP --- .../src/node/arduino-ide-backend-module.ts | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts index 223b52b8b..bd2dd868d 100644 --- a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts +++ b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts @@ -40,16 +40,8 @@ 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 { SerialServiceName } from './serial/serial-service-impl'; + 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'; @@ -205,19 +197,25 @@ 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(); + // 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( - SerialServicePath, - SerialService, - (service, client) => { - service.setClient(client); - client.onDidCloseConnection(() => service.dispose()); - return service; + bind(MonitorManagerProxyImpl).toSelf().inSingletonScope(); + bind(MonitorManagerProxy).toService(MonitorManagerProxyImpl); + bindBackendService( + 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; } ); }) From ebab0b226fea9e52d1533305797be010d17212ce Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Thu, 3 Mar 2022 10:52:27 +0100 Subject: [PATCH 02/35] Scaffold interfaces and classes for pluggable monitors --- .../browser/arduino-ide-frontend-module.ts | 6 ++++++ .../monitor-manager-proxy-client-impl.ts | 6 ++++++ .../src/common/monitor-manager-proxy.ts | 12 +++++++++++ .../src/node/arduino-ide-backend-module.ts | 3 +++ .../src/node/monitor-manager-proxy-impl.ts | 20 +++++++++++++++++++ .../src/node/monitor-manager.ts | 6 ++++++ 6 files changed, 53 insertions(+) create mode 100644 arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts create mode 100644 arduino-ide-extension/src/common/monitor-manager-proxy.ts create mode 100644 arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts create mode 100644 arduino-ide-extension/src/node/monitor-manager.ts diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index 5ceae9179..c06f9f7b2 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -275,6 +275,8 @@ import { IDEUpdaterDialogWidget, } from './dialogs/ide-updater/ide-updater-dialog'; import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider'; +import { MonitorManagerProxyClient } from '../common/monitor-manager-proxy'; +import { MonitorManagerProxyClientImpl } from './monitor-manager-proxy-client-impl'; const ElementQueries = require('css-element-queries/src/ElementQueries'); @@ -431,6 +433,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { // Serial service client to receive and delegate notifications from the backend. bind(SerialServiceClient).to(SerialServiceClientImpl).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); bind(WorkspaceVariableContribution).toSelf().inSingletonScope(); diff --git a/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts b/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts new file mode 100644 index 000000000..706027839 --- /dev/null +++ b/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts @@ -0,0 +1,6 @@ +import { injectable } from "@theia/core/shared/inversify"; +import { MonitorManagerProxyClient } from "../common/monitor-manager-proxy"; + +@injectable() +export class MonitorManagerProxyClientImpl implements MonitorManagerProxyClient { +} diff --git a/arduino-ide-extension/src/common/monitor-manager-proxy.ts b/arduino-ide-extension/src/common/monitor-manager-proxy.ts new file mode 100644 index 000000000..ba08b361b --- /dev/null +++ b/arduino-ide-extension/src/common/monitor-manager-proxy.ts @@ -0,0 +1,12 @@ +import { JsonRpcServer } from "@theia/core"; + +export const MonitorManagerProxyPath = '/services/monitor-manager-proxy'; +export const MonitorManagerProxy = Symbol('MonitorManagerProxy'); +export interface MonitorManagerProxy extends JsonRpcServer { + +} + +export const MonitorManagerProxyClient = Symbol('MonitorManagerProxyClient'); +export interface MonitorManagerProxyClient { + +} diff --git a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts index bd2dd868d..1646171ae 100644 --- a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts +++ b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts @@ -86,6 +86,9 @@ 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 } from './monitor-manager'; +import { MonitorManagerProxy, MonitorManagerProxyClient, MonitorManagerProxyPath } from '../common/monitor-manager-proxy'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(BackendApplication).toSelf().inSingletonScope(); diff --git a/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts b/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts new file mode 100644 index 000000000..814c7bc3d --- /dev/null +++ b/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts @@ -0,0 +1,20 @@ +import { inject, injectable } from "@theia/core/shared/inversify"; +import { MonitorManagerProxy, MonitorManagerProxyClient } from "../common/monitor-manager-proxy"; +import { MonitorManager } from "./monitor-manager"; + +@injectable() +export class MonitorManagerProxyImpl implements MonitorManagerProxy { + constructor( + @inject(MonitorManager) + protected readonly manager: MonitorManager, + ) { + } + + dispose(): void { + // TODO + } + + setClient(client: MonitorManagerProxyClient | undefined): void { + // TODO + } +} \ No newline at end of file diff --git a/arduino-ide-extension/src/node/monitor-manager.ts b/arduino-ide-extension/src/node/monitor-manager.ts new file mode 100644 index 000000000..1814c94af --- /dev/null +++ b/arduino-ide-extension/src/node/monitor-manager.ts @@ -0,0 +1,6 @@ +import { injectable } from "@theia/core/shared/inversify"; + +@injectable() +export class MonitorManager { + +} \ No newline at end of file From 3133b01c4a759299ca3a667f6802a8de47078107 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Fri, 4 Mar 2022 17:57:23 +0100 Subject: [PATCH 03/35] Implement MonitorService to handle pluggable monitor lifetime --- .../src/common/protocol/index.ts | 1 + .../src/common/protocol/monitor-service.ts | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 arduino-ide-extension/src/common/protocol/monitor-service.ts diff --git a/arduino-ide-extension/src/common/protocol/index.ts b/arduino-ide-extension/src/common/protocol/index.ts index 101905752..1a9a25c28 100644 --- a/arduino-ide-extension/src/common/protocol/index.ts +++ b/arduino-ide-extension/src/common/protocol/index.ts @@ -13,3 +13,4 @@ export * from './examples-service'; export * from './executable-service'; export * from './response-service'; export * from './notification-service'; +export * from './monitor-service'; diff --git a/arduino-ide-extension/src/common/protocol/monitor-service.ts b/arduino-ide-extension/src/common/protocol/monitor-service.ts new file mode 100644 index 000000000..d3d5c1edc --- /dev/null +++ b/arduino-ide-extension/src/common/protocol/monitor-service.ts @@ -0,0 +1,30 @@ +import { Event, JsonRpcServer } from "@theia/core"; +import { Board, Port } from './boards-service'; + +export const MonitorManagerProxyPath = '/services/monitor-manager-proxy'; +export const MonitorManagerProxy = Symbol('MonitorManagerProxy'); +export interface MonitorManagerProxy extends JsonRpcServer { + //set the monitor settings, which includes address, port and other monitor-specific settings + setMonitorSettings(board: Board, port: Port, settings: MonitorSettings): Promise; +} + +export const MonitorManagerProxyClient = Symbol('MonitorManagerProxyClient'); +export interface MonitorManagerProxyClient { + onWebSocketChanged: Event; + notifyWebSocketChanged(message: string): void; +} + +export interface MonitorSetting { + // 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 type MonitorSettings = Record; \ No newline at end of file From 750796d3a02479a55f68732f72d567b80ab7d433 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Fri, 4 Mar 2022 17:59:27 +0100 Subject: [PATCH 04/35] Rename WebSocketService to WebSocketProvider and uninjected it --- .../src/node/arduino-ide-backend-module.ts | 11 +++++------ .../src/node/serial/serial-service-impl.ts | 4 ++-- ...et-service-impl.ts => web-socket-provider-impl.ts} | 4 ++-- .../{web-socket-service.ts => web-socket-provider.ts} | 4 ++-- .../src/test/node/serial-service-impl.test.ts | 6 +++--- 5 files changed, 14 insertions(+), 15 deletions(-) rename arduino-ide-extension/src/node/web-socket/{web-socket-service-impl.ts => web-socket-provider-impl.ts} (91%) rename arduino-ide-extension/src/node/web-socket/{web-socket-service.ts => web-socket-provider.ts} (74%) diff --git a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts index 1646171ae..2e5e4b17c 100644 --- a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts +++ b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts @@ -82,13 +82,15 @@ 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 } from './monitor-manager'; -import { MonitorManagerProxy, MonitorManagerProxyClient, MonitorManagerProxyPath } from '../common/monitor-manager-proxy'; +import { + MonitorManagerProxy, + MonitorManagerProxyClient, + MonitorManagerProxyPath, +} from '../common/monitor-manager-proxy'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(BackendApplication).toSelf().inSingletonScope(); @@ -172,9 +174,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(); diff --git a/arduino-ide-extension/src/node/serial/serial-service-impl.ts b/arduino-ide-extension/src/node/serial/serial-service-impl.ts index db094d31e..5b6475c34 100644 --- a/arduino-ide-extension/src/node/serial/serial-service-impl.ts +++ b/arduino-ide-extension/src/node/serial/serial-service-impl.ts @@ -17,7 +17,7 @@ import { } 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 { WebSocketProvider } from '../web-socket/web-socket-provider'; import { SerialPlotter } from '../../browser/serial/plotter/protocol'; import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol'; @@ -86,7 +86,7 @@ export class SerialServiceImpl implements SerialService { @inject(MonitorClientProvider) protected readonly serialClientProvider: MonitorClientProvider, - @inject(WebSocketService) + @inject(WebSocketProvider) protected readonly webSocketService: WebSocketService ) { } diff --git a/arduino-ide-extension/src/node/web-socket/web-socket-service-impl.ts b/arduino-ide-extension/src/node/web-socket/web-socket-provider-impl.ts similarity index 91% rename from arduino-ide-extension/src/node/web-socket/web-socket-service-impl.ts rename to arduino-ide-extension/src/node/web-socket/web-socket-provider-impl.ts index 869c2cf8d..81d258a0b 100644 --- a/arduino-ide-extension/src/node/web-socket/web-socket-service-impl.ts +++ b/arduino-ide-extension/src/node/web-socket/web-socket-provider-impl.ts @@ -1,10 +1,10 @@ import { Emitter } from '@theia/core'; import { injectable } from '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; diff --git a/arduino-ide-extension/src/node/web-socket/web-socket-service.ts b/arduino-ide-extension/src/node/web-socket/web-socket-provider.ts similarity index 74% rename from arduino-ide-extension/src/node/web-socket/web-socket-service.ts rename to arduino-ide-extension/src/node/web-socket/web-socket-provider.ts index c793a07c4..6aa102040 100644 --- a/arduino-ide-extension/src/node/web-socket/web-socket-service.ts +++ b/arduino-ide-extension/src/node/web-socket/web-socket-provider.ts @@ -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; diff --git a/arduino-ide-extension/src/test/node/serial-service-impl.test.ts b/arduino-ide-extension/src/test/node/serial-service-impl.test.ts index 141c240a3..db77a8b87 100644 --- a/arduino-ide-extension/src/test/node/serial-service-impl.test.ts +++ b/arduino-ide-extension/src/test/node/serial-service-impl.test.ts @@ -7,7 +7,7 @@ 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 { WebSocketProvider } from '../../node/web-socket/web-socket-provider'; import { MonitorServiceClient } from '../../node/cli-protocol/cc/arduino/cli/monitor/v1/monitor_grpc_pb'; import { Status } from '../../common/protocol'; @@ -16,7 +16,7 @@ describe('SerialServiceImpl', () => { let logger: IMock; let serialClientProvider: IMock; - let webSocketService: IMock; + let webSocketService: IMock; beforeEach(() => { logger = Mock.ofType(); @@ -25,7 +25,7 @@ describe('SerialServiceImpl', () => { logger.setup((b) => b.error(It.isAnyString())); serialClientProvider = Mock.ofType(); - webSocketService = Mock.ofType(); + webSocketService = Mock.ofType(); subject = new SerialServiceImpl( logger.object, From 116b3d598417d15120d5fb055d3cddc775579688 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Fri, 4 Mar 2022 18:00:34 +0100 Subject: [PATCH 05/35] Moved some interfaces --- .../src/node/arduino-ide-backend-module.ts | 6 +- .../src/node/monitor-service.ts | 327 ++++++++++++++++++ 2 files changed, 328 insertions(+), 5 deletions(-) create mode 100644 arduino-ide-extension/src/node/monitor-service.ts diff --git a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts index 2e5e4b17c..6b3f585c8 100644 --- a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts +++ b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts @@ -86,11 +86,7 @@ import { ArduinoLocalizationContribution } from './arduino-localization-contribu import { LocalizationContribution } from '@theia/core/lib/node/i18n/localization-contribution'; import { MonitorManagerProxyImpl } from './monitor-manager-proxy-impl'; import { MonitorManager } from './monitor-manager'; -import { - MonitorManagerProxy, - MonitorManagerProxyClient, - MonitorManagerProxyPath, -} from '../common/monitor-manager-proxy'; +import { MonitorManagerProxy, MonitorManagerProxyClient, MonitorManagerProxyPath } from '../common/protocol/monitor-service'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(BackendApplication).toSelf().inSingletonScope(); diff --git a/arduino-ide-extension/src/node/monitor-service.ts b/arduino-ide-extension/src/node/monitor-service.ts new file mode 100644 index 000000000..cbb408f0f --- /dev/null +++ b/arduino-ide-extension/src/node/monitor-service.ts @@ -0,0 +1,327 @@ +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, MonitorSettings } from "../common/protocol"; +import { MonitorPortConfiguration, MonitorPortSetting, MonitorRequest, MonitorResponse } from "./cli-protocol/cc/arduino/cli/commands/v1/monitor_pb"; +import { CoreClientAware } 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 WebSocketProviderImpl from "./web-socket/web-socket-provider-impl"; + +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 | null; + + // Settings used by the currently running pluggable monitor. + // They can be freely modified while running. + protected settings: MonitorSettings; + + // List of messages received from the running pluggable monitor. + // These are flushed from time to time to the frontend. + protected messages: string[] = []; + + // Handles messages received from the frontend via websocket. + protected onMessageReceived?: Disposable; + + // Sends messages to the frontend from time to time. + protected flushMessagesInterval?: NodeJS.Timeout; + + // Triggered each time the number of clients connected + // to the this service WebSocket changes. + protected onWSClientsNumberChanged?: Disposable; + + // Used to notify that the monitor is being disposed + protected readonly onDisposeEmitter = new Emitter(); + readonly onDispose = this.onDisposeEmitter.event; + + protected readonly webSocketProvider: WebSocketProvider = new WebSocketProviderImpl(); + + constructor( + @inject(ILogger) + @named("monitor-service") + protected readonly logger: ILogger, + + private readonly board: Board, + private readonly port: Port, + ) { + 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(); + } + }); + } + + getWebsocketAddress(): 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. + * @returns a status to verify connection has been established. + */ + async start(): Promise { + if (this.duplex) { + return Status.ALREADY_CONNECTED; + } + + if (!this.board?.fqbn || !this.port?.address || !this.port?.protocol) { + return Status.CONFIG_MISSING + } + + this.logger.info("starting monitor"); + const coreClient = await this.coreClient(); + const { client, instance } = coreClient; + + this.duplex = client.monitor() + this.duplex + .on('close', () => { + this.logger.info(`monitor to ${this.port?.address} using ${this.port?.protocol} closed by client`) + }) + .on('end', () => { + 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(resolve => { + if (this.duplex?.write(req)) { + this.startMessagesHandlers(); + this.logger.info(`started monitor to ${this.port?.address} using ${this.port?.protocol}`) + resolve(Status.OK); + } + this.logger.warn(`failed starting monitor to ${this.port?.address} using ${this.port?.protocol}`) + resolve(Status.NOT_CONNECTED); + }); + + const connectTimeout = new Promise(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 when to the board/port combination + * the monitor is listening to. + * @returns + */ + async pause(): Promise { + return new Promise(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.cancel(); + this.duplex = null; + this.logger.info(`stopped monitor to ${this.port?.address} using ${this.port?.protocol}`) + resolve(); + }) + } + + /** + * Stop the monitor currently running + */ + async stop(): Promise { + return this.pause().finally( + this.stopMessagesHandlers + ); + } + + /** + * 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 { + if (!this.duplex) { + return Status.NOT_CONNECTED; + } + const coreClient = await this.coreClient(); + const { instance } = coreClient; + + const req = new MonitorRequest(); + req.setInstance(instance); + req.setTxData(new TextEncoder().encode(message)); + return new Promise(resolve => { + if (this.duplex) { + this.duplex?.write(req, () => { + resolve(Status.OK); + }); + return; + } + this.stop().then(() => resolve(Status.NOT_CONNECTED)); + }) + } + + /** + * Set monitor settings, if there is a running monitor they'll be sent + * to it, otherwise they'll be used when starting one. + * Only values in settings parameter will be change, other values won't + * be changed in any way. + * @param settings map of monitor settings to change + * @returns a status to verify settings have been sent. + */ + async changeSettings(settings: MonitorSettings): Promise { + 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; + } + 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: SerialPlotter.Protocol.Message = JSON.parse(msg); + + switch (message.command) { + case SerialPlotter.Protocol.Command.PLOTTER_SEND_MESSAGE: + this.send(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; + } + } + ) + } + } + + /** + * 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)/); +} From 2c95e7f0330c9c636cf02515d892aaa6c7438f04 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Fri, 4 Mar 2022 18:01:34 +0100 Subject: [PATCH 06/35] Changed upload settings --- .../browser/contributions/burn-bootloader.ts | 8 ++++- .../browser/contributions/upload-sketch.ts | 9 +++-- .../src/common/protocol/core-service.ts | 10 +++--- .../src/node/core-service-impl.ts | 35 +++++++++---------- 4 files changed, 35 insertions(+), 27 deletions(-) diff --git a/arduino-ide-extension/src/browser/contributions/burn-bootloader.ts b/arduino-ide-extension/src/browser/contributions/burn-bootloader.ts index 75aaef8fa..96953db7b 100644 --- a/arduino-ide-extension/src/browser/contributions/burn-bootloader.ts +++ b/arduino-ide-extension/src/browser/contributions/burn-bootloader.ts @@ -60,9 +60,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, diff --git a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts index df196cb7f..b86949fd3 100644 --- a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts @@ -226,6 +226,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 +252,7 @@ export class UploadSketch extends SketchContribution { const programmer = selectedProgrammer; options = { sketchUri, - fqbn, + board, optimizeForDebug, programmer, port, @@ -259,7 +264,7 @@ export class UploadSketch extends SketchContribution { } else { options = { sketchUri, - fqbn, + board, optimizeForDebug, port, verbose, diff --git a/arduino-ide-extension/src/common/protocol/core-service.ts b/arduino-ide-extension/src/common/protocol/core-service.ts index f8216f504..15aa85bb0 100644 --- a/arduino-ide-extension/src/common/protocol/core-service.ts +++ b/arduino-ide-extension/src/common/protocol/core-service.ts @@ -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; @@ -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; diff --git a/arduino-ide-extension/src/node/core-service-impl.ts b/arduino-ide-extension/src/node/core-service-impl.ts index 85a5af6a1..44cfcd636 100644 --- a/arduino-ide-extension/src/node/core-service-impl.ts +++ b/arduino-ide-extension/src/node/core-service-impl.ts @@ -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 { - 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 { 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); } } From 480492a7c8ad150d7be10be757d69741b913a240 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Fri, 4 Mar 2022 18:03:28 +0100 Subject: [PATCH 07/35] Enhance MonitorManager APIs --- .../monitor-manager-proxy-client-impl.ts | 7 + .../src/common/monitor-manager-proxy.ts | 12 -- .../src/node/monitor-manager-proxy-impl.ts | 45 ++++- .../src/node/monitor-manager.ts | 189 +++++++++++++++++- 4 files changed, 235 insertions(+), 18 deletions(-) delete mode 100644 arduino-ide-extension/src/common/monitor-manager-proxy.ts diff --git a/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts b/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts index 706027839..45956ce51 100644 --- a/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts +++ b/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts @@ -1,6 +1,13 @@ +import { Emitter } from "@theia/core"; import { injectable } from "@theia/core/shared/inversify"; import { MonitorManagerProxyClient } from "../common/monitor-manager-proxy"; @injectable() export class MonitorManagerProxyClientImpl implements MonitorManagerProxyClient { + protected readonly onWebSocketChangedEmitter = new Emitter(); + readonly onWebSocketChanged = this.onWebSocketChangedEmitter.event; + + notifyWebSocketChanged(message: number): void { + this.onWebSocketChangedEmitter.fire(message); + } } diff --git a/arduino-ide-extension/src/common/monitor-manager-proxy.ts b/arduino-ide-extension/src/common/monitor-manager-proxy.ts deleted file mode 100644 index ba08b361b..000000000 --- a/arduino-ide-extension/src/common/monitor-manager-proxy.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { JsonRpcServer } from "@theia/core"; - -export const MonitorManagerProxyPath = '/services/monitor-manager-proxy'; -export const MonitorManagerProxy = Symbol('MonitorManagerProxy'); -export interface MonitorManagerProxy extends JsonRpcServer { - -} - -export const MonitorManagerProxyClient = Symbol('MonitorManagerProxyClient'); -export interface MonitorManagerProxyClient { - -} diff --git a/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts b/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts index 814c7bc3d..d582ea09a 100644 --- a/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts +++ b/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts @@ -1,20 +1,57 @@ -import { inject, injectable } from "@theia/core/shared/inversify"; -import { MonitorManagerProxy, MonitorManagerProxyClient } from "../common/monitor-manager-proxy"; +import { Emitter, ILogger } from "@theia/core"; +import { inject, injectable, named } from "@theia/core/shared/inversify"; +import { Disposable } from "@theia/core/shared/vscode-languageserver-protocol"; +import { MonitorManagerProxy, MonitorManagerProxyClient, MonitorSettings, Status } from "../common/protocol"; +import { Board, Port } from "../common/protocol"; import { MonitorManager } from "./monitor-manager"; @injectable() export class MonitorManagerProxyImpl implements MonitorManagerProxy { + protected client: MonitorManagerProxyClient; + + protected selectedBoard: Board | null; + protected selectedPort: Port | null; + constructor( + @inject(ILogger) + @named("monitor-manager-proxy") + protected readonly logger: ILogger, + @inject(MonitorManager) protected readonly manager: MonitorManager, ) { } dispose(): void { - // TODO + // NOOP + } + + + // setMonitorConfig is called by the FE when trying to establish a monitor connection to a board or when changing some + // settings (such as the baudrate, when available) + async setMonitorSettings(board: Board, port: Port, settings: MonitorSettings): Promise { + + // check if it's a different connection or a change in the settings + if (board === this.selectedBoard && port === this.selectedPort) { + + // TODO: update the settings + return; + } + + const startStatus: Status = await this.manager.startMonitor(board, port); + + if (startStatus === Status.ALREADY_CONNECTED || startStatus === Status.OK) { + this.client.notifyWebSocketChanged(this.manager.getWebsocketAddress(board, port)); + } + } setClient(client: MonitorManagerProxyClient | undefined): void { - // TODO + if (!client) { + return; + } + this.client = client; + } + } \ No newline at end of file diff --git a/arduino-ide-extension/src/node/monitor-manager.ts b/arduino-ide-extension/src/node/monitor-manager.ts index 1814c94af..62434bdcd 100644 --- a/arduino-ide-extension/src/node/monitor-manager.ts +++ b/arduino-ide-extension/src/node/monitor-manager.ts @@ -1,6 +1,191 @@ -import { injectable } from "@theia/core/shared/inversify"; +import { Emitter, ILogger } from "@theia/core"; +import { inject, injectable, named } from "@theia/core/shared/inversify"; +import { Board, Port, Status, MonitorSetting, MonitorSettings } from "../common/protocol"; +import { EnumerateMonitorPortSettingsRequest, EnumerateMonitorPortSettingsResponse } from "./cli-protocol/cc/arduino/cli/commands/v1/monitor_pb"; +import { CoreClientAware } from "./core-client-provider"; +import { MonitorService } from "./monitor-service"; + +type MonitorID = string; @injectable() -export class MonitorManager { +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(); + + // Used to notify a monitor service that an upload process started + // to the board/port combination it manages + protected readonly onUploadStartedEmitter = new Emitter<{ board: Board, port: Port }>(); + readonly onUploadStarted = this.onUploadStartedEmitter.event; + + // Used to notify a monitor service that an upload process finished + // to the board/port combination it manages + + + + constructor( + @inject(ILogger) + @named('monitor-manager') + protected readonly logger: ILogger, + ) { + super(); + } + + /** + * 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 + */ + async portMonitorSettings(protocol: string, fqbn: string): Promise { + 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((resolve, reject) => { + client.enumerateMonitorPortSettings(req, (err, resp) => { + if (!!err) { + reject(err) + } + resolve(resp) + }) + }) + + let settings: MonitorSettings = {}; + 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; + } + + /** + * + * @param board + * @param port + */ + async startMonitor(board: Board, port: Port): Promise { + const monitorID = this.monitorID(board, port); + let monitor = this.monitorServices.get(monitorID); + if (!monitor) { + monitor = this.createMonitor(board, port) + } + return await monitor.start(); + // TODO: I need to return the address here right? + } + + async stopMonitor(board: Board, port: Port): Promise { + 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(); + } + + getWebsocketAddress(board: Board, port: Port): number { + const monitorID = this.monitorID(board, port); + + const monitor = this.monitorServices.get(monitorID); + if (!monitor) { + return -1; + } + return monitor.getWebsocketAddress(); + } + + /** + * 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 + * @param port + */ + async notifyUploadStarted(board?: Board, port?: Port): Promise { + 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; + } + 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 + * @param port + * @returns + */ + async notifyUploadFinished(board?: Board, port?: Port): Promise { + 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; + } + return await monitor.start(); + } + + /** + * + * @param board + * @param port + * @param settings map of monitor settings to change + */ + changeMonitorSettings(board: Board, port: Port, settings: Record) { + const monitorID = this.monitorID(board, port); + let monitor = this.monitorServices.get(monitorID); + if (!monitor) { + monitor = this.createMonitor(board, port) + monitor.changeSettings(settings); + } + } + + private createMonitor(board: Board, port: Port): MonitorService { + const monitorID = this.monitorID(board, port); + const monitor = new MonitorService( + this.logger, + board, + port + ); + 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}`; + } } \ No newline at end of file From c5695d3a76d1efb4b2ad3fd52cb9d5e11f4c6b3f Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Mon, 7 Mar 2022 10:29:08 +0100 Subject: [PATCH 08/35] Fixed WebSocketChange event signature --- .../src/browser/monitor-manager-proxy-client-impl.ts | 2 +- arduino-ide-extension/src/common/protocol/monitor-service.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts b/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts index 45956ce51..76425a4e7 100644 --- a/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts +++ b/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts @@ -1,6 +1,6 @@ import { Emitter } from "@theia/core"; import { injectable } from "@theia/core/shared/inversify"; -import { MonitorManagerProxyClient } from "../common/monitor-manager-proxy"; +import { MonitorManagerProxyClient } from "../common/protocol/monitor-service"; @injectable() export class MonitorManagerProxyClientImpl implements MonitorManagerProxyClient { diff --git a/arduino-ide-extension/src/common/protocol/monitor-service.ts b/arduino-ide-extension/src/common/protocol/monitor-service.ts index d3d5c1edc..0847068c3 100644 --- a/arduino-ide-extension/src/common/protocol/monitor-service.ts +++ b/arduino-ide-extension/src/common/protocol/monitor-service.ts @@ -10,8 +10,8 @@ export interface MonitorManagerProxy extends JsonRpcServer; - notifyWebSocketChanged(message: string): void; + onWebSocketChanged: Event; + notifyWebSocketChanged(message: number): void; } export interface MonitorSetting { From 61b8bdeec9323de581cca8f3fe0642a3d2af7214 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Mon, 7 Mar 2022 14:45:48 +0100 Subject: [PATCH 09/35] Add monitor proxy functions for the frontend --- .../src/common/protocol/monitor-service.ts | 6 +- .../src/node/monitor-manager-proxy-impl.ts | 68 ++++++++++----- .../src/node/monitor-manager.ts | 84 ++++++++++++------- .../src/node/monitor-service.ts | 2 +- 4 files changed, 109 insertions(+), 51 deletions(-) diff --git a/arduino-ide-extension/src/common/protocol/monitor-service.ts b/arduino-ide-extension/src/common/protocol/monitor-service.ts index 0847068c3..11217ca57 100644 --- a/arduino-ide-extension/src/common/protocol/monitor-service.ts +++ b/arduino-ide-extension/src/common/protocol/monitor-service.ts @@ -4,8 +4,10 @@ import { Board, Port } from './boards-service'; export const MonitorManagerProxyPath = '/services/monitor-manager-proxy'; export const MonitorManagerProxy = Symbol('MonitorManagerProxy'); export interface MonitorManagerProxy extends JsonRpcServer { - //set the monitor settings, which includes address, port and other monitor-specific settings - setMonitorSettings(board: Board, port: Port, settings: MonitorSettings): Promise; + startMonitor(board: Board, port: Port, settings?: MonitorSettings): Promise; + changeMonitorSettings(board: Board, port: Port, settings: MonitorSettings): Promise; + stopMonitor(board: Board, port: Port): Promise; + getSupportedSettings(protocol: string, fqbn: string): Promise; } export const MonitorManagerProxyClient = Symbol('MonitorManagerProxyClient'); diff --git a/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts b/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts index d582ea09a..2291228eb 100644 --- a/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts +++ b/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts @@ -1,6 +1,5 @@ -import { Emitter, ILogger } from "@theia/core"; +import { ILogger } from "@theia/core"; import { inject, injectable, named } from "@theia/core/shared/inversify"; -import { Disposable } from "@theia/core/shared/vscode-languageserver-protocol"; import { MonitorManagerProxy, MonitorManagerProxyClient, MonitorSettings, Status } from "../common/protocol"; import { Board, Port } from "../common/protocol"; import { MonitorManager } from "./monitor-manager"; @@ -9,9 +8,6 @@ import { MonitorManager } from "./monitor-manager"; export class MonitorManagerProxyImpl implements MonitorManagerProxy { protected client: MonitorManagerProxyClient; - protected selectedBoard: Board | null; - protected selectedPort: Port | null; - constructor( @inject(ILogger) @named("monitor-manager-proxy") @@ -26,24 +22,58 @@ export class MonitorManagerProxyImpl implements MonitorManagerProxy { // NOOP } + /** + * 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?: MonitorSettings): Promise { + if (settings) { + await this.changeMonitorSettings(board, port, settings); + } + const status = await this.manager.startMonitor(board, port); + if (status === Status.ALREADY_CONNECTED || status === Status.OK) { + this.client.notifyWebSocketChanged(this.manager.getWebsocketAddressPort(board, port)); + } + } - // setMonitorConfig is called by the FE when trying to establish a monitor connection to a board or when changing some - // settings (such as the baudrate, when available) - async setMonitorSettings(board: Board, port: Port, settings: MonitorSettings): Promise { - - // check if it's a different connection or a change in the settings - if (board === this.selectedBoard && port === this.selectedPort) { - - // TODO: update the settings + /** + * 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: MonitorSettings): Promise { + if (!this.manager.isStarted(board, port)) { + // Monitor is not running, no need to change settings return; } + return this.manager.changeMonitorSettings(board, port, settings); + } - const startStatus: Status = await this.manager.startMonitor(board, port); - - if (startStatus === Status.ALREADY_CONNECTED || startStatus === Status.OK) { - this.client.notifyWebSocketChanged(this.manager.getWebsocketAddress(board, port)); - } + /** + * Stops a running pluggable monitor. + * @param board board connected to port + * @param port port monitored + */ + async stopMonitor(board: Board, port: Port): Promise { + return this.manager.stopMonitor(board, port); + } + /** + * Returns the settings supported by the pluggable monitor for the specified + * protocol, the fqbn is necessary since it's used to tell different monitors + * using the same protocol. + * @param protocol protocol of a pluggable monitor + * @param fqbn unique ID of a board + * @returns a map of MonitorSetting + */ + async getSupportedSettings(protocol: string, fqbn: string): Promise { + return this.manager.portMonitorSettings(protocol, fqbn); } setClient(client: MonitorManagerProxyClient | undefined): void { @@ -51,7 +81,5 @@ export class MonitorManagerProxyImpl implements MonitorManagerProxy { return; } this.client = client; - } - } \ No newline at end of file diff --git a/arduino-ide-extension/src/node/monitor-manager.ts b/arduino-ide-extension/src/node/monitor-manager.ts index 62434bdcd..a1627d3c6 100644 --- a/arduino-ide-extension/src/node/monitor-manager.ts +++ b/arduino-ide-extension/src/node/monitor-manager.ts @@ -15,16 +15,6 @@ export class MonitorManager extends CoreClientAware { // be started. private monitorServices = new Map(); - // Used to notify a monitor service that an upload process started - // to the board/port combination it manages - protected readonly onUploadStartedEmitter = new Emitter<{ board: Board, port: Port }>(); - readonly onUploadStarted = this.onUploadStartedEmitter.event; - - // Used to notify a monitor service that an upload process finished - // to the board/port combination it manages - - - constructor( @inject(ILogger) @named('monitor-manager') @@ -55,7 +45,7 @@ export class MonitorManager extends CoreClientAware { } resolve(resp) }) - }) + }); let settings: MonitorSettings = {}; for (const iterator of res.getSettingsList()) { @@ -71,9 +61,28 @@ export class MonitorManager extends CoreClientAware { } /** - * - * @param board - * @param port + * 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 { const monitorID = this.monitorID(board, port); @@ -82,12 +91,16 @@ export class MonitorManager extends CoreClientAware { monitor = this.createMonitor(board, port) } return await monitor.start(); - // TODO: I need to return the address here right? } + /** + * 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 { const monitorID = this.monitorID(board, port); - const monitor = this.monitorServices.get(monitorID); if (!monitor) { // There's no monitor to stop, bail @@ -96,14 +109,20 @@ export class MonitorManager extends CoreClientAware { return await monitor.stop(); } - getWebsocketAddress(board: Board, port: Port): number { + /** + * 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.getWebsocketAddress(); + return monitor.getWebsocketAddressPort(); } /** @@ -111,8 +130,8 @@ export class MonitorManager extends CoreClientAware { * 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 - * @param port + * @param board board connected to port + * @param port port to monitor */ async notifyUploadStarted(board?: Board, port?: Port): Promise { if (!board || !port) { @@ -132,9 +151,10 @@ export class MonitorManager extends CoreClientAware { /** * Notifies the monitor service of that board/port combination * that an upload process started on that exact board/port combination. - * @param board - * @param port - * @returns + * @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 { if (!board || !port) { @@ -152,10 +172,11 @@ export class MonitorManager extends CoreClientAware { } /** - * - * @param board - * @param port - * @param settings map of monitor settings to change + * 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: Record) { const monitorID = this.monitorID(board, port); @@ -166,6 +187,13 @@ export class MonitorManager extends CoreClientAware { } } + /** + * 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 = new MonitorService( diff --git a/arduino-ide-extension/src/node/monitor-service.ts b/arduino-ide-extension/src/node/monitor-service.ts index cbb408f0f..a5459cf82 100644 --- a/arduino-ide-extension/src/node/monitor-service.ts +++ b/arduino-ide-extension/src/node/monitor-service.ts @@ -57,7 +57,7 @@ export class MonitorService extends CoreClientAware implements Disposable { }); } - getWebsocketAddress(): number { + getWebsocketAddressPort(): number { return this.webSocketProvider.getAddress().port; } From 31b704cdb9f8450ad90ed65eafe280d638f94254 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Tue, 8 Mar 2022 17:14:31 +0100 Subject: [PATCH 10/35] Moved settings to MonitorService --- .../src/node/monitor-manager.ts | 60 ++++++------------- .../src/node/monitor-service.ts | 54 ++++++++++++++++- 2 files changed, 71 insertions(+), 43 deletions(-) diff --git a/arduino-ide-extension/src/node/monitor-manager.ts b/arduino-ide-extension/src/node/monitor-manager.ts index a1627d3c6..1f38bac8c 100644 --- a/arduino-ide-extension/src/node/monitor-manager.ts +++ b/arduino-ide-extension/src/node/monitor-manager.ts @@ -1,7 +1,6 @@ -import { Emitter, ILogger } from "@theia/core"; +import { ILogger } from "@theia/core"; import { inject, injectable, named } from "@theia/core/shared/inversify"; -import { Board, Port, Status, MonitorSetting, MonitorSettings } from "../common/protocol"; -import { EnumerateMonitorPortSettingsRequest, EnumerateMonitorPortSettingsResponse } from "./cli-protocol/cc/arduino/cli/commands/v1/monitor_pb"; +import { Board, Port, Status, MonitorSettings } from "../common/protocol"; import { CoreClientAware } from "./core-client-provider"; import { MonitorService } from "./monitor-service"; @@ -23,43 +22,6 @@ export class MonitorManager extends CoreClientAware { super(); } - /** - * 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 - */ - async portMonitorSettings(protocol: string, fqbn: string): Promise { - 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((resolve, reject) => { - client.enumerateMonitorPortSettings(req, (err, resp) => { - if (!!err) { - reject(err) - } - resolve(resp) - }) - }); - - let settings: MonitorSettings = {}; - 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; - } - /** * Used to know if a monitor is started * @param board board connected to port @@ -178,7 +140,7 @@ export class MonitorManager extends CoreClientAware { * @param port port to monitor * @param settings monitor settings to change */ - changeMonitorSettings(board: Board, port: Port, settings: Record) { + changeMonitorSettings(board: Board, port: Port, settings: MonitorSettings) { const monitorID = this.monitorID(board, port); let monitor = this.monitorServices.get(monitorID); if (!monitor) { @@ -187,6 +149,22 @@ export class MonitorManager extends CoreClientAware { } } + /** + * 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): MonitorSettings { + const monitorID = this.monitorID(board, port); + const monitor = this.monitorServices.get(monitorID); + if (!monitor) { + return {}; + } + return monitor.currentSettings(); + } + /** * Creates a MonitorService that handles the lifetime and the * communication via WebSocket with the frontend. diff --git a/arduino-ide-extension/src/node/monitor-service.ts b/arduino-ide-extension/src/node/monitor-service.ts index a5459cf82..f843565b1 100644 --- a/arduino-ide-extension/src/node/monitor-service.ts +++ b/arduino-ide-extension/src/node/monitor-service.ts @@ -1,8 +1,8 @@ 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, MonitorSettings } from "../common/protocol"; -import { MonitorPortConfiguration, MonitorPortSetting, MonitorRequest, MonitorResponse } from "./cli-protocol/cc/arduino/cli/commands/v1/monitor_pb"; +import { Board, Port, Status, MonitorSettings, Monitor } from "../common/protocol"; +import { EnumerateMonitorPortSettingsRequest, EnumerateMonitorPortSettingsResponse, MonitorPortConfiguration, MonitorPortSetting, MonitorRequest, MonitorResponse } from "./cli-protocol/cc/arduino/cli/commands/v1/monitor_pb"; import { CoreClientAware } 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' @@ -55,6 +55,11 @@ export class MonitorService extends CoreClientAware implements Disposable { this.dispose(); } }); + + // Sets default settings for this monitor + this.portMonitorSettings(port.protocol, board.fqbn!).then( + settings => this.settings = settings + ); } getWebsocketAddressPort(): number { @@ -222,6 +227,51 @@ export class MonitorService extends CoreClientAware implements Disposable { }) } + /** + * + * @returns map of current monitor settings + */ + currentSettings(): MonitorSettings { + return this.settings; + } + + /** + * 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 { + 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((resolve, reject) => { + client.enumerateMonitorPortSettings(req, (err, resp) => { + if (!!err) { + reject(err) + } + resolve(resp) + }) + }); + + let settings: MonitorSettings = {}; + 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. From 9058abb01597c963aab3897b2f963e2c936286f2 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Thu, 10 Mar 2022 15:48:55 +0100 Subject: [PATCH 11/35] Remove several unnecessary serial monitor classes --- .../serial/serial-connection-manager.ts | 360 ---------------- .../src/browser/serial/serial-model.ts | 163 ------- .../serial/serial-service-client-impl.ts | 48 --- .../src/common/protocol/serial-service.ts | 102 ----- .../node/serial/monitor-client-provider.ts | 26 -- .../src/node/serial/serial-service-impl.ts | 397 ------------------ .../src/test/node/serial-service-impl.test.ts | 167 -------- 7 files changed, 1263 deletions(-) delete mode 100644 arduino-ide-extension/src/browser/serial/serial-connection-manager.ts delete mode 100644 arduino-ide-extension/src/browser/serial/serial-model.ts delete mode 100644 arduino-ide-extension/src/browser/serial/serial-service-client-impl.ts delete mode 100644 arduino-ide-extension/src/common/protocol/serial-service.ts delete mode 100644 arduino-ide-extension/src/node/serial/monitor-client-provider.ts delete mode 100644 arduino-ide-extension/src/node/serial/serial-service-impl.ts delete mode 100644 arduino-ide-extension/src/test/node/serial-service-impl.test.ts diff --git a/arduino-ide-extension/src/browser/serial/serial-connection-manager.ts b/arduino-ide-extension/src/browser/serial/serial-connection-manager.ts deleted file mode 100644 index e3fb2476e..000000000 --- a/arduino-ide-extension/src/browser/serial/serial-connection-manager.ts +++ /dev/null @@ -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 = { - board: undefined, - port: undefined, - baudRate: undefined, - }; - - protected readonly onConnectionChangedEmitter = new Emitter(); - - /** - * 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): Promise { - 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 { - 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 { - 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 { - 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 { - 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 { - if (!(await this.serialService.isSerialPortOpen())) { - return Status.NOT_CONNECTED; - } - return new Promise((resolve) => { - this.serialService - .sendMessageToSerial(data + this.serialModel.lineEnding) - .then(() => resolve(Status.OK)); - }); - } - - get onConnectionChanged(): Event { - return this.onConnectionChangedEmitter.event; - } - - get onRead(): Event<{ messages: any }> { - return this.onReadEmitter.event; - } - - protected async handleBoardConfigChange( - boardsConfig: BoardsConfig.Config - ): Promise { - const { selectedBoard: board, selectedPort: port } = boardsConfig; - const { baudRate } = this.serialModel; - const newConfig: Partial = { board, port, baudRate }; - this.setConfig(newConfig); - } -} - -export namespace Serial { - export namespace Config { - export function toString(config: Partial): string { - if (!isSerialConfig(config)) return ''; - const { board, port } = config; - return `${Board.toString(board)} ${port.address}`; - } - } -} - -function isSerialConfig(config: Partial): config is SerialConfig { - return !!config.board && !!config.baudRate && !!config.port; -} diff --git a/arduino-ide-extension/src/browser/serial/serial-model.ts b/arduino-ide-extension/src/browser/serial/serial-model.ts deleted file mode 100644 index fc6e352ec..000000000 --- a/arduino-ide-extension/src/browser/serial/serial-model.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { injectable, inject } from 'inversify'; -import { Emitter, Event } from '@theia/core/lib/common/event'; -import { SerialConfig } from '../../common/protocol'; -import { - FrontendApplicationContribution, - LocalStorageService, -} from '@theia/core/lib/browser'; -import { BoardsServiceProvider } from '../boards/boards-service-provider'; - -@injectable() -export class SerialModel implements FrontendApplicationContribution { - protected static STORAGE_ID = 'arduino-serial-model'; - - @inject(LocalStorageService) - protected readonly localStorageService: LocalStorageService; - - @inject(BoardsServiceProvider) - protected readonly boardsServiceClient: BoardsServiceProvider; - - protected readonly onChangeEmitter: Emitter< - SerialModel.State.Change - >; - protected _autoscroll: boolean; - protected _timestamp: boolean; - protected _baudRate: SerialConfig.BaudRate; - protected _lineEnding: SerialModel.EOL; - protected _interpolate: boolean; - - constructor() { - this._autoscroll = true; - this._timestamp = false; - this._baudRate = SerialConfig.BaudRate.DEFAULT; - this._lineEnding = SerialModel.EOL.DEFAULT; - this._interpolate = false; - this.onChangeEmitter = new Emitter< - SerialModel.State.Change - >(); - } - - onStart(): void { - this.localStorageService - .getData(SerialModel.STORAGE_ID) - .then((state) => { - if (state) { - this.restoreState(state); - } - }); - } - - get onChange(): Event> { - return this.onChangeEmitter.event; - } - - get autoscroll(): boolean { - return this._autoscroll; - } - - toggleAutoscroll(): void { - this._autoscroll = !this._autoscroll; - this.storeState(); - this.storeState().then(() => - this.onChangeEmitter.fire({ - property: 'autoscroll', - value: this._autoscroll, - }) - ); - } - - get timestamp(): boolean { - return this._timestamp; - } - - toggleTimestamp(): void { - this._timestamp = !this._timestamp; - this.storeState().then(() => - this.onChangeEmitter.fire({ - property: 'timestamp', - value: this._timestamp, - }) - ); - } - - get baudRate(): SerialConfig.BaudRate { - return this._baudRate; - } - - set baudRate(baudRate: SerialConfig.BaudRate) { - this._baudRate = baudRate; - this.storeState().then(() => - this.onChangeEmitter.fire({ - property: 'baudRate', - value: this._baudRate, - }) - ); - } - - get lineEnding(): SerialModel.EOL { - return this._lineEnding; - } - - set lineEnding(lineEnding: SerialModel.EOL) { - this._lineEnding = lineEnding; - this.storeState().then(() => - this.onChangeEmitter.fire({ - property: 'lineEnding', - value: this._lineEnding, - }) - ); - } - - get interpolate(): boolean { - return this._interpolate; - } - - set interpolate(i: boolean) { - this._interpolate = i; - this.storeState().then(() => - this.onChangeEmitter.fire({ - property: 'interpolate', - value: this._interpolate, - }) - ); - } - - protected restoreState(state: SerialModel.State): void { - this._autoscroll = state.autoscroll; - this._timestamp = state.timestamp; - this._baudRate = state.baudRate; - this._lineEnding = state.lineEnding; - this._interpolate = state.interpolate; - } - - protected async storeState(): Promise { - return this.localStorageService.setData(SerialModel.STORAGE_ID, { - autoscroll: this._autoscroll, - timestamp: this._timestamp, - baudRate: this._baudRate, - lineEnding: this._lineEnding, - interpolate: this._interpolate, - }); - } -} - -export namespace SerialModel { - export interface State { - autoscroll: boolean; - timestamp: boolean; - baudRate: SerialConfig.BaudRate; - lineEnding: EOL; - interpolate: boolean; - } - export namespace State { - export interface Change { - readonly property: K; - readonly value: State[K]; - } - } - - export type EOL = '' | '\n' | '\r' | '\r\n'; - export namespace EOL { - export const DEFAULT: EOL = '\n'; - } -} diff --git a/arduino-ide-extension/src/browser/serial/serial-service-client-impl.ts b/arduino-ide-extension/src/browser/serial/serial-service-client-impl.ts deleted file mode 100644 index 5a025fcf5..000000000 --- a/arduino-ide-extension/src/browser/serial/serial-service-client-impl.ts +++ /dev/null @@ -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(); - readonly onError = this.onErrorEmitter.event; - - protected readonly onWebSocketChangedEmitter = new Emitter(); - readonly onWebSocketChanged = this.onWebSocketChangedEmitter.event; - - protected readonly onBaudRateChangedEmitter = - new Emitter(); - readonly onBaudRateChanged = this.onBaudRateChangedEmitter.event; - - protected readonly onLineEndingChangedEmitter = - new Emitter(); - readonly onLineEndingChanged = this.onLineEndingChangedEmitter.event; - - protected readonly onInterpolateChangedEmitter = new Emitter(); - 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); - } -} diff --git a/arduino-ide-extension/src/common/protocol/serial-service.ts b/arduino-ide-extension/src/common/protocol/serial-service.ts deleted file mode 100644 index 0e77bb9cc..000000000 --- a/arduino-ide-extension/src/common/protocol/serial-service.ts +++ /dev/null @@ -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 { - clientsAttached(): Promise; - setSerialConfig(config: SerialConfig): Promise; - sendMessageToSerial(message: string): Promise; - updateWsConfigParam(config: Partial): Promise; - isSerialPortOpen(): Promise; - connectSerialIfRequired(): Promise; - disconnect(reason?: SerialError): Promise; - 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; - onWebSocketChanged: Event; - onLineEndingChanged: Event; - onBaudRateChanged: Event; - onInterpolateChanged: Event; - 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; - } -} diff --git a/arduino-ide-extension/src/node/serial/monitor-client-provider.ts b/arduino-ide-extension/src/node/serial/monitor-client-provider.ts deleted file mode 100644 index d73c98cfe..000000000 --- a/arduino-ide-extension/src/node/serial/monitor-client-provider.ts +++ /dev/null @@ -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 { - 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(); - } -} diff --git a/arduino-ide-extension/src/node/serial/serial-service-impl.ts b/arduino-ide-extension/src/node/serial/serial-service-impl.ts deleted file mode 100644 index 5b6475c34..000000000 --- a/arduino-ide-extension/src/node/serial/serial-service-impl.ts +++ /dev/null @@ -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 { WebSocketProvider } from '../web-socket/web-socket-provider'; -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(); - 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; - 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(WebSocketProvider) - protected readonly webSocketService: WebSocketService - ) { } - - async isSerialPortOpen(): Promise { - 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 { - return this.webSocketService.getConnectedClientsNumber.bind( - this.webSocketService - )(); - } - - public async connectSerialIfRequired(): Promise { - 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 { - this.serialConfig = config; - await this.disconnect(); - await this.connectSerialIfRequired(); - } - - async updateWsConfigParam( - config: Partial - ): Promise { - const msg: SerialPlotter.Protocol.Message = { - command: SerialPlotter.Protocol.Command.MIDDLEWARE_CONFIG_CHANGED, - data: config, - }; - this.webSocketService.sendMessage(JSON.stringify(msg)); - } - - private async connect(): Promise { - 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((resolve) => { - setTimeout(async () => { - resolve(Status.NOT_CONNECTED); - }, 1000); - }); - - const writePromise = (serialConnection: any) => { - return new Promise((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 { - return new Promise((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 { - if (!this.serialConnection) { - return Status.NOT_CONNECTED; - } - const req = new StreamingOpenRequest(); - req.setData(new TextEncoder().encode(message)); - return new Promise((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; -} diff --git a/arduino-ide-extension/src/test/node/serial-service-impl.test.ts b/arduino-ide-extension/src/test/node/serial-service-impl.test.ts deleted file mode 100644 index db77a8b87..000000000 --- a/arduino-ide-extension/src/test/node/serial-service-impl.test.ts +++ /dev/null @@ -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 { WebSocketProvider } from '../../node/web-socket/web-socket-provider'; -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; - let serialClientProvider: IMock; - let webSocketService: IMock; - - beforeEach(() => { - logger = Mock.ofType(); - logger.setup((b) => b.info(It.isAnyString())); - logger.setup((b) => b.warn(It.isAnyString())); - logger.setup((b) => b.error(It.isAnyString())); - - serialClientProvider = Mock.ofType(); - webSocketService = Mock.ofType(); - - 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; - }); - }); -}); From ee265aec9048c30692a474901a10f526fe18a36e Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Thu, 10 Mar 2022 16:07:10 +0100 Subject: [PATCH 12/35] Changed how connection is handled on upload --- .../browser/contributions/burn-bootloader.ts | 5 ----- .../browser/contributions/upload-sketch.ts | 6 ----- .../browser/contributions/verify-sketch.ts | 7 +++++- .../firmware-uploader-component.tsx | 5 +++-- .../firmware-uploader-dialog.tsx | 3 ++- .../protocol/arduino-firmware-uploader.ts | 4 +++- .../node/arduino-firmware-uploader-impl.ts | 22 ++++++++++--------- 7 files changed, 26 insertions(+), 26 deletions(-) diff --git a/arduino-ide-extension/src/browser/contributions/burn-bootloader.ts b/arduino-ide-extension/src/browser/contributions/burn-bootloader.ts index 96953db7b..b79ad079e 100644 --- a/arduino-ide-extension/src/browser/contributions/burn-bootloader.ts +++ b/arduino-ide-extension/src/browser/contributions/burn-bootloader.ts @@ -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; @@ -91,8 +88,6 @@ export class BurnBootloader extends SketchContribution { errorMessage = e.toString(); } this.messageService.error(errorMessage); - } finally { - await this.serialConnection.reconnectAfterUpload(); } } } diff --git a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts index b86949fd3..2aa75a1de 100644 --- a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts @@ -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; @@ -294,8 +290,6 @@ export class UploadSketch extends SketchContribution { } finally { this.uploadInProgress = false; this.onDidChangeEmitter.fire(); - - setTimeout(() => this.serialConnection.reconnectAfterUpload(), 5000); } } } diff --git a/arduino-ide-extension/src/browser/contributions/verify-sketch.ts b/arduino-ide-extension/src/browser/contributions/verify-sketch.ts index 898953ae8..2c060a6ee 100644 --- a/arduino-ide-extension/src/browser/contributions/verify-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/verify-sketch.ts @@ -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, diff --git a/arduino-ide-extension/src/browser/dialogs/firmware-uploader/firmware-uploader-component.tsx b/arduino-ide-extension/src/browser/dialogs/firmware-uploader/firmware-uploader-component.tsx index 8e5c6d32d..f892cc1b1 100644 --- a/arduino-ide-extension/src/browser/dialogs/firmware-uploader/firmware-uploader-component.tsx +++ b/arduino-ide-extension/src/browser/dialogs/firmware-uploader/firmware-uploader-component.tsx @@ -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; + flashFirmware: (firmware: FirmwareInfo, port: Port) => Promise; 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 { diff --git a/arduino-ide-extension/src/browser/dialogs/firmware-uploader/firmware-uploader-dialog.tsx b/arduino-ide-extension/src/browser/dialogs/firmware-uploader/firmware-uploader-dialog.tsx index 759757e39..e9f8149d1 100644 --- a/arduino-ide-extension/src/browser/dialogs/firmware-uploader/firmware-uploader-dialog.tsx +++ b/arduino-ide-extension/src/browser/dialogs/firmware-uploader/firmware-uploader-dialog.tsx @@ -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 { + protected flashFirmware(firmware: FirmwareInfo, port: Port): Promise { this.busyCallback(true); return this.arduinoFirmwareUploader .flash(firmware, port) diff --git a/arduino-ide-extension/src/common/protocol/arduino-firmware-uploader.ts b/arduino-ide-extension/src/common/protocol/arduino-firmware-uploader.ts index f1e2a439f..3cf9437d3 100644 --- a/arduino-ide-extension/src/common/protocol/arduino-firmware-uploader.ts +++ b/arduino-ide-extension/src/common/protocol/arduino-firmware-uploader.ts @@ -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; - flash(firmware: FirmwareInfo, port: string): Promise; + flash(firmware: FirmwareInfo, port: Port): Promise; uploadCertificates(command: string): Promise; updatableBoards(): Promise; availableFirmwares(fqbn: string): Promise; diff --git a/arduino-ide-extension/src/node/arduino-firmware-uploader-impl.ts b/arduino-ide-extension/src/node/arduino-firmware-uploader-impl.ts index d4150ea73..4e5eaa843 100644 --- a/arduino-ide-extension/src/node/arduino-firmware-uploader-impl.ts +++ b/arduino-ide-extension/src/node/arduino-firmware-uploader-impl.ts @@ -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 { + async flash(firmware: FirmwareInfo, port: Port): Promise { 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; } } From bf958fd8cf0f537fbff943c3524e276f15aae308 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Thu, 10 Mar 2022 16:13:50 +0100 Subject: [PATCH 13/35] Proxied more monitor methods to frontend --- .../monitor-manager-proxy-client-impl.ts | 104 ++++++++++++++++-- .../src/common/protocol/monitor-service.ts | 46 +++++++- .../src/node/monitor-manager-proxy-impl.ts | 18 +-- .../src/node/monitor-service.ts | 27 ++--- 4 files changed, 155 insertions(+), 40 deletions(-) diff --git a/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts b/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts index 76425a4e7..7af890a4e 100644 --- a/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts +++ b/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts @@ -1,13 +1,103 @@ -import { Emitter } from "@theia/core"; -import { injectable } from "@theia/core/shared/inversify"; -import { MonitorManagerProxyClient } from "../common/protocol/monitor-service"; +import { Emitter, JsonRpcProxy, MessageService } from "@theia/core"; +import { inject, injectable } from "@theia/core/shared/inversify"; +import { Board, Port } from "../common/protocol"; +import { Monitor, MonitorManagerProxy, MonitorManagerProxyClient, MonitorSettings } from "../common/protocol/monitor-service"; @injectable() export class MonitorManagerProxyClientImpl implements MonitorManagerProxyClient { - protected readonly onWebSocketChangedEmitter = new Emitter(); - readonly onWebSocketChanged = this.onWebSocketChangedEmitter.event; + // 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; - notifyWebSocketChanged(message: number): void { - this.onWebSocketChangedEmitter.fire(message); + // 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(MonitorManagerProxy) + protected server: JsonRpcProxy + ) { + + } + + /** + * 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}`); + } 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; + } catch { + this.messageService.error('Unable to close websocket'); + } + } + + async isWSConnected(): Promise { + return !!this.webSocket; + } + + async startMonitor(board: Board, port: Port, settings?: MonitorSettings): Promise { + 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 + data: settings, + })); } } diff --git a/arduino-ide-extension/src/common/protocol/monitor-service.ts b/arduino-ide-extension/src/common/protocol/monitor-service.ts index 11217ca57..71c4b7e65 100644 --- a/arduino-ide-extension/src/common/protocol/monitor-service.ts +++ b/arduino-ide-extension/src/common/protocol/monitor-service.ts @@ -7,13 +7,20 @@ export interface MonitorManagerProxy extends JsonRpcServer; changeMonitorSettings(board: Board, port: Port, settings: MonitorSettings): Promise; stopMonitor(board: Board, port: Port): Promise; - getSupportedSettings(protocol: string, fqbn: string): Promise; + getCurrentSettings(board: Board, port: Port): MonitorSettings; } export const MonitorManagerProxyClient = Symbol('MonitorManagerProxyClient'); export interface MonitorManagerProxyClient { - onWebSocketChanged: Event; - notifyWebSocketChanged(message: number): void; + onMessagesReceived: Event<{ messages: string[] }>; + connect(addressPort: number): void; + disconnect(): void; + getWebSocketPort(): number | undefined; + isWSConnected(): Promise; + startMonitor(board: Board, port: Port, settings?: MonitorSettings): Promise; + getCurrentSettings(board: Board, port: Port): MonitorSettings; + send(message: string): void; + changeSettings(settings: MonitorSettings): void } export interface MonitorSetting { @@ -29,4 +36,35 @@ export interface MonitorSetting { selectedValue: string; } -export type MonitorSettings = Record; \ No newline at end of file +export type MonitorSettings = Record; + +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.', + }; +} diff --git a/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts b/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts index 2291228eb..851a84868 100644 --- a/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts +++ b/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts @@ -19,7 +19,7 @@ export class MonitorManagerProxyImpl implements MonitorManagerProxy { } dispose(): void { - // NOOP + this.client?.disconnect(); } /** @@ -36,7 +36,8 @@ export class MonitorManagerProxyImpl implements MonitorManagerProxy { } const status = await this.manager.startMonitor(board, port); if (status === Status.ALREADY_CONNECTED || status === Status.OK) { - this.client.notifyWebSocketChanged(this.manager.getWebsocketAddressPort(board, port)); + // Monitor started correctly, connect it with the frontend + this.client.connect(this.manager.getWebsocketAddressPort(board, port)); } } @@ -65,15 +66,14 @@ export class MonitorManagerProxyImpl implements MonitorManagerProxy { } /** - * Returns the settings supported by the pluggable monitor for the specified - * protocol, the fqbn is necessary since it's used to tell different monitors - * using the same protocol. - * @param protocol protocol of a pluggable monitor - * @param fqbn unique ID of a board + * 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 */ - async getSupportedSettings(protocol: string, fqbn: string): Promise { - return this.manager.portMonitorSettings(protocol, fqbn); + getCurrentSettings(board: Board, port: Port): MonitorSettings { + return this.manager.currentMonitorSettings(board, port); } setClient(client: MonitorManagerProxyClient | undefined): void { diff --git a/arduino-ide-extension/src/node/monitor-service.ts b/arduino-ide-extension/src/node/monitor-service.ts index f843565b1..4503a3ca5 100644 --- a/arduino-ide-extension/src/node/monitor-service.ts +++ b/arduino-ide-extension/src/node/monitor-service.ts @@ -321,29 +321,16 @@ export class MonitorService extends CoreClientAware implements Disposable { if (!this.onMessageReceived) { this.onMessageReceived = this.webSocketProvider.onMessageReceived( (msg: string) => { - const message: SerialPlotter.Protocol.Message = JSON.parse(msg); + const message: Monitor.Message = JSON.parse(msg); switch (message.command) { - case SerialPlotter.Protocol.Command.PLOTTER_SEND_MESSAGE: + case Monitor.Command.SEND_MESSAGE: this.send(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; + break + case Monitor.Command.CHANGE_SETTINGS: + const settings: MonitorSettings = JSON.parse(message.data); + this.changeSettings(settings); + break } } ) From cbd5b4de1b35aaa08ab7850007ccdc9c36b859c7 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Thu, 10 Mar 2022 16:15:05 +0100 Subject: [PATCH 14/35] WebSocketProvider is not injectable anymore --- .../src/node/web-socket/web-socket-provider-impl.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/arduino-ide-extension/src/node/web-socket/web-socket-provider-impl.ts b/arduino-ide-extension/src/node/web-socket/web-socket-provider-impl.ts index 81d258a0b..268928dd2 100644 --- a/arduino-ide-extension/src/node/web-socket/web-socket-provider-impl.ts +++ b/arduino-ide-extension/src/node/web-socket/web-socket-provider-impl.ts @@ -1,9 +1,7 @@ import { Emitter } from '@theia/core'; -import { injectable } from 'inversify'; import * as WebSocket from 'ws'; import { WebSocketProvider } from './web-socket-provider'; -@injectable() export default class WebSocketProviderImpl implements WebSocketProvider { protected wsClients: WebSocket[]; protected server: WebSocket.Server; From 50239c5756ab431003567d7efcdfd54804df6a57 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Thu, 10 Mar 2022 16:19:25 +0100 Subject: [PATCH 15/35] Add generic monitor settings storaging --- .../src/browser/monitor-model.ts | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 arduino-ide-extension/src/browser/monitor-model.ts diff --git a/arduino-ide-extension/src/browser/monitor-model.ts b/arduino-ide-extension/src/browser/monitor-model.ts new file mode 100644 index 000000000..5082e98ae --- /dev/null +++ b/arduino-ide-extension/src/browser/monitor-model.ts @@ -0,0 +1,134 @@ +import { Emitter, Event } from "@theia/core"; +import { FrontendApplicationContribution, LocalStorageService } from "@theia/core/lib/browser"; +import { inject, injectable } from "@theia/core/shared/inversify"; + +@injectable() +export class MonitorModel implements FrontendApplicationContribution { + protected static STORAGE_ID = 'arduino-monitor-model'; + + @inject(LocalStorageService) + protected readonly localStorageService: LocalStorageService; + + protected readonly onChangeEmitter: Emitter>; + + protected _autoscroll: boolean; + protected _timestamp: boolean; + protected _lineEnding: MonitorModel.EOL; + protected _interpolate: boolean; + + constructor() { + this._autoscroll = true; + this._timestamp = false; + this._interpolate = false; + this._lineEnding = MonitorModel.EOL.DEFAULT; + + this.onChangeEmitter = new Emitter< + MonitorModel.State.Change + >(); + } + + onStart(): void { + this.localStorageService + .getData(MonitorModel.STORAGE_ID) + .then(this.restoreState); + } + + get onChange(): Event> { + 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 { + 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().then(() => { + this.onChangeEmitter.fire({ + property: 'autoscroll', + value: this._timestamp + }); + }); + } + + get timestamp(): boolean { + return this._timestamp; + } + + toggleTimestamp(): void { + this._timestamp = !this._timestamp; + this.storeState().then(() => + this.onChangeEmitter.fire({ + property: 'timestamp', + value: this._timestamp, + }) + ); + } + + get lineEnding(): MonitorModel.EOL { + return this._lineEnding; + } + + set lineEnding(lineEnding: MonitorModel.EOL) { + this._lineEnding = lineEnding; + this.storeState().then(() => + this.onChangeEmitter.fire({ + property: 'lineEnding', + value: this._lineEnding, + }) + ); + } + + get interpolate(): boolean { + return this._interpolate; + } + + set interpolate(i: boolean) { + this._interpolate = i; + this.storeState().then(() => + this.onChangeEmitter.fire({ + property: 'interpolate', + value: this._interpolate, + }) + ); + } +} + +export namespace MonitorModel { + export interface State { + autoscroll: boolean; + timestamp: boolean; + lineEnding: EOL; + interpolate: boolean; + } + export namespace State { + export interface Change { + readonly property: K; + readonly value: State[K]; + } + } + + export type EOL = '' | '\n' | '\r' | '\r\n'; + export namespace EOL { + export const DEFAULT: EOL = '\n'; + } +} From 6cf61c498a9dfe676b1d4d08d1cf9d74c447bc5b Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Thu, 10 Mar 2022 16:18:47 +0100 Subject: [PATCH 16/35] More serial classes removal --- .../browser/arduino-ide-frontend-module.ts | 23 ------------------- .../monitor/monitor-view-contribution.tsx | 5 ++-- .../src/common/protocol/index.ts | 1 - .../src/node/arduino-ide-backend-module.ts | 10 -------- 4 files changed, 3 insertions(+), 36 deletions(-) diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index c06f9f7b2..613b446ec 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -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'; @@ -409,8 +401,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { .inSingletonScope(); // Serial monitor - bind(SerialModel).toSelf().inSingletonScope(); - bind(FrontendApplicationContribution).toService(SerialModel); bind(MonitorWidget).toSelf(); bindViewContribution(bind, MonitorViewContribution); bind(TabBarToolbarContribution).toService(MonitorViewContribution); @@ -418,20 +408,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { 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 - ); - 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(); // Monitor manager proxy client to receive and delegate pluggable monitors // notifications from the backend diff --git a/arduino-ide-extension/src/browser/serial/monitor/monitor-view-contribution.tsx b/arduino-ide-extension/src/browser/serial/monitor/monitor-view-contribution.tsx index 970819c97..a335d6009 100644 --- a/arduino-ide-extension/src/browser/serial/monitor/monitor-view-contribution.tsx +++ b/arduino-ide-extension/src/browser/serial/monitor/monitor-view-contribution.tsx @@ -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({ diff --git a/arduino-ide-extension/src/common/protocol/index.ts b/arduino-ide-extension/src/common/protocol/index.ts index 1a9a25c28..4adf94223 100644 --- a/arduino-ide-extension/src/common/protocol/index.ts +++ b/arduino-ide-extension/src/common/protocol/index.ts @@ -6,7 +6,6 @@ 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'; diff --git a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts index 6b3f585c8..fd4b81833 100644 --- a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts +++ b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts @@ -40,7 +40,6 @@ import { ArduinoDaemon, ArduinoDaemonPath, } from '../common/protocol/arduino-daemon'; -import { SerialServiceName } from './serial/serial-service-impl'; import { ConfigServiceImpl } from './config-service-impl'; import { EnvVariablesServer as TheiaEnvVariablesServer } from '@theia/core/lib/common/env-variables'; @@ -303,15 +302,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { .inSingletonScope() .whenTargetNamed('config'); - // Logger for the serial service. - bind(ILogger) - .toDynamicValue((ctx) => { - const parentLogger = ctx.container.get(ILogger); - return parentLogger.child(SerialServiceName); - }) - .inSingletonScope() - .whenTargetNamed(SerialServiceName); - bind(DefaultGitInit).toSelf(); rebind(GitInit).toService(DefaultGitInit); From ad781f0bfc7e74f6d86481a51a105c8bd14952c2 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Thu, 10 Mar 2022 16:25:23 +0100 Subject: [PATCH 17/35] Remove unused file --- .../src/test/browser/fixtures/serial.ts | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 arduino-ide-extension/src/test/browser/fixtures/serial.ts diff --git a/arduino-ide-extension/src/test/browser/fixtures/serial.ts b/arduino-ide-extension/src/test/browser/fixtures/serial.ts deleted file mode 100644 index ab8b333a6..000000000 --- a/arduino-ide-extension/src/test/browser/fixtures/serial.ts +++ /dev/null @@ -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() {} -} From 6b7b33356da23c56c7692c6706e8dd16449ada9f Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Thu, 10 Mar 2022 17:03:30 +0100 Subject: [PATCH 18/35] Changed plotter contribution to use new manager proxy --- .../plotter/plotter-frontend-contribution.ts | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/arduino-ide-extension/src/browser/serial/plotter/plotter-frontend-contribution.ts b/arduino-ide-extension/src/browser/serial/plotter/plotter-frontend-contribution.ts index f4e957063..3f22f2071 100644 --- a/arduino-ide-extension/src/browser/serial/plotter/plotter-frontend-contribution.ts +++ b/arduino-ide-extension/src/browser/serial/plotter/plotter-frontend-contribution.ts @@ -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,27 @@ export class PlotterFrontendContribution extends Contribution { } protected async open(wsPort: number): Promise { + const board = this.boardsServiceProvider.boardsConfig.selectedBoard; + const port = this.boardsServiceProvider.boardsConfig.selectedPort; + let baudrates: number[] = []; + let currentBaudrate = -1; + if (board && port) { + const settings = this.monitorManagerProxy.getCurrentSettings(board, port); + if ('baudrate' in settings) { + // Convert from string to numbers + baudrates = settings['baudrate'].values.map(b => +b); + currentBaudrate = +settings['baudrate'].selectedValue; + } + } + const initConfig: Partial = { - 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( From 7889f40834216fca0ffc2a36bb8c8b846531b082 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Thu, 10 Mar 2022 17:04:07 +0100 Subject: [PATCH 19/35] Changed MonitorWidget and children to use new monitor proxy --- .../browser/serial/monitor/monitor-widget.tsx | 120 ++++++++++++------ .../monitor/serial-monitor-send-input.tsx | 28 ++-- .../monitor/serial-monitor-send-output.tsx | 19 ++- 3 files changed, 96 insertions(+), 71 deletions(-) diff --git a/arduino-ide-extension/src/browser/serial/monitor/monitor-widget.tsx b/arduino-ide-extension/src/browser/serial/monitor/monitor-widget.tsx index f5e68df54..a7e02cec0 100644 --- a/arduino-ide-extension/src/browser/serial/monitor/monitor-widget.tsx +++ b/arduino-ide-extension/src/browser/serial/monitor/monitor-widget.tsx @@ -9,14 +9,16 @@ 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, + MonitorSettings, +} from '../../../common/protocol'; +import { MonitorModel } from '../../monitor-model'; @injectable() export class MonitorWidget extends ReactWidget { @@ -26,11 +28,11 @@ export class MonitorWidget extends ReactWidget { ); static readonly ID = 'serial-monitor'; - @inject(SerialModel) - protected readonly serialModel: SerialModel; + @inject(MonitorModel) + protected readonly monitorModel: MonitorModel; - @inject(SerialConnectionManager) - protected readonly serialConnection: SerialConnectionManager; + @inject(MonitorManagerProxyClient) + protected readonly monitorManagerProxy: MonitorManagerProxyClient; @inject(BoardsServiceProvider) protected readonly boardsServiceProvider: BoardsServiceProvider; @@ -57,17 +59,27 @@ 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()) + ); + + this.toDispose.push( + this.boardsServiceProvider.onBoardsConfigChanged( + async ({ selectedBoard, selectedPort }) => { + if (selectedBoard && selectedBoard.fqbn && selectedPort) { + await this.monitorManagerProxy.startMonitor( + selectedBoard, + selectedPort + ); + } + } + ) ); } @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 +91,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 +126,7 @@ export class MonitorWidget extends ReactWidget { }; protected get lineEndings(): OptionsType< - SerialMonitorOutput.SelectOption + SerialMonitorOutput.SelectOption > { return [ { @@ -144,32 +151,61 @@ export class MonitorWidget extends ReactWidget { ]; } - protected get baudRates(): OptionsType< - SerialMonitorOutput.SelectOption - > { - const baudRates: Array = [ - 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 settings = this.getCurrentSettings(); + const baudRateSettings = settings['baudrate']; + if (!baudRateSettings) { + return []; + } + return baudRateSettings.values; + } + + protected get selectedBaudRate(): string { + const settings = this.getCurrentSettings(); + const baudRateSettings = settings['baudrate']; + if (!baudRateSettings) { + return ''; + } + 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 (
@@ -196,8 +232,8 @@ export class MonitorWidget extends ReactWidget {
@@ -208,18 +244,18 @@ export class MonitorWidget extends ReactWidget { protected readonly onSend = (value: string) => this.doSend(value); protected async doSend(value: string): Promise { - this.serialConnection.send(value); + this.monitorManagerProxy.send(value); } protected readonly onChangeLineEnding = ( - option: SerialMonitorOutput.SelectOption + option: SerialMonitorOutput.SelectOption ) => { - this.serialModel.lineEnding = option.value; + this.monitorModel.lineEnding = option.value; }; - protected readonly onChangeBaudRate = ( - option: SerialMonitorOutput.SelectOption - ) => { - this.serialModel.baudRate = option.value; + protected readonly onChangeBaudRate = (value: string) => { + const settings = this.getCurrentSettings(); + settings['baudrate'].selectedValue = value; + this.monitorManagerProxy.changeSettings(settings); }; } diff --git a/arduino-ide-extension/src/browser/serial/monitor/serial-monitor-send-input.tsx b/arduino-ide-extension/src/browser/serial/monitor/serial-monitor-send-input.tsx index e15d55eac..ed545fc86 100644 --- a/arduino-ide-extension/src/browser/serial/monitor/serial-monitor-send-input.tsx +++ b/arduino-ide-extension/src/browser/serial/monitor/serial-monitor-send-input.tsx @@ -3,12 +3,13 @@ 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'; 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; } @@ -33,21 +34,9 @@ export class SerialMonitorSendInput extends React.Component< } componentDidMount(): void { - this.props.serialConnection.isBESerialConnected().then((connected) => { + this.props.monitorManagerProxy.isWSConnected().then((connected) => { this.setState({ connected }); }); - - 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 }); - } - }), - ]); } componentWillUnmount(): void { @@ -70,14 +59,15 @@ export class SerialMonitorSendInput extends React.Component< } protected get placeholder(): string { - const serialConfig = this.props.serialConnection.getConfig(); - if (!this.state.connected || !serialConfig) { + const board = this.props.boardsServiceProvider.boardsConfig.selectedBoard; + const port = this.props.boardsServiceProvider.boardsConfig.selectedPort; + if (!this.state.connected || !board || !port) { return nls.localize( 'arduino/serial/notConnected', 'Not connected. Select a board and a port to connect automatically.' ); } - const { board, port } = serialConfig; + return nls.localize( 'arduino/serial/message', "Message ({0} + Enter to send message to '{1}' on '{2}')", diff --git a/arduino-ide-extension/src/browser/serial/monitor/serial-monitor-send-output.tsx b/arduino-ide-extension/src/browser/serial/monitor/serial-monitor-send-output.tsx index 2c3df28fc..383965124 100644 --- a/arduino-ide-extension/src/browser/serial/monitor/serial-monitor-send-output.tsx +++ b/arduino-ide-extension/src/browser/serial/monitor/serial-monitor-send-output.tsx @@ -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; readonly height: number; } From ce2f1c227a92bfea039a6e03f48a74dd08752ff8 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Fri, 11 Mar 2022 14:34:18 +0100 Subject: [PATCH 20/35] Updated MonitorWidget to use new monitor proxy --- .../browser/arduino-ide-frontend-module.ts | 16 +++++++++-- .../browser/serial/monitor/monitor-widget.tsx | 28 ++++++++++++------- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index 613b446ec..446bd09aa 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -152,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, 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'; @@ -267,7 +267,7 @@ import { IDEUpdaterDialogWidget, } from './dialogs/ide-updater/ide-updater-dialog'; import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider'; -import { MonitorManagerProxyClient } from '../common/monitor-manager-proxy'; +import { MonitorModel } from './monitor-model'; import { MonitorManagerProxyClientImpl } from './monitor-manager-proxy-client-impl'; const ElementQueries = require('css-element-queries/src/ElementQueries'); @@ -402,13 +402,23 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { // Serial monitor bind(MonitorWidget).toSelf(); + bind(MonitorModel).toSelf().inSingletonScope(); bindViewContribution(bind, MonitorViewContribution); bind(TabBarToolbarContribution).toService(MonitorViewContribution); bind(WidgetFactory).toDynamicValue((context) => ({ id: MonitorWidget.ID, - createWidget: () => context.container.get(MonitorWidget), + createWidget: () => { + return new MonitorWidget( + context.container.get(MonitorModel), + context.container.get(MonitorManagerProxyClient), + context.container.get(BoardsServiceProvider), + ); + } })); + bind(MonitorManagerProxy).toDynamicValue((context) => + WebSocketConnectionProvider.createProxy(context.container, MonitorManagerProxyPath) + ).inSingletonScope(); // Monitor manager proxy client to receive and delegate pluggable monitors // notifications from the backend diff --git a/arduino-ide-extension/src/browser/serial/monitor/monitor-widget.tsx b/arduino-ide-extension/src/browser/serial/monitor/monitor-widget.tsx index a7e02cec0..43f3a3b27 100644 --- a/arduino-ide-extension/src/browser/serial/monitor/monitor-widget.tsx +++ b/arduino-ide-extension/src/browser/serial/monitor/monitor-widget.tsx @@ -28,15 +28,6 @@ export class MonitorWidget extends ReactWidget { ); static readonly ID = 'serial-monitor'; - @inject(MonitorModel) - protected readonly monitorModel: MonitorModel; - - @inject(MonitorManagerProxyClient) - protected readonly monitorManagerProxy: MonitorManagerProxyClient; - - @inject(BoardsServiceProvider) - protected readonly boardsServiceProvider: BoardsServiceProvider; - protected widgetHeight: number; /** @@ -50,7 +41,16 @@ export class MonitorWidget extends ReactWidget { protected closing = false; protected readonly clearOutputEmitter = new Emitter(); - 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; @@ -62,6 +62,13 @@ export class MonitorWidget extends ReactWidget { 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 }) => { @@ -70,6 +77,7 @@ export class MonitorWidget extends ReactWidget { selectedBoard, selectedPort ); + this.update(); } } ) From b97af32bb83b4676d3b92bebdad4ad54b9377366 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Mon, 14 Mar 2022 11:01:47 +0100 Subject: [PATCH 21/35] Fix backend logger bindings --- .../src/node/arduino-ide-backend-module.ts | 20 ++++++++++++++++++- .../src/node/monitor-manager-proxy-impl.ts | 7 +------ .../src/node/monitor-manager.ts | 4 +++- .../src/node/monitor-service.ts | 4 +++- 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts index fd4b81833..9ecd1bdea 100644 --- a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts +++ b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts @@ -84,8 +84,9 @@ import { PlotterBackendContribution } from './plotter/plotter-backend-contributi 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 } from './monitor-manager'; +import { MonitorManager, MonitorManagerName } from './monitor-manager'; import { MonitorManagerProxy, MonitorManagerProxyClient, MonitorManagerProxyPath } from '../common/protocol/monitor-service'; +import { MonitorServiceName } from './monitor-service'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(BackendApplication).toSelf().inSingletonScope(); @@ -302,6 +303,23 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { .inSingletonScope() .whenTargetNamed('config'); + // Logger for the monitor manager and its services + bind(ILogger) + .toDynamicValue((ctx) => { + const parentLogger = ctx.container.get(ILogger); + return parentLogger.child(MonitorManagerName); + }) + .inSingletonScope() + .whenTargetNamed(MonitorManagerName); + + bind(ILogger) + .toDynamicValue((ctx) => { + const parentLogger = ctx.container.get(ILogger); + return parentLogger.child(MonitorServiceName); + }) + .inSingletonScope() + .whenTargetNamed(MonitorServiceName); + bind(DefaultGitInit).toSelf(); rebind(GitInit).toService(DefaultGitInit); diff --git a/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts b/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts index 851a84868..e1fa1eefc 100644 --- a/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts +++ b/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts @@ -1,5 +1,4 @@ -import { ILogger } from "@theia/core"; -import { inject, injectable, named } from "@theia/core/shared/inversify"; +import { inject, injectable } from "@theia/core/shared/inversify"; import { MonitorManagerProxy, MonitorManagerProxyClient, MonitorSettings, Status } from "../common/protocol"; import { Board, Port } from "../common/protocol"; import { MonitorManager } from "./monitor-manager"; @@ -9,10 +8,6 @@ export class MonitorManagerProxyImpl implements MonitorManagerProxy { protected client: MonitorManagerProxyClient; constructor( - @inject(ILogger) - @named("monitor-manager-proxy") - protected readonly logger: ILogger, - @inject(MonitorManager) protected readonly manager: MonitorManager, ) { diff --git a/arduino-ide-extension/src/node/monitor-manager.ts b/arduino-ide-extension/src/node/monitor-manager.ts index 1f38bac8c..183aec77d 100644 --- a/arduino-ide-extension/src/node/monitor-manager.ts +++ b/arduino-ide-extension/src/node/monitor-manager.ts @@ -6,6 +6,8 @@ import { MonitorService } from "./monitor-service"; type MonitorID = string; +export const MonitorManagerName = 'monitor-manager'; + @injectable() export class MonitorManager extends CoreClientAware { // Map of monitor services that manage the running pluggable monitors. @@ -16,7 +18,7 @@ export class MonitorManager extends CoreClientAware { constructor( @inject(ILogger) - @named('monitor-manager') + @named(MonitorManagerName) protected readonly logger: ILogger, ) { super(); diff --git a/arduino-ide-extension/src/node/monitor-service.ts b/arduino-ide-extension/src/node/monitor-service.ts index 4503a3ca5..e5a1069d5 100644 --- a/arduino-ide-extension/src/node/monitor-service.ts +++ b/arduino-ide-extension/src/node/monitor-service.ts @@ -8,6 +8,8 @@ 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 WebSocketProviderImpl from "./web-socket/web-socket-provider-impl"; +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. @@ -39,7 +41,7 @@ export class MonitorService extends CoreClientAware implements Disposable { constructor( @inject(ILogger) - @named("monitor-service") + @named(MonitorServiceName) protected readonly logger: ILogger, private readonly board: Board, From f9da9fc24b69a5e353f7dacc877a253cd0dd73e5 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Tue, 15 Mar 2022 14:10:31 +0100 Subject: [PATCH 22/35] Delete unnecessary Symbol --- arduino-ide-extension/src/node/web-socket/web-socket-provider.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/arduino-ide-extension/src/node/web-socket/web-socket-provider.ts b/arduino-ide-extension/src/node/web-socket/web-socket-provider.ts index 6aa102040..7c402ad54 100644 --- a/arduino-ide-extension/src/node/web-socket/web-socket-provider.ts +++ b/arduino-ide-extension/src/node/web-socket/web-socket-provider.ts @@ -1,7 +1,6 @@ import { Event } from '@theia/core/lib/common/event'; import * as WebSocket from 'ws'; -export const WebSocketProvider = Symbol('WebSocketProvider'); export interface WebSocketProvider { getAddress(): WebSocket.AddressInfo; sendMessage(message: string): void; From 397ca5665fc89b647f507e36b0191288308c83f6 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Tue, 15 Mar 2022 14:12:26 +0100 Subject: [PATCH 23/35] coreClientProvider is now set when constructing MonitorService --- arduino-ide-extension/src/node/monitor-manager.ts | 3 ++- arduino-ide-extension/src/node/monitor-service.ts | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/arduino-ide-extension/src/node/monitor-manager.ts b/arduino-ide-extension/src/node/monitor-manager.ts index 183aec77d..a51c40033 100644 --- a/arduino-ide-extension/src/node/monitor-manager.ts +++ b/arduino-ide-extension/src/node/monitor-manager.ts @@ -179,7 +179,8 @@ export class MonitorManager extends CoreClientAware { const monitor = new MonitorService( this.logger, board, - port + port, + this.coreClientProvider, ); monitor.onDispose((() => { this.monitorServices.delete(monitorID); diff --git a/arduino-ide-extension/src/node/monitor-service.ts b/arduino-ide-extension/src/node/monitor-service.ts index e5a1069d5..8d221c6ac 100644 --- a/arduino-ide-extension/src/node/monitor-service.ts +++ b/arduino-ide-extension/src/node/monitor-service.ts @@ -3,7 +3,7 @@ import { Disposable, Emitter, ILogger } from "@theia/core"; import { inject, named } from "@theia/core/shared/inversify"; import { Board, Port, Status, MonitorSettings, Monitor } from "../common/protocol"; import { EnumerateMonitorPortSettingsRequest, EnumerateMonitorPortSettingsResponse, MonitorPortConfiguration, MonitorPortSetting, MonitorRequest, MonitorResponse } from "./cli-protocol/cc/arduino/cli/commands/v1/monitor_pb"; -import { CoreClientAware } from "./core-client-provider"; +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 WebSocketProviderImpl from "./web-socket/web-socket-provider-impl"; @@ -46,6 +46,7 @@ export class MonitorService extends CoreClientAware implements Disposable { private readonly board: Board, private readonly port: Port, + protected readonly coreClientProvider: CoreClientProvider, ) { super(); @@ -98,6 +99,7 @@ export class MonitorService extends CoreClientAware implements Disposable { } this.logger.info("starting monitor"); + await this.coreClientProvider.initialized; const coreClient = await this.coreClient(); const { client, instance } = coreClient; @@ -151,6 +153,7 @@ export class MonitorService extends CoreClientAware implements Disposable { 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); @@ -212,6 +215,7 @@ export class MonitorService extends CoreClientAware implements Disposable { if (!this.duplex) { return Status.NOT_CONNECTED; } + await this.coreClientProvider.initialized; const coreClient = await this.coreClient(); const { instance } = coreClient; @@ -245,6 +249,7 @@ export class MonitorService extends CoreClientAware implements Disposable { * @returns a map of all the settings supported by the monitor */ private async portMonitorSettings(protocol: string, fqbn: string): Promise { + await this.coreClientProvider.initialized; const coreClient = await this.coreClient(); const { client, instance } = coreClient; const req = new EnumerateMonitorPortSettingsRequest(); @@ -295,6 +300,7 @@ export class MonitorService extends CoreClientAware implements Disposable { if (!this.duplex) { return Status.NOT_CONNECTED; } + await this.coreClientProvider.initialized; const coreClient = await this.coreClient(); const { instance } = coreClient; From a8d803e7c33e37a83e67c11d161662cfe19bc3bd Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Tue, 15 Mar 2022 14:13:34 +0100 Subject: [PATCH 24/35] Add missing binding --- arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index 446bd09aa..fea0754bc 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -402,6 +402,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { // Serial monitor bind(MonitorWidget).toSelf(); + bind(FrontendApplicationContribution).toService(MonitorModel); bind(MonitorModel).toSelf().inSingletonScope(); bindViewContribution(bind, MonitorViewContribution); bind(TabBarToolbarContribution).toService(MonitorViewContribution); From fbe8fb421a84fe461aa46adf0048e379632db3a1 Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Tue, 22 Mar 2022 15:10:20 +0100 Subject: [PATCH 25/35] Fix `MonitorManagerProxy` DI issue --- .../src/browser/arduino-ide-frontend-module.ts | 6 ++++-- .../src/browser/monitor-manager-proxy-client-impl.ts | 12 ++++++------ .../src/common/protocol/monitor-service.ts | 3 +++ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index fea0754bc..5f003e7cb 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -152,7 +152,7 @@ import { OutputChannelRegistryMainImpl as TheiaOutputChannelRegistryMainImpl, OutputChannelRegistryMainImpl, } from './theia/plugin-ext/output-channel-registry-main'; -import { ExecutableService, ExecutableServicePath, MonitorManagerProxy, MonitorManagerProxyClient, MonitorManagerProxyPath } 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'; @@ -417,8 +417,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { } })); + bind(MonitorManagerProxyFactory).toFactory((context) => () => context.container.get(MonitorManagerProxy)) + bind(MonitorManagerProxy).toDynamicValue((context) => - WebSocketConnectionProvider.createProxy(context.container, MonitorManagerProxyPath) + WebSocketConnectionProvider.createProxy(context.container, MonitorManagerProxyPath, context.container.get(MonitorManagerProxyClient)) ).inSingletonScope(); // Monitor manager proxy client to receive and delegate pluggable monitors diff --git a/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts b/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts index 7af890a4e..3666238a6 100644 --- a/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts +++ b/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts @@ -1,7 +1,7 @@ -import { Emitter, JsonRpcProxy, MessageService } from "@theia/core"; +import { Emitter, MessageService } from "@theia/core"; import { inject, injectable } from "@theia/core/shared/inversify"; import { Board, Port } from "../common/protocol"; -import { Monitor, MonitorManagerProxy, MonitorManagerProxyClient, MonitorSettings } from "../common/protocol/monitor-service"; +import { Monitor, MonitorManagerProxyClient, MonitorManagerProxyFactory, MonitorSettings } from "../common/protocol/monitor-service"; @injectable() export class MonitorManagerProxyClientImpl implements MonitorManagerProxyClient { @@ -26,8 +26,8 @@ export class MonitorManagerProxyClientImpl implements MonitorManagerProxyClient protected messageService: MessageService, // This is necessary to call the backend methods from the frontend - @inject(MonitorManagerProxy) - protected server: JsonRpcProxy + @inject(MonitorManagerProxyFactory) + protected server: MonitorManagerProxyFactory ) { } @@ -71,11 +71,11 @@ export class MonitorManagerProxyClientImpl implements MonitorManagerProxyClient } async startMonitor(board: Board, port: Port, settings?: MonitorSettings): Promise { - return this.server.startMonitor(board, port, settings); + return this.server().startMonitor(board, port, settings); } getCurrentSettings(board: Board, port: Port): MonitorSettings { - return this.server.getCurrentSettings(board, port); + return this.server().getCurrentSettings(board, port); } send(message: string): void { diff --git a/arduino-ide-extension/src/common/protocol/monitor-service.ts b/arduino-ide-extension/src/common/protocol/monitor-service.ts index 71c4b7e65..71dcbc4c0 100644 --- a/arduino-ide-extension/src/common/protocol/monitor-service.ts +++ b/arduino-ide-extension/src/common/protocol/monitor-service.ts @@ -1,6 +1,9 @@ import { Event, JsonRpcServer } from "@theia/core"; 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 { From eff960bb7f4f661ced36127409186e6e7291d54e Mon Sep 17 00:00:00 2001 From: Alberto Iannaccone Date: Fri, 8 Apr 2022 15:42:52 +0200 Subject: [PATCH 26/35] fix monitor connection --- .../monitor-manager-proxy-client-impl.ts | 204 ++--- .../monitor/serial-monitor-send-input.tsx | 36 +- .../src/common/protocol/monitor-service.ts | 106 +-- .../src/node/monitor-manager.ts | 351 ++++----- .../src/node/monitor-service.ts | 709 +++++++++--------- 5 files changed, 750 insertions(+), 656 deletions(-) diff --git a/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts b/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts index 3666238a6..2a7b7cc9f 100644 --- a/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts +++ b/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts @@ -1,103 +1,123 @@ -import { Emitter, MessageService } from "@theia/core"; -import { inject, injectable } from "@theia/core/shared/inversify"; -import { Board, Port } from "../common/protocol"; -import { Monitor, MonitorManagerProxyClient, MonitorManagerProxyFactory, MonitorSettings } from "../common/protocol/monitor-service"; +import { Emitter, MessageService } from '@theia/core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { Board, Port } from '../common/protocol'; +import { + Monitor, + MonitorManagerProxyClient, + MonitorManagerProxyFactory, + MonitorSettings, +} from '../common/protocol/monitor-service'; @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; - - // WebSocket used to handle pluggable monitor communication between - // frontend and backend. - private webSocket?: WebSocket; - private wsPort?: number; - - getWebSocketPort(): number | undefined { - return this.wsPort; +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(); + 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; } - - constructor( - @inject(MessageService) - protected messageService: MessageService, - - // This is necessary to call the backend methods from the frontend - @inject(MonitorManagerProxyFactory) - protected server: MonitorManagerProxyFactory - ) { - + try { + this.webSocket = new WebSocket(`ws://localhost:${addressPort}`); + this.onWSConnectionChangedEmitter.fire(true); + } catch { + this.messageService.error('Unable to connect to websocket'); + return; } - /** - * 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}`); - } 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; + 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'); } - - /** - * Disconnects the WebSocket if connected. - */ - disconnect(): void { - try { - this.webSocket?.close(); - this.webSocket = undefined; - } catch { - this.messageService.error('Unable to close websocket'); - } + } + + async isWSConnected(): Promise { + return !!this.webSocket; + } + + async startMonitor( + board: Board, + port: Port, + settings?: MonitorSettings + ): Promise { + 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; } - async isWSConnected(): Promise { - return !!this.webSocket; + this.webSocket.send( + JSON.stringify({ + command: Monitor.Command.SEND_MESSAGE, + data: message, + }) + ); + } + + changeSettings(settings: MonitorSettings): void { + if (!this.webSocket) { + return; } - async startMonitor(board: Board, port: Port, settings?: MonitorSettings): Promise { - 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 - data: settings, - })); - } + this.webSocket.send( + JSON.stringify({ + command: Monitor.Command.CHANGE_SETTINGS, + // TODO: This might be wrong, verify if it works + data: settings, + }) + ); + } } diff --git a/arduino-ide-extension/src/browser/serial/monitor/serial-monitor-send-input.tsx b/arduino-ide-extension/src/browser/serial/monitor/serial-monitor-send-input.tsx index ed545fc86..59ad9c9ad 100644 --- a/arduino-ide-extension/src/browser/serial/monitor/serial-monitor-send-input.tsx +++ b/arduino-ide-extension/src/browser/serial/monitor/serial-monitor-send-input.tsx @@ -5,6 +5,7 @@ import { isOSX } from '@theia/core/lib/common/os'; import { DisposableCollection, nls } from '@theia/core/lib/common'; 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 { @@ -27,16 +28,33 @@ export class SerialMonitorSendInput extends React.Component< constructor(props: Readonly) { 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.monitorManagerProxy.isWSConnected().then((connected) => { - this.setState({ connected }); + this.setState({ connected: true }); + + const checkWSConnection = new Promise((resolve) => { + this.props.monitorManagerProxy.onWSConnectionChanged((connected) => { + this.setState({ connected }); + resolve(true); + }); }); + + const checkWSTimeout = timeout(1000).then(() => false); + + Promise.race([checkWSConnection, checkWSTimeout]).then( + async (resolved) => { + if (!resolved) { + const connected = + await this.props.monitorManagerProxy.isWSConnected(); + this.setState({ connected }); + } + } + ); } componentWillUnmount(): void { @@ -49,7 +67,7 @@ export class SerialMonitorSendInput extends React.Component< MonitorManagerProxy; export const MonitorManagerProxyPath = '/services/monitor-manager-proxy'; export const MonitorManagerProxy = Symbol('MonitorManagerProxy'); -export interface MonitorManagerProxy extends JsonRpcServer { - startMonitor(board: Board, port: Port, settings?: MonitorSettings): Promise; - changeMonitorSettings(board: Board, port: Port, settings: MonitorSettings): Promise; - stopMonitor(board: Board, port: Port): Promise; - getCurrentSettings(board: Board, port: Port): MonitorSettings; +export interface MonitorManagerProxy + extends JsonRpcServer { + startMonitor( + board: Board, + port: Port, + settings?: MonitorSettings + ): Promise; + changeMonitorSettings( + board: Board, + port: Port, + settings: MonitorSettings + ): Promise; + stopMonitor(board: Board, port: Port): Promise; + getCurrentSettings(board: Board, port: Port): MonitorSettings; } export const MonitorManagerProxyClient = Symbol('MonitorManagerProxyClient'); export interface MonitorManagerProxyClient { - onMessagesReceived: Event<{ messages: string[] }>; - connect(addressPort: number): void; - disconnect(): void; - getWebSocketPort(): number | undefined; - isWSConnected(): Promise; - startMonitor(board: Board, port: Port, settings?: MonitorSettings): Promise; - getCurrentSettings(board: Board, port: Port): MonitorSettings; - send(message: string): void; - changeSettings(settings: MonitorSettings): void + onMessagesReceived: Event<{ messages: string[] }>; + onWSConnectionChanged: Event; + connect(addressPort: number): void; + disconnect(): void; + getWebSocketPort(): number | undefined; + isWSConnected(): Promise; + startMonitor( + board: Board, + port: Port, + settings?: MonitorSettings + ): Promise; + getCurrentSettings(board: Board, port: Port): MonitorSettings; + send(message: string): void; + changeSettings(settings: MonitorSettings): void; } export interface MonitorSetting { - // 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; + // 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 type MonitorSettings = Record; export namespace Monitor { - export enum Command { - SEND_MESSAGE = 'MONITOR_SEND_MESSAGE', - CHANGE_SETTINGS = 'MONITOR_CHANGE_SETTINGS', - } + export enum Command { + SEND_MESSAGE = 'MONITOR_SEND_MESSAGE', + CHANGE_SETTINGS = 'MONITOR_CHANGE_SETTINGS', + } - export type Message = { - command: Monitor.Command, - data: string; - } + export type Message = { + command: Monitor.Command; + data: string; + }; } -export interface Status { } +export interface Status {} export type OK = Status; export interface ErrorStatus extends Status { - readonly message: string; + 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 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.', + }; } diff --git a/arduino-ide-extension/src/node/monitor-manager.ts b/arduino-ide-extension/src/node/monitor-manager.ts index a51c40033..fa8b708f7 100644 --- a/arduino-ide-extension/src/node/monitor-manager.ts +++ b/arduino-ide-extension/src/node/monitor-manager.ts @@ -1,8 +1,8 @@ -import { ILogger } from "@theia/core"; -import { inject, injectable, named } from "@theia/core/shared/inversify"; -import { Board, Port, Status, MonitorSettings } from "../common/protocol"; -import { CoreClientAware } from "./core-client-provider"; -import { MonitorService } from "./monitor-service"; +import { ILogger } from '@theia/core'; +import { inject, injectable, named } from '@theia/core/shared/inversify'; +import { Board, Port, Status, MonitorSettings } from '../common/protocol'; +import { CoreClientAware } from './core-client-provider'; +import { MonitorService } from './monitor-service'; type MonitorID = string; @@ -10,191 +10,194 @@ 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(); + // 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(); - constructor( - @inject(ILogger) - @named(MonitorManagerName) - protected readonly logger: ILogger, - ) { - super(); - } + 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; + /** + * 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 { - const monitorID = this.monitorID(board, port); - let monitor = this.monitorServices.get(monitorID); - if (!monitor) { - monitor = this.createMonitor(board, port) - } - return await monitor.start(); + /** + * 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 { + const monitorID = this.monitorID(board, port); + let monitor = this.monitorServices.get(monitorID); + if (!monitor) { + monitor = this.createMonitor(board, port); } + return await monitor.start(); + } - /** - * Stop a pluggable monitor connected to the specified board/port - * combination. It's a noop if monitor is not running. - * @param board board connected to port - * @param port port monitored - */ - async stopMonitor(board: Board, port: Port): Promise { - 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(); + /** + * 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 { + 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(); + /** + * 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 { - 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; - } - return await monitor.pause(); + /** + * 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 { + if (!board || !port) { + // We have no way of knowing which monitor + // to retrieve if we don't have this information. + return; } - - /** - * 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 { - 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; - } - return await monitor.start(); + const monitorID = this.monitorID(board, port); + const monitor = this.monitorServices.get(monitorID); + if (!monitor) { + // There's no monitor running there, bail + return; } + return await monitor.pause(); + } - /** - * 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: MonitorSettings) { - const monitorID = this.monitorID(board, port); - let monitor = this.monitorServices.get(monitorID); - if (!monitor) { - monitor = this.createMonitor(board, port) - monitor.changeSettings(settings); - } + /** + * 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 { + if (!board || !port) { + // We have no way of knowing which monitor + // to retrieve if we don't have this information. + return Status.NOT_CONNECTED; } - - /** - * 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): MonitorSettings { - const monitorID = this.monitorID(board, port); - const monitor = this.monitorServices.get(monitorID); - if (!monitor) { - return {}; - } - return monitor.currentSettings(); + 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; } + return await monitor.start(); + } - /** - * 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 = new MonitorService( - this.logger, - board, - port, - this.coreClientProvider, - ); - monitor.onDispose((() => { - this.monitorServices.delete(monitorID); - }).bind(this)); - return monitor + /** + * 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: MonitorSettings) { + const monitorID = this.monitorID(board, port); + let monitor = this.monitorServices.get(monitorID); + if (!monitor) { + monitor = this.createMonitor(board, port); + monitor.changeSettings(settings); } + } - /** - * 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}`; + /** + * 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): MonitorSettings { + const monitorID = this.monitorID(board, port); + const monitor = this.monitorServices.get(monitorID); + if (!monitor) { + return {}; } -} \ No newline at end of file + 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 = new MonitorService( + this.logger, + board, + port, + 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}`; + } +} diff --git a/arduino-ide-extension/src/node/monitor-service.ts b/arduino-ide-extension/src/node/monitor-service.ts index 8d221c6ac..35f77f45c 100644 --- a/arduino-ide-extension/src/node/monitor-service.ts +++ b/arduino-ide-extension/src/node/monitor-service.ts @@ -1,365 +1,398 @@ -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, MonitorSettings, 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 WebSocketProviderImpl from "./web-socket/web-socket-provider-impl"; +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, + MonitorSettings, + 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 WebSocketProviderImpl from './web-socket/web-socket-provider-impl'; 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 | null; - - // Settings used by the currently running pluggable monitor. - // They can be freely modified while running. - protected settings: MonitorSettings; - - // List of messages received from the running pluggable monitor. - // These are flushed from time to time to the frontend. - protected messages: string[] = []; - - // Handles messages received from the frontend via websocket. - protected onMessageReceived?: Disposable; - - // Sends messages to the frontend from time to time. - protected flushMessagesInterval?: NodeJS.Timeout; - - // Triggered each time the number of clients connected - // to the this service WebSocket changes. - protected onWSClientsNumberChanged?: Disposable; - - // Used to notify that the monitor is being disposed - protected readonly onDisposeEmitter = new Emitter(); - readonly onDispose = this.onDisposeEmitter.event; - - protected readonly webSocketProvider: WebSocketProvider = new WebSocketProviderImpl(); - - constructor( - @inject(ILogger) - @named(MonitorServiceName) - protected readonly logger: ILogger, - - 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(); - } - }); - - // Sets default settings for this monitor - this.portMonitorSettings(port.protocol, board.fqbn!).then( - settings => this.settings = settings - ); + // Bidirectional gRPC stream used to receive and send data from the running + // pluggable monitor managed by the Arduino CLI. + protected duplex: ClientDuplexStream | null; + + // Settings used by the currently running pluggable monitor. + // They can be freely modified while running. + protected settings: MonitorSettings; + + // List of messages received from the running pluggable monitor. + // These are flushed from time to time to the frontend. + protected messages: string[] = []; + + // Handles messages received from the frontend via websocket. + protected onMessageReceived?: Disposable; + + // Sends messages to the frontend from time to time. + protected flushMessagesInterval?: NodeJS.Timeout; + + // Triggered each time the number of clients connected + // to the this service WebSocket changes. + protected onWSClientsNumberChanged?: Disposable; + + // Used to notify that the monitor is being disposed + protected readonly onDisposeEmitter = new Emitter(); + readonly onDispose = this.onDisposeEmitter.event; + + protected readonly webSocketProvider: WebSocketProvider = + new WebSocketProviderImpl(); + + constructor( + @inject(ILogger) + @named(MonitorServiceName) + protected readonly logger: ILogger, + + 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(); + } + }); + + // Sets default settings for this monitor + this.portMonitorSettings(port.protocol, board.fqbn!).then( + (settings) => (this.settings = settings) + ); + } + + 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. + * @returns a status to verify connection has been established. + */ + async start(): Promise { + if (this.duplex) { + return Status.ALREADY_CONNECTED; } - getWebsocketAddressPort(): number { - return this.webSocketProvider.getAddress().port; + if (!this.board?.fqbn || !this.port?.address || !this.port?.protocol) { + return Status.CONFIG_MISSING; } - dispose(): void { - this.stop(); - this.onDisposeEmitter.fire(); - } + this.logger.info('starting monitor'); + await this.coreClientProvider.initialized; + const coreClient = await this.coreClient(); + const { client, instance } = coreClient; - /** - * 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; + this.duplex = client.monitor(); + this.duplex + .on('close', () => { + this.logger.info( + `monitor to ${this.port?.address} using ${this.port?.protocol} closed by client` + ); + }) + .on('end', () => { + 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); } - - /** - * Start and connects a monitor using currently set board and port. - * If a monitor is already started or board fqbn, port address and/or protocol - * are missing nothing happens. - * @returns a status to verify connection has been established. - */ - async start(): Promise { - if (this.duplex) { - return Status.ALREADY_CONNECTED; - } - - if (!this.board?.fqbn || !this.port?.address || !this.port?.protocol) { - return Status.CONFIG_MISSING - } - - this.logger.info("starting monitor"); - await this.coreClientProvider.initialized; - const coreClient = await this.coreClient(); - const { client, instance } = coreClient; - - this.duplex = client.monitor() - this.duplex - .on('close', () => { - this.logger.info(`monitor to ${this.port?.address} using ${this.port?.protocol} closed by client`) - }) - .on('end', () => { - 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(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(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, - ]) + if (this.port?.address && this.port?.protocol) { + const port = new gRPCPort(); + port.setAddress(this.port.address); + port.setProtocol(this.port.protocol); + req.setPort(port); } - - /** - * 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 when to the board/port combination - * the monitor is listening to. - * @returns - */ - async pause(): Promise { - return new Promise(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.cancel(); - this.duplex = null; - this.logger.info(`stopped monitor to ${this.port?.address} using ${this.port?.protocol}`) - resolve(); - }) + 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); - /** - * Stop the monitor currently running - */ - async stop(): Promise { - return this.pause().finally( - this.stopMessagesHandlers + const connect = new Promise((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((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 when to the board/port combination + * the monitor is listening to. + * @returns + */ + async pause(): Promise { + 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.duplex = null; + this.logger.info( + `stopped monitor to ${this.port?.address} using ${this.port?.protocol}` + ); + resolve(); + }); + } + + /** + * Stop the monitor currently running + */ + async stop(): Promise { + 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 { + if (!this.duplex) { + return Status.NOT_CONNECTED; } - - /** - * 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 { - 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(resolve => { - if (this.duplex) { - this.duplex?.write(req, () => { - resolve(Status.OK); - }); - return; - } - this.stop().then(() => resolve(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((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(): MonitorSettings { + return this.settings; + } + + /** + * 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 { + 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( + (resolve, reject) => { + client.enumerateMonitorPortSettings(req, (err, resp) => { + if (!!err) { + reject(err); + } + resolve(resp); + }); + } + ); + + const settings: MonitorSettings = {}; + for (const iterator of res.getSettingsList()) { + settings[iterator.getSettingId()] = { + id: iterator.getSettingId(), + label: iterator.getLabel(), + type: iterator.getType(), + values: iterator.getEnumValuesList(), + selectedValue: iterator.getValue(), + }; } - - /** - * - * @returns map of current monitor settings - */ - currentSettings(): MonitorSettings { - return this.settings; + return settings; + } + + /** + * Set monitor settings, if there is a running monitor they'll be sent + * to it, otherwise they'll be used when starting one. + * Only values in settings parameter will be change, other values won't + * be changed in any way. + * @param settings map of monitor settings to change + * @returns a status to verify settings have been sent. + */ + async changeSettings(settings: MonitorSettings): Promise { + 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]; } - /** - * 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 { - 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((resolve, reject) => { - client.enumerateMonitorPortSettings(req, (err, resp) => { - if (!!err) { - reject(err) - } - resolve(resp) - }) - }); - - let settings: MonitorSettings = {}; - 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; + if (!this.duplex) { + return Status.NOT_CONNECTED; } - - /** - * Set monitor settings, if there is a running monitor they'll be sent - * to it, otherwise they'll be used when starting one. - * Only values in settings parameter will be change, other values won't - * be changed in any way. - * @param settings map of monitor settings to change - * @returns a status to verify settings have been sent. - */ - async changeSettings(settings: MonitorSettings): Promise { - 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]; + 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 = []; } - - 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 + }; + this.flushMessagesInterval = setInterval(flushMessagesToFrontend, 32); } - /** - * 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: MonitorSettings = JSON.parse(message.data); - this.changeSettings(settings); - break - } - } - ) + 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: MonitorSettings = 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; - } + } + + /** + * 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; + } + } } /** @@ -368,5 +401,5 @@ export class MonitorService extends CoreClientAware implements Disposable { * @returns an lines array */ function splitLines(s: string): string[] { - return s.split(/(?<=\n)/); + return s.split(/(?<=\n)/); } From 9b58c9d0c85da5d6160c4fe934c1c43549eb23e2 Mon Sep 17 00:00:00 2001 From: Alberto Iannaccone Date: Tue, 10 May 2022 15:53:48 +0200 Subject: [PATCH 27/35] delete duplex when connection is closed --- arduino-ide-extension/src/node/monitor-service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/arduino-ide-extension/src/node/monitor-service.ts b/arduino-ide-extension/src/node/monitor-service.ts index 35f77f45c..eabc39156 100644 --- a/arduino-ide-extension/src/node/monitor-service.ts +++ b/arduino-ide-extension/src/node/monitor-service.ts @@ -121,11 +121,13 @@ export class MonitorService extends CoreClientAware implements Disposable { 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` ); @@ -218,7 +220,6 @@ export class MonitorService extends CoreClientAware implements Disposable { // It's enough to close the connection with the client // to stop the monitor process this.duplex.end(); - this.duplex = null; this.logger.info( `stopped monitor to ${this.port?.address} using ${this.port?.protocol}` ); From 62eaeb1c7429b988e666d05c575cbcc17e36bc0f Mon Sep 17 00:00:00 2001 From: Alberto Iannaccone Date: Tue, 10 May 2022 15:54:04 +0200 Subject: [PATCH 28/35] update arduino-cli to 0.22.0 --- arduino-ide-extension/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index 426665701..5ed069807 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -157,7 +157,7 @@ ], "arduino": { "cli": { - "version": "0.21.0" + "version": "0.22.0" }, "fwuploader": { "version": "2.0.0" From 1982609c87dfcdd6d9fe70b24c803516d04ac50c Mon Sep 17 00:00:00 2001 From: Alberto Iannaccone Date: Thu, 12 May 2022 15:28:13 +0200 Subject: [PATCH 29/35] fix upload when monitor is open --- .../src/common/protocol/monitor-service.ts | 3 +++ .../src/node/monitor-manager.ts | 4 +++- .../src/node/monitor-service.ts | 16 +++++++++++++--- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/arduino-ide-extension/src/common/protocol/monitor-service.ts b/arduino-ide-extension/src/common/protocol/monitor-service.ts index f33ac10a4..765b7dc21 100644 --- a/arduino-ide-extension/src/common/protocol/monitor-service.ts +++ b/arduino-ide-extension/src/common/protocol/monitor-service.ts @@ -84,4 +84,7 @@ export namespace Status { export const CONFIG_MISSING: ErrorStatus = { message: 'Serial Config missing.', }; + export const UPLOAD_IN_PROGRESS: ErrorStatus = { + message: 'Upload in progress.', + }; } diff --git a/arduino-ide-extension/src/node/monitor-manager.ts b/arduino-ide-extension/src/node/monitor-manager.ts index fa8b708f7..e13708186 100644 --- a/arduino-ide-extension/src/node/monitor-manager.ts +++ b/arduino-ide-extension/src/node/monitor-manager.ts @@ -12,7 +12,7 @@ export const MonitorManagerName = 'monitor-manager'; 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 + // If either the board or port managed changes, a new service must // be started. private monitorServices = new Map(); @@ -109,6 +109,7 @@ export class MonitorManager extends CoreClientAware { // There's no monitor running there, bail return; } + monitor.setUploadInProgress(true); return await monitor.pause(); } @@ -132,6 +133,7 @@ export class MonitorManager extends CoreClientAware { // There's no monitor running there, bail return Status.NOT_CONNECTED; } + monitor.setUploadInProgress(false); return await monitor.start(); } diff --git a/arduino-ide-extension/src/node/monitor-service.ts b/arduino-ide-extension/src/node/monitor-service.ts index eabc39156..94b980b41 100644 --- a/arduino-ide-extension/src/node/monitor-service.ts +++ b/arduino-ide-extension/src/node/monitor-service.ts @@ -53,6 +53,8 @@ export class MonitorService extends CoreClientAware implements Disposable { protected readonly webSocketProvider: WebSocketProvider = new WebSocketProviderImpl(); + protected uploadInProgress = false; + constructor( @inject(ILogger) @named(MonitorServiceName) @@ -80,6 +82,10 @@ export class MonitorService extends CoreClientAware implements Disposable { ); } + setUploadInProgress(status: boolean): void { + this.uploadInProgress = status; + } + getWebsocketAddressPort(): number { return this.webSocketProvider.getAddress().port; } @@ -113,11 +119,14 @@ export class MonitorService extends CoreClientAware implements Disposable { return Status.CONFIG_MISSING; } + if (this.uploadInProgress) { + return Status.UPLOAD_IN_PROGRESS; + } + this.logger.info('starting monitor'); await this.coreClientProvider.initialized; const coreClient = await this.coreClient(); const { client, instance } = coreClient; - this.duplex = client.monitor(); this.duplex .on('close', () => { @@ -205,7 +214,7 @@ export class MonitorService extends CoreClientAware implements Disposable { * 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 when to the board/port combination + * This is mainly used to handle upload with the board/port combination * the monitor is listening to. * @returns */ @@ -223,7 +232,8 @@ export class MonitorService extends CoreClientAware implements Disposable { this.logger.info( `stopped monitor to ${this.port?.address} using ${this.port?.protocol}` ); - resolve(); + + this.duplex.on('end', resolve); }); } From 7bf4ea06375e1bb6d6974c52df84890d21a61128 Mon Sep 17 00:00:00 2001 From: Alberto Iannaccone Date: Tue, 17 May 2022 17:45:47 +0200 Subject: [PATCH 30/35] add MonitorSettingsProvider interface --- .../monitor-manager-proxy-client-impl.ts | 2 +- .../src/browser/monitor-model.ts | 259 +++++++++--------- .../browser/serial/monitor/monitor-widget.tsx | 6 +- .../plotter/plotter-frontend-contribution.ts | 2 +- .../src/common/protocol/monitor-service.ts | 3 +- .../src/node/monitor-manager-proxy-impl.ts | 146 +++++----- .../src/node/monitor-manager.ts | 7 +- .../src/node/monitor-service.ts | 29 +- .../monitor-settings-provider.ts | 14 + 9 files changed, 250 insertions(+), 218 deletions(-) create mode 100644 arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider.ts diff --git a/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts b/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts index 2a7b7cc9f..acde19986 100644 --- a/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts +++ b/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts @@ -5,8 +5,8 @@ import { Monitor, MonitorManagerProxyClient, MonitorManagerProxyFactory, - MonitorSettings, } from '../common/protocol/monitor-service'; +import { MonitorSettings } from '../node/monitor-settings/monitor-settings-provider'; @injectable() export class MonitorManagerProxyClientImpl diff --git a/arduino-ide-extension/src/browser/monitor-model.ts b/arduino-ide-extension/src/browser/monitor-model.ts index 5082e98ae..ccbd073c6 100644 --- a/arduino-ide-extension/src/browser/monitor-model.ts +++ b/arduino-ide-extension/src/browser/monitor-model.ts @@ -1,134 +1,139 @@ -import { Emitter, Event } from "@theia/core"; -import { FrontendApplicationContribution, LocalStorageService } from "@theia/core/lib/browser"; -import { inject, injectable } from "@theia/core/shared/inversify"; +import { Emitter, Event } from '@theia/core'; +import { + FrontendApplicationContribution, + LocalStorageService, +} from '@theia/core/lib/browser'; +import { inject, injectable } from '@theia/core/shared/inversify'; @injectable() export class MonitorModel implements FrontendApplicationContribution { - protected static STORAGE_ID = 'arduino-monitor-model'; - - @inject(LocalStorageService) - protected readonly localStorageService: LocalStorageService; - - protected readonly onChangeEmitter: Emitter>; - - protected _autoscroll: boolean; - protected _timestamp: boolean; - protected _lineEnding: MonitorModel.EOL; - protected _interpolate: boolean; - - constructor() { - this._autoscroll = true; - this._timestamp = false; - this._interpolate = false; - this._lineEnding = MonitorModel.EOL.DEFAULT; - - this.onChangeEmitter = new Emitter< - MonitorModel.State.Change - >(); - } - - onStart(): void { - this.localStorageService - .getData(MonitorModel.STORAGE_ID) - .then(this.restoreState); - } - - get onChange(): Event> { - 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 { - 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().then(() => { - this.onChangeEmitter.fire({ - property: 'autoscroll', - value: this._timestamp - }); - }); - } - - get timestamp(): boolean { - return this._timestamp; - } - - toggleTimestamp(): void { - this._timestamp = !this._timestamp; - this.storeState().then(() => - this.onChangeEmitter.fire({ - property: 'timestamp', - value: this._timestamp, - }) - ); - } - - get lineEnding(): MonitorModel.EOL { - return this._lineEnding; - } - - set lineEnding(lineEnding: MonitorModel.EOL) { - this._lineEnding = lineEnding; - this.storeState().then(() => - this.onChangeEmitter.fire({ - property: 'lineEnding', - value: this._lineEnding, - }) - ); - } - - get interpolate(): boolean { - return this._interpolate; - } - - set interpolate(i: boolean) { - this._interpolate = i; - this.storeState().then(() => - this.onChangeEmitter.fire({ - property: 'interpolate', - value: this._interpolate, - }) - ); - } + protected static STORAGE_ID = 'arduino-monitor-model'; + + @inject(LocalStorageService) + protected readonly localStorageService: LocalStorageService; + + protected readonly onChangeEmitter: Emitter< + MonitorModel.State.Change + >; + + protected _autoscroll: boolean; + protected _timestamp: boolean; + protected _lineEnding: MonitorModel.EOL; + protected _interpolate: boolean; + + constructor() { + this._autoscroll = true; + this._timestamp = false; + this._interpolate = false; + this._lineEnding = MonitorModel.EOL.DEFAULT; + + this.onChangeEmitter = new Emitter< + MonitorModel.State.Change + >(); + } + + onStart(): void { + this.localStorageService + .getData(MonitorModel.STORAGE_ID) + .then(this.restoreState); + } + + get onChange(): Event> { + 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 { + 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().then(() => { + this.onChangeEmitter.fire({ + property: 'autoscroll', + value: this._timestamp, + }); + }); + } + + get timestamp(): boolean { + return this._timestamp; + } + + toggleTimestamp(): void { + this._timestamp = !this._timestamp; + this.storeState().then(() => + this.onChangeEmitter.fire({ + property: 'timestamp', + value: this._timestamp, + }) + ); + } + + get lineEnding(): MonitorModel.EOL { + return this._lineEnding; + } + + set lineEnding(lineEnding: MonitorModel.EOL) { + this._lineEnding = lineEnding; + this.storeState().then(() => + this.onChangeEmitter.fire({ + property: 'lineEnding', + value: this._lineEnding, + }) + ); + } + + get interpolate(): boolean { + return this._interpolate; + } + + set interpolate(i: boolean) { + this._interpolate = i; + this.storeState().then(() => + this.onChangeEmitter.fire({ + property: 'interpolate', + value: this._interpolate, + }) + ); + } } export namespace MonitorModel { - export interface State { - autoscroll: boolean; - timestamp: boolean; - lineEnding: EOL; - interpolate: boolean; - } - export namespace State { - export interface Change { - readonly property: K; - readonly value: State[K]; - } - } - - export type EOL = '' | '\n' | '\r' | '\r\n'; - export namespace EOL { - export const DEFAULT: EOL = '\n'; - } + export interface State { + autoscroll: boolean; + timestamp: boolean; + lineEnding: EOL; + interpolate: boolean; + } + export namespace State { + export interface Change { + readonly property: K; + readonly value: State[K]; + } + } + + export type EOL = '' | '\n' | '\r' | '\r\n'; + export namespace EOL { + export const DEFAULT: EOL = '\n'; + } } diff --git a/arduino-ide-extension/src/browser/serial/monitor/monitor-widget.tsx b/arduino-ide-extension/src/browser/serial/monitor/monitor-widget.tsx index 43f3a3b27..9d9a08304 100644 --- a/arduino-ide-extension/src/browser/serial/monitor/monitor-widget.tsx +++ b/arduino-ide-extension/src/browser/serial/monitor/monitor-widget.tsx @@ -14,11 +14,9 @@ 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, - MonitorSettings, -} from '../../../common/protocol'; +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 { diff --git a/arduino-ide-extension/src/browser/serial/plotter/plotter-frontend-contribution.ts b/arduino-ide-extension/src/browser/serial/plotter/plotter-frontend-contribution.ts index 3f22f2071..1cc5e0f66 100644 --- a/arduino-ide-extension/src/browser/serial/plotter/plotter-frontend-contribution.ts +++ b/arduino-ide-extension/src/browser/serial/plotter/plotter-frontend-contribution.ts @@ -91,7 +91,7 @@ export class PlotterFrontendContribution extends Contribution { const settings = this.monitorManagerProxy.getCurrentSettings(board, port); if ('baudrate' in settings) { // Convert from string to numbers - baudrates = settings['baudrate'].values.map(b => +b); + baudrates = settings['baudrate'].values.map((b) => +b); currentBaudrate = +settings['baudrate'].selectedValue; } } diff --git a/arduino-ide-extension/src/common/protocol/monitor-service.ts b/arduino-ide-extension/src/common/protocol/monitor-service.ts index 765b7dc21..4cfb76a39 100644 --- a/arduino-ide-extension/src/common/protocol/monitor-service.ts +++ b/arduino-ide-extension/src/common/protocol/monitor-service.ts @@ -1,4 +1,5 @@ import { Event, JsonRpcServer } from '@theia/core'; +import { MonitorSettings } from '../../node/monitor-settings/monitor-settings-provider'; import { Board, Port } from './boards-service'; export const MonitorManagerProxyFactory = Symbol('MonitorManagerProxyFactory'); @@ -53,8 +54,6 @@ export interface MonitorSetting { selectedValue: string; } -export type MonitorSettings = Record; - export namespace Monitor { export enum Command { SEND_MESSAGE = 'MONITOR_SEND_MESSAGE', diff --git a/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts b/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts index e1fa1eefc..9e78274bb 100644 --- a/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts +++ b/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts @@ -1,80 +1,92 @@ -import { inject, injectable } from "@theia/core/shared/inversify"; -import { MonitorManagerProxy, MonitorManagerProxyClient, MonitorSettings, Status } from "../common/protocol"; -import { Board, Port } from "../common/protocol"; -import { MonitorManager } from "./monitor-manager"; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { + MonitorManagerProxy, + MonitorManagerProxyClient, + Status, +} from '../common/protocol'; +import { Board, Port } from '../common/protocol'; +import { MonitorManager } from './monitor-manager'; +import { MonitorSettings } from './monitor-settings/monitor-settings-provider'; @injectable() export class MonitorManagerProxyImpl implements MonitorManagerProxy { - protected client: MonitorManagerProxyClient; + protected client: MonitorManagerProxyClient; - constructor( - @inject(MonitorManager) - protected readonly manager: MonitorManager, - ) { - } + constructor( + @inject(MonitorManager) + protected readonly manager: MonitorManager + ) {} - dispose(): void { - this.client?.disconnect(); - } + 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?: MonitorSettings): Promise { - 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)); - } + /** + * 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?: MonitorSettings + ): Promise { + if (settings) { + await this.changeMonitorSettings(board, port, settings); } - - /** - * 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: MonitorSettings): Promise { - if (!this.manager.isStarted(board, port)) { - // Monitor is not running, no need to change settings - return; - } - return this.manager.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)); } + } - /** - * Stops a running pluggable monitor. - * @param board board connected to port - * @param port port monitored - */ - async stopMonitor(board: Board, port: Port): Promise { - return this.manager.stopMonitor(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: MonitorSettings + ): Promise { + if (!this.manager.isStarted(board, port)) { + // Monitor is not running, no need to change settings + return; } + return this.manager.changeMonitorSettings(board, port, settings); + } - /** - * 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): MonitorSettings { - return this.manager.currentMonitorSettings(board, port); - } + /** + * Stops a running pluggable monitor. + * @param board board connected to port + * @param port port monitored + */ + async stopMonitor(board: Board, port: Port): Promise { + 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): MonitorSettings { + return this.manager.currentMonitorSettings(board, port); + } - setClient(client: MonitorManagerProxyClient | undefined): void { - if (!client) { - return; - } - this.client = client; + setClient(client: MonitorManagerProxyClient | undefined): void { + if (!client) { + return; } -} \ No newline at end of file + this.client = client; + } +} diff --git a/arduino-ide-extension/src/node/monitor-manager.ts b/arduino-ide-extension/src/node/monitor-manager.ts index e13708186..d4a565577 100644 --- a/arduino-ide-extension/src/node/monitor-manager.ts +++ b/arduino-ide-extension/src/node/monitor-manager.ts @@ -1,8 +1,9 @@ import { ILogger } from '@theia/core'; import { inject, injectable, named } from '@theia/core/shared/inversify'; -import { Board, Port, Status, MonitorSettings } from '../common/protocol'; +import { Board, Port, Status } from '../common/protocol'; import { CoreClientAware } from './core-client-provider'; import { MonitorService } from './monitor-service'; +import { MonitorSettings } from './monitor-settings/monitor-settings-provider'; type MonitorID = string; @@ -54,7 +55,7 @@ export class MonitorManager extends CoreClientAware { if (!monitor) { monitor = this.createMonitor(board, port); } - return await monitor.start(); + return await monitor.start(monitorID); } /** @@ -134,7 +135,7 @@ export class MonitorManager extends CoreClientAware { return Status.NOT_CONNECTED; } monitor.setUploadInProgress(false); - return await monitor.start(); + return await monitor.start(monitorID); } /** diff --git a/arduino-ide-extension/src/node/monitor-service.ts b/arduino-ide-extension/src/node/monitor-service.ts index 94b980b41..405fceaaa 100644 --- a/arduino-ide-extension/src/node/monitor-service.ts +++ b/arduino-ide-extension/src/node/monitor-service.ts @@ -1,13 +1,7 @@ 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, - MonitorSettings, - Monitor, -} from '../common/protocol'; +import { Board, Port, Status, Monitor } from '../common/protocol'; import { EnumerateMonitorPortSettingsRequest, EnumerateMonitorPortSettingsResponse, @@ -20,6 +14,10 @@ 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 WebSocketProviderImpl from './web-socket/web-socket-provider-impl'; +import { + MonitorSettings, + MonitorSettingsProvider, +} from './monitor-settings/monitor-settings-provider'; export const MonitorServiceName = 'monitor-service'; @@ -50,6 +48,10 @@ export class MonitorService extends CoreClientAware implements Disposable { protected readonly onDisposeEmitter = new Emitter(); readonly onDispose = this.onDisposeEmitter.event; + @inject(MonitorSettingsProvider) + protected readonly monitorSettingsProvider: MonitorSettingsProvider; + + // TODO: use dependency injection protected readonly webSocketProvider: WebSocketProvider = new WebSocketProviderImpl(); @@ -75,11 +77,6 @@ export class MonitorService extends CoreClientAware implements Disposable { this.dispose(); } }); - - // Sets default settings for this monitor - this.portMonitorSettings(port.protocol, board.fqbn!).then( - (settings) => (this.settings = settings) - ); } setUploadInProgress(status: boolean): void { @@ -108,9 +105,10 @@ export class MonitorService extends CoreClientAware implements Disposable { * 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(): Promise { + async start(monitorID: string): Promise { if (this.duplex) { return Status.ALREADY_CONNECTED; } @@ -124,6 +122,10 @@ export class MonitorService extends CoreClientAware implements Disposable { } this.logger.info('starting monitor'); + this.settings = await this.monitorSettingsProvider.init( + monitorID, + this.coreClientProvider + ); await this.coreClientProvider.initialized; const coreClient = await this.coreClient(); const { client, instance } = coreClient; @@ -281,6 +283,7 @@ export class MonitorService extends CoreClientAware implements Disposable { 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 diff --git a/arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider.ts b/arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider.ts new file mode 100644 index 000000000..959ff1ad8 --- /dev/null +++ b/arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider.ts @@ -0,0 +1,14 @@ +import { MonitorSetting } from '../../common/protocol'; +import { CoreClientProvider } from '../core-client-provider'; + +export type MonitorSettings = Record; + +export const MonitorSettingsProvider = Symbol('MonitorSettingsProvider'); +export interface MonitorSettingsProvider { + init( + id: string, + coreClientProvider: CoreClientProvider + ): Promise; + get(): Promise; + set(settings: MonitorSettings): Promise; +} From 355dec8aaa209f0ce247f3d98bf949fa8f751832 Mon Sep 17 00:00:00 2001 From: Francesco Stasi Date: Thu, 19 May 2022 10:40:01 +0200 Subject: [PATCH 31/35] monitor settings provider stub --- .../monitor-settings-provider-impl.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider-impl.ts diff --git a/arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider-impl.ts b/arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider-impl.ts new file mode 100644 index 000000000..87bb38ab2 --- /dev/null +++ b/arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider-impl.ts @@ -0,0 +1,39 @@ +import { injectable } from 'inversify'; +import { CoreClientProvider } from '../core-client-provider'; +import { + MonitorSettings, + MonitorSettingsProvider, +} from './monitor-settings-provider'; + +@injectable() +export class MonitorSettingsProviderImpl implements MonitorSettingsProvider { + init( + id: string, + coreClientProvider: CoreClientProvider + ): Promise { + throw new Error('Method not implemented.'); + + // query the CLI (via coreClientProvider) and return all available settings for the pluggable monitor. + // store these for later checkings + + // check for the settings file in the user's home directory + // if it doesn't exist, create it + + // if it does exist, start searching for the longest prefix matching the id + + // at the end of the search you can have a hit or a miss + + // if you have a miss, create a new entry with the id and all default settings coming from the CLI + + // if you have a hit, check if the existing settings are present in the settings from the CLI + // if they are not present in the CLI, remove from the settings file + // if there are settings in the CLI that are not in the file, add to the file with the default from the CLI + // save the updated settings file + } + get(): Promise { + throw new Error('Method not implemented.'); + } + set(settings: MonitorSettings): Promise { + throw new Error('Method not implemented.'); + } +} From 80ade4c37e1696ef161e3c0354849780660d6872 Mon Sep 17 00:00:00 2001 From: Francesco Stasi Date: Thu, 19 May 2022 10:59:20 +0200 Subject: [PATCH 32/35] updated pseudo code --- .../monitor-settings-provider-impl.ts | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider-impl.ts b/arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider-impl.ts index 87bb38ab2..49d150ed3 100644 --- a/arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider-impl.ts +++ b/arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider-impl.ts @@ -7,33 +7,40 @@ import { @injectable() export class MonitorSettingsProviderImpl implements MonitorSettingsProvider { + // this is populated with all settings coming from the CLI. This should never get modified + // as it is used to double actual values set by the user + private monitorSettings: MonitorSettings; + + // this contains values for setting of the monitorSettings + // the key is MonitorSetting.id, the value should be one of the MonitorSetting.values + private monitorSettingsValues: Record; + init( id: string, coreClientProvider: CoreClientProvider ): Promise { throw new Error('Method not implemented.'); - // query the CLI (via coreClientProvider) and return all available settings for the pluggable monitor. - // store these for later checkings - - // check for the settings file in the user's home directory - // if it doesn't exist, create it - - // if it does exist, start searching for the longest prefix matching the id + // 1. query the CLI (via coreClientProvider) and return all available settings for the pluggable monitor. + // store these in `monitorSettings` for later checkings - // at the end of the search you can have a hit or a miss - - // if you have a miss, create a new entry with the id and all default settings coming from the CLI - - // if you have a hit, check if the existing settings are present in the settings from the CLI - // if they are not present in the CLI, remove from the settings file - // if there are settings in the CLI that are not in the file, add to the file with the default from the CLI - // save the updated settings file + // 2. check for the settings file in the user's home directory + // a. if it doesn't exist, create it as an empty json file + // 3. search the file, looking for the longest prefix matching the id + // a. miss: populate `monitorSettingsValues` with all default settings from `monitorSettings` + // b. hit: populate `monitorSettingsValues` with the result for the search + // i. purge the `monitorSettingsValues` removing keys that are not defined in `monitorSettings` + // and adding those that are missing + // ii. save the `monitorSettingsValues` in the file, using the id as the key } get(): Promise { throw new Error('Method not implemented.'); } set(settings: MonitorSettings): Promise { throw new Error('Method not implemented.'); + + // 1. parse the settings parameter and remove any setting that is not defined in `monitorSettings` + // 2. update `monitorSettingsValues` accordingly + // 3. save it to the file } } From 0427759fdb98856ec588b0d3bb92c35a7e7e2ff3 Mon Sep 17 00:00:00 2001 From: Alberto Iannaccone Date: Thu, 19 May 2022 11:06:23 +0200 Subject: [PATCH 33/35] refactor monitor settings interfaces --- .../monitor-manager-proxy-client-impl.ts | 8 +++++-- .../src/browser/monitor-model.ts | 1 + .../browser/serial/monitor/monitor-widget.tsx | 23 +++++++++++-------- .../plotter/plotter-frontend-contribution.ts | 9 ++++---- .../src/common/protocol/monitor-service.ts | 15 +++++++----- .../src/node/monitor-manager-proxy-impl.ts | 8 +++---- .../src/node/monitor-manager.ts | 10 +++++--- .../src/node/monitor-service.ts | 16 +++++++------ .../monitor-settings-provider-impl.ts | 9 ++++---- .../monitor-settings-provider.ts | 15 ++++++++---- 10 files changed, 70 insertions(+), 44 deletions(-) diff --git a/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts b/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts index acde19986..5a9293d49 100644 --- a/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts +++ b/arduino-ide-extension/src/browser/monitor-manager-proxy-client-impl.ts @@ -6,7 +6,10 @@ import { MonitorManagerProxyClient, MonitorManagerProxyFactory, } from '../common/protocol/monitor-service'; -import { MonitorSettings } from '../node/monitor-settings/monitor-settings-provider'; +import { + PluggableMonitorSettings, + MonitorSettings, +} from '../node/monitor-settings/monitor-settings-provider'; @injectable() export class MonitorManagerProxyClientImpl @@ -85,7 +88,7 @@ export class MonitorManagerProxyClientImpl async startMonitor( board: Board, port: Port, - settings?: MonitorSettings + settings?: PluggableMonitorSettings ): Promise { return this.server().startMonitor(board, port, settings); } @@ -116,6 +119,7 @@ export class MonitorManagerProxyClientImpl JSON.stringify({ command: Monitor.Command.CHANGE_SETTINGS, // TODO: This might be wrong, verify if it works + // SPOILER: It doesn't data: settings, }) ); diff --git a/arduino-ide-extension/src/browser/monitor-model.ts b/arduino-ide-extension/src/browser/monitor-model.ts index ccbd073c6..41baca1ab 100644 --- a/arduino-ide-extension/src/browser/monitor-model.ts +++ b/arduino-ide-extension/src/browser/monitor-model.ts @@ -118,6 +118,7 @@ export class MonitorModel implements FrontendApplicationContribution { } } +// TODO: Move this to /common export namespace MonitorModel { export interface State { autoscroll: boolean; diff --git a/arduino-ide-extension/src/browser/serial/monitor/monitor-widget.tsx b/arduino-ide-extension/src/browser/serial/monitor/monitor-widget.tsx index 9d9a08304..ce098be68 100644 --- a/arduino-ide-extension/src/browser/serial/monitor/monitor-widget.tsx +++ b/arduino-ide-extension/src/browser/serial/monitor/monitor-widget.tsx @@ -182,20 +182,22 @@ export class MonitorWidget extends ReactWidget { // This breaks if the user tries to open a monitor that // doesn't support the baudrate setting. protected get baudRates(): string[] { - const settings = this.getCurrentSettings(); - const baudRateSettings = settings['baudrate']; - if (!baudRateSettings) { + const { pluggableMonitorSettings } = this.getCurrentSettings(); + if (!pluggableMonitorSettings || !pluggableMonitorSettings['baudrate']) { return []; } + + const baudRateSettings = pluggableMonitorSettings['baudrate']; + return baudRateSettings.values; } protected get selectedBaudRate(): string { - const settings = this.getCurrentSettings(); - const baudRateSettings = settings['baudrate']; - if (!baudRateSettings) { + const { pluggableMonitorSettings } = this.getCurrentSettings(); + if (!pluggableMonitorSettings || !pluggableMonitorSettings['baudrate']) { return ''; } + const baudRateSettings = pluggableMonitorSettings['baudrate']; return baudRateSettings.selectedValue; } @@ -260,8 +262,11 @@ export class MonitorWidget extends ReactWidget { }; protected readonly onChangeBaudRate = (value: string) => { - const settings = this.getCurrentSettings(); - settings['baudrate'].selectedValue = value; - this.monitorManagerProxy.changeSettings(settings); + const { pluggableMonitorSettings } = this.getCurrentSettings(); + if (!pluggableMonitorSettings || !pluggableMonitorSettings['baudrate']) + return; + const baudRateSettings = pluggableMonitorSettings['baudrate']; + baudRateSettings.selectedValue = value; + this.monitorManagerProxy.changeSettings(pluggableMonitorSettings); }; } diff --git a/arduino-ide-extension/src/browser/serial/plotter/plotter-frontend-contribution.ts b/arduino-ide-extension/src/browser/serial/plotter/plotter-frontend-contribution.ts index 1cc5e0f66..083477e05 100644 --- a/arduino-ide-extension/src/browser/serial/plotter/plotter-frontend-contribution.ts +++ b/arduino-ide-extension/src/browser/serial/plotter/plotter-frontend-contribution.ts @@ -88,11 +88,12 @@ export class PlotterFrontendContribution extends Contribution { let baudrates: number[] = []; let currentBaudrate = -1; if (board && port) { - const settings = this.monitorManagerProxy.getCurrentSettings(board, port); - if ('baudrate' in settings) { + const { pluggableMonitorSettings } = + this.monitorManagerProxy.getCurrentSettings(board, port); + if (pluggableMonitorSettings && 'baudrate' in pluggableMonitorSettings) { // Convert from string to numbers - baudrates = settings['baudrate'].values.map((b) => +b); - currentBaudrate = +settings['baudrate'].selectedValue; + baudrates = pluggableMonitorSettings['baudrate'].values.map((b) => +b); + currentBaudrate = +pluggableMonitorSettings['baudrate'].selectedValue; } } diff --git a/arduino-ide-extension/src/common/protocol/monitor-service.ts b/arduino-ide-extension/src/common/protocol/monitor-service.ts index 4cfb76a39..18f67367e 100644 --- a/arduino-ide-extension/src/common/protocol/monitor-service.ts +++ b/arduino-ide-extension/src/common/protocol/monitor-service.ts @@ -1,5 +1,8 @@ import { Event, JsonRpcServer } from '@theia/core'; -import { MonitorSettings } from '../../node/monitor-settings/monitor-settings-provider'; +import { + PluggableMonitorSettings, + MonitorSettings, +} from '../../node/monitor-settings/monitor-settings-provider'; import { Board, Port } from './boards-service'; export const MonitorManagerProxyFactory = Symbol('MonitorManagerProxyFactory'); @@ -12,15 +15,15 @@ export interface MonitorManagerProxy startMonitor( board: Board, port: Port, - settings?: MonitorSettings + settings?: PluggableMonitorSettings ): Promise; changeMonitorSettings( board: Board, port: Port, - settings: MonitorSettings + settings: PluggableMonitorSettings ): Promise; stopMonitor(board: Board, port: Port): Promise; - getCurrentSettings(board: Board, port: Port): MonitorSettings; + getCurrentSettings(board: Board, port: Port): PluggableMonitorSettings; } export const MonitorManagerProxyClient = Symbol('MonitorManagerProxyClient'); @@ -34,14 +37,14 @@ export interface MonitorManagerProxyClient { startMonitor( board: Board, port: Port, - settings?: MonitorSettings + settings?: PluggableMonitorSettings ): Promise; getCurrentSettings(board: Board, port: Port): MonitorSettings; send(message: string): void; changeSettings(settings: MonitorSettings): void; } -export interface MonitorSetting { +export interface PluggableMonitorSetting { // The setting identifier readonly id: string; // A human-readable label of the setting (to be displayed on the GUI) diff --git a/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts b/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts index 9e78274bb..75ade7452 100644 --- a/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts +++ b/arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts @@ -6,7 +6,7 @@ import { } from '../common/protocol'; import { Board, Port } from '../common/protocol'; import { MonitorManager } from './monitor-manager'; -import { MonitorSettings } from './monitor-settings/monitor-settings-provider'; +import { PluggableMonitorSettings } from './monitor-settings/monitor-settings-provider'; @injectable() export class MonitorManagerProxyImpl implements MonitorManagerProxy { @@ -32,7 +32,7 @@ export class MonitorManagerProxyImpl implements MonitorManagerProxy { async startMonitor( board: Board, port: Port, - settings?: MonitorSettings + settings?: PluggableMonitorSettings ): Promise { if (settings) { await this.changeMonitorSettings(board, port, settings); @@ -54,7 +54,7 @@ export class MonitorManagerProxyImpl implements MonitorManagerProxy { async changeMonitorSettings( board: Board, port: Port, - settings: MonitorSettings + settings: PluggableMonitorSettings ): Promise { if (!this.manager.isStarted(board, port)) { // Monitor is not running, no need to change settings @@ -79,7 +79,7 @@ export class MonitorManagerProxyImpl implements MonitorManagerProxy { * @param port port monitored * @returns a map of MonitorSetting */ - getCurrentSettings(board: Board, port: Port): MonitorSettings { + getCurrentSettings(board: Board, port: Port): PluggableMonitorSettings { return this.manager.currentMonitorSettings(board, port); } diff --git a/arduino-ide-extension/src/node/monitor-manager.ts b/arduino-ide-extension/src/node/monitor-manager.ts index d4a565577..265f1f189 100644 --- a/arduino-ide-extension/src/node/monitor-manager.ts +++ b/arduino-ide-extension/src/node/monitor-manager.ts @@ -3,7 +3,7 @@ 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 { MonitorSettings } from './monitor-settings/monitor-settings-provider'; +import { PluggableMonitorSettings } from './monitor-settings/monitor-settings-provider'; type MonitorID = string; @@ -145,7 +145,11 @@ export class MonitorManager extends CoreClientAware { * @param port port to monitor * @param settings monitor settings to change */ - changeMonitorSettings(board: Board, port: Port, settings: MonitorSettings) { + changeMonitorSettings( + board: Board, + port: Port, + settings: PluggableMonitorSettings + ) { const monitorID = this.monitorID(board, port); let monitor = this.monitorServices.get(monitorID); if (!monitor) { @@ -161,7 +165,7 @@ export class MonitorManager extends CoreClientAware { * @param port port monitored * @returns map of current monitor settings */ - currentMonitorSettings(board: Board, port: Port): MonitorSettings { + currentMonitorSettings(board: Board, port: Port): PluggableMonitorSettings { const monitorID = this.monitorID(board, port); const monitor = this.monitorServices.get(monitorID); if (!monitor) { diff --git a/arduino-ide-extension/src/node/monitor-service.ts b/arduino-ide-extension/src/node/monitor-service.ts index 405fceaaa..1696756f7 100644 --- a/arduino-ide-extension/src/node/monitor-service.ts +++ b/arduino-ide-extension/src/node/monitor-service.ts @@ -15,7 +15,7 @@ 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 WebSocketProviderImpl from './web-socket/web-socket-provider-impl'; import { - MonitorSettings, + PluggableMonitorSettings, MonitorSettingsProvider, } from './monitor-settings/monitor-settings-provider'; @@ -28,7 +28,7 @@ export class MonitorService extends CoreClientAware implements Disposable { // Settings used by the currently running pluggable monitor. // They can be freely modified while running. - protected settings: MonitorSettings; + protected settings: PluggableMonitorSettings; // List of messages received from the running pluggable monitor. // These are flushed from time to time to the frontend. @@ -279,7 +279,7 @@ export class MonitorService extends CoreClientAware implements Disposable { * * @returns map of current monitor settings */ - currentSettings(): MonitorSettings { + currentSettings(): PluggableMonitorSettings { return this.settings; } @@ -294,7 +294,7 @@ export class MonitorService extends CoreClientAware implements Disposable { private async portMonitorSettings( protocol: string, fqbn: string - ): Promise { + ): Promise { await this.coreClientProvider.initialized; const coreClient = await this.coreClient(); const { client, instance } = coreClient; @@ -314,7 +314,7 @@ export class MonitorService extends CoreClientAware implements Disposable { } ); - const settings: MonitorSettings = {}; + const settings: PluggableMonitorSettings = {}; for (const iterator of res.getSettingsList()) { settings[iterator.getSettingId()] = { id: iterator.getSettingId(), @@ -335,7 +335,7 @@ export class MonitorService extends CoreClientAware implements Disposable { * @param settings map of monitor settings to change * @returns a status to verify settings have been sent. */ - async changeSettings(settings: MonitorSettings): Promise { + async changeSettings(settings: PluggableMonitorSettings): Promise { const config = new MonitorPortConfiguration(); for (const id in settings) { const s = new MonitorPortSetting(); @@ -384,7 +384,9 @@ export class MonitorService extends CoreClientAware implements Disposable { this.send(message.data); break; case Monitor.Command.CHANGE_SETTINGS: - const settings: MonitorSettings = JSON.parse(message.data); + const settings: PluggableMonitorSettings = JSON.parse( + message.data + ); this.changeSettings(settings); break; } diff --git a/arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider-impl.ts b/arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider-impl.ts index 49d150ed3..40dca6d71 100644 --- a/arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider-impl.ts +++ b/arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider-impl.ts @@ -1,8 +1,9 @@ import { injectable } from 'inversify'; import { CoreClientProvider } from '../core-client-provider'; import { - MonitorSettings, + PluggableMonitorSettings, MonitorSettingsProvider, + MonitorSettings, } from './monitor-settings-provider'; @injectable() @@ -18,7 +19,7 @@ export class MonitorSettingsProviderImpl implements MonitorSettingsProvider { init( id: string, coreClientProvider: CoreClientProvider - ): Promise { + ): Promise { throw new Error('Method not implemented.'); // 1. query the CLI (via coreClientProvider) and return all available settings for the pluggable monitor. @@ -33,10 +34,10 @@ export class MonitorSettingsProviderImpl implements MonitorSettingsProvider { // and adding those that are missing // ii. save the `monitorSettingsValues` in the file, using the id as the key } - get(): Promise { + get(): Promise { throw new Error('Method not implemented.'); } - set(settings: MonitorSettings): Promise { + set(settings: PluggableMonitorSettings): Promise { throw new Error('Method not implemented.'); // 1. parse the settings parameter and remove any setting that is not defined in `monitorSettings` diff --git a/arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider.ts b/arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider.ts index 959ff1ad8..5ca5ad4a7 100644 --- a/arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider.ts +++ b/arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider.ts @@ -1,14 +1,19 @@ -import { MonitorSetting } from '../../common/protocol'; +import { MonitorModel } from '../../browser/monitor-model'; +import { PluggableMonitorSetting } from '../../common/protocol'; import { CoreClientProvider } from '../core-client-provider'; -export type MonitorSettings = Record; +export type PluggableMonitorSettings = Record; +export interface MonitorSettings { + pluggableMonitorSettings?: PluggableMonitorSettings; + monitorUISettings?: Partial; +} export const MonitorSettingsProvider = Symbol('MonitorSettingsProvider'); export interface MonitorSettingsProvider { init( id: string, coreClientProvider: CoreClientProvider - ): Promise; - get(): Promise; - set(settings: MonitorSettings): Promise; + ): Promise; + get(): Promise; + set(settings: PluggableMonitorSettings): Promise; } From a4ff05a82b20ffbbfca9f3cbf96f90035d27dc18 Mon Sep 17 00:00:00 2001 From: Francesco Stasi Date: Thu, 19 May 2022 16:56:37 +0200 Subject: [PATCH 34/35] monitor service provider singleton --- .../src/node/arduino-ide-backend-module.ts | 11 +- .../src/node/monitor-service.ts | 12 +- .../monitor-settings-provider-impl.ts | 162 ++++++++++++++---- .../monitor-settings-provider.ts | 13 +- 4 files changed, 157 insertions(+), 41 deletions(-) diff --git a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts index 9ecd1bdea..da1ef8a08 100644 --- a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts +++ b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts @@ -85,8 +85,14 @@ import { ArduinoLocalizationContribution } from './arduino-localization-contribu 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 { + MonitorManagerProxy, + MonitorManagerProxyClient, + MonitorManagerProxyPath, +} from '../common/protocol/monitor-service'; import { MonitorServiceName } from './monitor-service'; +import { MonitorSettingsProvider } from './monitor-settings/monitor-settings-provider'; +import { MonitorSettingsProviderImpl } from './monitor-settings/monitor-settings-provider-impl'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(BackendApplication).toSelf().inSingletonScope(); @@ -198,6 +204,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { // a single MonitorManager is responsible for handling the actual connections to the pluggable monitors bind(MonitorManager).toSelf().inSingletonScope(); + bind(MonitorSettingsProviderImpl).toSelf().inSingletonScope(); + bind(MonitorSettingsProvider).toService(MonitorSettingsProviderImpl); + // Serial client provider per connected frontend. bind(ConnectionContainerModule).toConstantValue( ConnectionContainerModule.create(({ bind, bindBackendService }) => { diff --git a/arduino-ide-extension/src/node/monitor-service.ts b/arduino-ide-extension/src/node/monitor-service.ts index 1696756f7..e3facecf6 100644 --- a/arduino-ide-extension/src/node/monitor-service.ts +++ b/arduino-ide-extension/src/node/monitor-service.ts @@ -122,10 +122,18 @@ export class MonitorService extends CoreClientAware implements Disposable { } this.logger.info('starting monitor'); - this.settings = await this.monitorSettingsProvider.init( + + // 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, - this.coreClientProvider + defaultSettings ); + await this.coreClientProvider.initialized; const coreClient = await this.coreClient(); const { client, instance } = coreClient; diff --git a/arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider-impl.ts b/arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider-impl.ts index 40dca6d71..54e3b5690 100644 --- a/arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider-impl.ts +++ b/arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider-impl.ts @@ -1,47 +1,145 @@ -import { injectable } from 'inversify'; -import { CoreClientProvider } from '../core-client-provider'; +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, - MonitorSettings, } 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 { - // this is populated with all settings coming from the CLI. This should never get modified - // as it is used to double actual values set by the user - private monitorSettings: MonitorSettings; + @inject(EnvVariablesServer) + protected readonly envVariablesServer: EnvVariablesServer; + + protected ready = new Deferred(); + + // 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; + + private pluggableMonitorSettingsPath: string; + + @postConstruct() + protected async init(): Promise { + // get the monitor settings file path + const configDirUri = await this.envVariablesServer.getConfigDirUri(); + this.pluggableMonitorSettingsPath = join( + FileUri.fsPath(configDirUri), + MONITOR_SETTINGS_FILE + ); - // this contains values for setting of the monitorSettings - // the key is MonitorSetting.id, the value should be one of the MonitorSetting.values - private monitorSettingsValues: Record; + // read existing settings + this.readFile(); - init( - id: string, - coreClientProvider: CoreClientProvider + console.log(this.monitorSettings); + this.ready.resolve(); + } + + async getSettings( + monitorId: string, + defaultSettings: PluggableMonitorSettings ): Promise { - throw new Error('Method not implemented.'); - - // 1. query the CLI (via coreClientProvider) and return all available settings for the pluggable monitor. - // store these in `monitorSettings` for later checkings - - // 2. check for the settings file in the user's home directory - // a. if it doesn't exist, create it as an empty json file - // 3. search the file, looking for the longest prefix matching the id - // a. miss: populate `monitorSettingsValues` with all default settings from `monitorSettings` - // b. hit: populate `monitorSettingsValues` with the result for the search - // i. purge the `monitorSettingsValues` removing keys that are not defined in `monitorSettings` - // and adding those that are missing - // ii. save the `monitorSettingsValues` in the file, using the id as the key + // 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 { + // 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 { + 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 = {}; + } } - get(): Promise { - throw new Error('Method not implemented.'); + + private async writeFile() { + await promisify(fs.writeFile)( + this.pluggableMonitorSettingsPath, + JSON.stringify(this.monitorSettings) + ); } - set(settings: PluggableMonitorSettings): Promise { - throw new Error('Method not implemented.'); - // 1. parse the settings parameter and remove any setting that is not defined in `monitorSettings` - // 2. update `monitorSettingsValues` accordingly - // 3. save it to the file + 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 }; } } diff --git a/arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider.ts b/arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider.ts index 5ca5ad4a7..e8949a60b 100644 --- a/arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider.ts +++ b/arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider.ts @@ -1,6 +1,5 @@ import { MonitorModel } from '../../browser/monitor-model'; import { PluggableMonitorSetting } from '../../common/protocol'; -import { CoreClientProvider } from '../core-client-provider'; export type PluggableMonitorSettings = Record; export interface MonitorSettings { @@ -10,10 +9,12 @@ export interface MonitorSettings { export const MonitorSettingsProvider = Symbol('MonitorSettingsProvider'); export interface MonitorSettingsProvider { - init( - id: string, - coreClientProvider: CoreClientProvider + getSettings( + monitorId: string, + defaultSettings: PluggableMonitorSettings + ): Promise; + setSettings( + monitorId: string, + settings: PluggableMonitorSettings ): Promise; - get(): Promise; - set(settings: PluggableMonitorSettings): Promise; } From 9a16cf9e02a17630693e7bf83058861fd5b908b5 Mon Sep 17 00:00:00 2001 From: David Simpson <45690499+davegarthsimpson@users.noreply.github.com> Date: Fri, 20 May 2022 17:53:34 +0200 Subject: [PATCH 35/35] change MonitorService providers to injectable deps --- .../src/node/arduino-ide-backend-module.ts | 37 ++++++++++++++++++- .../src/node/monitor-manager.ts | 11 ++++-- .../src/node/monitor-service-factory.ts | 18 +++++++++ .../src/node/monitor-service.ts | 12 ++---- .../monitor-settings-provider-impl.ts | 2 +- .../web-socket/web-socket-provider-impl.ts | 2 + .../node/web-socket/web-socket-provider.ts | 1 + 7 files changed, 69 insertions(+), 14 deletions(-) create mode 100644 arduino-ide-extension/src/node/monitor-service-factory.ts diff --git a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts index da1ef8a08..50cc4d16c 100644 --- a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts +++ b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts @@ -90,9 +90,15 @@ import { MonitorManagerProxyClient, MonitorManagerProxyPath, } from '../common/protocol/monitor-service'; -import { MonitorServiceName } from './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(); @@ -204,9 +210,38 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { // 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); + + const monitorSettingsProvider = container.get( + MonitorSettingsProvider + ); + + const webSocketProvider = + container.get(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 }) => { diff --git a/arduino-ide-extension/src/node/monitor-manager.ts b/arduino-ide-extension/src/node/monitor-manager.ts index 265f1f189..02b79274a 100644 --- a/arduino-ide-extension/src/node/monitor-manager.ts +++ b/arduino-ide-extension/src/node/monitor-manager.ts @@ -3,6 +3,7 @@ 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; @@ -17,6 +18,9 @@ export class MonitorManager extends CoreClientAware { // be started. private monitorServices = new Map(); + @inject(MonitorServiceFactory) + private monitorServiceFactory: MonitorServiceFactory; + constructor( @inject(ILogger) @named(MonitorManagerName) @@ -183,12 +187,11 @@ export class MonitorManager extends CoreClientAware { */ private createMonitor(board: Board, port: Port): MonitorService { const monitorID = this.monitorID(board, port); - const monitor = new MonitorService( - this.logger, + const monitor = this.monitorServiceFactory({ board, port, - this.coreClientProvider - ); + coreClientProvider: this.coreClientProvider, + }); this.monitorServices.set(monitorID, monitor); monitor.onDispose( (() => { diff --git a/arduino-ide-extension/src/node/monitor-service-factory.ts b/arduino-ide-extension/src/node/monitor-service-factory.ts new file mode 100644 index 000000000..30213536f --- /dev/null +++ b/arduino-ide-extension/src/node/monitor-service-factory.ts @@ -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; +} diff --git a/arduino-ide-extension/src/node/monitor-service.ts b/arduino-ide-extension/src/node/monitor-service.ts index e3facecf6..d522f55e1 100644 --- a/arduino-ide-extension/src/node/monitor-service.ts +++ b/arduino-ide-extension/src/node/monitor-service.ts @@ -13,7 +13,6 @@ import { 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 WebSocketProviderImpl from './web-socket/web-socket-provider-impl'; import { PluggableMonitorSettings, MonitorSettingsProvider, @@ -48,19 +47,16 @@ export class MonitorService extends CoreClientAware implements Disposable { protected readonly onDisposeEmitter = new Emitter(); readonly onDispose = this.onDisposeEmitter.event; - @inject(MonitorSettingsProvider) - protected readonly monitorSettingsProvider: MonitorSettingsProvider; - - // TODO: use dependency injection - protected readonly webSocketProvider: WebSocketProvider = - new WebSocketProviderImpl(); - 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, diff --git a/arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider-impl.ts b/arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider-impl.ts index 54e3b5690..a32f8c482 100644 --- a/arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider-impl.ts +++ b/arduino-ide-extension/src/node/monitor-settings/monitor-settings-provider-impl.ts @@ -40,7 +40,7 @@ export class MonitorSettingsProviderImpl implements MonitorSettingsProvider { ); // read existing settings - this.readFile(); + await this.readFile(); console.log(this.monitorSettings); this.ready.resolve(); diff --git a/arduino-ide-extension/src/node/web-socket/web-socket-provider-impl.ts b/arduino-ide-extension/src/node/web-socket/web-socket-provider-impl.ts index 268928dd2..463dadcf7 100644 --- a/arduino-ide-extension/src/node/web-socket/web-socket-provider-impl.ts +++ b/arduino-ide-extension/src/node/web-socket/web-socket-provider-impl.ts @@ -1,7 +1,9 @@ import { Emitter } from '@theia/core'; +import { injectable } from '@theia/core/shared/inversify'; import * as WebSocket from 'ws'; import { WebSocketProvider } from './web-socket-provider'; +@injectable() export default class WebSocketProviderImpl implements WebSocketProvider { protected wsClients: WebSocket[]; protected server: WebSocket.Server; diff --git a/arduino-ide-extension/src/node/web-socket/web-socket-provider.ts b/arduino-ide-extension/src/node/web-socket/web-socket-provider.ts index 7c402ad54..6aa102040 100644 --- a/arduino-ide-extension/src/node/web-socket/web-socket-provider.ts +++ b/arduino-ide-extension/src/node/web-socket/web-socket-provider.ts @@ -1,6 +1,7 @@ import { Event } from '@theia/core/lib/common/event'; import * as WebSocket from 'ws'; +export const WebSocketProvider = Symbol('WebSocketProvider'); export interface WebSocketProvider { getAddress(): WebSocket.AddressInfo; sendMessage(message: string): void;