Skip to content

Commit 692c3f6

Browse files
Akos Kittajbicker
Akos Kitta
authored andcommitted
Implemented serial-monitoring for the backend.
Signed-off-by: Akos Kitta <[email protected]>
1 parent 8d79bb3 commit 692c3f6

File tree

8 files changed

+383
-3
lines changed

8 files changed

+383
-3
lines changed

arduino-ide-extension/src/browser/arduino-commands.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,14 @@ export namespace ArduinoCommands {
4343
id: "arduino-toggle-pro-mode"
4444
}
4545

46+
export const CONNECT_TODO: Command = {
47+
id: 'connect-to-attached-board',
48+
label: 'Connect to Attached Board'
49+
}
50+
51+
export const SEND: Command = {
52+
id: 'send',
53+
label: 'Send a Message to the Connected Board'
54+
}
55+
4656
}

arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
55
import { MessageService } from '@theia/core/lib/common/message-service';
66
import { CommandContribution, CommandRegistry, Command } from '@theia/core/lib/common/command';
77
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
8-
import { BoardsService } from '../common/protocol/boards-service';
8+
import { BoardsService, AttachedSerialBoard } from '../common/protocol/boards-service';
99
import { ArduinoCommands } from './arduino-commands';
1010
import { CoreService } from '../common/protocol/core-service';
1111
import { WorkspaceServiceExt } from './workspace-service-ext';
@@ -19,7 +19,18 @@ import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service
1919
import { SketchFactory } from './sketch-factory';
2020
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
2121
import { EditorManager, EditorMainMenu } from '@theia/editor/lib/browser';
22-
import { ContextMenuRenderer, OpenerService, Widget, StatusBar, ShellLayoutRestorer, StatusBarAlignment, LabelProvider } from '@theia/core/lib/browser';
22+
import {
23+
ContextMenuRenderer,
24+
OpenerService,
25+
Widget,
26+
StatusBar,
27+
ShellLayoutRestorer,
28+
StatusBarAlignment,
29+
QuickOpenItem,
30+
QuickOpenMode,
31+
QuickOpenService,
32+
LabelProvider
33+
} from '@theia/core/lib/browser';
2334
import { OpenFileDialogProps, FileDialogService } from '@theia/filesystem/lib/browser/file-dialog';
2435
import { FileSystem, FileStat } from '@theia/filesystem/lib/common';
2536
import { ArduinoToolbarContextMenu } from './arduino-file-menu';
@@ -34,6 +45,7 @@ import { MaybePromise } from '@theia/core/lib/common/types';
3445
import { BoardsConfigDialog } from './boards/boards-config-dialog';
3546
import { BoardsToolBarItem } from './boards/boards-toolbar-item';
3647
import { BoardsConfig } from './boards/boards-config';
48+
import { MonitorService } from '../common/protocol/monitor-service';
3749

3850
export namespace ArduinoMenus {
3951
export const SKETCH = [...MAIN_MENU_BAR, '3_sketch'];
@@ -56,6 +68,12 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
5668
@inject(CoreService)
5769
protected readonly coreService: CoreService;
5870

71+
@inject(MonitorService)
72+
protected readonly monitorService: MonitorService;
73+
74+
// TODO: make this better!
75+
protected connectionId: string | undefined;
76+
5977
@inject(WorkspaceServiceExt)
6078
protected readonly workspaceServiceExt: WorkspaceServiceExt;
6179

@@ -115,6 +133,9 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
115133

116134
@inject(LabelProvider)
117135
protected readonly labelProvider: LabelProvider;
136+
137+
@inject(QuickOpenService)
138+
protected readonly quickOpenService: QuickOpenService;
118139

119140
protected boardsToolbarItem: BoardsToolBarItem | null;
120141
protected wsSketchCount: number = 0;
@@ -293,14 +314,73 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
293314
this.boardsServiceClient.boardsConfig = boardsConfig;
294315
}
295316
}
296-
})
317+
});
297318
registry.registerCommand(ArduinoCommands.TOGGLE_PRO_MODE, {
298319
execute: () => {
299320
const oldModeState = ARDUINO_PRO_MODE;
300321
window.localStorage.setItem('arduino-pro-mode', oldModeState ? 'false' : 'true');
301322
registry.executeCommand('reset.layout');
302323
},
303324
isToggled: () => ARDUINO_PRO_MODE
325+
});
326+
registry.registerCommand(ArduinoCommands.CONNECT_TODO, {
327+
execute: async () => {
328+
const { boardsConfig } = this.boardsServiceClient;
329+
const { selectedBoard, selectedPort } = boardsConfig;
330+
if (!selectedBoard) {
331+
this.messageService.warn('No boards selected.');
332+
return;
333+
}
334+
const { name } = selectedBoard;
335+
if (!selectedPort) {
336+
this.messageService.warn(`No ports selected for board: '${name}'.`);
337+
return;
338+
}
339+
const attachedBoards = await this.boardsService.getAttachedBoards();
340+
const connectedBoard = attachedBoards.boards.filter(AttachedSerialBoard.is).find(board => BoardsConfig.Config.sameAs(boardsConfig, board));
341+
if (!connectedBoard) {
342+
this.messageService.warn(`The selected '${name}' board is not connected on ${selectedPort}.`);
343+
return;
344+
}
345+
if (this.connectionId) {
346+
console.log('>>> Disposing existing monitor connection before establishing a new one...');
347+
const result = await this.monitorService.disconnect(this.connectionId);
348+
if (!result) {
349+
// TODO: better!!!
350+
console.error(`Could not close connection: ${this.connectionId}. Check the backend logs.`);
351+
} else {
352+
console.log(`<<< Disposed ${this.connectionId} connection.`)
353+
}
354+
}
355+
const { connectionId } = await this.monitorService.connect({ board: selectedBoard, port: selectedPort });
356+
this.connectionId = connectionId;
357+
}
358+
});
359+
registry.registerCommand(ArduinoCommands.SEND, {
360+
isEnabled: () => !!this.connectionId,
361+
execute: async () => {
362+
const { monitorService, connectionId } = this;
363+
const model = {
364+
onType(lookFor: string, acceptor: (items: QuickOpenItem[]) => void): void {
365+
acceptor([
366+
new QuickOpenItem({
367+
label: "Type your message and press 'Enter' to send it to the board. Escape to cancel.",
368+
run: (mode: QuickOpenMode): boolean => {
369+
if (mode !== QuickOpenMode.OPEN) {
370+
return false;
371+
}
372+
monitorService.send(connectionId!, lookFor + '\n');
373+
return true;
374+
}
375+
})
376+
]);
377+
}
378+
};
379+
const options = {
380+
placeholder: "Your message. The message will be suffixed with a LF ['\\n'].",
381+
};
382+
this.quickOpenService.open(model, options);
383+
}
304384
})
305385
}
306386

arduino-ide-extension/src/browser/arduino-frontend-module.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ import { SilentSearchInWorkspaceContribution } from './customization/silent-sear
5454
import { LibraryListWidgetFrontendContribution } from './library/library-widget-frontend-contribution';
5555
import { LibraryItemRenderer } from './library/library-item-renderer';
5656
import { BoardItemRenderer } from './boards/boards-item-renderer';
57+
import { MonitorServiceClientImpl } from './monitor/monitor-service-client-impl';
58+
import { MonitorServicePath, MonitorService, MonitorServiceClient } from '../common/protocol/monitor-service';
5759
const ElementQueries = require('css-element-queries/src/ElementQueries');
5860

5961
if (!ARDUINO_PRO_MODE) {
@@ -149,6 +151,20 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
149151
return workspaceServiceExt;
150152
});
151153

154+
// Frontend binding for the monitor service.
155+
bind(MonitorService).toDynamicValue(context => {
156+
const connection = context.container.get(WebSocketConnectionProvider);
157+
const client = context.container.get(MonitorServiceClientImpl);
158+
return connection.createProxy(MonitorServicePath, client);
159+
}).inSingletonScope();
160+
// Monitor service client to receive and delegate notifications from the backend.
161+
bind(MonitorServiceClientImpl).toSelf().inSingletonScope();
162+
bind(MonitorServiceClient).toDynamicValue(context => {
163+
const client = context.container.get(MonitorServiceClientImpl);
164+
WebSocketConnectionProvider.createProxy(context.container, MonitorServicePath, client);
165+
return client;
166+
}).inSingletonScope();
167+
152168
bind(AWorkspaceService).toSelf().inSingletonScope();
153169
rebind(WorkspaceService).to(AWorkspaceService).inSingletonScope();
154170
bind(SketchFactory).toSelf().inSingletonScope();
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { injectable } from 'inversify';
2+
import { Emitter } from '@theia/core/lib/common/event';
3+
import { MonitorServiceClient, MonitorReadEvent, MonitorError } from '../../common/protocol/monitor-service';
4+
5+
@injectable()
6+
export class MonitorServiceClientImpl implements MonitorServiceClient {
7+
8+
protected readonly onReadEmitter = new Emitter<MonitorReadEvent>();
9+
protected readonly onErrorEmitter = new Emitter<MonitorError>();
10+
readonly onRead = this.onReadEmitter.event;
11+
readonly onError = this.onErrorEmitter.event;
12+
13+
notifyRead(event: MonitorReadEvent): void {
14+
this.onReadEmitter.fire(event);
15+
const { connectionId, data } = event;
16+
console.log(`Received data from ${connectionId}: ${data}`);
17+
}
18+
19+
notifyError(error: MonitorError): void {
20+
this.onErrorEmitter.fire(error);
21+
}
22+
23+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { JsonRpcServer } from '@theia/core';
2+
import { Board } from './boards-service';
3+
4+
export interface MonitorError {
5+
readonly message: string;
6+
readonly code: number
7+
}
8+
9+
export interface MonitorReadEvent {
10+
readonly connectionId: string;
11+
readonly data: string;
12+
}
13+
14+
export const MonitorServiceClient = Symbol('MonitorServiceClient');
15+
export interface MonitorServiceClient {
16+
notifyRead(event: MonitorReadEvent): void;
17+
notifyError(event: MonitorError): void;
18+
}
19+
20+
export const MonitorServicePath = '/services/serial-monitor';
21+
export const MonitorService = Symbol('MonitorService');
22+
export interface MonitorService extends JsonRpcServer<MonitorServiceClient> {
23+
connect(config: ConnectionConfig): Promise<{ connectionId: string }>;
24+
disconnect(connectionId: string): Promise<boolean>;
25+
send(connectionId: string, data: string | Uint8Array): Promise<void>;
26+
}
27+
28+
export interface ConnectionConfig {
29+
readonly board: Board;
30+
readonly port: string;
31+
/**
32+
* Defaults to [`SERIAL`](ConnectionType#SERIAL).
33+
*/
34+
readonly type?: ConnectionType;
35+
/**
36+
* Defaults to `9600`.
37+
*/
38+
readonly baudRate?: number;
39+
}
40+
41+
export enum ConnectionType {
42+
SERIAL = 0
43+
}

arduino-ide-extension/src/node/arduino-backend-module.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import { DefaultWorkspaceServerExt } from './default-workspace-server-ext';
1919
import { WorkspaceServer } from '@theia/workspace/lib/common';
2020
import { SketchesServiceImpl } from './sketches-service-impl';
2121
import { SketchesService, SketchesServicePath } from '../common/protocol/sketches-service';
22+
import { MonitorServiceImpl } from './monitor/monitor-service-impl';
23+
import { MonitorService, MonitorServicePath, MonitorServiceClient } from '../common/protocol/monitor-service';
24+
import { MonitorClientProvider } from './monitor/monitor-client-provider';
2225

2326
export default new ContainerModule((bind, unbind, isBound, rebind) => {
2427
bind(ArduinoDaemon).toSelf().inSingletonScope();
@@ -104,4 +107,25 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
104107
// If nothing was set previously.
105108
bind(DefaultWorkspaceServerExt).toSelf().inSingletonScope();
106109
rebind(WorkspaceServer).toService(DefaultWorkspaceServerExt);
110+
111+
// Shared monitor client provider service for the backend.
112+
bind(MonitorClientProvider).toSelf().inSingletonScope();
113+
114+
// Connection scoped service for the serial monitor.
115+
const monitorServiceConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => {
116+
bind(MonitorServiceImpl).toSelf().inSingletonScope();
117+
bind(MonitorService).toService(MonitorServiceImpl);
118+
bindBackendService<MonitorService, MonitorServiceClient>(MonitorServicePath, MonitorService, (service, client) => {
119+
service.setClient(client);
120+
client.onDidCloseConnection(() => service.dispose());
121+
return service;
122+
});
123+
});
124+
bind(ConnectionContainerModule).toConstantValue(monitorServiceConnectionModule);
125+
126+
// Logger for the monitor service.
127+
bind(ILogger).toDynamicValue(ctx => {
128+
const parentLogger = ctx.container.get<ILogger>(ILogger);
129+
return parentLogger.child('monitor-service');
130+
}).inSingletonScope().whenTargetNamed('monitor-service');
107131
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import * as grpc from '@grpc/grpc-js';
2+
import { injectable, postConstruct } from 'inversify';
3+
import { Deferred } from '@theia/core/lib/common/promise-util';
4+
import { MonitorClient } from '../cli-protocol/monitor/monitor_grpc_pb';
5+
6+
@injectable()
7+
export class MonitorClientProvider {
8+
9+
readonly deferred = new Deferred<MonitorClient>();
10+
11+
@postConstruct()
12+
protected init(): void {
13+
this.deferred.resolve(new MonitorClient('localhost:50051', grpc.credentials.createInsecure()));
14+
}
15+
16+
get client(): Promise<MonitorClient> {
17+
return this.deferred.promise;
18+
}
19+
20+
}

0 commit comments

Comments
 (0)