Guide to Custom Ground Control Station: Building a Vanilla JS Dashboard to Graph Drone Sensor Data via WebSerial
Custom Ground Control Station: Build a Vanilla JS Dashboard to Graph Drone Sensor Data via WebSerial
Overview
In this tutorial you create a lightweight Ground Control Station (GCS) that runs entirely in the browser. Using the Web Serial API you open a direct serial link to a drone (or any micro‑controller), read live sensor streams, and plot them in real time with Chart.js. No frameworks, no server‑side code – just vanilla JavaScript, HTML, and a dash of CSS.
Prerequisites
- Chrome (89+) or Edge – browsers that support the Web Serial API.
- A drone or development board (e.g., Arduino, Pixhawk) exposing sensor data over UART/USB.
- Basic knowledge of JavaScript, HTML, and CSS.
- Optional: Node.js if you want to serve the page locally.
Project Setup
1️⃣ Create the folder
mkdir vanilla-gcs && cd vanilla-gcs
2️⃣ Add index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Drone Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<!-- UI will be injected by script.js -->
<script src="script.js"></script>
</body>
</html>
3️⃣ Add script.js
// script.js – will be filled later
Understanding the WebSerial API
The Web Serial API lets JavaScript speak directly to serial devices. It works behind a permission prompt, keeping the user in control.
Key objects
navigator.serial– entry point to request ports.SerialPort– represents the opened connection.ReadableStream&WritableStream– for data flow.
Basic connection flow
async function connect() {
// 1️⃣ Prompt user to select a serial port
const port = await navigator.serial.requestPort();
// 2️⃣ Open the port at the expected baud rate
await port.open({ baudRate: 115200 });
// 3️⃣ Set up a text decoder to turn bytes into strings
const decoder = new TextDecoderStream();
const inputDone = port.readable.pipeTo(decoder.writable);
const inputStream = decoder.readable;
// 4️⃣ Listen for incoming lines
const reader = inputStream.getReader();
// … use reader.read() inside a loop
}
Reading Sensor Data from the Drone
Most flight controllers broadcast CSV‑style lines, e.g., ALT,12.3 or GPS,48.8584,2.2945. We parse each line and push the numeric values into an array that feeds Chart.js.
Live‑read loop
async function startReading(port) {
const decoder = new TextDecoderStream();
const inputDone = port.readable.pipeTo(decoder.writable);
const inputStream = decoder.readable;
const reader = inputStream.getReader();
let buffer = '';
while (true) {
const { value, done } = await reader.read();
if (done) break; // port closed
buffer += value;
const lines = buffer.split('\n');
buffer = lines.pop(); // incomplete line stays in buffer
for (const line of lines) {
if (!line.trim()) continue;
processLine(line.trim());
}
}
}
Parsing helper
function processLine(line) {
const [type, ...data] = line.split(',');
switch (type) {
case 'ALT':
altitudeChart.data.labels.push(Date.now());
altitudeChart.data.datasets[0].data.push(parseFloat(data[0]));
break;
case 'TMP':
temperatureChart.data.labels.push(Date.now());
temperatureChart.data.datasets[0].data.push(parseFloat(data[0]));
break;
// Add more cases as needed
}
// Keep only the last 60 points
trimChart(altitudeChart);
trimChart(temperatureChart);
altitudeChart.update('none');
temperatureChart.update('none');
}
function trimChart(chart) {
const max = 60;
if (chart.data.labels.length > max) {
chart.data.labels.splice(0, chart.data.labels.length - max);
chart.data.datasets[0].data.splice(0, chart.data.datasets[0].data.length - max);
}
}
Visualizing Data with Chart.js
Chart.js gives us responsive, animated line charts with minimal code.
Create canvas elements
<div class="card">
<h3>Altitude (m)</h3>
<canvas id="altitudeChart"></canvas>
</div>
<div class="card">
<h3>Temperature (°C)</h3>
<canvas id="temperatureChart"></canvas>
</div>
Instantiate the charts
const chartConfig = (label, color) => ({
type: 'line',
data: {
labels: [],
datasets: [{
label,
borderColor: color,
backgroundColor: 'rgba(0,0,0,0.05)',
data: [],
tension: 0.2,
fill: true,
}]
},
options: {
animation: { duration: 0 },
scales: {
x: { type: 'realtime', display: false },
y: { beginAtZero: true }
},
plugins: {
legend: { display: false }
}
}
});
const altitudeChart = new Chart(
document.getElementById('altitudeChart'),
chartConfig('Altitude', '#6B7C3A')
);
const temperatureChart = new Chart(
document.getElementById('temperatureChart'),
chartConfig('Temperature', '#ff6600')
);
Building a Clean UI with Vanilla JS
Connect button
Status indicator
Wire up the UI
document.getElementById('connectBtn').addEventListener('click', async () => {
try {
const port = await navigator.serial.requestPort();
await port.open({ baudRate: 115200 });
document.getElementById('status').textContent = 'Connected';
startReading(port);
} catch (err) {
console.error('Connection error:', err);
alert('Failed to connect: ' + err);
}
});
Error Handling & Graceful Disconnect
Serial devices can disappear or the user may close the tab. Implement cleanup to avoid memory leaks.
async function disconnect(port) {
if (port && port.readable) {
await port.readable.cancel();
await port.close();
}
document.getElementById('status').textContent = 'Disconnected';
}
window.addEventListener('beforeunload', async () => {
Comments
Post a Comment