Skip to content
This repository was archived by the owner on Oct 1, 2024. It is now read-only.

Serial Plotter #470

Open
wants to merge 47 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
49e3148
add simple serial plotter
nd0ut Feb 21, 2018
cf4f13a
Merge remote-tracking branch 'microsoft/master' into feature/serial-p…
nd0ut Sep 30, 2018
e5bae20
fix buffer by line splitting
nd0ut Sep 30, 2018
42189c6
refactor
nd0ut Sep 30, 2018
6ba7cda
improve performance
nd0ut Sep 30, 2018
c9a9d5b
add refresh rate input
nd0ut Sep 30, 2018
6f07b89
start monitor if not started
nd0ut Sep 30, 2018
2cec6d5
allow to update plot refresh rate from UI
nd0ut Sep 30, 2018
52c3eab
add reset and play/pause controls
nd0ut Sep 30, 2018
644cb58
fix current frame merging
nd0ut Sep 30, 2018
57fe335
fix linting
nd0ut Sep 30, 2018
6059f26
add new command to tests
nd0ut Oct 1, 2018
7d18b04
Merge branch 'master' into feature/serial-plotter
nd0ut Oct 10, 2018
0ca41fa
Merge remote-tracking branch 'microsoft/master' into feature/serial-p…
nd0ut Jan 14, 2019
51a678b
replace deprecated `previewHtml` with webview api
nd0ut Jan 14, 2019
0c1ae8a
add `plotRegex` setting to specify custom regex to parse serial output
nd0ut Jan 14, 2019
9965bc3
add command to open serial plotter
nd0ut Jan 14, 2019
35c939c
add $ to the end of plot regex in the package.json
nd0ut Jan 14, 2019
ca204e2
update readme
nd0ut Jan 14, 2019
e35f71b
use vscode `postMessage` instead of wss server
nd0ut Jan 14, 2019
d56fa34
refactor code
nd0ut Jan 16, 2019
a180542
fix message proxy
nd0ut Jan 16, 2019
d4fad26
styling
nd0ut Jan 16, 2019
2fa5908
fix typo
nd0ut Jan 16, 2019
4916f26
update readme
nd0ut Jan 16, 2019
611f41a
remove extra new lines at readme
nd0ut Jan 16, 2019
10ccbfc
rename `plotRegex` to `plotterRegex`
nd0ut Jan 16, 2019
f74ec47
remove unneeded `ws` package
nd0ut Jan 16, 2019
8314902
refactor
nd0ut Jan 16, 2019
8e4d1ca
remove unneeded `prop-types` package
nd0ut Jan 16, 2019
036be64
revert back empty dependencies at package.json
nd0ut Jan 16, 2019
11218af
update readme
nd0ut Jan 17, 2019
2f319e1
use dygraphs instead of highcharts
nd0ut Jan 22, 2019
41ba027
Merge branch 'master' into feature/serial-plotter
nd0ut Jan 22, 2019
58dcea4
Merge branch 'master' into feature/serial-plotter
nd0ut Mar 9, 2019
209ad3f
Merge branch 'master' into feature/serial-plotter
nd0ut May 24, 2019
5fb99b2
Merge branch 'master' into feature/serial-plotter
nd0ut May 29, 2019
6c318d4
fix arduino commands test
nd0ut May 30, 2019
b987abb
fix react component props type
nd0ut May 30, 2019
ecb92bf
Merge branch 'master' into feature/serial-plotter
nd0ut May 31, 2019
fdf9142
Merge branch 'master' into feature/serial-plotter
nd0ut Jul 21, 2019
86a9e48
remove highcharts from deps
nd0ut Jul 21, 2019
a5d044c
Merge branch 'master' into feature/serial-plotter
nd0ut Aug 26, 2019
03fcbd7
Merge branch 'dev' into feature/serial-plotter
adiazulay Aug 30, 2021
c60ad73
fix merge conflict issues
adiazulay Aug 31, 2021
e492275
fix linting errors
adiazulay Aug 31, 2021
5a6ebe4
Merge branch 'dev' into feature/serial-plotter
benmcmorran Jan 10, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -53,6 +53,7 @@ This extension provides several commands in the Command Palette (<kbd>F1</kbd> o
- **Arduino: Initialize**: Scaffold a VS Code project with an Arduino sketch.
- **Arduino: Library Manager**: Explore and manage libraries.
- **Arduino: Open Serial Monitor**: Open the serial monitor in the integrated output window.
- **Arduino: Open Serial Plotter**: Open the serial plotter.
- **Arduino: Select Serial Port**: Change the current serial port.
- **Arduino: Send Text to Serial Port**: Send a line of text via the current serial port.
- **Arduino: Upload**: Build sketch and upload to Arduino board.
@@ -97,7 +98,8 @@ The following Visual Studio Code settings are available for the Arduino extensio
"https://raw.githubusercontent.com/VSChina/azureiotdevkit_tools/master/package_azureboard_index.json",
"http://arduino.esp8266.com/stable/package_esp8266com_index.json"
],
"arduino.defaultBaudRate": 115200
"arduino.defaultBaudRate": 115200,
"arduino.plotterRegex": "^PLOT\\[(\\d+)\\]\\[(.+?)=(.+?)\\]$",
}
```
*Note:* You only need to set `arduino.path` in Visual Studio Code settings, other options are not required.
@@ -203,6 +205,45 @@ Steps to start debugging:

> To learn more about how to debug Arduino code, visit our [team blog](https://blogs.msdn.microsoft.com/iotdev/2017/05/27/debug-your-arduino-code-with-visual-studio-code/).
## Using Serial Plotter

You can start Serial Plotter by calling `Arduino: Open Serial Plotter` from Command Pallete.

By default, it looks for lines of the following format in the serial input: `PLOT[time][variable=value]`

For example, `PLOT[1234][cos=0.5]` means that we have variable named `cos` with it's value `0.5` at the time `1234`.

You can use snippet below to print variables in such format.

```c
void plot(String name, float value)
{
String time = String(millis());
Serial.println("PLOT[" + time + "][" + name + "=" + value + "]");
}
```
### Throttling (refresh rate)
This Plotter is not working in real time. It's built on top of web technologies
with [dygraphs](http://dygraphs.com/) library to create interactive chart, so it's pretty fast but not instant.
Plotter accumulates data and flushes it to the chart with some periodicity that we will call `throttling` or `refresh rate`. By default it's 100ms, but you can change it as you want. This value was chosen empirically, lower values ​​can lead to noticable lags.
### Time window
Data is recorded for a specified period of time called `Time window`. By default it's 20 seconds and you can also change it in the way you need. The greater the value, the more resources are required to render chart, and the more lags we have.
### Override log format
You can override default regex to specify your own format, but the order will be remain the same: time, variable name, variable value.
```json
{
"arduino.plotterRegex": "^(\\d+):(.+?)=(.+?)$"
}
```

## Change Log
See the [Change log](https://github.com/Microsoft/vscode-arduino/blob/master/CHANGELOG.md) for details about the changes in each version.

11 changes: 8 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -53,6 +53,7 @@
"onCommand:arduino.showLibraryManager",
"onCommand:arduino.showExamples",
"onCommand:arduino.initialize",
"onCommand:arduino.openSerialPlotter",
"onDebug"
],
"main": "./out/src/extension",
@@ -159,6 +160,10 @@
{
"command": "arduino.showExamples",
"title": "Arduino: Examples"
},
{
"command": "arduino.openSerialPlotter",
"title": "Arduino: Open Serial Plotter"
}
],
"menus": {
@@ -527,6 +532,10 @@
"type": "number",
"default": 115200
},
"arduino.plotterRegex": {
"type": "string",
"default": "^PLOT\\[(\\d+)\\]\\[(.+?)=(.+?)\\]$"
},
"arduino.disableIntelliSenseAutoGen": {
"type": "boolean",
"default": false,
@@ -631,6 +640,7 @@
"extract-zip": "^2.0.1",
"glob": "^7.1.1",
"iconv-lite": "^0.4.18",
"lodash.throttle": "^4.1.1",
"impor": "^0.1.1",
"node-usb-native": "^0.0.20",
"properties": "^1.2.1",
39 changes: 38 additions & 1 deletion src/arduino/arduinoContentProvider.ts
Original file line number Diff line number Diff line change
@@ -7,9 +7,11 @@ import * as vscode from "vscode";
import ArduinoActivator from "../arduinoActivator";
import ArduinoContext from "../arduinoContext";
import * as Constants from "../common/constants";
import { SERIAL_PLOTTER_URI } from "../common/constants";
import * as JSONHelper from "../common/cycle";
import { DeviceContext } from "../deviceContext";
import * as Logger from "../logger/logger";
import { SerialMonitor } from "../serialmonitor/serialMonitor";
import LocalWebServer from "./localWebServer";

export class ArduinoContentProvider implements vscode.TextDocumentContentProvider {
@@ -47,7 +49,12 @@ export class ArduinoContentProvider implements vscode.TextDocumentContentProvide
this.addHandlerWithLogger("load-examples", "/api/examples", async (req, res) => await this.getExamples(req, res));
this.addHandlerWithLogger("open-example", "/api/openexample", (req, res) => this.openExample(req, res), true);

// Arduino Serial Plotter
this.addHandlerWithLogger("show-serialplotter", "/serialplotter", (req, res) => this.getHtmlView(req, res));
this.addHandlerWithLogger("updateplotrate", "/api/updateplotrate", (req, res) => this.updatePlotRefreshRate(req, res), true);

await this._webserver.start();

}

public async provideTextDocumentContent(uri: vscode.Uri): Promise<string> {
@@ -63,6 +70,8 @@ export class ArduinoContentProvider implements vscode.TextDocumentContentProvide
type = "boardConfig";
} else if (uri.toString() === Constants.EXAMPLES_URI.toString()) {
type = "examples";
} else if (uri.toString() === Constants.SERIAL_PLOTTER_URI.toString()) {
type = "serialplotter";
}

const timeNow = new Date().getTime();
@@ -81,7 +90,17 @@ export class ArduinoContentProvider implements vscode.TextDocumentContentProvide
"theme=" + encodeURIComponent(theme.trim()) +
"&backgroundcolor=" + encodeURIComponent(backgroundcolor.trim()) +
"&color=" + encodeURIComponent(color.trim());
document.getElementById('frame').src = url;
var iframe = document.getElementById('frame');
iframe.onload = function() {
window.addEventListener('message', msg => {
var data = msg.data;
iframe.contentWindow.postMessage(data, url);
})
}
iframe.src = url;
};
</script>
</head>
@@ -291,6 +310,24 @@ export class ArduinoContentProvider implements vscode.TextDocumentContentProvide
}
}

public updatePlotRefreshRate(req, res) {
if (!req.body.rate) {
return res.status(400).send("BAD Request! Missing parameters!");
} else {
try {
const serialMonitor = SerialMonitor.getInstance();

serialMonitor.serialPlotter.setThrottling(req.body.rate);

return res.json({
status: "OK",
});
} catch (error) {
return res.status(500).send(`Update plot refresh rate failed with message "code:${error.code}, err:${error.stderr}"`);
}
}
}

private addHandlerWithLogger(handlerName: string, url: string, handler: (req, res) => void, post: boolean = false): void {
const wrappedHandler = async (req, res) => {
const guid = Uuid().replace(/-/g, "");
8 changes: 8 additions & 0 deletions src/arduino/vscodeSettings.ts
Original file line number Diff line number Diff line change
@@ -16,8 +16,10 @@ const configKeys = {
IGNORE_BOARDS: "arduino.ignoreBoards",
SKIP_HEADER_PROVIDER: "arduino.skipHeaderProvider",
DEFAULT_BAUD_RATE: "arduino.defaultBaudRate",
PLOTTER_REGEX: "arduino.plotterRegex",
USE_ARDUINO_CLI: "arduino.useArduinoCli",
DISABLE_INTELLISENSE_AUTO_GEN: "arduino.disableIntelliSenseAutoGen",

};

export interface IVscodeSettings {
@@ -32,6 +34,7 @@ export interface IVscodeSettings {
ignoreBoards: string[];
skipHeaderProvider: boolean;
defaultBaudRate: number;
plotterRegex: string;
useArduinoCli: boolean;
disableIntelliSenseAutoGen: boolean;
updateAdditionalUrls(urls: string | string[]): void;
@@ -101,8 +104,13 @@ export class VscodeSettings implements IVscodeSettings {
return this.getConfigValue<boolean>(configKeys.SKIP_HEADER_PROVIDER);
}

public get plotterRegex(): string {
return this.getConfigValue<string>(configKeys.PLOTTER_REGEX);
}

public get disableIntelliSenseAutoGen(): boolean {
return this.getConfigValue<boolean>(configKeys.DISABLE_INTELLISENSE_AUTO_GEN);

}

public async updateAdditionalUrls(value) {
2 changes: 2 additions & 0 deletions src/common/constants.ts
Original file line number Diff line number Diff line change
@@ -25,6 +25,7 @@ export const BOARD_MANAGER_URI = vscode.Uri.parse("arduino-manager://arduino/ard
export const LIBRARY_MANAGER_URI = vscode.Uri.parse("arduino-manager://arduino/arduino-librariesmanager");
export const BOARD_CONFIG_URI = vscode.Uri.parse("arduino-manager://arduino/arduino-config");
export const EXAMPLES_URI = vscode.Uri.parse("arduino-manager://arduino/arduino-examples");
export const SERIAL_PLOTTER_URI = vscode.Uri.parse("arduino-manager://arduino/arduino-serialplotter");

export const messages = {
ARDUINO_FILE_ERROR: "The arduino.json file format is not correct.",
@@ -43,6 +44,7 @@ export const statusBarPriority = {
PORT: 20,
OPEN_PORT: 30,
BAUD_RATE: 40,
OPEN_SERIAL_PLOTTER: 50,
BOARD: 60,
ENDING: 70,
SKETCH: 80,
14 changes: 12 additions & 2 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ const arduinoActivatorModule = impor("./arduinoActivator") as typeof import ("./
const arduinoContextModule = impor("./arduinoContext") as typeof import ("./arduinoContext");
import {
ARDUINO_CONFIG_FILE, ARDUINO_MANAGER_PROTOCOL, ARDUINO_MODE, BOARD_CONFIG_URI, BOARD_MANAGER_URI, EXAMPLES_URI,
LIBRARY_MANAGER_URI,
LIBRARY_MANAGER_URI, SERIAL_PLOTTER_URI,
} from "./common/constants";
import { validateArduinoPath } from "./common/platform";
import * as util from "./common/util";
@@ -29,8 +29,11 @@ const nsatModule =
impor("./nsat") as typeof import ("./nsat");
import { BuildMode } from "./arduino/arduino";
import { SerialMonitor } from "./serialmonitor/serialMonitor";
import { SerialPlotterPanel } from "./serialmonitor/serialPlotterPanel";
const usbDetectorModule = impor("./serialmonitor/usbDetector") as typeof import ("./serialmonitor/usbDetector");

const status: any = {};

export async function activate(context: vscode.ExtensionContext) {
Logger.configure(context);
const activeGuid = uuidModule().replace(/-/g, "");
@@ -129,7 +132,6 @@ export async function activate(context: vscode.ExtensionContext) {
arduinoContextModule.default.boardManager.currentBoard.name,
};
});

registerArduinoCommand("arduino.upload", async () => {
if (!arduinoContextModule.default.arduinoApp.building) {
await vscode.window.withProgress({
@@ -281,8 +283,10 @@ export async function activate(context: vscode.ExtensionContext) {
// serial monitor commands
const serialMonitor = SerialMonitor.getInstance();
context.subscriptions.push(serialMonitor);

registerNonArduinoCommand("arduino.selectSerialPort", () => serialMonitor.selectSerialPort(null, null));
registerNonArduinoCommand("arduino.openSerialMonitor", () => serialMonitor.openSerialMonitor());
registerNonArduinoCommand("arduino.openSerialPlotter", () => serialMonitor.openSerialPlotter());
registerNonArduinoCommand("arduino.changeBaudRate", () => serialMonitor.changeBaudRate());
registerNonArduinoCommand("arduino.sendMessageToSerialPort", () => serialMonitor.sendMessageToSerialPort());
registerNonArduinoCommand("arduino.closeSerialMonitor", (port, showWarning = true) => serialMonitor.closeSerialMonitor(port, showWarning));
@@ -406,6 +410,12 @@ export async function activate(context: vscode.ExtensionContext) {
arduinoContextModule.default.boardManager.currentBoard.name,
};
});
registerArduinoCommand("arduino.showSerialPlotter", async () => {
const html = await arduinoManagerProvider.provideTextDocumentContent(SERIAL_PLOTTER_URI);
const serialPlotter = SerialMonitor.getInstance().serialPlotter;

SerialPlotterPanel.createOrShow({serialPlotter, html});
});
}, 100);

setTimeout(() => {
524 changes: 282 additions & 242 deletions src/serialmonitor/serialMonitor.ts

Large diffs are not rendered by default.

125 changes: 125 additions & 0 deletions src/serialmonitor/serialPlotter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import * as throttle from "lodash.throttle";
import * as vscode from "vscode";

import { VscodeSettings } from "../arduino/vscodeSettings";
import { SerialPortCtrl } from "./serialportctrl";

enum MessageType {
Frame = "Frame",
Action = "Action",
}

enum Action {
Reset = "Reset",
}

interface IMessage {
type: MessageType;
}

interface IMessageFrame extends IMessage {
type: typeof MessageType.Frame;
time?: number;
[field: string]: string | number;
}

interface IMessageAction extends IMessage {
type: typeof MessageType.Action;
action: Action;
}

type ISendMessage = (message: IMessage) => void;

export class SerialPlotter implements vscode.Disposable {
public static DEFAULT_THROTTLING: number = 100;

private _throttling: number = SerialPlotter.DEFAULT_THROTTLING;
private _frame: IMessageFrame = null;
private _sendMessage: ISendMessage = null;

private disposableOnLineHandler: vscode.Disposable = null;
private sendFrame: () => void = null;

constructor() {
this.setThrottling(SerialPlotter.DEFAULT_THROTTLING);
this.emptyFrame();
}

public open() {
vscode.commands.executeCommand("arduino.showSerialPlotter");
}

public reset() {
this.setThrottling(SerialPlotter.DEFAULT_THROTTLING);
this.sendMessage({type: MessageType.Action, action: Action.Reset} as IMessageAction);

this.emptyFrame();
}

public dispose() {
this._sendMessage = undefined;
this._frame = undefined;

if (this.disposableOnLineHandler) {
this.disposableOnLineHandler.dispose();
this.disposableOnLineHandler = undefined;
}
}

public setSendMessageFn(sendMessage: ISendMessage) {
this._sendMessage = sendMessage;
}

public setSerialPortCtrl(serialPortCtrl: SerialPortCtrl) {
if (this.disposableOnLineHandler) {
this.disposableOnLineHandler.dispose();
}

this.disposableOnLineHandler = serialPortCtrl.onLine(this.handleSerialLine.bind(this));
}

public setThrottling(throttling: number): void {
this._throttling = throttling;
this.sendFrame = throttle(this._sendFrame, this._throttling, { leading: false });
}

private _sendFrame() {
if (!this._frame) {
return;
}

this.sendMessage(this._frame);
this.emptyFrame();
}

private sendMessage(msg: IMessage) {
if (!this._sendMessage) {
return;
}

this._sendMessage(msg);
}

private handleSerialLine(line: string): void {
const match = line.match(new RegExp(VscodeSettings.getInstance().plotterRegex));

if (!match) {
return;
}

const [, time, field, value] = match;

this._frame = {
...this._frame,
type: MessageType.Frame,
time: parseInt(time, 10),
[field]: parseFloat(value),
};

this.sendFrame();
}

private emptyFrame() {
this._frame = {type: MessageType.Frame};
}
}
44 changes: 44 additions & 0 deletions src/serialmonitor/serialPlotterPanel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as vscode from "vscode";
import { SerialPlotter } from "./serialPlotter";

export class SerialPlotterPanel {
public static currentPanel: SerialPlotterPanel | void = null;
public static serialPlotter: SerialPlotter | void = null;

public static createOrShow({html, serialPlotter}: {html: string, serialPlotter: SerialPlotter}): void {
if (SerialPlotterPanel.currentPanel) {
SerialPlotterPanel.currentPanel._panel.reveal();
return;
}

const panel = vscode.window.createWebviewPanel("arduinoSerialPlotter", "Arduino Serial Plotter", vscode.ViewColumn.Two, {
enableScripts: true,
retainContextWhenHidden: true,
});

panel.webview.html = html;

SerialPlotterPanel.serialPlotter = serialPlotter;
SerialPlotterPanel.currentPanel = new SerialPlotterPanel(panel);
}

private readonly _panel: vscode.WebviewPanel = null;

private constructor(panel: vscode.WebviewPanel) {
this._panel = panel;

this._panel.onDidDispose(() => this.dispose());

if (SerialPlotterPanel.serialPlotter) {
SerialPlotterPanel.serialPlotter.setSendMessageFn((msg) => panel.webview.postMessage(msg));
SerialPlotterPanel.serialPlotter.reset();
}
}

public dispose(): void {
SerialPlotterPanel.currentPanel = undefined;
SerialPlotterPanel.serialPlotter = undefined;

this._panel.dispose();
}
}
23 changes: 23 additions & 0 deletions src/serialmonitor/serialportctrl.ts
Original file line number Diff line number Diff line change
@@ -4,7 +4,9 @@
import { ChildProcess, execFileSync, spawn } from "child_process";
import * as os from "os";
import * as path from "path";
import * as vscode from "vscode";
import { OutputChannel } from "vscode";
import { VscodeSettings } from "../arduino/vscodeSettings";
import { DeviceContext } from "../deviceContext";

interface ISerialPortDetail {
@@ -62,10 +64,14 @@ export class SerialPortCtrl {
private _currentPort: string;
private _currentBaudRate: number;
private _currentSerialPort = null;
private _lineEmitter: vscode.EventEmitter<string>;
private _lineBuffer: Buffer;

public constructor(port: string, baudRate: number, private _outputChannel: OutputChannel) {
this._currentBaudRate = baudRate;
this._currentPort = port;
this._lineEmitter = new vscode.EventEmitter();
this._lineBuffer = Buffer.alloc(0);
}

/*
@@ -79,6 +85,10 @@ export class SerialPortCtrl {
return this._currentPort;
}

public onLine(listener: (string) => {}): vscode.Disposable {
return this._lineEmitter.event(listener);
}

public open(): Promise<void> {
this._outputChannel.appendLine(`[Starting] Opening the serial port - ${this._currentPort}`);
this._outputChannel.show();
@@ -98,6 +108,8 @@ export class SerialPortCtrl {
this._child.stdout.on("data", (data) => {
const jsonObj = JSON.parse(data.toString())
this._outputChannel.append(jsonObj["payload"] + "\n");

this.readLines(jsonObj["payload"]).forEach((line) => this._lineEmitter.fire(line));
});
// TODO: add message check to ensure _child spawned without errors
resolve();
@@ -183,4 +195,15 @@ export class SerialPortCtrl {
}
});
}

private readLines(buf: Buffer): string[] {
this._lineBuffer = Buffer.concat([this._lineBuffer, buf]);

const lastEndingIdx = this._lineBuffer.lastIndexOf("\r\n");
const lines = this._lineBuffer.slice(0, lastEndingIdx).toString().split("\r\n");

this._lineBuffer = this._lineBuffer.slice(lastEndingIdx + 1);

return lines;
}
}
6 changes: 6 additions & 0 deletions src/views/app/actions/api.ts
Original file line number Diff line number Diff line change
@@ -95,3 +95,9 @@ export function openExample(examplePath) {
examplePath,
}).then((response) => response.json());
}

export function updatePlotRefreshRate(rate) {
return postHTTP("/api/updateplotrate", {
rate,
}).then((response) => response.json());
}
282 changes: 282 additions & 0 deletions src/views/app/components/SerialPlotter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
import * as React from "react";
import { Button, ControlLabel, FormControl, FormGroup } from "react-bootstrap";
import * as API from "../actions/api";
import { chartConfig } from "./chartConfig";

import "dygraphs";

enum MessageType {
Frame = "Frame",
Action = "Action",
}

enum Action {
Reset = "Reset",
}

interface IMessage {
type: MessageType;
}

interface IMessageFrame extends IMessage {
type: typeof MessageType.Frame;
time?: number;
[field: string]: string | number;
}

interface IMessageAction extends IMessage {
type: typeof MessageType.Action;
action: Action;
}

interface ISerialPlotterState extends React.Props<any> {
rate: number;
active: boolean;
timeWindow: number;
}

const formatTime = (time: number) => {
const date = new Date(time);

const hh = date.getUTCHours().toString().padStart(2, "0");
const mm = date.getUTCMinutes().toString().padStart(2, "0");
const ss = date.getUTCSeconds().toString().padStart(2, "0");
const mss = date.getUTCMilliseconds().toString().padStart(3, "0");

return `${hh}:${mm}:${ss}.${mss}`;
};

const getFrameLabels = (msg: IMessageFrame) =>
Object.keys(msg).filter((label) => !["time", "type"].includes(label));

class SerialPlotter extends React.Component<React.Props<void>, ISerialPlotterState> {
public static INITIAL_THROTTLING = 100;
public static INITIAL_TIME_WINDOW = 1000 * 20;

public state = {
rate: SerialPlotter.INITIAL_THROTTLING,
timeWindow: SerialPlotter.INITIAL_TIME_WINDOW,
active: false,
};

private _graph: Dygraph = null;
private _data: number[][] = null;
private _lastValues: { [field: string]: number } = null;
private _labels: string[] = null;
private _timeWindow: number = SerialPlotter.INITIAL_TIME_WINDOW;

private _ref: HTMLElement = null;

public componentDidMount() {
this.initMessageHandler();
this.initChart();

window.addEventListener("resize", this.handleResize, true);
}

public render() {
return (
<div className="serialplotter">
<div>
<div className="graph" ref={(el) => (this._ref = el)} />
</div>
<div className="settings">
<div>
<div className="section">
<div className="parameters">
<FormGroup>
<ControlLabel>Refresh rate</ControlLabel>
<FormControl
type="number"
value={this.state.rate}
onChange={this.onRateChange}
/>
</FormGroup>

<FormGroup>
<ControlLabel>Time window</ControlLabel>
<FormControl
type="number"
value={this.state.timeWindow}
onChange={this.onTimeWindowChange}
/>
</FormGroup>
</div>
<Button
bsSize="small"
onClick={this.applyPlotSettings}
>
Apply
</Button>
</div>
</div>

<div>
<div className="section">
<Button bsSize="small" onClick={this.reset}>
Reset
</Button>
<Button
bsSize="small"
onClick={
this.state.active ? this.pause : this.play
}
>
{this.state.active ? "Pause" : "Play"}
</Button>
</div>
</div>
</div>
</div>
);
}

private initChart() {
if (this._graph) {
this._graph.destroy();
}

this._labels = [];
this._graph = new Dygraph(this._ref, [[0, 0]], {
labels: this._labels,
legend: "always",
showRangeSelector: true,
connectSeparatedPoints: true,
drawGapEdgePoints: true,
axes: {
x: {
valueFormatter: formatTime,
axisLabelFormatter: formatTime,
},
},
});

this._data = [];
this._lastValues = {};
}

private getFrameValues(msg: IMessageFrame, labels: string[]) {
return labels.map((label) => {
const value = msg[label] as number;

if (typeof value !== "undefined") {
this._lastValues[label] = value;

return value;
}

return this._lastValues[label] || null;
});
}

private getDataTimeWindow(time: number) {
const start = Math.max(0, time - this._timeWindow);
const startIdx = this._data.findIndex((data) => data[0] > start);
const timeWindowData = this._data.slice(startIdx);

return timeWindowData;
}

private updateChart() {
this._graph.updateOptions({
file: this._data,
labels: ["time", ...this._labels],
});
}

private addFrame(msg: IMessageFrame) {
if (!this._graph) {
return;
}

const labels = [...new Set([...this._labels, ...getFrameLabels(msg)])];
const values = this.getFrameValues(msg, labels);

const time = msg.time;
const frameData = [time, ...values];

this._data = [...this.getDataTimeWindow(time), frameData];
this._labels = labels;

this.updateChart();
}

private doAction(msg: IMessageAction) {
if (msg.action === Action.Reset) {
this.reset();
}
}

private play = () => {
this.setState({
active: true,
});
}

private pause = () => {
this.setState({
active: false,
});
}

private reset = () => {
this.initChart();
}

private initMessageHandler() {
window.addEventListener("message", (event) => {
if (!this.state.active) {
return;
}

const data: IMessage = event.data;

switch (data.type) {
case MessageType.Frame:
this.addFrame(data as IMessageFrame);
break;
case MessageType.Action:
this.doAction(data as IMessageAction);
break;
default:
// TODO: Add warning back in not in console
// console.warn("Unknown message type", data);
}
});

this.setState({
active: true,
});
}

private applyPlotSettings = () => {
API.updatePlotRefreshRate(this.state.rate);

this._timeWindow = this.state.timeWindow;

const lastData = this._data[this._data.length - 1];
const lastTime = lastData[0];

this._data = this.getDataTimeWindow(lastTime);

this.updateChart();
}

private onRateChange = (e) => {
this.setState({
rate: e.target.value,
});
}

private onTimeWindowChange = (e) => {
this.setState({
timeWindow: e.target.value,
});
}

private handleResize() {
(this._graph as any).resize();
}
}

export default SerialPlotter;
72 changes: 72 additions & 0 deletions src/views/app/components/chartConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
export const chartConfig = {
chart: {
zoomType: "x",
},
title: {
text: "Serial Plotter",
},
boost: {
enabled: true,
useGPUTranslations: true,
},
xAxis: {
type: "datetime",
crosshair: true,
title: {
text: "Time",
},
},
yAxis: {
title: {
text: "Value",
},
},
series: {
marker: {
enabled: false,
},
},
tooltip: {
animation: false,
split: true,
xDateFormat: "%H:%M:%S.%L",
},
legend: {
layout: "vertical",
align: "right",
verticalAlign: "middle",
title: {
text: "Legend",
},
},
plotOptions: {
series: {
showInNavigator: true,
},
},
rangeSelector: {
buttons: [
{
count: 10,
type: "second",
text: "10s",
},
{
count: 30,
type: "second",
text: "30s",
},
{
count: 1,
type: "minute",
text: "1m",
},
{
type: "all",
text: "All",
},
],
inputEnabled: false,
selected: 0,
},
};
3 changes: 2 additions & 1 deletion src/views/app/index.tsx
Original file line number Diff line number Diff line change
@@ -10,8 +10,8 @@ import BoardConfig from "./components/BoardConfig";
import BoardManager from "./components/BoardManager";
import ExampleTreeView from "./components/ExampleTreeView";
import LibraryManager from "./components/LibraryManager";
import SerialPlotter from "./components/SerialPlotter";
import reducer from "./reducers";

import "./styles";

class App extends React.Component<{}, {}> {
@@ -35,6 +35,7 @@ ReactDOM.render(
<Route path="librarymanager" component={LibraryManager} />
<Route path="boardconfig" component={BoardConfig} />
<Route path="examples" component={ExampleTreeView} />
<Route path="serialplotter" component={SerialPlotter} />
</Route>
</Router>
</Provider>,
40 changes: 39 additions & 1 deletion src/views/app/styles/board.scss
Original file line number Diff line number Diff line change
@@ -215,4 +215,42 @@ a {
.react-selector {
width: 70%;
}
}
}

.serialplotter {
padding: 12px;

.graph {
width: 100% !important;
}

.settings {
padding: 12px;
display: flex;
justify-content: space-between;
}

.section {
display: grid;
grid-auto-columns: auto;
grid-auto-flow: column;
grid-gap: 6px;
align-items: baseline;
border: 1px solid white;
padding: 6px;
}

.parameters {
display: grid;
grid-auto-columns: auto;
grid-auto-flow: row;
grid-gap: 6px;

input {
width: 80px;
}
}

.actions {
}
}
28 changes: 28 additions & 0 deletions src/views/package-lock.json
9 changes: 6 additions & 3 deletions src/views/package.json
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@
"author": "",
"license": "ISC",
"devDependencies": {
"@types/dygraphs": "2.1.0",
"@types/react": "^15.0.11",
"@types/react-bootstrap": "0.0.45",
"@types/react-dom": "^0.14.23",
@@ -20,9 +21,9 @@
"mini-css-extract-plugin": "^0.8.0",
"node-sass": "^4.14.1",
"rc-tree": "~1.4.5",
"react": "^15.4.2",
"react": "^15.6.2",
"react-bootstrap": "^0.30.7",
"react-dom": "^15.4.2",
"react-dom": "^15.6.2",
"react-list": "^0.8.4",
"react-redux": "^5.0.2",
"react-router": "^3.0.2",
@@ -36,5 +37,7 @@
"ts-loader": "^4.5.0",
"webpack": "^4.44.1"
},
"dependencies": {}
"dependencies": {
"dygraphs": "^2.1.0"
}
}
8 changes: 6 additions & 2 deletions src/views/tsconfig.json
Original file line number Diff line number Diff line change
@@ -7,9 +7,13 @@
"outDir": "out",
"alwaysStrict": true,
"sourceMap": true,
"rootDir": "."
"rootDir": ".",
"lib": [
"dom",
"es2017"
]
},
"exclude": [
"node_modules"
]
}
}
2 changes: 2 additions & 0 deletions test/extension.test.ts
Original file line number Diff line number Diff line change
@@ -43,10 +43,12 @@ suite("Arduino: Extension Tests", () => {
"arduino.showLibraryManager",
"arduino.showBoardConfig",
"arduino.showExamples",
"arduino.showSerialPlotter",
"arduino.changeBoardType",
"arduino.initialize",
"arduino.selectSerialPort",
"arduino.openSerialMonitor",
"arduino.openSerialPlotter",
"arduino.changeBaudRate",
"arduino.sendMessageToSerialPort",
"arduino.closeSerialMonitor",