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 ve endüstri sektörlerinde kullanıcılar bilgisayarlarına çevre birimleri 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şturma Tamamlandı
2. Spesifikasyonun ilk taslağını oluşturma Tamamlandı
3. Geri bildirim toplayın ve tasarımda iterasyon yapın 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 asenkrondur. 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çme 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 bit sayısı (7 veya 8).
  • stopBits: Bir karenin sonunda bulunan durdurma bitlerinin sayısı (1 veya 2).
  • parity: Eşitlik 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);
}

Arabellek taşması, çerçeveleme hataları veya parite hataları gibi bazı koşullarda, seri bağlantı noktası okumayla ilgili bazı kritik olmayan hatalar oluşabilir. 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 kritik 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 bir döngü kullanarak seri cihazdan sürekli veri okurken port.readable, bir hatayla karşılaşana kadar her zaman kilitli kalır. Bu durumda, reader.cancel() çağrısı reader.read()'u { value: undefined, done: true } ile hemen çözmeye zorlar ve böylece döngünün reader.releaseLock()'ı çağırması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 yayma işlemi hemen gerçekleşmediğinden, port.readable ve port.writable'in kilidinin ne zaman açıldığını algılamak için daha önce oluşturulan readableStreamClosed ve writableStreamClosed vaatlerini 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

USB cihaz tarafından seri bağlantı noktası sağlanıyorsa söz konusu cihaz sistemle bağlı veya sistemden ayrılmış olabilir. Web sitesine seri bağlantı noktasına erişim 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, Arduino gibi bazı cihazlar Veri Terminali Hazır (DTR) sinyali etkinleştirilirse 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.

Bu sorunu çözmek 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ılan verileri döndürmenize olanak tanıyan kendi dönüştürme 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üşünün: Bir widget hattan geçerken hattaki her adım widget'ı değiştirir. Böylece widget, son hedefine ulaştığında tamamen çalışır durumda olur.

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 ekleyebilir 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ştirici İpuçları

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

Web Serial API&#39;de hata ayıklama için 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ü platformlarında (ChromeOS, Linux, macOS ve Windows) kullanılabilir.

Çoklu dolgu

Android'de, WebUSB API ve Serial API polyfill kullanılarak USB tabanlı seri bağlantı noktaları desteklenir. 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ı istemlerine yanıt olarak kullanıcının belirli bir seri cihazı seçmek için etkin 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 mevcut bir soruna düşüncelerinizi ekleyin.

Uygulamayla ilgili sorunları bildirme

Chrome'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 hesabına tweet gönderin ve bu özelliğ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'un Unsplash'ta yer alan uçak fabrikası fotoğrafı.