Bir seri bağlantı noktasından okuma ve bu bağlantı noktasına yazma

Web Serial API, web sitelerinin seri cihazlarla iletişim kurmasına olanak tanır.

François Beaufort
François Beaufort

Web Serial API nedir?

Seri bağlantı noktası, verilerin bayt bayt gönderilmesine ve alınmasına olanak tanıyan çift yönlü bir iletişim arabirimidir.

Web Serial API, web sitelerinin JavaScript ile seri cihazlardan okuma ve seri cihazlara yazma işlemleri yapmasına olanak tanır. Seri cihazlar, kullanıcının sistemindeki bir seri bağlantı noktası veya seri bağlantı noktasını taklit eden çıkarılabilir USB ve Bluetooth cihazları aracılığıyla bağlanır.

Başka bir deyişle, Web Serial API, web sitelerinin mikrodenetleyiciler ve 3D yazıcılar gibi seri cihazlarla iletişim kurmasına izin vererek web ile fiziksel dünya arasında köprü kurar.

Bu API, işletim sistemlerinin uygulamaların düşük düzeyli USB API yerine daha yüksek düzeyli seri API'lerini kullanarak bazı seri bağlantı noktalarıyla iletişim kurmasını gerektirdiğinden WebUSB için de harika bir yardımcıdır.

Önerilen kullanım alanları

Eğitim, hobi ve endüstri sektörlerinde kullanıcılar çevre birimlerini bilgisayarlarına bağlar. Bu cihazlar genellikle özel yazılımların kullandığı seri bağlantı üzerinden mikro denetleyicilerle kontrol edilir. Bu cihazları kontrol etmek için kullanılan bazı özel yazılımlar web teknolojisiyle oluşturulur:

Bazı durumlarda web siteleri, kullanıcıların manuel olarak yüklediği bir aracı uygulama aracılığıyla cihazla iletişim kurar. Bazı uygulamalar ise Electron gibi bir çerçeve aracılığıyla paketlenmiş uygulama olarak sunulur. Bazı durumlarda ise kullanıcının ek bir adım gerçekleştirmesi gerekir. Örneğin, derlenmiş bir uygulamayı USB flash sürücü aracılığıyla cihaza kopyalaması gerekir.

Tüm bu durumlarda, web sitesi ile kontrol ettiği cihaz arasında doğrudan iletişim sağlanarak kullanıcı deneyimi iyileştirilir.

Mevcut durum

Step Durum
1. Açıklayıcı oluşturma Tamamlandı
2. Spesifikasyonun ilk taslağını oluşturma Tamamlandı
3. Geri bildirim toplama ve tasarım üzerinde yineleme yapma Tamamlandı
4. Kaynak denemesi Tamamlandı
5. Lansman Tamamlandı

Web Serial API'yi kullanma

Özellik algılama

Web Serial API'nin desteklenip desteklenmediğini kontrol etmek için şunu kullanın:

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

Seri bağlantı noktası açma

Web Serial API, tasarım gereği asenkron çalışır. Bu, web sitesi kullanıcı arayüzünün giriş beklenirken engellenmesini önler. Seri veriler herhangi bir zamanda alınabileceğinden ve bu verileri dinlemenin bir yolu gerektiğinden bu önemlidir.

Seri bağlantı noktasını açmak için önce bir SerialPort nesnesine erişin. Bunun için, dokunma veya fare tıklaması gibi bir kullanıcı hareketine yanıt olarak navigator.serial.requestPort() işlevini çağırarak kullanıcıdan tek bir seri bağlantı noktası seçmesini isteyebilir ya da web sitesine erişim izni verilen seri bağlantı noktalarının listesini döndüren navigator.serial.getPorts() işlevinden birini seçebilirsiniz.

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

navigator.serial.requestPort() işlevi, filtreleri tanımlayan isteğe bağlı bir nesne değişmezi alır. Bunlar, USB üzerinden bağlanan herhangi bir seri cihazı zorunlu bir USB tedarikçisi (usbVendorId) ve isteğe bağlı USB ürün tanımlayıcılarıyla (usbProductId) eşleştirmek için kullanılır.

// 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();
Bir web sitesindeki seri bağlantı noktası isteminin ekran görüntüsü
BBC micro:bit seçme istemi

Calling requestPort(), kullanıcıdan bir cihaz seçmesini ister ve SerialPort nesnesi döndürür. Bir SerialPort nesneniz olduğunda, istenen baud hızıyla port.open() işlevini çağırmak seri bağlantı noktasını açar. baudRate sözlük üyesi, verilerin seri hat üzerinden ne kadar hızlı gönderileceğini belirtir. Saniye başına bit (bps) birimiyle ifade edilir. Gönderdiğiniz ve aldığınız tüm veriler yanlış belirtilirse anlamsız olacağından, doğru değer için cihazınızın belgelerine bakın. Seri bağlantı noktası emülasyonu yapan bazı USB ve Bluetooth cihazlarda bu değer, emülasyon tarafından yoksayıldığı için güvenli bir şekilde herhangi bir değere ayarlanabilir.

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

Seri bağlantı noktası açarken aşağıdaki seçeneklerden herhangi birini de belirtebilirsiniz. Bu seçenekler isteğe bağlıdır ve uygun varsayılan değerlere sahiptir.

  • dataBits: Çerçeve başına veri bitlerinin sayısı (7 veya 8).
  • stopBits: Bir karenin sonundaki durdurma bitlerinin sayısı (1 veya 2).
  • parity: Parite modu ("none", "even" veya "odd").
  • bufferSize: Oluşturulması gereken okuma ve yazma arabelleklerinin boyutu (16 MB'tan küçük olmalıdır).
  • flowControl: Akış kontrolü modu ("none" veya "hardware").

Seri bağlantı noktasından okuma

Web Serial API'deki giriş ve çıkış akışları, Streams API tarafından işlenir.

Seri bağlantı noktası bağlantısı kurulduktan sonra readable ve writable SerialPort nesnesinin özellikleri ReadableStream ve WritableStream döndürür. Bunlar, seri cihazdan veri almak ve cihaza veri göndermek için kullanılır. Her ikisi de veri aktarımı için Uint8Array örneklerini kullanır.

Seri cihazdan yeni veriler geldiğinde port.readable.getReader().read(), iki özelliği eşzamansız olarak döndürür: value ve done boole değeri. done doğruysa seri bağlantı noktası kapatılmıştır veya artık veri gelmiyordur. port.readable.getReader() işlevini çağırmak bir okuyucu oluşturur ve readable öğesini bu okuyucuya kilitler. readable kilitliyken seri bağlantı noktası kapatılamaz.

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

Bazı durumlarda (ör. arabellek taşması, çerçeveleme hataları veya eşlik hataları) ölümcül olmayan bazı seri bağlantı noktası okuma hataları oluşabilir. Bunlar istisna olarak atılır ve port.readable değerini kontrol eden önceki döngünün üzerine başka bir döngü eklenerek yakalanabilir. Bu, hatalar ölümcül olmadığı sürece otomatik olarak yeni bir ReadableStream oluşturulduğu için çalışır. Seri cihazın kaldırılması gibi ölümcül bir hata oluşursa port.readable değeri boş olur.

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

Seri cihaz metni geri gönderirse aşağıda gösterildiği gibi port.readable karakterini bir TextDecoderStream üzerinden iletebilirsiniz. TextDecoderStream, tüm Uint8Array parçalarını alıp bunları dizelere dönüştüren bir dönüştürme akışıdır.

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

"Kendi arabelleğinizi getirin" okuyucusu kullanarak akıştan okuma yaparken belleğin nasıl ayrılacağını kontrol edebilirsiniz. ReadableStreamBYOBReader arayüzünü almak için port.readable.getReader({ mode: "byob" }) işlevini çağırın ve read() işlevini çağırırken kendi ArrayBuffer öğenizi sağlayın. Web Serial API'nin bu özelliği Chrome 106 veya sonraki sürümlerde desteklediğini unutmayın.

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

value.buffer dışındaki arabelleği yeniden kullanma örneğini aşağıda bulabilirsiniz:

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`.
}

Seri bağlantı noktasından belirli bir miktarda verinin nasıl okunacağına dair başka bir örneği aşağıda bulabilirsiniz:

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

Seri bağlantı noktasına yazma

Seri cihaza veri göndermek için verileri port.writable.getWriter().write()'ya iletin. Seri bağlantı noktasının daha sonra kapatılması için port.writable.getWriter() üzerinde releaseLock() çağrısının yapılması gerekir.

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

Aşağıda gösterildiği gibi, TextEncoderStream ile port.writable arasına yerleştirilmiş bir kanal üzerinden cihaza metin gönderin.

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

const writer = textEncoder.writable.getWriter();

await writer.write("hello");

Seri bağlantı noktasını kapatma

port.close(), readable ve writable üyeleri kilidi açılmışsa seri bağlantı noktasını kapatır. Bu, ilgili okuyucu ve yazıcı için releaseLock()'nın çağrıldığı anlamına gelir.

await port.close();

Ancak, bir döngü kullanarak seri cihazdan sürekli olarak veri okurken, hata oluşana kadar port.readable her zaman kilitlenir. Bu durumda, reader.cancel() işlevinin çağrılması reader.read() işlevinin { value: undefined, done: true } ile hemen çözülmesini zorlayacak ve bu nedenle döngünün reader.releaseLock() işlevini çağırmasına izin verecektir.

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

Dönüştürme akışları kullanılırken seri bağlantı noktasını kapatmak daha karmaşıktır. reader.cancel()'ı daha önce olduğu gibi arayın. Ardından writer.close() ve port.close()'ı arayın. Bu, hataların dönüştürme akışları aracılığıyla temel seri bağlantı noktasına yayılmasına neden olur. Hata yayılımı hemen gerçekleşmediğinden, port.readable ve port.writable kilidinin ne zaman açıldığını algılamak için daha önce oluşturulan readableStreamClosed ve writableStreamClosed sözlerini kullanmanız gerekir. reader iptal edildiğinde akış durdurulur. Bu nedenle, ortaya çıkan hatayı yakalamanız ve yoksaymanız gerekir.

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

Bağlantı ve bağlantı kesme işlemlerini dinleme

Seri bağlantı noktası bir USB cihaz tarafından sağlanıyorsa bu cihaz sisteme bağlanabilir veya sistemden bağlantısı kesilebilir. Web sitesine seri bağlantı noktasına erişme izni verildiğinde connect ve disconnect etkinliklerini izlemesi gerekir.

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

İşleme sinyalleri

Seri bağlantı noktası bağlantısını oluşturduktan sonra, cihaz algılama ve akış kontrolü için seri bağlantı noktası tarafından sunulan sinyalleri açıkça sorgulayabilir ve ayarlayabilirsiniz. Bu sinyaller, Boole değerleri olarak tanımlanır. Örneğin, Arduino gibi bazı cihazlar, Veri Terminali Hazır (DTR) sinyali değiştirilirse programlama moduna girer.

Çıkış sinyallerini ayarlama ve giriş sinyallerini alma işlemleri sırasıyla port.setSignals() ve port.getSignals() çağrılarıyla yapılır. Aşağıdaki kullanım örneklerine bakın.

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

Akışları dönüştürme

Seri cihazdan veri aldığınızda, tüm verileri aynı anda almanız gerekmez. Rastgele parçalara ayrılmış olabilir. Daha fazla bilgi için Streams API kavramları başlıklı makaleyi inceleyin.

Bu durumla başa çıkmak için TextDecoderStream gibi bazı yerleşik dönüştürme akışlarını kullanabilir veya gelen akışı ayrıştırmanıza ve ayrıştırılmış verileri döndürmenize olanak tanıyan kendi dönüştürme akışınızı oluşturabilirsiniz. Dönüştürme akışı, seri cihaz ile akışı tüketen okuma döngüsü arasında yer alır. Veriler kullanılmadan önce rastgele bir dönüşüm uygulayabilir. Bunu bir montaj hattı gibi düşünebilirsiniz: Bir widget hat boyunca ilerlerken hattaki her adımda widget değiştirilir. Böylece, son hedefine ulaştığında tam işlevsel bir widget haline gelir.

Uçak fabrikasının fotoğrafı
II. Dünya Savaşı'nda Castle Bromwich Uçak Fabrikası

Örneğin, bir akışı tüketen ve satır sonlarına göre parçalara ayıran bir dönüştürme akışı sınıfı oluşturmayı düşünün. transform() yöntemi, akış tarafından her yeni veri alındığında çağrılır. Verileri sıraya alabilir veya daha sonra kullanmak üzere kaydedebilir. flush() yöntemi, akış kapatıldığında çağrılır ve henüz işlenmemiş tüm verileri işler.

Dönüştürme akışı sınıfını kullanmak için gelen bir akışı bu sınıfa yönlendirmeniz gerekir. Read from a serial port (Seri bağlantı noktasından okuma) bölümündeki üçüncü kod örneğinde, orijinal giriş akışı yalnızca TextDecoderStream üzerinden yönlendirildiğinden, yeni LineBreakTransformer üzerinden yönlendirmek için pipeThrough()'ü çağırmamız gerekir.

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

Seri cihaz iletişim sorunlarında hata ayıklamak için seri cihaza giden veya seri cihazdan gelen akışları bölmek üzere tee() yöntemini kullanın. port.readable Oluşturulan iki akış birbirinden bağımsız olarak kullanılabilir. Bu sayede, inceleme için bir akışı konsola yazdırabilirsiniz.

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.

Seri bağlantı noktasına erişimi iptal etme

Web sitesi, SerialPort örneğinde forget() işlevini çağırarak artık tutmak istemediği seri bağlantı noktasına erişim izinlerini temizleyebilir. Örneğin, birçok cihazla paylaşılan bir bilgisayarda kullanılan bir eğitim web uygulamasında, kullanıcı tarafından oluşturulan çok sayıda izin birikmesi kötü bir kullanıcı deneyimine yol açar.

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

forget(), Chrome 103 veya sonraki sürümlerde kullanılabildiğinden bu özelliğin aşağıdakilerle desteklenip desteklenmediğini kontrol edin:

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

Geliştiriciler için İpuçları

Dahili sayfa olan about://device-log ile Chrome'da Web Serial API'yi hata ayıklamak kolaydır. Bu sayfada, seri cihazla ilgili tüm etkinlikleri tek bir yerde görebilirsiniz.

Web Serial API&#39;de hata ayıklama için kullanılan dahili sayfanın ekran görüntüsü.
Web Serial API'de hata ayıklama için Chrome'daki dahili sayfa.

Codelab

Google Developer codelab'de, 5x5 LED matrisinde resim göstermek için BBC micro:bit kartıyla etkileşim kurmak üzere Web Serial API'yi kullanacaksınız.

Tarayıcı desteği

Web Serial API, Chrome 89'da tüm masaüstü platformlarında (ChromeOS, Linux, macOS ve Windows) kullanılabilir.

Polyfill

Android'de, USB tabanlı seri bağlantı noktaları için WebUSB API ve Serial API polyfill kullanılarak destek sağlanabilir. Bu polyfill, yerleşik bir cihaz sürücüsü tarafından talep edilmediği için cihazın WebUSB API üzerinden erişilebilir olduğu donanım ve platformlarla sınırlıdır.

Güvenlik ve gizlilik

Spesifikasyon yazarları, kullanıcı kontrolü, şeffaflık ve ergonomi dahil olmak üzere Güçlü Web Platformu Özelliklerine Erişimi Kontrol Etme'de tanımlanan temel ilkeleri kullanarak Web Serial API'yi tasarlayıp uygulamıştır. Bu API'yi kullanma özelliği, temel olarak yalnızca tek bir seri cihaza erişim veren bir izin modeliyle sınırlıdır. Kullanıcı istemine yanıt olarak kullanıcının belirli bir seri cihazı seçmek için aktif adımlar atması gerekir.

Güvenlikle ilgili ödünleri anlamak için Web Serial API Açıklayıcı Dokümanı'nın güvenlik ve gizlilik bölümlerini inceleyin.

Geri bildirim

Chrome ekibi, Web Serial API ile ilgili düşüncelerinizi ve deneyimlerinizi öğrenmekten memnuniyet duyar.

API tasarımı hakkında bilgi verin.

API ile ilgili beklendiği gibi çalışmayan bir durum var mı? Yoksa fikrinizi uygulamak için eksik yöntemler veya özellikler mi var?

Web Serial API GitHub deposunda bir spesifikasyon sorunu bildirin veya düşüncelerinizi mevcut bir soruna ekleyin.

Uygulamayla ilgili sorun bildirme

Chrome'un uygulamasında bir hata mı buldunuz? Yoksa uygulama, spesifikasyondan farklı mı?

https://new.crbug.com adresinden hata bildiriminde bulunun. Mümkün olduğunca fazla ayrıntı eklediğinizden, hatayı yeniden oluşturmak için basit talimatlar sağladığınızdan ve Bileşenler'in Blink>Serial olarak ayarlandığından emin olun.

Desteğinizi gösterme

Web Serial API'yi kullanmayı planlıyor musunuz? Herkese açık desteğiniz, Chrome ekibinin özelliklere öncelik vermesine yardımcı olur ve diğer tarayıcı satıcılarına bu özelliklerin desteklenmesinin ne kadar önemli olduğunu gösterir.

#SerialAPI hashtag'ini kullanarak @ChromiumDev'e tweet gönderin ve nerede, nasıl kullandığınızı bize bildirin.

Faydalı bağlantılar

Demolar

Teşekkür

Bu makaleyi inceledikleri için Reilly Grant ve Joe Medley'e teşekkür ederiz. Birmingham Museums Trust'ın Unsplash'teki Aeroplane factory fotoğrafı.