mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-06-07 12:46:34 +00:00
[rewrite-me]: initial serial monitor changes
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
This commit is contained in:
parent
d22c0b9e55
commit
9efcbcf2ae
@ -23,10 +23,15 @@
|
|||||||
"@theia/terminal": "next",
|
"@theia/terminal": "next",
|
||||||
"@theia/workspace": "next",
|
"@theia/workspace": "next",
|
||||||
"@types/google-protobuf": "^3.7.1",
|
"@types/google-protobuf": "^3.7.1",
|
||||||
|
"@types/dateformat": "^3.0.1",
|
||||||
|
"@types/chance": "1.0.7",
|
||||||
"@types/ps-tree": "^1.1.0",
|
"@types/ps-tree": "^1.1.0",
|
||||||
"@types/react-select": "^3.0.0",
|
"@types/react-select": "^3.0.0",
|
||||||
"@types/which": "^1.3.1",
|
"@types/which": "^1.3.1",
|
||||||
|
"chance": "^1.1.3",
|
||||||
"css-element-queries": "^1.2.0",
|
"css-element-queries": "^1.2.0",
|
||||||
|
"dateformat": "^3.0.3",
|
||||||
|
"google-protobuf": "^3.11.0",
|
||||||
"p-queue": "^5.0.0",
|
"p-queue": "^5.0.0",
|
||||||
"ps-tree": "^1.2.0",
|
"ps-tree": "^1.2.0",
|
||||||
"react-select": "^3.0.4",
|
"react-select": "^3.0.4",
|
||||||
|
@ -41,7 +41,6 @@ import { MaybePromise } from '@theia/core/lib/common/types';
|
|||||||
import { BoardsConfigDialog } from './boards/boards-config-dialog';
|
import { BoardsConfigDialog } from './boards/boards-config-dialog';
|
||||||
import { BoardsToolBarItem } from './boards/boards-toolbar-item';
|
import { BoardsToolBarItem } from './boards/boards-toolbar-item';
|
||||||
import { BoardsConfig } from './boards/boards-config';
|
import { BoardsConfig } from './boards/boards-config';
|
||||||
import { MonitorService } from '../common/protocol/monitor-service';
|
|
||||||
import { ConfigService } from '../common/protocol/config-service';
|
import { ConfigService } from '../common/protocol/config-service';
|
||||||
import { MonitorConnection } from './monitor/monitor-connection';
|
import { MonitorConnection } from './monitor/monitor-connection';
|
||||||
import { MonitorViewContribution } from './monitor/monitor-view-contribution';
|
import { MonitorViewContribution } from './monitor/monitor-view-contribution';
|
||||||
@ -79,9 +78,6 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
|
|||||||
@inject(CoreService)
|
@inject(CoreService)
|
||||||
protected readonly coreService: CoreService;
|
protected readonly coreService: CoreService;
|
||||||
|
|
||||||
@inject(MonitorService)
|
|
||||||
protected readonly monitorService: MonitorService;
|
|
||||||
|
|
||||||
@inject(WorkspaceServiceExt)
|
@inject(WorkspaceServiceExt)
|
||||||
protected readonly workspaceServiceExt: WorkspaceServiceExt;
|
protected readonly workspaceServiceExt: WorkspaceServiceExt;
|
||||||
|
|
||||||
@ -336,7 +332,9 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
|
|||||||
}
|
}
|
||||||
|
|
||||||
const connectionConfig = this.monitorConnection.connectionConfig;
|
const connectionConfig = this.monitorConnection.connectionConfig;
|
||||||
|
if (connectionConfig) {
|
||||||
await this.monitorConnection.disconnect();
|
await this.monitorConnection.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { boardsConfig } = this.boardsServiceClient;
|
const { boardsConfig } = this.boardsServiceClient;
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
import { injectable, inject } from "inversify";
|
import { injectable, inject, postConstruct } from 'inversify';
|
||||||
import { MonitorService, ConnectionConfig } from "../../common/protocol/monitor-service";
|
import { Emitter, Event } from '@theia/core/lib/common/event';
|
||||||
import { Emitter, Event } from "@theia/core";
|
// import { ConnectionStatusService } from '@theia/core/lib/browser/connection-status-service';
|
||||||
|
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||||
|
import { MonitorService, MonitorConfig, MonitorError } from '../../common/protocol/monitor-service';
|
||||||
|
import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl';
|
||||||
|
import { Port, Board } from '../../common/protocol/boards-service';
|
||||||
|
import { MonitorServiceClientImpl } from './monitor-service-client-impl';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class MonitorConnection {
|
export class MonitorConnection {
|
||||||
@ -8,46 +13,102 @@ export class MonitorConnection {
|
|||||||
@inject(MonitorService)
|
@inject(MonitorService)
|
||||||
protected readonly monitorService: MonitorService;
|
protected readonly monitorService: MonitorService;
|
||||||
|
|
||||||
connectionId: string | undefined;
|
@inject(MonitorServiceClientImpl)
|
||||||
|
protected readonly monitorServiceClient: MonitorServiceClientImpl;
|
||||||
|
|
||||||
protected _connectionConfig: ConnectionConfig | undefined;
|
@inject(BoardsServiceClientImpl)
|
||||||
|
protected boardsServiceClient: BoardsServiceClientImpl;
|
||||||
|
|
||||||
|
@inject(MessageService)
|
||||||
|
protected messageService: MessageService;
|
||||||
|
|
||||||
|
// @inject(ConnectionStatusService)
|
||||||
|
// protected readonly connectionStatusService: ConnectionStatusService;
|
||||||
|
|
||||||
|
protected state: MonitorConnection.State | undefined;
|
||||||
protected readonly onConnectionChangedEmitter = new Emitter<string | undefined>();
|
protected readonly onConnectionChangedEmitter = new Emitter<string | undefined>();
|
||||||
|
|
||||||
readonly onConnectionChanged: Event<string | undefined> = this.onConnectionChangedEmitter.event;
|
readonly onConnectionChanged: Event<string | undefined> = this.onConnectionChangedEmitter.event;
|
||||||
|
|
||||||
get connectionConfig(): ConnectionConfig | undefined {
|
@postConstruct()
|
||||||
return this._connectionConfig;
|
protected init(): void {
|
||||||
|
this.monitorServiceClient.onError(error => {
|
||||||
|
if (this.state) {
|
||||||
|
const { code, connectionId, config } = error;
|
||||||
|
if (this.state.connectionId === connectionId) {
|
||||||
|
switch (code) {
|
||||||
|
case MonitorError.ErrorCodes.CLIENT_CANCEL: {
|
||||||
|
console.log(`Connection was canceled by client: ${MonitorConnection.State.toString(this.state)}.`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MonitorError.ErrorCodes.DEVICE_BUSY: {
|
||||||
|
const { port } = config;
|
||||||
|
this.messageService.warn(`Connection failed. Serial port is busy: ${Port.toString(port)}.`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MonitorError.ErrorCodes.DEVICE_NOT_CONFIGURED: {
|
||||||
|
const { port } = config;
|
||||||
|
this.messageService.info(`Disconnected from ${Port.toString(port)}.`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.state = undefined;
|
||||||
|
} else {
|
||||||
|
console.warn(`Received an error from unexpected connection: ${MonitorConnection.State.toString({ connectionId, config })}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect(config: ConnectionConfig): Promise<string | undefined> {
|
get connectionId(): string | undefined {
|
||||||
if (this.connectionId) {
|
return this.state ? this.state.connectionId : undefined;
|
||||||
await this.disconnect();
|
}
|
||||||
|
|
||||||
|
get connectionConfig(): MonitorConfig | undefined {
|
||||||
|
return this.state ? this.state.config : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect(config: MonitorConfig): Promise<string | undefined> {
|
||||||
|
if (this.state) {
|
||||||
|
throw new Error(`Already connected to ${MonitorConnection.State.toString(this.state)}.`);
|
||||||
}
|
}
|
||||||
const { connectionId } = await this.monitorService.connect(config);
|
const { connectionId } = await this.monitorService.connect(config);
|
||||||
this.connectionId = connectionId;
|
this.state = { connectionId, config };
|
||||||
this._connectionConfig = config;
|
this.onConnectionChangedEmitter.fire(connectionId);
|
||||||
|
|
||||||
this.onConnectionChangedEmitter.fire(this.connectionId);
|
|
||||||
|
|
||||||
return connectionId;
|
return connectionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
async disconnect(): Promise<boolean> {
|
async disconnect(): Promise<boolean> {
|
||||||
let result = true;
|
if (!this.state) {
|
||||||
const connections = await this.monitorService.getConnectionIds();
|
throw new Error('Not connected. Nothing to disconnect.');
|
||||||
if (this.connectionId && connections.findIndex(id => id === this.connectionId) >= 0) {
|
|
||||||
console.log('>>> Disposing existing monitor connection before establishing a new one...');
|
|
||||||
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.`);
|
|
||||||
this.connectionId = undefined;
|
|
||||||
this._connectionConfig = undefined;
|
|
||||||
this.onConnectionChangedEmitter.fire(this.connectionId);
|
|
||||||
}
|
}
|
||||||
|
console.log('>>> Disposing existing monitor connection before establishing a new one...');
|
||||||
|
const result = await this.monitorService.disconnect(this.state.connectionId);
|
||||||
|
if (result) {
|
||||||
|
console.log(`<<< Disposed connection. Was: ${MonitorConnection.State.toString(this.state)}`);
|
||||||
|
this.state = undefined;
|
||||||
|
this.onConnectionChangedEmitter.fire(undefined);
|
||||||
|
} else {
|
||||||
|
console.warn(`<<< Could not dispose connection. Activate connection: ${MonitorConnection.State.toString(this.state)}`);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace MonitorConnection {
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
readonly connectionId: string;
|
||||||
|
readonly config: MonitorConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace State {
|
||||||
|
export function toString(state: State): string {
|
||||||
|
const { connectionId, config } = state;
|
||||||
|
const { board, port } = config;
|
||||||
|
return `${Board.toString(board)} ${Port.toString(port)} [ID: ${connectionId}]`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,58 +1,93 @@
|
|||||||
import { injectable } from "inversify";
|
import { injectable } from 'inversify';
|
||||||
import { Emitter } from "@theia/core";
|
import { Emitter, Event } from '@theia/core/lib/common/event';
|
||||||
|
import { MonitorConfig } from '../../common/protocol/monitor-service';
|
||||||
export namespace MonitorModel {
|
|
||||||
export interface Data {
|
|
||||||
autoscroll: boolean,
|
|
||||||
timestamp: boolean,
|
|
||||||
baudRate: number,
|
|
||||||
lineEnding: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class MonitorModel {
|
export class MonitorModel {
|
||||||
|
|
||||||
protected readonly onChangeEmitter = new Emitter<void>();
|
protected readonly onChangeEmitter: Emitter<void>;
|
||||||
|
protected _autoscroll: boolean;
|
||||||
|
protected _timestamp: boolean;
|
||||||
|
protected _baudRate: MonitorConfig.BaudRate;
|
||||||
|
protected _lineEnding: MonitorModel.EOL;
|
||||||
|
|
||||||
readonly onChange = this.onChangeEmitter.event;
|
constructor() {
|
||||||
|
this._autoscroll = true;
|
||||||
|
this._timestamp = false;
|
||||||
|
this._baudRate = MonitorConfig.BaudRate.DEFAULT;
|
||||||
|
this._lineEnding = MonitorModel.EOL.DEFAULT;
|
||||||
|
this.onChangeEmitter = new Emitter<void>();
|
||||||
|
}
|
||||||
|
|
||||||
protected _autoscroll: boolean = true;
|
get onChange(): Event<void> {
|
||||||
protected _timestamp: boolean = false;
|
return this.onChangeEmitter.event;
|
||||||
baudRate: number;
|
}
|
||||||
lineEnding: string = '\n';
|
|
||||||
|
|
||||||
get autoscroll(): boolean {
|
get autoscroll(): boolean {
|
||||||
return this._autoscroll;
|
return this._autoscroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleAutoscroll(): void {
|
||||||
|
this._autoscroll = !this._autoscroll;
|
||||||
|
}
|
||||||
|
|
||||||
get timestamp(): boolean {
|
get timestamp(): boolean {
|
||||||
return this._timestamp;
|
return this._timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleAutoscroll(): void {
|
|
||||||
this._autoscroll = !this._autoscroll;
|
|
||||||
this.onChangeEmitter.fire(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleTimestamp(): void {
|
toggleTimestamp(): void {
|
||||||
this._timestamp = !this._timestamp;
|
this._timestamp = !this._timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
get baudRate(): MonitorConfig.BaudRate {
|
||||||
|
return this._baudRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
set baudRate(baudRate: MonitorConfig.BaudRate) {
|
||||||
|
this._baudRate = baudRate;
|
||||||
this.onChangeEmitter.fire(undefined);
|
this.onChangeEmitter.fire(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
restore(model: MonitorModel.Data) {
|
get lineEnding(): MonitorModel.EOL {
|
||||||
this._autoscroll = model.autoscroll;
|
return this._lineEnding;
|
||||||
this._timestamp = model.timestamp;
|
|
||||||
this.baudRate = model.baudRate;
|
|
||||||
this.lineEnding = model.lineEnding;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
store(): MonitorModel.Data {
|
set lineEnding(lineEnding: MonitorModel.EOL) {
|
||||||
|
this._lineEnding = lineEnding;
|
||||||
|
this.onChangeEmitter.fire(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
restore(state: MonitorModel.State) {
|
||||||
|
this._autoscroll = state.autoscroll;
|
||||||
|
this._timestamp = state.timestamp;
|
||||||
|
this._baudRate = state.baudRate;
|
||||||
|
this._lineEnding = state.lineEnding;
|
||||||
|
this.onChangeEmitter.fire(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
store(): MonitorModel.State {
|
||||||
return {
|
return {
|
||||||
autoscroll: this._autoscroll,
|
autoscroll: this._autoscroll,
|
||||||
timestamp: this._timestamp,
|
timestamp: this._timestamp,
|
||||||
baudRate: this.baudRate,
|
baudRate: this._baudRate,
|
||||||
lineEnding: this.lineEnding
|
lineEnding: this._lineEnding
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace MonitorModel {
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
autoscroll: boolean;
|
||||||
|
timestamp: boolean;
|
||||||
|
baudRate: MonitorConfig.BaudRate;
|
||||||
|
lineEnding: EOL;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EOL = '' | '\n' | '\r' | '\r\n';
|
||||||
|
export namespace EOL {
|
||||||
|
export const DEFAULT: EOL = '\n';
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -13,7 +13,7 @@ export class MonitorServiceClientImpl implements MonitorServiceClient {
|
|||||||
notifyRead(event: MonitorReadEvent): void {
|
notifyRead(event: MonitorReadEvent): void {
|
||||||
this.onReadEmitter.fire(event);
|
this.onReadEmitter.fire(event);
|
||||||
const { connectionId, data } = event;
|
const { connectionId, data } = event;
|
||||||
console.log(`Received data from ${connectionId}: ${data}`);
|
console.debug(`Received data from ${connectionId}: ${data}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyError(error: MonitorError): void {
|
notifyError(error: MonitorError): void {
|
||||||
|
@ -80,7 +80,7 @@ export class MonitorViewContribution extends AbstractViewContribution<MonitorWid
|
|||||||
isVisible: widget => widget instanceof MonitorWidget,
|
isVisible: widget => widget instanceof MonitorWidget,
|
||||||
execute: widget => {
|
execute: widget => {
|
||||||
if (widget instanceof MonitorWidget) {
|
if (widget instanceof MonitorWidget) {
|
||||||
widget.clear();
|
widget.clearConsole();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -124,4 +124,5 @@ export class MonitorViewContribution extends AbstractViewContribution<MonitorWid
|
|||||||
protected async doToggleTimestamp() {
|
protected async doToggleTimestamp() {
|
||||||
this.model.toggleTimestamp();
|
this.model.toggleTimestamp();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,24 +1,24 @@
|
|||||||
import { ReactWidget, Message, Widget, StatefulWidget } from "@theia/core/lib/browser";
|
|
||||||
import { postConstruct, injectable, inject } from "inversify";
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import Select, { components } from 'react-select';
|
import * as dateFormat from 'dateformat';
|
||||||
import { Styles } from "react-select/src/styles";
|
import { postConstruct, injectable, inject } from 'inversify';
|
||||||
import { ThemeConfig } from "react-select/src/theme";
|
import { ThemeConfig } from 'react-select/src/theme';
|
||||||
import { OptionsType } from "react-select/src/types";
|
import { OptionsType } from 'react-select/src/types';
|
||||||
import { MonitorServiceClientImpl } from "./monitor-service-client-impl";
|
import Select from 'react-select';
|
||||||
import { MessageService } from "@theia/core";
|
import { Styles } from 'react-select/src/styles';
|
||||||
import { ConnectionConfig, MonitorService } from "../../common/protocol/monitor-service";
|
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||||
import { MonitorConnection } from "./monitor-connection";
|
import { ReactWidget, Message, Widget, StatefulWidget } from '@theia/core/lib/browser';
|
||||||
import { BoardsServiceClientImpl } from "../boards/boards-service-client-impl";
|
import { MonitorServiceClientImpl } from './monitor-service-client-impl';
|
||||||
import { AttachedSerialBoard, BoardsService, Board } from "../../common/protocol/boards-service";
|
import { MonitorConfig, MonitorService } from '../../common/protocol/monitor-service';
|
||||||
import { BoardsConfig } from "../boards/boards-config";
|
import { AttachedSerialBoard, BoardsService } from '../../common/protocol/boards-service';
|
||||||
import { MonitorModel } from "./monitor-model";
|
import { BoardsConfig } from '../boards/boards-config';
|
||||||
|
import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl';
|
||||||
|
import { MonitorModel } from './monitor-model';
|
||||||
|
import { MonitorConnection } from './monitor-connection';
|
||||||
|
|
||||||
export namespace SerialMonitorSendField {
|
export namespace SerialMonitorSendField {
|
||||||
export interface Props {
|
export interface Props {
|
||||||
onSend: (text: string) => void
|
readonly onSend: (text: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
@ -44,7 +44,6 @@ export class SerialMonitorSendField extends React.Component<SerialMonitorSendFie
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <React.Fragment>
|
return <React.Fragment>
|
||||||
<form onSubmit={this.handleSubmit}>
|
|
||||||
<input
|
<input
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
ref={ref => this.inputField = ref}
|
ref={ref => this.inputField = ref}
|
||||||
@ -52,8 +51,10 @@ export class SerialMonitorSendField extends React.Component<SerialMonitorSendFie
|
|||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
value={this.state.value}
|
value={this.state.value}
|
||||||
onChange={this.handleChange} />
|
onChange={this.handleChange} />
|
||||||
<input className="btn" type="submit" value="Submit" />
|
<button className='button' onClick={this.handleSubmit}>Send</button>
|
||||||
</form>
|
{/* <input className='btn' type='submit' value='Submit' />
|
||||||
|
<form onSubmit={this.handleSubmit}>
|
||||||
|
</form> */}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,7 +62,7 @@ export class SerialMonitorSendField extends React.Component<SerialMonitorSendFie
|
|||||||
this.setState({ value: event.target.value });
|
this.setState({ value: event.target.value });
|
||||||
}
|
}
|
||||||
|
|
||||||
protected handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
protected handleSubmit(event: React.MouseEvent<HTMLButtonElement>) {
|
||||||
this.props.onSend(this.state.value);
|
this.props.onSend(this.state.value);
|
||||||
this.setState({ value: '' });
|
this.setState({ value: '' });
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -70,55 +71,43 @@ export class SerialMonitorSendField extends React.Component<SerialMonitorSendFie
|
|||||||
|
|
||||||
export namespace SerialMonitorOutput {
|
export namespace SerialMonitorOutput {
|
||||||
export interface Props {
|
export interface Props {
|
||||||
lines: string[];
|
readonly lines: string[];
|
||||||
model: MonitorModel;
|
readonly model: MonitorModel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SerialMonitorOutput extends React.Component<SerialMonitorOutput.Props> {
|
export class SerialMonitorOutput extends React.Component<SerialMonitorOutput.Props> {
|
||||||
protected theEnd: HTMLDivElement | null;
|
|
||||||
|
protected anchor: HTMLElement | null;
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let result = '';
|
|
||||||
|
|
||||||
const style: React.CSSProperties = {
|
|
||||||
whiteSpace: 'pre',
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const text of this.props.lines) {
|
|
||||||
result += text;
|
|
||||||
}
|
|
||||||
return <React.Fragment>
|
return <React.Fragment>
|
||||||
<div style={style}>{result}</div>
|
<div style={({ whiteSpace: 'pre', fontFamily: 'monospace' })}>
|
||||||
<div style={{ float: "left", clear: "both" }}
|
{this.props.lines.join('')}
|
||||||
ref={(el) => { this.theEnd = el; }}>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ float: 'left', clear: 'both' }} ref={element => { this.anchor = element; }} />
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected scrollToBottom() {
|
|
||||||
if (this.theEnd) {
|
|
||||||
this.theEnd.scrollIntoView();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.props.model.autoscroll) {
|
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
if (this.props.model.autoscroll) {
|
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected scrollToBottom() {
|
||||||
|
if (this.props.model.autoscroll && this.anchor) {
|
||||||
|
this.anchor.scrollIntoView();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectOption {
|
}
|
||||||
label: string;
|
|
||||||
value: string | number;
|
export interface SelectOption<T> {
|
||||||
|
readonly label: string;
|
||||||
|
readonly value: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
@ -126,22 +115,37 @@ export class MonitorWidget extends ReactWidget implements StatefulWidget {
|
|||||||
|
|
||||||
static readonly ID = 'serial-monitor';
|
static readonly ID = 'serial-monitor';
|
||||||
|
|
||||||
protected lines: string[];
|
@inject(MonitorServiceClientImpl)
|
||||||
protected tempData: string;
|
protected readonly serviceClient: MonitorServiceClientImpl;
|
||||||
|
|
||||||
|
@inject(MonitorConnection)
|
||||||
|
protected readonly connection: MonitorConnection;
|
||||||
|
|
||||||
|
@inject(MonitorService)
|
||||||
|
protected readonly monitorService: MonitorService;
|
||||||
|
|
||||||
|
@inject(BoardsServiceClientImpl)
|
||||||
|
protected readonly boardsServiceClient: BoardsServiceClientImpl;
|
||||||
|
|
||||||
|
@inject(MessageService)
|
||||||
|
protected readonly messageService: MessageService;
|
||||||
|
|
||||||
|
@inject(BoardsService)
|
||||||
|
protected readonly boardsService: BoardsService;
|
||||||
|
|
||||||
|
@inject(MonitorModel)
|
||||||
|
protected readonly model: MonitorModel;
|
||||||
|
|
||||||
|
protected lines: string[];
|
||||||
|
protected chunk: string;
|
||||||
protected widgetHeight: number;
|
protected widgetHeight: number;
|
||||||
|
|
||||||
protected continuePreviousConnection: boolean;
|
/**
|
||||||
|
* Do not touch or use it. It is for setting the focus on the `input` after the widget activation.
|
||||||
|
*/
|
||||||
|
protected focusNode: HTMLElement | undefined;
|
||||||
|
|
||||||
constructor(
|
constructor() {
|
||||||
@inject(MonitorServiceClientImpl) protected readonly serviceClient: MonitorServiceClientImpl,
|
|
||||||
@inject(MonitorConnection) protected readonly connection: MonitorConnection,
|
|
||||||
@inject(MonitorService) protected readonly monitorService: MonitorService,
|
|
||||||
@inject(BoardsServiceClientImpl) protected readonly boardsServiceClient: BoardsServiceClientImpl,
|
|
||||||
@inject(MessageService) protected readonly messageService: MessageService,
|
|
||||||
@inject(BoardsService) protected readonly boardsService: BoardsService,
|
|
||||||
@inject(MonitorModel) protected readonly model: MonitorModel
|
|
||||||
) {
|
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.id = MonitorWidget.ID;
|
this.id = MonitorWidget.ID;
|
||||||
@ -149,104 +153,82 @@ export class MonitorWidget extends ReactWidget implements StatefulWidget {
|
|||||||
this.title.iconClass = 'arduino-serial-monitor-tab-icon';
|
this.title.iconClass = 'arduino-serial-monitor-tab-icon';
|
||||||
|
|
||||||
this.lines = [];
|
this.lines = [];
|
||||||
this.tempData = '';
|
this.chunk = '';
|
||||||
|
|
||||||
this.scrollOptions = undefined;
|
this.scrollOptions = undefined;
|
||||||
|
|
||||||
this.toDisposeOnDetach.push(serviceClient.onRead(({ data, connectionId }) => {
|
|
||||||
this.tempData += data;
|
|
||||||
if (this.tempData.endsWith('\n')) {
|
|
||||||
if (this.model.timestamp) {
|
|
||||||
const nu = new Date();
|
|
||||||
const h = (100 + nu.getHours()).toString().substr(1)
|
|
||||||
const min = (100 + nu.getMinutes()).toString().substr(1)
|
|
||||||
const sec = (100 + nu.getSeconds()).toString().substr(1)
|
|
||||||
const ms = (1000 + nu.getMilliseconds()).toString().substr(1);
|
|
||||||
this.tempData = `${h}:${min}:${sec}.${ms} -> ` + this.tempData;
|
|
||||||
}
|
|
||||||
this.lines.push(this.tempData);
|
|
||||||
this.tempData = '';
|
|
||||||
this.update();
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// TODO onError
|
// TODO onError
|
||||||
}
|
}
|
||||||
|
|
||||||
@postConstruct()
|
@postConstruct()
|
||||||
protected init(): void {
|
protected init(): void {
|
||||||
|
this.toDisposeOnDetach.pushAll([
|
||||||
|
this.serviceClient.onRead(({ data }) => {
|
||||||
|
this.chunk += data;
|
||||||
|
const eolIndex = this.chunk.indexOf('\n');
|
||||||
|
if (eolIndex !== -1) {
|
||||||
|
const line = this.chunk.substring(0, eolIndex + 1);
|
||||||
|
this.chunk = this.chunk.slice(eolIndex + 1);
|
||||||
|
this.lines.push(`${this.model.timestamp ? `${dateFormat(new Date(), 'H:M:ss.l')} -> ` : ''}${line}`);
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
this.boardsServiceClient.onBoardsConfigChanged(config => {
|
||||||
|
const { selectedBoard, selectedPort } = config;
|
||||||
|
if (selectedBoard && selectedPort) {
|
||||||
|
this.boardsService.getAttachedBoards().then(({ boards }) => {
|
||||||
|
if (boards.filter(AttachedSerialBoard.is).some(board => BoardsConfig.Config.sameAs(config, board))) {
|
||||||
|
this.connect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})]);
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
clear(): void {
|
clearConsole(): void {
|
||||||
|
this.chunk = '';
|
||||||
this.lines = [];
|
this.lines = [];
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
storeState(): MonitorModel.Data {
|
storeState(): MonitorModel.State {
|
||||||
return this.model.store();
|
return this.model.store();
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreState(oldState: MonitorModel.Data): void {
|
restoreState(oldState: MonitorModel.State): void {
|
||||||
this.model.restore(oldState);
|
this.model.restore(oldState);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onAfterAttach(msg: Message) {
|
onBeforeAttach(msg: Message): void {
|
||||||
|
super.onBeforeAttach(msg);
|
||||||
|
this.clearConsole();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onAfterAttach(msg: Message): void {
|
||||||
super.onAfterAttach(msg);
|
super.onAfterAttach(msg);
|
||||||
this.clear();
|
|
||||||
this.connect();
|
this.connect();
|
||||||
this.toDisposeOnDetach.push(
|
|
||||||
this.boardsServiceClient.onBoardsChanged(async states => {
|
|
||||||
const currentConnectionConfig = this.connection.connectionConfig;
|
|
||||||
const connectedBoard = states.newState.boards
|
|
||||||
.filter(AttachedSerialBoard.is)
|
|
||||||
.find(board => {
|
|
||||||
const potentiallyConnected = currentConnectionConfig && currentConnectionConfig.board;
|
|
||||||
if (AttachedSerialBoard.is(potentiallyConnected)) {
|
|
||||||
return Board.equals(board, potentiallyConnected) && board.port === potentiallyConnected.port;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
if (connectedBoard && currentConnectionConfig) {
|
|
||||||
this.continuePreviousConnection = true;
|
|
||||||
this.connection.connect(currentConnectionConfig);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
this.toDisposeOnDetach.push(
|
|
||||||
this.boardsServiceClient.onBoardsConfigChanged(async boardConfig => {
|
|
||||||
this.connect();
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
this.toDisposeOnDetach.push(this.connection.onConnectionChanged(() => {
|
|
||||||
if (!this.continuePreviousConnection) {
|
|
||||||
this.clear();
|
|
||||||
} else {
|
|
||||||
this.continuePreviousConnection = false;
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onBeforeDetach(msg: Message) {
|
protected onBeforeDetach(msg: Message): void {
|
||||||
super.onBeforeDetach(msg);
|
super.onBeforeDetach(msg);
|
||||||
|
if (this.connection.connectionId) {
|
||||||
this.connection.disconnect();
|
this.connection.disconnect();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected onResize(msg: Widget.ResizeMessage) {
|
protected onResize(msg: Widget.ResizeMessage): void {
|
||||||
super.onResize(msg);
|
super.onResize(msg);
|
||||||
this.widgetHeight = msg.height;
|
this.widgetHeight = msg.height;
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async connect() {
|
protected async connect(): Promise<void> {
|
||||||
const config = await this.getConnectionConfig();
|
const config = await this.getConnectionConfig();
|
||||||
if (config) {
|
if (config) {
|
||||||
this.connection.connect(config);
|
this.connection.connect(config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getConnectionConfig(): Promise<ConnectionConfig | undefined> {
|
protected async getConnectionConfig(): Promise<MonitorConfig | undefined> {
|
||||||
const baudRate = this.model.baudRate;
|
const baudRate = this.model.baudRate;
|
||||||
const { boardsConfig } = this.boardsServiceClient;
|
const { boardsConfig } = this.boardsServiceClient;
|
||||||
const { selectedBoard, selectedPort } = boardsConfig;
|
const { selectedBoard, selectedPort } = boardsConfig;
|
||||||
@ -268,11 +250,11 @@ export class MonitorWidget extends ReactWidget implements StatefulWidget {
|
|||||||
return {
|
return {
|
||||||
baudRate,
|
baudRate,
|
||||||
board: selectedBoard,
|
board: selectedBoard,
|
||||||
port: selectedPort.address
|
port: selectedPort
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getLineEndings(): OptionsType<SelectOption> {
|
protected get lineEndings(): OptionsType<SelectOption<MonitorModel.EOL>> {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'No Line Ending',
|
label: 'No Line Ending',
|
||||||
@ -293,32 +275,29 @@ export class MonitorWidget extends ReactWidget implements StatefulWidget {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getBaudRates(): OptionsType<SelectOption> {
|
protected get baudRates(): OptionsType<SelectOption<MonitorConfig.BaudRate>> {
|
||||||
const baudRates = [300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200];
|
const baudRates: Array<MonitorConfig.BaudRate> = [300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200];
|
||||||
return baudRates.map<SelectOption>(baudRate => ({ label: baudRate + ' baud', value: baudRate }))
|
return baudRates.map(baudRate => ({ label: baudRate + ' baud', value: baudRate }));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): React.ReactNode {
|
protected render(): React.ReactNode {
|
||||||
const le = this.getLineEndings();
|
const { baudRates, lineEndings } = this;
|
||||||
const br = this.getBaudRates();
|
const lineEnding = lineEndings.find(item => item.value === this.model.lineEnding) || lineEndings[1]; // Defaults to `\n`.
|
||||||
const leVal = this.model.lineEnding && le.find(val => val.value === this.model.lineEnding);
|
const baudRate = baudRates.find(item => item.value === this.model.baudRate) || baudRates[4]; // Defaults to `9600`.
|
||||||
const brVal = this.model.baudRate && br.find(val => val.value === this.model.baudRate);
|
return <div className='serial-monitor-container'>
|
||||||
return <React.Fragment>
|
|
||||||
<div className='serial-monitor-container'>
|
|
||||||
<div className='head'>
|
<div className='head'>
|
||||||
<div className='send'>
|
<div className='send'>
|
||||||
<SerialMonitorSendField onSend={this.onSend} />
|
<SerialMonitorSendField onSend={this.onSend} />
|
||||||
</div>
|
</div>
|
||||||
<div className='config'>
|
<div className='config'>
|
||||||
{this.renderSelectField('arduino-serial-monitor-line-endings', le, leVal || le[1], this.onChangeLineEnding)}
|
{this.renderSelectField('arduino-serial-monitor-line-endings', lineEndings, lineEnding, this.onChangeLineEnding)}
|
||||||
{this.renderSelectField('arduino-serial-monitor-baud-rates', br, brVal || br[4], this.onChangeBaudRate)}
|
{this.renderSelectField('arduino-serial-monitor-baud-rates', baudRates, baudRate, this.onChangeBaudRate)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id='serial-monitor-output-container'>
|
<div id='serial-monitor-output-container'>
|
||||||
<SerialMonitorOutput model={this.model} lines={this.lines} />
|
<SerialMonitorOutput model={this.model} lines={this.lines} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>;
|
||||||
</React.Fragment>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readonly onSend = (value: string) => this.doSend(value);
|
protected readonly onSend = (value: string) => this.doSend(value);
|
||||||
@ -329,40 +308,45 @@ export class MonitorWidget extends ReactWidget implements StatefulWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readonly onChangeLineEnding = (le: SelectOption) => {
|
protected readonly onChangeLineEnding = (option: SelectOption<MonitorModel.EOL>) => {
|
||||||
this.model.lineEnding = typeof le.value === 'string' ? le.value : '\n';
|
this.model.lineEnding = typeof option.value === 'string' ? option.value : MonitorModel.EOL.DEFAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readonly onChangeBaudRate = async (br: SelectOption) => {
|
protected readonly onChangeBaudRate = async (option: SelectOption<MonitorConfig.BaudRate>) => {
|
||||||
await this.connection.disconnect();
|
await this.connection.disconnect();
|
||||||
this.model.baudRate = typeof br.value === 'number' ? br.value : 9600;
|
this.model.baudRate = typeof option.value === 'number' ? option.value : MonitorConfig.BaudRate.DEFAULT;
|
||||||
this.clear();
|
this.clearConsole();
|
||||||
const config = await this.getConnectionConfig();
|
const config = await this.getConnectionConfig();
|
||||||
if (config) {
|
if (config) {
|
||||||
await this.connection.connect(config);
|
await this.connection.connect(config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected renderSelectField(id: string, options: OptionsType<SelectOption>, defaultVal: SelectOption, onChange: (v: SelectOption) => void): React.ReactNode {
|
protected renderSelectField<T>(
|
||||||
|
id: string,
|
||||||
|
options: OptionsType<SelectOption<T>>,
|
||||||
|
defaultValue: SelectOption<T>,
|
||||||
|
onChange: (option: SelectOption<T>) => void): React.ReactNode {
|
||||||
|
|
||||||
const height = 25;
|
const height = 25;
|
||||||
const selectStyles: Styles = {
|
const styles: Styles = {
|
||||||
control: (provided, state) => ({
|
control: (styles, state) => ({
|
||||||
...provided,
|
...styles,
|
||||||
width: 200,
|
width: 200,
|
||||||
border: 'none'
|
color: 'var(--theia-ui-font-color1)'
|
||||||
}),
|
}),
|
||||||
dropdownIndicator: (p, s) => ({
|
dropdownIndicator: styles => ({
|
||||||
...p,
|
...styles,
|
||||||
padding: 0
|
padding: 0
|
||||||
}),
|
}),
|
||||||
indicatorSeparator: (p, s) => ({
|
indicatorSeparator: () => ({
|
||||||
display: 'none'
|
display: 'none'
|
||||||
}),
|
}),
|
||||||
indicatorsContainer: (p, s) => ({
|
indicatorsContainer: () => ({
|
||||||
padding: '0 5px'
|
padding: '0px 5px'
|
||||||
}),
|
}),
|
||||||
menu: (p, s) => ({
|
menu: styles => ({
|
||||||
...p,
|
...styles,
|
||||||
marginTop: 0
|
marginTop: 0
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
@ -375,20 +359,18 @@ export class MonitorWidget extends ReactWidget implements StatefulWidget {
|
|||||||
menuGutter: 4
|
menuGutter: 4
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const DropdownIndicator = (
|
const DropdownIndicator = () => {
|
||||||
props: React.Props<typeof components.DropdownIndicator>
|
|
||||||
) => {
|
|
||||||
return (
|
return (
|
||||||
<span className='fa fa-caret-down caret'></span>
|
<span className='fa fa-caret-down caret'></span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
return <Select
|
return <Select
|
||||||
options={options}
|
options={options}
|
||||||
defaultValue={defaultVal}
|
defaultValue={defaultValue}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
components={{ DropdownIndicator }}
|
components={{ DropdownIndicator }}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
styles={selectStyles}
|
styles={styles}
|
||||||
maxMenuHeight={this.widgetHeight - 40}
|
maxMenuHeight={this.widgetHeight - 40}
|
||||||
classNamePrefix='sms'
|
classNamePrefix='sms'
|
||||||
className='serial-monitor-select'
|
className='serial-monitor-select'
|
||||||
|
@ -81,26 +81,28 @@
|
|||||||
|
|
||||||
/* React Select Styles */
|
/* React Select Styles */
|
||||||
.serial-monitor-select .sms__control {
|
.serial-monitor-select .sms__control {
|
||||||
border: var(--theia-border-color1) var(--theia-border-width) solid;
|
border: var(--theia-border-color3) var(--theia-border-width) solid;
|
||||||
background: var(--theia-layout-color2);
|
background: var(--theia-layout-color2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.serial-monitor-select .sms__control--is-focused {
|
.serial-monitor-select .sms__control--is-focused {
|
||||||
border-color: var(--theia-border-color2) !important;
|
border-color: var(--theia-accent-color3) !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sms__control--is-focused:hover {
|
.sms__control--is-focused:hover {
|
||||||
border-color: var(--theia-border-color2) !important;
|
border-color: var(--theia-accent-color3) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.serial-monitor-select .sms__option--is-selected {
|
.serial-monitor-select .sms__option--is-selected {
|
||||||
background-color: var(--theia-ui-button-color-secondary-hover);
|
background-color: var(--theia-ui-button-color-secondary-hover);
|
||||||
color: var(--theia-content-font-color0);
|
color: var(--theia-content-font-color0);
|
||||||
|
border-color: var(--theia-accent-color3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.serial-monitor-select .sms__option--is-focused {
|
.serial-monitor-select .sms__option--is-focused {
|
||||||
background-color: var(--theia-ui-button-color-secondary-hover);
|
background-color: var(--theia-ui-button-color-secondary-hover);
|
||||||
|
border-color: var(--theia-accent-color3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.serial-monitor-select .sms__menu {
|
.serial-monitor-select .sms__menu {
|
||||||
|
@ -214,6 +214,11 @@ export namespace Board {
|
|||||||
return !!board.fqbn;
|
return !!board.fqbn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function toString(board: Board): string {
|
||||||
|
const fqbn = board.fqbn ? ` [${board.fqbn}]` : '';
|
||||||
|
return `${board.name}${fqbn}`;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AttachedSerialBoard extends Board {
|
export interface AttachedSerialBoard extends Board {
|
||||||
|
@ -1,9 +1,27 @@
|
|||||||
import { JsonRpcServer } from '@theia/core';
|
import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory';
|
||||||
import { Board } from './boards-service';
|
import { Board, Port } from './boards-service';
|
||||||
|
|
||||||
export interface MonitorError {
|
export interface MonitorError {
|
||||||
|
readonly connectionId: string;
|
||||||
readonly message: string;
|
readonly message: string;
|
||||||
readonly code: number
|
readonly code: number;
|
||||||
|
readonly config: MonitorConfig;
|
||||||
|
}
|
||||||
|
export namespace MonitorError {
|
||||||
|
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 monitor was opened on this port. For another electron-instance, Java IDE.
|
||||||
|
*/
|
||||||
|
export const DEVICE_BUSY = 3;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MonitorReadEvent {
|
export interface MonitorReadEvent {
|
||||||
@ -20,25 +38,34 @@ export interface MonitorServiceClient {
|
|||||||
export const MonitorServicePath = '/services/serial-monitor';
|
export const MonitorServicePath = '/services/serial-monitor';
|
||||||
export const MonitorService = Symbol('MonitorService');
|
export const MonitorService = Symbol('MonitorService');
|
||||||
export interface MonitorService extends JsonRpcServer<MonitorServiceClient> {
|
export interface MonitorService extends JsonRpcServer<MonitorServiceClient> {
|
||||||
connect(config: ConnectionConfig): Promise<{ connectionId: string }>;
|
connect(config: MonitorConfig): Promise<{ connectionId: string }>;
|
||||||
disconnect(connectionId: string): Promise<boolean>;
|
disconnect(connectionId: string): Promise<boolean>;
|
||||||
send(connectionId: string, data: string | Uint8Array): Promise<void>;
|
send(connectionId: string, data: string | Uint8Array): Promise<void>;
|
||||||
getConnectionIds(): Promise<string[]>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConnectionConfig {
|
export interface MonitorConfig {
|
||||||
readonly board: Board;
|
readonly board: Board;
|
||||||
readonly port: string;
|
readonly port: Port;
|
||||||
/**
|
/**
|
||||||
* Defaults to [`SERIAL`](ConnectionType#SERIAL).
|
* Defaults to [`SERIAL`](MonitorConfig#ConnectionType#SERIAL).
|
||||||
*/
|
*/
|
||||||
readonly type?: ConnectionType;
|
readonly type?: MonitorConfig.ConnectionType;
|
||||||
/**
|
/**
|
||||||
* Defaults to `9600`.
|
* Defaults to `9600`.
|
||||||
*/
|
*/
|
||||||
readonly baudRate?: number;
|
readonly baudRate?: MonitorConfig.BaudRate;
|
||||||
|
|
||||||
|
}
|
||||||
|
export namespace MonitorConfig {
|
||||||
|
|
||||||
|
export type BaudRate = 300 | 1200 | 2400 | 4800 | 9600 | 19200 | 38400 | 57600 | 115200;
|
||||||
|
export namespace BaudRate {
|
||||||
|
export const DEFAULT: BaudRate = 9600;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ConnectionType {
|
export enum ConnectionType {
|
||||||
SERIAL = 0
|
SERIAL = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -1,40 +1,66 @@
|
|||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
import * as grpc from '@grpc/grpc-js';
|
import { Chance } from 'chance';
|
||||||
|
import { ClientDuplexStream } from '@grpc/grpc-js';
|
||||||
import { TextDecoder, TextEncoder } from 'util';
|
import { TextDecoder, TextEncoder } from 'util';
|
||||||
import { injectable, inject, named } from 'inversify';
|
import { injectable, inject, named } from 'inversify';
|
||||||
|
import { Struct } from 'google-protobuf/google/protobuf/struct_pb';
|
||||||
import { ILogger, Disposable, DisposableCollection } from '@theia/core';
|
import { ILogger, Disposable, DisposableCollection } from '@theia/core';
|
||||||
import { MonitorService, MonitorServiceClient, ConnectionConfig, ConnectionType } from '../../common/protocol/monitor-service';
|
import { MonitorService, MonitorServiceClient, MonitorConfig, MonitorError } from '../../common/protocol/monitor-service';
|
||||||
import { StreamingOpenReq, StreamingOpenResp, MonitorConfig } from '../cli-protocol/monitor/monitor_pb';
|
import { StreamingOpenReq, StreamingOpenResp, MonitorConfig as GrpcMonitorConfig } from '../cli-protocol/monitor/monitor_pb';
|
||||||
import { MonitorClientProvider } from './monitor-client-provider';
|
import { MonitorClientProvider } from './monitor-client-provider';
|
||||||
import * as google_protobuf_struct_pb from "google-protobuf/google/protobuf/struct_pb";
|
import { Board, Port } from '../../common/protocol/boards-service';
|
||||||
|
|
||||||
export interface MonitorDuplex {
|
export interface MonitorDuplex {
|
||||||
readonly toDispose: Disposable;
|
readonly toDispose: Disposable;
|
||||||
readonly duplex: grpc.ClientDuplexStream<StreamingOpenReq, StreamingOpenResp>;
|
readonly duplex: ClientDuplexStream<StreamingOpenReq, StreamingOpenResp>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrorCode = { code: number };
|
interface ErrorWithCode extends Error {
|
||||||
type MonitorError = Error & ErrorCode;
|
readonly code: number;
|
||||||
namespace MonitorError {
|
}
|
||||||
|
namespace ErrorWithCode {
|
||||||
export function is(error: Error & Partial<ErrorCode>): error is MonitorError {
|
export function is(error: Error & { code?: number }): error is ErrorWithCode {
|
||||||
return typeof error.code === 'number';
|
return typeof error.code === 'number';
|
||||||
}
|
}
|
||||||
|
export function toMonitorError(error: Error, connectionId: string, config: MonitorConfig): MonitorError | undefined {
|
||||||
/**
|
if (is(error)) {
|
||||||
* The frontend has refreshed the browser, for instance.
|
const { code, message } = error;
|
||||||
*/
|
// TODO: apply a regex on the `message`, and use enums instead of a numbers for the error codes.
|
||||||
export function isClientCancelledError(error: MonitorError): boolean {
|
if (code === 1 && message === 'Cancelled on client') {
|
||||||
return error.code === 1 && error.message === 'Cancelled on client';
|
return {
|
||||||
|
connectionId,
|
||||||
|
message,
|
||||||
|
// message: `Cancelled on client. ${Board.toString(board)} from port ${Port.toString(port)}.`,
|
||||||
|
code: MonitorError.ErrorCodes.CLIENT_CANCEL,
|
||||||
|
config
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
if (code === 2) {
|
||||||
/**
|
switch (message) {
|
||||||
* When detaching a physical device when the duplex channel is still opened.
|
case 'device not configured': {
|
||||||
*/
|
return {
|
||||||
export function isDeviceNotConfiguredError(error: MonitorError): boolean {
|
connectionId,
|
||||||
return error.code === 2 && error.message === 'device not configured';
|
// message: ``,
|
||||||
|
message,
|
||||||
|
code: MonitorError.ErrorCodes.DEVICE_NOT_CONFIGURED,
|
||||||
|
config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'error opening serial monitor: Serial port busy': {
|
||||||
|
return {
|
||||||
|
connectionId,
|
||||||
|
// message: `Connection failed. Serial port is busy: ${Port.toString(port)}.`,
|
||||||
|
message,
|
||||||
|
code: MonitorError.ErrorCodes.DEVICE_BUSY,
|
||||||
|
config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.warn(`Unhandled error with code:`, error);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
@ -60,29 +86,33 @@ export class MonitorServiceImpl implements MonitorService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getConnectionIds(): Promise<string[]> {
|
async connect(config: MonitorConfig): Promise<{ connectionId: string }> {
|
||||||
return Array.from(this.connections.keys());
|
this.logger.info(`>>> Creating serial monitor connection for ${Board.toString(config.board)} on port ${Port.toString(config.port)}...`);
|
||||||
}
|
|
||||||
|
|
||||||
async connect(config: ConnectionConfig): Promise<{ connectionId: string }> {
|
|
||||||
const client = await this.monitorClientProvider.client;
|
const client = await this.monitorClientProvider.client;
|
||||||
const duplex = client.streamingOpen();
|
const duplex = client.streamingOpen();
|
||||||
const connectionId = v4();
|
const connectionId = `${new Chance(v4()).animal().replace(/\s+/g, '-').toLowerCase()}-monitor-connection`;
|
||||||
const toDispose = new DisposableCollection(
|
const toDispose = new DisposableCollection(
|
||||||
Disposable.create(() => this.disconnect(connectionId))
|
Disposable.create(() => this.disconnect(connectionId))
|
||||||
);
|
);
|
||||||
|
|
||||||
duplex.on('error', ((error: Error) => {
|
duplex.on('error', ((error: Error) => {
|
||||||
if (MonitorError.is(error) && (
|
// Dispose the connection on error.
|
||||||
MonitorError.isClientCancelledError(error)
|
// If the client has disconnected, we call `disconnect` and hit this error
|
||||||
|| MonitorError.isDeviceNotConfiguredError(error)
|
// no need to disconnect once more.
|
||||||
)) {
|
if (!toDispose.disposed) {
|
||||||
|
toDispose.dispose();
|
||||||
|
}
|
||||||
|
if (ErrorWithCode.is(error)) {
|
||||||
|
const monitorError = ErrorWithCode.toMonitorError(error, connectionId, config);
|
||||||
|
if (monitorError) {
|
||||||
if (this.client) {
|
if (this.client) {
|
||||||
this.client.notifyError(error);
|
this.client.notifyError(monitorError);
|
||||||
|
}
|
||||||
|
// Do not log the error, it was expected. The client will take care of the rest.
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.logger.error(`Error occurred for connection ${connectionId}.`, error);
|
this.logger.error(`Error occurred for connection ${connectionId}.`, error);
|
||||||
toDispose.dispose();
|
|
||||||
}).bind(this));
|
}).bind(this));
|
||||||
|
|
||||||
duplex.on('data', ((resp: StreamingOpenResp) => {
|
duplex.on('data', ((resp: StreamingOpenResp) => {
|
||||||
@ -95,18 +125,18 @@ export class MonitorServiceImpl implements MonitorService {
|
|||||||
|
|
||||||
const { type, port } = config;
|
const { type, port } = config;
|
||||||
const req = new StreamingOpenReq();
|
const req = new StreamingOpenReq();
|
||||||
const monitorConfig = new MonitorConfig();
|
const monitorConfig = new GrpcMonitorConfig();
|
||||||
monitorConfig.setType(this.mapType(type));
|
monitorConfig.setType(this.mapType(type));
|
||||||
monitorConfig.setTarget(port);
|
monitorConfig.setTarget(port.address);
|
||||||
if (config.baudRate !== undefined) {
|
if (config.baudRate !== undefined) {
|
||||||
const obj = google_protobuf_struct_pb.Struct.fromJavaScript({ 'BaudRate': config.baudRate });
|
monitorConfig.setAdditionalconfig(Struct.fromJavaScript({ 'BaudRate': config.baudRate }));
|
||||||
monitorConfig.setAdditionalconfig(obj);
|
|
||||||
}
|
}
|
||||||
req.setMonitorconfig(monitorConfig);
|
req.setMonitorconfig(monitorConfig);
|
||||||
|
|
||||||
return new Promise<{ connectionId: string }>(resolve => {
|
return new Promise<{ connectionId: string }>(resolve => {
|
||||||
duplex.write(req, () => {
|
duplex.write(req, () => {
|
||||||
this.connections.set(connectionId, { toDispose, duplex });
|
this.connections.set(connectionId, { toDispose, duplex });
|
||||||
|
this.logger.info(`<<< Serial monitor connection created for ${Board.toString(config.board)} on port ${Port.toString(config.port)}. ID: [${connectionId}]`);
|
||||||
resolve({ connectionId });
|
resolve({ connectionId });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -153,10 +183,10 @@ export class MonitorServiceImpl implements MonitorService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected mapType(type?: ConnectionType): MonitorConfig.TargetType {
|
protected mapType(type?: MonitorConfig.ConnectionType): GrpcMonitorConfig.TargetType {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case ConnectionType.SERIAL: return MonitorConfig.TargetType.SERIAL;
|
case MonitorConfig.ConnectionType.SERIAL: return GrpcMonitorConfig.TargetType.SERIAL;
|
||||||
default: return MonitorConfig.TargetType.SERIAL;
|
default: return GrpcMonitorConfig.TargetType.SERIAL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
"main": "src-gen/frontend/electron-main.js",
|
"main": "src-gen/frontend/electron-main.js",
|
||||||
"author": "Arduino SA",
|
"author": "Arduino SA",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"google-protobuf": "^3.5.0",
|
|
||||||
"arduino-ide-extension": "file:../working-copy/arduino-ide-extension"
|
"arduino-ide-extension": "file:../working-copy/arduino-ide-extension"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
@ -27,7 +26,6 @@
|
|||||||
"url": "git+https://github.com/arduino/arduino-pro-ide.git"
|
"url": "git+https://github.com/arduino/arduino-pro-ide.git"
|
||||||
},
|
},
|
||||||
"// Notes:": [
|
"// Notes:": [
|
||||||
"`google-protobuf` was declared as it is not picked up by the `electron-builder` as a runtime dependency.",
|
|
||||||
"The resolution for `fs-extra` was required due to this: https://spectrum.chat/theia/general/our-theia-electron-builder-app-no-longer-starts~f5cf09a0-6d88-448b-8818-24ad0ec2ee7c"
|
"The resolution for `fs-extra` was required due to this: https://spectrum.chat/theia/general/our-theia-electron-builder-app-no-longer-starts~f5cf09a0-6d88-448b-8818-24ad0ec2ee7c"
|
||||||
],
|
],
|
||||||
"build": {
|
"build": {
|
||||||
|
20
yarn.lock
20
yarn.lock
@ -2145,6 +2145,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8"
|
resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8"
|
||||||
integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==
|
integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==
|
||||||
|
|
||||||
|
"@types/chance@1.0.7":
|
||||||
|
version "1.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/chance/-/chance-1.0.7.tgz#c680a3891a505d8c626ec3a46bb1c0496419dfb6"
|
||||||
|
integrity sha512-LBOkJ7899SSLm08KicLYX3DqWUhfDspMLWNGuV1UPpL3iUENSvI0THGlf05n9yNHTR7zDlV/mCGZ7ZJ0ws8v3Q==
|
||||||
|
|
||||||
"@types/connect@*":
|
"@types/connect@*":
|
||||||
version "3.4.32"
|
version "3.4.32"
|
||||||
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.32.tgz#aa0e9616b9435ccad02bc52b5b454ffc2c70ba28"
|
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.32.tgz#aa0e9616b9435ccad02bc52b5b454ffc2c70ba28"
|
||||||
@ -2152,6 +2157,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/dateformat@^3.0.1":
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/dateformat/-/dateformat-3.0.1.tgz#98d747a2e5e9a56070c6bf14e27bff56204e34cc"
|
||||||
|
integrity sha512-KlPPdikagvL6ELjWsljbyDIPzNCeliYkqRpI+zea99vBBbCIA5JNshZAwQKTON139c87y9qvTFVgkFd14rtS4g==
|
||||||
|
|
||||||
"@types/diff@^3.2.2":
|
"@types/diff@^3.2.2":
|
||||||
version "3.5.3"
|
version "3.5.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/diff/-/diff-3.5.3.tgz#7c6c3721ba454d838790100faf7957116ee7deab"
|
resolved "https://registry.yarnpkg.com/@types/diff/-/diff-3.5.3.tgz#7c6c3721ba454d838790100faf7957116ee7deab"
|
||||||
@ -4429,6 +4439,11 @@ chalk@~0.4.0:
|
|||||||
has-color "~0.1.0"
|
has-color "~0.1.0"
|
||||||
strip-ansi "~0.1.0"
|
strip-ansi "~0.1.0"
|
||||||
|
|
||||||
|
chance@^1.1.3:
|
||||||
|
version "1.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/chance/-/chance-1.1.3.tgz#414f08634ee479c7a316b569050ea20751b82dd3"
|
||||||
|
integrity sha512-XeJsdoVAzDb1WRPRuMBesRSiWpW1uNTo5Fd7mYxPJsAfgX71+jfuCOHOdbyBz2uAUZ8TwKcXgWk3DMedFfJkbg==
|
||||||
|
|
||||||
changes-stream@^2.2.0:
|
changes-stream@^2.2.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/changes-stream/-/changes-stream-2.2.0.tgz#9cf2bdbc2173c29c634aec9948e5d23b24d37c18"
|
resolved "https://registry.yarnpkg.com/changes-stream/-/changes-stream-2.2.0.tgz#9cf2bdbc2173c29c634aec9948e5d23b24d37c18"
|
||||||
@ -6965,6 +6980,11 @@ google-protobuf@3.5.0:
|
|||||||
resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.5.0.tgz#b8cc63c74d83457bd8a9a904503c8efb26bca339"
|
resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.5.0.tgz#b8cc63c74d83457bd8a9a904503c8efb26bca339"
|
||||||
integrity sha1-uMxjx02DRXvYqakEUDyO+ya8ozk=
|
integrity sha1-uMxjx02DRXvYqakEUDyO+ya8ozk=
|
||||||
|
|
||||||
|
google-protobuf@^3.11.0:
|
||||||
|
version "3.11.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.11.0.tgz#9449d6029569aa4603fdaa154ec6a7681aea0787"
|
||||||
|
integrity sha512-Pni0ZGKZc/FpaRiz2n11+FfjzeuoUpnMdtuqoBUandjsEPsLDLh5C0yacl/bgNAFaZkyUgIGcGowW34u6mVWPw==
|
||||||
|
|
||||||
got@^7.0.0:
|
got@^7.0.0:
|
||||||
version "7.1.0"
|
version "7.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/got/-/got-7.1.0.tgz#05450fd84094e6bbea56f451a43a9c289166385a"
|
resolved "https://registry.yarnpkg.com/got/-/got-7.1.0.tgz#05450fd84094e6bbea56f451a43a9c289166385a"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user