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.

The bare minimum

The bare minimum

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!

The line

The line