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ı, bayt cinsinden veri baytı gönderme ve alma olanağı sağlayan çift yönlü bir iletişim arayüzüdür.

Web Serial API, web sitelerinin JavaScript ile seri cihazlardan okuması ve bu cihaza yazması için bir yol sağlar. Seri cihazlar, kullanıcının sistemindeki seri bağlantı noktası veya seri bağlantı noktasına emüle eden çıkarılabilir USB ve Bluetooth cihazları aracılığıyla bağlanır.

Diğer bir deyişle, Web Serial API, web sitelerinin mikrodenetleyiciler ve 3D yazıcılar gibi seri cihazlarla iletişim kurmasına olanak tanıyarak web ile fiziksel dünya arasında köprü oluşturur.

Bu API, işletim sistemlerinin bazı seri bağlantı noktalarıyla düşük seviyeli USB API yerine daha üst seviyedeki seri API'lerini kullanarak iletişim kurmasını gerektirdiğinden WebUSB için de mükemmel bir tamamlayıcıdır.

Önerilen kullanım alanları

Eğitim, hobi amaçlı ve endüstriyel sektörlerde kullanıcılar çevre birimi cihazlarını bilgisayarlarına bağlar. Bu cihazlar genellikle özel yazılımlar tarafından kullanılan seri bağlantı üzerinden mikrodenetleyiciler tarafından kontrol edilir. Bu cihazları kontrol eden bazı özel yazılımlar web teknolojisiyle oluşturulmuştur:

Bazı durumlarda web siteleri, kullanıcıların manuel olarak yükledikleri bir aracı uygulaması üzerinden cihazla iletişim kurar. Diğerlerinde ise uygulama Electron gibi bir çerçeve üzerinden paketlenmiş bir uygulama içinde teslim edilir. Bazı durumlarda ise kullanıcının, derlenmiş bir uygulamayı USB flash sürücüsü üzerinden cihaza kopyalama gibi ek bir adım gerçekleştirmesi 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

Adım Durum
1. Açıklayıcı oluşturun Tamamlandı
2. İlk spesifikasyon taslağını oluşturma Tamamlandı
3. Geri bildirim alma ve tasarımı yineleme 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 eşzamansızdır. Bu, giriş beklerken web sitesi kullanıcı arayüzünün engellenmesini önler. Seri verilerin her an alınabilmesi ve bu verileri dinlemek için bir yöntem kullanılması gerektiğinden bu önemlidir.

Bir 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 kullanıcıdan navigator.serial.requestPort() yöntemini çağırarak tek bir seri bağlantı noktası seçmesini isteyebilir veya web sitesine erişim izni verilen seri bağlantı noktalarının listesini döndüren navigator.serial.getPorts() işlevinden bir bağlantı noktası 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şmez değerini alır. Bunlar, USB üzerinden bağlanan tüm seri cihazları zorunlu bir USB tedarikçi firması (usbVendorId) ve isteğe bağlı USB ürün tanımlayıcıları (usbProductId) ile 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 mikro:biti seçmeyle ilgili kullanıcı istemi

requestPort() çağrısı yapıldığında, kullanıcıdan cihaz seçmesi istenir ve bir SerialPort nesnesi döndürülür. Bir SerialPort nesnesi olduğunda, port.open() adlı cihazın istenen bağlantı hızıyla çağrılması seri bağlantı noktasını açar. baudRate sözlük üyesi, verilerin bir seri satır üzerinden ne kadar hızlı gönderildiğini belirtir. Saniye başına bit (bps) cinsinden ifade edilir. Yanlış bir şekilde belirtilirse gönderdiğiniz ve aldığınız tüm veriler anlamsız olacağından cihazınızın dokümanlarında doğru değerin bulunup bulunmadığını kontrol edin. Seri bağlantı noktası emülasyonu yapan bazı USB ve Bluetooth cihazlarda bu değer, emülasyon tarafından yoksayıldığından 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 });

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

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

Seri bağlantı noktasından okuma

Web Serial API'deki giriş ve çıkış akışları, Streams API tarafından yönetilir.

Seri bağlantı noktası bağlantısı kurulduktan sonra, SerialPort nesnesindeki readable ve writable özellikleri bir 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() eşzamansız olarak iki özellik döndürür: value ve bir done boole. done değeri doğruysa seri bağlantı noktası kapatılmış veya başka veri gelmiyor demektir. port.readable.getReader() çağrıldığında bir okuyucu oluşturulur ve readable bu okuyucuya kilitlenir. readable kilitli durumdayken 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ı önemli olmayan seri bağlantı noktası okuma hataları; arabellek taşması, çerçeveleme hataları veya denklik hataları gibi bazı koşullarda ortaya çıkabilir. Bunlar istisna olarak kabul edilir ve port.readable öğesini kontrol eden bir öncekinin üzerine başka bir döngü ekleyerek yakalanabilir. Bunun nedeni, hatalar önemli olmadığı sürece otomatik olarak yeni bir ReadableStream'in otomatik olarak oluşturulmasıdır. Seri cihazın kaldırılması gibi önemli bir hata oluşursa port.readable boş hale gelir.

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 port.readable, aşağıda gösterildiği gibi bir TextDecoderStream üzerinden dikey çizgi oluşturabilir. TextDecoderStream, tüm Uint8Array parçalarını alıp dizelere dönüştüren bir dönüşüm 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);
}

"Bring Your Own Buffer" (Kendi Arabelleğinizi Getirin) okuyucusu kullanarak akıştan okuduğunuzda belleğin nasıl ayrılacağını kontrol edebilirsiniz. ReadableStreamBYOBReader arayüzünü edinmek için port.readable.getReader({ mode: "byob" }) numaralı telefonu arayın ve read() çağırırken kendi ArrayBuffer bilginizi sağlayın. Web Serial API'nin, Chrome 106 veya sonraki sürümlerde bu özelliği 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()...
  }
}

Aşağıda, value.buffer adlı arabelleğin nasıl yeniden kullanılacağına dair bir örnek verilmiştir:

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

Aşağıda, belirli miktarda verinin seri bağlantı noktasından nasıl okunacağına ilişkin başka bir örnek verilmiştir:

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() cihazına iletin. Seri bağlantı noktasının daha sonra kapatılabilmesi için port.writable.getWriter() üzerinde releaseLock() araması 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, port.writable bağlantısı olan TextEncoderStream aracılığıyla cihaza metni 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 üyelerinin kilidi açılmışsa seri bağlantı noktasını kapatır. Yani releaseLock(), ilgili okuyucu ve yazarı için çağrılır.

await port.close();

Ancak döngü kullanarak bir seri cihazdan sürekli olarak veri okurken port.readable bir hatayla karşılaşana kadar her zaman kilitli kalır. Bu durumda, reader.cancel() çağrılması reader.read() öğesini { value: undefined, done: true } ile hemen çözümlemeye zorlayarak döngünün reader.releaseLock() çağrısı yapmasına izin verir.

// 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üşüm akışları kullanılırken seri bağlantı noktasının kapatılması daha karmaşıktır. reader.cancel() adlı iş ortağını daha önce olduğu gibi arayın. Ardından writer.close() ve port.close() numaralı telefonu arayın. Bu işlem, dönüştürme akışları üzerinden temel seri bağlantı noktasına hataları yayar. Hata yayılımı hemen gerçekleşmediğinden port.readable ve port.writable öğelerinin kilidinin ne zaman açıldığını tespit etmek için daha önce oluşturulan readableStreamClosed ve writableStreamClosed sözlerini kullanmanız gerekir. reader öğesinin iptal edilmesi akışın iptal edilmesine neden olur. Bu nedenle, oluşan hatayı yakalayıp göz ardı etmeniz 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ıyı ve bağlantıyı kesmeyi dinleme

USB cihazı bir seri bağlantı noktası sağlıyorsa o cihaz sisteme bağlanmış veya sistemle bağlantısı kesilmiş olabilir. Web sitesinin bir seri bağlantı noktasına erişim 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.
});

Sinyalleri işleme

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

Çıkış sinyallerinin ayarlanması ve giriş sinyallerinin alınması sırasıyla port.setSignals() ve port.getSignals() çağrılarıyla gerçekleştirilir. Aşağıdaki kullanım örneklerini inceleyin.

// 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ı konusuna bakın.

Bu sorunu gidermek için TextDecoderStream gibi bazı yerleşik dönüşüm akışlarını kullanabilir veya gelen akışı ayrıştırarak ayrıştırılan verileri geri döndürmenizi sağlayan kendi dönüşüm akışınızı oluşturabilirsiniz. Dönüşüm akışı, seri cihaz ile akışı tüketen okuma döngüsü arasında bulunur. Veriler tüketilmeden önce rastgele bir dönüşüm uygulayabilir. Bunu bir derleme çizgisi gibi düşünün: Bir widget çizgi boyunca ilerledikçe, çizgideki her adım widget'ı değiştirir. Böylece, son hedefine geldiğinde tam olarak çalışan bir widget haline gelir.

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

Örneğin, bir akışı tüketen ve satır sonlarına göre parçalayan bir dönüşüm akış sınıfı oluşturmayı düşünün. Akış tarafından her yeni veri alındığında transform() yöntemi çağrılır. Verileri sıraya koyabilir 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üşüm akışı sınıfını kullanmak için gelen bir akışı akıştan geçirmeniz gerekir. Seri bağlantı noktasından okuma başlığı altındaki üçüncü kod örneğinde, orijinal giriş akışı yalnızca bir TextDecoderStream üzerinden bağlanmıştır. Bu nedenle, yeni LineBreakTransformer bağlantı noktası üzerinden iletebilmek için pipeThrough() yöntemini çağırmamız gerekmektedir.

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ıyla ilgili hataları ayıklamak amacıyla, seri cihaza giden veya bu cihazdan giden akışları bölmek için port.readable tee() yöntemini kullanın. Oluşturulan iki akış bağımsız olarak kullanılabilir ve bu sayede inceleme için konsola bir akış 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() yöntemini çağırarak artık tutmak istemediği bir seri bağlantı noktasına erişim izinlerini temizleyebilir. Örneğin, birçok cihazın bulunduğu paylaşılan bir bilgisayarda kullanılan eğitim amaçlı bir web uygulaması söz konusu olduğunda, kullanıcı tarafından oluşturulmuş çok sayıda izin bulunması kötü bir kullanıcı deneyimine neden olur.

// 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ştiricilere Yönelik İpuçları

Chrome'da Web Serial API'de hata ayıklamak, about://device-log seri numaralı cihazla ilgili tüm etkinlikleri tek bir yerde görebileceğiniz dahili sayfa ile kolaydır.

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

Codelab

Google Developers codelab'inde, resimleri 5x5 LED matrisinde göstermek üzere BBC micro:bit kartı ile etkileşime geçmek için Web Serial API'yi kullanacaksınız.

Tarayıcı desteği

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

Polyester Lifi

Android'de, WebUSB API ve Serial API polyfill kullanılarak USB tabanlı seri bağlantı noktaları desteklenir. Bu çoklu dolgu, yerleşik cihaz sürücüsü tarafından talep edilmediğinden cihazın WebUSB API üzerinden erişebildiği donanım ve platformlarla sınırlıdır.

Güvenlik ve gizlilik

Spesifikasyon yazarları, Güçlü Web Platformu Özelliklerine Erişimi Kontrol Etme bölümünde tanımlanan temel ilkeleri (kullanıcı kontrolü, şeffaflık ve ergonomi) kullanarak Web Serial API'yi tasarlamıştır ve uygulamıştır. Bu API'nin kullanılabilmesi için öncelikle aynı anda yalnızca tek bir seri cihaza erişim sağlayan bir izin modeli bulunur. Kullanıcı, kullanıcı istemine yanıt olarak belirli bir seri cihazı seçmek için etkin adımlar atmalıdır.

Güvenlik dengelerini anlamak için Web Serial API Explainer'ın güvenlik ve gizlilik bölümlerine göz atın.

Geri bildirim

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

Bize API tasarımı hakkında bilgi verin

API'de beklendiği gibi çalışmayan bir şey var mı? Yoksa fikrinizi uygulamak için gereken yöntemler veya özellikler eksik mi?

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

Uygulamayla ilgili bir sorunu bildirin

Chrome'un uygulamasında bir hata buldunuz mu? 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'i Blink>Serial olarak ayarladığınızdan emin olun. Glitch hızlı ve kolay yeniden oluşturmalar paylaşmak için idealdir.

Desteği göster

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 onları desteklemenin ne kadar kritik olduğunu gösterir.

#SerialAPI hashtag'ini kullanarak @ChromiumDev adresine tweet gönderip bu tweet'i nerede ve nasıl kullandığınızı bize bildirin.

Faydalı bağlantılar

Demolar

Teşekkür

Bu makaleyi yazdıkları yorumlar için Reilly Grant ve Joe Medley'ye teşekkür ediyoruz. Birmingham Museums Trust'ın Unsplash'teki uçak fabrikası fotoğrafı.