mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-04-27 00:37:20 +00:00

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>
398 lines
13 KiB
TypeScript
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}
|
|
/>
|
|
}
|
|
} |