An easy way to connect to a serial port from a web page.
Note
Since version 1.0.7 default response is an instance of Uint8Array.
To change the response put this code inside your constructor to change the default response.
this.getResponseAsArrayBuffer()
this.getResponseAsArrayHex()
this.getResponseAsUint8Array()
this.getResponseAsString()
Only choose one of them.
- 🔌 Easy serial port connection from web browsers
- 🎯 TypeScript support with full type definitions
- 🧪 Comprehensive test suite with Vitest
- 📦 Multiple module formats (ESM, UMD)
- 🔍 Source maps for debugging
- 🌐 Socket.io integration for remote connections
- ⚡ Event-driven architecture
- 🛡️ Custom error handling with
SerialError
npm install webserial-coreNote
If you are using Linux, you need to add your user to the dialout group to access the serial port.
sudo usermod -a -G dialout $USERAfter that, you need to log out and log in again to apply the changes.
You need to create a new class to configure your device functions. In this example, we are going to create a class to connect to an Arduino device.
The first step is having the Arduino code ready. In this case, we are going to use the following code.
// serial.ino
void setup() {
Serial.begin(9600);
}
void loop() {
if (Serial.available() > 0) { // Check if data to read is available
String comando = Serial.readStringUntil('\n'); // read the data until a new line is found
if(comando.startsWith("CONNECT")){
Serial.println("connected");
} else if (comando.startsWith("CREDITS")) {
Serial.println("created by danidoble");
} else if (comando.startsWith("HI")) {
Serial.println("hello there");
} else {
Serial.println("ara ara, what are you doing?");
}
}
}Create a new class to connect to the device.
// arduino.js
import { Core } from 'webserial-core';
export class Arduino extends Core {
constructor(
{
filters = null,
config_port = {
baudRate: 9600,
dataBits: 8,
stopBits: 1,
parity: "none",
bufferSize: 32768,
flowControl: "none",
},
no_device = 1,
} = {
filters: null,
config_port: {
baudRate: 9600,
dataBits: 8,
stopBits: 1,
parity: "none",
bufferSize: 32768,
flowControl: "none",
},
no_device: 1,
}
) {
super({ filters, config_port, no_device });
this.__internal__.device.type = "arduino";
Devices.registerType(this.__internal__.device.type);
if (Devices.getByNumber(this.typeDevice, no_device)) {
throw new Error(`Device ${this.typeDevice} ${no_device} already exists`);
}
this.__internal__.time.response_connection = 2e3;
this.__internal__.time.response_general = 2e3;
this.__internal__.serial.delay_first_connection = 1_000;
this.#registerAvailableListenersArduino();
Devices.add(this);
this.getResponseAsString();
}
#registerAvailableListenersArduino() {
/*const _ = [
'my_own_event_dispatched',
'my_other_own_event_dispatched',
];
for (const event of _) {
this.serialRegisterAvailableListener(event)
}
*/
}
serialMessage(codex) {
const message = {
code: [],
name: "",
description: "",
request: "",
no_code: 0,
};
message.code = codex;
switch (codex) {
case "connected":
message.name = "connected";
message.description = "Connection established";
message.request = "connect";
message.no_code = 100;
break;
case "created by danidoble":
message.name = "thanks";
message.description = "thanks for using this software";
message.request = "credits";
message.no_code = 101;
break;
case "hello there":
message.name = "hello there";
message.description = "hi human";
message.request = "hi";
message.no_code = 102;
break;
case "ara ara":
message.name = "ara ara";
message.description = "troll";
message.request = "ara ara";
message.no_code = 404;
break;
default:
message.name = "unknown";
message.description = "Unknown command";
message.request = "unknown";
message.no_code = 400;
break;
}
this.dispatch("serial:message", message);
}
serialSetConnectionConstant() {
return this.add0x(this.parseStringToBytes("CONNECT"));
}
async sayCredits() {
const arr = this.parseStringToBytes("CREDITS");
await this.appendToQueue(arr, "credits");
}
async sayHi() {
const arr = this.parseStringToBytes("HI");
await this.appendToQueue(arr, "hi");
}
async sayAra() {
const arr = this.parseStringToBytes("OTHER");
await this.appendToQueue(arr, "ara");
}
async sendCustomCode({ code = "" } = { code: "" }) {
if (typeof code !== "string") throw new Error("Invalid string");
const arr = this.parseStringToBytes(code);
await this.appendToQueue(arr, "custom");
}
}Then you can use the class to connect to your device.
// serialConnection.js
import { Arduino } from './arduino.js';
const arduino = new Arduino();
arduino.on('serial:message', (message) => {
console.log(message);
});
arduino.on('serial:timeout', (data) => {
console.log('serial:timeout', data.detail);
});
// if you need to debug the data sent
// arduino.on('serial:sent', data => {
// console.log('serial:sent',data.detail);
// });
arduino.on('serial:error', (event) => {
document.getElementById('log').innerText += event.detail.message + '\n\n';
});
// eslint-disable-next-line no-unused-vars
arduino.on('serial:disconnected', (event) => {
document.getElementById('log').innerText += 'Disconnected\n\n';
document.getElementById('disconnected').classList.remove('hidden');
document.getElementById('connect').classList.remove('hidden');
document.getElementById("disconnect")?.classList.add("hidden");
});
// eslint-disable-next-line no-unused-vars
arduino.on('serial:connecting', (event) => {
document.getElementById('log').innerText += 'Connecting\n\n';
});
// eslint-disable-next-line no-unused-vars
arduino.on('serial:connected', (event) => {
document.getElementById('log').innerText += 'Connected\n\n';
document.getElementById('disconnected').classList.add('hidden');
document.getElementById('need-permission').classList.add('hidden');
document.getElementById('connect').classList.add('hidden');
document.getElementById("disconnect")?.classList.remove("hidden");
});
// eslint-disable-next-line no-unused-vars
arduino.on('serial:need-permission', (event) => {
document.getElementById('disconnected').classList.remove('hidden');
document.getElementById('need-permission').classList.remove('hidden');
document.getElementById('connect').classList.remove('hidden');
document.getElementById("disconnect")?.classList.add("hidden");
});
// eslint-disable-next-line no-unused-vars
arduino.on('serial:soft-reload', (event) => {
// reset your variables
});
// eslint-disable-next-line no-unused-vars
arduino.on('serial:unsupported', (event) => {
document.getElementById('unsupported').classList.remove('hidden');
});
function tryConnect() {
arduino
.connect()
.then(() => {})
.catch(console.error);
}
document.addEventListener('DOMContentLoaded', () => {
tryConnect();
document.getElementById('connect')?.addEventListener('click', tryConnect);
document.getElementById("disconnect")?.addEventListener("click", async () => {
await board.disconnect().catch(console.error);
document.getElementById('log')?.innerText += 'Disconnected by user\n\n';
});
});But wait still need to create the HTML file.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Webserial</title>
<script src="./serialConnection.js" type="module"></script>
</head>
<body class="bg-neutral-950 text-white p-4 w-full">
<div class="webserial w-full max-w-xl mx-auto grid grid-cols-1 gap-y-4">
<div class="my-6"></div>
<button id="connect" class="hidden px-4 py-3 bg-gray-800 rounded-lg">Connect to serial</button>
<button id="disconnect" class="hidden px-4 py-3 bg-rose-800 rounded-lg">Disconnect device</button>
<div id="need-permission" class="hidden p-4 bg-rose-900 rounded-lg">
Woooah! It seems that you need to give permission to access the serial port.
Please, click the button 'Connect to serial' to try again.
</div>
<div id="disconnected" class="hidden p-4 bg-neutral-900 w-full">
The arduino is disconnected. Please, check the connection.
</div>
<div id="unsupported" class="hidden p-4 bg-orange-700 w-full absolute bottom-0 left-0">
This browser does not support the WebSerial API. Please, use a compatible browser.
</div>
<div id="log" class="bg-neutral-800 p-4 rounded-lg">
Log: <br>
</div>
</div>
<script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,container-queries"></script>
</body>
</html>