Akos Kitta 4353bfb5b9 Updated port discovery to support unknown boards
From now on, we do not retrieve the ports from the attached boards.
A board can be unknown but the port is still relevant.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:33 +02:00

398 lines
13 KiB
TypeScript

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.address
}
}
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}
/>
}
}