Skip to content

fix: propagate monitor errors to the frontend #1965

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 13, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -496,15 +496,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(TabBarToolbarContribution).toService(MonitorViewContribution);
bind(WidgetFactory).toDynamicValue((context) => ({
id: MonitorWidget.ID,
createWidget: () => {
return new MonitorWidget(
context.container.get<MonitorModel>(MonitorModel),
context.container.get<MonitorManagerProxyClient>(
MonitorManagerProxyClient
),
context.container.get<BoardsServiceProvider>(BoardsServiceProvider)
);
},
createWidget: () => context.container.get(MonitorWidget),
}));

bind(MonitorManagerProxyFactory).toFactory(
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import {
CommandRegistry,
ApplicationError,
Disposable,
Emitter,
MessageService,
nls,
} from '@theia/core';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { inject, injectable } from '@theia/core/shared/inversify';
import { NotificationManager } from '@theia/messages/lib/browser/notifications-manager';
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
import { Board, Port } from '../common/protocol';
import {
Monitor,
@@ -23,21 +26,31 @@ import { BoardsServiceProvider } from './boards/boards-service-provider';
export class MonitorManagerProxyClientImpl
implements MonitorManagerProxyClient
{
@inject(MessageService)
private readonly messageService: MessageService;
// This is necessary to call the backend methods from the frontend
@inject(MonitorManagerProxyFactory)
private readonly server: MonitorManagerProxyFactory;
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(NotificationManager)
private readonly notificationManager: NotificationManager;

// 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<{
private readonly onMessagesReceivedEmitter = new Emitter<{
messages: string[];
}>();
readonly onMessagesReceived = this.onMessagesReceivedEmitter.event;

protected readonly onMonitorSettingsDidChangeEmitter =
private readonly onMonitorSettingsDidChangeEmitter =
new Emitter<MonitorSettings>();
readonly onMonitorSettingsDidChange =
this.onMonitorSettingsDidChangeEmitter.event;

protected readonly onMonitorShouldResetEmitter = new Emitter();
private readonly onMonitorShouldResetEmitter = new Emitter<void>();
readonly onMonitorShouldReset = this.onMonitorShouldResetEmitter.event;

// WebSocket used to handle pluggable monitor communication between
@@ -51,29 +64,16 @@ export class MonitorManagerProxyClientImpl
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,

@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry,

@inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider
) {}

/**
* Connects a localhost WebSocket using the specified port.
* @param addressPort port of the WebSocket
*/
async connect(addressPort: number): Promise<void> {
if (!!this.webSocket) {
if (this.wsPort === addressPort) return;
else this.disconnect();
if (this.webSocket) {
if (this.wsPort === addressPort) {
return;
}
this.disconnect();
}
try {
this.webSocket = new WebSocket(`ws://localhost:${addressPort}`);
@@ -87,6 +87,9 @@ export class MonitorManagerProxyClientImpl
return;
}

const opened = new Deferred<void>();
this.webSocket.onopen = () => opened.resolve();
this.webSocket.onerror = () => opened.reject();
this.webSocket.onmessage = (message) => {
const parsedMessage = JSON.parse(message.data);
if (Array.isArray(parsedMessage))
@@ -99,19 +102,26 @@ export class MonitorManagerProxyClientImpl
}
};
this.wsPort = addressPort;
return opened.promise;
}

/**
* Disconnects the WebSocket if connected.
*/
disconnect(): void {
if (!this.webSocket) return;
if (!this.webSocket) {
return;
}
this.onBoardsConfigChanged?.dispose();
this.onBoardsConfigChanged = undefined;
try {
this.webSocket?.close();
this.webSocket.close();
this.webSocket = undefined;
} catch {
} catch (err) {
console.error(
'Could not close the websocket connection for the monitor.',
err
);
this.messageService.error(
nls.localize(
'arduino/monitor/unableToCloseWebSocket',
@@ -126,6 +136,7 @@ export class MonitorManagerProxyClientImpl
}

async startMonitor(settings?: PluggableMonitorSettings): Promise<void> {
await this.boardsServiceProvider.reconciled;
this.lastConnectedBoard = {
selectedBoard: this.boardsServiceProvider.boardsConfig.selectedBoard,
selectedPort: this.boardsServiceProvider.boardsConfig.selectedPort,
@@ -150,11 +161,11 @@ export class MonitorManagerProxyClientImpl
? Port.keyOf(this.lastConnectedBoard.selectedPort)
: undefined)
) {
this.onMonitorShouldResetEmitter.fire(null);
this.lastConnectedBoard = {
selectedBoard: selectedBoard,
selectedPort: selectedPort,
};
this.onMonitorShouldResetEmitter.fire();
} else {
// a board is plugged and it's the same as prev, rerun "this.startMonitor" to
// recreate the listener callback
@@ -167,7 +178,14 @@ export class MonitorManagerProxyClientImpl
const { selectedBoard, selectedPort } =
this.boardsServiceProvider.boardsConfig;
if (!selectedBoard || !selectedBoard.fqbn || !selectedPort) return;
await this.server().startMonitor(selectedBoard, selectedPort, settings);
try {
this.clearVisibleNotification();
await this.server().startMonitor(selectedBoard, selectedPort, settings);
} catch (err) {
const message = ApplicationError.is(err) ? err.message : String(err);
this.previousNotificationId = this.notificationId(message);
this.messageService.error(message);
}
}

getCurrentSettings(board: Board, port: Port): Promise<MonitorSettings> {
@@ -199,4 +217,24 @@ export class MonitorManagerProxyClientImpl
})
);
}

/**
* This is the internal (Theia) ID of the notification that is currently visible.
* It's stored here as a field to be able to close it before starting a new monitor connection. It's a hack.
*/
private previousNotificationId: string | undefined;
private clearVisibleNotification(): void {
if (this.previousNotificationId) {
this.notificationManager.clear(this.previousNotificationId);
this.previousNotificationId = undefined;
}
}

private notificationId(message: string, ...actions: string[]): string {
return this.notificationManager['getMessageId']({
text: message,
actions,
type: MessageType.Error,
});
}
}
84 changes: 37 additions & 47 deletions arduino-ide-extension/src/browser/monitor-model.ts
Original file line number Diff line number Diff line change
@@ -4,7 +4,14 @@ import {
LocalStorageService,
} from '@theia/core/lib/browser';
import { inject, injectable } from '@theia/core/shared/inversify';
import { MonitorManagerProxyClient } from '../common/protocol';
import {
isMonitorConnected,
MonitorConnectionStatus,
monitorConnectionStatusEquals,
MonitorEOL,
MonitorManagerProxyClient,
MonitorState,
} from '../common/protocol';
import { isNullOrUndefined } from '../common/utils';
import { MonitorSettings } from '../node/monitor-settings/monitor-settings-provider';

@@ -19,48 +26,48 @@ export class MonitorModel implements FrontendApplicationContribution {
protected readonly monitorManagerProxy: MonitorManagerProxyClient;

protected readonly onChangeEmitter: Emitter<
MonitorModel.State.Change<keyof MonitorModel.State>
MonitorState.Change<keyof MonitorState>
>;

protected _autoscroll: boolean;
protected _timestamp: boolean;
protected _lineEnding: MonitorModel.EOL;
protected _lineEnding: MonitorEOL;
protected _interpolate: boolean;
protected _darkTheme: boolean;
protected _wsPort: number;
protected _serialPort: string;
protected _connected: boolean;
protected _connectionStatus: MonitorConnectionStatus;

constructor() {
this._autoscroll = true;
this._timestamp = false;
this._interpolate = false;
this._lineEnding = MonitorModel.EOL.DEFAULT;
this._lineEnding = MonitorEOL.DEFAULT;
this._darkTheme = false;
this._wsPort = 0;
this._serialPort = '';
this._connected = true;
this._connectionStatus = 'not-connected';

this.onChangeEmitter = new Emitter<
MonitorModel.State.Change<keyof MonitorModel.State>
MonitorState.Change<keyof MonitorState>
>();
}

onStart(): void {
this.localStorageService
.getData<MonitorModel.State>(MonitorModel.STORAGE_ID)
.getData<MonitorState>(MonitorModel.STORAGE_ID)
.then(this.restoreState.bind(this));

this.monitorManagerProxy.onMonitorSettingsDidChange(
this.onMonitorSettingsDidChange.bind(this)
);
}

get onChange(): Event<MonitorModel.State.Change<keyof MonitorModel.State>> {
get onChange(): Event<MonitorState.Change<keyof MonitorState>> {
return this.onChangeEmitter.event;
}

protected restoreState(state: MonitorModel.State): void {
protected restoreState(state: MonitorState): void {
if (!state) {
return;
}
@@ -125,11 +132,11 @@ export class MonitorModel implements FrontendApplicationContribution {
this.timestamp = !this._timestamp;
}

get lineEnding(): MonitorModel.EOL {
get lineEnding(): MonitorEOL {
return this._lineEnding;
}

set lineEnding(lineEnding: MonitorModel.EOL) {
set lineEnding(lineEnding: MonitorEOL) {
if (lineEnding === this._lineEnding) return;
this._lineEnding = lineEnding;
this.monitorManagerProxy.changeSettings({
@@ -211,19 +218,26 @@ export class MonitorModel implements FrontendApplicationContribution {
);
}

get connected(): boolean {
return this._connected;
get connectionStatus(): MonitorConnectionStatus {
return this._connectionStatus;
}

set connected(connected: boolean) {
if (connected === this._connected) return;
this._connected = connected;
set connectionStatus(connectionStatus: MonitorConnectionStatus) {
if (
monitorConnectionStatusEquals(connectionStatus, this.connectionStatus)
) {
return;
}
this._connectionStatus = connectionStatus;
this.monitorManagerProxy.changeSettings({
monitorUISettings: { connected },
monitorUISettings: {
connectionStatus,
connected: isMonitorConnected(connectionStatus),
},
});
this.onChangeEmitter.fire({
property: 'connected',
value: this._connected,
property: 'connectionStatus',
value: this._connectionStatus,
});
}

@@ -238,7 +252,7 @@ export class MonitorModel implements FrontendApplicationContribution {
darkTheme,
wsPort,
serialPort,
connected,
connectionStatus,
} = monitorUISettings;

if (!isNullOrUndefined(autoscroll)) this.autoscroll = autoscroll;
@@ -248,31 +262,7 @@ export class MonitorModel implements FrontendApplicationContribution {
if (!isNullOrUndefined(darkTheme)) this.darkTheme = darkTheme;
if (!isNullOrUndefined(wsPort)) this.wsPort = wsPort;
if (!isNullOrUndefined(serialPort)) this.serialPort = serialPort;
if (!isNullOrUndefined(connected)) this.connected = connected;
if (!isNullOrUndefined(connectionStatus))
this.connectionStatus = connectionStatus;
};
}

// TODO: Move this to /common
export namespace MonitorModel {
export interface State {
autoscroll: boolean;
timestamp: boolean;
lineEnding: EOL;
interpolate: boolean;
darkTheme: boolean;
wsPort: number;
serialPort: string;
connected: boolean;
}
export namespace State {
export interface Change<K extends keyof State> {
readonly property: K;
readonly value: State[K];
}
}

export type EOL = '' | '\n' | '\r' | '\r\n';
export namespace EOL {
export const DEFAULT: EOL = '\n';
}
}
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@ import {
import { ArduinoToolbar } from '../../toolbar/arduino-toolbar';
import { ArduinoMenus } from '../../menu/arduino-menus';
import { nls } from '@theia/core/lib/common';
import { Event } from '@theia/core/lib/common/event';
import { MonitorModel } from '../../monitor-model';
import { MonitorManagerProxyClient } from '../../../common/protocol';

@@ -84,13 +85,13 @@ export class MonitorViewContribution
id: 'monitor-autoscroll',
render: () => this.renderAutoScrollButton(),
isVisible: (widget) => widget instanceof MonitorWidget,
onDidChange: this.model.onChange as any, // XXX: it's a hack. See: https://github.com/eclipse-theia/theia/pull/6696/
onDidChange: this.model.onChange as Event<unknown> as Event<void>,
});
registry.registerItem({
id: 'monitor-timestamp',
render: () => this.renderTimestampButton(),
isVisible: (widget) => widget instanceof MonitorWidget,
onDidChange: this.model.onChange as any, // XXX: it's a hack. See: https://github.com/eclipse-theia/theia/pull/6696/
onDidChange: this.model.onChange as Event<unknown> as Event<void>,
});
registry.registerItem({
id: SerialMonitor.Commands.CLEAR_OUTPUT.id,
@@ -143,8 +144,7 @@ export class MonitorViewContribution
protected async reset(): Promise<void> {
const widget = this.tryGetWidget();
if (widget) {
widget.dispose();
await this.openView({ activate: true, reveal: true });
widget.reset();
}
}

94 changes: 62 additions & 32 deletions arduino-ide-extension/src/browser/serial/monitor/monitor-widget.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import * as React from '@theia/core/shared/react';
import { injectable, inject } from '@theia/core/shared/inversify';
import {
injectable,
inject,
postConstruct,
} from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { Disposable } from '@theia/core/lib/common/disposable';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import {
ReactWidget,
Message,
@@ -13,9 +20,13 @@ import { SerialMonitorSendInput } from './serial-monitor-send-input';
import { SerialMonitorOutput } from './serial-monitor-send-output';
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
import { nls } from '@theia/core/lib/common';
import { MonitorManagerProxyClient } from '../../../common/protocol';
import {
MonitorEOL,
MonitorManagerProxyClient,
} from '../../../common/protocol';
import { MonitorModel } from '../../monitor-model';
import { MonitorSettings } from '../../../node/monitor-settings/monitor-settings-provider';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';

@injectable()
export class MonitorWidget extends ReactWidget {
@@ -40,40 +51,46 @@ export class MonitorWidget extends ReactWidget {
protected closing = false;
protected readonly clearOutputEmitter = new Emitter<void>();

constructor(
@inject(MonitorModel)
protected readonly monitorModel: MonitorModel,
@inject(MonitorModel)
private readonly monitorModel: MonitorModel;
@inject(MonitorManagerProxyClient)
private readonly monitorManagerProxy: MonitorManagerProxyClient;
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;

@inject(MonitorManagerProxyClient)
protected readonly monitorManagerProxy: MonitorManagerProxyClient,
private readonly toDisposeOnReset: DisposableCollection;

@inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider
) {
constructor() {
super();
this.id = MonitorWidget.ID;
this.title.label = MonitorWidget.LABEL;
this.title.iconClass = 'monitor-tab-icon';
this.title.closable = true;
this.scrollOptions = undefined;
this.toDisposeOnReset = new DisposableCollection();
this.toDispose.push(this.clearOutputEmitter);
this.toDispose.push(
Disposable.create(() => this.monitorManagerProxy.disconnect())
);
}

protected override onBeforeAttach(msg: Message): void {
this.update();
this.toDispose.push(this.monitorModel.onChange(() => this.update()));
this.getCurrentSettings().then(this.onMonitorSettingsDidChange.bind(this));
this.monitorManagerProxy.onMonitorSettingsDidChange(
this.onMonitorSettingsDidChange.bind(this)
);
@postConstruct()
protected init(): void {
this.toDisposeOnReset.dispose();
this.toDisposeOnReset.pushAll([
Disposable.create(() => this.monitorManagerProxy.disconnect()),
this.monitorModel.onChange(() => this.update()),
this.monitorManagerProxy.onMonitorSettingsDidChange((event) =>
this.updateSettings(event)
),
]);
this.startMonitor();
}

this.monitorManagerProxy.startMonitor();
reset(): void {
this.init();
}

onMonitorSettingsDidChange(settings: MonitorSettings): void {
private updateSettings(settings: MonitorSettings): void {
this.settings = {
...this.settings,
pluggableMonitorSettings: {
@@ -90,6 +107,7 @@ export class MonitorWidget extends ReactWidget {
}

override dispose(): void {
this.toDisposeOnReset.dispose();
super.dispose();
}

@@ -122,7 +140,7 @@ export class MonitorWidget extends ReactWidget {
this.update();
}

protected onFocusResolved = (element: HTMLElement | undefined) => {
protected onFocusResolved = (element: HTMLElement | undefined): void => {
if (this.closing || !this.isAttached) {
return;
}
@@ -132,7 +150,7 @@ export class MonitorWidget extends ReactWidget {
);
};

protected get lineEndings(): SerialMonitorOutput.SelectOption<MonitorModel.EOL>[] {
protected get lineEndings(): SerialMonitorOutput.SelectOption<MonitorEOL>[] {
return [
{
label: nls.localize('arduino/serial/noLineEndings', 'No Line Ending'),
@@ -156,11 +174,23 @@ export class MonitorWidget extends ReactWidget {
];
}

private getCurrentSettings(): Promise<MonitorSettings> {
private async startMonitor(): Promise<void> {
await this.appStateService.reachedState('ready');
await this.boardsServiceProvider.reconciled;
await this.syncSettings();
await this.monitorManagerProxy.startMonitor();
}

private async syncSettings(): Promise<void> {
const settings = await this.getCurrentSettings();
this.updateSettings(settings);
}

private async getCurrentSettings(): Promise<MonitorSettings> {
const board = this.boardsServiceProvider.boardsConfig.selectedBoard;
const port = this.boardsServiceProvider.boardsConfig.selectedPort;
if (!board || !port) {
return Promise.resolve(this.settings || {});
return this.settings || {};
}
return this.monitorManagerProxy.getCurrentSettings(board, port);
}
@@ -171,7 +201,7 @@ export class MonitorWidget extends ReactWidget {
: undefined;

const baudrateOptions = baudrate?.values.map((b) => ({
label: b + ' baud',
label: nls.localize('arduino/monitor/baudRate', '{0} baud', b),
value: b,
}));
const baudrateSelectedOption = baudrateOptions?.find(
@@ -181,7 +211,7 @@ export class MonitorWidget extends ReactWidget {
const lineEnding =
this.lineEndings.find(
(item) => item.value === this.monitorModel.lineEnding
) || this.lineEndings[1]; // Defaults to `\n`.
) || MonitorEOL.DEFAULT;

return (
<div className="serial-monitor">
@@ -228,13 +258,13 @@ export class MonitorWidget extends ReactWidget {
);
}

protected readonly onSend = (value: string) => this.doSend(value);
protected async doSend(value: string): Promise<void> {
protected readonly onSend = (value: string): void => this.doSend(value);
protected doSend(value: string): void {
this.monitorManagerProxy.send(value);
}

protected readonly onChangeLineEnding = (
option: SerialMonitorOutput.SelectOption<MonitorModel.EOL>
option: SerialMonitorOutput.SelectOption<MonitorEOL>
): void => {
this.monitorModel.lineEnding = option.value;
};
Original file line number Diff line number Diff line change
@@ -5,6 +5,10 @@ import { DisposableCollection, nls } from '@theia/core/lib/common';
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
import { MonitorModel } from '../../monitor-model';
import { Unknown } from '../../../common/nls';
import {
isMonitorConnectionError,
MonitorConnectionStatus,
} from '../../../common/protocol';

class HistoryList {
private readonly items: string[] = [];
@@ -62,7 +66,7 @@ export namespace SerialMonitorSendInput {
}
export interface State {
text: string;
connected: boolean;
connectionStatus: MonitorConnectionStatus;
history: HistoryList;
}
}
@@ -75,18 +79,27 @@ export class SerialMonitorSendInput extends React.Component<

constructor(props: Readonly<SerialMonitorSendInput.Props>) {
super(props);
this.state = { text: '', connected: true, history: new HistoryList() };
this.state = {
text: '',
connectionStatus: 'not-connected',
history: new HistoryList(),
};
this.onChange = this.onChange.bind(this);
this.onSend = this.onSend.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
}

override componentDidMount(): void {
this.setState({ connected: this.props.monitorModel.connected });
this.setState({
connectionStatus: this.props.monitorModel.connectionStatus,
});
this.toDisposeBeforeUnmount.push(
this.props.monitorModel.onChange(({ property }) => {
if (property === 'connected')
this.setState({ connected: this.props.monitorModel.connected });
if (property === 'connected' || property === 'connectionStatus') {
this.setState({
connectionStatus: this.props.monitorModel.connectionStatus,
});
}
})
);
}
@@ -97,44 +110,83 @@ export class SerialMonitorSendInput extends React.Component<
}

override render(): React.ReactNode {
const status = this.state.connectionStatus;
const input = this.renderInput(status);
if (status !== 'connecting') {
return input;
}
return <label>{input}</label>;
}

private renderInput(status: MonitorConnectionStatus): React.ReactNode {
const inputClassName = this.inputClassName(status);
const placeholder = this.placeholder;
const readOnly = Boolean(inputClassName);
return (
<input
ref={this.setRef}
type="text"
className={`theia-input ${this.shouldShowWarning() ? 'warning' : ''}`}
placeholder={this.placeholder}
value={this.state.text}
className={`theia-input ${inputClassName}`}
readOnly={readOnly}
placeholder={placeholder}
title={placeholder}
value={readOnly ? '' : this.state.text} // always show the placeholder if cannot edit the <input>
onChange={this.onChange}
onKeyDown={this.onKeyDown}
/>
);
}

private inputClassName(
status: MonitorConnectionStatus
): 'error' | 'warning' | '' {
if (isMonitorConnectionError(status)) {
return 'error';
}
if (status === 'connected') {
return '';
}
return 'warning';
}

protected shouldShowWarning(): boolean {
const board = this.props.boardsServiceProvider.boardsConfig.selectedBoard;
const port = this.props.boardsServiceProvider.boardsConfig.selectedPort;
return !this.state.connected || !board || !port;
return !this.state.connectionStatus || !board || !port;
}

protected get placeholder(): string {
if (this.shouldShowWarning()) {
const status = this.state.connectionStatus;
if (isMonitorConnectionError(status)) {
return status.errorMessage;
}
if (status === 'not-connected') {
return nls.localize(
'arduino/serial/notConnected',
'Not connected. Select a board and a port to connect automatically.'
);
}

const board = this.props.boardsServiceProvider.boardsConfig.selectedBoard;
const port = this.props.boardsServiceProvider.boardsConfig.selectedPort;
const boardLabel = board
? Board.toString(board, {
useFqbn: false,
})
: Unknown;
const portLabel = port ? port.address : Unknown;
if (status === 'connecting') {
return nls.localize(
'arduino/serial/connecting',
"Connecting to '{0}' on '{1}'...",
boardLabel,
portLabel
);
}
return nls.localize(
'arduino/serial/message',
"Message (Enter to send message to '{0}' on '{1}')",
board
? Board.toString(board, {
useFqbn: false,
})
: Unknown,
port ? port.address : Unknown
boardLabel,
portLabel
);
}

32 changes: 22 additions & 10 deletions arduino-ide-extension/src/browser/style/index.css
Original file line number Diff line number Diff line change
@@ -29,9 +29,11 @@
/* https://github.com/arduino/arduino-ide/pull/1662#issuecomment-1324997134 */
body {
--theia-icon-loading: url(../icons/loading-light.svg);
--theia-icon-loading-warning: url(../icons/loading-dark.svg);
}
body.theia-dark {
--theia-icon-loading: url(../icons/loading-dark.svg);
--theia-icon-loading-warning: url(../icons/loading-light.svg);
}

.theia-input.warning:focus {
@@ -48,22 +50,32 @@ body.theia-dark {
}

.theia-input.warning::placeholder {
/* Chrome, Firefox, Opera, Safari 10.1+ */
color: var(--theia-warningForeground);
background-color: var(--theia-warningBackground);
opacity: 1; /* Firefox */
}

.theia-input.warning:-ms-input-placeholder {
/* Internet Explorer 10-11 */
color: var(--theia-warningForeground);
background-color: var(--theia-warningBackground);
.hc-black.hc-theia.theia-hc .theia-input.warning,
.hc-black.hc-theia.theia-hc .theia-input.warning::placeholder {
color: var(--theia-warningBackground);
background-color: var(--theia-warningForeground);
}

.theia-input.warning::-ms-input-placeholder {
/* Microsoft Edge */
color: var(--theia-warningForeground);
background-color: var(--theia-warningBackground);
.theia-input.error:focus {
outline-width: 1px;
outline-style: solid;
outline-offset: -1px;
opacity: 1 !important;
color: var(--theia-errorForeground);
background-color: var(--theia-errorBackground);
}

.theia-input.error {
background-color: var(--theia-errorBackground);
}

.theia-input.error::placeholder {
color: var(--theia-errorForeground);
background-color: var(--theia-errorBackground);
}

/* Makes the sidepanel a bit wider when opening the widget */
33 changes: 29 additions & 4 deletions arduino-ide-extension/src/browser/style/monitor.css
Original file line number Diff line number Diff line change
@@ -20,22 +20,47 @@

.serial-monitor .head {
display: flex;
padding: 5px;
padding: 0px 5px 5px 0px;
height: 27px;
background-color: var(--theia-activityBar-background);
}

.serial-monitor .head .send {
display: flex;
flex: 1;
margin-right: 2px;
}

.serial-monitor .head .send > input {
.serial-monitor .head .send > label:before {
content: "";
position: absolute;
top: -1px;
background: var(--theia-icon-loading-warning) center center no-repeat;
animation: theia-spin 1.25s linear infinite;
width: 30px;
height: 30px;
}

.serial-monitor .head .send > label {
position: relative;
width: 100%;
display: flex;
align-self: baseline;
}

.serial-monitor .head .send > input,
.serial-monitor .head .send > label > input {
line-height: var(--theia-content-line-height);
height: 27px;
width: 100%;
}

.serial-monitor .head .send > input:focus {
.serial-monitor .head .send > label > input {
padding-left: 30px;
box-sizing: border-box;
}

.serial-monitor .head .send > input:focus,
.serial-monitor .head .send > label > input:focus {
border-color: var(--theia-focusBorder);
}

10 changes: 5 additions & 5 deletions arduino-ide-extension/src/common/protocol/core-service.ts
Original file line number Diff line number Diff line change
@@ -73,12 +73,12 @@ export namespace CoreError {
UploadUsingProgrammer: 4003,
BurnBootloader: 4004,
};
export const VerifyFailed = create(Codes.Verify);
export const UploadFailed = create(Codes.Upload);
export const UploadUsingProgrammerFailed = create(
export const VerifyFailed = declareCoreError(Codes.Verify);
export const UploadFailed = declareCoreError(Codes.Upload);
export const UploadUsingProgrammerFailed = declareCoreError(
Codes.UploadUsingProgrammer
);
export const BurnBootloaderFailed = create(Codes.BurnBootloader);
export const BurnBootloaderFailed = declareCoreError(Codes.BurnBootloader);
export function is(
error: unknown
): error is ApplicationError<number, ErrorLocation[]> {
@@ -88,7 +88,7 @@ export namespace CoreError {
Object.values(Codes).includes(error.code)
);
}
function create(
function declareCoreError(
code: number
): ApplicationError.Constructor<number, ErrorLocation[]> {
return ApplicationError.declare(
186 changes: 165 additions & 21 deletions arduino-ide-extension/src/common/protocol/monitor-service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Event, JsonRpcServer } from '@theia/core';
import { ApplicationError, Event, JsonRpcServer, nls } from '@theia/core';
import {
PluggableMonitorSettings,
MonitorSettings,
@@ -31,7 +31,7 @@ export interface MonitorManagerProxyClient {
onMessagesReceived: Event<{ messages: string[] }>;
onMonitorSettingsDidChange: Event<MonitorSettings>;
onMonitorShouldReset: Event<void>;
connect(addressPort: number): void;
connect(addressPort: number): Promise<void>;
disconnect(): void;
getWebSocketPort(): number | undefined;
isWSConnected(): Promise<boolean>;
@@ -46,7 +46,7 @@ export interface PluggableMonitorSetting {
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)
// The setting type (at the moment only "enum" is available)
readonly type: string;
// The values allowed on "enum" types
readonly values: string[];
@@ -72,24 +72,168 @@ export namespace Monitor {
};
}

export interface Status {}
export type OK = Status;
export interface ErrorStatus extends Status {
readonly message: string;
export const MonitorErrorCodes = {
ConnectionFailed: 6001,
NotConnected: 6002,
AlreadyConnected: 6003,
MissingConfiguration: 6004,
} as const;

export const ConnectionFailedError = declareMonitorError(
MonitorErrorCodes.ConnectionFailed
);
export const NotConnectedError = declareMonitorError(
MonitorErrorCodes.NotConnected
);
export const AlreadyConnectedError = declareMonitorError(
MonitorErrorCodes.AlreadyConnected
);
export const MissingConfigurationError = declareMonitorError(
MonitorErrorCodes.MissingConfiguration
);

export function createConnectionFailedError(
port: Port,
details?: string
): ApplicationError<number, PortDescriptor> {
const { protocol, address } = port;
let message;
if (details) {
const detailsWithPeriod = details.endsWith('.') ? details : `${details}.`;
message = nls.localize(
'arduino/monitor/connectionFailedErrorWithDetails',
'{0} Could not connect to {1} {2} port.',
detailsWithPeriod,
address,
protocol
);
} else {
message = nls.localize(
'arduino/monitor/connectionFailedError',
'Could not connect to {0} {1} port.',
address,
protocol
);
}
return ConnectionFailedError(message, { protocol, address });
}
export function createNotConnectedError(
port: Port
): ApplicationError<number, PortDescriptor> {
const { protocol, address } = port;
return NotConnectedError(
nls.localize(
'arduino/monitor/notConnectedError',
'Not connected to {0} {1} port.',
address,
protocol
),
{ protocol, address }
);
}
export function createAlreadyConnectedError(
port: Port
): ApplicationError<number, PortDescriptor> {
const { protocol, address } = port;
return AlreadyConnectedError(
nls.localize(
'arduino/monitor/alreadyConnectedError',
'Could not connect to {0} {1} port. Already connected.',
address,
protocol
),
{ protocol, address }
);
}
export function createMissingConfigurationError(
port: Port
): ApplicationError<number, PortDescriptor> {
const { protocol, address } = port;
return MissingConfigurationError(
nls.localize(
'arduino/monitor/missingConfigurationError',
'Could not connect to {0} {1} port. The monitor configuration is missing.',
address,
protocol
),
{ protocol, address }
);
}

/**
* Bare minimum representation of a port. Supports neither UI labels nor properties.
*/
interface PortDescriptor {
readonly protocol: string;
readonly address: string;
}
function declareMonitorError(
code: number
): ApplicationError.Constructor<number, PortDescriptor> {
return ApplicationError.declare(
code,
(message: string, data: PortDescriptor) => ({ data, message })
);
}

export interface MonitorConnectionError {
readonly errorMessage: string;
}

export type MonitorConnectionStatus =
| 'connecting'
| 'connected'
| 'not-connected'
| MonitorConnectionError;

export function monitorConnectionStatusEquals(
left: MonitorConnectionStatus,
right: MonitorConnectionStatus
): boolean {
if (typeof left === 'object' && typeof right === 'object') {
return left.errorMessage === right.errorMessage;
}
return left === right;
}

/**
* @deprecated see `MonitorState#connected`
*/
export function isMonitorConnected(
status: MonitorConnectionStatus
): status is 'connected' {
return status === 'connected';
}
export namespace Status {
export function isOK(status: Status & { message?: string }): status is OK {
return !!status && typeof status.message !== 'string';

export function isMonitorConnectionError(
status: MonitorConnectionStatus
): status is MonitorConnectionError {
return typeof status === 'object';
}

export interface MonitorState {
autoscroll: boolean;
timestamp: boolean;
lineEnding: MonitorEOL;
interpolate: boolean;
darkTheme: boolean;
wsPort: number;
serialPort: string;
connectionStatus: MonitorConnectionStatus;
/**
* @deprecated This property is never get by IDE2 only set. This value is present to be backward compatible with the plotter app.
* IDE2 uses `MonitorState#connectionStatus`.
*/
connected: boolean;
}
export namespace MonitorState {
export interface Change<K extends keyof MonitorState> {
readonly property: K;
readonly value: MonitorState[K];
}
export const OK: OK = {};
export const NOT_CONNECTED: ErrorStatus = { message: 'Not connected.' };
export const ALREADY_CONNECTED: ErrorStatus = {
message: 'Already connected.',
};
export const CONFIG_MISSING: ErrorStatus = {
message: 'Serial Config missing.',
};
export const UPLOAD_IN_PROGRESS: ErrorStatus = {
message: 'Upload in progress.',
};
}

export type MonitorEOL = '' | '\n' | '\r' | '\r\n';
export namespace MonitorEOL {
export const DEFAULT: MonitorEOL = '\n';
}
4 changes: 2 additions & 2 deletions arduino-ide-extension/src/node/core-service-impl.ts
Original file line number Diff line number Diff line change
@@ -23,7 +23,7 @@ import {
UploadUsingProgrammerResponse,
} from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb';
import { ResponseService } from '../common/protocol/response-service';
import { OutputMessage, Port, Status } from '../common/protocol';
import { OutputMessage, Port } from '../common/protocol';
import { ArduinoCoreServiceClient } from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb';
import { Port as RpcPort } from './cli-protocol/cc/arduino/cli/commands/v1/port_pb';
import { ApplicationError, CommandService, Disposable, nls } from '@theia/core';
@@ -392,7 +392,7 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
}: {
fqbn?: string | undefined;
port?: Port | undefined;
}): Promise<Status> {
}): Promise<void> {
this.boardDiscovery.setUploadInProgress(false);
return this.monitorManager.notifyUploadFinished(fqbn, port);
}
14 changes: 9 additions & 5 deletions arduino-ide-extension/src/node/monitor-manager-proxy-impl.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,6 @@ 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';
@@ -41,11 +40,16 @@ export class MonitorManagerProxyImpl implements MonitorManagerProxy {
await this.changeMonitorSettings(board, port, settings);
}

const connectToClient = (status: Status) => {
if (status === Status.ALREADY_CONNECTED || status === Status.OK) {
// Monitor started correctly, connect it with the frontend
this.client.connect(this.manager.getWebsocketAddressPort(board, port));
const connectToClient = async () => {
const address = this.manager.getWebsocketAddressPort(board, port);
if (!this.client) {
throw new Error(
`No client was connected to this monitor manager. Board: ${
board.fqbn ?? board.name
}, port: ${port.address}, address: ${address}`
);
}
await this.client.connect(address);
};
return this.manager.startMonitor(board, port, connectToClient);
}
33 changes: 21 additions & 12 deletions arduino-ide-extension/src/node/monitor-manager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { ILogger } from '@theia/core';
import { inject, injectable, named } from '@theia/core/shared/inversify';
import { Board, BoardsService, Port, Status } from '../common/protocol';
import {
AlreadyConnectedError,
Board,
BoardsService,
Port,
} from '../common/protocol';
import { CoreClientAware } from './core-client-provider';
import { MonitorService } from './monitor-service';
import { MonitorServiceFactory } from './monitor-service-factory';
@@ -36,7 +41,7 @@ export class MonitorManager extends CoreClientAware {
private monitorServiceStartQueue: {
monitorID: string;
serviceStartParams: [Board, Port];
connectToClient: (status: Status) => void;
connectToClient: () => Promise<void>;
}[] = [];

@inject(MonitorServiceFactory)
@@ -104,7 +109,7 @@ export class MonitorManager extends CoreClientAware {
async startMonitor(
board: Board,
port: Port,
connectToClient: (status: Status) => void
connectToClient: () => Promise<void>
): Promise<void> {
const monitorID = this.monitorID(board.fqbn, port);

@@ -127,8 +132,14 @@ export class MonitorManager extends CoreClientAware {
return;
}

const result = await monitor.start();
connectToClient(result);
try {
await connectToClient();
await monitor.start();
} catch (err) {
if (!AlreadyConnectedError.is(err)) {
throw err;
}
}
}

/**
@@ -202,8 +213,7 @@ export class MonitorManager extends CoreClientAware {
async notifyUploadFinished(
fqbn?: string | undefined,
port?: Port
): Promise<Status> {
let status: Status = Status.NOT_CONNECTED;
): Promise<void> {
let portDidChangeOnUpload = false;

// We have no way of knowing which monitor
@@ -214,7 +224,7 @@ export class MonitorManager extends CoreClientAware {

const monitor = this.monitorServices.get(monitorID);
if (monitor) {
status = await monitor.start();
await monitor.start();
}

// this monitorID will only be present in "disposedForUpload"
@@ -232,7 +242,6 @@ export class MonitorManager extends CoreClientAware {
}

await this.startQueuedServices(portDidChangeOnUpload);
return status;
}

async startQueuedServices(portDidChangeOnUpload: boolean): Promise<void> {
@@ -246,7 +255,7 @@ export class MonitorManager extends CoreClientAware {

for (const {
monitorID,
serviceStartParams: [_, port],
serviceStartParams: [, port],
connectToClient,
} of queued) {
const boardsState = await this.boardsService.getState();
@@ -261,8 +270,8 @@ export class MonitorManager extends CoreClientAware {
const monitorService = this.monitorServices.get(monitorID);

if (monitorService) {
const result = await monitorService.start();
connectToClient(result);
await connectToClient();
await monitorService.start();
}
}
}
372 changes: 217 additions & 155 deletions arduino-ide-extension/src/node/monitor-service.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { MonitorModel } from '../../browser/monitor-model';
import { PluggableMonitorSetting } from '../../common/protocol';
import { MonitorState, PluggableMonitorSetting } from '../../common/protocol';

export type PluggableMonitorSettings = Record<string, PluggableMonitorSetting>;
export interface MonitorSettings {
pluggableMonitorSettings?: PluggableMonitorSettings;
monitorUISettings?: Partial<MonitorModel.State>;
monitorUISettings?: Partial<MonitorState>;
}

export const MonitorSettingsProvider = Symbol('MonitorSettingsProvider');
8 changes: 8 additions & 0 deletions i18n/en.json
Original file line number Diff line number Diff line change
@@ -328,6 +328,13 @@
"tools": "Tools"
},
"monitor": {
"alreadyConnectedError": "Could not connect to {0} {1} port. Already connected.",
"baudRate": "{0} baud",
"connectionFailedError": "Could not connect to {0} {1} port.",
"connectionFailedErrorWithDetails": "{0} Could not connect to {1} {2} port.",
"connectionTimeout": "Timeout. The IDE has not received the 'success' message from the monitor after successfully connecting to it",
"missingConfigurationError": "Could not connect to {0} {1} port. The monitor configuration is missing.",
"notConnectedError": "Not connected to {0} {1} port.",
"unableToCloseWebSocket": "Unable to close websocket",
"unableToConnectToWebSocket": "Unable to connect to websocket"
},
@@ -408,6 +415,7 @@
"serial": {
"autoscroll": "Autoscroll",
"carriageReturn": "Carriage Return",
"connecting": "Connecting to '{0}' on '{1}'...",
"message": "Message (Enter to send message to '{0}' on '{1}')",
"newLine": "New Line",
"newLineCarriageReturn": "Both NL & CR",