1. The idea
Since having a web interface is not a part of the first few spirals for my final project, I decided to keep this week’s attempt simple and more of laying down a base for potential future work rather than making a cool interface. I wanted to try Node.js just because I’ve never written JavaScript before and I’ve heard Node.js a lot. The idea is to incorporate the boards I made last week, which consists of 2 Xiao RP2040 sending MPU6050 angle data through nRF24L01, and display the received angle data on a webpage.
2. The code
I first spent some time looking up Node.js documentation & demo projects and asking ChatGPT about the high level organization of this setup. Basically, after initializing a Node.js project and installing the required libraries (including “express” & “serialport”), I need a server.js to read data through serial port sent by my Xiao RP2040 receiver and to serve a local HTML interface, and a index.html file in the “public” folder for creating the actual web interface. This all sounds straightforward enough, and it helps when I have a little bit more experience than I had before this class thanks to the web development requirement for documentation.
To start, I referenced multiple demo projects & ChatGPT’s code and created a server.js file. However, I kept getting error message of “TypeError: SerialPort is not a constructor”. I had a hard time figuring what the problem is, as the first few lines of my code look exactly the same as the different examples. After some more searching and asking ChatGPT, I figured that the syntax for defining “SerialPort” is different after version 10.x.x of the Node SerialPort library, and the example I was referencing is using an older version. Later on I stick to refering the officail website and making sure I choose the right version of documentation. After updating the syntax as shown below
const SerialPort = require('serialport') -> const { SerialPort } = require('serialport');
const Readline = require('@serialport/parser-readline') -> const { Readline } = require('@serialport/parser-readline');
I still got the “TypeError: Readline is not a constructor” error message, which is frustrating. After carefully reading the documentation and asking ChatGPT, I channged the name form “Readline” to “ReadlineParser”, as this is what it’s called in the newer version (v12.x.x), and it worked. I thought the name is arbitrary but apparently it’s not. Now with the updated code and matching serial port path & baudrate as my Xiao RP2040, the server.js code looks like this:
const express = require('express');
const { SerialPort } = require('serialport');
const { ReadlineParser } = require('@serialport/parser-readline');
const app = express();
const port = 3000;
const serialPort = new SerialPort({
path: '/dev/cu.usbmodem1101',
baudRate: 9600
});
const parser = serialPort.pipe(new ReadlineParser({ delimiter: '\n' }));
let latestData = "No data yet"; // Default value to show if no data is received
// Listen for data from the Xiao
parser.on('data', (data) => {
latestData = data.trim(); // Store the latest data
console.log(`Received: ${latestData}`); // Log it for debugging
});
// Serve the data via an API endpoint
app.get('/data', (req, res) => {
res.json({ value: latestData });
});
// Serve the HTML interface
app.use(express.static('public'));
// Start the server
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
I then created a very simple index.html just to display the value, and it worked!
<!DOCTYPE html>
<html>
<head>
<title>RP2040 Data</title>
<script>
async function fetchData() {
try {
const response = await fetch('/data');
const data = await response.json();
document.getElementById('data').innerText = data.value;
} catch (error) {
console.error("Error fetching data:", error);
}
}
setInterval(fetchData, 1000); // Fetch data every second
</script>
</head>
<body>
<h1>RP2040 Data</h1>
<p>Value: <span id="data">Loading...</span></p>
</body>
</html>
Which is ugly but is a successful hello world. I forgot to make a screenshot while it’s working since I happily moved on to adding charts.
I then updated the page with chart.js in an attempt to create a line graph showing the changing of angle, also with the help of ChatGPT. I defined the y limit of the chart to be -180 to 180 to include the full data range of the sensor.
<!DOCTYPE html>
<html>
<head>
<title>RP2040 Data Visualization</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
canvas {
max-width: 600px;
margin: auto;
}
</style>
</head>
<body>
<h1>RP2040 Data Visualization</h1>
<canvas id="dataChart"></canvas>
<script>
// Initialize the chart
const ctx = document.getElementById('dataChart');
const chart = new Chart(ctx, {
type: 'line', // Line chart
data: {
labels: [], // Time or sample labels
datasets: [{
label: 'Sensor Data',
data: [], // Sensor data
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 2,
fill: true
}]
},
options: {
responsive: true,
scales: {
x: {
title: {
display: true,
text: 'Time (s)'
}
},
y: {
title: {
display: true,
text: 'Value'
},
beginAtZero: true,
min: -180,
max: 180
}
}
}
});
// Function to fetch data from the server
async function fetchData() {
try {
const response = await fetch('/data');
const result = await response.json();
const value = parseFloat(result.value); // Convert to number
const now = new Date().toLocaleTimeString(); // Timestamp for x-axis
// Add new data to the chart
chart.data.labels.push(now); // Add timestamp to labels
chart.data.datasets[0].data.push(value); // Add value to dataset
// Remove old data if there are too many points
if (chart.data.labels.length > 50) { // Limit to 50 points
chart.data.labels.shift();
chart.data.datasets[0].data.shift();
}
chart.update(); // Update the chart
console.log('Chart labels:', chart.data.labels);
console.log('Chart data:', chart.data.datasets[0].data);
} catch (error) {
console.error("Error fetching data:", error);
}
}
// Fetch data every second
setInterval(fetchData, 1000);
</script>
</body>
</html>
At first, the chart itself is there but no data is showing up, so I added console.log in my code and opened the Console tab in Arc while running, which is the first time I used these functions and it feels cool. It turns out that the data fetched is all NaN. After some poking around I realized the data type I sent from the Xiao RP2040 through serial port is not float but rather characters. Fixed it and now I can see the line updating as I move the transmitter Xiao around!