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ı, verileri bayt bayt gönderip almaya olanak tanıyan iki yönlü bir iletişim arayüzüdür.

Web Serial API, web sitelerinin JavaScript ile seri cihazlardan veri okumasına ve bu cihazlara veri yazmasına olanak tanır. Seri cihazlar, kullanıcının sistemindeki seri bağlantı noktası veya seri bağlantı noktasını taklit eden çıkarılabilir USB ve Bluetooth cihazlar üzerinden 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.

İşletim sistemleri, uygulamaların bazı seri bağlantı noktalarıyla iletişim kurarken düşük düzeyli USB API yerine daha yüksek düzeyli seri API'lerini kullanmasını gerektirdiğinden bu API, WebUSB ile de mükemmel bir uyum sağlar.

Önerilen kullanım alanları

Eğitim, hobi amaçlı ve sanayi sektörlerinde kullanıcılar, çevrebirimi 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 etmek için kullanılan bazı özel yazılımlar web teknolojisiyle geliştirilmiştir:

Bazı durumlarda web siteleri, kullanıcıların manuel olarak yüklediği bir aracı uygulaması aracılığıyla cihazla iletişim kurar. Diğerlerinde ise uygulama, Electron gibi bir çerçeve aracılığıyla paketlenmiş bir uygulama olarak yayınlanır. Bazı durumlarda ise kullanıcının, derlenmiş bir uygulamayı USB flash sürücü aracılığıyla cihaza kopyalama gibi ek bir adım uygulaması gerekir.

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

Mevcut durum

Step Durum
1. Açıklayıcı oluşturun Tamamlandı
2. İlk spesifikasyon taslağını oluşturun Tamamlandı
3. Geri bildirim alma ve tasarım üzerinde 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:

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 sayede, web sitesi kullanıcı arayüzü, giriş beklerken engellenmez. Seri veriler herhangi bir zamanda alınabileceğinden ve dinlenmesi gerektiğinden bu durum ö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 sitesinin erişmesine izin 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ğlı herhangi bir seri cihazı zorunlu USB tedarikçisiyle (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ı isteğinin ekran görüntüsü
BBC micro:bit seçmeyle ilgili kullanıcı istemi

requestPort() çağrısı, kullanıcıdan bir cihaz seçmesini ister ve bir SerialPort nesnesi döndürür. Bir SerialPort nesnesi oluşturduktan sonra, port.open() işlevini istenen baud hızıyla çağırmak seri bağlantı noktasını açar. baudRate sözlük üyesi, verilerin seri hat üzerinden ne kadar hızlı gönderildiğini belirtir. Saniye başına bit (bps) cinsinden ifade edilir. Bu parametre yanlış belirtilirse gönderdiğiniz ve aldığınız tüm veriler anlamsız olacaktır. Bu nedenle, doğru değeri öğrenmek için cihazınızın belgelerini inceleyin. Seri bağlantı noktasını taklit eden bazı USB ve Bluetooth cihazlarda bu değer, taklit tarafından yok sayıldığı için herhangi bir değere güvenli bir şekilde 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 kullanışlı varsayılan değerlere sahiptir.

  • dataBits: Kare başına veri biti sayısı (7 veya 8).
  • stopBits: Bir karenin sonunda bulunan 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ısı kurulduktan sonra SerialPort nesnesinin readable ve writable özellikleri bir ReadableStream ve WritableStream döndürür. Bunlar, seri cihazdan veri almak ve seri cihaza veri göndermek için kullanılır. Her ikisi de veri aktarımı için Uint8Array örneklerini kullanır.

Seri cihazdan yeni veri geldiğinde port.readable.getReader().read(), value ve done boole türü iki özelliği eşzamansız olarak döndürür. done doğruysa seri bağlantı noktası kapatılmış veya artık veri gelmiyordur. port.readable.getReader() çağrısı yapıldığında bir okuyucu oluşturulur ve readable bu okuyucuya kilitlenir. 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);
}

Önemli olmayan bazı seri bağlantı noktası okuma hataları; arabellek taşması, çerçeveleme hataları veya parite hataları gibi bazı koşullarda ortaya çıkabilir. Bunlar istisna olarak atılır ve öncekinin üzerine port.readable değerini kontrol eden başka bir döngü ekleyerek yakalanabilir. Bu, hatalar ölümcül olmadığı sürece otomatik olarak yeni bir ReadableStream oluşturulduğu için işe yarar. Seri cihazın kaldırılması gibi önemli bir hata oluşursa port.readable null 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 metin gönderirse port.readable değerini aşağıda gösterildiği gibi bir TextDecoderStream üzerinden aktarabilirsiniz. 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);
}

"Own Buffer" okuyucu kullanarak akıştan veri okurken belleğin nasıl ayrıldığını kontrol edebilirsiniz. ReadableStreamBYOBReader arayüzünü almak için port.readable.getReader({ mode: "byob" })'ü çağırın ve read()'i çağırırken kendi ArrayBuffer'ınızı 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 içindeki arabelleğin nasıl yeniden kullanılacağına dair bir örnek aşağıda 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`.
}

Seri bağlantı noktasından belirli miktarda verinin nasıl okunacağına dair başka bir örnek aşağıda 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()'e iletin. Seri bağlantı noktasının daha sonra kapatılması için port.writable.getWriter() üzerinde releaseLock() çağrısı 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ında aktarılan bir boru aracılığıyla 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 yazar için releaseLock() çağrıldığı anlamına gelir.

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() işlevinin çağrılması, reader.read() ürününün { value: undefined, done: true } ile hemen çözülmesine zorlanır ve böylece 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ıldığında seri bağlantı noktalarını kapatmak daha karmaşıktır. reader.cancel()'ü eskisi gibi arayın. Ardından writer.close() ve port.close()'u arayın. Bu, dönüştürme akışları aracılığıyla hataları temel seri bağlantı noktasına iletir. Hata yayılımı hemen gerçekleşmediğinden, port.readable ve port.writable kilidinin ne zaman açıldığını tespit etmek için daha önce oluşturulan readableStreamClosed ve writableStreamClosed taahhütlerini kullanmanız gerekir. reader'ü iptal etmek aktarımın iptal edilmesine neden olur. Bu nedenle, ortaya çıkan hatayı yakalayıp 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 seslerini dinleme

Bir USB cihazı tarafından seri bağlantı noktası sağlanırsa bu cihaz sisteme bağlanmış veya bağlantısı kesilmiş olabilir. Web sitesine seri bağlantı noktasına erişme izni verildiğinde connect ve disconnect etkinliklerini izlemelidir.

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ı 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, Veri Terminali Hazır (DTR) sinyali açılıp açılmazsa Arduino gibi bazı cihazlar programlama moduna girer.

Çıkış sinyallerini ayarlama ve giriş sinyallerini alma işlemleri sırasıyla port.setSignals() ve port.getSignals() çağrılarak 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 bir kerede almayabilirsiniz. İsteğe bağlı olarak parçalara ayrılabilir. Daha fazla bilgi için Streams API kavramları başlıklı makaleyi inceleyin.

Bununla başa çıkmak için TextDecoderStream gibi bazı yerleşik dönüşüm akışlarını kullanabilir veya gelen akışı ayrıştırıp ayrıştırılmış veriler döndürmenize olanak tanıyan 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 yer alır. Veriler kullanılmadan önce rastgele bir dönüştürme işlemi uygulayabilir. Bunu bir montaj hattı gibi düşünebilirsiniz: Bir widget çizginin ilerisinde olduğunda, satırdaki her adım widget'ı değiştirir. Böylece, son hedefine vardığında widget tam işlevsel hale gelir.

Bir uçak fabrikasının fotoğrafı
II. Dünya Savaşı 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ını nasıl oluşturacağınızı düşünün. transform() yöntemi, akış her yeni veri aldığı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üşüm akışı sınıfını kullanmak için gelen bir akışı bu sınıftan geçirmeniz gerekir. Seri bağlantı noktasından okuma bölümündeki üçüncü kod örneğinde, orijinal giriş akışı yalnızca bir TextDecoderStream üzerinden aktarılmıştır. Bu nedenle, yeni LineBreakTransformer'imiz üzerinden aktarmak 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ını ayıklamak için seri cihaza gelen veya cihazdan giden akışları bölmek üzere port.readable sınıfının tee() yöntemini kullanın. Oluşturulan iki akış 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()'yi çağırarak artık tutmaya istekli olmadığı 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ında, kullanıcı tarafından oluşturulan çok sayıda izin birikmesi kötü bir kullanıcı deneyimi oluşturur.

// 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ğıdaki sürümlerde desteklenip desteklenmediğini kontrol edin:

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

Geliştiriciler İçin İpuçları

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

Web Serial API&#39;de hata ayıklamayla ilgili 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şimde bulunmak üzere Web Seri API'yi kullanacaksınız.

Tarayıcı desteği

Web Seri API'si, Chrome 89'da tüm masaüstü platformlarda (ChromeOS, Linux, macOS ve Windows) kullanılabilir.

Çoklu dolgu

Android'de, WebUSB API ve Serial API polyfill aracılığıyla USB tabanlı seri bağlantı noktaları desteklenebilir. Bu polyfill, yerleşik bir cihaz sürücüsü tarafından hak talebinde bulunulmadığı için cihaza WebUSB API üzerinden erişilebilen donanım ve platformlarla sınırlıdır.

Güvenlik ve gizlilik

Spesifikasyon yazarları, Web Serial API'yi tasarlarken ve uygularken kullanıcı kontrolü, şeffaflık ve ergonomi gibi Güçlü Web Platformu Özelliklerine Erişimi Kontrol Etme başlıklı makalede tanımlanan temel ilkeleri temel almıştır. Bu API'nin kullanılabilmesi, temel olarak tek seferde yalnızca tek bir seri cihaza erişim izni veren bir izin modeliyle sınırlandırılmıştı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 avantajları ve dezavantajları anlamak için Web Seri API Açıklayıcısı'nın güvenlik ve gizlilik bölümlerine göz atın.

Geri bildirim

Chrome ekibi, Web Seri API'si hakkındaki düşüncelerinizi ve deneyimlerinizi öğrenmekten memnuniyet duyar.

API tasarımı hakkında bilgi verin

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

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

Uygulamayla ilgili sorunları bildirme

Chrome&#39;un uygulamasında bir hata mı buldunuz? Yoksa uygulama, spesifikasyondan farklı mı?

https://new.crbug.com adresinden hata kaydı oluşturun. Mümkün olduğunca fazla ayrıntı eklediğinizden, hatayı yeniden oluşturmayla ilgili basit talimatlar sağladığınızdan ve Bileşenler'in Blink>Serial olarak ayarlandığından emin olun. Glitch, hızlı ve kolay yeniden oluşturma işlemlerini paylaşmak için mükemmel bir araçtır.

Destek 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ı tedarikçi firmalarına bu özellikleri desteklemenin ne kadar önemli olduğunu gösterir.

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

Faydalı bağlantılar

Demolar

Teşekkür ederiz

Bu makaleyi inceleyen Reilly Grant ve Joe Medley'e teşekkür ederiz. Birmingham Museums Trust'ın Unsplash'ta yer alan uçak fabrikası fotoğrafı.