diff --git a/index.html b/index.html
index a8f30c4..e7f3efa 100644
--- a/index.html
+++ b/index.html
@@ -97,8 +97,21 @@
+
+
+
+
+
+
+
diff --git a/js/common/plotter.js b/js/common/plotter.js
new file mode 100644
index 0000000..c224b05
--- /dev/null
+++ b/js/common/plotter.js
@@ -0,0 +1,193 @@
+import Chart from "chart.js/auto";
+
+let textLineBuffer = "";
+let textLine;
+
+let defaultColors = ['#8888ff', '#ff8888', '#88ff88'];
+
+/**
+ * @name LineBreakTransformer
+ * Helper to parse the incoming string messages into lines.
+ */
+class LineBreakTransformer {
+ constructor() {
+ // A container for holding stream data until a new line.
+ this.container = '';
+ }
+
+ transform(chunk, linesList) {
+ this.container += chunk;
+ const lines = this.container.split('\n');
+ this.container = lines.pop();
+ lines.forEach(line => linesList.push(line));
+ }
+
+}
+
+let lineTransformer = new LineBreakTransformer()
+
+export function plotValues(chartObj, serialMessage, bufferSize) {
+ /*
+ Given a string serialMessage, parse it into the plottable value(s) that
+ it contains if any, and plot those values onto the given chartObj. If
+ the serialMessage doesn't represent a complete textLine it will be stored
+ into a buffer and combined with subsequent serialMessages until a full
+ textLine is formed.
+ */
+ let currentLines = []
+ lineTransformer.transform(serialMessage, currentLines)
+
+ for (textLine of currentLines) {
+
+ textLine = textLine.replace("\r", "").replace("\n", "")
+ if (textLine.length === 0) {
+ continue;
+ }
+
+ let valuesToPlot;
+
+ // handle possible tuple in textLine
+ if (textLine.startsWith("(") && textLine.endsWith(")")) {
+ textLine = "[" + textLine.substring(1, textLine.length - 1) + "]";
+ console.log("after tuple conversion: " + textLine);
+ }
+
+ // handle possible list in textLine
+ if (textLine.startsWith("[") && textLine.endsWith("]")) {
+ valuesToPlot = JSON.parse(textLine);
+ for (let i = 0; i < valuesToPlot.length; i++) {
+ valuesToPlot[i] = parseFloat(valuesToPlot[i])
+ }
+
+ } else { // handle possible CSV in textLine
+ valuesToPlot = textLine.split(",")
+ for (let i = 0; i < valuesToPlot.length; i++) {
+ valuesToPlot[i] = parseFloat(valuesToPlot[i])
+ }
+ }
+
+ if (valuesToPlot === undefined || valuesToPlot.length === 0) {
+ continue;
+ }
+
+ try {
+ while (chartObj.data.labels.length > bufferSize) {
+ chartObj.data.labels.shift();
+ for (let i = 0; i < chartObj.data.datasets.length; i++) {
+ while (chartObj.data.datasets[i].data.length > bufferSize) {
+ chartObj.data.datasets[i].data.shift();
+ }
+ }
+ }
+ chartObj.data.labels.push("");
+
+ for (let i = 0; i < valuesToPlot.length; i++) {
+ if (isNaN(valuesToPlot[i])) {
+ continue;
+ }
+ if (i > chartObj.data.datasets.length - 1) {
+ let curColor = '#000000';
+ if (i < defaultColors.length) {
+ curColor = defaultColors[i];
+ }
+ chartObj.data.datasets.push({
+ label: i.toString(),
+ data: [],
+ borderColor: curColor,
+ backgroundColor: curColor
+ });
+ }
+ chartObj.data.datasets[i].data.push(valuesToPlot[i]);
+ }
+
+ updatePlotterScales(chartObj);
+ chartObj.update();
+ } catch (e) {
+ console.log("JSON parse error");
+ // This line isn't a valid data value
+ }
+ }
+}
+
+function updatePlotterScales(chartObj) {
+ /*
+ Update the scale of the plotter so that maximum and minimum values are sure
+ to be shown within the plotter instead of going outside the visible range.
+ */
+ let allData = []
+ for (let i = 0; i < chartObj.data.datasets.length; i++) {
+ allData = allData.concat(chartObj.data.datasets[i].data)
+ }
+ chartObj.options.scales.y.min = Math.min(...allData) - 10
+ chartObj.options.scales.y.max = Math.max(...allData) + 10
+}
+
+export async function setupPlotterChart(workflow) {
+ /*
+ Initialize the plotter chart and configure it.
+ */
+ let initialData = []
+ Chart.defaults.backgroundColor = '#444444';
+ Chart.defaults.borderColor = '#000000';
+ Chart.defaults.color = '#000000';
+ Chart.defaults.aspectRatio = 3/2;
+ workflow.plotterChart = new Chart(
+ document.getElementById('plotter-canvas'),
+ {
+ type: 'line',
+ options: {
+ animation: false,
+ scales: {
+ y: {
+ min: -1,
+ max: 1,
+ grid:{
+ color: "#666"
+ },
+ border: {
+ color: "#444"
+ }
+ },
+ x:{
+ grid: {
+ display: true,
+ color: "#666"
+ },
+ border: {
+ color: "#444"
+ }
+ }
+ }
+ },
+ data: {
+ labels: initialData.map(row => row.timestamp),
+ datasets: [
+ {
+ label: '0',
+ data: initialData.map(row => row.value)
+ }
+ ]
+ }
+ }
+ );
+
+ // Set up a listener to respond to user changing the grid choice configuration
+ // dropdown
+ workflow.plotterGridLines.addEventListener('change', (event) => {
+ let gridChoice = event.target.value;
+ if (gridChoice === "x"){
+ workflow.plotterChart.options.scales.x.grid.display = true;
+ workflow.plotterChart.options.scales.y.grid.display = false;
+ }else if (gridChoice === "y"){
+ workflow.plotterChart.options.scales.y.grid.display = true;
+ workflow.plotterChart.options.scales.x.grid.display = false;
+ }else if (gridChoice === "both"){
+ workflow.plotterChart.options.scales.y.grid.display = true;
+ workflow.plotterChart.options.scales.x.grid.display = true;
+ }else if (gridChoice === "none"){
+ workflow.plotterChart.options.scales.y.grid.display = false;
+ workflow.plotterChart.options.scales.x.grid.display = false;
+ }
+ workflow.plotterChart.update();
+ });
+}
diff --git a/js/script.js b/js/script.js
index 83ece82..3f0f1ab 100644
--- a/js/script.js
+++ b/js/script.js
@@ -20,6 +20,7 @@ import { ButtonValueDialog, MessageModal } from './common/dialogs.js';
import { isLocal, switchUrl, getUrlParam } from './common/utilities.js';
import { CONNTYPE } from './constants.js';
import './layout.js'; // load for side effects only
+import {setupPlotterChart} from "./common/plotter.js";
import { mainContent, showSerial } from './layout.js';
// Instantiate workflows
@@ -33,6 +34,7 @@ let unchanged = 0;
let connectionPromise = null;
const btnRestart = document.querySelector('.btn-restart');
+const btnPlotter = document.querySelector('.btn-plotter');
const btnClear = document.querySelector('.btn-clear');
const btnConnect = document.querySelectorAll('.btn-connect');
const btnNew = document.querySelectorAll('.btn-new');
@@ -42,6 +44,7 @@ const btnSaveAs = document.querySelectorAll('.btn-save-as');
const btnSaveRun = document.querySelectorAll('.btn-save-run');
const btnInfo = document.querySelector('.btn-info');
const terminalTitle = document.getElementById('terminal-title');
+const serialPlotter = document.getElementById('plotter');
const messageDialog = new MessageModal("message");
const connectionType = new ButtonValueDialog("connection-type");
@@ -130,9 +133,28 @@ btnRestart.addEventListener('click', async function(e) {
// Clear Button
btnClear.addEventListener('click', async function(e) {
+ if (workflow.plotterChart){
+ workflow.plotterChart.data.datasets.forEach((dataSet, index) => {
+ workflow.plotterChart.data.datasets[index].data = [];
+ });
+ workflow.plotterChart.data.labels = [];
+ workflow.plotterChart.options.scales.y.min = -1;
+ workflow.plotterChart.options.scales.y.max = 1;
+ workflow.plotterChart.update();
+ }
state.terminal.clear();
});
+// Plotter Button
+btnPlotter.addEventListener('click', async function(e){
+ serialPlotter.classList.toggle("hidden");
+ if (!workflow.plotterEnabled){
+ await setupPlotterChart(workflow);
+ workflow.plotterEnabled = true;
+ }
+ state.fitter.fit();
+});
+
btnInfo.addEventListener('click', async function(e) {
if (await checkConnected()) {
await workflow.showInfo(getDocState());
diff --git a/js/workflows/workflow.js b/js/workflows/workflow.js
index 42f5491..0889768 100644
--- a/js/workflows/workflow.js
+++ b/js/workflows/workflow.js
@@ -4,6 +4,7 @@ import {FileHelper} from '../common/file.js';
import {UnsavedDialog} from '../common/dialogs.js';
import {FileDialog, FILE_DIALOG_OPEN, FILE_DIALOG_SAVE} from '../common/file_dialog.js';
import {CONNTYPE, CONNSTATE} from '../constants.js';
+import {plotValues} from '../common/plotter.js'
/*
* This class will encapsulate all of the common workflow-related functions
@@ -47,6 +48,8 @@ class Workflow {
this._unsavedDialog = new UnsavedDialog("unsaved");
this._fileDialog = new FileDialog("files", this.showBusy.bind(this));
this.repl = new REPL();
+ this.plotterEnabled = false;
+ this.plotterChart = false;
}
async init(params) {
@@ -59,6 +62,8 @@ class Workflow {
this._loadFileContents = params.loadFileFunc;
this._showMessage = params.showMessageFunc;
this.loader = document.getElementById("loader");
+ this.plotterBufferSize = document.getElementById('buffer-size');
+ this.plotterGridLines = document.getElementById('plot-gridlines-select');
if ("terminalTitle" in params) {
this.terminalTitle = params.terminalTitle;
}
@@ -159,6 +164,9 @@ class Workflow {
}
writeToTerminal(data) {
+ if (this.plotterEnabled) {
+ plotValues(this.plotterChart, data, this.plotterBufferSize.value);
+ }
this.terminal.write(data);
}
diff --git a/package-lock.json b/package-lock.json
index 7397f0a..f668a9e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0",
+ "chart.js": "^4.4.4",
"codemirror": "^6.0.1",
"file-saver": "^2.0.5",
"focus-trap": "^7.5.4",
@@ -142,6 +143,11 @@
"node": ">=6"
}
},
+ "node_modules/@kurkle/color": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
+ "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
+ },
"node_modules/@lezer/common": {
"version": "1.2.1",
"license": "MIT"
@@ -451,6 +457,17 @@
"node": ">=8"
}
},
+ "node_modules/chart.js": {
+ "version": "4.4.4",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.4.tgz",
+ "integrity": "sha512-emICKGBABnxhMjUjlYRR12PmOXhJ2eJjEHL2/dZlWjxRAZT1D8xplLFq5M0tMQK8ja+wBS/tuVEJB5C6r7VxJA==",
+ "dependencies": {
+ "@kurkle/color": "^0.3.0"
+ },
+ "engines": {
+ "pnpm": ">=8"
+ }
+ },
"node_modules/chokidar": {
"version": "3.6.0",
"dev": true,
diff --git a/package.json b/package.json
index c5f7ab4..1cfa427 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0",
+ "chart.js": "^4.4.4",
"codemirror": "^6.0.1",
"file-saver": "^2.0.5",
"focus-trap": "^7.5.4",
diff --git a/sass/layout/_layout.scss b/sass/layout/_layout.scss
index a5182f3..e2fa129 100644
--- a/sass/layout/_layout.scss
+++ b/sass/layout/_layout.scss
@@ -72,6 +72,18 @@
}
#serial-page {
+ #plotter {
+ flex: 2 1 0;
+ background: #777;
+ position: relative;
+ width: 99%;
+ overflow: hidden;
+ padding: 10px 20px;
+
+ &.hidden{
+ display: none;
+ }
+ }
#terminal {
flex: 1 1 0%;
background: #333;
@@ -99,6 +111,9 @@
}
}
}
+ #buffer-size{
+ width: 70px;
+ }
}
#ble-instructions,