mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-07-12 13:56:34 +00:00
Implemented serial-monitoring for the backend.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
This commit is contained in:
parent
8d79bb3ffb
commit
692c3f6e3f
@ -43,4 +43,14 @@ export namespace ArduinoCommands {
|
||||
id: "arduino-toggle-pro-mode"
|
||||
}
|
||||
|
||||
export const CONNECT_TODO: Command = {
|
||||
id: 'connect-to-attached-board',
|
||||
label: 'Connect to Attached Board'
|
||||
}
|
||||
|
||||
export const SEND: Command = {
|
||||
id: 'send',
|
||||
label: 'Send a Message to the Connected Board'
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { CommandContribution, CommandRegistry, Command } from '@theia/core/lib/common/command';
|
||||
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { BoardsService } from '../common/protocol/boards-service';
|
||||
import { BoardsService, AttachedSerialBoard } from '../common/protocol/boards-service';
|
||||
import { ArduinoCommands } from './arduino-commands';
|
||||
import { CoreService } from '../common/protocol/core-service';
|
||||
import { WorkspaceServiceExt } from './workspace-service-ext';
|
||||
@ -19,7 +19,18 @@ import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service
|
||||
import { SketchFactory } from './sketch-factory';
|
||||
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
|
||||
import { EditorManager, EditorMainMenu } from '@theia/editor/lib/browser';
|
||||
import { ContextMenuRenderer, OpenerService, Widget, StatusBar, ShellLayoutRestorer, StatusBarAlignment, LabelProvider } from '@theia/core/lib/browser';
|
||||
import {
|
||||
ContextMenuRenderer,
|
||||
OpenerService,
|
||||
Widget,
|
||||
StatusBar,
|
||||
ShellLayoutRestorer,
|
||||
StatusBarAlignment,
|
||||
QuickOpenItem,
|
||||
QuickOpenMode,
|
||||
QuickOpenService,
|
||||
LabelProvider
|
||||
} from '@theia/core/lib/browser';
|
||||
import { OpenFileDialogProps, FileDialogService } from '@theia/filesystem/lib/browser/file-dialog';
|
||||
import { FileSystem, FileStat } from '@theia/filesystem/lib/common';
|
||||
import { ArduinoToolbarContextMenu } from './arduino-file-menu';
|
||||
@ -34,6 +45,7 @@ import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { BoardsConfigDialog } from './boards/boards-config-dialog';
|
||||
import { BoardsToolBarItem } from './boards/boards-toolbar-item';
|
||||
import { BoardsConfig } from './boards/boards-config';
|
||||
import { MonitorService } from '../common/protocol/monitor-service';
|
||||
|
||||
export namespace ArduinoMenus {
|
||||
export const SKETCH = [...MAIN_MENU_BAR, '3_sketch'];
|
||||
@ -56,6 +68,12 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
|
||||
@inject(CoreService)
|
||||
protected readonly coreService: CoreService;
|
||||
|
||||
@inject(MonitorService)
|
||||
protected readonly monitorService: MonitorService;
|
||||
|
||||
// TODO: make this better!
|
||||
protected connectionId: string | undefined;
|
||||
|
||||
@inject(WorkspaceServiceExt)
|
||||
protected readonly workspaceServiceExt: WorkspaceServiceExt;
|
||||
|
||||
@ -116,6 +134,9 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
|
||||
@inject(LabelProvider)
|
||||
protected readonly labelProvider: LabelProvider;
|
||||
|
||||
@inject(QuickOpenService)
|
||||
protected readonly quickOpenService: QuickOpenService;
|
||||
|
||||
protected boardsToolbarItem: BoardsToolBarItem | null;
|
||||
protected wsSketchCount: number = 0;
|
||||
|
||||
@ -293,7 +314,7 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
|
||||
this.boardsServiceClient.boardsConfig = boardsConfig;
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
registry.registerCommand(ArduinoCommands.TOGGLE_PRO_MODE, {
|
||||
execute: () => {
|
||||
const oldModeState = ARDUINO_PRO_MODE;
|
||||
@ -301,6 +322,65 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
|
||||
registry.executeCommand('reset.layout');
|
||||
},
|
||||
isToggled: () => ARDUINO_PRO_MODE
|
||||
});
|
||||
registry.registerCommand(ArduinoCommands.CONNECT_TODO, {
|
||||
execute: async () => {
|
||||
const { boardsConfig } = this.boardsServiceClient;
|
||||
const { selectedBoard, selectedPort } = boardsConfig;
|
||||
if (!selectedBoard) {
|
||||
this.messageService.warn('No boards selected.');
|
||||
return;
|
||||
}
|
||||
const { name } = selectedBoard;
|
||||
if (!selectedPort) {
|
||||
this.messageService.warn(`No ports selected for board: '${name}'.`);
|
||||
return;
|
||||
}
|
||||
const attachedBoards = await this.boardsService.getAttachedBoards();
|
||||
const connectedBoard = attachedBoards.boards.filter(AttachedSerialBoard.is).find(board => BoardsConfig.Config.sameAs(boardsConfig, board));
|
||||
if (!connectedBoard) {
|
||||
this.messageService.warn(`The selected '${name}' board is not connected on ${selectedPort}.`);
|
||||
return;
|
||||
}
|
||||
if (this.connectionId) {
|
||||
console.log('>>> Disposing existing monitor connection before establishing a new one...');
|
||||
const result = await this.monitorService.disconnect(this.connectionId);
|
||||
if (!result) {
|
||||
// TODO: better!!!
|
||||
console.error(`Could not close connection: ${this.connectionId}. Check the backend logs.`);
|
||||
} else {
|
||||
console.log(`<<< Disposed ${this.connectionId} connection.`)
|
||||
}
|
||||
}
|
||||
const { connectionId } = await this.monitorService.connect({ board: selectedBoard, port: selectedPort });
|
||||
this.connectionId = connectionId;
|
||||
}
|
||||
});
|
||||
registry.registerCommand(ArduinoCommands.SEND, {
|
||||
isEnabled: () => !!this.connectionId,
|
||||
execute: async () => {
|
||||
const { monitorService, connectionId } = this;
|
||||
const model = {
|
||||
onType(lookFor: string, acceptor: (items: QuickOpenItem[]) => void): void {
|
||||
acceptor([
|
||||
new QuickOpenItem({
|
||||
label: "Type your message and press 'Enter' to send it to the board. Escape to cancel.",
|
||||
run: (mode: QuickOpenMode): boolean => {
|
||||
if (mode !== QuickOpenMode.OPEN) {
|
||||
return false;
|
||||
}
|
||||
monitorService.send(connectionId!, lookFor + '\n');
|
||||
return true;
|
||||
}
|
||||
})
|
||||
]);
|
||||
}
|
||||
};
|
||||
const options = {
|
||||
placeholder: "Your message. The message will be suffixed with a LF ['\\n'].",
|
||||
};
|
||||
this.quickOpenService.open(model, options);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -54,6 +54,8 @@ import { SilentSearchInWorkspaceContribution } from './customization/silent-sear
|
||||
import { LibraryListWidgetFrontendContribution } from './library/library-widget-frontend-contribution';
|
||||
import { LibraryItemRenderer } from './library/library-item-renderer';
|
||||
import { BoardItemRenderer } from './boards/boards-item-renderer';
|
||||
import { MonitorServiceClientImpl } from './monitor/monitor-service-client-impl';
|
||||
import { MonitorServicePath, MonitorService, MonitorServiceClient } from '../common/protocol/monitor-service';
|
||||
const ElementQueries = require('css-element-queries/src/ElementQueries');
|
||||
|
||||
if (!ARDUINO_PRO_MODE) {
|
||||
@ -149,6 +151,20 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
|
||||
return workspaceServiceExt;
|
||||
});
|
||||
|
||||
// Frontend binding for the monitor service.
|
||||
bind(MonitorService).toDynamicValue(context => {
|
||||
const connection = context.container.get(WebSocketConnectionProvider);
|
||||
const client = context.container.get(MonitorServiceClientImpl);
|
||||
return connection.createProxy(MonitorServicePath, client);
|
||||
}).inSingletonScope();
|
||||
// Monitor service client to receive and delegate notifications from the backend.
|
||||
bind(MonitorServiceClientImpl).toSelf().inSingletonScope();
|
||||
bind(MonitorServiceClient).toDynamicValue(context => {
|
||||
const client = context.container.get(MonitorServiceClientImpl);
|
||||
WebSocketConnectionProvider.createProxy(context.container, MonitorServicePath, client);
|
||||
return client;
|
||||
}).inSingletonScope();
|
||||
|
||||
bind(AWorkspaceService).toSelf().inSingletonScope();
|
||||
rebind(WorkspaceService).to(AWorkspaceService).inSingletonScope();
|
||||
bind(SketchFactory).toSelf().inSingletonScope();
|
||||
|
@ -0,0 +1,23 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { MonitorServiceClient, MonitorReadEvent, MonitorError } from '../../common/protocol/monitor-service';
|
||||
|
||||
@injectable()
|
||||
export class MonitorServiceClientImpl implements MonitorServiceClient {
|
||||
|
||||
protected readonly onReadEmitter = new Emitter<MonitorReadEvent>();
|
||||
protected readonly onErrorEmitter = new Emitter<MonitorError>();
|
||||
readonly onRead = this.onReadEmitter.event;
|
||||
readonly onError = this.onErrorEmitter.event;
|
||||
|
||||
notifyRead(event: MonitorReadEvent): void {
|
||||
this.onReadEmitter.fire(event);
|
||||
const { connectionId, data } = event;
|
||||
console.log(`Received data from ${connectionId}: ${data}`);
|
||||
}
|
||||
|
||||
notifyError(error: MonitorError): void {
|
||||
this.onErrorEmitter.fire(error);
|
||||
}
|
||||
|
||||
}
|
43
arduino-ide-extension/src/common/protocol/monitor-service.ts
Normal file
43
arduino-ide-extension/src/common/protocol/monitor-service.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { JsonRpcServer } from '@theia/core';
|
||||
import { Board } from './boards-service';
|
||||
|
||||
export interface MonitorError {
|
||||
readonly message: string;
|
||||
readonly code: number
|
||||
}
|
||||
|
||||
export interface MonitorReadEvent {
|
||||
readonly connectionId: string;
|
||||
readonly data: string;
|
||||
}
|
||||
|
||||
export const MonitorServiceClient = Symbol('MonitorServiceClient');
|
||||
export interface MonitorServiceClient {
|
||||
notifyRead(event: MonitorReadEvent): void;
|
||||
notifyError(event: MonitorError): void;
|
||||
}
|
||||
|
||||
export const MonitorServicePath = '/services/serial-monitor';
|
||||
export const MonitorService = Symbol('MonitorService');
|
||||
export interface MonitorService extends JsonRpcServer<MonitorServiceClient> {
|
||||
connect(config: ConnectionConfig): Promise<{ connectionId: string }>;
|
||||
disconnect(connectionId: string): Promise<boolean>;
|
||||
send(connectionId: string, data: string | Uint8Array): Promise<void>;
|
||||
}
|
||||
|
||||
export interface ConnectionConfig {
|
||||
readonly board: Board;
|
||||
readonly port: string;
|
||||
/**
|
||||
* Defaults to [`SERIAL`](ConnectionType#SERIAL).
|
||||
*/
|
||||
readonly type?: ConnectionType;
|
||||
/**
|
||||
* Defaults to `9600`.
|
||||
*/
|
||||
readonly baudRate?: number;
|
||||
}
|
||||
|
||||
export enum ConnectionType {
|
||||
SERIAL = 0
|
||||
}
|
@ -19,6 +19,9 @@ import { DefaultWorkspaceServerExt } from './default-workspace-server-ext';
|
||||
import { WorkspaceServer } from '@theia/workspace/lib/common';
|
||||
import { SketchesServiceImpl } from './sketches-service-impl';
|
||||
import { SketchesService, SketchesServicePath } from '../common/protocol/sketches-service';
|
||||
import { MonitorServiceImpl } from './monitor/monitor-service-impl';
|
||||
import { MonitorService, MonitorServicePath, MonitorServiceClient } from '../common/protocol/monitor-service';
|
||||
import { MonitorClientProvider } from './monitor/monitor-client-provider';
|
||||
|
||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(ArduinoDaemon).toSelf().inSingletonScope();
|
||||
@ -104,4 +107,25 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
// If nothing was set previously.
|
||||
bind(DefaultWorkspaceServerExt).toSelf().inSingletonScope();
|
||||
rebind(WorkspaceServer).toService(DefaultWorkspaceServerExt);
|
||||
|
||||
// Shared monitor client provider service for the backend.
|
||||
bind(MonitorClientProvider).toSelf().inSingletonScope();
|
||||
|
||||
// Connection scoped service for the serial monitor.
|
||||
const monitorServiceConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => {
|
||||
bind(MonitorServiceImpl).toSelf().inSingletonScope();
|
||||
bind(MonitorService).toService(MonitorServiceImpl);
|
||||
bindBackendService<MonitorService, MonitorServiceClient>(MonitorServicePath, MonitorService, (service, client) => {
|
||||
service.setClient(client);
|
||||
client.onDidCloseConnection(() => service.dispose());
|
||||
return service;
|
||||
});
|
||||
});
|
||||
bind(ConnectionContainerModule).toConstantValue(monitorServiceConnectionModule);
|
||||
|
||||
// Logger for the monitor service.
|
||||
bind(ILogger).toDynamicValue(ctx => {
|
||||
const parentLogger = ctx.container.get<ILogger>(ILogger);
|
||||
return parentLogger.child('monitor-service');
|
||||
}).inSingletonScope().whenTargetNamed('monitor-service');
|
||||
});
|
||||
|
@ -0,0 +1,20 @@
|
||||
import * as grpc from '@grpc/grpc-js';
|
||||
import { injectable, postConstruct } from 'inversify';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { MonitorClient } from '../cli-protocol/monitor/monitor_grpc_pb';
|
||||
|
||||
@injectable()
|
||||
export class MonitorClientProvider {
|
||||
|
||||
readonly deferred = new Deferred<MonitorClient>();
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.deferred.resolve(new MonitorClient('localhost:50051', grpc.credentials.createInsecure()));
|
||||
}
|
||||
|
||||
get client(): Promise<MonitorClient> {
|
||||
return this.deferred.promise;
|
||||
}
|
||||
|
||||
}
|
164
arduino-ide-extension/src/node/monitor/monitor-service-impl.ts
Normal file
164
arduino-ide-extension/src/node/monitor/monitor-service-impl.ts
Normal file
@ -0,0 +1,164 @@
|
||||
import { v4 } from 'uuid';
|
||||
import * as grpc from '@grpc/grpc-js';
|
||||
import { TextDecoder, TextEncoder } from 'util';
|
||||
import { injectable, inject, named } from 'inversify';
|
||||
import { ILogger, Disposable, DisposableCollection } from '@theia/core';
|
||||
import { MonitorService, MonitorServiceClient, ConnectionConfig, ConnectionType } from '../../common/protocol/monitor-service';
|
||||
import { StreamingOpenReq, StreamingOpenResp, MonitorConfig } from '../cli-protocol/monitor/monitor_pb';
|
||||
import { MonitorClientProvider } from './monitor-client-provider';
|
||||
|
||||
export interface MonitorDuplex {
|
||||
readonly toDispose: Disposable;
|
||||
readonly duplex: grpc.ClientDuplexStream<StreamingOpenReq, StreamingOpenResp>;
|
||||
}
|
||||
|
||||
type ErrorCode = { code: number };
|
||||
type MonitorError = Error & ErrorCode;
|
||||
namespace MonitorError {
|
||||
|
||||
export function is(error: Error & Partial<ErrorCode>): error is MonitorError {
|
||||
return typeof error.code === 'number';
|
||||
}
|
||||
|
||||
/**
|
||||
* The frontend has refreshed the browser, for instance.
|
||||
*/
|
||||
export function isClientCancelledError(error: MonitorError): boolean {
|
||||
return error.code === 1 && error.message === 'Cancelled on client';
|
||||
}
|
||||
|
||||
/**
|
||||
* When detaching a physical device when the duplex channel is still opened.
|
||||
*/
|
||||
export function isDeviceNotConfiguredError(error: MonitorError): boolean {
|
||||
return error.code === 2 && error.message === 'device not configured';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class MonitorServiceImpl implements MonitorService {
|
||||
|
||||
@inject(ILogger)
|
||||
@named('monitor-service')
|
||||
protected readonly logger: ILogger;
|
||||
|
||||
@inject(MonitorClientProvider)
|
||||
protected readonly monitorClientProvider: MonitorClientProvider;
|
||||
|
||||
protected client?: MonitorServiceClient;
|
||||
protected readonly connections = new Map<string, MonitorDuplex>();
|
||||
|
||||
setClient(client: MonitorServiceClient | undefined): void {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
for (const [connectionId, duplex] of this.connections.entries()) {
|
||||
this.doDisconnect(connectionId, duplex);
|
||||
}
|
||||
}
|
||||
|
||||
async connect(config: ConnectionConfig): Promise<{ connectionId: string }> {
|
||||
const client = await this.monitorClientProvider.client;
|
||||
const duplex = client.streamingOpen();
|
||||
const connectionId = v4();
|
||||
const toDispose = new DisposableCollection(
|
||||
Disposable.create(() => this.disconnect(connectionId))
|
||||
);
|
||||
|
||||
duplex.on('error', ((error: Error) => {
|
||||
if (MonitorError.is(error) && (
|
||||
MonitorError.isClientCancelledError(error)
|
||||
|| MonitorError.isDeviceNotConfiguredError(error)
|
||||
)) {
|
||||
if (this.client) {
|
||||
this.client.notifyError(error);
|
||||
}
|
||||
}
|
||||
this.logger.error(`Error occurred for connection ${connectionId}.`, error);
|
||||
toDispose.dispose();
|
||||
}).bind(this));
|
||||
|
||||
duplex.on('data', ((resp: StreamingOpenResp) => {
|
||||
if (this.client) {
|
||||
const raw = resp.getData();
|
||||
const data = typeof raw === 'string' ? raw : new TextDecoder('utf8').decode(raw);
|
||||
this.client.notifyRead({ connectionId, data });
|
||||
}
|
||||
}).bind(this));
|
||||
|
||||
const { type, port } = config;
|
||||
const req = new StreamingOpenReq();
|
||||
const monitorConfig = new MonitorConfig();
|
||||
monitorConfig.setType(this.mapType(type));
|
||||
monitorConfig.setTarget(port);
|
||||
if (config.baudRate !== undefined) {
|
||||
monitorConfig.setAdditionalconfig({ 'BaudRate': config.baudRate });
|
||||
}
|
||||
req.setMonitorconfig(monitorConfig);
|
||||
|
||||
return new Promise<{ connectionId: string }>(resolve => {
|
||||
duplex.write(req, () => {
|
||||
this.connections.set(connectionId, { toDispose, duplex });
|
||||
resolve({ connectionId });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async disconnect(connectionId: string): Promise<boolean> {
|
||||
this.logger.info(`>>> Received disconnect request for connection: ${connectionId}`);
|
||||
const disposable = this.connections.get(connectionId);
|
||||
if (!disposable) {
|
||||
this.logger.warn(`<<< No connection was found for ID: ${connectionId}`);
|
||||
return false;
|
||||
}
|
||||
const result = await this.doDisconnect(connectionId, disposable);
|
||||
if (result) {
|
||||
this.logger.info(`<<< Successfully disconnected from ${connectionId}.`);
|
||||
} else {
|
||||
this.logger.info(`<<< Could not disconnected from ${connectionId}.`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
protected async doDisconnect(connectionId: string, duplex: MonitorDuplex): Promise<boolean> {
|
||||
const { toDispose } = duplex;
|
||||
this.logger.info(`>>> Disposing monitor connection: ${connectionId}...`);
|
||||
try {
|
||||
toDispose.dispose();
|
||||
this.logger.info(`<<< Connection disposed: ${connectionId}.`);
|
||||
return true;
|
||||
} catch (e) {
|
||||
this.logger.error(`<<< Error occurred when disposing monitor connection: ${connectionId}. ${e}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async send(connectionId: string, data: string): Promise<void> {
|
||||
const duplex = this.duplex(connectionId);
|
||||
if (duplex) {
|
||||
const req = new StreamingOpenReq();
|
||||
req.setData(new TextEncoder().encode(data));
|
||||
return new Promise<void>(resolve => duplex.duplex.write(req, resolve));
|
||||
} else {
|
||||
throw new Error(`No connection with ID: ${connectionId}.`);
|
||||
}
|
||||
}
|
||||
|
||||
protected mapType(type?: ConnectionType): MonitorConfig.TargetType {
|
||||
switch (type) {
|
||||
case ConnectionType.SERIAL: return MonitorConfig.TargetType.SERIAL;
|
||||
default: return MonitorConfig.TargetType.SERIAL;
|
||||
}
|
||||
}
|
||||
|
||||
protected duplex(connectionId: string): MonitorDuplex | undefined {
|
||||
const monitorClient = this.connections.get(connectionId);
|
||||
if (!monitorClient) {
|
||||
this.logger.warn(`Could not find monitor client for connection ID: ${connectionId}`);
|
||||
}
|
||||
return monitorClient;
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user