fix: propagate monitor errors to the frontend

- Handle when the board's platform is not installed (Closes #1974)
 - UX: Smoother monitor widget reset (Closes #1985)
 - Fixed monitor <input> readOnly state (Closes #1984)
 - Set monitor widget header color (Ref #682)

Closes #1508

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
Akos Kitta
2023-03-17 15:41:34 +01:00
committed by Akos Kitta
parent ab5c63c4b7
commit 80d5b5afa7
16 changed files with 722 additions and 357 deletions

View File

@@ -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();
}
}

View File

@@ -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)
);
this.monitorManagerProxy.startMonitor();
@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();
}
onMonitorSettingsDidChange(settings: MonitorSettings): void {
reset(): void {
this.init();
}
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;
};

View File

@@ -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
);
}