Merge pull request #54 from bcmi-labs/monitor

Serial Monitor
This commit is contained in:
Jan Bicker 2019-09-06 13:06:56 +02:00 committed by GitHub
commit 3e0842e93a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1089 additions and 1738 deletions

View File

@ -23,7 +23,10 @@
"@theia/search-in-workspace": "next",
"@types/ps-tree": "^1.1.0",
"@types/which": "^1.3.1",
"@types/react-select": "^3.0.0",
"@types/google-protobuf": "^3.7.1",
"css-element-queries": "^1.2.0",
"react-select": "^3.0.4",
"p-queue": "^5.0.0",
"ps-tree": "^1.2.0",
"tree-kill": "^1.2.1",

View File

@ -42,15 +42,4 @@ export namespace ArduinoCommands {
export const TOGGLE_PRO_MODE: Command = {
id: "arduino-toggle-pro-mode"
}
export const CONNECT_TODO: Command = {
id: 'connect-to-attached-board',
label: 'Connect to Attached Board'
}
export const SEND: Command = {
id: 'send',
label: 'Send a Message to the Connected Board'
}
}

View File

@ -5,7 +5,7 @@ import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
import { MessageService } from '@theia/core/lib/common/message-service';
import { CommandContribution, CommandRegistry, Command } from '@theia/core/lib/common/command';
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { BoardsService, AttachedSerialBoard } from '../common/protocol/boards-service';
import { BoardsService } from '../common/protocol/boards-service';
import { ArduinoCommands } from './arduino-commands';
import { CoreService } from '../common/protocol/core-service';
import { WorkspaceServiceExt } from './workspace-service-ext';
@ -26,8 +26,6 @@ import {
StatusBar,
ShellLayoutRestorer,
StatusBarAlignment,
QuickOpenItem,
QuickOpenMode,
QuickOpenService,
LabelProvider
} from '@theia/core/lib/browser';
@ -47,6 +45,8 @@ import { BoardsToolBarItem } from './boards/boards-toolbar-item';
import { BoardsConfig } from './boards/boards-config';
import { MonitorService } from '../common/protocol/monitor-service';
import { ConfigService } from '../common/protocol/config-service';
import { MonitorConnection } from './monitor/monitor-connection';
import { MonitorViewContribution } from './monitor/monitor-view-contribution';
export namespace ArduinoMenus {
export const SKETCH = [...MAIN_MENU_BAR, '3_sketch'];
@ -72,9 +72,6 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
@inject(MonitorService)
protected readonly monitorService: MonitorService;
// TODO: make this better!
protected connectionId: string | undefined;
@inject(WorkspaceServiceExt)
protected readonly workspaceServiceExt: WorkspaceServiceExt;
@ -143,6 +140,8 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(MonitorConnection)
protected readonly monitorConnection: MonitorConnection;
protected boardsToolbarItem: BoardsToolBarItem | null;
protected wsSketchCount: number = 0;
@ -197,13 +196,19 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
commands={this.commands}
boardsServiceClient={this.boardsServiceClient}
boardService={this.boardsService} />,
isVisible: widget => this.isArduinoToolbar(widget)
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left'
});
registry.registerItem({
id: 'toggle-serial-monitor',
command: MonitorViewContribution.OPEN_SERIAL_MONITOR,
tooltip: 'Toggle Serial Monitor',
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'right'
})
}
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(ArduinoCommands.VERIFY, {
isVisible: widget => this.isArduinoToolbar(widget),
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
isEnabled: widget => true,
execute: async () => {
const widget = this.getCurrentWidget();
@ -231,7 +236,7 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
}
});
registry.registerCommand(ArduinoCommands.UPLOAD, {
isVisible: widget => this.isArduinoToolbar(widget),
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
isEnabled: widget => true,
execute: async () => {
const widget = this.getCurrentWidget();
@ -244,6 +249,9 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
return;
}
const connectionConfig = this.monitorConnection.connectionConfig;
await this.monitorConnection.disconnect();
try {
const { boardsConfig } = this.boardsServiceClient;
if (!boardsConfig || !boardsConfig.selectedBoard) {
@ -256,12 +264,16 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
await this.coreService.upload({ uri: uri.toString(), board: boardsConfig.selectedBoard, port: selectedPort });
} catch (e) {
await this.messageService.error(e.toString());
} finally {
if (connectionConfig) {
await this.monitorConnection.connect(connectionConfig);
}
}
}
});
registry.registerCommand(ArduinoCommands.SHOW_OPEN_CONTEXT_MENU, {
isVisible: widget => this.isArduinoToolbar(widget),
isEnabled: widget => this.isArduinoToolbar(widget),
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
isEnabled: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
execute: async (widget: Widget, target: EventTarget) => {
if (this.wsSketchCount) {
const el = (target as HTMLElement).parentElement;
@ -287,8 +299,8 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
}
})
registry.registerCommand(ArduinoCommands.SAVE_SKETCH, {
isEnabled: widget => this.isArduinoToolbar(widget),
isVisible: widget => this.isArduinoToolbar(widget),
isEnabled: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
execute: async (sketch: Sketch) => {
registry.executeCommand(CommonCommands.SAVE_ALL.id);
}
@ -324,65 +336,6 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
},
isToggled: () => ARDUINO_PRO_MODE
});
registry.registerCommand(ArduinoCommands.CONNECT_TODO, {
execute: async () => {
const { boardsConfig } = this.boardsServiceClient;
const { selectedBoard, selectedPort } = boardsConfig;
if (!selectedBoard) {
this.messageService.warn('No boards selected.');
return;
}
const { name } = selectedBoard;
if (!selectedPort) {
this.messageService.warn(`No ports selected for board: '${name}'.`);
return;
}
const attachedBoards = await this.boardsService.getAttachedBoards();
const connectedBoard = attachedBoards.boards.filter(AttachedSerialBoard.is).find(board => BoardsConfig.Config.sameAs(boardsConfig, board));
if (!connectedBoard) {
this.messageService.warn(`The selected '${name}' board is not connected on ${selectedPort}.`);
return;
}
if (this.connectionId) {
console.log('>>> Disposing existing monitor connection before establishing a new one...');
const result = await this.monitorService.disconnect(this.connectionId);
if (!result) {
// TODO: better!!!
console.error(`Could not close connection: ${this.connectionId}. Check the backend logs.`);
} else {
console.log(`<<< Disposed ${this.connectionId} connection.`)
}
}
const { connectionId } = await this.monitorService.connect({ board: selectedBoard, port: selectedPort });
this.connectionId = connectionId;
}
});
registry.registerCommand(ArduinoCommands.SEND, {
isEnabled: () => !!this.connectionId,
execute: async () => {
const { monitorService, connectionId } = this;
const model = {
onType(lookFor: string, acceptor: (items: QuickOpenItem[]) => void): void {
acceptor([
new QuickOpenItem({
label: "Type your message and press 'Enter' to send it to the board. Escape to cancel.",
run: (mode: QuickOpenMode): boolean => {
if (mode !== QuickOpenMode.OPEN) {
return false;
}
monitorService.send(connectionId!, lookFor + '\n');
return true;
}
})
]);
}
};
const options = {
placeholder: "Your message. The message will be suffixed with a LF ['\\n'].",
};
this.quickOpenService.open(model, options);
}
})
}
registerMenus(registry: MenuModelRegistry) {
@ -555,13 +508,6 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
return undefined;
}
private isArduinoToolbar(maybeToolbarWidget: any): boolean {
if (maybeToolbarWidget instanceof ArduinoToolbar) {
return true;
}
return false;
}
private toUri(arg: any): URI | undefined {
if (arg instanceof URI) {
return arg;

View File

@ -57,12 +57,12 @@ import { BoardItemRenderer } from './boards/boards-item-renderer';
import { MonitorServiceClientImpl } from './monitor/monitor-service-client-impl';
import { MonitorServicePath, MonitorService, MonitorServiceClient } from '../common/protocol/monitor-service';
import { ConfigService, ConfigServicePath } from '../common/protocol/config-service';
import { MonitorWidget } from './monitor/monitor-widget';
import { MonitorViewContribution } from './monitor/monitor-view-contribution';
import { MonitorConnection } from './monitor/monitor-connection';
import { MonitorModel } from './monitor/monitor-model';
const ElementQueries = require('css-element-queries/src/ElementQueries');
if (!ARDUINO_PRO_MODE) {
require('../../src/browser/style/silent-bottom-panel-tabs.css');
}
export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind) => {
ElementQueries.listen();
ElementQueries.init();
@ -155,12 +155,23 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
return workspaceServiceExt;
});
// Serial Monitor
bind(MonitorModel).toSelf().inSingletonScope();
bind(MonitorWidget).toSelf();
bindViewContribution(bind, MonitorViewContribution);
bind(TabBarToolbarContribution).toService(MonitorViewContribution);
bind(WidgetFactory).toDynamicValue(context => ({
id: MonitorWidget.ID,
createWidget: () => context.container.get(MonitorWidget)
}));
// Frontend binding for the monitor service.
bind(MonitorService).toDynamicValue(context => {
const connection = context.container.get(WebSocketConnectionProvider);
const client = context.container.get(MonitorServiceClientImpl);
return connection.createProxy(MonitorServicePath, client);
}).inSingletonScope();
// MonitorConnection
bind(MonitorConnection).toSelf().inSingletonScope();
// Monitor service client to receive and delegate notifications from the backend.
bind(MonitorServiceClientImpl).toSelf().inSingletonScope();
bind(MonitorServiceClient).toDynamicValue(context => {

View File

@ -0,0 +1,53 @@
import { injectable, inject } from "inversify";
import { MonitorService, ConnectionConfig } from "../../common/protocol/monitor-service";
import { Emitter, Event } from "@theia/core";
@injectable()
export class MonitorConnection {
@inject(MonitorService)
protected readonly monitorService: MonitorService;
connectionId: string | undefined;
protected _connectionConfig: ConnectionConfig | undefined;
protected readonly onConnectionChangedEmitter = new Emitter<string | undefined>();
readonly onConnectionChanged: Event<string | undefined> = this.onConnectionChangedEmitter.event;
get connectionConfig(): ConnectionConfig | undefined {
return this._connectionConfig;
}
async connect(config: ConnectionConfig): Promise<string | undefined> {
if (this.connectionId) {
await this.disconnect();
}
const { connectionId } = await this.monitorService.connect(config);
this.connectionId = connectionId;
this._connectionConfig = config;
this.onConnectionChangedEmitter.fire(this.connectionId);
return connectionId;
}
async disconnect(): Promise<boolean> {
let result = true;
const connections = await this.monitorService.getConnectionIds();
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);
}
}
return result;
}
}

View File

@ -0,0 +1,58 @@
import { injectable } from "inversify";
import { Emitter } from "@theia/core";
export namespace MonitorModel {
export interface Data {
autoscroll: boolean,
timestamp: boolean,
baudRate: number,
lineEnding: string
}
}
@injectable()
export class MonitorModel {
protected readonly onChangeEmitter = new Emitter<void>();
readonly onChange = this.onChangeEmitter.event;
protected _autoscroll: boolean = true;
protected _timestamp: boolean = false;
baudRate: number;
lineEnding: string = '\n';
get autoscroll(): boolean {
return this._autoscroll;
}
get timestamp(): boolean {
return this._timestamp;
}
toggleAutoscroll(): void {
this._autoscroll = !this._autoscroll;
this.onChangeEmitter.fire(undefined);
}
toggleTimestamp(): void {
this._timestamp = !this._timestamp;
this.onChangeEmitter.fire(undefined);
}
restore(model: MonitorModel.Data) {
this._autoscroll = model.autoscroll;
this._timestamp = model.timestamp;
this.baudRate = model.baudRate;
this.lineEnding = model.lineEnding;
}
store(): MonitorModel.Data {
return {
autoscroll: this._autoscroll,
timestamp: this._timestamp,
baudRate: this.baudRate,
lineEnding: this.lineEnding
}
}
}

View File

@ -0,0 +1,127 @@
import * as React from 'react';
import { injectable, inject } from "inversify";
import { AbstractViewContribution } from "@theia/core/lib/browser";
import { MonitorWidget } from "./monitor-widget";
import { MenuModelRegistry, Command, CommandRegistry } from "@theia/core";
import { ArduinoMenus } from "../arduino-frontend-contribution";
import { TabBarToolbarContribution, TabBarToolbarRegistry } from "@theia/core/lib/browser/shell/tab-bar-toolbar";
import { MonitorModel } from './monitor-model';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
export namespace SerialMonitor {
export namespace Commands {
export const AUTOSCROLL: Command = {
id: 'serial-monitor-autoscroll',
label: 'Autoscroll'
}
export const TIMESTAMP: Command = {
id: 'serial-monitor-timestamp',
label: 'Timestamp'
}
export const CLEAR_OUTPUT: Command = {
id: 'serial-monitor-clear-output',
label: 'Clear Output',
iconClass: 'clear-all'
}
}
}
@injectable()
export class MonitorViewContribution extends AbstractViewContribution<MonitorWidget> implements TabBarToolbarContribution {
static readonly OPEN_SERIAL_MONITOR = MonitorWidget.ID + ':toggle';
@inject(MonitorModel) protected readonly model: MonitorModel;
constructor() {
super({
widgetId: MonitorWidget.ID,
widgetName: 'Serial Monitor',
defaultWidgetOptions: {
area: 'bottom'
},
toggleCommandId: MonitorViewContribution.OPEN_SERIAL_MONITOR,
toggleKeybinding: 'ctrl+shift+m'
})
}
registerMenus(menus: MenuModelRegistry): void {
if (this.toggleCommand) {
menus.registerMenuAction(ArduinoMenus.TOOLS, {
commandId: this.toggleCommand.id,
label: 'Serial Monitor'
});
}
}
async registerToolbarItems(registry: TabBarToolbarRegistry) {
registry.registerItem({
id: 'monitor-autoscroll',
render: () => this.renderAutoScrollButton(),
isVisible: widget => widget instanceof MonitorWidget,
onDidChange: this.model.onChange
});
registry.registerItem({
id: 'monitor-timestamp',
render: () => this.renderTimestampButton(),
isVisible: widget => widget instanceof MonitorWidget,
onDidChange: this.model.onChange
});
registry.registerItem({
id: SerialMonitor.Commands.CLEAR_OUTPUT.id,
command: SerialMonitor.Commands.CLEAR_OUTPUT.id,
tooltip: 'Clear Output'
});
}
registerCommands(commands: CommandRegistry): void {
commands.registerCommand(SerialMonitor.Commands.CLEAR_OUTPUT, {
isEnabled: widget => widget instanceof MonitorWidget,
isVisible: widget => widget instanceof MonitorWidget,
execute: widget => {
if (widget instanceof MonitorWidget) {
widget.clear();
}
}
});
if (this.toggleCommand) {
commands.registerCommand(this.toggleCommand, {
execute: () => this.openView({
toggle: true,
activate: true
}),
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'right'
});
}
}
protected renderAutoScrollButton(): React.ReactNode {
return <React.Fragment key='autoscroll-toolbar-item'>
<div
title='Toggle Autoscroll'
className={`item enabled fa fa-angle-double-down arduino-monitor ${this.model.autoscroll ? 'toggled' : ''}`}
onClick={this.toggleAutoScroll}
></div>
</React.Fragment>;
}
protected readonly toggleAutoScroll = () => this.doToggleAutoScroll();
protected async doToggleAutoScroll() {
this.model.toggleAutoscroll();
}
protected renderTimestampButton(): React.ReactNode {
return <React.Fragment key='line-ending-toolbar-item'>
<div
title='Toggle Timestamp'
className={`item enabled fa fa-clock-o arduino-monitor ${this.model.timestamp ? 'toggled' : ''}`}
onClick={this.toggleTimestamp}
></div>
</React.Fragment>;
}
protected readonly toggleTimestamp = () => this.doToggleTimestamp();
protected async doToggleTimestamp() {
this.model.toggleTimestamp();
}
}

View File

@ -0,0 +1,398 @@
import { ReactWidget, Message, Widget, StatefulWidget } from "@theia/core/lib/browser";
import { postConstruct, injectable, inject } from "inversify";
import * as React from 'react';
import Select, { components } from 'react-select';
import { Styles } from "react-select/src/styles";
import { ThemeConfig } from "react-select/src/theme";
import { OptionsType } from "react-select/src/types";
import { MonitorServiceClientImpl } from "./monitor-service-client-impl";
import { MessageService } from "@theia/core";
import { ConnectionConfig, MonitorService } from "../../common/protocol/monitor-service";
import { MonitorConnection } from "./monitor-connection";
import { BoardsServiceClientImpl } from "../boards/boards-service-client-impl";
import { AttachedSerialBoard, BoardsService, Board } from "../../common/protocol/boards-service";
import { BoardsConfig } from "../boards/boards-config";
import { MonitorModel } from "./monitor-model";
export namespace SerialMonitorSendField {
export interface Props {
onSend: (text: string) => void
}
export interface State {
value: string;
}
}
export class SerialMonitorSendField extends React.Component<SerialMonitorSendField.Props, SerialMonitorSendField.State> {
protected inputField: HTMLInputElement | null;
constructor(props: SerialMonitorSendField.Props) {
super(props);
this.state = { value: '' };
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
componentDidMount() {
if (this.inputField) {
this.inputField.focus();
}
}
render() {
return <React.Fragment>
<form onSubmit={this.handleSubmit}>
<input
tabIndex={-1}
ref={ref => this.inputField = ref}
type='text' id='serial-monitor-send'
autoComplete='off'
value={this.state.value}
onChange={this.handleChange} />
<input className="btn" type="submit" value="Submit" />
</form>
</React.Fragment>
}
protected handleChange(event: React.ChangeEvent<HTMLInputElement>) {
this.setState({ value: event.target.value });
}
protected handleSubmit(event: React.FormEvent<HTMLFormElement>) {
this.props.onSend(this.state.value);
this.setState({ value: '' });
event.preventDefault();
}
}
export namespace SerialMonitorOutput {
export interface Props {
lines: string[];
model: MonitorModel;
}
}
export class SerialMonitorOutput extends React.Component<SerialMonitorOutput.Props> {
protected theEnd: HTMLDivElement | null;
render() {
let result = '';
const style: React.CSSProperties = {
whiteSpace: 'pre',
fontFamily: 'monospace',
};
for (const text of this.props.lines) {
result += text;
}
return <React.Fragment>
<div style={style}>{result}</div>
<div style={{ float: "left", clear: "both" }}
ref={(el) => { this.theEnd = el; }}>
</div>
</React.Fragment>;
}
protected scrollToBottom() {
if (this.theEnd) {
this.theEnd.scrollIntoView();
}
}
componentDidMount() {
if (this.props.model.autoscroll) {
this.scrollToBottom();
}
}
componentDidUpdate() {
if (this.props.model.autoscroll) {
this.scrollToBottom();
}
}
}
export interface SelectOption {
label: string;
value: string | number;
}
@injectable()
export class MonitorWidget extends ReactWidget implements StatefulWidget {
static readonly ID = 'serial-monitor';
protected lines: string[];
protected tempData: string;
protected widgetHeight: number;
protected continuePreviousConnection: boolean;
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();
this.id = MonitorWidget.ID;
this.title.label = 'Serial Monitor';
this.title.iconClass = 'arduino-serial-monitor-tab-icon';
this.lines = [];
this.tempData = '';
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
}
@postConstruct()
protected init(): void {
this.update();
}
clear(): void {
this.lines = [];
this.update();
}
storeState(): MonitorModel.Data {
return this.model.store();
}
restoreState(oldState: MonitorModel.Data): void {
this.model.restore(oldState);
}
protected onAfterAttach(msg: Message) {
super.onAfterAttach(msg);
this.clear();
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) {
super.onBeforeDetach(msg);
this.connection.disconnect();
}
protected onResize(msg: Widget.ResizeMessage) {
super.onResize(msg);
this.widgetHeight = msg.height;
this.update();
}
protected async connect() {
const config = await this.getConnectionConfig();
if (config) {
this.connection.connect(config);
}
}
protected async getConnectionConfig(): Promise<ConnectionConfig | undefined> {
const baudRate = this.model.baudRate;
const { boardsConfig } = this.boardsServiceClient;
const { selectedBoard, selectedPort } = boardsConfig;
if (!selectedBoard) {
this.messageService.warn('No boards selected.');
return;
}
const { name } = selectedBoard;
if (!selectedPort) {
this.messageService.warn(`No ports selected for board: '${name}'.`);
return;
}
const attachedBoards = await this.boardsService.getAttachedBoards();
const connectedBoard = attachedBoards.boards.filter(AttachedSerialBoard.is).find(board => BoardsConfig.Config.sameAs(boardsConfig, board));
if (!connectedBoard) {
return;
}
return {
baudRate,
board: selectedBoard,
port: selectedPort
}
}
protected getLineEndings(): OptionsType<SelectOption> {
return [
{
label: 'No Line Ending',
value: ''
},
{
label: 'Newline',
value: '\n'
},
{
label: 'Carriage Return',
value: '\r'
},
{
label: 'Both NL & CR',
value: '\r\n'
}
]
}
protected getBaudRates(): OptionsType<SelectOption> {
const baudRates = [300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200];
return baudRates.map<SelectOption>(baudRate => ({ label: baudRate + ' baud', value: baudRate }))
}
protected render(): React.ReactNode {
const le = this.getLineEndings();
const br = this.getBaudRates();
const leVal = this.model.lineEnding && le.find(val => val.value === this.model.lineEnding);
const brVal = this.model.baudRate && br.find(val => val.value === this.model.baudRate);
return <React.Fragment>
<div className='serial-monitor-container'>
<div className='head'>
<div className='send'>
<SerialMonitorSendField onSend={this.onSend} />
</div>
<div className='config'>
{this.renderSelectField('arduino-serial-monitor-line-endings', le, leVal || le[1], this.onChangeLineEnding)}
{this.renderSelectField('arduino-serial-monitor-baud-rates', br, brVal || br[4], this.onChangeBaudRate)}
</div>
</div>
<div id='serial-monitor-output-container'>
<SerialMonitorOutput model={this.model} lines={this.lines} />
</div>
</div>
</React.Fragment>;
}
protected readonly onSend = (value: string) => this.doSend(value);
protected async doSend(value: string) {
const { connectionId } = this.connection;
if (connectionId) {
this.monitorService.send(connectionId, value + this.model.lineEnding);
}
}
protected readonly onChangeLineEnding = (le: SelectOption) => {
this.model.lineEnding = typeof le.value === 'string' ? le.value : '\n';
}
protected readonly onChangeBaudRate = async (br: SelectOption) => {
await this.connection.disconnect();
this.model.baudRate = typeof br.value === 'number' ? br.value : 9600;
this.clear();
const config = await this.getConnectionConfig();
if (config) {
await this.connection.connect(config);
}
}
protected renderSelectField(id: string, options: OptionsType<SelectOption>, defaultVal: SelectOption, onChange: (v: SelectOption) => void): React.ReactNode {
const height = 25;
const selectStyles: Styles = {
control: (provided, state) => ({
...provided,
width: 200,
border: 'none'
}),
dropdownIndicator: (p, s) => ({
...p,
padding: 0
}),
indicatorSeparator: (p, s) => ({
display: 'none'
}),
indicatorsContainer: (p, s) => ({
padding: '0 5px'
}),
menu: (p, s) => ({
...p,
marginTop: 0
})
};
const theme: ThemeConfig = theme => ({
...theme,
borderRadius: 0,
spacing: {
controlHeight: height,
baseUnit: 2,
menuGutter: 4
}
});
const DropdownIndicator = (
props: React.Props<typeof components.DropdownIndicator>
) => {
return (
<span className='fa fa-caret-down caret'></span>
);
};
return <Select
options={options}
defaultValue={defaultVal}
onChange={onChange}
components={{ DropdownIndicator }}
theme={theme}
styles={selectStyles}
maxMenuHeight={this.widgetHeight - 40}
classNamePrefix='sms'
className='serial-monitor-select'
id={id}
/>
}
}

View File

@ -196,6 +196,7 @@ button.theia-button.main {
background: #f7f7f7;
border: 3px solid var(--theia-border-color2);
margin: -3px;
z-index: 1000;
}
.arduino-boards-dropdown-item {

View File

@ -2,3 +2,4 @@
@import './board-select-dialog.css';
@import './main.css';
@import './editor.css';
@import './serial-monitor.css';

View File

@ -11,6 +11,7 @@
cursor: text;
}
#toggle-serial-monitor.arduino-tool-icon:hover,
#arduino-verify.arduino-tool-icon:hover,
#arduino-save-file.arduino-tool-icon:hover,
#arduino-show-open-context-menu.arduino-tool-icon:hover,
@ -18,6 +19,7 @@
background-position-y: 60px;
}
#toggle-serial-monitor.arduino-tool-icon,
#arduino-verify.arduino-tool-icon,
#arduino-save-file.arduino-tool-icon,
#arduino-show-open-context-menu.arduino-tool-icon,
@ -54,10 +56,36 @@
background-position-x: 92px;
}
#toggle-serial-monitor {
background: url(../icons/buttons.svg);
background-size: 800%;
background-position-y: 28px;
background-position-x: 28px;
}
.p-TabBar-toolbar .item.arduino-tool-item {
margin-left: 3px;
}
#arduino-toolbar-container {
display: flex;
width: 100%;
}
.p-TabBar-toolbar.theia-arduino-toolbar {
flex: 1;
}
#theia-top-panel .p-TabBar-toolbar.theia-arduino-toolbar.right {
justify-content: flex-start;
min-width: 190px;
}
#theia-top-panel .p-TabBar-toolbar.theia-arduino-toolbar.left {
min-width: 398px;
justify-content: flex-end;
}
.arduino-tool-item.item.connected-boards {
opacity: 1;
}

View File

@ -0,0 +1,116 @@
.p-TabBar.theia-app-centers .p-TabBar-tabIcon.arduino-serial-monitor-tab-icon {
background: url(../icons/buttons.svg);
background-size: 800%;
background-position-y: 41px;
background-position-x: 19px;
}
.serial-monitor-container {
height: 100%;
display: flex;
flex-direction: column;
}
.serial-monitor-container .head {
display: flex;
padding: 5px;
background: var(--theia-brand-color2);
height: 27px;
}
.serial-monitor-container .head .send {
display: flex;
flex:1;
}
.serial-monitor-container .head .send .btn {
display: flex;
padding: 0 5px;
align-items: center;
background: var(--theia-brand-color3);
color: var(--theia-ui-dialog-font-color);
}
.serial-monitor-container .head .send form {
flex: 1;
display: flex;
}
.serial-monitor-container .head .send input#serial-monitor-send {
background: var(--theia-layout-color0);
flex: 1;
}
.serial-monitor-container .head .send input:focus {
outline: none;
}
.serial-monitor-container .head .config {
display: flex;
}
.serial-monitor-container .head .config .serial-monitor-select {
margin-left: 5px;
}
#serial-monitor-output-container {
overflow: auto;
flex: 1;
padding: 6px;
}
.p-TabBar-toolbar .item.arduino-monitor {
width: 24px;
justify-content: center;
font-size: medium;
box-sizing: border-box;
}
.p-TabBar-toolbar .item.arduino-monitor.toggled {
background: var(--theia-brand-color2);
}
.p-TabBar-toolbar .item .clear-all {
background: var(--theia-icon-clear) no-repeat;
}
/* React Select Styles */
.serial-monitor-select .sms__control {
border: var(--theia-border-color1) var(--theia-border-width) solid;
}
.serial-monitor-select .sms__control--is-focused {
border-color: var(--theia-border-color2) !important;
box-shadow: none !important;
}
.sms__control--is-focused:hover {
border-color: var(--theia-border-color2) !important;
}
.serial-monitor-select .sms__option--is-selected {
background-color: var(--theia-ui-button-color-secondary-hover);
color: var(--theia-content-font-color0);
}
.serial-monitor-select .sms__option--is-focused {
background-color: var(--theia-ui-button-color-secondary-hover);
}
.serial-monitor-select .sms__menu {
background-color: var(--theia-layout-color1);
border: 1px solid var(--theia-border-color2);
border-top: none;
box-shadow: none;
}
.serial-monitor-select .sms__control.sms__control--menu-is-open {
border: 1px solid;
border-color: var(--theia-border-color2) !important;
border-bottom: none;
}
.sms__menu-list {
padding-top: 0 !important;
padding-bottom: 0 !important;
}

View File

@ -1,3 +0,0 @@
.p-Widget.p-TabBar.theia-app-centers.theia-app-bottom .p-TabBar-content-container.ps {
display: none;
}

View File

@ -1,26 +1,45 @@
import { FrontendApplicationContribution, FrontendApplication } from "@theia/core/lib/browser";
import { FrontendApplicationContribution, FrontendApplication, Widget, Message } from "@theia/core/lib/browser";
import { injectable, inject } from "inversify";
import { ArduinoToolbar } from "./arduino-toolbar";
import { TabBarToolbarRegistry } from "@theia/core/lib/browser/shell/tab-bar-toolbar";
import { CommandRegistry } from "@theia/core";
import { LabelParser } from "@theia/core/lib/browser/label-parser";
export class ArduinoToolbarContainer extends Widget {
protected toolbars: ArduinoToolbar[];
constructor(...toolbars: ArduinoToolbar[]) {
super();
this.id = 'arduino-toolbar-container';
this.toolbars = toolbars;
}
onAfterAttach(msg: Message) {
for (const toolbar of this.toolbars) {
Widget.attach(toolbar, this.node);
}
}
}
@injectable()
export class ArduinoToolbarContribution implements FrontendApplicationContribution {
protected toolbarWidget: ArduinoToolbar;
protected arduinoToolbarContainer: ArduinoToolbarContainer;
constructor(
@inject(TabBarToolbarRegistry) protected tabBarToolBarRegistry: TabBarToolbarRegistry,
@inject(CommandRegistry) protected commandRegistry: CommandRegistry,
@inject(LabelParser) protected labelParser: LabelParser) {
this.toolbarWidget = new ArduinoToolbar(tabBarToolBarRegistry, commandRegistry, labelParser);
const leftToolbarWidget = new ArduinoToolbar(tabBarToolBarRegistry, commandRegistry, labelParser, 'left');
const rightToolbarWidget = new ArduinoToolbar(tabBarToolBarRegistry, commandRegistry, labelParser, 'right');
this.arduinoToolbarContainer = new ArduinoToolbarContainer(leftToolbarWidget, rightToolbarWidget);
}
onStart(app: FrontendApplication) {
app.shell.addWidget(this.toolbarWidget, {
app.shell.addWidget(this.arduinoToolbarContainer, {
area: 'top'
})
});
}
}

View File

@ -8,6 +8,7 @@ export const ARDUINO_TOOLBAR_ITEM_CLASS = 'arduino-tool-item';
export namespace ArduinoToolbarComponent {
export interface Props {
side: 'left' | 'right',
items: (TabBarToolbarItem | ReactTabBarToolbarItem)[],
commands: CommandRegistry,
commandIsEnabled: (id: string) => boolean,
@ -44,10 +45,18 @@ export class ArduinoToolbarComponent extends React.Component<ArduinoToolbarCompo
}
render(): React.ReactNode {
return <React.Fragment>
<div key='arduino-toolbar-tooltip' className={'arduino-toolbar-tooltip'}>{this.state.tooltip}</div>
const tooltip = <div key='arduino-toolbar-tooltip' className={'arduino-toolbar-tooltip'}>{this.state.tooltip}</div>;
const items = [
<React.Fragment key={this.props.side + '-arduino-toolbar-tooltip'}>
{[...this.props.items].map(item => TabBarToolbarItem.is(item) ? this.renderItem(item) : item.render())}
</React.Fragment>;
</React.Fragment>
]
if (this.props.side === 'left') {
items.unshift(tooltip);
} else {
items.push(tooltip)
}
return items;
}
}
@ -58,10 +67,11 @@ export class ArduinoToolbar extends ReactWidget {
constructor(
protected readonly tabBarToolbarRegistry: TabBarToolbarRegistry,
protected readonly commands: CommandRegistry,
protected readonly labelParser: LabelParser
protected readonly labelParser: LabelParser,
public readonly side: 'left' | 'right'
) {
super();
this.id = 'arduino-toolbar';
this.id = side + '-arduino-toolbar';
this.addClass(TabBarToolbar.Styles.TAB_BAR_TOOLBAR);
this.init();
this.tabBarToolbarRegistry.onDidChange(() => this.updateToolbar());
@ -82,7 +92,7 @@ export class ArduinoToolbar extends ReactWidget {
}
protected init(): void {
this.node.classList.add('theia-arduino-toolbar');
this.node.classList.add('theia-arduino-toolbar', this.side);
this.update();
}
@ -93,6 +103,8 @@ export class ArduinoToolbar extends ReactWidget {
protected render(): React.ReactNode {
return <ArduinoToolbarComponent
key='arduino-toolbar-component'
side={this.side}
items={[...this.items.values()]}
commands={this.commands}
commandIsEnabled={this.doCommandIsEnabled}
@ -107,3 +119,9 @@ export class ArduinoToolbar extends ReactWidget {
}
}
}
export namespace ArduinoToolbar {
export function is(maybeToolbarWidget: any): maybeToolbarWidget is ArduinoToolbar {
return maybeToolbarWidget instanceof ArduinoToolbar;
}
}

View File

@ -23,6 +23,7 @@ export interface MonitorService extends JsonRpcServer<MonitorServiceClient> {
connect(config: ConnectionConfig): Promise<{ connectionId: string }>;
disconnect(connectionId: string): Promise<boolean>;
send(connectionId: string, data: string | Uint8Array): Promise<void>;
getConnectionIds(): Promise<string[]>;
}
export interface ConnectionConfig {

View File

@ -6,6 +6,7 @@ import { ILogger, Disposable, DisposableCollection } from '@theia/core';
import { MonitorService, MonitorServiceClient, ConnectionConfig, ConnectionType } from '../../common/protocol/monitor-service';
import { StreamingOpenReq, StreamingOpenResp, MonitorConfig } from '../cli-protocol/monitor/monitor_pb';
import { MonitorClientProvider } from './monitor-client-provider';
import * as google_protobuf_struct_pb from "google-protobuf/google/protobuf/struct_pb";
export interface MonitorDuplex {
readonly toDispose: Disposable;
@ -59,6 +60,10 @@ export class MonitorServiceImpl implements MonitorService {
}
}
async getConnectionIds(): Promise<string[]> {
return Array.from(this.connections.keys());
}
async connect(config: ConnectionConfig): Promise<{ connectionId: string }> {
const client = await this.monitorClientProvider.client;
const duplex = client.streamingOpen();
@ -94,7 +99,8 @@ export class MonitorServiceImpl implements MonitorService {
monitorConfig.setType(this.mapType(type));
monitorConfig.setTarget(port);
if (config.baudRate !== undefined) {
monitorConfig.setAdditionalconfig({ 'BaudRate': config.baudRate });
const obj = google_protobuf_struct_pb.Struct.fromJavaScript({ 'BaudRate': config.baudRate });
monitorConfig.setAdditionalconfig(obj);
}
req.setMonitorconfig(monitorConfig);
@ -122,11 +128,12 @@ export class MonitorServiceImpl implements MonitorService {
return result;
}
protected async doDisconnect(connectionId: string, duplex: MonitorDuplex): Promise<boolean> {
const { toDispose } = duplex;
protected async doDisconnect(connectionId: string, monitorDuplex: MonitorDuplex): Promise<boolean> {
const { duplex } = monitorDuplex;
this.logger.info(`>>> Disposing monitor connection: ${connectionId}...`);
try {
toDispose.dispose();
duplex.cancel();
this.connections.delete(connectionId);
this.logger.info(`<<< Connection disposed: ${connectionId}.`);
return true;
} catch (e) {

1802
yarn.lock

File diff suppressed because it is too large Load Diff