├── .babelrc ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── hooks └── useArduino.ts ├── next-env.d.ts ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── index.module.scss └── index.tsx ├── styles ├── colors.scss ├── fonts.scss ├── global.scss └── theme.scss └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "next/babel", 5 | { 6 | "styled-jsx/babel": { 7 | "plugins": ["@styled-jsx/plugin-sass"] 8 | } 9 | } 10 | ] 11 | ], 12 | } 13 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["@typescript-eslint", "react", "prettier"], 4 | "parserOptions": { 5 | "ecmaVersion": 7, 6 | "sourceType": "module", 7 | "ecmaFeatures": { 8 | "jsx": true 9 | } 10 | }, 11 | "extends": [ 12 | "plugin:@typescript-eslint/recommended", 13 | "plugin:prettier/recommended", 14 | "prettier/@typescript-eslint", 15 | "plugin:react/recommended" 16 | ], 17 | "rules": { 18 | "@typescript-eslint/no-unused-vars": [ 19 | "error", 20 | { 21 | "argsIgnorePattern": "[iI]gnored", 22 | "varsIgnorePattern": "[iI]gnored" 23 | } 24 | ], 25 | "react/jsx-uses-vars": "error", 26 | "react/react-in-jsx-scope": "off" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | /secrets/ 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env 30 | .env.local 31 | .env.development.local 32 | .env.test.local 33 | .env.production.local 34 | 35 | # vercel 36 | .vercel 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Darash Desai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Arduino Serial Data Viewer 2 | 3 | Visit the web tool here: [https://arduino-serial-data-viewer.vercel.app/](https://arduino-serial-data-viewer.vercel.app/) 4 | 5 | ## What is Arduino Serial Data Viewer? 6 | Arduino Serial Data Viewer was created in response to a lack of simple tools that enable rapid bootstrapping and development of data-centric Arduino projects. The core goals of this tool are to: 7 | 1. Provide a simple means to visualize serial data output from an Arduino in real-time 8 | 2. Facilitate data exporting for down-stream analysis 9 | 3. Expand accessibility by wrapping this functionality into a cross-platform and readily available web tool made possible through the current Chrome Web Serial API 10 | 11 | The Arduino Serial Data Viewer was built for use together with [`arduino-serial-data-exporter`](https://github.com/lyvewave/arduino-serial-data-exporter), which provides a more streamlined approach to capturing and exporting data collected on an Arduino via serial communication. This library is not required, but data captured using the data viewer is expected in a flat JSON format. More on this below. 12 | 13 | ## How does it work? 14 | The data viewer tool utilizes the Chrome Web Serial API to connect to your Arduino device. Data received from the Arduino is expected as a stream of JSON objects, with each object packaging any number of variables into a single time point plotted on the chart in real time. Object properties are treated as variable names that are automatically used to name the data series and appear in the chart legend. 15 | 16 | Data collection is automatically started when the tool connects to an Arduino and continues until it is disconnected. All data received during that time is plotted on the real time chart. Once the Arduino has been disconnected, and data plotted on the chart can be exported to a CSV for further data analysis. You may also use the `Average` button to perform basic statistical analysis of the data set. 17 | 18 | ## Getting Started 19 | ### Enabling the Arduino Serial Data Viewer web tool 20 | To use Arduino Serial Data Viewer, you must first enable the experimental Web Serial API in Chrome. To do so: 21 | 1. Navigate to `chrome://flags` by typing it in the URL bar of Chrome and hitting enter. You will be directed to a section of your Chrome settings. 22 | 2. In the search bar at the top, search for `enable-experimental-web-platform-features`. 23 | 3. Click on the dropdown to the right under `Experimental Web Platform features` and selected `Enabled`. 24 | 4. At the bottom right of the screen, click on `Relaunch` to restart Chrome and apply the new setting. 25 | 26 | That's it for the web tool! Now to get your Arduino set up. 27 | 28 | ### Setting up your Arduino 29 | While not the only way to leverage the data viewer, the recommended approach is to use the [`arduino-serial-data-exporter`](https://github.com/lyvewave/arduino-serial-data-exporter) Arduino library. Check out the link for instructions and examples of how to install and use it. Here's a quick code snippet to demonstrate how simple it is: 30 | 31 | ``` 32 | #include 33 | #include "SerialDataExporter.h" 34 | 35 | int bufferSizes[] = {255, 3, 2}; 36 | SerialDataExporter exporter = SerialDataExporter(Serial, bufferSizes); 37 | 38 | void setup() { 39 | Serial.begin(9600); // Initialize serial communication 40 | delay(250); 41 | } 42 | 43 | int counter1 = 0; 44 | int counter2 = 1; 45 | double counter3 = 3.1415926; 46 | void loop() { 47 | exporter.add("x", counter1); // Export counter1 as a variable named x 48 | exporter.add("y", counter2); // Export counter2 as a variable named y 49 | exporter.add("z", counter3); // Export counter3 as a variable named z 50 | exporter.exportJSON(); // Send the data via Serial 51 | 52 | counter1++; 53 | counter2++; 54 | counter3++; 55 | 56 | delay(500); 57 | } 58 | ``` 59 | -------------------------------------------------------------------------------- /hooks/useArduino.ts: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | let port: SerialPort | null = null; 4 | let readingTask: Promise | null = null; 5 | let reader: ReadableStreamDefaultReader | null = null; 6 | let outputStream: WritableStream | null = null; 7 | let writingTask: Promise | null = null; 8 | 9 | interface UseArduinoOptions { 10 | /** 11 | * Callback function invoked when data is read from the serial. 12 | * 13 | * @param value The value that was read. 14 | */ 15 | onRead?(value: string): void; 16 | 17 | /** 18 | * Optional delimeter for incoming serial data that triggers the `onRead()` 19 | * callback. This defaults to no delimeter, which triggers the callback as 20 | * soon as new read data is available. Note that the delimiter is not returned 21 | * with the read data. 22 | */ 23 | readDelimiter?: string; 24 | } 25 | 26 | type ArduinoStatus = "connected" | "disconnected"; 27 | 28 | type UseArduinoResult = { 29 | connect: (baudRate: number) => Promise; 30 | 31 | disconnect: () => Promise; 32 | 33 | write: (output: string) => Promise; 34 | 35 | status: ArduinoStatus; 36 | }; 37 | 38 | export function useArduino({ 39 | onRead, 40 | readDelimiter = "", 41 | }: UseArduinoOptions): UseArduinoResult { 42 | const [status, setStatus] = useState("disconnected"); 43 | 44 | // Reads data from the input stream 45 | const readLoop = async (): Promise => { 46 | if (!reader || !onRead) return; 47 | 48 | let readData = ""; 49 | while (true) { 50 | const { value, done } = await reader.read(); 51 | if (value) { 52 | if (readDelimiter === "") { 53 | // Call `onRead()` immediately if not delimiter was provided 54 | onRead(value); 55 | } else { 56 | // Split out the new read value based on the delimeter 57 | const parsedStrings = value.split(readDelimiter); 58 | if (parsedStrings.length === 1) { 59 | // Append the new read data if the delimiter was not found 60 | readData += value; 61 | } else { 62 | // Call `onRead()` with the appended data from the first split 63 | onRead(readData + parsedStrings.shift()); 64 | 65 | // Set readData to the last element 66 | if (parsedStrings.length > 0) { 67 | readData = parsedStrings.pop() || ""; 68 | } 69 | 70 | // Call `onRead()` callback on remaining string tokens 71 | parsedStrings.forEach((parsedValue) => { 72 | onRead(parsedValue); 73 | }); 74 | } 75 | } 76 | } 77 | 78 | if (done) { 79 | reader.releaseLock(); 80 | break; 81 | } 82 | } 83 | }; 84 | 85 | const write = async (output: string): Promise => { 86 | if (!outputStream) return; 87 | 88 | const writer = outputStream.getWriter(); 89 | await writer.write(output); 90 | writer.releaseLock(); 91 | }; 92 | 93 | const connect = async (baudRate: number): Promise => { 94 | if (!navigator.serial || status === "connected" || port) return; 95 | 96 | port = await navigator.serial.requestPort(); 97 | await port.open({ baudRate }); 98 | 99 | // Set up input stream to read data from the serial line. Only do this if 100 | // an onRead() callback function was provided to pass the read data on to. 101 | if (onRead) { 102 | const decoder = new TextDecoderStream(); 103 | readingTask = port.readable.pipeTo(decoder.writable); 104 | 105 | const inputStream = decoder.readable; 106 | reader = inputStream.getReader(); 107 | readLoop(); 108 | } 109 | 110 | // Set up output stream to write data to the serial line 111 | const encoder = new TextEncoderStream(); 112 | writingTask = encoder.readable.pipeTo(port.writable); 113 | outputStream = encoder.writable; 114 | 115 | setStatus("connected"); 116 | }; 117 | 118 | const disconnect = async (): Promise => { 119 | if (status === "disconnected" || !port) return; 120 | 121 | if (reader && readingTask) { 122 | await reader.cancel(); 123 | await readingTask.catch((error) => 124 | console.log("Error while disconnecting", error) 125 | ); 126 | 127 | reader = null; 128 | readingTask = null; 129 | } 130 | 131 | if (outputStream && writingTask) { 132 | await outputStream.getWriter().close(); 133 | await writingTask; 134 | 135 | outputStream = null; 136 | writingTask = null; 137 | } 138 | 139 | await port.close(); 140 | port = null; 141 | 142 | setStatus("disconnected"); 143 | }; 144 | 145 | return { 146 | status, 147 | connect, 148 | disconnect, 149 | write, 150 | }; 151 | } 152 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // Close function is missing from @types/dom-serial 5 | interface SerialPort { 6 | close(): Promise; 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arduino-serial-data-viewer", 3 | "version": "0.1.0", 4 | "description": "Arduino Serial Data Viewer", 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/lyvewave/arduino-serial-data-viewer.git" 13 | }, 14 | "author": "darash@lyvewave.com", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/lyvewave/arduino-serial-data-viewer/issues" 18 | }, 19 | "homepage": "https://github.com/lyvewave/arduino-serial-data-viewer#readme", 20 | "dependencies": { 21 | "@types/dom-serial": "^1.0.0", 22 | "@types/node": "^14.14.22", 23 | "ahooks": "^2.9.4", 24 | "apexcharts": "^3.23.1", 25 | "bootstrap": "^4.5.3", 26 | "next": "^10.0.5", 27 | "react": "^17.0.1", 28 | "react-apexcharts": "^1.3.7", 29 | "react-bootstrap": "^1.4.3", 30 | "react-dom": "^17.0.1", 31 | "sass": "^1.32.4", 32 | "typescript": "^4.1.3" 33 | }, 34 | "devDependencies": { 35 | "@typescript-eslint/eslint-plugin": "^4.14.0", 36 | "@typescript-eslint/parser": "^4.14.0", 37 | "eslint": "^7.18.0", 38 | "eslint-config-prettier": "^7.2.0", 39 | "eslint-plugin-prettier": "^3.3.1", 40 | "eslint-plugin-react": "^7.22.0", 41 | "prettier": "^2.2.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { AppProps } from "next/app"; 2 | import "styles/global.scss"; 3 | 4 | export default function App({ Component, pageProps }: AppProps): JSX.Element { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /pages/index.module.scss: -------------------------------------------------------------------------------- 1 | @import "styles/theme"; 2 | 3 | .Index { 4 | margin-top: 40px; 5 | 6 | h1 { 7 | margin-left: 16px; 8 | } 9 | 10 | :global { 11 | .card { 12 | padding: 20px; 13 | border: 1px solid $gray-100; 14 | } 15 | 16 | .row { 17 | margin-bottom: 20px; 18 | } 19 | } 20 | 21 | .chartArea { 22 | padding-left: 0px; 23 | } 24 | 25 | .axisTitle { 26 | font-weight: 500; 27 | font-size: 1.2rem; 28 | font-family: $title-font; 29 | } 30 | 31 | .controlPanel { 32 | padding: 10px; 33 | height: 100%; 34 | 35 | .statusIndicator { 36 | width: 100%; 37 | margin-bottom: 24px; 38 | font-family: $title-font; 39 | 40 | :global { 41 | .input-group-text { 42 | border-radius: $btn-corner-radius; 43 | border-top-right-radius: 0; 44 | border-bottom-right-radius: 0; 45 | flex: 1; 46 | } 47 | 48 | .btn { 49 | width: 120px; 50 | padding: 0px 16px; 51 | } 52 | } 53 | 54 | .statusLabel { 55 | font-weight: 500; 56 | margin-right: 16px; 57 | } 58 | 59 | .status { 60 | font-weight: 500; 61 | float: right; 62 | display: inline-block; 63 | text-transform: capitalize; 64 | } 65 | } 66 | } 67 | 68 | .statistics { 69 | :global(.table) { 70 | margin-top: 30px; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, useState, useRef } from "react"; 2 | import dynamic from "next/dynamic"; 3 | 4 | import Card from "react-bootstrap/Card"; 5 | import Container from "react-bootstrap/Container"; 6 | import Col from "react-bootstrap/Col"; 7 | import Row from "react-bootstrap/Row"; 8 | import Button from "react-bootstrap/Button"; 9 | import InputGroup from "react-bootstrap/InputGroup"; 10 | import Table from "react-bootstrap/Table"; 11 | 12 | const Chart = dynamic(() => import("react-apexcharts"), { ssr: false }); 13 | import { exec } from "apexcharts"; 14 | 15 | import { useThrottleFn } from "ahooks"; 16 | import { useArduino } from "hooks/useArduino"; 17 | import styles from "./index.module.scss"; 18 | 19 | type DataSeries = { name?: string; data: { x: number; y: number }[] }[]; 20 | type DataStats = { name: string; mean: number; stdev: number }[]; 21 | 22 | declare global { 23 | interface Window { 24 | ApexCharts: { 25 | exec: typeof exec; 26 | }; 27 | } 28 | } 29 | 30 | /** 31 | * Interval at which the chart is updated. Note that data received within this 32 | * interval is still captured and stored, but not plotted on the chart. 33 | */ 34 | const CHART_UPDATE_INTERVAL = 250; 35 | 36 | /** 37 | * The number of samples that are visible on the chart at any given time. 38 | * Samples that fall outside of this window are still plotted, but are 39 | * offscreen. 40 | */ 41 | const WINDOW_SIZE = 100; 42 | 43 | const CHART_OPTIONS = { 44 | chart: { 45 | id: "line-chart", 46 | type: "line", 47 | width: 600, 48 | height: 350, 49 | offsetX: 10, 50 | animations: { 51 | enabled: true, 52 | speed: CHART_UPDATE_INTERVAL, 53 | easing: "linear", 54 | dynamicAnimation: { 55 | speed: CHART_UPDATE_INTERVAL, 56 | }, 57 | }, 58 | toolbar: { 59 | show: true, 60 | tools: { 61 | download: false, 62 | }, 63 | }, 64 | }, 65 | legend: { 66 | show: true, 67 | position: "top", 68 | showForSingleSeries: false, 69 | floating: true, 70 | }, 71 | dataLabels: { enabled: false }, 72 | stroke: { 73 | curve: "straight", 74 | width: 2.5, 75 | }, 76 | title: { 77 | align: "center", 78 | }, 79 | markers: { size: 0 }, 80 | grid: { show: false }, 81 | yaxis: { 82 | title: { 83 | text: "Value", 84 | style: { 85 | color: "#444", 86 | cssClass: styles.axisTitle, 87 | }, 88 | }, 89 | axisBorder: { 90 | show: true, 91 | color: "#ccc", 92 | offsetX: 0, 93 | offsetY: 0, 94 | width: 0.75, 95 | }, 96 | }, 97 | xaxis: { 98 | type: "numeric", 99 | title: { 100 | text: "Sample", 101 | offsetY: 10, 102 | style: { 103 | color: "#444", 104 | cssClass: styles.axisTitle, 105 | }, 106 | }, 107 | tickAmount: 10, 108 | axisBorder: { 109 | show: true, 110 | color: "#ccc", 111 | offsetX: 0, 112 | offsetY: 0, 113 | height: 0.75, 114 | }, 115 | min: 0, 116 | max: 10, 117 | }, 118 | }; 119 | 120 | const Index = (): ReactElement => { 121 | const serialData = useRef([]); 122 | 123 | /** The current chart data. */ 124 | const chartDataRef = useRef([]); 125 | const chartData = chartDataRef.current; 126 | 127 | /** 128 | * Stores the current collection of series for the plot. A new series is 129 | * created for each unique variable name that is received from the serial 130 | * data. A unique index is stored in this map for each series so that data 131 | * index order for the chart is maintained even when data for a particular 132 | * variable is only provided intermitently. 133 | */ 134 | const series = useRef>(new Map()).current; 135 | 136 | /** Stores a set of statistics generated for the current data set. */ 137 | const [statistics, setStatistics] = useState([]); 138 | 139 | const { run: updateChartData } = useThrottleFn( 140 | (jsonString: string, index: number): void => { 141 | try { 142 | const data = JSON.parse(jsonString); 143 | 144 | Object.keys(data).forEach((key) => { 145 | if (!series.has(key)) { 146 | series.set(key, series.size); 147 | window.ApexCharts.exec("line-chart", "appendSeries", { 148 | name: key, 149 | data: [], 150 | }); 151 | 152 | chartData.push({ 153 | name: key, 154 | data: [], 155 | }); 156 | } 157 | 158 | const seriesIndex = series.get(key); 159 | if (seriesIndex !== undefined) { 160 | chartData[seriesIndex].data.push({ x: index, y: data[key] }); 161 | } 162 | }); 163 | 164 | window.ApexCharts.exec("line-chart", "updateOptions", { 165 | xaxis: { 166 | min: Math.max(0, index - WINDOW_SIZE), 167 | max: index, 168 | }, 169 | series: chartData, 170 | }); 171 | } catch (error) { 172 | console.log("Error parsing data:", error); 173 | } 174 | }, 175 | { wait: CHART_UPDATE_INTERVAL } 176 | ); 177 | 178 | const { connect, disconnect, status } = useArduino({ 179 | readDelimiter: "\n", 180 | onRead: (value) => { 181 | serialData.current.push(value); 182 | updateChartData(value, serialData.current.length - 1); 183 | }, 184 | }); 185 | 186 | const handleConnect = async (): Promise => { 187 | if (status === "connected") return; 188 | 189 | await connect(9600); 190 | 191 | // Clear the current data if there is any 192 | if (serialData.current.length > 0) { 193 | handleClear(); 194 | } 195 | 196 | window.ApexCharts.exec("line-chart", "updateOptions", { 197 | chart: { 198 | animations: { enabled: true }, 199 | }, 200 | }); 201 | }; 202 | 203 | const handleDisconnect = (): void => { 204 | if (status === "disconnected") return; 205 | disconnect(); 206 | 207 | // Update chart data to include the full high resolution data set received 208 | // from the arduino, since real-time chart updates and data parsing are 209 | // throttled. 210 | const highResData: DataSeries = []; 211 | const entries = Array.from(series.entries()).sort( 212 | (e1, e2) => e1[1] - e2[1] 213 | ); 214 | entries.forEach((entry) => { 215 | highResData.push({ 216 | name: entry[0], 217 | data: [], 218 | }); 219 | }); 220 | 221 | serialData.current.forEach((jsonValue, dataIndex) => { 222 | try { 223 | const dataObject = JSON.parse(jsonValue); 224 | entries.forEach(([seriesKey, seriesIndex]) => { 225 | if (typeof dataObject[seriesKey] !== "undefined") { 226 | highResData[seriesIndex].data.push({ 227 | x: dataIndex, 228 | y: dataObject[seriesKey], 229 | }); 230 | } 231 | }); 232 | } catch (error) { 233 | console.log("Error parsing JSON data:", error); 234 | } 235 | }); 236 | 237 | chartDataRef.current = highResData; 238 | 239 | window.ApexCharts.exec("line-chart", "updateOptions", { 240 | chart: { 241 | animations: { enabled: false }, 242 | }, 243 | series: highResData, 244 | }); 245 | }; 246 | 247 | const handleClear = (): void => { 248 | serialData.current = []; 249 | chartDataRef.current = []; 250 | series.clear(); 251 | 252 | window.ApexCharts.exec("line-chart", "updateOptions", { 253 | xaxis: { 254 | min: CHART_OPTIONS.xaxis.min, 255 | max: CHART_OPTIONS.xaxis.max, 256 | }, 257 | }); 258 | 259 | setStatistics([]); 260 | }; 261 | 262 | const exportToCSV = (): void => { 263 | let csvData = "data:text/csv;charset=utf-8,sample,"; 264 | 265 | // Print headers 266 | const entries = Array.from(series.entries()).sort( 267 | (e1, e2) => e1[1] - e2[1] 268 | ); 269 | csvData += entries.map((entry) => entry[0]).join(",") + "\n"; 270 | 271 | serialData.current.forEach((jsonValue, index) => { 272 | try { 273 | const dataObject = JSON.parse(jsonValue); 274 | const rowData: (string | number)[] = [index]; 275 | entries.forEach(([seriesKey]) => { 276 | typeof dataObject === "undefined" 277 | ? rowData.push("") 278 | : rowData.push(dataObject[seriesKey]); 279 | }); 280 | 281 | csvData += rowData.join(",") + "\n"; 282 | } catch (error) { 283 | console.log("Error parsing JSON data:", error); 284 | } 285 | }); 286 | 287 | const encodedUri = encodeURI(csvData); 288 | const link = document.createElement("a"); 289 | link.setAttribute("href", encodedUri); 290 | link.setAttribute("download", "data.csv"); 291 | 292 | document.body.appendChild(link); 293 | link.click(); 294 | 295 | document.body.removeChild(link); 296 | }; 297 | 298 | const calculateStatistics = (): void => { 299 | const entries = Array.from(series.entries()).sort( 300 | (e1, e2) => e1[1] - e2[1] 301 | ); 302 | 303 | const dataStats: DataStats = chartData.map((seriesData, index) => { 304 | const numSamples = seriesData.data.length; 305 | const mean = seriesData.data.reduce( 306 | (acc, { y }) => acc + y / numSamples, 307 | 0 308 | ); 309 | const stdev = Math.sqrt( 310 | seriesData.data.reduce( 311 | (acc, { y }) => acc + Math.pow(y - mean, 2), 312 | 0 313 | ) / numSamples 314 | ); 315 | 316 | return { 317 | name: entries[index][0], 318 | mean, 319 | stdev, 320 | }; 321 | }); 322 | 323 | setStatistics(dataStats); 324 | }; 325 | 326 | return ( 327 | 328 | 329 | 330 |

Arduino Serial Data Viewer

331 | 332 |
333 | 334 | 335 | 336 | 337 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | Status: 352 | 357 | {status} 358 | 359 | 360 | 361 | {status === "disconnected" ? ( 362 | 363 | ) : ( 364 | 365 | )} 366 | 367 | 368 | 369 | 375 | 381 | 387 | 388 | 389 | 390 | 391 | 392 | {statistics.length > 0 && ( 393 | 394 | 395 | 396 | 397 |

Statistics

398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | {statistics.map(({ name, mean, stdev }) => { 409 | const rsd = (stdev / mean) * 100; 410 | return ( 411 | 412 | 413 | 414 | 415 | 416 | 417 | ); 418 | })} 419 | 420 |
VariableMeanStdevRSD
{name}{mean.toFixed(3)}{stdev.toFixed(3)}{rsd.toFixed(3)}%
421 |
422 |
423 | 424 |
425 | )} 426 |
427 | ); 428 | }; 429 | 430 | export default Index; 431 | -------------------------------------------------------------------------------- /styles/colors.scss: -------------------------------------------------------------------------------- 1 | $teal: #00878F; 2 | $lightTeal: #62AEB2; 3 | $orange: #E47128; 4 | $yellow: #E5AD24; 5 | $brown: #8C7965; 6 | $red: #FC5A5A; 7 | -------------------------------------------------------------------------------- /styles/fonts.scss: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&family=Roboto:wght@300;400;500&display=swap"); 2 | 3 | $title-font: "Poppins", sans-serif; 4 | $header-font: "Poppins", sans-serif; 5 | $action-font: "Poppins", sans-serif; 6 | $text-font: "Roboto", sans-serif; 7 | $link-font: "Poppins", sans-serif; 8 | -------------------------------------------------------------------------------- /styles/global.scss: -------------------------------------------------------------------------------- 1 | @import "./theme"; 2 | @import "node_modules/bootstrap/scss/bootstrap"; 3 | 4 | html, 5 | body { 6 | padding: 0; 7 | margin: 0; 8 | background-color: $background-color; 9 | font-family: $text-font; 10 | font-size: 16px; 11 | } 12 | 13 | * { 14 | box-sizing: border-box; 15 | } 16 | 17 | h1 { 18 | font-family: $header-font; 19 | font-weight: 600; 20 | font-size: 1.875rem; 21 | color: $header-color; 22 | margin-bottom: 16px; 23 | } 24 | 25 | h2 { 26 | font-family: $header-font; 27 | font-weight: 600; 28 | font-size: 1.5rem; 29 | color: $header-color; 30 | margin-bottom: 16px; 31 | } 32 | 33 | a { 34 | color: inherit; 35 | text-decoration: none; 36 | } 37 | 38 | .card { 39 | border-radius: $corner-radius; 40 | background-color: white; 41 | box-shadow: 0px 15px 25px 0px rgba(151, 151, 151, 0.15); 42 | } 43 | 44 | .btn { 45 | padding: 0px 32px; 46 | font-family: $action-font; 47 | font-weight: 300; 48 | font-size: 0.9325rem; 49 | vertical-align: middle; 50 | border-radius: $btn-corner-radius; 51 | border-width: 0; 52 | min-width: 120px; 53 | height: 40px; 54 | 55 | &.focus, 56 | &:focus { 57 | box-shadow: none; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /styles/theme.scss: -------------------------------------------------------------------------------- 1 | @import "./fonts"; 2 | @import "./colors"; 3 | 4 | @import "node_modules/bootstrap/scss/_functions"; 5 | @import "node_modules/bootstrap/scss/_variables"; 6 | 7 | $theme-colors: ( 8 | "primary": $teal, 9 | "secondary": $orange, 10 | "warning": $yellow, 11 | "info": $lightTeal, 12 | "danger": $red, 13 | ); 14 | 15 | $background-color: $gray-100; 16 | $header-color: $gray-700; 17 | 18 | $corner-radius: 20px; 19 | $btn-corner-radius: 8px; 20 | $input-corner-radius: 10px; 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "baseUrl": "." 21 | }, 22 | "include": [ 23 | "next-env.d.ts", 24 | "**/*.ts", 25 | "**/*.tsx" 26 | ], 27 | "exclude": [ 28 | "node_modules" 29 | ] 30 | } 31 | --------------------------------------------------------------------------------