xxxxxxxxxx
/**
* @fileoverview
* This example demonstrates how to receive accelerometer data (timestamp, x, y, z)
* from an Arduino using the Web Serial API (currently supported in Chrome/Edge over
* HTTPS or localhost). It visualizes the data in two ways:
* 1) A 3D scene (using p5.EasyCam) showing (x, y, z) readings as points and lines.
* 2) A 2D HUD (heads-up display) overlay, which includes:
* - A title and brief explanation
* - A rolling graph of x, y, z over time
* - Numeric readouts for x, y, z
*
* A "Connect to Arduino" button lets you pick the serial port and start/stop streaming.
* The 2D HUD is drawn on its own p5.Graphics, ensuring it stays fixed on screen
* while the 3D view is orbitable/zoomable via p5.EasyCam.
*
* IMPORTANT:
* - This requires using Chrome/Edge.
* - The Arduino (or microcontroller) must send lines of CSV in the format:
* "timestamp,xValue,yValue,zValue"
* each line ending in a newline.
* - For an example, you might do:
* Serial.print(millis()); Serial.print(",");
* Serial.print(xAccel); Serial.print(",");
* Serial.print(yAccel); Serial.print(",");
* Serial.println(zAccel);
*/
////////////////////////////////////////////////////////////////////////////////
// GLOBAL VARIABLES AND SETTINGS
////////////////////////////////////////////////////////////////////////////////
// Turn this on (true) to see debug logs in the console
const DEBUG = false;
/**
* Debug log helper. Logs to console only if DEBUG is true.
* @param {...any} args - Messages/variables to log
*/
function debugLog(args) {
if (DEBUG) console.log(args);
}
/**
* Debug warning helper. Logs to console only if DEBUG is true.
* @param {...any} args - Messages/variables to log as warnings
*/
function debugWarn(args) {
if (DEBUG) console.warn(args);
}
/**
* Debug error helper. Logs to console only if DEBUG is true.
* @param {...any} args - Messages/variables to log as errors
*/
function debugError(args) {
if (DEBUG) console.error(args);
}
// ------------------- SERIAL COMMUNICATION STATE -------------------
// The Serial port object (from Web Serial API)
let port;
// The reader used to read incoming data from the serial port
let reader;
// Connection state
let isConnected = false;
// A string describing current connection status (shown on the HUD or console)
let connectionStatus = "Disconnected";
// A buffer to accumulate raw text from the serial port until we see a newline
let incomingLineBuffer = "";
// ------------------- ACCELEROMETER DATA -------------------
/**
* @typedef {Object} AccelData
* @property {number} timestamp - The reading timestamp (in Arduino's time unit, e.g. millis)
* @property {number} x - Accelerometer X value (in g's)
* @property {number} y - Accelerometer Y value (in g's)
* @property {number} z - Accelerometer Z value (in g's)
*/
/** @type {AccelData} */
let accelData = { timestamp: 0, x: 0, y: 0, z: 0 };
/**
* Arrays storing recent accelerometer readings for graphing.
* We only store up to maxHistoryPoints in each array.
*/
let maxHistoryPoints = 60;
let xHistory = [];
let yHistory = [];
let zHistory = [];
// Track the last timestamp we recorded, to prevent duplicate data
let lastRecordedTimestamp = 0;
// ------------------- HUD (Heads-Up Display) -------------------
/**
* The p5.Graphics buffer used for 2D overlay text/graphs.
* We'll draw the HUD to this buffer, then display it as an image in the main draw().
*/
let hud;
// The total vertical space (in pixels) occupied by the HUD at the top of the canvas.
let overlayHeight;
// A custom font used for drawing text (loaded in preload)
let myFont;
// The "Connect to Arduino" button
let connectButton;
// Colors for the axes and data values
let axis_X_color;
let axis_Y_color;
let axis_Z_color;
// ------------------- EASY CAM (3D) -------------------
/**
* p5.EasyCam instance for orbit/zoom control of the 3D scene.
*/
let easycam;
////////////////////////////////////////////////////////////////////////////////
// p5 PRELOAD + SETUP + DRAW
////////////////////////////////////////////////////////////////////////////////
/**
* Preload function (part of p5.js lifecycle).
* Called before setup() to load external files such as fonts or images.
*/
function preload() {
// Load your TTF or OTF font. Make sure it's accessible (same folder, or correct path).
// Example: "roboto-regular-webfont.ttf" in the same directory.
myFont = loadFont('roboto-regular-webfont.ttf');
}
/**
* p5.js setup function.
* Initializes the canvas, EasyCam, HUD graphics, and the connect button.
*/
function setup() {
// Create the canvas in WEBGL mode so we can do 3D rendering
// Optionally add { antialias: true } for smoother edges if your browser supports it:
// createCanvas(windowWidth, windowHeight, WEBGL, { antialias: true });
createCanvas(windowWidth, windowHeight, WEBGL);
// Apply the custom font for text
textFont(myFont);
textAlign(LEFT, TOP);
// Set a desired frame rate (30fps).
frameRate(30);
// Initialize colors for the axes and data values
axis_X_color = color(255, 0, 0); // Red
axis_Y_color = color(0, 255, 0); // Green
axis_Z_color = color(96, 96, 255); // Blue-ish
// Create an EasyCam so we can orbit/zoom the 3D scene
easycam = createEasyCam();
easycam.setDistance(300); // Position camera some distance away from the origin
// Orbit the camera by a small angle to give an off-axis view (in radians)
easycam.rotateX(radians(5));
easycam.rotateY(radians(10));
// The HUD will occupy the top portion of the canvas (about 160px tall here).
// We'll store it in overlayHeight for reuse.
overlayHeight = 160;
// Create a separate p5.Graphics for the HUD (in P2D mode)
hud = createGraphics(windowWidth, overlayHeight, P2D);
hud.textFont(myFont);
hud.textAlign(LEFT, TOP);
// Create and position the "Connect to Arduino" button below the HUD area.
// We'll place it so it doesn't overlap the HUD or the 3D scene above it.
connectButton = createButton("Connect to Arduino");
connectButton.position(20, overlayHeight + 20);
connectButton.mousePressed(connectSerial);
// When the user closes the window/tab, attempt to disconnect from the serial port.
window.addEventListener('beforeunload', disconnectSerial);
}
/**
* p5.js draw function.
* Called every frame to update and display the sketch.
*/
function draw() {
// Fill the background with black (0,0,0)
background(0);
////////////////////////////////////////////////////////////////////////////
// 1) 3D SCENE RENDERING (via EasyCam)
////////////////////////////////////////////////////////////////////////////
push();
// We'll scale up the coordinate space so that "1 unit" is visually larger
// For example, if the sensor reading is "1.0 g," that becomes 100 in the scene.
scale(100);
// Rotate the entire scene 90° around the X-axis so that Z is "up".
// Without this, the default p5 3D view has Y as "up."
rotateX(HALF_PI);
// Draw color-coded X, Y, Z axes
drawColorAxes();
// Optionally, you could draw a reference sphere at the origin:
// noFill();
// stroke(128, 128, 128, 50);
// strokeWeight(0.25);
// sphere(1);
// Draw reference circles in the XY, XZ, and YZ planes
// This helps visualize the origin in 3D.
stroke(255, 255, 0);
strokeWeight(0.25);
noFill();
// XY plane circle:
ellipse(0, 0, 2, 2);
// XZ plane circle:
push();
rotateX(HALF_PI);
ellipse(0, 0, 2, 2);
pop();
// YZ plane circle:
push();
rotateY(HALF_PI);
ellipse(0, 0, 2, 2);
pop();
// Plot the accelerometer readings as lines + points
drawAccelHistory();
pop();
////////////////////////////////////////////////////////////////////////////
// 2) HUD (HEADS-UP DISPLAY) - 2D OVERLAY
////////////////////////////////////////////////////////////////////////////
// Use EasyCam's built-in HUD functions to draw in screen space.
easycam.beginHUD();
drawHUD();
easycam.endHUD();
}
////////////////////////////////////////////////////////////////////////////////
// 3D-DRAWING HELPER FUNCTIONS
////////////////////////////////////////////////////////////////////////////////
/**
* Draw 3 color-coded axes in the same colors used in the HUD:
* X = red, Y = green, Z = blue
* This helps us see the coordinate orientation in the 3D scene.
*/
function drawColorAxes() {
strokeWeight(0.5);
// X axis (red)
push();
stroke(axis_X_color);
line(0, 0, 0, 1, 0, 0); // from origin to x=1
pop();
// Y axis (green)
push();
stroke(axis_Y_color);
line(0, 0, 0, 0, 1, 0); // from origin to y=1
pop();
// Z axis (blue)
push();
stroke(axis_Z_color);
line(0, 0, 0, 0, 0, 1); // from origin to z=1
pop();
}
/**
* Draws the stored accelerometer history points in 3D, connecting consecutive
* points with lines, and drawing a larger point to highlight the newest reading.
*
* Note: (0,0,0) is our 3D "origin," so lines are drawn from the origin
* to the newest accelerometer point.
*/
function drawAccelHistory() {
// We will color older points red, newer points green
let oldestColor = color(255, 0, 0);
let newestColor = color(0, 255, 0);
// Loop over the entire history
for (let i = 0; i < xHistory.length; i++) {
// We'll map i such that i=0 is the newest entry, i=(length-1) is the oldest
// so let's define ii to index from newest to oldest
let ii = (xHistory.length - 1) - i;
// For color interpolation: t=0 => newest, t=1 => oldest
let t = (xHistory.length > 1)
? map(i, 0, xHistory.length - 1, 0, 1)
: 0;
let c = lerpColor(newestColor, oldestColor, t);
// If we have a previous point, connect them with a thin line
if (ii > 0) {
stroke(c);
strokeWeight(0.25);
line(
xHistory[ii - 1], yHistory[ii - 1], zHistory[ii - 1],
xHistory[ii], yHistory[ii], zHistory[ii]
);
}
// Mark the newest (ii === length-1) point with a thicker stroke
if (ii === xHistory.length - 1) {
// Draw a faint line from origin to the newest point
stroke(128);
strokeWeight(0.25);
line(
0, 0, 0,
xHistory[ii], yHistory[ii], zHistory[ii]
);
// Mark the newest point with a larger "dot"
stroke(c);
strokeWeight(8);
point(xHistory[ii], yHistory[ii], zHistory[ii]);
}
}
}
////////////////////////////////////////////////////////////////////////////////
// HUD-DRAWING HELPER
////////////////////////////////////////////////////////////////////////////////
/**
* Draws the 2D Heads-Up Display (HUD) onto the hud p5.Graphics buffer, then
* draws that buffer to the main canvas at (0,0).
*
* The HUD includes:
* - A title and short explanation at the top
* - A rolling graph area showing X, Y, Z over time
* - Numeric readouts for X, Y, Z to the right of the graph
*/
function drawHUD() {
// 1) Clear the hud graphics buffer and draw a semi-transparent background
hud.clear();
hud.noStroke();
hud.fill(30, 30, 30, 200);
hud.rect(0, 0, width, overlayHeight);
// 2) Title and short explanation
hud.fill(255);
hud.textSize(24);
hud.text("Accelerometer Visualization", 10, 5);
hud.textSize(12);
hud.text("Below is a rolling graph of accelerometer readings.\n"
+ "Use mouse or touch gestures to orbit/zoom the 3D view.\n"
+ "Click 'Connect to Arduino' (below) to start receiving data.",
10, 35);
// 3) Define the area for the graph
let labelWidth = 120;
let graphTop = 85; // some space below the explanation
let graphLeft = 10;
let graphWidth = width - labelWidth;
let graphHeight = overlayHeight - (graphTop + 10);
// 4) Draw the graph bounding box
hud.stroke(150);
hud.noFill();
hud.rect(graphLeft, graphTop, graphWidth, graphHeight);
// 5) Plot each axis on the graph
// - X axis = red
// - Y axis = green
// - Z axis = blue
hud.strokeWeight(1);
// X-axis (red)
hud.stroke(axis_X_color);
hud.noFill();
hud.beginShape();
for (let i = 0; i < xHistory.length; i++) {
let px = map(i, 0, maxHistoryPoints - 1, graphLeft, graphLeft + graphWidth);
let py = map(xHistory[i], -2, 2, graphTop + graphHeight, graphTop);
hud.vertex(px, py);
}
hud.endShape();
// Y-axis (green)
hud.stroke(axis_Y_color);
hud.beginShape();
for (let i = 0; i < yHistory.length; i++) {
let px = map(i, 0, maxHistoryPoints - 1, graphLeft, graphLeft + graphWidth);
let py = map(yHistory[i], -2, 2, graphTop + graphHeight, graphTop);
hud.vertex(px, py);
}
hud.endShape();
// Z-axis (blue)
hud.stroke(axis_Z_color);
hud.beginShape();
for (let i = 0; i < zHistory.length; i++) {
let px = map(i, 0, maxHistoryPoints - 1, graphLeft, graphLeft + graphWidth);
let py = map(zHistory[i], -2, 2, graphTop + graphHeight, graphTop);
hud.vertex(px, py);
}
hud.endShape();
// 6) Show numeric readouts for X, Y, Z to the right of the graph
let textX = graphLeft + graphWidth + 20;
let textY = graphTop + 5;
hud.noStroke();
// X (red)
hud.fill(axis_X_color);
hud.text(`X: ${nf(accelData.x, 1, 5)} g`, textX, textY);
textY += 15;
// Y (green)
hud.fill(axis_Y_color);
hud.text(`Y: ${nf(accelData.y, 1, 5)} g`, textX, textY);
textY += 15;
// Z (blue)
hud.fill(axis_Z_color);
hud.text(`Z: ${nf(accelData.z, 1, 5)} g`, textX, textY);
// 7) Now draw the HUD buffer onto the main canvas at (0,0).
image(hud, 0, 0, width, overlayHeight);
}
////////////////////////////////////////////////////////////////////////////////
// WINDOW / RESIZING
////////////////////////////////////////////////////////////////////////////////
/**
* p5.js windowResized function.
* Invoked whenever the browser window changes size.
* We update our canvas and HUD sizes accordingly.
*/
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
// Re-calculate how tall the HUD should be (20% of new window height)
overlayHeight = 0.20 * height;
hud.resizeCanvas(windowWidth, overlayHeight);
// Move the connect button so it remains below the HUD on resize
connectButton.position(20, overlayHeight + 20);
}
////////////////////////////////////////////////////////////////////////////////
// WEB SERIAL LOGIC
////////////////////////////////////////////////////////////////////////////////
/**
* Attempts to connect to the Arduino via the Web Serial API.
* If already connected, calls disconnectSerial() first to reset.
*/
async function connectSerial() {
if (isConnected) {
debugLog("Already connected. Disconnecting first...");
await disconnectSerial();
return;
}
try {
debugLog("Requesting serial port...");
// This will prompt the user to select a device in their browser
port = await navigator.serial.requestPort();
debugLog("Port selected:", port);
debugLog("Opening port at baudRate 57600...");
await port.open({ baudRate: 57600 });
debugLog("Port opened successfully.");
debugLog("Creating reader for the serial port...");
reader = port.readable.getReader();
debugLog("Reader created.");
isConnected = true;
connectionStatus = "Connected";
debugLog("Serial Port Connected!");
// Begin reading serial data in the background
readSerial();
// Switch button text to reflect the new state
document.querySelector("button").textContent = "Disconnect from Arduino";
} catch (err) {
connectionStatus = "Connection failed: " + err.message;
debugError("Error during connectSerial:", err);
}
}
/**
* Closes the serial port and resets state.
* If we're not connected or have no reader, does nothing.
*/
async function disconnectSerial() {
if (!isConnected || !reader) {
debugLog("Not connected or reader not available. Nothing to disconnect.");
return;
}
try {
debugLog("Cancelling reader...");
await reader.cancel();
debugLog("Reader cancelled. Releasing lock...");
reader.releaseLock();
debugLog("Closing port...");
await port.close();
isConnected = false;
connectionStatus = "Disconnected";
document.querySelector("button").textContent = "Connect to Arduino";
debugLog("Serial Port Disconnected!");
} catch (err) {
debugError("Error during disconnectSerial:", err);
}
}
/**
* Continuously reads data from the serial port while the connection is open.
* Splits the incoming text into lines, and processes each line as CSV data.
*/
async function readSerial() {
debugLog("Entering readSerial loop...");
while (isConnected) {
try {
const { value, done } = await reader.read();
if (done) {
// If done is true, the stream has ended (port closed).
debugWarn("Reader.done returned true. Serial connection closed.");
connectionStatus = "Connection closed";
isConnected = false;
document.querySelector("button").textContent = "Connect to Arduino";
break;
}
if (value) {
// Decode the chunk of bytes as text
debugLog("Read", value.length, "bytes from serial port.");
const decodedData = new TextDecoder().decode(value);
incomingLineBuffer += decodedData;
// Split on newline; each line should be like: "timestamp,x,y,z"
let lines = incomingLineBuffer.split("\n");
for (let i = 0; i < lines.length - 1; i++) {
let line = lines[i].trim();
if (line.length > 0) {
processIncomingData(line);
}
}
// Save any partial line for the next iteration
incomingLineBuffer = lines[lines.length - 1];
}
} catch (err) {
debugError("Serial Read Error:", err);
connectionStatus = "Read error: " + err.message;
isConnected = false;
document.querySelector("button").textContent = "Connect to Arduino";
break;
}
}
}
/**
* Processes a single line of CSV text from the Arduino.
* The expected format is: "timestamp,x,y,z"
* where each field is convertible to a number.
* @param {string} data - A line of comma-separated values (no trailing newline).
*/
function processIncomingData(data) {
let parts = data.split(",");
if (parts.length >= 4) {
accelData.timestamp = Number(parts[0]);
accelData.x = Number(parts[1]);
accelData.y = Number(parts[2]);
accelData.z = Number(parts[3]);
debugLog("Processed Data:", accelData);
// Only store new data if the timestamp is increasing
if (accelData.timestamp > lastRecordedTimestamp) {
xHistory.push(accelData.x);
yHistory.push(accelData.y);
zHistory.push(accelData.z);
lastRecordedTimestamp = accelData.timestamp;
}
// Limit the length of each history array to maxHistoryPoints
if (xHistory.length > maxHistoryPoints) {
xHistory.shift();
yHistory.shift();
zHistory.shift();
}
} else {
debugWarn("Incomplete data received:", data);
}
}
// ----------------------------------------------------------------------------------
// OPTIONAL: This ensures that if the user closes the tab or browser window,
// we attempt to cleanly close the serial port.
// Not all browsers will finalize this properly, but it's a good practice.
// ----------------------------------------------------------------------------------
window.addEventListener('beforeunload', disconnectSerial);