Membaca dari dan menulis ke port serial

Web Serial API memungkinkan situs berkomunikasi dengan perangkat serial.

François Beaufort
François Beaufort

Apa itu Web Serial API?

Port serial adalah antarmuka komunikasi dua arah yang memungkinkan pengiriman dan penerimaan data byte demi byte.

Web Serial API menyediakan cara bagi situs untuk membaca dan menulis ke perangkat serial dengan JavaScript. Perangkat serial terhubung melalui port serial di sistem pengguna atau melalui perangkat USB dan Bluetooth yang dapat dilepas yang mengemulasi port serial.

Dengan kata lain, Web Serial API menjembatani web dan dunia fisik dengan mengizinkan situs berkomunikasi dengan perangkat serial, seperti mikrokontroler dan printer 3D.

API ini juga merupakan pendamping yang sangat baik untuk WebUSB karena sistem operasi mewajibkan aplikasi berkomunikasi dengan beberapa port serial menggunakan API serial tingkat tinggi, bukan API USB tingkat rendah.

Kasus penggunaan yang disarankan

Di sektor pendidikan, penggemar, dan industri, pengguna menghubungkan perangkat periferal ke komputer mereka. Perangkat ini sering dikontrol oleh mikrokontroler melalui koneksi serial yang digunakan oleh software kustom. Beberapa software kustom untuk mengontrol perangkat ini dibuat dengan teknologi web:

Dalam beberapa kasus, situs berkomunikasi dengan perangkat melalui aplikasi agen yang diinstal pengguna secara manual. Di aplikasi lain, aplikasi dikirimkan dalam aplikasi paket melalui framework seperti Electron. Di kasus lain, pengguna harus melakukan langkah tambahan seperti menyalin aplikasi yang dikompilasi ke perangkat melalui flash drive USB.

Dalam semua kasus ini, pengalaman pengguna akan ditingkatkan dengan menyediakan komunikasi langsung antara situs dan perangkat yang dikontrolnya.

Status saat ini

Langkah Status
1. Membuat penjelasan Selesai
2. Buat draf awal spesifikasi Selesai
3. Mengumpulkan masukan & mengulangi desain Selesai
4. Uji coba origin Selesai
5. Luncurkan Selesai

Menggunakan Web Serial API

Deteksi fitur

Untuk memeriksa apakah Web Serial API didukung, gunakan:

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

Membuka port serial

Web Serial API dirancang secara asinkron. Hal ini mencegah UI situs diblokir saat menunggu input, yang penting karena data serial dapat diterima kapan saja, sehingga memerlukan cara untuk memantaunya.

Untuk membuka port serial, akses objek SerialPort terlebih dahulu. Untuk melakukannya, Anda dapat meminta pengguna memilih satu port serial dengan memanggil navigator.serial.requestPort() sebagai respons terhadap gestur pengguna seperti sentuhan atau klik mouse, atau memilih salah satu dari navigator.serial.getPorts() yang menampilkan daftar port serial yang telah diberi akses oleh situs.

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();

Fungsi navigator.serial.requestPort() mengambil literal objek opsional yang menentukan filter. ID ini digunakan untuk mencocokkan perangkat serial yang terhubung melalui USB dengan vendor USB wajib (usbVendorId) dan ID produk USB opsional (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 perintah port serial di situs
Perintah pengguna untuk memilih BBC micro:bit

Memanggil requestPort() akan meminta pengguna untuk memilih perangkat dan menampilkan objek SerialPort. Setelah memiliki objek SerialPort, memanggil port.open() dengan kecepatan transmisi yang diinginkan akan membuka port serial. Anggota kamus baudRate menentukan seberapa cepat data dikirim melalui jalur serial. Nilai ini dinyatakan dalam satuan bit per detik (bps). Periksa dokumentasi perangkat Anda untuk mengetahui nilai yang benar karena semua data yang Anda kirim dan terima akan tidak terbaca jika nilai ini ditentukan secara salah. Untuk beberapa perangkat USB dan Bluetooth yang mengemulasi port serial, nilai ini dapat ditetapkan dengan aman ke nilai apa pun karena diabaikan oleh emulasi.

// 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 });

Anda juga dapat menentukan salah satu opsi di bawah saat membuka port serial. Opsi ini bersifat opsional dan memiliki nilai default yang praktis.

  • dataBits: Jumlah bit data per frame (7 atau 8).
  • stopBits: Jumlah bit berhenti di akhir frame (1 atau 2).
  • parity: Mode paritas ("none", "even", atau "odd").
  • bufferSize: Ukuran buffer baca dan tulis yang harus dibuat (harus kurang dari 16 MB).
  • flowControl: Mode kontrol alur ("none" atau "hardware").

Membaca dari port serial

Aliran input dan output di Web Serial API ditangani oleh Streams API.

Setelah koneksi port serial dibuat, properti readable dan writable dari objek SerialPort akan menampilkan ReadableStream dan WritableStream. Port tersebut akan digunakan untuk menerima data dari dan mengirim data ke perangkat serial. Keduanya menggunakan instance Uint8Array untuk transfer data.

Saat data baru tiba dari perangkat serial, port.readable.getReader().read() menampilkan dua properti secara asinkron: value dan boolean done. Jika done benar, port serial telah ditutup atau tidak ada lagi data yang masuk. Memanggil port.readable.getReader() akan membuat pembaca dan mengunci readable ke pembaca tersebut. Saat readable dikunci, port serial tidak dapat ditutup.

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);
}

Beberapa error baca port serial non-fatal dapat terjadi dalam beberapa kondisi seperti buffer overflow, error framing, atau error paritas. Pengecualian tersebut ditampilkan sebagai pengecualian dan dapat ditangkap dengan menambahkan loop lain di atas loop sebelumnya yang memeriksa port.readable. Hal ini berfungsi karena selama error tidak fatal, ReadableStream baru akan dibuat secara otomatis. Jika terjadi error fatal, seperti perangkat serial dihapus, maka port.readable akan menjadi 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.
  }
}

Jika perangkat serial mengirimkan kembali teks, Anda dapat menyalurkan port.readable melalui TextDecoderStream seperti yang ditunjukkan di bawah. TextDecoderStream adalah transform stream yang mengambil semua potongan Uint8Array dan mengonversinya menjadi string.

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);
}

Anda dapat mengontrol cara memori dialokasikan saat membaca dari aliran data menggunakan pembaca "Bawa Buffer Anda Sendiri". Panggil port.readable.getReader({ mode: "byob" }) untuk mendapatkan antarmuka ReadableStreamBYOBReader dan berikan ArrayBuffer Anda sendiri saat memanggil read(). Perhatikan bahwa Web Serial API mendukung fitur ini di Chrome 106 atau yang lebih baru.

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()...
  }
}

Berikut adalah contoh cara menggunakan kembali buffer dari 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`.
}

Berikut contoh lain cara membaca jumlah data tertentu dari port serial:

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);

Menulis ke port serial

Untuk mengirim data ke perangkat serial, teruskan data ke port.writable.getWriter().write(). Memanggil releaseLock() di port.writable.getWriter() diperlukan agar port serial ditutup nanti.

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();

Kirim teks ke perangkat melalui TextEncoderStream yang di-pipe ke port.writable seperti yang ditunjukkan di bawah.

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

const writer = textEncoder.writable.getWriter();

await writer.write("hello");

Menutup port serial

port.close() menutup port serial jika anggota readable dan writable-nya tidak dikunci, yang berarti releaseLock() telah dipanggil untuk pembaca dan penulisnya masing-masing.

await port.close();

Namun, saat membaca data secara terus-menerus dari perangkat serial menggunakan loop, port.readable akan selalu dikunci hingga terjadi error. Dalam hal ini, memanggil reader.cancel() akan memaksa reader.read() untuk segera diselesaikan dengan { value: undefined, done: true } dan oleh karena itu memungkinkan loop memanggil 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;
});

Menutup port serial lebih rumit saat menggunakan transform streams. Panggil reader.cancel() seperti sebelumnya. Kemudian, panggil writer.close() dan port.close(). Hal ini menyebarkan error melalui aliran transformasi ke port serial yang mendasarinya. Karena propagasi error tidak terjadi secara langsung, Anda perlu menggunakan promise readableStreamClosed dan writableStreamClosed yang dibuat sebelumnya untuk mendeteksi kapan port.readable dan port.writable telah dibuka. Membatalkan reader akan menyebabkan streaming dibatalkan; itulah sebabnya Anda harus menangkap dan mengabaikan error yang dihasilkan.

// 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();

Mendengarkan koneksi dan pemutusan koneksi

Jika port serial disediakan oleh perangkat USB, perangkat tersebut dapat dihubungkan atau diputus sambungannya dari sistem. Jika situs telah diberi izin untuk mengakses port serial, situs tersebut harus memantau peristiwa connect dan 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.
});

Menangani sinyal

Setelah membuat koneksi port serial, Anda dapat secara eksplisit membuat kueri dan menyetel sinyal yang diekspos oleh port serial untuk deteksi perangkat dan kontrol alur. Sinyal ini ditentukan sebagai nilai boolean. Misalnya, beberapa perangkat seperti Arduino akan memasuki mode pemrograman jika sinyal Data Terminal Ready (DTR) diaktifkan.

Sinyal output dan sinyal input masing-masing ditetapkan dengan memanggil port.setSignals() dan port.getSignals(). Lihat contoh penggunaan di bawah.

// 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}`);

Mengubah aliran

Saat menerima data dari perangkat serial, Anda tidak akan selalu mendapatkan semua data sekaligus. File ini dapat dibagi menjadi beberapa bagian secara arbitrer. Untuk mengetahui informasi selengkapnya, lihat Konsep Streams API.

Untuk mengatasi hal ini, Anda dapat menggunakan beberapa aliran transformasi bawaan seperti TextDecoderStream atau membuat aliran transformasi Anda sendiri yang memungkinkan Anda mengurai aliran masuk dan menampilkan data yang diurai. Transform stream berada di antara perangkat serial dan loop baca yang menggunakan stream. Dapat menerapkan transformasi arbitrer sebelum data digunakan. Anggap saja seperti lini perakitan: saat widget bergerak di sepanjang lini, setiap langkah dalam lini akan mengubah widget, sehingga saat mencapai tujuan akhirnya, widget tersebut berfungsi sepenuhnya.

Foto pabrik pesawat terbang
World War II Castle Bromwich Aeroplane Factory

Misalnya, pertimbangkan cara membuat class aliran transformasi yang menggunakan aliran dan mengelompokkannya berdasarkan jeda baris. Metode transform()-nya dipanggil setiap kali data baru diterima oleh aliran. Data dapat diantrekan atau disimpan untuk nanti. Metode flush() dipanggil saat streaming ditutup, dan menangani data apa pun yang belum diproses.

Untuk menggunakan class stream transformasi, Anda perlu menyalurkan stream masuk melalui class tersebut. Dalam contoh kode ketiga di bagian Membaca dari port serial, aliran input asli hanya disalurkan melalui TextDecoderStream, jadi kita perlu memanggil pipeThrough() untuk menyalurkannya melalui LineBreakTransformer baru.

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();

Untuk men-debug masalah komunikasi perangkat serial, gunakan metode tee() dari port.readable untuk memisahkan aliran yang menuju ke atau dari perangkat serial. Kedua aliran yang dibuat dapat digunakan secara independen dan memungkinkan Anda mencetak salah satunya ke konsol untuk diperiksa.

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.

Mencabut akses ke port serial

Situs dapat menghapus izin untuk mengakses port serial yang tidak lagi ingin dipertahankannya dengan memanggil forget() pada instance SerialPort. Misalnya, untuk aplikasi web pendidikan yang digunakan di komputer bersama dengan banyak perangkat, sejumlah besar izin yang dihasilkan pengguna yang terakumulasi akan menciptakan pengalaman pengguna yang buruk.

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

Karena forget() tersedia di Chrome 103 atau yang lebih baru, periksa apakah fitur ini didukung dengan langkah-langkah berikut:

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

Tips Developer

Proses men-debug Web Serial API di Chrome menjadi mudah dengan halaman internal, about://device-log tempat Anda dapat melihat semua peristiwa terkait perangkat serial di satu tempat.

Screenshot halaman internal untuk men-debug Web Serial API.
Halaman internal di Chrome untuk men-debug Web Serial API.

Codelab

Dalam codelab Google Developer, Anda akan menggunakan Web Serial API untuk berinteraksi dengan papan BBC micro:bit guna menampilkan gambar pada matriks LED 5x5.

Dukungan browser

Web Serial API tersedia di semua platform desktop (ChromeOS, Linux, macOS, dan Windows) di Chrome 89.

Polyfill

Di Android, dukungan untuk port serial berbasis USB dapat dilakukan menggunakan WebUSB API dan polyfill Serial API. Polyfill ini terbatas pada hardware dan platform tempat perangkat dapat diakses melalui WebUSB API karena belum diklaim oleh driver perangkat bawaan.

Keamanan dan privasi

Penulis spesifikasi telah mendesain dan menerapkan Web Serial API menggunakan prinsip inti yang ditentukan dalam Mengontrol Akses ke Fitur Platform Web yang Canggih, termasuk kontrol pengguna, transparansi, dan ergonomi. Kemampuan untuk menggunakan API ini terutama dibatasi oleh model izin yang memberikan akses hanya ke satu perangkat serial dalam satu waktu. Sebagai respons terhadap perintah pengguna, pengguna harus mengambil langkah-langkah aktif untuk memilih perangkat serial tertentu.

Untuk memahami kompromi keamanan, lihat bagian keamanan dan privasi dari Penjelasan Web Serial API.

Masukan

Tim Chrome ingin mendengar pendapat dan pengalaman Anda terkait Web Serial API.

Beri tahu kami tentang desain API

Apakah ada sesuatu tentang API yang tidak berfungsi seperti yang diharapkan? Atau apakah ada metode atau properti yang tidak ada dan perlu Anda terapkan untuk mewujudkan ide Anda?

Laporkan masalah spesifikasi di repositori GitHub Web Serial API atau tambahkan pendapat Anda ke masalah yang ada.

Melaporkan masalah terkait penerapan

Apakah Anda menemukan bug pada penerapan Chrome? Atau apakah implementasinya berbeda dari spesifikasi?

Laporkan bug di https://new.crbug.com. Pastikan untuk menyertakan detail sebanyak mungkin, berikan petunjuk sederhana untuk mereproduksi bug, dan tetapkan Komponen ke Blink>Serial.

Menunjukkan dukungan

Apakah Anda berencana menggunakan Web Serial API? Dukungan publik Anda membantu tim Chrome memprioritaskan fitur dan menunjukkan kepada vendor browser lain betapa pentingnya dukungan untuk fitur tersebut.

Kirim tweet ke @ChromiumDev menggunakan hashtag #SerialAPI dan beri tahu kami di mana dan bagaimana Anda menggunakannya.

Link bermanfaat

Demo

Ucapan terima kasih

Terima kasih kepada Reilly Grant dan Joe Medley atas ulasan mereka terhadap artikel ini. Foto pabrik pesawat oleh Birmingham Museums Trust di Unsplash.