Back

Ce que vous pouvez faire avec la Web Serial API

Ce que vous pouvez faire avec la Web Serial API

La Web Serial API permet à une page web d’ouvrir un flux d’octets en lecture/écriture vers un périphérique physique communiquant en série — un adaptateur USB-série, une carte de développement microcontrôleur, une imprimante 3D ou un périphérique Bluetooth Classic appairé — sans application native ni extension de navigateur. Elle s’inscrit dans la même lignée d’API d’accès matériel du navigateur que MediaDevices (caméras et microphones), WebUSB (interfaces USB brutes) et WebHID (manettes de jeu et claviers) : la page demande l’accès, le navigateur affiche une boîte de dialogue de permission, et l’utilisateur sélectionne le périphérique.

Avant de vous lancer dans un projet de week-end, il convient de bien cerner les limites de la compatibilité. La Web Serial API est prise en charge sur ordinateur dans Chrome 89+, Edge 89+, Opera 75+ et Firefox 151+, selon le tableau de compatibilité navigateur de MDN ; Safari n’a annoncé aucune prise en charge. L’API requiert un contexte sécurisé (HTTPS ou localhost) et un geste utilisateur pour appeler requestPort() — il est impossible de demander un port au chargement de la page. Cet article présente six catégories concrètes de projets que vous pouvez construire sur cette base, chacune accompagnée du code spécifique à la Web Serial API, du couplage matériel correspondant, et de l’écueil le plus fréquent à éviter.

Points clés à retenir

  • La Web Serial API est prise en charge sur ordinateur dans Chrome 89+, Edge 89+, Opera 75+ et Firefox 151+ ; Safari n’a annoncé aucune prise en charge, et l’API requiert à la fois un contexte sécurisé et un geste utilisateur pour appeler requestPort().
  • La Web Serial API existe indépendamment de WebUSB car les pilotes série du système d’exploitation prennent en charge les adaptateurs USB-série avant que WebUSB puisse y accéder ; les périphériques qui s’énumèrent comme des ports COM ou des nœuds /dev/tty ont donc besoin de la Web Serial API.
  • Appeler reader.releaseLock() avant port.close() permet d’éviter les erreurs « port déjà ouvert » causées par des verrous de flux non libérés lors d’une reconnexion.
  • Depuis Chrome 117, la Web Serial API peut communiquer avec des périphériques Bluetooth Classic RFCOMM/SPP appairés via allowedBluetoothServiceClassIds et les filtres bluetoothServiceClassId — du série sans fil sans appel WebBluetooth séparé.
  • Chrome 130 a ajouté SerialPort.connected, un booléen permettant de distinguer un port physiquement présent d’un port fermé par l’application.

Le modèle minimal connexion-lecture-écriture-nettoyage

Tout projet Web Serial repose sur les quatre mêmes étapes : demander un port, l’ouvrir à un débit en bauds, lire ou écrire via le flux, puis nettoyer en libérant le verrou avant de fermer. L’extrait ci-dessous constitue la référence vers laquelle renvoie le reste de cet article. Il utilise l’argument optionnel filters pour restreindre le sélecteur de port du navigateur par identifiant de fournisseur et de produit USB — conformément à la définition de SerialPortRequestOptions dans la spécification WICG Web Serial, cela exclut les ports série non pertinents afin que l’utilisateur ne voie que les périphériques appropriés.

// Doit être exécuté dans un gestionnaire de geste utilisateur (ex. : un écouteur de clic).
const port = await navigator.serial.requestPort({
  filters: [{ usbVendorId: 0x2341 }], // optionnel ; omettre pour "tout port"
});
await port.open({ baudRate: 115200 });

const reader = port.readable.getReader();
try {
  while (true) {
    const { value, done } = await reader.read();
    if (done) break;          // le reader a été annulé
    console.log(value);       // value est un chunk Uint8Array
  }
} finally {
  reader.releaseLock();       // libérer AVANT de fermer
  await port.close();
}

Ne pas appeler reader.releaseLock() avant port.close() est une cause fréquente d’erreurs « port déjà ouvert » lors d’une reconnexion — le verrou maintient le flux lisible, et close() est rejeté tant qu’un flux est verrouillé, conformément aux étapes de SerialPort.close() dans la spécification. Pour les protocoles textuels, faites transiter les flux par TextEncoderStream et TextDecoderStream plutôt que de décoder manuellement les chunks Uint8Array bruts ; le guide Web Serial de MDN couvre les détails d’encodage.

Exemples d’utilisation de la Web Serial API : six projets pour un week-end

Les six catégories de projets les plus adaptées à la Web Serial API sont : les outils de débogage à lecture intensive, les clients de protocole à écriture intensive, les boucles de streaming longue durée, la visualisation en temps réel, les affichages pilotés depuis le navigateur, et les interfaces de configuration de périphériques. Chaque entrée ci-dessous isole la logique propre à la Web Serial API plutôt que de parcourir une application complète.

1. Un moniteur série dans le navigateur

Un moniteur série est l’outil de débogage à lecture intensive qui remplace le Moniteur Série de l’IDE Arduino ou screen /dev/ttyUSB0 par un onglet de navigateur. Il ouvre un port, fait transiter les octets entrants par un décodeur et ajoute les lignes décodées au DOM. C’est le projet le plus simple présenté ici et le meilleur point de départ, car il met en œuvre la boucle de lecture sans les complications d’un protocole d’écriture.

Couplage matériel/protocole : tout périphérique UART — un Arduino, un ESP32, un adaptateur USB-série — émettant du texte délimité par des sauts de ligne.

const port = await navigator.serial.requestPort();
await port.open({ baudRate: 9600 });

const decoder = new TextDecoderStream();
port.readable.pipeTo(decoder.writable);
const reader = decoder.readable.getReader();

let buffer = "";
while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  buffer += value;
  const lines = buffer.split("\n");
  buffer = lines.pop() ?? "";        // conserver la dernière ligne partielle
  lines.forEach(line => appendToLog(line));
}

Écueil : les données série arrivent avec des découpages arbitraires, pas nécessairement en lignes entières — un seul read() peut retourner une demi-ligne ou trois lignes, il faut donc buffériser jusqu’à rencontrer un délimiteur avant d’afficher.

2. Un outil de flashage de firmware

Un outil de flashage de firmware écrit un binaire compilé dans la mémoire flash d’un microcontrôleur via le port série, à la manière d’un outil esptool dans le navigateur pour les puces ESP32. Il est à forte composante écriture et lié à un protocole précis : avant tout transfert de données utiles, la puce doit être mise en mode bootloader, et les données sont encapsulées selon le protocole de flashage de la puce.

Couplage matériel/protocole : ESP32/ESP8266 via le protocole bootloader série encapsulé SLIP ; la séquence d’entrée est pilotée par les signaux de contrôle DTR et RTS.

const port = await navigator.serial.requestPort();
await port.open({ baudRate: 115200 });

// Basculer les signaux de contrôle pour forcer l'ESP en mode téléchargement.
// Voir la séquence de boot-mode documentée par Espressif.
await port.setSignals({ dataTerminalReady: false, requestToSend: true });
await port.setSignals({ dataTerminalReady: true, requestToSend: false });
// ...envoyer ensuite les commandes bootloader encapsulées via port.writable

Les outils de flashage de type ESPTool doivent basculer les signaux de contrôle DTR et RTS dans une séquence précise pour forcer la puce en mode téléchargement avant d’envoyer toute commande d’écriture ; Espressif documente le comportement des signaux de boot-mode, et la Web Serial API expose ces bascules via SerialPort.setSignals().

Écueil : si la séquence DTR/RTS du bootloader est omise, la puce reste dans son firmware applicatif au lieu d’entrer en mode bootloader série, ce qui empêche le flashage de se dérouler correctement.

3. Un streamer de jobs G-code

Un streamer G-code envoie un travail d’impression ou de découpe à une imprimante 3D ou une machine CNC ligne par ligne, en attendant que le firmware accuse réception de chaque ligne avant d’envoyer la suivante. Il s’agit d’une boucle d’écriture longue durée avec contrôle de flux, ce qui en fait un projet sensiblement plus complexe qu’un simple envoi sans accusé de réception.

Couplage matériel/protocole : firmware Marlin ou GRBL via USB série, échangeant du G-code ligne par ligne avec des accusés de réception ok.

const encoder = new TextEncoderStream();
encoder.readable.pipeTo(port.writable);
const writer = encoder.writable.getWriter();

const decoder = new TextDecoderStream();
port.readable.pipeTo(decoder.writable);
const reader = decoder.readable.getReader();

for (const line of gcodeLines) {
  await writer.write(line + "\n");
  // Bloquer jusqu'à ce que le firmware confirme avoir traité la ligne.
  let ack = "";
  while (!ack.includes("ok")) {
    const { value } = await reader.read();
    ack += value;
  }
}

Les streamers G-code doivent attendre l’accusé de réception ok de l’imprimante après chaque ligne avant d’envoyer la suivante ; la référence G-code RepRap définit ce mécanisme de handshake, et inonder le buffer série sans l’appliquer provoque un débordement de la file de commandes du firmware en cours de travail.

Écueil : le ok peut arriver entremêlé avec des rapports de température et d’autres lignes non sollicitées ; il faut donc rechercher le token plutôt que de supposer que la prochaine ligne lue est l’accusé de réception.

4. Un tableau de bord de télémétrie en temps réel

Un tableau de bord de télémétrie lit un flux continu de données de capteurs depuis un port série et les affiche sous forme de graphique en direct dans le navigateur — température, tension, axes d’accéléromètre, positions GPS. La partie Web Serial se résume à une boucle de lecture ; la valeur ajoutée réside dans le fait d’injecter directement les valeurs décodées dans une bibliothèque de graphiques s’exécutant dans le même onglet.

Couplage matériel/protocole : tout microcontrôleur équipé de capteurs émettant des lignes CSV, ou un module GPS émettant des trames NMEA 0183.

const decoder = new TextDecoderStream();
port.readable.pipeTo(decoder.writable);
const reader = decoder.readable.getReader();

let buffer = "";
while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  buffer += value;
  let nl;
  while ((nl = buffer.indexOf("\n")) >= 0) {
    const [t, v] = buffer.slice(0, nl).split(",");
    pushSample(Number(t), Number(v));   // alimenter votre graphique
    buffer = buffer.slice(nl + 1);
  }
}

Écueil : un capteur rapide peut émettre des lignes plus vite que le graphique ne peut se redessiner ; regroupez les échantillons et mettez à jour l’affichage via requestAnimationFrame plutôt que de redessiner à chaque read(), sous peine de bloquer le thread principal.

5. Un contrôleur d’affichage ou visuel

Un contrôleur d’affichage calcule des pixels ou des trames dans le navigateur et les envoie à un écran physique — une matrice de LEDs, un écran OLED ou un panneau ePaper. Ici, le navigateur joue le rôle de moteur de rendu : vous calculez un bitmap ou un tableau de luminosité en JavaScript et écrivez les octets attendus par le contrôleur de l’écran.

Couplage matériel/protocole : une matrice de LEDs pilotée par un MAX7219, un écran OLED SSD1306, ou un module ePaper derrière un microcontrôleur acceptant des octets de framebuffer via UART.

const writer = port.writable.getWriter();

// Trame 8x8 : un octet par ligne (bit à 1 = LED allumée).
function renderFrame(rows: number[]) {
  return writer.write(Uint8Array.from(rows));
}

await renderFrame([
  0b00111100, 0b01000010, 0b10100101, 0b10000001,
  0b10100101, 0b10011001, 0b01000010, 0b00111100,
]);

Écueil : les écritures ne sont pas automatiquement synchronisées avec le taux de rafraîchissement de l’écran ; animer depuis le navigateur peut provoquer des déchirures ou des pertes de trames. Envoyez une trame complète par mise à jour et laissez le microcontrôleur gérer le timing propre à l’écran, plutôt que de streamer des lignes partielles.

6. Une interface de configuration de périphérique

Une interface de configuration de périphérique lit et écrit des paramètres sur un périphérique connecté en série — paramètres d’un contrôleur de vol sur un drone, mémoires de canaux d’une radio amateur, valeurs de registres d’un module IoT. C’est ici que le filtrage par VID/PID prend tout son sens : les applications de configuration ciblent un périphérique connu, et filtrer le sélecteur de port par identifiant de fournisseur et de produit garantit que l’utilisateur ne voit que son périphérique, et non tous les ports COM de la machine.

Couplage matériel/protocole : un contrôleur de vol utilisant le protocole MSP, le protocole CAT/CI-V d’une radio, ou tout module disposant d’un jeu de commandes d’accès aux registres documenté — sélectionné avec les filtres requestPort.

// Restreindre le sélecteur à un fournisseur/produit connu.
const port = await navigator.serial.requestPort({
  filters: [{ usbVendorId: 0x10c4, usbProductId: 0xea60 }], // exemple CP210x
});
await port.open({ baudRate: 115200 });

const writer = port.writable.getWriter();
await writer.write(buildReadConfigCommand());   // demander les paramètres actuels
// ...lire la réponse, remplir les champs du formulaire, écrire à la sauvegarde

Passer filters: [{ usbVendorId, usbProductId }] à requestPort() restreint la boîte de dialogue aux ports correspondants, conformément à la spécification SerialPortRequestOptions. Pour éviter la boîte de dialogue lors d’une visite ultérieure, navigator.serial.getPorts() retourne les ports déjà approuvés par l’utilisateur.

Écueil : les identifiants fournisseur/produit affichés dans le sélecteur proviennent de la puce pont USB-série (FTDI, CP210x, CH340), et non du périphérique final ; plusieurs produits sans lien peuvent donc partager le même usbVendorId. Filtrez sur usbVendorId et usbProductId conjointement dès que possible.

Quand la Web Serial API est-elle le bon outil par rapport aux alternatives ?

Utilisez la Web Serial API lorsque le périphérique s’énumère comme un port COM ou un nœud /dev/tty — c’est-à-dire lorsque le système d’exploitation a déjà chargé un pilote série pour lui. La Web Serial API existe indépendamment de WebUSB car les pilotes série du système d’exploitation prennent en charge les adaptateurs USB-série avant que WebUSB puisse y accéder, une lacune que l’explainer WICG Web Serial décrit explicitement. Pour d’autres classes de périphériques, il convient d’utiliser une API différente.

APIType de périphériqueModèle de permissionCompatibilité navigateurIdéal pour
Web SerialPériphériques sur port série/COM/ttySélecteur par geste utilisateurChrome/Edge 89+, Opera 75+, Firefox 151+Microcontrôleurs, imprimantes, dongles USB-série
WebUSBInterfaces USB brutes (sans pilote OS)Sélecteur par geste utilisateurBasé sur ChromiumPériphériques USB personnalisés sans pilote série
WebHIDPériphériques d’interface humaineSélecteur par geste utilisateurBasé sur ChromiumManettes de jeu, claviers, périphériques HID personnalisés
Web BluetoothBluetooth Low Energy (GATT)Sélecteur par geste utilisateurBasé sur ChromiumCapteurs BLE, balises, objets connectés
WebSocket + daemon backendTout typeMédié par le serveurTous les navigateursPortée multi-navigateur, traitement côté serveur

Si vous avez besoin de prendre en charge les utilisateurs de Firefox et Safari dès aujourd’hui et que la logique du périphérique peut résider côté serveur, un petit daemon natif exposant un WebSocket constitue la solution de repli portable — au prix d’une étape d’installation que la Web Serial API évite.

Ajouts récents à connaître

Chrome 117 a ajouté la prise en charge de Bluetooth Classic RFCOMM/SPP à la Web Serial API, et Chrome 130 a introduit le booléen SerialPort.connected pour distinguer les ports physiquement présents de ceux fermés par l’application. Ces deux fonctionnalités sont absentes de la plupart des tutoriels existants et méritent d’être intégrées dans vos projets.

Depuis Chrome 117 sur ordinateur, la Web Serial API peut communiquer avec des périphériques Bluetooth Classic RFCOMM/SPP appairés : passez allowedBluetoothServiceClassIds (ou une entrée filters: [{ bluetoothServiceClassId }]) pour faire apparaître les services RFCOMM personnalisés dans le sélecteur requestPort(), le service class étant accessible via port.getInfo().bluetoothServiceClassId — sans appel WebBluetooth séparé. Cette voie Bluetooth est réservée à Chromium (Chrome/Edge 117+, Opera 103+) et est encore marquée comme expérimentale sur MDN ; la prise en charge générale de la Web Serial API dans Firefox 151 n’inclut pas encore les options de service class Bluetooth. Les détails sont disponibles dans l’article Chrome Serial over Bluetooth on the web. Cela transforme le série sans fil en le même flux lecture/écriture que vous avez déjà écrit pour USB.

Chrome 130 a ajouté SerialPort.connected, un booléen qui vaut true lorsque le port est physiquement présent mais pas nécessairement ouvert. Il permet à l’interface de reconnexion de distinguer « périphérique débranché » de « port fermé par l’application » — associez-le aux événements connect et disconnect pour piloter un indicateur de connexion en temps réel sans recourir au polling.

requestPort() est rejeté silencieusement lorsque l’utilisateur ferme la boîte de dialogue de permission de la Web Serial API, et la plupart des implémentations ne restituent pas ce rejet sous forme d’état visible ; le bouton de connexion revient simplement à son état par défaut et la page semble ne rien faire. Ce cas d’échec UX unique se retrouve dans les six catégories de projets présentées. Les replays de session sur des flux de connexion Web Serial révèlent souvent un schéma caractéristique : les utilisateurs cliquent deux ou trois fois de suite sur le bouton de connexion avant d’abandonner — signe révélateur que le rejet n’est pas communiqué. Interceptez le rejet de requestPort() et affichez un message explicite « aucun périphérique sélectionné » ou « port déjà utilisé » ; les ports détenus par un autre processus (l’IDE Arduino, screen ou ModemManager) échouent de la même manière silencieuse.

Choisissez une catégorie, câblez le bloc minimal connexion-lecture-écriture-nettoyage présenté en début d’article, et vérifiez que la matrice de compatibilité correspond à votre audience avant de consacrer un week-end au projet. Le chemin le plus rapide vers un build fonctionnel est un moniteur série connecté à une carte que vous possédez déjà — une fois la boucle de lecture et le nettoyage bien en place, les cinq autres catégories ne sont que des variations sur le même flux d’octets. La Web Serial API est en revanche le mauvais outil lorsque votre audience inclut des utilisateurs de Safari, lorsque le périphérique ne s’énumère pas comme un port COM ou un nœud /dev/tty (préférez alors WebUSB, WebHID ou Web Bluetooth), ou lorsque la logique de traitement appartient véritablement au côté serveur — dans ces cas, un petit daemon natif derrière un WebSocket offre une portée plus large au prix d’une étape d’installation.

FAQ

Fermer la boîte de dialogue de permission provoque le rejet de la promesse requestPort() plutôt que sa résolution, et la plupart des implémentations ne restituent jamais ce rejet sous forme d'état visible, si bien que le bouton revient silencieusement à son état par défaut. Encapsulez l'appel requestPort() dans un bloc try/catch et affichez un message explicite « aucun périphérique sélectionné » en cas de rejet. Les ports déjà détenus par un autre processus comme l'IDE Arduino, screen ou ModemManager échouent de la même manière silencieuse.

Utilisez la Web Serial API pour un Arduino car le système d'exploitation charge un pilote série qui prend en charge l'adaptateur USB-série, faisant s'énumérer la carte comme un port COM ou un nœud /dev/tty que WebUSB ne peut pas atteindre. WebUSB est destiné aux interfaces USB brutes sans pilote série OS, comme les périphériques USB personnalisés. Si un périphérique apparaît comme un port COM, il nécessite la Web Serial API, pas WebUSB.

Oui. Appelez navigator.serial.getPorts() pour récupérer le tableau des ports déjà approuvés par l'utilisateur lors de sessions précédentes, puis ouvrez-en un directement sans invite. Cela contourne entièrement le sélecteur par geste utilisateur pour les visites ultérieures. Dans Chrome 130 et versions ultérieures, SerialPort.connected retourne un booléen indiquant si le port est physiquement présent, permettant à l'interface de reconnexion de distinguer un périphérique débranché d'un port simplement fermé par l'application.

La Web Serial API fonctionne sur ordinateur dans Chrome 89+, Edge 89+, Opera 75+ et Firefox 151+, selon les données de compatibilité navigateur de MDN. Safari n'a annoncé aucune prise en charge et n'a publié aucune feuille de route. L'API requiert également un contexte sécurisé, c'est-à-dire HTTPS ou localhost, ainsi qu'un geste utilisateur pour appeler requestPort(). Si vous avez besoin d'une couverture large de Firefox et Safari dès aujourd'hui, un daemon natif exposant un WebSocket constitue la solution de repli portable au prix d'une étape d'installation.

Une erreur « port déjà ouvert » lors d'une reconnexion signifie presque toujours qu'un verrou de flux n'a pas été libéré. Si vous appelez port.close() alors qu'un reader détient encore le flux lisible, la fermeture est rejetée car le flux est verrouillé. Appelez toujours reader.releaseLock() avant port.close(), idéalement dans un bloc finally, afin que le verrou soit libéré quelle que soit la façon dont la boucle de lecture se termine. Il en va de même pour les writers obtenus depuis port.writable.

Gain control over your UX

See how users are using your site as if you were sitting next to them, learn and iterate faster with OpenReplay. — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay