Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serial plotter implementation #245

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,21 @@
<div id="serial-bar">
<button class="purple-button btn-restart">Restart<i class="fa-solid fa-redo"></i></button>
<button class="purple-button btn-clear">Clear<i class="fa-solid fa-broom"></i></button>
<button class="purple-button btn-plotter">Plotter<i class="fa-solid fa-chart-line"></i></button>
<div id="terminal-title"></div>
</div>
<div id="plotter" class="hidden">
<label for="buffer-size">Buffer Size</label>
<input type="number" id="buffer-size" value="20">
<label for="plot-gridlines-select">Grid Lines</label>
<select id="plot-gridlines-select">
<option value="both">Both</option>
<option value="x">X Only</option>
<option value="y">Y Only</option>
<option value="none">None</option>
</select>
<canvas id="plotter-canvas"></canvas>
</div>
<div id="terminal"></div>
</div>
</div>
Expand Down
193 changes: 193 additions & 0 deletions js/common/plotter.js
Original file line number Diff line number Diff line change
@@ -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();
});
}
22 changes: 22 additions & 0 deletions js/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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');
Expand All @@ -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");
Expand Down Expand Up @@ -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());
Expand Down
8 changes: 8 additions & 0 deletions js/workflows/workflow.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
}
Expand Down Expand Up @@ -159,6 +164,9 @@ class Workflow {
}

writeToTerminal(data) {
if (this.plotterEnabled) {
plotValues(this.plotterChart, data, this.plotterBufferSize.value);
}
this.terminal.write(data);
}

Expand Down
15 changes: 15 additions & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
15 changes: 15 additions & 0 deletions sass/layout/_layout.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -99,6 +111,9 @@
}
}
}
#buffer-size{
width: 70px;
}
}

#ble-instructions,
Expand Down
Loading