+
+
(this._ref = el)} />
+
+
+
+
+
+
+ Refresh rate
+
+
+
+
+ Time window
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ 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;
diff --git a/src/views/app/components/chartConfig.ts b/src/views/app/components/chartConfig.ts
new file mode 100644
index 00000000..49fcd208
--- /dev/null
+++ b/src/views/app/components/chartConfig.ts
@@ -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,
+ },
+};
diff --git a/src/views/app/index.tsx b/src/views/app/index.tsx
index 52c6111d..5b2b3f11 100644
--- a/src/views/app/index.tsx
+++ b/src/views/app/index.tsx
@@ -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(
+
,
diff --git a/src/views/app/styles/board.scss b/src/views/app/styles/board.scss
index eb42759d..85e59a9c 100644
--- a/src/views/app/styles/board.scss
+++ b/src/views/app/styles/board.scss
@@ -215,4 +215,42 @@ a {
.react-selector {
width: 70%;
}
-}
\ No newline at end of file
+}
+
+.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 {
+ }
+}
diff --git a/src/views/package-lock.json b/src/views/package-lock.json
index 6e07c825..ba2d8cda 100644
--- a/src/views/package-lock.json
+++ b/src/views/package-lock.json
@@ -21,6 +21,21 @@
}
}
},
+ "@types/dygraphs": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/dygraphs/-/dygraphs-2.1.0.tgz",
+ "integrity": "sha512-v61ndhl/F215QQVBJka4W2hpCAsmRY/TRMOeIq8fMBlGmnbvO7GNxD6DxC+vNS1lLaaZEqUsQbHkZsqg5cPh5w==",
+ "dev": true,
+ "requires": {
+ "@types/google.visualization": "*"
+ }
+ },
+ "@types/google.visualization": {
+ "version": "0.0.68",
+ "resolved": "https://registry.npmjs.org/@types/google.visualization/-/google.visualization-0.0.68.tgz",
+ "integrity": "sha512-LkLniL1TYykhz+ZdRof3Bi8cp1OhqoK11Tj1RM2bPtGVBNexQ0eRnOrOWcWTdi80Sz9DzJ4JIG2rTlSJBVV58w==",
+ "dev": true
+ },
"@types/history": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@types/history/-/history-3.2.4.tgz",
@@ -1666,6 +1681,11 @@
}
}
},
+ "dygraphs": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/dygraphs/-/dygraphs-2.1.0.tgz",
+ "integrity": "sha1-L7/SyAPq0CMH3z+vjU3T71XLIHU="
+ },
"ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
@@ -3258,6 +3278,14 @@
"read-pkg-up": "^1.0.1",
"redent": "^1.0.0",
"trim-newlines": "^1.0.0"
+ },
+ "dependencies": {
+ "minimist": {
+ "version": "1.2.0",
+ "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+ "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+ "dev": true
+ }
}
},
"micromatch": {
diff --git a/src/views/package.json b/src/views/package.json
index 27a9cb6b..d21e9ae3 100644
--- a/src/views/package.json
+++ b/src/views/package.json
@@ -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"
+ }
}
diff --git a/src/views/tsconfig.json b/src/views/tsconfig.json
index 9def2d1d..cb71ceaa 100644
--- a/src/views/tsconfig.json
+++ b/src/views/tsconfig.json
@@ -7,9 +7,13 @@
"outDir": "out",
"alwaysStrict": true,
"sourceMap": true,
- "rootDir": "."
+ "rootDir": ".",
+ "lib": [
+ "dom",
+ "es2017"
+ ]
},
"exclude": [
"node_modules"
]
-}
\ No newline at end of file
+}
diff --git a/test/extension.test.ts b/test/extension.test.ts
index 6c91b0e9..8f4c20e0 100644
--- a/test/extension.test.ts
+++ b/test/extension.test.ts
@@ -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",