Lettura e scrittura su una porta seriale

L'API Web Serial consente ai siti web di comunicare con i dispositivi seriali.

François Beaufort
François Beaufort

Che cos'è l'API Web Serial?

Una porta seriale è un'interfaccia di comunicazione bidirezionale che consente di inviare e che riceve dati byte per byte.

L'API Web Serial consente ai siti web di leggere e scrivere su un dispositivo seriale con JavaScript. I dispositivi seriali sono collegati tramite un porta seriale sul sistema dell'utente o tramite dispositivi USB e Bluetooth rimovibili che emula una porta seriale.

In altre parole, l'API Web Serial collega il web e il mondo fisico tramite consentendo ai siti web di comunicare con dispositivi seriali, come i microcontroller e stampanti 3D.

Questa API è inoltre un'ottima compagna di WebUSB, in quanto i sistemi operativi richiedono per comunicare con alcune porte seriali usando i loro modelli l'API seriale anziché l'API USB di basso livello.

Casi d'uso suggeriti

Nel settore educativo, amatoriale e industriale, gli utenti connettono ai propri computer. Questi dispositivi sono spesso controllati tramite una connessione seriale usata da un software personalizzato. Alcune software per controllare questi dispositivi è realizzato con tecnologia web:

In alcuni casi, i siti web comunicano con il dispositivo tramite un agente che gli utenti hanno installato manualmente. In altri casi, l'applicazione forniti in un'applicazione pacchettizzata tramite un framework come Electron. In altri casi, l'utente deve eseguire un passaggio aggiuntivo come copiando un'applicazione compilata sul dispositivo tramite un'unità flash USB.

In tutti questi casi, l'esperienza utente verrà migliorata fornendo servizi diretti la comunicazione tra il sito web e il dispositivo che controlla.

Stato attuale

Passaggio Stato
1. Crea messaggio esplicativo Completato
2. Crea la bozza iniziale delle specifiche Completato
3. Raccogli feedback e esegui l'iterazione del design Completato
4. Prova dell'origine Completato
5. lancio Completato

Utilizzo dell'API Web Serial

Rilevamento delle caratteristiche

Per verificare se l'API Web Serial è supportata, utilizza:

if ("serial" in navigator) {
  // The Web Serial API is supported.
}

Apri una porta seriale

L'API Web Serial è asincrona per definizione. Questo impedisce all'interfaccia utente del sito web blocco in attesa di input, il che è importante perché i dati seriali ricevute in qualsiasi momento, richiedendo un modo per ascoltarle.

Per aprire una porta seriale, devi prima accedere a un oggetto SerialPort. A questo scopo, puoi puoi chiedere all'utente di selezionare una singola porta seriale chiamando navigator.serial.requestPort() in risposta al gesto dell'utente, ad esempio il tocco o un clic del mouse, oppure scegline una da navigator.serial.getPorts() che restituisce un elenco di porte seriali a cui è stato concesso l'accesso al sito web.

document.querySelector('button').addEventListener('click', async () => {
  // Prompt user to select any serial port.
  const port = await navigator.serial.requestPort();
});
// Get all serial ports the user has previously granted the website access to.
const ports = await navigator.serial.getPorts();

La funzione navigator.serial.requestPort() prende un valore letterale oggetto facoltativo che definisce i filtri. che si abbinano a qualsiasi dispositivo seriale collegato USB con un fornitore USB obbligatorio (usbVendorId) e un prodotto USB facoltativo identificatori (usbProductId).

// Filter on devices with the Arduino Uno USB Vendor/Product IDs.
const filters = [
  { usbVendorId: 0x2341, usbProductId: 0x0043 },
  { usbVendorId: 0x2341, usbProductId: 0x0001 }
];

// Prompt user to select an Arduino Uno device.
const port = await navigator.serial.requestPort({ filters });

const { usbProductId, usbVendorId } = port.getInfo();
Screenshot di una richiesta di porta seriale su un sito web
Prompt dell'utente per la selezione di un micro:bit della BBC

La chiamata a requestPort() richiede all'utente di selezionare un dispositivo e restituisce un Oggetto SerialPort. Quando hai un oggetto SerialPort, la chiamata a port.open() con la velocità di trasmissione desiderata, aprirà la porta seriale. Dizionario baudRate specifica la velocità di invio dei dati su una linea seriale. È espresso in di bit al secondo (bps). Controlla la documentazione del dispositivo per corretto in quanto tutti i dati inviati e ricevuti saranno senza senso se si tratta di non è specificato correttamente. Per alcuni dispositivi USB e Bluetooth che emulano un numero seriale porta questo valore può essere tranquillamente impostato su qualsiasi valore, in quanto viene ignorato dal dell'emulazione.

// Prompt user to select any serial port.
const port = await navigator.serial.requestPort();

// Wait for the serial port to open.
await port.open({ baudRate: 9600 });

Puoi anche specificare una delle opzioni seguenti quando apri una porta seriale. Questi Le opzioni sono facoltative e hanno pratici valori predefiniti.

  • dataBits: il numero di bit di dati per frame (7 o 8).
  • stopBits: il numero di bit di stop alla fine di un frame (1 o 2).
  • parity: la modalità di parità ("none", "even" o "odd").
  • bufferSize: la dimensione dei buffer di lettura e scrittura che devono essere creati (deve essere inferiore a 16 MB).
  • flowControl: la modalità di controllo del flusso ("none" o "hardware").

Lettura da una porta seriale

I flussi di input e output nell'API Web Serial vengono gestiti dall'API Streams.

Una volta stabilita la connessione alla porta seriale, i readable e writable dell'oggetto SerialPort restituiscono un ReadableStream e un WritableStream. Verranno utilizzati per ricevere e inviare dati ai dispositivo seriale. Entrambi utilizzano istanze Uint8Array per il trasferimento di dati.

Quando arrivano nuovi dati dal dispositivo seriale, port.readable.getReader().read() restituisce due proprietà in modo asincrono: value e un valore booleano done. Se done è true, la porta seriale è stata chiusa o non sono disponibili altri dati in. La chiamata a port.readable.getReader() crea un lettore e blocca readable a li annotino. Mentre readable è bloccato, la porta seriale non può essere chiusa.

const reader = port.readable.getReader();

// Listen to data coming from the serial device.
while (true) {
  const { value, done } = await reader.read();
  if (done) {
    // Allow the serial port to be closed later.
    reader.releaseLock();
    break;
  }
  // value is a Uint8Array.
  console.log(value);
}

Alcuni errori di lettura non irreversibili delle porte seriali possono verificarsi in determinate condizioni, ad esempio overflow del buffer, errori di inquadratura o di parità. Vengono lanciate come e può essere rilevato aggiungendo un altro loop sopra il precedente che controlla port.readable. Questo funziona perché finché gli errori vengono non irreversibile, viene creato automaticamente un nuovo ReadableStream. Se si verifica un errore irreversibile quando il dispositivo seriale viene rimosso, port.readable diventa null.

while (port.readable) {
  const reader = port.readable.getReader();

  try {
    while (true) {
      const { value, done } = await reader.read();
      if (done) {
        // Allow the serial port to be closed later.
        reader.releaseLock();
        break;
      }
      if (value) {
        console.log(value);
      }
    }
  } catch (error) {
    // TODO: Handle non-fatal read error.
  }
}

Se il dispositivo seriale invia un messaggio, puoi inviare port.readable tramite un TextDecoderStream come mostrato di seguito. Un TextDecoderStream è un flusso di trasformazione che prende tutti i blocchi Uint8Array e li converte in stringhe.

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

// Listen to data coming from the serial device.
while (true) {
  const { value, done } = await reader.read();
  if (done) {
    // Allow the serial port to be closed later.
    reader.releaseLock();
    break;
  }
  // value is a string.
  console.log(value);
}

Puoi controllare la modalità di allocazione della memoria quando leggi dallo stream utilizzando un "Bring Your Own Buffer" Reader. Chiama port.readable.getReader({ mode: "byob" }) per ottenere l'interfaccia ReadableStreamBYOBReader e fornisci il tuo ArrayBuffer quando chiami read(). Tieni presente che l'API Web Serial supporta questa funzionalità in Chrome 106 o versioni successive.

try {
  const reader = port.readable.getReader({ mode: "byob" });
  // Call reader.read() to read data into a buffer...
} catch (error) {
  if (error instanceof TypeError) {
    // BYOB readers are not supported.
    // Fallback to port.readable.getReader()...
  }
}

Ecco un esempio di come riutilizzare il buffer di value.buffer:

const bufferSize = 1024; // 1kB
let buffer = new ArrayBuffer(bufferSize);

// Set `bufferSize` on open() to at least the size of the buffer.
await port.open({ baudRate: 9600, bufferSize });

const reader = port.readable.getReader({ mode: "byob" });
while (true) {
  const { value, done } = await reader.read(new Uint8Array(buffer));
  if (done) {
    break;
  }
  buffer = value.buffer;
  // Handle `value`.
}

Ecco un altro esempio di come leggere una quantità specifica di dati da una porta seriale:

async function readInto(reader, buffer) {
  let offset = 0;
  while (offset < buffer.byteLength) {
    const { value, done } = await reader.read(
      new Uint8Array(buffer, offset)
    );
    if (done) {
      break;
    }
    buffer = value.buffer;
    offset += value.byteLength;
  }
  return buffer;
}

const reader = port.readable.getReader({ mode: "byob" });
let buffer = new ArrayBuffer(512);
// Read the first 512 bytes.
buffer = await readInto(reader, buffer);
// Then read the next 512 bytes.
buffer = await readInto(reader, buffer);

Scrivi su una porta seriale

Per inviare i dati a un dispositivo seriale, passali a port.writable.getWriter().write(). Chiamata a releaseLock() attiva È richiesto port.writable.getWriter() per chiudere la porta seriale in un secondo momento.

const writer = port.writable.getWriter();

const data = new Uint8Array([104, 101, 108, 108, 111]); // hello
await writer.write(data);


// Allow the serial port to be closed later.
writer.releaseLock();

Invia un messaggio al dispositivo tramite un TextEncoderStream trasmesso al numero port.writable come mostrato di seguito.

const textEncoder = new TextEncoderStream();
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);

const writer = textEncoder.writable.getWriter();

await writer.write("hello");

Chiudere una porta seriale

port.close() chiude la porta seriale se i relativi membri readable e writable sono sbloccati, ovvero releaseLock() è stato chiamato per il rispettivo Reader e writer.

await port.close();

Tuttavia, durante la lettura continua dei dati da un dispositivo seriale tramite un loop, port.readable sarà sempre bloccato finché non riscontra un errore. In questo richiesta, la chiamata al numero reader.cancel() forzerà la risoluzione di reader.read() con { value: undefined, done: true }, consentendo quindi loop per chiamare reader.releaseLock().

// Without transform streams.

let keepReading = true;
let reader;

async function readUntilClosed() {
  while (port.readable && keepReading) {
    reader = port.readable.getReader();
    try {
      while (true) {
        const { value, done } = await reader.read();
        if (done) {
          // reader.cancel() has been called.
          break;
        }
        // value is a Uint8Array.
        console.log(value);
      }
    } catch (error) {
      // Handle error...
    } finally {
      // Allow the serial port to be closed later.
      reader.releaseLock();
    }
  }

  await port.close();
}

const closedPromise = readUntilClosed();

document.querySelector('button').addEventListener('click', async () => {
  // User clicked a button to close the serial port.
  keepReading = false;
  // Force reader.read() to resolve immediately and subsequently
  // call reader.releaseLock() in the loop example above.
  reader.cancel();
  await closedPromise;
});

Chiudere una porta seriale è più complicato quando si utilizzano stream di trasformazione. Chiama reader.cancel() come prima. Poi chiama writer.close() e port.close(). In questo modo gli errori vengono propagati i flussi della trasformazione verso la porta seriale sottostante. Poiché la propagazione degli errori non avviene immediatamente, devi usare readableStreamClosed e writableStreamClosed promesse create in anticipo per rilevare quando port.readable e port.writable sono stati sbloccati. L'annullamento di reader fa sì che il flusso di dati da interrompere; per questo devi individuare e ignorare l'errore risultante.

// With transform streams.

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

// Listen to data coming from the serial device.
while (true) {
  const { value, done } = await reader.read();
  if (done) {
    reader.releaseLock();
    break;
  }
  // value is a string.
  console.log(value);
}

const textEncoder = new TextEncoderStream();
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);

reader.cancel();
await readableStreamClosed.catch(() => { /* Ignore the error */ });

writer.close();
await writableStreamClosed;

await port.close();

Ascolta connessione e disconnessione

Se un dispositivo USB fornisce una porta seriale, questo potrebbe essere collegato o disconnesso dal sistema. Quando al sito web viene concessa l'autorizzazione accede a una porta seriale, deve monitorare gli eventi connect e disconnect.

navigator.serial.addEventListener("connect", (event) => {
  // TODO: Automatically open event.target or warn user a port is available.
});

navigator.serial.addEventListener("disconnect", (event) => {
  // TODO: Remove |event.target| from the UI.
  // If the serial port was opened, a stream error would be observed as well.
});

Gestire gli indicatori

Dopo aver stabilito la connessione alla porta seriale, puoi eseguire query e impostare e segnali esposti dalla porta seriale per il rilevamento dei dispositivi e il controllo del flusso. Questi sono definiti come valori booleani. Ad esempio, alcuni dispositivi come Arduino entrerà in una modalità di programmazione se il segnale Terminale dati pronto (DTR) attivata/disattivata.

L'impostazione dei segnali di output e la ricezione dei segnali di input vengono eseguiti rispettivamente chiamata a port.setSignals() e port.getSignals(). Consulta gli esempi di utilizzo riportati di seguito.

// Turn off Serial Break signal.
await port.setSignals({ break: false });

// Turn on Data Terminal Ready (DTR) signal.
await port.setSignals({ dataTerminalReady: true });

// Turn off Request To Send (RTS) signal.
await port.setSignals({ requestToSend: false });
const signals = await port.getSignals();
console.log(`Clear To Send:       ${signals.clearToSend}`);
console.log(`Data Carrier Detect: ${signals.dataCarrierDetect}`);
console.log(`Data Set Ready:      ${signals.dataSetReady}`);
console.log(`Ring Indicator:      ${signals.ringIndicator}`);

Trasformazione dei flussi

Quando ricevi i dati dal dispositivo seriale, non avrai necessariamente tutti contemporaneamente i dati. Può essere suddiviso arbitrariamente. Per ulteriori informazioni, vedi Concetti dell'API Streams.

Per risolvere questo problema, puoi utilizzare alcuni flussi di trasformazione integrati come TextDecoderStream o creare il tuo flusso di trasformazione che ti consente di analizzare il flusso in entrata e restituire i dati analizzati. Il flusso di trasformazione si trova tra il dispositivo seriale e il loop di lettura che utilizza il flusso. Può una trasformazione arbitraria prima che i dati vengano consumati. È una sorta di catena di montaggio: man mano che un widget scende lungo la linea, ogni passaggio della linea modifica del widget, in modo che quando arriva alla destinazione finale, widget funzionante.

Foto di una fabbrica di aerei
Fabbrica di aeroplani del Castello di Bromwich della Seconda Guerra Mondiale

Ad esempio, considera come creare una classe di flusso di trasformazione che utilizza un lo streaming e lo suddivide in base alle interruzioni di riga. Il suo metodo transform() è chiamato ogni volta che lo stream riceve nuovi dati. Può accodare i dati e conservarli per un secondo momento. Il metodo flush() viene chiamato alla chiusura dello stream e gestisce tutti i dati non ancora elaborati.

Per utilizzare la classe del flusso di trasformazione, devi indirizzare un flusso in entrata attraverso li annotino. Nel terzo esempio di codice in Lettura da una porta seriale, lo stream di input originale è stato trasmesso solo attraverso un TextDecoderStream, quindi dobbiamo chiamare pipeThrough() per trasmetterlo attraverso il nostro nuovo LineBreakTransformer.

class LineBreakTransformer {
  constructor() {
    // A container for holding stream data until a new line.
    this.chunks = "";
  }

  transform(chunk, controller) {
    // Append new chunks to existing chunks.
    this.chunks += chunk;
    // For each line breaks in chunks, send the parsed lines out.
    const lines = this.chunks.split("\r\n");
    this.chunks = lines.pop();
    lines.forEach((line) => controller.enqueue(line));
  }

  flush(controller) {
    // When the stream is closed, flush any remaining chunks out.
    controller.enqueue(this.chunks);
  }
}
const textDecoder = new TextDecoderStream();
const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
const reader = textDecoder.readable
  .pipeThrough(new TransformStream(new LineBreakTransformer()))
  .getReader();

Per eseguire il debug dei problemi di comunicazione con dispositivi seriali, usa il metodo tee() di port.readable per suddividere gli stream da o verso il dispositivo seriale. I due I flussi creati possono essere consumati in modo indipendente e questo consente di stampare alla console per l'ispezione.

const [appReadable, devReadable] = port.readable.tee();

// You may want to update UI with incoming data from appReadable
// and log incoming data in JS console for inspection from devReadable.

Revocare l'accesso a una porta seriale

Il sito web può eliminare le autorizzazioni per accedere a una porta seriale che non è più che ti interessa mantenere chiamando forget() sull'istanza SerialPort. Per per un'applicazione web didattica utilizzata su un computer condiviso con molte dispositivi, un numero elevato di autorizzazioni generate dagli utenti un'esperienza utente positiva.

// Voluntarily revoke access to this serial port.
await port.forget();

Poiché forget() è disponibile in Chrome 103 o versioni successive, controlla se questa funzionalità è supportati con quanto segue:

if ("serial" in navigator && "forget" in SerialPort.prototype) {
  // forget() is supported.
}

Suggerimenti per gli sviluppatori

Il debug dell'API Web Serial in Chrome è semplice grazie alla pagina interna, about://device-log, dove puoi vedere tutti gli eventi relativi ai dispositivi seriali in uno in un unico posto.

Screenshot della pagina interna per il debug dell&#39;API Web Serial.
Pagina interna in Chrome per il debug dell'API Web Serial.

Codelab

Nel codelab di Google Developers, utilizzerai l'API Web Serial per interagire con una scheda BBC micro:bit per mostrare le immagini sulla matrice LED 5x5.

Supporto browser

L'API Web Serial è disponibile su tutte le piattaforme desktop (ChromeOS, Linux, macOS, e Windows) in Chrome 89.

Polyfill

Su Android, il supporto per le porte seriali basate su USB è possibile utilizzando l'API WebUSB e il polyfill dell'API Serial. Questo polyfill è limitato all'hardware e piattaforme in cui il dispositivo è accessibile tramite l'API WebUSB perché non ha è stato rivendicato da un driver di dispositivo integrato.

Sicurezza e privacy

Gli autori delle specifiche hanno progettato e implementato l'API Web Serial utilizzando le principi definiti in Controllo dell'accesso a potenti funzionalità della piattaforma web, tra cui controllo dell'utente, trasparenza ed ergonomia. Possibilità di utilizzare L'API è controllata principalmente da un modello di autorizzazione che concede l'accesso solo a un singolo un dispositivo seriale alla volta. In risposta al prompt di un utente, quest'ultimo deve attivare passaggi per selezionare un determinato dispositivo seriale.

Per comprendere i compromessi in termini di sicurezza, consulta le pagine relative a sicurezza e privacy dell'API Web Serial.

Feedback

Il team di Chrome vorrebbe conoscere la tua opinione ed esperienza con gli API Web Serial.

Parlaci della progettazione dell'API

C'è qualcosa che non funziona come previsto nell'API? Oppure ci sono mancano metodi o proprietà necessari per implementare la tua idea?

Segnala un problema relativo alle specifiche nel repository GitHub dell'API Web Serial oppure aggiungi il tuo a un problema esistente.

Segnalare un problema con l'implementazione

Hai trovato un bug nell'implementazione di Chrome? Oppure l'implementazione rispetto alle specifiche?

Segnala un bug all'indirizzo https://new.crbug.com. Assicurati di includere fornire maggiori dettagli possibili, fornire semplici istruzioni per riprodurre il bug e Componenti impostati su Blink>Serial. Glitch è perfetto per e condividere riproduzioni facili e veloci.

Mostra il tuo sostegno

Intendi utilizzare l'API Web Serial? Il tuo supporto pubblico aiuta Chrome del team assegna la priorità alle funzionalità e mostra ad altri fornitori di browser quanto sia fondamentale supportarle.

Invia un tweet a @ChromiumDev utilizzando l'hashtag #SerialAPI: e facci sapere dove e come lo utilizzi.

Link utili

Demo

Ringraziamenti

Grazie a Reilly Grant e Joe Medley per le loro recensioni su questo articolo. Foto della fabbrica di aeroplani di Birmingham Museums Trust su Unsplash.